channelstate
Version:
A lightweight cross-context state management library built on the BroadcastChannel API.
134 lines (127 loc) • 3.9 kB
JavaScript
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 };