mutts 1.0.5 → 1.0.6

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 (66) hide show
  1. package/README.md +1 -0
  2. package/dist/chunks/{_tslib-Mzh1rNsX.esm.js → _tslib-MCKDzsSq.esm.js} +2 -2
  3. package/dist/chunks/_tslib-MCKDzsSq.esm.js.map +1 -0
  4. package/dist/chunks/decorator-BGILvPtN.esm.js +627 -0
  5. package/dist/chunks/decorator-BGILvPtN.esm.js.map +1 -0
  6. package/dist/chunks/decorator-BQ2eBTCj.js +651 -0
  7. package/dist/chunks/decorator-BQ2eBTCj.js.map +1 -0
  8. package/dist/chunks/{index-Cvxdw6Ax.js → index-CDCOjzTy.js} +396 -500
  9. package/dist/chunks/index-CDCOjzTy.js.map +1 -0
  10. package/dist/chunks/{index-qiWwozOc.esm.js → index-DiP0RXoZ.esm.js} +301 -403
  11. package/dist/chunks/index-DiP0RXoZ.esm.js.map +1 -0
  12. package/dist/decorator.d.ts +3 -3
  13. package/dist/decorator.esm.js +1 -1
  14. package/dist/decorator.js +1 -1
  15. package/dist/destroyable.esm.js +4 -4
  16. package/dist/destroyable.esm.js.map +1 -1
  17. package/dist/destroyable.js +4 -4
  18. package/dist/destroyable.js.map +1 -1
  19. package/dist/devtools/panel.js.map +1 -1
  20. package/dist/eventful.esm.js +1 -1
  21. package/dist/index.esm.js +48 -3
  22. package/dist/index.esm.js.map +1 -1
  23. package/dist/index.js +48 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/mutts.umd.js +1 -1
  26. package/dist/mutts.umd.js.map +1 -1
  27. package/dist/mutts.umd.min.js +1 -1
  28. package/dist/mutts.umd.min.js.map +1 -1
  29. package/dist/reactive.d.ts +25 -0
  30. package/dist/reactive.esm.js +3 -3
  31. package/dist/reactive.js +4 -4
  32. package/dist/std-decorators.d.ts +1 -1
  33. package/dist/std-decorators.esm.js +10 -10
  34. package/dist/std-decorators.esm.js.map +1 -1
  35. package/dist/std-decorators.js +10 -10
  36. package/dist/std-decorators.js.map +1 -1
  37. package/docs/ai/manual.md +14 -95
  38. package/docs/reactive/advanced.md +6 -107
  39. package/docs/reactive/debugging.md +158 -0
  40. package/docs/reactive.md +6 -5
  41. package/package.json +16 -66
  42. package/src/decorator.ts +11 -9
  43. package/src/destroyable.ts +3 -3
  44. package/src/index.ts +46 -0
  45. package/src/reactive/change.ts +1 -1
  46. package/src/reactive/debug.ts +1 -1
  47. package/src/reactive/deep-touch.ts +1 -1
  48. package/src/reactive/deep-watch.ts +1 -1
  49. package/src/reactive/effect-context.ts +2 -2
  50. package/src/reactive/effects.ts +44 -16
  51. package/src/reactive/index.ts +1 -1
  52. package/src/reactive/interface.ts +9 -8
  53. package/src/reactive/memoize.ts +77 -31
  54. package/src/reactive/proxy.ts +4 -4
  55. package/src/reactive/registry.ts +67 -0
  56. package/src/reactive/tracking.ts +12 -41
  57. package/src/reactive/types.ts +37 -0
  58. package/src/std-decorators.ts +9 -9
  59. package/src/utils.ts +141 -0
  60. package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
  61. package/dist/chunks/decorator-DLvrD0UF.js +0 -265
  62. package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
  63. package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
  64. package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
  65. package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
  66. package/dist/chunks/index-qiWwozOc.esm.js.map +0 -1
@@ -4,7 +4,8 @@ import { withEffect } from './effect-context'
4
4
  import { effect, getActiveEffect, untracked } from './effects'
5
5
  import { isNonReactive, nonReactiveClass, nonReactiveObjects } from './non-reactive-state'
6
6
  import { unwrap } from './proxy-state'
7
- import { dependant, markWithRoot } from './tracking'
7
+ import { dependant } from './tracking'
8
+ import { markWithRoot } from './registry'
8
9
  import {
9
10
  type DependencyAccess,
10
11
  nonReactiveMark,
@@ -89,11 +90,11 @@ function watchObject(
89
90
  const myParentEffect = getActiveEffect()
90
91
  if (deep) return deepWatch(value, changed, { immediate })!
91
92
  return effect(
92
- markWithRoot(function watchObjectEffect() {
93
+ function watchObjectEffect() {
93
94
  dependant(value)
94
95
  if (immediate) withEffect(myParentEffect, () => changed(value))
95
96
  immediate = true
96
- }, changed)
97
+ }
97
98
  )
98
99
  }
99
100
 
@@ -111,7 +112,7 @@ function watchCallBack<T>(
111
112
  if (oldValue !== newValue)
112
113
  withEffect(
113
114
  myParentEffect,
114
- markWithRoot(() => {
115
+ () => {
115
116
  if (oldValue === unsetYet) {
116
117
  if (immediate) changed(newValue)
117
118
  } else changed(newValue, oldValue)
@@ -120,10 +121,10 @@ function watchCallBack<T>(
120
121
  if (deepCleanup) deepCleanup()
121
122
  deepCleanup = deepWatch(
122
123
  newValue as object,
123
- markWithRoot((value) => changed(value as T, value as T), changed)
124
+ (value) => changed(value as T, value as T)
124
125
  )
125
126
  }
126
- }, changed)
127
+ }
127
128
  )
128
129
  }, value)
129
130
  )
@@ -214,9 +215,9 @@ export function derived<T>(compute: (dep: DependencyAccess) => T): {
214
215
  rv,
215
216
  untracked(() =>
216
217
  effect(
217
- markWithRoot(function derivedEffect(access) {
218
+ function derivedEffect(access) {
218
219
  rv.value = compute(access)
219
- }, compute)
220
+ }
220
221
  )
221
222
  )
222
223
  )
@@ -1,8 +1,10 @@
1
1
  import { decorator } from '../decorator'
2
- import { renamed } from '../utils'
2
+ import { deepCompare, renamed } from '../utils'
3
3
  import { touched1 } from './change'
4
- import { effect, root } from './effects'
5
- import { dependant, getRoot, markWithRoot } from './tracking'
4
+ import { effect, root, untracked } from './effects'
5
+ import { dependant } from './tracking'
6
+ import { getRoot, markWithRoot } from './registry'
7
+ import { options, rootFunction } from './types'
6
8
 
7
9
  export type Memoizable = object | any[] | symbol | ((...args: any[]) => any)
8
10
 
@@ -12,7 +14,8 @@ type MemoCacheTree<Result> = {
12
14
  branches?: WeakMap<Memoizable, MemoCacheTree<Result>>
13
15
  }
14
16
 
15
- const memoizedRegistry = new WeakMap<Function, Function>()
17
+ const memoizedRegistry = new WeakMap<any, Function>()
18
+ const wrapperRegistry = new WeakMap<Function, Function>()
16
19
 
17
20
  function getBranch<Result>(tree: MemoCacheTree<Result>, key: Memoizable): MemoCacheTree<Result> {
18
21
  tree.branches ??= new WeakMap()
@@ -38,18 +41,33 @@ function memoizeFunction<Result, Args extends Memoizable[]>(
38
41
  throw new Error('memoize expects non-null object arguments')
39
42
 
40
43
  let node: MemoCacheTree<Result> = cacheRoot
44
+ // Note: decorators add `this` as first argument
41
45
  for (const arg of localArgs) {
42
46
  node = getBranch(node, arg)
43
47
  }
44
48
 
45
49
  dependant(node, 'memoize')
46
- if ('result' in node) return node.result!
50
+ if ('result' in node) {
51
+ if (options.onMemoizationDiscrepancy) {
52
+ const wasVerification = options.isVerificationRun
53
+ options.isVerificationRun = true
54
+ try {
55
+ const fresh = untracked(() => fn(...localArgs))
56
+ if (!deepCompare(node.result, fresh)) {
57
+ options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'calculation')
58
+ }
59
+ } finally {
60
+ options.isVerificationRun = wasVerification
61
+ }
62
+ }
63
+ return node.result!
64
+ }
47
65
 
48
66
  // Create memoize internal effect to track dependencies and invalidate cache
49
67
  // Use untracked to prevent the effect creation from being affected by parent effects
50
68
  node.cleanup = root(() =>
51
69
  effect(
52
- markWithRoot(() => {
70
+ () => {
53
71
  // Execute the function and track its dependencies
54
72
  // The function execution will automatically track dependencies on reactive objects
55
73
  node.result = fn(...localArgs)
@@ -57,11 +75,31 @@ function memoizeFunction<Result, Args extends Memoizable[]>(
57
75
  // When dependencies change, clear the cache and notify consumers
58
76
  delete node.result
59
77
  touched1(node, { type: 'invalidate', prop: localArgs }, 'memoize')
78
+ // Lazy memoization: stop the effect so it doesn't re-run immediately.
79
+ // It will be re-created on next access.
80
+ if (node.cleanup) {
81
+ node.cleanup()
82
+ node.cleanup = undefined
83
+ }
60
84
  }
61
- }, fnRoot),
85
+ },
62
86
  { opaque: true }
63
87
  )
64
88
  )
89
+
90
+ if (options.onMemoizationDiscrepancy) {
91
+ const wasVerification = options.isVerificationRun
92
+ options.isVerificationRun = true
93
+ try {
94
+ const fresh = untracked(() => fn(...localArgs))
95
+ if (!deepCompare(node.result, fresh)) {
96
+ options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'comparison')
97
+ }
98
+ } finally {
99
+ options.isVerificationRun = wasVerification
100
+ }
101
+ }
102
+
65
103
  return node.result!
66
104
  }, fn)
67
105
 
@@ -71,35 +109,43 @@ function memoizeFunction<Result, Args extends Memoizable[]>(
71
109
  }
72
110
 
73
111
  export const memoize = decorator({
74
- getter(original, propertyKey) {
75
- const memoized = memoizeFunction(
76
- markWithRoot(
77
- renamed(
78
- (that: object) => {
79
- return original.call(that)
80
- },
81
- `${String(this.constructor.name)}.${String(propertyKey)}`
82
- ),
83
- original
84
- )
85
- )
112
+ getter(original, target, propertyKey) {
86
113
  return function (this: any) {
114
+ let wrapper = wrapperRegistry.get(original)
115
+ if (!wrapper) {
116
+ wrapper = markWithRoot(
117
+ renamed((that: object) => {
118
+ return original.call(that)
119
+ }, `${String(target?.constructor?.name ?? target?.name ?? 'Object')}.${String(propertyKey)}`),
120
+ {
121
+ method: original,
122
+ propertyKey,
123
+ ...((original as any)[rootFunction] ? { [rootFunction]: (original as any)[rootFunction] } : {}),
124
+ }
125
+ )
126
+ wrapperRegistry.set(original, wrapper)
127
+ }
128
+ const memoized = memoizeFunction(wrapper as any)
87
129
  return memoized(this)
88
130
  }
89
131
  },
90
- method(original, name) {
91
- const memoized = memoizeFunction(
92
- markWithRoot(
93
- renamed(
94
- (that: object, ...args: object[]) => {
95
- return original.call(that, ...args)
96
- },
97
- `${String(this.constructor.name)}.${String(name)}`
98
- ),
99
- original
100
- )
101
- ) as (...args: object[]) => unknown
132
+ method(original, target, name) {
102
133
  return function (this: any, ...args: object[]) {
134
+ let wrapper = wrapperRegistry.get(original)
135
+ if (!wrapper) {
136
+ wrapper = markWithRoot(
137
+ renamed((that: object, ...args: object[]) => {
138
+ return original.call(that, ...args)
139
+ }, `${String(target?.constructor?.name ?? target?.name ?? 'Object')}.${String(name)}`),
140
+ {
141
+ method: original,
142
+ propertyKey: name,
143
+ ...((original as any)[rootFunction] ? { [rootFunction]: (original as any)[rootFunction] } : {}),
144
+ }
145
+ )
146
+ wrapperRegistry.set(original, wrapper)
147
+ }
148
+ const memoized = memoizeFunction(wrapper as any) as (...args: object[]) => unknown
103
149
  return memoized(this, ...args)
104
150
  }
105
151
  },
@@ -127,12 +127,12 @@ const reactiveHandlers = {
127
127
  const receiverDesc = Object.getOwnPropertyDescriptor(unwrappedReceiver, prop)
128
128
  const targetDesc = Object.getOwnPropertyDescriptor(unwrappedObj, prop)
129
129
  const desc = receiverDesc || targetDesc
130
- // If it's a getter-only accessor (has getter but no setter), read without tracking
131
- // to avoid breaking memoization invalidation when the getter calls memoized functions
130
+ // We *need* to use `receiver` and not `unwrappedObj` here, otherwise we break
131
+ // the dependency tracking for memoized getters
132
132
  if (desc?.get && !desc?.set) {
133
- oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, unwrappedReceiver))
133
+ oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, receiver))
134
134
  } else {
135
- oldVal = Reflect.get(unwrappedObj, prop, unwrappedReceiver)
135
+ oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, receiver))
136
136
  }
137
137
  }
138
138
  if (objectsWithDeepWatchers.has(obj)) {
@@ -0,0 +1,67 @@
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
+ }
60
+
61
+ // Flag to disable dependency tracking for the current active effect (not globally)
62
+ export const trackingDisabledEffects = new WeakSet<ScopedCallback>()
63
+ export let globalTrackingDisabled = false
64
+
65
+ export function setGlobalTrackingDisabled(value: boolean): void {
66
+ globalTrackingDisabled = value
67
+ }
@@ -1,45 +1,14 @@
1
1
  import { getActiveEffect } from './effect-context'
2
+ import {
3
+ effectToReactiveObjects,
4
+ getRoot,
5
+ globalTrackingDisabled,
6
+ setGlobalTrackingDisabled,
7
+ trackingDisabledEffects,
8
+ watchers,
9
+ } from './registry'
2
10
  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
11
+ import { allProps, type ScopedCallback } from './types'
43
12
 
44
13
  export function getTrackingDisabled(): boolean {
45
14
  const active = getActiveEffect()
@@ -50,7 +19,7 @@ export function getTrackingDisabled(): boolean {
50
19
  export function setTrackingDisabled(value: boolean): void {
51
20
  const active = getActiveEffect()
52
21
  if (!active) {
53
- globalTrackingDisabled = value
22
+ setGlobalTrackingDisabled(value)
54
23
  return
55
24
  }
56
25
  const root = getRoot(active)
@@ -58,6 +27,8 @@ export function setTrackingDisabled(value: boolean): void {
58
27
  else trackingDisabledEffects.delete(root)
59
28
  }
60
29
 
30
+
31
+
61
32
  /**
62
33
  * Marks a property as a dependency of the current effect
63
34
  * @param obj - The object containing the property
@@ -146,6 +146,16 @@ export const allProps = Symbol('all-props')
146
146
  */
147
147
  export const projectionInfo = Symbol('projection-info')
148
148
 
149
+ /**
150
+ * Symbol to check if an effect is stopped
151
+ */
152
+ export const stopped = Symbol('stopped')
153
+
154
+ /**
155
+ * Symbol to access effect cleanup function
156
+ */
157
+ export const cleanup = Symbol('cleanup')
158
+
149
159
  /**
150
160
  * Context for a running projection item effect
151
161
  */
@@ -278,6 +288,28 @@ export const options = {
278
288
  * @default 'throw'
279
289
  */
280
290
  maxEffectReaction: 'throw' as 'throw' | 'debug' | 'warn',
291
+ /**
292
+ * Callback called when a memoization discrepancy is detected (debug only)
293
+ * When defined, memoized functions will run a second time (untracked) to verify consistency.
294
+ * If the untracked run returns a different value than the cached one, this callback is triggered.
295
+ *
296
+ * This is the primary tool for detecting missing reactive dependencies in computed values.
297
+ *
298
+ * @param cached - The value currently in the memoization cache
299
+ * @param fresh - The value obtained by re-running the function untracked
300
+ * @param fn - The memoized function itself
301
+ * @param args - Arguments passed to the function
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args) => {
306
+ * throw new Error(`Memoization discrepancy in ${fn.name}!`);
307
+ * };
308
+ * ```
309
+ */
310
+ onMemoizationDiscrepancy: undefined as
311
+ | ((cached: any, fresh: any, fn: Function, args: any[], cause: "calculation" | "comparison") => void)
312
+ | undefined,
281
313
  /**
282
314
  * How to handle cycles detected in effect batches
283
315
  * - 'throw': Throw an error with cycle information (default, recommended for development)
@@ -287,6 +319,11 @@ export const options = {
287
319
  * @default 'throw'
288
320
  */
289
321
  cycleHandling: 'throw' as 'throw' | 'warn' | 'break' | 'strict',
322
+ /**
323
+ * Internal flag used by memoization discrepancy detector to avoid counting calls in tests
324
+ * @warning Do not modify this flag manually, this flag is given by the engine
325
+ */
326
+ isVerificationRun: false,
290
327
  /**
291
328
  * Maximum depth for deep watching traversal
292
329
  * Used to prevent infinite recursion in circular references
@@ -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
 
package/src/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { prototypeForwarding } from './reactive/types'
2
+
1
3
  type ElementTypes<T extends readonly unknown[]> = {
2
4
  [K in keyof T]: T[K] extends readonly (infer U)[] ? U : T[K]
3
5
  }
@@ -115,3 +117,142 @@ export function isOwnAccessor(obj: any, prop: any) {
115
117
  const opd = Object.getOwnPropertyDescriptor(obj, prop)
116
118
  return !!(opd?.get || opd?.set)
117
119
  }
120
+
121
+ /**
122
+ * Deeply compares two values.
123
+ * For objects, compares prototypes with === and then own properties recursively.
124
+ * Uses a cache to handle circular references.
125
+ * @param a - First value
126
+ * @param b - Second value
127
+ * @param cache - Map for circular reference protection (internal use)
128
+ * @returns True if values are deeply equal
129
+ */
130
+ export function deepCompare(a: any, b: any, cache = new Map<object, Set<object>>()): boolean {
131
+ // Unwrap mutts proxies if present
132
+ while (a && typeof a === 'object' && prototypeForwarding in a) {
133
+ a = (a as any)[prototypeForwarding]
134
+ }
135
+ while (b && typeof b === 'object' && prototypeForwarding in b) {
136
+ b = (b as any)[prototypeForwarding]
137
+ }
138
+
139
+ if (a === b) return true
140
+
141
+ if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
142
+ return a === b
143
+ }
144
+
145
+ // Prototype check
146
+ const protoA = Object.getPrototypeOf(a)
147
+ const protoB = Object.getPrototypeOf(b)
148
+ if (protoA !== protoB) {
149
+ console.warn(`[deepCompare] prototype mismatch:`, { nameA: a?.constructor?.name, nameB: b?.constructor?.name })
150
+ return false
151
+ }
152
+ // Circular reference protection
153
+ let compared = cache.get(a)
154
+ if (compared?.has(b)) return true
155
+ if (!compared) {
156
+ compared = new Set()
157
+ cache.set(a, compared)
158
+ }
159
+ compared.add(b)
160
+
161
+ // Handle specific object types
162
+ if (Array.isArray(a)) {
163
+ if (!Array.isArray(b)) {
164
+ console.warn(`[deepCompare] B is not an array`)
165
+ return false
166
+ }
167
+ if (a.length !== b.length) {
168
+ console.warn(`[deepCompare] array length mismatch:`, { lenA: a.length, lenB: b.length })
169
+ return false
170
+ }
171
+ for (let i = 0; i < a.length; i++) {
172
+ if (!deepCompare(a[i], b[i], cache)) {
173
+ console.warn(`[deepCompare] array element mismatch at index ${i}`)
174
+ return false
175
+ }
176
+ }
177
+ return true
178
+ }
179
+
180
+ if (a instanceof Date) {
181
+ const match = b instanceof Date && a.getTime() === b.getTime()
182
+ if (!match) console.warn(`[deepCompare] Date mismatch`)
183
+ return match
184
+ }
185
+ if (a instanceof RegExp) {
186
+ const match = b instanceof RegExp && a.toString() === b.toString()
187
+ if (!match) console.warn(`[deepCompare] RegExp mismatch`)
188
+ return match
189
+ }
190
+ if (a instanceof Set) {
191
+ if (!(b instanceof Set) || a.size !== b.size) {
192
+ console.warn(`[deepCompare] Set size mismatch`)
193
+ return false
194
+ }
195
+ for (const val of a) {
196
+ let found = false
197
+ for (const bVal of b) {
198
+ if (deepCompare(val, bVal, cache)) {
199
+ found = true
200
+ break
201
+ }
202
+ }
203
+ if (!found) {
204
+ console.warn(`[deepCompare] missing Set element`)
205
+ return false
206
+ }
207
+ }
208
+ return true
209
+ }
210
+ if (a instanceof Map) {
211
+ if (!(b instanceof Map) || a.size !== b.size) {
212
+ console.warn(`[deepCompare] Map size mismatch`)
213
+ return false
214
+ }
215
+ for (const [key, val] of a) {
216
+ if (!b.has(key)) {
217
+ let foundMatch = false
218
+ for (const [bKey, bVal] of b) {
219
+ if (deepCompare(key, bKey, cache) && deepCompare(val, bVal, cache)) {
220
+ foundMatch = true
221
+ break
222
+ }
223
+ }
224
+ if (!foundMatch) {
225
+ console.warn(`[deepCompare] missing Map key`)
226
+ return false
227
+ }
228
+ } else {
229
+ if (!deepCompare(val, b.get(key), cache)) {
230
+ console.warn(`[deepCompare] Map value mismatch for key`)
231
+ return false
232
+ }
233
+ }
234
+ }
235
+ return true
236
+ }
237
+
238
+ // Compare own properties
239
+ const keysA = Object.keys(a)
240
+ const keysB = Object.keys(b)
241
+ if (keysA.length !== keysB.length) {
242
+ console.warn(`[deepCompare] keys length mismatch:`, { lenA: keysA.length, lenB: keysB.length, keysA, keysB, a, b })
243
+ return false
244
+ }
245
+
246
+ for (const key of keysA) {
247
+ if (!Object.prototype.hasOwnProperty.call(b, key)) {
248
+ console.warn(`[deepCompare] missing key ${String(key)} in B`)
249
+ return false
250
+ }
251
+ if (!deepCompare(a[key], b[key], cache)) {
252
+ console.warn(`[deepCompare] value mismatch for key ${String(key)}:`, { valA: a[key], valB: b[key] })
253
+ return false
254
+ }
255
+ }
256
+
257
+ return true
258
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"_tslib-Mzh1rNsX.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}