| 1 | /** Read-side recovery only: callers retain their mounted UI and unsent drafts. */ |
| 2 | export interface SnapshotRecoveryPorts<Scope, Snapshot> { |
| 3 | subscribe(callback: () => void): () => void; |
| 4 | capture(): Scope; |
| 5 | isCurrent(scope: Scope): boolean; |
| 6 | read(scope: Scope): Promise<Snapshot>; |
| 7 | apply(snapshot: Snapshot, scope: Scope): void; |
| 8 | failed?(error: unknown): void; |
| 9 | timer(callback: () => void, delay: number): unknown; |
| 10 | clearTimer(timer: unknown): void; |
| 11 | } |
| 12 | |
| 13 | export function startDesktopEventRecovery<Scope, Snapshot>(ports: SnapshotRecoveryPorts<Scope, Snapshot>): () => void { |
| 14 | let disposed = false, inFlight = false, requested = 0, failures = 0; |
| 15 | let timer: unknown; |
| 16 | const repair = async () => { |
| 17 | if (disposed || inFlight) return; |
| 18 | ports.clearTimer(timer); |
| 19 | timer = undefined; |
| 20 | inFlight = true; |
| 21 | const version = requested; |
| 22 | const scope = ports.capture(); |
| 23 | let retry = false; |
| 24 | try { |
| 25 | const snapshot = await ports.read(scope); |
| 26 | if (disposed || version !== requested) return; |
| 27 | if (!ports.isCurrent(scope)) { retry = true; return; } |
| 28 | ports.apply(snapshot, scope); |
| 29 | failures = 0; |
| 30 | } catch (error) { |
| 31 | if (!disposed && version === requested) { failures++; retry = true; ports.failed?.(error); } |
| 32 | } finally { |
| 33 | inFlight = false; |
| 34 | if (!disposed && version !== requested) void repair(); |
| 35 | else if (!disposed && retry) timer = ports.timer(() => { void repair(); }, Math.min(30000, 1000 * 2 ** Math.min(failures, 5))); |
| 36 | } |
| 37 | }; |
| 38 | const off = ports.subscribe(() => { requested++; void repair(); }); |
| 39 | return () => { disposed = true; off(); ports.clearTimer(timer); }; |
| 40 | } |
| 41 |