@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
import { atom } from '../Atom'
|
|
3
|
+
import { whyAmIRunning } from '../capture'
|
|
4
|
+
import { computed } from '../Computed'
|
|
5
|
+
import { react, reactor } from '../EffectScheduler'
|
|
6
|
+
|
|
7
|
+
// Tests for SPEC.md §13 (debugging aids).
|
|
8
|
+
// Rule IDs like [D2] in test names refer to that document.
|
|
9
|
+
|
|
10
|
+
describe('whyAmIRunning (D)', () => {
|
|
11
|
+
it('[D2] throws when called outside of a reactive context', () => {
|
|
12
|
+
expect(() => whyAmIRunning()).toThrow('whyAmIRunning() called outside of a reactive context')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('[D3] logs which ancestor atoms caused an effect to run', () => {
|
|
16
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
17
|
+
|
|
18
|
+
const name = atom('name', 'Bob')
|
|
19
|
+
const stop = react('greeting', () => {
|
|
20
|
+
whyAmIRunning()
|
|
21
|
+
name.get()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(log).not.toHaveBeenCalled()
|
|
25
|
+
|
|
26
|
+
name.set('Alice')
|
|
27
|
+
|
|
28
|
+
expect(log).toHaveBeenCalledTimes(1)
|
|
29
|
+
expect(log.mock.calls[0][0]).toContain('Effect(greeting) is executing because:')
|
|
30
|
+
expect(log.mock.calls[0][0]).toContain('Atom(name) changed')
|
|
31
|
+
|
|
32
|
+
stop()
|
|
33
|
+
log.mockRestore()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('[D3] logs which ancestors caused a computed to recompute, including nested computeds', () => {
|
|
37
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
38
|
+
|
|
39
|
+
const a = atom('a', 1)
|
|
40
|
+
const double = computed('double', () => a.get() * 2)
|
|
41
|
+
const quadruple = computed('quadruple', () => {
|
|
42
|
+
whyAmIRunning()
|
|
43
|
+
return double.get() * 2
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const stop = react('r', () => {
|
|
47
|
+
quadruple.get()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(log).not.toHaveBeenCalled()
|
|
51
|
+
|
|
52
|
+
a.set(2)
|
|
53
|
+
|
|
54
|
+
expect(log).toHaveBeenCalledTimes(1)
|
|
55
|
+
expect(log.mock.calls[0][0]).toContain('Computed(quadruple) is recomputing because:')
|
|
56
|
+
expect(log.mock.calls[0][0]).toContain('Computed(double) changed')
|
|
57
|
+
expect(log.mock.calls[0][0]).toContain('Atom(a) changed')
|
|
58
|
+
|
|
59
|
+
stop()
|
|
60
|
+
log.mockRestore()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('[D3] logs that an effect was executed manually when no ancestors changed', () => {
|
|
64
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
65
|
+
|
|
66
|
+
const a = atom('a', 1)
|
|
67
|
+
const r = reactor('manual', () => {
|
|
68
|
+
whyAmIRunning()
|
|
69
|
+
a.get()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
r.start()
|
|
73
|
+
expect(log).not.toHaveBeenCalled()
|
|
74
|
+
|
|
75
|
+
r.stop()
|
|
76
|
+
r.start({ force: true })
|
|
77
|
+
|
|
78
|
+
expect(log).toHaveBeenCalledTimes(1)
|
|
79
|
+
expect(log.mock.calls[0][0]).toContain('Effect(manual) was executed manually.')
|
|
80
|
+
|
|
81
|
+
r.stop()
|
|
82
|
+
log.mockRestore()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { promiseWithResolve, sleep } from '@tldraw/utils'
|
|
2
|
+
import { vi } from 'vitest'
|
|
3
|
+
import { atom } from '../Atom'
|
|
4
|
+
import { computed } from '../Computed'
|
|
5
|
+
import { react } from '../EffectScheduler'
|
|
6
|
+
import { deferAsyncEffects, transact, transaction } from '../transactions'
|
|
7
|
+
|
|
8
|
+
// Tests for SPEC.md §12 (async transactions: deferAsyncEffects).
|
|
9
|
+
// Rule IDs like [AT3] in test names refer to that document.
|
|
10
|
+
|
|
11
|
+
describe('deferAsyncEffects (AT)', () => {
|
|
12
|
+
it('[AT1] defers effects until the async transaction commits', async () => {
|
|
13
|
+
const a = atom('', 0)
|
|
14
|
+
const b = atom('', 0)
|
|
15
|
+
const effectCalls = vi.fn()
|
|
16
|
+
|
|
17
|
+
react('', () => {
|
|
18
|
+
a.get()
|
|
19
|
+
b.get()
|
|
20
|
+
effectCalls()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(effectCalls).toHaveBeenCalledTimes(1)
|
|
24
|
+
|
|
25
|
+
const txPromise = deferAsyncEffects(async () => {
|
|
26
|
+
a.set(1)
|
|
27
|
+
expect(effectCalls).toHaveBeenCalledTimes(1) // no effect yet
|
|
28
|
+
await sleep(1)
|
|
29
|
+
b.set(2)
|
|
30
|
+
expect(effectCalls).toHaveBeenCalledTimes(1) // still no effect
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await txPromise
|
|
34
|
+
expect(effectCalls).toHaveBeenCalledTimes(2) // effect runs after commit
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('[AT1] keeps computed signals up to date during the async transaction', async () => {
|
|
38
|
+
const a = atom('', 1)
|
|
39
|
+
const doubled = computed('', () => a.get() * 2)
|
|
40
|
+
|
|
41
|
+
expect(doubled.get()).toBe(2)
|
|
42
|
+
|
|
43
|
+
await deferAsyncEffects(async () => {
|
|
44
|
+
a.set(5)
|
|
45
|
+
// computed should update during transaction
|
|
46
|
+
expect(doubled.get()).toBe(10)
|
|
47
|
+
await sleep(1)
|
|
48
|
+
a.set(10)
|
|
49
|
+
expect(doubled.get()).toBe(20)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// computed should update after commit
|
|
53
|
+
expect(doubled.get()).toBe(20)
|
|
54
|
+
expect(a.get()).toBe(10)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('[AT2] throws if kicked off during a sync transaction', async () => {
|
|
58
|
+
const a = atom('', 0)
|
|
59
|
+
let txp: any = null
|
|
60
|
+
transact(() => {
|
|
61
|
+
txp = deferAsyncEffects(async () => {
|
|
62
|
+
expect(a.get()).toBe(1)
|
|
63
|
+
a.set(2)
|
|
64
|
+
})
|
|
65
|
+
a.set(1)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await expect(txp).rejects.toMatchInlineSnapshot(
|
|
69
|
+
`[Error: deferAsyncEffects cannot be called during a sync transaction]`
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('[AT2] allows sync transactions inside the async body', async () => {
|
|
74
|
+
const a = atom('', 0)
|
|
75
|
+
|
|
76
|
+
await deferAsyncEffects(async () => {
|
|
77
|
+
a.set(1)
|
|
78
|
+
transaction(() => {
|
|
79
|
+
a.set(2)
|
|
80
|
+
})
|
|
81
|
+
expect(a.get()).toBe(2)
|
|
82
|
+
})
|
|
83
|
+
expect(a.get()).toBe(2)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('[AT2] allows transact inside the async body', async () => {
|
|
87
|
+
const a = atom('', 0)
|
|
88
|
+
|
|
89
|
+
await deferAsyncEffects(async () => {
|
|
90
|
+
a.set(1)
|
|
91
|
+
transact(() => {
|
|
92
|
+
a.set(2)
|
|
93
|
+
})
|
|
94
|
+
expect(a.get()).toBe(2)
|
|
95
|
+
})
|
|
96
|
+
expect(a.get()).toBe(2)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('[AT3] rolls back all changes on exception', async () => {
|
|
100
|
+
const a = atom('', 0)
|
|
101
|
+
const b = atom('', 0)
|
|
102
|
+
|
|
103
|
+
await expect(
|
|
104
|
+
deferAsyncEffects(async () => {
|
|
105
|
+
a.set(1)
|
|
106
|
+
b.set(2)
|
|
107
|
+
throw new Error('test error')
|
|
108
|
+
})
|
|
109
|
+
).rejects.toThrow('test error')
|
|
110
|
+
|
|
111
|
+
expect(a.get()).toBe(0)
|
|
112
|
+
expect(b.get()).toBe(0)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('[AT3][AT4] rolls back everything when a nested async transaction throws', async () => {
|
|
116
|
+
const a = atom('', 0)
|
|
117
|
+
const b = atom('', 0)
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
deferAsyncEffects(async () => {
|
|
121
|
+
a.set(1)
|
|
122
|
+
|
|
123
|
+
await deferAsyncEffects(async () => {
|
|
124
|
+
b.set(2)
|
|
125
|
+
throw new Error('inner error')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
).rejects.toThrow('inner error')
|
|
129
|
+
|
|
130
|
+
expect(a.get()).toBe(0) // all changes should be rolled back
|
|
131
|
+
expect(b.get()).toBe(0)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('[AT4] nested async transactions join the outer one', async () => {
|
|
135
|
+
const a = atom('', 0)
|
|
136
|
+
|
|
137
|
+
await deferAsyncEffects(async () => {
|
|
138
|
+
a.set(1)
|
|
139
|
+
await deferAsyncEffects(async () => {
|
|
140
|
+
a.set(2)
|
|
141
|
+
})
|
|
142
|
+
expect(a.get()).toBe(2)
|
|
143
|
+
})
|
|
144
|
+
expect(a.get()).toBe(2)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('[AT4] concurrent async transactions are merged', async () => {
|
|
148
|
+
const a = atom('', 0)
|
|
149
|
+
const b = atom('', 0)
|
|
150
|
+
const results: number[] = []
|
|
151
|
+
|
|
152
|
+
const tx1 = deferAsyncEffects(async () => {
|
|
153
|
+
a.set(1)
|
|
154
|
+
await sleep(10)
|
|
155
|
+
results.push(a.get())
|
|
156
|
+
return 'tx1'
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const tx2 = deferAsyncEffects(async () => {
|
|
160
|
+
b.set(2)
|
|
161
|
+
await sleep(5)
|
|
162
|
+
results.push(b.get())
|
|
163
|
+
return 'tx2'
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const [result1, result2] = await Promise.all([tx1, tx2])
|
|
167
|
+
|
|
168
|
+
expect(result1).toBe('tx1')
|
|
169
|
+
expect(result2).toBe('tx2')
|
|
170
|
+
expect(a.get()).toBe(1)
|
|
171
|
+
expect(b.get()).toBe(2)
|
|
172
|
+
expect(results).toEqual([2, 1])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('[AT4] overlapping transactions leak state to each other but group their effects', async () => {
|
|
176
|
+
const a = atom('', 0)
|
|
177
|
+
|
|
178
|
+
let txp = null
|
|
179
|
+
|
|
180
|
+
const p = deferAsyncEffects(async () => {
|
|
181
|
+
a.set(1)
|
|
182
|
+
const x = promiseWithResolve()
|
|
183
|
+
txp = deferAsyncEffects(async () => {
|
|
184
|
+
a.set(2)
|
|
185
|
+
x.resolve(null)
|
|
186
|
+
await sleep(10)
|
|
187
|
+
a.set(3)
|
|
188
|
+
return 'inner'
|
|
189
|
+
})
|
|
190
|
+
await x
|
|
191
|
+
// inner transactions leak, this can't be avoided without AsyncContext
|
|
192
|
+
// but at least we can group effects.
|
|
193
|
+
expect(a.get()).toBe(2)
|
|
194
|
+
return 'outer'
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
await expect(p).resolves.toBe('outer')
|
|
198
|
+
await expect(txp).resolves.toBe('inner')
|
|
199
|
+
expect(a.get()).toBe(3)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('[AT5] waits for the reaction phase to finish if kicked off during a reaction', async () => {
|
|
203
|
+
const a = atom('', 0)
|
|
204
|
+
const b = atom('', 0)
|
|
205
|
+
|
|
206
|
+
let txp: any = null
|
|
207
|
+
|
|
208
|
+
react('', () => {
|
|
209
|
+
a.get()
|
|
210
|
+
txp = deferAsyncEffects(async () => {
|
|
211
|
+
await sleep(1)
|
|
212
|
+
b.set(a.get() + 1)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
await txp
|
|
217
|
+
|
|
218
|
+
expect(a.get()).toBe(0)
|
|
219
|
+
expect(b.get()).toBe(1)
|
|
220
|
+
|
|
221
|
+
a.set(1)
|
|
222
|
+
|
|
223
|
+
await txp
|
|
224
|
+
|
|
225
|
+
expect(a.get()).toBe(1)
|
|
226
|
+
expect(b.get()).toBe(2)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('[AT6] resolves to the return value of the function', async () => {
|
|
230
|
+
await expect(deferAsyncEffects(async () => 'value')).resolves.toBe('value')
|
|
231
|
+
})
|
|
232
|
+
})
|
|
@@ -1,41 +1,15 @@
|
|
|
1
1
|
import { atom } from '../Atom'
|
|
2
|
-
import { computed } from '../Computed'
|
|
2
|
+
import { computed, isUninitialized } from '../Computed'
|
|
3
3
|
import { EffectScheduler, react } from '../EffectScheduler'
|
|
4
4
|
import { haveParentsChanged } from '../helpers'
|
|
5
5
|
import { getGlobalEpoch, transact } from '../transactions'
|
|
6
6
|
import { RESET_VALUE } from '../types'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const a = atom('', 1)
|
|
11
|
-
react('', () => {
|
|
12
|
-
if (a.get() === 2) throw new Error('test')
|
|
13
|
-
})
|
|
14
|
-
expect(() => a.set(2)).toThrowErrorMatchingInlineSnapshot(`[Error: test]`)
|
|
15
|
-
expect(a.get()).toBe(2)
|
|
16
|
-
})
|
|
17
|
-
it('will not roll back the changes in a transaction', () => {
|
|
18
|
-
const a = atom('', 1)
|
|
19
|
-
const b = atom('', 2)
|
|
8
|
+
// Tests for SPEC.md §7 (errors in computed signals) and rule CE6 (effects that throw).
|
|
9
|
+
// Rule IDs like [CE1] in test names refer to that document.
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
expect(() =>
|
|
26
|
-
transact(() => {
|
|
27
|
-
a.set(3)
|
|
28
|
-
b.set(1)
|
|
29
|
-
})
|
|
30
|
-
).toThrowErrorMatchingInlineSnapshot(`[Error: test]`)
|
|
31
|
-
|
|
32
|
-
expect(a.get()).toBe(3)
|
|
33
|
-
expect(b.get()).toBe(1)
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
describe('derivations that error', () => {
|
|
38
|
-
it('will cache thrown values', () => {
|
|
11
|
+
describe('computed signals that throw (CE)', () => {
|
|
12
|
+
it('[CE1] cache thrown values until a parent changes', () => {
|
|
39
13
|
let numComputations = 0
|
|
40
14
|
const a = atom('', 1)
|
|
41
15
|
const b = computed('', () => {
|
|
@@ -62,7 +36,7 @@ describe('derivations that error', () => {
|
|
|
62
36
|
expect(b.get()).toBe(3)
|
|
63
37
|
})
|
|
64
38
|
|
|
65
|
-
it('
|
|
39
|
+
it('[CE2] entering the error state notifies effects, but consecutive errors do not', () => {
|
|
66
40
|
const a = atom('', 1)
|
|
67
41
|
let numComputations = 0
|
|
68
42
|
const b = computed('', () => {
|
|
@@ -85,21 +59,45 @@ describe('derivations that error', () => {
|
|
|
85
59
|
|
|
86
60
|
a.set(2)
|
|
87
61
|
|
|
62
|
+
// entering the error state is a change
|
|
88
63
|
expect(numReactions).toBe(2)
|
|
89
64
|
expect(numComputations).toBe(2)
|
|
90
65
|
|
|
91
66
|
a.set(4)
|
|
92
67
|
|
|
68
|
+
// erroring again while already in the error state is not a change
|
|
93
69
|
expect(numComputations).toBe(3)
|
|
94
70
|
expect(numReactions).toBe(2)
|
|
95
71
|
|
|
96
72
|
a.set(3)
|
|
97
73
|
|
|
74
|
+
// recovering is a change
|
|
98
75
|
expect(numComputations).toBe(4)
|
|
99
76
|
expect(numReactions).toBe(3)
|
|
100
77
|
})
|
|
101
78
|
|
|
102
|
-
it('
|
|
79
|
+
it('[CE3] discard the previous value: recovery receives UNINITIALIZED as previousValue', () => {
|
|
80
|
+
const a = atom('', 1)
|
|
81
|
+
const previousValues: unknown[] = []
|
|
82
|
+
const b = computed('', (prev) => {
|
|
83
|
+
previousValues.push(prev)
|
|
84
|
+
if (a.get() === 2) throw new Error('test')
|
|
85
|
+
return a.get()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(b.get()).toBe(1)
|
|
89
|
+
|
|
90
|
+
a.set(2)
|
|
91
|
+
expect(() => b.get()).toThrowErrorMatchingInlineSnapshot(`[Error: test]`)
|
|
92
|
+
|
|
93
|
+
a.set(3)
|
|
94
|
+
expect(b.get()).toBe(3)
|
|
95
|
+
|
|
96
|
+
expect(previousValues.map(isUninitialized)).toEqual([true, false, true])
|
|
97
|
+
expect(previousValues[1]).toBe(1)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('[CE4] clear the history buffer when an error is thrown', () => {
|
|
103
101
|
const a = atom('', 1)
|
|
104
102
|
const b = computed(
|
|
105
103
|
'',
|
|
@@ -142,24 +140,54 @@ describe('derivations that error', () => {
|
|
|
142
140
|
expect(b.getDiffSince(errorEpoch)).toEqual(RESET_VALUE)
|
|
143
141
|
expect(b.getDiffSince(errorEpoch + 1)).toEqual([1])
|
|
144
142
|
})
|
|
143
|
+
|
|
144
|
+
it('[CE5] haveParentsChanged will not throw if one of the parents is throwing', () => {
|
|
145
|
+
const a = atom('', 1)
|
|
146
|
+
const scheduler = new EffectScheduler('', () => {
|
|
147
|
+
a.get()
|
|
148
|
+
throw new Error('test')
|
|
149
|
+
})
|
|
150
|
+
expect(() => {
|
|
151
|
+
scheduler.attach()
|
|
152
|
+
scheduler.execute()
|
|
153
|
+
}).toThrowErrorMatchingInlineSnapshot(`[Error: test]`)
|
|
154
|
+
|
|
155
|
+
expect(haveParentsChanged(scheduler)).toBe(false)
|
|
156
|
+
|
|
157
|
+
expect(() => a.set(2)).toThrowErrorMatchingInlineSnapshot(`[Error: test]`)
|
|
158
|
+
|
|
159
|
+
// haveParentsChanged should still be false because it already
|
|
160
|
+
// executed the effect and it errored
|
|
161
|
+
expect(haveParentsChanged(scheduler)).toBe(false)
|
|
162
|
+
})
|
|
145
163
|
})
|
|
146
164
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
describe('effects that throw (CE6)', () => {
|
|
166
|
+
it('[CE6] will not roll back the atom value', () => {
|
|
167
|
+
const a = atom('', 1)
|
|
168
|
+
react('', () => {
|
|
169
|
+
if (a.get() === 2) throw new Error('test')
|
|
170
|
+
})
|
|
171
|
+
expect(() => a.set(2)).toThrowErrorMatchingInlineSnapshot(`[Error: test]`)
|
|
172
|
+
expect(a.get()).toBe(2)
|
|
152
173
|
})
|
|
153
|
-
expect(() => {
|
|
154
|
-
scheduler.attach()
|
|
155
|
-
scheduler.execute()
|
|
156
|
-
}).toThrowErrorMatchingInlineSnapshot(`[Error: test]`)
|
|
157
174
|
|
|
158
|
-
|
|
175
|
+
it('[CE6] will not roll back the changes in a transaction', () => {
|
|
176
|
+
const a = atom('', 1)
|
|
177
|
+
const b = atom('', 2)
|
|
159
178
|
|
|
160
|
-
|
|
179
|
+
react('', () => {
|
|
180
|
+
if (a.get() + b.get() === 4) throw new Error('test')
|
|
181
|
+
})
|
|
161
182
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
expect(() =>
|
|
184
|
+
transact(() => {
|
|
185
|
+
a.set(3)
|
|
186
|
+
b.set(1)
|
|
187
|
+
})
|
|
188
|
+
).toThrowErrorMatchingInlineSnapshot(`[Error: test]`)
|
|
189
|
+
|
|
190
|
+
expect(a.get()).toBe(3)
|
|
191
|
+
expect(b.get()).toBe(1)
|
|
192
|
+
})
|
|
165
193
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { atom, isAtom } from '../Atom'
|
|
2
|
+
import { computed, getComputedInstance, isComputed } from '../Computed'
|
|
3
|
+
import { isSignal } from '../isSignal'
|
|
4
|
+
|
|
5
|
+
// Tests for SPEC.md §14 (type guards and module duplication).
|
|
6
|
+
// Rule G2 (singleton sharing across module copies) is covered at the unit level in helpers.test.ts.
|
|
7
|
+
|
|
8
|
+
describe('type guards (G1)', () => {
|
|
9
|
+
const a = atom('a', 1)
|
|
10
|
+
const c = computed('c', () => a.get() * 2)
|
|
11
|
+
|
|
12
|
+
class Foo {
|
|
13
|
+
x = atom('x', 1)
|
|
14
|
+
@computed
|
|
15
|
+
getY() {
|
|
16
|
+
return this.x.get()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const decorated = getComputedInstance(new Foo(), 'getY')
|
|
20
|
+
|
|
21
|
+
const nonSignals = [null, undefined, 0, 1, NaN, 'hello', {}, [], () => {}, Symbol('s')]
|
|
22
|
+
|
|
23
|
+
it('[G1] isAtom is true exactly for atoms', () => {
|
|
24
|
+
expect(isAtom(a)).toBe(true)
|
|
25
|
+
expect(isAtom(c)).toBe(false)
|
|
26
|
+
expect(isAtom(decorated)).toBe(false)
|
|
27
|
+
for (const value of nonSignals) {
|
|
28
|
+
expect(isAtom(value)).toBe(false)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('[G1] isComputed is true exactly for computed signals', () => {
|
|
33
|
+
expect(isComputed(c)).toBe(true)
|
|
34
|
+
expect(isComputed(decorated)).toBe(true)
|
|
35
|
+
expect(isComputed(a)).toBe(false)
|
|
36
|
+
for (const value of nonSignals) {
|
|
37
|
+
expect(isComputed(value)).toBe(false)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('[G1] isSignal is true exactly for atoms and computed signals', () => {
|
|
42
|
+
expect(isSignal(a)).toBe(true)
|
|
43
|
+
expect(isSignal(c)).toBe(true)
|
|
44
|
+
expect(isSignal(decorated)).toBe(true)
|
|
45
|
+
for (const value of nonSignals) {
|
|
46
|
+
expect(isSignal(value)).toBe(false)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|