From 3aada9949d51024a92a8b5c6cb70d12f9c3cac16 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 21 Dec 2025 19:59:48 +0000
Subject: [PATCH] =auth refactored via rest, referral system set up for Jane, some javascript consolidation

---
 assets/js/concise/DataStore.js |  305 +++++++++++++++++---------------------------------
 1 files changed, 105 insertions(+), 200 deletions(-)

diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index ffe1891..3d46224 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -110,7 +110,7 @@
 			};
 
 			store.config.headers = {
-				'X-WP-Nonce': jvbSettings?.nonce,
+				'X-WP-Nonce': window.auth.getNonce(),
 				...store.config.headers
 			};
 
@@ -183,49 +183,6 @@
 	}
 
 	/**
-	 * Normalize data before saving - convert Sets/Maps automatically
-	 */
-	normalizeForStorage(obj) {
-		if (obj === null || obj === undefined) return obj;
-
-		// Convert Set to Array
-		if (obj instanceof Set) {
-			return Array.from(obj);
-		}
-
-		// Convert Map to Object
-		if (obj instanceof Map) {
-			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));
-		}
-
-		// Handle Objects
-		if (typeof obj === 'object') {
-			const normalized = {};
-			for (const [key, value] of Object.entries(obj)) {
-				normalized[key] = this.normalizeForStorage(value);
-			}
-			return normalized;
-		}
-
-		return obj;
-	}
-
-	/**
 	 * Convert FormData to plain object for storage
 	 */
 	formDataToObject(formData) {
@@ -286,63 +243,6 @@
 	}
 
 	/**
-	 * Strip DOM references from object
-	 */
-	stripDOMReferences(obj, visited = new WeakSet()) {
-		if (obj === null || obj === undefined) return obj;
-
-		const type = typeof obj;
-		if (type === 'string' || type === 'number' || type === 'boolean') {
-			return obj;
-		}
-
-		// Prevent circular references
-		if (type === 'object' && visited.has(obj)) {
-			return '[Circular]';
-		}
-
-		// Remove DOM elements
-		if (obj instanceof HTMLElement ||
-			obj instanceof NodeList ||
-			obj instanceof HTMLCollection ||
-			obj.nodeType !== undefined) {
-			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;
-		}
-
-		// Handle Arrays
-		if (Array.isArray(obj)) {
-			visited.add(obj);
-			return obj.map(item => this.stripDOMReferences(item, visited)).filter(v => v !== null);
-		}
-
-		// Handle Objects
-		if (type === 'object') {
-			visited.add(obj);
-			const cleaned = {};
-			for (const [key, value] of Object.entries(obj)) {
-				const cleanedValue = this.stripDOMReferences(value, visited);
-				if (cleanedValue !== null) {
-					cleaned[key] = cleanedValue;
-				}
-			}
-			return cleaned;
-		}
-
-		return obj;
-	}
-
-	/**
 	 * Initialize database for a specific store
 	 */
 	async initDB(name) {
@@ -644,15 +544,37 @@
 				signal: controller.signal
 			});
 
-			if (response.status === 304 && cached) {
+			if (response.status === 304) {
+				// 304 means "Not Modified" - use cached data if available
+				if (cached) {
+					this.notify(name, 'data-loaded', {
+						cached: true,
+						notModified: true,
+						items: cached.items || []
+					});
+					return cached;
+				}
+
+				// No cached data but server says not modified - return empty result
+				// This can happen on first load when cache headers exist but data doesn't
 				this.notify(name, 'data-loaded', {
-					cached: true,
+					cached: false,
 					notModified: true,
-					items: cached.items || []
+					items: []
 				});
-				return cached;
+
+				// Initialize empty lastResponse
+				store.lastResponse = {
+					has_more: false,
+					total: 0,
+					pages: 1,
+					queue_stats: {}
+				};
+
+				return { items: [] };
 			}
 
+			// Now check for other non-OK responses
 			if (!response.ok) {
 				throw new Error(`HTTP ${response.status}: ${response.statusText}`);
 			}
@@ -662,7 +584,6 @@
 			if (store.config.useHttpCaching) {
 				this.storeResponseHeaders(name, cacheKey, response);
 			}
-
 			await this.processFetchedData(name, data, cacheKey);
 
 			this.notify(name, 'data-loaded', {
@@ -711,8 +632,30 @@
 		const store = this.stores.get(name);
 		const items = data.items || [];
 
-		for (const item of items) {
-			await this.save(name, item);
+		// Batch process all items in a single transaction
+		if (store.db && items.length > 0) {
+			const tx = store.db.transaction([store.config.storeName], 'readwrite');
+			const objectStore = tx.objectStore(store.config.storeName);
+
+			for (const item of items) {
+				const result = this.processForStorage(item, store.config.validateData);
+				if (result.valid) {
+					const key = this.getItemKey(result.data, store.config.keyPath);
+
+					// Store in memory
+					store.data.set(key, item);
+
+					// Queue for batch write
+					await objectStore.put(result.data);
+				}
+			}
+
+			// Wait for transaction to complete
+			await new Promise((resolve, reject) => {
+				tx.oncomplete = () => resolve();
+				tx.onerror = () => reject(tx.error);
+			});
+
 		}
 
 		const cacheEntry = {
@@ -727,9 +670,11 @@
 		await this.saveToCache(name, cacheKey, cacheEntry);
 
 		store.lastResponse = {
+			...data,
 			has_more: data.has_more || false,
 			total: data.total || items.length,
-			pages: data.pages || 1
+			pages: data.pages || 1,
+			queue_stats: data.queue_stats || {}
 		};
 	}
 
@@ -740,26 +685,11 @@
 	async save(name, item) {
 		const store = this.stores.get(name);
 
-		// Auto-normalize Sets/Maps
-		let processed = this.normalizeForStorage(item);
-
-		if (processed.data instanceof FormData) {
-			processed = {
-				...processed,
-				data: this.formDataToObject(processed.data)
-			};
+		const result = this.processForStorage(item, store.config.validateData);
+		if (!result.valid) {
+			throw new Error(`Non-serializable data: ${result.error}`);
 		}
-
-		processed = this.stripDOMReferences(processed);
-
-		// Validate data is serializable
-		if (store.config.validateData) {
-			const validation = this.validateSerializable(processed);
-			if (!validation.valid) {
-				console.error(`Cannot save non-serializable data to store "${name}":`, validation.error);
-				throw new Error(`Non-serializable data: ${validation.error}`);
-			}
-		}
+		const processed = result.data;
 
 		const key = this.getItemKey(processed, store.config.keyPath);
 
@@ -777,102 +707,74 @@
 		return key;
 	}
 
-	/**
-	 * Validate that data is IndexedDB-serializable
-	 * Rejects: DOM elements, FormData, Blobs, Functions, etc.
-	 */
-	validateSerializable(obj, path = 'root') {
-		// Primitives are fine
-		if (obj === null || obj === undefined) {
-			return { valid: true };
-		}
+	processForStorage(obj, validate = true, path = 'root') {
+		if (obj === null || obj === undefined) return { valid: true, data: obj };
 
 		const type = typeof obj;
-		if (type === 'string' || type === 'number' || type === 'boolean') {
-			return { valid: true };
+
+		// Handle primitives
+		if (['string', 'number', 'boolean'].includes(type)) {
+			return { valid: true, data: obj };
 		}
 
-		// Functions cannot be serialized
+		// Reject functions
 		if (type === 'function') {
-			return {
-				valid: false,
-				error: `Function at ${path}`
-			};
+			return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null };
 		}
 
-		// Date is serializable
-		if (obj instanceof Date) {
-			return { valid: true };
+		// DOM elements
+		if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
+			return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null };
 		}
 
-		if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
-			return { valid: true };
-		}
-
-		// Reject DOM elements
-		if (obj instanceof HTMLElement ||
-			obj instanceof NodeList ||
-			obj instanceof HTMLCollection ||
-			(obj.nodeType !== undefined)) {
-			return {
-				valid: false,
-				error: `DOM element at ${path}`
-			};
-		}
-
-		// Reject FormData
+		// FormData - convert and continue
 		if (obj instanceof FormData) {
-			return {
-				valid: false,
-				error: `FormData at ${path}. Convert to object first.`
-			};
+			return validate
+				? { valid: false, error: `FormData at ${path}` }
+				: { valid: true, data: this.formDataToObject(obj) };
 		}
 
-		// Reject Blobs/Files
-		if (obj instanceof Blob || obj instanceof File) {
-			return {
-				valid: false,
-				error: `Blob/File at ${path}. Handle file uploads separately.`
-			};
+		// Preserve safe types
+		if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) {
+			return { valid: true, data: obj };
+		}
+
+		// Convert Sets to Arrays
+		if (obj instanceof Set) {
+			const arr = Array.from(obj);
+			return this.processForStorage(arr, validate, path);
+		}
+
+		// Convert Maps to Objects
+		if (obj instanceof Map) {
+			obj = Object.fromEntries(obj);
 		}
 
 		// Arrays
 		if (Array.isArray(obj)) {
+			const processed = [];
 			for (let i = 0; i < obj.length; i++) {
-				const result = this.validateSerializable(obj[i], `${path}[${i}]`);
+				const result = this.processForStorage(obj[i], validate, `${path}[${i}]`);
 				if (!result.valid) return result;
+				if (result.data !== null) processed.push(result.data);
 			}
-			return { valid: true };
+			return { valid: true, data: processed };
 		}
 
-		// Plain objects
+		// Objects
 		if (type === 'object') {
-			// Check for Sets/Maps (IndexedDB doesn't support them)
-			if (obj instanceof Set) {
-				return {
-					valid: false,
-					error: `Set at ${path}. Convert to Array first: Array.from(set)`
-				};
-			}
-			if (obj instanceof Map) {
-				return {
-					valid: false,
-					error: `Map at ${path}. Convert to Object first: Object.fromEntries(map)`
-				};
-			}
-
-			// Check all properties
+			const processed = {};
 			for (const [key, value] of Object.entries(obj)) {
-				const result = this.validateSerializable(value, `${path}.${key}`);
+				const result = this.processForStorage(value, validate, `${path}.${key}`);
 				if (!result.valid) return result;
+				if (result.data !== null) processed[key] = result.data;
 			}
-			return { valid: true };
+			return { valid: true, data: processed };
 		}
 
-		return {
-			valid: false,
-			error: `Unknown type at ${path}: ${type}`
-		};
+		return validate
+			? { valid: false, error: `Unknown type at ${path}` }
+			: { valid: true, data: null };
 	}
 
 	async delete(name, id) {
@@ -1094,7 +996,6 @@
 				acc[key] = filters[key];
 				return acc;
 			}, {});
-
 		return JSON.stringify(normalized);
 	}
 
@@ -1144,6 +1045,10 @@
 }
 
 // Initialize singleton on DOMContentLoaded
-document.addEventListener('DOMContentLoaded', function() {
-	window.jvbStore = new DataStore();
+document.addEventListener('DOMContentLoaded', async function() {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbStore = new DataStore();
+		}
+	});
 });

--
Gitblit v1.10.0