protoobject
Version:
A universal class for creating any JSON objects and simple manipulations with them.
642 lines (641 loc) • 24.1 kB
JavaScript
"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();