From 7a9054bb3f033c98067b3196378311dae54c5fbf Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 20 Jan 2026 01:31:53 +0000
Subject: [PATCH] =OperationQueue refactor to the JVBase/managers/queue namespace

---
 assets/js/concise/DataStore.js |  152 ++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 134 insertions(+), 18 deletions(-)

diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 00211e2..3610c26 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -144,8 +144,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),
@@ -784,6 +787,47 @@
 		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 };
 
@@ -871,11 +915,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());
@@ -959,27 +1070,30 @@
 		const allItems = Array.from(store.data.values());
 		const searchQuery = store.filters.search?.toLowerCase().trim() || '';
 
+		const filterPredicates = [];
+		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;
+
+			// Comma-separated values
+			if (typeof value === 'string' && value.includes(',')) {
+				const accepted = value.split(',').map(v => v.trim());
+				filterPredicates.push(item => accepted.includes(String(item[key])));
+				continue;
+			}
+
+			filterPredicates.push(item => String(item[key]) === String(value));
+		}
+
 		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;
-
-				// 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;
-				}
-
-				if (String(item[key]) !== String(value)) return false;
+			// Apply all non-search filters
+			for (const predicate of filterPredicates) {
+				if (!predicate(item)) return false;
 			}
 
 			// Apply search if present
 			return !(searchQuery && !this.searchObject(item, searchQuery));
-
-
 		});
 
 		return this.applyOrdering(filtered, store);
@@ -1021,7 +1135,9 @@
 	}
 
 	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;
@@ -1102,7 +1218,7 @@
 		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]) => {

--
Gitblit v1.10.0