UNPKG

@legendapp/state

Version:
383 lines (380 loc) 15.1 kB
import { when, isPromise, observable, setAtPath, isPrimitive } from '@legendapp/state'; const MetadataSuffix = '__legend_metadata'; function requestToPromise(request) { return new Promise((resolve) => (request.onsuccess = () => resolve())); } class ObservablePersistIndexedDB { constructor() { this.tableData = {}; this.tableMetadata = {}; this.tablesAdjusted = new Map(); this.isSaveTaskQueued = false; this.pendingSaves = new Map(); this.promisesQueued = []; this.doSave = this.doSave.bind(this); } async initialize(config) { if (typeof indexedDB === 'undefined') return; if (process.env.NODE_ENV === 'development' && !(config === null || config === void 0 ? void 0 : config.indexedDB)) { console.error('[legend-state] Must configure ObservablePersistIndexedDB'); } const { databaseName, version, tableNames } = config.indexedDB; const openRequest = indexedDB.open(databaseName, version); openRequest.onerror = () => { console.error('Error', openRequest.error); }; openRequest.onupgradeneeded = () => { const db = openRequest.result; const { tableNames } = config.indexedDB; // Create a table for each name with "id" as the key tableNames.forEach((table) => { if (!db.objectStoreNames.contains(table)) { db.createObjectStore(table, { keyPath: 'id', }); } }); }; return new Promise((resolve) => { openRequest.onsuccess = async () => { this.db = openRequest.result; // Load each table const objectStoreNames = this.db.objectStoreNames; const tables = tableNames.filter((table) => objectStoreNames.contains(table)); try { const transaction = this.db.transaction(tables, 'readonly'); await Promise.all(tables.map((table) => this.initTable(table, transaction))); } catch (err) { console.error('[legend-state] Error loading IndexedDB', err); } resolve(); }; }); } loadTable(table, config) { var _a; if (!this.tableData[table]) { const transaction = this.db.transaction(table, 'readonly'); return this.initTable(table, transaction).then(() => this.loadTable(table, config)); } const prefix = (_a = config.indexedDB) === null || _a === void 0 ? void 0 : _a.prefixID; if (prefix) { const tableName = prefix ? table + '/' + prefix : table; if (this.tablesAdjusted.has(tableName)) { const promise = when(this.tablesAdjusted.get(tableName)); if (isPromise(promise)) { return promise; } } else { const obsLoaded = observable(false); this.tablesAdjusted.set(tableName, obsLoaded); const data = this.getTable(table, config); let hasPromise = false; let promises; if (data) { const keys = Object.keys(data); promises = keys.map(async (key) => { const value = data[key]; if (isPromise(value)) { hasPromise = true; return value.then(() => { data[key] = value; }); } else { data[key] = value; } }); } if (hasPromise) { return Promise.all(promises).then(() => { obsLoaded.set(true); }); } else { obsLoaded.set(true); } } } } getTable(table, config) { const configIDB = config.indexedDB; const prefix = configIDB === null || configIDB === void 0 ? void 0 : configIDB.prefixID; const data = this.tableData[prefix ? table + '/' + prefix : table]; if (data && (configIDB === null || configIDB === void 0 ? void 0 : configIDB.itemID)) { return data[configIDB.itemID]; } else { return data; } } getMetadata(table, config) { const { tableName } = this.getMetadataTableName(table, config); return this.tableMetadata[tableName]; } async setMetadata(table, metadata, config) { const { tableName, tableNameBase } = this.getMetadataTableName(table, config); // Assign new metadata into the table, and make sure it has the id this.tableMetadata[tableName] = Object.assign(metadata, { id: tableNameBase + MetadataSuffix, }); this.tableMetadata[tableName] = metadata; const store = this.transactionStore(table); return store.put(metadata); } async deleteMetadata(table, config) { const { tableName, tableNameBase } = this.getMetadataTableName(table, config); delete this.tableMetadata[tableName]; const store = this.transactionStore(table); const key = tableNameBase + MetadataSuffix; store.delete(key); } async set(table, changes, config) { var _a, _b; if (typeof indexedDB === 'undefined') return; if (!this.pendingSaves.has(config)) { this.pendingSaves.set(config, {}); } const pendingSaves = this.pendingSaves.get(config); const realTable = table; const prefixID = (_a = config.indexedDB) === null || _a === void 0 ? void 0 : _a.prefixID; if (prefixID) { table += '/' + prefixID; } const prev = this.tableData[table]; const itemID = (_b = config.indexedDB) === null || _b === void 0 ? void 0 : _b.itemID; if (!pendingSaves[table]) { pendingSaves[table] = { tableName: realTable, items: new Set() }; } const pendingTable = pendingSaves[table]; // Combine changes into a minimal set of saves for (let i = 0; i < changes.length; i++) { // eslint-disable-next-line prefer-const let { path, valueAtPath, pathTypes } = changes[i]; if (itemID) { path = [itemID].concat(path); pathTypes.splice(0, 0, 'object'); } if (path.length > 0) { // If change is deep in an object save it to IDB by the first key const key = path[0]; if (!this.tableData[table]) { this.tableData[table] = {}; } this.tableData[table] = setAtPath(this.tableData[table], path, pathTypes, valueAtPath); pendingTable.items.add(key); } else { // Set the whole table this.tableData[table] = valueAtPath; pendingTable.tablePrev = prev; break; } } return new Promise((resolve) => { this.promisesQueued.push(resolve); if (!this.isSaveTaskQueued) { this.isSaveTaskQueued = true; queueMicrotask(this.doSave); } }); } async doSave() { this.isSaveTaskQueued = false; const promisesQueued = this.promisesQueued; this.promisesQueued = []; const promises = []; let lastPut; this.pendingSaves.forEach((pendingSaves, config) => { Object.keys(pendingSaves).forEach((table) => { const pendingTable = pendingSaves[table]; const { tablePrev, items, tableName } = pendingTable; const store = this.transactionStore(tableName); const tableValue = this.tableData[table]; if (tablePrev) { promises.push(this._setTable(table, tablePrev, tableValue, store, config)); } else { items.forEach((key) => { lastPut = this._setItem(table, key, tableValue[key], store, config); }); } // Clear pending saves items.clear(); delete pendingTable.tablePrev; }); }); this.pendingSaves.clear(); // setTable awaits multiple sets and deletes so we need to await that to get // the lastPut from it. if (promises.length) { const puts = await Promise.all(promises); lastPut = puts[puts.length - 1]; } if (lastPut) { await requestToPromise(lastPut); } promisesQueued.forEach((resolve) => resolve()); } async deleteTable(table, config) { const configIDB = config.indexedDB; const prefixID = configIDB === null || configIDB === void 0 ? void 0 : configIDB.prefixID; const tableName = prefixID ? table + '/' + prefixID : table; let data = this.tableData[tableName]; const itemID = configIDB === null || configIDB === void 0 ? void 0 : configIDB.itemID; if (data && (configIDB === null || configIDB === void 0 ? void 0 : configIDB.itemID)) { const dataTemp = data[itemID]; delete data[itemID]; data = dataTemp; } else { delete this.tableData[tableName]; delete this.tableData[tableName + '_transformed']; } if (typeof indexedDB === 'undefined') return; this.deleteMetadata(table, config); if (data) { const store = this.transactionStore(table); let result; if (!prefixID && !itemID) { result = requestToPromise(store.clear()); } else { const keys = Object.keys(data); result = Promise.all(keys.map((key) => { if (prefixID) { key = prefixID + '/' + key; } return requestToPromise(store.delete(key)); })); } // Clear the table from IDB return result; } } // Private getMetadataTableName(table, config) { const configIDB = config.indexedDB; let name = ''; if (configIDB) { const { prefixID, itemID } = configIDB; if (itemID) { name = itemID; } if (prefixID) { name = prefixID + (name ? '/' + name : ''); } } return { tableNameBase: name, tableName: name ? table + '/' + name : table }; } initTable(table, transaction) { const store = transaction.objectStore(table); const allRequest = store.getAll(); if (!this.tableData[table]) { this.tableData[table] = {}; } return new Promise((resolve) => { allRequest.onsuccess = () => { const arr = allRequest.result; let metadata; if (!this.tableData[table]) { this.tableData[table] = {}; } for (let i = 0; i < arr.length; i++) { const val = arr[i]; // In case id is a number convert it to a string if (!val.id.includes) { val.id = val.id + ''; } if (val.id.endsWith(MetadataSuffix)) { const id = val.id.replace(MetadataSuffix, ''); // Save this as metadata delete val.id; metadata = val; const tableName = id ? table + '/' + id : table; this.tableMetadata[tableName] = metadata; } else { let tableName = table; if (val.id.includes('/')) { const [prefix, id] = val.id.split('/'); tableName += '/' + prefix; val.id = id; } if (!this.tableData[tableName]) { this.tableData[tableName] = {}; } this.tableData[tableName][val.id] = val; } } resolve(); }; }); } transactionStore(table) { const transaction = this.db.transaction(table, 'readwrite'); return transaction.objectStore(table); } _setItem(table, key, value, store, config) { var _a; if (!value) { if (this.tableData[table]) { delete this.tableData[table][key]; } return store.delete(key); } else { if (isPrimitive(value)) return; if (value.id === undefined) { // If value does not have its own ID, assign it the key from the Record value.id = key; } if (config) { if (!this.tableData[table]) { this.tableData[table] = {}; } this.tableData[table][key] = value; const prefixID = (_a = config.indexedDB) === null || _a === void 0 ? void 0 : _a.prefixID; if (prefixID) { { value = Object.assign({}, value, { id: prefixID + '/' + value.id, }); } } } return store.put(value); } } async _setTable(table, prev, value, store, config) { const keys = Object.keys(value); let lastSet; // Do a set for each key in the object const sets = await Promise.all(keys.map((key) => { const val = value[key]; return this._setItem(table, key, val, store, config); })); lastSet = sets[sets.length - 1]; // Delete keys that are no longer in the object if (prev) { const keysOld = Object.keys(prev); const deletes = (await Promise.all(keysOld.map((key) => { if (value[key] === undefined) { return this._setItem(table, key, null, store, config); } }))).filter((a) => !!a); if (deletes.length > 0) { lastSet = deletes[deletes.length - 1]; } } return lastSet; } } export { ObservablePersistIndexedDB }; //# sourceMappingURL=indexeddb.mjs.map