oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1// Copied verbatim from Docs (oficina/docs/fixtures/resync-controller.test.mjs), the first
2// implementation of the live-sync client; Slides is the second, and the
3// extraction to a shared module is slice 4. Only the header name differs:
4// Docs-Seq is Slides-Seq here.
5// fixtures/resync-controller.test.mjs — regression test for fix
6// round 2's blocking finding: a resync that fails once used to give
7// up forever, silently recreating the 204-but-never-integrated data
8// loss for the rest of the session (see resync-controller.mjs's own
9// header comment for the full mechanism). Runs under Node's own test
10// runner (node --test); no DOM, no ProseMirror, no real Y.Doc —
11// attemptResync/timing/the 'online' listener are all injected.
12import { test } from 'node:test'
13import assert from 'node:assert/strict'
14import { createResyncController } from './resync-controller.mjs'
15
16// A tiny fake clock: setTimeoutFn/clearTimeoutFn below don't run real
17// timers at all — advance(ms) runs every callback whose deadline has
18// now passed, in the order they were scheduled. Deterministic and
19// instant, unlike waiting out a real 30s backoff cap in a test.
20function makeClock() {
21 let now = 0
22 let id = 0
23 const pending = new Map()
24 return {
25 setTimeoutFn: (fn, ms) => {
26 const t = ++id
27 pending.set(t, { fn, at: now + ms })
28 return t
29 },
30 clearTimeoutFn: (t) => pending.delete(t),
31 advance: (ms) => {
32 now += ms
33 for (const [t, { fn, at }] of [...pending]) {
34 if (at <= now) {
35 pending.delete(t)
36 fn()
37 }
38 }
39 },
40 }
41}
42
43test('a resync that fails is retried with backoff until it succeeds, without ever running twice at once', async () => {
44 const clock = makeClock()
45 let onlineHandler = null
46 const calls = []
47 let succeedOnCall = 3
48
49 const attemptResync = () => {
50 calls.push(clock)
51 return calls.length >= succeedOnCall
52 ? Promise.resolve()
53 : Promise.reject(new Error('network down'))
54 }
55
56 const divergedEvents = []
57 const recoveredEvents = []
58 const controller = createResyncController({
59 attemptResync,
60 onDiverged: () => divergedEvents.push(true),
61 onRecovered: () => recoveredEvents.push(true),
62 backoffMs: (attempt) => attempt * 1000, // 1s, 2s, 3s — deterministic and small for the test
63 setTimeoutFn: clock.setTimeoutFn,
64 clearTimeoutFn: clock.clearTimeoutFn,
65 addOnlineListener: (fn) => { onlineHandler = fn },
66 })
67
68 assert.equal(controller.diverged, false)
69 controller.reportDivergence()
70 // The first attempt runs synchronously off reportDivergence itself —
71 // "knowingly diverged from the instant a batch is dropped," not
72 // from whenever the first attempt happens to finish.
73 await Promise.resolve()
74 await Promise.resolve()
75 assert.equal(calls.length, 1, 'the first attempt must run immediately, not wait for a timer')
76 assert.equal(controller.diverged, true, 'still diverged: without retry, this fix round 2 regression stays true forever')
77 assert.deepEqual(divergedEvents, [true])
78
79 // Calling reportDivergence again while already diverged must not
80 // reset the attempt count or fire onDiverged a second time — it is
81 // still the same episode of divergence — but it is free to trigger
82 // another immediate attempt rather than silently waiting out
83 // whatever backoff is already scheduled; the resyncing guard keeps
84 // that from ever running two attempts at once.
85 controller.reportDivergence()
86 await Promise.resolve()
87 await Promise.resolve()
88 assert.equal(calls.length, 2, 'reportDivergence is free to retry eagerly; it must never fire onDiverged twice')
89 assert.deepEqual(divergedEvents, [true])
90 assert.equal(controller.diverged, true, 'still failing (call 2 of 3)')
91
92 // The pending backoff timer from the FIRST failed attempt is still
93 // live; it must not cause a THIRD concurrent/duplicate call on top
94 // of the eager one just above.
95 clock.advance(1000)
96 await Promise.resolve()
97 await Promise.resolve()
98 assert.equal(calls.length, 2, 'a stale timer from a superseded attempt must not double up on the eager retry')
99
100 clock.advance(2000)
101 await Promise.resolve()
102 await Promise.resolve()
103 assert.equal(calls.length, 3, 'the next scheduled retry succeeds')
104 assert.equal(controller.diverged, false, 'recovered')
105 assert.deepEqual(recoveredEvents, [true])
106
107 // A stray, already-scheduled retry firing after recovery must be a
108 // no-op — re-running resync against an already-healthy document
109 // would discard everything typed since recovery for nothing.
110 clock.advance(10000)
111 assert.equal(calls.length, 3, 'no further attempts once recovered')
112})
113
114test("the browser's online event retries immediately, not just on the next backoff tick", async () => {
115 const clock = makeClock()
116 let onlineHandler = null
117 let calls = 0
118 const attemptResync = () => {
119 calls++
120 return calls >= 2 ? Promise.resolve() : Promise.reject(new Error('offline'))
121 }
122
123 const controller = createResyncController({
124 attemptResync,
125 backoffMs: () => 60000, // a long backoff the test never advances to,
126 // so the only thing that can trigger the second attempt here is
127 // the online listener itself.
128 setTimeoutFn: clock.setTimeoutFn,
129 clearTimeoutFn: clock.clearTimeoutFn,
130 addOnlineListener: (fn) => { onlineHandler = fn },
131 })
132
133 controller.reportDivergence()
134 await Promise.resolve()
135 await Promise.resolve()
136 assert.equal(calls, 1)
137 assert.equal(controller.diverged, true)
138
139 assert.equal(typeof onlineHandler, 'function', 'an online listener must be registered')
140 onlineHandler()
141 await Promise.resolve()
142 await Promise.resolve()
143 assert.equal(calls, 2, "the online event must trigger a fresh attempt without waiting for the 60s backoff")
144 assert.equal(controller.diverged, false)
145})
146
147test('reportDivergence before any failure runs the first attempt without a caller ever calling online', async () => {
148 const clock = makeClock()
149 const controller = createResyncController({
150 attemptResync: () => Promise.resolve(),
151 setTimeoutFn: clock.setTimeoutFn,
152 clearTimeoutFn: clock.clearTimeoutFn,
153 addOnlineListener: () => {},
154 })
155 controller.reportDivergence()
156 await Promise.resolve()
157 await Promise.resolve()
158 assert.equal(controller.diverged, false, 'a resync that succeeds on the first try recovers immediately')
159})
160