UNPKG

@stainless-code/persist

Version:

Hydration-aware persistence for any reactive store — zero-dep persistSource core; codecs, backends, cross-tab transport, source + framework hydration adapters ship as opt-in recipes

92 lines (91 loc) • 2.72 kB
//#region src/adapters/transport/crosstab.ts /** * Bridge a `BroadcastChannel` as the cross-tab transport for backends that * fire no `storage` events (IndexedDB). Posts `storageArea: null` on every * event so key-only matching applies in every tab (each tab owns its own * backend instance — reference equality on `raw` would fail across tabs). * * @example * ```ts * import { createBroadcastCrossTab } from "@stainless-code/persist/transport/crosstab"; * import { createIdbStorage } from "@stainless-code/persist/backends/idb"; * import { persistStore } from "@stainless-code/persist/sources/tanstack-store"; * * const bridge = createBroadcastCrossTab({ channelName: "app:prefs" })!; * const persist = persistStore(store, { * name: "app:prefs:v1", * storage: bridge.wrap(createIdbStorage()!), * crossTab: true, * crossTabEventTarget: bridge.crossTabEventTarget, * }); * // on teardown: persist.destroy(); bridge.close(); * ``` */ function createBroadcastCrossTab(options) { if (typeof BroadcastChannel === "undefined") return; const channel = new BroadcastChannel(options.channelName); const handlerMap = /* @__PURE__ */ new Map(); const crossTabEventTarget = { addEventListener(_type, listener) { const handler = (event) => { listener(event.data); }; handlerMap.set(listener, handler); channel.addEventListener("message", handler); }, removeEventListener(_type, listener) { const handler = handlerMap.get(listener); if (handler) { channel.removeEventListener("message", handler); handlerMap.delete(listener); } } }; function postMessage(message) { try { channel.postMessage(message); } catch {} } return { crossTabEventTarget, wrap(storage) { return { getItem: (name) => storage.getItem(name), raw: storage.raw, setItem(name, value) { const result = storage.setItem(name, value); Promise.resolve(result).then(() => { postMessage({ key: name, newValue: "1", storageArea: null }); }).catch(() => {}); return result; }, removeItem(name) { const present = Promise.resolve(storage.getItem(name)).then((v) => v != null, () => true); const result = storage.removeItem(name); present.then((wasPresent) => { if (!wasPresent) return; Promise.resolve(result).then(() => { postMessage({ key: name, newValue: null, storageArea: null }); }).catch(() => {}); }); return result; } }; }, close() { for (const handler of handlerMap.values()) channel.removeEventListener("message", handler); handlerMap.clear(); channel.close(); } }; } //#endregion export { createBroadcastCrossTab };