mutts 1.0.5 → 1.0.7

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 (114) hide show
  1. package/README.md +2 -1
  2. package/dist/browser.d.ts +2 -0
  3. package/dist/browser.esm.js +70 -0
  4. package/dist/browser.esm.js.map +1 -0
  5. package/dist/browser.js +161 -0
  6. package/dist/browser.js.map +1 -0
  7. package/dist/chunks/{index-Cvxdw6Ax.js → index-BFYK02LG.js} +5377 -4059
  8. package/dist/chunks/index-BFYK02LG.js.map +1 -0
  9. package/dist/chunks/{index-qiWwozOc.esm.js → index-CNR6QRUl.esm.js} +5247 -3963
  10. package/dist/chunks/index-CNR6QRUl.esm.js.map +1 -0
  11. package/dist/mutts.umd.js +1 -1
  12. package/dist/mutts.umd.js.map +1 -1
  13. package/dist/mutts.umd.min.js +1 -1
  14. package/dist/mutts.umd.min.js.map +1 -1
  15. package/dist/node.d.ts +2 -0
  16. package/dist/node.esm.js +45 -0
  17. package/dist/node.esm.js.map +1 -0
  18. package/dist/node.js +136 -0
  19. package/dist/node.js.map +1 -0
  20. package/docs/ai/api-reference.md +0 -2
  21. package/docs/ai/manual.md +14 -95
  22. package/docs/reactive/advanced.md +7 -111
  23. package/docs/reactive/collections.md +0 -125
  24. package/docs/reactive/core.md +27 -24
  25. package/docs/reactive/debugging.md +168 -0
  26. package/docs/reactive/project.md +1 -1
  27. package/docs/reactive/scan.md +78 -0
  28. package/docs/reactive.md +8 -6
  29. package/docs/std-decorators.md +1 -0
  30. package/docs/zone.md +88 -0
  31. package/package.json +47 -65
  32. package/src/async/browser.ts +87 -0
  33. package/src/async/index.ts +8 -0
  34. package/src/async/node.ts +46 -0
  35. package/src/decorator.ts +15 -9
  36. package/src/destroyable.ts +4 -4
  37. package/src/index.ts +54 -0
  38. package/src/indexable.ts +42 -0
  39. package/src/mixins.ts +2 -2
  40. package/src/reactive/array.ts +149 -141
  41. package/src/reactive/buffer.ts +168 -0
  42. package/src/reactive/change.ts +3 -3
  43. package/src/reactive/debug.ts +1 -1
  44. package/src/reactive/deep-touch.ts +1 -1
  45. package/src/reactive/deep-watch.ts +1 -1
  46. package/src/reactive/effect-context.ts +15 -91
  47. package/src/reactive/effects.ts +138 -170
  48. package/src/reactive/index.ts +10 -13
  49. package/src/reactive/interface.ts +20 -33
  50. package/src/reactive/map.ts +48 -61
  51. package/src/reactive/memoize.ts +87 -31
  52. package/src/reactive/project.ts +43 -22
  53. package/src/reactive/proxy.ts +18 -43
  54. package/src/reactive/record.ts +3 -3
  55. package/src/reactive/register.ts +5 -7
  56. package/src/reactive/registry.ts +59 -0
  57. package/src/reactive/set.ts +42 -56
  58. package/src/reactive/tracking.ts +5 -62
  59. package/src/reactive/types.ts +79 -19
  60. package/src/std-decorators.ts +9 -9
  61. package/src/utils.ts +203 -19
  62. package/src/zone.ts +127 -0
  63. package/dist/chunks/_tslib-BgjropY9.js +0 -81
  64. package/dist/chunks/_tslib-BgjropY9.js.map +0 -1
  65. package/dist/chunks/_tslib-Mzh1rNsX.esm.js +0 -75
  66. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
  67. package/dist/chunks/decorator-DLvrD0UF.js +0 -265
  68. package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
  69. package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
  70. package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
  71. package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
  72. package/dist/chunks/index-qiWwozOc.esm.js.map +0 -1
  73. package/dist/decorator.d.ts +0 -107
  74. package/dist/decorator.esm.js +0 -2
  75. package/dist/decorator.esm.js.map +0 -1
  76. package/dist/decorator.js +0 -11
  77. package/dist/decorator.js.map +0 -1
  78. package/dist/destroyable.d.ts +0 -90
  79. package/dist/destroyable.esm.js +0 -109
  80. package/dist/destroyable.esm.js.map +0 -1
  81. package/dist/destroyable.js +0 -116
  82. package/dist/destroyable.js.map +0 -1
  83. package/dist/eventful.d.ts +0 -20
  84. package/dist/eventful.esm.js +0 -66
  85. package/dist/eventful.esm.js.map +0 -1
  86. package/dist/eventful.js +0 -68
  87. package/dist/eventful.js.map +0 -1
  88. package/dist/index.d.ts +0 -19
  89. package/dist/index.esm.js +0 -8
  90. package/dist/index.esm.js.map +0 -1
  91. package/dist/index.js +0 -95
  92. package/dist/index.js.map +0 -1
  93. package/dist/indexable.d.ts +0 -243
  94. package/dist/indexable.esm.js +0 -285
  95. package/dist/indexable.esm.js.map +0 -1
  96. package/dist/indexable.js +0 -291
  97. package/dist/indexable.js.map +0 -1
  98. package/dist/promiseChain.d.ts +0 -21
  99. package/dist/promiseChain.esm.js +0 -78
  100. package/dist/promiseChain.esm.js.map +0 -1
  101. package/dist/promiseChain.js +0 -80
  102. package/dist/promiseChain.js.map +0 -1
  103. package/dist/reactive.d.ts +0 -885
  104. package/dist/reactive.esm.js +0 -5
  105. package/dist/reactive.esm.js.map +0 -1
  106. package/dist/reactive.js +0 -59
  107. package/dist/reactive.js.map +0 -1
  108. package/dist/std-decorators.d.ts +0 -52
  109. package/dist/std-decorators.esm.js +0 -196
  110. package/dist/std-decorators.esm.js.map +0 -1
  111. package/dist/std-decorators.js +0 -204
  112. package/dist/std-decorators.js.map +0 -1
  113. package/src/reactive/mapped.ts +0 -129
  114. package/src/reactive/zone.ts +0 -208
@@ -1,8 +1,9 @@
1
+ import { FunctionWrapper } from '../zone'
1
2
  import { ArrayReadForward, forwardArray, getAt, Indexable, setAt } from '../indexable'
2
3
  import { effect } from './effects'
3
4
  import { unreactive } from './interface'
4
5
  import { reactive } from './proxy'
5
- import { type DependencyFunction, prototypeForwarding, type ScopedCallback } from './types'
6
+ import { type ScopedCallback } from './types'
6
7
 
7
8
  // TODO: use register in a real-world crud situation, have "events" for add, delete, update
8
9
 
@@ -36,7 +37,7 @@ function getRegisterBase<T>() {
36
37
  interface RegisterInstance<T> extends ArrayReadForward<T> {
37
38
  [index: number]: T
38
39
  }
39
-
40
+ // TODO: What to do with prototype forwarding ?
40
41
  @unreactive
41
42
  class RegisterClass<T, K extends PropertyKey = PropertyKey>
42
43
  extends getRegisterBase<any>()
@@ -51,12 +52,12 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
51
52
  readonly #usage = new Map<K, number>()
52
53
  readonly #valueInfo = new Map<T, { key: K; stop?: ScopedCallback }>()
53
54
  readonly #keyEffects = new Set<ScopedCallback>()
54
- readonly #ascend: DependencyFunction
55
+ readonly #ascend: FunctionWrapper
55
56
 
56
57
  constructor(keyFn: KeyFunction<T, K>, initial?: Iterable<T>) {
57
58
  super()
58
59
  /* Moved below initialization */
59
- let ascendGet: DependencyFunction | undefined
60
+ let ascendGet: FunctionWrapper | undefined
60
61
  effect(({ ascend }) => {
61
62
  ascendGet = ascend
62
63
  })
@@ -65,9 +66,6 @@ class RegisterClass<T, K extends PropertyKey = PropertyKey>
65
66
  this.#keyFn = keyFn
66
67
  this.#keys = reactive([] as K[])
67
68
  this.#values = reactive(new Map<K, T>())
68
- Object.defineProperties(this, {
69
- [prototypeForwarding]: { value: this.#keys },
70
- })
71
69
  if (initial) this.push(...initial)
72
70
  }
73
71
 
@@ -0,0 +1,59 @@
1
+ import { rootFunction, type ScopedCallback } from './types'
2
+
3
+ // Track which effects are watching which reactive objects for cleanup
4
+ export const effectToReactiveObjects = new WeakMap<ScopedCallback, Set<object>>()
5
+
6
+ // Track effects per reactive object and property
7
+ export const watchers = new WeakMap<object, Map<any, Set<ScopedCallback>>>()
8
+
9
+ // runEffect -> set<stop>
10
+ export const effectChildren = new WeakMap<ScopedCallback, Set<ScopedCallback>>()
11
+
12
+ // Track parent effect relationships for hierarchy traversal (used in deep touch filtering)
13
+ export const effectParent = new WeakMap<ScopedCallback, ScopedCallback | undefined>()
14
+
15
+ // Track reverse mapping to ensure unicity: One Root -> One Function
16
+ const reverseRoots = new WeakMap<any, WeakRef<Function>>()
17
+
18
+ /**
19
+ * Marks a function with its root function for effect tracking
20
+ * Enforces strict unicity: A root function can only identify ONE function.
21
+ * @param fn - The function to mark
22
+ * @param root - The root function
23
+ * @returns The marked function
24
+ */
25
+ export function markWithRoot<T extends Function>(fn: T, root: any): T {
26
+ // Check for collision
27
+ const existingRef = reverseRoots.get(root)
28
+ const existing = existingRef?.deref()
29
+
30
+ if (existing && existing !== fn) {
31
+ const rootName = root.name || 'anonymous'
32
+ const existingName = existing.name || 'anonymous'
33
+ const fnName = fn.name || 'anonymous'
34
+ throw new Error(
35
+ `[reactive] Abusive Shared Root detected: Root '${rootName}' is already identifying function '${existingName}'. ` +
36
+ `Cannot reuse it for '${fnName}'. Shared roots cause lost updates and broken identity logic.`
37
+ )
38
+ }
39
+
40
+ // Always update the map so subsequent checks find this one
41
+ // (Last writer wins for the check)
42
+ reverseRoots.set(root, new WeakRef(fn))
43
+
44
+ // Mark fn with the new root
45
+ return Object.defineProperty(fn, rootFunction, {
46
+ value: getRoot(root),
47
+ writable: false,
48
+ })
49
+ }
50
+
51
+ /**
52
+ * Gets the root function of a function for effect tracking
53
+ * @param fn - The function to get the root of
54
+ * @returns The root function
55
+ */
56
+ export function getRoot<T extends Function | undefined>(fn: T): T {
57
+ while (fn && rootFunction in fn) fn = fn[rootFunction] as T
58
+ return fn
59
+ }
@@ -1,49 +1,38 @@
1
+ import { contentRef } from '../utils'
1
2
  import { touched, touched1 } from './change'
2
3
  import { makeReactiveEntriesIterator, makeReactiveIterator } from './non-reactive'
3
4
  import { reactive } from './proxy'
4
5
  import { dependant } from './tracking'
5
- import { prototypeForwarding } from './types'
6
-
7
- const native = Symbol('native')
8
6
 
9
7
  /**
10
8
  * Reactive wrapper around JavaScript's WeakSet class
11
9
  * Only tracks individual value operations, no size tracking (WeakSet limitation)
12
10
  */
13
- export class ReactiveWeakSet<T extends object> {
14
- readonly [native]!: WeakSet<T>
15
- readonly content!: symbol
16
-
17
- constructor(original: WeakSet<T>) {
18
- Object.defineProperties(this, {
19
- [native]: { value: original },
20
- [prototypeForwarding]: { value: original },
21
- content: { value: Symbol('WeakSetContent') },
22
- [Symbol.toStringTag]: { value: 'ReactiveWeakSet' },
23
- })
11
+ export abstract class ReactiveWeakSet<T extends object> extends WeakSet<T> {
12
+ get [Symbol.toStringTag]() {
13
+ return 'ReactiveWeakSet'
24
14
  }
25
-
26
15
  add(value: T): this {
27
- const had = this[native].has(value)
28
- this[native].add(value)
16
+ const had = this.has(value)
17
+ this.add(value)
29
18
  if (!had) {
30
19
  // touch the specific value and the collection view
31
- touched1(this.content, { type: 'add', prop: value }, value)
20
+ touched1(contentRef(this), { type: 'add', prop: value }, value)
32
21
  // no size/allProps for WeakSet
33
22
  }
34
23
  return this
35
24
  }
36
25
 
37
26
  delete(value: T): boolean {
38
- const had = this[native].has(value)
39
- const res = this[native].delete(value)
40
- if (had) touched1(this.content, { type: 'del', prop: value }, value)
27
+ const had = this.has(value)
28
+ const res = this.delete(value)
29
+ if (had) touched1(contentRef(this), { type: 'del', prop: value }, value)
41
30
  return res
42
31
  }
43
32
 
44
33
  has(value: T): boolean {
45
- dependant(this.content, value)
46
- return this[native].has(value)
34
+ dependant(contentRef(this), value)
35
+ return this.has(value)
47
36
  }
48
37
  }
49
38
 
@@ -51,86 +40,79 @@ export class ReactiveWeakSet<T extends object> {
51
40
  * Reactive wrapper around JavaScript's Set class
52
41
  * Tracks size changes, individual value operations, and collection-wide operations
53
42
  */
54
- export class ReactiveSet<T> {
55
- readonly [native]!: Set<T>
56
- readonly content!: symbol
57
- constructor(original: Set<T>) {
58
- Object.defineProperties(this, {
59
- [native]: { value: original },
60
- [prototypeForwarding]: { value: original },
61
- content: { value: Symbol('SetContent') },
62
- [Symbol.toStringTag]: { value: 'ReactiveSet' },
63
- })
43
+ export abstract class ReactiveSet<T> extends Set<T> {
44
+ get [Symbol.toStringTag]() {
45
+ return 'ReactiveSet'
64
46
  }
65
47
 
66
48
  get size(): number {
67
49
  // size depends on the wrapper instance, like Map counterpart
68
50
  dependant(this, 'size')
69
- return this[native].size
51
+ return this.size
70
52
  }
71
53
 
72
54
  add(value: T): this {
73
- const had = this[native].has(value)
55
+ const had = this.has(value)
74
56
  const reactiveValue = reactive(value)
75
- this[native].add(reactiveValue)
57
+ this.add(reactiveValue)
76
58
  if (!had) {
77
59
  const evolution = { type: 'add', prop: reactiveValue } as const
78
60
  // touch for value-specific and aggregate dependencies
79
- touched1(this.content, evolution, reactiveValue)
61
+ touched1(contentRef(this), evolution, reactiveValue)
80
62
  touched1(this, evolution, 'size')
81
63
  }
82
64
  return this
83
65
  }
84
66
 
85
67
  clear(): void {
86
- const hadEntries = this[native].size > 0
87
- this[native].clear()
68
+ const hadEntries = this.size > 0
69
+ this.clear()
88
70
  if (hadEntries) {
89
71
  const evolution = { type: 'bunch', method: 'clear' } as const
90
72
  touched1(this, evolution, 'size')
91
- touched(this.content, evolution)
73
+ touched(contentRef(this), evolution)
92
74
  }
93
75
  }
94
76
 
95
77
  delete(value: T): boolean {
96
- const had = this[native].has(value)
97
- const res = this[native].delete(value)
78
+ const had = this.has(value)
79
+ const res = this.delete(value)
98
80
  if (had) {
99
81
  const evolution = { type: 'del', prop: value } as const
100
- touched1(this.content, evolution, value)
82
+ touched1(contentRef(this), evolution, value)
101
83
  touched1(this, evolution, 'size')
102
84
  }
103
85
  return res
104
86
  }
105
87
 
106
88
  has(value: T): boolean {
107
- dependant(this.content, value)
108
- return this[native].has(value)
89
+ dependant(contentRef(this), value)
90
+ return this.has(value)
109
91
  }
110
92
 
111
93
  entries(): Generator<[T, T]> {
112
- dependant(this.content)
113
- return makeReactiveEntriesIterator(this[native].entries())
94
+ dependant(contentRef(this))
95
+ return makeReactiveEntriesIterator(this.entries())
114
96
  }
115
97
 
116
98
  forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
117
- dependant(this.content)
118
- this[native].forEach(callbackfn, thisArg)
99
+ dependant(contentRef(this))
100
+ this.forEach(callbackfn, thisArg)
119
101
  }
120
102
 
121
103
  keys(): Generator<T> {
122
- dependant(this.content)
123
- return makeReactiveIterator(this[native].keys())
104
+ dependant(contentRef(this))
105
+ return makeReactiveIterator(this.keys())
124
106
  }
125
107
 
126
108
  values(): Generator<T> {
127
- dependant(this.content)
128
- return makeReactiveIterator(this[native].values())
109
+ dependant(contentRef(this))
110
+ return makeReactiveIterator(this.values())
129
111
  }
130
112
 
131
- [Symbol.iterator](): Iterator<T> {
132
- dependant(this.content)
133
- const nativeIterator = this[native][Symbol.iterator]()
113
+ [Symbol.iterator](): SetIterator<T> {
114
+ dependant(contentRef(this))
115
+ const nativeIterator = this[Symbol.iterator]()
134
116
  return {
135
117
  next() {
136
118
  const result = nativeIterator.next()
@@ -139,6 +121,10 @@ export class ReactiveSet<T> {
139
121
  }
140
122
  return { value: reactive(result.value), done: false }
141
123
  },
124
+ [Symbol.iterator]() {
125
+ return this
126
+ },
127
+ [Symbol.dispose]() {},
142
128
  }
143
129
  }
144
130
  }
@@ -1,62 +1,10 @@
1
1
  import { getActiveEffect } from './effect-context'
2
2
  import { unwrap } from './proxy-state'
3
- import { allProps, rootFunction, type ScopedCallback } from './types'
4
-
5
- // Track which effects are watching which reactive objects for cleanup
6
- export const effectToReactiveObjects = new WeakMap<ScopedCallback, Set<object>>()
7
-
8
- // Track effects per reactive object and property
9
- export const watchers = new WeakMap<object, Map<any, Set<ScopedCallback>>>()
10
-
11
- // runEffect -> set<stop>
12
- export const effectChildren = new WeakMap<ScopedCallback, Set<ScopedCallback>>()
13
-
14
- // Track parent effect relationships for hierarchy traversal (used in deep touch filtering)
15
- export const effectParent = new WeakMap<ScopedCallback, ScopedCallback | undefined>()
16
-
17
- /**
18
- * Marks a function with its root function for effect tracking
19
- * @param fn - The function to mark
20
- * @param root - The root function
21
- * @returns The marked function
22
- */
23
- export function markWithRoot<T extends Function>(fn: T, root: Function): T {
24
- // Mark fn with the new root
25
- return Object.defineProperty(fn, rootFunction, {
26
- value: getRoot(root),
27
- writable: false,
28
- })
29
- }
30
-
31
- /**
32
- * Gets the root function of a function for effect tracking
33
- * @param fn - The function to get the root of
34
- * @returns The root function
35
- */
36
- export function getRoot<T extends Function | undefined>(fn: T): T {
37
- return (fn as any)?.[rootFunction] || fn
38
- }
39
-
40
- // Flag to disable dependency tracking for the current active effect (not globally)
41
- const trackingDisabledEffects = new WeakSet<ScopedCallback>()
42
- let globalTrackingDisabled = false
43
-
44
- export function getTrackingDisabled(): boolean {
45
- const active = getActiveEffect()
46
- if (!active) return globalTrackingDisabled
47
- return trackingDisabledEffects.has(getRoot(active))
48
- }
49
-
50
- export function setTrackingDisabled(value: boolean): void {
51
- const active = getActiveEffect()
52
- if (!active) {
53
- globalTrackingDisabled = value
54
- return
55
- }
56
- const root = getRoot(active)
57
- if (value) trackingDisabledEffects.add(root)
58
- else trackingDisabledEffects.delete(root)
59
- }
3
+ import {
4
+ effectToReactiveObjects,
5
+ watchers,
6
+ } from './registry'
7
+ import { allProps, type ScopedCallback } from './types'
60
8
 
61
9
  /**
62
10
  * Marks a property as a dependency of the current effect
@@ -70,15 +18,10 @@ export function dependant(obj: any, prop: any = allProps) {
70
18
  // Early return if no active effect, tracking disabled, or invalid prop
71
19
  if (
72
20
  !currentActiveEffect ||
73
- getTrackingDisabled() ||
74
21
  (typeof prop === 'symbol' && prop !== allProps)
75
22
  )
76
23
  return
77
24
 
78
- registerDependency(obj, prop, currentActiveEffect)
79
- }
80
-
81
- function registerDependency(obj: any, prop: any, currentActiveEffect: ScopedCallback) {
82
25
  let objectWatchers = watchers.get(obj)
83
26
  if (!objectWatchers) {
84
27
  objectWatchers = new Map<PropertyKey, Set<ScopedCallback>>()
@@ -1,11 +1,8 @@
1
1
  // biome-ignore-all lint/suspicious/noConfusingVoidType: Type 'void' is not assignable to type 'ScopedCallback | undefined'.
2
2
  // Argument of type '() => void' is not assignable to parameter of type '(dep: DependencyFunction) => ScopedCallback | undefined'.
3
3
 
4
- /**
5
- * Function type for dependency tracking in effects
6
- * Restores the active effect context for dependency tracking
7
- */
8
- export type DependencyFunction = <T>(cb: () => T) => T
4
+ import { FunctionWrapper } from "../zone"
5
+
9
6
  /**
10
7
  * Dependency access passed to user callbacks within effects/watch
11
8
  * Provides functions to track dependencies and information about the effect execution
@@ -25,7 +22,7 @@ export interface DependencyAccess {
25
22
  * })
26
23
  * ```
27
24
  */
28
- tracked: DependencyFunction
25
+ tracked: FunctionWrapper
29
26
  /**
30
27
  * Tracks dependencies in the parent effect context
31
28
  * Use this when child effects should track dependencies in the parent,
@@ -43,7 +40,7 @@ export interface DependencyAccess {
43
40
  * })
44
41
  * ```
45
42
  */
46
- ascend: DependencyFunction
43
+ ascend: FunctionWrapper
47
44
  /**
48
45
  * Indicates whether the effect is running as a reaction (i.e. not the first call)
49
46
  * - `false`: First execution when the effect is created
@@ -131,10 +128,6 @@ export const nonReactiveMark = Symbol('non-reactive')
131
128
  * Symbol to mark class properties as non-reactive
132
129
  */
133
130
  export const unreactiveProperties = Symbol('unreactive-properties')
134
- /**
135
- * Symbol for prototype forwarding in reactive objects
136
- */
137
- export const prototypeForwarding: unique symbol = Symbol('prototype-forwarding')
138
131
 
139
132
  /**
140
133
  * Symbol representing all properties in reactive tracking
@@ -146,6 +139,16 @@ export const allProps = Symbol('all-props')
146
139
  */
147
140
  export const projectionInfo = Symbol('projection-info')
148
141
 
142
+ /**
143
+ * Symbol to check if an effect is stopped
144
+ */
145
+ export const stopped = Symbol('stopped')
146
+
147
+ /**
148
+ * Symbol to access effect cleanup function
149
+ */
150
+ export const cleanup = Symbol('cleanup')
151
+
149
152
  /**
150
153
  * Context for a running projection item effect
151
154
  */
@@ -169,6 +172,7 @@ export enum ReactiveErrorCode {
169
172
  MaxReactionExceeded = 'MAX_REACTION_EXCEEDED',
170
173
  WriteInComputed = 'WRITE_IN_COMPUTED',
171
174
  TrackingError = 'TRACKING_ERROR',
175
+ BrokenEffects = 'BROKEN_EFFECTS',
172
176
  }
173
177
 
174
178
  export type CycleDebugInfo = {
@@ -189,6 +193,11 @@ export type MaxReactionDebugInfo = {
189
193
  effect: string
190
194
  }
191
195
 
196
+ export type BrokenEffectsDebugInfo = {
197
+ code: ReactiveErrorCode.BrokenEffects
198
+ cause: any
199
+ }
200
+
192
201
  export type GenericDebugInfo = {
193
202
  code: ReactiveErrorCode
194
203
  causalChain?: string[]
@@ -200,6 +209,7 @@ export type ReactiveDebugInfo =
200
209
  | CycleDebugInfo
201
210
  | MaxDepthDebugInfo
202
211
  | MaxReactionDebugInfo
212
+ | BrokenEffectsDebugInfo
203
213
  | GenericDebugInfo
204
214
 
205
215
  /**
@@ -213,6 +223,14 @@ export class ReactiveError extends Error {
213
223
  super(message)
214
224
  this.name = 'ReactiveError'
215
225
  }
226
+
227
+ get code(): ReactiveErrorCode | undefined {
228
+ return this.debugInfo?.code
229
+ }
230
+
231
+ get cause(): any {
232
+ return (this.debugInfo as any)?.cause
233
+ }
216
234
  }
217
235
 
218
236
  // biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
@@ -259,7 +277,7 @@ export const options = {
259
277
  * @param effect - The effect that is already running
260
278
  * @param runningChain - The array of effects from the detected one to the currently running one
261
279
  */
262
- skipRunningEffect: (_effect: ScopedCallback, _runningChain: ScopedCallback[]) => {},
280
+ skipRunningEffect: (_effect: ScopedCallback) => {},
263
281
  /**
264
282
  * Debug purpose: maximum effect chain (like call stack max depth)
265
283
  * Used to prevent infinite loops
@@ -279,14 +297,55 @@ export const options = {
279
297
  */
280
298
  maxEffectReaction: 'throw' as 'throw' | 'debug' | 'warn',
281
299
  /**
282
- * How to handle cycles detected in effect batches
283
- * - 'throw': Throw an error with cycle information (default, recommended for development)
284
- * - 'warn': Log a warning and break the cycle by executing one effect
285
- * - 'break': Silently break the cycle by executing one effect (recommended for production)
286
- * - 'strict': Prevent cycle creation by checking graph before execution (throws error)
287
- * @default 'throw'
300
+ * Callback called when a memoization discrepancy is detected (debug only)
301
+ * When defined, memoized functions will run a second time (untracked) to verify consistency.
302
+ * If the untracked run returns a different value than the cached one, this callback is triggered.
303
+ *
304
+ * This is the primary tool for detecting missing reactive dependencies in computed values.
305
+ *
306
+ * @param cached - The value currently in the memoization cache
307
+ * @param fresh - The value obtained by re-running the function untracked
308
+ * @param fn - The memoized function itself
309
+ * @param args - Arguments passed to the function
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args) => {
314
+ * throw new Error(`Memoization discrepancy in ${fn.name}!`);
315
+ * };
316
+ * ```
317
+ */
318
+ onMemoizationDiscrepancy: undefined as
319
+ | ((
320
+ cached: any,
321
+ fresh: any,
322
+ fn: Function,
323
+ args: any[],
324
+ cause: 'calculation' | 'comparison'
325
+ ) => void)
326
+ | undefined,
327
+ /**
328
+ * How to handle cycles detected in effect batches.
329
+ *
330
+ * - `'none'` (Default): High-performance mode. Disables dependency graph maintenance and
331
+ * Topological Sorting in favor of a simple FIFO queue. Use this for trustworthy, acyclic UI code.
332
+ * Cycle detection is heuristic (uses execution counts).
333
+ *
334
+ * - `'throw'`: Traditional Topological Sorting. Guarantees dependency order and catches
335
+ * circular dependencies mathematically before execution.
336
+ *
337
+ * - `'warn'`: Topological sorting, but logs a warning instead of throwing on cycles.
338
+ * - `'break'`: Topological sorting, but silently breaks cycles.
339
+ * - `'strict'`: Prevents cycle creation by checking the graph *during* dependency discovery.
340
+ *
341
+ * @default 'none'
342
+ */
343
+ cycleHandling: 'none' as 'none' | 'throw' | 'warn' | 'break' | 'strict',
344
+ /**
345
+ * Internal flag used by memoization discrepancy detector to avoid counting calls in tests
346
+ * @warning Do not modify this flag manually, this flag is given by the engine
288
347
  */
289
- cycleHandling: 'throw' as 'throw' | 'warn' | 'break' | 'strict',
348
+ isVerificationRun: false,
290
349
  /**
291
350
  * Maximum depth for deep watching traversal
292
351
  * Used to prevent infinite recursion in circular references
@@ -351,6 +410,7 @@ export const options = {
351
410
  * Configuration for zone hooks - control which async APIs are hooked
352
411
  * Each option controls whether the corresponding async API is wrapped to preserve effect context
353
412
  * Only applies when asyncMode is enabled (truthy)
413
+ * @deprecated Should take all when we made sure PIXI.create, Game.create, ... are -> .root()
354
414
  */
355
415
  zones: {
356
416
  /**
@@ -7,7 +7,7 @@ const syncCalculating: { object: object; prop: PropertyKey }[] = []
7
7
  * Prevents circular dependencies and provides automatic cache invalidation
8
8
  */
9
9
  export const cached = decorator({
10
- getter(original, propertyKey) {
10
+ getter(original, _target, propertyKey) {
11
11
  return function (this: any) {
12
12
  const alreadyCalculating = syncCalculating.findIndex(
13
13
  (c) => c.object === this && c.prop === propertyKey
@@ -83,19 +83,19 @@ export function describe(descriptor: {
83
83
  */
84
84
  export const deprecated = Object.assign(
85
85
  decorator({
86
- method(original, propertyKey) {
86
+ method(original, _target, propertyKey) {
87
87
  return function (this: any, ...args: any[]) {
88
88
  deprecated.warn(this, propertyKey)
89
89
  return original.apply(this, args)
90
90
  }
91
91
  },
92
- getter(original, propertyKey) {
92
+ getter(original, _target, propertyKey) {
93
93
  return function (this: any) {
94
94
  deprecated.warn(this, propertyKey)
95
95
  return original.call(this)
96
96
  }
97
97
  },
98
- setter(original, propertyKey) {
98
+ setter(original, _target, propertyKey) {
99
99
  return function (this: any, value: any) {
100
100
  deprecated.warn(this, propertyKey)
101
101
  return original.call(this, value)
@@ -111,19 +111,19 @@ export const deprecated = Object.assign(
111
111
  },
112
112
  default(message: string) {
113
113
  return decorator({
114
- method(original, propertyKey) {
114
+ method(original, _target, propertyKey) {
115
115
  return function (this: any, ...args: any[]) {
116
116
  deprecated.warn(this, propertyKey, message)
117
117
  return original.apply(this, args)
118
118
  }
119
119
  },
120
- getter(original, propertyKey) {
120
+ getter(original, _target, propertyKey) {
121
121
  return function (this: any) {
122
122
  deprecated.warn(this, propertyKey, message)
123
123
  return original.call(this)
124
124
  }
125
125
  },
126
- setter(original, propertyKey) {
126
+ setter(original, _target, propertyKey) {
127
127
  return function (this: any, value: any) {
128
128
  deprecated.warn(this, propertyKey, message)
129
129
  return original.call(this, value)
@@ -157,7 +157,7 @@ export const deprecated = Object.assign(
157
157
  */
158
158
  export function debounce(delay: number) {
159
159
  return decorator({
160
- method(original, _propertyKey) {
160
+ method(original, _target, _propertyKey) {
161
161
  let timeoutId: ReturnType<typeof setTimeout> | null = null
162
162
 
163
163
  return function (this: any, ...args: any[]) {
@@ -183,7 +183,7 @@ export function debounce(delay: number) {
183
183
  */
184
184
  export function throttle(delay: number) {
185
185
  return decorator({
186
- method(original, _propertyKey) {
186
+ method(original, _target, _propertyKey) {
187
187
  let lastCallTime = 0
188
188
  let timeoutId: ReturnType<typeof setTimeout> | null = null
189
189