import { reactive, ref, toRaw, watch } from 'vue' export type SyncWrite = (state: State, updating?: boolean) => void export interface Sync { enabled?: boolean init: (channelKey: string, onUpdate: (data: Partial) => void, state: State, persist?: boolean) => SyncWrite | undefined } interface BuiltinSync extends Sync { channels: BroadcastChannel[] disable: () => void listener?: (event: StorageEvent) => void } const builtinSync: BuiltinSync = { channels: [], enabled: true, init(channelKey: string, onUpdate: (data: Partial) => void, state: State, persist = false) { let stateChannel: BroadcastChannel if (!__SLIDEV_HAS_SERVER__ && !persist) { stateChannel = new BroadcastChannel(channelKey) stateChannel.addEventListener('message', (event: MessageEvent>) => onUpdate(event.data)) this.channels.push(stateChannel) } else if (!__SLIDEV_HAS_SERVER__ && persist) { this.listener = function (event: StorageEvent) { if (event && event.key === channelKey && event.newValue) onUpdate(JSON.parse(event.newValue) as Partial) } window.addEventListener('storage', this.listener) const serializedState = window.localStorage.getItem(channelKey) if (serializedState) onUpdate(JSON.parse(serializedState) as Partial) } return (state: State, updating = false) => { if (this.enabled) { if (!persist && stateChannel && !updating) stateChannel.postMessage(toRaw(state)) if (persist && !updating) window.localStorage.setItem(channelKey, JSON.stringify(state)) } } }, disable() { this.enabled = false this.channels.forEach(channel => channel.close()) if (this.listener) { window.removeEventListener('storage', this.listener) } }, } const syncInterfaces: Sync[] = reactive([builtinSync]) const channels: Map) => void, persist?: boolean, state: object }> = new Map() const syncWrites = ref[]>>({}) export function disableBuiltinSync() { builtinSync.disable() } export function addSyncMethod(sync: Sync) { syncInterfaces.push(sync) for (const [channelKey, { onUpdate, persist, state }] of channels.entries()) { const write = sync.init(channelKey, onUpdate, state, persist) if (write) { syncWrites.value[channelKey].push(write) } } } export function createSyncState(serverState: State, defaultState: State, persist = false) { const onPatchCallbacks: ((state: State) => void)[] = [] let patching = false let updating = false let patchingTimeout: NodeJS.Timeout let updatingTimeout: NodeJS.Timeout const state = __SLIDEV_HAS_SERVER__ ? reactive(serverState) as State : reactive(defaultState) as State function onPatch(fn: (state: State) => void) { onPatchCallbacks.push(fn) } function patch(key: K, value: State[K]) { if (state[key] === value) return clearTimeout(patchingTimeout) patching = true state[key] = value patchingTimeout = setTimeout(() => patching = false, 0) } function onUpdate(patch: Partial) { if (!patching) { clearTimeout(updatingTimeout) updating = true Object.entries(patch).forEach(([key, value]) => { state[key as keyof State] = value as State[keyof State] }) updatingTimeout = setTimeout(() => updating = false, 0) } } function init(channelKey: string) { channels.set(channelKey, { onUpdate, persist, state }) syncWrites.value[channelKey] = syncInterfaces .map(sync => sync.init(channelKey, onUpdate, state, persist)) .filter((x): x is SyncWrite => Boolean(x)) function onStateChanged() { syncWrites.value[channelKey].forEach(write => write?.(toRaw(state), updating)) if (!patching) onPatchCallbacks.forEach((fn: (state: State) => void) => fn(state)) } watch(state, onStateChanged, { deep: true }) } return { init, onPatch, onUpdate, patch, state } }