@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
JavaScript
'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