| | |
| | | endpoint: store.config.endpoint, |
| | | filters: { ...store.filters }, |
| | | etag: response.headers.get('ETag'), |
| | | lastModified: response.headers.get('Last-Modified') |
| | | lastModified: response.headers.get('Last-Modified'), |
| | | has_more: data.has_more || false |
| | | }; |
| | | |
| | | store.cache.set(cacheKey, cacheEntry); |
| | |
| | | queue_stats: data.queue_stats || {} |
| | | }; |
| | | |
| | | for (let [key, value] of Object.entries(store.filters)) { |
| | | if (typeof value === 'string' && value.includes(',')) { |
| | | this.createSplitCacheEntries(name, items, key, store.filters, response); |
| | | } |
| | | } |
| | | |
| | | // Emit events for items with status changes |
| | | changes.forEach(changeInfo => { |
| | | if (changeInfo.statusChanged) { |
| | |
| | | }); |
| | | } |
| | | |
| | | createSplitCacheEntries(name, items, key, filters, response) { |
| | | const store = this.stores.get(name); |
| | | const keys = filters[key].split(',').map(v => v.trim()); |
| | | |
| | | keys.forEach(value => { |
| | | let temp = {}; |
| | | temp[key] = value; |
| | | const newFilters = { |
| | | ... filters, |
| | | [key]: value |
| | | }; |
| | | const cacheKey = this.generateCacheKey(newFilters); |
| | | if(store.cache.has(cacheKey)) return; |
| | | let filteredItems = this.filterByIndex(name,temp).map(item => this.getItemKey(item, store.config.keyPath)); |
| | | |
| | | const entry = { |
| | | key: cacheKey, |
| | | items: filteredItems, |
| | | timestamp: Date.now(), |
| | | endpoint: store.config.endpoint, |
| | | filters: newFilters, |
| | | etag: response.headers.get('Etag'), |
| | | lastModified: response.headers.get('Last-Modified'), |
| | | has_more: filteredItems.length === 20, |
| | | } |
| | | store.cache.set(cacheKey, entry); |
| | | if (store.db?.objectStoreNames.contains('cache')) { |
| | | this.withTransaction(name, 'cache', 'readwrite', (objectStore) =>{ |
| | | objectStore.put(entry); |
| | | }); |
| | | } |
| | | }) |
| | | } |
| | | /*********************************************************************** |
| | | * SAVE OPERATIONS |
| | | ***********************************************************************/ |
| | |
| | | |
| | | // FormData - convert and continue |
| | | if (obj instanceof FormData) { |
| | | if (validate) return { valid: false, error: `FormData at ${path}` }; |
| | | console.debug(`[DataStore] Converted FormData at ${path}`); |
| | | console.debug(`[DataStore] Converting FormData at ${path}`); |
| | | return { valid: true, data: this.formDataToObject(obj) }; |
| | | } |
| | | |
| | |
| | | const cacheKey = this.generateCacheKey(store.filters); |
| | | const cacheEntry = store.cache.get(cacheKey); |
| | | |
| | | // First check if we have cached results for exact filters |
| | | if (cacheEntry && cacheEntry.items) { |
| | | return cacheEntry.items.reduce((acc, id) => { |
| | | const item = store.data.get(id); |
| | |
| | | }, []); |
| | | } |
| | | |
| | | // If we have a search filter and complete base data, filter locally |
| | | if (store.filters.search && store.filters.search.trim()) { |
| | | const searchQuery = store.filters.search.toLowerCase().trim(); |
| | | |
| | | // Get all items and filter them locally |
| | | const allItems = Array.from(store.data.values()); |
| | | |
| | | // Filter by current filters (excluding search and page) |
| | | let filtered = allItems.filter(item => { |
| | | // Apply all filters except search and page |
| | | for (const [key, value] of Object.entries(store.filters)) { |
| | | if (key === 'search' || key === 'page') continue; |
| | | |
| | | if (value !== null && value !== undefined && value !== '') { |
| | | if (item[key] !== value) return false; |
| | | } |
| | | } |
| | | return true; |
| | | }); |
| | | |
| | | // Apply search filter to common searchable fields |
| | | filtered = filtered.filter(item => { |
| | | // Search in common fields: name, title, path, description |
| | | const searchableFields = ['name', 'title', 'path', 'description', 'slug']; |
| | | |
| | | return searchableFields.some(field => { |
| | | const value = item[field]; |
| | | if (!value) return false; |
| | | return value.toLowerCase().includes(searchQuery); |
| | | }); |
| | | }); |
| | | |
| | | return filtered; |
| | | } |
| | | |
| | | // Fallback to all data |
| | | return this.getAll(name); |
| | | } |
| | | |
| | |
| | | |
| | | if (clearAll) { |
| | | store.filters = { ...store.config.filters }; |
| | | } else { |
| | | // Apply updates (null/undefined/'' = delete) |
| | | } |
| | | |
| | | Object.entries(updates).forEach(([key, value]) => { |
| | | if (value === null || value === undefined || value === '') { |
| | | delete store.filters[key]; |
| | |
| | | store.filters[key] = value; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | const shouldFetch = await this.shouldFetchWithFilters(name, updates, oldFilters); |
| | | |
| | | this.notify(name, 'filters-changed', { |
| | | oldFilters, |
| | |
| | | updates |
| | | }); |
| | | |
| | | if (store.config.endpoint) { |
| | | if (store.config.endpoint && shouldFetch) { |
| | | await this.fetch(name); |
| | | } else if (store.config.endpoint) { |
| | | this.notify(name, 'data-loaded'); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Determine if we need to fetch or can use local data |
| | | * @param {string} name - Store name |
| | | * @param {object} updates - Filter updates being applied |
| | | * @param {object} oldFilters - Previous filter state |
| | | * @returns {Promise<boolean>} - True if fetch is needed, false if local filtering suffices |
| | | */ |
| | | async shouldFetchWithFilters(name, updates, oldFilters) { |
| | | const store = this.stores.get(name); |
| | | |
| | | // If no endpoint or no lastResponse, always fetch |
| | | if (!store.config.endpoint || !store.lastResponse) { |
| | | return true; |
| | | } |
| | | |
| | | // PAGE OPTIMIZATION: Don't fetch if trying to go beyond available pages |
| | | if ('page' in updates) { |
| | | const newPage = updates.page; |
| | | const oldPage = oldFilters.page || 1; |
| | | |
| | | // If trying to go to a higher page but no more data available |
| | | if (newPage > oldPage && !store.lastResponse.has_more) { |
| | | // Reset page to last valid page |
| | | store.filters.page = oldPage; |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // SEARCH OPTIMIZATION: Check if we need to fetch for search |
| | | if ('search' in updates) { |
| | | const searchQuery = updates.search?.trim() || ''; |
| | | const oldSearch = oldFilters.search?.trim() || ''; |
| | | |
| | | // If search is being cleared, we might already have the data |
| | | if (!searchQuery && oldSearch) { |
| | | // Check if we have all base data (without search) |
| | | const baseFilters = { ...store.filters }; |
| | | delete baseFilters.search; |
| | | baseFilters.page = 1; |
| | | |
| | | // If we have complete base data, no need to fetch |
| | | if (this.hasCompleteData(store, baseFilters)) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // If search is new or changed, check if we have all data to filter locally |
| | | if (searchQuery && searchQuery !== oldSearch) { |
| | | // Check: do we have all data for base filters (no search, page 1)? |
| | | const baseFilters = { ...store.filters }; |
| | | delete baseFilters.search; |
| | | baseFilters.page = 1; |
| | | |
| | | // If we have complete base data, we can filter locally |
| | | if (this.hasCompleteData(store, baseFilters)) { |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Default: fetch is needed |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Check if we have complete data for given filters |
| | | * @param {object} store - Store instance |
| | | * @param {object} filters - Filters to check |
| | | * @returns {boolean} - True if we have all data |
| | | */ |
| | | hasCompleteData(store, filters) { |
| | | const cacheKey = this.generateCacheKey(filters); |
| | | const cached = store.cache.get(cacheKey); |
| | | |
| | | if (!cached) return false; |
| | | |
| | | // Check if cache indicates no more data |
| | | return cached.has_more === false || store.lastResponse?.has_more === false; |
| | | } |
| | | |
| | | setFilter(name, key, value) { |
| | | return this.updateFilters(name, { [key]: value }); |
| | | } |
| | |
| | | |
| | | const hasChanges = Object.keys(filters).some( |
| | | key => store.filters[key] !== filters[key] |
| | | ) || Object.keys(store.filters).some( |
| | | key => !(key in filters) && filters !== store.config.filters |
| | | ); |
| | | |
| | | if (!hasChanges) return; |