@tldraw/sync-core 5.2.0-canary.4a316fdfb2bb → 5.2.0-canary.4e00299c3e28

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 (49) hide show
  1. package/DOCS.md +662 -0
  2. package/README.md +9 -1
  3. package/dist-cjs/index.js +1 -1
  4. package/dist-cjs/lib/TLSocketRoom.js +4 -1
  5. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  6. package/dist-cjs/lib/TLSyncClient.js +2 -3
  7. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  8. package/dist-cjs/lib/TLSyncRoom.js +3 -2
  9. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  10. package/dist-esm/index.mjs +1 -1
  11. package/dist-esm/lib/TLSocketRoom.mjs +4 -1
  12. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  13. package/dist-esm/lib/TLSyncClient.mjs +2 -4
  14. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  15. package/dist-esm/lib/TLSyncRoom.mjs +3 -2
  16. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  17. package/package.json +9 -8
  18. package/src/lib/ClientWebSocketAdapter.test.ts +486 -508
  19. package/src/lib/DurableObjectSqliteSyncWrapper.test.ts +102 -0
  20. package/src/lib/InMemorySyncStorage.test.ts +294 -0
  21. package/src/lib/MicrotaskNotifier.test.ts +79 -254
  22. package/src/lib/{NodeSqliteSyncWrapper.test.ts → NodeSqliteWrapper.test.ts} +29 -25
  23. package/src/lib/SQLiteSyncStorage.test.ts +239 -0
  24. package/src/lib/ServerSocketAdapter.test.ts +60 -199
  25. package/src/lib/TLSocketRoom.ts +6 -1
  26. package/src/lib/TLSyncClient.test.ts +1127 -846
  27. package/src/lib/TLSyncClient.ts +4 -4
  28. package/src/lib/TLSyncRoom.ts +5 -2
  29. package/src/lib/TLSyncStorage.test.ts +225 -0
  30. package/src/lib/chunk.test.ts +372 -0
  31. package/src/lib/diff.test.ts +885 -0
  32. package/src/lib/interval.test.ts +43 -0
  33. package/src/lib/protocol.test.ts +43 -0
  34. package/src/lib/recordDiff.test.ts +159 -0
  35. package/src/test/TLSocketRoom.test.ts +1109 -894
  36. package/src/test/TLSyncRoom.test.ts +1735 -893
  37. package/src/test/storageContractSuite.ts +618 -0
  38. package/src/test/upgradeDowngrade.test.ts +137 -37
  39. package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
  40. package/src/lib/RoomSession.test.ts +0 -101
  41. package/src/lib/computeTombstonePruning.test.ts +0 -352
  42. package/src/lib/server-types.test.ts +0 -44
  43. package/src/test/InMemorySyncStorage.test.ts +0 -1780
  44. package/src/test/SQLiteSyncStorage.test.ts +0 -1485
  45. package/src/test/chunk.test.ts +0 -385
  46. package/src/test/customMessages.test.ts +0 -36
  47. package/src/test/diff.test.ts +0 -784
  48. package/src/test/presenceMode.test.ts +0 -149
  49. package/src/test/validation.test.ts +0 -186
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1
+ import { vi } from 'vitest'
2
2
  import { MicrotaskNotifier } from './MicrotaskNotifier'
3
3
 
4
4
  // Helper to flush all pending microtasks
@@ -17,239 +17,159 @@ describe('MicrotaskNotifier', () => {
17
17
  consoleErrorSpy.mockRestore()
18
18
  })
19
19
 
20
- describe('basic functionality', () => {
21
- it('calls registered listeners when notified', async () => {
20
+ describe('deferred delivery (MN1)', () => {
21
+ it('[MN1] does not call listeners synchronously', () => {
22
22
  const notifier = new MicrotaskNotifier<[string]>()
23
23
  const listener = vi.fn()
24
24
 
25
25
  notifier.register(listener)
26
- await flushMicrotasks() // Wait for registration
27
-
28
26
  notifier.notify('hello')
29
- await flushMicrotasks() // Wait for notification
30
27
 
31
- expect(listener).toHaveBeenCalledWith('hello')
32
- expect(listener).toHaveBeenCalledTimes(1)
28
+ expect(listener).not.toHaveBeenCalled()
33
29
  })
34
30
 
35
- it('calls multiple listeners', async () => {
36
- const notifier = new MicrotaskNotifier<[number]>()
31
+ it('[MN1] delivers the notification arguments to every listener on a microtask', async () => {
32
+ const notifier = new MicrotaskNotifier<[string, number, boolean]>()
37
33
  const listener1 = vi.fn()
38
34
  const listener2 = vi.fn()
39
- const listener3 = vi.fn()
40
35
 
41
36
  notifier.register(listener1)
42
37
  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
38
  await flushMicrotasks()
60
39
 
61
40
  notifier.notify('test', 123, true)
62
41
  await flushMicrotasks()
63
42
 
64
- expect(listener).toHaveBeenCalledWith('test', 123, true)
43
+ expect(listener1).toHaveBeenCalledExactlyOnceWith('test', 123, true)
44
+ expect(listener2).toHaveBeenCalledExactlyOnceWith('test', 123, true)
65
45
  })
66
46
 
67
- it('supports object arguments', async () => {
68
- const notifier = new MicrotaskNotifier<[{ id: string; value: number }]>()
69
- const listener = vi.fn()
47
+ it('[MN1] delivers multiple notifications in order', async () => {
48
+ const notifier = new MicrotaskNotifier<[number]>()
49
+ const calls: number[] = []
50
+ const listener = vi.fn((n: number) => calls.push(n))
70
51
 
71
52
  notifier.register(listener)
72
53
  await flushMicrotasks()
73
54
 
74
- const obj = { id: 'abc', value: 100 }
75
- notifier.notify(obj)
55
+ notifier.notify(1)
56
+ notifier.notify(2)
57
+ notifier.notify(3)
76
58
  await flushMicrotasks()
77
59
 
78
- expect(listener).toHaveBeenCalledWith(obj)
60
+ expect(calls).toEqual([1, 2, 3])
79
61
  })
80
62
 
81
- it('unsubscribe removes the listener', async () => {
63
+ it('[MN1] notify with no listeners does not throw', async () => {
82
64
  const notifier = new MicrotaskNotifier<[string]>()
83
- const listener = vi.fn()
84
-
85
- const unsubscribe = notifier.register(listener)
86
- await flushMicrotasks()
87
-
88
- unsubscribe()
89
65
 
90
- notifier.notify('should not receive')
66
+ expect(() => notifier.notify('test')).not.toThrow()
91
67
  await flushMicrotasks()
92
-
93
- expect(listener).not.toHaveBeenCalled()
94
68
  })
95
69
  })
96
70
 
97
- describe('microtask deferral', () => {
98
- it('does not call listeners synchronously', () => {
71
+ describe('deferred registration (MN2)', () => {
72
+ it('[MN2] a listener misses notifications issued before register was called', async () => {
99
73
  const notifier = new MicrotaskNotifier<[string]>()
100
74
  const listener = vi.fn()
101
75
 
76
+ notifier.notify('before registration')
102
77
  notifier.register(listener)
103
- notifier.notify('hello')
78
+ await flushMicrotasks()
104
79
 
105
- // Listener should not be called yet (registration is deferred)
106
80
  expect(listener).not.toHaveBeenCalled()
107
81
  })
108
82
 
109
- it('defers registration to microtask queue', async () => {
83
+ it('[MN2] a listener receives notifications issued after register in the same synchronous block', async () => {
110
84
  const notifier = new MicrotaskNotifier<[string]>()
111
85
  const listener = vi.fn()
112
86
 
87
+ // Both register and notify queue microtasks; microtasks run FIFO, so
88
+ // the add runs first and the notification is delivered
113
89
  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
90
  notifier.notify('same sync block')
118
91
 
119
92
  await flushMicrotasks()
120
93
 
121
- // Listener DOES receive it because microtasks are FIFO ordered:
122
- // 1. add listener
123
- // 2. notify
124
- expect(listener).toHaveBeenCalledWith('same sync block')
94
+ expect(listener).toHaveBeenCalledExactlyOnceWith('same sync block')
125
95
  })
126
96
 
127
- it('does not receive notifications that completed before registration was queued', async () => {
97
+ it('[MN2] registering during a notification defers the new listener to later notifications', async () => {
128
98
  const notifier = new MicrotaskNotifier<[string]>()
129
- const listener = vi.fn()
130
-
131
- // Notify first
132
- notifier.notify('before registration')
133
- await flushMicrotasks() // Notification completes
99
+ const lateListener = vi.fn()
100
+ const listener1 = vi.fn(() => {
101
+ notifier.register(lateListener)
102
+ })
134
103
 
135
- // Register after
136
- notifier.register(listener)
104
+ notifier.register(listener1)
137
105
  await flushMicrotasks()
138
106
 
139
- // Listener should NOT have been called for the earlier notification
140
- expect(listener).not.toHaveBeenCalled()
141
- })
107
+ notifier.notify('first')
108
+ await flushMicrotasks()
142
109
 
143
- it('notifications after registration completes are received', async () => {
144
- const notifier = new MicrotaskNotifier<[string]>()
145
- const listener = vi.fn()
110
+ expect(lateListener).not.toHaveBeenCalled()
146
111
 
147
- notifier.register(listener)
148
- await flushMicrotasks() // Wait for registration
112
+ await flushMicrotasks() // late registration completes
149
113
 
150
- notifier.notify('after registration')
114
+ notifier.notify('second')
151
115
  await flushMicrotasks()
152
116
 
153
- expect(listener).toHaveBeenCalledWith('after registration')
117
+ expect(lateListener).toHaveBeenCalledExactlyOnceWith('second')
154
118
  })
155
119
  })
156
120
 
157
- describe('race conditions and edge cases', () => {
158
- it('unsubscribing before add microtask runs prevents registration', async () => {
121
+ describe('unsubscribing (MN3)', () => {
122
+ it('[MN3] unsubscribe is synchronous and stops future notifications', async () => {
159
123
  const notifier = new MicrotaskNotifier<[string]>()
160
124
  const listener = vi.fn()
125
+ const other = vi.fn()
161
126
 
162
127
  const unsubscribe = notifier.register(listener)
163
- unsubscribe() // Unsubscribe immediately, before add microtask runs
164
-
128
+ notifier.register(other)
165
129
  await flushMicrotasks()
166
130
 
131
+ unsubscribe()
167
132
  notifier.notify('should not receive')
168
133
  await flushMicrotasks()
169
134
 
170
135
  expect(listener).not.toHaveBeenCalled()
136
+ // other listeners are unaffected
137
+ expect(other).toHaveBeenCalledExactlyOnceWith('should not receive')
171
138
  })
172
139
 
173
- it('double unsubscribe is safe', async () => {
140
+ it('[MN3] unsubscribing before the add microtask runs prevents the registration entirely', async () => {
174
141
  const notifier = new MicrotaskNotifier<[string]>()
175
142
  const listener = vi.fn()
176
143
 
177
144
  const unsubscribe = notifier.register(listener)
178
- await flushMicrotasks()
145
+ unsubscribe() // before the add microtask runs
179
146
 
180
- // Call unsubscribe multiple times
181
- unsubscribe()
182
- unsubscribe()
183
- unsubscribe()
147
+ await flushMicrotasks()
184
148
 
185
- notifier.notify('test')
149
+ notifier.notify('should not receive')
186
150
  await flushMicrotasks()
187
151
 
188
152
  expect(listener).not.toHaveBeenCalled()
189
153
  })
190
154
 
191
- it('unsubscribing one listener does not affect others', async () => {
155
+ it('[MN3] double unsubscribe is safe', async () => {
192
156
  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()
157
+ const listener = vi.fn()
203
158
 
204
- notifier.notify('test')
159
+ const unsubscribe = notifier.register(listener)
205
160
  await flushMicrotasks()
206
161
 
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()
162
+ unsubscribe()
163
+ unsubscribe()
164
+ unsubscribe()
222
165
 
223
166
  notifier.notify('test')
224
167
  await flushMicrotasks()
225
168
 
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)
169
+ expect(listener).not.toHaveBeenCalled()
250
170
  })
251
171
 
252
- it('registering the same function twice creates independent subscriptions', async () => {
172
+ it('[MN3] registering the same function twice', async () => {
253
173
  const notifier = new MicrotaskNotifier<[string]>()
254
174
  const listener = vi.fn()
255
175
 
@@ -260,37 +180,29 @@ describe('MicrotaskNotifier', () => {
260
180
  notifier.notify('test')
261
181
  await flushMicrotasks()
262
182
 
263
- // Same function registered twice, but Set deduplicates
264
- // So it should only be called once
183
+ // The listener set deduplicates the same function reference, so it is
184
+ // only called once per notification
265
185
  expect(listener).toHaveBeenCalledTimes(1)
266
186
 
267
- // Unsubscribing the first should remove it
187
+ // Unsubscribing via the first registration removes the function for
188
+ // both registrations
268
189
  unsub1()
269
190
  notifier.notify('test2')
270
191
  await flushMicrotasks()
271
192
 
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
193
  expect(listener).toHaveBeenCalledTimes(1)
276
194
  })
277
195
 
278
- it('unsubscribing during notification iteration is safe', async () => {
196
+ it('[MN3] unsubscribing during notification iteration takes effect for later notifications', async () => {
279
197
  const notifier = new MicrotaskNotifier<[string]>()
280
- const results: string[] = []
281
198
  let unsub2: (() => void) | undefined = undefined
282
199
 
283
200
  const listener1 = vi.fn(() => {
284
- results.push('listener1')
285
201
  // Unsubscribe listener2 while iteration is in progress
286
202
  unsub2?.()
287
203
  })
288
- const listener2 = vi.fn(() => {
289
- results.push('listener2')
290
- })
291
- const listener3 = vi.fn(() => {
292
- results.push('listener3')
293
- })
204
+ const listener2 = vi.fn()
205
+ const listener3 = vi.fn()
294
206
 
295
207
  notifier.register(listener1)
296
208
  unsub2 = notifier.register(listener2)
@@ -300,14 +212,9 @@ describe('MicrotaskNotifier', () => {
300
212
  notifier.notify('test')
301
213
  await flushMicrotasks()
302
214
 
303
- // All listeners should have been called because Set iteration
304
- // continues over the original snapshot
305
215
  expect(listener1).toHaveBeenCalled()
306
- // listener2 might or might not be called depending on iteration order
307
- // But listener3 should definitely be called
308
216
  expect(listener3).toHaveBeenCalled()
309
217
 
310
- // After the notification, listener2 should be unsubscribed
311
218
  listener1.mockClear()
312
219
  listener2.mockClear()
313
220
  listener3.mockClear()
@@ -319,111 +226,29 @@ describe('MicrotaskNotifier', () => {
319
226
  expect(listener2).not.toHaveBeenCalled()
320
227
  expect(listener3).toHaveBeenCalled()
321
228
  })
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
229
  })
401
230
 
402
- describe('empty notifier', () => {
403
- it('notify with no listeners does not throw', async () => {
231
+ describe('listener errors (MN4)', () => {
232
+ it('[MN4] a listener that throws is caught and logged, and remaining listeners still run', async () => {
404
233
  const notifier = new MicrotaskNotifier<[string]>()
234
+ const errorListener = vi.fn(() => {
235
+ throw new Error('listener error')
236
+ })
237
+ const normalListener = vi.fn()
405
238
 
406
- expect(() => {
407
- notifier.notify('test')
408
- }).not.toThrow()
409
-
239
+ notifier.register(errorListener)
240
+ notifier.register(normalListener)
410
241
  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
242
 
417
- const unsub = notifier.register(listener)
243
+ notifier.notify('test')
418
244
  await flushMicrotasks()
419
245
 
420
- unsub()
421
-
422
- expect(() => {
423
- notifier.notify('test')
424
- }).not.toThrow()
425
-
426
- await flushMicrotasks()
246
+ expect(errorListener).toHaveBeenCalled()
247
+ expect(normalListener).toHaveBeenCalled()
248
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
249
+ 'Error in MicrotaskNotifier listener',
250
+ expect.any(Error)
251
+ )
427
252
  })
428
253
  })
429
254
  })
@@ -2,7 +2,7 @@ import { DatabaseSync } from 'node:sqlite'
2
2
  import { beforeEach, describe, expect, it } from 'vitest'
3
3
  import { NodeSqliteWrapper } from './NodeSqliteWrapper'
4
4
 
5
- describe('NodeSqliteSyncWrapper', () => {
5
+ describe('NodeSqliteWrapper', () => {
6
6
  let db: DatabaseSync
7
7
  let wrapper: NodeSqliteWrapper
8
8
 
@@ -13,7 +13,7 @@ describe('NodeSqliteSyncWrapper', () => {
13
13
  })
14
14
 
15
15
  describe('exec', () => {
16
- it('executes DDL statements', () => {
16
+ it('[NW1] executes DDL statements', () => {
17
17
  wrapper.exec('CREATE TABLE another (id INTEGER PRIMARY KEY)')
18
18
  // Verify table exists by inserting
19
19
  wrapper.prepare('INSERT INTO another (id) VALUES (?)').run(1)
@@ -21,7 +21,7 @@ describe('NodeSqliteSyncWrapper', () => {
21
21
  expect(results).toEqual([{ id: 1 }])
22
22
  })
23
23
 
24
- it('executes multi-statement DDL', () => {
24
+ it('[NW1] executes multi-statement DDL', () => {
25
25
  wrapper.exec(`
26
26
  CREATE TABLE t1 (id INTEGER PRIMARY KEY);
27
27
  CREATE TABLE t2 (id INTEGER PRIMARY KEY);
@@ -39,7 +39,7 @@ describe('NodeSqliteSyncWrapper', () => {
39
39
 
40
40
  describe('prepare', () => {
41
41
  describe('all()', () => {
42
- it('returns all results as an array', () => {
42
+ it('[NW1] returns all results as an array', () => {
43
43
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
44
44
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(2, 'bob', 200)
45
45
 
@@ -53,7 +53,7 @@ describe('NodeSqliteSyncWrapper', () => {
53
53
  ])
54
54
  })
55
55
 
56
- it('handles queries with bindings', () => {
56
+ it('[NW1] handles queries with bindings', () => {
57
57
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
58
58
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(2, 'bob', 200)
59
59
 
@@ -64,7 +64,7 @@ describe('NodeSqliteSyncWrapper', () => {
64
64
  expect(results).toEqual([{ name: 'bob' }])
65
65
  })
66
66
 
67
- it('returns empty array for DML statements', () => {
67
+ it('[NW1] returns empty array for DML statements', () => {
68
68
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
69
69
 
70
70
  const stmt = wrapper.prepare('UPDATE test SET value = ? WHERE id = ?')
@@ -77,7 +77,7 @@ describe('NodeSqliteSyncWrapper', () => {
77
77
  expect(result).toEqual([{ value: 999 }])
78
78
  })
79
79
 
80
- it('handles INSERT with RETURNING clause', () => {
80
+ it('[NW1] handles INSERT with RETURNING clause', () => {
81
81
  const results = wrapper
82
82
  .prepare<{
83
83
  id: number
@@ -92,7 +92,7 @@ describe('NodeSqliteSyncWrapper', () => {
92
92
  })
93
93
 
94
94
  describe('iterate()', () => {
95
- it('returns results via iteration', () => {
95
+ it('[NW1] returns results via iteration', () => {
96
96
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
97
97
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(2, 'bob', 200)
98
98
 
@@ -110,7 +110,7 @@ describe('NodeSqliteSyncWrapper', () => {
110
110
  ])
111
111
  })
112
112
 
113
- it('handles queries with bindings', () => {
113
+ it('[NW1] handles queries with bindings', () => {
114
114
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
115
115
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(2, 'bob', 200)
116
116
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(3, 'carol', 300)
@@ -127,7 +127,7 @@ describe('NodeSqliteSyncWrapper', () => {
127
127
  })
128
128
 
129
129
  describe('run()', () => {
130
- it('executes DML without returning results', () => {
130
+ it('[NW1] executes DML without returning results', () => {
131
131
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
132
132
 
133
133
  // Verify the insert happened
@@ -135,7 +135,7 @@ describe('NodeSqliteSyncWrapper', () => {
135
135
  expect(results).toEqual([{ id: 1 }])
136
136
  })
137
137
 
138
- it('executes UPDATE', () => {
138
+ it('[NW1] executes UPDATE', () => {
139
139
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
140
140
  wrapper.prepare('UPDATE test SET value = ? WHERE id = ?').run(999, 1)
141
141
 
@@ -145,7 +145,7 @@ describe('NodeSqliteSyncWrapper', () => {
145
145
  expect(results).toEqual([{ value: 999 }])
146
146
  })
147
147
 
148
- it('executes DELETE', () => {
148
+ it('[NW1] executes DELETE', () => {
149
149
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
150
150
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(2, 'bob', 200)
151
151
  wrapper.prepare('DELETE FROM test WHERE id = ?').run(1)
@@ -156,7 +156,7 @@ describe('NodeSqliteSyncWrapper', () => {
156
156
  })
157
157
 
158
158
  describe('prepared statement reuse', () => {
159
- it('can be reused with different bindings', () => {
159
+ it('[NW1] can be reused with different bindings', () => {
160
160
  const insert = wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)')
161
161
  insert.run(1, 'alice', 100)
162
162
  insert.run(2, 'bob', 200)
@@ -166,7 +166,7 @@ describe('NodeSqliteSyncWrapper', () => {
166
166
  expect(results).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }])
167
167
  })
168
168
 
169
- it('iterate can be called multiple times', () => {
169
+ it('[NW1] iterate can be called multiple times', () => {
170
170
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
171
171
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(2, 'bob', 200)
172
172
 
@@ -189,7 +189,7 @@ describe('NodeSqliteSyncWrapper', () => {
189
189
  })
190
190
  })
191
191
 
192
- it('only executes first statement in multi-statement SQL (node:sqlite limitation)', () => {
192
+ it('[NW1] only executes first statement in multi-statement SQL (node:sqlite limitation)', () => {
193
193
  // node:sqlite's prepare() silently ignores statements after the first semicolon
194
194
  wrapper.prepare('INSERT INTO test (id) VALUES (1); INSERT INTO test (id) VALUES (2)').run()
195
195
 
@@ -198,7 +198,7 @@ describe('NodeSqliteSyncWrapper', () => {
198
198
  expect(results).toEqual([{ id: 1 }])
199
199
  })
200
200
 
201
- it('handles string with semicolon in value (not multi-statement)', () => {
201
+ it('[NW1] handles string with semicolon in value (not multi-statement)', () => {
202
202
  wrapper
203
203
  .prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)')
204
204
  .run(1, 'hello; world', 100)
@@ -209,7 +209,7 @@ describe('NodeSqliteSyncWrapper', () => {
209
209
  })
210
210
 
211
211
  describe('transaction', () => {
212
- it('commits on success', () => {
212
+ it('[NW2] commits on success', () => {
213
213
  wrapper.transaction(() => {
214
214
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
215
215
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(2, 'bob', 200)
@@ -219,7 +219,7 @@ describe('NodeSqliteSyncWrapper', () => {
219
219
  expect(results).toEqual([{ id: 1 }, { id: 2 }])
220
220
  })
221
221
 
222
- it('rolls back on error', () => {
222
+ it('[NW2] rolls back on error', () => {
223
223
  expect(() => {
224
224
  wrapper.transaction(() => {
225
225
  wrapper
@@ -233,7 +233,7 @@ describe('NodeSqliteSyncWrapper', () => {
233
233
  expect(results).toEqual([])
234
234
  })
235
235
 
236
- it('returns the callback result', () => {
236
+ it('[NW2] returns the callback result', () => {
237
237
  const result = wrapper.transaction(() => {
238
238
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
239
239
  return 'done'
@@ -242,7 +242,7 @@ describe('NodeSqliteSyncWrapper', () => {
242
242
  expect(result).toBe('done')
243
243
  })
244
244
 
245
- it('supports nested reads within transaction', () => {
245
+ it('[NW2] supports nested reads within transaction', () => {
246
246
  wrapper.prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)').run(1, 'alice', 100)
247
247
 
248
248
  const result = wrapper.transaction(() => {
@@ -257,16 +257,20 @@ describe('NodeSqliteSyncWrapper', () => {
257
257
  expect(result).toBe(150)
258
258
  })
259
259
 
260
- it('re-throws the original error after rollback', () => {
260
+ it('[NW2] rolls back and rethrows the original error', () => {
261
261
  const customError = new Error('custom error')
262
262
 
263
- try {
263
+ expect(() => {
264
264
  wrapper.transaction(() => {
265
+ wrapper
266
+ .prepare('INSERT INTO test (id, name, value) VALUES (?, ?, ?)')
267
+ .run(1, 'alice', 100)
265
268
  throw customError
266
269
  })
267
- } catch (e) {
268
- expect(e).toBe(customError)
269
- }
270
+ }).toThrow(customError)
271
+
272
+ // the write was rolled back
273
+ expect(wrapper.prepare('SELECT * FROM test').all()).toEqual([])
270
274
  })
271
275
  })
272
276
  })