@tldraw/sync-core 4.2.2 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +58 -483
- package/dist-cjs/index.js +3 -13
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/TLSocketRoom.js +69 -117
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +0 -7
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +688 -357
- package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
- package/dist-cjs/lib/chunk.js +2 -2
- package/dist-cjs/lib/chunk.js.map +1 -1
- package/dist-esm/index.d.mts +58 -483
- package/dist-esm/index.mjs +5 -20
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/TLSocketRoom.mjs +70 -121
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +0 -7
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +702 -370
- package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
- package/dist-esm/lib/chunk.mjs +2 -2
- package/dist-esm/lib/chunk.mjs.map +1 -1
- package/package.json +11 -12
- package/src/index.ts +3 -32
- package/src/lib/ClientWebSocketAdapter.test.ts +0 -3
- package/src/lib/RoomSession.test.ts +0 -1
- package/src/lib/RoomSession.ts +0 -2
- package/src/lib/TLSocketRoom.ts +114 -228
- package/src/lib/TLSyncClient.ts +0 -12
- package/src/lib/TLSyncRoom.ts +913 -473
- package/src/lib/chunk.ts +2 -2
- package/src/test/FuzzEditor.ts +5 -4
- package/src/test/TLSocketRoom.test.ts +49 -255
- package/src/test/TLSyncRoom.test.ts +534 -1024
- package/src/test/TestServer.ts +1 -12
- package/src/test/customMessages.test.ts +1 -1
- package/src/test/presenceMode.test.ts +6 -6
- package/src/test/pruneTombstones.test.ts +178 -0
- package/src/test/syncFuzz.test.ts +4 -2
- package/src/test/upgradeDowngrade.test.ts +8 -290
- package/src/test/validation.test.ts +10 -15
- package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js +0 -55
- package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js.map +0 -7
- package/dist-cjs/lib/InMemorySyncStorage.js +0 -287
- package/dist-cjs/lib/InMemorySyncStorage.js.map +0 -7
- package/dist-cjs/lib/MicrotaskNotifier.js +0 -50
- package/dist-cjs/lib/MicrotaskNotifier.js.map +0 -7
- package/dist-cjs/lib/NodeSqliteWrapper.js +0 -48
- package/dist-cjs/lib/NodeSqliteWrapper.js.map +0 -7
- package/dist-cjs/lib/SQLiteSyncStorage.js +0 -428
- package/dist-cjs/lib/SQLiteSyncStorage.js.map +0 -7
- package/dist-cjs/lib/TLSyncStorage.js +0 -76
- package/dist-cjs/lib/TLSyncStorage.js.map +0 -7
- package/dist-cjs/lib/recordDiff.js +0 -52
- package/dist-cjs/lib/recordDiff.js.map +0 -7
- package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs +0 -35
- package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs.map +0 -7
- package/dist-esm/lib/InMemorySyncStorage.mjs +0 -272
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +0 -7
- package/dist-esm/lib/MicrotaskNotifier.mjs +0 -30
- package/dist-esm/lib/MicrotaskNotifier.mjs.map +0 -7
- package/dist-esm/lib/NodeSqliteWrapper.mjs +0 -28
- package/dist-esm/lib/NodeSqliteWrapper.mjs.map +0 -7
- package/dist-esm/lib/SQLiteSyncStorage.mjs +0 -414
- package/dist-esm/lib/SQLiteSyncStorage.mjs.map +0 -7
- package/dist-esm/lib/TLSyncStorage.mjs +0 -56
- package/dist-esm/lib/TLSyncStorage.mjs.map +0 -7
- package/dist-esm/lib/recordDiff.mjs +0 -32
- package/dist-esm/lib/recordDiff.mjs.map +0 -7
- package/src/lib/DurableObjectSqliteSyncWrapper.ts +0 -95
- package/src/lib/InMemorySyncStorage.ts +0 -387
- package/src/lib/MicrotaskNotifier.test.ts +0 -429
- package/src/lib/MicrotaskNotifier.ts +0 -38
- package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
- package/src/lib/NodeSqliteSyncWrapper.test.ts +0 -272
- package/src/lib/NodeSqliteWrapper.ts +0 -99
- package/src/lib/SQLiteSyncStorage.ts +0 -627
- package/src/lib/TLSyncStorage.ts +0 -216
- package/src/lib/computeTombstonePruning.test.ts +0 -352
- package/src/lib/recordDiff.ts +0 -73
- package/src/test/InMemorySyncStorage.test.ts +0 -1684
- package/src/test/SQLiteSyncStorage.test.ts +0 -1378
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { MicrotaskNotifier } from './MicrotaskNotifier'
|
|
3
|
-
|
|
4
|
-
// Helper to flush all pending microtasks
|
|
5
|
-
async function flushMicrotasks() {
|
|
6
|
-
await Promise.resolve()
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
describe('MicrotaskNotifier', () => {
|
|
10
|
-
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
consoleErrorSpy.mockRestore()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
describe('basic functionality', () => {
|
|
21
|
-
it('calls registered listeners when notified', async () => {
|
|
22
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
23
|
-
const listener = vi.fn()
|
|
24
|
-
|
|
25
|
-
notifier.register(listener)
|
|
26
|
-
await flushMicrotasks() // Wait for registration
|
|
27
|
-
|
|
28
|
-
notifier.notify('hello')
|
|
29
|
-
await flushMicrotasks() // Wait for notification
|
|
30
|
-
|
|
31
|
-
expect(listener).toHaveBeenCalledWith('hello')
|
|
32
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('calls multiple listeners', async () => {
|
|
36
|
-
const notifier = new MicrotaskNotifier<[number]>()
|
|
37
|
-
const listener1 = vi.fn()
|
|
38
|
-
const listener2 = vi.fn()
|
|
39
|
-
const listener3 = vi.fn()
|
|
40
|
-
|
|
41
|
-
notifier.register(listener1)
|
|
42
|
-
notifier.register(listener2)
|
|
43
|
-
notifier.register(listener3)
|
|
44
|
-
await flushMicrotasks()
|
|
45
|
-
|
|
46
|
-
notifier.notify(42)
|
|
47
|
-
await flushMicrotasks()
|
|
48
|
-
|
|
49
|
-
expect(listener1).toHaveBeenCalledWith(42)
|
|
50
|
-
expect(listener2).toHaveBeenCalledWith(42)
|
|
51
|
-
expect(listener3).toHaveBeenCalledWith(42)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('supports multiple arguments', async () => {
|
|
55
|
-
const notifier = new MicrotaskNotifier<[string, number, boolean]>()
|
|
56
|
-
const listener = vi.fn()
|
|
57
|
-
|
|
58
|
-
notifier.register(listener)
|
|
59
|
-
await flushMicrotasks()
|
|
60
|
-
|
|
61
|
-
notifier.notify('test', 123, true)
|
|
62
|
-
await flushMicrotasks()
|
|
63
|
-
|
|
64
|
-
expect(listener).toHaveBeenCalledWith('test', 123, true)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('supports object arguments', async () => {
|
|
68
|
-
const notifier = new MicrotaskNotifier<[{ id: string; value: number }]>()
|
|
69
|
-
const listener = vi.fn()
|
|
70
|
-
|
|
71
|
-
notifier.register(listener)
|
|
72
|
-
await flushMicrotasks()
|
|
73
|
-
|
|
74
|
-
const obj = { id: 'abc', value: 100 }
|
|
75
|
-
notifier.notify(obj)
|
|
76
|
-
await flushMicrotasks()
|
|
77
|
-
|
|
78
|
-
expect(listener).toHaveBeenCalledWith(obj)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('unsubscribe removes the listener', async () => {
|
|
82
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
83
|
-
const listener = vi.fn()
|
|
84
|
-
|
|
85
|
-
const unsubscribe = notifier.register(listener)
|
|
86
|
-
await flushMicrotasks()
|
|
87
|
-
|
|
88
|
-
unsubscribe()
|
|
89
|
-
|
|
90
|
-
notifier.notify('should not receive')
|
|
91
|
-
await flushMicrotasks()
|
|
92
|
-
|
|
93
|
-
expect(listener).not.toHaveBeenCalled()
|
|
94
|
-
})
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
describe('microtask deferral', () => {
|
|
98
|
-
it('does not call listeners synchronously', () => {
|
|
99
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
100
|
-
const listener = vi.fn()
|
|
101
|
-
|
|
102
|
-
notifier.register(listener)
|
|
103
|
-
notifier.notify('hello')
|
|
104
|
-
|
|
105
|
-
// Listener should not be called yet (registration is deferred)
|
|
106
|
-
expect(listener).not.toHaveBeenCalled()
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('defers registration to microtask queue', async () => {
|
|
110
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
111
|
-
const listener = vi.fn()
|
|
112
|
-
|
|
113
|
-
notifier.register(listener)
|
|
114
|
-
|
|
115
|
-
// Notify immediately - since both register and notify queue microtasks,
|
|
116
|
-
// and microtasks execute in FIFO order, the add runs first, then notify
|
|
117
|
-
notifier.notify('same sync block')
|
|
118
|
-
|
|
119
|
-
await flushMicrotasks()
|
|
120
|
-
|
|
121
|
-
// Listener DOES receive it because microtasks are FIFO ordered:
|
|
122
|
-
// 1. add listener
|
|
123
|
-
// 2. notify
|
|
124
|
-
expect(listener).toHaveBeenCalledWith('same sync block')
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('does not receive notifications that completed before registration was queued', async () => {
|
|
128
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
129
|
-
const listener = vi.fn()
|
|
130
|
-
|
|
131
|
-
// Notify first
|
|
132
|
-
notifier.notify('before registration')
|
|
133
|
-
await flushMicrotasks() // Notification completes
|
|
134
|
-
|
|
135
|
-
// Register after
|
|
136
|
-
notifier.register(listener)
|
|
137
|
-
await flushMicrotasks()
|
|
138
|
-
|
|
139
|
-
// Listener should NOT have been called for the earlier notification
|
|
140
|
-
expect(listener).not.toHaveBeenCalled()
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('notifications after registration completes are received', async () => {
|
|
144
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
145
|
-
const listener = vi.fn()
|
|
146
|
-
|
|
147
|
-
notifier.register(listener)
|
|
148
|
-
await flushMicrotasks() // Wait for registration
|
|
149
|
-
|
|
150
|
-
notifier.notify('after registration')
|
|
151
|
-
await flushMicrotasks()
|
|
152
|
-
|
|
153
|
-
expect(listener).toHaveBeenCalledWith('after registration')
|
|
154
|
-
})
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
describe('race conditions and edge cases', () => {
|
|
158
|
-
it('unsubscribing before add microtask runs prevents registration', async () => {
|
|
159
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
160
|
-
const listener = vi.fn()
|
|
161
|
-
|
|
162
|
-
const unsubscribe = notifier.register(listener)
|
|
163
|
-
unsubscribe() // Unsubscribe immediately, before add microtask runs
|
|
164
|
-
|
|
165
|
-
await flushMicrotasks()
|
|
166
|
-
|
|
167
|
-
notifier.notify('should not receive')
|
|
168
|
-
await flushMicrotasks()
|
|
169
|
-
|
|
170
|
-
expect(listener).not.toHaveBeenCalled()
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('double unsubscribe is safe', async () => {
|
|
174
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
175
|
-
const listener = vi.fn()
|
|
176
|
-
|
|
177
|
-
const unsubscribe = notifier.register(listener)
|
|
178
|
-
await flushMicrotasks()
|
|
179
|
-
|
|
180
|
-
// Call unsubscribe multiple times
|
|
181
|
-
unsubscribe()
|
|
182
|
-
unsubscribe()
|
|
183
|
-
unsubscribe()
|
|
184
|
-
|
|
185
|
-
notifier.notify('test')
|
|
186
|
-
await flushMicrotasks()
|
|
187
|
-
|
|
188
|
-
expect(listener).not.toHaveBeenCalled()
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it('unsubscribing one listener does not affect others', async () => {
|
|
192
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
193
|
-
const listener1 = vi.fn()
|
|
194
|
-
const listener2 = vi.fn()
|
|
195
|
-
const listener3 = vi.fn()
|
|
196
|
-
|
|
197
|
-
const unsub1 = notifier.register(listener1)
|
|
198
|
-
notifier.register(listener2)
|
|
199
|
-
notifier.register(listener3)
|
|
200
|
-
await flushMicrotasks()
|
|
201
|
-
|
|
202
|
-
unsub1()
|
|
203
|
-
|
|
204
|
-
notifier.notify('test')
|
|
205
|
-
await flushMicrotasks()
|
|
206
|
-
|
|
207
|
-
expect(listener1).not.toHaveBeenCalled()
|
|
208
|
-
expect(listener2).toHaveBeenCalledWith('test')
|
|
209
|
-
expect(listener3).toHaveBeenCalledWith('test')
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
it('error in one listener does not prevent others from being called', async () => {
|
|
213
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
214
|
-
const errorListener = vi.fn(() => {
|
|
215
|
-
throw new Error('listener error')
|
|
216
|
-
})
|
|
217
|
-
const normalListener = vi.fn()
|
|
218
|
-
|
|
219
|
-
notifier.register(errorListener)
|
|
220
|
-
notifier.register(normalListener)
|
|
221
|
-
await flushMicrotasks()
|
|
222
|
-
|
|
223
|
-
notifier.notify('test')
|
|
224
|
-
await flushMicrotasks()
|
|
225
|
-
|
|
226
|
-
expect(errorListener).toHaveBeenCalled()
|
|
227
|
-
expect(normalListener).toHaveBeenCalled()
|
|
228
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
229
|
-
'Error in MicrotaskNotifier listener',
|
|
230
|
-
expect.any(Error)
|
|
231
|
-
)
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
it('multiple rapid notifications are all delivered', async () => {
|
|
235
|
-
const notifier = new MicrotaskNotifier<[number]>()
|
|
236
|
-
const listener = vi.fn()
|
|
237
|
-
|
|
238
|
-
notifier.register(listener)
|
|
239
|
-
await flushMicrotasks()
|
|
240
|
-
|
|
241
|
-
notifier.notify(1)
|
|
242
|
-
notifier.notify(2)
|
|
243
|
-
notifier.notify(3)
|
|
244
|
-
await flushMicrotasks()
|
|
245
|
-
|
|
246
|
-
expect(listener).toHaveBeenCalledTimes(3)
|
|
247
|
-
expect(listener).toHaveBeenNthCalledWith(1, 1)
|
|
248
|
-
expect(listener).toHaveBeenNthCalledWith(2, 2)
|
|
249
|
-
expect(listener).toHaveBeenNthCalledWith(3, 3)
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
it('registering the same function twice creates independent subscriptions', async () => {
|
|
253
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
254
|
-
const listener = vi.fn()
|
|
255
|
-
|
|
256
|
-
const unsub1 = notifier.register(listener)
|
|
257
|
-
const _unsub2 = notifier.register(listener)
|
|
258
|
-
await flushMicrotasks()
|
|
259
|
-
|
|
260
|
-
notifier.notify('test')
|
|
261
|
-
await flushMicrotasks()
|
|
262
|
-
|
|
263
|
-
// Same function registered twice, but Set deduplicates
|
|
264
|
-
// So it should only be called once
|
|
265
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
266
|
-
|
|
267
|
-
// Unsubscribing the first should remove it
|
|
268
|
-
unsub1()
|
|
269
|
-
notifier.notify('test2')
|
|
270
|
-
await flushMicrotasks()
|
|
271
|
-
|
|
272
|
-
// The second registration is the same function, already removed by unsub1
|
|
273
|
-
// Actually, since it's the same function reference, unsub1 removed it
|
|
274
|
-
// and unsub2 would try to remove it again (which is a no-op)
|
|
275
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
it('unsubscribing during notification iteration is safe', async () => {
|
|
279
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
280
|
-
const results: string[] = []
|
|
281
|
-
let unsub2: (() => void) | undefined = undefined
|
|
282
|
-
|
|
283
|
-
const listener1 = vi.fn(() => {
|
|
284
|
-
results.push('listener1')
|
|
285
|
-
// Unsubscribe listener2 while iteration is in progress
|
|
286
|
-
unsub2?.()
|
|
287
|
-
})
|
|
288
|
-
const listener2 = vi.fn(() => {
|
|
289
|
-
results.push('listener2')
|
|
290
|
-
})
|
|
291
|
-
const listener3 = vi.fn(() => {
|
|
292
|
-
results.push('listener3')
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
notifier.register(listener1)
|
|
296
|
-
unsub2 = notifier.register(listener2)
|
|
297
|
-
notifier.register(listener3)
|
|
298
|
-
await flushMicrotasks()
|
|
299
|
-
|
|
300
|
-
notifier.notify('test')
|
|
301
|
-
await flushMicrotasks()
|
|
302
|
-
|
|
303
|
-
// All listeners should have been called because Set iteration
|
|
304
|
-
// continues over the original snapshot
|
|
305
|
-
expect(listener1).toHaveBeenCalled()
|
|
306
|
-
// listener2 might or might not be called depending on iteration order
|
|
307
|
-
// But listener3 should definitely be called
|
|
308
|
-
expect(listener3).toHaveBeenCalled()
|
|
309
|
-
|
|
310
|
-
// After the notification, listener2 should be unsubscribed
|
|
311
|
-
listener1.mockClear()
|
|
312
|
-
listener2.mockClear()
|
|
313
|
-
listener3.mockClear()
|
|
314
|
-
|
|
315
|
-
notifier.notify('test2')
|
|
316
|
-
await flushMicrotasks()
|
|
317
|
-
|
|
318
|
-
expect(listener1).toHaveBeenCalled()
|
|
319
|
-
expect(listener2).not.toHaveBeenCalled()
|
|
320
|
-
expect(listener3).toHaveBeenCalled()
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
it('registering during notification is deferred', async () => {
|
|
324
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
325
|
-
const lateListener = vi.fn()
|
|
326
|
-
const results: string[] = []
|
|
327
|
-
|
|
328
|
-
const listener1 = vi.fn(() => {
|
|
329
|
-
results.push('listener1')
|
|
330
|
-
// Register a new listener during notification
|
|
331
|
-
notifier.register(lateListener)
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
notifier.register(listener1)
|
|
335
|
-
await flushMicrotasks()
|
|
336
|
-
|
|
337
|
-
notifier.notify('first')
|
|
338
|
-
await flushMicrotasks()
|
|
339
|
-
|
|
340
|
-
// lateListener was registered during notification but deferred
|
|
341
|
-
expect(lateListener).not.toHaveBeenCalled()
|
|
342
|
-
|
|
343
|
-
// Now the late listener should be registered
|
|
344
|
-
await flushMicrotasks()
|
|
345
|
-
|
|
346
|
-
notifier.notify('second')
|
|
347
|
-
await flushMicrotasks()
|
|
348
|
-
|
|
349
|
-
expect(lateListener).toHaveBeenCalledWith('second')
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
it('handles interleaved register/notify/unsubscribe', async () => {
|
|
353
|
-
const notifier = new MicrotaskNotifier<[number]>()
|
|
354
|
-
const listener = vi.fn()
|
|
355
|
-
|
|
356
|
-
// Register
|
|
357
|
-
const unsub = notifier.register(listener)
|
|
358
|
-
|
|
359
|
-
// Notify in the same sync block - listener will receive this
|
|
360
|
-
// because microtasks are FIFO (add first, then notify)
|
|
361
|
-
notifier.notify(1)
|
|
362
|
-
|
|
363
|
-
await flushMicrotasks()
|
|
364
|
-
|
|
365
|
-
expect(listener).toHaveBeenCalledWith(1)
|
|
366
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
367
|
-
|
|
368
|
-
// Notify again after registration
|
|
369
|
-
notifier.notify(2)
|
|
370
|
-
await flushMicrotasks()
|
|
371
|
-
|
|
372
|
-
expect(listener).toHaveBeenCalledWith(2)
|
|
373
|
-
expect(listener).toHaveBeenCalledTimes(2)
|
|
374
|
-
|
|
375
|
-
// Unsubscribe
|
|
376
|
-
unsub()
|
|
377
|
-
|
|
378
|
-
// Notify after unsubscribe
|
|
379
|
-
notifier.notify(3)
|
|
380
|
-
await flushMicrotasks()
|
|
381
|
-
|
|
382
|
-
expect(listener).toHaveBeenCalledTimes(2) // No new calls after unsubscribe
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
it('notifications are processed in order', async () => {
|
|
386
|
-
const notifier = new MicrotaskNotifier<[number]>()
|
|
387
|
-
const calls: number[] = []
|
|
388
|
-
const listener = vi.fn((n: number) => calls.push(n))
|
|
389
|
-
|
|
390
|
-
notifier.register(listener)
|
|
391
|
-
await flushMicrotasks()
|
|
392
|
-
|
|
393
|
-
notifier.notify(1)
|
|
394
|
-
notifier.notify(2)
|
|
395
|
-
notifier.notify(3)
|
|
396
|
-
await flushMicrotasks()
|
|
397
|
-
|
|
398
|
-
expect(calls).toEqual([1, 2, 3])
|
|
399
|
-
})
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
describe('empty notifier', () => {
|
|
403
|
-
it('notify with no listeners does not throw', async () => {
|
|
404
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
405
|
-
|
|
406
|
-
expect(() => {
|
|
407
|
-
notifier.notify('test')
|
|
408
|
-
}).not.toThrow()
|
|
409
|
-
|
|
410
|
-
await flushMicrotasks()
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
it('notify after all listeners unsubscribed does not throw', async () => {
|
|
414
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
415
|
-
const listener = vi.fn()
|
|
416
|
-
|
|
417
|
-
const unsub = notifier.register(listener)
|
|
418
|
-
await flushMicrotasks()
|
|
419
|
-
|
|
420
|
-
unsub()
|
|
421
|
-
|
|
422
|
-
expect(() => {
|
|
423
|
-
notifier.notify('test')
|
|
424
|
-
}).not.toThrow()
|
|
425
|
-
|
|
426
|
-
await flushMicrotasks()
|
|
427
|
-
})
|
|
428
|
-
})
|
|
429
|
-
})
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A notifier that queues its notifications to the microtask queue.
|
|
3
|
-
* This is useful for avoiding race conditions where callbacks are triggered prematurely.
|
|
4
|
-
*/
|
|
5
|
-
export class MicrotaskNotifier<T extends unknown[]> {
|
|
6
|
-
private listeners = new Set<(...props: T) => void>()
|
|
7
|
-
|
|
8
|
-
notify(...props: T) {
|
|
9
|
-
queueMicrotask(() => {
|
|
10
|
-
for (const listener of this.listeners) {
|
|
11
|
-
try {
|
|
12
|
-
listener(...props)
|
|
13
|
-
} catch (error) {
|
|
14
|
-
console.error('Error in MicrotaskNotifier listener', error)
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
register(_listener: (...props: T) => void) {
|
|
21
|
-
// Track if unsubscribe was called before the add microtask ran
|
|
22
|
-
let didDelete = false
|
|
23
|
-
|
|
24
|
-
// We defer the add to the microtask queue to ensure the callback isn't invoked
|
|
25
|
-
// for changes that happened before this registration
|
|
26
|
-
queueMicrotask(() => {
|
|
27
|
-
if (didDelete) return
|
|
28
|
-
this.listeners.add(_listener)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
return () => {
|
|
32
|
-
if (didDelete) return
|
|
33
|
-
didDelete = true
|
|
34
|
-
// Synchronous delete ensures immediate unsubscription
|
|
35
|
-
this.listeners.delete(_listener)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|