mutts 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -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-Cvxdw6Ax.js → index-BFYK02LG.js} +5377 -4059
- package/dist/chunks/index-BFYK02LG.js.map +1 -0
- package/dist/chunks/{index-qiWwozOc.esm.js → index-CNR6QRUl.esm.js} +5247 -3963
- package/dist/chunks/index-CNR6QRUl.esm.js.map +1 -0
- 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/ai/manual.md +14 -95
- package/docs/reactive/advanced.md +7 -111
- package/docs/reactive/collections.md +0 -125
- package/docs/reactive/core.md +27 -24
- package/docs/reactive/debugging.md +168 -0
- package/docs/reactive/project.md +1 -1
- package/docs/reactive/scan.md +78 -0
- package/docs/reactive.md +8 -6
- package/docs/std-decorators.md +1 -0
- package/docs/zone.md +88 -0
- package/package.json +47 -65
- 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 +15 -9
- package/src/destroyable.ts +4 -4
- package/src/index.ts +54 -0
- 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 +3 -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 +15 -91
- package/src/reactive/effects.ts +138 -170
- package/src/reactive/index.ts +10 -13
- package/src/reactive/interface.ts +20 -33
- package/src/reactive/map.ts +48 -61
- package/src/reactive/memoize.ts +87 -31
- package/src/reactive/project.ts +43 -22
- package/src/reactive/proxy.ts +18 -43
- package/src/reactive/record.ts +3 -3
- package/src/reactive/register.ts +5 -7
- package/src/reactive/registry.ts +59 -0
- package/src/reactive/set.ts +42 -56
- package/src/reactive/tracking.ts +5 -62
- package/src/reactive/types.ts +79 -19
- package/src/std-decorators.ts +9 -9
- package/src/utils.ts +203 -19
- 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-Mzh1rNsX.esm.js +0 -75
- 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
- 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 -8
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -95
- 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 -885
- 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/effects.ts
CHANGED
|
@@ -2,23 +2,20 @@ 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
9
|
import {
|
|
12
10
|
effectChildren,
|
|
13
11
|
effectParent,
|
|
14
12
|
effectToReactiveObjects,
|
|
15
13
|
getRoot,
|
|
16
|
-
getTrackingDisabled,
|
|
17
14
|
markWithRoot,
|
|
18
|
-
setTrackingDisabled,
|
|
19
15
|
watchers,
|
|
20
|
-
} from './
|
|
16
|
+
} from './registry'
|
|
21
17
|
import {
|
|
18
|
+
cleanup as cleanupSymbol,
|
|
22
19
|
type DependencyAccess,
|
|
23
20
|
type EffectOptions,
|
|
24
21
|
type Evolution,
|
|
@@ -27,8 +24,11 @@ import {
|
|
|
27
24
|
ReactiveErrorCode,
|
|
28
25
|
// type AsyncExecutionMode,
|
|
29
26
|
type ScopedCallback,
|
|
27
|
+
stopped,
|
|
30
28
|
} from './types'
|
|
31
29
|
|
|
30
|
+
import { unwrap } from './proxy-state'
|
|
31
|
+
|
|
32
32
|
/**
|
|
33
33
|
* Finds a cycle in a sequence of functions by looking for the first repetition
|
|
34
34
|
*/
|
|
@@ -55,11 +55,8 @@ function formatRoots(roots: Function[], limit = 20): string {
|
|
|
55
55
|
return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
import { ensureZoneHooked } from './zone'
|
|
59
|
-
|
|
60
58
|
type EffectTracking = (obj: any, evolution: Evolution, prop: any) => void
|
|
61
59
|
|
|
62
|
-
export { captureEffectStack, withEffectStack, getActiveEffect, effectStack }
|
|
63
60
|
export interface ActivationRecord {
|
|
64
61
|
effect: ScopedCallback
|
|
65
62
|
obj: any
|
|
@@ -207,6 +204,7 @@ function getOrCreateClosure(
|
|
|
207
204
|
* @param targetRoot - Root function of the effect being triggered
|
|
208
205
|
*/
|
|
209
206
|
function addGraphEdge(callerRoot: Function, targetRoot: Function) {
|
|
207
|
+
if (options.cycleHandling === 'none') return
|
|
210
208
|
// Skip if edge already exists
|
|
211
209
|
const triggers = effectTriggers.get(callerRoot)
|
|
212
210
|
if (triggers?.has(targetRoot)) {
|
|
@@ -297,6 +295,7 @@ function addGraphEdge(callerRoot: Function, targetRoot: Function) {
|
|
|
297
295
|
* @param end - Target node
|
|
298
296
|
* @param exclude - Node to exclude from the path
|
|
299
297
|
* @returns true if a path exists without going through the excluded node
|
|
298
|
+
* @todo Can be REALLY costly - optimise or make optional or ...
|
|
300
299
|
*/
|
|
301
300
|
function hasPathExcluding(start: Function, end: Function, exclude: Function): boolean {
|
|
302
301
|
if (start === end) return true
|
|
@@ -331,6 +330,7 @@ function hasPathExcluding(start: Function, end: Function, exclude: Function): bo
|
|
|
331
330
|
* @param effect - The effect being cleaned up
|
|
332
331
|
*/
|
|
333
332
|
function cleanupEffectFromGraph(effect: ScopedCallback) {
|
|
333
|
+
if (options.cycleHandling === 'none') return
|
|
334
334
|
const root = getRoot(effect)
|
|
335
335
|
|
|
336
336
|
// Get closures before removing direct edges (needed for propagation)
|
|
@@ -453,6 +453,7 @@ const batchCleanups = new Set<ScopedCallback>()
|
|
|
453
453
|
* Called once when batch starts or when new effects are added
|
|
454
454
|
*/
|
|
455
455
|
function computeAllInDegrees(batch: BatchQueue): void {
|
|
456
|
+
if (options.cycleHandling === 'none') return
|
|
456
457
|
const activeEffect = getActiveEffect()
|
|
457
458
|
const activeRoot = activeEffect ? getRoot(activeEffect) : null
|
|
458
459
|
|
|
@@ -592,6 +593,10 @@ function getCyclePathForEdge(callerRoot: Function, targetRoot: Function): Functi
|
|
|
592
593
|
* Checks if adding an edge would create a cycle
|
|
593
594
|
* Uses causesClosure to check if callerRoot is already a cause of targetRoot
|
|
594
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
|
+
*
|
|
595
600
|
* @param callerRoot - Root of the effect that triggers
|
|
596
601
|
* @param targetRoot - Root of the effect being triggered
|
|
597
602
|
* @returns true if adding this edge would create a cycle
|
|
@@ -621,16 +626,25 @@ function wouldCreateCycle(callerRoot: Function, targetRoot: Function): boolean {
|
|
|
621
626
|
* @param immediate - If true, don't create edges in the dependency graph
|
|
622
627
|
*/
|
|
623
628
|
function addToBatch(effect: ScopedCallback, caller?: ScopedCallback, immediate?: boolean) {
|
|
629
|
+
(effect as any)[cleanupSymbol]?.()
|
|
630
|
+
// If the effect was stopped during cleanup (e.g. lazy memoization), don't add it to the batch
|
|
631
|
+
if ((effect as any)[stopped]) return
|
|
632
|
+
|
|
624
633
|
if (!batchQueue) return
|
|
625
634
|
|
|
626
635
|
const root = getRoot(effect)
|
|
627
636
|
|
|
628
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
|
+
|
|
629
643
|
batchQueue.all.set(root, effect)
|
|
630
644
|
|
|
631
645
|
// 2. Add to global graph (if caller exists and not immediate) - USE ROOTS ONLY
|
|
632
646
|
// When immediate is true, don't create edges - the effect is not considered as a consequence
|
|
633
|
-
if (caller && !immediate) {
|
|
647
|
+
if (caller && !immediate && options.cycleHandling !== 'none') {
|
|
634
648
|
const callerRoot = getRoot(caller)
|
|
635
649
|
|
|
636
650
|
// Check for cycle BEFORE adding edge
|
|
@@ -797,14 +811,22 @@ function executeNext(effectuatedRoots: Function[]): any {
|
|
|
797
811
|
let nextEffect: ScopedCallback | null = null
|
|
798
812
|
let nextRoot: Function | null = null
|
|
799
813
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
+
}
|
|
808
830
|
}
|
|
809
831
|
}
|
|
810
832
|
|
|
@@ -897,9 +919,8 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
897
919
|
// Nested batch - add to existing
|
|
898
920
|
options?.chain(roots, getRoot(getActiveEffect()))
|
|
899
921
|
const caller = getActiveEffect()
|
|
900
|
-
for (let i = 0; i < effect.length; i++)
|
|
922
|
+
for (let i = 0; i < effect.length; i++)
|
|
901
923
|
addToBatch(effect[i], caller, immediate === 'immediate')
|
|
902
|
-
}
|
|
903
924
|
if (immediate) {
|
|
904
925
|
const firstReturn: { value?: any } = {}
|
|
905
926
|
// Execute immediately (before batch returns)
|
|
@@ -925,30 +946,30 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
925
946
|
inDegrees: new Map(),
|
|
926
947
|
}
|
|
927
948
|
|
|
928
|
-
// Add initial effects
|
|
929
949
|
const caller = getActiveEffect()
|
|
930
|
-
for (let i = 0; i < effect.length; i++) {
|
|
931
|
-
addToBatch(effect[i], caller, immediate === 'immediate')
|
|
932
|
-
}
|
|
933
|
-
|
|
934
950
|
const effectuatedRoots: ScopedCallback[] = []
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
951
|
+
const firstReturn: { value?: any } = {}
|
|
952
|
+
|
|
953
|
+
try {
|
|
954
|
+
if (immediate) {
|
|
955
|
+
// Execute initial effects in providing order
|
|
940
956
|
for (let i = 0; i < effect.length; i++) {
|
|
941
957
|
try {
|
|
942
958
|
const rv = effect[i]()
|
|
943
959
|
if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
|
|
944
960
|
} finally {
|
|
945
|
-
|
|
946
|
-
batchQueue.all.delete(root)
|
|
961
|
+
batchQueue.all.delete(getRoot(effect[i]))
|
|
947
962
|
}
|
|
948
963
|
}
|
|
949
|
-
|
|
950
|
-
//
|
|
951
|
-
|
|
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) {
|
|
952
973
|
if (effectuatedRoots.length > options.maxEffectChain) {
|
|
953
974
|
const cycle = findCycleInChain(effectuatedRoots as any)
|
|
954
975
|
const trace = formatRoots(effectuatedRoots as any)
|
|
@@ -988,118 +1009,55 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
988
1009
|
break
|
|
989
1010
|
}
|
|
990
1011
|
}
|
|
991
|
-
if (!batchQueue || batchQueue.all.size === 0) break
|
|
992
1012
|
const rv = executeNext(effectuatedRoots)
|
|
993
|
-
// If executeNext() returned null but batch is not empty, it means a cycle was detected
|
|
994
|
-
// and an error was thrown, so we won't reach here
|
|
995
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
|
|
996
1022
|
}
|
|
997
|
-
const cleanups = Array.from(batchCleanups)
|
|
998
|
-
batchCleanups.clear()
|
|
999
|
-
for (const cleanup of cleanups) cleanup()
|
|
1000
|
-
return firstReturn.value
|
|
1001
|
-
} finally {
|
|
1002
|
-
activationRegistry = undefined
|
|
1003
|
-
batchQueue = undefined
|
|
1004
|
-
options.endChain()
|
|
1005
|
-
}
|
|
1006
|
-
} else {
|
|
1007
|
-
// Execute in dependency order
|
|
1008
|
-
const firstReturn: { value?: any } = {}
|
|
1009
|
-
try {
|
|
1010
|
-
// Outer loop: continue while there are effects OR cleanups pending.
|
|
1011
|
-
// This ensures effects triggered by cleanups are not lost.
|
|
1012
|
-
while (batchQueue.all.size > 0 || batchCleanups.size > 0) {
|
|
1013
|
-
// Inner loop: execute all pending effects
|
|
1014
|
-
while (batchQueue.all.size > 0) {
|
|
1015
|
-
if (effectuatedRoots.length > options.maxEffectChain) {
|
|
1016
|
-
const cycle = findCycleInChain(effectuatedRoots as any)
|
|
1017
|
-
const trace = formatRoots(effectuatedRoots as any)
|
|
1018
|
-
const message = cycle
|
|
1019
|
-
? `Max effect chain reached (cycle detected: ${formatRoots(cycle)})`
|
|
1020
|
-
: `Max effect chain reached (trace: ${trace})`
|
|
1021
|
-
|
|
1022
|
-
const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : []
|
|
1023
|
-
const queued = queuedRoots.map((r) => r.name || '<anonymous>')
|
|
1024
|
-
const debugInfo = {
|
|
1025
|
-
code: ReactiveErrorCode.MaxDepthExceeded,
|
|
1026
|
-
effectuatedRoots,
|
|
1027
|
-
cycle,
|
|
1028
|
-
trace,
|
|
1029
|
-
maxEffectChain: options.maxEffectChain,
|
|
1030
|
-
queued: queued.slice(0, 50),
|
|
1031
|
-
queuedCount: queued.length,
|
|
1032
|
-
// Try to get causation for the last effect
|
|
1033
|
-
causalChain:
|
|
1034
|
-
effectuatedRoots.length > 0
|
|
1035
|
-
? getTriggerChain(
|
|
1036
|
-
batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1])!
|
|
1037
|
-
)
|
|
1038
|
-
: [],
|
|
1039
|
-
}
|
|
1040
|
-
switch (options.maxEffectReaction) {
|
|
1041
|
-
case 'throw':
|
|
1042
|
-
throw new ReactiveError(`[reactive] ${message}`, debugInfo)
|
|
1043
|
-
case 'debug':
|
|
1044
|
-
// biome-ignore lint/suspicious/noDebugger: This is the whole point here
|
|
1045
|
-
debugger
|
|
1046
|
-
throw new ReactiveError(`[reactive] ${message}`, debugInfo)
|
|
1047
|
-
case 'warn':
|
|
1048
|
-
options.warn(
|
|
1049
|
-
`[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`
|
|
1050
|
-
)
|
|
1051
|
-
break
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
const rv = executeNext(effectuatedRoots)
|
|
1055
|
-
// executeNext() returns null when batch is complete or cycle detected (throws error)
|
|
1056
|
-
// But functions can legitimately return null, so we check batchQueue.all.size instead
|
|
1057
|
-
if (batchQueue.all.size === 0) {
|
|
1058
|
-
// Batch complete
|
|
1059
|
-
break
|
|
1060
|
-
}
|
|
1061
|
-
// If executeNext() returned null but batch is not empty, it means a cycle was detected
|
|
1062
|
-
// and an error was thrown, so we won't reach here
|
|
1063
|
-
if (rv !== undefined && !('value' in firstReturn)) firstReturn.value = rv
|
|
1064
|
-
// Note: executeNext() already removed it from batchQueue, so we track by count
|
|
1065
|
-
}
|
|
1066
|
-
// Process cleanups. If they trigger new effects, the outer loop will catch them.
|
|
1067
|
-
if (batchCleanups.size > 0) {
|
|
1068
|
-
const cleanups = Array.from(batchCleanups)
|
|
1069
|
-
batchCleanups.clear()
|
|
1070
|
-
for (const cleanup of cleanups) cleanup()
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
return firstReturn.value
|
|
1074
|
-
} finally {
|
|
1075
|
-
activationRegistry = undefined
|
|
1076
|
-
batchQueue = undefined
|
|
1077
|
-
options.endChain()
|
|
1078
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()
|
|
1079
1033
|
}
|
|
1080
1034
|
}
|
|
1081
1035
|
}
|
|
1082
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
|
+
|
|
1083
1041
|
/**
|
|
1084
1042
|
* Decorator that makes methods atomic - batches all effects triggered within the method
|
|
1085
1043
|
*/
|
|
1086
1044
|
export const atomic = decorator({
|
|
1087
1045
|
method(original) {
|
|
1088
|
-
return function (...args: any[]) {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
)
|
|
1046
|
+
return function (this: any, ...args: any[]) {
|
|
1047
|
+
const atomicEffect = () => original.apply(this, args)
|
|
1048
|
+
// Debug: helpful to have a name
|
|
1049
|
+
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` })
|
|
1050
|
+
return batch(atomicEffect, 'immediate')
|
|
1093
1051
|
}
|
|
1094
1052
|
},
|
|
1095
1053
|
default<Args extends any[], Return>(
|
|
1096
1054
|
original: (...args: Args) => Return
|
|
1097
1055
|
): (...args: Args) => Return {
|
|
1098
1056
|
return function (this: any, ...args: Args) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
)
|
|
1057
|
+
const atomicEffect = () => original.apply(this, args)
|
|
1058
|
+
// Debug: helpful to have a name
|
|
1059
|
+
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` })
|
|
1060
|
+
return batch(atomicEffect, 'immediate')
|
|
1103
1061
|
}
|
|
1104
1062
|
},
|
|
1105
1063
|
})
|
|
@@ -1120,10 +1078,10 @@ export function effect(
|
|
|
1120
1078
|
//biome-ignore lint/suspicious/noConfusingVoidType: We have to
|
|
1121
1079
|
fn: (access: DependencyAccess) => ScopedCallback | undefined | void | Promise<any>,
|
|
1122
1080
|
effectOptions?: EffectOptions
|
|
1123
|
-
): ScopedCallback {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1081
|
+
): ScopedCallback & {
|
|
1082
|
+
[stopped]: boolean
|
|
1083
|
+
[cleanupSymbol]: () => void
|
|
1084
|
+
} {
|
|
1127
1085
|
|
|
1128
1086
|
// Use per-effect asyncMode or fall back to global option
|
|
1129
1087
|
const asyncMode = effectOptions?.asyncMode ?? options.asyncMode ?? 'cancel'
|
|
@@ -1136,10 +1094,13 @@ export function effect(
|
|
|
1136
1094
|
}
|
|
1137
1095
|
}
|
|
1138
1096
|
let cleanup: (() => void) | null = null
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
const
|
|
1142
|
-
|
|
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
|
+
}*/
|
|
1143
1104
|
let effectStopped = false
|
|
1144
1105
|
let hasReacted = false
|
|
1145
1106
|
let runningPromise: Promise<any> | null = null
|
|
@@ -1150,7 +1111,7 @@ export function effect(
|
|
|
1150
1111
|
if (cleanup) {
|
|
1151
1112
|
const prevCleanup = cleanup
|
|
1152
1113
|
cleanup = null
|
|
1153
|
-
|
|
1114
|
+
untracked(() => prevCleanup())
|
|
1154
1115
|
}
|
|
1155
1116
|
|
|
1156
1117
|
// Handle async modes when effect is retriggered
|
|
@@ -1174,7 +1135,7 @@ export function effect(
|
|
|
1174
1135
|
let reactionCleanup: ScopedCallback | undefined
|
|
1175
1136
|
let result: any
|
|
1176
1137
|
try {
|
|
1177
|
-
result =
|
|
1138
|
+
result = tracked(() => fn({ tracked, ascend, reaction: hasReacted }))
|
|
1178
1139
|
if (
|
|
1179
1140
|
result &&
|
|
1180
1141
|
typeof result !== 'function' &&
|
|
@@ -1250,6 +1211,28 @@ export function effect(
|
|
|
1250
1211
|
}
|
|
1251
1212
|
// Mark the runEffect callback with the original function as its root
|
|
1252
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)
|
|
1253
1236
|
|
|
1254
1237
|
// Register strict mode if enabled
|
|
1255
1238
|
if (effectOptions?.opaque) {
|
|
@@ -1262,7 +1245,6 @@ export function effect(
|
|
|
1262
1245
|
|
|
1263
1246
|
batch(runEffect, 'immediate')
|
|
1264
1247
|
|
|
1265
|
-
const parent = parentsForAscend[0]
|
|
1266
1248
|
// Store parent relationship for hierarchy traversal
|
|
1267
1249
|
effectParent.set(runEffect, parent)
|
|
1268
1250
|
// Only ROOT effects are registered for GC cleanup and zone tracking
|
|
@@ -1283,7 +1265,7 @@ export function effect(
|
|
|
1283
1265
|
fr.unregister(stopEffect)
|
|
1284
1266
|
}
|
|
1285
1267
|
if (isRootEffect) {
|
|
1286
|
-
const callIfCollected = () => stopEffect()
|
|
1268
|
+
const callIfCollected = augmentedRv(() => stopEffect())
|
|
1287
1269
|
fr.register(
|
|
1288
1270
|
callIfCollected,
|
|
1289
1271
|
() => {
|
|
@@ -1300,15 +1282,16 @@ export function effect(
|
|
|
1300
1282
|
children = new Set()
|
|
1301
1283
|
effectChildren.set(parent, children)
|
|
1302
1284
|
}
|
|
1303
|
-
const subEffectCleanup = ()
|
|
1285
|
+
const subEffectCleanup = augmentedRv(() => {
|
|
1304
1286
|
children.delete(subEffectCleanup)
|
|
1305
1287
|
if (children.size === 0) {
|
|
1306
1288
|
effectChildren.delete(parent)
|
|
1307
1289
|
}
|
|
1308
1290
|
// Execute this child effect cleanup (which triggers its own mainCleanup)
|
|
1309
1291
|
stopEffect()
|
|
1310
|
-
}
|
|
1292
|
+
})
|
|
1311
1293
|
children.add(subEffectCleanup)
|
|
1294
|
+
|
|
1312
1295
|
return subEffectCleanup
|
|
1313
1296
|
}
|
|
1314
1297
|
|
|
@@ -1318,17 +1301,7 @@ export function effect(
|
|
|
1318
1301
|
* @param fn - The function to execute
|
|
1319
1302
|
*/
|
|
1320
1303
|
export function untracked<T>(fn: () => T): T {
|
|
1321
|
-
|
|
1322
|
-
// This prevents the parent effect from tracking dependencies during fn execution
|
|
1323
|
-
const wasTrackingDisabled = getTrackingDisabled()
|
|
1324
|
-
setTrackingDisabled(true)
|
|
1325
|
-
|
|
1326
|
-
try {
|
|
1327
|
-
return fn()
|
|
1328
|
-
} finally {
|
|
1329
|
-
// Restore tracking state
|
|
1330
|
-
setTrackingDisabled(wasTrackingDisabled)
|
|
1331
|
-
}
|
|
1304
|
+
return effectHistory.present.root(fn)
|
|
1332
1305
|
}
|
|
1333
1306
|
|
|
1334
1307
|
/**
|
|
@@ -1337,11 +1310,7 @@ export function untracked<T>(fn: () => T): T {
|
|
|
1337
1310
|
* @param fn - The function to execute
|
|
1338
1311
|
*/
|
|
1339
1312
|
export function root<T>(fn: () => T): T {
|
|
1340
|
-
|
|
1341
|
-
withEffect(undefined, () => {
|
|
1342
|
-
rv = fn()
|
|
1343
|
-
})
|
|
1344
|
-
return rv
|
|
1313
|
+
return effectHistory.root(fn)
|
|
1345
1314
|
}
|
|
1346
1315
|
|
|
1347
1316
|
export { effectTrackers }
|
|
@@ -1397,18 +1366,17 @@ export function biDi<T>(
|
|
|
1397
1366
|
set = get.set
|
|
1398
1367
|
get = get.get
|
|
1399
1368
|
}
|
|
1400
|
-
|
|
1369
|
+
let programatticallySetValue: any = Symbol()
|
|
1401
1370
|
effect(
|
|
1402
1371
|
markWithRoot(() => {
|
|
1403
|
-
|
|
1404
|
-
|
|
1372
|
+
const newValue = get()
|
|
1373
|
+
if (unwrap(newValue) !== programatticallySetValue) received(newValue)
|
|
1374
|
+
}, received)
|
|
1405
1375
|
)
|
|
1406
|
-
return
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
})
|
|
1376
|
+
return set
|
|
1377
|
+
? atomic((value: T) => {
|
|
1378
|
+
programatticallySetValue = unwrap(value)
|
|
1379
|
+
set(value)
|
|
1380
|
+
})
|
|
1381
|
+
: () => {}
|
|
1414
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'
|
|
49
|
+
import { nonReactiveObjects } from './non-reactive-state'
|
|
50
|
+
import { metaProtos, objectToProxy, proxyToObject } from './proxy'
|
|
51
|
+
import { effectToReactiveObjects, watchers } from './registry'
|
|
54
52
|
import { ReactiveSet, ReactiveWeakSet } from './set'
|
|
55
|
-
import { effectToReactiveObjects, watchers } from './tracking'
|
|
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
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { decorator, type GenericClassDecorator } from '../decorator'
|
|
2
2
|
import { deepWatch } from './deep-watch'
|
|
3
|
-
import {
|
|
4
|
-
import { effect, getActiveEffect, untracked } from './effects'
|
|
3
|
+
import { effect, untracked } from './effects'
|
|
5
4
|
import { isNonReactive, nonReactiveClass, nonReactiveObjects } from './non-reactive-state'
|
|
6
5
|
import { unwrap } from './proxy-state'
|
|
7
|
-
import {
|
|
6
|
+
import { markWithRoot } from './registry'
|
|
7
|
+
import { dependant } from './tracking'
|
|
8
8
|
import {
|
|
9
9
|
type DependencyAccess,
|
|
10
10
|
nonReactiveMark,
|
|
@@ -86,15 +86,12 @@ function watchObject(
|
|
|
86
86
|
changed: (value: object) => void,
|
|
87
87
|
{ immediate = false, deep = false } = {}
|
|
88
88
|
): ScopedCallback {
|
|
89
|
-
const myParentEffect = getActiveEffect()
|
|
90
89
|
if (deep) return deepWatch(value, changed, { immediate })!
|
|
91
|
-
return effect(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}, changed)
|
|
97
|
-
)
|
|
90
|
+
return effect(function watchObjectEffect() {
|
|
91
|
+
dependant(value)
|
|
92
|
+
if (immediate) changed(value)
|
|
93
|
+
immediate = true
|
|
94
|
+
})
|
|
98
95
|
}
|
|
99
96
|
|
|
100
97
|
function watchCallBack<T>(
|
|
@@ -102,29 +99,20 @@ function watchCallBack<T>(
|
|
|
102
99
|
changed: (value: T, oldValue?: T) => void,
|
|
103
100
|
{ immediate = false, deep = false } = {}
|
|
104
101
|
): ScopedCallback {
|
|
105
|
-
const myParentEffect = getActiveEffect()
|
|
106
102
|
let oldValue: T | typeof unsetYet = unsetYet
|
|
107
103
|
let deepCleanup: ScopedCallback | undefined
|
|
108
104
|
const cbCleanup = effect(
|
|
109
105
|
markWithRoot(function watchCallBackEffect(access) {
|
|
110
106
|
const newValue = value(access)
|
|
111
107
|
if (oldValue !== newValue)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (deepCleanup) deepCleanup()
|
|
121
|
-
deepCleanup = deepWatch(
|
|
122
|
-
newValue as object,
|
|
123
|
-
markWithRoot((value) => changed(value as T, value as T), changed)
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
}, changed)
|
|
127
|
-
)
|
|
108
|
+
if (oldValue === unsetYet) {
|
|
109
|
+
if (immediate) changed(newValue)
|
|
110
|
+
} else changed(newValue, oldValue)
|
|
111
|
+
oldValue = newValue
|
|
112
|
+
if (deep) {
|
|
113
|
+
if (deepCleanup) deepCleanup()
|
|
114
|
+
deepCleanup = deepWatch(newValue as object, (value) => changed(value as T, value as T))
|
|
115
|
+
}
|
|
128
116
|
}, value)
|
|
129
117
|
)
|
|
130
118
|
return () => {
|
|
@@ -153,6 +141,7 @@ function deepNonReactive<T>(obj: T): T {
|
|
|
153
141
|
})
|
|
154
142
|
} catch {}
|
|
155
143
|
if (!(nonReactiveMark in (obj as object))) nonReactiveObjects.add(obj as object)
|
|
144
|
+
// Finally, not deep
|
|
156
145
|
//for (const key in obj) deepNonReactive(obj[key])
|
|
157
146
|
return obj
|
|
158
147
|
}
|
|
@@ -213,11 +202,9 @@ export function derived<T>(compute: (dep: DependencyAccess) => T): {
|
|
|
213
202
|
return cleanedBy(
|
|
214
203
|
rv,
|
|
215
204
|
untracked(() =>
|
|
216
|
-
effect(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}, compute)
|
|
220
|
-
)
|
|
205
|
+
effect(function derivedEffect(access) {
|
|
206
|
+
rv.value = compute(access)
|
|
207
|
+
})
|
|
221
208
|
)
|
|
222
209
|
)
|
|
223
210
|
}
|