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/change.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { recordTriggerLink } from './debug'
|
|
2
2
|
import { bubbleUpChange, objectsWithDeepWatchers } from './deep-watch-state'
|
|
3
3
|
import { getActiveEffect, isRunning } from './effect-context'
|
|
4
|
-
import { batch, effectTrackers, opaqueEffects } from './effects'
|
|
4
|
+
import { batch, effectTrackers, hasBatched, opaqueEffects, recordActivation } from './effects'
|
|
5
5
|
import { unwrap } from './proxy-state'
|
|
6
|
-
import { watchers } from './
|
|
6
|
+
import { watchers } from './registry'
|
|
7
7
|
import { allProps, type Evolution, options, type ScopedCallback, type State } from './types'
|
|
8
8
|
|
|
9
9
|
const states = new WeakMap<object, State>()
|
|
@@ -49,7 +49,10 @@ export function collectEffects(
|
|
|
49
49
|
options.skipRunningEffect(effect, runningChain as any)
|
|
50
50
|
continue
|
|
51
51
|
}
|
|
52
|
-
effects.
|
|
52
|
+
if (!effects.has(effect)) {
|
|
53
|
+
effects.add(effect)
|
|
54
|
+
if (!hasBatched(effect)) recordActivation(effect, obj, evolution, key)
|
|
55
|
+
}
|
|
53
56
|
const trackers = effectTrackers.get(effect)
|
|
54
57
|
recordTriggerLink(sourceEffect, effect, obj, key, evolution)
|
|
55
58
|
if (trackers) {
|
|
@@ -119,6 +122,7 @@ export function touchedOpaque(obj: any, evolution: Evolution, prop: any) {
|
|
|
119
122
|
continue
|
|
120
123
|
}
|
|
121
124
|
effects.add(effect)
|
|
125
|
+
recordActivation(effect, obj, evolution, prop)
|
|
122
126
|
const trackers = effectTrackers.get(effect)
|
|
123
127
|
recordTriggerLink(sourceEffect, effect, obj, prop, evolution)
|
|
124
128
|
if (trackers) {
|
package/src/reactive/debug.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Provides graph data for tooling (DevTools panel, etc.)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { effectParent, effectToReactiveObjects, getRoot } from './
|
|
8
|
+
import { effectParent, effectToReactiveObjects, getRoot } from './registry'
|
|
9
9
|
import { allProps, type Evolution, options, type ScopedCallback } from './types'
|
|
10
10
|
|
|
11
11
|
const EXTERNAL_SOURCE = Symbol('external-source')
|
|
@@ -3,7 +3,7 @@ import { bubbleUpChange, objectsWithDeepWatchers } from './deep-watch-state'
|
|
|
3
3
|
import { batch } from './effects'
|
|
4
4
|
import { isNonReactive } from './non-reactive-state'
|
|
5
5
|
import { unwrap } from './proxy-state'
|
|
6
|
-
import { effectParent, watchers } from './
|
|
6
|
+
import { effectParent, watchers } from './registry'
|
|
7
7
|
import { allProps, type Evolution, options, type ScopedCallback } from './types'
|
|
8
8
|
|
|
9
9
|
function isObject(value: any): value is object {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { effect } from './effects'
|
|
7
7
|
import { isNonReactive } from './non-reactive-state'
|
|
8
8
|
import { reactive, unwrap } from './proxy'
|
|
9
|
-
import { markWithRoot } from './
|
|
9
|
+
import { markWithRoot } from './registry'
|
|
10
10
|
import { options, type ScopedCallback } from './types'
|
|
11
11
|
|
|
12
12
|
function isObject(value: any): value is object {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { effectParent, getRoot } from './
|
|
1
|
+
import { effectParent, getRoot } from './registry'
|
|
2
2
|
import { ReactiveError, type ScopedCallback } from './types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -77,7 +77,7 @@ export function getActiveEffect() {
|
|
|
77
77
|
* @returns The result of the function
|
|
78
78
|
*/
|
|
79
79
|
export function withEffect<T>(effect: ScopedCallback | undefined, fn: () => T): T {
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
if (getRoot(effect) === getRoot(getActiveEffect())) return fn()
|
|
82
82
|
stack.unshift(effect)
|
|
83
83
|
try {
|
package/src/reactive/effects.ts
CHANGED
|
@@ -8,16 +8,18 @@ import {
|
|
|
8
8
|
withEffect,
|
|
9
9
|
withEffectStack,
|
|
10
10
|
} from './effect-context'
|
|
11
|
+
import {
|
|
12
|
+
getTrackingDisabled,
|
|
13
|
+
setTrackingDisabled,
|
|
14
|
+
} from './tracking'
|
|
11
15
|
import {
|
|
12
16
|
effectChildren,
|
|
13
17
|
effectParent,
|
|
14
18
|
effectToReactiveObjects,
|
|
15
19
|
getRoot,
|
|
16
|
-
getTrackingDisabled,
|
|
17
20
|
markWithRoot,
|
|
18
|
-
setTrackingDisabled,
|
|
19
21
|
watchers,
|
|
20
|
-
} from './
|
|
22
|
+
} from './registry'
|
|
21
23
|
import {
|
|
22
24
|
type DependencyAccess,
|
|
23
25
|
type EffectOptions,
|
|
@@ -27,6 +29,8 @@ import {
|
|
|
27
29
|
ReactiveErrorCode,
|
|
28
30
|
// type AsyncExecutionMode,
|
|
29
31
|
type ScopedCallback,
|
|
32
|
+
cleanup as cleanupSymbol,
|
|
33
|
+
stopped,
|
|
30
34
|
} from './types'
|
|
31
35
|
|
|
32
36
|
/**
|
|
@@ -60,6 +64,69 @@ import { ensureZoneHooked } from './zone'
|
|
|
60
64
|
type EffectTracking = (obj: any, evolution: Evolution, prop: any) => void
|
|
61
65
|
|
|
62
66
|
export { captureEffectStack, withEffectStack, getActiveEffect, effectStack }
|
|
67
|
+
export interface ActivationRecord {
|
|
68
|
+
effect: ScopedCallback
|
|
69
|
+
obj: any
|
|
70
|
+
evolution: Evolution
|
|
71
|
+
prop: any
|
|
72
|
+
batchId: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Nested map structure for efficient counting and batch cleanup
|
|
76
|
+
// batchId -> effect root -> obj -> prop -> count
|
|
77
|
+
let activationRegistry: Map<Function, Map<any, Map<any, number>>> | undefined
|
|
78
|
+
|
|
79
|
+
export const activationLog: Omit<ActivationRecord, 'batchId'>[] = new Array(100)
|
|
80
|
+
|
|
81
|
+
export function getActivationLog() {
|
|
82
|
+
return activationLog
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function recordActivation(
|
|
86
|
+
effect: ScopedCallback,
|
|
87
|
+
obj: any,
|
|
88
|
+
evolution: Evolution,
|
|
89
|
+
prop: any
|
|
90
|
+
) {
|
|
91
|
+
const root = getRoot(effect)
|
|
92
|
+
|
|
93
|
+
if (!activationRegistry) return
|
|
94
|
+
let effectData = activationRegistry.get(root)
|
|
95
|
+
if (!effectData) {
|
|
96
|
+
effectData = new Map()
|
|
97
|
+
activationRegistry.set(root, effectData)
|
|
98
|
+
}
|
|
99
|
+
let objData = effectData.get(obj)
|
|
100
|
+
if (!objData) {
|
|
101
|
+
objData = new Map()
|
|
102
|
+
effectData.set(obj, objData)
|
|
103
|
+
}
|
|
104
|
+
const count = (objData.get(prop) ?? 0) + 1
|
|
105
|
+
objData.set(prop, count)
|
|
106
|
+
|
|
107
|
+
// Keep a limited history for diagnostics
|
|
108
|
+
activationLog.unshift({
|
|
109
|
+
effect,
|
|
110
|
+
obj,
|
|
111
|
+
evolution,
|
|
112
|
+
prop,
|
|
113
|
+
})
|
|
114
|
+
activationLog.pop()
|
|
115
|
+
|
|
116
|
+
if (count >= options.maxTriggerPerBatch) {
|
|
117
|
+
const effectName = (root as any)?.name || 'anonymous'
|
|
118
|
+
const message = `Aggressive trigger detected: effect "${effectName}" triggered ${count} times in the batch by the same cause.`
|
|
119
|
+
if (options.maxEffectReaction === 'throw') {
|
|
120
|
+
throw new ReactiveError(message, {
|
|
121
|
+
code: ReactiveErrorCode.MaxReactionExceeded,
|
|
122
|
+
count,
|
|
123
|
+
effect: effectName,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
options.warn(`[reactive] ${message}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
63
130
|
/**
|
|
64
131
|
* Registers a debug callback that is called when the current effect is triggered by a dependency change
|
|
65
132
|
*
|
|
@@ -234,6 +301,7 @@ function addGraphEdge(callerRoot: Function, targetRoot: Function) {
|
|
|
234
301
|
* @param end - Target node
|
|
235
302
|
* @param exclude - Node to exclude from the path
|
|
236
303
|
* @returns true if a path exists without going through the excluded node
|
|
304
|
+
* @todo Can be REALLY costly - optimise or make optional or ...
|
|
237
305
|
*/
|
|
238
306
|
function hasPathExcluding(start: Function, end: Function, exclude: Function): boolean {
|
|
239
307
|
if (start === end) return true
|
|
@@ -380,6 +448,9 @@ interface BatchQueue {
|
|
|
380
448
|
// Track currently executing effects to prevent re-execution
|
|
381
449
|
// These are all the effects triggered under `activeEffect`
|
|
382
450
|
let batchQueue: BatchQueue | undefined
|
|
451
|
+
export function hasBatched(effect: ScopedCallback) {
|
|
452
|
+
return batchQueue?.all.has(getRoot(effect))
|
|
453
|
+
}
|
|
383
454
|
const batchCleanups = new Set<ScopedCallback>()
|
|
384
455
|
|
|
385
456
|
/**
|
|
@@ -555,6 +626,11 @@ function wouldCreateCycle(callerRoot: Function, targetRoot: Function): boolean {
|
|
|
555
626
|
* @param immediate - If true, don't create edges in the dependency graph
|
|
556
627
|
*/
|
|
557
628
|
function addToBatch(effect: ScopedCallback, caller?: ScopedCallback, immediate?: boolean) {
|
|
629
|
+
const cleanupFn = (effect as any)[cleanupSymbol]
|
|
630
|
+
if (cleanupFn) cleanupFn()
|
|
631
|
+
// If the effect was stopped during cleanup (e.g. lazy memoization), don't add it to the batch
|
|
632
|
+
if ((effect as any)[stopped]) return
|
|
633
|
+
|
|
558
634
|
if (!batchQueue) return
|
|
559
635
|
|
|
560
636
|
const root = getRoot(effect)
|
|
@@ -851,6 +927,8 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
851
927
|
// Otherwise, effects will be picked up in next executeNext() call
|
|
852
928
|
} else {
|
|
853
929
|
// New batch - initialize
|
|
930
|
+
if (!activationRegistry) activationRegistry = new Map()
|
|
931
|
+
else throw new Error('Batch already in progress')
|
|
854
932
|
options.beginChain(roots)
|
|
855
933
|
batchQueue = {
|
|
856
934
|
all: new Map(),
|
|
@@ -931,6 +1009,7 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
931
1009
|
for (const cleanup of cleanups) cleanup()
|
|
932
1010
|
return firstReturn.value
|
|
933
1011
|
} finally {
|
|
1012
|
+
activationRegistry = undefined
|
|
934
1013
|
batchQueue = undefined
|
|
935
1014
|
options.endChain()
|
|
936
1015
|
}
|
|
@@ -1003,11 +1082,11 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
1003
1082
|
}
|
|
1004
1083
|
return firstReturn.value
|
|
1005
1084
|
} finally {
|
|
1085
|
+
activationRegistry = undefined
|
|
1006
1086
|
batchQueue = undefined
|
|
1007
1087
|
options.endChain()
|
|
1008
1088
|
}
|
|
1009
1089
|
}
|
|
1010
|
-
|
|
1011
1090
|
}
|
|
1012
1091
|
}
|
|
1013
1092
|
|
|
@@ -1016,21 +1095,21 @@ export function batch(effect: ScopedCallback | ScopedCallback[], immediate?: 'im
|
|
|
1016
1095
|
*/
|
|
1017
1096
|
export const atomic = decorator({
|
|
1018
1097
|
method(original) {
|
|
1019
|
-
return function (...args: any[]) {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
)
|
|
1098
|
+
return function (this: any, ...args: any[]) {
|
|
1099
|
+
const atomicEffect = () => original.apply(this, args)
|
|
1100
|
+
// Debug: helpful to have a name
|
|
1101
|
+
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` })
|
|
1102
|
+
return batch(atomicEffect, 'immediate')
|
|
1024
1103
|
}
|
|
1025
1104
|
},
|
|
1026
1105
|
default<Args extends any[], Return>(
|
|
1027
1106
|
original: (...args: Args) => Return
|
|
1028
1107
|
): (...args: Args) => Return {
|
|
1029
1108
|
return function (this: any, ...args: Args) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
)
|
|
1109
|
+
const atomicEffect = () => original.apply(this, args)
|
|
1110
|
+
// Debug: helpful to have a name
|
|
1111
|
+
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` })
|
|
1112
|
+
return batch(atomicEffect, 'immediate')
|
|
1034
1113
|
}
|
|
1035
1114
|
},
|
|
1036
1115
|
})
|
|
@@ -1069,7 +1148,7 @@ export function effect(
|
|
|
1069
1148
|
let cleanup: (() => void) | null = null
|
|
1070
1149
|
// capture the parent effect at creation time for ascend
|
|
1071
1150
|
const parentsForAscend = captureEffectStack()
|
|
1072
|
-
const tracked =
|
|
1151
|
+
const tracked = <T>(cb: () => T) => withEffect(runEffect, cb)
|
|
1073
1152
|
const ascend = <T>(cb: () => T) => withEffectStack(parentsForAscend, cb)
|
|
1074
1153
|
let effectStopped = false
|
|
1075
1154
|
let hasReacted = false
|
|
@@ -1213,8 +1292,25 @@ export function effect(
|
|
|
1213
1292
|
cleanupEffectFromGraph(runEffect)
|
|
1214
1293
|
fr.unregister(stopEffect)
|
|
1215
1294
|
}
|
|
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
|
+
}
|
|
1216
1312
|
if (isRootEffect) {
|
|
1217
|
-
const callIfCollected = () => stopEffect()
|
|
1313
|
+
const callIfCollected = augmentedRv(() => stopEffect())
|
|
1218
1314
|
fr.register(
|
|
1219
1315
|
callIfCollected,
|
|
1220
1316
|
() => {
|
|
@@ -1231,15 +1327,16 @@ export function effect(
|
|
|
1231
1327
|
children = new Set()
|
|
1232
1328
|
effectChildren.set(parent, children)
|
|
1233
1329
|
}
|
|
1234
|
-
const subEffectCleanup = ()
|
|
1330
|
+
const subEffectCleanup = augmentedRv(() => {
|
|
1235
1331
|
children.delete(subEffectCleanup)
|
|
1236
1332
|
if (children.size === 0) {
|
|
1237
1333
|
effectChildren.delete(parent)
|
|
1238
1334
|
}
|
|
1239
1335
|
// Execute this child effect cleanup (which triggers its own mainCleanup)
|
|
1240
1336
|
stopEffect()
|
|
1241
|
-
}
|
|
1337
|
+
})
|
|
1242
1338
|
children.add(subEffectCleanup)
|
|
1339
|
+
|
|
1243
1340
|
return subEffectCleanup
|
|
1244
1341
|
}
|
|
1245
1342
|
|
package/src/reactive/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
biDi,
|
|
18
18
|
defer,
|
|
19
19
|
effect,
|
|
20
|
+
getActivationLog,
|
|
20
21
|
getActiveEffect,
|
|
21
22
|
root,
|
|
22
23
|
trackEffect,
|
|
@@ -26,7 +27,7 @@ export { cleanedBy, cleanup, derived, unreactive, watch } from './interface'
|
|
|
26
27
|
export { mapped, ReadOnlyError, reduced } from './mapped'
|
|
27
28
|
export { type Memoizable, memoize } from './memoize'
|
|
28
29
|
export { immutables, isNonReactive, registerNativeReactivity } from './non-reactive'
|
|
29
|
-
export { project } from './project'
|
|
30
|
+
export { getActiveProjection, project } from './project'
|
|
30
31
|
export { isReactive, ReactiveBase, reactive, unwrap } from './proxy'
|
|
31
32
|
export { organize, organized } from './record'
|
|
32
33
|
export { Register, register } from './register'
|
|
@@ -51,7 +52,7 @@ import { ReactiveMap, ReactiveWeakMap } from './map'
|
|
|
51
52
|
import { nonReactiveObjects, registerNativeReactivity } from './non-reactive-state'
|
|
52
53
|
import { objectToProxy, proxyToObject } from './proxy'
|
|
53
54
|
import { ReactiveSet, ReactiveWeakSet } from './set'
|
|
54
|
-
import { effectToReactiveObjects, watchers } from './
|
|
55
|
+
import { effectToReactiveObjects, watchers } from './registry'
|
|
55
56
|
|
|
56
57
|
// Register native collection types to use specialized reactive wrappers
|
|
57
58
|
registerNativeReactivity(WeakMap, ReactiveWeakMap)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { decorator, GenericClassDecorator } from '../decorator'
|
|
1
|
+
import { decorator, type GenericClassDecorator } from '../decorator'
|
|
2
2
|
import { deepWatch } from './deep-watch'
|
|
3
3
|
import { withEffect } from './effect-context'
|
|
4
4
|
import { effect, getActiveEffect, untracked } from './effects'
|
|
5
5
|
import { isNonReactive, nonReactiveClass, nonReactiveObjects } from './non-reactive-state'
|
|
6
6
|
import { unwrap } from './proxy-state'
|
|
7
|
-
import { dependant
|
|
7
|
+
import { dependant } from './tracking'
|
|
8
|
+
import { markWithRoot } from './registry'
|
|
8
9
|
import {
|
|
9
10
|
type DependencyAccess,
|
|
10
11
|
nonReactiveMark,
|
|
@@ -89,11 +90,11 @@ function watchObject(
|
|
|
89
90
|
const myParentEffect = getActiveEffect()
|
|
90
91
|
if (deep) return deepWatch(value, changed, { immediate })!
|
|
91
92
|
return effect(
|
|
92
|
-
|
|
93
|
+
function watchObjectEffect() {
|
|
93
94
|
dependant(value)
|
|
94
95
|
if (immediate) withEffect(myParentEffect, () => changed(value))
|
|
95
96
|
immediate = true
|
|
96
|
-
}
|
|
97
|
+
}
|
|
97
98
|
)
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -111,7 +112,7 @@ function watchCallBack<T>(
|
|
|
111
112
|
if (oldValue !== newValue)
|
|
112
113
|
withEffect(
|
|
113
114
|
myParentEffect,
|
|
114
|
-
|
|
115
|
+
() => {
|
|
115
116
|
if (oldValue === unsetYet) {
|
|
116
117
|
if (immediate) changed(newValue)
|
|
117
118
|
} else changed(newValue, oldValue)
|
|
@@ -120,10 +121,10 @@ function watchCallBack<T>(
|
|
|
120
121
|
if (deepCleanup) deepCleanup()
|
|
121
122
|
deepCleanup = deepWatch(
|
|
122
123
|
newValue as object,
|
|
123
|
-
|
|
124
|
+
(value) => changed(value as T, value as T)
|
|
124
125
|
)
|
|
125
126
|
}
|
|
126
|
-
}
|
|
127
|
+
}
|
|
127
128
|
)
|
|
128
129
|
}, value)
|
|
129
130
|
)
|
|
@@ -214,9 +215,9 @@ export function derived<T>(compute: (dep: DependencyAccess) => T): {
|
|
|
214
215
|
rv,
|
|
215
216
|
untracked(() =>
|
|
216
217
|
effect(
|
|
217
|
-
|
|
218
|
+
function derivedEffect(access) {
|
|
218
219
|
rv.value = compute(access)
|
|
219
|
-
}
|
|
220
|
+
}
|
|
220
221
|
)
|
|
221
222
|
)
|
|
222
223
|
)
|
package/src/reactive/map.ts
CHANGED
|
@@ -12,13 +12,13 @@ const native = Symbol('native')
|
|
|
12
12
|
* Only tracks individual key operations, no size tracking (WeakMap limitation)
|
|
13
13
|
*/
|
|
14
14
|
export class ReactiveWeakMap<K extends object, V> {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
readonly [native]!: WeakMap<K, V>
|
|
16
|
+
readonly content!: symbol
|
|
17
17
|
constructor(original: WeakMap<K, V>) {
|
|
18
18
|
Object.defineProperties(this, {
|
|
19
19
|
[native]: { value: original },
|
|
20
20
|
[prototypeForwarding]: { value: original },
|
|
21
|
-
content: { value: Symbol('
|
|
21
|
+
content: { value: Symbol('WeakMapContent') },
|
|
22
22
|
[Symbol.toStringTag]: { value: 'ReactiveWeakMap' },
|
|
23
23
|
})
|
|
24
24
|
}
|
|
@@ -62,14 +62,14 @@ export class ReactiveWeakMap<K extends object, V> {
|
|
|
62
62
|
* Tracks size changes, individual key operations, and collection-wide operations
|
|
63
63
|
*/
|
|
64
64
|
export class ReactiveMap<K, V> {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
readonly [native]!: Map<K, V>
|
|
66
|
+
readonly content!: symbol
|
|
67
67
|
|
|
68
68
|
constructor(original: Map<K, V>) {
|
|
69
69
|
Object.defineProperties(this, {
|
|
70
70
|
[native]: { value: original },
|
|
71
71
|
[prototypeForwarding]: { value: original },
|
|
72
|
-
content: { value: Symbol('
|
|
72
|
+
content: { value: Symbol('MapContent') },
|
|
73
73
|
[Symbol.toStringTag]: { value: 'ReactiveMap' },
|
|
74
74
|
})
|
|
75
75
|
}
|
package/src/reactive/mapped.ts
CHANGED
|
@@ -5,10 +5,10 @@ import { effect, untracked } from './effects'
|
|
|
5
5
|
import { cleanedBy } from './interface'
|
|
6
6
|
import { reactive } from './proxy'
|
|
7
7
|
import { dependant } from './tracking'
|
|
8
|
-
import { prototypeForwarding, ScopedCallback } from './types'
|
|
8
|
+
import { prototypeForwarding, type ScopedCallback } from './types'
|
|
9
9
|
|
|
10
10
|
// TODO: Lazy reactivity ?
|
|
11
|
-
export class ReadOnlyError extends Error {}
|
|
11
|
+
export class ReadOnlyError extends Error { }
|
|
12
12
|
/**
|
|
13
13
|
* Reactive wrapper around JavaScript's Array class with full array method support
|
|
14
14
|
* Tracks length changes, individual index operations, and collection-wide operations
|
|
@@ -29,7 +29,6 @@ class ReactiveReadOnlyArrayClass extends Indexable(ReactiveBaseArray, {
|
|
|
29
29
|
throw new ReadOnlyError(`Setting length to ${value} on a read-only array`)
|
|
30
30
|
},
|
|
31
31
|
}) {
|
|
32
|
-
declare length: number
|
|
33
32
|
constructor(original: any[]) {
|
|
34
33
|
super()
|
|
35
34
|
Object.defineProperties(this, {
|
package/src/reactive/memoize.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { decorator } from '../decorator'
|
|
2
|
-
import { renamed } from '../utils'
|
|
2
|
+
import { deepCompare, renamed } from '../utils'
|
|
3
3
|
import { touched1 } from './change'
|
|
4
|
-
import { effect, root } from './effects'
|
|
5
|
-
import { dependant
|
|
4
|
+
import { effect, root, untracked } from './effects'
|
|
5
|
+
import { dependant } from './tracking'
|
|
6
|
+
import { getRoot, markWithRoot } from './registry'
|
|
7
|
+
import { options, rootFunction } from './types'
|
|
6
8
|
|
|
7
9
|
export type Memoizable = object | any[] | symbol | ((...args: any[]) => any)
|
|
8
10
|
|
|
@@ -12,7 +14,8 @@ type MemoCacheTree<Result> = {
|
|
|
12
14
|
branches?: WeakMap<Memoizable, MemoCacheTree<Result>>
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
const memoizedRegistry = new WeakMap<
|
|
17
|
+
const memoizedRegistry = new WeakMap<any, Function>()
|
|
18
|
+
const wrapperRegistry = new WeakMap<Function, Function>()
|
|
16
19
|
|
|
17
20
|
function getBranch<Result>(tree: MemoCacheTree<Result>, key: Memoizable): MemoCacheTree<Result> {
|
|
18
21
|
tree.branches ??= new WeakMap()
|
|
@@ -38,18 +41,33 @@ function memoizeFunction<Result, Args extends Memoizable[]>(
|
|
|
38
41
|
throw new Error('memoize expects non-null object arguments')
|
|
39
42
|
|
|
40
43
|
let node: MemoCacheTree<Result> = cacheRoot
|
|
44
|
+
// Note: decorators add `this` as first argument
|
|
41
45
|
for (const arg of localArgs) {
|
|
42
46
|
node = getBranch(node, arg)
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
dependant(node, 'memoize')
|
|
46
|
-
if ('result' in node)
|
|
50
|
+
if ('result' in node) {
|
|
51
|
+
if (options.onMemoizationDiscrepancy) {
|
|
52
|
+
const wasVerification = options.isVerificationRun
|
|
53
|
+
options.isVerificationRun = true
|
|
54
|
+
try {
|
|
55
|
+
const fresh = untracked(() => fn(...localArgs))
|
|
56
|
+
if (!deepCompare(node.result, fresh)) {
|
|
57
|
+
options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'calculation')
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
options.isVerificationRun = wasVerification
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return node.result!
|
|
64
|
+
}
|
|
47
65
|
|
|
48
66
|
// Create memoize internal effect to track dependencies and invalidate cache
|
|
49
67
|
// Use untracked to prevent the effect creation from being affected by parent effects
|
|
50
68
|
node.cleanup = root(() =>
|
|
51
69
|
effect(
|
|
52
|
-
|
|
70
|
+
() => {
|
|
53
71
|
// Execute the function and track its dependencies
|
|
54
72
|
// The function execution will automatically track dependencies on reactive objects
|
|
55
73
|
node.result = fn(...localArgs)
|
|
@@ -57,11 +75,31 @@ function memoizeFunction<Result, Args extends Memoizable[]>(
|
|
|
57
75
|
// When dependencies change, clear the cache and notify consumers
|
|
58
76
|
delete node.result
|
|
59
77
|
touched1(node, { type: 'invalidate', prop: localArgs }, 'memoize')
|
|
78
|
+
// Lazy memoization: stop the effect so it doesn't re-run immediately.
|
|
79
|
+
// It will be re-created on next access.
|
|
80
|
+
if (node.cleanup) {
|
|
81
|
+
node.cleanup()
|
|
82
|
+
node.cleanup = undefined
|
|
83
|
+
}
|
|
60
84
|
}
|
|
61
|
-
},
|
|
85
|
+
},
|
|
62
86
|
{ opaque: true }
|
|
63
87
|
)
|
|
64
88
|
)
|
|
89
|
+
|
|
90
|
+
if (options.onMemoizationDiscrepancy) {
|
|
91
|
+
const wasVerification = options.isVerificationRun
|
|
92
|
+
options.isVerificationRun = true
|
|
93
|
+
try {
|
|
94
|
+
const fresh = untracked(() => fn(...localArgs))
|
|
95
|
+
if (!deepCompare(node.result, fresh)) {
|
|
96
|
+
options.onMemoizationDiscrepancy(node.result, fresh, fn, localArgs, 'comparison')
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
options.isVerificationRun = wasVerification
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
65
103
|
return node.result!
|
|
66
104
|
}, fn)
|
|
67
105
|
|
|
@@ -71,35 +109,43 @@ function memoizeFunction<Result, Args extends Memoizable[]>(
|
|
|
71
109
|
}
|
|
72
110
|
|
|
73
111
|
export const memoize = decorator({
|
|
74
|
-
getter(original, propertyKey) {
|
|
75
|
-
const memoized = memoizeFunction(
|
|
76
|
-
markWithRoot(
|
|
77
|
-
renamed(
|
|
78
|
-
(that: object) => {
|
|
79
|
-
return original.call(that)
|
|
80
|
-
},
|
|
81
|
-
`${String(this.constructor.name)}.${String(propertyKey)}`
|
|
82
|
-
),
|
|
83
|
-
original
|
|
84
|
-
)
|
|
85
|
-
)
|
|
112
|
+
getter(original, target, propertyKey) {
|
|
86
113
|
return function (this: any) {
|
|
114
|
+
let wrapper = wrapperRegistry.get(original)
|
|
115
|
+
if (!wrapper) {
|
|
116
|
+
wrapper = markWithRoot(
|
|
117
|
+
renamed((that: object) => {
|
|
118
|
+
return original.call(that)
|
|
119
|
+
}, `${String(target?.constructor?.name ?? target?.name ?? 'Object')}.${String(propertyKey)}`),
|
|
120
|
+
{
|
|
121
|
+
method: original,
|
|
122
|
+
propertyKey,
|
|
123
|
+
...((original as any)[rootFunction] ? { [rootFunction]: (original as any)[rootFunction] } : {}),
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
wrapperRegistry.set(original, wrapper)
|
|
127
|
+
}
|
|
128
|
+
const memoized = memoizeFunction(wrapper as any)
|
|
87
129
|
return memoized(this)
|
|
88
130
|
}
|
|
89
131
|
},
|
|
90
|
-
method(original, name) {
|
|
91
|
-
const memoized = memoizeFunction(
|
|
92
|
-
markWithRoot(
|
|
93
|
-
renamed(
|
|
94
|
-
(that: object, ...args: object[]) => {
|
|
95
|
-
return original.call(that, ...args)
|
|
96
|
-
},
|
|
97
|
-
`${String(this.constructor.name)}.${String(name)}`
|
|
98
|
-
),
|
|
99
|
-
original
|
|
100
|
-
)
|
|
101
|
-
) as (...args: object[]) => unknown
|
|
132
|
+
method(original, target, name) {
|
|
102
133
|
return function (this: any, ...args: object[]) {
|
|
134
|
+
let wrapper = wrapperRegistry.get(original)
|
|
135
|
+
if (!wrapper) {
|
|
136
|
+
wrapper = markWithRoot(
|
|
137
|
+
renamed((that: object, ...args: object[]) => {
|
|
138
|
+
return original.call(that, ...args)
|
|
139
|
+
}, `${String(target?.constructor?.name ?? target?.name ?? 'Object')}.${String(name)}`),
|
|
140
|
+
{
|
|
141
|
+
method: original,
|
|
142
|
+
propertyKey: name,
|
|
143
|
+
...((original as any)[rootFunction] ? { [rootFunction]: (original as any)[rootFunction] } : {}),
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
wrapperRegistry.set(original, wrapper)
|
|
147
|
+
}
|
|
148
|
+
const memoized = memoizeFunction(wrapper as any) as (...args: object[]) => unknown
|
|
103
149
|
return memoized(this, ...args)
|
|
104
150
|
}
|
|
105
151
|
},
|