@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
JavaScript
//#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 };