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 6ddf722..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",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")}};
\ 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