From 0c211ffceeb01fc6bd9cbdbc50206d087dcc1ab9 Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Tue, 7 Sep 2021 16:17:58 +0530 Subject: [PATCH 01/23] Make load() accept DOM elements --- README.md | 3 +- dist/indexed-cache.esm.js | 76 ++++++++++++++++++++++----------------- dist/indexed-cache.min.js | 2 +- src/index.js | 76 ++++++++++++++++++++++----------------- 4 files changed, 91 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 7e33964..b5d1cc7 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ new IndexedCache({ }).load(); ``` -To manually prune all objects in the database except for a given list of keys, after `await init()`, call `.prune([list of keys])`. +- `load()` can be called with a DOM Node or NodeList. When none are given, it scans the entire DOM. +- To manually prune all objects in the database except for a given list of keys, after `await init()`, call `.prune([list of keys])`. Licensed under the MIT license. diff --git a/dist/indexed-cache.esm.js b/dist/indexed-cache.esm.js index 4d11119..323520c 100644 --- a/dist/indexed-cache.esm.js +++ b/dist/indexed-cache.esm.js @@ -41,11 +41,11 @@ class IndexedCache { } // Initialize the DB and then scan and setup DOM elements to cache. - async load () { + async load (elements) { // This will setup the elements on the page irrespective of whether // the DB is available or not. let objs = []; - await this._setupElements().then((_objs) => { + await this._setupElements(elements).then((_objs) => { objs = _objs; }); @@ -103,44 +103,52 @@ class IndexedCache { // Scan all matching elements and either: // a) if indexedDB is not available, fallback to loading the assets natively. - // b) if DB is available but the object is not cached, fetch(), cache in B, and apply the blob. + // b) if DB is available but the object is not cached, fetch(), cache in the DB, and apply the blob. // c) if DB is available and the object is cached, apply the cached blob. - - async _setupElements () { + // elements should either be null or be a NodeList. + async _setupElements (elements) { const objs = []; + // If there are no elements, scan the entire DOM for groups of each tag type. + if (elements instanceof NodeList) { + elements = Array.from(elements); + } else if (elements instanceof Node) { + elements = [elements]; + } else { + const sel = this.opt.tags.map((t) => `${t}[data-src]:not([data-indexed])`).join(','); + elements = document.querySelectorAll(sel); + } + // Get all tags of a particular tag on the page that has the data-src attrib. - this.opt.tags.forEach((tag) => { - document.querySelectorAll(`${tag}[data-src]:not([data-indexed])`).forEach((el) => { - const obj = { - el: el, - key: el.dataset.key || el.dataset.src, - src: el.dataset.src, - hash: el.dataset.hash || el.dataset.src, - isAsync: el.tagName !== 'SCRIPT' || el.hasAttribute('async') || el.hasAttribute('defer'), - expiry: null - }; + // document.querySelectorAll(`${tag}[data-src]:not([data-indexed])`).forEach((el) => { + elements.forEach((el) => { + if ('indexed' in el.dataset) { + return + } + const obj = { + el: el, + key: el.dataset.key || el.dataset.src, + src: el.dataset.src, + hash: el.dataset.hash || el.dataset.src, + isAsync: el.tagName !== 'SCRIPT' || el.hasAttribute('async') || el.hasAttribute('defer'), + expiry: null + }; - // If there is a global expiry or an expiry on the object, compute that. - const exp = el.dataset.expiry || this.opt.expiry; - if (exp) { - obj.expiry = new Date(new Date().getTime() + (parseInt(exp) * 60000)); - } + // If there is a global expiry or an expiry on the object, compute that. + const exp = el.dataset.expiry || this.opt.expiry; + if (exp) { + obj.expiry = new Date(new Date().getTime() + (parseInt(exp) * 60000)); + } - // If for any reason the store is not initialized, fall back to - // the native asset loading mechanism. - if (this.db) { - objs.push(obj); - } else { - this._applyOriginal(obj); - } - }); + // If for any reason the store is not initialized, fall back to + // the native asset loading mechanism. + if (this.db) { + objs.push(obj); + } else { + this._applyOriginal(obj); + } }); - if (objs.length === 0) { - return objs - } - const promises = []; objs.forEach((obj) => { if (obj.isAsync) { @@ -156,6 +164,10 @@ class IndexedCache { } }); + if (promises.length === 0) { + return objs + } + // Once the assets have been fetched, apply them synchronously. Since // the time take to execute a script is not guaranteed, use the onload() event // of each element to load the next element. diff --git a/dist/indexed-cache.min.js b/dist/indexed-cache.min.js index b8f2209..36649a2 100644 --- a/dist/indexed-cache.min.js +++ b/dist/indexed-cache.min.js @@ -1,3 +1,3 @@ /* indexed-cache - v0.3.2 * Kailash Nadh. Licensed MIT */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).IndexedCache=t()}(this,(function(){"use strict";function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function t(e,t,r,n,o,a,s){try{var i=e[a](s),c=i.value}catch(e){return void r(e)}i.done?t(c):Promise.resolve(c).then(n,o)}function r(e){return function(){var r=this,n=arguments;return new Promise((function(o,a){var s=e.apply(r,n);function i(e){t(s,o,a,i,c,"next",e)}function c(e){t(s,o,a,i,c,"throw",e)}i(void 0)}))}}function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}let o=!1;return class{constructor(t){if(o)throw new Error("indexed-cache is already loaded");o=!0,this.opt=function(t){for(var r=1;r{e.db=t})).catch((e=>{console.log("error initializing cache DB. failing over.",e)})))}))()}load(){var e=this;return r((function*(){let t=[];if(yield e._setupElements().then((e=>{t=e})),e.db&&0!==t.length&&e.opt.prune){const r=t.reduce(((e,t)=>(e.push(t.key),e)),[]);e._prune(r)}}))()}deleteKey(e){this._store().delete(e)}prune(e){this._prune(e)}clear(){this._store().clear()}_initDB(e,t){return r((function*(){return new Promise(((r,n)=>{window.indexedDB||n(new Error("indexedDB is not available"));const o=window.indexedDB.open(e);o.onupgradeneeded=e=>{const n=e.target.result;e.target.result.objectStoreNames.contains(t)||(n.createObjectStore(t,{keyPath:"key"}),e.target.transaction.oncomplete=()=>{r(n)})},o.onsuccess=()=>r(o.result),o.onerror=e=>n(e.target.error)}))}))()}_setupElements(){var e=this;return r((function*(){const t=[];if(e.opt.tags.forEach((r=>{document.querySelectorAll(`${r}[data-src]:not([data-indexed])`).forEach((r=>{const n={el:r,key:r.dataset.key||r.dataset.src,src:r.dataset.src,hash:r.dataset.hash||r.dataset.src,isAsync:"SCRIPT"!==r.tagName||r.hasAttribute("async")||r.hasAttribute("defer"),expiry:null},o=r.dataset.expiry||e.opt.expiry;o&&(n.expiry=new Date((new Date).getTime()+6e4*parseInt(o))),e.db?t.push(n):e._applyOriginal(n)}))})),0===t.length)return t;const r=[];return t.forEach((t=>{t.isAsync?e._loadObject(t).then((r=>{e._applyBlob(t,r.data.blob)})).catch((r=>{e._applyOriginal(t)})):r.push(e._loadObject(t))})),yield Promise.all(r).then((t=>{t.forEach(((r,n)=>{n>=t.length-1||(r.obj.el.onload=()=>{e._applyBlob(t[n+1].obj,t[n+1].data.blob)})})),e._applyBlob(t[0].obj,t[0].data.blob)})),t}))()}_loadObject(e){var t=this;return r((function*(){return new Promise(((r,n)=>{t._getBlob(e).then((t=>{r({obj:e,data:t})})).catch((o=>{""===o.toString()&&console.log("error getting cache blob:",o),t._fetchAsset(e).then((t=>{r({obj:e,data:t})})).catch((e=>{n(new Error("error fetching asset: "+e))}))}))}))}))()}_getBlob(e){var t=this;return r((function*(){return new Promise(((r,n)=>{const o=t._store().get(e.key);o.onsuccess=o=>{const a=o.target.result;if(a&&(!e.hash||a.hash===e.hash))return a.expiry&&new Date>new Date(a.expiry)?(t.deleteKey(a.key),void n(new Error(""))):void r(a);n(new Error(""))},o.onerror=e=>{n(e.target.error)}}))}))()}_fetchAsset(e){var t=this;return r((function*(){return new Promise(((r,n)=>{fetch(e.src).then((o=>{o.ok?o.blob().then((o=>{const a={key:e.key,hash:e.hash,expiry:e.expiry,blob:o},s=t._store().put(a);s.onsuccess=e=>r(a),s.onerror=e=>n(e.target.error)})):n(new Error(`error fetching asset: ${o.status}`))})).catch((e=>{n(new Error(e.toString()))}))}))}))()}_applyOriginal(e){switch(e.el.tagName){case"SCRIPT":case"IMG":e.el.setAttribute("src",e.src);break;case"LINK":e.el.setAttribute("href",e.src)}e.el.dataset.indexed=!0}_applyBlob(e,t){const r=window.URL.createObjectURL(t);switch(e.el.tagName){case"SCRIPT":case"IMG":e.el.src=r;break;case"LINK":e.el.href=r}e.el.dataset.indexed=!0}_prune(e){const t=e.reduce(((e,t)=>(e[t]=!0,e)),{});this._store().getAllKeys().onsuccess=e=>{e.target.result.forEach((e=>{e in t||this.deleteKey(e)}))}}_store(){return this.db.transaction(this.opt.storeName,"readwrite").objectStore(this.opt.storeName)}}})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).IndexedCache=t()}(this,(function(){"use strict";function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function t(e,t,r,n,o,s,a){try{var i=e[s](a),c=i.value}catch(e){return void r(e)}i.done?t(c):Promise.resolve(c).then(n,o)}function r(e){return function(){var r=this,n=arguments;return new Promise((function(o,s){var a=e.apply(r,n);function i(e){t(a,o,s,i,c,"next",e)}function c(e){t(a,o,s,i,c,"throw",e)}i(void 0)}))}}function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}let o=!1;return class{constructor(t){if(o)throw new Error("indexed-cache is already loaded");o=!0,this.opt=function(t){for(var r=1;r{e.db=t})).catch((e=>{console.log("error initializing cache DB. failing over.",e)})))}))()}load(e){var t=this;return r((function*(){let r=[];if(yield t._setupElements(e).then((e=>{r=e})),t.db&&0!==r.length&&t.opt.prune){const e=r.reduce(((e,t)=>(e.push(t.key),e)),[]);t._prune(e)}}))()}deleteKey(e){this._store().delete(e)}prune(e){this._prune(e)}clear(){this._store().clear()}_initDB(e,t){return r((function*(){return new Promise(((r,n)=>{window.indexedDB||n(new Error("indexedDB is not available"));const o=window.indexedDB.open(e);o.onupgradeneeded=e=>{const n=e.target.result;e.target.result.objectStoreNames.contains(t)||(n.createObjectStore(t,{keyPath:"key"}),e.target.transaction.oncomplete=()=>{r(n)})},o.onsuccess=()=>r(o.result),o.onerror=e=>n(e.target.error)}))}))()}_setupElements(e){var t=this;return r((function*(){const r=[];if(e instanceof NodeList)e=Array.from(e);else if(e instanceof Node)e=[e];else{const r=t.opt.tags.map((e=>`${e}[data-src]:not([data-indexed])`)).join(",");e=document.querySelectorAll(r)}e.forEach((e=>{if("indexed"in e.dataset)return;const n={el:e,key:e.dataset.key||e.dataset.src,src:e.dataset.src,hash:e.dataset.hash||e.dataset.src,isAsync:"SCRIPT"!==e.tagName||e.hasAttribute("async")||e.hasAttribute("defer"),expiry:null},o=e.dataset.expiry||t.opt.expiry;o&&(n.expiry=new Date((new Date).getTime()+6e4*parseInt(o))),t.db?r.push(n):t._applyOriginal(n)}));const n=[];return r.forEach((e=>{e.isAsync?t._loadObject(e).then((r=>{t._applyBlob(e,r.data.blob)})).catch((r=>{t._applyOriginal(e)})):n.push(t._loadObject(e))})),0===n.length||(yield Promise.all(n).then((e=>{e.forEach(((r,n)=>{n>=e.length-1||(r.obj.el.onload=()=>{t._applyBlob(e[n+1].obj,e[n+1].data.blob)})})),t._applyBlob(e[0].obj,e[0].data.blob)}))),r}))()}_loadObject(e){var t=this;return r((function*(){return new Promise(((r,n)=>{t._getBlob(e).then((t=>{r({obj:e,data:t})})).catch((o=>{""===o.toString()&&console.log("error getting cache blob:",o),t._fetchAsset(e).then((t=>{r({obj:e,data:t})})).catch((e=>{n(new Error("error fetching asset: "+e))}))}))}))}))()}_getBlob(e){var t=this;return r((function*(){return new Promise(((r,n)=>{const o=t._store().get(e.key);o.onsuccess=o=>{const s=o.target.result;if(s&&(!e.hash||s.hash===e.hash))return s.expiry&&new Date>new Date(s.expiry)?(t.deleteKey(s.key),void n(new Error(""))):void r(s);n(new Error(""))},o.onerror=e=>{n(e.target.error)}}))}))()}_fetchAsset(e){var t=this;return r((function*(){return new Promise(((r,n)=>{fetch(e.src).then((o=>{o.ok?o.blob().then((o=>{const s={key:e.key,hash:e.hash,expiry:e.expiry,blob:o},a=t._store().put(s);a.onsuccess=e=>r(s),a.onerror=e=>n(e.target.error)})):n(new Error(`error fetching asset: ${o.status}`))})).catch((e=>{n(new Error(e.toString()))}))}))}))()}_applyOriginal(e){switch(e.el.tagName){case"SCRIPT":case"IMG":e.el.setAttribute("src",e.src);break;case"LINK":e.el.setAttribute("href",e.src)}e.el.dataset.indexed=!0}_applyBlob(e,t){const r=window.URL.createObjectURL(t);switch(e.el.tagName){case"SCRIPT":case"IMG":e.el.src=r;break;case"LINK":e.el.href=r}e.el.dataset.indexed=!0}_prune(e){const t=e.reduce(((e,t)=>(e[t]=!0,e)),{});this._store().getAllKeys().onsuccess=e=>{e.target.result.forEach((e=>{e in t||this.deleteKey(e)}))}}_store(){return this.db.transaction(this.opt.storeName,"readwrite").objectStore(this.opt.storeName)}}})); diff --git a/src/index.js b/src/index.js index b3351b0..6b43739 100644 --- a/src/index.js +++ b/src/index.js @@ -41,11 +41,11 @@ export default class IndexedCache { } // Initialize the DB and then scan and setup DOM elements to cache. - async load () { + async load (elements) { // This will setup the elements on the page irrespective of whether // the DB is available or not. let objs = [] - await this._setupElements().then((_objs) => { + await this._setupElements(elements).then((_objs) => { objs = _objs }) @@ -103,44 +103,52 @@ export default class IndexedCache { // Scan all matching elements and either: // a) if indexedDB is not available, fallback to loading the assets natively. - // b) if DB is available but the object is not cached, fetch(), cache in B, and apply the blob. + // b) if DB is available but the object is not cached, fetch(), cache in the DB, and apply the blob. // c) if DB is available and the object is cached, apply the cached blob. - - async _setupElements () { + // elements should either be null or be a NodeList. + async _setupElements (elements) { const objs = [] + // If there are no elements, scan the entire DOM for groups of each tag type. + if (elements instanceof NodeList) { + elements = Array.from(elements) + } else if (elements instanceof Node) { + elements = [elements] + } else { + const sel = this.opt.tags.map((t) => `${t}[data-src]:not([data-indexed])`).join(',') + elements = document.querySelectorAll(sel) + } + // Get all tags of a particular tag on the page that has the data-src attrib. - this.opt.tags.forEach((tag) => { - document.querySelectorAll(`${tag}[data-src]:not([data-indexed])`).forEach((el) => { - const obj = { - el: el, - key: el.dataset.key || el.dataset.src, - src: el.dataset.src, - hash: el.dataset.hash || el.dataset.src, - isAsync: el.tagName !== 'SCRIPT' || el.hasAttribute('async') || el.hasAttribute('defer'), - expiry: null - } + // document.querySelectorAll(`${tag}[data-src]:not([data-indexed])`).forEach((el) => { + elements.forEach((el) => { + if ('indexed' in el.dataset) { + return + } + const obj = { + el: el, + key: el.dataset.key || el.dataset.src, + src: el.dataset.src, + hash: el.dataset.hash || el.dataset.src, + isAsync: el.tagName !== 'SCRIPT' || el.hasAttribute('async') || el.hasAttribute('defer'), + expiry: null + } - // If there is a global expiry or an expiry on the object, compute that. - const exp = el.dataset.expiry || this.opt.expiry - if (exp) { - obj.expiry = new Date(new Date().getTime() + (parseInt(exp) * 60000)) - } + // If there is a global expiry or an expiry on the object, compute that. + const exp = el.dataset.expiry || this.opt.expiry + if (exp) { + obj.expiry = new Date(new Date().getTime() + (parseInt(exp) * 60000)) + } - // If for any reason the store is not initialized, fall back to - // the native asset loading mechanism. - if (this.db) { - objs.push(obj) - } else { - this._applyOriginal(obj) - } - }) + // If for any reason the store is not initialized, fall back to + // the native asset loading mechanism. + if (this.db) { + objs.push(obj) + } else { + this._applyOriginal(obj) + } }) - if (objs.length === 0) { - return objs - } - const promises = [] objs.forEach((obj) => { if (obj.isAsync) { @@ -156,6 +164,10 @@ export default class IndexedCache { } }) + if (promises.length === 0) { + return objs + } + // Once the assets have been fetched, apply them synchronously. Since // the time take to execute a script is not guaranteed, use the onload() event // of each element to load the next element. From 9d3e5602266dd5abdf7a937183d9d3c562eeb78f Mon Sep 17 00:00:00 2001 From: wiwium Date: Wed, 8 Sep 2021 01:55:19 +0530 Subject: [PATCH 02/23] simplified educe to a map --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 6b43739..e858c20 100644 --- a/src/index.js +++ b/src/index.js @@ -57,7 +57,7 @@ export default class IndexedCache { // referenced on the page. if (this.opt.prune) { // Pass the list of keys found on the page. - const keys = objs.reduce((obj, v) => { obj.push(v.key); return obj }, []) + const keys = objs.map(obj => obj.key) this._prune(keys) } } From b45a05e6a757cfdf7e02594533d5865bd59f1aca Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Wed, 8 Sep 2021 13:25:07 +0530 Subject: [PATCH 03/23] Handle exceptions thrown by Safari private mode --- src/index.js | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/index.js b/src/index.js index e858c20..5bd2a3a 100644 --- a/src/index.js +++ b/src/index.js @@ -215,27 +215,31 @@ export default class IndexedCache { // (hash changed or date expired), fetch the asset over HTTP, cache it, and load it. async _getBlob (obj) { return new Promise((resolve, reject) => { - const req = this._store().get(obj.key) - req.onsuccess = (e) => { - const data = e.target.result + try { + const req = this._store().get(obj.key) + req.onsuccess = (e) => { + const data = e.target.result + + // Reject if there is no stored data, or if the hash has changed. + if (!data || (obj.hash && (data.hash !== obj.hash))) { + reject(new Error('')) + return + } - // Reject if there is no stored data, or if the hash has changed. - if (!data || (obj.hash && (data.hash !== obj.hash))) { - reject(new Error('')) - return - } + // Reject and delete if the object has expired. + if (data.expiry && new Date() > new Date(data.expiry)) { + this.deleteKey(data.key) + reject(new Error('')) + return + } - // Reject and delete if the object has expired. - if (data.expiry && new Date() > new Date(data.expiry)) { - this.deleteKey(data.key) - reject(new Error('')) - return + resolve(data) } - resolve(data) - } - - req.onerror = (e) => { + req.onerror = (e) => { + reject(e.target.error) + } + } catch (e) { reject(e.target.error) } }) @@ -258,9 +262,13 @@ export default class IndexedCache { blob: b } - const req = this._store().put(data) - req.onsuccess = (e) => resolve(data) - req.onerror = (e) => reject(e.target.error) + try { + const req = this._store().put(data) + req.onsuccess = (e) => resolve(data) + req.onerror = (e) => reject(e.target.error) + } catch (e) { + reject(e.target.error) + } }) }).catch((e) => { reject(new Error(e.toString())) From 0a8ee130b85db8864ea17c12b1e147cffff1bd1c Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Wed, 8 Sep 2021 13:25:42 +0530 Subject: [PATCH 04/23] Bump version --- README.md | 2 +- dist/indexed-cache.esm.js | 58 ++++++++++++++++++++++----------------- dist/indexed-cache.min.js | 4 +-- package.json | 2 +- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index b5d1cc7..f9ab21f 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ To cache and sideload static assets: Always include and invoke indexed-cache at the end, right before . Use the unpkg CDN or download and host the script locally (dist/indexed-cache.min.js). !--> - + diff --git a/dist/indexed-cache.esm.js b/dist/indexed-cache.esm.js index 323520c..b015790 100644 --- a/dist/indexed-cache.esm.js +++ b/dist/indexed-cache.esm.js @@ -57,7 +57,7 @@ class IndexedCache { // referenced on the page. if (this.opt.prune) { // Pass the list of keys found on the page. - const keys = objs.reduce((obj, v) => { obj.push(v.key); return obj }, []); + const keys = objs.map(obj => obj.key); this._prune(keys); } } @@ -215,29 +215,33 @@ class IndexedCache { // (hash changed or date expired), fetch the asset over HTTP, cache it, and load it. async _getBlob (obj) { return new Promise((resolve, reject) => { - const req = this._store().get(obj.key); - req.onsuccess = (e) => { - const data = e.target.result; - - // Reject if there is no stored data, or if the hash has changed. - if (!data || (obj.hash && (data.hash !== obj.hash))) { - reject(new Error('')); - return - } - - // Reject and delete if the object has expired. - if (data.expiry && new Date() > new Date(data.expiry)) { - this.deleteKey(data.key); - reject(new Error('')); - return - } - - resolve(data); - }; + try { + const req = this._store().get(obj.key); + req.onsuccess = (e) => { + const data = e.target.result; + + // Reject if there is no stored data, or if the hash has changed. + if (!data || (obj.hash && (data.hash !== obj.hash))) { + reject(new Error('')); + return + } + + // Reject and delete if the object has expired. + if (data.expiry && new Date() > new Date(data.expiry)) { + this.deleteKey(data.key); + reject(new Error('')); + return + } + + resolve(data); + }; - req.onerror = (e) => { + req.onerror = (e) => { + reject(e.target.error); + }; + } catch (e) { reject(e.target.error); - }; + } }) } @@ -258,9 +262,13 @@ class IndexedCache { blob: b }; - const req = this._store().put(data); - req.onsuccess = (e) => resolve(data); - req.onerror = (e) => reject(e.target.error); + try { + const req = this._store().put(data); + req.onsuccess = (e) => resolve(data); + req.onerror = (e) => reject(e.target.error); + } catch (e) { + reject(e.target.error); + } }); }).catch((e) => { reject(new Error(e.toString())); diff --git a/dist/indexed-cache.min.js b/dist/indexed-cache.min.js index 36649a2..963369d 100644 --- a/dist/indexed-cache.min.js +++ b/dist/indexed-cache.min.js @@ -1,3 +1,3 @@ -/* indexed-cache - v0.3.2 +/* indexed-cache - v0.3.3 * Kailash Nadh. Licensed MIT */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).IndexedCache=t()}(this,(function(){"use strict";function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function t(e,t,r,n,o,s,a){try{var i=e[s](a),c=i.value}catch(e){return void r(e)}i.done?t(c):Promise.resolve(c).then(n,o)}function r(e){return function(){var r=this,n=arguments;return new Promise((function(o,s){var a=e.apply(r,n);function i(e){t(a,o,s,i,c,"next",e)}function c(e){t(a,o,s,i,c,"throw",e)}i(void 0)}))}}function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}let o=!1;return class{constructor(t){if(o)throw new Error("indexed-cache is already loaded");o=!0,this.opt=function(t){for(var r=1;r{e.db=t})).catch((e=>{console.log("error initializing cache DB. failing over.",e)})))}))()}load(e){var t=this;return r((function*(){let r=[];if(yield t._setupElements(e).then((e=>{r=e})),t.db&&0!==r.length&&t.opt.prune){const e=r.reduce(((e,t)=>(e.push(t.key),e)),[]);t._prune(e)}}))()}deleteKey(e){this._store().delete(e)}prune(e){this._prune(e)}clear(){this._store().clear()}_initDB(e,t){return r((function*(){return new Promise(((r,n)=>{window.indexedDB||n(new Error("indexedDB is not available"));const o=window.indexedDB.open(e);o.onupgradeneeded=e=>{const n=e.target.result;e.target.result.objectStoreNames.contains(t)||(n.createObjectStore(t,{keyPath:"key"}),e.target.transaction.oncomplete=()=>{r(n)})},o.onsuccess=()=>r(o.result),o.onerror=e=>n(e.target.error)}))}))()}_setupElements(e){var t=this;return r((function*(){const r=[];if(e instanceof NodeList)e=Array.from(e);else if(e instanceof Node)e=[e];else{const r=t.opt.tags.map((e=>`${e}[data-src]:not([data-indexed])`)).join(",");e=document.querySelectorAll(r)}e.forEach((e=>{if("indexed"in e.dataset)return;const n={el:e,key:e.dataset.key||e.dataset.src,src:e.dataset.src,hash:e.dataset.hash||e.dataset.src,isAsync:"SCRIPT"!==e.tagName||e.hasAttribute("async")||e.hasAttribute("defer"),expiry:null},o=e.dataset.expiry||t.opt.expiry;o&&(n.expiry=new Date((new Date).getTime()+6e4*parseInt(o))),t.db?r.push(n):t._applyOriginal(n)}));const n=[];return r.forEach((e=>{e.isAsync?t._loadObject(e).then((r=>{t._applyBlob(e,r.data.blob)})).catch((r=>{t._applyOriginal(e)})):n.push(t._loadObject(e))})),0===n.length||(yield Promise.all(n).then((e=>{e.forEach(((r,n)=>{n>=e.length-1||(r.obj.el.onload=()=>{t._applyBlob(e[n+1].obj,e[n+1].data.blob)})})),t._applyBlob(e[0].obj,e[0].data.blob)}))),r}))()}_loadObject(e){var t=this;return r((function*(){return new Promise(((r,n)=>{t._getBlob(e).then((t=>{r({obj:e,data:t})})).catch((o=>{""===o.toString()&&console.log("error getting cache blob:",o),t._fetchAsset(e).then((t=>{r({obj:e,data:t})})).catch((e=>{n(new Error("error fetching asset: "+e))}))}))}))}))()}_getBlob(e){var t=this;return r((function*(){return new Promise(((r,n)=>{const o=t._store().get(e.key);o.onsuccess=o=>{const s=o.target.result;if(s&&(!e.hash||s.hash===e.hash))return s.expiry&&new Date>new Date(s.expiry)?(t.deleteKey(s.key),void n(new Error(""))):void r(s);n(new Error(""))},o.onerror=e=>{n(e.target.error)}}))}))()}_fetchAsset(e){var t=this;return r((function*(){return new Promise(((r,n)=>{fetch(e.src).then((o=>{o.ok?o.blob().then((o=>{const s={key:e.key,hash:e.hash,expiry:e.expiry,blob:o},a=t._store().put(s);a.onsuccess=e=>r(s),a.onerror=e=>n(e.target.error)})):n(new Error(`error fetching asset: ${o.status}`))})).catch((e=>{n(new Error(e.toString()))}))}))}))()}_applyOriginal(e){switch(e.el.tagName){case"SCRIPT":case"IMG":e.el.setAttribute("src",e.src);break;case"LINK":e.el.setAttribute("href",e.src)}e.el.dataset.indexed=!0}_applyBlob(e,t){const r=window.URL.createObjectURL(t);switch(e.el.tagName){case"SCRIPT":case"IMG":e.el.src=r;break;case"LINK":e.el.href=r}e.el.dataset.indexed=!0}_prune(e){const t=e.reduce(((e,t)=>(e[t]=!0,e)),{});this._store().getAllKeys().onsuccess=e=>{e.target.result.forEach((e=>{e in t||this.deleteKey(e)}))}}_store(){return this.db.transaction(this.opt.storeName,"readwrite").objectStore(this.opt.storeName)}}})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).IndexedCache=t()}(this,(function(){"use strict";function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function t(e,t,r,n,o,a,s){try{var i=e[a](s),c=i.value}catch(e){return void r(e)}i.done?t(c):Promise.resolve(c).then(n,o)}function r(e){return function(){var r=this,n=arguments;return new Promise((function(o,a){var s=e.apply(r,n);function i(e){t(s,o,a,i,c,"next",e)}function c(e){t(s,o,a,i,c,"throw",e)}i(void 0)}))}}function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}let o=!1;return class{constructor(t){if(o)throw new Error("indexed-cache is already loaded");o=!0,this.opt=function(t){for(var r=1;r{e.db=t})).catch((e=>{console.log("error initializing cache DB. failing over.",e)})))}))()}load(e){var t=this;return r((function*(){let r=[];if(yield t._setupElements(e).then((e=>{r=e})),t.db&&0!==r.length&&t.opt.prune){const e=r.map((e=>e.key));t._prune(e)}}))()}deleteKey(e){this._store().delete(e)}prune(e){this._prune(e)}clear(){this._store().clear()}_initDB(e,t){return r((function*(){return new Promise(((r,n)=>{window.indexedDB||n(new Error("indexedDB is not available"));const o=window.indexedDB.open(e);o.onupgradeneeded=e=>{const n=e.target.result;e.target.result.objectStoreNames.contains(t)||(n.createObjectStore(t,{keyPath:"key"}),e.target.transaction.oncomplete=()=>{r(n)})},o.onsuccess=()=>r(o.result),o.onerror=e=>n(e.target.error)}))}))()}_setupElements(e){var t=this;return r((function*(){const r=[];if(e instanceof NodeList)e=Array.from(e);else if(e instanceof Node)e=[e];else{const r=t.opt.tags.map((e=>`${e}[data-src]:not([data-indexed])`)).join(",");e=document.querySelectorAll(r)}e.forEach((e=>{if("indexed"in e.dataset)return;const n={el:e,key:e.dataset.key||e.dataset.src,src:e.dataset.src,hash:e.dataset.hash||e.dataset.src,isAsync:"SCRIPT"!==e.tagName||e.hasAttribute("async")||e.hasAttribute("defer"),expiry:null},o=e.dataset.expiry||t.opt.expiry;o&&(n.expiry=new Date((new Date).getTime()+6e4*parseInt(o))),t.db?r.push(n):t._applyOriginal(n)}));const n=[];return r.forEach((e=>{e.isAsync?t._loadObject(e).then((r=>{t._applyBlob(e,r.data.blob)})).catch((r=>{t._applyOriginal(e)})):n.push(t._loadObject(e))})),0===n.length||(yield Promise.all(n).then((e=>{e.forEach(((r,n)=>{n>=e.length-1||(r.obj.el.onload=()=>{t._applyBlob(e[n+1].obj,e[n+1].data.blob)})})),t._applyBlob(e[0].obj,e[0].data.blob)}))),r}))()}_loadObject(e){var t=this;return r((function*(){return new Promise(((r,n)=>{t._getBlob(e).then((t=>{r({obj:e,data:t})})).catch((o=>{""===o.toString()&&console.log("error getting cache blob:",o),t._fetchAsset(e).then((t=>{r({obj:e,data:t})})).catch((e=>{n(new Error("error fetching asset: "+e))}))}))}))}))()}_getBlob(e){var t=this;return r((function*(){return new Promise(((r,n)=>{try{const o=t._store().get(e.key);o.onsuccess=o=>{const a=o.target.result;if(a&&(!e.hash||a.hash===e.hash))return a.expiry&&new Date>new Date(a.expiry)?(t.deleteKey(a.key),void n(new Error(""))):void r(a);n(new Error(""))},o.onerror=e=>{n(e.target.error)}}catch(e){n(e.target.error)}}))}))()}_fetchAsset(e){var t=this;return r((function*(){return new Promise(((r,n)=>{fetch(e.src).then((o=>{o.ok?o.blob().then((o=>{const a={key:e.key,hash:e.hash,expiry:e.expiry,blob:o};try{const e=t._store().put(a);e.onsuccess=e=>r(a),e.onerror=e=>n(e.target.error)}catch(e){n(e.target.error)}})):n(new Error(`error fetching asset: ${o.status}`))})).catch((e=>{n(new Error(e.toString()))}))}))}))()}_applyOriginal(e){switch(e.el.tagName){case"SCRIPT":case"IMG":e.el.setAttribute("src",e.src);break;case"LINK":e.el.setAttribute("href",e.src)}e.el.dataset.indexed=!0}_applyBlob(e,t){const r=window.URL.createObjectURL(t);switch(e.el.tagName){case"SCRIPT":case"IMG":e.el.src=r;break;case"LINK":e.el.href=r}e.el.dataset.indexed=!0}_prune(e){const t=e.reduce(((e,t)=>(e[t]=!0,e)),{});this._store().getAllKeys().onsuccess=e=>{e.target.result.forEach((e=>{e in t||this.deleteKey(e)}))}}_store(){return this.db.transaction(this.opt.storeName,"readwrite").objectStore(this.opt.storeName)}}})); diff --git a/package.json b/package.json index 3b2e9bb..83ad585 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knadh/indexed-cache", - "version": "0.3.2", + "version": "0.3.3", "description": "Sideload static assets on pages and cache them in IndexedDB ensuring longer-term storage.", "main": "src/index.js", "files": [ From cf564c09622b8b9b3cfd8192cec84eb52f4790e1 Mon Sep 17 00:00:00 2001 From: jj Date: Wed, 8 Sep 2021 15:31:46 +0530 Subject: [PATCH 05/23] fix: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9ab21f..16cd89a 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Use if at least a few of these are true: - CORS. - First-paint "flash" (needs to be handled manually) as scripts and styles only load after HTML is fetched and rendered by the browser. - Browser compatibility. -- Empty space or line breaks between the opening and closing `` tags will be executed as an inline script by the browser, after which the browser will not load the remote script when applied. Ensure that the opening and closing script ags have nothing between then. +- Empty space or line breaks between the opening and closing `` tags will be executed as an inline script by the browser, after which the browser will not load the remote script when applied. Ensure that the opening and closing script tags have nothing between then. ## Usage From fa7a6504deeb0ed7e96868f5169fcea6f7e43b32 Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Thu, 9 Sep 2021 12:38:44 +0530 Subject: [PATCH 06/23] Refactor blob load related error handling in multiple places. - Use `Promise.allSettled()` to capture failures. - Refactor `_applyOriginal()` and `_applyBlob()` to a single method `_applyElement()` that fails over the element to the original URL if the blob is null. - Make `_getObject()` return a dummy data{} object with null blob instead of a reject() in case of failed DB+HTTP fetch so that `_applyElement()` gracefully fails over to the original URL. - Rename `_getBlob()` to `_getDBblob()` for clarity. - Add ` + diff --git a/dist/indexed-cache.min.js b/dist/indexed-cache.min.js index c434394..3cec51b 100644 --- a/dist/indexed-cache.min.js +++ b/dist/indexed-cache.min.js @@ -1,3 +1,3 @@ -/* indexed-cache - v0.3.3 +/* indexed-cache - v0.4.0 * Kailash Nadh. Licensed MIT */ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).IndexedCache=t()}(this,(function(){"use strict";function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function t(e,t,r,n,o,a,s){try{var i=e[a](s),c=i.value}catch(e){return void r(e)}i.done?t(c):Promise.resolve(c).then(n,o)}function r(e){return function(){var r=this,n=arguments;return new Promise((function(o,a){var s=e.apply(r,n);function i(e){t(s,o,a,i,c,"next",e)}function c(e){t(s,o,a,i,c,"throw",e)}i(void 0)}))}}function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}let o=!1;return class{constructor(t){if(o)throw new Error("indexed-cache is already loaded");o=!0,this.opt=function(t){for(var r=1;r{e.db=t})).catch((e=>{console.log("error initializing cache DB. failing over.",e)})))}))()}load(e){var t=this;return r((function*(){let r=[];if(yield t._setupElements(e).then((e=>{r=e})),t.db&&0!==r.length&&t.opt.prune){const e=r.map((e=>e.key));t._prune(e)}}))()}deleteKey(e){this._store().delete(e)}prune(e){this._prune(e)}clear(){this._store().clear()}_initDB(e,t){var n=this;return r((function*(){return new Promise(((r,o)=>{window.indexedDB||o(new Error("indexedDB is not available"));const a=window.indexedDB.open(e);a.onupgradeneeded=e=>{const n=e.target.result;e.target.result.objectStoreNames.contains(t)||(n.createObjectStore(t,{keyPath:"key"}),e.target.transaction.oncomplete=()=>{r(n)})},a.onsuccess=()=>r(a.result),a.onerror=e=>o(e.target.error),setTimeout((()=>{n.db||o(new Error("Opening IndexedbDB timed out"))}),200)}))}))()}_setupElements(e){var t=this;return r((function*(){const r=[];if(e instanceof NodeList)e=Array.from(e);else if(e instanceof Node)e=[e];else{const r=t.opt.tags.map((e=>`${e}[data-src]:not([data-indexed])`)).join(",");e=document.querySelectorAll(r)}e.forEach((e=>{if("indexed"in e.dataset)return;const n={el:e,key:e.dataset.key||e.dataset.src,src:e.dataset.src,hash:e.dataset.hash||e.dataset.src,isAsync:"SCRIPT"!==e.tagName||e.hasAttribute("async")||e.hasAttribute("defer"),expiry:null},o=e.dataset.expiry||t.opt.expiry;o&&(n.expiry=new Date((new Date).getTime()+6e4*parseInt(o))),t.db?r.push(n):t._applyElement(n)}));const n=[];return r.forEach((e=>{e.isAsync?t._getObject(e).then((r=>{t._applyElement(e,r.data.blob)})).catch((r=>{t._applyElement(e)})):n.push(t._getObject(e))})),0===n.length||(yield Promise.allSettled(n).then((e=>{e.forEach(((n,o)=>{"rejected"!==n.status?o>=e.length-1||(n.value.obj.el.onload=n.value.obj.el.onerror=()=>{t._applyElement(e[o+1].value.obj,e[o+1].value.data.blob)}):t._applyElement(r[o])})),t._applyElement(e[0].value.obj,e[0].value.data.blob)}))),r}))()}_getObject(e){return new Promise(((t,r)=>{this._getDBblob(e).then((r=>{t({obj:e,data:r})})).catch((r=>{"Error"!==r.toString()&&console.log("error getting cache blob:",r),this._fetchObject(e).then((r=>{t({obj:e,data:r})})).catch((r=>{t({obj:e,data:{key:e.key,hash:e.hash,expiry:e.expiry,blob:null}})}))}))}))}_getDBblob(e){return new Promise(((t,r)=>{try{const n=this._store().get(e.key);n.onsuccess=n=>{const o=n.target.result;if(o&&(!e.hash||o.hash===e.hash))return o.expiry&&new Date>new Date(o.expiry)?(this.deleteKey(o.key),void r(new Error(""))):void t(o);r(new Error(""))},n.onerror=e=>{r(e.target.error)}}catch(e){r(e.target.error)}}))}_fetchObject(e){return new Promise(((t,r)=>{fetch(e.src).then((n=>{n.ok?n.blob().then((n=>{const o={key:e.key,hash:e.hash,expiry:e.expiry,blob:n};try{const e=this._store().put(o);e.onsuccess=()=>t(o),e.onerror=e=>r(e.target.error)}catch(e){r(e)}})):r(new Error(`error fetching asset: ${n.status}`))})).catch((e=>r(e)))}))}_applyElement(e,t){let r=e.src;switch(t&&(r=window.URL.createObjectURL(t)),e.el.tagName){case"SCRIPT":case"IMG":e.el.src=r;break;case"LINK":e.el.href=r}e.el.dataset.indexed=!0}_prune(e){const t=e.reduce(((e,t)=>(e[t]=!0,e)),{});this._store().getAllKeys().onsuccess=e=>{e.target.result.forEach((e=>{e in t||this.deleteKey(e)}))}}_store(){return this.db.transaction(this.opt.storeName,"readwrite").objectStore(this.opt.storeName)}}})); diff --git a/package.json b/package.json index 83ad585..add9c9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knadh/indexed-cache", - "version": "0.3.3", + "version": "0.4.0", "description": "Sideload static assets on pages and cache them in IndexedDB ensuring longer-term storage.", "main": "src/index.js", "files": [ From 93109dbab36a8472c7d761da73fddc43f9b99168 Mon Sep 17 00:00:00 2001 From: wiwium Date: Thu, 9 Sep 2021 17:27:19 +0530 Subject: [PATCH 09/23] async/await clean up --- src/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 1f2692f..b2c6123 100644 --- a/src/index.js +++ b/src/index.js @@ -44,10 +44,7 @@ export default class IndexedCache { async load (elements) { // This will setup the elements on the page irrespective of whether // the DB is available or not. - let objs = [] - await this._setupElements(elements).then((_objs) => { - objs = _objs - }) + const objs = await this._setupElements(elements) if (!this.db || objs.length === 0) { return @@ -76,7 +73,7 @@ export default class IndexedCache { } // Initialize the indexedDB database and create the store. - async _initDB (dbName, storeName) { + _initDB (dbName, storeName) { return new Promise((resolve, reject) => { if (!window.indexedDB) { reject(new Error('indexedDB is not available')) From 6491932d4509cd18a731db3c8c4a5b45864120bc Mon Sep 17 00:00:00 2001 From: Vivek R Date: Thu, 9 Sep 2021 17:39:12 +0530 Subject: [PATCH 10/23] chore: add gitignore --- .gitignore | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b78295e --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.DS_Store +node_modules + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? From 9a8bd3fdeeee4521d03ee3b564e97f744f62e84f Mon Sep 17 00:00:00 2001 From: Vivek R Date: Thu, 9 Sep 2021 17:40:16 +0530 Subject: [PATCH 11/23] fix: add polyfill for Promise.allSettled --- src/index.js | 4 +++- src/polyfills.js | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/polyfills.js diff --git a/src/index.js b/src/index.js index 1f2692f..ab1f841 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,6 @@ +import { allSettled } from './polyfills' let _icLoaded = false + export default class IndexedCache { constructor (options) { if (_icLoaded) { @@ -178,7 +180,7 @@ export default class IndexedCache { // Once the assets have been fetched, apply them synchronously. Since // the time take to execute a script is not guaranteed, use the onload() event // of each element to load the next element. - await Promise.allSettled(promises).then((results) => { + await allSettled(promises).then((results) => { results.forEach((cur, n) => { if (cur.status === 'rejected') { this._applyElement(objs[n]) diff --git a/src/polyfills.js b/src/polyfills.js new file mode 100644 index 0000000..172013e --- /dev/null +++ b/src/polyfills.js @@ -0,0 +1,7 @@ +export function allSettled (promises) { + const wrappedPromises = promises.map(p => Promise.resolve(p) + .then( + val => ({ status: 'fulfilled', value: val }), + err => ({ status: 'rejected', reason: err }))) + return Promise.all(wrappedPromises) +} From 87a2dab3e2512ff522efe3d435e1d7fb0c7a1082 Mon Sep 17 00:00:00 2001 From: Vivek R Date: Thu, 9 Sep 2021 17:41:50 +0530 Subject: [PATCH 12/23] chore: bump version to v0.4.1 --- README.md | 12 ++++++++---- dist/indexed-cache.esm.js | 11 ++++++++++- dist/indexed-cache.min.js | 4 ++-- package.json | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1f61ca2..b3de5d6 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,28 @@ indexed-cache is a tiny Javascript library that "sideloads" static assets (script, link, and img tags) on webpages using the fetch() API and caches them in an IndexedDB store to eliminate the dependency on the standard browser static asset cache, and to eliminate HTTP requests on subsequent page loads. Javascript, CSS, and image assets are stored in IndexedDB as Blob()s. -### For very specific scenarios only! +### For very specific scenarios only + This library is only meant to be used in very specific scenarios. Unlike the browser's asset cache, IndexedDB is not cleared automatically, providing a longer term static file storage on the client side. The lib uses ES6 (and IndexedDB) and is only expected to work on recent versions of modern browsers. Ideally, this should have been handled with ServiceWorkers, but they don't work in mobile webviews. Use if at least a few of these are true: + - There are large static files (JS, CSS) that rarely change. - High traffic from a large number of returning users who access web pages with the same assets regularly and frequently. - The pages are mostly inside mobile webviews where browser cache gets evicted (OS pressure) causing the same assets to be fetched afresh over and over wasting bandwidth. - Bandwidth is a concern. ### Features + - Supports script, img, link tags. - Respects `defer / async` on script tags. - Can invalidate cached items with a TTL per tag. - Can invalidate cached items with a simple random hash per tag. ### Gotchas + - CORS. - First-paint "flash" (needs to be handled manually) as scripts and styles only load after HTML is fetched and rendered by the browser. - Browser compatibility. @@ -30,6 +34,7 @@ Use if at least a few of these are true: ## Usage To cache and sideload static assets: + - Change the original `src` (`href` for CSS) attribute on tags to `data-src`. - Give tags a unique ID with `data-key`. The cached items are stored in the database with this key. The actual filenames of the assets can change freely, like in the case of JS build systems. - Load and invoke indexed-cache at the end. @@ -73,16 +78,15 @@ To cache and sideload static assets: Always include and invoke indexed-cache at the end, right before . Use the unpkg CDN or download and host the script locally (dist/indexed-cache.min.js). !--> - + ``` - #### Optional configuration -One or more of these optional params can be passed during initialization. Default values are shown below. +One or more of these optional params can be passed during initialization. Default values are shown below. ```javascript new IndexedCache({ diff --git a/dist/indexed-cache.esm.js b/dist/indexed-cache.esm.js index c1b6aec..0658fe7 100644 --- a/dist/indexed-cache.esm.js +++ b/dist/indexed-cache.esm.js @@ -1,4 +1,13 @@ +function allSettled (promises) { + const wrappedPromises = promises.map(p => Promise.resolve(p) + .then( + val => ({ status: 'fulfilled', value: val }), + err => ({ status: 'rejected', reason: err }))); + return Promise.all(wrappedPromises) +} + let _icLoaded = false; + class IndexedCache { constructor (options) { if (_icLoaded) { @@ -178,7 +187,7 @@ class IndexedCache { // Once the assets have been fetched, apply them synchronously. Since // the time take to execute a script is not guaranteed, use the onload() event // of each element to load the next element. - await Promise.allSettled(promises).then((results) => { + await allSettled(promises).then((results) => { results.forEach((cur, n) => { if (cur.status === 'rejected') { this._applyElement(objs[n]); diff --git a/dist/indexed-cache.min.js b/dist/indexed-cache.min.js index 3cec51b..a531f55 100644 --- a/dist/indexed-cache.min.js +++ b/dist/indexed-cache.min.js @@ -1,3 +1,3 @@ -/* indexed-cache - v0.4.0 +/* indexed-cache - v0.4.1 * Kailash Nadh. Licensed MIT */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).IndexedCache=t()}(this,(function(){"use strict";function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function t(e,t,r,n,o,a,s){try{var i=e[a](s),c=i.value}catch(e){return void r(e)}i.done?t(c):Promise.resolve(c).then(n,o)}function r(e){return function(){var r=this,n=arguments;return new Promise((function(o,a){var s=e.apply(r,n);function i(e){t(s,o,a,i,c,"next",e)}function c(e){t(s,o,a,i,c,"throw",e)}i(void 0)}))}}function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}let o=!1;return class{constructor(t){if(o)throw new Error("indexed-cache is already loaded");o=!0,this.opt=function(t){for(var r=1;r{e.db=t})).catch((e=>{console.log("error initializing cache DB. failing over.",e)})))}))()}load(e){var t=this;return r((function*(){let r=[];if(yield t._setupElements(e).then((e=>{r=e})),t.db&&0!==r.length&&t.opt.prune){const e=r.map((e=>e.key));t._prune(e)}}))()}deleteKey(e){this._store().delete(e)}prune(e){this._prune(e)}clear(){this._store().clear()}_initDB(e,t){var n=this;return r((function*(){return new Promise(((r,o)=>{window.indexedDB||o(new Error("indexedDB is not available"));const a=window.indexedDB.open(e);a.onupgradeneeded=e=>{const n=e.target.result;e.target.result.objectStoreNames.contains(t)||(n.createObjectStore(t,{keyPath:"key"}),e.target.transaction.oncomplete=()=>{r(n)})},a.onsuccess=()=>r(a.result),a.onerror=e=>o(e.target.error),setTimeout((()=>{n.db||o(new Error("Opening IndexedbDB timed out"))}),200)}))}))()}_setupElements(e){var t=this;return r((function*(){const r=[];if(e instanceof NodeList)e=Array.from(e);else if(e instanceof Node)e=[e];else{const r=t.opt.tags.map((e=>`${e}[data-src]:not([data-indexed])`)).join(",");e=document.querySelectorAll(r)}e.forEach((e=>{if("indexed"in e.dataset)return;const n={el:e,key:e.dataset.key||e.dataset.src,src:e.dataset.src,hash:e.dataset.hash||e.dataset.src,isAsync:"SCRIPT"!==e.tagName||e.hasAttribute("async")||e.hasAttribute("defer"),expiry:null},o=e.dataset.expiry||t.opt.expiry;o&&(n.expiry=new Date((new Date).getTime()+6e4*parseInt(o))),t.db?r.push(n):t._applyElement(n)}));const n=[];return r.forEach((e=>{e.isAsync?t._getObject(e).then((r=>{t._applyElement(e,r.data.blob)})).catch((r=>{t._applyElement(e)})):n.push(t._getObject(e))})),0===n.length||(yield Promise.allSettled(n).then((e=>{e.forEach(((n,o)=>{"rejected"!==n.status?o>=e.length-1||(n.value.obj.el.onload=n.value.obj.el.onerror=()=>{t._applyElement(e[o+1].value.obj,e[o+1].value.data.blob)}):t._applyElement(r[o])})),t._applyElement(e[0].value.obj,e[0].value.data.blob)}))),r}))()}_getObject(e){return new Promise(((t,r)=>{this._getDBblob(e).then((r=>{t({obj:e,data:r})})).catch((r=>{"Error"!==r.toString()&&console.log("error getting cache blob:",r),this._fetchObject(e).then((r=>{t({obj:e,data:r})})).catch((r=>{t({obj:e,data:{key:e.key,hash:e.hash,expiry:e.expiry,blob:null}})}))}))}))}_getDBblob(e){return new Promise(((t,r)=>{try{const n=this._store().get(e.key);n.onsuccess=n=>{const o=n.target.result;if(o&&(!e.hash||o.hash===e.hash))return o.expiry&&new Date>new Date(o.expiry)?(this.deleteKey(o.key),void r(new Error(""))):void t(o);r(new Error(""))},n.onerror=e=>{r(e.target.error)}}catch(e){r(e.target.error)}}))}_fetchObject(e){return new Promise(((t,r)=>{fetch(e.src).then((n=>{n.ok?n.blob().then((n=>{const o={key:e.key,hash:e.hash,expiry:e.expiry,blob:n};try{const e=this._store().put(o);e.onsuccess=()=>t(o),e.onerror=e=>r(e.target.error)}catch(e){r(e)}})):r(new Error(`error fetching asset: ${n.status}`))})).catch((e=>r(e)))}))}_applyElement(e,t){let r=e.src;switch(t&&(r=window.URL.createObjectURL(t)),e.el.tagName){case"SCRIPT":case"IMG":e.el.src=r;break;case"LINK":e.el.href=r}e.el.dataset.indexed=!0}_prune(e){const t=e.reduce(((e,t)=>(e[t]=!0,e)),{});this._store().getAllKeys().onsuccess=e=>{e.target.result.forEach((e=>{e in t||this.deleteKey(e)}))}}_store(){return this.db.transaction(this.opt.storeName,"readwrite").objectStore(this.opt.storeName)}}})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).IndexedCache=t()}(this,(function(){"use strict";function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function t(e,t,r,n,o,a,s){try{var i=e[a](s),c=i.value}catch(e){return void r(e)}i.done?t(c):Promise.resolve(c).then(n,o)}function r(e){return function(){var r=this,n=arguments;return new Promise((function(o,a){var s=e.apply(r,n);function i(e){t(s,o,a,i,c,"next",e)}function c(e){t(s,o,a,i,c,"throw",e)}i(void 0)}))}}function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}let o=!1;return class{constructor(t){if(o)throw new Error("indexed-cache is already loaded");o=!0,this.opt=function(t){for(var r=1;r{e.db=t})).catch((e=>{console.log("error initializing cache DB. failing over.",e)})))}))()}load(e){var t=this;return r((function*(){let r=[];if(yield t._setupElements(e).then((e=>{r=e})),t.db&&0!==r.length&&t.opt.prune){const e=r.map((e=>e.key));t._prune(e)}}))()}deleteKey(e){this._store().delete(e)}prune(e){this._prune(e)}clear(){this._store().clear()}_initDB(e,t){var n=this;return r((function*(){return new Promise(((r,o)=>{window.indexedDB||o(new Error("indexedDB is not available"));const a=window.indexedDB.open(e);a.onupgradeneeded=e=>{const n=e.target.result;e.target.result.objectStoreNames.contains(t)||(n.createObjectStore(t,{keyPath:"key"}),e.target.transaction.oncomplete=()=>{r(n)})},a.onsuccess=()=>r(a.result),a.onerror=e=>o(e.target.error),setTimeout((()=>{n.db||o(new Error("Opening IndexedbDB timed out"))}),200)}))}))()}_setupElements(e){var t=this;return r((function*(){const r=[];if(e instanceof NodeList)e=Array.from(e);else if(e instanceof Node)e=[e];else{const r=t.opt.tags.map((e=>`${e}[data-src]:not([data-indexed])`)).join(",");e=document.querySelectorAll(r)}e.forEach((e=>{if("indexed"in e.dataset)return;const n={el:e,key:e.dataset.key||e.dataset.src,src:e.dataset.src,hash:e.dataset.hash||e.dataset.src,isAsync:"SCRIPT"!==e.tagName||e.hasAttribute("async")||e.hasAttribute("defer"),expiry:null},o=e.dataset.expiry||t.opt.expiry;o&&(n.expiry=new Date((new Date).getTime()+6e4*parseInt(o))),t.db?r.push(n):t._applyElement(n)}));const n=[];return r.forEach((e=>{e.isAsync?t._getObject(e).then((r=>{t._applyElement(e,r.data.blob)})).catch((r=>{t._applyElement(e)})):n.push(t._getObject(e))})),0===n.length||(yield function(e){const t=e.map((e=>Promise.resolve(e).then((e=>({status:"fulfilled",value:e})),(e=>({status:"rejected",reason:e})))));return Promise.all(t)}(n).then((e=>{e.forEach(((n,o)=>{"rejected"!==n.status?o>=e.length-1||(n.value.obj.el.onload=n.value.obj.el.onerror=()=>{t._applyElement(e[o+1].value.obj,e[o+1].value.data.blob)}):t._applyElement(r[o])})),t._applyElement(e[0].value.obj,e[0].value.data.blob)}))),r}))()}_getObject(e){return new Promise(((t,r)=>{this._getDBblob(e).then((r=>{t({obj:e,data:r})})).catch((r=>{"Error"!==r.toString()&&console.log("error getting cache blob:",r),this._fetchObject(e).then((r=>{t({obj:e,data:r})})).catch((r=>{t({obj:e,data:{key:e.key,hash:e.hash,expiry:e.expiry,blob:null}})}))}))}))}_getDBblob(e){return new Promise(((t,r)=>{try{const n=this._store().get(e.key);n.onsuccess=n=>{const o=n.target.result;if(o&&(!e.hash||o.hash===e.hash))return o.expiry&&new Date>new Date(o.expiry)?(this.deleteKey(o.key),void r(new Error(""))):void t(o);r(new Error(""))},n.onerror=e=>{r(e.target.error)}}catch(e){r(e.target.error)}}))}_fetchObject(e){return new Promise(((t,r)=>{fetch(e.src).then((n=>{n.ok?n.blob().then((n=>{const o={key:e.key,hash:e.hash,expiry:e.expiry,blob:n};try{const e=this._store().put(o);e.onsuccess=()=>t(o),e.onerror=e=>r(e.target.error)}catch(e){r(e)}})):r(new Error(`error fetching asset: ${n.status}`))})).catch((e=>r(e)))}))}_applyElement(e,t){let r=e.src;switch(t&&(r=window.URL.createObjectURL(t)),e.el.tagName){case"SCRIPT":case"IMG":e.el.src=r;break;case"LINK":e.el.href=r}e.el.dataset.indexed=!0}_prune(e){const t=e.reduce(((e,t)=>(e[t]=!0,e)),{});this._store().getAllKeys().onsuccess=e=>{e.target.result.forEach((e=>{e in t||this.deleteKey(e)}))}}_store(){return this.db.transaction(this.opt.storeName,"readwrite").objectStore(this.opt.storeName)}}})); diff --git a/package.json b/package.json index add9c9c..86105d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knadh/indexed-cache", - "version": "0.4.0", + "version": "0.4.1", "description": "Sideload static assets on pages and cache them in IndexedDB ensuring longer-term storage.", "main": "src/index.js", "files": [ From 843e0242af8be16b059689ba05f7f2d2c3eb80fc Mon Sep 17 00:00:00 2001 From: Vivek R Date: Fri, 10 Sep 2021 12:44:41 +0530 Subject: [PATCH 13/23] fix: add legacy bundle (ES5 + polyfills) --- README.md | 45 +++- babel.config.js | 12 -- dist/indexed-cache.esm.js | 350 ------------------------------- dist/indexed-cache.esm.min.js | 3 + dist/indexed-cache.legacy.min.js | 3 + package.json | 3 +- rollup.config.js | 62 +++++- 7 files changed, 109 insertions(+), 369 deletions(-) delete mode 100644 babel.config.js delete mode 100644 dist/indexed-cache.esm.js create mode 100644 dist/indexed-cache.esm.min.js create mode 100644 dist/indexed-cache.legacy.min.js diff --git a/README.md b/README.md index b3de5d6..cbfe2f4 100644 --- a/README.md +++ b/README.md @@ -78,12 +78,53 @@ To cache and sideload static assets: Always include and invoke indexed-cache at the end, right before . Use the unpkg CDN or download and host the script locally (dist/indexed-cache.min.js). !--> - - + + + + + + ``` +#### Load modern and legacy bundle conditionally + +Here is an example on how to load modern(ESM) bundle and legacy bundle conditionally based on browser support. + +```html + + + + + + +``` + #### Optional configuration One or more of these optional params can be passed during initialization. Default values are shown below. diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index e8c1493..0000000 --- a/babel.config.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - presets: [ - [ - '@babel/preset-env', - { - targets: { - node: '6.5' - } - } - ] - ] -} diff --git a/dist/indexed-cache.esm.js b/dist/indexed-cache.esm.js deleted file mode 100644 index 0658fe7..0000000 --- a/dist/indexed-cache.esm.js +++ /dev/null @@ -1,350 +0,0 @@ -function allSettled (promises) { - const wrappedPromises = promises.map(p => Promise.resolve(p) - .then( - val => ({ status: 'fulfilled', value: val }), - err => ({ status: 'rejected', reason: err }))); - return Promise.all(wrappedPromises) -} - -let _icLoaded = false; - -class IndexedCache { - constructor (options) { - if (_icLoaded) { - throw new Error('indexed-cache is already loaded') - } - _icLoaded = true; - - this.opt = { - tags: ['script', 'img', 'link'], - dbName: 'indexed-cache', - storeName: 'objects', - - // If this is enabled, all objects in the cash with keys not - // found on elements on the page (data-key) will be deleted by load(). - // This can be problematic in scenarios where there are multiple - // pages on the same domain that have different assets, some on - // certain pages and some on other. - prune: false, - - // Default expiry for an object in minutes (default 3 months). - // Set to null for no expiry. - expiry: 131400, - - ...options - }; - this.db = null; - } - - // This should be called before calling any other methods. - async init () { - if (this.db) { - return - } - - await this._initDB(this.opt.dbName, this.opt.storeName).then((db) => { - this.db = db; - }).catch((e) => { - console.log('error initializing cache DB. failing over.', e); - }); - } - - // Initialize the DB and then scan and setup DOM elements to cache. - async load (elements) { - // This will setup the elements on the page irrespective of whether - // the DB is available or not. - let objs = []; - await this._setupElements(elements).then((_objs) => { - objs = _objs; - }); - - if (!this.db || objs.length === 0) { - return - } - - // If pruning is enabled, delete all cached elements that are no longer - // referenced on the page. - if (this.opt.prune) { - // Pass the list of keys found on the page. - const keys = objs.map(obj => obj.key); - this._prune(keys); - } - } - - deleteKey (key) { - this._store().delete(key); - } - - // Prune all objects in the DB that are not in the given list of keys. - prune (keys) { - this._prune(keys); - } - - clear () { - this._store().clear(); - } - - // Initialize the indexedDB database and create the store. - async _initDB (dbName, storeName) { - return new Promise((resolve, reject) => { - if (!window.indexedDB) { - reject(new Error('indexedDB is not available')); - } - - const req = window.indexedDB.open(dbName); - - // Setup the DB schema for the first time. - req.onupgradeneeded = (e) => { - const db = e.target.result; - if (!e.target.result.objectStoreNames.contains(storeName)) { - db.createObjectStore(storeName, { keyPath: 'key' }); - e.target.transaction.oncomplete = () => { - resolve(db); - }; - } - }; - - req.onsuccess = () => resolve(req.result); - - req.onerror = (e) => reject(e.target.error); - - // Hacky fix for IndexedDB randomly locking up in Safari. - setTimeout(() => { - if (!this.db) { - reject(new Error('Opening IndexedbDB timed out')); - } - }, 200); - }) - } - - // Scan all matching elements and either: - // a) if indexedDB is not available, fallback to loading the assets natively. - // b) if DB is available but the object is not cached, fetch(), cache in the DB, and apply the blob. - // c) if DB is available and the object is cached, apply the cached blob. - // elements should either be null or be a NodeList. - async _setupElements (elements) { - const objs = []; - - // If there are no elements, scan the entire DOM for groups of each tag type. - if (elements instanceof NodeList) { - elements = Array.from(elements); - } else if (elements instanceof Node) { - elements = [elements]; - } else { - const sel = this.opt.tags.map((t) => `${t}[data-src]:not([data-indexed])`).join(','); - elements = document.querySelectorAll(sel); - } - - // Get all tags of a particular tag on the page that has the data-src attrib. - // document.querySelectorAll(`${tag}[data-src]:not([data-indexed])`).forEach((el) => { - elements.forEach((el) => { - if ('indexed' in el.dataset) { - return - } - const obj = { - el: el, - key: el.dataset.key || el.dataset.src, - src: el.dataset.src, - hash: el.dataset.hash || el.dataset.src, - isAsync: el.tagName !== 'SCRIPT' || el.hasAttribute('async') || el.hasAttribute('defer'), - expiry: null - }; - - // If there is a global expiry or an expiry on the object, compute that. - const exp = el.dataset.expiry || this.opt.expiry; - if (exp) { - obj.expiry = new Date(new Date().getTime() + (parseInt(exp) * 60000)); - } - - // If for any reason the store is not initialized, fall back to - // the native asset loading mechanism. - if (this.db) { - objs.push(obj); - } else { - this._applyElement(obj); - } - }); - - const promises = []; - objs.forEach((obj) => { - if (obj.isAsync) { - // Load and apply async objects asynchronously. - this._getObject(obj).then((result) => { - this._applyElement(obj, result.data.blob); - }).catch((e) => { - this._applyElement(obj); - }); - } else { - // Load non-async objects asynchronously (but apply synchronously). - promises.push(this._getObject(obj)); - } - }); - - if (promises.length === 0) { - return objs - } - - // Once the assets have been fetched, apply them synchronously. Since - // the time take to execute a script is not guaranteed, use the onload() event - // of each element to load the next element. - await allSettled(promises).then((results) => { - results.forEach((cur, n) => { - if (cur.status === 'rejected') { - this._applyElement(objs[n]); - return - } - - if (n >= results.length - 1) { - return - } - - cur.value.obj.el.onload = cur.value.obj.el.onerror = () => { - this._applyElement(results[n + 1].value.obj, results[n + 1].value.data.blob); - }; - }); - - // Start the chain by loading the first element. - this._applyElement(results[0].value.obj, results[0].value.data.blob); - }); - - return objs - } - - // Get the object from the DB and if that fails, fetch() it over HTTP - // This function should not reject a promise and in the case of failure, - // will return a dummy data object as if it were fetched from the DB. - _getObject (obj) { - return new Promise((resolve, reject) => { - // Get the stored blob. - this._getDBblob(obj).then((data) => { - resolve({ obj, data }); - }).catch((e) => { - // If there is no cause, the object is not cached or has expired. - if (e.toString() !== 'Error') { - console.log('error getting cache blob:', e); - } - - // Couldn't get the stored blog. Attempt to fetch() and cache. - this._fetchObject(obj).then((data) => { - resolve({ obj, data }); - }).catch((e) => { - // Everything failed. Failover to loading assets natively. - resolve({ - obj, - data: { - key: obj.key, - hash: obj.hash, - expiry: obj.expiry, - blob: null - } - }); - }); - }); - }) - } - - // Get the blob of an asset stored in the DB. If there is no entry or it has expired - // (hash changed or date expired), fetch the asset over HTTP, cache it, and load it. - _getDBblob (obj) { - return new Promise((resolve, reject) => { - try { - const req = this._store().get(obj.key); - req.onsuccess = (e) => { - const data = e.target.result; - - // Reject if there is no stored data, or if the hash has changed. - if (!data || (obj.hash && (data.hash !== obj.hash))) { - reject(new Error('')); - return - } - - // Reject and delete if the object has expired. - if (data.expiry && new Date() > new Date(data.expiry)) { - this.deleteKey(data.key); - reject(new Error('')); - return - } - - resolve(data); - }; - - req.onerror = (e) => { - reject(e.target.error); - }; - } catch (e) { - reject(e.target.error); - } - }) - } - - // Fetch an asset and cache it. - _fetchObject (obj) { - return new Promise((resolve, reject) => { - fetch(obj.src).then((r) => { - // HTTP request failed. - if (!r.ok) { - reject(new Error(`error fetching asset: ${r.status}`)); - return - } - - // Write the fetched blob to the DB. - r.blob().then((b) => { - const data = { - key: obj.key, - hash: obj.hash, - expiry: obj.expiry, - blob: b - }; - - // onerror() may not always trigger like in the private mode in Safari. - try { - const req = this._store().put(data); - req.onsuccess = () => resolve(data); - req.onerror = (e) => reject(e.target.error); - } catch (e) { - reject(e); - } - }); - }).catch((e) => reject(e)); - }) - } - - // Apply the Blob (if given), or the original obj.src URL to the given element. - _applyElement (obj, blob) { - let url = obj.src; - if (blob) { - url = window.URL.createObjectURL(blob); - } - - switch (obj.el.tagName) { - case 'SCRIPT': - case 'IMG': - obj.el.src = url; - break - case 'LINK': - obj.el.href = url; - } - obj.el.dataset.indexed = true; - } - - // Delete all objects in cache that are not in the given list of objects. - _prune (keys) { - // Prepare a { key: true } lookup map of all keys found on the page. - const keyMap = keys.reduce((obj, v) => { obj[v] = true; return obj }, {}); - - const req = this._store().getAllKeys(); - req.onsuccess = (e) => { - e.target.result.forEach((key) => { - if (!(key in keyMap)) { - this.deleteKey(key); - } - }); - }; - } - - _store () { - return this.db.transaction(this.opt.storeName, 'readwrite').objectStore(this.opt.storeName) - } -} - -export { IndexedCache as default }; diff --git a/dist/indexed-cache.esm.min.js b/dist/indexed-cache.esm.min.js new file mode 100644 index 0000000..986ab17 --- /dev/null +++ b/dist/indexed-cache.esm.min.js @@ -0,0 +1,3 @@ +/* indexed-cache - v0.4.1 +* Kailash Nadh. Licensed MIT */ +let e=!1;class t{constructor(t){if(e)throw new Error("indexed-cache is already loaded");e=!0,this.opt={tags:["script","img","link"],dbName:"indexed-cache",storeName:"objects",prune:!1,expiry:131400,...t},this.db=null}async init(){this.db||await this._initDB(this.opt.dbName,this.opt.storeName).then((e=>{this.db=e})).catch((e=>{console.log("error initializing cache DB. failing over.",e)}))}async load(e){let t=[];if(await this._setupElements(e).then((e=>{t=e})),this.db&&0!==t.length&&this.opt.prune){const e=t.map((e=>e.key));this._prune(e)}}deleteKey(e){this._store().delete(e)}prune(e){this._prune(e)}clear(){this._store().clear()}async _initDB(e,t){return new Promise(((r,s)=>{window.indexedDB||s(new Error("indexedDB is not available"));const a=window.indexedDB.open(e);a.onupgradeneeded=e=>{const s=e.target.result;e.target.result.objectStoreNames.contains(t)||(s.createObjectStore(t,{keyPath:"key"}),e.target.transaction.oncomplete=()=>{r(s)})},a.onsuccess=()=>r(a.result),a.onerror=e=>s(e.target.error),setTimeout((()=>{this.db||s(new Error("Opening IndexedbDB timed out"))}),200)}))}async _setupElements(e){const t=[];if(e instanceof NodeList)e=Array.from(e);else if(e instanceof Node)e=[e];else{const t=this.opt.tags.map((e=>`${e}[data-src]:not([data-indexed])`)).join(",");e=document.querySelectorAll(t)}e.forEach((e=>{if("indexed"in e.dataset)return;const r={el:e,key:e.dataset.key||e.dataset.src,src:e.dataset.src,hash:e.dataset.hash||e.dataset.src,isAsync:"SCRIPT"!==e.tagName||e.hasAttribute("async")||e.hasAttribute("defer"),expiry:null},s=e.dataset.expiry||this.opt.expiry;s&&(r.expiry=new Date((new Date).getTime()+6e4*parseInt(s))),this.db?t.push(r):this._applyElement(r)}));const r=[];return t.forEach((e=>{e.isAsync?this._getObject(e).then((t=>{this._applyElement(e,t.data.blob)})).catch((t=>{this._applyElement(e)})):r.push(this._getObject(e))})),0===r.length||await function(e){const t=e.map((e=>Promise.resolve(e).then((e=>({status:"fulfilled",value:e})),(e=>({status:"rejected",reason:e})))));return Promise.all(t)}(r).then((e=>{e.forEach(((r,s)=>{"rejected"!==r.status?s>=e.length-1||(r.value.obj.el.onload=r.value.obj.el.onerror=()=>{this._applyElement(e[s+1].value.obj,e[s+1].value.data.blob)}):this._applyElement(t[s])})),this._applyElement(e[0].value.obj,e[0].value.data.blob)})),t}_getObject(e){return new Promise(((t,r)=>{this._getDBblob(e).then((r=>{t({obj:e,data:r})})).catch((r=>{"Error"!==r.toString()&&console.log("error getting cache blob:",r),this._fetchObject(e).then((r=>{t({obj:e,data:r})})).catch((r=>{t({obj:e,data:{key:e.key,hash:e.hash,expiry:e.expiry,blob:null}})}))}))}))}_getDBblob(e){return new Promise(((t,r)=>{try{const s=this._store().get(e.key);s.onsuccess=s=>{const a=s.target.result;if(a&&(!e.hash||a.hash===e.hash))return a.expiry&&new Date>new Date(a.expiry)?(this.deleteKey(a.key),void r(new Error(""))):void t(a);r(new Error(""))},s.onerror=e=>{r(e.target.error)}}catch(e){r(e.target.error)}}))}_fetchObject(e){return new Promise(((t,r)=>{fetch(e.src).then((s=>{s.ok?s.blob().then((s=>{const a={key:e.key,hash:e.hash,expiry:e.expiry,blob:s};try{const e=this._store().put(a);e.onsuccess=()=>t(a),e.onerror=e=>r(e.target.error)}catch(e){r(e)}})):r(new Error(`error fetching asset: ${s.status}`))})).catch((e=>r(e)))}))}_applyElement(e,t){let r=e.src;switch(t&&(r=window.URL.createObjectURL(t)),e.el.tagName){case"SCRIPT":case"IMG":e.el.src=r;break;case"LINK":e.el.href=r}e.el.dataset.indexed=!0}_prune(e){const t=e.reduce(((e,t)=>(e[t]=!0,e)),{});this._store().getAllKeys().onsuccess=e=>{e.target.result.forEach((e=>{e in t||this.deleteKey(e)}))}}_store(){return this.db.transaction(this.opt.storeName,"readwrite").objectStore(this.opt.storeName)}}export{t as default}; diff --git a/dist/indexed-cache.legacy.min.js b/dist/indexed-cache.legacy.min.js new file mode 100644 index 0000000..c9504dc --- /dev/null +++ b/dist/indexed-cache.legacy.min.js @@ -0,0 +1,3 @@ +/* indexed-cache - v0.4.1 +* Kailash Nadh. Licensed MIT */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("core-js/modules/es.array.map.js"),require("core-js/modules/es.object.to-string.js"),require("core-js/modules/es.promise.js"),require("core-js/modules/es.array.from.js"),require("core-js/modules/es.string.iterator.js"),require("core-js/modules/es.array.join.js"),require("core-js/modules/web.dom-collections.for-each.js"),require("core-js/modules/es.regexp.to-string.js"),require("core-js/modules/es.array.iterator.js"),require("core-js/modules/web.dom-collections.iterator.js"),require("core-js/modules/web.url.js")):"function"==typeof define&&define.amd?define(["core-js/modules/es.array.map.js","core-js/modules/es.object.to-string.js","core-js/modules/es.promise.js","core-js/modules/es.array.from.js","core-js/modules/es.string.iterator.js","core-js/modules/es.array.join.js","core-js/modules/web.dom-collections.for-each.js","core-js/modules/es.regexp.to-string.js","core-js/modules/es.array.iterator.js","core-js/modules/web.dom-collections.iterator.js","core-js/modules/web.url.js"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).IndexedCache=e()}(this,(function(){"use strict";function t(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,n)}return r}function e(t,e,r,n,i,o,s){try{var a=t[o](s),c=a.value}catch(t){return void r(t)}a.done?e(c):Promise.resolve(c).then(n,i)}function r(t){return function(){var r=this,n=arguments;return new Promise((function(i,o){var s=t.apply(r,n);function a(t){e(s,i,o,a,c,"next",t)}function c(t){e(s,i,o,a,c,"throw",t)}a(void 0)}))}}function n(t,e){for(var r=0;r=0;--o){var s=this.tryEntries[o],a=s.completion;if("root"===s.tryLoc)return i("end");if(s.tryLoc<=this.prev){var c=n.call(s,"catchLoc"),u=n.call(s,"finallyLoc");if(c&&u){if(this.prev=0;--r){var i=this.tryEntries[r];if(i.tryLoc<=this.prev&&n.call(i,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),L(r),y}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var i=n.arg;L(r)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(t,r,n){return this.delegate={iterator:N(t),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=e),y}},t}(t.exports);try{regeneratorRuntime=e}catch(t){"object"==typeof globalThis?globalThis.regeneratorRuntime=e:Function("r","regeneratorRuntime = r")(e)}}({exports:{}});var a=!1;return function(){function e(r){if(function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e),a)throw new Error("indexed-cache is already loaded");a=!0,this.opt=function(e){for(var r=1;r=t.length-1||(n.value.obj.el.onload=n.value.obj.el.onerror=function(){o(this,s),this._applyElement(t[i+1].value.obj,t[i+1].value.data.blob)}.bind(this)):this._applyElement(r[i])}.bind(this)),this._applyElement(t[0].value.obj,t[0].value.data.blob)}.bind(this));case 9:return t.abrupt("return",r);case 10:case"end":return t.stop()}}),t,this)}))),function(t){return l.apply(this,arguments)})},{key:"_getObject",value:function(t){var e=this;return new Promise(function(r,n){var i=this;o(this,e),this._getDBblob(t).then(function(e){o(this,i),r({obj:t,data:e})}.bind(this)).catch(function(e){var n=this;o(this,i),"Error"!==e.toString()&&console.log("error getting cache blob:",e),this._fetchObject(t).then(function(e){o(this,n),r({obj:t,data:e})}.bind(this)).catch(function(e){o(this,n),r({obj:t,data:{key:t.key,hash:t.hash,expiry:t.expiry,blob:null}})}.bind(this))}.bind(this))}.bind(this))}},{key:"_getDBblob",value:function(t){var e=this;return new Promise(function(r,n){var i=this;o(this,e);try{var s=this._store().get(t.key);s.onsuccess=function(e){o(this,i);var s=e.target.result;if(s&&(!t.hash||s.hash===t.hash))return s.expiry&&new Date>new Date(s.expiry)?(this.deleteKey(s.key),void n(new Error(""))):void r(s);n(new Error(""))}.bind(this),s.onerror=function(t){o(this,i),n(t.target.error)}.bind(this)}catch(t){n(t.target.error)}}.bind(this))}},{key:"_fetchObject",value:function(t){var e=this;return new Promise(function(r,n){var i=this;o(this,e),fetch(t.src).then(function(e){var s=this;o(this,i),e.ok?e.blob().then(function(e){var i=this;o(this,s);var a={key:t.key,hash:t.hash,expiry:t.expiry,blob:e};try{var c=this._store().put(a);c.onsuccess=function(){return o(this,i),r(a)}.bind(this),c.onerror=function(t){return o(this,i),n(t.target.error)}.bind(this)}catch(t){n(t)}}.bind(this)):n(new Error("error fetching asset: ".concat(e.status)))}.bind(this)).catch(function(t){return o(this,i),n(t)}.bind(this))}.bind(this))}},{key:"_applyElement",value:function(t,e){var r=t.src;switch(e&&(r=window.URL.createObjectURL(e)),t.el.tagName){case"SCRIPT":case"IMG":t.el.src=r;break;case"LINK":t.el.href=r}t.el.dataset.indexed=!0}},{key:"_prune",value:function(t){var e=this,r=t.reduce(function(t,r){return o(this,e),t[r]=!0,t}.bind(this),{});this._store().getAllKeys().onsuccess=function(t){var n=this;o(this,e),t.target.result.forEach(function(t){o(this,n),t in r||this.deleteKey(t)}.bind(this))}.bind(this)}},{key:"_store",value:function(){return this.db.transaction(this.opt.storeName,"readwrite").objectStore(this.opt.storeName)}}])&&n(c.prototype,u),h&&n(c,h),e}()})); diff --git a/package.json b/package.json index 86105d8..f23c0db 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "access": "public" }, "browser": "dist/indexed-cache.min.js", - "esm": "dist/indexed-cache.esm.js", + "browserLegacy": "dist/indexed-cache.legacy.min.js", + "esm": "dist/indexed-cache.esm.min.js", "devDependencies": { "@babel/core": "^7.14.6", "@babel/preset-env": "^7.14.7", diff --git a/rollup.config.js b/rollup.config.js index 7aa11b0..4e15c6e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,10 +9,11 @@ const preamble = `/* indexed-cache - v${pkg.version} * ${pkg.author}. Licensed ${pkg.license} */` export default [ + // UMD build only for modern browsers. { input: 'src/index.js', output: { - name: 'IndexedCache', // Name of the global object + name: 'IndexedCache', file: pkg.browser, format: 'umd', sourcemap: false @@ -23,8 +24,18 @@ export default [ }), resolve(), babel({ - exclude: 'node_modules/**', // only transpile our source code - babelHelpers: 'bundled' + exclude: 'node_modules/**', + babelHelpers: 'bundled', + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: '6.5' + } + } + ] + ] }), commonjs(), production && terser({ @@ -32,6 +43,46 @@ export default [ }) ] }, + // UMD build for legacy browsers which has polyfills + es5 transpilation. + { + input: 'src/index.js', + output: { + name: 'IndexedCache', + file: pkg.browserLegacy, + format: 'umd', + sourcemap: false + }, + plugins: [ + eslint({ + throwOnError: true + }), + resolve(), + babel({ + exclude: 'node_modules/**', + babelHelpers: 'bundled', + presets: [ + [ + '@babel/preset-env', + { + targets: { + browsers: '> 0.5%, not op_mini all, not dead' + }, + modules: false, + spec: true, + useBuiltIns: 'usage', + forceAllTransforms: true, + corejs: 3 + } + ] + ] + }), + commonjs(), + production && terser({ + output: { preamble } + }) + ] + }, + // ESM build which can be used as module. { input: 'src/index.js', output: [ @@ -43,7 +94,10 @@ export default [ ], plugins: [ eslint(), - resolve() + resolve(), + production && terser({ + output: { preamble } + }) ] } ] From 8f4547af15ac0b80bc65766df018a51e1bcfb8ec Mon Sep 17 00:00:00 2001 From: Vivek R Date: Fri, 10 Sep 2021 14:57:31 +0530 Subject: [PATCH 14/23] fix: check if db is available before calling this._store --- src/index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/index.js b/src/index.js index ab1f841..5de299b 100644 --- a/src/index.js +++ b/src/index.js @@ -65,6 +65,10 @@ export default class IndexedCache { } deleteKey (key) { + if (!this.db) { + return + } + this._store().delete(key) } @@ -74,6 +78,10 @@ export default class IndexedCache { } clear () { + if (!this.db) { + return + } + this._store().clear() } @@ -322,6 +330,10 @@ export default class IndexedCache { // Delete all objects in cache that are not in the given list of objects. _prune (keys) { + if (!this.db) { + return + } + // Prepare a { key: true } lookup map of all keys found on the page. const keyMap = keys.reduce((obj, v) => { obj[v] = true; return obj }, {}) From 500b03649159bd32d2929cc3a0be84a82adfc7c5 Mon Sep 17 00:00:00 2001 From: Vivek R Date: Fri, 10 Sep 2021 15:01:09 +0530 Subject: [PATCH 15/23] chore: bump version to v0.4.2 --- README.md | 8 ++++---- dist/indexed-cache.esm.min.js | 4 ++-- dist/indexed-cache.legacy.min.js | 4 ++-- dist/indexed-cache.min.js | 4 ++-- package.json | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cbfe2f4..7f94058 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,10 @@ To cache and sideload static assets: Always include and invoke indexed-cache at the end, right before . Use the unpkg CDN or download and host the script locally (dist/indexed-cache.min.js). !--> - + - + + + - + + ` tags will be executed as an inline script by the browser, after which the browser will not load the remote script when applied. Ensure that the opening and closing script tags have nothing between then. +- Scripts that rely on the `document.onload` event will need the event to be triggered for them manually once indexed-cache loads with a `document.dispatchEvent(new Event("load"));` ## Usage @@ -114,11 +115,14 @@ Here is an example on how to load modern(ESM) bundle and legacy bundle condition - +