From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh

---
 assets/js/min/dataStore.min.js |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index e4bc7b4..073b3a6 100644
--- a/assets/js/min/dataStore.min.js
+++ b/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()}};
\ No newline at end of file
+(()=>{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){if(!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))}else"string"!=typeof s||s.includes(",")||o.push(e=>e.taxonomy===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&&this.hasCompleteData(r,r.filters))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)})})})();
\ No newline at end of file

--
Gitblit v1.10.0