@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,199 +1,203 @@
|
|
|
1
1
|
import { atom } from '../Atom'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { computed } from '../Computed'
|
|
3
|
+
import { react } from '../EffectScheduler'
|
|
4
|
+
import { getGlobalEpoch } from '../transactions'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const a = atom('', 1)
|
|
6
|
+
// Tests for SPEC.md §2 (the epoch clock), §3 (equality), and §4 (atoms).
|
|
7
|
+
// Rule IDs like [EP3] in test names refer to that document.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const a = atom('', 1)
|
|
9
|
+
describe('the epoch clock (EP)', () => {
|
|
10
|
+
it('[EP1] is shared by all atoms', () => {
|
|
11
|
+
const startEpoch = getGlobalEpoch()
|
|
12
|
+
const a = atom('a', 1)
|
|
13
|
+
const b = atom('b', 1)
|
|
14
14
|
|
|
15
15
|
a.set(2)
|
|
16
|
+
expect(getGlobalEpoch()).toBe(startEpoch + 1)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
b.set(2)
|
|
19
|
+
expect(getGlobalEpoch()).toBe(startEpoch + 2)
|
|
18
20
|
})
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
it('[EP2] does not advance when signals are created', () => {
|
|
20
23
|
const startEpoch = getGlobalEpoch()
|
|
21
|
-
atom('', 3)
|
|
24
|
+
atom('a', 3)
|
|
25
|
+
computed('c', () => 1)
|
|
22
26
|
expect(getGlobalEpoch()).toBe(startEpoch)
|
|
23
27
|
})
|
|
24
|
-
|
|
28
|
+
|
|
29
|
+
it('[EP3] advances by exactly one when an atom is set to a new value', () => {
|
|
25
30
|
const startEpoch = getGlobalEpoch()
|
|
26
|
-
const a = atom('', 3)
|
|
31
|
+
const a = atom('a', 3)
|
|
27
32
|
a.set(4)
|
|
28
33
|
expect(getGlobalEpoch()).toBe(startEpoch + 1)
|
|
29
34
|
})
|
|
30
|
-
it('can store history', () => {
|
|
31
|
-
const a = atom('', 1, { historyLength: 3, computeDiff: (a, b) => b - a })
|
|
32
35
|
|
|
36
|
+
it('[EP4] does not advance when an atom is set to an equal value', () => {
|
|
37
|
+
const a = atom('a', 3)
|
|
33
38
|
const startEpoch = getGlobalEpoch()
|
|
39
|
+
a.set(3)
|
|
40
|
+
expect(getGlobalEpoch()).toBe(startEpoch)
|
|
41
|
+
})
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
it('[EP5] is recorded on the atom as lastChangedEpoch when it changes', () => {
|
|
44
|
+
const a = atom('a', 1)
|
|
36
45
|
|
|
37
|
-
a.set(
|
|
46
|
+
a.set(2)
|
|
47
|
+
const changedEpoch = getGlobalEpoch()
|
|
48
|
+
expect(a.lastChangedEpoch).toBe(changedEpoch)
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
// other atoms changing does not move this atom's lastChangedEpoch
|
|
51
|
+
const b = atom('b', 1)
|
|
52
|
+
b.set(2)
|
|
53
|
+
expect(a.lastChangedEpoch).toBe(changedEpoch)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
40
56
|
|
|
41
|
-
|
|
57
|
+
describe('equality (EQ)', () => {
|
|
58
|
+
it('[EQ1] treats === and Object.is values as equal by default', () => {
|
|
59
|
+
const value = { hello: true }
|
|
60
|
+
const a = atom('a', value)
|
|
61
|
+
const startEpoch = getGlobalEpoch()
|
|
42
62
|
|
|
43
|
-
|
|
63
|
+
a.set(value)
|
|
64
|
+
expect(getGlobalEpoch()).toBe(startEpoch)
|
|
44
65
|
|
|
45
|
-
|
|
66
|
+
const n = atom('n', NaN)
|
|
67
|
+
n.set(NaN)
|
|
68
|
+
expect(getGlobalEpoch()).toBe(startEpoch)
|
|
69
|
+
})
|
|
46
70
|
|
|
47
|
-
|
|
71
|
+
it('[EQ1] consults the old value’s equals method by default', () => {
|
|
72
|
+
class Box {
|
|
73
|
+
constructor(public value: number) {}
|
|
74
|
+
equals(other: unknown) {
|
|
75
|
+
return other instanceof Box && other.value === this.value
|
|
76
|
+
}
|
|
77
|
+
}
|
|
48
78
|
|
|
49
|
-
|
|
79
|
+
const original = new Box(1)
|
|
80
|
+
const a = atom('a', original)
|
|
50
81
|
|
|
51
|
-
|
|
52
|
-
expect(a.
|
|
82
|
+
a.set(new Box(1))
|
|
83
|
+
expect(a.get()).toBe(original)
|
|
84
|
+
|
|
85
|
+
const different = new Box(2)
|
|
86
|
+
a.set(different)
|
|
87
|
+
expect(a.get()).toBe(different)
|
|
53
88
|
})
|
|
54
|
-
it('has history independent of other atoms', () => {
|
|
55
|
-
const a = atom('', 1, { historyLength: 3, computeDiff: (a, b) => b - a })
|
|
56
|
-
const b = atom('', 1, { historyLength: 3, computeDiff: (a, b) => b - a })
|
|
57
89
|
|
|
58
|
-
|
|
90
|
+
it('[EQ2] does not consult the new value’s equals method', () => {
|
|
91
|
+
const oldValue = { x: 1 }
|
|
92
|
+
const newValue = { x: 1, equals: () => true }
|
|
59
93
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
b.set(-20)
|
|
63
|
-
expect(b.getDiffSince(startEpoch)).toEqual([-6, -5, -10])
|
|
64
|
-
expect(b.getDiffSince(getGlobalEpoch())).toEqual([])
|
|
94
|
+
const a = atom('a', oldValue)
|
|
95
|
+
a.set(newValue)
|
|
65
96
|
|
|
66
|
-
expect(a.
|
|
67
|
-
a.set(5)
|
|
68
|
-
expect(a.getDiffSince(startEpoch)).toEqual([+4])
|
|
69
|
-
expect(b.getDiffSince(startEpoch)).toEqual([-6, -5, -10])
|
|
70
|
-
expect(b.getDiffSince(getGlobalEpoch())).toEqual([])
|
|
97
|
+
expect(a.get()).toBe(newValue)
|
|
71
98
|
})
|
|
72
|
-
it('still updates history during transactions', () => {
|
|
73
|
-
const a = atom('', 1, { historyLength: 3, computeDiff: (a, b) => b - a })
|
|
74
99
|
|
|
75
|
-
|
|
100
|
+
it('[EQ3] uses a custom isEqual instead of default equality when provided', () => {
|
|
101
|
+
const foo = { hello: true }
|
|
102
|
+
const bar = { hello: true }
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
expect(a.getDiffSince(startEpoch)).toEqual([])
|
|
104
|
+
const a = atom('a', foo)
|
|
79
105
|
|
|
80
|
-
|
|
106
|
+
a.set(bar)
|
|
81
107
|
|
|
82
|
-
|
|
108
|
+
expect(a.get()).toBe(bar)
|
|
83
109
|
|
|
84
|
-
|
|
110
|
+
const b = atom('b', foo, { isEqual: (a, b) => a.hello === b.hello })
|
|
85
111
|
|
|
86
|
-
|
|
112
|
+
b.set(bar)
|
|
87
113
|
|
|
88
|
-
|
|
114
|
+
expect(b.get()).toBe(foo)
|
|
115
|
+
})
|
|
89
116
|
|
|
90
|
-
|
|
117
|
+
it('[EQ4] makes setting an equal value a complete no-op', () => {
|
|
118
|
+
const initial = { x: 1 }
|
|
119
|
+
const a = atom('a', initial, {
|
|
120
|
+
isEqual: (a, b) => a.x === b.x,
|
|
121
|
+
historyLength: 5,
|
|
122
|
+
computeDiff: () => 'diff',
|
|
91
123
|
})
|
|
92
124
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
125
|
+
const effect = vi.fn(() => {
|
|
126
|
+
a.get()
|
|
127
|
+
})
|
|
128
|
+
const stop = react('r', effect)
|
|
129
|
+
expect(effect).toHaveBeenCalledTimes(1)
|
|
97
130
|
|
|
98
131
|
const startEpoch = getGlobalEpoch()
|
|
132
|
+
const lastChanged = a.lastChangedEpoch
|
|
99
133
|
|
|
100
|
-
|
|
101
|
-
expect(a.getDiffSince(startEpoch)).toEqual([])
|
|
134
|
+
a.set({ x: 1 })
|
|
102
135
|
|
|
103
|
-
|
|
136
|
+
expect(getGlobalEpoch()).toBe(startEpoch)
|
|
137
|
+
expect(a.lastChangedEpoch).toBe(lastChanged)
|
|
138
|
+
expect(a.get()).toBe(initial)
|
|
139
|
+
expect(a.getDiffSince(startEpoch)).toEqual([])
|
|
140
|
+
expect(effect).toHaveBeenCalledTimes(1)
|
|
104
141
|
|
|
105
|
-
|
|
142
|
+
stop()
|
|
143
|
+
})
|
|
144
|
+
})
|
|
106
145
|
|
|
107
|
-
|
|
108
|
-
|
|
146
|
+
describe('atoms (A)', () => {
|
|
147
|
+
it('[A1] contain data', () => {
|
|
148
|
+
const a = atom('', 1)
|
|
109
149
|
|
|
110
|
-
expect(a.
|
|
150
|
+
expect(a.get()).toBe(1)
|
|
111
151
|
})
|
|
112
|
-
|
|
113
|
-
|
|
152
|
+
|
|
153
|
+
it('[A2] can be updated with set', () => {
|
|
114
154
|
const a = atom('', 1)
|
|
115
155
|
|
|
116
|
-
a.
|
|
156
|
+
a.set(2)
|
|
117
157
|
|
|
118
158
|
expect(a.get()).toBe(2)
|
|
119
|
-
expect(getGlobalEpoch()).toBe(startEpoch + 1)
|
|
120
159
|
})
|
|
121
|
-
it('supports passing diffs in .set', () => {
|
|
122
|
-
const a = atom('', 1, { historyLength: 3 })
|
|
123
|
-
|
|
124
|
-
const startEpoch = getGlobalEpoch()
|
|
125
160
|
|
|
126
|
-
|
|
127
|
-
|
|
161
|
+
it('[A3] set returns the value of the atom after the call', () => {
|
|
162
|
+
const a = atom('', 1)
|
|
128
163
|
|
|
129
|
-
a.set(
|
|
130
|
-
|
|
164
|
+
expect(a.set(2)).toBe(2)
|
|
165
|
+
// setting an equal value returns the unchanged current value
|
|
166
|
+
expect(a.set(2)).toBe(2)
|
|
131
167
|
})
|
|
132
|
-
it('does not push history if nothing changed', () => {
|
|
133
|
-
const a = atom('', 1, { historyLength: 3 })
|
|
134
168
|
|
|
169
|
+
it('[A4] update(fn) sets the atom to fn(currentValue)', () => {
|
|
135
170
|
const startEpoch = getGlobalEpoch()
|
|
171
|
+
const a = atom('', 1)
|
|
136
172
|
|
|
137
|
-
a.
|
|
138
|
-
expect(a.getDiffSince(startEpoch)).toEqual([+4])
|
|
139
|
-
a.set(5, +4)
|
|
140
|
-
expect(a.getDiffSince(startEpoch)).toEqual([+4])
|
|
141
|
-
})
|
|
142
|
-
it('clears the history buffer if you fail to provide a diff', () => {
|
|
143
|
-
const a = atom('', 1, { historyLength: 3 })
|
|
144
|
-
const startEpoch = getGlobalEpoch()
|
|
145
|
-
|
|
146
|
-
a.set(5, +4)
|
|
147
|
-
|
|
148
|
-
expect(a.getDiffSince(startEpoch)).toEqual([+4])
|
|
149
|
-
|
|
150
|
-
a.set(6)
|
|
173
|
+
a.update((value) => value + 1)
|
|
151
174
|
|
|
152
|
-
expect(a.
|
|
175
|
+
expect(a.get()).toBe(2)
|
|
176
|
+
expect(getGlobalEpoch()).toBe(startEpoch + 1)
|
|
153
177
|
})
|
|
154
|
-
})
|
|
155
178
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
let val = 0
|
|
161
|
-
const r = reactor('', () => {
|
|
162
|
-
val = a.get()
|
|
179
|
+
it('[A5] __unsafe__getWithoutCapture returns the value without capturing a dependency', () => {
|
|
180
|
+
const a = atom('a', 1)
|
|
181
|
+
const effect = vi.fn(() => {
|
|
182
|
+
a.__unsafe__getWithoutCapture()
|
|
163
183
|
})
|
|
184
|
+
const stop = react('r', effect)
|
|
164
185
|
|
|
165
|
-
expect(
|
|
166
|
-
|
|
167
|
-
r.start()
|
|
168
|
-
|
|
169
|
-
expect(val).toBe(234)
|
|
170
|
-
|
|
171
|
-
a.set(939)
|
|
172
|
-
|
|
173
|
-
expect(val).toBe(939)
|
|
174
|
-
|
|
175
|
-
r.stop()
|
|
186
|
+
expect(effect).toHaveBeenCalledTimes(1)
|
|
176
187
|
|
|
177
|
-
a.set(
|
|
188
|
+
a.set(2)
|
|
178
189
|
|
|
179
|
-
expect(
|
|
180
|
-
|
|
190
|
+
expect(effect).toHaveBeenCalledTimes(1)
|
|
191
|
+
stop()
|
|
181
192
|
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('isEqual can provide custom equality checks', () => {
|
|
185
|
-
const foo = { hello: true }
|
|
186
|
-
const bar = { hello: true }
|
|
187
|
-
|
|
188
|
-
const a = atom('a', foo)
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
it('[A6] are independent of each other', () => {
|
|
195
|
+
const a = atom('a', 1)
|
|
196
|
+
const b = atom('b', 10)
|
|
191
197
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const b = atom('b', foo, { isEqual: (a, b) => a.hello === b.hello })
|
|
195
|
-
|
|
196
|
-
b.set(bar)
|
|
198
|
+
a.set(2)
|
|
197
199
|
|
|
198
|
-
|
|
200
|
+
expect(b.get()).toBe(10)
|
|
201
|
+
expect(b.lastChangedEpoch).toBeLessThan(a.lastChangedEpoch)
|
|
202
|
+
})
|
|
199
203
|
})
|
|
@@ -7,10 +7,207 @@ import {
|
|
|
7
7
|
unsafe__withoutCapture,
|
|
8
8
|
} from '../capture'
|
|
9
9
|
import { computed } from '../Computed'
|
|
10
|
-
import { react } from '../EffectScheduler'
|
|
10
|
+
import { react, reactor } from '../EffectScheduler'
|
|
11
11
|
import { advanceGlobalEpoch, getGlobalEpoch } from '../transactions'
|
|
12
12
|
import { Child } from '../types'
|
|
13
13
|
|
|
14
|
+
// Tests for SPEC.md §5 (dependency capture).
|
|
15
|
+
// Rule IDs like [CAP2] in test names refer to that document.
|
|
16
|
+
|
|
17
|
+
describe('dependency capture (CAP)', () => {
|
|
18
|
+
it('[CAP1][CAP3] captures each dereferenced signal once, in first-dereference order', () => {
|
|
19
|
+
const a = atom('a', 1)
|
|
20
|
+
const b = atom('b', 2)
|
|
21
|
+
|
|
22
|
+
const r = reactor('r', () => {
|
|
23
|
+
a.get()
|
|
24
|
+
a.get()
|
|
25
|
+
b.get()
|
|
26
|
+
a.get()
|
|
27
|
+
})
|
|
28
|
+
r.start()
|
|
29
|
+
|
|
30
|
+
expect(r.scheduler.parents).toEqual([a, b])
|
|
31
|
+
r.stop()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('[CAP2] the parent set is exactly the signals dereferenced in the latest run', () => {
|
|
35
|
+
const which = atom('which', true)
|
|
36
|
+
const b = atom('b', 1)
|
|
37
|
+
const c = atom('c', 2)
|
|
38
|
+
|
|
39
|
+
let runs = 0
|
|
40
|
+
const stop = react('r', () => {
|
|
41
|
+
runs++
|
|
42
|
+
if (which.get()) {
|
|
43
|
+
b.get()
|
|
44
|
+
} else {
|
|
45
|
+
c.get()
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
expect(runs).toBe(1)
|
|
49
|
+
|
|
50
|
+
// c is not currently a parent
|
|
51
|
+
c.set(3)
|
|
52
|
+
expect(runs).toBe(1)
|
|
53
|
+
|
|
54
|
+
which.set(false)
|
|
55
|
+
expect(runs).toBe(2)
|
|
56
|
+
|
|
57
|
+
// b is no longer a parent after the latest run
|
|
58
|
+
b.set(10)
|
|
59
|
+
expect(runs).toBe(2)
|
|
60
|
+
|
|
61
|
+
// c now is
|
|
62
|
+
c.set(4)
|
|
63
|
+
expect(runs).toBe(3)
|
|
64
|
+
|
|
65
|
+
stop()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('[CAP4] capture contexts nest: an effect captures a computed, not its parents', () => {
|
|
69
|
+
const a = atom('a', 1)
|
|
70
|
+
const double = computed('double', () => a.get() * 2)
|
|
71
|
+
|
|
72
|
+
const r = reactor('r', () => {
|
|
73
|
+
double.get()
|
|
74
|
+
})
|
|
75
|
+
r.start()
|
|
76
|
+
|
|
77
|
+
expect(r.scheduler.parents).toEqual([double])
|
|
78
|
+
expect(double.parents).toEqual([a])
|
|
79
|
+
r.stop()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('[CAP7] liveness propagates transitively up the graph', () => {
|
|
83
|
+
const a = atom('a', 1)
|
|
84
|
+
const c = computed('c', () => a.get())
|
|
85
|
+
|
|
86
|
+
// lazily dereferencing a computed does not attach anything
|
|
87
|
+
c.get()
|
|
88
|
+
expect(a.children.isEmpty).toBe(true)
|
|
89
|
+
expect(c.children.isEmpty).toBe(true)
|
|
90
|
+
|
|
91
|
+
const r = reactor('r', () => {
|
|
92
|
+
c.get()
|
|
93
|
+
})
|
|
94
|
+
r.start()
|
|
95
|
+
|
|
96
|
+
expect(a.children.isEmpty).toBe(false)
|
|
97
|
+
expect(c.children.isEmpty).toBe(false)
|
|
98
|
+
|
|
99
|
+
r.stop()
|
|
100
|
+
|
|
101
|
+
expect(a.children.isEmpty).toBe(true)
|
|
102
|
+
expect(c.children.isEmpty).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('[CAP6] dereferencing signals outside a capture context captures nothing', () => {
|
|
106
|
+
expect(() => {
|
|
107
|
+
maybeCaptureParent(atom('', 1))
|
|
108
|
+
}).not.toThrow()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('unsafe__withoutCapture (CAP5)', () => {
|
|
113
|
+
it('[CAP5] short-circuits the current capture frame in a computed', () => {
|
|
114
|
+
const atomA = atom('a', 1)
|
|
115
|
+
const atomB = atom('b', 1)
|
|
116
|
+
const atomC = atom('c', 1)
|
|
117
|
+
|
|
118
|
+
const child = computed('', () => {
|
|
119
|
+
return atomA.get() + atomB.get() + unsafe__withoutCapture(() => atomC.get())
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
let lastValue: number | undefined
|
|
123
|
+
let numReactions = 0
|
|
124
|
+
|
|
125
|
+
react('', () => {
|
|
126
|
+
numReactions++
|
|
127
|
+
lastValue = child.get()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(lastValue).toBe(3)
|
|
131
|
+
expect(numReactions).toBe(1)
|
|
132
|
+
|
|
133
|
+
atomA.set(2)
|
|
134
|
+
|
|
135
|
+
expect(lastValue).toBe(4)
|
|
136
|
+
expect(numReactions).toBe(2)
|
|
137
|
+
|
|
138
|
+
atomB.set(2)
|
|
139
|
+
|
|
140
|
+
expect(lastValue).toBe(5)
|
|
141
|
+
expect(numReactions).toBe(3)
|
|
142
|
+
|
|
143
|
+
atomC.set(2)
|
|
144
|
+
|
|
145
|
+
// The reaction should not have run because C was not captured
|
|
146
|
+
expect(lastValue).toBe(5)
|
|
147
|
+
expect(numReactions).toBe(3)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('[CAP5] short-circuits the current capture frame in an effect', () => {
|
|
151
|
+
const atomA = atom('a', 1)
|
|
152
|
+
const atomB = atom('b', 1)
|
|
153
|
+
const atomC = atom('c', 1)
|
|
154
|
+
|
|
155
|
+
let lastValue: number | undefined
|
|
156
|
+
let numReactions = 0
|
|
157
|
+
|
|
158
|
+
react('', () => {
|
|
159
|
+
numReactions++
|
|
160
|
+
lastValue = atomA.get() + atomB.get() + unsafe__withoutCapture(() => atomC.get())
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(lastValue).toBe(3)
|
|
164
|
+
expect(numReactions).toBe(1)
|
|
165
|
+
|
|
166
|
+
atomA.set(2)
|
|
167
|
+
|
|
168
|
+
expect(lastValue).toBe(4)
|
|
169
|
+
expect(numReactions).toBe(2)
|
|
170
|
+
|
|
171
|
+
atomB.set(2)
|
|
172
|
+
|
|
173
|
+
expect(lastValue).toBe(5)
|
|
174
|
+
expect(numReactions).toBe(3)
|
|
175
|
+
|
|
176
|
+
atomC.set(2)
|
|
177
|
+
|
|
178
|
+
// The reaction should not have run because C was not captured
|
|
179
|
+
expect(lastValue).toBe(5)
|
|
180
|
+
expect(numReactions).toBe(3)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('[CAP5] restores the capture context even if the wrapped function throws', () => {
|
|
184
|
+
const a = atom('a', 1)
|
|
185
|
+
let runs = 0
|
|
186
|
+
|
|
187
|
+
const stop = react('r', () => {
|
|
188
|
+
runs++
|
|
189
|
+
try {
|
|
190
|
+
unsafe__withoutCapture(() => {
|
|
191
|
+
throw new Error('oops')
|
|
192
|
+
})
|
|
193
|
+
} catch {
|
|
194
|
+
// ignore
|
|
195
|
+
}
|
|
196
|
+
// captured only if the capture context was restored
|
|
197
|
+
a.get()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
expect(runs).toBe(1)
|
|
201
|
+
|
|
202
|
+
a.set(2)
|
|
203
|
+
|
|
204
|
+
expect(runs).toBe(2)
|
|
205
|
+
stop()
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Internal contract of startCapturingParents / maybeCaptureParent / stopCapturingParents [CAP8].
|
|
210
|
+
|
|
14
211
|
const emptyChild = (props: Partial<Child> = {}) =>
|
|
15
212
|
({
|
|
16
213
|
parentEpochs: [],
|
|
@@ -21,8 +218,8 @@ const emptyChild = (props: Partial<Child> = {}) =>
|
|
|
21
218
|
...props,
|
|
22
219
|
}) as Child
|
|
23
220
|
|
|
24
|
-
describe('capturing parents', () => {
|
|
25
|
-
it('can be started and stopped', () => {
|
|
221
|
+
describe('capturing parents (CAP8, internal)', () => {
|
|
222
|
+
it('[CAP8] can be started and stopped', () => {
|
|
26
223
|
const a = atom('', 1)
|
|
27
224
|
const startEpoch = getGlobalEpoch()
|
|
28
225
|
|
|
@@ -42,7 +239,7 @@ describe('capturing parents', () => {
|
|
|
42
239
|
expect(child.parents).toEqual([a])
|
|
43
240
|
})
|
|
44
241
|
|
|
45
|
-
it('can handle several parents', () => {
|
|
242
|
+
it('[CAP8] can handle several parents', () => {
|
|
46
243
|
const atomA = atom('', 1)
|
|
47
244
|
const atomAEpoch = getGlobalEpoch()
|
|
48
245
|
advanceGlobalEpoch() // let's say time has passed
|
|
@@ -75,7 +272,7 @@ describe('capturing parents', () => {
|
|
|
75
272
|
expect(child.parents).toEqual([atomA, atomB, atomC])
|
|
76
273
|
})
|
|
77
274
|
|
|
78
|
-
it('will reorder if parents are captured in different orders each time', () => {
|
|
275
|
+
it('[CAP8] will reorder if parents are captured in different orders each time', () => {
|
|
79
276
|
const atomA = atom('', 1)
|
|
80
277
|
advanceGlobalEpoch() // let's say time has passed
|
|
81
278
|
const atomB = atom('', 1)
|
|
@@ -109,7 +306,7 @@ describe('capturing parents', () => {
|
|
|
109
306
|
expect(child.parents).toEqual([atomA, atomC, atomB])
|
|
110
307
|
})
|
|
111
308
|
|
|
112
|
-
it('will shrink the parent arrays if the number of captured parents shrinks', () => {
|
|
309
|
+
it('[CAP8] will shrink the parent arrays if the number of captured parents shrinks', () => {
|
|
113
310
|
const atomA = atom('', 1)
|
|
114
311
|
const atomAEpoch = getGlobalEpoch()
|
|
115
312
|
advanceGlobalEpoch() // let's say time has passed
|
|
@@ -159,82 +356,4 @@ describe('capturing parents', () => {
|
|
|
159
356
|
expect(child.parents).toBe(originalParents)
|
|
160
357
|
expect(child.parentEpochs).toBe(originalParentEpochs)
|
|
161
358
|
})
|
|
162
|
-
|
|
163
|
-
it('doesnt do anything if you dont start capturing', () => {
|
|
164
|
-
expect(() => {
|
|
165
|
-
maybeCaptureParent(atom('', 1))
|
|
166
|
-
}).not.toThrow()
|
|
167
|
-
})
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
describe(unsafe__withoutCapture, () => {
|
|
171
|
-
it('allows executing comptuer code in a context that short-circuits the current capture frame', () => {
|
|
172
|
-
const atomA = atom('a', 1)
|
|
173
|
-
const atomB = atom('b', 1)
|
|
174
|
-
const atomC = atom('c', 1)
|
|
175
|
-
|
|
176
|
-
const child = computed('', () => {
|
|
177
|
-
return atomA.get() + atomB.get() + unsafe__withoutCapture(() => atomC.get())
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
let lastValue: number | undefined
|
|
181
|
-
let numReactions = 0
|
|
182
|
-
|
|
183
|
-
react('', () => {
|
|
184
|
-
numReactions++
|
|
185
|
-
lastValue = child.get()
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
expect(lastValue).toBe(3)
|
|
189
|
-
expect(numReactions).toBe(1)
|
|
190
|
-
|
|
191
|
-
atomA.set(2)
|
|
192
|
-
|
|
193
|
-
expect(lastValue).toBe(4)
|
|
194
|
-
expect(numReactions).toBe(2)
|
|
195
|
-
|
|
196
|
-
atomB.set(2)
|
|
197
|
-
|
|
198
|
-
expect(lastValue).toBe(5)
|
|
199
|
-
expect(numReactions).toBe(3)
|
|
200
|
-
|
|
201
|
-
atomC.set(2)
|
|
202
|
-
|
|
203
|
-
// The reaction should not have run because C was not captured
|
|
204
|
-
expect(lastValue).toBe(5)
|
|
205
|
-
expect(numReactions).toBe(3)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('allows executing reactor code in a context that short-circuits the current capture frame', () => {
|
|
209
|
-
const atomA = atom('a', 1)
|
|
210
|
-
const atomB = atom('b', 1)
|
|
211
|
-
const atomC = atom('c', 1)
|
|
212
|
-
|
|
213
|
-
let lastValue: number | undefined
|
|
214
|
-
let numReactions = 0
|
|
215
|
-
|
|
216
|
-
react('', () => {
|
|
217
|
-
numReactions++
|
|
218
|
-
lastValue = atomA.get() + atomB.get() + unsafe__withoutCapture(() => atomC.get())
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
expect(lastValue).toBe(3)
|
|
222
|
-
expect(numReactions).toBe(1)
|
|
223
|
-
|
|
224
|
-
atomA.set(2)
|
|
225
|
-
|
|
226
|
-
expect(lastValue).toBe(4)
|
|
227
|
-
expect(numReactions).toBe(2)
|
|
228
|
-
|
|
229
|
-
atomB.set(2)
|
|
230
|
-
|
|
231
|
-
expect(lastValue).toBe(5)
|
|
232
|
-
expect(numReactions).toBe(3)
|
|
233
|
-
|
|
234
|
-
atomC.set(2)
|
|
235
|
-
|
|
236
|
-
// The reaction should not have run because C was not captured
|
|
237
|
-
expect(lastValue).toBe(5)
|
|
238
|
-
expect(numReactions).toBe(3)
|
|
239
|
-
})
|
|
240
359
|
})
|