@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.
- package/DOCS.md +662 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/TLSocketRoom.js +4 -1
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +2 -3
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +3 -2
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/TLSocketRoom.mjs +4 -1
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +2 -4
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +3 -2
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/package.json +9 -8
- package/src/lib/ClientWebSocketAdapter.test.ts +486 -508
- package/src/lib/DurableObjectSqliteSyncWrapper.test.ts +102 -0
- package/src/lib/InMemorySyncStorage.test.ts +294 -0
- package/src/lib/MicrotaskNotifier.test.ts +79 -254
- package/src/lib/{NodeSqliteSyncWrapper.test.ts → NodeSqliteWrapper.test.ts} +29 -25
- package/src/lib/SQLiteSyncStorage.test.ts +239 -0
- package/src/lib/ServerSocketAdapter.test.ts +60 -199
- package/src/lib/TLSocketRoom.ts +6 -1
- package/src/lib/TLSyncClient.test.ts +1127 -846
- package/src/lib/TLSyncClient.ts +4 -4
- package/src/lib/TLSyncRoom.ts +5 -2
- package/src/lib/TLSyncStorage.test.ts +225 -0
- package/src/lib/chunk.test.ts +372 -0
- package/src/lib/diff.test.ts +885 -0
- package/src/lib/interval.test.ts +43 -0
- package/src/lib/protocol.test.ts +43 -0
- package/src/lib/recordDiff.test.ts +159 -0
- package/src/test/TLSocketRoom.test.ts +1109 -894
- package/src/test/TLSyncRoom.test.ts +1735 -893
- package/src/test/storageContractSuite.ts +618 -0
- package/src/test/upgradeDowngrade.test.ts +137 -37
- package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
- package/src/lib/RoomSession.test.ts +0 -101
- package/src/lib/computeTombstonePruning.test.ts +0 -352
- package/src/lib/server-types.test.ts +0 -44
- package/src/test/InMemorySyncStorage.test.ts +0 -1780
- package/src/test/SQLiteSyncStorage.test.ts +0 -1485
- package/src/test/chunk.test.ts +0 -385
- package/src/test/customMessages.test.ts +0 -36
- package/src/test/diff.test.ts +0 -784
- package/src/test/presenceMode.test.ts +0 -149
- package/src/test/validation.test.ts +0 -186
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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('
|
|
21
|
-
it('
|
|
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).
|
|
32
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
28
|
+
expect(listener).not.toHaveBeenCalled()
|
|
33
29
|
})
|
|
34
30
|
|
|
35
|
-
it('
|
|
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(
|
|
43
|
+
expect(listener1).toHaveBeenCalledExactlyOnceWith('test', 123, true)
|
|
44
|
+
expect(listener2).toHaveBeenCalledExactlyOnceWith('test', 123, true)
|
|
65
45
|
})
|
|
66
46
|
|
|
67
|
-
it('
|
|
68
|
-
const notifier = new MicrotaskNotifier<[
|
|
69
|
-
const
|
|
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
|
-
|
|
75
|
-
notifier.notify(
|
|
55
|
+
notifier.notify(1)
|
|
56
|
+
notifier.notify(2)
|
|
57
|
+
notifier.notify(3)
|
|
76
58
|
await flushMicrotasks()
|
|
77
59
|
|
|
78
|
-
expect(
|
|
60
|
+
expect(calls).toEqual([1, 2, 3])
|
|
79
61
|
})
|
|
80
62
|
|
|
81
|
-
it('
|
|
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('
|
|
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('
|
|
98
|
-
it('
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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('
|
|
97
|
+
it('[MN2] registering during a notification defers the new listener to later notifications', async () => {
|
|
128
98
|
const notifier = new MicrotaskNotifier<[string]>()
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await flushMicrotasks() // Notification completes
|
|
99
|
+
const lateListener = vi.fn()
|
|
100
|
+
const listener1 = vi.fn(() => {
|
|
101
|
+
notifier.register(lateListener)
|
|
102
|
+
})
|
|
134
103
|
|
|
135
|
-
|
|
136
|
-
notifier.register(listener)
|
|
104
|
+
notifier.register(listener1)
|
|
137
105
|
await flushMicrotasks()
|
|
138
106
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})
|
|
107
|
+
notifier.notify('first')
|
|
108
|
+
await flushMicrotasks()
|
|
142
109
|
|
|
143
|
-
|
|
144
|
-
const notifier = new MicrotaskNotifier<[string]>()
|
|
145
|
-
const listener = vi.fn()
|
|
110
|
+
expect(lateListener).not.toHaveBeenCalled()
|
|
146
111
|
|
|
147
|
-
|
|
148
|
-
await flushMicrotasks() // Wait for registration
|
|
112
|
+
await flushMicrotasks() // late registration completes
|
|
149
113
|
|
|
150
|
-
notifier.notify('
|
|
114
|
+
notifier.notify('second')
|
|
151
115
|
await flushMicrotasks()
|
|
152
116
|
|
|
153
|
-
expect(
|
|
117
|
+
expect(lateListener).toHaveBeenCalledExactlyOnceWith('second')
|
|
154
118
|
})
|
|
155
119
|
})
|
|
156
120
|
|
|
157
|
-
describe('
|
|
158
|
-
it('
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
145
|
+
unsubscribe() // before the add microtask runs
|
|
179
146
|
|
|
180
|
-
|
|
181
|
-
unsubscribe()
|
|
182
|
-
unsubscribe()
|
|
183
|
-
unsubscribe()
|
|
147
|
+
await flushMicrotasks()
|
|
184
148
|
|
|
185
|
-
notifier.notify('
|
|
149
|
+
notifier.notify('should not receive')
|
|
186
150
|
await flushMicrotasks()
|
|
187
151
|
|
|
188
152
|
expect(listener).not.toHaveBeenCalled()
|
|
189
153
|
})
|
|
190
154
|
|
|
191
|
-
it('
|
|
155
|
+
it('[MN3] double unsubscribe is safe', async () => {
|
|
192
156
|
const notifier = new MicrotaskNotifier<[string]>()
|
|
193
|
-
const
|
|
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.
|
|
159
|
+
const unsubscribe = notifier.register(listener)
|
|
205
160
|
await flushMicrotasks()
|
|
206
161
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
264
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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('
|
|
403
|
-
it('
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
243
|
+
notifier.notify('test')
|
|
418
244
|
await flushMicrotasks()
|
|
419
245
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
expect((
|
|
423
|
-
|
|
424
|
-
|
|
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('
|
|
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('
|
|
260
|
+
it('[NW2] rolls back and rethrows the original error', () => {
|
|
261
261
|
const customError = new Error('custom error')
|
|
262
262
|
|
|
263
|
-
|
|
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
|
-
}
|
|
268
|
-
|
|
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
|
})
|