UNPKG

channelstate

Version:

A lightweight cross-context state management library built on the BroadcastChannel API.

134 lines (127 loc) 3.9 kB
import { produce } from 'immer'; import { useSyncExternalStore, useDebugValue } from 'react'; import { createStore } from './vanilla.mjs'; function uuid() { if (typeof crypto !== "undefined" && crypto.randomUUID) { return crypto.randomUUID(); } return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === "x" ? r : r & 3 | 8; return v.toString(16); }); } function createChannelStateImpl(channelName, creator) { const channel = new BroadcastChannel(channelName); const id = uuid(); const { setupStore, onMessage, onInit, onClose } = creator( Object.assign({ id }, channel, { postMessage(message) { channel.postMessage({ ...message, sender: id }); } }) ); channel.addEventListener("message", ({ data }) => { if (data.action && data.sender && data.sender !== id) { onMessage(data); } }); onInit?.(); return Object.assign(setupStore(), { id, close() { onClose?.(); channel.close(); } }); } function createPrimaryChannelStateImpl(channelName, initialState, options) { const syncOnInit = options?.syncOnInit !== false; return createChannelStateImpl(channelName, (api) => { const store = createStore(initialState); const postSyncMessage = (receiver) => { api.postMessage({ action: "sync", receiver, state: store.getState() }); }; const unsubscribe = store.subscribe(() => { postSyncMessage(); }); return { setupStore() { return store; }, onMessage(message) { switch (message.action) { case "write": { const { state, replace } = message; store.setState(state, replace); break; } case "requestSync": { postSyncMessage(message.sender); break; } } }, onInit() { if (syncOnInit) postSyncMessage(); }, onClose() { unsubscribe(); } }; }); } function createReplicaChannelStateImpl(channelName, initialState, options) { const syncOnInit = options?.syncOnInit !== false; return createChannelStateImpl(channelName, (api) => { const store = createStore(initialState); return { setupStore() { const setState = (state, replace) => { api.postMessage({ action: "write", state, replace }); }; return { ...store, setState }; }, onMessage(message) { switch (message.action) { case "sync": { const { receiver, state } = message; if (!receiver || receiver === api.id) { store.setState(state, true); } break; } } }, onInit() { if (syncOnInit) api.postMessage({ action: "requestSync" }); } }; }); } function useChannelStateImpl(api) { const state = useSyncExternalStore(api.subscribe, api.getState, api.getInitialState); const setState = (nextStateOrUpdater, replace) => { const nextState = typeof nextStateOrUpdater === "function" ? produce(api.getState(), nextStateOrUpdater) : nextStateOrUpdater; api.setState(nextState, replace); }; useDebugValue(state); return [state, setState]; } function create(isPrimary, channelName, initialState, options) { const createImpl = isPrimary ? createPrimaryChannelStateImpl : createReplicaChannelStateImpl; const api = createImpl(channelName, initialState, options); const useChannelState = () => useChannelStateImpl(api); return Object.assign(useChannelState, api); } create.primary = (channelName, initialState, options) => { return create(true, channelName, initialState, options); }; create.replica = (channelName, initialState, options) => { return create(false, channelName, initialState, options); }; export { create };