@tldraw/state 5.1.0 → 5.2.0-canary.019da1aa690a
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 +563 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/Computed.js +1 -1
- package/dist-cjs/lib/Computed.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/Computed.mjs +1 -1
- package/dist-esm/lib/Computed.mjs.map +2 -2
- package/package.json +8 -4
- package/src/lib/Computed.ts +1 -1
- package/src/lib/__tests__/{arraySet.test.ts → ArraySet.test.ts} +83 -0
- package/src/lib/__tests__/EffectScheduler.test.ts +355 -34
- package/src/lib/__tests__/HistoryBuffer.test.ts +19 -2
- package/src/lib/__tests__/atom.test.ts +132 -128
- package/src/lib/__tests__/capture.test.ts +203 -84
- package/src/lib/__tests__/computed.test.ts +163 -438
- package/src/lib/__tests__/debug.test.ts +84 -0
- package/src/lib/__tests__/deferAsyncEffects.test.ts +232 -0
- package/src/lib/__tests__/errors.test.ts +75 -47
- package/src/lib/__tests__/fuzz.tlstate.test.ts +1 -1
- package/src/lib/__tests__/guards.test.ts +49 -0
- package/src/lib/__tests__/helpers.test.ts +46 -58
- package/src/lib/__tests__/history.test.ts +524 -0
- package/src/lib/__tests__/localStorageAtom.test.ts +91 -11
- package/src/lib/__tests__/propagation.test.ts +279 -0
- package/src/lib/__tests__/transactions.test.ts +49 -435
- package/src/lib/__tests__/reactor.test.ts +0 -197
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { promiseWithResolve, sleep } from '@tldraw/utils'
|
|
2
|
-
import { vi } from 'vitest'
|
|
3
1
|
import { atom } from '../Atom'
|
|
4
2
|
import { computed } from '../Computed'
|
|
5
3
|
import { react } from '../EffectScheduler'
|
|
6
|
-
import {
|
|
4
|
+
import { getGlobalEpoch, transact, transaction } from '../transactions'
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
// Tests for SPEC.md §11 (transactions), plus rule EP6.
|
|
7
|
+
// Rule IDs like [T4] in test names refer to that document.
|
|
8
|
+
|
|
9
|
+
describe('transactions (T)', () => {
|
|
10
|
+
it('[T2][T3][T4][T6] batch changes, defer effects, and can be rolled back', () => {
|
|
10
11
|
const firstName = atom('', 'John')
|
|
11
12
|
const lastName = atom('', 'Doe')
|
|
12
13
|
|
|
@@ -32,11 +33,13 @@ describe('transactions', () => {
|
|
|
32
33
|
firstName.set('Wilbur')
|
|
33
34
|
expect(numTimesComputed).toBe(1)
|
|
34
35
|
expect(numTimesReacted).toBe(1)
|
|
36
|
+
// [T6] effects never observe intermediate in-transaction values
|
|
35
37
|
expect(name).toBe('John Doe')
|
|
36
38
|
lastName.set('Jones')
|
|
37
39
|
expect(numTimesComputed).toBe(1)
|
|
38
40
|
expect(numTimesReacted).toBe(1)
|
|
39
41
|
expect(name).toBe('John Doe')
|
|
42
|
+
// [T2] reads inside the transaction see the latest values
|
|
40
43
|
expect(fullName.get()).toBe('Wilbur Jones')
|
|
41
44
|
|
|
42
45
|
expect(numTimesComputed).toBe(2)
|
|
@@ -46,7 +49,7 @@ describe('transactions', () => {
|
|
|
46
49
|
rollback()
|
|
47
50
|
})
|
|
48
51
|
|
|
49
|
-
//
|
|
52
|
+
// [T6] the aborted transaction still flushes effects, which observe the restored values
|
|
50
53
|
expect(numTimesComputed).toBe(3)
|
|
51
54
|
expect(numTimesReacted).toBe(2)
|
|
52
55
|
|
|
@@ -54,7 +57,26 @@ describe('transactions', () => {
|
|
|
54
57
|
expect(name).toBe('John Doe')
|
|
55
58
|
})
|
|
56
59
|
|
|
57
|
-
it('
|
|
60
|
+
it('[T1] returns the value of the function, even when rolled back', () => {
|
|
61
|
+
expect(transaction(() => 'hello')).toBe('hello')
|
|
62
|
+
expect(transact(() => 42)).toBe(42)
|
|
63
|
+
expect(
|
|
64
|
+
transaction((rollback) => {
|
|
65
|
+
rollback()
|
|
66
|
+
return 'rolled back'
|
|
67
|
+
})
|
|
68
|
+
).toBe('rolled back')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('[EP6] advances the global epoch when aborted', () => {
|
|
72
|
+
const startEpoch = getGlobalEpoch()
|
|
73
|
+
transaction((rollback) => {
|
|
74
|
+
rollback()
|
|
75
|
+
})
|
|
76
|
+
expect(getGlobalEpoch()).toBeGreaterThan(startEpoch)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('[T7] nested transactions roll back independently', () => {
|
|
58
80
|
const atomA = atom('', 0)
|
|
59
81
|
const atomB = atom('', 0)
|
|
60
82
|
|
|
@@ -148,7 +170,7 @@ describe('transactions', () => {
|
|
|
148
170
|
expect(atomB.get()).toBe(-2)
|
|
149
171
|
})
|
|
150
172
|
|
|
151
|
-
it('
|
|
173
|
+
it('[T7] an outer rollback undoes a committed inner transaction', () => {
|
|
152
174
|
const a = atom('', 'a')
|
|
153
175
|
|
|
154
176
|
transaction((rollback) => {
|
|
@@ -160,10 +182,26 @@ describe('transactions', () => {
|
|
|
160
182
|
|
|
161
183
|
expect(a.get()).toBe('a')
|
|
162
184
|
})
|
|
185
|
+
|
|
186
|
+
it('[T4] rollback restores computed signals too', () => {
|
|
187
|
+
const firstName = atom('', 'John')
|
|
188
|
+
const lastName = atom('', 'Doe')
|
|
189
|
+
|
|
190
|
+
const fullName = computed('', () => `${firstName.get()} ${lastName.get()}`)
|
|
191
|
+
|
|
192
|
+
transaction((rollback) => {
|
|
193
|
+
firstName.set('Jane')
|
|
194
|
+
lastName.set('Jones')
|
|
195
|
+
expect(fullName.get()).toBe('Jane Jones')
|
|
196
|
+
rollback()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
expect(fullName.get()).toBe('John Doe')
|
|
200
|
+
})
|
|
163
201
|
})
|
|
164
202
|
|
|
165
|
-
describe('transact', () => {
|
|
166
|
-
it('
|
|
203
|
+
describe('transact (T)', () => {
|
|
204
|
+
it('[T5] aborts and rethrows if the function throws', () => {
|
|
167
205
|
const a = atom('', 'a')
|
|
168
206
|
|
|
169
207
|
try {
|
|
@@ -180,7 +218,7 @@ describe('transact', () => {
|
|
|
180
218
|
expect.assertions(2)
|
|
181
219
|
})
|
|
182
220
|
|
|
183
|
-
it('
|
|
221
|
+
it('[T1][T8] joins the current transaction instead of nesting, so an inner throw restores nothing', () => {
|
|
184
222
|
const a = atom('', 'a')
|
|
185
223
|
|
|
186
224
|
transact(() => {
|
|
@@ -202,427 +240,3 @@ describe('transact', () => {
|
|
|
202
240
|
expect.assertions(3)
|
|
203
241
|
})
|
|
204
242
|
})
|
|
205
|
-
|
|
206
|
-
describe('setting atoms during a reaction', () => {
|
|
207
|
-
it('should work', () => {
|
|
208
|
-
const a = atom('', 0)
|
|
209
|
-
const b = atom('', 0)
|
|
210
|
-
|
|
211
|
-
react('', () => {
|
|
212
|
-
b.set(a.get() + 1)
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
expect(a.get()).toBe(0)
|
|
216
|
-
expect(b.get()).toBe(1)
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('should throw an error if it gets into a loop', () => {
|
|
220
|
-
expect(() => {
|
|
221
|
-
const a = atom('', 0)
|
|
222
|
-
|
|
223
|
-
react('', () => {
|
|
224
|
-
a.set(a.get() + 1)
|
|
225
|
-
})
|
|
226
|
-
}).toThrowErrorMatchingInlineSnapshot(`[Error: Reaction update depth limit exceeded]`)
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('should work with a transaction running', () => {
|
|
230
|
-
const a = atom('', 0)
|
|
231
|
-
|
|
232
|
-
react('', () => {
|
|
233
|
-
transact(() => {
|
|
234
|
-
if (a.get() < 10) {
|
|
235
|
-
a.set(a.get() + 1)
|
|
236
|
-
}
|
|
237
|
-
})
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
expect(a.get()).toBe(10)
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
it('[regression 1] should allow computeds to be updated properly', () => {
|
|
244
|
-
const a = atom('', 0)
|
|
245
|
-
const b = atom('', 0)
|
|
246
|
-
const c = computed('', () => b.get() * 2)
|
|
247
|
-
|
|
248
|
-
let cValue = 0
|
|
249
|
-
|
|
250
|
-
react('', () => {
|
|
251
|
-
b.set(a.get() + 1)
|
|
252
|
-
cValue = c.get()
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
expect(a.get()).toBe(0)
|
|
256
|
-
expect(b.get()).toBe(1)
|
|
257
|
-
expect(cValue).toBe(2)
|
|
258
|
-
|
|
259
|
-
transact(() => {
|
|
260
|
-
a.set(1)
|
|
261
|
-
})
|
|
262
|
-
expect(cValue).toBe(4)
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
it('[regression 2] should allow computeds to be updated properly', () => {
|
|
266
|
-
const a = atom('', 0)
|
|
267
|
-
const b = atom('', 1)
|
|
268
|
-
const c = atom('', 0)
|
|
269
|
-
const d = computed('', () => a.get() * 2)
|
|
270
|
-
|
|
271
|
-
let dValue = 0
|
|
272
|
-
react('', () => {
|
|
273
|
-
// update a, causes a and d to be traversed (but not updated)
|
|
274
|
-
a.set(b.get())
|
|
275
|
-
// update c
|
|
276
|
-
c.set(a.get())
|
|
277
|
-
// make sure that when we get d, it is updated properly
|
|
278
|
-
dValue = d.get()
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
expect(a.get()).toBe(1)
|
|
282
|
-
expect(b.get()).toBe(1)
|
|
283
|
-
expect(c.get()).toBe(1)
|
|
284
|
-
|
|
285
|
-
expect(dValue).toBe(2)
|
|
286
|
-
|
|
287
|
-
transact(() => {
|
|
288
|
-
b.set(2)
|
|
289
|
-
})
|
|
290
|
-
expect(dValue).toBe(4)
|
|
291
|
-
})
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
test('it should be possible to run a transaction during a reaction', () => {
|
|
295
|
-
const a = atom('', 0)
|
|
296
|
-
const b = atom('', 0)
|
|
297
|
-
|
|
298
|
-
react('', () => {
|
|
299
|
-
transaction(() => {
|
|
300
|
-
b.set(a.get() + 1)
|
|
301
|
-
})
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
expect(a.get()).toBe(0)
|
|
305
|
-
expect(b.get()).toBe(1)
|
|
306
|
-
|
|
307
|
-
a.set(1)
|
|
308
|
-
|
|
309
|
-
expect(b.get()).toBe(2)
|
|
310
|
-
|
|
311
|
-
transaction(() => {
|
|
312
|
-
a.set(2)
|
|
313
|
-
expect(b.get()).toBe(2)
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
expect(b.get()).toBe(3)
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
test('it should be possible to abort a transaction during a reaction', () => {
|
|
320
|
-
const a = atom('', 0)
|
|
321
|
-
const b = atom('', 0)
|
|
322
|
-
|
|
323
|
-
const unsub = react('', () => {
|
|
324
|
-
transaction((rollback) => {
|
|
325
|
-
b.set(a.get() + 1)
|
|
326
|
-
rollback()
|
|
327
|
-
})
|
|
328
|
-
expect(b.get()).toBe(0)
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
expect(a.get()).toBe(0)
|
|
332
|
-
expect(b.get()).toBe(0)
|
|
333
|
-
|
|
334
|
-
unsub()
|
|
335
|
-
|
|
336
|
-
react('', () => {
|
|
337
|
-
transaction(() => {
|
|
338
|
-
b.set(3)
|
|
339
|
-
try {
|
|
340
|
-
transaction(() => {
|
|
341
|
-
b.set(a.get() + 1)
|
|
342
|
-
throw new Error('oops')
|
|
343
|
-
})
|
|
344
|
-
} catch (e: any) {
|
|
345
|
-
expect(e.message).toBe('oops')
|
|
346
|
-
} finally {
|
|
347
|
-
expect(b.get()).toBe(3)
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
expect(b.get()).toBe(3)
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
expect(a.get()).toBe(0)
|
|
354
|
-
expect(b.get()).toBe(3)
|
|
355
|
-
|
|
356
|
-
expect.assertions(8)
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
it('should defer all side effects until the end of the outer transaction', () => {
|
|
360
|
-
const a = atom('', 0)
|
|
361
|
-
const b = atom('', 0)
|
|
362
|
-
const c = atom('', 0)
|
|
363
|
-
|
|
364
|
-
const aChanged = vi.fn()
|
|
365
|
-
const bChanged = vi.fn()
|
|
366
|
-
const cChanged = vi.fn()
|
|
367
|
-
|
|
368
|
-
react('', () => {
|
|
369
|
-
a.get()
|
|
370
|
-
aChanged()
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
react('', () => {
|
|
374
|
-
transaction(() => {
|
|
375
|
-
a.set(b.get() + 1)
|
|
376
|
-
})
|
|
377
|
-
bChanged()
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
react('', () => {
|
|
381
|
-
transaction(() => {
|
|
382
|
-
b.set(c.get() + 1)
|
|
383
|
-
})
|
|
384
|
-
cChanged()
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
expect(aChanged).toHaveBeenCalledTimes(3)
|
|
388
|
-
expect(bChanged).toHaveBeenCalledTimes(2)
|
|
389
|
-
expect(cChanged).toHaveBeenCalledTimes(1)
|
|
390
|
-
|
|
391
|
-
expect(a.__unsafe__getWithoutCapture()).toBe(2)
|
|
392
|
-
|
|
393
|
-
cChanged.mockImplementationOnce(() => {
|
|
394
|
-
// b was .set() during c's reaction
|
|
395
|
-
expect(b.__unsafe__getWithoutCapture()).toBe(2)
|
|
396
|
-
// a was not yet set because the effect was deferred
|
|
397
|
-
// util the end of the reaction
|
|
398
|
-
expect(a.__unsafe__getWithoutCapture()).toBe(2)
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
c.set(1)
|
|
402
|
-
|
|
403
|
-
expect(a.__unsafe__getWithoutCapture()).toBe(3)
|
|
404
|
-
expect(cChanged).toHaveBeenCalledTimes(2)
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
describe('asyncTransaction', () => {
|
|
408
|
-
it('works if kicked off during a reaction', async () => {
|
|
409
|
-
const a = atom('', 0)
|
|
410
|
-
const b = atom('', 0)
|
|
411
|
-
|
|
412
|
-
let txp: any = null
|
|
413
|
-
|
|
414
|
-
react('', () => {
|
|
415
|
-
a.get()
|
|
416
|
-
txp = deferAsyncEffects(async () => {
|
|
417
|
-
await sleep(1)
|
|
418
|
-
b.set(a.get() + 1)
|
|
419
|
-
})
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
await txp
|
|
423
|
-
|
|
424
|
-
expect(a.get()).toBe(0)
|
|
425
|
-
expect(b.get()).toBe(1)
|
|
426
|
-
|
|
427
|
-
a.set(1)
|
|
428
|
-
|
|
429
|
-
await txp
|
|
430
|
-
|
|
431
|
-
expect(a.get()).toBe(1)
|
|
432
|
-
expect(b.get()).toBe(2)
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
it('throws an error if kicked off during a sync transaction', async () => {
|
|
436
|
-
const a = atom('', 0)
|
|
437
|
-
let txp: any = null
|
|
438
|
-
transact(() => {
|
|
439
|
-
txp = deferAsyncEffects(async () => {
|
|
440
|
-
expect(a.get()).toBe(1)
|
|
441
|
-
a.set(2)
|
|
442
|
-
})
|
|
443
|
-
a.set(1)
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
await expect(txp).rejects.toMatchInlineSnapshot(
|
|
447
|
-
`[Error: deferAsyncEffects cannot be called during a sync transaction]`
|
|
448
|
-
)
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
it('can have nested sync transactions', async () => {
|
|
452
|
-
const a = atom('', 0)
|
|
453
|
-
|
|
454
|
-
await deferAsyncEffects(async () => {
|
|
455
|
-
a.set(1)
|
|
456
|
-
transaction(() => {
|
|
457
|
-
a.set(2)
|
|
458
|
-
})
|
|
459
|
-
expect(a.get()).toBe(2)
|
|
460
|
-
})
|
|
461
|
-
expect(a.get()).toBe(2)
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
it('can have nested async transactions', async () => {
|
|
465
|
-
const a = atom('', 0)
|
|
466
|
-
|
|
467
|
-
await deferAsyncEffects(async () => {
|
|
468
|
-
a.set(1)
|
|
469
|
-
await deferAsyncEffects(async () => {
|
|
470
|
-
a.set(2)
|
|
471
|
-
})
|
|
472
|
-
expect(a.get()).toBe(2)
|
|
473
|
-
})
|
|
474
|
-
expect(a.get()).toBe(2)
|
|
475
|
-
})
|
|
476
|
-
|
|
477
|
-
it('allows transact to be called inside asyncTransaction', async () => {
|
|
478
|
-
const a = atom('', 0)
|
|
479
|
-
|
|
480
|
-
await deferAsyncEffects(async () => {
|
|
481
|
-
a.set(1)
|
|
482
|
-
transact(() => {
|
|
483
|
-
a.set(2)
|
|
484
|
-
})
|
|
485
|
-
expect(a.get()).toBe(2)
|
|
486
|
-
})
|
|
487
|
-
expect(a.get()).toBe(2)
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
it('allows overlapping transactions', async () => {
|
|
491
|
-
const a = atom('', 0)
|
|
492
|
-
|
|
493
|
-
let txp = null
|
|
494
|
-
|
|
495
|
-
const p = deferAsyncEffects(async () => {
|
|
496
|
-
a.set(1)
|
|
497
|
-
const x = promiseWithResolve()
|
|
498
|
-
txp = deferAsyncEffects(async () => {
|
|
499
|
-
a.set(2)
|
|
500
|
-
x.resolve(null)
|
|
501
|
-
await sleep(10)
|
|
502
|
-
a.set(3)
|
|
503
|
-
return 'inner'
|
|
504
|
-
})
|
|
505
|
-
await x
|
|
506
|
-
// inner transactions leak, this can't be avoided without AsyncContext
|
|
507
|
-
// but at least we can group effects.
|
|
508
|
-
expect(a.get()).toBe(2)
|
|
509
|
-
return 'outer'
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
await expect(p).resolves.toBe('outer')
|
|
513
|
-
await expect(txp).resolves.toBe('inner')
|
|
514
|
-
expect(a.get()).toBe(3)
|
|
515
|
-
})
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
describe('async tests generated by claude', () => {
|
|
519
|
-
// Add these tests to your existing asyncTransaction describe block
|
|
520
|
-
|
|
521
|
-
it('should rollback on exception', async () => {
|
|
522
|
-
const a = atom('', 0)
|
|
523
|
-
const b = atom('', 0)
|
|
524
|
-
|
|
525
|
-
await expect(
|
|
526
|
-
deferAsyncEffects(async () => {
|
|
527
|
-
a.set(1)
|
|
528
|
-
b.set(2)
|
|
529
|
-
throw new Error('test error')
|
|
530
|
-
})
|
|
531
|
-
).rejects.toThrow('test error')
|
|
532
|
-
|
|
533
|
-
expect(a.get()).toBe(0)
|
|
534
|
-
expect(b.get()).toBe(0)
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
it('should defer effects until async transaction commits', async () => {
|
|
538
|
-
const a = atom('', 0)
|
|
539
|
-
const b = atom('', 0)
|
|
540
|
-
const effectCalls = vi.fn()
|
|
541
|
-
|
|
542
|
-
react('', () => {
|
|
543
|
-
a.get()
|
|
544
|
-
b.get()
|
|
545
|
-
effectCalls()
|
|
546
|
-
})
|
|
547
|
-
|
|
548
|
-
expect(effectCalls).toHaveBeenCalledTimes(1)
|
|
549
|
-
|
|
550
|
-
const txPromise = deferAsyncEffects(async () => {
|
|
551
|
-
a.set(1)
|
|
552
|
-
expect(effectCalls).toHaveBeenCalledTimes(1) // no effect yet
|
|
553
|
-
await sleep(1)
|
|
554
|
-
b.set(2)
|
|
555
|
-
expect(effectCalls).toHaveBeenCalledTimes(1) // still no effect
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
await txPromise
|
|
559
|
-
expect(effectCalls).toHaveBeenCalledTimes(2) // effect runs after commit
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
it('should handle computed signals properly in async transactions', async () => {
|
|
563
|
-
const a = atom('', 1)
|
|
564
|
-
const doubled = computed('', () => a.get() * 2)
|
|
565
|
-
|
|
566
|
-
expect(doubled.get()).toBe(2)
|
|
567
|
-
|
|
568
|
-
await deferAsyncEffects(async () => {
|
|
569
|
-
a.set(5)
|
|
570
|
-
// computed should update during transaction
|
|
571
|
-
expect(doubled.get()).toBe(10)
|
|
572
|
-
await sleep(1)
|
|
573
|
-
a.set(10)
|
|
574
|
-
expect(doubled.get()).toBe(20)
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
// computed should update after commit
|
|
578
|
-
expect(doubled.get()).toBe(20)
|
|
579
|
-
expect(a.get()).toBe(10)
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
it('should handle multiple concurrent async transactions', async () => {
|
|
583
|
-
const a = atom('', 0)
|
|
584
|
-
const b = atom('', 0)
|
|
585
|
-
const results: number[] = []
|
|
586
|
-
|
|
587
|
-
const tx1 = deferAsyncEffects(async () => {
|
|
588
|
-
a.set(1)
|
|
589
|
-
await sleep(10)
|
|
590
|
-
results.push(a.get())
|
|
591
|
-
return 'tx1'
|
|
592
|
-
})
|
|
593
|
-
|
|
594
|
-
const tx2 = deferAsyncEffects(async () => {
|
|
595
|
-
b.set(2)
|
|
596
|
-
await sleep(5)
|
|
597
|
-
results.push(b.get())
|
|
598
|
-
return 'tx2'
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
const [result1, result2] = await Promise.all([tx1, tx2])
|
|
602
|
-
|
|
603
|
-
expect(result1).toBe('tx1')
|
|
604
|
-
expect(result2).toBe('tx2')
|
|
605
|
-
expect(a.get()).toBe(1)
|
|
606
|
-
expect(b.get()).toBe(2)
|
|
607
|
-
expect(results).toEqual([2, 1])
|
|
608
|
-
})
|
|
609
|
-
|
|
610
|
-
it('should handle exception in nested async transaction', async () => {
|
|
611
|
-
const a = atom('', 0)
|
|
612
|
-
const b = atom('', 0)
|
|
613
|
-
|
|
614
|
-
await expect(
|
|
615
|
-
deferAsyncEffects(async () => {
|
|
616
|
-
a.set(1)
|
|
617
|
-
|
|
618
|
-
await deferAsyncEffects(async () => {
|
|
619
|
-
b.set(2)
|
|
620
|
-
throw new Error('inner error')
|
|
621
|
-
})
|
|
622
|
-
})
|
|
623
|
-
).rejects.toThrow('inner error')
|
|
624
|
-
|
|
625
|
-
expect(a.get()).toBe(0) // all changes should be rolled back
|
|
626
|
-
expect(b.get()).toBe(0)
|
|
627
|
-
})
|
|
628
|
-
})
|