| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| const WS_OPEN = 1; |
|
|
| export interface RefocusSocketLike { |
| status?: string; |
| webSocket?: { readyState?: number } | null; |
| lastMessageReceived?: number; |
| } |
|
|
| export interface RefocusProviderLike { |
| synced: boolean; |
| connect: () => Promise<unknown>; |
| forceSync: () => void; |
| on: (event: string, callback: (...args: any[]) => void) => unknown; |
| off: (event: string, callback: (...args: any[]) => void) => unknown; |
| configuration?: { websocketProvider?: RefocusSocketLike }; |
| } |
|
|
| export interface RefocusEditorLike { |
| isEditable: boolean; |
| isDestroyed: boolean; |
| setEditable: (editable: boolean) => unknown; |
| } |
|
|
| export interface RefocusSyncOptions { |
| provider: RefocusProviderLike; |
| editor: RefocusEditorLike; |
| |
| graceMs?: number; |
| |
| probeMs?: number; |
| } |
|
|
| export interface RefocusSync { |
| |
| onVisibility: () => void; |
| |
| dispose: () => void; |
| } |
|
|
| export function createRefocusSync({ |
| provider, |
| editor, |
| graceMs = 4000, |
| probeMs = 2000, |
| }: RefocusSyncOptions): RefocusSync { |
| let graceTimer: ReturnType<typeof setTimeout> | null = null; |
| let probeTimer: ReturnType<typeof setTimeout> | null = null; |
|
|
| const clearTimers = () => { |
| if (graceTimer) clearTimeout(graceTimer); |
| if (probeTimer) clearTimeout(probeTimer); |
| graceTimer = null; |
| probeTimer = null; |
| }; |
|
|
| const unlock = () => { |
| clearTimers(); |
| if (!editor.isDestroyed && !editor.isEditable) editor.setEditable(true); |
| }; |
|
|
| const lock = () => { |
| if (!editor.isDestroyed && editor.isEditable) editor.setEditable(false); |
| if (graceTimer) clearTimeout(graceTimer); |
| |
| |
| graceTimer = setTimeout(unlock, graceMs); |
| }; |
|
|
| const resync = () => { |
| provider.connect().catch(() => {}); |
| provider.forceSync(); |
| }; |
|
|
| const socket = () => provider.configuration?.websocketProvider; |
|
|
| |
| const looksDead = () => { |
| const ws = socket(); |
| if (!ws) return !provider.synced; |
| if (ws.status !== "connected") return true; |
| if (ws.webSocket && ws.webSocket.readyState !== WS_OPEN) return true; |
| return false; |
| }; |
|
|
| const onSynced = () => unlock(); |
| provider.on("synced", onSynced); |
|
|
| const onVisibility = () => { |
| const before = socket()?.lastMessageReceived ?? 0; |
| resync(); |
|
|
| |
| if (looksDead() || !provider.synced) { |
| lock(); |
| return; |
| } |
|
|
| |
| |
| |
| |
| if (probeTimer) clearTimeout(probeTimer); |
| probeTimer = setTimeout(() => { |
| const advanced = (socket()?.lastMessageReceived ?? 0) > before; |
| if (!advanced) { |
| resync(); |
| lock(); |
| } |
| }, probeMs); |
| }; |
|
|
| const dispose = () => { |
| provider.off("synced", onSynced); |
| clearTimers(); |
| unlock(); |
| }; |
|
|
| return { onVisibility, dispose }; |
| } |
|
|