UNPKG

use-idb-store

Version:

A React state hook that syncs state to IndexedDB for persistent, offline-friendly, and large-scale data storage.

824 lines (810 loc) 36.3 kB
import { useState, useRef, useEffect } from 'react'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * Converts an IndexedDB request into a Promise * @template T The type of the result that will be returned by the IndexedDB request * @param request The IndexedDB request to convert to a Promise * @returns A Promise that resolves with the request result or rejects with an error message */ function idbRequestToPromise(request) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, new Promise(function (resolve, reject) { request.addEventListener("success", function () { resolve(request.result); }); request.addEventListener("error", function (event) { // Extract more detailed error information var target = event.target; var error = target.error; if (error) { var detailedError = new Error("IndexedDB Error: ".concat(error.name, " - ").concat(error.message)); detailedError.originalError = error; detailedError.event = event; reject(detailedError); } else { reject(new Error("IndexedDB operation failed with unknown error")); } }); })]; }); }); } /** * Safely executes an async operation with error handling * @template T The type of the result that will be returned by the operation * @param fn The async function to execute * @returns A Promise that resolves with the result or null if an error occurs */ function safeCall(fn) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 2, , 3]); return [4 /*yield*/, fn()]; case 1: return [2 /*return*/, _b.sent()]; case 2: _b.sent(); return [2 /*return*/, null]; case 3: return [2 /*return*/]; } }); }); } /** * Safely executes an async operation that returns void with error handling * @param fn The async function to execute * @returns A Promise that resolves to void, silently handling any errors */ function safeVoidCall(fn) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 2, , 3]); return [4 /*yield*/, fn()]; case 1: _b.sent(); return [3 /*break*/, 3]; case 2: _b.sent(); return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); } var DB = /** @class */ (function () { function DB() { this.indexedDB = null; this.stores = new Map(); this.version = undefined; this.dbInitPromise = null; } DB.getInstance = function () { if (!DB.instance) { DB.instance = new DB(); } return DB.instance; }; DB.prototype.getCurrentDatabaseVersion = function () { return __awaiter(this, void 0, void 0, function () { var request, db, currentVersion; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 2, , 3]); request = indexedDB.open("use-idb-store"); return [4 /*yield*/, idbRequestToPromise(request)]; case 1: db = _b.sent(); currentVersion = db.version; db.close(); return [2 /*return*/, currentVersion]; case 2: _b.sent(); // Database doesn't exist yet, start with version 1 return [2 /*return*/, 1]; case 3: return [2 /*return*/]; } }); }); }; DB.prototype.openDatabase = function () { return __awaiter(this, void 0, void 0, function () { var _a, missingStores, missingStores, db, error_1; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: // If there's already an initialization in progress, wait for it if (this.dbInitPromise) { return [2 /*return*/, this.dbInitPromise]; } if (!(this.version === undefined)) return [3 /*break*/, 2]; _a = this; return [4 /*yield*/, this.getCurrentDatabaseVersion()]; case 1: _a.version = _b.sent(); _b.label = 2; case 2: // If database is already open, check if we need to upgrade if (this.indexedDB) { missingStores = Array.from(this.stores.keys()).filter(function (name) { return !_this.indexedDB.objectStoreNames.contains(name); }); if (missingStores.length === 0) { return [2 /*return*/, this.indexedDB]; } // Close the current connection to upgrade this.indexedDB.close(); this.indexedDB = null; this.version++; } else { missingStores = Array.from(this.stores.keys()); if (missingStores.length > 0) { this.version++; } } // Create initialization promise this.dbInitPromise = this.initializeDatabase(); _b.label = 3; case 3: _b.trys.push([3, 5, , 6]); return [4 /*yield*/, this.dbInitPromise]; case 4: db = _b.sent(); this.dbInitPromise = null; return [2 /*return*/, db]; case 5: error_1 = _b.sent(); this.dbInitPromise = null; throw error_1; case 6: return [2 /*return*/]; } }); }); }; DB.prototype.initializeDatabase = function () { return __awaiter(this, void 0, void 0, function () { var request, _a, error_2; var _this = this; return __generator(this, function (_b) { switch (_b.label) { case 0: request = indexedDB.open("use-idb-store", this.version); request.onupgradeneeded = function (event) { var db = event.target.result; // Create all registered stores _this.stores.forEach(function (schema, name) { if (!db.objectStoreNames.contains(name)) { try { db.createObjectStore(name, schema); } catch (error) { console.error("Error creating store ".concat(name, ":"), error); throw error; } } }); }; request.onblocked = function () { console.warn("Database opening blocked. Close other tabs/windows using this database."); }; _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); _a = this; return [4 /*yield*/, idbRequestToPromise(request)]; case 2: _a.indexedDB = _b.sent(); return [2 /*return*/, this.indexedDB]; case 3: error_2 = _b.sent(); console.error("Database opening failed:", error_2); throw error_2; case 4: return [2 /*return*/]; } }); }); }; DB.prototype.createStore = function (name, schema) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: // Register the store this.stores.set(name, schema); // Open/upgrade database to include this store return [4 /*yield*/, this.openDatabase()]; case 1: // Open/upgrade database to include this store _a.sent(); return [2 /*return*/]; } }); }); }; DB.prototype.getStore = function (name) { return __awaiter(this, void 0, void 0, function () { var db; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.openDatabase()]; case 1: db = _a.sent(); // Verify store exists if (!db.objectStoreNames.contains(name)) { throw new Error("Store \"".concat(name, "\" not found in database. Available stores: ").concat(Array.from(db.objectStoreNames).join(", "))); } return [2 /*return*/, db.transaction(name, "readwrite").objectStore(name)]; } }); }); }; DB.prototype.clearStore = function (name) { return __awaiter(this, void 0, void 0, function () { var store; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.getStore(name)]; case 1: store = _a.sent(); return [4 /*yield*/, idbRequestToPromise(store.clear())]; case 2: _a.sent(); return [2 /*return*/]; } }); }); }; DB.prototype.deleteStore = function (name) { return __awaiter(this, void 0, void 0, function () { var _a, request, _b, error_3; var _this = this; return __generator(this, function (_c) { switch (_c.label) { case 0: // Remove from registry this.stores.delete(name); if (!this.indexedDB) return [3 /*break*/, 6]; this.indexedDB.close(); this.indexedDB = null; if (!(this.version === undefined)) return [3 /*break*/, 2]; _a = this; return [4 /*yield*/, this.getCurrentDatabaseVersion()]; case 1: _a.version = _c.sent(); _c.label = 2; case 2: this.version++; request = indexedDB.open("use-idb-store", this.version); request.onupgradeneeded = function (event) { var db = event.target.result; // Delete the store if it exists if (db.objectStoreNames.contains(name)) { db.deleteObjectStore(name); } // Recreate remaining stores that should exist _this.stores.forEach(function (schema, storeName) { if (!db.objectStoreNames.contains(storeName)) { try { db.createObjectStore(storeName, schema); } catch (error) { console.error("Error recreating store ".concat(storeName, ":"), error); throw error; } } }); }; _c.label = 3; case 3: _c.trys.push([3, 5, , 6]); _b = this; return [4 /*yield*/, idbRequestToPromise(request)]; case 4: _b.indexedDB = _c.sent(); return [3 /*break*/, 6]; case 5: error_3 = _c.sent(); console.error("Database update failed during store deletion:", error_3); throw error_3; case 6: return [2 /*return*/]; } }); }); }; return DB; }()); var Store = /** @class */ (function () { function Store(name, schema) { this.isInitialized = false; this.initPromise = null; this.eventListeners = { change: [], error: [], }; this.db = DB.getInstance(); this.name = name; this.schema = schema; this.initPromise = this.setup(); } Store.getInstance = function (name, schema) { if (!Store.instance[name]) { Store.instance[name] = new Store(name, schema); } return Store.instance[name]; }; Store.prototype.setup = function () { return __awaiter(this, void 0, void 0, function () { var error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, this.db.createStore(this.name, this.schema)]; case 1: _a.sent(); this.isInitialized = true; return [3 /*break*/, 3]; case 2: error_1 = _a.sent(); throw error_1; case 3: return [2 /*return*/]; } }); }); }; Store.prototype.ensureInitialized = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: if (!!this.isInitialized) return [3 /*break*/, 4]; if (!this.initPromise) return [3 /*break*/, 2]; return [4 /*yield*/, this.initPromise]; case 1: _a.sent(); return [3 /*break*/, 4]; case 2: this.initPromise = this.setup(); return [4 /*yield*/, this.initPromise]; case 3: _a.sent(); _a.label = 4; case 4: return [2 /*return*/]; } }); }); }; Store.prototype.getStore = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.ensureInitialized()]; case 1: _a.sent(); return [2 /*return*/, this.db.getStore(this.name)]; } }); }); }; Store.prototype.getItem = function (key) { return __awaiter(this, void 0, void 0, function () { var store, request, error_2; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, this.getStore()]; case 1: store = _a.sent(); request = store.get(key); return [2 /*return*/, idbRequestToPromise(request)]; case 2: error_2 = _a.sent(); this.emit("error", error_2); return [2 /*return*/, null]; case 3: return [2 /*return*/]; } }); }); }; Store.prototype.getAllItems = function () { return __awaiter(this, void 0, void 0, function () { var store, request_1, result_1, error_3; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, this.getStore()]; case 1: store = _a.sent(); request_1 = store.openCursor(); result_1 = {}; return [2 /*return*/, new Promise(function (resolve, reject) { request_1.onsuccess = function (event) { var cursor = event.target .result; if (cursor) { result_1[cursor.key.toString()] = cursor.value; cursor.continue(); } else { resolve(result_1); } }; request_1.onerror = function () { return reject(request_1.error); }; })]; case 2: error_3 = _a.sent(); this.emit("error", error_3); return [2 /*return*/, {}]; case 3: return [2 /*return*/]; } }); }); }; Store.prototype.addItem = function (key, value) { return __awaiter(this, void 0, void 0, function () { var store, request, result, error_4; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 3, , 4]); return [4 /*yield*/, this.getStore()]; case 1: store = _a.sent(); request = store.add(value, key); return [4 /*yield*/, idbRequestToPromise(request)]; case 2: result = _a.sent(); this.emit("change", { add: { key: key, value: value } }); return [2 /*return*/, result]; case 3: error_4 = _a.sent(); this.emit("error", error_4); throw error_4; case 4: return [2 /*return*/]; } }); }); }; Store.prototype.addOrUpdateItem = function (key, value) { return __awaiter(this, void 0, void 0, function () { var store, request, result, error_5; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 3, , 4]); return [4 /*yield*/, this.getStore()]; case 1: store = _a.sent(); request = store.put(value, key); return [4 /*yield*/, idbRequestToPromise(request)]; case 2: result = _a.sent(); this.emit("change", { addOrUpdate: { key: key, value: value } }); return [2 /*return*/, result]; case 3: error_5 = _a.sent(); throw error_5; case 4: return [2 /*return*/]; } }); }); }; Store.prototype.updateItem = function (key, value) { return __awaiter(this, void 0, void 0, function () { var store, request_2, error_6; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, this.getStore()]; case 1: store = _a.sent(); request_2 = store.openCursor(key); return [2 /*return*/, new Promise(function (resolve, reject) { request_2.onsuccess = function (event) { var cursor = event.target .result; if (cursor) { var existingItem = cursor.value; var updatedItem = __assign(__assign({}, existingItem), value); var updateRequest_1 = cursor.update(updatedItem); updateRequest_1.onsuccess = function () { resolve(key); _this.emit("change", { update: key }); }; updateRequest_1.onerror = function () { reject(updateRequest_1.error); _this.emit("error", updateRequest_1.error); }; } else { reject(new Error("Item with key ".concat(key, " not found in store ").concat(_this.name))); _this.emit("error", new Error("Item with key ".concat(key, " not found in store ").concat(_this.name))); } }; request_2.onerror = function () { return reject(request_2.error); }; })]; case 2: error_6 = _a.sent(); this.emit("error", error_6); throw error_6; case 3: return [2 /*return*/]; } }); }); }; Store.prototype.deleteItem = function (key) { return __awaiter(this, void 0, void 0, function () { var store, request, error_7; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 3, , 4]); return [4 /*yield*/, this.getStore()]; case 1: store = _a.sent(); request = store.delete(key); return [4 /*yield*/, idbRequestToPromise(request)]; case 2: _a.sent(); this.emit("change", { delete: { key: key } }); return [3 /*break*/, 4]; case 3: error_7 = _a.sent(); this.emit("error", error_7); throw error_7; case 4: return [2 /*return*/]; } }); }); }; /** * Emits an event to all registered listeners for a specific event type * @param event - The type of event to emit ('change' or 'error') * @param detail - The detail to emit with the event */ Store.prototype.emit = function (event, detail) { var listeners = this.eventListeners[event]; if (listeners && listeners.length > 0) { // Create a custom event with the data var customEvent_1 = new CustomEvent(event, { detail: detail }); listeners.forEach(function (callback) { return callback(customEvent_1); }); } }; /** * Registers an event listener for the specified store event */ Store.prototype.on = function (event, callback) { this.eventListeners[event].push(callback); }; /** * Removes an event listener from the store */ Store.prototype.off = function (event, callback) { this.eventListeners[event] = this.eventListeners[event].filter(function (listener) { return listener !== callback; }); }; /** * Clears all items from the store. */ Store.prototype.clear = function () { return __awaiter(this, void 0, void 0, function () { var store, error_8; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, this.getStore()]; case 1: store = _a.sent(); store.clear(); this.emit("change", { clear: true }); return [3 /*break*/, 3]; case 2: error_8 = _a.sent(); this.emit("error", error_8); throw error_8; case 3: return [2 /*return*/]; } }); }); }; /** * Destroys the store and deletes it from the database. */ Store.prototype.destroy = function () { return __awaiter(this, void 0, void 0, function () { var error_9; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, this.db.deleteStore(this.name)]; case 1: _a.sent(); this.emit("change", { destroy: true }); return [3 /*break*/, 3]; case 2: error_9 = _a.sent(); this.emit("error", error_9); throw error_9; case 3: return [2 /*return*/]; } }); }); }; Store.instance = {}; return Store; }()); var useIndexedDbStore = function (name, _a) { var _b = _a === void 0 ? {} : _a, schema = _b.schema; // States. // HACK: Force update to trigger re-render. var _c = useState({}), forceUpdate = _c[1]; // Refs. var storeRef = useRef(null); var valuesRef = useRef({}); var isLoadingRef = useRef(true); var isReadyRef = useRef(false); var errorRef = useRef(null); // useEffect. useEffect(function () { if (storeRef.current) return; storeRef.current = Store.getInstance(name, schema); var loadData = function () { return __awaiter(void 0, void 0, void 0, function () { var data, err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); if (!storeRef.current) return [2 /*return*/]; isLoadingRef.current = true; errorRef.current = null; return [4 /*yield*/, storeRef.current.getAllItems()]; case 1: data = _a.sent(); valuesRef.current = data; isLoadingRef.current = false; return [3 /*break*/, 3]; case 2: err_1 = _a.sent(); console.error("Error loading indexed DB data:", err_1); errorRef.current = err_1 instanceof Error ? err_1 : new Error(String(err_1)); isLoadingRef.current = false; return [3 /*break*/, 3]; case 3: forceUpdate({}); return [2 /*return*/]; } }); }); }; var handleError = function (err) { errorRef.current = err instanceof Error ? err : new Error(String(err)); isLoadingRef.current = false; forceUpdate({}); }; // Initial load. loadData().then(function () { isReadyRef.current = true; forceUpdate({}); }); storeRef.current.on("change", loadData); storeRef.current.on("error", handleError); return function () { // TODO: Unsubscribe from changes. // storeRef.current?.off("change", loadData); // storeRef.current?.off("error", handleError); }; }, []); // Handlers. var mutations = { getValue: function (id) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, safeCall(function () { return __awaiter(void 0, void 0, void 0, function () { var storeValue; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, storeRef.current.getItem(id)]; case 1: storeValue = _b.sent(); return [2 /*return*/, (_a = storeValue !== null && storeValue !== void 0 ? storeValue : valuesRef.current[id]) !== null && _a !== void 0 ? _a : null]; } }); }); })]; }); }); }, addValue: function (id, value) { return safeVoidCall(function () { return storeRef.current.addItem(id, value); }); }, deleteValue: function (id) { return safeVoidCall(function () { return storeRef.current.deleteItem(id); }); }, updateValue: function (id, value) { return safeVoidCall(function () { return storeRef.current.updateItem(id, value); }); }, addOrUpdateValue: function (id, value) { return safeVoidCall(function () { return storeRef.current.addOrUpdateItem(id, value); }); }, }; return { values: valuesRef.current, mutations: mutations, isLoading: isLoadingRef.current, error: errorRef.current, isReady: isReadyRef.current, }; }; export { DB, Store, idbRequestToPromise, safeCall, safeVoidCall, useIndexedDbStore }; //# sourceMappingURL=index.esm.js.map