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/index.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
+
export { attend, lift, type ScanResult, scan } from './buffer'
|
|
1
2
|
export { getState, touched, touched1 } from './change'
|
|
2
|
-
export type { ReactivityGraph } from './debug'
|
|
3
|
-
export {
|
|
4
|
-
buildReactivityGraph,
|
|
5
|
-
enableDevTools,
|
|
6
|
-
isDevtoolsEnabled,
|
|
7
|
-
registerEffectForDebug,
|
|
8
|
-
registerObjectForDebug,
|
|
9
|
-
setEffectName,
|
|
10
|
-
setObjectName,
|
|
11
|
-
} from './debug'
|
|
12
3
|
export { deepWatch } from './deep-watch'
|
|
4
|
+
export { cleanedBy, derived, effectAggregator, getActiveEffect } from './effect-context'
|
|
13
5
|
export {
|
|
14
6
|
addBatchCleanup,
|
|
15
7
|
atomic,
|
|
@@ -17,26 +9,32 @@ export {
|
|
|
17
9
|
defer,
|
|
18
10
|
effect,
|
|
19
11
|
getActivationLog,
|
|
12
|
+
onEffectThrow,
|
|
13
|
+
onEffectTrigger,
|
|
14
|
+
reset,
|
|
20
15
|
root,
|
|
21
|
-
trackEffect,
|
|
22
16
|
untracked,
|
|
23
17
|
} from './effects'
|
|
24
|
-
export { cleanedBy, cleanup, derived, unreactive, watch } from './interface'
|
|
25
18
|
export { type Memoizable, memoize } from './memoize'
|
|
26
19
|
export { immutables, isNonReactive, registerNativeReactivity } from './non-reactive'
|
|
27
20
|
export { getActiveProjection, project } from './project'
|
|
28
21
|
export { isReactive, ReactiveBase, reactive, unwrap } from './proxy'
|
|
29
22
|
export { organize, organized } from './record'
|
|
30
|
-
export { scan, type ScanResult } from './buffer'
|
|
31
23
|
export { Register, register } from './register'
|
|
32
24
|
export {
|
|
33
|
-
|
|
25
|
+
cleanup,
|
|
26
|
+
type EffectAccess as DependencyAccess,
|
|
27
|
+
type EffectCleanup,
|
|
28
|
+
type EffectOptions,
|
|
29
|
+
type EffectTrigger,
|
|
34
30
|
type Evolution,
|
|
35
31
|
options as reactiveOptions,
|
|
36
32
|
ReactiveError,
|
|
37
33
|
ReactiveErrorCode,
|
|
38
34
|
type ScopedCallback,
|
|
39
35
|
} from './types'
|
|
36
|
+
export { unreactive, watch } from './watch'
|
|
37
|
+
export { describe } from './describe'
|
|
40
38
|
|
|
41
39
|
import { ReactiveArray } from './array'
|
|
42
40
|
import {
|
package/src/reactive/map.ts
CHANGED
|
@@ -10,10 +10,6 @@ import { dependant } from './tracking'
|
|
|
10
10
|
* Only tracks individual key operations, no size tracking (WeakMap limitation)
|
|
11
11
|
*/
|
|
12
12
|
export abstract class ReactiveWeakMap<K extends object, V> extends WeakMap<K, V> {
|
|
13
|
-
get [Symbol.toStringTag]() {
|
|
14
|
-
return 'ReactiveWeakMap'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
13
|
// Implement WeakMap interface methods with reactivity
|
|
18
14
|
delete(key: K): boolean {
|
|
19
15
|
const hadKey = this.has(key)
|
|
@@ -53,10 +49,6 @@ export abstract class ReactiveWeakMap<K extends object, V> extends WeakMap<K, V>
|
|
|
53
49
|
* Tracks size changes, individual key operations, and collection-wide operations
|
|
54
50
|
*/
|
|
55
51
|
export abstract class ReactiveMap<K, V> extends Map<K, V> {
|
|
56
|
-
get [Symbol.toStringTag]() {
|
|
57
|
-
return 'ReactiveMap'
|
|
58
|
-
}
|
|
59
|
-
|
|
60
52
|
// Implement Map interface methods with reactivity
|
|
61
53
|
get size(): number {
|
|
62
54
|
dependant(this, 'size') // The ReactiveMap instance still goes through proxy
|
|
@@ -97,23 +89,15 @@ export abstract class ReactiveMap<K, V> extends Map<K, V> {
|
|
|
97
89
|
|
|
98
90
|
[Symbol.iterator](): MapIterator<[K, V]> {
|
|
99
91
|
dependant(contentRef(this))
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
value: [result.value[0], reactive(result.value[1])],
|
|
109
|
-
done: false,
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
[Symbol.iterator]() {
|
|
113
|
-
return this
|
|
114
|
-
},
|
|
115
|
-
[Symbol.dispose]() {},
|
|
92
|
+
const it: MapIterator<[K, V]> = Map.prototype[Symbol.iterator].call(this)
|
|
93
|
+
const nativeNext = it.next.bind(it)
|
|
94
|
+
it.next = () => {
|
|
95
|
+
const result = nativeNext()
|
|
96
|
+
if (result.done) return result
|
|
97
|
+
const [key, value] = result.value
|
|
98
|
+
return { value: [reactive(key), reactive(value)], done: false }
|
|
116
99
|
}
|
|
100
|
+
return it
|
|
117
101
|
}
|
|
118
102
|
|
|
119
103
|
// Implement Map methods with reactivity
|
package/src/reactive/memoize.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { touched1 } from './change'
|
|
|
4
4
|
import { effect, root, untracked } from './effects'
|
|
5
5
|
import { getRoot, markWithRoot } from './registry'
|
|
6
6
|
import { dependant } from './tracking'
|
|
7
|
-
import { options, rootFunction } from './types'
|
|
7
|
+
import { optionCall, options, rootFunction } from './types'
|
|
8
8
|
|
|
9
9
|
export type Memoizable = object | any[] | symbol | ((...args: any[]) => any)
|
|
10
10
|
|
|
@@ -54,7 +54,7 @@ function memoizeFunction<Result, Args extends Memoizable[]>(
|
|
|
54
54
|
try {
|
|
55
55
|
const fresh = untracked(() => fn(...localArgs))
|
|
56
56
|
if (!deepCompare(node.result, fresh)) {
|
|
57
|
-
|
|
57
|
+
optionCall('onMemoizationDiscrepancy', node.result, fresh, fn, localArgs, 'calculation')
|
|
58
58
|
}
|
|
59
59
|
} finally {
|
|
60
60
|
options.isVerificationRun = wasVerification
|
|
@@ -93,7 +93,7 @@ function memoizeFunction<Result, Args extends Memoizable[]>(
|
|
|
93
93
|
try {
|
|
94
94
|
const fresh = untracked(() => fn(...localArgs))
|
|
95
95
|
if (!deepCompare(node.result, fresh)) {
|
|
96
|
-
|
|
96
|
+
optionCall('onMemoizationDiscrepancy', node.result, fresh, fn, localArgs, 'comparison')
|
|
97
97
|
}
|
|
98
98
|
} finally {
|
|
99
99
|
options.isVerificationRun = wasVerification
|
|
@@ -108,6 +108,29 @@ function memoizeFunction<Result, Args extends Memoizable[]>(
|
|
|
108
108
|
return memoized as (...args: Args) => Result
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Decorator and function wrapper for memoizing computed values based on reactive dependencies.
|
|
113
|
+
*
|
|
114
|
+
* When used as a decorator on getters or methods, it caches the result and automatically
|
|
115
|
+
* invalidates the cache when reactive dependencies change.
|
|
116
|
+
*
|
|
117
|
+
* When used as a function wrapper, it memoizes based on object arguments (WeakMap-based cache).
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* class User {
|
|
122
|
+
* @memoize
|
|
123
|
+
* get fullName() {
|
|
124
|
+
* return `${this.firstName} ${this.lastName}`
|
|
125
|
+
* }
|
|
126
|
+
* }
|
|
127
|
+
*
|
|
128
|
+
* // Or as a function wrapper
|
|
129
|
+
* const expensive = memoize((obj: SomeObject) => {
|
|
130
|
+
* return heavyComputation(obj)
|
|
131
|
+
* })
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
111
134
|
export const memoize = decorator({
|
|
112
135
|
getter(original, target, propertyKey) {
|
|
113
136
|
return function (this: any) {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# project
|
|
2
|
+
|
|
3
|
+
This document explains how `project(...)` composes reactive effects, and why it uses `ascend(...)` when creating per-item/per-key effects.
|
|
4
|
+
|
|
5
|
+
## What `project` builds
|
|
6
|
+
|
|
7
|
+
`project` is a reactive “mapping” primitive.
|
|
8
|
+
|
|
9
|
+
- A **source** collection (`Array`, `Map`, `Register`, plain record)
|
|
10
|
+
- A **target** collection of the same shape
|
|
11
|
+
- A set of effects that keep target in sync with source:
|
|
12
|
+
- **structure effect** (length/keys watcher)
|
|
13
|
+
- **item effect** per key/index (computes the projected value)
|
|
14
|
+
|
|
15
|
+
The important property is **stability**: changing one source entry should only re-run the effect that owns that entry.
|
|
16
|
+
|
|
17
|
+
## Effects involved
|
|
18
|
+
|
|
19
|
+
For `project.array(source, apply)` the implementation creates:
|
|
20
|
+
|
|
21
|
+
- **Length effect**: watches the set of valid indexes by reading `source.length`.
|
|
22
|
+
- **Index effects**: created lazily (when an index appears), one effect per index.
|
|
23
|
+
|
|
24
|
+
The other variants (`project.map`, `project.record`, `project.register`) follow the same pattern:
|
|
25
|
+
|
|
26
|
+
- a *keys effect* (watches which keys exist)
|
|
27
|
+
- a *key effect* per key
|
|
28
|
+
|
|
29
|
+
## Cleanup and “garbage collection”
|
|
30
|
+
|
|
31
|
+
`effect(...)` has two cleanup pathways:
|
|
32
|
+
|
|
33
|
+
- **explicit**: you keep the returned stop function and call it.
|
|
34
|
+
- **GC-driven** (Node with `--expose-gc`): for **root effects only**, a `FinalizationRegistry` calls `stopEffect()` when the stop function itself becomes unreachable.
|
|
35
|
+
|
|
36
|
+
Parenting matters because only root effects are eligible for automatic GC cleanup.
|
|
37
|
+
|
|
38
|
+
### Parent-child relationship
|
|
39
|
+
|
|
40
|
+
When an effect is created while another effect is active, it becomes a **child**:
|
|
41
|
+
|
|
42
|
+
- the parent keeps a reference to a child-cleanup function
|
|
43
|
+
- when the parent is cleaned up, it calls child-cleanups
|
|
44
|
+
|
|
45
|
+
This guarantees that **unstored** children do not leak and do not rely on GC.
|
|
46
|
+
|
|
47
|
+
## Why `project` uses `ascend(...)`
|
|
48
|
+
|
|
49
|
+
In `project.ts`, per-item effects are created inside the structure effect.
|
|
50
|
+
|
|
51
|
+
If we created item effects “normally” inside the structure effect body, they would be parented to the structure effect.
|
|
52
|
+
|
|
53
|
+
That is *not* what we want:
|
|
54
|
+
|
|
55
|
+
- The structure effect re-runs whenever the set of keys/indexes changes.
|
|
56
|
+
- On each re-run, `effect(...)` executes the previous cleanup first.
|
|
57
|
+
- If item effects were children of the structure effect, that cleanup would stop all item effects on every structural update.
|
|
58
|
+
|
|
59
|
+
That would break stability:
|
|
60
|
+
|
|
61
|
+
- pushing a new element would unnecessarily dispose and recreate existing item effects
|
|
62
|
+
- nested projections would be especially unstable
|
|
63
|
+
|
|
64
|
+
### What `ascend` does here
|
|
65
|
+
|
|
66
|
+
`ascend` is a wrapper around `effectHistory.zoned`.
|
|
67
|
+
|
|
68
|
+
It executes a function in the **parent effect context** (the effect that was active when the structure effect itself was created).
|
|
69
|
+
|
|
70
|
+
So the parent chain becomes:
|
|
71
|
+
|
|
72
|
+
- owning effect (the effect that called `project(...)`)
|
|
73
|
+
- structure effect (length/keys)
|
|
74
|
+
- item effects (per index/key)
|
|
75
|
+
|
|
76
|
+
All of them share the same owner.
|
|
77
|
+
|
|
78
|
+
This gives the intended semantics:
|
|
79
|
+
|
|
80
|
+
- structure effect may re-run many times without killing item effects
|
|
81
|
+
- when the owning effect stops (explicitly or by GC), it will stop:
|
|
82
|
+
- the structure effect
|
|
83
|
+
- all item effects
|
|
84
|
+
- all nested projections under those item effects
|
|
85
|
+
|
|
86
|
+
## Projection context
|
|
87
|
+
|
|
88
|
+
During an item effect’s first run, `project` calls:
|
|
89
|
+
|
|
90
|
+
- `setActiveProjection({ source, key, target, depth, parent })`
|
|
91
|
+
|
|
92
|
+
This attaches a `ProjectionContext` to the currently running effect via `effectProjectionMetadata`.
|
|
93
|
+
|
|
94
|
+
`getActiveProjection()` is how nested projections discover their parent projection.
|
|
95
|
+
|
|
96
|
+
The goal is to allow:
|
|
97
|
+
|
|
98
|
+
- a nested projection to compute its own `depth`
|
|
99
|
+
- a nested projection to know its parent projection (for debugging/introspection)
|
|
100
|
+
|
|
101
|
+
## Testing expectations
|
|
102
|
+
|
|
103
|
+
The behavior that matters (and that tests should lock down) is:
|
|
104
|
+
|
|
105
|
+
- item effects are not disposed on structural updates of the same projection
|
|
106
|
+
- disposing an outer item stops all nested projection effects under that item
|
|
107
|
+
- disposing the owning effect (or letting it be GC’ed) stops all projection effects created under it
|
package/src/reactive/project.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { getActiveEffect } from './effect-context'
|
|
1
|
+
import { setEffectName } from '../../debug/debug'
|
|
2
|
+
import { FoolProof, named } from '../utils'
|
|
3
|
+
import { cleanedBy, getActiveEffect } from './effect-context'
|
|
4
4
|
import { effect, untracked } from './effects'
|
|
5
|
-
import { cleanedBy, cleanup } from './interface'
|
|
6
5
|
import { reactive } from './proxy'
|
|
7
6
|
import { Register } from './register'
|
|
8
|
-
import {
|
|
9
|
-
|
|
7
|
+
import {
|
|
8
|
+
cleanup,
|
|
9
|
+
type EffectTrigger,
|
|
10
|
+
type ProjectionContext,
|
|
11
|
+
projectionInfo,
|
|
12
|
+
type ScopedCallback,
|
|
13
|
+
} from './types'
|
|
10
14
|
/**
|
|
11
15
|
* Maps projection effects (item effects) to their projection context
|
|
12
16
|
*/
|
|
13
|
-
export const effectProjectionMetadata = new WeakMap<
|
|
17
|
+
export const effectProjectionMetadata = new WeakMap<EffectTrigger, ProjectionContext>()
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Returns the projection context of the currently running effect, if any.
|
|
@@ -19,11 +23,10 @@ export function getActiveProjection(): ProjectionContext | undefined {
|
|
|
19
23
|
const active = getActiveEffect()
|
|
20
24
|
return active ? effectProjectionMetadata.get(active) : undefined
|
|
21
25
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*/
|
|
26
|
+
function setActiveProjection(projection: ProjectionContext | undefined) {
|
|
27
|
+
const active = getActiveEffect()
|
|
28
|
+
if (active) effectProjectionMetadata.set(active, projection)
|
|
29
|
+
}
|
|
27
30
|
|
|
28
31
|
type ProjectOldValue<Target> = Target extends readonly (infer Item)[]
|
|
29
32
|
? Item
|
|
@@ -105,41 +108,33 @@ function projectArray<SourceValue, ResultValue>(
|
|
|
105
108
|
const parent = getActiveProjection()
|
|
106
109
|
const depth = parent ? parent.depth + 1 : 0
|
|
107
110
|
|
|
108
|
-
const cleanupLength = effect(
|
|
111
|
+
const cleanupLength = effect(named(`project:${source[Symbol.toStringTag]}.length`, ({ ascend }) => {
|
|
109
112
|
const length = source.length
|
|
110
113
|
normalizeTargetLength(length)
|
|
111
114
|
const existing = Array.from(indexEffects.keys())
|
|
112
115
|
for (let i = 0; i < length; i++) {
|
|
113
116
|
if (indexEffects.has(i)) continue
|
|
114
|
-
ascend(()
|
|
117
|
+
ascend(function projectArrayIndexAscend() {
|
|
115
118
|
const index = i
|
|
116
|
-
const stop = effect(
|
|
119
|
+
const stop = effect(named(`project:${source[Symbol.toStringTag]}[${index}]`, ({ reaction }) => {
|
|
120
|
+
if (!reaction) setActiveProjection({ source, key: index, target, depth, parent })
|
|
117
121
|
const previous = untracked(() => target[index])
|
|
118
122
|
const accessBase = {
|
|
119
123
|
key: index,
|
|
120
124
|
source,
|
|
121
125
|
get: () => FoolProof.get(source as any, index, source),
|
|
122
|
-
set: (value: SourceValue) =>
|
|
123
|
-
FoolProof.set(source as any, index, value, source),
|
|
126
|
+
set: (value: SourceValue) => FoolProof.set(source as any, index, value, source),
|
|
124
127
|
old: previous,
|
|
125
128
|
} as ProjectAccess<SourceValue, number, readonly SourceValue[], ResultValue[]>
|
|
126
129
|
defineAccessValue(accessBase)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
})
|
|
130
|
+
target[index] = apply(accessBase, target)
|
|
131
|
+
}))
|
|
130
132
|
setEffectName(stop, `project[${depth}]:${index}`)
|
|
131
|
-
effectProjectionMetadata.set(stop, {
|
|
132
|
-
source,
|
|
133
|
-
key: index,
|
|
134
|
-
target,
|
|
135
|
-
depth,
|
|
136
|
-
parent,
|
|
137
|
-
})
|
|
138
133
|
indexEffects.set(i, stop)
|
|
139
134
|
})
|
|
140
135
|
}
|
|
141
136
|
for (const index of existing) if (index >= length) disposeIndex(index)
|
|
142
|
-
})
|
|
137
|
+
}))
|
|
143
138
|
|
|
144
139
|
return makeCleanup(target, indexEffects, () => cleanupLength(), {
|
|
145
140
|
source,
|
|
@@ -160,7 +155,7 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
|
160
155
|
ResultValue
|
|
161
156
|
>
|
|
162
157
|
): ProjectResult<Map<Key, ResultValue>> {
|
|
163
|
-
|
|
158
|
+
source = reactive(source) as Register<SourceValue, Key>
|
|
164
159
|
const rawTarget = new Map<Key, ResultValue>()
|
|
165
160
|
const target = reactive(rawTarget) as Map<Key, ResultValue>
|
|
166
161
|
const keyEffects = new Map<Key, ScopedCallback>()
|
|
@@ -179,19 +174,20 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
|
179
174
|
|
|
180
175
|
const cleanupKeys = effect(function projectRegisterEffect({ ascend }) {
|
|
181
176
|
const keys = new Set<Key>()
|
|
182
|
-
for (const key of
|
|
177
|
+
for (const key of source.mapKeys()) keys.add(key)
|
|
183
178
|
|
|
184
179
|
for (const key of keys) {
|
|
185
180
|
if (keyEffects.has(key)) continue
|
|
186
181
|
ascend(() => {
|
|
187
|
-
const stop = effect(function projectRegisterKeyEffect() {
|
|
182
|
+
const stop = effect(function projectRegisterKeyEffect({ reaction }) {
|
|
183
|
+
if (!reaction) setActiveProjection({ source, key, target, depth, parent })
|
|
188
184
|
const previous = untracked(() => target.get(key))
|
|
189
185
|
const accessBase = {
|
|
190
186
|
key,
|
|
191
|
-
source:
|
|
192
|
-
get: () =>
|
|
187
|
+
source: source,
|
|
188
|
+
get: () => source.get(key) as SourceValue,
|
|
193
189
|
set: (value: SourceValue) => {
|
|
194
|
-
|
|
190
|
+
source.set(key, value)
|
|
195
191
|
return true
|
|
196
192
|
},
|
|
197
193
|
old: previous,
|
|
@@ -201,13 +197,6 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
|
201
197
|
target.set(key, produced)
|
|
202
198
|
})
|
|
203
199
|
setEffectName(stop, `project[${depth}]:${String(key)}`)
|
|
204
|
-
effectProjectionMetadata.set(stop, {
|
|
205
|
-
source: observedSource,
|
|
206
|
-
key,
|
|
207
|
-
target,
|
|
208
|
-
depth,
|
|
209
|
-
parent,
|
|
210
|
-
})
|
|
211
200
|
keyEffects.set(key, stop)
|
|
212
201
|
})
|
|
213
202
|
}
|
|
@@ -216,7 +205,7 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
|
216
205
|
})
|
|
217
206
|
|
|
218
207
|
return makeCleanup(target, keyEffects, () => cleanupKeys(), {
|
|
219
|
-
source:
|
|
208
|
+
source: source,
|
|
220
209
|
target,
|
|
221
210
|
apply,
|
|
222
211
|
depth,
|
|
@@ -234,7 +223,7 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
|
234
223
|
ResultValue
|
|
235
224
|
>
|
|
236
225
|
): ProjectResult<Record<keyof Source, ResultValue>> {
|
|
237
|
-
|
|
226
|
+
source = reactive(source) as Source
|
|
238
227
|
const target = reactive({} as Record<keyof Source, ResultValue>)
|
|
239
228
|
const keyEffects = new Map<PropertyKey, ScopedCallback>()
|
|
240
229
|
|
|
@@ -252,24 +241,25 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
|
252
241
|
|
|
253
242
|
const cleanupKeys = effect(function projectRecordEffect({ ascend }) {
|
|
254
243
|
const keys = new Set<PropertyKey>()
|
|
255
|
-
for (const key in
|
|
256
|
-
const observed = Reflect.ownKeys(
|
|
244
|
+
for (const key in source) keys.add(key)
|
|
245
|
+
const observed = Reflect.ownKeys(source)
|
|
257
246
|
for (const key of observed) keys.add(key)
|
|
258
247
|
|
|
259
248
|
for (const key of keys) {
|
|
260
249
|
if (keyEffects.has(key)) continue
|
|
261
250
|
ascend(() => {
|
|
262
|
-
const stop = effect(function projectRecordKeyEffect() {
|
|
251
|
+
const stop = effect(function projectRecordKeyEffect({ reaction }) {
|
|
252
|
+
if (!reaction) setActiveProjection({ source, key, target, depth, parent })
|
|
263
253
|
const sourceKey = key as keyof Source
|
|
264
254
|
const previous = untracked(
|
|
265
255
|
() => (target as Record<PropertyKey, ResultValue | undefined>)[key]
|
|
266
256
|
)
|
|
267
257
|
const accessBase = {
|
|
268
258
|
key: sourceKey,
|
|
269
|
-
source:
|
|
270
|
-
get: () => FoolProof.get(
|
|
259
|
+
source: source,
|
|
260
|
+
get: () => FoolProof.get(source, sourceKey, source),
|
|
271
261
|
set: (value: Source[typeof sourceKey]) =>
|
|
272
|
-
FoolProof.set(
|
|
262
|
+
FoolProof.set(source, sourceKey, value, source),
|
|
273
263
|
old: previous,
|
|
274
264
|
} as ProjectAccess<
|
|
275
265
|
Source[typeof sourceKey],
|
|
@@ -282,13 +272,6 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
|
282
272
|
;(target as any)[sourceKey] = produced
|
|
283
273
|
})
|
|
284
274
|
setEffectName(stop, `project[${depth}]:${String(key)}`)
|
|
285
|
-
effectProjectionMetadata.set(stop, {
|
|
286
|
-
source: observedSource,
|
|
287
|
-
key,
|
|
288
|
-
target,
|
|
289
|
-
depth,
|
|
290
|
-
parent,
|
|
291
|
-
})
|
|
292
275
|
keyEffects.set(key, stop)
|
|
293
276
|
})
|
|
294
277
|
}
|
|
@@ -297,7 +280,7 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
|
297
280
|
})
|
|
298
281
|
|
|
299
282
|
return makeCleanup(target, keyEffects, () => cleanupKeys(), {
|
|
300
|
-
source:
|
|
283
|
+
source: source,
|
|
301
284
|
target,
|
|
302
285
|
apply,
|
|
303
286
|
depth,
|
|
@@ -309,7 +292,7 @@ function projectMap<Key, Value, ResultValue>(
|
|
|
309
292
|
source: Map<Key, Value>,
|
|
310
293
|
apply: ProjectCallback<Value, Key, Map<Key, ResultValue>, Map<Key, Value>, ResultValue>
|
|
311
294
|
): ProjectResult<Map<Key, ResultValue>> {
|
|
312
|
-
|
|
295
|
+
source = reactive(source) as Map<Key, Value>
|
|
313
296
|
const rawTarget = new Map<Key, ResultValue>()
|
|
314
297
|
const target = reactive(rawTarget) as Map<Key, ResultValue>
|
|
315
298
|
const keyEffects = new Map<Key, ScopedCallback>()
|
|
@@ -328,19 +311,20 @@ function projectMap<Key, Value, ResultValue>(
|
|
|
328
311
|
|
|
329
312
|
const cleanupKeys = effect(function projectMapEffect({ ascend }) {
|
|
330
313
|
const keys = new Set<Key>()
|
|
331
|
-
for (const key of
|
|
314
|
+
for (const key of source.keys()) keys.add(key)
|
|
332
315
|
|
|
333
316
|
for (const key of keys) {
|
|
334
317
|
if (keyEffects.has(key)) continue
|
|
335
318
|
ascend(() => {
|
|
336
|
-
const stop = effect(function projectMapKeyEffect() {
|
|
319
|
+
const stop = effect(function projectMapKeyEffect({ reaction }) {
|
|
320
|
+
if (!reaction) setActiveProjection({ source, key, target, depth, parent })
|
|
337
321
|
const previous = untracked(() => target.get(key))
|
|
338
322
|
const accessBase = {
|
|
339
323
|
key,
|
|
340
|
-
source:
|
|
341
|
-
get: () =>
|
|
324
|
+
source: source,
|
|
325
|
+
get: () => source.get(key) as Value,
|
|
342
326
|
set: (value: Value) => {
|
|
343
|
-
|
|
327
|
+
source.set(key, value)
|
|
344
328
|
return true
|
|
345
329
|
},
|
|
346
330
|
old: previous,
|
|
@@ -350,13 +334,6 @@ function projectMap<Key, Value, ResultValue>(
|
|
|
350
334
|
target.set(key, produced)
|
|
351
335
|
})
|
|
352
336
|
setEffectName(stop, `project[${depth}]:${String(key)}`)
|
|
353
|
-
effectProjectionMetadata.set(stop, {
|
|
354
|
-
source: observedSource,
|
|
355
|
-
key,
|
|
356
|
-
target,
|
|
357
|
-
depth,
|
|
358
|
-
parent,
|
|
359
|
-
})
|
|
360
337
|
keyEffects.set(key, stop)
|
|
361
338
|
})
|
|
362
339
|
}
|
|
@@ -365,7 +342,7 @@ function projectMap<Key, Value, ResultValue>(
|
|
|
365
342
|
})
|
|
366
343
|
|
|
367
344
|
return makeCleanup(target, keyEffects, () => cleanupKeys(), {
|
|
368
|
-
source:
|
|
345
|
+
source: source,
|
|
369
346
|
target,
|
|
370
347
|
apply,
|
|
371
348
|
depth,
|
package/src/reactive/proxy.ts
CHANGED
|
@@ -35,11 +35,18 @@ const hasReentry: any[] = []
|
|
|
35
35
|
const reactiveHandlers = {
|
|
36
36
|
[Symbol.toStringTag]: 'MutTs Reactive',
|
|
37
37
|
get(obj: any, prop: PropertyKey, receiver: any) {
|
|
38
|
-
if (obj && typeof obj === 'object' &&
|
|
38
|
+
if (obj && typeof obj === 'object' && prop !== Symbol.toStringTag) {
|
|
39
39
|
const metaProto = metaProtos.get(obj.constructor)
|
|
40
|
-
if (metaProto && prop
|
|
40
|
+
if (metaProto && Object.hasOwn(metaProto, prop)) {
|
|
41
41
|
const desc = Object.getOwnPropertyDescriptor(metaProto, prop)!
|
|
42
|
-
|
|
42
|
+
if (desc.get) {
|
|
43
|
+
if (!Object.hasOwn(obj, prop)) return desc.get.call(obj)
|
|
44
|
+
// For own properties (e.g., array length): only override if writable/configurable
|
|
45
|
+
const ownDesc = Object.getOwnPropertyDescriptor(obj, prop)!
|
|
46
|
+
if (ownDesc.configurable || ownDesc.writable || ownDesc.get)
|
|
47
|
+
return desc.get.call(obj)
|
|
48
|
+
} else if (!Object.hasOwn(obj, prop))
|
|
49
|
+
return (...args: any[]) => desc.value.apply(obj, args)
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
if (prop === nonReactiveMark) return false
|
|
@@ -103,6 +110,17 @@ const reactiveHandlers = {
|
|
|
103
110
|
if (unwrappedObj[unreactiveProperties]?.has(prop) || unwrappedObj !== unwrappedReceiver)
|
|
104
111
|
return FoolProof.set(obj, prop, value, receiver)
|
|
105
112
|
const newValue = unwrap(value)
|
|
113
|
+
// metaProto setter dispatch (e.g., reactive array length)
|
|
114
|
+
if (obj && typeof obj === 'object' && prop !== Symbol.toStringTag) {
|
|
115
|
+
const metaProto = obj.constructor && metaProtos.get(obj.constructor)
|
|
116
|
+
if (metaProto && Object.hasOwn(metaProto, prop)) {
|
|
117
|
+
const desc = Object.getOwnPropertyDescriptor(metaProto, prop)!
|
|
118
|
+
if (desc.set) {
|
|
119
|
+
desc.set.call(obj, newValue)
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
106
124
|
// Read old value, using withEffect(undefined, ...) for getter-only accessors to avoid
|
|
107
125
|
// breaking memoization dependency tracking during SET operations
|
|
108
126
|
let oldVal = absent
|
package/src/reactive/record.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { FoolProof } from '../utils'
|
|
2
|
+
import { attend } from './buffer'
|
|
2
3
|
import { touched1 } from './change'
|
|
3
|
-
import {
|
|
4
|
-
import { cleanedBy, cleanup } from './interface'
|
|
4
|
+
import { cleanedBy } from './effect-context'
|
|
5
5
|
import { reactive } from './proxy'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
cleanup,
|
|
8
|
+
type EffectCloser,
|
|
9
|
+
type ScopedCallback,
|
|
10
|
+
} from './types'
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Provides type-safe access to a source object's property within the organized callback.
|
|
@@ -50,7 +54,7 @@ export type OrganizedCallback<Source extends Record<PropertyKey, any>, Target ex
|
|
|
50
54
|
* The target object where organized data will be stored
|
|
51
55
|
*/
|
|
52
56
|
target: Target
|
|
53
|
-
) =>
|
|
57
|
+
) => EffectCloser | undefined
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
60
|
* The result type of the organized function, combining the target object with cleanup capability.
|
|
@@ -125,51 +129,32 @@ export function organized<
|
|
|
125
129
|
): OrganizedResult<Target> {
|
|
126
130
|
const observedSource = reactive(source) as Source
|
|
127
131
|
const target = reactive(baseTarget) as Target
|
|
128
|
-
const keyEffects = new Map<PropertyKey, ScopedCallback>()
|
|
129
132
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
key: sourceKey,
|
|
150
|
-
get: () => FoolProof.get(observedSource, sourceKey, observedSource),
|
|
151
|
-
set: (value: Source[typeof sourceKey]) =>
|
|
152
|
-
FoolProof.set(observedSource, sourceKey, value, observedSource),
|
|
153
|
-
}
|
|
154
|
-
Object.defineProperty(accessBase, 'value', {
|
|
155
|
-
get: accessBase.get,
|
|
156
|
-
set: accessBase.set,
|
|
157
|
-
configurable: true,
|
|
158
|
-
enumerable: true,
|
|
159
|
-
})
|
|
160
|
-
return apply(accessBase as OrganizedAccess<Source, typeof sourceKey>, target)
|
|
161
|
-
})
|
|
162
|
-
keyEffects.set(key, stop)
|
|
133
|
+
const stop = attend(
|
|
134
|
+
() => {
|
|
135
|
+
const keys: PropertyKey[] = []
|
|
136
|
+
for (const key in observedSource) keys.push(key)
|
|
137
|
+
return keys
|
|
138
|
+
},
|
|
139
|
+
(key) => {
|
|
140
|
+
const sourceKey = key as keyof Source
|
|
141
|
+
const accessBase = {
|
|
142
|
+
key: sourceKey,
|
|
143
|
+
get: () => FoolProof.get(observedSource, sourceKey, observedSource),
|
|
144
|
+
set: (value: Source[typeof sourceKey]) =>
|
|
145
|
+
FoolProof.set(observedSource, sourceKey, value, observedSource),
|
|
146
|
+
}
|
|
147
|
+
Object.defineProperty(accessBase, 'value', {
|
|
148
|
+
get: accessBase.get,
|
|
149
|
+
set: accessBase.set,
|
|
150
|
+
configurable: true,
|
|
151
|
+
enumerable: true,
|
|
163
152
|
})
|
|
153
|
+
return apply(accessBase as OrganizedAccess<Source, typeof sourceKey>, target)
|
|
164
154
|
}
|
|
155
|
+
)
|
|
165
156
|
|
|
166
|
-
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
return cleanedBy(target, () => {
|
|
170
|
-
cleanupKeys()
|
|
171
|
-
for (const key of Array.from(keyEffects.keys())) disposeKey(key)
|
|
172
|
-
}) as OrganizedResult<Target>
|
|
157
|
+
return cleanedBy(target, stop) as OrganizedResult<Target>
|
|
173
158
|
}
|
|
174
159
|
|
|
175
160
|
/**
|