UNPKG

@channel-state/core

Version:

Core-library for channel-state, providing framework-agnostic, zero-dependency state management with cross-context synchronization and persistence.

277 lines (275 loc) 8.18 kB
'use strict'; // src/ChannelState.ts var ChannelStore = class { /** * Creates an instance of ChannelStore. * @param options The options for configuring the store. */ constructor(options) { this._db = null; this._subscribers = /* @__PURE__ */ new Set(); this._statusSubscribers = /* @__PURE__ */ new Set(); this._dbKey = "state"; this._initialStateRequestTimeout = null; /** * The current status of the store. */ this.status = "initializing"; /** * Handles messages received from the BroadcastChannel, updating the cache and notifying subscribers. * @param messageEvent The MessageEvent containing the broadcasted data. * @private */ this._handleBroadcast = (messageEvent) => { if (this.status === "destroyed") { return; } const message = messageEvent.data; if (message.senderId === this._instanceId) { return; } if (message.type === "STATE_REQUEST") { this._channel.postMessage({ type: "STATE_UPDATE", payload: this._value, senderId: this._instanceId }); return; } if (this._initialStateRequestTimeout) { clearTimeout(this._initialStateRequestTimeout); this._initialStateRequestTimeout = null; } this._value = message.payload; this.status = "ready"; this._notifySubscribers(); this._notifyStatusSubscribers(); }; this._name = options.name; this._persist = options.persist ?? false; this._initial = options.initial; this._prefixedName = `channel-state__${this._name}`; this._instanceId = crypto.randomUUID(); this._value = structuredClone(this._initial); this._channel = new BroadcastChannel(this._prefixedName); this._channel.addEventListener("message", this._handleBroadcast); if (this._persist) { this._initDB(); } else { this._requestInitialStateFromOtherTabs(); } } _initDB() { if (this.status === "destroyed") { return; } const request = indexedDB.open(this._prefixedName, 1); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains(this._prefixedName)) { db.createObjectStore(this._prefixedName); } }; request.onsuccess = () => { this._db = request.result; this._loadCacheFromDB(); }; request.onerror = () => { console.error("IndexedDB init failed:", request.error); this.status = "ready"; this._notifyStatusSubscribers(); }; } _loadCacheFromDB() { if (this.status === "destroyed") { return; } if (!this._db) return; const tx = this._db.transaction(this._prefixedName, "readonly"); const store = tx.objectStore(this._prefixedName); const req = store.get(this._dbKey); req.onsuccess = () => { const val = req.result; if (val === void 0) { this.status = "ready"; this._notifySubscribers(); this._notifyStatusSubscribers(); } else { this._value = val; this.status = "ready"; this._notifySubscribers(); this._notifyStatusSubscribers(); } }; req.onerror = () => { this._requestInitialStateFromOtherTabs(); }; } _requestInitialStateFromOtherTabs() { if (this.status === "destroyed") { return; } this._channel.postMessage({ type: "STATE_REQUEST", senderId: this._instanceId }); this._initialStateRequestTimeout = setTimeout(() => { if (this.status !== "ready") { this.status = "ready"; this._notifySubscribers(); this._notifyStatusSubscribers(); } this._initialStateRequestTimeout = null; }, 500); } /** * Notifies all registered subscribers about a change in the store's state. * @private */ _notifySubscribers() { this._subscribers.forEach((subscriber) => { subscriber(this._value); }); } _notifyStatusSubscribers() { this._statusSubscribers.forEach((subscriber) => { subscriber(this.status); }); } /** * Triggers a change notification by posting the current cache to the BroadcastChannel * and notifying local subscribers. * @private */ _triggerChange() { if (this.status === "destroyed") { return; } this._channel.postMessage({ type: "STATE_UPDATE", payload: this._value, senderId: this._instanceId }); this._notifySubscribers(); } /** * Synchronously retrieves the current state from the cache. * @returns The current state of the store. */ get() { return this._value; } /** * Sets the new value for the store's state, updates the value, and optionally persists it to IndexedDB asynchronously. * @param value The new state value to set. * @returns A Promise that resolves when the state has been set and persisted (if applicable). */ set(value) { if (this.status === "destroyed") { return; } this._value = value; if (!this._persist || this._db === null) { this._triggerChange(); return; } void new Promise((resolve, reject) => { const db = this._db; if (!db) { reject(new Error("Database not initialized")); return; } const tx = db.transaction(this._prefixedName, "readwrite"); const store = tx.objectStore(this._prefixedName); const req = store.put(value, this._dbKey); req.onsuccess = () => { this._triggerChange(); resolve(); }; req.onerror = () => { reject(new Error(req.error?.message ?? "unknown error")); }; }); } /** * Subscribes a callback function to state changes. * @param callback The function to call when the state changes. * @returns A function that can be called to unsubscribe the callback. */ subscribe(callback) { if (this.status === "destroyed") { throw new Error("ChannelStore is destroyed"); } this._subscribers.add(callback); return () => { this._subscribers.delete(callback); }; } /** * Subscribes a callback function to status changes. * @param callback The function to call when the status changes. * @returns A function that can be called to unsubscribe the callback. */ subscribeStatus(callback) { if (this.status === "destroyed") { throw new Error("ChannelStore is destroyed"); } this._statusSubscribers.add(callback); return () => { this._statusSubscribers.delete(callback); }; } /** * Cleans up resources used by the ChannelStore, including closing the BroadcastChannel * and IndexedDB connection, and clearing subscribers. */ destroy() { if (this.status === "destroyed") { return; } this.status = "destroyed"; this._notifyStatusSubscribers(); this._channel.close(); this._subscribers.clear(); this._statusSubscribers.clear(); this._db?.close(); if (this._initialStateRequestTimeout) { clearTimeout(this._initialStateRequestTimeout); this._initialStateRequestTimeout = null; } } /** * Resets the store's state to its initial value. * @returns A Promise that resolves when the state has been reset. */ reset() { if (this.status === "destroyed") { return Promise.resolve(); } this._value = structuredClone(this._initial); if (!this._db) { this._triggerChange(); return Promise.resolve(); } return new Promise((resolve, reject) => { const db = this._db; if (!db) { reject(new Error("IndexedDB is not available")); return; } const tx = db.transaction(this._prefixedName, "readwrite"); const store = tx.objectStore(this._prefixedName); const req = store.put(this._initial, this._dbKey); req.onsuccess = () => { this._triggerChange(); resolve(); }; req.onerror = () => { reject(new Error(req.error?.message ?? "unknown error")); }; }); } }; exports.ChannelStore = ChannelStore; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map