返回 slidev
syncState.ts
根目录 / packages / client / state / syncState.ts
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
123 lines TYPESCRIPT