@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,55 +1,37 @@
|
|
|
1
1
|
import { vi } from 'vitest'
|
|
2
2
|
import { atom } from '../Atom'
|
|
3
3
|
import { Computed, _Computed, computed, getComputedInstance, isUninitialized } from '../Computed'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { advanceGlobalEpoch, getGlobalEpoch
|
|
7
|
-
|
|
4
|
+
import { GLOBAL_START_EPOCH } from '../constants'
|
|
5
|
+
import { react } from '../EffectScheduler'
|
|
6
|
+
import { advanceGlobalEpoch, getGlobalEpoch } from '../transactions'
|
|
7
|
+
|
|
8
|
+
// Tests for SPEC.md §6 (computed signals).
|
|
9
|
+
// Rule IDs like [C2] in test names refer to that document.
|
|
8
10
|
|
|
9
11
|
function getLastCheckedEpoch(derivation: Computed<any>): number {
|
|
10
12
|
return (derivation as any).lastCheckedEpoch
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
describe('
|
|
14
|
-
it('
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const derivation = computed('',
|
|
18
|
-
|
|
19
|
-
expect(derive).toHaveBeenCalledTimes(0)
|
|
20
|
-
|
|
21
|
-
expect(derivation.get()).toBe(1)
|
|
22
|
-
expect(derivation.get()).toBe(1)
|
|
23
|
-
expect(derivation.get()).toBe(1)
|
|
24
|
-
|
|
25
|
-
expect(derive).toHaveBeenCalledTimes(1)
|
|
26
|
-
|
|
27
|
-
advanceGlobalEpoch()
|
|
28
|
-
advanceGlobalEpoch()
|
|
29
|
-
advanceGlobalEpoch()
|
|
30
|
-
advanceGlobalEpoch()
|
|
31
|
-
|
|
32
|
-
expect(derivation.get()).toBe(1)
|
|
33
|
-
expect(derivation.get()).toBe(1)
|
|
34
|
-
expect(derivation.get()).toBe(1)
|
|
35
|
-
advanceGlobalEpoch()
|
|
36
|
-
advanceGlobalEpoch()
|
|
37
|
-
expect(derivation.get()).toBe(1)
|
|
38
|
-
expect(derivation.get()).toBe(1)
|
|
15
|
+
describe('computed signals (C)', () => {
|
|
16
|
+
it('[C1] are lazy: the compute function does not run until the first get', () => {
|
|
17
|
+
const a = atom('a', 1)
|
|
18
|
+
const double = vi.fn(() => a.get() * 2)
|
|
19
|
+
const derivation = computed('double', double)
|
|
39
20
|
|
|
40
|
-
expect(
|
|
21
|
+
expect(double).toHaveBeenCalledTimes(0)
|
|
41
22
|
|
|
42
|
-
|
|
23
|
+
a.set(2)
|
|
24
|
+
expect(double).toHaveBeenCalledTimes(0)
|
|
43
25
|
|
|
44
|
-
expect(derivation.
|
|
26
|
+
expect(derivation.get()).toBe(4)
|
|
27
|
+
expect(double).toHaveBeenCalledTimes(1)
|
|
45
28
|
})
|
|
46
29
|
|
|
47
|
-
it('
|
|
30
|
+
it('[C2] recompute only when a parent has changed', () => {
|
|
48
31
|
const a = atom('', 1)
|
|
49
32
|
const double = vi.fn(() => a.get() * 2)
|
|
50
33
|
const derivation = computed('', double)
|
|
51
34
|
const startEpoch = getGlobalEpoch()
|
|
52
|
-
expect(double).toHaveBeenCalledTimes(0)
|
|
53
35
|
|
|
54
36
|
expect(derivation.get()).toBe(2)
|
|
55
37
|
expect(double).toHaveBeenCalledTimes(1)
|
|
@@ -65,6 +47,7 @@ describe('derivations', () => {
|
|
|
65
47
|
const nextEpoch = getGlobalEpoch()
|
|
66
48
|
expect(nextEpoch > startEpoch).toBe(true)
|
|
67
49
|
|
|
50
|
+
// lazy: no recomputation until deref
|
|
68
51
|
expect(double).toHaveBeenCalledTimes(1)
|
|
69
52
|
expect(derivation.lastChangedEpoch).toBe(startEpoch)
|
|
70
53
|
expect(derivation.get()).toBe(4)
|
|
@@ -72,94 +55,17 @@ describe('derivations', () => {
|
|
|
72
55
|
expect(double).toHaveBeenCalledTimes(2)
|
|
73
56
|
expect(derivation.lastChangedEpoch).toBe(nextEpoch)
|
|
74
57
|
|
|
75
|
-
|
|
76
|
-
expect(double).toHaveBeenCalledTimes(2)
|
|
77
|
-
expect(derivation.lastChangedEpoch).toBe(nextEpoch)
|
|
78
|
-
|
|
79
|
-
// creating an unrelated atom and setting it will have no effect
|
|
58
|
+
// changing an unrelated atom has no effect
|
|
80
59
|
const unrelatedAtom = atom('', 1)
|
|
81
60
|
unrelatedAtom.set(2)
|
|
82
61
|
unrelatedAtom.set(3)
|
|
83
|
-
unrelatedAtom.set(5)
|
|
84
62
|
|
|
85
63
|
expect(derivation.get()).toBe(4)
|
|
86
64
|
expect(double).toHaveBeenCalledTimes(2)
|
|
87
65
|
expect(derivation.lastChangedEpoch).toBe(nextEpoch)
|
|
88
66
|
})
|
|
89
67
|
|
|
90
|
-
it('
|
|
91
|
-
const startEpoch = getGlobalEpoch()
|
|
92
|
-
const a = atom('', 1)
|
|
93
|
-
|
|
94
|
-
const derivation = computed('', () => a.get() * 2, {
|
|
95
|
-
historyLength: 3,
|
|
96
|
-
computeDiff: (a, b) => {
|
|
97
|
-
return b - a
|
|
98
|
-
},
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
derivation.get()
|
|
102
|
-
|
|
103
|
-
expect(derivation.getDiffSince(startEpoch)).toHaveLength(0)
|
|
104
|
-
|
|
105
|
-
a.set(2)
|
|
106
|
-
|
|
107
|
-
expect(derivation.getDiffSince(startEpoch)).toEqual([+2])
|
|
108
|
-
|
|
109
|
-
a.set(3)
|
|
110
|
-
|
|
111
|
-
expect(derivation.getDiffSince(startEpoch)).toEqual([+2, +2])
|
|
112
|
-
|
|
113
|
-
a.set(5)
|
|
114
|
-
|
|
115
|
-
expect(derivation.getDiffSince(startEpoch)).toEqual([+2, +2, +4])
|
|
116
|
-
|
|
117
|
-
a.set(6)
|
|
118
|
-
// should fail now because we don't have enough hisstory
|
|
119
|
-
expect(derivation.getDiffSince(startEpoch)).toEqual(RESET_VALUE)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('doesnt update history if it doesnt change', () => {
|
|
123
|
-
const startEpoch = getGlobalEpoch()
|
|
124
|
-
const a = atom('', 1)
|
|
125
|
-
|
|
126
|
-
const floor = vi.fn((n: number) => Math.floor(n))
|
|
127
|
-
const derivation = computed('', () => floor(a.get()), {
|
|
128
|
-
historyLength: 3,
|
|
129
|
-
computeDiff: (a, b) => {
|
|
130
|
-
return b - a
|
|
131
|
-
},
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
expect(derivation.get()).toBe(1)
|
|
135
|
-
expect(derivation.getDiffSince(startEpoch)).toHaveLength(0)
|
|
136
|
-
|
|
137
|
-
a.set(1.2)
|
|
138
|
-
|
|
139
|
-
expect(derivation.get()).toBe(1)
|
|
140
|
-
expect(derivation.getDiffSince(startEpoch)).toHaveLength(0)
|
|
141
|
-
expect(floor).toHaveBeenCalledTimes(2)
|
|
142
|
-
|
|
143
|
-
a.set(1.5)
|
|
144
|
-
|
|
145
|
-
expect(derivation.get()).toBe(1)
|
|
146
|
-
expect(derivation.getDiffSince(startEpoch)).toHaveLength(0)
|
|
147
|
-
expect(floor).toHaveBeenCalledTimes(3)
|
|
148
|
-
|
|
149
|
-
a.set(1.9)
|
|
150
|
-
|
|
151
|
-
expect(derivation.get()).toBe(1)
|
|
152
|
-
expect(derivation.getDiffSince(startEpoch)).toHaveLength(0)
|
|
153
|
-
expect(floor).toHaveBeenCalledTimes(4)
|
|
154
|
-
|
|
155
|
-
a.set(2.3)
|
|
156
|
-
|
|
157
|
-
expect(derivation.get()).toBe(2)
|
|
158
|
-
expect(derivation.getDiffSince(startEpoch)).toEqual([+1])
|
|
159
|
-
expect(floor).toHaveBeenCalledTimes(5)
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('updates the lastCheckedEpoch whenever the globalEpoch advances', () => {
|
|
68
|
+
it('[C2] update their lastCheckedEpoch without recomputing when the epoch advances for unrelated reasons', () => {
|
|
163
69
|
const startEpoch = getGlobalEpoch()
|
|
164
70
|
const a = atom('', 1)
|
|
165
71
|
|
|
@@ -178,371 +84,154 @@ describe('derivations', () => {
|
|
|
178
84
|
expect(double).toHaveBeenCalledTimes(1)
|
|
179
85
|
})
|
|
180
86
|
|
|
181
|
-
it('
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const derivation = computed('',
|
|
87
|
+
it('[C3] never recompute if the first execution captured no parents', () => {
|
|
88
|
+
const derive = vi.fn(() => 1)
|
|
89
|
+
const startEpoch = getGlobalEpoch()
|
|
90
|
+
const derivation = computed('', derive)
|
|
185
91
|
|
|
186
|
-
expect(
|
|
92
|
+
expect(derive).toHaveBeenCalledTimes(0)
|
|
93
|
+
|
|
94
|
+
expect(derivation.get()).toBe(1)
|
|
95
|
+
expect(derivation.get()).toBe(1)
|
|
187
96
|
|
|
188
|
-
expect(
|
|
97
|
+
expect(derive).toHaveBeenCalledTimes(1)
|
|
189
98
|
|
|
190
|
-
|
|
99
|
+
advanceGlobalEpoch()
|
|
100
|
+
advanceGlobalEpoch()
|
|
191
101
|
|
|
192
|
-
expect(derivation.get()).toBe(
|
|
193
|
-
expect(
|
|
194
|
-
|
|
102
|
+
expect(derivation.get()).toBe(1)
|
|
103
|
+
expect(derivation.get()).toBe(1)
|
|
104
|
+
|
|
105
|
+
expect(derive).toHaveBeenCalledTimes(1)
|
|
106
|
+
|
|
107
|
+
expect(derivation.parents.length).toBe(0)
|
|
108
|
+
expect(derivation.lastChangedEpoch).toBe(startEpoch)
|
|
195
109
|
})
|
|
196
110
|
|
|
197
|
-
it('
|
|
198
|
-
const a = atom('', 1)
|
|
199
|
-
const
|
|
200
|
-
|
|
111
|
+
it('[C4] pass the previous value and the last-computed epoch to the compute function', () => {
|
|
112
|
+
const a = atom('a', 1)
|
|
113
|
+
const calls: Array<[unknown, number]> = []
|
|
114
|
+
const derivation = computed('', (prev, lastComputedEpoch) => {
|
|
115
|
+
calls.push([prev, lastComputedEpoch])
|
|
201
116
|
return a.get() * 2
|
|
202
117
|
})
|
|
203
|
-
const derivation = computed('', double)
|
|
204
118
|
|
|
205
119
|
expect(derivation.get()).toBe(2)
|
|
120
|
+
expect(isUninitialized(calls[0][0])).toBe(true)
|
|
121
|
+
expect(calls[0][1]).toBe(GLOBAL_START_EPOCH)
|
|
206
122
|
|
|
207
|
-
const
|
|
123
|
+
const firstComputedEpoch = getGlobalEpoch()
|
|
208
124
|
|
|
209
125
|
a.set(2)
|
|
210
126
|
|
|
211
127
|
expect(derivation.get()).toBe(4)
|
|
212
|
-
expect(
|
|
213
|
-
|
|
214
|
-
expect(
|
|
215
|
-
expect.assertions(6)
|
|
128
|
+
expect(isUninitialized(calls[1][0])).toBe(false)
|
|
129
|
+
expect(calls[1][0]).toBe(2)
|
|
130
|
+
expect(calls[1][1]).toBe(firstComputedEpoch)
|
|
216
131
|
})
|
|
217
132
|
|
|
218
|
-
it('
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
let numTimesComputed = 0
|
|
223
|
-
const fullName = computed('', () => {
|
|
224
|
-
numTimesComputed++
|
|
225
|
-
return `${firstName.get()} ${lastName.get()}`
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
let numTimesReacted = 0
|
|
229
|
-
let name = ''
|
|
230
|
-
const r = reactor('', () => {
|
|
231
|
-
name = fullName.get()
|
|
232
|
-
numTimesReacted++
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
expect(numTimesReacted).toBe(0)
|
|
236
|
-
expect(name).toBe('')
|
|
237
|
-
|
|
238
|
-
r.start()
|
|
239
|
-
|
|
240
|
-
expect(numTimesReacted).toBe(1)
|
|
241
|
-
expect(numTimesComputed).toBe(1)
|
|
242
|
-
expect(name).toBe('John Doe')
|
|
243
|
-
|
|
244
|
-
firstName.set('Jane')
|
|
133
|
+
it('[C5] keep the previous value and epoch when recomputation produces an equal value', () => {
|
|
134
|
+
const a = atom('a', 1.2)
|
|
135
|
+
const floored = computed('floored', () => Math.floor(a.get()))
|
|
245
136
|
|
|
246
|
-
expect(
|
|
247
|
-
|
|
248
|
-
expect(name).toBe('Jane Doe')
|
|
249
|
-
|
|
250
|
-
firstName.set('Jane')
|
|
251
|
-
firstName.set('Jane')
|
|
252
|
-
firstName.set('Jane')
|
|
253
|
-
|
|
254
|
-
expect(numTimesComputed).toBe(2)
|
|
255
|
-
expect(numTimesReacted).toBe(2)
|
|
256
|
-
expect(name).toBe('Jane Doe')
|
|
257
|
-
|
|
258
|
-
transact(() => {
|
|
259
|
-
firstName.set('Wilbur')
|
|
260
|
-
expect(numTimesComputed).toBe(2)
|
|
261
|
-
expect(numTimesReacted).toBe(2)
|
|
262
|
-
expect(name).toBe('Jane Doe')
|
|
263
|
-
lastName.set('Jones')
|
|
264
|
-
expect(numTimesComputed).toBe(2)
|
|
265
|
-
expect(numTimesReacted).toBe(2)
|
|
266
|
-
expect(name).toBe('Jane Doe')
|
|
267
|
-
expect(fullName.get()).toBe('Wilbur Jones')
|
|
268
|
-
|
|
269
|
-
expect(numTimesComputed).toBe(3)
|
|
270
|
-
expect(numTimesReacted).toBe(2)
|
|
271
|
-
expect(name).toBe('Jane Doe')
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
expect(numTimesComputed).toBe(3)
|
|
275
|
-
expect(numTimesReacted).toBe(3)
|
|
276
|
-
expect(name).toBe('Wilbur Jones')
|
|
277
|
-
})
|
|
137
|
+
expect(floored.get()).toBe(1)
|
|
138
|
+
const changedEpoch = floored.lastChangedEpoch
|
|
278
139
|
|
|
279
|
-
|
|
280
|
-
const firstName = atom('', 'John')
|
|
281
|
-
const lastName = atom('', 'Doe')
|
|
140
|
+
a.set(1.9)
|
|
282
141
|
|
|
283
|
-
|
|
142
|
+
expect(floored.get()).toBe(1)
|
|
143
|
+
expect(floored.lastChangedEpoch).toBe(changedEpoch)
|
|
284
144
|
|
|
285
|
-
|
|
286
|
-
firstName.set('Jane')
|
|
287
|
-
lastName.set('Jones')
|
|
288
|
-
expect(fullName.get()).toBe('Jane Jones')
|
|
289
|
-
rollback()
|
|
290
|
-
})
|
|
145
|
+
a.set(2.3)
|
|
291
146
|
|
|
292
|
-
expect(
|
|
147
|
+
expect(floored.get()).toBe(2)
|
|
148
|
+
expect(floored.lastChangedEpoch).toBeGreaterThan(changedEpoch)
|
|
293
149
|
})
|
|
294
150
|
|
|
295
|
-
it('
|
|
296
|
-
const
|
|
297
|
-
const b = atom('', 1)
|
|
151
|
+
it('[C5] never invoke isEqual for the first computation', () => {
|
|
152
|
+
const isEqual = vi.fn((a, b) => a === b)
|
|
298
153
|
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
computeDiff: (a, b) => b - a,
|
|
302
|
-
})
|
|
154
|
+
const a = atom('a', 1)
|
|
155
|
+
const b = computed('b', () => a.get() * 2, { isEqual })
|
|
303
156
|
|
|
304
|
-
|
|
157
|
+
expect(b.get()).toBe(2)
|
|
158
|
+
expect(isEqual).not.toHaveBeenCalled()
|
|
159
|
+
expect(b.get()).toBe(2)
|
|
160
|
+
expect(isEqual).not.toHaveBeenCalled()
|
|
305
161
|
|
|
306
|
-
|
|
307
|
-
expect(c.getDiffSince(startEpoch)).toEqual([])
|
|
308
|
-
a.set(2)
|
|
309
|
-
b.set(2)
|
|
310
|
-
expect(c.getDiffSince(startEpoch)).toEqual([+2])
|
|
311
|
-
rollback()
|
|
312
|
-
})
|
|
162
|
+
a.set(2)
|
|
313
163
|
|
|
314
|
-
expect(
|
|
164
|
+
expect(b.get()).toBe(4)
|
|
165
|
+
expect(isEqual).toHaveBeenCalledTimes(1)
|
|
166
|
+
expect(b.get()).toBe(4)
|
|
167
|
+
expect(isEqual).toHaveBeenCalledTimes(1)
|
|
315
168
|
})
|
|
316
169
|
|
|
317
|
-
it('
|
|
318
|
-
const a = atom('', 1)
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
historyLength: 3,
|
|
323
|
-
computeDiff: (a, b) => b - a,
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
expect(c.getDiffSince(getGlobalEpoch() - 1)).toEqual(RESET_VALUE)
|
|
327
|
-
})
|
|
328
|
-
})
|
|
170
|
+
it('[C6] stop propagation early in chains when an intermediate value does not change', () => {
|
|
171
|
+
const a = atom('a', 1.2)
|
|
172
|
+
const floored = computed('floored', () => Math.floor(a.get()))
|
|
173
|
+
const tens = vi.fn(() => floored.get() * 10)
|
|
174
|
+
const derivation = computed('tens', tens)
|
|
329
175
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
type: 'CHANGE'
|
|
333
|
-
path: string[]
|
|
334
|
-
value: any
|
|
335
|
-
oldValue: any
|
|
336
|
-
}
|
|
337
|
-
| { type: 'CREATE'; path: string[]; value: any }
|
|
338
|
-
| { type: 'REMOVE'; path: string[]; oldValue: any }
|
|
339
|
-
|
|
340
|
-
function getIncrementalRecordMapper<In, Out>(
|
|
341
|
-
obj: Signal<Record<string, In>, Difference[]>,
|
|
342
|
-
mapper: (t: In, k: string) => Out
|
|
343
|
-
): Computed<Record<string, Out>> {
|
|
344
|
-
function computeFromScratch() {
|
|
345
|
-
const input = obj.get()
|
|
346
|
-
return Object.fromEntries(Object.entries(input).map(([k, v]) => [k, mapper(v, k)]))
|
|
347
|
-
}
|
|
348
|
-
return computed('', (previousValue, lastComputedEpoch) => {
|
|
349
|
-
if (isUninitialized(previousValue)) {
|
|
350
|
-
return computeFromScratch()
|
|
351
|
-
}
|
|
352
|
-
const diff = obj.getDiffSince(lastComputedEpoch)
|
|
353
|
-
if (diff === RESET_VALUE) {
|
|
354
|
-
return computeFromScratch()
|
|
355
|
-
}
|
|
356
|
-
if (diff.length === 0) {
|
|
357
|
-
return previousValue
|
|
358
|
-
}
|
|
176
|
+
expect(derivation.get()).toBe(10)
|
|
177
|
+
expect(tens).toHaveBeenCalledTimes(1)
|
|
359
178
|
|
|
360
|
-
|
|
179
|
+
a.set(1.5)
|
|
361
180
|
|
|
362
|
-
|
|
181
|
+
expect(derivation.get()).toBe(10)
|
|
182
|
+
expect(tens).toHaveBeenCalledTimes(1)
|
|
363
183
|
|
|
364
|
-
|
|
365
|
-
for (const change of diff.flat()) {
|
|
366
|
-
const key = change.path[0] as string
|
|
367
|
-
if (changedKeys.has(key)) {
|
|
368
|
-
continue
|
|
369
|
-
}
|
|
370
|
-
switch (change.type) {
|
|
371
|
-
case 'CHANGE':
|
|
372
|
-
case 'CREATE':
|
|
373
|
-
changedKeys.add(key)
|
|
374
|
-
if (key in newUpstream) {
|
|
375
|
-
result[key] = mapper(newUpstream[key], change.path[0] as string)
|
|
376
|
-
} else {
|
|
377
|
-
// key was removed later in this patch
|
|
378
|
-
}
|
|
379
|
-
break
|
|
380
|
-
case 'REMOVE':
|
|
381
|
-
if (key in result) {
|
|
382
|
-
delete result[key]
|
|
383
|
-
}
|
|
384
|
-
break
|
|
385
|
-
default:
|
|
386
|
-
assertNever(change)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
184
|
+
a.set(2.5)
|
|
389
185
|
|
|
390
|
-
|
|
186
|
+
expect(derivation.get()).toBe(20)
|
|
187
|
+
expect(tens).toHaveBeenCalledTimes(2)
|
|
391
188
|
})
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
describe('incremental derivations', () => {
|
|
395
|
-
it('should be possible', () => {
|
|
396
|
-
type NumberMap = Record<string, number>
|
|
397
|
-
|
|
398
|
-
const nodes = atom<NumberMap, Difference[]>(
|
|
399
|
-
'',
|
|
400
|
-
{
|
|
401
|
-
a: 1,
|
|
402
|
-
b: 2,
|
|
403
|
-
c: 3,
|
|
404
|
-
d: 4,
|
|
405
|
-
e: 5,
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
historyLength: 10,
|
|
409
|
-
computeDiff: (valA, valB) => {
|
|
410
|
-
const result: Difference[] = []
|
|
411
|
-
for (const keyA in valA) {
|
|
412
|
-
if (!(keyA in valB)) {
|
|
413
|
-
result.push({
|
|
414
|
-
type: 'REMOVE',
|
|
415
|
-
oldValue: valA[keyA],
|
|
416
|
-
path: [keyA],
|
|
417
|
-
})
|
|
418
|
-
} else if (valA[keyA] != valB[keyA]) {
|
|
419
|
-
result.push({
|
|
420
|
-
type: 'CHANGE',
|
|
421
|
-
oldValue: valA[keyA],
|
|
422
|
-
path: [keyA],
|
|
423
|
-
value: valB[keyA],
|
|
424
|
-
})
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
for (const keyB in valB) {
|
|
429
|
-
if (!(keyB in valA)) {
|
|
430
|
-
result.push({
|
|
431
|
-
type: 'CREATE',
|
|
432
|
-
value: valB[keyB],
|
|
433
|
-
path: [keyB],
|
|
434
|
-
})
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return result
|
|
438
|
-
},
|
|
439
|
-
}
|
|
440
|
-
)
|
|
441
189
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
expect(doubledNodes.get()).toEqual({
|
|
447
|
-
a: 2,
|
|
448
|
-
b: 4,
|
|
449
|
-
c: 6,
|
|
450
|
-
d: 8,
|
|
451
|
-
e: 10,
|
|
452
|
-
})
|
|
453
|
-
expect(mapper).toHaveBeenCalledTimes(5)
|
|
454
|
-
|
|
455
|
-
nodes.update((ns) => ({ ...ns, a: 10 }))
|
|
456
|
-
|
|
457
|
-
expect(doubledNodes.get()).toEqual({
|
|
458
|
-
a: 20,
|
|
459
|
-
b: 4,
|
|
460
|
-
c: 6,
|
|
461
|
-
d: 8,
|
|
462
|
-
e: 10,
|
|
463
|
-
})
|
|
190
|
+
it('[C7] isActivelyListening is true exactly when something is listening downstream', () => {
|
|
191
|
+
const a = atom('a', 1)
|
|
192
|
+
const c = computed('c', () => a.get())
|
|
464
193
|
|
|
465
|
-
expect(
|
|
194
|
+
expect(c.isActivelyListening).toBe(false)
|
|
466
195
|
|
|
467
|
-
|
|
468
|
-
|
|
196
|
+
c.get()
|
|
197
|
+
expect(c.isActivelyListening).toBe(false)
|
|
469
198
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
b: 4,
|
|
473
|
-
c: 6,
|
|
474
|
-
e: 10,
|
|
199
|
+
const stop = react('r', () => {
|
|
200
|
+
c.get()
|
|
475
201
|
})
|
|
476
|
-
expect(
|
|
477
|
-
|
|
478
|
-
nodes.update((ns) => ({ ...ns, f: 50, g: 60 }))
|
|
202
|
+
expect(c.isActivelyListening).toBe(true)
|
|
479
203
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
b: 4,
|
|
483
|
-
c: 6,
|
|
484
|
-
e: 10,
|
|
485
|
-
f: 100,
|
|
486
|
-
g: 120,
|
|
487
|
-
})
|
|
488
|
-
expect(mapper).toHaveBeenCalledTimes(8)
|
|
489
|
-
|
|
490
|
-
nodes.set({ ...nodes.get() })
|
|
491
|
-
// no changes so no new calls to mapper
|
|
492
|
-
expect(doubledNodes.get()).toEqual({
|
|
493
|
-
a: 20,
|
|
494
|
-
b: 4,
|
|
495
|
-
c: 6,
|
|
496
|
-
e: 10,
|
|
497
|
-
f: 100,
|
|
498
|
-
g: 120,
|
|
499
|
-
})
|
|
500
|
-
expect(mapper).toHaveBeenCalledTimes(8)
|
|
501
|
-
|
|
502
|
-
// make several changes
|
|
503
|
-
|
|
504
|
-
nodes.update((ns) => ({ ...ns, a: 1 }))
|
|
505
|
-
nodes.update((ns) => ({ ...ns, b: 9 }))
|
|
506
|
-
nodes.update((ns) => ({ ...ns, c: 17 }))
|
|
507
|
-
nodes.update(({ f: _f, g: _g, ...others }) => ({ ...others }))
|
|
508
|
-
nodes.update((ns) => ({ ...ns, d: 4 }))
|
|
509
|
-
nodes.update((ns) => ({ ...ns, a: 4 }))
|
|
510
|
-
|
|
511
|
-
// nothing was called because we didn't deref yet
|
|
512
|
-
expect(mapper).toHaveBeenCalledTimes(8)
|
|
513
|
-
|
|
514
|
-
expect(doubledNodes.get()).toEqual({
|
|
515
|
-
a: 8,
|
|
516
|
-
b: 18,
|
|
517
|
-
c: 34,
|
|
518
|
-
d: 8,
|
|
519
|
-
e: 10,
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
expect(mapper).toHaveBeenCalledTimes(12)
|
|
204
|
+
stop()
|
|
205
|
+
expect(c.isActivelyListening).toBe(false)
|
|
523
206
|
})
|
|
524
207
|
})
|
|
525
208
|
|
|
526
|
-
describe('computed
|
|
527
|
-
it('
|
|
209
|
+
describe('the computed decorator (C8, C9, C10)', () => {
|
|
210
|
+
it('[C8] makes a class method behave as a cached, reactive computed', () => {
|
|
211
|
+
const compute = vi.fn(function (this: Foo) {
|
|
212
|
+
return this.a.get() * 2
|
|
213
|
+
})
|
|
528
214
|
class Foo {
|
|
529
215
|
a = atom('a', 1)
|
|
530
216
|
@computed
|
|
531
217
|
getB() {
|
|
532
|
-
return
|
|
218
|
+
return compute.call(this)
|
|
533
219
|
}
|
|
534
220
|
}
|
|
535
221
|
|
|
536
222
|
const foo = new Foo()
|
|
537
223
|
|
|
538
224
|
expect(foo.getB()).toBe(2)
|
|
225
|
+
expect(foo.getB()).toBe(2)
|
|
226
|
+
expect(compute).toHaveBeenCalledTimes(1)
|
|
539
227
|
|
|
540
228
|
foo.a.set(2)
|
|
541
229
|
|
|
542
230
|
expect(foo.getB()).toBe(4)
|
|
231
|
+
expect(compute).toHaveBeenCalledTimes(2)
|
|
543
232
|
})
|
|
544
233
|
|
|
545
|
-
it('
|
|
234
|
+
it('[C8] honors options passed to the decorator', () => {
|
|
546
235
|
let numComputations = 0
|
|
547
236
|
class Foo {
|
|
548
237
|
a = atom('a', 1)
|
|
@@ -564,48 +253,84 @@ describe('computed as a decorator', () => {
|
|
|
564
253
|
const secondVal = foo.getB()
|
|
565
254
|
expect(secondVal).toEqual({ b: 1 })
|
|
566
255
|
|
|
256
|
+
// [EQ4] the previous value object is retained when the new value is equal
|
|
567
257
|
expect(firstVal).toBe(secondVal)
|
|
568
258
|
expect(numComputations).toBe(2)
|
|
569
259
|
})
|
|
570
|
-
})
|
|
571
260
|
|
|
572
|
-
|
|
573
|
-
it('can retrieve the underlying computed instance', () => {
|
|
261
|
+
it('[C8] creates a separate computed per instance', () => {
|
|
574
262
|
class Foo {
|
|
575
263
|
a = atom('a', 1)
|
|
264
|
+
@computed
|
|
265
|
+
getB() {
|
|
266
|
+
return this.a.get() * 2
|
|
267
|
+
}
|
|
268
|
+
}
|
|
576
269
|
|
|
577
|
-
|
|
270
|
+
const foo1 = new Foo()
|
|
271
|
+
const foo2 = new Foo()
|
|
272
|
+
|
|
273
|
+
foo1.a.set(10)
|
|
274
|
+
|
|
275
|
+
expect(foo1.getB()).toBe(20)
|
|
276
|
+
expect(foo2.getB()).toBe(2)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('[C9] getComputedInstance retrieves the underlying computed, creating it on demand', () => {
|
|
280
|
+
class Foo {
|
|
281
|
+
a = atom('a', 1)
|
|
282
|
+
|
|
283
|
+
@computed
|
|
578
284
|
getB() {
|
|
579
|
-
return
|
|
285
|
+
return this.a.get() * 2
|
|
580
286
|
}
|
|
581
287
|
}
|
|
582
288
|
|
|
583
289
|
const foo = new Foo()
|
|
584
290
|
|
|
291
|
+
// the method has not been called yet
|
|
585
292
|
const bInst = getComputedInstance(foo, 'getB')
|
|
586
293
|
|
|
587
294
|
expect(bInst).toBeDefined()
|
|
588
295
|
expect(bInst).toBeInstanceOf(_Computed)
|
|
296
|
+
expect(bInst.get()).toBe(2)
|
|
297
|
+
|
|
298
|
+
foo.a.set(2)
|
|
299
|
+
expect(bInst.get()).toBe(4)
|
|
589
300
|
})
|
|
590
|
-
})
|
|
591
301
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const isEqual = vi.fn((a, b) => a === b)
|
|
302
|
+
it('[C10] works on getters (legacy decorators) but logs a one-time deprecation warning', () => {
|
|
303
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
595
304
|
|
|
596
|
-
|
|
597
|
-
|
|
305
|
+
class Foo {
|
|
306
|
+
a = atom('a', 1)
|
|
307
|
+
// eslint-disable-next-line tldraw/no-setter-getter
|
|
308
|
+
@computed
|
|
309
|
+
get b() {
|
|
310
|
+
return this.a.get() * 2
|
|
311
|
+
}
|
|
312
|
+
}
|
|
598
313
|
|
|
599
|
-
expect(
|
|
600
|
-
expect(
|
|
601
|
-
expect(b.get()).toBe(2)
|
|
602
|
-
expect(isEqual).not.toHaveBeenCalled()
|
|
314
|
+
expect(warn).toHaveBeenCalledTimes(1)
|
|
315
|
+
expect(warn.mock.calls[0][0]).toContain('deprecated')
|
|
603
316
|
|
|
604
|
-
|
|
317
|
+
const foo = new Foo()
|
|
318
|
+
expect(foo.b).toBe(2)
|
|
319
|
+
foo.a.set(2)
|
|
320
|
+
expect(foo.b).toBe(4)
|
|
605
321
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
322
|
+
// the warning is logged once per process, not once per class
|
|
323
|
+
class Bar {
|
|
324
|
+
a = atom('a', 1)
|
|
325
|
+
// eslint-disable-next-line tldraw/no-setter-getter
|
|
326
|
+
@computed
|
|
327
|
+
get b() {
|
|
328
|
+
return this.a.get()
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
expect(new Bar().b).toBe(1)
|
|
332
|
+
expect(warn).toHaveBeenCalledTimes(1)
|
|
333
|
+
|
|
334
|
+
warn.mockRestore()
|
|
610
335
|
})
|
|
611
336
|
})
|