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