Jake Vanderwerf
2025-09-30 2cb91676044ecd0abd9c45b4835abb8b0d042312
1
window.jvbStore=class{constructor(e={}){this.config={name:"default",endpoint:!1,apiBase:jvbSettings.api,TTL:36e5,showLoading:!0,headers:{},filters:{},...e},this.config.endpoint||console.warn("No endpoint set. Only saving locally"),this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.headers={"X-WP-Nonce":jvbSettings.nonce,...this.config.headers},this.items=new Map,this.cache=new Map,this.httpHeaders=new Map,this.domCache=new Map,this.forms=new Map,this.filters=e.filters??{},this.subscribers=new Set,this.db=null,this.currentRequest=null,this.cachedContent=JSON.parse(cacheJVB.cache)||{},this.lastTimestampUpdate=Date.now(),this.initDB(),document.addEventListener("beforeUnload",(()=>this.destroy()))}async initDB(){if(!("indexedDB"in window))return;const e=indexedDB.open(`jvb_${this.config.name}_db`,1);e.onupgradeneeded=e=>{const t=e.target.result;if(t.objectStoreNames.contains("items")||t.createObjectStore("items",{keyPath:"id"}),t.objectStoreNames.contains("dom")||t.createObjectStore("dom",{keyPath:"id"}),!t.objectStoreNames.contains("forms")){let e=t.createObjectStore("forms",{keyPath:"formId"});e.createIndex("status","status",{unique:!1}),e.createIndex("operationId","operationId",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1})}if(!t.objectStoreNames.contains("cache")){const e=t.createObjectStore("cache",{keyPath:"key"});e.createIndex("timestamp","timestamp",{unique:!1}),e.createIndex("endpoint","endpoint",{unique:!1}),e.createIndex("filters","filters",{unique:!1})}t.objectStoreNames.contains("headers")||t.createObjectStore("headers",{keyPath:"key"})},e.onsuccess=e=>{this.db=e.target.result,this.loadFromDB()},e.onerror=e=>{console.error("IndexedDB error:",e)}}async loadFromDB(){if(this.db)try{await Promise.all([this.loadItems(),this.loadCache(),this.loadHeaders(),this.loadDOMCache(),this.loadForms()])}catch(e){console.error("Error loading from DB:",e)}}async loadItems(){if(this.db)return new Promise((e=>{this.db.transaction(["items"],"readonly").objectStore("items").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.items.set(e.id,e)})),this.notify("items-loaded",{items:Array.from(this.items.values())}),e()}}))}async loadCache(){if(this.db)return new Promise((e=>{this.db.transaction(["cache"],"readonly").objectStore("cache").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.isCacheValid(e)&&this.cache.set(e.key,e)})),e()}}))}async loadHeaders(){if(this.db)return new Promise((e=>{this.db.transaction(["headers"],"readonly").objectStore("headers").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.httpHeaders.set(e.key,e)})),e()}}))}async loadDOMCache(){if(this.db)return new Promise((e=>{this.db.transaction(["dom"],"readonly").objectStore("dom").getAll().onsuccess=t=>{t.target.result.forEach((e=>{const t={};Object.entries(e.views).forEach((([e,s])=>{const i=document.createElement("div");i.innerHTML=s,t[e]=i.firstElementChild})),this.domCache.set(e.id,t)})),e()}}))}async loadForms(){if(this.db)return new Promise((e=>{this.db.transaction(["forms"],"readonly").objectStore("forms").getAll().onsuccess=t=>{t.target.result.forEach((e=>{this.forms.set(e.key,e)})),e()}}))}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading.showModal():this.loading.close()}async fetch(e=null,t={}){const{filters:s=this.filters,headers:i={}}=t;this.config.showLoading&&this.setLoading(!0);const r=e||this.config.endpoint;if(!r)throw new Error("No endpoint specified");const a=this.generateCacheKey(r,s),o=this.cleanFilters(s),n=new URLSearchParams(o),c=`${this.config.apiBase}${r}${n.toString()?"?"+n:""}`,h={...this.headers,...i},d=this.generateHeaderKey(c),l=this.httpHeaders.get(d),f=this.cache.get(a);l&&f&&(l.etag&&(h["If-None-Match"]=l.etag),l.lastModified&&(h["If-Modified-Since"]=l.lastModified));try{const e=await fetch(c,{method:"GET",headers:h});if(console.log("DataStore response status: ",e.status),304===e.status&&(console.debug(`304 Not Modified for ${c}`),f))return f.timestamp=Date.now(),this.cache.set(a,f),await this.saveCacheToDB(a,f),this.currentRequest={filters:o,data:f.data,cached:!0},this.notify("data-cached",{data:f.data,filters:o,cached:!0}),f.data;if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);this.storeResponseHeaders(d,e);const t=await e.json();console.log("Fetched data: ",t);const s={key:a,endpoint:r,data:t,timestamp:Date.now(),filters:o};return this.cache.set(a,s),await this.saveCacheToDB(a,s),t.items&&this.config.endpoint===r&&this.updateItems(t.items),this.currentRequest={filters:o,data:t,cached:!1},this.notify("data-fetched",{endpoint:r,data:t,filters:o}),t}catch(e){if(console.error("Fetch error:",e),f)return console.warn("Returning stale cache due to fetch error"),this.currentRequest={filters:o,data:f.data,cached:!0,stale:!0},this.notify("stale-cache-used",{data:f.data,filters:o}),f.data;throw this.notify("fetch-error",{error:e,filters:o}),e}finally{this.config.showLoading&&this.setLoading(!1)}}updateItems(e){this.items.clear(),e.forEach((e=>{this.items.set(e.id,e)})),this.saveItemsToDB(),this.notify("items-updated",{items:e})}getCurrentRequest(){return this.currentRequest}getItem(e){let t=parseInt(e);e=isNaN(t)?e:t;const s=this.items.get(e);return s?this.unserializeData(s):null}setItem(e,t,s=!0){if(s&&this.items.has(e)){let s=this.getItem(e);t=window.deepMerge(s,t)}const i=this.serializeData(t);return this.items.set(e,i),this.saveItemsToDB(),this.notify("item-stored",t),t}hasUnrecoverableFiles(e){if(!e||"object"!=typeof e)return!1;if(e._wasFile||e._wasBlob)return!0;if(Array.isArray(e))return e.some((e=>this.hasUnrecoverableFiles(e)));if(e instanceof FormData){for(const[t,s]of e.entries())if(s instanceof File||s instanceof Blob)return!0;return!1}return Object.values(e).some((e=>this.hasUnrecoverableFiles(e)))}serializeFormData(e){const t={};for(const[s,i]of e.entries())i instanceof File||(s in t?(Array.isArray(t[s])||(t[s]=[t[s]]),t[s].push(i)):t[s]=i);return t}serializeData(e){if(!e)return null;if(e instanceof HTMLElement)return null;if("object"!=typeof e)return e;if(null===e)return null;if(e instanceof FormData)return{_type:"FormData",...this.serializeFormData(e)};if(Array.isArray(e))return e.map((e=>this.serializeData(e)));if(e instanceof Date)return{_type:"Date",value:e.toISOString()};const t={};for(const[s,i]of Object.entries(e))t[s]=this.serializeData(i);return t}unserializeData(e){if(!e||"object"!=typeof e)return e;if(null===e)return null;if(e._type)switch(e._type){case"FormData":return this.unserializeFormData(e);case"File":return{_wasFile:!0,_fileMetadata:e,name:e.name,type:e.type,size:e.size};case"Blob":return{_wasBlob:!0,_blobMetadata:e,type:e.type,size:e.size};case"Date":return new Date(e.value)}if(Array.isArray(e))return e.map((e=>this.unserializeData(e)));const t={};for(const[s,i]of Object.entries(e))t[s]=this.unserializeData(i);return t}unserializeFormData(e){const t=new FormData;for(const[s,i]of Object.entries(e))Array.isArray(i)?i.forEach((e=>{e?._isFile?(console.warn(`Cannot restore file "${e.name}" from stored data`),t.append(s+"_was_file",JSON.stringify(e))):t.append(s,e)})):i?._isFile?(console.warn(`Cannot restore file "${i.name}" from stored data`),t.append(s+"_was_file",JSON.stringify(i))):null!=i&&t.append(s,i);return t}clearItem(e){this.items.delete(e),this.db&&this.db.transaction(["items"],"readwrite").objectStore("items").delete(e)}cleanFilters(e){const t={};return Object.entries(e).forEach((([e,s])=>{null!=s&&""!==s&&("taxonomies"===e&&"object"==typeof s?Object.entries(s).forEach((([e,s])=>{Array.isArray(s)&&s.length>0?t[`tax_${e}`]=s.join(","):s&&(t[`tax_${e}`]=s)})):"date"===e&&"object"==typeof s?(s.after&&(t.after=s.after),s.before&&(t.before=s.before)):t[e]=s)})),t}setFilter(e,t){const s=this.filters[e];""===t||null==t?delete this.filters[e]:this.filters[e]=t,this.notify("filters-changed",{filters:this.filters,changed:{key:e,oldValue:s,newValue:t}}),this.config.endpoint&&this.fetch()}removeFilter(e){const t=this.filters[e];void 0!==t&&(delete this.filters[e],this.notify("filters-changed",{filters:this.filters,removed:{key:e,oldValue:t}}),this.config.endpoint&&this.fetch())}clearFilters(){const e={...this.filters};this.filters=this.config.filters,this.notify("filters-cleared",{oldFilters:e,filters:this.filters}),this.config.endpoint&&this.fetch()}generateCacheKey(e,t){const s=Object.keys(t).sort().reduce(((e,s)=>(e[s]=t[s],e)),{});return`${e}_${JSON.stringify(s)}`}generateHeaderKey(e){return`headers_${e}`}isCacheValid(e,t=this.config.TTL){return!(!e||!e.timestamp)&&Date.now()-e.timestamp<t}storeResponseHeaders(e,t){const s={key:e,etag:t.headers.get("ETag"),lastModified:t.headers.get("Last-Modified"),timestamp:Date.now()};this.httpHeaders.set(e,s),this.saveHeadersToDB(e,s)}clearCache(){this.cache.clear(),this.db&&this.db.transaction(["cache"],"readwrite").objectStore("cache").clear(),this.notify("cache-cleared")}invalidateCache(e){const t=[];this.cache.forEach(((s,i)=>{("string"==typeof e&&i.includes(e)||e instanceof RegExp&&e.test(i))&&t.push(i)})),t.forEach((e=>{this.cache.delete(e),this.db&&this.db.transaction(["cache"],"readwrite").objectStore("cache").delete(e)})),this.notify("cache-invalidated",{count:t.length})}storeDOMElement(e,t,s){this.domCache.has(e)||this.domCache.set(e,{});const i=this.domCache.get(e);i[t]=s.cloneNode(!0),this.domCache.set(e,i),this.saveDOMCacheToDB(e,i)}getDOMElement(e,t){const s=this.domCache.get(e);return s&&s[t]?s[t].cloneNode(!0):null}hasDOMElement(e,t){const s=this.domCache.get(e);return s&&s[t]}clearDOMCache(e){this.domCache.delete(e),this.db&&this.db.transaction(["dom"],"readwrite").objectStore("dom").delete(e)}clearAllDOMCache(){this.domCache.clear(),this.db&&this.db.transaction(["dom"],"readwrite").objectStore("dom").clear()}renderOrRetrieve(e,t,s){const i=this.getDOMElement(e.id,t);if(i)return i;const r=s(e);return this.storeDOMElement(e.id,t,r),r}async saveItemsToDB(){if(!this.db)return;const e=this.db.transaction(["items"],"readwrite").objectStore("items");e.clear(),this.items.forEach((t=>{t._deleted||e.put(t)}))}async saveCacheToDB(e,t){this.db&&this.db.transaction(["cache"],"readwrite").objectStore("cache").put(t)}async saveHeadersToDB(e,t){this.db&&this.db.transaction(["headers"],"readwrite").objectStore("headers").put(t)}async saveDOMCacheToDB(e,t){if(!this.db)return;const s={id:e,views:{}};Object.entries(t).forEach((([e,t])=>{t&&t.outerHTML&&(s.views[e]=t.outerHTML)})),this.db.transaction(["dom"],"readwrite").objectStore("dom").put(s)}async saveFormsToDB(e,t){this.db&&this.db.transaction(["forms"],"readwrite").objectStore("forms").put(t)}storeForm(e,t){this.forms.set(e,t),this.saveFormsToDB(e,t)}getForm(e){return this.forms.has(e)?this.forms.get(e):null}getAllForms(){return this.forms}clearForm(e){this.forms.delete(e),this.db&&this.db.transaction(["forms"],"readwrite").objectStore("forms").delete(e)}clearAllForms(){this.forms.clear(),this.db&&this.db.transaction(["forms"],"readwrite").objectStore("dom").clear()}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){this.db&&this.db.close(),this.subscribers.clear(),this.items.clear(),this.cache.clear(),this.domCache.clear(),this.httpHeaders.clear()}};