UNPKG

@instantdb/core

Version:
297 lines • 10.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const PersistedObject_ts_1 = require("./utils/PersistedObject.js"); // Any time these are updates to the data format or new stores are added, // the version must be updated. // onupgradeneeded will be called, which is where you can // move objects from one idb to another. // We create a new IDB for each version change instead of // using their built-in versioning because they have no ability // to roll back and if multiple tabs are active, then you'll just // be stuck. const version = 6; const storeNames = ['kv', 'querySubs', 'syncSubs']; const _exhaustiveCheck = null; function logErrorCb(source) { return function logError(event) { console.error('Error in IndexedDB event', { source, event }); }; } async function existingDb(name) { return new Promise((resolve) => { const request = indexedDB.open(name); request.onerror = (_event) => { resolve(null); }; request.onsuccess = (event) => { const target = event.target; const db = target.result; resolve(db); }; request.onupgradeneeded = (event) => { const target = event.target; target.transaction?.abort(); resolve(null); }; }); } async function upgradeQuerySubs5To6(hash, value, querySubStore) { const subs = // Backwards compatibility for older versions where we JSON.stringified before storing typeof value === 'string' ? JSON.parse(value) : value; if (!subs) { return; } const putReqs = new Set(); return new Promise((resolve, reject) => { const objects = {}; for (const [hash, v] of Object.entries(subs)) { const value = typeof v === 'string' ? JSON.parse(v) : v; if (value.lastAccessed) { const objectMeta = { createdAt: value.lastAccessed, updatedAt: value.lastAccessed, size: value.result?.store?.triples?.length ?? 0, }; objects[hash] = objectMeta; } const putReq = querySubStore.put(value, hash); putReqs.add(putReq); } const meta = { objects }; const metaPutReq = querySubStore.put(meta, PersistedObject_ts_1.META_KEY); putReqs.add(metaPutReq); for (const r of putReqs) { r.onsuccess = () => { putReqs.delete(r); if (putReqs.size === 0) { resolve(); } }; r.onerror = (event) => { logErrorCb(`Move ${hash} to querySubs store failed`); reject(event); }; } }); } async function moveKvEntry5To6(k, value, kvStore) { const request = kvStore.put(value, k); return new Promise((resolve, reject) => { request.onsuccess = () => resolve(); request.onerror = (event) => reject(event); }); } async function upgrade5To6(appId, v6Db) { const v5db = await existingDb(`instant_${appId}_5`); if (!v5db) { return; } const data = await new Promise((resolve, reject) => { const v5Tx = v5db.transaction(['kv'], 'readonly'); const objectStore = v5Tx.objectStore('kv'); const cursorReq = objectStore.openCursor(); cursorReq.onerror = (event) => { reject(event); }; const data = []; cursorReq.onsuccess = () => { const cursor = cursorReq.result; if (cursor) { const key = cursor.key; const value = cursor.value; data.push([key, value]); cursor.continue(); } else { resolve(data); } }; cursorReq.onerror = (event) => { reject(event); }; }); const v6Tx = v6Db.transaction(['kv', 'querySubs'], 'readwrite'); const kvStore = v6Tx.objectStore('kv'); const querySubStore = v6Tx.objectStore('querySubs'); const promises = []; const kvMeta = { objects: {} }; for (const [key, value] of data) { switch (key) { case 'querySubs': { const p = upgradeQuerySubs5To6(key, value, querySubStore); promises.push(p); break; } default: { const p = moveKvEntry5To6(key, value, kvStore); promises.push(p); const objectMeta = { createdAt: Date.now(), updatedAt: Date.now(), size: 0, }; kvMeta.objects[key] = objectMeta; break; } } } const p = moveKvEntry5To6(PersistedObject_ts_1.META_KEY, kvMeta, kvStore); promises.push(p); await Promise.all(promises); await new Promise((resolve, reject) => { v6Tx.oncomplete = (e) => resolve(e); v6Tx.onerror = (e) => reject(e); v6Tx.onabort = (e) => reject(e); }); } // We create many IndexedDBStorage instances that talk to the same // underlying db, but we only get one `onupgradeneeded` event. This holds // the upgrade promises so that we wait until upgrade finishes before // we start writing. const upgradePromises = new Map(); class IndexedDBStorage extends PersistedObject_ts_1.StoreInterface { dbName; _storeName; _appId; _prefix; _dbPromise; constructor(appId, storeName) { super(appId, storeName); this.dbName = `instant_${appId}_${version}`; this._storeName = storeName; this._appId = appId; this._dbPromise = this._init(); } _init() { return new Promise((resolve, reject) => { let requiresUpgrade = false; const request = indexedDB.open(this.dbName, 1); request.onerror = (event) => { reject(event); }; request.onsuccess = (event) => { const target = event.target; const db = target.result; if (!requiresUpgrade) { const p = upgradePromises.get(this.dbName); if (!p) { resolve(db); } else { p.then(() => resolve(db)).catch(() => resolve(db)); } } else { const p = upgrade5To6(this._appId, db).catch((e) => { logErrorCb('Error upgrading store from version 5 to 6.')(e); }); upgradePromises.set(this.dbName, p); p.then(() => resolve(db)).catch(() => resolve(db)); } }; request.onupgradeneeded = (event) => { requiresUpgrade = true; this._upgradeStore(event); }; }); } _upgradeStore(event) { const target = event.target; const db = target.result; for (const storeName of storeNames) { if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName); } } } async getItem(k) { const db = await this._dbPromise; return new Promise((resolve, reject) => { const transaction = db.transaction([this._storeName], 'readonly'); const objectStore = transaction.objectStore(this._storeName); const request = objectStore.get(k); request.onerror = (event) => { reject(event); }; request.onsuccess = (_event) => { if (request.result) { resolve(request.result); } else { resolve(null); } }; }); } async setItem(k, v) { const db = await this._dbPromise; return new Promise((resolve, reject) => { const transaction = db.transaction([this._storeName], 'readwrite'); const objectStore = transaction.objectStore(this._storeName); const request = objectStore.put(v, k); request.onerror = (event) => { reject(event); }; request.onsuccess = (_event) => { resolve(); }; }); } // Performs all writes in a transaction so that all succeed or none succeed. async multiSet(keyValuePairs) { const db = await this._dbPromise; return new Promise((resolve, reject) => { const transaction = db.transaction([this._storeName], 'readwrite'); const objectStore = transaction.objectStore(this._storeName); const requests = new Set(); for (const [k, v] of keyValuePairs) { const request = objectStore.put(v, k); requests.add(request); } for (const request of requests) { request.onerror = (event) => { transaction.abort(); reject(event); }; request.onsuccess = (_event) => { requests.delete(request); // Last request to finish resolves the transaction if (requests.size === 0) { resolve(); } }; } }); } async removeItem(k) { const db = await this._dbPromise; return new Promise((resolve, reject) => { const transaction = db.transaction([this._storeName], 'readwrite'); const objectStore = transaction.objectStore(this._storeName); const request = objectStore.delete(k); request.onerror = (event) => { reject(event); }; request.onsuccess = (_event) => { resolve(); }; }); } async getAllKeys() { const db = await this._dbPromise; return new Promise((resolve, reject) => { const transaction = db.transaction([this._storeName], 'readonly'); const objectStore = transaction.objectStore(this._storeName); const request = objectStore.getAllKeys(); request.onerror = (event) => { reject(event); }; request.onsuccess = (_event) => { resolve(request.result.filter((x) => typeof x === 'string')); }; }); } } exports.default = IndexedDBStorage; //# sourceMappingURL=IndexedDBStorage.js.map