window.jvbStore=class{constructor(e={}){this.config={name:"default",version:1,storeName:"items",keyPath:"id",indexes:[],endpoint:null,saveToServer:!1,apiBase:jvbSettings.api,headers:{},filters:{},required:null,icon:null,getBlobs:null,TTL:36e5,useHttpCaching:!0,cacheKeyStrategy:"filters",showLoading:!0,stripDOMReferences:!0,storeBlobs:!1,...e},this.db=null,this.data=new Map,this.cache=new Map,this.isFetching=!1,this.pendingFetch=null,this.httpHeaders=new Map,this.subscribers=new Set,this.currentRequest=null,this.filters=this.config.filters??{},this.headers={"X-WP-Nonce":jvbSettings?.nonce,...this.config.headers},this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.initDB(),window.addEventListener("beforeunload",(()=>this.destroy()))}async initDB(){if(!("indexedDB"in window))return void console.warn("IndexedDB not supported");const e=`jvb_${this.config.name}_db`,t=indexedDB.open(e,this.config.version);t.onupgradeneeded=e=>{const t=e.target.result;if(!t.objectStoreNames.contains(this.config.storeName)){const e=t.createObjectStore(this.config.storeName,{keyPath:this.config.keyPath});this.config.indexes.forEach((t=>{e.createIndex(t.name,t.keyPath||t.name,{unique:t.unique||!1})}))}if(this.config.endpoint&&!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})}this.config.useHttpCaching&&!t.objectStoreNames.contains("headers")&&t.createObjectStore("headers",{keyPath:"key"}),this.config.storeBlobs&&!t.objectStoreNames.contains("blobs")&&t.createObjectStore("blobs",{keyPath:"uploadId"}),this.config.onUpgrade&&this.config.onUpgrade(t,e.oldVersion,e.newVersion)},t.onsuccess=async e=>{this.db=e.target.result;const t=[this.loadFromDB()];this.db.objectStoreNames.contains("cache")&&t.push(this.loadCache()),this.config.useHttpCaching&&this.db.objectStoreNames.contains("headers")&&t.push(this.loadHeaders()),await Promise.all(t),this.notify("db-init"),this.config.endpoint&&this.fetch()},t.onerror=t=>{console.error(`IndexedDB error for ${e}:`,t),this.config.onError&&this.config.onError(t)}}async loadFromDB(){if(this.db)return new Promise((async(e,t)=>{const s=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName).getAll();s.onsuccess=async t=>{const s=t.target.result;for(const e of s){e.data?._isFormData&&this.config.getBlobs&&(e.data=await this.objectToFormData(e.data));const t=this.getItemKey(e);this.data.set(t,e)}this.notify("data-loaded",{count:s.length}),e(s)},s.onerror=e=>t(e)}))}async loadData(){if(this.db)return new Promise(((e,t)=>{const s=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName).getAll();s.onsuccess=t=>{t.target.result.forEach((e=>{const t=this.config.stripDOMReferences?this.stripDOMReferences(e):e,s=this.getItemKey(t);this.data.set(s,t)})),e()},s.onerror=e=>t(e)}))}stripDOMReferences(e){if(!e||"object"!=typeof e)return e;if(Array.isArray(e))return e.map((e=>this.stripDOMReferences(e)));const t={};for(const[s,i]of Object.entries(e))this.isDOMReference(s,i)||(i instanceof Set?t[s]=Array.from(i):i instanceof Map?t[s]=Object.fromEntries(i):t[s]="object"==typeof i&&null!==i?this.stripDOMReferences(i):i);return t}isDOMReference(e,t){if(t instanceof HTMLElement||t instanceof NodeList||t instanceof HTMLCollection||t&&void 0!==t.nodeType)return!0;const s=["element","el","dom","node","ui","container","wrapper"],i=e.toLowerCase();return!(!s.includes(i)&&!s.some((e=>i===e||i.startsWith(e+"_")||i.endsWith("_"+e))))}getItemKey(e){if("function"==typeof this.config.keyPath)return this.config.keyPath(e);const t=this.config.keyPath.split(".");let s=e;for(const e of t)s=s?.[e];return s}async save(e){const t=this.getItemKey(e);this.data.set(t,e);let s={...e};return s.data instanceof FormData&&(s.data=this.formDataToObject(s.data)),this.config.stripDOMReferences&&(s=this.stripDOMReferences(s)),await this.saveToDB(s),this.config.endpoint&&this.saveToServer(e),this.notify("item-saved",{item:s,key:t}),s}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,i]of e.entries())i instanceof File||i instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(i)):t.entries[s]=i);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,i]of Object.entries(e.entries))Array.isArray(i)?i.forEach((e=>t.append(s,e))):t.append(s,i);if(this.config.getBlobs&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids),i=await this.config.getBlobs(s);for(const e of i)if(e){const s=new File([e.data],e.name,{type:e.type,lastModified:e.lastModified});t.append("files[]",s)}}return t}async saveToDB(e){if(this.db)return new Promise(((t,s)=>{const i=this.db.transaction([this.config.storeName],"readwrite").objectStore(this.config.storeName).put(e);i.onsuccess=()=>t(),i.onerror=e=>s(e)}))}async saveMany(e){if(!this.db)return;const t=this.db.transaction([this.config.storeName],"readwrite").objectStore(this.config.storeName),s=e.map((e=>{const s=this.config.stripDOMReferences?this.stripDOMReferences(e):e,i=this.getItemKey(s);return this.data.set(i,s),t.put(s)}));await Promise.all(s),this.notify("items-saved",{count:e.length})}get(e){return this.data.get(e)}getAll(){return Array.from(this.data.values())}async delete(e,t=null){if(this.data.delete(e),t||(t=this.config.storeName),this.db){const s=this.db.transaction([t],"readwrite").objectStore(t);await s.delete(e)}this.notify("item-deleted",{key:e})}async saveBlob(e,t){if(!this.db)return;const s=this.db.transaction(["blobs"],"readwrite").objectStore("blobs");await s.put({uploadId:e,data:t,type:t.type,name:t.name,lastModified:t.lastModified||Date.now()})}async getBlob(e){return this.db?new Promise((t=>{const s=this.db.transaction(["blobs"],"readonly").objectStore("blobs").get(e);s.onsuccess=()=>t(s.result),s.onerror=()=>t(null)})):null}async clear(){if(this.data.clear(),this.cache.clear(),this.httpHeaders.clear(),this.domCache&&this.domCache.clear(),this.db){const e=[this.config.storeName];this.config.endpoint&&e.push("cache"),this.config.useHttpCaching&&e.push("headers");const t=this.db.transaction(e,"readwrite");e.forEach((e=>{this.db.objectStoreNames.contains(e)&&t.objectStore(e).clear()}))}this.notify("data-cleared")}async fetch(e={}){if(!this.config.endpoint)throw new Error("No endpoint configured for fetch");const{filters:t=this.filters,headers:s={}}=e;if(this.config.required&&""===this.filters[this.config.required])return void console.log(this.config.storeName+": Not fetch as we don't have the required items");const i=this.generateCacheKey(t);if(console.log("CacheKey: ",i),this.isFetching&&this.currentCacheKey===i)return new Promise((e=>{this.pendingFetches||(this.pendingFetches=[]),this.pendingFetches.push(e)}));this.isFetching=!0,this.currentCacheKey=i;let n=null;this.config.showLoading&&this.setLoading(!0);const o=this.cache.get(i);if(console.log("Cached Data: ",o),o&&this.isCacheValid(o))return console.log("Returning cached data: "),this.isFetching=!1,this.currentCacheKey=null,this.config.showLoading&&this.setLoading(!1),o.data;const r={...this.headers,...s};if(this.config.useHttpCaching){const e=this.httpHeaders.get(i);e&&(e.etag&&(r["If-None-Match"]=e.etag),e.lastModified&&(r["If-Modified-Since"]=e.lastModified))}const a=this.cleanFilters(t),c=new URLSearchParams(a),h=`${this.config.apiBase}${this.config.endpoint}${c.toString()?"?"+c:""}`;try{const e=await fetch(h,{method:"GET",headers:r});if(304===e.status&&o)return o.timestamp=Date.now(),o.fromCache=!0,o.isError=!1,this.saveCache(i,o),console.log(this.config.storeName+" Data loaded from cache"),this.notify("data-loaded",o),n=o.data,o.data;if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);const s=await e.json();this.config.useHttpCaching&&this.storeResponseHeaders(i,e);const a={key:i,data:s,timestamp:Date.now(),endpoint:this.config.endpoint,filters:t};console.log(this.config.storeName+"Fetched fresh from server"),this.cache.set(i,a),this.saveCache(i,a);let c=Array.isArray(s)?s:s.items;return await this.saveMany(c),this.notify("data-loaded",{data:{items:c,...s},count:c.length,filters:t,fromCache:!1,isError:!1}),n=s,s}catch(e){if(console.error("Fetch error:",e),o)return console.warn("Using stale cache due to fetch error"),o.isError=!0,this.notify("data-loaded",o),n=o.data,o.data;throw e}finally{this.config.showLoading&&this.setLoading(!1),this.isFetching=!1,this.currentCacheKey=null,this.pendingFetches&&this.pendingFetches.length>0&&(this.pendingFetches.forEach((e=>e(n))),this.pendingFetches=[])}}async saveToServer(e){if(!this.config.saveToServer||!jvbSettings.currentUser)return;if(!this.config.endpoint&&this.config.saveToServer)throw new Error("No endpoint configured for saving to server");let t,s=this.config.headers;s["X-WP-Nonce"]=jvbSettings.nonce,e instanceof FormData?(e.append("user",jvbSettings.currentUser),t=e):(t=JSON.stringify({...e,user:jvbSettings.currentUser}),s["Content-Type"]="application/json");const i=await fetch(`${this.config.apiBase}${this.config.endpoint}`,{method:"POST",headers:s,body:t}),n=await i.json();this.notify("saved-to-server",{success:n.ok&&n.success})}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}generateCacheKey(e){if("custom"===this.config.cacheKeyStrategy&&this.config.generateCacheKey)return this.config.generateCacheKey(e);const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}setFilter(e,t){this.filters||(this.filters={});const s=this.filters[e];s!==t&&(""===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&&window.debouncer.schedule(this.config.endpoint,this.fetch.bind(this),100))}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&&window.debouncer.schedule(this.config.endpoint,this.fetch.bind(this),100))}clearFilters(){const e={...this.filters};this.filters=this.config.filters,this.notify("filters-cleared",{oldFilters:e,filters:this.filters}),this.config.endpoint&&this.fetch()}async setFilters(e){Object.keys(e).some((t=>this.filters[t]!==e[t]))&&(this.filters={...this.filters,...e},this.notify("filters-changed",{filters:this.filters,changed:e}),this.config.endpoint&&window.debouncer.schedule(this.config.endpoint,this.fetch.bind(this),100))}isCacheValid(e){return!(!e||!e.timestamp)&&Date.now()-e.timestamp<this.config.TTL}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.db&&this.db.objectStoreNames.contains("headers")&&this.db.transaction(["headers"],"readwrite").objectStore("headers").put(s)}async saveCache(e,t){if(!this.db||!this.db.objectStoreNames.contains("cache"))return;const s=this.db.transaction(["cache"],"readwrite").objectStore("cache");await s.put(t)}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()}}))}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t={}){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("Subscriber error:",e)}}))}async query(e,t){return this.db?new Promise(((s,i)=>{const n=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName);if(!n.indexNames.contains(e))return void i(new Error(`Index ${e} does not exist`));const o=n.index(e),r=void 0!==t?o.getAll(t):o.getAll();r.onsuccess=e=>{const t=e.target.result.map((e=>this.config.stripDOMReferences?this.stripDOMReferences(e):e));s(t)},r.onerror=e=>i(e)})):[]}async count(){return this.db?new Promise(((e,t)=>{const s=this.db.transaction([this.config.storeName],"readonly").objectStore(this.config.storeName).count();s.onsuccess=t=>e(t.target.result),s.onerror=e=>t(e)})):this.data.size}setLoading(e){console.log("on"),this.body.classList.toggle("loading",e),e?this.loading.showModal():this.loading.close()}destroy(){this.currentRequest&&this.currentRequest.abort(),this.subscribers.clear(),this.data.clear(),this.cache.clear(),this.httpHeaders.clear(),this.db&&(this.db.close(),this.db=null)}clearCache(){this.cache.clear(),this.db&&this.db.transaction(["cache"],"readwrite").objectStore("cache").clear(),this.notify("cache-cleared")}};
|