@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.
@@ -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 { reactor } from '../EffectScheduler'
5
- import { assertNever } from '../helpers'
6
- import { advanceGlobalEpoch, getGlobalEpoch, transact, transaction } from '../transactions'
7
- import { RESET_VALUE, Signal } from '../types'
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('derivations', () => {
14
- it('will cache a value forever if it has no parents', () => {
15
- const derive = vi.fn(() => 1)
16
- const startEpoch = getGlobalEpoch()
17
- const derivation = computed('', derive)
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(derive).toHaveBeenCalledTimes(1)
21
+ expect(double).toHaveBeenCalledTimes(0)
41
22
 
42
- expect(derivation.parents.length).toBe(0)
23
+ a.set(2)
24
+ expect(double).toHaveBeenCalledTimes(0)
43
25
 
44
- expect(derivation.lastChangedEpoch).toBe(startEpoch)
26
+ expect(derivation.get()).toBe(4)
27
+ expect(double).toHaveBeenCalledTimes(1)
45
28
  })
46
29
 
47
- it('will update when parent atoms update', () => {
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
- expect(derivation.get()).toBe(4)
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('supports history', () => {
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('receives UNINTIALIZED as the previousValue the first time it computes', () => {
182
- const a = atom('', 1)
183
- const double = vi.fn((_prevValue) => a.get() * 2)
184
- const derivation = computed('', double)
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(derivation.get()).toBe(2)
92
+ expect(derive).toHaveBeenCalledTimes(0)
93
+
94
+ expect(derivation.get()).toBe(1)
95
+ expect(derivation.get()).toBe(1)
187
96
 
188
- expect(isUninitialized(double.mock.calls[0][0])).toBe(true)
97
+ expect(derive).toHaveBeenCalledTimes(1)
189
98
 
190
- a.set(2)
99
+ advanceGlobalEpoch()
100
+ advanceGlobalEpoch()
191
101
 
192
- expect(derivation.get()).toBe(4)
193
- expect(isUninitialized(double.mock.calls[1][0])).toBe(false)
194
- expect(double.mock.calls[1][0]).toBe(2)
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('receives the lastChangedEpoch as the second parameter each time it recomputes', () => {
198
- const a = atom('', 1)
199
- const double = vi.fn((_prevValue, lastChangedEpoch) => {
200
- expect(lastChangedEpoch).toBe(derivation.lastChangedEpoch)
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 startEpoch = getGlobalEpoch()
123
+ const firstComputedEpoch = getGlobalEpoch()
208
124
 
209
125
  a.set(2)
210
126
 
211
127
  expect(derivation.get()).toBe(4)
212
- expect(derivation.lastChangedEpoch).toBeGreaterThan(startEpoch)
213
-
214
- expect(double).toHaveBeenCalledTimes(2)
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('can be reacted to', () => {
219
- const firstName = atom('', 'John')
220
- const lastName = atom('', 'Doe')
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(numTimesComputed).toBe(2)
247
- expect(numTimesReacted).toBe(2)
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
- it('will roll back to their initial value if a transaciton is aborted', () => {
280
- const firstName = atom('', 'John')
281
- const lastName = atom('', 'Doe')
140
+ a.set(1.9)
282
141
 
283
- const fullName = computed('', () => `${firstName.get()} ${lastName.get()}`)
142
+ expect(floored.get()).toBe(1)
143
+ expect(floored.lastChangedEpoch).toBe(changedEpoch)
284
144
 
285
- transaction((rollback) => {
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(fullName.get()).toBe('John Doe')
147
+ expect(floored.get()).toBe(2)
148
+ expect(floored.lastChangedEpoch).toBeGreaterThan(changedEpoch)
293
149
  })
294
150
 
295
- it('will add history items if a transaction is aborted', () => {
296
- const a = atom('', 1)
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 c = computed('', () => a.get() + b.get(), {
300
- historyLength: 3,
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
- const startEpoch = getGlobalEpoch()
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
- transaction((rollback) => {
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(c.getDiffSince(startEpoch)).toEqual([2, -2])
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('will return RESET_VALUE if .getDiffSince is called with an epoch before initialization', () => {
318
- const a = atom('', 1)
319
- const b = atom('', 1)
320
-
321
- const c = computed('', () => a.get() + b.get(), {
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
- type Difference =
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
- const newUpstream = obj.get()
179
+ a.set(1.5)
361
180
 
362
- const result = { ...previousValue } as Record<string, Out>
181
+ expect(derivation.get()).toBe(10)
182
+ expect(tens).toHaveBeenCalledTimes(1)
363
183
 
364
- const changedKeys = new Set<string>()
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
- return result
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
- const mapper = vi.fn((val) => val * 2)
443
-
444
- const doubledNodes = getIncrementalRecordMapper(nodes, mapper)
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(mapper).toHaveBeenCalledTimes(6)
194
+ expect(c.isActivelyListening).toBe(false)
466
195
 
467
- // remove d
468
- nodes.update(({ d: _d, ...others }) => others)
196
+ c.get()
197
+ expect(c.isActivelyListening).toBe(false)
469
198
 
470
- expect(doubledNodes.get()).toEqual({
471
- a: 20,
472
- b: 4,
473
- c: 6,
474
- e: 10,
199
+ const stop = react('r', () => {
200
+ c.get()
475
201
  })
476
- expect(mapper).toHaveBeenCalledTimes(6)
477
-
478
- nodes.update((ns) => ({ ...ns, f: 50, g: 60 }))
202
+ expect(c.isActivelyListening).toBe(true)
479
203
 
480
- expect(doubledNodes.get()).toEqual({
481
- a: 20,
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 as a decorator', () => {
527
- it('can be used to decorate a class', () => {
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 this.a.get() * 2
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('can be used to decorate a class with custom properties', () => {
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
- describe(getComputedInstance, () => {
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
- @computed({ isEqual: (a, b) => a.b === b.b })
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 { b: this.a.get() * this.a.get() }
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
- describe('computed isEqual', () => {
593
- it('does not get called for the initialization', () => {
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
- const a = atom('a', 1)
597
- const b = computed('b', () => a.get() * 2, { isEqual })
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(b.get()).toBe(2)
600
- expect(isEqual).not.toHaveBeenCalled()
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
- a.set(2)
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
- expect(b.get()).toBe(4)
607
- expect(isEqual).toHaveBeenCalledTimes(1)
608
- expect(b.get()).toBe(4)
609
- expect(isEqual).toHaveBeenCalledTimes(1)
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
  })