(()=>{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.1){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${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:{},required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,httpHeaders:new Map,subscribers:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};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),delete:t=>this.delete(e,t),get:t=>this.get(e,t),getAll:()=>this.getAll(e),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),clearHttpHeaders:t=>this.clearHttpHeaders(e,t),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})}t.useHttpCaching&&!e.objectStoreNames.contains("headers")&&e.createObjectStore("headers",{keyPath:"key"})}loadStoreDataInBackground(e){const t=this.stores.get(e);if(!t?.db)return;const s=[this.loadStoreData(e),this.loadStoreCache(e),this.loadStoreHeaders(e)];Promise.all(s).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 loadStoreData(e){const t=this.stores.get(e);if(t?.db)return new Promise((s=>{const r=t.db.transaction([t.config.storeName],"readonly").objectStore(t.config.storeName).getAll();r.onsuccess=r=>{const i=r.target.result||[];i.forEach((e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.notify(e,"data-loaded",{count:i.length}),s(i)},r.onerror=()=>s([])}))}async loadStoreCache(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("cache"))return new Promise((e=>{const s=t.db.transaction(["cache"],"readonly").objectStore("cache").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)})),e()},s.onerror=()=>e()}))}async loadStoreHeaders(e){const t=this.stores.get(e);if(t?.db&&t.db.objectStoreNames.contains("headers"))return new Promise((e=>{const s=t.db.transaction(["headers"],"readonly").objectStore("headers").getAll();s.onsuccess=s=>{(s.target.result||[]).forEach((e=>{t.httpHeaders.set(e.key,e)})),e()},s.onerror=()=>e()}))}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 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))return this.notify(e,"data-loaded",{cached:!0,items:r.items||[]}),r;t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers},o=t.httpHeaders.get(s);t.config.useHttpCaching&&o&&(o.etag&&(a["If-None-Match"]=o.etag),o.lastModified&&(a["If-Modified-Since"]=o.lastModified));const n=new AbortController;t.currentRequest=n;const c=await fetch(i,{method:"GET",headers:a,signal:n.signal});if(304===c.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(!c.ok)throw new Error(`HTTP ${c.status}: ${c.statusText}`);const d=await c.json();return t.config.useHttpCaching&&this.storeResponseHeaders(e,s,c),await this.processFetchedData(e,d,s),this.notify(e,"data-loaded",{cached:!1,items:d.items||[]}),d}catch(t){throw"AbortError"!==t.name&&(console.error(`Fetch error for store "${e}":`,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){const r=this.stores.get(e),i=t.items||[];if(r.db&&i.length>0){const e=r.db.transaction([r.config.storeName],"readwrite"),t=e.objectStore(r.config.storeName);for(const e of i){const s=this.processForStorage(e,r.config.validateData);if(s.valid){const i=this.getItemKey(s.data,r.config.keyPath);r.data.set(i,e),await t.put(s.data)}}await new Promise(((t,s)=>{e.oncomplete=()=>t(),e.onerror=()=>s(e.error)}))}const a={key:s,items:i.map((e=>this.getItemKey(e,r.config.keyPath))),timestamp:Date.now(),endpoint:r.config.endpoint,filters:{...r.filters}};r.cache.set(s,a),await this.saveToCache(e,s,a),r.lastResponse={...t,has_more:t.has_more||!1,total:t.total||i.length,pages:t.pages||1,queue_stats:t.queue_stats||{}}}async save(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);if(s.data.set(a,t),s.db){const e=s.db.transaction([s.config.storeName],"readwrite").objectStore(s.config.storeName);await e.put(i)}return this.notify(e,"item-saved",{item:t,key:a}),a}processForStorage(e,t=!0,s="root"){if(null==e)return{valid:!0,data:e};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:null};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:null};if(e instanceof FormData)return t?{valid:!1,error:`FormData at ${s}`}:{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e))return{valid:!0,data:e};if(e instanceof Set){const r=Array.from(e);return this.processForStorage(r,t,s)}if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i{const r=t.data.get(s);return r&&e.push(r),e}),[]):this.getAll(e)}async clear(e){const t=this.stores.get(e);if(t.data.clear(),t.cache.clear(),t.db){const e=t.db.transaction([t.config.storeName],"readwrite").objectStore(t.config.storeName);await e.clear()}this.notify(e,"data-cleared")}setFilter(e,t,s){const r=this.stores.get(e),i=r.filters[t];null==s||""===s?delete r.filters[t]:r.filters[t]=s,this.notify(e,"filters-changed",{filters:r.filters,changed:{key:t,oldValue:i,newValue:s}}),r.config.endpoint&&this.fetch(e)}async setFilters(e,t){const s=this.stores.get(e);Object.keys(t).some((e=>s.filters[e]!==t[e]))&&(s.filters={...s.filters,...t},this.notify(e,"filters-changed",{filters:s.filters,changed:t}),s.config.endpoint&&await this.fetch(e))}removeFilter(e,t){const s=this.stores.get(e),r=s.filters[t];void 0!==r&&(delete s.filters[t],this.notify(e,"filters-changed",{filters:s.filters,removed:{key:t,oldValue:r}}),s.config.endpoint&&this.fetch(e))}clearFilters(e){const t=this.stores.get(e),s={...t.filters};t.filters={...t.config.filters},this.notify(e,"filters-cleared",{oldFilters:s,filters:t.filters}),t.config.endpoint&&this.fetch(e)}clearCache(e){const t=this.stores.get(e);if(t.cache.clear(),t.db&&t.db.objectStoreNames.contains("cache")){t.db.transaction(["cache"],"readwrite").objectStore("cache").clear()}this.notify(e,"cache-cleared")}clearHttpHeaders(e,t=null){const s=this.stores.get(e);if(t){if(s.httpHeaders.delete(t),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").delete(t)}}else if(s.httpHeaders.clear(),s.db&&s.db.objectStoreNames.contains("headers")){s.db.transaction(["headers"],"readwrite").objectStore("headers").clear()}}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)}}))}storeResponseHeaders(e,t,s){const r=this.stores.get(e),i={key:t,etag:s.headers.get("ETag"),lastModified:s.headers.get("Last-Modified"),timestamp:Date.now()};if(r.httpHeaders.set(t,i),r.db&&r.db.objectStoreNames.contains("headers")){r.db.transaction(["headers"],"readwrite").objectStore("headers").put(i)}}async saveToCache(e,t,s){const r=this.stores.get(e);if(!r.db||!r.db.objectStoreNames.contains("cache"))return;const i=r.db.transaction(["cache"],"readwrite").objectStore("cache");await i.put(s)}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{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)}))}))})();