mutts 1.0.7 → 1.0.9
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/README.md +72 -17
- package/dist/browser.cjs +3111 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.ts +1391 -1
- package/dist/browser.esm.js +264 -35
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/async-browser-lvzLOCgk.cjs +297 -0
- package/dist/chunks/async-browser-lvzLOCgk.cjs.map +1 -0
- package/dist/chunks/async-core-CRLKP3l-.cjs +29 -0
- package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
- package/dist/chunks/async-node-C3DeIb0y.cjs +96 -0
- package/dist/chunks/async-node-C3DeIb0y.cjs.map +1 -0
- package/dist/chunks/index-2vea86wD.esm.js +3011 -0
- package/dist/chunks/index-2vea86wD.esm.js.map +1 -0
- package/dist/chunks/index-CtA2AWl3.esm.js +4376 -0
- package/dist/chunks/index-CtA2AWl3.esm.js.map +1 -0
- package/dist/chunks/index-VTO-b2vR.cjs +4461 -0
- package/dist/chunks/index-VTO-b2vR.cjs.map +1 -0
- package/dist/debug/debug.d.ts +122 -0
- package/dist/debug/debug.d.ts.map +1 -0
- package/dist/debug/index.d.ts +4 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/lineage-panel.d.ts +5 -0
- package/dist/debug/lineage-panel.d.ts.map +1 -0
- package/dist/debug/lineage.d.ts +79 -0
- package/dist/debug/lineage.d.ts.map +1 -0
- package/dist/debug.cjs +35 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.ts +209 -0
- package/dist/debug.esm.js +2 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/devtools/devtool/devtools.d.ts +1 -0
- package/dist/devtools/devtool/devtools.d.ts.map +1 -0
- package/dist/devtools/devtool/panel.d.ts +2 -0
- package/dist/devtools/devtool/panel.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/node.cjs +107 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.ts +2 -2
- package/dist/node.esm.js +86 -33
- package/dist/node.esm.js.map +1 -1
- package/dist/src/async/browser.d.ts +2 -0
- package/dist/src/async/browser.d.ts.map +1 -0
- package/dist/src/async/index.d.ts +19 -0
- package/dist/src/async/index.d.ts.map +1 -0
- package/dist/src/async/node.d.ts +2 -0
- package/dist/src/async/node.d.ts.map +1 -0
- package/dist/src/decorator.d.ts +106 -0
- package/dist/src/decorator.d.ts.map +1 -0
- package/dist/src/destroyable.d.ts +87 -0
- package/dist/src/destroyable.d.ts.map +1 -0
- package/dist/src/entry-browser.d.ts +3 -0
- package/dist/src/entry-browser.d.ts.map +1 -0
- package/dist/src/entry-node.d.ts +3 -0
- package/dist/src/entry-node.d.ts.map +1 -0
- package/dist/src/eventful.d.ts +20 -0
- package/dist/src/eventful.d.ts.map +1 -0
- package/dist/src/flavored.d.ts +33 -0
- package/dist/src/flavored.d.ts.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/indexable.d.ts +243 -0
- package/dist/src/indexable.d.ts.map +1 -0
- package/dist/src/introspection.d.ts +27 -0
- package/dist/src/introspection.d.ts.map +1 -0
- package/dist/src/iterableWeak.d.ts +53 -0
- package/dist/src/iterableWeak.d.ts.map +1 -0
- package/dist/src/mixins.d.ts +25 -0
- package/dist/src/mixins.d.ts.map +1 -0
- package/dist/src/promiseChain.d.ts +20 -0
- package/dist/src/promiseChain.d.ts.map +1 -0
- package/dist/src/reactive/array.d.ts +48 -0
- package/dist/src/reactive/array.d.ts.map +1 -0
- package/dist/src/reactive/buffer.d.ts +120 -0
- package/dist/src/reactive/buffer.d.ts.map +1 -0
- package/dist/src/reactive/change.d.ts +29 -0
- package/dist/src/reactive/change.d.ts.map +1 -0
- package/dist/src/reactive/deep-touch.d.ts +28 -0
- package/dist/src/reactive/deep-touch.d.ts.map +1 -0
- package/dist/src/reactive/deep-watch-state.d.ts +25 -0
- package/dist/src/reactive/deep-watch-state.d.ts.map +1 -0
- package/dist/src/reactive/deep-watch.d.ts +20 -0
- package/dist/src/reactive/deep-watch.d.ts.map +1 -0
- package/dist/src/reactive/describe.d.ts +12 -0
- package/dist/src/reactive/describe.d.ts.map +1 -0
- package/dist/src/reactive/effect-context.d.ts +34 -0
- package/dist/src/reactive/effect-context.d.ts.map +1 -0
- package/dist/src/reactive/effects.d.ts +164 -0
- package/dist/src/reactive/effects.d.ts.map +1 -0
- package/dist/src/reactive/index.d.ts +19 -0
- package/dist/src/reactive/index.d.ts.map +1 -0
- package/dist/src/reactive/map.d.ts +28 -0
- package/dist/src/reactive/map.d.ts.map +1 -0
- package/dist/src/reactive/memoize.d.ts +28 -0
- package/dist/src/reactive/memoize.d.ts.map +1 -0
- package/dist/src/reactive/non-reactive-state.d.ts +9 -0
- package/dist/src/reactive/non-reactive-state.d.ts.map +1 -0
- package/dist/src/reactive/non-reactive.d.ts +11 -0
- package/dist/src/reactive/non-reactive.d.ts.map +1 -0
- package/dist/src/reactive/project.d.ts +40 -0
- package/dist/src/reactive/project.d.ts.map +1 -0
- package/dist/src/reactive/proxy-state.d.ts +8 -0
- package/dist/src/reactive/proxy-state.d.ts.map +1 -0
- package/dist/src/reactive/proxy.d.ts +23 -0
- package/dist/src/reactive/proxy.d.ts.map +1 -0
- package/dist/src/reactive/record.d.ts +115 -0
- package/dist/src/reactive/record.d.ts.map +1 -0
- package/dist/src/reactive/register.d.ts +125 -0
- package/dist/src/reactive/register.d.ts.map +1 -0
- package/dist/src/reactive/registry.d.ts +21 -0
- package/dist/src/reactive/registry.d.ts.map +1 -0
- package/dist/src/reactive/set.d.ts +26 -0
- package/dist/src/reactive/set.d.ts.map +1 -0
- package/dist/src/reactive/tracking.d.ts +7 -0
- package/dist/src/reactive/tracking.d.ts.map +1 -0
- package/dist/src/reactive/types.d.ts +424 -0
- package/dist/src/reactive/types.d.ts.map +1 -0
- package/dist/src/reactive/watch.d.ts +48 -0
- package/dist/src/reactive/watch.d.ts.map +1 -0
- package/dist/src/std-decorators.d.ts +45 -0
- package/dist/src/std-decorators.d.ts.map +1 -0
- package/dist/src/utils.d.ts +49 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/zone.d.ts +40 -0
- package/dist/src/zone.d.ts.map +1 -0
- package/dist/types-DaHFfhlN.d.ts +414 -0
- package/docs/ai/api-reference.md +1 -1
- package/docs/ai/manual.md +443 -17
- package/docs/events.md +57 -2
- package/docs/flavored.md +210 -0
- package/docs/reactive/advanced.md +2 -3
- package/docs/reactive/attend.md +143 -0
- package/docs/reactive/collections.md +55 -0
- package/docs/reactive/core.md +120 -4
- package/docs/reactive/debugging.md +8 -10
- package/docs/reactive/describe.md +85 -0
- package/docs/reactive/error-handling.md +121 -0
- package/docs/reactive/project.md +1 -1
- package/docs/reactive/scan.md +215 -0
- package/docs/reactive.md +2 -0
- package/docs/std-decorators.md +171 -52
- package/docs/zone.md +9 -6
- package/package.json +44 -27
- package/src/async/browser.ts +277 -41
- package/src/async/index.ts +23 -4
- package/src/async/node.ts +78 -32
- package/src/entry-browser.ts +5 -0
- package/src/entry-node.ts +5 -0
- package/src/eventful.ts +82 -49
- package/src/flavored.ts +106 -0
- package/src/index.d.ts +12 -9
- package/src/index.ts +2 -1
- package/src/introspection.ts +1 -1
- package/src/reactive/array.ts +160 -80
- package/src/reactive/buffer.ts +187 -27
- package/src/reactive/change.ts +16 -23
- package/src/reactive/deep-touch.ts +19 -14
- package/src/reactive/deep-watch-state.ts +3 -3
- package/src/reactive/deep-watch.ts +7 -4
- package/src/reactive/describe.ts +39 -0
- package/src/reactive/effect-context.ts +71 -6
- package/src/reactive/effects.ts +445 -393
- package/src/reactive/index.ts +12 -14
- package/src/reactive/map.ts +8 -24
- package/src/reactive/memoize.ts +26 -3
- package/src/reactive/project.md +107 -0
- package/src/reactive/project.ts +48 -71
- package/src/reactive/proxy.ts +21 -3
- package/src/reactive/record.ts +30 -45
- package/src/reactive/register.ts +138 -19
- package/src/reactive/registry.ts +22 -9
- package/src/reactive/set.ts +7 -20
- package/src/reactive/tracking.ts +9 -12
- package/src/reactive/types.ts +104 -24
- package/src/reactive/{interface.ts → watch.ts} +29 -59
- package/src/std-decorators.ts +56 -17
- package/src/utils.ts +3 -4
- package/src/zone.ts +43 -28
- package/dist/browser.js +0 -161
- package/dist/browser.js.map +0 -1
- package/dist/chunks/index-BFYK02LG.js +0 -6378
- package/dist/chunks/index-BFYK02LG.js.map +0 -1
- package/dist/chunks/index-CNR6QRUl.esm.js +0 -6291
- package/dist/chunks/index-CNR6QRUl.esm.js.map +0 -1
- package/dist/node.js +0 -136
- package/dist/node.js.map +0 -1
- package/src/reactive/debug.ts +0 -517
package/src/reactive/buffer.ts
CHANGED
|
@@ -1,9 +1,90 @@
|
|
|
1
1
|
import { FoolProof } from '../utils'
|
|
2
|
+
import { cleanedBy } from '.'
|
|
2
3
|
import { effect, untracked } from './effects'
|
|
3
|
-
import { cleanedBy, cleanup } from './interface'
|
|
4
4
|
import { memoize } from './memoize'
|
|
5
5
|
import { reactive } from './proxy'
|
|
6
|
-
import type { ScopedCallback } from './types'
|
|
6
|
+
import type { cleanup, EffectAccess, EffectCleanup, EffectCloser, ScopedCallback } from './types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reactively attends to each entry of a collection or each key yielded by an
|
|
10
|
+
* enumeration callback. For each key, an inner effect runs the callback. When a
|
|
11
|
+
* key disappears, its inner effect is disposed. The callback may return a cleanup
|
|
12
|
+
* (like a regular effect closer).
|
|
13
|
+
*
|
|
14
|
+
* Accepts arrays, records, Maps, Sets, or a raw `() => Iterable<Key>` callback.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // Record shorthand
|
|
19
|
+
* attend(record, (key) => { console.log(key, record[key]) })
|
|
20
|
+
*
|
|
21
|
+
* // Array shorthand
|
|
22
|
+
* attend(array, (index) => { console.log(index, array[index]) })
|
|
23
|
+
*
|
|
24
|
+
* // Raw enumeration callback
|
|
25
|
+
* attend(() => Object.keys(record), (key) => { ... })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function attend<T>(
|
|
29
|
+
source: readonly T[],
|
|
30
|
+
callback: (index: number) => EffectCloser | void
|
|
31
|
+
): ScopedCallback
|
|
32
|
+
export function attend<K, V>(
|
|
33
|
+
source: Map<K, V>,
|
|
34
|
+
callback: (key: K) => EffectCloser | void
|
|
35
|
+
): ScopedCallback
|
|
36
|
+
export function attend<T>(
|
|
37
|
+
source: Set<T>,
|
|
38
|
+
callback: (value: T) => EffectCloser | void
|
|
39
|
+
): ScopedCallback
|
|
40
|
+
export function attend<S extends Record<PropertyKey, any>>(
|
|
41
|
+
source: S,
|
|
42
|
+
callback: (key: keyof S & string) => EffectCloser | void
|
|
43
|
+
): ScopedCallback
|
|
44
|
+
export function attend<Key>(
|
|
45
|
+
enumerate: () => Iterable<Key>,
|
|
46
|
+
callback: (key: Key) => EffectCloser | void
|
|
47
|
+
): ScopedCallback
|
|
48
|
+
export function attend(
|
|
49
|
+
source: any,
|
|
50
|
+
callback: (key: any) => EffectCloser | void
|
|
51
|
+
): ScopedCallback {
|
|
52
|
+
const enumerate: () => Iterable<any> =
|
|
53
|
+
typeof source === 'function'
|
|
54
|
+
? source
|
|
55
|
+
: Array.isArray(source)
|
|
56
|
+
? () => Array.from({ length: source.length }, (_, i) => i)
|
|
57
|
+
: source instanceof Map
|
|
58
|
+
? () => source.keys()
|
|
59
|
+
: source instanceof Set
|
|
60
|
+
? () => source.values()
|
|
61
|
+
: () => Object.keys(source)
|
|
62
|
+
|
|
63
|
+
const keyEffects = new Map<any, ScopedCallback>()
|
|
64
|
+
|
|
65
|
+
const outer = effect(({ ascend }) => {
|
|
66
|
+
const keys = new Set<any>()
|
|
67
|
+
for (const key of enumerate()) keys.add(key)
|
|
68
|
+
|
|
69
|
+
for (const key of keys) {
|
|
70
|
+
if (keyEffects.has(key)) continue
|
|
71
|
+
keyEffects.set(key, ascend(() => effect(() => callback(key))))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const key of Array.from(keyEffects.keys())) {
|
|
75
|
+
if (!keys.has(key)) {
|
|
76
|
+
keyEffects.get(key)!()
|
|
77
|
+
keyEffects.delete(key)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
outer()
|
|
84
|
+
for (const stop of keyEffects.values()) stop()
|
|
85
|
+
keyEffects.clear()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
7
88
|
|
|
8
89
|
/**
|
|
9
90
|
* Result of a reactive scan, which is a reactive array of accumulated values
|
|
@@ -15,22 +96,22 @@ export type ScanResult<Output> = readonly Output[] & { [cleanup]: ScopedCallback
|
|
|
15
96
|
* Perform a reactive scan over an array of items.
|
|
16
97
|
*
|
|
17
98
|
* This implementation is highly optimized for performance and fine-grained reactivity:
|
|
18
|
-
* - **Incremental Updates**: Changes to an item only trigger re-computation from that
|
|
99
|
+
* - **Incremental Updates**: Changes to an item only trigger re-computation from that
|
|
19
100
|
* point onwards in the result chain.
|
|
20
|
-
* - **Move Optimization**: If items are moved within the array, their accumulated
|
|
101
|
+
* - **Move Optimization**: If items are moved within the array, their accumulated
|
|
21
102
|
* values are reused as long as their predecessor remains the same.
|
|
22
|
-
* - **Duplicate Support**: Correctly handles multiple occurrences of the same object
|
|
103
|
+
* - **Duplicate Support**: Correctly handles multiple occurrences of the same object
|
|
23
104
|
* instance using an internal occurrence tracking mechanism.
|
|
24
|
-
* - **Memory Efficient**: Uses `WeakMap` for caching intermediates, which are
|
|
105
|
+
* - **Memory Efficient**: Uses `WeakMap` for caching intermediates, which are
|
|
25
106
|
* automatically cleared when source items are garbage collected.
|
|
26
107
|
*
|
|
27
108
|
* @example
|
|
28
109
|
* ```typescript
|
|
29
110
|
* const source = reactive([{ val: 1 }, { val: 2 }, { val: 3 }])
|
|
30
111
|
* const sum = scan(source, (acc, item) => acc + item.val, 0)
|
|
31
|
-
*
|
|
112
|
+
*
|
|
32
113
|
* expect([...sum]).toEqual([1, 3, 6])
|
|
33
|
-
*
|
|
114
|
+
*
|
|
34
115
|
* // Modifying an item only re-computes subsequent sums
|
|
35
116
|
* source[1].val = 10
|
|
36
117
|
* expect([...sum]).toEqual([1, 11, 14])
|
|
@@ -48,16 +129,19 @@ export function scan<Input extends object, Output>(
|
|
|
48
129
|
): ScanResult<Output> {
|
|
49
130
|
const observedSource = reactive(source)
|
|
50
131
|
const result = reactive([] as Output[])
|
|
51
|
-
|
|
132
|
+
|
|
52
133
|
// Track effects for each index to dispose them when the array shrinks
|
|
53
|
-
const indexEffects = new Map<number,
|
|
134
|
+
const indexEffects = new Map<number, EffectCleanup>()
|
|
54
135
|
// Mapping from index to its current intermediate object
|
|
55
136
|
const indexToIntermediate = reactive([] as Intermediate[])
|
|
56
137
|
const intermediaries = new WeakMap<Input, Intermediate[]>()
|
|
57
138
|
|
|
58
139
|
class Intermediate {
|
|
59
140
|
public prev: Intermediate | undefined
|
|
60
|
-
constructor(
|
|
141
|
+
constructor(
|
|
142
|
+
public val: Input,
|
|
143
|
+
prev: Intermediate | undefined
|
|
144
|
+
) {
|
|
61
145
|
this.prev = prev
|
|
62
146
|
}
|
|
63
147
|
|
|
@@ -83,12 +167,14 @@ export function scan<Input extends object, Output>(
|
|
|
83
167
|
const mainEffect = effect(function scanMainEffect({ ascend }) {
|
|
84
168
|
const length = observedSource.length
|
|
85
169
|
const occurrenceCount = new Map<Input, number>()
|
|
86
|
-
let prev: Intermediate | undefined
|
|
170
|
+
let prev: Intermediate | undefined
|
|
87
171
|
|
|
88
172
|
for (let i = 0; i < length; i++) {
|
|
89
173
|
const val = FoolProof.get(observedSource as any, i, observedSource) as Input
|
|
90
|
-
|
|
91
|
-
if (
|
|
174
|
+
|
|
175
|
+
if (
|
|
176
|
+
!(val && (typeof val === 'object' || typeof val === 'function' || typeof val === 'symbol'))
|
|
177
|
+
) {
|
|
92
178
|
throw new Error('scan: items must be objects (WeakKey) for intermediate caching')
|
|
93
179
|
}
|
|
94
180
|
|
|
@@ -106,7 +192,7 @@ export function scan<Input extends object, Output>(
|
|
|
106
192
|
intermediate = reactive(new Intermediate(val, prev))
|
|
107
193
|
list[count] = intermediate
|
|
108
194
|
} else {
|
|
109
|
-
// Update the link.
|
|
195
|
+
// Update the link.
|
|
110
196
|
if (untracked(() => intermediate.prev) !== prev) {
|
|
111
197
|
intermediate.prev = prev
|
|
112
198
|
}
|
|
@@ -141,7 +227,7 @@ export function scan<Input extends object, Output>(
|
|
|
141
227
|
for (const index of Array.from(indexEffects.keys())) {
|
|
142
228
|
if (index >= length) disposeIndex(index)
|
|
143
229
|
}
|
|
144
|
-
|
|
230
|
+
|
|
145
231
|
// Ensure result length matches source length
|
|
146
232
|
untracked(() => {
|
|
147
233
|
if (result.length !== length) {
|
|
@@ -150,19 +236,93 @@ export function scan<Input extends object, Output>(
|
|
|
150
236
|
})
|
|
151
237
|
})
|
|
152
238
|
|
|
153
|
-
return cleanedBy(result, () => {
|
|
239
|
+
return cleanedBy(result, (() => {
|
|
154
240
|
mainEffect()
|
|
155
241
|
for (const stop of indexEffects.values()) stop()
|
|
156
242
|
indexEffects.clear()
|
|
157
|
-
}) as ScanResult<Output>
|
|
243
|
+
}) as EffectCleanup) as ScanResult<Output>
|
|
158
244
|
}
|
|
159
245
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Lifts a callback that returns an array into a reactive array that automatically
|
|
248
|
+
* synchronizes with the source array returned by the callback.
|
|
249
|
+
*
|
|
250
|
+
* The returned reactive array will update whenever the callback's dependencies change,
|
|
251
|
+
* efficiently syncing only the elements that differ from the previous result.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* const items = reactive([1, 2, 3])
|
|
256
|
+
* const doubled = lift(() => items.map(x => x * 2))
|
|
257
|
+
*
|
|
258
|
+
* console.log([...doubled]) // [2, 4, 6]
|
|
259
|
+
*
|
|
260
|
+
* items.push(4)
|
|
261
|
+
* console.log([...doubled]) // [2, 4, 6, 8]
|
|
262
|
+
* ```
|
|
263
|
+
*
|
|
264
|
+
* @param cb Callback function that returns an array
|
|
265
|
+
* @returns A reactive array synchronized with the callback's result, with a [cleanup] property to stop tracking
|
|
266
|
+
*/
|
|
267
|
+
export function lift<Output extends any[]>(
|
|
268
|
+
cb: (access: EffectAccess) => Output
|
|
269
|
+
): Output & { [cleanup]: ScopedCallback }
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Lifts a callback that returns an object into a reactive object that automatically
|
|
273
|
+
* synchronizes with the source object returned by the callback.
|
|
274
|
+
*
|
|
275
|
+
* The returned reactive object will update whenever the callback's dependencies change,
|
|
276
|
+
* efficiently syncing only the properties that differ from the previous result using
|
|
277
|
+
* Object.assign(). Properties that no longer exist in the source are automatically removed.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```typescript
|
|
281
|
+
* const user = reactive({ name: 'John', age: 30 })
|
|
282
|
+
* const profile = lift(() => ({
|
|
283
|
+
* displayName: user.name.toUpperCase(),
|
|
284
|
+
* isAdult: user.age >= 18,
|
|
285
|
+
* description: `${user.name} is ${user.age} years old`
|
|
286
|
+
* }))
|
|
287
|
+
*
|
|
288
|
+
* console.log(profile.displayName) // JOHN
|
|
289
|
+
* console.log(profile.isAdult) // true
|
|
290
|
+
*
|
|
291
|
+
* user.name = 'Jane'
|
|
292
|
+
* console.log(profile.displayName) // JANE
|
|
293
|
+
* console.log(profile.description) // Jane is 30 years old
|
|
294
|
+
* ```
|
|
295
|
+
*
|
|
296
|
+
* @param cb Callback function that returns an object
|
|
297
|
+
* @returns A reactive object synchronized with the callback's result, with a [cleanup] property to stop tracking
|
|
298
|
+
*/
|
|
299
|
+
export function lift<Output extends object>(
|
|
300
|
+
cb: (access: EffectAccess) => Output
|
|
301
|
+
): Output & { [cleanup]: ScopedCallback }
|
|
302
|
+
export function lift<Output extends any[] | object>(
|
|
303
|
+
cb: (access: EffectAccess) => Output
|
|
304
|
+
): Output & { [cleanup]: ScopedCallback } {
|
|
305
|
+
let result: Output
|
|
306
|
+
const liftCleanup = effect((access) => {
|
|
307
|
+
const source = cb(access)
|
|
308
|
+
if (!source || typeof source !== 'object')
|
|
309
|
+
throw new Error('lift callback must return an array or object')
|
|
310
|
+
if (!result) {
|
|
311
|
+
if (Array.isArray(source)) {
|
|
312
|
+
result = reactive([]) as Output
|
|
313
|
+
} else {
|
|
314
|
+
result = reactive({}) as Output
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (Array.isArray(source) && Array.isArray(result)) {
|
|
318
|
+
if (result.length !== source.length) result.length = source.length
|
|
319
|
+
for (let i = 0; i < source.length; i++) if (result[i] !== source[i]) result[i] = source[i]
|
|
320
|
+
} else if (!Array.isArray(source) && !Array.isArray(result)) {
|
|
321
|
+
Object.assign(result, source)
|
|
322
|
+
for (const key of Object.keys(result)) if (!(key in source)) delete result[key]
|
|
323
|
+
} else {
|
|
324
|
+
throw new Error('lift callback must return the same type as the previous result')
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
return cleanedBy(result as Output, liftCleanup)
|
|
328
|
+
}
|
package/src/reactive/change.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { recordTriggerLink } from '
|
|
1
|
+
import { recordTriggerLink } from '../../debug/debug'
|
|
2
2
|
import { bubbleUpChange, objectsWithDeepWatchers } from './deep-watch-state'
|
|
3
3
|
import { getActiveEffect, isRunning } from './effect-context'
|
|
4
|
-
import { batch,
|
|
4
|
+
import { batch, hasBatched, recordActivation } from './effects'
|
|
5
5
|
import { unwrap } from './proxy-state'
|
|
6
|
-
import { watchers } from './registry'
|
|
7
|
-
import { allProps, type
|
|
6
|
+
import { getEffectNode, watchers } from './registry'
|
|
7
|
+
import { allProps, type EffectTrigger, type Evolution, optionCall, type State } from './types'
|
|
8
8
|
|
|
9
9
|
const states = new WeakMap<object, State>()
|
|
10
10
|
|
|
@@ -34,31 +34,28 @@ export function getState(obj: any) {
|
|
|
34
34
|
export function collectEffects(
|
|
35
35
|
obj: any,
|
|
36
36
|
evolution: Evolution,
|
|
37
|
-
effects: Set<
|
|
38
|
-
objectWatchers: Map<any, Set<
|
|
37
|
+
effects: Set<EffectTrigger>,
|
|
38
|
+
objectWatchers: Map<any, Set<EffectTrigger>>,
|
|
39
39
|
...keyChains: Iterable<any>[]
|
|
40
40
|
) {
|
|
41
41
|
const sourceEffect = getActiveEffect()
|
|
42
42
|
for (const keys of keyChains)
|
|
43
43
|
for (const key of keys) {
|
|
44
44
|
const deps = objectWatchers.get(key)
|
|
45
|
+
// console.log(`[DEBUG] collectEffects: checking ${String(key)}. Found deps: ${deps ? deps.size : 'none'}`)
|
|
45
46
|
if (deps)
|
|
46
47
|
for (const effect of deps) {
|
|
48
|
+
// console.log(`[DEBUG] collectEffects: found dependency ${effect.name || 'anonymous'} for ${String(key)}`)
|
|
47
49
|
const runningChain = isRunning(effect)
|
|
48
50
|
if (runningChain) {
|
|
49
|
-
|
|
51
|
+
optionCall('skipRunningEffect', effect)
|
|
50
52
|
continue
|
|
51
53
|
}
|
|
52
54
|
if (!effects.has(effect)) {
|
|
53
55
|
effects.add(effect)
|
|
54
56
|
if (!hasBatched(effect)) recordActivation(effect, obj, evolution, key)
|
|
55
57
|
}
|
|
56
|
-
const trackers = effectTrackers.get(effect)
|
|
57
58
|
recordTriggerLink(sourceEffect, effect, obj, key, evolution)
|
|
58
|
-
if (trackers) {
|
|
59
|
-
for (const tracker of trackers) tracker(obj, evolution, key)
|
|
60
|
-
trackers.delete(effect)
|
|
61
|
-
}
|
|
62
59
|
}
|
|
63
60
|
}
|
|
64
61
|
}
|
|
@@ -85,10 +82,10 @@ export function touched(obj: any, evolution: Evolution, props?: Iterable<any>) {
|
|
|
85
82
|
const objectWatchers = watchers.get(obj)
|
|
86
83
|
if (objectWatchers) {
|
|
87
84
|
// Note: we have to collect effects to remove duplicates in the specific case when no batch is running
|
|
88
|
-
const effects = new Set<
|
|
85
|
+
const effects = new Set<EffectTrigger>()
|
|
89
86
|
if (props) collectEffects(obj, evolution, effects, objectWatchers, [allProps], props)
|
|
90
87
|
else collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys())
|
|
91
|
-
|
|
88
|
+
optionCall('touched', obj, evolution, props as any[] | undefined, effects)
|
|
92
89
|
batch(Array.from(effects))
|
|
93
90
|
}
|
|
94
91
|
|
|
@@ -110,29 +107,25 @@ export function touchedOpaque(obj: any, evolution: Evolution, prop: any) {
|
|
|
110
107
|
const deps = objectWatchers.get(prop)
|
|
111
108
|
if (!deps) return
|
|
112
109
|
|
|
113
|
-
const effects = new Set<
|
|
110
|
+
const effects = new Set<EffectTrigger>()
|
|
114
111
|
const sourceEffect = getActiveEffect()
|
|
115
112
|
|
|
116
113
|
for (const effect of deps) {
|
|
117
|
-
|
|
114
|
+
const node = getEffectNode(effect)
|
|
115
|
+
if (!node.isOpaque) continue
|
|
118
116
|
|
|
119
117
|
const runningChain = isRunning(effect)
|
|
120
118
|
if (runningChain) {
|
|
121
|
-
|
|
119
|
+
optionCall('skipRunningEffect', effect)
|
|
122
120
|
continue
|
|
123
121
|
}
|
|
124
122
|
effects.add(effect)
|
|
125
123
|
recordActivation(effect, obj, evolution, prop)
|
|
126
|
-
const trackers = effectTrackers.get(effect)
|
|
127
124
|
recordTriggerLink(sourceEffect, effect, obj, prop, evolution)
|
|
128
|
-
if (trackers) {
|
|
129
|
-
for (const tracker of trackers) tracker(obj, evolution, prop)
|
|
130
|
-
trackers.delete(effect)
|
|
131
|
-
}
|
|
132
125
|
}
|
|
133
126
|
|
|
134
127
|
if (effects.size > 0) {
|
|
135
|
-
|
|
128
|
+
optionCall('touched', obj, evolution, [prop], effects)
|
|
136
129
|
batch(Array.from(effects))
|
|
137
130
|
}
|
|
138
131
|
}
|
|
@@ -3,8 +3,8 @@ import { bubbleUpChange, objectsWithDeepWatchers } from './deep-watch-state'
|
|
|
3
3
|
import { batch } from './effects'
|
|
4
4
|
import { isNonReactive } from './non-reactive-state'
|
|
5
5
|
import { unwrap } from './proxy-state'
|
|
6
|
-
import {
|
|
7
|
-
import { allProps, type
|
|
6
|
+
import { getEffectNode, watchers } from './registry'
|
|
7
|
+
import { allProps, type EffectCleanup, type EffectTrigger, type Evolution, optionCall, options } from './types'
|
|
8
8
|
|
|
9
9
|
function isObject(value: any): value is object {
|
|
10
10
|
return typeof value === 'object' && value !== null
|
|
@@ -196,31 +196,35 @@ function diffObjectProperties(
|
|
|
196
196
|
/**
|
|
197
197
|
* Checks if an effect or any of its ancestors is in the allowed set
|
|
198
198
|
*/
|
|
199
|
-
function hasAncestorInSet(
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
function hasAncestorInSet(
|
|
200
|
+
effect: EffectTrigger | EffectCleanup,
|
|
201
|
+
allowedSet: Set<EffectTrigger | EffectCleanup>
|
|
202
|
+
): boolean {
|
|
203
|
+
let current: EffectTrigger | EffectCleanup | undefined = effect
|
|
204
|
+
const visited = new WeakSet<EffectTrigger | EffectCleanup>()
|
|
202
205
|
while (current && !visited.has(current)) {
|
|
203
206
|
visited.add(current)
|
|
204
207
|
if (allowedSet.has(current)) return true
|
|
205
|
-
|
|
208
|
+
const node = getEffectNode(current as EffectTrigger)
|
|
209
|
+
current = node.parent
|
|
206
210
|
}
|
|
207
211
|
return false
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
export function dispatchNotifications(notifications: PendingNotification[]) {
|
|
211
215
|
if (!notifications.length) return
|
|
212
|
-
const combinedEffects = new Set<
|
|
216
|
+
const combinedEffects = new Set<EffectTrigger>()
|
|
213
217
|
|
|
214
218
|
// Extract origin from first notification (all should have the same origin from a single deep touch)
|
|
215
219
|
const origin = notifications[0]?.origin
|
|
216
|
-
let allowedEffects: Set<
|
|
220
|
+
let allowedEffects: Set<EffectTrigger> | undefined
|
|
217
221
|
|
|
218
222
|
// If origin exists, compute allowed effects (those that depend on origin.obj[origin.prop])
|
|
219
223
|
if (origin) {
|
|
220
|
-
allowedEffects = new Set<
|
|
224
|
+
allowedEffects = new Set<EffectTrigger>()
|
|
221
225
|
const originWatchers = watchers.get(origin.obj)
|
|
222
226
|
if (originWatchers) {
|
|
223
|
-
const originEffects = new Set<
|
|
227
|
+
const originEffects = new Set<EffectTrigger>()
|
|
224
228
|
collectEffects(
|
|
225
229
|
origin.obj,
|
|
226
230
|
{ type: 'set', prop: origin.prop },
|
|
@@ -240,16 +244,17 @@ export function dispatchNotifications(notifications: PendingNotification[]) {
|
|
|
240
244
|
const obj = unwrap(target)
|
|
241
245
|
addState(obj, evolution)
|
|
242
246
|
const objectWatchers = watchers.get(obj)
|
|
243
|
-
let currentEffects: Set<
|
|
247
|
+
let currentEffects: Set<EffectTrigger> | undefined
|
|
244
248
|
const propsArray = [prop]
|
|
245
249
|
if (objectWatchers) {
|
|
246
|
-
|
|
250
|
+
// console.log(`[DEBUG] dispatchNotifications: processing ${obj.constructor.name} (has watchers)`)
|
|
251
|
+
currentEffects = new Set<EffectTrigger>()
|
|
247
252
|
collectEffects(obj, evolution, currentEffects, objectWatchers, [allProps], propsArray)
|
|
248
253
|
|
|
249
254
|
// Filter effects by ancestor chain if origin exists
|
|
250
255
|
// Include effects that either directly depend on origin or have an ancestor that does
|
|
251
256
|
if (origin && allowedEffects) {
|
|
252
|
-
const filteredEffects = new Set<
|
|
257
|
+
const filteredEffects = new Set<EffectTrigger>()
|
|
253
258
|
for (const effect of currentEffects) {
|
|
254
259
|
// Check if effect itself is allowed OR has an ancestor that is allowed
|
|
255
260
|
if (allowedEffects.has(effect) || hasAncestorInSet(effect, allowedEffects)) {
|
|
@@ -261,7 +266,7 @@ export function dispatchNotifications(notifications: PendingNotification[]) {
|
|
|
261
266
|
|
|
262
267
|
for (const effect of currentEffects) combinedEffects.add(effect)
|
|
263
268
|
}
|
|
264
|
-
|
|
269
|
+
optionCall('touched', obj, evolution, propsArray, currentEffects)
|
|
265
270
|
if (objectsWithDeepWatchers.has(obj)) bubbleUpChange(obj, evolution)
|
|
266
271
|
}
|
|
267
272
|
if (combinedEffects.size) batch([...combinedEffects])
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { batch } from './effects'
|
|
2
|
-
import type {
|
|
2
|
+
import type { EffectTrigger, Evolution } from './types'
|
|
3
3
|
|
|
4
4
|
// Track which objects contain which other objects (back-references)
|
|
5
5
|
export const objectParents = new WeakMap<object, Set<{ parent: object; prop: PropertyKey }>>()
|
|
@@ -8,10 +8,10 @@ export const objectParents = new WeakMap<object, Set<{ parent: object; prop: Pro
|
|
|
8
8
|
export const objectsWithDeepWatchers = new WeakSet<object>()
|
|
9
9
|
|
|
10
10
|
// Track deep watchers per object
|
|
11
|
-
export const deepWatchers = new WeakMap<object, Set<
|
|
11
|
+
export const deepWatchers = new WeakMap<object, Set<EffectTrigger>>()
|
|
12
12
|
|
|
13
13
|
// Track which effects are doing deep watching
|
|
14
|
-
export const effectToDeepWatchedObjects = new WeakMap<
|
|
14
|
+
export const effectToDeepWatchedObjects = new WeakMap<EffectTrigger, Set<object>>()
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Add a back-reference from child to parent
|
|
@@ -7,7 +7,7 @@ import { effect } from './effects'
|
|
|
7
7
|
import { isNonReactive } from './non-reactive-state'
|
|
8
8
|
import { reactive, unwrap } from './proxy'
|
|
9
9
|
import { markWithRoot } from './registry'
|
|
10
|
-
import {
|
|
10
|
+
import { type EffectCleanup, type EffectTrigger, options } from './types'
|
|
11
11
|
|
|
12
12
|
function isObject(value: any): value is object {
|
|
13
13
|
return typeof value === 'object' && value !== null
|
|
@@ -42,11 +42,14 @@ export function deepWatch<T extends object>(
|
|
|
42
42
|
target: T,
|
|
43
43
|
callback: (value: T) => void,
|
|
44
44
|
{ immediate = false } = {}
|
|
45
|
-
):
|
|
45
|
+
): EffectCleanup | undefined {
|
|
46
46
|
if (target === null || target === undefined) return undefined
|
|
47
47
|
if (typeof target !== 'object') throw new Error('Target of deep watching must be an object')
|
|
48
|
-
// Create a wrapper callback that matches
|
|
49
|
-
const wrappedCallback:
|
|
48
|
+
// Create a wrapper callback that matches EffectTrigger signature
|
|
49
|
+
const wrappedCallback: EffectTrigger = markWithRoot(
|
|
50
|
+
(() => callback(target)) as EffectTrigger,
|
|
51
|
+
callback
|
|
52
|
+
)
|
|
50
53
|
|
|
51
54
|
// Use the existing effect system to register dependencies
|
|
52
55
|
return effect(() => {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { attend } from './buffer'
|
|
2
|
+
import { touched1 } from './change'
|
|
3
|
+
import { cleanedBy } from './effect-context'
|
|
4
|
+
import { reactive } from './proxy'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reactively defines properties on a target object based on a descriptors record.
|
|
8
|
+
*
|
|
9
|
+
* It tracks the keys of the descriptors record and defines/updates/removes
|
|
10
|
+
* properties on the target object as they change.
|
|
11
|
+
*
|
|
12
|
+
* @param descriptors - A reactive record of property descriptors.
|
|
13
|
+
* @param target - The object to define properties on (defaults to a new object).
|
|
14
|
+
* @returns The target object with reactive property definitions.
|
|
15
|
+
*/
|
|
16
|
+
export function describe<T extends object>(
|
|
17
|
+
descriptors: Record<PropertyKey, PropertyDescriptor>,
|
|
18
|
+
target: T = Object.create(null) as T
|
|
19
|
+
): T {
|
|
20
|
+
descriptors = reactive(descriptors)
|
|
21
|
+
|
|
22
|
+
const stop = attend(
|
|
23
|
+
() => Reflect.ownKeys(descriptors),
|
|
24
|
+
(key) => {
|
|
25
|
+
const desc = (descriptors as any)[key]
|
|
26
|
+
if (desc) {
|
|
27
|
+
Object.defineProperty(target, key, {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
...desc,
|
|
30
|
+
configurable: true,
|
|
31
|
+
})
|
|
32
|
+
touched1(target, { type: 'set', prop: key }, key)
|
|
33
|
+
}
|
|
34
|
+
return () => Reflect.deleteProperty(target, key)
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return cleanedBy(target, stop)
|
|
39
|
+
}
|
|
@@ -1,18 +1,83 @@
|
|
|
1
1
|
import { tag } from '../utils'
|
|
2
2
|
import { asyncZone, ZoneAggregator, ZoneHistory } from '../zone'
|
|
3
|
+
import { effect, untracked } from './effects'
|
|
3
4
|
import { getRoot } from './registry'
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
cleanup,
|
|
7
|
+
type EffectAccess,
|
|
8
|
+
type EffectTrigger,
|
|
9
|
+
type ScopedCallback,
|
|
10
|
+
stopped,
|
|
11
|
+
} from './types'
|
|
5
12
|
|
|
6
|
-
export const effectHistory = tag(new ZoneHistory<
|
|
7
|
-
tag(effectHistory.present,
|
|
13
|
+
export const effectHistory = tag('effectHistory', new ZoneHistory<EffectTrigger>())
|
|
14
|
+
tag('effectHistory.present', effectHistory.present)
|
|
8
15
|
asyncZone.add(effectHistory)
|
|
9
|
-
export const effectAggregator = tag(new ZoneAggregator(effectHistory.present), 'effectAggregator')
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Aggregator for zones that need to be tracked along effects.
|
|
19
|
+
* ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
|
|
20
|
+
*/
|
|
21
|
+
export const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present))
|
|
22
|
+
|
|
23
|
+
export function isRunning(effect: EffectTrigger): boolean {
|
|
12
24
|
const root = getRoot(effect)
|
|
13
25
|
return effectHistory.some((e) => getRoot(e) === root)
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
export function getActiveEffect() {
|
|
17
29
|
return effectHistory.present.active
|
|
18
|
-
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ADD a cleanup function to an object using the cleanup symbol.
|
|
34
|
+
* The cleanup function will be called when the object needs to be disposed.
|
|
35
|
+
*
|
|
36
|
+
* Note: most of the time, you don't need to use this function directly.
|
|
37
|
+
* The main use if for the cleanup function to be stored with the object, as GC calls the cleanup function when the *function* is garbage collected.
|
|
38
|
+
*
|
|
39
|
+
* @param obj - The object to attach the cleanup function to
|
|
40
|
+
* @param cleanupFn - The cleanup function to attach
|
|
41
|
+
* @returns The object with the cleanup function attached
|
|
42
|
+
*/
|
|
43
|
+
export function cleanedBy<T extends object>(obj: T, cleanupFn: ScopedCallback) {
|
|
44
|
+
const oldCleanup = obj[cleanup]
|
|
45
|
+
return Object.defineProperty(obj, cleanup, {
|
|
46
|
+
value: oldCleanup
|
|
47
|
+
? Object.defineProperties(
|
|
48
|
+
() => {
|
|
49
|
+
oldCleanup()
|
|
50
|
+
cleanupFn()
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
[stopped]: { get: () => oldCleanup[stopped] || cleanupFn[stopped] },
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
: cleanupFn,
|
|
57
|
+
writable: false,
|
|
58
|
+
enumerable: false,
|
|
59
|
+
configurable: true,
|
|
60
|
+
}) as T & { [cleanup]: ScopedCallback }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#region greedy caching
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a derived value that automatically recomputes when dependencies change
|
|
67
|
+
* @param compute - Function that computes the derived value
|
|
68
|
+
* @returns Object with value and cleanup function
|
|
69
|
+
*/
|
|
70
|
+
export function derived<T>(compute: (dep: EffectAccess) => T): {
|
|
71
|
+
value: T
|
|
72
|
+
[cleanup]: ScopedCallback
|
|
73
|
+
} {
|
|
74
|
+
const rv = { value: undefined as unknown as T }
|
|
75
|
+
return cleanedBy(
|
|
76
|
+
rv,
|
|
77
|
+
untracked(() =>
|
|
78
|
+
effect(function derivedEffect(access) {
|
|
79
|
+
rv.value = compute(access)
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
}
|