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.
- package/README.md +1 -0
- package/dist/chunks/{_tslib-Mzh1rNsX.esm.js → _tslib-MCKDzsSq.esm.js} +2 -2
- package/dist/chunks/_tslib-MCKDzsSq.esm.js.map +1 -0
- package/dist/chunks/decorator-BGILvPtN.esm.js +627 -0
- package/dist/chunks/decorator-BGILvPtN.esm.js.map +1 -0
- package/dist/chunks/decorator-BQ2eBTCj.js +651 -0
- package/dist/chunks/decorator-BQ2eBTCj.js.map +1 -0
- package/dist/chunks/{index-Cvxdw6Ax.js → index-CDCOjzTy.js} +396 -500
- package/dist/chunks/index-CDCOjzTy.js.map +1 -0
- package/dist/chunks/{index-qiWwozOc.esm.js → index-DiP0RXoZ.esm.js} +301 -403
- package/dist/chunks/index-DiP0RXoZ.esm.js.map +1 -0
- package/dist/decorator.d.ts +3 -3
- package/dist/decorator.esm.js +1 -1
- package/dist/decorator.js +1 -1
- package/dist/destroyable.esm.js +4 -4
- package/dist/destroyable.esm.js.map +1 -1
- package/dist/destroyable.js +4 -4
- package/dist/destroyable.js.map +1 -1
- package/dist/devtools/panel.js.map +1 -1
- package/dist/eventful.esm.js +1 -1
- package/dist/index.esm.js +48 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +48 -4
- package/dist/index.js.map +1 -1
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/reactive.d.ts +25 -0
- package/dist/reactive.esm.js +3 -3
- package/dist/reactive.js +4 -4
- package/dist/std-decorators.d.ts +1 -1
- package/dist/std-decorators.esm.js +10 -10
- package/dist/std-decorators.esm.js.map +1 -1
- package/dist/std-decorators.js +10 -10
- package/dist/std-decorators.js.map +1 -1
- package/docs/ai/manual.md +14 -95
- package/docs/reactive/advanced.md +6 -107
- package/docs/reactive/debugging.md +158 -0
- package/docs/reactive.md +6 -5
- package/package.json +16 -66
- package/src/decorator.ts +11 -9
- package/src/destroyable.ts +3 -3
- package/src/index.ts +46 -0
- package/src/reactive/change.ts +1 -1
- package/src/reactive/debug.ts +1 -1
- package/src/reactive/deep-touch.ts +1 -1
- package/src/reactive/deep-watch.ts +1 -1
- package/src/reactive/effect-context.ts +2 -2
- package/src/reactive/effects.ts +44 -16
- package/src/reactive/index.ts +1 -1
- package/src/reactive/interface.ts +9 -8
- package/src/reactive/memoize.ts +77 -31
- package/src/reactive/proxy.ts +4 -4
- package/src/reactive/registry.ts +67 -0
- package/src/reactive/tracking.ts +12 -41
- package/src/reactive/types.ts +37 -0
- package/src/std-decorators.ts +9 -9
- package/src/utils.ts +141 -0
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +0 -1
- package/dist/chunks/decorator-DLvrD0UF.js +0 -265
- package/dist/chunks/decorator-DLvrD0UF.js.map +0 -1
- package/dist/chunks/decorator-DqiszP7i.esm.js +0 -253
- package/dist/chunks/decorator-DqiszP7i.esm.js.map +0 -1
- package/dist/chunks/index-Cvxdw6Ax.js.map +0 -1
- 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
|
|
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
|
-
|
|
93
|
+
function watchObjectEffect() {
|
|
93
94
|
dependant(value)
|
|
94
95
|
if (immediate) withEffect(myParentEffect, () => changed(value))
|
|
95
96
|
immediate = true
|
|
96
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
124
|
+
(value) => changed(value as T, value as T)
|
|
124
125
|
)
|
|
125
126
|
}
|
|
126
|
-
}
|
|
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
|
-
|
|
218
|
+
function derivedEffect(access) {
|
|
218
219
|
rv.value = compute(access)
|
|
219
|
-
}
|
|
220
|
+
}
|
|
220
221
|
)
|
|
221
222
|
)
|
|
222
223
|
)
|
package/src/reactive/memoize.ts
CHANGED
|
@@ -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
|
|
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<
|
|
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)
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
},
|
package/src/reactive/proxy.ts
CHANGED
|
@@ -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
|
-
//
|
|
131
|
-
//
|
|
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,
|
|
133
|
+
oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, receiver))
|
|
134
134
|
} else {
|
|
135
|
-
oldVal = Reflect.get(unwrappedObj, prop,
|
|
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
|
+
}
|
package/src/reactive/tracking.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
package/src/reactive/types.ts
CHANGED
|
@@ -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
|
package/src/std-decorators.ts
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|