From a9b3b28d001941921aa70d37fdc87c758a163a44 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 05 Jun 2026 16:47:03 +0000
Subject: [PATCH] =Some hefty changes to FeedBlock. Transitioning to loading first page in php to save on extra requests. Got a bit to do yet, but I have to work on Northeh for a bit here.
---
assets/js/concise/DataStoreOld.js | 1812 +++++++++++++++++++++++++++++++--------------------------
1 files changed, 990 insertions(+), 822 deletions(-)
diff --git a/assets/js/concise/DataStoreOld.js b/assets/js/concise/DataStoreOld.js
index 3686ac2..4aa4119 100644
--- a/assets/js/concise/DataStoreOld.js
+++ b/assets/js/concise/DataStoreOld.js
@@ -1,682 +1,1014 @@
/**
- * Handles GET Requests, storing responses by a key of filters, set with setFilter method
- * Stores:
- * - Items: the individual item data, mapped by postID/termID
- * - Cache: the cacheKey generated by filters, and the results in the value
- * - httpHeaders: If Modified Since tracking
- * - domCache: rendered DOM elements, to reduce on-page rendering
+ * DataStore - Singleton pattern managing multiple store namespaces
+ *
+ * Usage:
+ * window.jvbStore = new DataStore();
+ * this.store = window.jvbStore.register('feed', { config });
*/
-class DataStore {
- constructor(config = {}) {
- this.config = {
- name: 'default',
- endpoint: false,
- apiBase: jvbSettings.api,
- TTL: 3600000, // 1 hour default
- showLoading: true,
- headers: {},
- filters: {},
- ...config
- };
- if (!this.config.endpoint) {
- console.warn('No endpoint set. Only saving locally');
+class DataStoreOld {
+ constructor() {
+ // Singleton pattern
+ if (DataStore.instance) {
+ return DataStore.instance;
}
+ DataStore.instance = this;
+ // Shared resources
+ this.dbConfig = new Map(); // Definitions for the databases
+ this.databases = new Map(); // Shared IndexedDB connections
+ this.stores = new Map(); // Registered store namespaces
+ this.subscribers = new Map(); // Per-store event subscribers
+ this.pendingInits = new Map(); // Track initialization promises
+ this.fetchQueue = [];
+
+ // Global state
+ this._initialized = false;
this.body = document.body;
this.loading = document.querySelector('dialog.loading');
- this.headers = {
- 'X-WP-Nonce': jvbSettings.nonce,
- ...this.config.headers
- };
+ this.init();
- // Data stores
- this.items = new Map();
- this.cache = new Map(); //TODO: call this resultsCache
- this.httpHeaders = new Map();
- this.domCache = new Map();
- this.forms = new Map();
-
- // State management
- this.filters = config.filters ?? {};
- this.subscribers = new Set();
- this.db = null;
- this.currentRequest = null;
-
- // Server Timestamps - needed?
- this.cachedContent = JSON.parse(cacheJVB.cache) || {};
- this.lastTimestampUpdate = Date.now();
-
- this.initDB();
- document.addEventListener('beforeUnload', () =>this.destroy());
+ // window.addEventListener('beforeunload', () => this.destroy());
}
- async initDB() {
- if (!('indexedDB' in window)) return;
+ async init() {
+ if (this._initialized) return;
+ this._initialized = true;
- const request = indexedDB.open(`jvb_${this.config.name}_db`, 1);
-
- request.onupgradeneeded = (e) => {
- const db = e.target.result;
- // Items store
- if (!db.objectStoreNames.contains('items')) {
- db.createObjectStore('items', { keyPath: 'id' });
- }
-
- // DOM cache for rendered elements
- if (!db.objectStoreNames.contains('dom')) {
- db.createObjectStore('dom', { keyPath: 'id' });
- }
-
- if (!db.objectStoreNames.contains('forms')) {
- let forms = db.createObjectStore('forms', {
- keyPath: 'formId',
- });
- forms.createIndex('status', 'status', {unique:false});
- forms.createIndex('operationId', 'operationId', {unique:false});
- forms.createIndex('timestamp', 'timestamp', {unique:false});
- }
-
- // Cache store for GET requests with endpoint index
- if (!db.objectStoreNames.contains('cache')) {
- const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
- cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
- cacheStore.createIndex('endpoint', 'endpoint', { unique: false });
- cacheStore.createIndex('filters', 'filters', { unique: false });
- }
-
- // HTTP headers store
- if (!db.objectStoreNames.contains('headers')) {
- db.createObjectStore('headers', { keyPath: 'key' });
- }
- };
-
- request.onsuccess = (e) => {
- this.db = e.target.result;
- this.loadFromDB();
- };
-
- request.onerror = (e) => {
- console.error('IndexedDB error:', e);
- };
- }
-
- async loadFromDB() {
- if (!this.db) return;
-
- try {
- await Promise.all([
- this.loadItems(),
- this.loadCache(),
- this.loadHeaders(),
- this.loadDOMCache(),
- this.loadForms()
- ]);
- } catch (error) {
- console.error('Error loading from DB:', error);
+ if (!('indexedDB' in window)) {
+ console.warn('IndexedDB not supported');
}
}
- async loadItems() {
- if (!this.db) return;
-
- return new Promise((resolve) => {
- const tx = this.db.transaction(['items'], 'readonly');
- const store = tx.objectStore('items');
- const request = store.getAll();
-
- request.onsuccess = (e) => {
- e.target.result.forEach(item => {
- this.items.set(item.id, item);
- });
- this.notify('items-loaded', { items: Array.from(this.items.values()) });
- resolve();
- };
- });
- }
-
- async loadCache() {
- if (!this.db) return;
-
- return new Promise((resolve) => {
- const tx = this.db.transaction(['cache'], 'readonly');
- const store = tx.objectStore('cache');
- const request = store.getAll();
-
- request.onsuccess = (e) => {
- e.target.result.forEach(item => {
- if (this.isCacheValid(item)) {
- this.cache.set(item.key, item);
- }
- });
- resolve();
- };
- });
- }
-
- async loadHeaders() {
- if (!this.db) return;
-
- return new Promise((resolve) => {
- const tx = this.db.transaction(['headers'], 'readonly');
- const store = tx.objectStore('headers');
- const request = store.getAll();
-
- request.onsuccess = (e) => {
- e.target.result.forEach(header => {
- this.httpHeaders.set(header.key, header);
- });
- resolve();
- };
- });
- }
-
- async loadDOMCache() {
- if (!this.db) return;
-
- return new Promise((resolve) => {
- const tx = this.db.transaction(['dom'], 'readonly');
- const store = tx.objectStore('dom');
- const request = store.getAll();
-
- request.onsuccess = (e) => {
- e.target.result.forEach(domEntry => {
- // Convert stored HTML back to DOM elements
- const reconstructed = {};
- Object.entries(domEntry.views).forEach(([viewName, html]) => {
- const temp = document.createElement('div');
- temp.innerHTML = html;
- reconstructed[viewName] = temp.firstElementChild;
- });
- this.domCache.set(domEntry.id, reconstructed);
- });
- resolve();
- };
- });
- }
-
- async loadForms() {
- if (!this.db) return;
-
- return new Promise((resolve) => {
- const tx = this.db.transaction(['forms'], 'readonly');
- const store = tx.objectStore('forms');
- const request = store.getAll();
-
- request.onsuccess = (e) => {
- e.target.result.forEach(form => {
- this.forms.set(form.key, form);
- });
- resolve();
- };
- });
- }
-
- setLoading(on) {
- this.body.classList.toggle('loading', on);
- if (on) {
- this.loading.showModal();
- } else {
- this.loading.close();
- }
-
- }
-
/**
- * Main fetch method with caching and conditional requests
+ * Register a new store namespace
+ * @param {string} name Database Name
+ * @param {object|array} configs An object defining the store, or an array of objects defining the stores
+ * @param {number} version the database version
*/
- async fetch(endpoint = null, options = {}) {
- const {
- filters = this.filters,
- headers = {},
- } = options;
+ register(name, configs = [], version = 1.1) {
+ if (!Array.isArray(configs)) configs = [configs];
+ if (configs.length === 0) return;
- if (this.config.showLoading) {
- this.setLoading(true);
- }
-
-
- // Use provided endpoint or config endpoint
- const apiEndpoint = endpoint || this.config.endpoint;
- if (!apiEndpoint) {
- throw new Error('No endpoint specified');
- }
-
- // Generate cache key from endpoint and filters
- const cacheKey = this.generateCacheKey(apiEndpoint, filters);
- const cleanedFilters = this.cleanFilters(filters);
-
- // Build request URL
- const params = new URLSearchParams(cleanedFilters);
- const url = `${this.config.apiBase}${apiEndpoint}${params.toString() ? '?' + params : ''}`;
-
- // Prepare headers with conditional requests
- const requestHeaders = {
- ...this.headers,
- ...headers
- };
-
- // Add conditional headers from stored data
- const headerKey = this.generateHeaderKey(url);
- const storedHeaders = this.httpHeaders.get(headerKey);
- const cachedData = this.cache.get(cacheKey);
-
- if (storedHeaders && cachedData) {
- if (storedHeaders.etag) {
- requestHeaders['If-None-Match'] = storedHeaders.etag;
- }
- if (storedHeaders.lastModified) {
- requestHeaders['If-Modified-Since'] = storedHeaders.lastModified;
- }
- }
-
- try {
- const response = await fetch(url, {
- method: 'GET',
- headers: requestHeaders
+ if (!this.dbConfig.has(name)) {
+ this.dbConfig.set(name, {
+ dbName: `jvb_${name}`,
+ version: version,
+ stores: {},
+ _initialized: false
});
-
- // Handle 304 Not Modified - return cached data
- if (response.status === 304) {
- console.debug(`304 Not Modified for ${url}`);
- if (cachedData) {
- // Update timestamp but keep data
- cachedData.timestamp = Date.now();
- this.cache.set(cacheKey, cachedData);
- await this.saveCacheToDB(cacheKey, cachedData);
-
- // Store current request info
- this.currentRequest = {
- filters: cleanedFilters,
- data: cachedData.data,
- cached: true
- };
-
- //TODO: should this be items-loaded?
- this.notify('data-cached', {
- data: cachedData.data,
- filters: cleanedFilters,
- cached: true
- });
- return cachedData.data;
- }
- }
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
-
- // Store response headers for future conditional requests
- this.storeResponseHeaders(headerKey, response);
-
- const data = await response.json();
-
- // Cache the response
- const cacheEntry = {
- key: cacheKey,
- endpoint: apiEndpoint,
- data: data,
- timestamp: Date.now(),
- filters: cleanedFilters
- };
-
- this.cache.set(cacheKey, cacheEntry);
- await this.saveCacheToDB(cacheKey, cacheEntry);
-
- // Update items if data contains them
- if (data.items && this.config.endpoint === apiEndpoint) {
- this.updateItems(data.items);
- }
-
- // Store current request info
- this.currentRequest = {
- filters: cleanedFilters,
- data: data,
- cached: false
- };
-
- this.notify('data-fetched', {
- endpoint: apiEndpoint,
- data: data,
- filters: cleanedFilters
- });
- return data;
-
- } catch (error) {
- console.error('Fetch error:', error);
-
- // Try to return stale cache on error
- if (cachedData) {
- console.warn('Returning stale cache due to fetch error');
- this.currentRequest = {
- filters: cleanedFilters,
- data: cachedData.data,
- cached: true,
- stale: true
- };
- this.notify('stale-cache-used', {
- data: cachedData.data,
- filters: cleanedFilters
- });
- return cachedData.data;
- }
-
- this.notify('fetch-error', { error, filters: cleanedFilters });
- throw error;
- } finally {
- if (this.config.showLoading) {
- this.setLoading(false);
- }
}
- }
- /**
- * Update items in local store
- */
- updateItems(items) {
- this.items.clear();
- items.forEach(item => {
- this.items.set(item.id, item);
+ let dbEntry = this.dbConfig.get(name);
+
+ configs.forEach(config => {
+ if (!config.storeName) {
+ throw new Error(`Store config for "${name}" missing storeName`);
+ }
+ if (!config.keyPath) {
+ throw new Error(`Store "${config.storeName}" requires keyPath`);
+ }
+
+
+ const storeKey = `${name}_${config.storeName}`;
+
+ const store = {
+ config: {
+ // Storage
+ dbName: dbEntry.dbName,
+ storeName: 'items',
+ keyPath: 'id',
+ indexes: [],
+
+ // API
+ endpoint: null,
+ apiBase: jvbSettings.api,
+ filters: {},
+ required: null,
+
+ // Cache
+ TTL: 3600000, // 1 hour
+ useHttpCaching: true,
+
+ // Behavior
+ showLoading: false,
+ delayFetch: true,
+ validateData: true, // Validate data is serializable
+ ...config
+ },
+ dbKey: name,
+ storeKey: storeKey,
+ data: new Map(),
+ cache: new Map(),
+ httpHeaders: new Map(),
+ subscribers: new Map(),
+ filters: {...(config.filters || {}) },
+ isFetching: false,
+ currentRequest: null,
+ lastResponse: null,
+ _initialized: false
+ };
+
+ store.config.headers = {
+ 'X-WP-Nonce': window.auth.getNonce(),
+ ...store.config.headers
+ };
+
+ dbEntry.stores[config.storeName] = storeKey;
+
+ this.stores.set(storeKey, store);
+ if (!this.subscribers.has(storeKey)) {
+ this.subscribers.set(storeKey, new Set());
+ }
});
- this.saveItemsToDB();
- this.notify('items-updated', { items });
+
+
+ // Initialize database asynchronously
+ this.initDB(name).catch(error => {
+ console.error(`Failed to initialize store "${name}":`, error);
+ });
+
+ const apis = {};
+ for (const [storeName, storeKey] of Object.entries(dbEntry.stores)) {
+ apis[storeName] = this.getStoreAPI(storeKey);
+ }
+ return apis;
}
/**
- * Get current request data and state
+ * Get the API object for a registered store
*/
- getCurrentRequest() {
- return this.currentRequest;
+ getStoreAPI(name) {
+ const api = {
+ // Data methods
+ fetch: () => this.fetch(name),
+ save: (item) => this.save(name, item),
+ delete: (id) => this.delete(name, id),
+ get: (id) => this.get(name, id),
+ getAll: () => this.getAll(name),
+ getFiltered: () => this.getFiltered(name),
+ clear: () => this.clear(name),
+
+ // Filter methods
+ setFilter: (key, value) => this.setFilter(name, key, value),
+ setFilters: (filters) => this.setFilters(name, filters),
+ removeFilter: (key) => this.removeFilter(name, key),
+ clearFilters: () => this.clearFilters(name),
+
+ // Cache methods
+ clearCache: () => this.clearCache(name),
+ clearHttpHeaders: (key) => this.clearHttpHeaders(name, key),
+
+ // Event methods
+ subscribe: (callback) => this.subscribe(name, callback),
+
+ // Utility
+ ensureInitialized: () => this.ensureStoreInitialized(name),
+
+ // Exposed properties (read-only)
+ get filters() {
+ return { ...api.getStore().filters };
+ },
+ get lastResponse() {
+ return api.getStore().lastResponse;
+ },
+ get data() {
+ return api.getStore().data;
+ },
+
+ getStore: () => this.stores.get(name)
+ };
+
+ return api;
}
/**
- * Get a specific item by ID
+ * Convert FormData to plain object for storage
*/
- getItem(id) {
- let check = parseInt(id);
- id = isNaN(check) ? id : check;
- const item = this.items.get(id);
- return item ? this.unserializeData(item) : null;
- }
-
- setItem(id, data, mergeExisting = true) {
- if (mergeExisting && this.items.has(id)) {
- let existing = this.getItem(id); // Get unserialized version
- data = window.deepMerge(existing, data);
- }
-
- const serialized = this.serializeData(data);
- this.items.set(id, serialized); // Store serialized version
- this.saveItemsToDB();
- this.notify('item-stored', data); // Notify with original data
- return data;
- }
-
- hasUnrecoverableFiles(data) {
- if (!data || typeof data !== 'object') return false;
-
- if (data._wasFile || data._wasBlob) return true;
-
- if (Array.isArray(data)) {
- return data.some(item => this.hasUnrecoverableFiles(item));
- }
-
- if (data instanceof FormData) {
- for (const [key, value] of data.entries()) {
- if (value instanceof File || value instanceof Blob) return true;
- }
- return false;
- }
-
- return Object.values(data).some(value => this.hasUnrecoverableFiles(value));
- }
-
- serializeFormData(formData) {
- const obj = {};
+ formDataToObject(formData) {
+ const obj = {
+ _isFormData: true,
+ entries: {}
+ };
for (const [key, value] of formData.entries()) {
- // Handle file metadata (can't store actual file)
- if (value instanceof File) {
+ // Skip File/Blob objects - they're stored separately in UploadManager
+ if (value instanceof File || value instanceof Blob) {
continue;
}
- // Check if key already exists (for multiple values)
- if (key in obj) {
- // Convert to array if not already
- if (!Array.isArray(obj[key])) {
- obj[key] = [obj[key]];
+
+ // Handle multiple values for same key
+ if (obj.entries[key]) {
+ if (!Array.isArray(obj.entries[key])) {
+ obj.entries[key] = [obj.entries[key]];
}
- obj[key].push(value);
+ obj.entries[key].push(value);
} else {
- obj[key] = value;
+ obj.entries[key] = value;
}
}
+
return obj;
}
- serializeData(data) {
- if (!data) return null;
+ /**
+ * Convert stored object back to FormData
+ */
+ async objectToFormData(obj) {
+ if (!obj._isFormData) return obj;
- if (data instanceof HTMLElement) {
- return null;
- }
- if (typeof data !== 'object') return data;
+ const formData = new FormData();
- if (data === null) return null;
-
- if (data instanceof FormData) {
- return {
- _type: 'FormData',
- ... this.serializeFormData(data)
- };
- }
-
- // Handle Arrays
- if (Array.isArray(data)) {
- return data.map(item => this.serializeData(item));
- }
-
- // Handle Date objects
- if (data instanceof Date) {
- return {
- _type: 'Date',
- value: data.toISOString()
- };
- }
-
- // Handle plain objects
- const output = {};
- for (const [key, value] of Object.entries(data)) {
- output[key] = this.serializeData(value);
- }
- return output;
- }
-
- unserializeData(data) {
- if (!data || typeof data !== 'object') return data;
- if (data === null) return null;
-
- // Check for special types
- if (data._type) {
- switch (data._type) {
- case 'FormData':
- return this.unserializeFormData(data);
- case 'File':
- // Can't reconstruct File, return metadata with warning flag
- return {
- _wasFile: true,
- _fileMetadata: data,
- name: data.name,
- type: data.type,
- size: data.size
- };
- case 'Blob':
- // Can't reconstruct Blob
- return {
- _wasBlob: true,
- _blobMetadata: data,
- type: data.type,
- size: data.size
- };
- case 'Date':
- return new Date(data.value);
+ // Restore text entries
+ for (const [key, value] of Object.entries(obj.entries)) {
+ if (Array.isArray(value)) {
+ value.forEach(v => formData.append(key, v));
+ } else {
+ formData.append(key, value);
}
}
- // Handle Arrays
- if (Array.isArray(data)) {
- return data.map(item => this.unserializeData(item));
- }
+ if (window.jvbUploads && obj.entries.upload_ids) {
+ const uploadIds = JSON.parse(obj.entries.upload_ids);
- // Handle plain objects
- const output = {};
- for (const [key, value] of Object.entries(data)) { // Fixed: 'of' not 'in'
- output[key] = this.unserializeData(value);
- }
- return output;
- }
- unserializeFormData(data) {
- const formData = new FormData();
-
- for (const [key, value] of Object.entries(data)) {
- if (Array.isArray(value)) {
- value.forEach(item => {
- if (item?._isFile) {
- console.warn(`Cannot restore file "${item.name}" from stored data`);
- // Optionally append metadata as JSON string for reference
- formData.append(key + '_was_file', JSON.stringify(item));
- } else {
- formData.append(key, item);
- }
- });
- } else if (value?._isFile) {
- console.warn(`Cannot restore file "${value.name}" from stored data`);
- // Optionally append metadata as JSON string for reference
- formData.append(key + '_was_file', JSON.stringify(value));
- } else if (value !== null && value !== undefined) {
- formData.append(key, value);
+ for (const uploadId of uploadIds) {
+ const file = await window.jvbUploads.getBlobData(uploadId);
+ if (file) {
+ formData.append('files[]', file);
+ }
}
}
return formData;
}
+ /**
+ * Initialize database for a specific store
+ */
+ async initDB(name) {
+ const db = this.dbConfig.get(name);
+ if (!db || db._initialized) return;
- clearItem(key) {
- this.items.delete(key);
- if (this.db) {
- const tx = this.db.transaction(['items'], 'readwrite');
- const store = tx.objectStore('items');
- store.delete(key);
+ if (this.pendingInits.has(name)) {
+ return this.pendingInits.get(name);
+ }
+
+ const initPromise = this._performDBInit(name);
+ this.pendingInits.set(name, initPromise);
+
+ try {
+ await initPromise;
+ db._initialized = true;
+ } finally {
+ this.pendingInits.delete(name);
}
}
- /**
- * Filter helpers
- */
- cleanFilters(filters) {
- const cleaned = {};
- Object.entries(filters).forEach(([key, value]) => {
- if (value !== null && value !== undefined && value !== '') {
- // Handle special cases based on existing patterns
- if (key === 'taxonomies' && typeof value === 'object') {
- Object.entries(value).forEach(([taxName, terms]) => {
- if (Array.isArray(terms) && terms.length > 0) {
- cleaned[`tax_${taxName}`] = terms.join(',');
- } else if (terms) {
- cleaned[`tax_${taxName}`] = terms;
+ async _performDBInit(name) {
+ const database = this.dbConfig.get(name);
+ const { dbName, version } = database;
+ const stores = Object.values(database.stores);
+
+ try {
+ if (!this.databases.has(dbName)) {
+ const db = await this.openDatabase(dbName, version, (db) => {
+ stores.forEach(store => {
+ let storeObj = this.stores.get(store);
+ if (storeObj) {
+ this.setupStores(db, storeObj.config);
}
});
- } else if (key === 'date' && typeof value === 'object') {
- if (value.after) cleaned.after = value.after;
- if (value.before) cleaned.before = value.before;
+ });
+ this.databases.set(dbName, db);
+ }
+
+ stores.forEach(storeName => {
+ let store = this.stores.get(storeName);
+ if (store) {
+ store.db = this.databases.get(dbName);
+ store._initialized = true;
+ this.loadStoreDataInBackground(storeName);
+ this.notify(storeName, 'db-init');
+ }
+ })
+
+ } catch (error) {
+ console.error(`Failed to initialize database for store "${name}":`, error);
+ throw error;
+ }
+ }
+
+ openDatabase(dbName, version, onUpgrade) {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(dbName, version);
+
+ request.onupgradeneeded = (e) => {
+ if (onUpgrade) {
+ onUpgrade(e.target.result, e.oldVersion, e.newVersion);
+ }
+ };
+
+ request.onsuccess = (e) => resolve(e.target.result);
+ request.onerror = (e) => reject(e.target.error);
+ request.onblocked = () => {
+ console.warn(`Database ${dbName} blocked. Close other tabs.`);
+ };
+ });
+ }
+
+ setupStores(db, config) {
+ // Main store
+ if (!db.objectStoreNames.contains(config.storeName)) {
+ const store = db.createObjectStore(config.storeName, {
+ keyPath: config.keyPath
+ });
+
+ config.indexes.forEach(index => {
+ store.createIndex(
+ index.name,
+ index.keyPath || index.name,
+ { unique: index.unique || false }
+ );
+ });
+ }
+
+ // Cache store
+ if (config.endpoint && !db.objectStoreNames.contains('cache')) {
+ const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
+ cacheStore.createIndex('timestamp', 'timestamp', { unique: false });
+ }
+
+ // HTTP headers store
+ if (config.useHttpCaching && !db.objectStoreNames.contains('headers')) {
+ db.createObjectStore('headers', { keyPath: 'key' });
+ }
+ }
+
+ loadStoreDataInBackground(name) {
+ const store = this.stores.get(name);
+ if (!store?.db) return;
+
+ const tasks = [
+ this.loadStoreData(name),
+ this.loadStoreCache(name),
+ this.loadStoreHeaders(name)
+ ];
+
+ Promise.all(tasks)
+ .then(() => {
+ this.notify(name, 'data-ready');
+
+ // Add to fetch queue instead of immediate fetch
+ if (store.config.endpoint && store.config.delayFetch) {
+ this.fetchQueue.push(name);
+
+ // Start processing queue if not already running
+ if (this.fetchQueue.length === 1) {
+ this.processFetchQueue();
+ }
+ } else if (store.config.endpoint && !store.config.delayFetch) {
+ // Immediate fetch
+ if ('requestIdleCallback' in window) {
+ requestIdleCallback(() => this.fetch(name), { timeout: 2000 });
+ } else {
+ setTimeout(() => this.fetch(name), 100);
+ }
+ }
+ })
+ .catch(error => {
+ console.error(`Background load error for store "${name}":`, error);
+ });
+ }
+
+ async processFetchQueue() {
+ if (this.fetchQueue.length === 0) return;
+
+ const name = this.fetchQueue.shift();
+ const store = this.stores.get(name);
+
+ if (!store) {
+ // Store was removed, continue with next
+ return this.processFetchQueue();
+ }
+
+ try {
+ await this.fetch(name);
+ } catch (error) {
+ console.error(`Queue fetch error for "${name}":`, error);
+ }
+
+ // Process next item with idle callback
+ if (this.fetchQueue.length > 0) {
+ if ('requestIdleCallback' in window) {
+ requestIdleCallback(() => this.processFetchQueue(), { timeout: 2000 });
+ } else {
+ setTimeout(() => this.processFetchQueue(), 50);
+ }
+ }
+ }
+
+ async loadStoreData(name) {
+ const store = this.stores.get(name);
+ if (!store?.db) return;
+
+ return new Promise((resolve) => {
+ const tx = store.db.transaction([store.config.storeName], 'readonly');
+ const objectStore = tx.objectStore(store.config.storeName);
+ const request = objectStore.getAll();
+
+ request.onsuccess = (e) => {
+ const items = e.target.result || [];
+ items.forEach(item => {
+ const key = this.getItemKey(item, store.config.keyPath);
+ store.data.set(key, item);
+ });
+ this.notify(name, 'data-loaded', { count: items.length });
+ resolve(items);
+ };
+
+ request.onerror = () => resolve([]);
+ });
+ }
+
+ async loadStoreCache(name) {
+ const store = this.stores.get(name);
+ if (!store?.db || !store.db.objectStoreNames.contains('cache')) return;
+
+ return new Promise((resolve) => {
+ const tx = store.db.transaction(['cache'], 'readonly');
+ const objectStore = tx.objectStore('cache');
+ const request = objectStore.getAll();
+
+ request.onsuccess = (e) => {
+ (e.target.result || []).forEach(item => {
+ if (this.isCacheValid(item, store.config.TTL)) {
+ store.cache.set(item.key, item);
+ }
+ });
+ resolve();
+ };
+
+ request.onerror = () => resolve();
+ });
+ }
+
+ async loadStoreHeaders(name) {
+ const store = this.stores.get(name);
+ if (!store?.db || !store.db.objectStoreNames.contains('headers')) return;
+
+ return new Promise((resolve) => {
+ const tx = store.db.transaction(['headers'], 'readonly');
+ const objectStore = tx.objectStore('headers');
+ const request = objectStore.getAll();
+
+ request.onsuccess = (e) => {
+ (e.target.result || []).forEach(header => {
+ store.httpHeaders.set(header.key, header);
+ });
+ resolve();
+ };
+
+ request.onerror = () => resolve();
+ });
+ }
+
+ async ensureStoreInitialized(name) {
+ const store = this.stores.get(name);
+ if (!store) {
+ throw new Error(`Store "${name}" not registered`);
+ }
+
+ if (!store._initialized) {
+ await this.initDB(store.dbKey);
+ }
+ }
+
+ async fetch(name) {
+ await this.ensureStoreInitialized(name);
+
+ const store = this.stores.get(name);
+
+ if (store.isFetching) return;
+
+ // Check required filters
+ if (store.config.required) {
+ const required = Array.isArray(store.config.required)
+ ? store.config.required
+ : [store.config.required];
+
+ const missing = required.some(key =>
+ !store.filters[key] || store.filters[key] === ''
+ );
+
+ if (missing) return;
+ }
+
+ store.isFetching = true;
+
+ try {
+ // Check cache
+ const cacheKey = this.generateCacheKey(store.filters);
+ const cached = store.cache.get(cacheKey);
+
+ if (cached && this.isCacheValid(cached, store.config.TTL)) {
+ this.notify(name, 'data-loaded', {
+ cached: true,
+ items: cached.items || []
+ });
+ return cached;
+ }
+
+ if (store.config.showLoading) {
+ this.setLoading(true);
+ }
+
+ const url = this.buildFetchUrl(name);
+ const headers = { ...store.config.headers };
+ const cachedHeaders = store.httpHeaders.get(cacheKey);
+
+ if (store.config.useHttpCaching && cachedHeaders) {
+ if (cachedHeaders.etag) {
+ headers['If-None-Match'] = cachedHeaders.etag;
+ }
+ if (cachedHeaders.lastModified) {
+ headers['If-Modified-Since'] = cachedHeaders.lastModified;
+ }
+ }
+
+ const controller = new AbortController();
+ store.currentRequest = controller;
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers,
+ signal: controller.signal
+ });
+
+ 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: false,
+ notModified: true,
+ items: []
+ });
+
+ // 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}`);
+ }
+
+ const data = await response.json();
+
+ if (store.config.useHttpCaching) {
+ this.storeResponseHeaders(name, cacheKey, response);
+ }
+ await this.processFetchedData(name, data, cacheKey);
+
+ this.notify(name, 'data-loaded', {
+ cached: false,
+ items: data.items || []
+ });
+
+ return data;
+
+ } catch (error) {
+ if (error.name !== 'AbortError') {
+ console.error(`Fetch error for store "${name}":`, error);
+ this.notify(name, 'fetch-error', { error });
+ }
+ throw error;
+
+ } finally {
+ store.isFetching = false;
+ store.currentRequest = null;
+
+ if (store.config.showLoading) {
+ this.setLoading(false);
+ }
+ }
+ }
+
+ buildFetchUrl(name) {
+ const store = this.stores.get(name);
+ const params = new URLSearchParams();
+
+ Object.entries(store.filters).forEach(([key, value]) => {
+ if (value !== null && value !== undefined && value !== '') {
+ if (typeof value === 'object') {
+ params.set(key, JSON.stringify(value));
} else {
- cleaned[key] = value;
+ params.set(key, value);
}
}
});
- return cleaned;
- }
- setFilter(key, value) {
- const oldValue = this.filters[key];
-
- if (value === '' || value === null || value === undefined) {
- delete this.filters[key];
- } else {
- this.filters[key] = value;
- }
-
- this.notify('filters-changed', {
- filters: this.filters,
- changed: { key, oldValue, newValue: value }
- });
-
- // Auto-fetch if endpoint is configured
- if (this.config.endpoint) {
- this.fetch();
- }
+ const baseUrl = store.config.apiBase + store.config.endpoint;
+ return params.toString() ? `${baseUrl}?${params}` : baseUrl;
}
/**
- * Remove a filter
+ * Process fetched data (batch from server)
*/
- removeFilter(key) {
- const oldValue = this.filters[key];
+ async processFetchedData(name, data, cacheKey) {
+ const store = this.stores.get(name);
+ const items = data.items || [];
+ const changes = []; // Track all changes
+
+ // Batch process with 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) {
+ try {
+ // Use shared save logic
+ const changeInfo = await this._saveItem(name, item, false);
+ changes.push(changeInfo);
+
+ // Queue for batch write
+ await objectStore.put(changeInfo.processed);
+ } catch (error) {
+ console.error(`Error processing item:`, error);
+ }
+ }
+
+ // Wait for transaction to complete
+ await new Promise((resolve, reject) => {
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ });
+ }
+
+ // Update cache
+ const cacheEntry = {
+ key: cacheKey,
+ items: items.map(item => this.getItemKey(item, store.config.keyPath)),
+ timestamp: Date.now(),
+ endpoint: store.config.endpoint,
+ filters: { ...store.filters }
+ };
+
+ store.cache.set(cacheKey, cacheEntry);
+ await this.saveToCache(name, cacheKey, cacheEntry);
+
+ // Update lastResponse metadata
+ store.lastResponse = {
+ ...data,
+ has_more: data.has_more || false,
+ total: data.total || items.length,
+ pages: data.pages || 1,
+ queue_stats: data.queue_stats || {}
+ };
+
+ // Emit events for items with status changes
+ changes.forEach(changeInfo => {
+ if (changeInfo.statusChanged) {
+ this.notify(name, 'item-saved', {
+ item: changeInfo.item,
+ key: changeInfo.key,
+ previousItem: changeInfo.previousItem
+ });
+ }
+ });
+ }
+
+
+ /**
+ * Internal method: Save a single item with full tracking
+ */
+ async _saveItem(name, item, emitEvent = true) {
+ const store = this.stores.get(name);
+
+ const result = this.processForStorage(item, store.config.validateData);
+ if (!result.valid) {
+ throw new Error(`Non-serializable data: ${result.error}`);
+ }
+ const processed = result.data;
+
+ const key = this.getItemKey(processed, store.config.keyPath);
+
+ // Capture previous state
+ const previousItem = store.data.get(key);
+
+ // Update in-memory store (with original data intact)
+ store.data.set(key, item);
+
+ // Write to IndexedDB happens in batch transaction (if provided)
+ // or individual transaction (if not)
+
+ // Return change info for event emission
+ return {
+ item,
+ previousItem,
+ key,
+ processed,
+ statusChanged: previousItem && previousItem.status !== item.status
+ };
+ }
+
+ /**
+ * Save single item (public API)
+ */
+ async save(name, item) {
+ const store = this.stores.get(name);
+ const changeInfo = await this._saveItem(name, item);
+
+ // Write to IndexedDB immediately for single saves
+ if (store.db) {
+ const tx = store.db.transaction([store.config.storeName], 'readwrite');
+ const objectStore = tx.objectStore(store.config.storeName);
+ await objectStore.put(changeInfo.processed);
+ }
+
+ // Always emit for explicit saves
+ this.notify(name, 'item-saved', {
+ item: changeInfo.item,
+ key: changeInfo.key,
+ previousItem: changeInfo.previousItem
+ });
+
+ return changeInfo.key;
+ }
+
+ processForStorage(obj, validate = true, path = 'root') {
+ if (obj === null || obj === undefined) return { valid: true, data: obj };
+
+ const type = typeof obj;
+
+ // Handle primitives
+ if (['string', 'number', 'boolean'].includes(type)) {
+ return { valid: true, data: obj };
+ }
+
+ // Reject functions
+ if (type === 'function') {
+ return validate ? { valid: false, error: `Function at ${path}` } : { valid: true, data: null };
+ }
+
+ // DOM elements
+ if (obj instanceof HTMLElement || obj.nodeType !== undefined) {
+ return validate ? { valid: false, error: `DOM element at ${path}` } : { valid: true, data: null };
+ }
+
+ // FormData - convert and continue
+ if (obj instanceof FormData) {
+ return validate
+ ? { valid: false, error: `FormData at ${path}` }
+ : { valid: true, data: this.formDataToObject(obj) };
+ }
+
+ // 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.processForStorage(obj[i], validate, `${path}[${i}]`);
+ if (!result.valid) return result;
+ if (result.data !== null) processed.push(result.data);
+ }
+ return { valid: true, data: processed };
+ }
+
+ // Objects
+ if (type === 'object') {
+ const 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;
+ }
+ return { valid: true, data: processed };
+ }
+
+ return validate
+ ? { valid: false, error: `Unknown type at ${path}` }
+ : { valid: true, data: null };
+ }
+
+ async delete(name, id) {
+ const store = this.stores.get(name);
+ store.data.delete(id);
+
+ if (store.db) {
+ const tx = store.db.transaction([store.config.storeName], 'readwrite');
+ const objectStore = tx.objectStore(store.config.storeName);
+ await objectStore.delete(id);
+ }
+
+ this.notify(name, 'item-deleted', { id });
+ }
+
+ get(name, id) {
+ const store = this.stores.get(name);
+ return store.data.get(id);
+ }
+
+ getAll(name) {
+ const store = this.stores.get(name);
+ return Array.from(store.data.values());
+ }
+
+ getFiltered(name) {
+ const store = this.stores.get(name);
+ const cacheKey = this.generateCacheKey(store.filters);
+ const cacheEntry = store.cache.get(cacheKey);
+
+ if (cacheEntry && cacheEntry.items) {
+ return cacheEntry.items.reduce((acc, id) => {
+ const item = store.data.get(id);
+ if (item) acc.push(item);
+ return acc;
+ }, []);
+ }
+
+ return this.getAll(name);
+ }
+
+ async clear(name) {
+ const store = this.stores.get(name);
+ store.data.clear();
+ store.cache.clear();
+
+ if (store.db) {
+ const tx = store.db.transaction([store.config.storeName], 'readwrite');
+ const objectStore = tx.objectStore(store.config.storeName);
+ await objectStore.clear();
+ }
+
+ this.notify(name, 'data-cleared');
+ }
+
+ setFilter(name, key, value) {
+ const store = this.stores.get(name);
+ const oldValue = store.filters[key];
+
+ if (value === null || value === undefined || value === '') {
+ delete store.filters[key];
+ } else {
+ store.filters[key] = value;
+ }
+ this.notify(name, 'filters-changed', {
+ filters: store.filters,
+ changed: { key, oldValue, newValue: value }
+ });
+
+ if (store.config.endpoint) {
+ this.fetch(name);
+ }
+ }
+
+ async setFilters(name, filters) {
+ const store = this.stores.get(name);
+
+ const hasChanges = Object.keys(filters).some(
+ key => store.filters[key] !== filters[key]
+ );
+
+ if (!hasChanges) return;
+
+ store.filters = { ...store.filters, ...filters };
+
+ this.notify(name, 'filters-changed', {
+ filters: store.filters,
+ changed: filters
+ });
+
+ if (store.config.endpoint) {
+ await this.fetch(name);
+ }
+ }
+
+ removeFilter(name, key) {
+ const store = this.stores.get(name);
+ const oldValue = store.filters[key];
if (oldValue !== undefined) {
- delete this.filters[key];
- this.notify('filters-changed', {
- filters: this.filters,
+ delete store.filters[key];
+
+ this.notify(name, 'filters-changed', {
+ filters: store.filters,
removed: { key, oldValue }
});
- // Auto-fetch if endpoint is configured
- if (this.config.endpoint) {
- this.fetch();
+ if (store.config.endpoint) {
+ this.fetch(name);
}
}
}
- /**
- * Clear all filters
- */
- clearFilters() {
- const oldFilters = { ...this.filters };
- //Restore baseline filters
- this.filters = this.config.filters;
+ clearFilters(name) {
+ const store = this.stores.get(name);
+ const oldFilters = { ...store.filters };
- this.notify('filters-cleared', {
+ store.filters = { ...store.config.filters };
+
+ this.notify(name, 'filters-cleared', {
oldFilters,
- filters: this.filters
+ filters: store.filters
});
- // Auto-fetch if endpoint is configured
- if (this.config.endpoint) {
- this.fetch();
+ if (store.config.endpoint) {
+ this.fetch(name);
}
}
- /**
- * Cache management
- */
- generateCacheKey(endpoint, filters) {
- const sorted = Object.keys(filters).sort().reduce((obj, key) => {
- obj[key] = filters[key];
- return obj;
- }, {});
- return `${endpoint}_${JSON.stringify(sorted)}`;
+ clearCache(name) {
+ const store = this.stores.get(name);
+ store.cache.clear();
+
+ if (store.db && store.db.objectStoreNames.contains('cache')) {
+ const tx = store.db.transaction(['cache'], 'readwrite');
+ const objectStore = tx.objectStore('cache');
+ objectStore.clear();
+ }
+
+ this.notify(name, 'cache-cleared');
}
- generateHeaderKey(url) {
- return `headers_${url}`;
+ clearHttpHeaders(name, cacheKey = null) {
+ const store = this.stores.get(name);
+
+ if (cacheKey) {
+ store.httpHeaders.delete(cacheKey);
+
+ if (store.db && store.db.objectStoreNames.contains('headers')) {
+ const tx = store.db.transaction(['headers'], 'readwrite');
+ const objectStore = tx.objectStore('headers');
+ objectStore.delete(cacheKey);
+ }
+ } else {
+ store.httpHeaders.clear();
+
+ if (store.db && store.db.objectStoreNames.contains('headers')) {
+ const tx = store.db.transaction(['headers'], 'readwrite');
+ const objectStore = tx.objectStore('headers');
+ objectStore.clear();
+ }
+ }
}
- isCacheValid(cacheEntry, maxAge = this.config.TTL) {
- if (!cacheEntry || !cacheEntry.timestamp) return false;
- return (Date.now() - cacheEntry.timestamp) < maxAge;
+ subscribe(name, callback) {
+ if (!this.subscribers.has(name)) {
+ this.subscribers.set(name, new Set());
+ }
+ const subscribers = this.subscribers.get(name);
+ subscribers.add(callback);
+ return () => subscribers.delete(callback);
}
- storeResponseHeaders(key, response) {
+ notify(name, event, data = {}) {
+ const subscribers = this.subscribers.get(name);
+ if (!subscribers) return;
+
+ subscribers.forEach(callback => {
+ try {
+ callback(event, data);
+ } catch (error) {
+ console.error(`Subscriber error for store "${name}":`, error);
+ }
+ });
+ }
+
+ storeResponseHeaders(name, key, response) {
+ const store = this.stores.get(name);
+
const headers = {
key,
etag: response.headers.get('ETag'),
@@ -684,248 +1016,84 @@
timestamp: Date.now()
};
- this.httpHeaders.set(key, headers);
- this.saveHeadersToDB(key, headers);
- }
+ store.httpHeaders.set(key, headers);
-
-
- clearCache() {
- this.cache.clear();
-
- if (this.db) {
- const tx = this.db.transaction(['cache'], 'readwrite');
- const store = tx.objectStore('cache');
- store.clear();
- }
-
- this.notify('cache-cleared');
- }
-
- invalidateCache(pattern) {
- const keysToDelete = [];
-
- this.cache.forEach((value, key) => {
- if (typeof pattern === 'string' && key.includes(pattern)) {
- keysToDelete.push(key);
- } else if (pattern instanceof RegExp && pattern.test(key)) {
- keysToDelete.push(key);
- }
- });
-
- keysToDelete.forEach(key => {
- this.cache.delete(key);
- if (this.db) {
- const tx = this.db.transaction(['cache'], 'readwrite');
- const store = tx.objectStore('cache');
- store.delete(key);
- }
- });
-
- this.notify('cache-invalidated', { count: keysToDelete.length });
- }
-
- /**
- * DOM Cache Management
- */
-
- /**
- * Store rendered DOM element for a specific item and view
- */
- storeDOMElement(itemId, viewName, element) {
- if (!this.domCache.has(itemId)) {
- this.domCache.set(itemId, {});
- }
-
- const itemCache = this.domCache.get(itemId);
- itemCache[viewName] = element.cloneNode(true);
- this.domCache.set(itemId, itemCache);
-
- // Save to IndexedDB
- this.saveDOMCacheToDB(itemId, itemCache);
- }
-
-
- /**
- * Retrieve cached DOM element for a specific item and view
- */
- getDOMElement(itemId, viewName) {
- const itemCache = this.domCache.get(itemId);
- if (itemCache && itemCache[viewName]) {
- return itemCache[viewName].cloneNode(true);
- }
- return null;
- }
-
- /**
- * Check if DOM element exists in cache
- */
- hasDOMElement(itemId, viewName) {
- const itemCache = this.domCache.get(itemId);
- return itemCache && itemCache[viewName];
- }
-
- /**
- * Clear DOM cache for a specific item
- */
- clearDOMCache(itemId) {
- this.domCache.delete(itemId);
-
- if (this.db) {
- const tx = this.db.transaction(['dom'], 'readwrite');
- const store = tx.objectStore('dom');
- store.delete(itemId);
+ if (store.db && store.db.objectStoreNames.contains('headers')) {
+ const tx = store.db.transaction(['headers'], 'readwrite');
+ const objectStore = tx.objectStore('headers');
+ objectStore.put(headers);
}
}
- /**
- * Clear all DOM cache
- */
- clearAllDOMCache() {
- this.domCache.clear();
+ async saveToCache(name, key, data) {
+ const store = this.stores.get(name);
+ if (!store.db || !store.db.objectStoreNames.contains('cache')) return;
- if (this.db) {
- const tx = this.db.transaction(['dom'], 'readwrite');
- const store = tx.objectStore('dom');
- store.clear();
+ const tx = store.db.transaction(['cache'], 'readwrite');
+ const objectStore = tx.objectStore('cache');
+ await objectStore.put(data);
+ }
+
+ generateCacheKey(filters) {
+ const normalized = Object.keys(filters)
+ .sort()
+ .reduce((acc, key) => {
+ acc[key] = filters[key];
+ return acc;
+ }, {});
+ return JSON.stringify(normalized);
+ }
+
+ isCacheValid(entry, ttl) {
+ if (!entry || !entry.timestamp) return false;
+ const age = Date.now() - entry.timestamp;
+ return age < ttl;
+ }
+
+ getItemKey(item, keyPath) {
+ if (typeof keyPath === 'function') {
+ return keyPath(item);
+ }
+
+ const keys = keyPath.split('.');
+ let value = item;
+
+ for (const key of keys) {
+ value = value?.[key];
+ }
+
+ return value;
+ }
+
+ setLoading(on) {
+ this.body.classList.toggle('loading', on);
+ if (on) {
+ this.loading?.showModal();
+ } else {
+ this.loading?.close();
}
}
- /**
- * Helper method to render or retrieve cached DOM elements
- */
- renderOrRetrieve(item, viewName, renderFunction) {
- // Check cache first
- const cached = this.getDOMElement(item.id, viewName);
- if (cached) {
- return cached;
- }
-
- // Render new element
- const element = renderFunction(item);
-
- // Cache the rendered element
- this.storeDOMElement(item.id, viewName, element);
-
- return element;
- }
- /**
- * Database operations
- */
- async saveItemsToDB() {
- if (!this.db) return;
-
- const tx = this.db.transaction(['items'], 'readwrite');
- const store = tx.objectStore('items');
-
- store.clear();
- this.items.forEach(item => {
- if (!item._deleted) {
- store.put(item);
- }
- });
- }
-
- async saveCacheToDB(key, data) {
- if (!this.db) return;
-
- const tx = this.db.transaction(['cache'], 'readwrite');
- const store = tx.objectStore('cache');
- store.put(data);
- }
-
- async saveHeadersToDB(key, headers) {
- if (!this.db) return;
-
- const tx = this.db.transaction(['headers'], 'readwrite');
- const store = tx.objectStore('headers');
- store.put(headers);
- }
-
- async saveDOMCacheToDB(itemId, domCache) {
- if (!this.db) return;
-
- // Convert DOM elements to HTML strings for storage
- const serialized = {
- id: itemId,
- views: {}
- };
-
- Object.entries(domCache).forEach(([viewName, element]) => {
- if (element && element.outerHTML) {
- serialized.views[viewName] = element.outerHTML;
- }
- });
-
- const tx = this.db.transaction(['dom'], 'readwrite');
- const store = tx.objectStore('dom');
- store.put(serialized);
- }
-
- async saveFormsToDB(key, form) {
- if (!this.db) return;
-
- const tx = this.db.transaction(['forms'], 'readwrite');
- const store = tx.objectStore('forms');
- store.put(form);
- }
-
- storeForm(key, form) {
- this.forms.set(key, form);
- this.saveFormsToDB(key, form);
- }
-
- getForm(key) {
- return this.forms.has(key) ? this.forms.get(key) : null;
- }
-
- getAllForms() {
- return this.forms;
- }
-
- clearForm(key) {
- this.forms.delete(key);
- if (this.db) {
- const tx = this.db.transaction(['forms'], 'readwrite');
- const store = tx.objectStore('forms');
- store.delete(key);
- }
- }
-
- clearAllForms() {
- this.forms.clear();
- if (this.db) {
- const tx = this.db.transaction(['forms'], 'readwrite');
- const store = tx.objectStore('dom');
- store.clear();
- }
- }
-
- /**
- * Event system
- */
- subscribe(callback) {
- this.subscribers.add(callback);
- return () => this.subscribers.delete(callback);
- }
-
- notify(event, data) {
- this.subscribers.forEach(cb => cb(event, data));
- }
-
- /**
- * Cleanup
- */
destroy() {
- if (this.db) {
- this.db.close();
- }
+ this.stores.forEach(store => {
+ if (store.currentRequest) {
+ store.currentRequest.abort();
+ }
+ });
+
+ this.databases.forEach(db => db.close());
+ this.stores.clear();
this.subscribers.clear();
- this.items.clear();
- this.cache.clear();
- this.domCache.clear();
- this.httpHeaders.clear();
+ this.databases.clear();
+ this.pendingInits.clear();
}
}
-window.jvbStore = DataStore;
+// Initialize singleton on DOMContentLoaded
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe((event) => {
+ if (event === 'auth-loaded') {
+ window.jvbStore = new DataStore();
+ }
+ });
+});
--
Gitblit v1.10.0