| | |
| | | * this.store = window.jvbStore.register('feed', { config }); |
| | | */ |
| | | class DataStore { |
| | | |
| | | constructor() { |
| | | // Singleton pattern |
| | | if (DataStore.instance) { |
| | |
| | | delete: (id) => this.delete(name, id), |
| | | get: (id) => this.get(name, id), |
| | | getAll: () => this.getAll(name), |
| | | getAllByIndex: (indexName, value) => this.getAllByIndex(name, indexName, value), |
| | | filterByIndex: (criteria) => this.filterByIndex(name, criteria), |
| | | getFiltered: () => this.getFiltered(name), |
| | | clear: () => this.clear(name), |
| | | |
| | |
| | | |
| | | // Reject functions |
| | | if (type === 'function') { |
| | | return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null }; |
| | | if (validate) return { valid: false, error: `Function at ${path}` }; |
| | | console.debug(`[DataStore] Stripped function at ${path}`); |
| | | return { valid: true, data: undefined }; |
| | | } |
| | | |
| | | // DOM elements |
| | | if (obj instanceof HTMLElement || obj.nodeType !== undefined) { |
| | | return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null }; |
| | | if (validate) return { valid: false, error: `DOM element at ${path}` }; |
| | | console.debug(`[DataStore] Stripped DOM element at ${path}`); |
| | | return { valid: true, data: undefined }; |
| | | } |
| | | |
| | | // FormData - convert and continue |
| | | if (obj instanceof FormData) { |
| | | return validate |
| | | ? { valid: false, error: `FormData at ${path}` } |
| | | : { valid: true, data: this.formDataToObject(obj) }; |
| | | if (validate) return { valid: false, error: `FormData at ${path}` }; |
| | | console.debug(`[DataStore] Converted FormData at ${path}`); |
| | | return { valid: true, data: this.formDataToObject(obj) }; |
| | | } |
| | | |
| | | // Preserve safe types |
| | | if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) { |
| | | if (obj instanceof Date || obj instanceof ArrayBuffer || ArrayBuffer.isView(obj) || obj instanceof Blob) { |
| | | return { valid: true, data: obj }; |
| | | } |
| | | |
| | | // Convert Sets to Arrays |
| | | if (obj instanceof Set) { |
| | | const arr = Array.from(obj); |
| | | return this.processForStorage(arr, validate, path); |
| | | return this.processForStorage(Array.from(obj), validate, path); |
| | | } |
| | | |
| | | // Convert Maps to Objects |
| | |
| | | for (let i = 0; i < obj.length; i++) { |
| | | const result = this.processForStorage(obj[i], validate, `${path}[${i}]`); |
| | | if (!result.valid) return result; |
| | | if (result.data !== null) processed.push(result.data); |
| | | if (result.data !== undefined) processed.push(result.data); |
| | | } |
| | | return { valid: true, data: processed }; |
| | | } |
| | |
| | | for (const [key, value] of Object.entries(obj)) { |
| | | const result = this.processForStorage(value, validate, `${path}.${key}`); |
| | | if (!result.valid) return result; |
| | | if (result.data !== null) processed[key] = result.data; |
| | | if (result.data !== undefined) processed[key] = result.data; |
| | | } |
| | | return { valid: true, data: processed }; |
| | | } |
| | | |
| | | return validate |
| | | ? { valid: false, error: `Unknown type at ${path}` } |
| | | : { valid: true, data: null }; |
| | | if (validate) return { valid: false, error: `Unknown type at ${path}` }; |
| | | console.debug(`[DataStore] Stripped unknown type at ${path}`); |
| | | return { valid: true, data: undefined }; |
| | | } |
| | | |
| | | /*********************************************************************** |
| | |
| | | const store = this.stores.get(name); |
| | | return Array.from(store.data.values()); |
| | | } |
| | | /** |
| | | * Filter in-memory data by multiple index/value pairs |
| | | * @param {string} name - Store name |
| | | * @param {Object} criteria - Object of { indexName: acceptedValue(s) } |
| | | * @returns {Array} - Items matching ALL criteria |
| | | * |
| | | * @example |
| | | * filterByIndex(name, { field: 'upload_123', status: ['queued', 'uploading'] }) |
| | | */ |
| | | filterByIndex(name, criteria) { |
| | | const store = this.stores.get(name); |
| | | if (!store) return []; |
| | | |
| | | return Array.from(store.data.values()).filter(item => { |
| | | return Object.entries(criteria).every(([key, value]) => { |
| | | const accepted = Array.isArray(value) ? value : [value]; |
| | | return accepted.includes(item[key]); |
| | | }); |
| | | }); |
| | | } |
| | | /** |
| | | * Get all items matching an index value |
| | | * @param {string} name - Store name |
| | | * @param {string} indexName - Name of the index to query |
| | | * @param {*} value - Value to match |
| | | * @returns {Promise<Array>} - Matching items |
| | | */ |
| | | async getAllByIndex(name, indexName, value) { |
| | | const store = this.stores.get(name); |
| | | const values = Array.isArray(value) ? value : [value]; |
| | | |
| | | // Try IndexedDB index query first (more efficient for large datasets) |
| | | if (store.db && store.db.objectStoreNames.contains(store.config.storeName)) { |
| | | try { |
| | | const tx = store.db.transaction([store.config.storeName], 'readonly'); |
| | | const objectStore = tx.objectStore(store.config.storeName); |
| | | |
| | | if (objectStore.indexNames.contains(indexName)) { |
| | | const index = objectStore.index(indexName); |
| | | |
| | | const results = await Promise.all( |
| | | values.map(v => new Promise((resolve, reject) => { |
| | | const request = index.getAll(v); |
| | | request.onsuccess = () => resolve(request.result || []); |
| | | request.onerror = () => reject(request.error); |
| | | })) |
| | | ); |
| | | |
| | | return results.flat(); |
| | | } |
| | | } catch (error) { |
| | | console.warn(`Index query failed for "${indexName}", falling back to filter:`, error); |
| | | } |
| | | } |
| | | |
| | | // Fallback: filter in-memory data |
| | | return Array.from(store.data.values()).filter(item => values.includes(item[indexName])); |
| | | } |
| | | |
| | | getFiltered(name) { |
| | | const store = this.stores.get(name); |
| | |
| | | } |
| | | |
| | | /*********************************************************************** |
| | | * FILTER OPERATIONS (UNIFIED) |
| | | * FILTER OPERATIONS |
| | | ***********************************************************************/ |
| | | |
| | | /** |
| | | * Unified filter update - handles all filter operations |
| | | */ |
| | | async updateFilters(name, updates, clearAll = false) { |
| | | const store = this.stores.get(name); |
| | | const oldFilters = { ...store.filters }; |