mutts 1.0.6 → 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.
- package/README.md +1 -1
- package/dist/browser.d.ts +2 -0
- package/dist/browser.esm.js +70 -0
- package/dist/browser.esm.js.map +1 -0
- package/dist/browser.js +161 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunks/{index-CDCOjzTy.js → index-BFYK02LG.js} +5760 -4338
- package/dist/chunks/index-BFYK02LG.js.map +1 -0
- package/dist/chunks/{index-DiP0RXoZ.esm.js → index-CNR6QRUl.esm.js} +5440 -4054
- package/dist/chunks/index-CNR6QRUl.esm.js.map +1 -0
- package/dist/devtools/panel.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/node.d.ts +2 -0
- package/dist/node.esm.js +45 -0
- package/dist/node.esm.js.map +1 -0
- package/dist/node.js +136 -0
- package/dist/node.js.map +1 -0
- package/docs/ai/api-reference.md +0 -2
- package/docs/reactive/advanced.md +2 -5
- package/docs/reactive/collections.md +0 -125
- package/docs/reactive/core.md +27 -24
- package/docs/reactive/debugging.md +12 -2
- package/docs/reactive/project.md +1 -1
- package/docs/reactive/scan.md +78 -0
- package/docs/reactive.md +2 -1
- package/docs/std-decorators.md +1 -0
- package/docs/zone.md +88 -0
- package/package.json +42 -10
- package/src/async/browser.ts +87 -0
- package/src/async/index.ts +8 -0
- package/src/async/node.ts +46 -0
- package/src/decorator.ts +5 -1
- package/src/destroyable.ts +1 -1
- package/src/index.ts +22 -14
- package/src/indexable.ts +42 -0
- package/src/mixins.ts +2 -2
- package/src/reactive/array.ts +149 -141
- package/src/reactive/buffer.ts +168 -0
- package/src/reactive/change.ts +2 -2
- package/src/reactive/effect-context.ts +15 -91
- package/src/reactive/effects.ts +119 -179
- package/src/reactive/index.ts +10 -13
- package/src/reactive/interface.ts +19 -33
- package/src/reactive/map.ts +48 -61
- package/src/reactive/memoize.ts +19 -9
- package/src/reactive/project.ts +43 -22
- package/src/reactive/proxy.ts +16 -41
- package/src/reactive/record.ts +3 -3
- package/src/reactive/register.ts +5 -7
- package/src/reactive/registry.ts +9 -17
- package/src/reactive/set.ts +42 -56
- package/src/reactive/tracking.ts +1 -29
- package/src/reactive/types.ts +46 -23
- package/src/utils.ts +80 -37
- package/src/zone.ts +127 -0
- package/dist/chunks/_tslib-BgjropY9.js +0 -81
- package/dist/chunks/_tslib-BgjropY9.js.map +0 -1
- package/dist/chunks/_tslib-MCKDzsSq.esm.js +0 -75
- package/dist/chunks/_tslib-MCKDzsSq.esm.js.map +0 -1
- package/dist/chunks/decorator-BGILvPtN.esm.js +0 -627
- package/dist/chunks/decorator-BGILvPtN.esm.js.map +0 -1
- package/dist/chunks/decorator-BQ2eBTCj.js +0 -651
- package/dist/chunks/decorator-BQ2eBTCj.js.map +0 -1
- package/dist/chunks/index-CDCOjzTy.js.map +0 -1
- package/dist/chunks/index-DiP0RXoZ.esm.js.map +0 -1
- package/dist/decorator.d.ts +0 -107
- package/dist/decorator.esm.js +0 -2
- package/dist/decorator.esm.js.map +0 -1
- package/dist/decorator.js +0 -11
- package/dist/decorator.js.map +0 -1
- package/dist/destroyable.d.ts +0 -90
- package/dist/destroyable.esm.js +0 -109
- package/dist/destroyable.esm.js.map +0 -1
- package/dist/destroyable.js +0 -116
- package/dist/destroyable.js.map +0 -1
- package/dist/eventful.d.ts +0 -20
- package/dist/eventful.esm.js +0 -66
- package/dist/eventful.esm.js.map +0 -1
- package/dist/eventful.js +0 -68
- package/dist/eventful.js.map +0 -1
- package/dist/index.d.ts +0 -19
- package/dist/index.esm.js +0 -53
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -139
- package/dist/index.js.map +0 -1
- package/dist/indexable.d.ts +0 -243
- package/dist/indexable.esm.js +0 -285
- package/dist/indexable.esm.js.map +0 -1
- package/dist/indexable.js +0 -291
- package/dist/indexable.js.map +0 -1
- package/dist/promiseChain.d.ts +0 -21
- package/dist/promiseChain.esm.js +0 -78
- package/dist/promiseChain.esm.js.map +0 -1
- package/dist/promiseChain.js +0 -80
- package/dist/promiseChain.js.map +0 -1
- package/dist/reactive.d.ts +0 -910
- package/dist/reactive.esm.js +0 -5
- package/dist/reactive.esm.js.map +0 -1
- package/dist/reactive.js +0 -59
- package/dist/reactive.js.map +0 -1
- package/dist/std-decorators.d.ts +0 -52
- package/dist/std-decorators.esm.js +0 -196
- package/dist/std-decorators.esm.js.map +0 -1
- package/dist/std-decorators.js +0 -204
- package/dist/std-decorators.js.map +0 -1
- package/src/reactive/mapped.ts +0 -129
- package/src/reactive/zone.ts +0 -208
package/src/reactive/change.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function collectEffects(
|
|
|
46
46
|
for (const effect of deps) {
|
|
47
47
|
const runningChain = isRunning(effect)
|
|
48
48
|
if (runningChain) {
|
|
49
|
-
options.skipRunningEffect(effect
|
|
49
|
+
options.skipRunningEffect(effect)
|
|
50
50
|
continue
|
|
51
51
|
}
|
|
52
52
|
if (!effects.has(effect)) {
|
|
@@ -118,7 +118,7 @@ export function touchedOpaque(obj: any, evolution: Evolution, prop: any) {
|
|
|
118
118
|
|
|
119
119
|
const runningChain = isRunning(effect)
|
|
120
120
|
if (runningChain) {
|
|
121
|
-
options.skipRunningEffect(effect
|
|
121
|
+
options.skipRunningEffect(effect)
|
|
122
122
|
continue
|
|
123
123
|
}
|
|
124
124
|
effects.add(effect)
|
|
@@ -1,94 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const rootEffect = getRoot(effect)
|
|
15
|
-
|
|
16
|
-
// Check if the effect is directly in the stack
|
|
17
|
-
const rootIndex = stack.indexOf(rootEffect)
|
|
18
|
-
if (rootIndex !== -1) {
|
|
19
|
-
return stack.slice(0, rootIndex + 1).reverse()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Check if any effect in the stack is a descendant of this effect
|
|
23
|
-
// (i.e., walk up the parent chain from each stack effect to see if we reach this effect)
|
|
24
|
-
for (let i = 0; i < stack.length; i++) {
|
|
25
|
-
const stackEffect = stack[i]
|
|
26
|
-
let current: ScopedCallback | undefined = stackEffect
|
|
27
|
-
const visited = new WeakSet<ScopedCallback>()
|
|
28
|
-
const ancestorChain: ScopedCallback[] = []
|
|
29
|
-
// TODO: That's perhaps a lot of computations for an `assert`
|
|
30
|
-
// Walk up the parent chain to find if this effect is an ancestor
|
|
31
|
-
while (current && !visited.has(current)) {
|
|
32
|
-
visited.add(current)
|
|
33
|
-
const currentRoot = getRoot(current)
|
|
34
|
-
ancestorChain.push(currentRoot)
|
|
35
|
-
if (currentRoot === rootEffect) {
|
|
36
|
-
// Found a descendant - build the full chain from ancestor to active
|
|
37
|
-
// The ancestorChain contains [descendant, parent, ..., ancestor] (walking up)
|
|
38
|
-
// We need [ancestor (effect), ..., parent, descendant, ...stack from descendant to active]
|
|
39
|
-
const chainFromAncestor = ancestorChain.reverse() // [ancestor, ..., descendant]
|
|
40
|
-
// Prepend the actual effect we're checking (in case current is a wrapper)
|
|
41
|
-
if (chainFromAncestor[0] !== rootEffect) {
|
|
42
|
-
chainFromAncestor.unshift(rootEffect)
|
|
43
|
-
}
|
|
44
|
-
// Append the rest of the stack from the descendant to the active effect
|
|
45
|
-
const stackFromDescendant = stack.slice(0, i + 1).reverse() // [descendant, ..., active]
|
|
46
|
-
// Remove duplicate descendant (it's both at end of chainFromAncestor and start of stackFromDescendant)
|
|
47
|
-
if (chainFromAncestor.length > 0 && stackFromDescendant.length > 0) {
|
|
48
|
-
stackFromDescendant.shift() // Remove duplicate descendant
|
|
49
|
-
}
|
|
50
|
-
return [...chainFromAncestor, ...stackFromDescendant]
|
|
51
|
-
}
|
|
52
|
-
current = effectParent.get(current)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
export function withEffectStack<T>(snapshot: (ScopedCallback | undefined)[], fn: () => T): T {
|
|
59
|
-
const previousStack = stack.slice()
|
|
60
|
-
assignStack(snapshot)
|
|
61
|
-
try {
|
|
62
|
-
return fn()
|
|
63
|
-
} finally {
|
|
64
|
-
assignStack(previousStack)
|
|
65
|
-
}
|
|
1
|
+
import { tag } from '../utils'
|
|
2
|
+
import { asyncZone, ZoneAggregator, ZoneHistory } from '../zone'
|
|
3
|
+
import { getRoot } from './registry'
|
|
4
|
+
import { type ScopedCallback } from './types'
|
|
5
|
+
|
|
6
|
+
export const effectHistory = tag(new ZoneHistory<ScopedCallback>(), 'effectHistory')
|
|
7
|
+
tag(effectHistory.present, 'effectHistory.present')
|
|
8
|
+
asyncZone.add(effectHistory)
|
|
9
|
+
export const effectAggregator = tag(new ZoneAggregator(effectHistory.present), 'effectAggregator')
|
|
10
|
+
|
|
11
|
+
export function isRunning(effect: ScopedCallback): boolean {
|
|
12
|
+
const root = getRoot(effect)
|
|
13
|
+
return effectHistory.some((e) => getRoot(e) === root)
|
|
66
14
|
}
|
|
67
15
|
|
|
68
16
|
export function getActiveEffect() {
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Executes a function with a specific effect context
|
|
74
|
-
* @param effect - The effect to use as context
|
|
75
|
-
* @param fn - The function to execute
|
|
76
|
-
* @param keepParent - Whether to keep the parent effect context
|
|
77
|
-
* @returns The result of the function
|
|
78
|
-
*/
|
|
79
|
-
export function withEffect<T>(effect: ScopedCallback | undefined, fn: () => T): T {
|
|
80
|
-
|
|
81
|
-
if (getRoot(effect) === getRoot(getActiveEffect())) return fn()
|
|
82
|
-
stack.unshift(effect)
|
|
83
|
-
try {
|
|
84
|
-
return fn()
|
|
85
|
-
} finally {
|
|
86
|
-
const recoveredEffect = stack.shift()
|
|
87
|
-
if (recoveredEffect !== effect) throw new ReactiveError('[reactive] Effect stack mismatch')
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function assignStack(values: (ScopedCallback | undefined)[]) {
|
|
92
|
-
stack.length = 0
|
|
93
|
-
stack.push(...values)
|
|
94
|
-
}
|
|
17
|
+
return effectHistory.present.active
|
|
18
|
+
}
|
package/src/reactive/effects.ts
CHANGED
|
@@ -2,16 +2,10 @@ import { decorator } from '../decorator'
|
|
|
2
2
|
import { IterableWeakSet } from '../iterableWeak'
|
|
3
3
|
import { getTriggerChain, isDevtoolsEnabled, registerEffectForDebug } from './debug'
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
effectAggregator,
|
|
6
|
+
effectHistory,
|
|
7
7
|
getActiveEffect,
|
|
8
|
-
withEffect,
|
|
9
|
-
withEffectStack,
|
|
10
8
|
} from './effect-context'
|
|
11
|
-
import {
|
|
12
|
-
getTrackingDisabled,
|
|
13
|
-
setTrackingDisabled,
|
|
14
|
-
} from './tracking'
|
|
15
9
|
import {
|
|
16
10
|
effectChildren,
|
|
17
11
|
effectParent,
|
|
@@ -21,6 +15,7 @@ import {
|
|
|
21
15
|
watchers,
|
|
22
16
|
} from './registry'
|
|
23
17
|
import {
|
|
18
|
+
cleanup as cleanupSymbol,
|
|
24
19
|
type DependencyAccess,
|
|
25
20
|
type EffectOptions,
|
|
26
21
|
type Evolution,
|
|
@@ -29,10 +24,11 @@ import {
|
|
|
29
24
|
ReactiveErrorCode,
|
|
30
25
|
// type AsyncExecutionMode,
|
|
31
26
|
type ScopedCallback,
|
|
32
|
-
cleanup as cleanupSymbol,
|
|
33
27
|
stopped,
|
|
34
28
|
} from './types'
|
|
35
29
|
|
|
30
|
+
import { unwrap } from './proxy-state'
|
|
31
|
+
|
|
36
32
|
/**
|
|
37
33
|
* Finds a cycle in a sequence of functions by looking for the first repetition
|
|
38
34
|
*/
|
|
@@ -59,11 +55,8 @@ function formatRoots(roots: Function[], limit = 20): string {
|
|
|
59
55
|
return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
import { ensureZoneHooked } from './zone'
|
|
63
|
-
|
|
64
58
|
type EffectTracking = (obj: any, evolution: Evolution, prop: any) => void
|
|
65
59
|
|
|
66
|
-
export { captureEffectStack, withEffectStack, getActiveEffect, effectStack }
|
|
67
60
|
export interface ActivationRecord {
|
|
68
61
|
effect: ScopedCallback
|
|
69
62
|
obj: any
|
|
@@ -211,6 +204,7 @@ function getOrCreateClosure(
|
|
|
211
204
|
* @param targetRoot - Root function of the effect being triggered
|
|
212
205
|
*/
|
|
213
206
|
function addGraphEdge(callerRoot: Function, targetRoot: Function) {
|
|
207
|
+
if (options.cycleHandling === 'none') return
|
|
214
208
|
// Skip if edge already exists
|
|
215
209
|
const triggers = effectTriggers.get(callerRoot)
|
|
216
210
|
if (triggers?.has(targetRoot)) {
|
|
@@ -336,6 +330,7 @@ function hasPathExcluding(start: Function, end: Function, exclude: Function): bo
|
|
|
336
330
|
* @param effect - The effect being cleaned up
|
|
337
331
|
*/
|
|
338
332
|
function cleanupEffectFromGraph(effect: ScopedCallback) {
|
|
333
|
+
if (options.cycleHandling === 'none') return
|
|
339
334
|
const root = getRoot(effect)
|
|
340
335
|
|
|
341
336
|
// Get closures before removing direct edges (needed for propagation)
|
|
@@ -458,6 +453,7 @@ const batchCleanups = new Set<ScopedCallback>()
|
|
|
458
453
|
* Called once when batch starts or when new effects are added
|
|
459
454
|
*/
|
|
460
455
|
function computeAllInDegrees(batch: BatchQueue): void {
|
|
456
|
+
if (options.cycleHandling === 'none') return
|
|
461
457
|
const activeEffect = getActiveEffect()
|
|
462
458
|
const activeRoot = activeEffect ? getRoot(activeEffect) : null
|
|
463
459
|
|
|
@@ -597,6 +593,10 @@ function getCyclePathForEdge(callerRoot: Function, targetRoot: Function): Functi
|
|
|
597
593
|
* Checks if adding an edge would create a cycle
|
|
598
594
|
* Uses causesClosure to check if callerRoot is already a cause of targetRoot
|
|
599
595
|
* Self-loops (callerRoot === targetRoot) are explicitly ignored and return false
|
|
596
|
+
*
|
|
597
|
+
* **Note**: This is the primary optimization benefit of the transitive closure system.
|
|
598
|
+
* It allows detecting cycles in O(1) time before they are executed.
|
|
599
|
+
*
|
|
600
600
|
* @param callerRoot - Root of the effect that triggers
|
|
601
601
|
* @param targetRoot - Root of the effect being triggered
|
|
602
602
|
* @returns true if adding this edge would create a cycle
|
|
@@ -626,8 +626,7 @@ function wouldCreateCycle(callerRoot: Function, targetRoot: Function): boolean {
|
|
|
626
626
|
* @param immediate - If true, don't create edges in the dependency graph
|
|
627
627
|
*/
|
|
628
628
|
function addToBatch(effect: ScopedCallback, caller?: ScopedCallback, immediate?: boolean) {
|
|
629
|
-
|
|
630
|
-
if (cleanupFn) cleanupFn()
|
|
629
|
+
(effect as any)[cleanupSymbol]?.()
|
|
631
630
|
// If the effect was stopped during cleanup (e.g. lazy memoization), don't add it to the batch
|
|
632
631
|
if ((effect as any)[stopped]) return
|
|
633
632
|
|
|
@@ -636,11 +635,16 @@ function addToBatch(effect: ScopedCallback, caller?: ScopedCallback, immediate?:
|
|
|
636
635
|
const root = getRoot(effect)
|
|
637
636
|
|
|
638
637
|
// 1. Add to batch first (needed for cycle detection)
|
|
638
|
+
if (options.cycleHandling === 'none' && batchQueue.all.has(root)) {
|
|
639
|
+
// If already present in flat mode, remove it so that the next set puts it at the end
|
|
640
|
+
batchQueue.all.delete(root)
|
|
641
|
+
}
|
|
642
|
+
|
|
639
643
|
batchQueue.all.set(root, effect)
|
|
640
644
|
|
|
641
645
|
// 2. Add to global graph (if caller exists and not immediate) - USE ROOTS ONLY
|
|
642
646
|
// When immediate is true, don't create edges - the effect is not considered as a consequence
|
|
643
|
-
if (caller && !immediate) {
|
|
647
|
+
if (caller && !immediate && options.cycleHandling !== 'none') {
|
|
644
648
|
const callerRoot = getRoot(caller)
|
|
645
649
|
|
|
646
650
|
// Check for cycle BEFORE adding edge
|
|
@@ -807,14 +811,22 @@ function executeNext(effectuatedRoots: Function[]): any {
|
|
|
807
811
|
let nextEffect: ScopedCallback | null = null
|
|
808
812
|
let nextRoot: Function | null = null
|
|
809
813
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
814
|
+
if (options.cycleHandling === 'none') {
|
|
815
|
+
// In flat mode, we just take the first effect in the queue (FIFO)
|
|
816
|
+
const first = batchQueue!.all.entries().next().value
|
|
817
|
+
if (first) {
|
|
818
|
+
;[nextRoot, nextEffect] = first
|
|
819
|
+
}
|
|
820
|
+
} else {
|
|
821
|
+
// Find an effect with in-degree 0 (no dependencies in batch that still need execution)
|
|
822
|
+
// Using cached in-degrees for O(n) lookup instead of O(n²)
|
|
823
|
+
for (const [root, effect] of batchQueue!.all) {
|
|
824
|
+
const inDegree = batchQueue!.inDegrees.get(root) ?? 0
|
|
825
|
+
if (inDegree === 0) {
|
|
826
|
+
nextEffect = effect
|
|
827
|
+
nextRoot = root
|
|
828
|
+
break
|
|
829
|
+
}
|
|
818
830
|
}
|
|
819
831
|
}
|
|
820
832
|
|
|
@@ -907,9 +919,8 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
907
919
|
// Nested batch - add to existing
|
|
908
920
|
options?.chain(roots, getRoot(getActiveEffect()))
|
|
909
921
|
const caller = getActiveEffect()
|
|
910
|
-
for (let i = 0; i < effect.length; i++)
|
|
922
|
+
for (let i = 0; i < effect.length; i++)
|
|
911
923
|
addToBatch(effect[i], caller, immediate === 'immediate')
|
|
912
|
-
}
|
|
913
924
|
if (immediate) {
|
|
914
925
|
const firstReturn: { value?: any } = {}
|
|
915
926
|
// Execute immediately (before batch returns)
|
|
@@ -935,30 +946,30 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
935
946
|
inDegrees: new Map(),
|
|
936
947
|
}
|
|
937
948
|
|
|
938
|
-
// Add initial effects
|
|
939
949
|
const caller = getActiveEffect()
|
|
940
|
-
for (let i = 0; i < effect.length; i++) {
|
|
941
|
-
addToBatch(effect[i], caller, immediate === 'immediate')
|
|
942
|
-
}
|
|
943
|
-
|
|
944
950
|
const effectuatedRoots: ScopedCallback[] = []
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
951
|
+
const firstReturn: { value?: any } = {}
|
|
952
|
+
|
|
953
|
+
try {
|
|
954
|
+
if (immediate) {
|
|
955
|
+
// Execute initial effects in providing order
|
|
950
956
|
for (let i = 0; i < effect.length; i++) {
|
|
951
957
|
try {
|
|
952
958
|
const rv = effect[i]()
|
|
953
959
|
if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
|
|
954
960
|
} finally {
|
|
955
|
-
|
|
956
|
-
batchQueue.all.delete(root)
|
|
961
|
+
batchQueue.all.delete(getRoot(effect[i]))
|
|
957
962
|
}
|
|
958
963
|
}
|
|
959
|
-
|
|
960
|
-
//
|
|
961
|
-
|
|
964
|
+
} else {
|
|
965
|
+
// Add initial effects to batch and compute dependencies
|
|
966
|
+
for (let i = 0; i < effect.length; i++) addToBatch(effect[i], caller, false)
|
|
967
|
+
computeAllInDegrees(batchQueue)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Processing loop for all triggered effects and cleanups
|
|
971
|
+
while (batchQueue.all.size > 0 || batchCleanups.size > 0) {
|
|
972
|
+
if (batchQueue.all.size > 0) {
|
|
962
973
|
if (effectuatedRoots.length > options.maxEffectChain) {
|
|
963
974
|
const cycle = findCycleInChain(effectuatedRoots as any)
|
|
964
975
|
const trace = formatRoots(effectuatedRoots as any)
|
|
@@ -998,98 +1009,35 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
998
1009
|
break
|
|
999
1010
|
}
|
|
1000
1011
|
}
|
|
1001
|
-
if (!batchQueue || batchQueue.all.size === 0) break
|
|
1002
1012
|
const rv = executeNext(effectuatedRoots)
|
|
1003
|
-
// If executeNext() returned null but batch is not empty, it means a cycle was detected
|
|
1004
|
-
// and an error was thrown, so we won't reach here
|
|
1005
1013
|
if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
|
|
1014
|
+
} else {
|
|
1015
|
+
// Process cleanups. If they trigger more effects, they will be caught in the next iteration.
|
|
1016
|
+
const cleanups = Array.from(batchCleanups)
|
|
1017
|
+
batchCleanups.clear()
|
|
1018
|
+
for (const cleanup of cleanups) cleanup()
|
|
1019
|
+
|
|
1020
|
+
// In immediate mode, we traditionally don't process recursive effects from cleanups.
|
|
1021
|
+
// If we want to keep that behavior: if (immediate) break
|
|
1006
1022
|
}
|
|
1007
|
-
const cleanups = Array.from(batchCleanups)
|
|
1008
|
-
batchCleanups.clear()
|
|
1009
|
-
for (const cleanup of cleanups) cleanup()
|
|
1010
|
-
return firstReturn.value
|
|
1011
|
-
} finally {
|
|
1012
|
-
activationRegistry = undefined
|
|
1013
|
-
batchQueue = undefined
|
|
1014
|
-
options.endChain()
|
|
1015
|
-
}
|
|
1016
|
-
} else {
|
|
1017
|
-
// Execute in dependency order
|
|
1018
|
-
const firstReturn: { value?: any } = {}
|
|
1019
|
-
try {
|
|
1020
|
-
// Outer loop: continue while there are effects OR cleanups pending.
|
|
1021
|
-
// This ensures effects triggered by cleanups are not lost.
|
|
1022
|
-
while (batchQueue.all.size > 0 || batchCleanups.size > 0) {
|
|
1023
|
-
// Inner loop: execute all pending effects
|
|
1024
|
-
while (batchQueue.all.size > 0) {
|
|
1025
|
-
if (effectuatedRoots.length > options.maxEffectChain) {
|
|
1026
|
-
const cycle = findCycleInChain(effectuatedRoots as any)
|
|
1027
|
-
const trace = formatRoots(effectuatedRoots as any)
|
|
1028
|
-
const message = cycle
|
|
1029
|
-
? `Max effect chain reached (cycle detected: ${formatRoots(cycle)})`
|
|
1030
|
-
: `Max effect chain reached (trace: ${trace})`
|
|
1031
|
-
|
|
1032
|
-
const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : []
|
|
1033
|
-
const queued = queuedRoots.map((r) => r.name || '<anonymous>')
|
|
1034
|
-
const debugInfo = {
|
|
1035
|
-
code: ReactiveErrorCode.MaxDepthExceeded,
|
|
1036
|
-
effectuatedRoots,
|
|
1037
|
-
cycle,
|
|
1038
|
-
trace,
|
|
1039
|
-
maxEffectChain: options.maxEffectChain,
|
|
1040
|
-
queued: queued.slice(0, 50),
|
|
1041
|
-
queuedCount: queued.length,
|
|
1042
|
-
// Try to get causation for the last effect
|
|
1043
|
-
causalChain:
|
|
1044
|
-
effectuatedRoots.length > 0
|
|
1045
|
-
? getTriggerChain(
|
|
1046
|
-
batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1])!
|
|
1047
|
-
)
|
|
1048
|
-
: [],
|
|
1049
|
-
}
|
|
1050
|
-
switch (options.maxEffectReaction) {
|
|
1051
|
-
case 'throw':
|
|
1052
|
-
throw new ReactiveError(`[reactive] ${message}`, debugInfo)
|
|
1053
|
-
case 'debug':
|
|
1054
|
-
// biome-ignore lint/suspicious/noDebugger: This is the whole point here
|
|
1055
|
-
debugger
|
|
1056
|
-
throw new ReactiveError(`[reactive] ${message}`, debugInfo)
|
|
1057
|
-
case 'warn':
|
|
1058
|
-
options.warn(
|
|
1059
|
-
`[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`
|
|
1060
|
-
)
|
|
1061
|
-
break
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
const rv = executeNext(effectuatedRoots)
|
|
1065
|
-
// executeNext() returns null when batch is complete or cycle detected (throws error)
|
|
1066
|
-
// But functions can legitimately return null, so we check batchQueue.all.size instead
|
|
1067
|
-
if (batchQueue.all.size === 0) {
|
|
1068
|
-
// Batch complete
|
|
1069
|
-
break
|
|
1070
|
-
}
|
|
1071
|
-
// If executeNext() returned null but batch is not empty, it means a cycle was detected
|
|
1072
|
-
// and an error was thrown, so we won't reach here
|
|
1073
|
-
if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
|
|
1074
|
-
// Note: executeNext() already removed it from batchQueue, so we track by count
|
|
1075
|
-
}
|
|
1076
|
-
// Process cleanups. If they trigger new effects, the outer loop will catch them.
|
|
1077
|
-
if (batchCleanups.size > 0) {
|
|
1078
|
-
const cleanups = Array.from(batchCleanups)
|
|
1079
|
-
batchCleanups.clear()
|
|
1080
|
-
for (const cleanup of cleanups) cleanup()
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
return firstReturn.value
|
|
1084
|
-
} finally {
|
|
1085
|
-
activationRegistry = undefined
|
|
1086
|
-
batchQueue = undefined
|
|
1087
|
-
options.endChain()
|
|
1088
1023
|
}
|
|
1024
|
+
return firstReturn.value
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
throw error instanceof ReactiveError
|
|
1027
|
+
? error
|
|
1028
|
+
: new ReactiveError('Effects are broken', { code: ReactiveErrorCode.BrokenEffects, cause: error })
|
|
1029
|
+
} finally {
|
|
1030
|
+
activationRegistry = undefined
|
|
1031
|
+
batchQueue = undefined
|
|
1032
|
+
options.endChain()
|
|
1089
1033
|
}
|
|
1090
1034
|
}
|
|
1091
1035
|
}
|
|
1092
1036
|
|
|
1037
|
+
// Inject batch function to allow atomic game loops in requestAnimationFrame/setTimeout/...
|
|
1038
|
+
// TODO: perhaps introduce somewhere a way to wrap async functions - find out if it's necessary
|
|
1039
|
+
// wrapAsync(fn=> batch(fn, 'immediate'))
|
|
1040
|
+
|
|
1093
1041
|
/**
|
|
1094
1042
|
* Decorator that makes methods atomic - batches all effects triggered within the method
|
|
1095
1043
|
*/
|
|
@@ -1130,10 +1078,10 @@ export function effect(
|
|
|
1130
1078
|
//biome-ignore lint/suspicious/noConfusingVoidType: We have to
|
|
1131
1079
|
fn: (access: DependencyAccess) => ScopedCallback | undefined | void | Promise<any>,
|
|
1132
1080
|
effectOptions?: EffectOptions
|
|
1133
|
-
): ScopedCallback {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1081
|
+
): ScopedCallback & {
|
|
1082
|
+
[stopped]: boolean
|
|
1083
|
+
[cleanupSymbol]: () => void
|
|
1084
|
+
} {
|
|
1137
1085
|
|
|
1138
1086
|
// Use per-effect asyncMode or fall back to global option
|
|
1139
1087
|
const asyncMode = effectOptions?.asyncMode ?? options.asyncMode ?? 'cancel'
|
|
@@ -1146,10 +1094,13 @@ export function effect(
|
|
|
1146
1094
|
}
|
|
1147
1095
|
}
|
|
1148
1096
|
let cleanup: (() => void) | null = null
|
|
1149
|
-
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1097
|
+
const tracked = effectHistory.present.with(runEffect, ()=> effectAggregator.zoned)
|
|
1098
|
+
const ascend = effectHistory.zoned
|
|
1099
|
+
//const parent = effectHistory.present.active // TODO: Double-check parenting (untracked -> stop children) - use case or untracked(effect)
|
|
1100
|
+
let parent = effectHistory.present.active
|
|
1101
|
+
/*if (!parent) {
|
|
1102
|
+
for (const h of effectHistory.active.history) parent = h
|
|
1103
|
+
}*/
|
|
1153
1104
|
let effectStopped = false
|
|
1154
1105
|
let hasReacted = false
|
|
1155
1106
|
let runningPromise: Promise<any> | null = null
|
|
@@ -1160,7 +1111,7 @@ export function effect(
|
|
|
1160
1111
|
if (cleanup) {
|
|
1161
1112
|
const prevCleanup = cleanup
|
|
1162
1113
|
cleanup = null
|
|
1163
|
-
|
|
1114
|
+
untracked(() => prevCleanup())
|
|
1164
1115
|
}
|
|
1165
1116
|
|
|
1166
1117
|
// Handle async modes when effect is retriggered
|
|
@@ -1184,7 +1135,7 @@ export function effect(
|
|
|
1184
1135
|
let reactionCleanup: ScopedCallback | undefined
|
|
1185
1136
|
let result: any
|
|
1186
1137
|
try {
|
|
1187
|
-
result =
|
|
1138
|
+
result = tracked(() => fn({ tracked, ascend, reaction: hasReacted }))
|
|
1188
1139
|
if (
|
|
1189
1140
|
result &&
|
|
1190
1141
|
typeof result !== 'function' &&
|
|
@@ -1260,6 +1211,28 @@ export function effect(
|
|
|
1260
1211
|
}
|
|
1261
1212
|
// Mark the runEffect callback with the original function as its root
|
|
1262
1213
|
markWithRoot(runEffect, fn)
|
|
1214
|
+
function augmentedRv(rv: ScopedCallback) {
|
|
1215
|
+
return Object.defineProperties(rv, {
|
|
1216
|
+
[stopped]: {
|
|
1217
|
+
get() {
|
|
1218
|
+
return effectStopped
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1221
|
+
[cleanupSymbol]: {
|
|
1222
|
+
value: () => {
|
|
1223
|
+
if (cleanup) {
|
|
1224
|
+
const prevCleanup = cleanup
|
|
1225
|
+
cleanup = null
|
|
1226
|
+
untracked(() => prevCleanup())
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
}) as ScopedCallback & {
|
|
1231
|
+
[stopped]: boolean
|
|
1232
|
+
[cleanupSymbol]: () => void
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
augmentedRv(runEffect)
|
|
1263
1236
|
|
|
1264
1237
|
// Register strict mode if enabled
|
|
1265
1238
|
if (effectOptions?.opaque) {
|
|
@@ -1272,7 +1245,6 @@ export function effect(
|
|
|
1272
1245
|
|
|
1273
1246
|
batch(runEffect, 'immediate')
|
|
1274
1247
|
|
|
1275
|
-
const parent = parentsForAscend[0]
|
|
1276
1248
|
// Store parent relationship for hierarchy traversal
|
|
1277
1249
|
effectParent.set(runEffect, parent)
|
|
1278
1250
|
// Only ROOT effects are registered for GC cleanup and zone tracking
|
|
@@ -1292,23 +1264,6 @@ export function effect(
|
|
|
1292
1264
|
cleanupEffectFromGraph(runEffect)
|
|
1293
1265
|
fr.unregister(stopEffect)
|
|
1294
1266
|
}
|
|
1295
|
-
function augmentedRv(rv: ScopedCallback): ScopedCallback {
|
|
1296
|
-
Object.defineProperty(rv, stopped, {
|
|
1297
|
-
get() {
|
|
1298
|
-
return effectStopped
|
|
1299
|
-
},
|
|
1300
|
-
})
|
|
1301
|
-
Object.defineProperty(rv, cleanupSymbol, {
|
|
1302
|
-
value: () => {
|
|
1303
|
-
if (cleanup) {
|
|
1304
|
-
const prevCleanup = cleanup
|
|
1305
|
-
cleanup = null
|
|
1306
|
-
withEffect(undefined, () => prevCleanup())
|
|
1307
|
-
}
|
|
1308
|
-
},
|
|
1309
|
-
})
|
|
1310
|
-
return rv
|
|
1311
|
-
}
|
|
1312
1267
|
if (isRootEffect) {
|
|
1313
1268
|
const callIfCollected = augmentedRv(() => stopEffect())
|
|
1314
1269
|
fr.register(
|
|
@@ -1346,17 +1301,7 @@ export function effect(
|
|
|
1346
1301
|
* @param fn - The function to execute
|
|
1347
1302
|
*/
|
|
1348
1303
|
export function untracked<T>(fn: () => T): T {
|
|
1349
|
-
|
|
1350
|
-
// This prevents the parent effect from tracking dependencies during fn execution
|
|
1351
|
-
const wasTrackingDisabled = getTrackingDisabled()
|
|
1352
|
-
setTrackingDisabled(true)
|
|
1353
|
-
|
|
1354
|
-
try {
|
|
1355
|
-
return fn()
|
|
1356
|
-
} finally {
|
|
1357
|
-
// Restore tracking state
|
|
1358
|
-
setTrackingDisabled(wasTrackingDisabled)
|
|
1359
|
-
}
|
|
1304
|
+
return effectHistory.present.root(fn)
|
|
1360
1305
|
}
|
|
1361
1306
|
|
|
1362
1307
|
/**
|
|
@@ -1365,11 +1310,7 @@ export function untracked<T>(fn: () => T): T {
|
|
|
1365
1310
|
* @param fn - The function to execute
|
|
1366
1311
|
*/
|
|
1367
1312
|
export function root<T>(fn: () => T): T {
|
|
1368
|
-
|
|
1369
|
-
withEffect(undefined, () => {
|
|
1370
|
-
rv = fn()
|
|
1371
|
-
})
|
|
1372
|
-
return rv
|
|
1313
|
+
return effectHistory.root(fn)
|
|
1373
1314
|
}
|
|
1374
1315
|
|
|
1375
1316
|
export { effectTrackers }
|
|
@@ -1425,18 +1366,17 @@ export function biDi<T>(
|
|
|
1425
1366
|
set = get.set
|
|
1426
1367
|
get = get.get
|
|
1427
1368
|
}
|
|
1428
|
-
|
|
1369
|
+
let programatticallySetValue: any = Symbol()
|
|
1429
1370
|
effect(
|
|
1430
1371
|
markWithRoot(() => {
|
|
1431
|
-
|
|
1432
|
-
|
|
1372
|
+
const newValue = get()
|
|
1373
|
+
if (unwrap(newValue) !== programatticallySetValue) received(newValue)
|
|
1374
|
+
}, received)
|
|
1433
1375
|
)
|
|
1434
|
-
return
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
}
|
|
1441
|
-
})
|
|
1376
|
+
return set
|
|
1377
|
+
? atomic((value: T) => {
|
|
1378
|
+
programatticallySetValue = unwrap(value)
|
|
1379
|
+
set(value)
|
|
1380
|
+
})
|
|
1381
|
+
: () => {}
|
|
1442
1382
|
}
|
package/src/reactive/index.ts
CHANGED
|
@@ -13,33 +13,30 @@ export { deepWatch } from './deep-watch'
|
|
|
13
13
|
export {
|
|
14
14
|
addBatchCleanup,
|
|
15
15
|
atomic,
|
|
16
|
-
batch, // TODO: Batch is now exported for testing purposes, though it shouldn't be - modify the tests to go through `atomic`
|
|
17
16
|
biDi,
|
|
18
17
|
defer,
|
|
19
18
|
effect,
|
|
20
19
|
getActivationLog,
|
|
21
|
-
getActiveEffect,
|
|
22
20
|
root,
|
|
23
21
|
trackEffect,
|
|
24
22
|
untracked,
|
|
25
23
|
} from './effects'
|
|
26
24
|
export { cleanedBy, cleanup, derived, unreactive, watch } from './interface'
|
|
27
|
-
export { mapped, ReadOnlyError, reduced } from './mapped'
|
|
28
25
|
export { type Memoizable, memoize } from './memoize'
|
|
29
26
|
export { immutables, isNonReactive, registerNativeReactivity } from './non-reactive'
|
|
30
27
|
export { getActiveProjection, project } from './project'
|
|
31
28
|
export { isReactive, ReactiveBase, reactive, unwrap } from './proxy'
|
|
32
29
|
export { organize, organized } from './record'
|
|
30
|
+
export { scan, type ScanResult } from './buffer'
|
|
33
31
|
export { Register, register } from './register'
|
|
34
32
|
export {
|
|
35
33
|
type DependencyAccess,
|
|
36
|
-
type DependencyFunction,
|
|
37
34
|
type Evolution,
|
|
38
35
|
options as reactiveOptions,
|
|
39
36
|
ReactiveError,
|
|
37
|
+
ReactiveErrorCode,
|
|
40
38
|
type ScopedCallback,
|
|
41
39
|
} from './types'
|
|
42
|
-
export { isZoneEnabled, setZoneEnabled } from './zone'
|
|
43
40
|
|
|
44
41
|
import { ReactiveArray } from './array'
|
|
45
42
|
import {
|
|
@@ -49,17 +46,17 @@ import {
|
|
|
49
46
|
objectsWithDeepWatchers,
|
|
50
47
|
} from './deep-watch'
|
|
51
48
|
import { ReactiveMap, ReactiveWeakMap } from './map'
|
|
52
|
-
import { nonReactiveObjects
|
|
53
|
-
import { objectToProxy, proxyToObject } from './proxy'
|
|
54
|
-
import { ReactiveSet, ReactiveWeakSet } from './set'
|
|
49
|
+
import { nonReactiveObjects } from './non-reactive-state'
|
|
50
|
+
import { metaProtos, objectToProxy, proxyToObject } from './proxy'
|
|
55
51
|
import { effectToReactiveObjects, watchers } from './registry'
|
|
52
|
+
import { ReactiveSet, ReactiveWeakSet } from './set'
|
|
56
53
|
|
|
57
54
|
// Register native collection types to use specialized reactive wrappers
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
metaProtos.set(Array, ReactiveArray.prototype)
|
|
56
|
+
metaProtos.set(Set, ReactiveSet.prototype)
|
|
57
|
+
metaProtos.set(WeakSet, ReactiveWeakSet.prototype)
|
|
58
|
+
metaProtos.set(Map, ReactiveMap.prototype)
|
|
59
|
+
metaProtos.set(WeakMap, ReactiveWeakMap.prototype)
|
|
63
60
|
|
|
64
61
|
/**
|
|
65
62
|
* Object containing internal reactive system state for debugging and profiling
|