@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.
Files changed (84) hide show
  1. package/dist-cjs/index.d.ts +58 -483
  2. package/dist-cjs/index.js +3 -13
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/RoomSession.js.map +1 -1
  5. package/dist-cjs/lib/TLSocketRoom.js +69 -117
  6. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  7. package/dist-cjs/lib/TLSyncClient.js +0 -7
  8. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  9. package/dist-cjs/lib/TLSyncRoom.js +688 -357
  10. package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
  11. package/dist-cjs/lib/chunk.js +2 -2
  12. package/dist-cjs/lib/chunk.js.map +1 -1
  13. package/dist-esm/index.d.mts +58 -483
  14. package/dist-esm/index.mjs +5 -20
  15. package/dist-esm/index.mjs.map +2 -2
  16. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  17. package/dist-esm/lib/TLSocketRoom.mjs +70 -121
  18. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  19. package/dist-esm/lib/TLSyncClient.mjs +0 -7
  20. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  21. package/dist-esm/lib/TLSyncRoom.mjs +702 -370
  22. package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
  23. package/dist-esm/lib/chunk.mjs +2 -2
  24. package/dist-esm/lib/chunk.mjs.map +1 -1
  25. package/package.json +11 -12
  26. package/src/index.ts +3 -32
  27. package/src/lib/ClientWebSocketAdapter.test.ts +0 -3
  28. package/src/lib/RoomSession.test.ts +0 -1
  29. package/src/lib/RoomSession.ts +0 -2
  30. package/src/lib/TLSocketRoom.ts +114 -228
  31. package/src/lib/TLSyncClient.ts +0 -12
  32. package/src/lib/TLSyncRoom.ts +913 -473
  33. package/src/lib/chunk.ts +2 -2
  34. package/src/test/FuzzEditor.ts +5 -4
  35. package/src/test/TLSocketRoom.test.ts +49 -255
  36. package/src/test/TLSyncRoom.test.ts +534 -1024
  37. package/src/test/TestServer.ts +1 -12
  38. package/src/test/customMessages.test.ts +1 -1
  39. package/src/test/presenceMode.test.ts +6 -6
  40. package/src/test/pruneTombstones.test.ts +178 -0
  41. package/src/test/syncFuzz.test.ts +4 -2
  42. package/src/test/upgradeDowngrade.test.ts +8 -290
  43. package/src/test/validation.test.ts +10 -15
  44. package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js +0 -55
  45. package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js.map +0 -7
  46. package/dist-cjs/lib/InMemorySyncStorage.js +0 -287
  47. package/dist-cjs/lib/InMemorySyncStorage.js.map +0 -7
  48. package/dist-cjs/lib/MicrotaskNotifier.js +0 -50
  49. package/dist-cjs/lib/MicrotaskNotifier.js.map +0 -7
  50. package/dist-cjs/lib/NodeSqliteWrapper.js +0 -48
  51. package/dist-cjs/lib/NodeSqliteWrapper.js.map +0 -7
  52. package/dist-cjs/lib/SQLiteSyncStorage.js +0 -428
  53. package/dist-cjs/lib/SQLiteSyncStorage.js.map +0 -7
  54. package/dist-cjs/lib/TLSyncStorage.js +0 -76
  55. package/dist-cjs/lib/TLSyncStorage.js.map +0 -7
  56. package/dist-cjs/lib/recordDiff.js +0 -52
  57. package/dist-cjs/lib/recordDiff.js.map +0 -7
  58. package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs +0 -35
  59. package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs.map +0 -7
  60. package/dist-esm/lib/InMemorySyncStorage.mjs +0 -272
  61. package/dist-esm/lib/InMemorySyncStorage.mjs.map +0 -7
  62. package/dist-esm/lib/MicrotaskNotifier.mjs +0 -30
  63. package/dist-esm/lib/MicrotaskNotifier.mjs.map +0 -7
  64. package/dist-esm/lib/NodeSqliteWrapper.mjs +0 -28
  65. package/dist-esm/lib/NodeSqliteWrapper.mjs.map +0 -7
  66. package/dist-esm/lib/SQLiteSyncStorage.mjs +0 -414
  67. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +0 -7
  68. package/dist-esm/lib/TLSyncStorage.mjs +0 -56
  69. package/dist-esm/lib/TLSyncStorage.mjs.map +0 -7
  70. package/dist-esm/lib/recordDiff.mjs +0 -32
  71. package/dist-esm/lib/recordDiff.mjs.map +0 -7
  72. package/src/lib/DurableObjectSqliteSyncWrapper.ts +0 -95
  73. package/src/lib/InMemorySyncStorage.ts +0 -387
  74. package/src/lib/MicrotaskNotifier.test.ts +0 -429
  75. package/src/lib/MicrotaskNotifier.ts +0 -38
  76. package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
  77. package/src/lib/NodeSqliteSyncWrapper.test.ts +0 -272
  78. package/src/lib/NodeSqliteWrapper.ts +0 -99
  79. package/src/lib/SQLiteSyncStorage.ts +0 -627
  80. package/src/lib/TLSyncStorage.ts +0 -216
  81. package/src/lib/computeTombstonePruning.test.ts +0 -352
  82. package/src/lib/recordDiff.ts +0 -73
  83. package/src/test/InMemorySyncStorage.test.ts +0 -1684
  84. 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
- }