@tldraw/state 5.3.2 → 5.4.0-canary.02cd0bd3b597
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 +64 -63
- package/README.md +35 -36
- package/dist-cjs/index.d.ts +26 -27
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/ArraySet.js +47 -144
- package/dist-cjs/lib/ArraySet.js.map +2 -2
- package/dist-cjs/lib/Atom.js +12 -26
- package/dist-cjs/lib/Atom.js.map +2 -2
- package/dist-cjs/lib/Computed.js +36 -64
- package/dist-cjs/lib/Computed.js.map +2 -2
- package/dist-cjs/lib/EffectScheduler.js +1 -1
- package/dist-cjs/lib/EffectScheduler.js.map +2 -2
- package/dist-cjs/lib/HistoryBuffer.js +8 -8
- package/dist-cjs/lib/HistoryBuffer.js.map +2 -2
- package/dist-cjs/lib/capture.js +1 -3
- package/dist-cjs/lib/capture.js.map +2 -2
- package/dist-cjs/lib/constants.js.map +2 -2
- package/dist-cjs/lib/helpers.js +3 -11
- package/dist-cjs/lib/helpers.js.map +2 -2
- package/dist-cjs/lib/localStorageAtom.js +7 -2
- package/dist-cjs/lib/localStorageAtom.js.map +2 -2
- package/dist-cjs/lib/transactions.js +11 -19
- package/dist-cjs/lib/transactions.js.map +2 -2
- package/dist-cjs/lib/types.js.map +1 -1
- package/dist-cjs/lib/warnings.js +2 -4
- package/dist-cjs/lib/warnings.js.map +2 -2
- package/dist-esm/index.d.mts +26 -27
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ArraySet.mjs +47 -144
- package/dist-esm/lib/ArraySet.mjs.map +2 -2
- package/dist-esm/lib/Atom.mjs +12 -26
- package/dist-esm/lib/Atom.mjs.map +2 -2
- package/dist-esm/lib/Computed.mjs +36 -64
- package/dist-esm/lib/Computed.mjs.map +2 -2
- package/dist-esm/lib/EffectScheduler.mjs +1 -1
- package/dist-esm/lib/EffectScheduler.mjs.map +2 -2
- package/dist-esm/lib/HistoryBuffer.mjs +8 -8
- package/dist-esm/lib/HistoryBuffer.mjs.map +2 -2
- package/dist-esm/lib/capture.mjs +1 -3
- package/dist-esm/lib/capture.mjs.map +2 -2
- package/dist-esm/lib/constants.mjs.map +2 -2
- package/dist-esm/lib/helpers.mjs +3 -11
- package/dist-esm/lib/helpers.mjs.map +2 -2
- package/dist-esm/lib/localStorageAtom.mjs +7 -2
- package/dist-esm/lib/localStorageAtom.mjs.map +2 -2
- package/dist-esm/lib/transactions.mjs +11 -19
- package/dist-esm/lib/transactions.mjs.map +2 -2
- package/dist-esm/lib/types.mjs.map +1 -1
- package/dist-esm/lib/warnings.mjs +2 -4
- package/dist-esm/lib/warnings.mjs.map +2 -2
- package/package.json +2 -2
- package/src/lib/ArraySet.ts +68 -176
- package/src/lib/Atom.ts +27 -31
- package/src/lib/Computed.ts +68 -96
- package/src/lib/EffectScheduler.ts +9 -8
- package/src/lib/HistoryBuffer.ts +12 -10
- package/src/lib/__tests__/ArraySet.test.ts +39 -13
- package/src/lib/__tests__/EffectScheduler.test.ts +18 -0
- package/src/lib/__tests__/HistoryBuffer.test.ts +6 -3
- package/src/lib/__tests__/computed.test.ts +75 -0
- package/src/lib/__tests__/errors.test.ts +24 -0
- package/src/lib/__tests__/helpers.test.ts +7 -11
- package/src/lib/__tests__/history.test.ts +32 -2
- package/src/lib/__tests__/localStorageAtom.test.ts +15 -0
- package/src/lib/capture.ts +13 -13
- package/src/lib/constants.ts +3 -22
- package/src/lib/helpers.ts +15 -140
- package/src/lib/localStorageAtom.ts +9 -2
- package/src/lib/transactions.ts +23 -47
- package/src/lib/types.ts +7 -7
- package/src/lib/warnings.ts +2 -10
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { exhaustiveSwitchError } from '@tldraw/utils'
|
|
1
2
|
import { vi } from 'vitest'
|
|
2
3
|
import { atom } from '../Atom'
|
|
3
4
|
import { Computed, UNINITIALIZED, computed, isUninitialized, withDiff } from '../Computed'
|
|
4
5
|
import { react } from '../EffectScheduler'
|
|
5
|
-
import { EMPTY_ARRAY
|
|
6
|
+
import { EMPTY_ARRAY } from '../helpers'
|
|
6
7
|
import { getGlobalEpoch, transact, transaction } from '../transactions'
|
|
7
8
|
import { RESET_VALUE, Signal } from '../types'
|
|
8
9
|
|
|
@@ -78,6 +79,35 @@ describe('atom history (H)', () => {
|
|
|
78
79
|
expect(calls).toEqual([[1, 5, epochBeforeSet, getGlobalEpoch()]])
|
|
79
80
|
})
|
|
80
81
|
|
|
82
|
+
it('[H2][C2] a computed read inside computeDiff still sees the change afterwards', () => {
|
|
83
|
+
// computeDiff runs before the epoch ticks, so a dependent computed it reads is not stamped as
|
|
84
|
+
// checked at the epoch of the write that is still in progress.
|
|
85
|
+
const a = atom('', 1, {
|
|
86
|
+
historyLength: 10,
|
|
87
|
+
computeDiff: (prev, next) => {
|
|
88
|
+
c.get()
|
|
89
|
+
return next - prev
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
const c: Computed<number> = computed('', () => a.get() * 2)
|
|
93
|
+
const seen: number[] = []
|
|
94
|
+
react('', () => seen.push(c.get()))
|
|
95
|
+
|
|
96
|
+
a.set(2)
|
|
97
|
+
|
|
98
|
+
expect(c.get()).toBe(4)
|
|
99
|
+
expect(seen).toEqual([2, 4])
|
|
100
|
+
expect(a.getDiffSince(a.lastChangedEpoch - 1)).toEqual([1])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('[H2] records an explicit null diff rather than treating it as missing', () => {
|
|
104
|
+
const a = atom<number, null | number>('', 1, { historyLength: 3 })
|
|
105
|
+
const startEpoch = a.lastChangedEpoch
|
|
106
|
+
a.set(2, 1)
|
|
107
|
+
a.set(3, null)
|
|
108
|
+
expect(a.getDiffSince(startEpoch)).toEqual([1, null])
|
|
109
|
+
})
|
|
110
|
+
|
|
81
111
|
it('[H4] clears the history buffer if no diff can be determined', () => {
|
|
82
112
|
const a = atom('', 1, { historyLength: 3 })
|
|
83
113
|
const startEpoch = getGlobalEpoch()
|
|
@@ -383,7 +413,7 @@ function getIncrementalRecordMapper<In, Out>(
|
|
|
383
413
|
}
|
|
384
414
|
break
|
|
385
415
|
default:
|
|
386
|
-
|
|
416
|
+
exhaustiveSwitchError(change)
|
|
387
417
|
}
|
|
388
418
|
}
|
|
389
419
|
|
|
@@ -260,4 +260,19 @@ describe('localStorageAtom', () => {
|
|
|
260
260
|
cleanup2()
|
|
261
261
|
})
|
|
262
262
|
})
|
|
263
|
+
|
|
264
|
+
describe('outside the browser', () => {
|
|
265
|
+
it('[LS6] does not throw without a window and returns a working atom', () => {
|
|
266
|
+
vi.stubGlobal('window', undefined)
|
|
267
|
+
try {
|
|
268
|
+
const [atom, cleanup] = localStorageAtom('test-key', 'initial-value')
|
|
269
|
+
expect(atom.get()).toBe('initial-value')
|
|
270
|
+
atom.set('next')
|
|
271
|
+
expect(atom.get()).toBe('next')
|
|
272
|
+
expect(() => cleanup()).not.toThrow()
|
|
273
|
+
} finally {
|
|
274
|
+
vi.unstubAllGlobals()
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
})
|
|
263
278
|
})
|
package/src/lib/capture.ts
CHANGED
|
@@ -24,10 +24,10 @@ const inst = singleton('capture', () => ({ stack: null as null | CaptureStackFra
|
|
|
24
24
|
* @example
|
|
25
25
|
* ```ts
|
|
26
26
|
* const name = atom('name', 'Sam')
|
|
27
|
-
* const time = atom('time',
|
|
27
|
+
* const time = atom('time', Date.now())
|
|
28
28
|
*
|
|
29
29
|
* setInterval(() => {
|
|
30
|
-
* time.set(
|
|
30
|
+
* time.set(Date.now())
|
|
31
31
|
* })
|
|
32
32
|
*
|
|
33
33
|
* react('log name changes', () => {
|
|
@@ -149,21 +149,20 @@ export function stopCapturingParents() {
|
|
|
149
149
|
*/
|
|
150
150
|
export function maybeCaptureParent(p: Signal<any, any>) {
|
|
151
151
|
if (inst.stack) {
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
// if the child did deref this parent already during this capture session then 0 <= idx < stack.offset
|
|
157
|
-
|
|
158
|
-
if (wasCapturedAlready) {
|
|
152
|
+
// `add` returns false when the parent was already captured this run. In array mode both
|
|
153
|
+
// `has` and `add` scan with indexOf, so going straight to `add` halves the scans per
|
|
154
|
+
// captured parent.
|
|
155
|
+
if (!inst.stack.child.parentSet.add(p)) {
|
|
159
156
|
return
|
|
160
157
|
}
|
|
161
158
|
|
|
162
|
-
inst.stack.child.parentSet.add(p)
|
|
163
159
|
if (inst.stack.child.isActivelyListening) {
|
|
164
160
|
attach(p, inst.stack.child)
|
|
165
161
|
}
|
|
166
162
|
|
|
163
|
+
// Parents are recorded in deref order. If the slot at this offset held a different parent
|
|
164
|
+
// last run, that parent may have moved later in the order or been dropped; only
|
|
165
|
+
// stopCapturingParents can tell, so remember it for then.
|
|
167
166
|
if (inst.stack.offset < inst.stack.child.parents.length) {
|
|
168
167
|
const maybeRemovedParent = inst.stack.child.parents[inst.stack.offset]
|
|
169
168
|
if (maybeRemovedParent !== p) {
|
|
@@ -183,7 +182,8 @@ export function maybeCaptureParent(p: Signal<any, any>) {
|
|
|
183
182
|
|
|
184
183
|
/**
|
|
185
184
|
* A debugging tool that tells you why a computed signal or effect is running.
|
|
186
|
-
* Call in the body of a computed signal or effect function.
|
|
185
|
+
* Call in the body of a computed signal or effect function. Nothing is logged for the run that
|
|
186
|
+
* calls it; from the next run on, each run logs the ancestors that changed.
|
|
187
187
|
*
|
|
188
188
|
* @example
|
|
189
189
|
* ```ts
|
|
@@ -195,8 +195,8 @@ export function maybeCaptureParent(p: Signal<any, any>) {
|
|
|
195
195
|
*
|
|
196
196
|
* name.set('Alice')
|
|
197
197
|
*
|
|
198
|
-
* //
|
|
199
|
-
* //
|
|
198
|
+
* // Effect(greeting) is executing because:
|
|
199
|
+
* // ↳ Atom(name) changed
|
|
200
200
|
* ```
|
|
201
201
|
*
|
|
202
202
|
* @public
|
package/src/lib/constants.ts
CHANGED
|
@@ -1,26 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Sentinel below any real epoch. Derived signals and effects start with their epochs set to this
|
|
3
|
+
* so they are dirty until their first run; real epochs start at `GLOBAL_START_EPOCH + 1`.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* be computed/executed at least once when first accessed or started. The value -1 is used because:
|
|
6
|
-
* - Global epoch starts at 0 (GLOBAL_START_EPOCH + 1)
|
|
7
|
-
* - Any derived signal initialized with GLOBAL_START_EPOCH (-1) will be considered dirty when compared to any positive epoch
|
|
8
|
-
* - This forces initial computation/execution without requiring special initialization logic
|
|
9
|
-
*
|
|
10
|
-
* Used by:
|
|
11
|
-
* - Computed signals to track when they were last changed
|
|
12
|
-
* - Effect schedulers to track when they were last executed
|
|
13
|
-
* - Transaction system for initial global and reaction epoch values
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* // In Computed class constructor
|
|
18
|
-
* lastChangedEpoch = GLOBAL_START_EPOCH // -1, marking as dirty
|
|
19
|
-
*
|
|
20
|
-
* // When global epoch is 5, this computed will be dirty since -1 < 5
|
|
21
|
-
* const needsComputation = this.lastChangedEpoch < globalEpoch
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* @public
|
|
5
|
+
* @internal
|
|
25
6
|
*/
|
|
26
7
|
export const GLOBAL_START_EPOCH = -1
|
package/src/lib/helpers.ts
CHANGED
|
@@ -1,42 +1,23 @@
|
|
|
1
1
|
import { Child, Signal } from './types'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Get whether the given value is a child.
|
|
5
|
-
*
|
|
6
|
-
* @param x The value to check.
|
|
7
|
-
* @returns True if the value is a child, false otherwise.
|
|
8
|
-
* @internal
|
|
9
|
-
*/
|
|
10
3
|
function isChild(x: any): x is Child {
|
|
11
4
|
return x && typeof x === 'object' && 'parents' in x
|
|
12
5
|
}
|
|
13
6
|
|
|
14
7
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* This function is used internally to determine if a computed signal or effect needs to
|
|
19
|
-
* be re-evaluated because one of its dependencies has changed.
|
|
8
|
+
* Whether any of the child's parents changed since the child last recorded their epochs. O(parents);
|
|
9
|
+
* returns at the first changed parent, so parents after it are not brought up to date.
|
|
20
10
|
*
|
|
21
|
-
* @param child - The child (computed signal or effect) to check for parent changes
|
|
22
|
-
* @returns `true` if any parent signal has changed since the child last observed it, `false` otherwise
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* const childSignal = computed('child', () => parentAtom.get())
|
|
26
|
-
* // Check if the child needs to recompute
|
|
27
|
-
* if (haveParentsChanged(childSignal)) {
|
|
28
|
-
* // Recompute the child's value
|
|
29
|
-
* }
|
|
30
|
-
* ```
|
|
31
11
|
* @internal
|
|
32
12
|
*/
|
|
33
13
|
export function haveParentsChanged(child: Child): boolean {
|
|
34
14
|
for (let i = 0, n = child.parents.length; i < n; i++) {
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
const parent = child.parents[i]
|
|
16
|
+
// Bring the parent up to date first: a computed parent's `lastChangedEpoch` only moves when
|
|
17
|
+
// it is re-derived.
|
|
18
|
+
parent.__unsafe__getWithoutCapture(true)
|
|
37
19
|
|
|
38
|
-
|
|
39
|
-
if (child.parents[i].lastChangedEpoch !== child.parentEpochs[i]) {
|
|
20
|
+
if (parent.lastChangedEpoch !== child.parentEpochs[i]) {
|
|
40
21
|
return true
|
|
41
22
|
}
|
|
42
23
|
}
|
|
@@ -45,32 +26,17 @@ export function haveParentsChanged(child: Child): boolean {
|
|
|
45
26
|
}
|
|
46
27
|
|
|
47
28
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* This function is used internally to clean up the dependency graph when signals are no
|
|
53
|
-
* longer needed or when dependencies change.
|
|
29
|
+
* Removes `child` from `parent.children`. A computed parent that loses its last child stops
|
|
30
|
+
* listening itself, recursively, so a signal's `children` set is empty whenever nothing downstream
|
|
31
|
+
* is actively listening.
|
|
54
32
|
*
|
|
55
|
-
* @param parent - The parent signal to detach from
|
|
56
|
-
* @param child - The child signal to detach
|
|
57
|
-
* @example
|
|
58
|
-
* ```ts
|
|
59
|
-
* // When a computed signal's dependencies change
|
|
60
|
-
* const oldParent = atom('old', 1)
|
|
61
|
-
* const child = computed('child', () => oldParent.get())
|
|
62
|
-
* // Later, detach the child from the old parent
|
|
63
|
-
* detach(oldParent, child)
|
|
64
|
-
* ```
|
|
65
33
|
* @internal
|
|
66
34
|
*/
|
|
67
35
|
export function detach(parent: Signal<any>, child: Child) {
|
|
68
|
-
// If the child is not attached to the parent, do nothing.
|
|
69
36
|
if (!parent.children.remove(child)) {
|
|
70
37
|
return
|
|
71
38
|
}
|
|
72
39
|
|
|
73
|
-
// If the parent has no more children, then detach the parent from its parents.
|
|
74
40
|
if (parent.children.isEmpty && isChild(parent)) {
|
|
75
41
|
for (let i = 0, n = parent.parents.length; i < n; i++) {
|
|
76
42
|
detach(parent.parents[i], parent)
|
|
@@ -79,32 +45,16 @@ export function detach(parent: Signal<any>, child: Child) {
|
|
|
79
45
|
}
|
|
80
46
|
|
|
81
47
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* attach to its own parents to maintain the dependency chain.
|
|
48
|
+
* Adds `child` to `parent.children`. A computed parent that gains its first child starts
|
|
49
|
+
* listening itself, recursively, so that changes to any ancestor are traversed down to the child.
|
|
85
50
|
*
|
|
86
|
-
* This function is used internally when dependencies are captured during computed signal
|
|
87
|
-
* evaluation or effect execution.
|
|
88
|
-
*
|
|
89
|
-
* @param parent - The parent signal to attach to
|
|
90
|
-
* @param child - The child signal to attach
|
|
91
|
-
* @example
|
|
92
|
-
* ```ts
|
|
93
|
-
* // When a computed signal captures a new dependency
|
|
94
|
-
* const parentAtom = atom('parent', 1)
|
|
95
|
-
* const child = computed('child', () => parentAtom.get())
|
|
96
|
-
* // Internally, attach is called to establish the dependency
|
|
97
|
-
* attach(parentAtom, child)
|
|
98
|
-
* ```
|
|
99
51
|
* @internal
|
|
100
52
|
*/
|
|
101
53
|
export function attach(parent: Signal<any>, child: Child) {
|
|
102
|
-
// If the child is already attached to the parent, do nothing.
|
|
103
54
|
if (!parent.children.add(child)) {
|
|
104
55
|
return
|
|
105
56
|
}
|
|
106
57
|
|
|
107
|
-
// If the parent itself is a child, add the parent to the parent's parents.
|
|
108
58
|
if (isChild(parent)) {
|
|
109
59
|
for (let i = 0, n = parent.parents.length; i < n; i++) {
|
|
110
60
|
attach(parent.parents[i], parent)
|
|
@@ -113,25 +63,10 @@ export function attach(parent: Signal<any>, child: Child) {
|
|
|
113
63
|
}
|
|
114
64
|
|
|
115
65
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* 1. Reference equality (`===`)
|
|
120
|
-
* 2. `Object.is()` equality (handles NaN and -0/+0 cases)
|
|
121
|
-
* 3. Custom `.equals()` method when the left-hand value provides one
|
|
122
|
-
*
|
|
123
|
-
* This is used internally to determine if a signal's value has actually changed
|
|
124
|
-
* when setting new values, preventing unnecessary updates and re-computations.
|
|
66
|
+
* The default equality used for change detection: `===`, then `Object.is` (so `NaN` equals
|
|
67
|
+
* `NaN`; `0` and `-0` are already equal by `===`), then the old value's own `.equals(b)` method
|
|
68
|
+
* if it has one. Only the old value's `equals` is consulted.
|
|
125
69
|
*
|
|
126
|
-
* @param a - The first value to compare
|
|
127
|
-
* @param b - The second value to compare
|
|
128
|
-
* @returns `true` if the values are considered equal, `false` otherwise
|
|
129
|
-
* @example
|
|
130
|
-
* ```ts
|
|
131
|
-
* equals(1, 1) // true
|
|
132
|
-
* equals(NaN, NaN) // true (unlike === which returns false)
|
|
133
|
-
* equals({ equals: (other: any) => other.id === 1 }, { id: 1 }) // Uses custom equals method
|
|
134
|
-
* ```
|
|
135
70
|
* @internal
|
|
136
71
|
*/
|
|
137
72
|
export function equals(a: any, b: any): boolean {
|
|
@@ -140,32 +75,6 @@ export function equals(a: any, b: any): boolean {
|
|
|
140
75
|
return shallowEquals
|
|
141
76
|
}
|
|
142
77
|
|
|
143
|
-
/**
|
|
144
|
-
* A TypeScript utility function for exhaustiveness checking in switch statements and
|
|
145
|
-
* conditional branches. This function should never be called at runtime—it exists
|
|
146
|
-
* purely for compile-time type checking and is `undefined` in emitted JavaScript.
|
|
147
|
-
*
|
|
148
|
-
* @param x - A value that should be of type `never`
|
|
149
|
-
* @throws Always at runtime because the identifier is undefined
|
|
150
|
-
* @example
|
|
151
|
-
* ```ts
|
|
152
|
-
* type Color = 'red' | 'blue'
|
|
153
|
-
*
|
|
154
|
-
* function handleColor(color: Color) {
|
|
155
|
-
* switch (color) {
|
|
156
|
-
* case 'red':
|
|
157
|
-
* return 'Stop'
|
|
158
|
-
* case 'blue':
|
|
159
|
-
* return 'Go'
|
|
160
|
-
* default:
|
|
161
|
-
* return assertNever(color) // TypeScript error if not all cases handled
|
|
162
|
-
* }
|
|
163
|
-
* }
|
|
164
|
-
* ```
|
|
165
|
-
* @public
|
|
166
|
-
*/
|
|
167
|
-
export declare function assertNever(x: never): never
|
|
168
|
-
|
|
169
78
|
/**
|
|
170
79
|
* Creates or retrieves a singleton instance using a global symbol registry.
|
|
171
80
|
* This ensures that the same instance is shared across all code that uses
|
|
@@ -199,37 +108,3 @@ export function singleton<T>(key: string, init: () => T): T {
|
|
|
199
108
|
* @public
|
|
200
109
|
*/
|
|
201
110
|
export const EMPTY_ARRAY: [] = singleton('empty_array', () => Object.freeze([]) as any)
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Checks if a signal has any active reactors (effects or computed signals) that are
|
|
205
|
-
* currently listening to it. This determines whether changes to the signal will
|
|
206
|
-
* cause any side effects or recomputations to occur.
|
|
207
|
-
*
|
|
208
|
-
* A signal is considered to have active reactors if any of its child dependencies
|
|
209
|
-
* are actively listening for changes.
|
|
210
|
-
*
|
|
211
|
-
* @param signal - The signal to check for active reactors
|
|
212
|
-
* @returns `true` if the signal has active reactors, `false` otherwise
|
|
213
|
-
* @example
|
|
214
|
-
* ```ts
|
|
215
|
-
* const count = atom('count', 0)
|
|
216
|
-
*
|
|
217
|
-
* console.log(hasReactors(count)) // false - no effects listening
|
|
218
|
-
*
|
|
219
|
-
* const stop = react('logger', () => console.log(count.get()))
|
|
220
|
-
* console.log(hasReactors(count)) // true - effect is listening
|
|
221
|
-
*
|
|
222
|
-
* stop()
|
|
223
|
-
* console.log(hasReactors(count)) // false - effect stopped
|
|
224
|
-
* ```
|
|
225
|
-
* @public
|
|
226
|
-
*/
|
|
227
|
-
export function hasReactors(signal: Signal<any>) {
|
|
228
|
-
for (const child of signal.children) {
|
|
229
|
-
if (child.isActivelyListening) {
|
|
230
|
-
return true
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return false
|
|
235
|
-
}
|
|
@@ -75,12 +75,19 @@ export function localStorageAtom<Value, Diff = unknown>(
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// The storage helpers above tolerate environments without localStorage (Node, SSR); do the
|
|
79
|
+
// same here rather than throwing on `window`.
|
|
80
|
+
const canListen = typeof window !== 'undefined'
|
|
81
|
+
if (canListen) {
|
|
82
|
+
window.addEventListener('storage', handleStorageEvent)
|
|
83
|
+
}
|
|
79
84
|
|
|
80
85
|
// Combined cleanup function
|
|
81
86
|
const cleanup = () => {
|
|
82
87
|
reactCleanup()
|
|
83
|
-
|
|
88
|
+
if (canListen) {
|
|
89
|
+
window.removeEventListener('storage', handleStorageEvent)
|
|
90
|
+
}
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
return [outAtom, cleanup]
|
package/src/lib/transactions.ts
CHANGED
|
@@ -17,21 +17,11 @@ class Transaction {
|
|
|
17
17
|
|
|
18
18
|
initialAtomValues = new Map<_Atom, any>()
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Get whether this transaction is a root (no parents).
|
|
22
|
-
*
|
|
23
|
-
* @public
|
|
24
|
-
*/
|
|
25
20
|
// eslint-disable-next-line tldraw/no-setter-getter
|
|
26
21
|
get isRoot() {
|
|
27
22
|
return this.parent === null
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
/**
|
|
31
|
-
* Commit the transaction's changes.
|
|
32
|
-
*
|
|
33
|
-
* @public
|
|
34
|
-
*/
|
|
35
25
|
commit() {
|
|
36
26
|
if (inst.globalIsReacting) {
|
|
37
27
|
// if we're committing during a reaction we actually need to
|
|
@@ -44,9 +34,17 @@ class Transaction {
|
|
|
44
34
|
flushChanges(this.initialAtomValues.keys())
|
|
45
35
|
} else {
|
|
46
36
|
// For transactions with parents, add the transaction's initial values to the parent's.
|
|
37
|
+
// A parent that has recorded nothing yet adopts the map outright: this transaction is
|
|
38
|
+
// finished with it, and the common nested case is a single inner transaction doing all
|
|
39
|
+
// the writes.
|
|
40
|
+
const parentValues = this.parent!.initialAtomValues
|
|
41
|
+
if (parentValues.size === 0) {
|
|
42
|
+
this.parent!.initialAtomValues = this.initialAtomValues
|
|
43
|
+
return
|
|
44
|
+
}
|
|
47
45
|
this.initialAtomValues.forEach((value, atom) => {
|
|
48
|
-
if (!
|
|
49
|
-
|
|
46
|
+
if (!parentValues.has(atom)) {
|
|
47
|
+
parentValues.set(atom, value)
|
|
50
48
|
}
|
|
51
49
|
})
|
|
52
50
|
}
|
|
@@ -60,13 +58,11 @@ class Transaction {
|
|
|
60
58
|
abort() {
|
|
61
59
|
inst.globalEpoch++
|
|
62
60
|
|
|
63
|
-
// Reset each of the transaction's atoms to its initial value.
|
|
64
61
|
this.initialAtomValues.forEach((value, atom) => {
|
|
65
62
|
atom.set(value)
|
|
66
63
|
atom.historyBuffer?.clear()
|
|
67
64
|
})
|
|
68
65
|
|
|
69
|
-
// Commit the changes.
|
|
70
66
|
this.commit()
|
|
71
67
|
}
|
|
72
68
|
}
|
|
@@ -115,7 +111,9 @@ export function getIsReacting() {
|
|
|
115
111
|
return inst.globalIsReacting
|
|
116
112
|
}
|
|
117
113
|
|
|
118
|
-
//
|
|
114
|
+
// The set `traverseChild` collects reactors into. Module-level rather than a closure so that a
|
|
115
|
+
// flush over thousands of atoms doesn't allocate a visitor per atom; traversal never runs user
|
|
116
|
+
// code, so nothing can re-enter and swap it mid-walk.
|
|
119
117
|
let traverseReactors: Set<Reactor>
|
|
120
118
|
|
|
121
119
|
function traverseChild(child: Child) {
|
|
@@ -132,11 +130,6 @@ function traverseChild(child: Child) {
|
|
|
132
130
|
}
|
|
133
131
|
}
|
|
134
132
|
|
|
135
|
-
function traverse(reactors: Set<Reactor>, child: Child) {
|
|
136
|
-
traverseReactors = reactors
|
|
137
|
-
traverseChild(child)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
133
|
/**
|
|
141
134
|
* Collect all of the reactors that need to run for an atom and run them.
|
|
142
135
|
*
|
|
@@ -154,11 +147,10 @@ function flushChanges(atoms: Iterable<_Atom>) {
|
|
|
154
147
|
inst.globalIsReacting = true
|
|
155
148
|
inst.reactionEpoch = inst.globalEpoch
|
|
156
149
|
|
|
157
|
-
// Collect all of the visited reactors.
|
|
158
150
|
const reactors = new Set<Reactor>()
|
|
159
|
-
|
|
151
|
+
traverseReactors = reactors
|
|
160
152
|
for (const atom of atoms) {
|
|
161
|
-
atom.children.visit(
|
|
153
|
+
atom.children.visit(traverseChild)
|
|
162
154
|
}
|
|
163
155
|
|
|
164
156
|
// Run each reactor.
|
|
@@ -185,14 +177,7 @@ function flushChanges(atoms: Iterable<_Atom>) {
|
|
|
185
177
|
}
|
|
186
178
|
}
|
|
187
179
|
|
|
188
|
-
/**
|
|
189
|
-
* Handle a change to an atom.
|
|
190
|
-
*
|
|
191
|
-
* @param atom The atom that changed.
|
|
192
|
-
* @param previousValue The atom's previous value.
|
|
193
|
-
*
|
|
194
|
-
* @internal
|
|
195
|
-
*/
|
|
180
|
+
/** @internal */
|
|
196
181
|
export function atomDidChange(atom: _Atom, previousValue: any) {
|
|
197
182
|
if (inst.currentTransaction) {
|
|
198
183
|
// If we are in a transaction, then all we have to do is preserve
|
|
@@ -214,16 +199,11 @@ export function atomDidChange(atom: _Atom, previousValue: any) {
|
|
|
214
199
|
}
|
|
215
200
|
|
|
216
201
|
function traverseAtomForCleanup(atom: _Atom) {
|
|
217
|
-
|
|
218
|
-
atom.children.visit(
|
|
202
|
+
traverseReactors = inst.cleanupReactors ??= new Set()
|
|
203
|
+
atom.children.visit(traverseChild)
|
|
219
204
|
}
|
|
220
205
|
|
|
221
|
-
/**
|
|
222
|
-
* Advances the global epoch counter by one.
|
|
223
|
-
* This is used internally to track when changes occur across the reactive system.
|
|
224
|
-
*
|
|
225
|
-
* @internal
|
|
226
|
-
*/
|
|
206
|
+
/** @internal */
|
|
227
207
|
export function advanceGlobalEpoch() {
|
|
228
208
|
inst.globalEpoch++
|
|
229
209
|
}
|
|
@@ -251,7 +231,7 @@ export function advanceGlobalEpoch() {
|
|
|
251
231
|
* // Logs "Hello, Jane Smith!"
|
|
252
232
|
* ```
|
|
253
233
|
*
|
|
254
|
-
* If the function throws, the transaction is aborted and any signals that were updated during the transaction revert to their state before the transaction began.
|
|
234
|
+
* If the function throws, the transaction is aborted and any signals that were updated during the transaction revert to their state before the transaction began. An aborted transaction still flushes effects: effects whose parents went through a change-and-restore round trip are checked again and, if a parent's value differs from what they last saw (an atom they read directly always will), run once more with the restored values.
|
|
255
235
|
*
|
|
256
236
|
* @example
|
|
257
237
|
* ```ts
|
|
@@ -269,8 +249,9 @@ export function advanceGlobalEpoch() {
|
|
|
269
249
|
* throw new Error('oops')
|
|
270
250
|
* })
|
|
271
251
|
*
|
|
272
|
-
* // Does not log
|
|
273
252
|
* // firstName.get() === 'John'
|
|
253
|
+
* // Logs "Hello, John Doe!" again: effects whose parents were changed and restored still run,
|
|
254
|
+
* // and observe the restored values.
|
|
274
255
|
* ```
|
|
275
256
|
*
|
|
276
257
|
* A `rollback` callback is passed into the function.
|
|
@@ -293,9 +274,9 @@ export function advanceGlobalEpoch() {
|
|
|
293
274
|
* rollback()
|
|
294
275
|
* })
|
|
295
276
|
*
|
|
296
|
-
* // Does not log
|
|
297
277
|
* // firstName.get() === 'John'
|
|
298
278
|
* // lastName.get() === 'Doe'
|
|
279
|
+
* // Logs "Hello, John Doe!" again, as above.
|
|
299
280
|
* ```
|
|
300
281
|
*
|
|
301
282
|
* @param fn - The function to run in a transaction, called with a function to roll back the change.
|
|
@@ -305,7 +286,6 @@ export function advanceGlobalEpoch() {
|
|
|
305
286
|
export function transaction<T>(fn: (rollback: () => void) => T) {
|
|
306
287
|
const txn = new Transaction(inst.currentTransaction, true)
|
|
307
288
|
|
|
308
|
-
// Set the current transaction to the transaction
|
|
309
289
|
inst.currentTransaction = txn
|
|
310
290
|
|
|
311
291
|
try {
|
|
@@ -313,10 +293,8 @@ export function transaction<T>(fn: (rollback: () => void) => T) {
|
|
|
313
293
|
let rollback = false
|
|
314
294
|
|
|
315
295
|
try {
|
|
316
|
-
// Run the function.
|
|
317
296
|
result = fn(() => (rollback = true))
|
|
318
297
|
} catch (e) {
|
|
319
|
-
// Abort the transaction if the function throws.
|
|
320
298
|
txn.abort()
|
|
321
299
|
throw e
|
|
322
300
|
}
|
|
@@ -326,7 +304,6 @@ export function transaction<T>(fn: (rollback: () => void) => T) {
|
|
|
326
304
|
}
|
|
327
305
|
|
|
328
306
|
if (rollback) {
|
|
329
|
-
// If the rollback was triggered, abort the transaction.
|
|
330
307
|
txn.abort()
|
|
331
308
|
} else {
|
|
332
309
|
txn.commit()
|
|
@@ -334,7 +311,6 @@ export function transaction<T>(fn: (rollback: () => void) => T) {
|
|
|
334
311
|
|
|
335
312
|
return result
|
|
336
313
|
} finally {
|
|
337
|
-
// Set the current transaction to the transaction's parent.
|
|
338
314
|
inst.currentTransaction = txn.parent
|
|
339
315
|
}
|
|
340
316
|
}
|
package/src/lib/types.ts
CHANGED
|
@@ -9,12 +9,12 @@ import { ArraySet } from './ArraySet'
|
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* ```ts
|
|
12
|
-
* import { atom,
|
|
12
|
+
* import { atom, RESET_VALUE } from '@tldraw/state'
|
|
13
13
|
*
|
|
14
|
-
* const count = atom('count', 0, { historyLength: 3 })
|
|
15
|
-
* const oldEpoch =
|
|
14
|
+
* const count = atom('count', 0, { historyLength: 3, computeDiff: (prev, next) => next - prev })
|
|
15
|
+
* const oldEpoch = count.lastChangedEpoch
|
|
16
16
|
*
|
|
17
|
-
* // Make
|
|
17
|
+
* // Make more changes than the history length can hold
|
|
18
18
|
* count.set(1)
|
|
19
19
|
* count.set(2)
|
|
20
20
|
* count.set(3)
|
|
@@ -183,8 +183,8 @@ export interface Child {
|
|
|
183
183
|
* A function type that computes the difference between two values of a signal.
|
|
184
184
|
*
|
|
185
185
|
* This function is used to generate incremental diffs that can be applied to
|
|
186
|
-
* reconstruct state changes over time
|
|
187
|
-
*
|
|
186
|
+
* reconstruct state changes over time, so that downstream computeds and effects can
|
|
187
|
+
* update incrementally instead of recomputing from scratch.
|
|
188
188
|
*
|
|
189
189
|
* The function should analyze the previous and current values and return a
|
|
190
190
|
* diff object that represents the change. If the diff cannot be computed
|
|
@@ -193,7 +193,7 @@ export interface Child {
|
|
|
193
193
|
*
|
|
194
194
|
* @param previousValue - The previous value of the signal
|
|
195
195
|
* @param currentValue - The current value of the signal
|
|
196
|
-
* @param lastComputedEpoch -
|
|
196
|
+
* @param lastComputedEpoch - For an atom, the epoch when the previous value was set. For a computed, the epoch at which it was last checked (the same value its compute function receives), so that `other.getDiffSince(lastComputedEpoch)` yields exactly the changes not yet accounted for.
|
|
197
197
|
* @param currentEpoch - The epoch when the current value was set
|
|
198
198
|
* @returns A diff object representing the change, or the unique symbol RESET_VALUE if no diff can be computed
|
|
199
199
|
*
|
package/src/lib/warnings.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Flag to track whether the computed getter deprecation warning has already been shown.
|
|
3
|
-
* Prevents the same warning from being logged multiple times during application runtime.
|
|
4
|
-
*
|
|
5
|
-
* @internal
|
|
6
|
-
*/
|
|
7
|
-
let didWarnComputedGetter = false
|
|
1
|
+
import { warnOnce } from '@tldraw/utils'
|
|
8
2
|
|
|
9
3
|
/**
|
|
10
4
|
* Logs a deprecation warning for the deprecated `@computed` getter decorator syntax.
|
|
@@ -37,9 +31,7 @@ let didWarnComputedGetter = false
|
|
|
37
31
|
* @internal
|
|
38
32
|
*/
|
|
39
33
|
export function logComputedGetterWarning() {
|
|
40
|
-
|
|
41
|
-
didWarnComputedGetter = true
|
|
42
|
-
console.warn(
|
|
34
|
+
warnOnce(
|
|
43
35
|
`Using \`@computed\` as a decorator for getters is deprecated and will be removed in the near future. Please refactor to use \`@computed\` as a decorator for methods.
|
|
44
36
|
|
|
45
37
|
// Before
|