| 1 | import { reactive, ref, toRaw, watch } from 'vue' |
| 2 | |
| 3 | export type SyncWrite<State extends object> = (state: State, updating?: boolean) => void |
| 4 | |
| 5 | export interface Sync { |
| 6 | enabled?: boolean |
| 7 | init: <State extends object>(channelKey: string, onUpdate: (data: Partial<State>) => void, state: State, persist?: boolean) => SyncWrite<State> | undefined |
| 8 | } |
| 9 | |
| 10 | interface BuiltinSync extends Sync { |
| 11 | channels: BroadcastChannel[] |
| 12 | disable: () => void |
| 13 | listener?: (event: StorageEvent) => void |
| 14 | } |
| 15 | |
| 16 | const builtinSync: BuiltinSync = { |
| 17 | channels: [], |
| 18 | enabled: true, |
| 19 | init<State extends object>(channelKey: string, onUpdate: (data: Partial<State>) => void, state: State, persist = false) { |
| 20 | let stateChannel: BroadcastChannel |
| 21 | if (!__SLIDEV_HAS_SERVER__ && !persist) { |
| 22 | stateChannel = new BroadcastChannel(channelKey) |
| 23 | stateChannel.addEventListener('message', (event: MessageEvent<Partial<State>>) => onUpdate(event.data)) |
| 24 | this.channels.push(stateChannel) |
| 25 | } |
| 26 | else if (!__SLIDEV_HAS_SERVER__ && persist) { |
| 27 | this.listener = function (event: StorageEvent) { |
| 28 | if (event && event.key === channelKey && event.newValue) |
| 29 | onUpdate(JSON.parse(event.newValue) as Partial<State>) |
| 30 | } |
| 31 | window.addEventListener('storage', this.listener) |
| 32 | const serializedState = window.localStorage.getItem(channelKey) |
| 33 | if (serializedState) |
| 34 | onUpdate(JSON.parse(serializedState) as Partial<State>) |
| 35 | } |
| 36 | return (state: State, updating = false) => { |
| 37 | if (this.enabled) { |
| 38 | if (!persist && stateChannel && !updating) |
| 39 | stateChannel.postMessage(toRaw(state)) |
| 40 | if (persist && !updating) |
| 41 | window.localStorage.setItem(channelKey, JSON.stringify(state)) |
| 42 | } |
| 43 | } |
| 44 | }, |
| 45 | disable() { |
| 46 | this.enabled = false |
| 47 | this.channels.forEach(channel => channel.close()) |
| 48 | if (this.listener) { |
| 49 | window.removeEventListener('storage', this.listener) |
| 50 | } |
| 51 | }, |
| 52 | } |
| 53 | const syncInterfaces: Sync[] = reactive([builtinSync]) |
| 54 | const channels: Map<string, { onUpdate: (data: Partial<object>) => void, persist?: boolean, state: object }> = new Map() |
| 55 | const syncWrites = ref<Record<string, SyncWrite<object>[]>>({}) |
| 56 | |
| 57 | export function disableBuiltinSync() { |
| 58 | builtinSync.disable() |
| 59 | } |
| 60 | |
| 61 | export function addSyncMethod(sync: Sync) { |
| 62 | syncInterfaces.push(sync) |
| 63 | for (const [channelKey, { onUpdate, persist, state }] of channels.entries()) { |
| 64 | const write = sync.init(channelKey, onUpdate, state, persist) |
| 65 | if (write) { |
| 66 | syncWrites.value[channelKey].push(write) |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | export function createSyncState<State extends object>(serverState: State, defaultState: State, persist = false) { |
| 72 | const onPatchCallbacks: ((state: State) => void)[] = [] |
| 73 | let patching = false |
| 74 | let updating = false |
| 75 | let patchingTimeout: NodeJS.Timeout |
| 76 | let updatingTimeout: NodeJS.Timeout |
| 77 | |
| 78 | const state = __SLIDEV_HAS_SERVER__ |
| 79 | ? reactive<State>(serverState) as State |
| 80 | : reactive<State>(defaultState) as State |
| 81 | |
| 82 | function onPatch(fn: (state: State) => void) { |
| 83 | onPatchCallbacks.push(fn) |
| 84 | } |
| 85 | |
| 86 | function patch<K extends keyof State>(key: K, value: State[K]) { |
| 87 | if (state[key] === value) |
| 88 | return |
| 89 | clearTimeout(patchingTimeout) |
| 90 | patching = true |
| 91 | state[key] = value |
| 92 | patchingTimeout = setTimeout(() => patching = false, 0) |
| 93 | } |
| 94 | |
| 95 | function onUpdate(patch: Partial<State>) { |
| 96 | if (!patching) { |
| 97 | clearTimeout(updatingTimeout) |
| 98 | updating = true |
| 99 | Object.entries(patch).forEach(([key, value]) => { |
| 100 | state[key as keyof State] = value as State[keyof State] |
| 101 | }) |
| 102 | updatingTimeout = setTimeout(() => updating = false, 0) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | function init(channelKey: string) { |
| 107 | channels.set(channelKey, { onUpdate, persist, state }) |
| 108 | syncWrites.value[channelKey] = syncInterfaces |
| 109 | .map(sync => sync.init<State>(channelKey, onUpdate, state, persist)) |
| 110 | .filter((x): x is SyncWrite<object> => Boolean(x)) |
| 111 | |
| 112 | function onStateChanged() { |
| 113 | syncWrites.value[channelKey].forEach(write => write?.(toRaw(state), updating)) |
| 114 | if (!patching) |
| 115 | onPatchCallbacks.forEach((fn: (state: State) => void) => fn(state)) |
| 116 | } |
| 117 | |
| 118 | watch(state, onStateChanged, { deep: true }) |
| 119 | } |
| 120 | |
| 121 | return { init, onPatch, onUpdate, patch, state } |
| 122 | } |
| 123 |