| | |
| | | // 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), |
| | |
| | | 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 }; |
| | | |
| | |
| | | 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()); |
| | |
| | | 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); |
| | |
| | | } |
| | | |
| | | 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; |
| | |
| | | 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]) => { |