Jake Vanderwerf
2026-05-12 457c329237f97069063e641b10f384a52d584f21
assets/js/min/dataStore.min.js
@@ -1 +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()}};
(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.25){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`${jvbBase.base}${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach(t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,isAuth:!1,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order","context","source",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)}),this.initDB(e).catch(t=>{console.error(`Failed to initialize store "${e}":`,t)});const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach(e=>t.append(s,e)):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,e=>{i.forEach(t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)})});this.databases.set(s,e)}i.forEach(e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))})}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}})}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach(e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})})}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise(e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])}):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)}),this.loadFromObjectStore(e,"cache",e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)})]).then(()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback(()=>this.fetch(e),{timeout:2e3}):setTimeout(()=>this.fetch(e),100))}).catch(t=>{console.error(`Background load error for store "${e}":`,t)})}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback(()=>this.processFetchQueue(),{timeout:2e3}):setTimeout(()=>this.processFetchQueue(),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise((e,a)=>{const o=i.db.transaction(t,s),n=t.map(e=>o.objectStore(e)),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>{const e=o.error||new Error("Transaction failed with unknown error");a(e)};try{h=r(c,o)}catch(e){a(e||new Error("Callback failed with unknown error"))}})):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some(e=>!t.filters[e]||""===t.filters[e]))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL)){let t=r.items.map(t=>this.get(e,t));return this.notify(e,"data-loaded",{cached:!0,items:t??[]}),r}t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;let n;if(t.currentRequest=o,n=t.isAuth?await window.auth.fetch(i,{method:"GET",headers:a,signal:o.signal}):await fetch(i,{method:"GET",headers:a,signal:o.signal}),!n.ok){const e=await n.text();throw new Error(`HTTP error! status: ${n.status}, message: ${e}`)}if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){if(!("AbortError"===t?.name))throw console.error(`Fetch error for store "${e}":`,t.message),console.dir(t),this.notify(e,"fetch-error",{error:t}),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach(([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))});const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=(t.items||[]).filter(e=>e&&"object"==typeof e),o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",t=>{a.forEach(s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}})});const n={key:s,items:a.map(e=>this.getItemKey(e,i.config.keyPath)),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",e=>{e.put(n)}),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}};for(let[t,s]of Object.entries(i.filters))"string"==typeof s&&s.includes(",")&&this.createSplitCacheEntries(e,a,t,i.filters,r);o.forEach(t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})})}createSplitCacheEntries(e,t,s,r,i){const a=this.stores.get(e);r[s].split(",").map(e=>e.trim()).forEach(t=>{let o={};o[s]=t;const n={...r,[s]:t},c=this.generateCacheKey(n);if(a.cache.has(c))return;let h=this.filterByIndex(e,o).map(e=>this.getItemKey(e,a.config.keyPath));const l={key:c,items:h,timestamp:Date.now(),endpoint:a.config.endpoint,filters:n,etag:i.headers.get("Etag"),lastModified:i.headers.get("Last-Modified"),has_more:20===h.length};a.cache.set(c,l),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",e=>{e.put(l)})})}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",e=>{e.put(r.processed)}),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach(t=>{const s=this._saveItem(e,t);i.push(s)}),await this.withTransaction(e,s.config.storeName,"readwrite",e=>{i.forEach(t=>{e.put(t.processed)})}),this.notify(e,"items-saved",{count:i.length,keys:i.map(e=>e.key)}),i.map(e=>e.key)}processForStorage(e,t=!0,s="root"){if(null===e)return{valid:!0,data:null};if(void 0===e)return t?{valid:!1,error:`Undefined value at ${s}`}:{valid:!0,data:void 0};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){if(void 0===a)continue;const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0===e.data&&null!==a||(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",e=>{e.delete(t)}),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach(e=>{s.data.delete(e)}),await this.withTransaction(e,s.config.storeName,"readwrite",e=>{r.forEach(t=>{e.delete(t)})}),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e},[]):i.map(e=>r.data.get(e)??null)}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter(e=>!(!e||"object"!=typeof e)&&Object.entries(t).every(([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map(e=>new Promise((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter(e=>i.includes(e[t]))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items){const e=r.items.reduce((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e},[]);return this.applyOrdering(e,t)}const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];t.filters.taxonomy&&"object"==typeof t.filters.taxonomy&&Object.entries(t.filters.taxonomy).forEach(([e,t])=>{const s=Array.isArray(t)?t:[t];o.push(t=>{if(!t.taxonomies||!t.taxonomies[e])return!1;const r=Object.keys(t.taxonomies[e]).map(e=>parseInt(e));return s.some(e=>r.includes(parseInt(e)))})});for(const[e,s]of Object.entries(t.filters))if("taxonomy"!==e&&!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map(e=>e.trim());o.push(s=>t.includes(String(s[e])))}else o.push(t=>String(t[e])===String(s));const n=i.filter(e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))});return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();return["random","rand"].includes(s)||["random","rand"].includes(r)?this.shuffle(e):(e.sort((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.title||e.name||"").toLowerCase(),a=(t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||e.date||0),a=new Date(t.modified||t.date||0);break;default:i=new Date(e.date||e.modified||0),a=new Date(t.date||t.modified||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0}),e)}shuffle(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",e=>{e.clear()}),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach(([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t}),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t});const a=await this.shouldFetchWithFilters(e,t,i);if(r.config.endpoint&&a)await this.fetch(e);else{const t=this.getFiltered(e);this.notify(e,"data-loaded",{cached:!0,items:t})}}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every(([e,t])=>(r.ignoreFilters.has(e),!0)))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some(e=>s.filters[e]!==t[e])||Object.keys(s.filters).some(e=>!(e in t)&&t!==s.config.filters))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",e=>{e.clear()}),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce((t,s)=>(t[s]=e[s],t),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach(r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}})}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach(e=>{e.currentRequest&&e.currentRequest.abort()}),this.databases.forEach(e=>e.close()),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",async function(){window.auth.subscribe(t=>{"auth-loaded"===t&&(window.jvbStore=new e)})})})();