From 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Nov 2025 07:42:23 +0000
Subject: [PATCH] =Feed block mostly good! Referrals look good to go. Ready for Madi and Heidi to approve
---
assets/js/concise/DataStore.js | 269 +++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 199 insertions(+), 70 deletions(-)
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 47f72e8..ffe1891 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -14,10 +14,11 @@
DataStore.instance = this;
// Shared resources
- this.databases = new Map(); // Shared IndexedDB connections
- this.stores = new Map(); // Registered store namespaces
- this.subscribers = new Map(); // Per-store event subscribers
- this.pendingInits = new Map(); // Track initialization promises
+ this.dbConfig = new Map(); // Definitions for the databases
+ this.databases = new Map(); // Shared IndexedDB connections
+ this.stores = new Map(); // Registered store namespaces
+ this.subscribers = new Map(); // Per-store event subscribers
+ this.pendingInits = new Map(); // Track initialization promises
this.fetchQueue = [];
// Global state
@@ -27,7 +28,7 @@
this.init();
- window.addEventListener('beforeunload', () => this.destroy());
+ // window.addEventListener('beforeunload', () => this.destroy());
}
async init() {
@@ -41,71 +42,97 @@
/**
* Register a new store namespace
+ * @param {string} name Database Name
+ * @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, config = {}) {
- if (this.stores.has(name)) {
- console.warn(`Store "${name}" already registered`);
- return this.getStoreAPI(name);
+ register(name, configs = [], version = 1.1) {
+ if (!Array.isArray(configs)) configs = [configs];
+ if (configs.length === 0) return;
+
+ if (!this.dbConfig.has(name)) {
+ this.dbConfig.set(name, {
+ dbName: `jvb_${name}`,
+ version: version,
+ stores: {},
+ _initialized: false
+ });
}
- if (!config.keyPath) {
- throw new Error(`Store "${name}" requires a keyPath`);
- }
+ let dbEntry = this.dbConfig.get(name);
- const store = {
- name,
- config: {
- // Storage
- dbName: `jvb_${name}_db`,
- version: 1,
- storeName: 'items',
- keyPath: 'id',
- indexes: [],
+ configs.forEach(config => {
+ if (!config.storeName) {
+ throw new Error(`Store config for "${name}" missing storeName`);
+ }
+ if (!config.keyPath) {
+ throw new Error(`Store "${config.storeName}" requires keyPath`);
+ }
- // API
- endpoint: null,
- apiBase: jvbSettings.api,
- filters: {},
- required: null,
- // Cache
- TTL: 3600000, // 1 hour
- useHttpCaching: true,
+ const storeKey = `${name}_${config.storeName}`;
- // Behavior
- showLoading: false,
- delayFetch: true,
- validateData: true, // Validate data is serializable
+ const store = {
+ config: {
+ // Storage
+ dbName: dbEntry.dbName,
+ storeName: 'items',
+ keyPath: 'id',
+ indexes: [],
- ...config
- },
+ // API
+ endpoint: null,
+ apiBase: jvbSettings.api,
+ filters: {},
+ required: null,
- // State
- db: null,
- data: new Map(),
- cache: new Map(),
- httpHeaders: new Map(),
- filters: { ...config.filters },
- isFetching: false,
- currentRequest: null,
- lastResponse: null,
- _initialized: false
- };
+ // Cache
+ TTL: 3600000, // 1 hour
+ useHttpCaching: true,
- store.config.headers = {
- 'X-WP-Nonce': jvbSettings?.nonce,
- ...store.config.headers
- };
+ // Behavior
+ showLoading: false,
+ delayFetch: true,
+ validateData: true, // Validate data is serializable
+ ...config
+ },
+ dbKey: name,
+ storeKey: storeKey,
+ data: new Map(),
+ cache: new Map(),
+ httpHeaders: new Map(),
+ subscribers: new Map(),
+ filters: {...(config.filters || {}) },
+ isFetching: false,
+ currentRequest: null,
+ lastResponse: null,
+ _initialized: false
+ };
- this.stores.set(name, store);
- this.subscribers.set(name, new Set());
+ store.config.headers = {
+ 'X-WP-Nonce': jvbSettings?.nonce,
+ ...store.config.headers
+ };
+
+ dbEntry.stores[config.storeName] = storeKey;
+
+ this.stores.set(storeKey, store);
+ if (!this.subscribers.has(storeKey)) {
+ this.subscribers.set(storeKey, new Set());
+ }
+ });
+
// Initialize database asynchronously
- this.initStoreDB(name).catch(error => {
+ this.initDB(name).catch(error => {
console.error(`Failed to initialize store "${name}":`, error);
});
- return this.getStoreAPI(name);
+ const apis = {};
+ for (const [storeName, storeKey] of Object.entries(dbEntry.stores)) {
+ apis[storeName] = this.getStoreAPI(storeKey);
+ }
+ return apis;
}
/**
@@ -171,6 +198,16 @@
return Object.fromEntries(obj);
}
+ // Preserve ArrayBuffer and TypedArrays (needed for blob storage)
+ if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+ return obj;
+ }
+
+ // Preserve Date objects
+ if (obj instanceof Date) {
+ return obj;
+ }
+
// Handle Arrays
if (Array.isArray(obj)) {
return obj.map(item => this.normalizeForStorage(item));
@@ -189,6 +226,66 @@
}
/**
+ * Convert FormData to plain object for storage
+ */
+ formDataToObject(formData) {
+ const obj = {
+ _isFormData: true,
+ entries: {}
+ };
+
+ for (const [key, value] of formData.entries()) {
+ // Skip File/Blob objects - they're stored separately in UploadManager
+ if (value instanceof File || value instanceof Blob) {
+ continue;
+ }
+
+ // Handle multiple values for same key
+ if (obj.entries[key]) {
+ if (!Array.isArray(obj.entries[key])) {
+ obj.entries[key] = [obj.entries[key]];
+ }
+ obj.entries[key].push(value);
+ } else {
+ obj.entries[key] = value;
+ }
+ }
+
+ return obj;
+ }
+
+ /**
+ * Convert stored object back to FormData
+ */
+ async objectToFormData(obj) {
+ if (!obj._isFormData) return obj;
+
+ const formData = new FormData();
+
+ // Restore text entries
+ for (const [key, value] of Object.entries(obj.entries)) {
+ if (Array.isArray(value)) {
+ value.forEach(v => formData.append(key, v));
+ } else {
+ formData.append(key, value);
+ }
+ }
+
+ if (window.jvbUploads && obj.entries.upload_ids) {
+ const uploadIds = JSON.parse(obj.entries.upload_ids);
+
+ for (const uploadId of uploadIds) {
+ const file = await window.jvbUploads.getBlobData(uploadId);
+ if (file) {
+ formData.append('files[]', file);
+ }
+ }
+ }
+
+ return formData;
+ }
+
+ /**
* Strip DOM references from object
*/
stripDOMReferences(obj, visited = new WeakSet()) {
@@ -212,6 +309,12 @@
return null;
}
+ // ✅ PRESERVE ArrayBuffer and TypedArrays (needed for blob storage)
+ if (obj instanceof ArrayBuffer ||
+ ArrayBuffer.isView(obj)) {
+ return obj;
+ }
+
// Handle Date
if (obj instanceof Date) {
return obj;
@@ -242,40 +345,52 @@
/**
* Initialize database for a specific store
*/
- async initStoreDB(name) {
- const store = this.stores.get(name);
- if (!store || store._initialized) return;
+ async initDB(name) {
+ const db = this.dbConfig.get(name);
+ if (!db || db._initialized) return;
if (this.pendingInits.has(name)) {
return this.pendingInits.get(name);
}
- const initPromise = this._performStoreInit(name);
+ const initPromise = this._performDBInit(name);
this.pendingInits.set(name, initPromise);
try {
await initPromise;
- store._initialized = true;
+ db._initialized = true;
} finally {
this.pendingInits.delete(name);
}
}
- async _performStoreInit(name) {
- const store = this.stores.get(name);
- const { dbName, version } = store.config;
+ async _performDBInit(name) {
+ const database = this.dbConfig.get(name);
+ const { dbName, version } = database;
+ const stores = Object.values(database.stores);
try {
if (!this.databases.has(dbName)) {
const db = await this.openDatabase(dbName, version, (db) => {
- this.setupStores(db, store.config);
+ stores.forEach(store => {
+ let storeObj = this.stores.get(store);
+ if (storeObj) {
+ this.setupStores(db, storeObj.config);
+ }
+ });
});
this.databases.set(dbName, db);
}
- store.db = this.databases.get(dbName);
- this.loadStoreDataInBackground(name);
- this.notify(name, 'db-init');
+ stores.forEach(storeName => {
+ let store = this.stores.get(storeName);
+ if (store) {
+ store.db = this.databases.get(dbName);
+ store._initialized = true;
+ this.loadStoreDataInBackground(storeName);
+ this.notify(storeName, 'db-init');
+ }
+ })
} catch (error) {
console.error(`Failed to initialize database for store "${name}":`, error);
@@ -464,7 +579,7 @@
}
if (!store._initialized) {
- await this.initStoreDB(name);
+ await this.initDB(store.dbKey);
}
}
@@ -627,6 +742,14 @@
// Auto-normalize Sets/Maps
let processed = this.normalizeForStorage(item);
+
+ if (processed.data instanceof FormData) {
+ processed = {
+ ...processed,
+ data: this.formDataToObject(processed.data)
+ };
+ }
+
processed = this.stripDOMReferences(processed);
// Validate data is serializable
@@ -640,10 +763,10 @@
const key = this.getItemKey(processed, store.config.keyPath);
- // Store in memory
+ // Store the original in memory (with original data intact)
store.data.set(key, item);
- // Store in IndexedDB
+ // Store processed in IndexedDB
if (store.db) {
const tx = store.db.transaction([store.config.storeName], 'readwrite');
const objectStore = tx.objectStore(store.config.storeName);
@@ -682,6 +805,10 @@
return { valid: true };
}
+ if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+ return { valid: true };
+ }
+
// Reject DOM elements
if (obj instanceof HTMLElement ||
obj instanceof NodeList ||
@@ -810,7 +937,6 @@
} else {
store.filters[key] = value;
}
-
this.notify(name, 'filters-changed', {
filters: store.filters,
changed: { key, oldValue, newValue: value }
@@ -912,6 +1038,9 @@
}
subscribe(name, callback) {
+ if (!this.subscribers.has(name)) {
+ this.subscribers.set(name, new Set());
+ }
const subscribers = this.subscribers.get(name);
subscribers.add(callback);
return () => subscribers.delete(callback);
--
Gitblit v1.10.0