UNPKG

protoobject

Version:

A universal class for creating any JSON objects and simple manipulations with them.

642 lines (641 loc) 24.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProtoObjectBrowserStorage = void 0; /** * Universal ProtoObject browser storage utility * Supports localStorage, sessionStorage, IndexedDB, cookies, BroadcastChannel, and Web Workers */ class ProtoObjectBrowserStorage { /** * Save ProtoObject instance to browser storage */ static async save(key, obj, options = { type: "local" }) { try { const json = obj.toJSON(); const serialized = JSON.stringify(json); return await this.saveData(key, json, serialized, options); } catch (error) { console.error("ProtoObjectBrowserStorage.save error:", error); return false; } } /** * Load ProtoObject instance from browser storage */ static async load(key, ClassConstructor, options = { type: "local" }) { try { const data = await this.loadData(key, options); return data ? ClassConstructor.fromJSON(data) : undefined; } catch (error) { console.error("ProtoObjectBrowserStorage.load error:", error); return undefined; } } /** * Remove item from browser storage */ static async remove(key, options = { type: "local" }) { return await this.removeData(key, options); } /** * Check if key exists in browser storage */ static async exists(key, options = { type: "local" }) { return await this.existsData(key, options); } /** * Get all keys with optional prefix filter */ static async getKeys(prefix, options = { type: "local" }) { return await this.getKeysData(prefix, options); } /** * Clear storage with optional prefix filter */ static async clear(prefix, options = { type: "local" }) { try { const keys = await this.getKeys(prefix, options); let removed = 0; for (const key of keys) { if (await this.remove(key, options)) { removed += 1; } } return removed; } catch (error) { console.error("ProtoObjectBrowserStorage.clear error:", error); return 0; } } // Private delegation methods to reduce code duplication static async saveData(key, json, serialized, options) { switch (options.type) { case "local": return this.saveToWebStorage(key, serialized, "localStorage"); case "session": return this.saveToWebStorage(key, serialized, "sessionStorage"); case "indexeddb": return await this.saveToIndexedDB(key, json, options); case "cookies": return this.saveToCookies(key, serialized, options); case "broadcast": return this.saveToBroadcast(key, json, options); case "worker": return await this.saveToWorker(key, json, options); default: console.error(`Unsupported storage type: ${options.type}`); return false; } } static async loadData(key, options) { switch (options.type) { case "local": return this.loadFromWebStorage(key, "localStorage"); case "session": return this.loadFromWebStorage(key, "sessionStorage"); case "indexeddb": return await this.loadFromIndexedDB(key, options); case "cookies": return this.loadFromCookies(key); case "broadcast": return this.loadFromBroadcast(key, options); case "worker": return await this.loadFromWorker(key, options); default: console.error(`Unsupported storage type: ${options.type}`); return undefined; } } static async removeData(key, options) { try { switch (options.type) { case "local": return this.removeFromWebStorage(key, "localStorage"); case "session": return this.removeFromWebStorage(key, "sessionStorage"); case "indexeddb": return await this.removeFromIndexedDB(key, options); case "cookies": return this.removeFromCookies(key, options); case "broadcast": return this.removeFromBroadcast(key, options); case "worker": return await this.removeFromWorker(key, options); default: console.error(`Unsupported storage type: ${options.type}`); return false; } } catch (error) { console.error("ProtoObjectBrowserStorage.remove error:", error); return false; } } static async existsData(key, options) { try { switch (options.type) { case "local": return this.existsInWebStorage(key, "localStorage"); case "session": return this.existsInWebStorage(key, "sessionStorage"); case "indexeddb": return await this.existsInIndexedDB(key, options); case "cookies": return this.existsInCookies(key); case "broadcast": return this.existsInBroadcast(key, options); case "worker": return await this.existsInWorker(key, options); default: console.error(`Unsupported storage type: ${options.type}`); return false; } } catch (error) { console.error("ProtoObjectBrowserStorage.exists error:", error); return false; } } static async getKeysData(prefix, options) { try { switch (options.type) { case "local": return this.getKeysFromWebStorage(prefix, "localStorage"); case "session": return this.getKeysFromWebStorage(prefix, "sessionStorage"); case "indexeddb": return await this.getKeysFromIndexedDB(prefix, options); case "cookies": return this.getKeysFromCookies(prefix); case "broadcast": return this.getKeysFromBroadcast(prefix, options); case "worker": return await this.getKeysFromWorker(prefix, options); default: console.error(`Unsupported storage type: ${options.type}`); return []; } } catch (error) { console.error("ProtoObjectBrowserStorage.getKeys error:", error); return []; } } // Private methods for Web Storage (localStorage/sessionStorage) static saveToWebStorage(key, serialized, storageType) { if (typeof window === "undefined" || !window[storageType]) { return false; } window[storageType].setItem(key, serialized); return true; } static loadFromWebStorage(key, storageType) { if (typeof window === "undefined" || !window[storageType]) { return undefined; } const serialized = window[storageType].getItem(key); return serialized ? JSON.parse(serialized) : undefined; } static removeFromWebStorage(key, storageType) { if (typeof window === "undefined" || !window[storageType]) { return false; } window[storageType].removeItem(key); return true; } static existsInWebStorage(key, storageType) { if (typeof window === "undefined" || !window[storageType]) { return false; } return window[storageType].getItem(key) !== null; } static getKeysFromWebStorage(prefix, storageType) { if (typeof window === "undefined" || !window[storageType]) { return []; } const keys = []; const storage = window[storageType]; for (let i = 0; i < storage.length; i++) { const key = storage.key(i); if (key && (!prefix || key.startsWith(prefix))) { keys.push(key); } } return keys; } // Private methods for IndexedDB static async getIndexedDB(options) { if (typeof window === "undefined" || !window.indexedDB) { return undefined; } const dbName = options.dbName || "ProtoObjectDB"; if (this.idbDatabases.has(dbName)) { return this.idbDatabases.get(dbName); } return new Promise((resolve, reject) => { const request = indexedDB.open(dbName, 1); request.onerror = () => reject(request.error); request.onsuccess = () => { const db = request.result; this.idbDatabases.set(dbName, db); resolve(db); }; request.onupgradeneeded = () => { const db = request.result; const storeName = options.storeName || "objects"; if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName); } }; }); } static async saveToIndexedDB(key, data, options) { const db = await this.getIndexedDB(options); if (!db) return false; return new Promise((resolve) => { const transaction = db.transaction([options.storeName || "objects"], "readwrite"); const store = transaction.objectStore(options.storeName || "objects"); const request = store.put(data, key); request.onsuccess = () => resolve(true); request.onerror = () => resolve(false); }); } static async loadFromIndexedDB(key, options) { const db = await this.getIndexedDB(options); if (!db) return undefined; return new Promise((resolve) => { const transaction = db.transaction([options.storeName || "objects"], "readonly"); const store = transaction.objectStore(options.storeName || "objects"); const request = store.get(key); request.onsuccess = () => resolve(request.result || undefined); request.onerror = () => resolve(undefined); }); } static async removeFromIndexedDB(key, options) { const db = await this.getIndexedDB(options); if (!db) return false; return new Promise((resolve) => { const transaction = db.transaction([options.storeName || "objects"], "readwrite"); const store = transaction.objectStore(options.storeName || "objects"); const request = store.delete(key); request.onsuccess = () => resolve(true); request.onerror = () => resolve(false); }); } static async existsInIndexedDB(key, options) { const data = await this.loadFromIndexedDB(key, options); return data !== undefined; } static async getKeysFromIndexedDB(prefix, options) { const db = await this.getIndexedDB(options); if (!db) return []; return new Promise((resolve) => { const transaction = db.transaction([options.storeName || "objects"], "readonly"); const store = transaction.objectStore(options.storeName || "objects"); const request = store.getAllKeys(); request.onsuccess = () => { const keys = request.result .map((key) => String(key)) .filter((key) => !prefix || key.startsWith(prefix)); resolve(keys); }; request.onerror = () => resolve([]); }); } // Private methods for Cookies static saveToCookies(key, serialized, options) { if (typeof document === "undefined") { return false; } try { const maxAge = options.maxAge || 31536000; // 1 year default const domain = options.domain ? `; domain=${options.domain}` : ""; const path = `; path=${options.path || "/"}`; document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(serialized)}; max-age=${maxAge}${domain}${path}`; return true; } catch { return false; } } static loadFromCookies(key) { if (typeof document === "undefined") { return undefined; } try { const encodedKey = encodeURIComponent(key); const cookies = document.cookie.split(";"); for (const cookie of cookies) { const [cookieKey, cookieValue] = cookie.trim().split("="); if (cookieKey === encodedKey && cookieValue) { const decoded = decodeURIComponent(cookieValue); return JSON.parse(decoded); } } return undefined; } catch { return undefined; } } static removeFromCookies(key, options) { if (typeof document === "undefined") { return false; } try { const domain = options.domain ? `; domain=${options.domain}` : ""; const path = `; path=${options.path || "/"}`; document.cookie = `${encodeURIComponent(key)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT${domain}${path}`; return true; } catch { return false; } } static existsInCookies(key) { return this.loadFromCookies(key) !== undefined; } static getKeysFromCookies(prefix) { if (typeof document === "undefined") { return []; } try { const keys = []; const cookies = document.cookie.split(";"); for (const cookie of cookies) { const [cookieKey] = cookie.trim().split("="); if (cookieKey) { const decoded = decodeURIComponent(cookieKey); if (!prefix || decoded.startsWith(prefix)) { keys.push(decoded); } } } return keys; } catch { return []; } } // Private methods for BroadcastChannel static getBroadcastChannel(options) { if (typeof window === "undefined" || !window.BroadcastChannel) { return undefined; } const channelName = options.channelName || "protoobject-channel"; if (!this.channels.has(channelName)) { this.channels.set(channelName, new BroadcastChannel(channelName)); } return this.channels.get(channelName); } static saveToBroadcast(key, data, options) { const channel = this.getBroadcastChannel(options); if (!channel) return false; try { channel.postMessage({ type: "save", key, data }); return true; } catch { return false; } } static loadFromBroadcast(key, options) { const channel = this.getBroadcastChannel(options); if (!channel) return undefined; // BroadcastChannel is async by nature, this is a synchronous fallback // In real usage, you'd typically listen for messages try { channel.postMessage({ type: "load", key }); return undefined; // Would need async implementation for real usage } catch { return undefined; } } static removeFromBroadcast(key, options) { const channel = this.getBroadcastChannel(options); if (!channel) return false; try { channel.postMessage({ type: "remove", key }); return true; } catch { return false; } } static existsInBroadcast(key, options) { const channel = this.getBroadcastChannel(options); if (!channel) return false; try { channel.postMessage({ type: "exists", key }); return false; // Would need async implementation for real usage } catch { return false; } } static getKeysFromBroadcast(prefix, options) { const channel = this.getBroadcastChannel(options); if (!channel) return []; try { channel.postMessage({ type: "getKeys", prefix }); return []; // Would need async implementation for real usage } catch { return []; } } // Private methods for Web Worker static getWorker(options) { if (typeof window === "undefined" || !window.Worker || !options.workerScript) { return undefined; } if (!this.workers.has(options.workerScript)) { try { const worker = new Worker(options.workerScript); this.workers.set(options.workerScript, worker); } catch { return undefined; } } return this.workers.get(options.workerScript); } static async saveToWorker(key, data, options) { const worker = this.getWorker(options); if (!worker) return false; return new Promise((resolve) => { const messageId = Date.now().toString(); const handleMessage = (event) => { if (event.data.id === messageId) { worker.removeEventListener("message", handleMessage); resolve(event.data.success || false); } }; worker.addEventListener("message", handleMessage); worker.postMessage({ id: messageId, type: "save", key, data }); // Timeout after 5 seconds setTimeout(() => { worker.removeEventListener("message", handleMessage); resolve(false); }, 5000); }); } static async loadFromWorker(key, options) { const worker = this.getWorker(options); if (!worker) return undefined; return new Promise((resolve) => { const messageId = Date.now().toString(); const handleMessage = (event) => { if (event.data.id === messageId) { worker.removeEventListener("message", handleMessage); resolve(event.data.data || undefined); } }; worker.addEventListener("message", handleMessage); worker.postMessage({ id: messageId, type: "load", key }); // Timeout after 5 seconds setTimeout(() => { worker.removeEventListener("message", handleMessage); resolve(undefined); }, 5000); }); } static async removeFromWorker(key, options) { const worker = this.getWorker(options); if (!worker) return false; return new Promise((resolve) => { const messageId = Date.now().toString(); const handleMessage = (event) => { if (event.data.id === messageId) { worker.removeEventListener("message", handleMessage); resolve(event.data.success || false); } }; worker.addEventListener("message", handleMessage); worker.postMessage({ id: messageId, type: "remove", key }); // Timeout after 5 seconds setTimeout(() => { worker.removeEventListener("message", handleMessage); resolve(false); }, 5000); }); } static async existsInWorker(key, options) { const worker = this.getWorker(options); if (!worker) return false; return new Promise((resolve) => { const messageId = Date.now().toString(); const handleMessage = (event) => { if (event.data.id === messageId) { worker.removeEventListener("message", handleMessage); resolve(event.data.exists || false); } }; worker.addEventListener("message", handleMessage); worker.postMessage({ id: messageId, type: "exists", key }); // Timeout after 5 seconds setTimeout(() => { worker.removeEventListener("message", handleMessage); resolve(false); }, 5000); }); } static async getKeysFromWorker(prefix, options) { const worker = this.getWorker(options); if (!worker) return []; return new Promise((resolve) => { const messageId = Date.now().toString(); const handleMessage = (event) => { if (event.data.id === messageId) { worker.removeEventListener("message", handleMessage); resolve(event.data.keys || []); } }; worker.addEventListener("message", handleMessage); worker.postMessage({ id: messageId, type: "getKeys", prefix }); // Timeout after 5 seconds setTimeout(() => { worker.removeEventListener("message", handleMessage); resolve([]); }, 5000); }); } /** * Array operations */ static async saveArray(key, objects, options = { type: "local" }) { try { const jsonArray = objects.map((obj) => obj.toJSON()); const serialized = JSON.stringify({ array: jsonArray }); return await this.saveData(key, { array: jsonArray }, serialized, options); } catch (error) { console.error("ProtoObjectBrowserStorage.saveArray error:", error); return false; } } static async loadArray(key, ClassConstructor, options = { type: "local" }) { try { const data = await this.loadData(key, options); if (!data || !Array.isArray(data.array)) { return undefined; } return data.array.map((json) => ClassConstructor.fromJSON(json)); } catch (error) { console.error("ProtoObjectBrowserStorage.loadArray error:", error); return undefined; } } /** * Utility method to check storage support */ static getStorageSupport() { return { local: typeof window !== "undefined" && !!window.localStorage, session: typeof window !== "undefined" && !!window.sessionStorage, indexeddb: typeof window !== "undefined" && !!window.indexedDB, cookies: typeof document !== "undefined", broadcast: typeof window !== "undefined" && !!window.BroadcastChannel, worker: typeof window !== "undefined" && !!window.Worker, }; } /** * Cleanup method to close connections */ static cleanup() { // Close IndexedDB connections this.idbDatabases.forEach((db) => db.close()); this.idbDatabases.clear(); // Close BroadcastChannels this.channels.forEach((channel) => channel.close()); this.channels.clear(); // Terminate workers this.workers.forEach((worker) => worker.terminate()); this.workers.clear(); } } exports.ProtoObjectBrowserStorage = ProtoObjectBrowserStorage; ProtoObjectBrowserStorage.workers = new Map(); ProtoObjectBrowserStorage.channels = new Map(); ProtoObjectBrowserStorage.idbDatabases = new Map();