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/concise/DataStore.js | 355 ++++++++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 268 insertions(+), 87 deletions(-)
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 00211e2..539c82b 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -45,13 +45,12 @@
* @param {object|array} configs An object defining the store, or an array of objects defining the stores
* @param {number} version the database version
*/
- register(name, configs = [], version = 1.1) {
+ register(name, configs = [], version = 1.25) {
if (!Array.isArray(configs)) configs = [configs];
if (configs.length === 0) return;
-
if (!this.dbConfig.has(name)) {
this.dbConfig.set(name, {
- dbName: `jvb_${name}`,
+ dbName: `${jvbBase.base}${name}`,
version: version,
stores: {},
_initialized: false
@@ -85,6 +84,8 @@
ignore: [], //any filters to ignore when filtering store locally
required: null,
+ isAuth: false,
+
// Cache
TTL: 3600000, // 1 hour
useHttpCaching: true,
@@ -108,6 +109,7 @@
store.ignoreFilters = new Set([
... ['search', 'page', 'per_page', 'orderby', 'order'],
+ ... ['context', 'source'],
... store.config.ignore
]);
@@ -124,6 +126,7 @@
}
});
+
// Initialize database asynchronously
this.initDB(name).catch(error => {
console.error(`Failed to initialize store "${name}":`, error);
@@ -144,8 +147,11 @@
// Data methods
fetch: () => this.fetch(name),
save: (item) => this.save(name, item),
+ saveMany: (items) => this.saveMany(name, items),
delete: (id) => this.delete(name, id),
+ deleteMany: (items) => this.deleteMany(name, items),
get: (id) => this.get(name, id),
+ getMany: (ids) => this.getMany(name, ids),
getAll: () => this.getAll(name),
getAllByIndex: (indexName, value) => this.getAllByIndex(name, indexName, value),
filterByIndex: (criteria) => this.filterByIndex(name, criteria),
@@ -468,13 +474,16 @@
let result;
tx.oncomplete = () => resolve(result);
- tx.onerror = () => reject(tx.error);
+ tx.onerror = () => {
+ const error = tx.error || new Error('Transaction failed with unknown error');
+ reject(error);
+ };
// Call callback immediately to queue operations
try {
result = callback(objectStore, tx);
} catch (error) {
- reject(error);
+ reject(error || new Error('Callback failed with unknown error'));
}
});
}
@@ -511,9 +520,10 @@
const cached = store.cache.get(cacheKey);
if (cached && this.isCacheValid(cached, store.config.TTL)) {
+ let items = cached.items.map(itemId => this.get(name, itemId));
this.notify(name, 'data-loaded', {
cached: true,
- items: cached.items || []
+ items: items??[]
});
return cached;
}
@@ -534,11 +544,27 @@
const controller = new AbortController();
store.currentRequest = controller;
- const response = await fetch(url, {
- method: 'GET',
- headers,
- signal: controller.signal
- });
+ let response;
+ if (store.isAuth) {
+ response = await window.auth.fetch(url, {
+ method: 'GET',
+ headers,
+ signal: controller.signal
+ });
+ } else {
+ response = await fetch(url, {
+ method: 'GET',
+ headers,
+ signal: controller.signal
+ });
+ }
+
+ if (!response.ok) {
+ // Access the error details from the response body
+ const errorBody = await response.text();
+ // Throw a new error with a descriptive message
+ throw new Error(`HTTP error! status: ${response.status}, message: ${errorBody}`);
+ }
if (response.status === 304) {
// 304 means "Not Modified" - use cached data if available
@@ -573,7 +599,6 @@
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
-
const data = await response.json();
await this.processFetchedData(name, data, cacheKey, response);
@@ -586,11 +611,14 @@
return data;
} catch (error) {
- if (error.name !== 'AbortError') {
- console.error(`Fetch error for store "${name}":`, error);
+ const isAbortError = error?.name === 'AbortError';
+
+ if (!isAbortError) {
+ console.error(`Fetch error for store "${name}":`, error.message);
+ console.dir(error);
this.notify(name, 'fetch-error', { error });
+ throw error;
}
- throw error;
} finally {
store.isFetching = false;
@@ -625,8 +653,8 @@
*/
async processFetchedData(name, data, cacheKey, response) {
const store = this.stores.get(name);
- const items = data.items || [];
- const changes = []; // Track all changes
+ const items = (data.items || []).filter(item => item && typeof item === 'object');
+ const changes = [];
// Batch process with single transaction
if (store.db && items.length > 0) {
@@ -784,8 +812,57 @@
return changeInfo.key;
}
+ /**
+ * Save multiple items in a single transaction (batch write)
+ * @param {string} name - Store name
+ * @param {Array|Map} items - Array of items or Map of items to save
+ * @returns {Promise<Array>} - Array of saved keys
+ */
+ async saveMany(name, items) {
+ const store = this.stores.get(name);
+ if (!store) return [];
+
+ // Convert Map to array if needed
+ const itemArray = items instanceof Map
+ ? Array.from(items.values())
+ : Array.isArray(items) ? items : Object.values(items);
+
+ if (itemArray.length === 0) return [];
+
+ const changes = [];
+
+ // Process all items and update in-memory store
+ itemArray.forEach(item => {
+ const changeInfo = this._saveItem(name, item);
+ changes.push(changeInfo);
+ });
+
+ // Single transaction for all writes
+ await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+ changes.forEach(changeInfo => {
+ objectStore.put(changeInfo.processed);
+ });
+ });
+
+ // Notify once for batch
+ this.notify(name, 'items-saved', {
+ count: changes.length,
+ keys: changes.map(c => c.key)
+ });
+
+ return changes.map(c => c.key);
+ }
+
processForStorage(obj, validate = true, path = 'root') {
- if (obj === null || obj === undefined) return { valid: true, data: obj };
+ if (obj === null) {
+ return { valid: true, data: null };
+ }
+ if (obj === undefined) {
+ if (validate) {
+ return { valid: false, error: `Undefined value at ${path}` };
+ }
+ return { valid: true, data: undefined };
+ }
const type = typeof obj;
@@ -844,9 +921,13 @@
if (type === 'object') {
const processed = {};
for (const [key, value] of Object.entries(obj)) {
+ if (value === undefined) continue;
const result = this.processForStorage(value, validate, `${path}.${key}`);
if (!result.valid) return result;
- if (result.data !== undefined) processed[key] = result.data;
+ // Include null values, skip undefined
+ if (result.data !== undefined || value === null) {
+ processed[key] = result.data;
+ }
}
return { valid: true, data: processed };
}
@@ -871,11 +952,78 @@
this.notify(name, 'item-deleted', { id });
}
+ /**
+ * Delete multiple items in a single transaction (batch delete)
+ * @param {string} name - Store name
+ * @param {Array|Set} ids - Array or Set of IDs to delete
+ * @returns {Promise<Array>} - Array of deleted IDs
+ */
+ async deleteMany(name, ids) {
+ const store = this.stores.get(name);
+ if (!store) return [];
+
+ // Convert Set to array if needed
+ const idArray = ids instanceof Set
+ ? Array.from(ids)
+ : Array.isArray(ids) ? ids : Object.keys(ids);
+
+ if (idArray.length === 0) return [];
+
+ // Update in-memory store
+ idArray.forEach(id => {
+ store.data.delete(id);
+ });
+
+ // Single transaction for all deletes
+ await this.withTransaction(name, store.config.storeName, 'readwrite', (objectStore) => {
+ idArray.forEach(id => {
+ objectStore.delete(id);
+ });
+ });
+
+ // Notify once for batch
+ this.notify(name, 'items-deleted', {
+ count: idArray.length,
+ ids: idArray
+ });
+
+ return idArray;
+ }
+
get(name, id) {
const store = this.stores.get(name);
return store.data.get(id);
}
+ /**
+ * Get multiple items by IDs in a single call
+ * @param {string} name - Store name
+ * @param {Array|Set} ids - Array or Set of IDs to retrieve
+ * @param {boolean} skipMissing - If true, omit missing items; if false, include null for missing
+ * @returns {Array} - Array of items (in same order as IDs)
+ */
+ getMany(name, ids, skipMissing = true) {
+ const store = this.stores.get(name);
+ if (!store) return [];
+
+ const idArray = ids instanceof Set
+ ? Array.from(ids)
+ : Array.isArray(ids) ? ids : Object.keys(ids);
+
+ if (idArray.length === 0) return [];
+
+ if (skipMissing) {
+ return idArray.reduce((acc, id) => {
+ const item = store.data.get(id);
+ if (item) acc.push(item);
+ return acc;
+ }, []);
+ }
+
+ // Preserve order, include null for missing
+ return idArray.map(id => store.data.get(id) ?? null);
+ }
+
getAll(name) {
const store = this.stores.get(name);
return Array.from(store.data.values());
@@ -894,6 +1042,7 @@
if (!store) return [];
return Array.from(store.data.values()).filter(item => {
+ if (!item || typeof item !== 'object') return false;
return Object.entries(criteria).every(([key, value]) => {
const accepted = Array.isArray(value) ? value : [value];
return accepted.includes(item[key]);
@@ -946,40 +1095,63 @@
// First check if we have cached results for exact filters
if (cacheEntry?.items) {
- return this.applyOrdering(
- cacheEntry.items.reduce((acc, id) => {
- const item = store.data.get(id);
- if (item) acc.push(item);
- return acc;
- }, []),
- store
- );
+ const items = cacheEntry.items.reduce((acc, id) => {
+ const item = store.data.get(id);
+ if (item) acc.push(item);
+ return acc;
+ }, []);
+ return this.applyOrdering(items, store);
}
const allItems = Array.from(store.data.values());
+
const searchQuery = store.filters.search?.toLowerCase().trim() || '';
- const filtered = allItems.filter(item => {
- // Apply all non-ignored filters
- for (const [key, value] of Object.entries(store.filters)) {
- if (store.ignoreFilters.has(key)) continue;
- if (value === null || value === undefined || value === '') continue;
- if (value === 'all') continue;
+ const filterPredicates = [];
- // Comma-separated values
- if (typeof value === 'string' && value.includes(',')) {
- const accepted = value.split(',').map(v => v.trim());
- if (!accepted.includes(String(item[key]))) return false;
- continue;
+ // Handle taxonomy filters separately
+ if (store.filters.taxonomy && typeof store.filters.taxonomy === 'object') {
+ Object.entries(store.filters.taxonomy).forEach(([taxonomy, termIds]) => {
+ const acceptedTermIds = Array.isArray(termIds) ? termIds : [termIds];
+
+ filterPredicates.push(item => {
+ if (!item.taxonomies || !item.taxonomies[taxonomy]) {
+ return false;
+ }
+ const itemTermIds = Object.keys(item.taxonomies[taxonomy]).map(id => parseInt(id));
+ const matches = acceptedTermIds.some(termId => itemTermIds.includes(parseInt(termId)));
+ return matches;
+ });
+ });
+ }
+
+ // Handle other filters
+ for (const [key, value] of Object.entries(store.filters)) {
+ if (key === 'taxonomy') {
+ if (typeof value === 'string' && !value.includes(',')) {
+ filterPredicates.push(item => item.taxonomy === value);
}
-
- if (String(item[key]) !== String(value)) return false;
+ continue;
}
+ if (store.ignoreFilters.has(key)) {
+ continue;
+ }
+ if (value === null || value === undefined || value === '') continue;
+ if (value === 'all') continue;
- // Apply search if present
+ if (typeof value === 'string' && value.includes(',')) {
+ const accepted = value.split(',').map(v => v.trim());
+ filterPredicates.push(item => accepted.includes(String(item[key])));
+ } else {
+ filterPredicates.push(item => String(item[key]) === String(value));
+ }
+ }
+
+ const filtered = allItems.filter(item => {
+ for (const predicate of filterPredicates) {
+ if (!predicate(item)) return false;
+ }
return !(searchQuery && !this.searchObject(item, searchQuery));
-
-
});
return this.applyOrdering(filtered, store);
@@ -989,39 +1161,54 @@
if (!Array.isArray(items)) items = Array.from(items);
if (items.length === 0) return items;
- if (store.filters.orderby || store.filters.order) {
- const orderby = store.filters.orderby || 'date';
- const order = (store.filters.order || 'desc').toLowerCase();
+ const orderby = store.filters.orderby || 'date';
+ const order = (store.filters.order || 'desc').toLowerCase();
- items.sort((a, b) => {
- let aVal, bVal;
-
- switch (orderby) {
- case 'alphabetical':
- case 'title':
- aVal = (a.fields?.post_title || a.title || a.name || '').toLowerCase();
- bVal = (b.fields?.post_title || b.title || b.name || '').toLowerCase();
- break;
- case 'modified':
- aVal = new Date(a.modified || 0);
- bVal = new Date(b.modified || 0);
- break;
- case 'date':
- default:
- aVal = new Date(a.date || 0);
- bVal = new Date(b.date || 0);
- }
-
- if (aVal < bVal) return order === 'asc' ? -1 : 1;
- if (aVal > bVal) return order === 'asc' ? 1 : -1;
- return 0;
- });
+ // Handle random ordering
+ if (['random', 'rand'].includes(orderby) || ['random', 'rand'].includes(order)) {
+ return this.shuffle(items);
}
+
+ items.sort((a, b) => {
+ let aVal, bVal;
+
+ switch (orderby) {
+ case 'alphabetical':
+ case 'title':
+ aVal = (a.title || a.name || '').toLowerCase();
+ bVal = (b.title || b.name || '').toLowerCase();
+ break;
+ case 'modified':
+ aVal = new Date(a.modified || a.date || 0);
+ bVal = new Date(b.modified || b.date || 0);
+ break;
+ case 'date':
+ default:
+ aVal = new Date(a.date || a.modified || 0);
+ bVal = new Date(b.date || b.modified || 0);
+ }
+
+ if (aVal < bVal) return order === 'asc' ? -1 : 1;
+ if (aVal > bVal) return order === 'asc' ? 1 : -1;
+ return 0;
+ });
+
return items;
}
+ shuffle(items) {
+ const array = items.slice();
+ for (let i = array.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+ return array;
+ }
+
searchObject(obj, search) {
- if (!obj || typeof obj !== 'object') return false;
+ if (!obj || typeof obj !== 'object') {
+ return typeof obj === 'string' && obj.toLowerCase().includes(search);
+ }
for (const value of Object.values(obj)) {
if (value === null || value === undefined) continue;
@@ -1068,23 +1255,22 @@
store.filters[key] = value;
}
});
-
this.notify(name, 'filters-changed', {
oldFilters,
filters: store.filters,
updates
});
- this.notify(name, 'data-loaded', {
- cached: true,
- items: this.getFiltered(name)
- });
-
const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters);
+
if (store.config.endpoint && shouldFetch) {
await this.fetch(name);
- } else if (store.config.endpoint) {
- this.notify(name, 'data-loaded');
+ } else {
+ const filtered = this.getFiltered(name);
+ this.notify(name, 'data-loaded', {
+ cached: true,
+ items: filtered
+ });
}
}
@@ -1102,16 +1288,11 @@
if (!store.config.endpoint || !store.lastResponse) {
return true;
}
-
- if (store.lastResponse.has_more === false) {
- // Check if new filters are a subset of what we have
- const isSubsetFilter = Object.entries(updates).every(([key, value]) => {
- if (store.ignoreFilters.has(key)) return true;
- if (key === 'page') return true; // Handle pagination locally
- return true; // We have all data, can filter locally
- });
- if (isSubsetFilter) return false;
+ if (store.lastResponse.has_more === false) {
+ if (this.hasCompleteData(store, store.filters)) {
+ return false;
+ }
}
if ('page' in updates) {
--
Gitblit v1.10.0