mutts 1.0.4 → 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 +2 -1
- 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-GRBSx0mB.js → index-CDCOjzTy.js} +543 -495
- package/dist/chunks/index-CDCOjzTy.js.map +1 -0
- package/dist/chunks/{index-79Kk8D6e.esm.js → index-DiP0RXoZ.esm.js} +452 -404
- 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 +50 -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 +54 -1
- package/dist/reactive.esm.js +3 -3
- package/dist/reactive.js +6 -4
- package/dist/reactive.js.map +1 -1
- 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/core.md +16 -16
- package/docs/reactive/debugging.md +158 -0
- package/docs/reactive.md +8 -0
- package/package.json +16 -66
- package/src/decorator.ts +11 -9
- package/src/destroyable.ts +5 -5
- package/src/index.ts +46 -0
- package/src/reactive/array.ts +3 -5
- package/src/reactive/change.ts +7 -3
- 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 +114 -17
- package/src/reactive/index.ts +3 -2
- package/src/reactive/interface.ts +10 -9
- package/src/reactive/map.ts +6 -6
- package/src/reactive/mapped.ts +2 -3
- package/src/reactive/memoize.ts +77 -31
- package/src/reactive/project.ts +103 -6
- package/src/reactive/proxy.ts +4 -4
- package/src/reactive/registry.ts +67 -0
- package/src/reactive/set.ts +6 -6
- package/src/reactive/tracking.ts +12 -41
- package/src/reactive/types.ts +59 -0
- package/src/reactive/zone.ts +1 -1
- package/src/std-decorators.ts +10 -10
- 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-79Kk8D6e.esm.js.map +0 -1
- package/dist/chunks/index-GRBSx0mB.js.map +0 -1
- /package/{src/reactive/project.project.md → docs/reactive/project.md} +0 -0
package/src/reactive/project.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { ReflectGet, ReflectSet } from '../utils'
|
|
2
|
+
import { setEffectName } from './debug'
|
|
3
|
+
import { getActiveEffect } from './effect-context'
|
|
2
4
|
import { effect, untracked } from './effects'
|
|
3
5
|
import { cleanedBy, cleanup } from './interface'
|
|
4
6
|
import { reactive } from './proxy'
|
|
5
7
|
import { Register } from './register'
|
|
6
|
-
import { type ScopedCallback } from './types'
|
|
8
|
+
import { type ProjectionContext, projectionInfo, type ScopedCallback } from './types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maps projection effects (item effects) to their projection context
|
|
12
|
+
*/
|
|
13
|
+
export const effectProjectionMetadata = new WeakMap<ScopedCallback, ProjectionContext>()
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns the projection context of the currently running effect, if any.
|
|
17
|
+
*/
|
|
18
|
+
export function getActiveProjection(): ProjectionContext | undefined {
|
|
19
|
+
const active = getActiveEffect()
|
|
20
|
+
return active ? effectProjectionMetadata.get(active) : undefined
|
|
21
|
+
}
|
|
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
|
+
*/
|
|
7
27
|
|
|
8
28
|
type ProjectOldValue<Target> = Target extends readonly (infer Item)[]
|
|
9
29
|
? Item
|
|
@@ -50,8 +70,17 @@ function defineAccessValue<Access extends { get(): unknown; set(value: unknown):
|
|
|
50
70
|
function makeCleanup<Result extends object>(
|
|
51
71
|
target: Result,
|
|
52
72
|
effectMap: Map<unknown, ScopedCallback>,
|
|
53
|
-
onDispose: () => void
|
|
73
|
+
onDispose: () => void,
|
|
74
|
+
metadata?: any
|
|
54
75
|
): ProjectResult<Result> {
|
|
76
|
+
if (metadata) {
|
|
77
|
+
Object.defineProperty(target, projectionInfo, {
|
|
78
|
+
value: metadata,
|
|
79
|
+
writable: false,
|
|
80
|
+
enumerable: false,
|
|
81
|
+
configurable: true,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
55
84
|
return cleanedBy(target, () => {
|
|
56
85
|
onDispose()
|
|
57
86
|
for (const stop of effectMap.values()) stop?.()
|
|
@@ -80,6 +109,9 @@ function projectArray<SourceValue, ResultValue>(
|
|
|
80
109
|
}
|
|
81
110
|
}
|
|
82
111
|
|
|
112
|
+
const parent = getActiveProjection()
|
|
113
|
+
const depth = parent ? parent.depth + 1 : 0
|
|
114
|
+
|
|
83
115
|
const cleanupLength = effect(function projectArrayLengthEffect({ ascend }) {
|
|
84
116
|
const length = observedSource.length
|
|
85
117
|
normalizeTargetLength(length)
|
|
@@ -102,13 +134,27 @@ function projectArray<SourceValue, ResultValue>(
|
|
|
102
134
|
const produced = apply(accessBase, target)
|
|
103
135
|
target[index] = produced
|
|
104
136
|
})
|
|
137
|
+
setEffectName(stop, `project[${depth}]:${index}`)
|
|
138
|
+
effectProjectionMetadata.set(stop, {
|
|
139
|
+
source: observedSource,
|
|
140
|
+
key: index,
|
|
141
|
+
target,
|
|
142
|
+
depth,
|
|
143
|
+
parent,
|
|
144
|
+
})
|
|
105
145
|
indexEffects.set(i, stop)
|
|
106
146
|
})
|
|
107
147
|
}
|
|
108
148
|
for (const index of existing) if (index >= length) disposeIndex(index)
|
|
109
149
|
})
|
|
110
150
|
|
|
111
|
-
return makeCleanup(target, indexEffects, () => cleanupLength()
|
|
151
|
+
return makeCleanup(target, indexEffects, () => cleanupLength(), {
|
|
152
|
+
source: observedSource,
|
|
153
|
+
target,
|
|
154
|
+
apply,
|
|
155
|
+
depth,
|
|
156
|
+
parent,
|
|
157
|
+
} as ProjectionContext)
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
@@ -135,6 +181,9 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
|
135
181
|
}
|
|
136
182
|
}
|
|
137
183
|
|
|
184
|
+
const parent = getActiveProjection()
|
|
185
|
+
const depth = parent ? parent.depth + 1 : 0
|
|
186
|
+
|
|
138
187
|
const cleanupKeys = effect(function projectRegisterEffect({ ascend }) {
|
|
139
188
|
const keys = new Set<Key>()
|
|
140
189
|
for (const key of observedSource.mapKeys()) keys.add(key)
|
|
@@ -158,6 +207,14 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
|
158
207
|
const produced = apply(accessBase, target)
|
|
159
208
|
target.set(key, produced)
|
|
160
209
|
})
|
|
210
|
+
setEffectName(stop, `project[${depth}]:${String(key)}`)
|
|
211
|
+
effectProjectionMetadata.set(stop, {
|
|
212
|
+
source: observedSource,
|
|
213
|
+
key,
|
|
214
|
+
target,
|
|
215
|
+
depth,
|
|
216
|
+
parent,
|
|
217
|
+
})
|
|
161
218
|
keyEffects.set(key, stop)
|
|
162
219
|
})
|
|
163
220
|
}
|
|
@@ -165,7 +222,13 @@ function projectRegister<Key extends PropertyKey, SourceValue, ResultValue>(
|
|
|
165
222
|
for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
|
|
166
223
|
})
|
|
167
224
|
|
|
168
|
-
return makeCleanup(target, keyEffects, () => cleanupKeys()
|
|
225
|
+
return makeCleanup(target, keyEffects, () => cleanupKeys(), {
|
|
226
|
+
source: observedSource,
|
|
227
|
+
target,
|
|
228
|
+
apply,
|
|
229
|
+
depth,
|
|
230
|
+
parent,
|
|
231
|
+
} as ProjectionContext)
|
|
169
232
|
}
|
|
170
233
|
|
|
171
234
|
function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
@@ -191,6 +254,9 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
|
191
254
|
}
|
|
192
255
|
}
|
|
193
256
|
|
|
257
|
+
const parent = getActiveProjection()
|
|
258
|
+
const depth = parent ? parent.depth + 1 : 0
|
|
259
|
+
|
|
194
260
|
const cleanupKeys = effect(function projectRecordEffect({ ascend }) {
|
|
195
261
|
const keys = new Set<PropertyKey>()
|
|
196
262
|
for (const key in observedSource) keys.add(key)
|
|
@@ -222,6 +288,14 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
|
222
288
|
const produced = apply(accessBase, target)
|
|
223
289
|
;(target as any)[sourceKey] = produced
|
|
224
290
|
})
|
|
291
|
+
setEffectName(stop, `project[${depth}]:${String(key)}`)
|
|
292
|
+
effectProjectionMetadata.set(stop, {
|
|
293
|
+
source: observedSource,
|
|
294
|
+
key,
|
|
295
|
+
target,
|
|
296
|
+
depth,
|
|
297
|
+
parent,
|
|
298
|
+
})
|
|
225
299
|
keyEffects.set(key, stop)
|
|
226
300
|
})
|
|
227
301
|
}
|
|
@@ -229,7 +303,13 @@ function projectRecord<Source extends Record<PropertyKey, any>, ResultValue>(
|
|
|
229
303
|
for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
|
|
230
304
|
})
|
|
231
305
|
|
|
232
|
-
return makeCleanup(target, keyEffects, () => cleanupKeys()
|
|
306
|
+
return makeCleanup(target, keyEffects, () => cleanupKeys(), {
|
|
307
|
+
source: observedSource,
|
|
308
|
+
target,
|
|
309
|
+
apply,
|
|
310
|
+
depth,
|
|
311
|
+
parent,
|
|
312
|
+
} as ProjectionContext)
|
|
233
313
|
}
|
|
234
314
|
|
|
235
315
|
function projectMap<Key, Value, ResultValue>(
|
|
@@ -250,6 +330,9 @@ function projectMap<Key, Value, ResultValue>(
|
|
|
250
330
|
}
|
|
251
331
|
}
|
|
252
332
|
|
|
333
|
+
const parent = getActiveProjection()
|
|
334
|
+
const depth = parent ? parent.depth + 1 : 0
|
|
335
|
+
|
|
253
336
|
const cleanupKeys = effect(function projectMapEffect({ ascend }) {
|
|
254
337
|
const keys = new Set<Key>()
|
|
255
338
|
for (const key of observedSource.keys()) keys.add(key)
|
|
@@ -273,6 +356,14 @@ function projectMap<Key, Value, ResultValue>(
|
|
|
273
356
|
const produced = apply(accessBase, target)
|
|
274
357
|
target.set(key, produced)
|
|
275
358
|
})
|
|
359
|
+
setEffectName(stop, `project[${depth}]:${String(key)}`)
|
|
360
|
+
effectProjectionMetadata.set(stop, {
|
|
361
|
+
source: observedSource,
|
|
362
|
+
key,
|
|
363
|
+
target,
|
|
364
|
+
depth,
|
|
365
|
+
parent,
|
|
366
|
+
})
|
|
276
367
|
keyEffects.set(key, stop)
|
|
277
368
|
})
|
|
278
369
|
}
|
|
@@ -280,7 +371,13 @@ function projectMap<Key, Value, ResultValue>(
|
|
|
280
371
|
for (const key of Array.from(keyEffects.keys())) if (!keys.has(key)) disposeKey(key)
|
|
281
372
|
})
|
|
282
373
|
|
|
283
|
-
return makeCleanup(target, keyEffects, () => cleanupKeys()
|
|
374
|
+
return makeCleanup(target, keyEffects, () => cleanupKeys(), {
|
|
375
|
+
source: observedSource,
|
|
376
|
+
target,
|
|
377
|
+
apply,
|
|
378
|
+
depth,
|
|
379
|
+
parent,
|
|
380
|
+
} as ProjectionContext)
|
|
284
381
|
}
|
|
285
382
|
|
|
286
383
|
type ProjectOverload = {
|
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/set.ts
CHANGED
|
@@ -11,14 +11,14 @@ const native = Symbol('native')
|
|
|
11
11
|
* Only tracks individual value operations, no size tracking (WeakSet limitation)
|
|
12
12
|
*/
|
|
13
13
|
export class ReactiveWeakSet<T extends object> {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
readonly [native]!: WeakSet<T>
|
|
15
|
+
readonly content!: symbol
|
|
16
16
|
|
|
17
17
|
constructor(original: WeakSet<T>) {
|
|
18
18
|
Object.defineProperties(this, {
|
|
19
19
|
[native]: { value: original },
|
|
20
20
|
[prototypeForwarding]: { value: original },
|
|
21
|
-
content: { value: Symbol('
|
|
21
|
+
content: { value: Symbol('WeakSetContent') },
|
|
22
22
|
[Symbol.toStringTag]: { value: 'ReactiveWeakSet' },
|
|
23
23
|
})
|
|
24
24
|
}
|
|
@@ -52,13 +52,13 @@ export class ReactiveWeakSet<T extends object> {
|
|
|
52
52
|
* Tracks size changes, individual value operations, and collection-wide operations
|
|
53
53
|
*/
|
|
54
54
|
export class ReactiveSet<T> {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
readonly [native]!: Set<T>
|
|
56
|
+
readonly content!: symbol
|
|
57
57
|
constructor(original: Set<T>) {
|
|
58
58
|
Object.defineProperties(this, {
|
|
59
59
|
[native]: { value: original },
|
|
60
60
|
[prototypeForwarding]: { value: original },
|
|
61
|
-
content: { value: Symbol('
|
|
61
|
+
content: { value: Symbol('SetContent') },
|
|
62
62
|
[Symbol.toStringTag]: { value: 'ReactiveSet' },
|
|
63
63
|
})
|
|
64
64
|
}
|
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
|
@@ -141,6 +141,32 @@ export const prototypeForwarding: unique symbol = Symbol('prototype-forwarding')
|
|
|
141
141
|
*/
|
|
142
142
|
export const allProps = Symbol('all-props')
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Symbol for accessing projection information on reactive objects
|
|
146
|
+
*/
|
|
147
|
+
export const projectionInfo = Symbol('projection-info')
|
|
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
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Context for a running projection item effect
|
|
161
|
+
*/
|
|
162
|
+
export interface ProjectionContext {
|
|
163
|
+
source: any
|
|
164
|
+
key?: any
|
|
165
|
+
target: any
|
|
166
|
+
depth: number
|
|
167
|
+
parent?: ProjectionContext
|
|
168
|
+
}
|
|
169
|
+
|
|
144
170
|
// Symbol to mark functions with their root function
|
|
145
171
|
const rootFunction = Symbol('root-function')
|
|
146
172
|
|
|
@@ -250,12 +276,40 @@ export const options = {
|
|
|
250
276
|
* @default 100
|
|
251
277
|
*/
|
|
252
278
|
maxEffectChain: 100,
|
|
279
|
+
/**
|
|
280
|
+
* Maximum number of times an effect can be triggered by the same cause in a single batch
|
|
281
|
+
* Used to detect aggressive re-computation or infinite loops
|
|
282
|
+
* @default 10
|
|
283
|
+
*/
|
|
284
|
+
maxTriggerPerBatch: 10,
|
|
253
285
|
/**
|
|
254
286
|
* Debug purpose: maximum effect reaction (like call stack max depth)
|
|
255
287
|
* Used to prevent infinite loops
|
|
256
288
|
* @default 'throw'
|
|
257
289
|
*/
|
|
258
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,
|
|
259
313
|
/**
|
|
260
314
|
* How to handle cycles detected in effect batches
|
|
261
315
|
* - 'throw': Throw an error with cycle information (default, recommended for development)
|
|
@@ -265,6 +319,11 @@ export const options = {
|
|
|
265
319
|
* @default 'throw'
|
|
266
320
|
*/
|
|
267
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,
|
|
268
327
|
/**
|
|
269
328
|
* Maximum depth for deep watching traversal
|
|
270
329
|
* Used to prevent infinite recursion in circular references
|
package/src/reactive/zone.ts
CHANGED
package/src/std-decorators.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decorator, GenericClassDecorator } from './decorator'
|
|
1
|
+
import { decorator, type GenericClassDecorator } from './decorator'
|
|
2
2
|
|
|
3
3
|
// In order to avoid async re-entrance, we could use zone.js or something like that.
|
|
4
4
|
const syncCalculating: { object: object; prop: PropertyKey }[] = []
|
|
@@ -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
|
|