import { Mutex } from "async-mutex"; import { LeaderNode, ChannelEvents } from "@local-logic/channel"; /** * Allows for state management outside of a React application */ export class StateManager { leaderNode: LeaderNode; defaultState: T; onMessageHandlers: ((event: CustomEvent) => void)[] = []; isConnected: Promise; private clientLock: Mutex; constructor( defaultState: T, channelId?: string, crossOrigin?: { targetOrigin: string; window: Window; location?: Window["location"]; }, ) { this.leaderNode = new LeaderNode( channelId ?? "ll-state-manager", crossOrigin, ); const location = crossOrigin?.location ?? (typeof window !== "undefined" && window.location ? window.location : undefined); this.leaderNode.connect({ ...defaultState, location: { hash: location?.hash, host: location?.host, hostname: location?.hostname, href: location?.href, origin: location?.origin, pathname: location?.pathname, port: location?.port, protocol: location?.protocol, search: location?.search, }, }); this.defaultState = defaultState; this.isConnected = this.leaderNode.connection; this.clientLock = new Mutex(); } public async setState(callback: (prev: T) => T) { await this.leaderNode.connection; /** * We need a Mutex in order to synchronize this process. Without this, we * can get race conditions and the state may not be set correctly. * * https://www.npmjs.com/package/async-mutex */ const release = await this.clientLock.acquire(); const prevState = await this.getState(); const state = callback(prevState as T); this.leaderNode.message(ChannelEvents.SET_STATE, state); release(); } public async getState() { await this.leaderNode.connection; const response = await this.leaderNode.eventRequest( ChannelEvents.GET_STATE, ); return response.data; } public async onChange(callback: C) { await this.leaderNode.connection; this.leaderNode.onMessage((e) => { if (e.name === ChannelEvents.ON_CHANGE) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore callback(e.data); } }); } public destroy() { this.leaderNode.disconnect(); } }