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.
Files changed (191) hide show
  1. package/README.md +72 -17
  2. package/dist/browser.cjs +3111 -0
  3. package/dist/browser.cjs.map +1 -0
  4. package/dist/browser.d.ts +1391 -1
  5. package/dist/browser.esm.js +264 -35
  6. package/dist/browser.esm.js.map +1 -1
  7. package/dist/chunks/async-browser-lvzLOCgk.cjs +297 -0
  8. package/dist/chunks/async-browser-lvzLOCgk.cjs.map +1 -0
  9. package/dist/chunks/async-core-CRLKP3l-.cjs +29 -0
  10. package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
  11. package/dist/chunks/async-node-C3DeIb0y.cjs +96 -0
  12. package/dist/chunks/async-node-C3DeIb0y.cjs.map +1 -0
  13. package/dist/chunks/index-2vea86wD.esm.js +3011 -0
  14. package/dist/chunks/index-2vea86wD.esm.js.map +1 -0
  15. package/dist/chunks/index-CtA2AWl3.esm.js +4376 -0
  16. package/dist/chunks/index-CtA2AWl3.esm.js.map +1 -0
  17. package/dist/chunks/index-VTO-b2vR.cjs +4461 -0
  18. package/dist/chunks/index-VTO-b2vR.cjs.map +1 -0
  19. package/dist/debug/debug.d.ts +122 -0
  20. package/dist/debug/debug.d.ts.map +1 -0
  21. package/dist/debug/index.d.ts +4 -0
  22. package/dist/debug/index.d.ts.map +1 -0
  23. package/dist/debug/lineage-panel.d.ts +5 -0
  24. package/dist/debug/lineage-panel.d.ts.map +1 -0
  25. package/dist/debug/lineage.d.ts +79 -0
  26. package/dist/debug/lineage.d.ts.map +1 -0
  27. package/dist/debug.cjs +35 -0
  28. package/dist/debug.cjs.map +1 -0
  29. package/dist/debug.d.ts +209 -0
  30. package/dist/debug.esm.js +2 -0
  31. package/dist/debug.esm.js.map +1 -0
  32. package/dist/devtools/devtool/devtools.d.ts +1 -0
  33. package/dist/devtools/devtool/devtools.d.ts.map +1 -0
  34. package/dist/devtools/devtool/panel.d.ts +2 -0
  35. package/dist/devtools/devtool/panel.d.ts.map +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/mutts.umd.js +1 -1
  38. package/dist/mutts.umd.js.map +1 -1
  39. package/dist/mutts.umd.min.js +1 -1
  40. package/dist/mutts.umd.min.js.map +1 -1
  41. package/dist/node.cjs +107 -0
  42. package/dist/node.cjs.map +1 -0
  43. package/dist/node.d.ts +2 -2
  44. package/dist/node.esm.js +86 -33
  45. package/dist/node.esm.js.map +1 -1
  46. package/dist/src/async/browser.d.ts +2 -0
  47. package/dist/src/async/browser.d.ts.map +1 -0
  48. package/dist/src/async/index.d.ts +19 -0
  49. package/dist/src/async/index.d.ts.map +1 -0
  50. package/dist/src/async/node.d.ts +2 -0
  51. package/dist/src/async/node.d.ts.map +1 -0
  52. package/dist/src/decorator.d.ts +106 -0
  53. package/dist/src/decorator.d.ts.map +1 -0
  54. package/dist/src/destroyable.d.ts +87 -0
  55. package/dist/src/destroyable.d.ts.map +1 -0
  56. package/dist/src/entry-browser.d.ts +3 -0
  57. package/dist/src/entry-browser.d.ts.map +1 -0
  58. package/dist/src/entry-node.d.ts +3 -0
  59. package/dist/src/entry-node.d.ts.map +1 -0
  60. package/dist/src/eventful.d.ts +20 -0
  61. package/dist/src/eventful.d.ts.map +1 -0
  62. package/dist/src/flavored.d.ts +33 -0
  63. package/dist/src/flavored.d.ts.map +1 -0
  64. package/dist/src/index.d.ts +14 -0
  65. package/dist/src/index.d.ts.map +1 -0
  66. package/dist/src/indexable.d.ts +243 -0
  67. package/dist/src/indexable.d.ts.map +1 -0
  68. package/dist/src/introspection.d.ts +27 -0
  69. package/dist/src/introspection.d.ts.map +1 -0
  70. package/dist/src/iterableWeak.d.ts +53 -0
  71. package/dist/src/iterableWeak.d.ts.map +1 -0
  72. package/dist/src/mixins.d.ts +25 -0
  73. package/dist/src/mixins.d.ts.map +1 -0
  74. package/dist/src/promiseChain.d.ts +20 -0
  75. package/dist/src/promiseChain.d.ts.map +1 -0
  76. package/dist/src/reactive/array.d.ts +48 -0
  77. package/dist/src/reactive/array.d.ts.map +1 -0
  78. package/dist/src/reactive/buffer.d.ts +120 -0
  79. package/dist/src/reactive/buffer.d.ts.map +1 -0
  80. package/dist/src/reactive/change.d.ts +29 -0
  81. package/dist/src/reactive/change.d.ts.map +1 -0
  82. package/dist/src/reactive/deep-touch.d.ts +28 -0
  83. package/dist/src/reactive/deep-touch.d.ts.map +1 -0
  84. package/dist/src/reactive/deep-watch-state.d.ts +25 -0
  85. package/dist/src/reactive/deep-watch-state.d.ts.map +1 -0
  86. package/dist/src/reactive/deep-watch.d.ts +20 -0
  87. package/dist/src/reactive/deep-watch.d.ts.map +1 -0
  88. package/dist/src/reactive/describe.d.ts +12 -0
  89. package/dist/src/reactive/describe.d.ts.map +1 -0
  90. package/dist/src/reactive/effect-context.d.ts +34 -0
  91. package/dist/src/reactive/effect-context.d.ts.map +1 -0
  92. package/dist/src/reactive/effects.d.ts +164 -0
  93. package/dist/src/reactive/effects.d.ts.map +1 -0
  94. package/dist/src/reactive/index.d.ts +19 -0
  95. package/dist/src/reactive/index.d.ts.map +1 -0
  96. package/dist/src/reactive/map.d.ts +28 -0
  97. package/dist/src/reactive/map.d.ts.map +1 -0
  98. package/dist/src/reactive/memoize.d.ts +28 -0
  99. package/dist/src/reactive/memoize.d.ts.map +1 -0
  100. package/dist/src/reactive/non-reactive-state.d.ts +9 -0
  101. package/dist/src/reactive/non-reactive-state.d.ts.map +1 -0
  102. package/dist/src/reactive/non-reactive.d.ts +11 -0
  103. package/dist/src/reactive/non-reactive.d.ts.map +1 -0
  104. package/dist/src/reactive/project.d.ts +40 -0
  105. package/dist/src/reactive/project.d.ts.map +1 -0
  106. package/dist/src/reactive/proxy-state.d.ts +8 -0
  107. package/dist/src/reactive/proxy-state.d.ts.map +1 -0
  108. package/dist/src/reactive/proxy.d.ts +23 -0
  109. package/dist/src/reactive/proxy.d.ts.map +1 -0
  110. package/dist/src/reactive/record.d.ts +115 -0
  111. package/dist/src/reactive/record.d.ts.map +1 -0
  112. package/dist/src/reactive/register.d.ts +125 -0
  113. package/dist/src/reactive/register.d.ts.map +1 -0
  114. package/dist/src/reactive/registry.d.ts +21 -0
  115. package/dist/src/reactive/registry.d.ts.map +1 -0
  116. package/dist/src/reactive/set.d.ts +26 -0
  117. package/dist/src/reactive/set.d.ts.map +1 -0
  118. package/dist/src/reactive/tracking.d.ts +7 -0
  119. package/dist/src/reactive/tracking.d.ts.map +1 -0
  120. package/dist/src/reactive/types.d.ts +424 -0
  121. package/dist/src/reactive/types.d.ts.map +1 -0
  122. package/dist/src/reactive/watch.d.ts +48 -0
  123. package/dist/src/reactive/watch.d.ts.map +1 -0
  124. package/dist/src/std-decorators.d.ts +45 -0
  125. package/dist/src/std-decorators.d.ts.map +1 -0
  126. package/dist/src/utils.d.ts +49 -0
  127. package/dist/src/utils.d.ts.map +1 -0
  128. package/dist/src/zone.d.ts +40 -0
  129. package/dist/src/zone.d.ts.map +1 -0
  130. package/dist/types-DaHFfhlN.d.ts +414 -0
  131. package/docs/ai/api-reference.md +1 -1
  132. package/docs/ai/manual.md +443 -17
  133. package/docs/events.md +57 -2
  134. package/docs/flavored.md +210 -0
  135. package/docs/reactive/advanced.md +2 -3
  136. package/docs/reactive/attend.md +143 -0
  137. package/docs/reactive/collections.md +55 -0
  138. package/docs/reactive/core.md +120 -4
  139. package/docs/reactive/debugging.md +8 -10
  140. package/docs/reactive/describe.md +85 -0
  141. package/docs/reactive/error-handling.md +121 -0
  142. package/docs/reactive/project.md +1 -1
  143. package/docs/reactive/scan.md +215 -0
  144. package/docs/reactive.md +2 -0
  145. package/docs/std-decorators.md +171 -52
  146. package/docs/zone.md +9 -6
  147. package/package.json +44 -27
  148. package/src/async/browser.ts +277 -41
  149. package/src/async/index.ts +23 -4
  150. package/src/async/node.ts +78 -32
  151. package/src/entry-browser.ts +5 -0
  152. package/src/entry-node.ts +5 -0
  153. package/src/eventful.ts +82 -49
  154. package/src/flavored.ts +106 -0
  155. package/src/index.d.ts +12 -9
  156. package/src/index.ts +2 -1
  157. package/src/introspection.ts +1 -1
  158. package/src/reactive/array.ts +160 -80
  159. package/src/reactive/buffer.ts +187 -27
  160. package/src/reactive/change.ts +16 -23
  161. package/src/reactive/deep-touch.ts +19 -14
  162. package/src/reactive/deep-watch-state.ts +3 -3
  163. package/src/reactive/deep-watch.ts +7 -4
  164. package/src/reactive/describe.ts +39 -0
  165. package/src/reactive/effect-context.ts +71 -6
  166. package/src/reactive/effects.ts +445 -393
  167. package/src/reactive/index.ts +12 -14
  168. package/src/reactive/map.ts +8 -24
  169. package/src/reactive/memoize.ts +26 -3
  170. package/src/reactive/project.md +107 -0
  171. package/src/reactive/project.ts +48 -71
  172. package/src/reactive/proxy.ts +21 -3
  173. package/src/reactive/record.ts +30 -45
  174. package/src/reactive/register.ts +138 -19
  175. package/src/reactive/registry.ts +22 -9
  176. package/src/reactive/set.ts +7 -20
  177. package/src/reactive/tracking.ts +9 -12
  178. package/src/reactive/types.ts +104 -24
  179. package/src/reactive/{interface.ts → watch.ts} +29 -59
  180. package/src/std-decorators.ts +56 -17
  181. package/src/utils.ts +3 -4
  182. package/src/zone.ts +43 -28
  183. package/dist/browser.js +0 -161
  184. package/dist/browser.js.map +0 -1
  185. package/dist/chunks/index-BFYK02LG.js +0 -6378
  186. package/dist/chunks/index-BFYK02LG.js.map +0 -1
  187. package/dist/chunks/index-CNR6QRUl.esm.js +0 -6291
  188. package/dist/chunks/index-CNR6QRUl.esm.js.map +0 -1
  189. package/dist/node.js +0 -136
  190. package/dist/node.js.map +0 -1
  191. package/src/reactive/debug.ts +0 -517
@@ -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
- type DependencyAccess,
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 {
@@ -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 nativeIterator = this[Symbol.iterator]()
101
- return {
102
- next() {
103
- const result = nativeIterator.next()
104
- if (result.done) {
105
- return result
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
@@ -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
- options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'calculation')
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
- options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'comparison')
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
@@ -1,16 +1,20 @@
1
- import { FoolProof } from '../utils'
2
- import { setEffectName } from './debug'
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 { type ProjectionContext, projectionInfo, type ScopedCallback } from './types'
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<ScopedCallback, ProjectionContext>()
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
- /* TODO
24
- It seems to work and I feel like it's correct but I couldn't validate theoretically that `ascend`
25
- is the correct way to deal with nested effects.
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(function projectArrayLengthEffect({ ascend }) {
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(function projectArrayIndexEffect() {
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
- const produced = apply(accessBase, target)
128
- target[index] = produced
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
- const observedSource = reactive(source) as Register<SourceValue, Key>
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 observedSource.mapKeys()) keys.add(key)
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: observedSource,
192
- get: () => observedSource.get(key) as SourceValue,
187
+ source: source,
188
+ get: () => source.get(key) as SourceValue,
193
189
  set: (value: SourceValue) => {
194
- observedSource.set(key, value)
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: observedSource,
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
- const observedSource = reactive(source) as Source
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 observedSource) keys.add(key)
256
- const observed = Reflect.ownKeys(observedSource)
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: observedSource,
270
- get: () => FoolProof.get(observedSource, sourceKey, observedSource),
259
+ source: source,
260
+ get: () => FoolProof.get(source, sourceKey, source),
271
261
  set: (value: Source[typeof sourceKey]) =>
272
- FoolProof.set(observedSource, sourceKey, value, observedSource),
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: observedSource,
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
- const observedSource = reactive(source) as Map<Key, Value>
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 observedSource.keys()) keys.add(key)
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: observedSource,
341
- get: () => observedSource.get(key) as Value,
324
+ source: source,
325
+ get: () => source.get(key) as Value,
342
326
  set: (value: Value) => {
343
- observedSource.set(key, value)
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: observedSource,
345
+ source: source,
369
346
  target,
370
347
  apply,
371
348
  depth,
@@ -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' && !Object.hasOwn(obj, prop)) {
38
+ if (obj && typeof obj === 'object' && prop !== Symbol.toStringTag) {
39
39
  const metaProto = metaProtos.get(obj.constructor)
40
- if (metaProto && prop in metaProto) {
40
+ if (metaProto && Object.hasOwn(metaProto, prop)) {
41
41
  const desc = Object.getOwnPropertyDescriptor(metaProto, prop)!
42
- return desc.get ? desc.get.call(obj) : (...args) => desc.value.apply(obj, args)
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
@@ -1,9 +1,13 @@
1
1
  import { FoolProof } from '../utils'
2
+ import { attend } from './buffer'
2
3
  import { touched1 } from './change'
3
- import { effect } from './effects'
4
- import { cleanedBy, cleanup } from './interface'
4
+ import { cleanedBy } from './effect-context'
5
5
  import { reactive } from './proxy'
6
- import { type ScopedCallback } from './types'
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
- ) => ScopedCallback | undefined
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
- function disposeKey(key: PropertyKey) {
131
- const stopEffect = keyEffects.get(key)
132
- if (stopEffect) {
133
- keyEffects.delete(key)
134
- stopEffect()
135
- }
136
- }
137
-
138
- const cleanupKeys = effect(function organizedKeysEffect({ ascend }) {
139
- //const keys = Reflect.ownKeys(observedSource) as PropertyKey[]
140
- const keys = new Set<PropertyKey>()
141
- for (const key in observedSource) keys.add(key)
142
-
143
- for (const key of keys) {
144
- if (keyEffects.has(key)) continue
145
- ascend(() => {
146
- const stop = effect(function organizedKeyEffect() {
147
- const sourceKey = key as keyof Source
148
- const accessBase = {
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
- for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
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
  /**