mutts 1.0.8 → 1.0.9
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 +19 -2
- package/dist/browser.cjs +1030 -4406
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +310 -573
- package/dist/browser.esm.js +18 -24
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{async-browser-CA0jPWIi.cjs → async-browser-lvzLOCgk.cjs} +17 -24
- package/dist/chunks/async-browser-lvzLOCgk.cjs.map +1 -0
- package/dist/chunks/{async-core-UqHzvJ-S.cjs → async-core-CRLKP3l-.cjs} +8 -4
- package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
- package/dist/chunks/{async-node-BYHuGTni.cjs → async-node-C3DeIb0y.cjs} +5 -12
- package/dist/chunks/async-node-C3DeIb0y.cjs.map +1 -0
- package/dist/chunks/index-2vea86wD.esm.js +3011 -0
- package/dist/chunks/index-2vea86wD.esm.js.map +1 -0
- package/dist/chunks/index-CtA2AWl3.esm.js +4376 -0
- package/dist/chunks/index-CtA2AWl3.esm.js.map +1 -0
- package/dist/chunks/index-VTO-b2vR.cjs +4461 -0
- package/dist/chunks/index-VTO-b2vR.cjs.map +1 -0
- package/dist/{reactive → debug}/debug.d.ts +19 -8
- package/dist/debug/debug.d.ts.map +1 -0
- package/dist/debug/index.d.ts +4 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/lineage-panel.d.ts +5 -0
- package/dist/debug/lineage-panel.d.ts.map +1 -0
- package/dist/debug/lineage.d.ts +79 -0
- package/dist/debug/lineage.d.ts.map +1 -0
- package/dist/debug.cjs +35 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.ts +209 -0
- package/dist/debug.esm.js +2 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/index.d.ts +1 -13
- 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.cjs +65 -63
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.esm.js +5 -11
- package/dist/node.esm.js.map +1 -1
- package/dist/{async → src/async}/browser.d.ts.map +1 -1
- package/dist/{async → src/async}/index.d.ts +2 -1
- package/dist/src/async/index.d.ts.map +1 -0
- package/dist/{async → src/async}/node.d.ts.map +1 -1
- package/dist/src/decorator.d.ts.map +1 -0
- package/dist/src/destroyable.d.ts.map +1 -0
- package/dist/src/entry-browser.d.ts.map +1 -0
- package/dist/src/entry-node.d.ts.map +1 -0
- package/dist/src/eventful.d.ts +20 -0
- package/dist/src/eventful.d.ts.map +1 -0
- package/dist/src/flavored.d.ts +33 -0
- package/dist/src/flavored.d.ts.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/indexable.d.ts.map +1 -0
- package/dist/{introspection.d.ts → src/introspection.d.ts} +1 -1
- package/dist/src/introspection.d.ts.map +1 -0
- package/dist/src/iterableWeak.d.ts.map +1 -0
- package/dist/src/mixins.d.ts.map +1 -0
- package/dist/src/promiseChain.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/array.d.ts +9 -10
- package/dist/src/reactive/array.d.ts.map +1 -0
- package/dist/src/reactive/buffer.d.ts +120 -0
- package/dist/src/reactive/buffer.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/change.d.ts +2 -2
- package/dist/src/reactive/change.d.ts.map +1 -0
- package/dist/src/reactive/deep-touch.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/deep-watch-state.d.ts +3 -3
- package/dist/src/reactive/deep-watch-state.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/deep-watch.d.ts +2 -1
- package/dist/src/reactive/deep-watch.d.ts.map +1 -0
- package/dist/src/reactive/describe.d.ts +12 -0
- package/dist/src/reactive/describe.d.ts.map +1 -0
- package/dist/src/reactive/effect-context.d.ts +34 -0
- package/dist/src/reactive/effect-context.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/effects.d.ts +31 -18
- package/dist/src/reactive/effects.d.ts.map +1 -0
- package/dist/src/reactive/index.d.ts +19 -0
- package/dist/src/reactive/index.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/map.d.ts +0 -2
- package/dist/src/reactive/map.d.ts.map +1 -0
- package/dist/src/reactive/memoize.d.ts +28 -0
- package/dist/src/reactive/memoize.d.ts.map +1 -0
- package/dist/src/reactive/non-reactive-state.d.ts.map +1 -0
- package/dist/src/reactive/non-reactive.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/project.d.ts +1 -2
- package/dist/src/reactive/project.d.ts.map +1 -0
- package/dist/src/reactive/proxy-state.d.ts.map +1 -0
- package/dist/src/reactive/proxy.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/record.d.ts +2 -3
- package/dist/src/reactive/record.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/register.d.ts +61 -0
- package/dist/src/reactive/register.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/registry.d.ts +6 -5
- package/dist/src/reactive/registry.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/set.d.ts +0 -2
- package/dist/src/reactive/set.d.ts.map +1 -0
- package/dist/src/reactive/tracking.d.ts.map +1 -0
- package/dist/{reactive → src/reactive}/types.d.ts +69 -21
- package/dist/src/reactive/types.d.ts.map +1 -0
- package/dist/{reactive/interface.d.ts → src/reactive/watch.d.ts} +7 -23
- package/dist/src/reactive/watch.d.ts.map +1 -0
- package/dist/{std-decorators.d.ts → src/std-decorators.d.ts} +1 -6
- package/dist/src/std-decorators.d.ts.map +1 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/{zone.d.ts → src/zone.d.ts} +2 -2
- package/dist/src/zone.d.ts.map +1 -0
- package/dist/types-DaHFfhlN.d.ts +414 -0
- package/docs/ai/api-reference.md +1 -1
- package/docs/ai/manual.md +443 -17
- package/docs/events.md +57 -2
- package/docs/flavored.md +210 -0
- package/docs/reactive/advanced.md +2 -3
- package/docs/reactive/attend.md +143 -0
- package/docs/reactive/collections.md +55 -0
- package/docs/reactive/core.md +120 -4
- package/docs/reactive/debugging.md +8 -10
- package/docs/reactive/describe.md +85 -0
- package/docs/reactive/error-handling.md +121 -0
- package/docs/reactive/project.md +1 -1
- package/docs/reactive/scan.md +215 -0
- package/docs/reactive.md +2 -0
- package/docs/std-decorators.md +102 -51
- package/docs/zone.md +3 -7
- package/package.json +6 -1
- package/src/async/browser.ts +110 -106
- package/src/async/index.ts +15 -11
- package/src/async/node.ts +5 -17
- package/src/eventful.ts +82 -49
- package/src/flavored.ts +106 -0
- package/src/index.d.ts +12 -12
- package/src/index.ts +2 -2
- package/src/introspection.ts +1 -1
- package/src/reactive/array.ts +29 -36
- package/src/reactive/buffer.ts +187 -27
- package/src/reactive/change.ts +16 -23
- package/src/reactive/deep-touch.ts +19 -14
- package/src/reactive/deep-watch-state.ts +3 -3
- package/src/reactive/deep-watch.ts +7 -4
- package/src/reactive/describe.ts +39 -0
- package/src/reactive/effect-context.ts +69 -4
- package/src/reactive/effects.ts +445 -393
- package/src/reactive/index.ts +12 -15
- package/src/reactive/map.ts +9 -25
- package/src/reactive/memoize.ts +26 -3
- package/src/reactive/project.md +107 -0
- package/src/reactive/project.ts +48 -71
- package/src/reactive/proxy.ts +21 -3
- package/src/reactive/record.ts +30 -45
- package/src/reactive/register.ts +138 -19
- package/src/reactive/registry.ts +22 -9
- package/src/reactive/set.ts +8 -21
- package/src/reactive/tracking.ts +9 -12
- package/src/reactive/types.ts +104 -24
- package/src/reactive/{interface.ts → watch.ts} +29 -59
- package/src/std-decorators.ts +56 -17
- package/src/utils.ts +2 -3
- package/src/zone.ts +28 -24
- package/dist/async/index.d.ts.map +0 -1
- package/dist/chunks/async-browser-CA0jPWIi.cjs.map +0 -1
- package/dist/chunks/async-core-UqHzvJ-S.cjs.map +0 -1
- package/dist/chunks/async-node-BYHuGTni.cjs.map +0 -1
- package/dist/chunks/index-DhaOVusv.esm.js +0 -6411
- package/dist/chunks/index-DhaOVusv.esm.js.map +0 -1
- package/dist/decorator.d.ts.map +0 -1
- package/dist/destroyable.d.ts.map +0 -1
- package/dist/entry-browser.d.ts.map +0 -1
- package/dist/entry-node.d.ts.map +0 -1
- package/dist/eventful.d.ts +0 -18
- package/dist/eventful.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/indexable.d.ts.map +0 -1
- package/dist/introspection.d.ts.map +0 -1
- package/dist/iterableWeak.d.ts.map +0 -1
- package/dist/mixins.d.ts.map +0 -1
- package/dist/promiseChain.d.ts.map +0 -1
- package/dist/reactive/array.d.ts.map +0 -1
- package/dist/reactive/buffer.d.ts +0 -44
- package/dist/reactive/buffer.d.ts.map +0 -1
- package/dist/reactive/change.d.ts.map +0 -1
- package/dist/reactive/debug.d.ts.map +0 -1
- package/dist/reactive/deep-touch.d.ts.map +0 -1
- package/dist/reactive/deep-watch-state.d.ts.map +0 -1
- package/dist/reactive/deep-watch.d.ts.map +0 -1
- package/dist/reactive/effect-context.d.ts +0 -7
- package/dist/reactive/effect-context.d.ts.map +0 -1
- package/dist/reactive/effects.d.ts.map +0 -1
- package/dist/reactive/index.d.ts +0 -20
- package/dist/reactive/index.d.ts.map +0 -1
- package/dist/reactive/interface.d.ts.map +0 -1
- package/dist/reactive/map.d.ts.map +0 -1
- package/dist/reactive/memoize.d.ts +0 -5
- package/dist/reactive/memoize.d.ts.map +0 -1
- package/dist/reactive/non-reactive-state.d.ts.map +0 -1
- package/dist/reactive/non-reactive.d.ts.map +0 -1
- package/dist/reactive/project.d.ts.map +0 -1
- package/dist/reactive/proxy-state.d.ts.map +0 -1
- package/dist/reactive/proxy.d.ts.map +0 -1
- package/dist/reactive/record.d.ts.map +0 -1
- package/dist/reactive/register.d.ts.map +0 -1
- package/dist/reactive/registry.d.ts.map +0 -1
- package/dist/reactive/set.d.ts.map +0 -1
- package/dist/reactive/tracking.d.ts.map +0 -1
- package/dist/reactive/types.d.ts.map +0 -1
- package/dist/std-decorators.d.ts.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/zone.d.ts.map +0 -1
- package/src/reactive/debug.ts +0 -517
- /package/dist/{async → src/async}/browser.d.ts +0 -0
- /package/dist/{async → src/async}/node.d.ts +0 -0
- /package/dist/{decorator.d.ts → src/decorator.d.ts} +0 -0
- /package/dist/{destroyable.d.ts → src/destroyable.d.ts} +0 -0
- /package/dist/{entry-browser.d.ts → src/entry-browser.d.ts} +0 -0
- /package/dist/{entry-node.d.ts → src/entry-node.d.ts} +0 -0
- /package/dist/{indexable.d.ts → src/indexable.d.ts} +0 -0
- /package/dist/{iterableWeak.d.ts → src/iterableWeak.d.ts} +0 -0
- /package/dist/{mixins.d.ts → src/mixins.d.ts} +0 -0
- /package/dist/{promiseChain.d.ts → src/promiseChain.d.ts} +0 -0
- /package/dist/{reactive → src/reactive}/deep-touch.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/non-reactive-state.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/non-reactive.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/proxy-state.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/proxy.d.ts +0 -0
- /package/dist/{reactive → src/reactive}/tracking.d.ts +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
|
@@ -0,0 +1,4376 @@
|
|
|
1
|
+
// Queue for hooks registered before the environment is ready (circular dependency fix)
|
|
2
|
+
const hooks = new Set();
|
|
3
|
+
const asyncHooks = {
|
|
4
|
+
addHook(hook) {
|
|
5
|
+
hooks.add(hook);
|
|
6
|
+
return () => hooks.delete(hook);
|
|
7
|
+
},
|
|
8
|
+
/**
|
|
9
|
+
* [Hack] Sanitize a promise (or value) to prevent context leaks.
|
|
10
|
+
* Default: Identity function.
|
|
11
|
+
* Browser: Uses Macrotask wrapping to break microtask chains.
|
|
12
|
+
*/
|
|
13
|
+
sanitizePromise(p) {
|
|
14
|
+
return p;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Register a hook that will be called whenever an asynchronous operation is initiated.
|
|
19
|
+
* The hook should return a restorer function which will be called just before the async callback runs.
|
|
20
|
+
* That restorer should in turn return an undoer function which will be called just after the async callback finishes.
|
|
21
|
+
*/
|
|
22
|
+
const asyncHook = (hook) => asyncHooks.addHook(hook);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Combines multiple arrays into an array of tuples, stopping at the shortest array length
|
|
26
|
+
* @param args - Arrays to zip together
|
|
27
|
+
* @returns Array of tuples containing elements from each input array
|
|
28
|
+
*/
|
|
29
|
+
function zip(...args) {
|
|
30
|
+
if (!args.length)
|
|
31
|
+
return [];
|
|
32
|
+
const minLength = Math.min(...args.map((arr) => arr.length));
|
|
33
|
+
const result = [];
|
|
34
|
+
for (let i = 0; i < minLength; i++) {
|
|
35
|
+
const tuple = args.map((arr) => arr[i]);
|
|
36
|
+
result.push(tuple);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Checks if two arrays are strictly equal (shallow comparison)
|
|
42
|
+
* @param a - First value
|
|
43
|
+
* @param b - Second value
|
|
44
|
+
* @returns True if arrays are equal or values are strictly equal
|
|
45
|
+
*/
|
|
46
|
+
function arrayEquals(a, b) {
|
|
47
|
+
if (a === b)
|
|
48
|
+
return true;
|
|
49
|
+
if (!Array.isArray(a) || !Array.isArray(b))
|
|
50
|
+
return false;
|
|
51
|
+
if (a.length !== b.length)
|
|
52
|
+
return false;
|
|
53
|
+
for (let i = 0; i < a.length; i++) {
|
|
54
|
+
if (a[i] !== b[i])
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const nativeConstructors = new Set([
|
|
60
|
+
Object,
|
|
61
|
+
Array,
|
|
62
|
+
Date,
|
|
63
|
+
Function,
|
|
64
|
+
Set,
|
|
65
|
+
Map,
|
|
66
|
+
WeakMap,
|
|
67
|
+
WeakSet,
|
|
68
|
+
Promise,
|
|
69
|
+
Error,
|
|
70
|
+
TypeError,
|
|
71
|
+
ReferenceError,
|
|
72
|
+
SyntaxError,
|
|
73
|
+
RangeError,
|
|
74
|
+
URIError,
|
|
75
|
+
EvalError,
|
|
76
|
+
Reflect,
|
|
77
|
+
Proxy,
|
|
78
|
+
RegExp,
|
|
79
|
+
String,
|
|
80
|
+
Number,
|
|
81
|
+
Boolean,
|
|
82
|
+
]);
|
|
83
|
+
/**
|
|
84
|
+
* Checks if a function is a constructor (class or constructor function)
|
|
85
|
+
* @param fn - The function to check
|
|
86
|
+
* @returns True if the function is a constructor
|
|
87
|
+
*/
|
|
88
|
+
function isConstructor(fn) {
|
|
89
|
+
return (fn &&
|
|
90
|
+
typeof fn === 'function' &&
|
|
91
|
+
(nativeConstructors.has(fn) || fn.toString?.().startsWith('class ')));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Renames a function with a new name
|
|
95
|
+
* @param fct - The function to rename
|
|
96
|
+
* @param name - The new name for the function
|
|
97
|
+
* @returns The function with the new name
|
|
98
|
+
*/
|
|
99
|
+
function renamed(fct, name) {
|
|
100
|
+
return Object.defineProperties(fct, {
|
|
101
|
+
name: {
|
|
102
|
+
value: name,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const FoolProof = {
|
|
107
|
+
get(obj, prop, receiver) {
|
|
108
|
+
// Check if Node is available and obj is an instance of Node
|
|
109
|
+
if (typeof Node !== 'undefined' && obj instanceof Node)
|
|
110
|
+
return obj[prop];
|
|
111
|
+
return Reflect.get(obj, prop, receiver);
|
|
112
|
+
},
|
|
113
|
+
set(obj, prop, value, receiver) {
|
|
114
|
+
// Check if Node is available and obj is an instance of Node
|
|
115
|
+
if (typeof Node !== 'undefined' && obj instanceof Node) {
|
|
116
|
+
obj[prop] = value;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (!(obj instanceof Object) && !Reflect.has(obj, prop)) {
|
|
120
|
+
Object.defineProperty(obj, prop, {
|
|
121
|
+
value,
|
|
122
|
+
configurable: true,
|
|
123
|
+
writable: true,
|
|
124
|
+
enumerable: true,
|
|
125
|
+
});
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return Reflect.set(obj, prop, value, receiver);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
function isOwnAccessor(obj, prop) {
|
|
132
|
+
const opd = Object.getOwnPropertyDescriptor(obj, prop);
|
|
133
|
+
return !!(opd?.get || opd?.set);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Deeply compares two values.
|
|
137
|
+
* For objects, compares prototypes with === and then own properties recursively.
|
|
138
|
+
* Uses a cache to handle circular references.
|
|
139
|
+
* @param a - First value
|
|
140
|
+
* @param b - Second value
|
|
141
|
+
* @param cache - Map for circular reference protection (internal use)
|
|
142
|
+
* @returns True if values are deeply equal
|
|
143
|
+
*/
|
|
144
|
+
function deepCompare(a, b, cache = new Map()) {
|
|
145
|
+
if (a === b)
|
|
146
|
+
return true;
|
|
147
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
|
|
148
|
+
return a === b;
|
|
149
|
+
}
|
|
150
|
+
// Prototype check
|
|
151
|
+
const protoA = Object.getPrototypeOf(a);
|
|
152
|
+
const protoB = Object.getPrototypeOf(b);
|
|
153
|
+
if (protoA !== protoB) {
|
|
154
|
+
console.warn(`[deepCompare] prototype mismatch:`, {
|
|
155
|
+
nameA: a?.constructor?.name,
|
|
156
|
+
nameB: b?.constructor?.name,
|
|
157
|
+
});
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
// Circular reference protection
|
|
161
|
+
let compared = cache.get(a);
|
|
162
|
+
if (compared?.has(b))
|
|
163
|
+
return true;
|
|
164
|
+
if (!compared) {
|
|
165
|
+
compared = new Set();
|
|
166
|
+
cache.set(a, compared);
|
|
167
|
+
}
|
|
168
|
+
compared.add(b);
|
|
169
|
+
// Handle specific object types
|
|
170
|
+
if (Array.isArray(a)) {
|
|
171
|
+
if (!Array.isArray(b)) {
|
|
172
|
+
console.warn(`[deepCompare] B is not an array`);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (a.length !== b.length) {
|
|
176
|
+
console.warn(`[deepCompare] array length mismatch:`, { lenA: a.length, lenB: b.length });
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
for (let i = 0; i < a.length; i++) {
|
|
180
|
+
if (!deepCompare(a[i], b[i], cache)) {
|
|
181
|
+
console.warn(`[deepCompare] array element mismatch at index ${i}`);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (a instanceof Date) {
|
|
188
|
+
const match = b instanceof Date && a.getTime() === b.getTime();
|
|
189
|
+
if (!match)
|
|
190
|
+
console.warn(`[deepCompare] Date mismatch`);
|
|
191
|
+
return match;
|
|
192
|
+
}
|
|
193
|
+
if (a instanceof RegExp) {
|
|
194
|
+
const match = b instanceof RegExp && a.toString() === b.toString();
|
|
195
|
+
if (!match)
|
|
196
|
+
console.warn(`[deepCompare] RegExp mismatch`);
|
|
197
|
+
return match;
|
|
198
|
+
}
|
|
199
|
+
if (a instanceof Set) {
|
|
200
|
+
if (!(b instanceof Set) || a.size !== b.size) {
|
|
201
|
+
console.warn(`[deepCompare] Set size mismatch`);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
for (const val of a) {
|
|
205
|
+
let found = false;
|
|
206
|
+
for (const bVal of b) {
|
|
207
|
+
if (deepCompare(val, bVal, cache)) {
|
|
208
|
+
found = true;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!found) {
|
|
213
|
+
console.warn(`[deepCompare] missing Set element`);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (a instanceof Map) {
|
|
220
|
+
if (!(b instanceof Map) || a.size !== b.size) {
|
|
221
|
+
console.warn(`[deepCompare] Map size mismatch`);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
for (const [key, val] of a) {
|
|
225
|
+
if (!b.has(key)) {
|
|
226
|
+
let foundMatch = false;
|
|
227
|
+
for (const [bKey, bVal] of b) {
|
|
228
|
+
if (deepCompare(key, bKey, cache) && deepCompare(val, bVal, cache)) {
|
|
229
|
+
foundMatch = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!foundMatch) {
|
|
234
|
+
console.warn(`[deepCompare] missing Map key`);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
if (!deepCompare(val, b.get(key), cache)) {
|
|
240
|
+
console.warn(`[deepCompare] Map value mismatch for key`);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
// Compare own properties
|
|
248
|
+
const keysA = Object.keys(a);
|
|
249
|
+
const keysB = Object.keys(b);
|
|
250
|
+
if (keysA.length !== keysB.length) {
|
|
251
|
+
console.warn(`[deepCompare] keys length mismatch:`, {
|
|
252
|
+
lenA: keysA.length,
|
|
253
|
+
lenB: keysB.length,
|
|
254
|
+
keysA,
|
|
255
|
+
keysB,
|
|
256
|
+
a,
|
|
257
|
+
b,
|
|
258
|
+
});
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
for (const key of keysA) {
|
|
262
|
+
if (!Object.hasOwn(b, key)) {
|
|
263
|
+
console.warn(`[deepCompare] missing key ${String(key)} in B`);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
if (!deepCompare(a[key], b[key], cache)) {
|
|
267
|
+
console.warn(`[deepCompare] value mismatch for key ${String(key)}:`, {
|
|
268
|
+
valA: a[key],
|
|
269
|
+
valB: b[key],
|
|
270
|
+
});
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
const contentRefs = new WeakMap();
|
|
277
|
+
function contentRef(container) {
|
|
278
|
+
if (!contentRefs.has(container))
|
|
279
|
+
contentRefs.set(container, Object.seal(Object.create(null, {
|
|
280
|
+
contentOf: { value: container, writable: false, configurable: false },
|
|
281
|
+
})));
|
|
282
|
+
return contentRefs.get(container);
|
|
283
|
+
}
|
|
284
|
+
function tag(name, obj) {
|
|
285
|
+
Object.defineProperties(obj, {
|
|
286
|
+
[Symbol.toStringTag]: {
|
|
287
|
+
value: name,
|
|
288
|
+
writable: false,
|
|
289
|
+
configurable: false,
|
|
290
|
+
},
|
|
291
|
+
toString: {
|
|
292
|
+
value: () => name,
|
|
293
|
+
writable: false,
|
|
294
|
+
configurable: false,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
return obj;
|
|
298
|
+
}
|
|
299
|
+
function named(name, fn) {
|
|
300
|
+
Object.defineProperty(fn, 'name', {
|
|
301
|
+
value: fn.name ? `${fn.name}::${name}` : name,
|
|
302
|
+
writable: false,
|
|
303
|
+
configurable: true,
|
|
304
|
+
});
|
|
305
|
+
return fn;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// biome-ignore-all lint/suspicious/noConfusingVoidType: We *love* voids
|
|
309
|
+
// Standardized decorator system that works with both Legacy and Modern decorators
|
|
310
|
+
/**
|
|
311
|
+
* Error thrown when decorator operations fail
|
|
312
|
+
*/
|
|
313
|
+
class DecoratorError extends Error {
|
|
314
|
+
constructor(message) {
|
|
315
|
+
super(message);
|
|
316
|
+
this.name = 'DecoratorException';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Creates a decorator that works with Legacy decorator proposals
|
|
321
|
+
* @param description - The decorator description object
|
|
322
|
+
* @returns A decorator function compatible with Legacy decorators
|
|
323
|
+
*/
|
|
324
|
+
function legacyDecorator(description) {
|
|
325
|
+
return function (target, propertyKey, descriptor, ...args) {
|
|
326
|
+
if (propertyKey === undefined) {
|
|
327
|
+
if (isConstructor(target)) {
|
|
328
|
+
if (!('class' in description))
|
|
329
|
+
throw new Error('Decorator cannot be applied to a class');
|
|
330
|
+
return description.class(target);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else if (typeof target === 'object' && ['string', 'symbol'].includes(typeof propertyKey)) {
|
|
334
|
+
if (!descriptor)
|
|
335
|
+
throw new Error('Decorator cannot be applied to a field');
|
|
336
|
+
else if (typeof descriptor === 'object' && 'configurable' in descriptor) {
|
|
337
|
+
if ('get' in descriptor || 'set' in descriptor) {
|
|
338
|
+
if (!('getter' in description || 'setter' in description))
|
|
339
|
+
throw new Error('Decorator cannot be applied to a getter or setter');
|
|
340
|
+
if ('getter' in description) {
|
|
341
|
+
const newGetter = description.getter(descriptor.get, target, propertyKey);
|
|
342
|
+
if (newGetter)
|
|
343
|
+
descriptor.get = newGetter;
|
|
344
|
+
}
|
|
345
|
+
if ('setter' in description) {
|
|
346
|
+
const newSetter = description.setter(descriptor.set, target, propertyKey);
|
|
347
|
+
if (newSetter)
|
|
348
|
+
descriptor.set = newSetter;
|
|
349
|
+
}
|
|
350
|
+
return descriptor;
|
|
351
|
+
}
|
|
352
|
+
else if (typeof descriptor.value === 'function') {
|
|
353
|
+
if (!('method' in description))
|
|
354
|
+
throw new Error('Decorator cannot be applied to a method');
|
|
355
|
+
const newMethod = description.method(descriptor.value, target, propertyKey);
|
|
356
|
+
if (newMethod)
|
|
357
|
+
descriptor.value = newMethod;
|
|
358
|
+
return descriptor;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!('default' in description))
|
|
363
|
+
throw new Error('Decorator do not have a default implementation');
|
|
364
|
+
return description.default.call(this, target, propertyKey, descriptor, ...args);
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Creates a decorator that works with Modern decorator proposals
|
|
369
|
+
* @param description - The decorator description object
|
|
370
|
+
* @returns A decorator function compatible with Modern decorators
|
|
371
|
+
*/
|
|
372
|
+
function modernDecorator(description) {
|
|
373
|
+
/*return function (target: any, context?: DecoratorContext, ...args: any[]) {*/
|
|
374
|
+
return function (target, context, ...args) {
|
|
375
|
+
if (!context?.kind || typeof context.kind !== 'string') {
|
|
376
|
+
if (!('default' in description))
|
|
377
|
+
throw new Error('Decorator do not have a default implementation');
|
|
378
|
+
return description.default.call(this, target, context, ...args);
|
|
379
|
+
}
|
|
380
|
+
switch (context.kind) {
|
|
381
|
+
case 'class':
|
|
382
|
+
if (!('class' in description))
|
|
383
|
+
throw new Error('Decorator cannot be applied to a class');
|
|
384
|
+
return description.class(target);
|
|
385
|
+
case 'field':
|
|
386
|
+
throw new Error('Decorator cannot be applied to a field');
|
|
387
|
+
case 'getter':
|
|
388
|
+
if (!('getter' in description))
|
|
389
|
+
throw new Error('Decorator cannot be applied to a getter');
|
|
390
|
+
return description.getter(target, target, context.name);
|
|
391
|
+
case 'setter':
|
|
392
|
+
if (!('setter' in description))
|
|
393
|
+
throw new Error('Decorator cannot be applied to a setter');
|
|
394
|
+
return description.setter(target, target, context.name);
|
|
395
|
+
case 'method':
|
|
396
|
+
if (!('method' in description))
|
|
397
|
+
throw new Error('Decorator cannot be applied to a method');
|
|
398
|
+
return description.method(target, target, context.name);
|
|
399
|
+
case 'accessor': {
|
|
400
|
+
if (!('getter' in description || 'setter' in description))
|
|
401
|
+
throw new Error('Decorator cannot be applied to a getter or setter');
|
|
402
|
+
const rv = {};
|
|
403
|
+
if ('getter' in description) {
|
|
404
|
+
const newGetter = description.getter(target.get, target, context.name);
|
|
405
|
+
if (newGetter)
|
|
406
|
+
rv.get = newGetter;
|
|
407
|
+
}
|
|
408
|
+
if ('setter' in description) {
|
|
409
|
+
const newSetter = description.setter(target.set, target, context.name);
|
|
410
|
+
if (newSetter)
|
|
411
|
+
rv.set = newSetter;
|
|
412
|
+
}
|
|
413
|
+
return rv;
|
|
414
|
+
}
|
|
415
|
+
//return description.accessor?.(target, context.name, target)
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Detects if the decorator is being called in modern (Modern) or legacy (Legacy) mode
|
|
421
|
+
* based on the arguments passed to the decorator function
|
|
422
|
+
*/
|
|
423
|
+
function detectDecoratorMode(_target, contextOrKey, _descriptor) {
|
|
424
|
+
// Modern decorators have a context object as the second parameter
|
|
425
|
+
// Legacy decorators have a string/symbol key as the second parameter
|
|
426
|
+
if (typeof contextOrKey === 'object' &&
|
|
427
|
+
contextOrKey !== null &&
|
|
428
|
+
typeof contextOrKey.kind === 'string') {
|
|
429
|
+
return 'modern';
|
|
430
|
+
}
|
|
431
|
+
return 'legacy';
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Main decorator factory that automatically detects and works with both Legacy and Modern decorator proposals
|
|
435
|
+
* @param description - The decorator description object
|
|
436
|
+
* @returns A decorator that works in both Legacy and Modern environments
|
|
437
|
+
*/
|
|
438
|
+
const decorator = (description) => {
|
|
439
|
+
const modern = modernDecorator(description);
|
|
440
|
+
const legacy = legacyDecorator(description);
|
|
441
|
+
return ((target, contextOrKey, ...args) => {
|
|
442
|
+
const mode = detectDecoratorMode(target, contextOrKey, args[0]);
|
|
443
|
+
return mode === 'modern'
|
|
444
|
+
? modern(target, contextOrKey, ...args)
|
|
445
|
+
: legacy(target, contextOrKey, ...args);
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Creates a flavored (extensible) version of a function with chainable property modifiers.
|
|
451
|
+
*
|
|
452
|
+
* Each property defined in `flavors` returns a new flavored function that transforms
|
|
453
|
+
* how the original function is called. This enables a fluent API where properties
|
|
454
|
+
* create specialized variants of the base function.
|
|
455
|
+
*
|
|
456
|
+
* @param fn - The base function to flavor
|
|
457
|
+
* @param flavors - Object defining the flavor properties (getters or methods)
|
|
458
|
+
* @returns A proxy of the function with the flavor properties attached
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* ```typescript
|
|
462
|
+
* function greet(name: string, options?: { loud?: boolean }) {
|
|
463
|
+
* const greeting = `Hello, ${name}!`
|
|
464
|
+
* return options?.loud ? greeting.toUpperCase() : greeting
|
|
465
|
+
* }
|
|
466
|
+
*
|
|
467
|
+
* const flavoredGreet = flavored(greet, {
|
|
468
|
+
* get loud() {
|
|
469
|
+
* return createFlavor(this, (name, opts) => [name, { ...opts, loud: true }])
|
|
470
|
+
* }
|
|
471
|
+
* })
|
|
472
|
+
*
|
|
473
|
+
* flavoredGreet('World') // "Hello, World!"
|
|
474
|
+
* flavoredGreet.loud('World') // "HELLO, WORLD!"
|
|
475
|
+
* ```
|
|
476
|
+
*/
|
|
477
|
+
/**
|
|
478
|
+
* Creates a flavored (extensible) version of a function with chainable property modifiers.
|
|
479
|
+
*/
|
|
480
|
+
function flavored(fn, flavors) {
|
|
481
|
+
fn.flavors = flavors;
|
|
482
|
+
return new Proxy(fn, {
|
|
483
|
+
get(target, prop, receiver) {
|
|
484
|
+
if (prop in flavors) {
|
|
485
|
+
return Reflect.get(flavors, prop, receiver);
|
|
486
|
+
}
|
|
487
|
+
return target[prop];
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Creates a new flavored function that transforms arguments before calling the base.
|
|
493
|
+
*
|
|
494
|
+
* @param fn - The base flavored function
|
|
495
|
+
* @param transform - Function that receives the original arguments and returns transformed arguments
|
|
496
|
+
* @returns A new flavored function with the transformation applied
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* ```typescript
|
|
500
|
+
* const loudGreet = createFlavor(greet, (name, opts) => [name, { ...opts, loud: true }])
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
function createFlavor(fn, transform, name) {
|
|
504
|
+
const fct = function flavorWrapper(...args) {
|
|
505
|
+
return fn.apply(this, transform(...args));
|
|
506
|
+
};
|
|
507
|
+
if (name)
|
|
508
|
+
named(name, fct);
|
|
509
|
+
return flavored(fct, fn.flavors || {});
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Creates a new flavored function that merges options objects.
|
|
513
|
+
* Useful when the last argument is an options object that should be merged.
|
|
514
|
+
*
|
|
515
|
+
* @param fn - The base flavored function
|
|
516
|
+
* @param defaultOptions - Options to merge with the provided options
|
|
517
|
+
* @returns A new flavored function with merged options
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* ```typescript
|
|
521
|
+
* const opaqueEffect = flavorOptions(effect, { opaque: true })
|
|
522
|
+
* const namedEffect = flavorOptions(effect, { name: 'myEffect' })
|
|
523
|
+
* ```
|
|
524
|
+
*/
|
|
525
|
+
function flavorOptions(fn, defaultOptions, name) {
|
|
526
|
+
const fct = function flavorOptionsWrapper(...args) {
|
|
527
|
+
const lastArg = args[args.length - 1];
|
|
528
|
+
const mergedArgs = lastArg !== undefined && typeof lastArg === 'object'
|
|
529
|
+
? [...args.slice(0, -1), { ...defaultOptions, ...lastArg }]
|
|
530
|
+
: [...args, defaultOptions];
|
|
531
|
+
return fn.apply(this, mergedArgs);
|
|
532
|
+
};
|
|
533
|
+
if (name)
|
|
534
|
+
named(name, fct);
|
|
535
|
+
return flavored(fct, fn.flavors || {});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/// <reference lib="esnext.collection" />
|
|
539
|
+
var _a, _b;
|
|
540
|
+
/**
|
|
541
|
+
* Uses weak references but still may iterate through them
|
|
542
|
+
* Note: The behavior is highly dependant on the garbage collector - some entries are perhaps deemed to be collected: don't resuscitate them
|
|
543
|
+
*/
|
|
544
|
+
class IterableWeakMap {
|
|
545
|
+
constructor(entries) {
|
|
546
|
+
this.uuids = new WeakMap();
|
|
547
|
+
this.refs = {};
|
|
548
|
+
this[_a] = 'IterableWeakMap';
|
|
549
|
+
// Create a FinalizationRegistry to clean up refs when keys are garbage collected
|
|
550
|
+
this.registry = new FinalizationRegistry((uuid) => {
|
|
551
|
+
delete this.refs[uuid];
|
|
552
|
+
});
|
|
553
|
+
if (entries)
|
|
554
|
+
for (const [k, v] of entries)
|
|
555
|
+
this.set(k, v);
|
|
556
|
+
}
|
|
557
|
+
createIterator(cb) {
|
|
558
|
+
const { refs } = this;
|
|
559
|
+
return (function* () {
|
|
560
|
+
for (const uuid of Object.keys(refs)) {
|
|
561
|
+
const [keyRef, value] = refs[uuid];
|
|
562
|
+
const key = keyRef.deref();
|
|
563
|
+
if (key)
|
|
564
|
+
yield cb(key, value);
|
|
565
|
+
else
|
|
566
|
+
delete refs[uuid];
|
|
567
|
+
}
|
|
568
|
+
return undefined;
|
|
569
|
+
})();
|
|
570
|
+
}
|
|
571
|
+
clear() {
|
|
572
|
+
// Unregister all keys from the FinalizationRegistry
|
|
573
|
+
for (const uuid of Object.keys(this.refs)) {
|
|
574
|
+
const key = this.refs[uuid][0].deref();
|
|
575
|
+
if (key)
|
|
576
|
+
this.registry.unregister(key);
|
|
577
|
+
}
|
|
578
|
+
this.uuids = new WeakMap();
|
|
579
|
+
this.refs = {};
|
|
580
|
+
}
|
|
581
|
+
delete(key) {
|
|
582
|
+
const uuid = this.uuids.get(key);
|
|
583
|
+
if (!uuid)
|
|
584
|
+
return false;
|
|
585
|
+
delete this.refs[uuid];
|
|
586
|
+
this.uuids.delete(key);
|
|
587
|
+
this.registry.unregister(key);
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
forEach(callbackfn, thisArg) {
|
|
591
|
+
for (const [k, v] of this)
|
|
592
|
+
callbackfn.call(thisArg ?? this, v, k, thisArg ?? this);
|
|
593
|
+
}
|
|
594
|
+
get(key) {
|
|
595
|
+
const uuid = this.uuids.get(key);
|
|
596
|
+
if (!uuid)
|
|
597
|
+
return undefined;
|
|
598
|
+
return this.refs[uuid][1];
|
|
599
|
+
}
|
|
600
|
+
has(key) {
|
|
601
|
+
return this.uuids.has(key);
|
|
602
|
+
}
|
|
603
|
+
set(key, value) {
|
|
604
|
+
let uuid = this.uuids.get(key);
|
|
605
|
+
if (uuid) {
|
|
606
|
+
this.refs[uuid][1] = value;
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
uuid = crypto.randomUUID();
|
|
610
|
+
this.uuids.set(key, uuid);
|
|
611
|
+
this.refs[uuid] = [new WeakRef(key), value];
|
|
612
|
+
// Register key for cleanup when garbage collected
|
|
613
|
+
this.registry.register(key, uuid, key);
|
|
614
|
+
}
|
|
615
|
+
return this;
|
|
616
|
+
}
|
|
617
|
+
get size() {
|
|
618
|
+
return [...this].length;
|
|
619
|
+
}
|
|
620
|
+
entries() {
|
|
621
|
+
return this.createIterator((key, value) => [key, value]);
|
|
622
|
+
}
|
|
623
|
+
keys() {
|
|
624
|
+
return this.createIterator((key, _value) => key);
|
|
625
|
+
}
|
|
626
|
+
values() {
|
|
627
|
+
return this.createIterator((_key, value) => value);
|
|
628
|
+
}
|
|
629
|
+
[Symbol.iterator]() {
|
|
630
|
+
return this.entries();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
_a = Symbol.toStringTag;
|
|
634
|
+
/**
|
|
635
|
+
* Uses weak references but still may iterate through them
|
|
636
|
+
* Note: The behavior is highly dependant on the garbage collector - some entries are perhaps deemed to be collected: don't resuscitate them
|
|
637
|
+
*/
|
|
638
|
+
class IterableWeakSet {
|
|
639
|
+
constructor(entries) {
|
|
640
|
+
this.uuids = new WeakMap();
|
|
641
|
+
this.refs = {};
|
|
642
|
+
this[_b] = 'IterableWeakSet';
|
|
643
|
+
// Create a FinalizationRegistry to clean up refs when values are garbage collected
|
|
644
|
+
this.registry = new FinalizationRegistry((uuid) => {
|
|
645
|
+
delete this.refs[uuid];
|
|
646
|
+
});
|
|
647
|
+
if (entries)
|
|
648
|
+
for (const k of entries)
|
|
649
|
+
this.add(k);
|
|
650
|
+
}
|
|
651
|
+
createIterator(cb) {
|
|
652
|
+
const { refs } = this;
|
|
653
|
+
return (function* () {
|
|
654
|
+
for (const uuid of Object.keys(refs)) {
|
|
655
|
+
const key = refs[uuid].deref();
|
|
656
|
+
if (key)
|
|
657
|
+
yield cb(key);
|
|
658
|
+
else
|
|
659
|
+
delete refs[uuid];
|
|
660
|
+
}
|
|
661
|
+
return undefined;
|
|
662
|
+
})();
|
|
663
|
+
}
|
|
664
|
+
clear() {
|
|
665
|
+
// Unregister all values from the FinalizationRegistry
|
|
666
|
+
for (const uuid of Object.keys(this.refs)) {
|
|
667
|
+
const value = this.refs[uuid].deref();
|
|
668
|
+
if (value)
|
|
669
|
+
this.registry.unregister(value);
|
|
670
|
+
}
|
|
671
|
+
this.uuids = new WeakMap();
|
|
672
|
+
this.refs = {};
|
|
673
|
+
}
|
|
674
|
+
add(value) {
|
|
675
|
+
let uuid = this.uuids.get(value);
|
|
676
|
+
if (!uuid) {
|
|
677
|
+
uuid = crypto.randomUUID();
|
|
678
|
+
this.uuids.set(value, uuid);
|
|
679
|
+
this.refs[uuid] = new WeakRef(value);
|
|
680
|
+
// Register value for cleanup when garbage collected
|
|
681
|
+
this.registry.register(value, uuid, value);
|
|
682
|
+
}
|
|
683
|
+
return this;
|
|
684
|
+
}
|
|
685
|
+
delete(value) {
|
|
686
|
+
const uuid = this.uuids.get(value);
|
|
687
|
+
if (!uuid)
|
|
688
|
+
return false;
|
|
689
|
+
delete this.refs[uuid];
|
|
690
|
+
this.uuids.delete(value);
|
|
691
|
+
this.registry.unregister(value);
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
forEach(callbackfn, thisArg) {
|
|
695
|
+
for (const value of this)
|
|
696
|
+
callbackfn.call(thisArg ?? this, value, value, thisArg ?? this);
|
|
697
|
+
}
|
|
698
|
+
has(value) {
|
|
699
|
+
return this.uuids.has(value);
|
|
700
|
+
}
|
|
701
|
+
get size() {
|
|
702
|
+
return [...this].length;
|
|
703
|
+
}
|
|
704
|
+
entries() {
|
|
705
|
+
return this.createIterator((key) => [key, key]);
|
|
706
|
+
}
|
|
707
|
+
keys() {
|
|
708
|
+
return this.createIterator((key) => key);
|
|
709
|
+
}
|
|
710
|
+
values() {
|
|
711
|
+
return this.createIterator((key) => key);
|
|
712
|
+
}
|
|
713
|
+
[Symbol.iterator]() {
|
|
714
|
+
return this.keys();
|
|
715
|
+
}
|
|
716
|
+
union(other) {
|
|
717
|
+
const others = {
|
|
718
|
+
[Symbol.iterator]() {
|
|
719
|
+
return other.keys();
|
|
720
|
+
},
|
|
721
|
+
};
|
|
722
|
+
const that = this;
|
|
723
|
+
return new Set((function* () {
|
|
724
|
+
yield* that;
|
|
725
|
+
for (const value of others)
|
|
726
|
+
if (!that.has(value))
|
|
727
|
+
yield value;
|
|
728
|
+
})());
|
|
729
|
+
}
|
|
730
|
+
intersection(other) {
|
|
731
|
+
const that = this;
|
|
732
|
+
return new Set((function* () {
|
|
733
|
+
for (const value of that)
|
|
734
|
+
if (other.has(value))
|
|
735
|
+
yield value;
|
|
736
|
+
})());
|
|
737
|
+
}
|
|
738
|
+
difference(other) {
|
|
739
|
+
const that = this;
|
|
740
|
+
return new Set((function* () {
|
|
741
|
+
for (const value of that)
|
|
742
|
+
if (!other.has(value))
|
|
743
|
+
yield value;
|
|
744
|
+
})());
|
|
745
|
+
}
|
|
746
|
+
symmetricDifference(other) {
|
|
747
|
+
const others = {
|
|
748
|
+
[Symbol.iterator]() {
|
|
749
|
+
return other.keys();
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
const that = this;
|
|
753
|
+
return new Set((function* () {
|
|
754
|
+
for (const value of that)
|
|
755
|
+
if (!other.has(value))
|
|
756
|
+
yield value;
|
|
757
|
+
for (const value of others)
|
|
758
|
+
if (!that.has(value))
|
|
759
|
+
yield value;
|
|
760
|
+
})());
|
|
761
|
+
}
|
|
762
|
+
isSubsetOf(other) {
|
|
763
|
+
for (const value of this)
|
|
764
|
+
if (!other.has(value))
|
|
765
|
+
return false;
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
isSupersetOf(other) {
|
|
769
|
+
const others = {
|
|
770
|
+
[Symbol.iterator]() {
|
|
771
|
+
return other.keys();
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
for (const value of others)
|
|
775
|
+
if (!this.has(value))
|
|
776
|
+
return false;
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
isDisjointFrom(other) {
|
|
780
|
+
for (const value of this)
|
|
781
|
+
if (other.has(value))
|
|
782
|
+
return false;
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
_b = Symbol.toStringTag;
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Creates a mixin that can be used both as a class (extends) and as a function (mixin)
|
|
790
|
+
*
|
|
791
|
+
* This function supports:
|
|
792
|
+
* - Using mixins as base classes: `class MyClass extends MyMixin`
|
|
793
|
+
* - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
|
|
794
|
+
* - Composing mixins: `const Composed = MixinA(MixinB)`
|
|
795
|
+
* - Type-safe property inference for all patterns
|
|
796
|
+
*
|
|
797
|
+
* @param mixinFunction - The function that creates the mixin
|
|
798
|
+
* @param unwrapFunction - Optional function to unwrap reactive objects for method calls
|
|
799
|
+
* @returns A mixin that can be used both as a class and as a function
|
|
800
|
+
*/
|
|
801
|
+
function mixin(mixinFunction, unwrapFunction) {
|
|
802
|
+
/**
|
|
803
|
+
* Cache for mixin results to ensure the same base class always returns the same mixed class
|
|
804
|
+
*/
|
|
805
|
+
const mixinCache = new WeakMap();
|
|
806
|
+
// Apply the mixin to Object as the base class
|
|
807
|
+
const MixedBase = mixinFunction(Object);
|
|
808
|
+
mixinCache.set(Object, MixedBase);
|
|
809
|
+
// Create the proxy that handles both constructor and function calls
|
|
810
|
+
return new Proxy(MixedBase, {
|
|
811
|
+
// Handle `MixinClass(SomeBase)` - use as mixin function
|
|
812
|
+
apply(_target, _thisArg, args) {
|
|
813
|
+
if (args.length === 0) {
|
|
814
|
+
throw new Error('Mixin requires a base class');
|
|
815
|
+
}
|
|
816
|
+
const baseClass = args[0];
|
|
817
|
+
if (typeof baseClass !== 'function') {
|
|
818
|
+
throw new Error('Mixin requires a constructor function');
|
|
819
|
+
}
|
|
820
|
+
// Check if it's a valid constructor or a mixin
|
|
821
|
+
if (!isConstructor(baseClass) &&
|
|
822
|
+
!(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
|
|
823
|
+
throw new Error('Mixin requires a valid constructor');
|
|
824
|
+
}
|
|
825
|
+
// Check cache first
|
|
826
|
+
const cached = mixinCache.get(baseClass);
|
|
827
|
+
if (cached) {
|
|
828
|
+
return cached;
|
|
829
|
+
}
|
|
830
|
+
let usedBase = baseClass;
|
|
831
|
+
if (unwrapFunction) {
|
|
832
|
+
// Create a proxied base class that handles method unwrapping
|
|
833
|
+
const ProxiedBaseClass = class extends baseClass {
|
|
834
|
+
};
|
|
835
|
+
// Proxy the prototype methods to handle unwrapping
|
|
836
|
+
const originalPrototype = baseClass.prototype;
|
|
837
|
+
const proxiedPrototype = new Proxy(originalPrototype, {
|
|
838
|
+
get(target, prop, receiver) {
|
|
839
|
+
const value = FoolProof.get(target, prop, receiver);
|
|
840
|
+
// Only wrap methods that are likely to access private fields
|
|
841
|
+
// Skip symbols and special properties that the reactive system needs
|
|
842
|
+
if (typeof value === 'function' &&
|
|
843
|
+
typeof prop === 'string' &&
|
|
844
|
+
!['constructor', 'toString', 'valueOf'].includes(prop)) {
|
|
845
|
+
// Return a wrapped version that uses unwrapped context
|
|
846
|
+
return function (...args) {
|
|
847
|
+
// Use the unwrapping function if provided, otherwise use this
|
|
848
|
+
const context = unwrapFunction(this);
|
|
849
|
+
return value.apply(context, args);
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
return value;
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
// Set the proxied prototype
|
|
856
|
+
Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
|
|
857
|
+
usedBase = ProxiedBaseClass;
|
|
858
|
+
}
|
|
859
|
+
// Create the mixed class using the proxied base class
|
|
860
|
+
const mixedClass = mixinFunction(usedBase);
|
|
861
|
+
// Cache the result
|
|
862
|
+
mixinCache.set(baseClass, mixedClass);
|
|
863
|
+
return mixedClass;
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/******************************************************************************
|
|
869
|
+
Copyright (c) Microsoft Corporation.
|
|
870
|
+
|
|
871
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
872
|
+
purpose with or without fee is hereby granted.
|
|
873
|
+
|
|
874
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
875
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
876
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
877
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
878
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
879
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
880
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
881
|
+
***************************************************************************** */
|
|
882
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
886
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
887
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
888
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
889
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
890
|
+
var _, done = false;
|
|
891
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
892
|
+
var context = {};
|
|
893
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
894
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
895
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
896
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
897
|
+
if (kind === "accessor") {
|
|
898
|
+
if (result === void 0) continue;
|
|
899
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
900
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
901
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
902
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
903
|
+
}
|
|
904
|
+
else if (_ = accept(result)) {
|
|
905
|
+
if (kind === "field") initializers.unshift(_);
|
|
906
|
+
else descriptor[key] = _;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
910
|
+
done = true;
|
|
911
|
+
}
|
|
912
|
+
function __runInitializers(thisArg, initializers, value) {
|
|
913
|
+
var useValue = arguments.length > 2;
|
|
914
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
915
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
916
|
+
}
|
|
917
|
+
return useValue ? value : void 0;
|
|
918
|
+
}
|
|
919
|
+
function __setFunctionName(f, name, prefix) {
|
|
920
|
+
if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
|
|
921
|
+
return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
|
|
922
|
+
}
|
|
923
|
+
function __classPrivateFieldGet(receiver, state, kind, f) {
|
|
924
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
925
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
926
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function __classPrivateFieldSet(receiver, state, value, kind, f) {
|
|
930
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
931
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
932
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
933
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
937
|
+
var e = new Error(message);
|
|
938
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
// biome-ignore-all lint/suspicious/noConfusingVoidType: Type 'void' is not assignable to type 'ScopedCallback | undefined'.
|
|
942
|
+
// Argument of type '() => void' is not assignable to parameter of type '(dep: DependencyFunction) => ScopedCallback | undefined'.
|
|
943
|
+
// Track native reactivity
|
|
944
|
+
const nativeReactive = Symbol('native-reactive');
|
|
945
|
+
/**
|
|
946
|
+
* Symbol to mark individual objects as non-reactive
|
|
947
|
+
*/
|
|
948
|
+
const nonReactiveMark = Symbol('non-reactive');
|
|
949
|
+
/**
|
|
950
|
+
* Symbol to mark class properties as non-reactive
|
|
951
|
+
*/
|
|
952
|
+
const unreactiveProperties = Symbol('unreactive-properties');
|
|
953
|
+
/**
|
|
954
|
+
* Symbol representing all properties in reactive tracking
|
|
955
|
+
*/
|
|
956
|
+
const allProps = Symbol('all-props');
|
|
957
|
+
/**
|
|
958
|
+
* Symbol for accessing projection information on reactive objects
|
|
959
|
+
*/
|
|
960
|
+
const projectionInfo = Symbol('projection-info');
|
|
961
|
+
/**
|
|
962
|
+
* Symbol to check if an effect is stopped
|
|
963
|
+
*/
|
|
964
|
+
const stopped = Symbol('stopped');
|
|
965
|
+
/**
|
|
966
|
+
* Symbol to access effect cleanup function
|
|
967
|
+
*/
|
|
968
|
+
const cleanup = Symbol('cleanup');
|
|
969
|
+
// Symbol to mark functions with their root function
|
|
970
|
+
const rootFunction = Symbol('root-function');
|
|
971
|
+
/**
|
|
972
|
+
* Structured error codes for machine-readable diagnosis
|
|
973
|
+
*/
|
|
974
|
+
var ReactiveErrorCode;
|
|
975
|
+
(function (ReactiveErrorCode) {
|
|
976
|
+
ReactiveErrorCode["CycleDetected"] = "CYCLE_DETECTED";
|
|
977
|
+
ReactiveErrorCode["MaxDepthExceeded"] = "MAX_DEPTH_EXCEEDED";
|
|
978
|
+
ReactiveErrorCode["MaxReactionExceeded"] = "MAX_REACTION_EXCEEDED";
|
|
979
|
+
ReactiveErrorCode["WriteInComputed"] = "WRITE_IN_COMPUTED";
|
|
980
|
+
ReactiveErrorCode["TrackingError"] = "TRACKING_ERROR";
|
|
981
|
+
ReactiveErrorCode["BrokenEffects"] = "BROKEN_EFFECTS";
|
|
982
|
+
})(ReactiveErrorCode || (ReactiveErrorCode = {}));
|
|
983
|
+
/**
|
|
984
|
+
* Error class for reactive system errors
|
|
985
|
+
*/
|
|
986
|
+
class ReactiveError extends Error {
|
|
987
|
+
constructor(message, debugInfo) {
|
|
988
|
+
super(message);
|
|
989
|
+
this.debugInfo = debugInfo;
|
|
990
|
+
this.name = 'ReactiveError';
|
|
991
|
+
}
|
|
992
|
+
get code() {
|
|
993
|
+
return this.debugInfo?.code;
|
|
994
|
+
}
|
|
995
|
+
get cause() {
|
|
996
|
+
return this.debugInfo?.cause;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
|
|
1000
|
+
/**
|
|
1001
|
+
* Global options for the reactive system
|
|
1002
|
+
*/
|
|
1003
|
+
const options = {
|
|
1004
|
+
/**
|
|
1005
|
+
* Debug purpose: called when an effect is entered
|
|
1006
|
+
* @param effect - The effect that is entered
|
|
1007
|
+
*/
|
|
1008
|
+
enter: (_effect) => { },
|
|
1009
|
+
/**
|
|
1010
|
+
* Debug purpose: called when an effect is left
|
|
1011
|
+
* @param effect - The effect that is left
|
|
1012
|
+
*/
|
|
1013
|
+
leave: (_effect) => { },
|
|
1014
|
+
/**
|
|
1015
|
+
* Debug purpose: called when an effect is chained
|
|
1016
|
+
* @param target - The effect that is being triggered
|
|
1017
|
+
* @param caller - The effect that is calling the target
|
|
1018
|
+
*/
|
|
1019
|
+
chain: (_targets, _caller) => { },
|
|
1020
|
+
/**
|
|
1021
|
+
* Debug purpose: called when an effect chain is started
|
|
1022
|
+
* @param target - The effect that is being triggered
|
|
1023
|
+
*/
|
|
1024
|
+
beginChain: (_targets) => { },
|
|
1025
|
+
/**
|
|
1026
|
+
* Debug purpose: called when an effect chain is ended
|
|
1027
|
+
*/
|
|
1028
|
+
endChain: () => { },
|
|
1029
|
+
garbageCollected: (_fn) => { },
|
|
1030
|
+
/**
|
|
1031
|
+
* Debug purpose: called when an object is touched
|
|
1032
|
+
* @param obj - The object that is touched
|
|
1033
|
+
* @param evolution - The type of change
|
|
1034
|
+
* @param props - The properties that changed
|
|
1035
|
+
* @param deps - The dependencies that changed
|
|
1036
|
+
*/
|
|
1037
|
+
touched: (_obj, _evolution, _props, _deps) => { },
|
|
1038
|
+
/**
|
|
1039
|
+
* Debug purpose: called when an effect is skipped because it's already running
|
|
1040
|
+
* @param effect - The effect that is already running
|
|
1041
|
+
* @param runningChain - The array of effects from the detected one to the currently running one
|
|
1042
|
+
*/
|
|
1043
|
+
skipRunningEffect: (_effect) => { },
|
|
1044
|
+
/**
|
|
1045
|
+
* Debug purpose: maximum effect chain (like call stack max depth)
|
|
1046
|
+
* Used to prevent infinite loops
|
|
1047
|
+
* @default 100
|
|
1048
|
+
*/
|
|
1049
|
+
maxEffectChain: 100,
|
|
1050
|
+
/**
|
|
1051
|
+
* Maximum number of times an effect can be triggered by the same cause in a single batch
|
|
1052
|
+
* Used to detect aggressive re-computation or infinite loops
|
|
1053
|
+
* @default 10
|
|
1054
|
+
*/
|
|
1055
|
+
maxTriggerPerBatch: 10,
|
|
1056
|
+
/**
|
|
1057
|
+
* Debug purpose: maximum effect reaction (like call stack max depth)
|
|
1058
|
+
* Used to prevent infinite loops
|
|
1059
|
+
* @default 'throw'
|
|
1060
|
+
*/
|
|
1061
|
+
maxEffectReaction: 'throw',
|
|
1062
|
+
/**
|
|
1063
|
+
* Callback called when a memoization discrepancy is detected (debug only)
|
|
1064
|
+
* When defined, memoized functions will run a second time (untracked) to verify consistency.
|
|
1065
|
+
* If the untracked run returns a different value than the cached one, this callback is triggered.
|
|
1066
|
+
*
|
|
1067
|
+
* This is the primary tool for detecting missing reactive dependencies in computed values.
|
|
1068
|
+
*
|
|
1069
|
+
* @param cached - The value currently in the memoization cache
|
|
1070
|
+
* @param fresh - The value obtained by re-running the function untracked
|
|
1071
|
+
* @param fn - The memoized function itself
|
|
1072
|
+
* @param args - Arguments passed to the function
|
|
1073
|
+
*
|
|
1074
|
+
* @example
|
|
1075
|
+
* ```typescript
|
|
1076
|
+
* reactiveOptions.onMemoizationDiscrepancy = (cached, fresh, fn, args) => {
|
|
1077
|
+
* throw new Error(`Memoization discrepancy in ${fn.name}!`);
|
|
1078
|
+
* };
|
|
1079
|
+
* ```
|
|
1080
|
+
*/
|
|
1081
|
+
onMemoizationDiscrepancy: undefined,
|
|
1082
|
+
/**
|
|
1083
|
+
* How to handle cycles detected in effect batches.
|
|
1084
|
+
*
|
|
1085
|
+
* - `'production'` (Default): High-performance mode. Disables dependency graph maintenance and
|
|
1086
|
+
* Topological Sorting in favor of a simple FIFO queue. Use this for trustworthy, acyclic UI code.
|
|
1087
|
+
* Cycle detection is heuristic (uses maxEffectChain execution counts).
|
|
1088
|
+
*
|
|
1089
|
+
* - `'development'`: Maintains direct dependency graph for early cycle detection during edge creation.
|
|
1090
|
+
* Catches cycles before effects execute via DFS check when adding edges. Throws immediately with
|
|
1091
|
+
* basic path information. Good balance of debugging help with moderate overhead.
|
|
1092
|
+
*
|
|
1093
|
+
* - `'debug'`: Full diagnostic mode with transitive closures and topological sorting.
|
|
1094
|
+
* Provides detailed cycle path reporting. Highest overhead but most informative for bug hunting.
|
|
1095
|
+
*
|
|
1096
|
+
* @default 'production'
|
|
1097
|
+
*/
|
|
1098
|
+
cycleHandling: 'development',
|
|
1099
|
+
/**
|
|
1100
|
+
* Internal flag used by memoization discrepancy detector to avoid counting calls in tests
|
|
1101
|
+
* @warning Do not modify this flag manually, this flag is given by the engine
|
|
1102
|
+
*/
|
|
1103
|
+
isVerificationRun: false,
|
|
1104
|
+
/**
|
|
1105
|
+
* Maximum depth for deep watching traversal
|
|
1106
|
+
* Used to prevent infinite recursion in circular references
|
|
1107
|
+
* @default 100
|
|
1108
|
+
*/
|
|
1109
|
+
maxDeepWatchDepth: 100,
|
|
1110
|
+
/**
|
|
1111
|
+
* Only react on instance members modification (not inherited properties)
|
|
1112
|
+
* For instance, do not track class methods
|
|
1113
|
+
* @default true
|
|
1114
|
+
*/
|
|
1115
|
+
instanceMembers: true,
|
|
1116
|
+
/**
|
|
1117
|
+
* Ignore accessors (getters and setters) and only track direct properties
|
|
1118
|
+
* @default true
|
|
1119
|
+
*/
|
|
1120
|
+
ignoreAccessors: true,
|
|
1121
|
+
/**
|
|
1122
|
+
* Enable recursive touching when objects with the same prototype are replaced
|
|
1123
|
+
* When enabled, replacing an object with another of the same prototype triggers
|
|
1124
|
+
* recursive diffing instead of notifying parent effects
|
|
1125
|
+
* @default true
|
|
1126
|
+
*/
|
|
1127
|
+
recursiveTouching: true,
|
|
1128
|
+
/**
|
|
1129
|
+
* Default async execution mode for effects that return Promises
|
|
1130
|
+
* - 'cancel': Cancel previous async execution when dependencies change (default, enables async zone)
|
|
1131
|
+
* - 'queue': Queue next execution to run after current completes (enables async zone)
|
|
1132
|
+
* - 'ignore': Ignore new executions while async work is running (enables async zone)
|
|
1133
|
+
* - false: Disable async zone and async mode handling (effects run concurrently)
|
|
1134
|
+
*
|
|
1135
|
+
* **When truthy:** Enables async zone (Promise.prototype wrapping) for automatic context
|
|
1136
|
+
* preservation in Promise callbacks. Warning: This modifies Promise.prototype globally.
|
|
1137
|
+
* Only enable if no other library modifies Promise.prototype.
|
|
1138
|
+
*
|
|
1139
|
+
* **When false:** Async zone is disabled. Use `tracked()` manually in Promise callbacks.
|
|
1140
|
+
*
|
|
1141
|
+
* Can be overridden per-effect via EffectOptions
|
|
1142
|
+
* @default 'cancel'
|
|
1143
|
+
*/
|
|
1144
|
+
asyncMode: 'cancel',
|
|
1145
|
+
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
1146
|
+
warn: (...args) => console.warn(...args),
|
|
1147
|
+
/**
|
|
1148
|
+
* Configuration for the introspection system
|
|
1149
|
+
*/
|
|
1150
|
+
introspection: {
|
|
1151
|
+
/**
|
|
1152
|
+
* Whether to keep a history of mutations for debugging
|
|
1153
|
+
* @default false
|
|
1154
|
+
*/
|
|
1155
|
+
enableHistory: false,
|
|
1156
|
+
/**
|
|
1157
|
+
* Number of mutations to keep in history
|
|
1158
|
+
* @default 50
|
|
1159
|
+
*/
|
|
1160
|
+
historySize: 50,
|
|
1161
|
+
},
|
|
1162
|
+
/**
|
|
1163
|
+
* Configuration for zone hooks - control which async APIs are hooked
|
|
1164
|
+
* Each option controls whether the corresponding async API is wrapped to preserve effect context
|
|
1165
|
+
* Only applies when asyncMode is enabled (truthy)
|
|
1166
|
+
* @deprecated Should take all when we made sure PIXI.create, Game.create, ... are -> .root()
|
|
1167
|
+
*/
|
|
1168
|
+
zones: {
|
|
1169
|
+
/**
|
|
1170
|
+
* Hook setTimeout to preserve effect context
|
|
1171
|
+
* @default true
|
|
1172
|
+
*/
|
|
1173
|
+
setTimeout: true,
|
|
1174
|
+
/**
|
|
1175
|
+
* Hook setInterval to preserve effect context
|
|
1176
|
+
* @default true
|
|
1177
|
+
*/
|
|
1178
|
+
setInterval: true,
|
|
1179
|
+
/**
|
|
1180
|
+
* Hook requestAnimationFrame (runs in untracked context when hooked)
|
|
1181
|
+
* @default true
|
|
1182
|
+
*/
|
|
1183
|
+
requestAnimationFrame: true,
|
|
1184
|
+
/**
|
|
1185
|
+
* Hook queueMicrotask to preserve effect context
|
|
1186
|
+
* @default true
|
|
1187
|
+
*/
|
|
1188
|
+
queueMicrotask: true,
|
|
1189
|
+
},
|
|
1190
|
+
};
|
|
1191
|
+
function optionCall(name, ...args) {
|
|
1192
|
+
const fn = options[name];
|
|
1193
|
+
if (typeof fn !== 'function')
|
|
1194
|
+
return;
|
|
1195
|
+
try {
|
|
1196
|
+
;
|
|
1197
|
+
fn(...args);
|
|
1198
|
+
}
|
|
1199
|
+
catch (error) {
|
|
1200
|
+
options.warn(`options.${name} threw`, error);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Track which effects are watching which reactive objects for cleanup
|
|
1205
|
+
let effectToReactiveObjects = new WeakMap();
|
|
1206
|
+
// Track effects per reactive object and property
|
|
1207
|
+
let watchers = new WeakMap();
|
|
1208
|
+
// Track effect metadata and relationships
|
|
1209
|
+
let effectNodes = new WeakMap();
|
|
1210
|
+
function getEffectNode(effect) {
|
|
1211
|
+
let node = effectNodes.get(effect);
|
|
1212
|
+
if (!node) {
|
|
1213
|
+
node = {};
|
|
1214
|
+
effectNodes.set(effect, node);
|
|
1215
|
+
}
|
|
1216
|
+
return node;
|
|
1217
|
+
}
|
|
1218
|
+
// Track reverse mapping to ensure unicity: One Root -> One Function
|
|
1219
|
+
let reverseRoots = new WeakMap();
|
|
1220
|
+
function resetRegistry() {
|
|
1221
|
+
effectToReactiveObjects = new WeakMap();
|
|
1222
|
+
watchers = new WeakMap();
|
|
1223
|
+
effectNodes = new WeakMap();
|
|
1224
|
+
reverseRoots = new WeakMap();
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Marks a function with its root function for effect tracking
|
|
1228
|
+
* Enforces strict unicity: A root function can only identify ONE function.
|
|
1229
|
+
* @param fn - The function to mark
|
|
1230
|
+
* @param root - The root function
|
|
1231
|
+
* @returns The marked function
|
|
1232
|
+
*/
|
|
1233
|
+
function markWithRoot(fn, root) {
|
|
1234
|
+
// Check for collision
|
|
1235
|
+
const existingRef = reverseRoots.get(root);
|
|
1236
|
+
const existing = existingRef?.deref();
|
|
1237
|
+
if (existing && existing !== fn) {
|
|
1238
|
+
const rootName = root.name || 'anonymous';
|
|
1239
|
+
const existingName = existing.name || 'anonymous';
|
|
1240
|
+
const fnName = fn.name || 'anonymous';
|
|
1241
|
+
throw new Error(`[reactive] Abusive Shared Root detected: Root '${rootName}' is already identifying function '${existingName}'. ` +
|
|
1242
|
+
`Cannot reuse it for '${fnName}'. Shared roots cause lost updates and broken identity logic.`);
|
|
1243
|
+
}
|
|
1244
|
+
// Always update the map so subsequent checks find this one
|
|
1245
|
+
// (Last writer wins for the check)
|
|
1246
|
+
reverseRoots.set(root, new WeakRef(fn));
|
|
1247
|
+
// Mark fn with the new root
|
|
1248
|
+
return Object.defineProperty(fn, rootFunction, {
|
|
1249
|
+
value: getRoot(root),
|
|
1250
|
+
writable: false,
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Gets the root function of a function for effect tracking
|
|
1255
|
+
* @param fn - The function to get the root of
|
|
1256
|
+
* @returns The root function
|
|
1257
|
+
*/
|
|
1258
|
+
function getRoot(fn) {
|
|
1259
|
+
while (fn && rootFunction in fn)
|
|
1260
|
+
fn = fn[rootFunction];
|
|
1261
|
+
return fn;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
var _ZoneAggregator_zones;
|
|
1265
|
+
function isu(z) {
|
|
1266
|
+
return z;
|
|
1267
|
+
}
|
|
1268
|
+
class AZone {
|
|
1269
|
+
enter(value) {
|
|
1270
|
+
const prev = this.active;
|
|
1271
|
+
this.active = value;
|
|
1272
|
+
return prev;
|
|
1273
|
+
}
|
|
1274
|
+
leave(entered) {
|
|
1275
|
+
this.active = entered;
|
|
1276
|
+
}
|
|
1277
|
+
with(value, fn) {
|
|
1278
|
+
const entered = this.enter(value);
|
|
1279
|
+
let res;
|
|
1280
|
+
try {
|
|
1281
|
+
res = fn();
|
|
1282
|
+
}
|
|
1283
|
+
finally {
|
|
1284
|
+
this.leave(entered);
|
|
1285
|
+
}
|
|
1286
|
+
// [HACK]: Sanitization
|
|
1287
|
+
// See BROWSER_ASYNC_POLYFILL.md
|
|
1288
|
+
return asyncHooks.sanitizePromise(res);
|
|
1289
|
+
}
|
|
1290
|
+
root(fn) {
|
|
1291
|
+
const prev = this.enter();
|
|
1292
|
+
try {
|
|
1293
|
+
return fn();
|
|
1294
|
+
}
|
|
1295
|
+
finally {
|
|
1296
|
+
this.leave(prev);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
get zoned() {
|
|
1300
|
+
const active = this.active;
|
|
1301
|
+
return named(`${this}@${active}`, (fn) => this.with(active, fn));
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
class Zone extends AZone {
|
|
1305
|
+
}
|
|
1306
|
+
class ZoneHistory extends AZone {
|
|
1307
|
+
has(value) {
|
|
1308
|
+
return this.history.has(value);
|
|
1309
|
+
}
|
|
1310
|
+
some(predicate) {
|
|
1311
|
+
for (const value of this.history)
|
|
1312
|
+
if (predicate(value))
|
|
1313
|
+
return true;
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
constructor(controlled = new Zone()) {
|
|
1317
|
+
super();
|
|
1318
|
+
this.controlled = controlled;
|
|
1319
|
+
this.history = new Set();
|
|
1320
|
+
const self = this;
|
|
1321
|
+
this.present = Object.create(controlled, Object.getOwnPropertyDescriptors({
|
|
1322
|
+
get active() {
|
|
1323
|
+
return controlled.active;
|
|
1324
|
+
},
|
|
1325
|
+
set active(value) {
|
|
1326
|
+
controlled.active = value;
|
|
1327
|
+
},
|
|
1328
|
+
enter(value) {
|
|
1329
|
+
if (value && self.history.has(value))
|
|
1330
|
+
throw new Error('ZoneHistory: re-entering historical zone');
|
|
1331
|
+
if (value !== undefined)
|
|
1332
|
+
self.history.add(value);
|
|
1333
|
+
return { added: value, entered: isu(controlled).enter(value) };
|
|
1334
|
+
},
|
|
1335
|
+
leave(entered) {
|
|
1336
|
+
if (entered.added !== undefined)
|
|
1337
|
+
self.history.delete(entered.added);
|
|
1338
|
+
return isu(controlled).leave(entered.entered);
|
|
1339
|
+
},
|
|
1340
|
+
}));
|
|
1341
|
+
}
|
|
1342
|
+
get active() {
|
|
1343
|
+
return { present: this.controlled.active, history: new Set(this.history) };
|
|
1344
|
+
}
|
|
1345
|
+
set active(value) {
|
|
1346
|
+
this.history = value?.history ? new Set(value.history) : new Set();
|
|
1347
|
+
this.controlled.active = value?.present;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
class ZoneAggregator extends AZone {
|
|
1351
|
+
constructor(...zones) {
|
|
1352
|
+
super();
|
|
1353
|
+
_ZoneAggregator_zones.set(this, new Set());
|
|
1354
|
+
for (const z of zones)
|
|
1355
|
+
__classPrivateFieldGet(this, _ZoneAggregator_zones, "f").add(z);
|
|
1356
|
+
}
|
|
1357
|
+
get active() {
|
|
1358
|
+
const rv = new Map();
|
|
1359
|
+
for (const z of __classPrivateFieldGet(this, _ZoneAggregator_zones, "f"))
|
|
1360
|
+
if (z.active !== undefined)
|
|
1361
|
+
rv.set(z, z.active);
|
|
1362
|
+
return rv;
|
|
1363
|
+
}
|
|
1364
|
+
set active(value) {
|
|
1365
|
+
for (const z of __classPrivateFieldGet(this, _ZoneAggregator_zones, "f"))
|
|
1366
|
+
z.active = value?.get(z);
|
|
1367
|
+
}
|
|
1368
|
+
enter(value) {
|
|
1369
|
+
const entered = new Map();
|
|
1370
|
+
for (const z of __classPrivateFieldGet(this, _ZoneAggregator_zones, "f")) {
|
|
1371
|
+
const v = value?.get(z);
|
|
1372
|
+
entered.set(z, isu(z).enter(v));
|
|
1373
|
+
}
|
|
1374
|
+
return entered;
|
|
1375
|
+
}
|
|
1376
|
+
leave(entered) {
|
|
1377
|
+
for (const z of __classPrivateFieldGet(this, _ZoneAggregator_zones, "f"))
|
|
1378
|
+
isu(z).leave(entered.get(z));
|
|
1379
|
+
}
|
|
1380
|
+
add(z) {
|
|
1381
|
+
__classPrivateFieldGet(this, _ZoneAggregator_zones, "f").add(z);
|
|
1382
|
+
}
|
|
1383
|
+
delete(z) {
|
|
1384
|
+
__classPrivateFieldGet(this, _ZoneAggregator_zones, "f").delete(z);
|
|
1385
|
+
}
|
|
1386
|
+
clear() {
|
|
1387
|
+
__classPrivateFieldGet(this, _ZoneAggregator_zones, "f").clear();
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
_ZoneAggregator_zones = new WeakMap();
|
|
1391
|
+
const asyncZone = tag('async', new ZoneAggregator());
|
|
1392
|
+
asyncHooks.addHook(() => {
|
|
1393
|
+
const zone = asyncZone.active;
|
|
1394
|
+
return () => {
|
|
1395
|
+
const prev = asyncZone.active;
|
|
1396
|
+
asyncZone.active = zone;
|
|
1397
|
+
return () => (asyncZone.active = prev);
|
|
1398
|
+
};
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
const effectHistory = tag('effectHistory', new ZoneHistory());
|
|
1402
|
+
tag('effectHistory.present', effectHistory.present);
|
|
1403
|
+
asyncZone.add(effectHistory);
|
|
1404
|
+
/**
|
|
1405
|
+
* Aggregator for zones that need to be tracked along effects.
|
|
1406
|
+
* ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
|
|
1407
|
+
*/
|
|
1408
|
+
const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
|
|
1409
|
+
function isRunning(effect) {
|
|
1410
|
+
const root = getRoot(effect);
|
|
1411
|
+
return effectHistory.some((e) => getRoot(e) === root);
|
|
1412
|
+
}
|
|
1413
|
+
function getActiveEffect() {
|
|
1414
|
+
return effectHistory.present.active;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* ADD a cleanup function to an object using the cleanup symbol.
|
|
1418
|
+
* The cleanup function will be called when the object needs to be disposed.
|
|
1419
|
+
*
|
|
1420
|
+
* Note: most of the time, you don't need to use this function directly.
|
|
1421
|
+
* The main use if for the cleanup function to be stored with the object, as GC calls the cleanup function when the *function* is garbage collected.
|
|
1422
|
+
*
|
|
1423
|
+
* @param obj - The object to attach the cleanup function to
|
|
1424
|
+
* @param cleanupFn - The cleanup function to attach
|
|
1425
|
+
* @returns The object with the cleanup function attached
|
|
1426
|
+
*/
|
|
1427
|
+
function cleanedBy(obj, cleanupFn) {
|
|
1428
|
+
const oldCleanup = obj[cleanup];
|
|
1429
|
+
return Object.defineProperty(obj, cleanup, {
|
|
1430
|
+
value: oldCleanup
|
|
1431
|
+
? Object.defineProperties(() => {
|
|
1432
|
+
oldCleanup();
|
|
1433
|
+
cleanupFn();
|
|
1434
|
+
}, {
|
|
1435
|
+
[stopped]: { get: () => oldCleanup[stopped] || cleanupFn[stopped] },
|
|
1436
|
+
})
|
|
1437
|
+
: cleanupFn,
|
|
1438
|
+
writable: false,
|
|
1439
|
+
enumerable: false,
|
|
1440
|
+
configurable: true,
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
//#region greedy caching
|
|
1444
|
+
/**
|
|
1445
|
+
* Creates a derived value that automatically recomputes when dependencies change
|
|
1446
|
+
* @param compute - Function that computes the derived value
|
|
1447
|
+
* @returns Object with value and cleanup function
|
|
1448
|
+
*/
|
|
1449
|
+
function derived(compute) {
|
|
1450
|
+
const rv = { value: undefined };
|
|
1451
|
+
return cleanedBy(rv, untracked(() => effect(function derivedEffect(access) {
|
|
1452
|
+
rv.value = compute(access);
|
|
1453
|
+
})));
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
const effectMarker = {
|
|
1457
|
+
enter: 'effect:enter',
|
|
1458
|
+
leave: 'effect:leave'
|
|
1459
|
+
};
|
|
1460
|
+
/**
|
|
1461
|
+
* Parses a single stack line into a structured frame.
|
|
1462
|
+
* @param line - A line from Error.stack
|
|
1463
|
+
*/
|
|
1464
|
+
function parseStackLine(line) {
|
|
1465
|
+
// Node.js format: " at functionName (file:line:column)"
|
|
1466
|
+
// or " at file:line:column"
|
|
1467
|
+
const nodeMatch = line.match(/^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+))\)?$/);
|
|
1468
|
+
// Browser format (Firefox/Safari): "functionName@file:line:column" or "file:line:column"
|
|
1469
|
+
// Chrome/Edge use Node-like format but without "at" sometimes or with different prefixes
|
|
1470
|
+
const browserMatch = line.match(/(?:(.+?)(?:\@|\(?))?(?:(.+?):(\d+):(\d+))(?:\@|\)?)$/);
|
|
1471
|
+
const match = nodeMatch || browserMatch;
|
|
1472
|
+
if (!match)
|
|
1473
|
+
return null;
|
|
1474
|
+
const [, functionName = 'anonymous', fileName, lineNumber, columnNumber] = match;
|
|
1475
|
+
return {
|
|
1476
|
+
functionName: functionName.trim(),
|
|
1477
|
+
fileName,
|
|
1478
|
+
lineNumber: parseInt(lineNumber, 10),
|
|
1479
|
+
columnNumber: parseInt(columnNumber, 10),
|
|
1480
|
+
raw: line.trim(),
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
let internalFile;
|
|
1484
|
+
/**
|
|
1485
|
+
* Gets the current call stack
|
|
1486
|
+
* @param skipFrames - Number of frames to skip
|
|
1487
|
+
* @param error - Optional error to use as source of stack
|
|
1488
|
+
*/
|
|
1489
|
+
function getStackFrame(error = new Error()) {
|
|
1490
|
+
if (!error.stack)
|
|
1491
|
+
return [];
|
|
1492
|
+
const lines = error.stack.split('\n');
|
|
1493
|
+
const lastLine = lines.findIndex((line) => line.includes(effectMarker.enter));
|
|
1494
|
+
if (lastLine !== -1)
|
|
1495
|
+
lines.splice(lastLine);
|
|
1496
|
+
const firstLine = lines.findLastIndex((line) => line.includes(effectMarker.leave));
|
|
1497
|
+
if (firstLine !== -1)
|
|
1498
|
+
lines.splice(0, firstLine + 1);
|
|
1499
|
+
else {
|
|
1500
|
+
// Dynamically identify the library's internal files if not already done
|
|
1501
|
+
if (!internalFile && lines[1]) {
|
|
1502
|
+
const selfFrame = parseStackLine(lines[1]);
|
|
1503
|
+
if (selfFrame) {
|
|
1504
|
+
internalFile = selfFrame.fileName;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
// Determine the "base" directory of the library to skip other internal files
|
|
1508
|
+
// We look for "src" or "dist" to be more specific than just the project root
|
|
1509
|
+
const srcIndex = internalFile ? internalFile.lastIndexOf('/src/') : -1;
|
|
1510
|
+
const distIndex = internalFile ? internalFile.lastIndexOf('/dist/') : -1;
|
|
1511
|
+
internalFile ? (srcIndex !== -1 ? internalFile.substring(0, srcIndex + 5) :
|
|
1512
|
+
(distIndex !== -1 ? internalFile.substring(0, distIndex + 6) :
|
|
1513
|
+
internalFile.substring(0, internalFile.lastIndexOf('/') + 1))) : undefined;
|
|
1514
|
+
let l;
|
|
1515
|
+
for (l = 1; l < lines.length; l++) {
|
|
1516
|
+
const frame = parseStackLine(lines[l]);
|
|
1517
|
+
if (!frame)
|
|
1518
|
+
continue;
|
|
1519
|
+
// Robust skipping: if we are still in the internal area, skip it.
|
|
1520
|
+
const isInternal = /Lineage$/.test(frame.functionName) ||
|
|
1521
|
+
frame.functionName === 'eval' ||
|
|
1522
|
+
[`getStackFrame`, `captureLineage`].includes(frame.functionName);
|
|
1523
|
+
if (!isInternal)
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
lines.splice(0, l);
|
|
1527
|
+
}
|
|
1528
|
+
return lines.map(parseStackLine).filter(Boolean);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Traces the lineage of the current execution through nested effects
|
|
1532
|
+
* @param effect - Starting effect (defaults to active effect)
|
|
1533
|
+
*/
|
|
1534
|
+
function getLineage(effect) {
|
|
1535
|
+
const currentEffect = effect ?? getActiveEffect();
|
|
1536
|
+
const currentStack = getStackFrame(); // Robustly skips internal mutts frames
|
|
1537
|
+
const segments = [];
|
|
1538
|
+
if (!currentEffect) {
|
|
1539
|
+
segments.push({
|
|
1540
|
+
effectName: 'root',
|
|
1541
|
+
stack: filterNodeModules(currentStack),
|
|
1542
|
+
});
|
|
1543
|
+
return segments;
|
|
1544
|
+
}
|
|
1545
|
+
let current = currentEffect;
|
|
1546
|
+
let lastStack = currentStack;
|
|
1547
|
+
while (current) {
|
|
1548
|
+
const rootFn = getRoot(current);
|
|
1549
|
+
if (!rootFn.name)
|
|
1550
|
+
debugger;
|
|
1551
|
+
const effectName = rootFn.name || 'anonymous';
|
|
1552
|
+
const filteredStack = filterNodeModules(lastStack);
|
|
1553
|
+
segments.push({
|
|
1554
|
+
effectName,
|
|
1555
|
+
stack: filteredStack,
|
|
1556
|
+
});
|
|
1557
|
+
// Move to parent
|
|
1558
|
+
const node = getEffectNode(current);
|
|
1559
|
+
const parent = node.parent;
|
|
1560
|
+
const creationStack = getEffectNode(current).creationStack;
|
|
1561
|
+
if (parent) {
|
|
1562
|
+
current = parent;
|
|
1563
|
+
lastStack = creationStack ?? [];
|
|
1564
|
+
}
|
|
1565
|
+
else if (creationStack) {
|
|
1566
|
+
segments.push({
|
|
1567
|
+
effectName: 'root',
|
|
1568
|
+
stack: filterNodeModules(creationStack),
|
|
1569
|
+
});
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
else {
|
|
1573
|
+
break;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return segments;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Filters out node_modules frames and groups them
|
|
1580
|
+
* @param frames - Array of stack frames
|
|
1581
|
+
*/
|
|
1582
|
+
function filterNodeModules(frames) {
|
|
1583
|
+
const result = [];
|
|
1584
|
+
let inNodeModules = false;
|
|
1585
|
+
for (const frame of frames) {
|
|
1586
|
+
const isNodeModule = frame.fileName.includes('/node_modules/');
|
|
1587
|
+
if (isNodeModule && !inNodeModules) {
|
|
1588
|
+
// Start of node_modules block
|
|
1589
|
+
inNodeModules = true;
|
|
1590
|
+
result.push({
|
|
1591
|
+
functionName: '...node_modules...',
|
|
1592
|
+
fileName: '[filtered]',
|
|
1593
|
+
lineNumber: 0,
|
|
1594
|
+
columnNumber: 0,
|
|
1595
|
+
raw: ' at ...node_modules...'
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
else if (!isNodeModule && inNodeModules) {
|
|
1599
|
+
// End of node_modules block
|
|
1600
|
+
inNodeModules = false;
|
|
1601
|
+
result.push(frame);
|
|
1602
|
+
}
|
|
1603
|
+
else if (!isNodeModule) {
|
|
1604
|
+
// Regular frame
|
|
1605
|
+
result.push(frame);
|
|
1606
|
+
}
|
|
1607
|
+
// Skip frames inside node_modules
|
|
1608
|
+
}
|
|
1609
|
+
return result;
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Formats lineage segments into a single stack-like string
|
|
1613
|
+
* @param segments - Lineage segments
|
|
1614
|
+
*/
|
|
1615
|
+
function formatLineage(segments) {
|
|
1616
|
+
const result = [];
|
|
1617
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1618
|
+
const segment = segments[i];
|
|
1619
|
+
if (i > 0) {
|
|
1620
|
+
result.push(` --- effect: ${segments[i - 1].effectName} ---`);
|
|
1621
|
+
}
|
|
1622
|
+
for (const frame of segment.stack) {
|
|
1623
|
+
result.push(` ${frame.raw}`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return result.join('\n');
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Logs lineage segments to console with grouping
|
|
1630
|
+
* @param segments - Lineage segments
|
|
1631
|
+
*/
|
|
1632
|
+
function nodeLineage(segments) {
|
|
1633
|
+
console.groupCollapsed(`🦴 Effect Lineage Trace (${segments.length} segment${segments.length === 1 ? '' : 's'})`);
|
|
1634
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1635
|
+
const segment = segments[i];
|
|
1636
|
+
// Add segment header
|
|
1637
|
+
const isLast = i === segments.length - 1;
|
|
1638
|
+
const prefix = i === 0 ? '📍' : isLast ? '└─' : '├─';
|
|
1639
|
+
console.groupCollapsed(`${prefix} Effect: ${segment.effectName}`);
|
|
1640
|
+
// Add stack frames
|
|
1641
|
+
for (let j = 0; j < segment.stack.length; j++) {
|
|
1642
|
+
const frame = segment.stack[j];
|
|
1643
|
+
const isLastFrame = j === segment.stack.length - 1;
|
|
1644
|
+
const framePrefix = isLastFrame ? '└─' : '├─';
|
|
1645
|
+
if (frame.functionName === '...node_modules...') {
|
|
1646
|
+
console.log(`%c${framePrefix} ${frame.functionName}`, 'color: #888; font-style: italic;');
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
const fnStyle = frame.functionName === 'anonymous' ? 'color: #888;' : 'color: #1a7f37; font-weight: bold;';
|
|
1650
|
+
const fileStyle = 'color: #0550ae;';
|
|
1651
|
+
console.log(`%c${framePrefix} %c${frame.functionName} %c(${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})`, 'color: #888;', fnStyle, fileStyle);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
console.groupEnd();
|
|
1655
|
+
}
|
|
1656
|
+
console.groupEnd();
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Legacy version of nodeLineage that returns a formatted string (useful for comparison or logs)
|
|
1660
|
+
* @param segments - Lineage segments
|
|
1661
|
+
* @deprecated TODO: remove me and all the code running around
|
|
1662
|
+
*/
|
|
1663
|
+
function nodeLineageLegacy(segments) {
|
|
1664
|
+
// ANSI color codes for Node.js terminal
|
|
1665
|
+
const colors = {
|
|
1666
|
+
reset: '\x1b[0m',
|
|
1667
|
+
bright: '\x1b[1m',
|
|
1668
|
+
dim: '\x1b[2m',
|
|
1669
|
+
green: '\x1b[32m',
|
|
1670
|
+
yellow: '\x1b[33m',
|
|
1671
|
+
blue: '\x1b[34m',
|
|
1672
|
+
magenta: '\x1b[35m',
|
|
1673
|
+
cyan: '\x1b[36m',
|
|
1674
|
+
gray: '\x1b[90m',
|
|
1675
|
+
};
|
|
1676
|
+
const result = [];
|
|
1677
|
+
result.push(`${colors.bright}${colors.cyan}🦴 Effect Lineage Trace (${segments.length} segments)${colors.reset}`);
|
|
1678
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1679
|
+
const segment = segments[i];
|
|
1680
|
+
const isLast = i === segments.length - 1;
|
|
1681
|
+
const prefix = i === 0 ? '📍' : isLast ? '└─' : '├─';
|
|
1682
|
+
const connector = isLast ? ' ' : '│ ';
|
|
1683
|
+
result.push(`${colors.gray}${connector}${colors.reset}${colors.bright}${colors.magenta}${prefix} Effect: ${segment.effectName}${colors.reset}`);
|
|
1684
|
+
for (let j = 0; j < segment.stack.length; j++) {
|
|
1685
|
+
const frame = segment.stack[j];
|
|
1686
|
+
const framePrefix = j === segment.stack.length - 1 ? ' └─' : ' ├─';
|
|
1687
|
+
if (frame.functionName === '...node_modules...') {
|
|
1688
|
+
result.push(`${colors.gray} ${connector}${colors.reset}${colors.dim}${framePrefix} ${colors.yellow}${frame.functionName}${colors.reset}`);
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
const fnColor = frame.functionName === 'anonymous' ? colors.gray : colors.green;
|
|
1692
|
+
result.push(`${colors.gray} ${connector}${colors.reset}${colors.dim}${framePrefix}${colors.reset} ${fnColor}${frame.functionName}${colors.reset} ${colors.gray}(${colors.reset}${colors.blue}${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}${colors.reset}${colors.gray})${colors.reset}`);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (i < segments.length - 1)
|
|
1696
|
+
result.push('');
|
|
1697
|
+
}
|
|
1698
|
+
return result.join('\n');
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Captures and logs lineage to console
|
|
1702
|
+
*/
|
|
1703
|
+
function captureNodeLineage() {
|
|
1704
|
+
nodeLineage(getLineage());
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Custom formatter for Chrome DevTools to render lineage data nicely.
|
|
1708
|
+
*/
|
|
1709
|
+
const lineageFormatter = {
|
|
1710
|
+
header: (obj) => {
|
|
1711
|
+
if (obj && obj.__isLineage__) {
|
|
1712
|
+
// Try to detect DevTools theme - default to dark colors if uncertain
|
|
1713
|
+
const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
|
1714
|
+
const headerColor = isDark ? '#cd9d5d' : '#704214';
|
|
1715
|
+
return [
|
|
1716
|
+
'span',
|
|
1717
|
+
{ style: `color: ${headerColor}; font-weight: bold;` },
|
|
1718
|
+
`🦴 Effect Lineage (${obj.segments.length} segments)`,
|
|
1719
|
+
];
|
|
1720
|
+
}
|
|
1721
|
+
return null;
|
|
1722
|
+
},
|
|
1723
|
+
hasBody: (obj) => obj && obj.__isLineage__,
|
|
1724
|
+
body: (obj) => {
|
|
1725
|
+
if (!obj || !obj.__isLineage__)
|
|
1726
|
+
return null;
|
|
1727
|
+
const segments = obj.segments;
|
|
1728
|
+
// Try to detect DevTools theme
|
|
1729
|
+
const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
|
1730
|
+
const colors = isDark ? {
|
|
1731
|
+
frameText: '#ccc',
|
|
1732
|
+
functionName: '#ffffff',
|
|
1733
|
+
segmentBg: '#2d2d2d',
|
|
1734
|
+
} : {
|
|
1735
|
+
frameText: '#555',
|
|
1736
|
+
functionName: '#222',
|
|
1737
|
+
segmentBg: '#eee',
|
|
1738
|
+
};
|
|
1739
|
+
const children = segments.map((segment, i) => {
|
|
1740
|
+
const frames = segment.stack.map((frame) => [
|
|
1741
|
+
'div',
|
|
1742
|
+
{ style: `margin-left: 20px; color: ${colors.frameText}; font-family: monospace; font-size: 11px;` },
|
|
1743
|
+
['span', { style: `color: ${colors.functionName};` }, `at ${frame.functionName} `],
|
|
1744
|
+
['span', {}, `${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`],
|
|
1745
|
+
]);
|
|
1746
|
+
const segmentHeader = [
|
|
1747
|
+
'div',
|
|
1748
|
+
{ style: `margin-top: 5px; padding: 2px 5px; background: ${colors.segmentBg}; border-radius: 3px; font-weight: bold;` },
|
|
1749
|
+
i === 0 ? `📍 Current: ${segment.effectName}` : `↖ Effect: ${segment.effectName}`,
|
|
1750
|
+
];
|
|
1751
|
+
return ['div', {}, segmentHeader, ...frames];
|
|
1752
|
+
});
|
|
1753
|
+
return ['div', { style: 'padding: 5px; line-height: 1.4;' }, ...children];
|
|
1754
|
+
},
|
|
1755
|
+
};
|
|
1756
|
+
/**
|
|
1757
|
+
* Wraps lineage data in a way that the Chrome Formatter can recognize.
|
|
1758
|
+
*/
|
|
1759
|
+
function wrapLineageForDebug(segments) {
|
|
1760
|
+
return {
|
|
1761
|
+
__isLineage__: true,
|
|
1762
|
+
segments,
|
|
1763
|
+
toString: () => formatLineage(segments),
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// Track which objects contain which other objects (back-references)
|
|
1768
|
+
const objectParents = new WeakMap();
|
|
1769
|
+
// Track which objects have deep watchers
|
|
1770
|
+
const objectsWithDeepWatchers = new WeakSet();
|
|
1771
|
+
// Track deep watchers per object
|
|
1772
|
+
const deepWatchers = new WeakMap();
|
|
1773
|
+
// Track which effects are doing deep watching
|
|
1774
|
+
const effectToDeepWatchedObjects = new WeakMap();
|
|
1775
|
+
/**
|
|
1776
|
+
* Add a back-reference from child to parent
|
|
1777
|
+
*/
|
|
1778
|
+
function addBackReference(child, parent, prop) {
|
|
1779
|
+
let parents = objectParents.get(child);
|
|
1780
|
+
if (!parents) {
|
|
1781
|
+
parents = new Set();
|
|
1782
|
+
objectParents.set(child, parents);
|
|
1783
|
+
}
|
|
1784
|
+
parents.add({ parent, prop });
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Remove a back-reference from child to parent
|
|
1788
|
+
*/
|
|
1789
|
+
function removeBackReference(child, parent, prop) {
|
|
1790
|
+
const parents = objectParents.get(child);
|
|
1791
|
+
if (parents) {
|
|
1792
|
+
for (const entry of parents) {
|
|
1793
|
+
if (entry.parent === parent && entry.prop === prop) {
|
|
1794
|
+
parents.delete(entry);
|
|
1795
|
+
break;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (parents.size === 0) {
|
|
1799
|
+
objectParents.delete(child);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Check if an object needs back-references (has deep watchers or parents with deep watchers)
|
|
1805
|
+
*/
|
|
1806
|
+
function needsBackReferences(obj) {
|
|
1807
|
+
// Fast path: check if object itself has deep watchers
|
|
1808
|
+
if (objectsWithDeepWatchers.has(obj))
|
|
1809
|
+
return true;
|
|
1810
|
+
// Slow path: check if any parent has deep watchers (recursive)
|
|
1811
|
+
return hasParentWithDeepWatchers(obj);
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Bubble up changes through the back-reference chain
|
|
1815
|
+
*/
|
|
1816
|
+
function bubbleUpChange(changedObject, evolution) {
|
|
1817
|
+
const parents = objectParents.get(changedObject);
|
|
1818
|
+
if (!parents)
|
|
1819
|
+
return;
|
|
1820
|
+
for (const { parent } of parents) {
|
|
1821
|
+
// Trigger deep watchers on parent
|
|
1822
|
+
const parentDeepWatchers = deepWatchers.get(parent);
|
|
1823
|
+
if (parentDeepWatchers)
|
|
1824
|
+
for (const watcher of parentDeepWatchers)
|
|
1825
|
+
batch(watcher);
|
|
1826
|
+
// Continue bubbling up
|
|
1827
|
+
bubbleUpChange(parent);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
function hasParentWithDeepWatchers(obj) {
|
|
1831
|
+
const parents = objectParents.get(obj);
|
|
1832
|
+
if (!parents)
|
|
1833
|
+
return false;
|
|
1834
|
+
for (const { parent } of parents) {
|
|
1835
|
+
if (objectsWithDeepWatchers.has(parent))
|
|
1836
|
+
return true;
|
|
1837
|
+
if (hasParentWithDeepWatchers(parent))
|
|
1838
|
+
return true;
|
|
1839
|
+
}
|
|
1840
|
+
return false;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const objectToProxy = new WeakMap();
|
|
1844
|
+
const proxyToObject = new WeakMap();
|
|
1845
|
+
function storeProxyRelationship(target, proxy) {
|
|
1846
|
+
objectToProxy.set(target, proxy);
|
|
1847
|
+
proxyToObject.set(proxy, target);
|
|
1848
|
+
}
|
|
1849
|
+
function getExistingProxy(target) {
|
|
1850
|
+
return objectToProxy.get(target);
|
|
1851
|
+
}
|
|
1852
|
+
function trackProxyObject(proxy, target) {
|
|
1853
|
+
proxyToObject.set(proxy, target);
|
|
1854
|
+
}
|
|
1855
|
+
function unwrap(obj) {
|
|
1856
|
+
let current = obj;
|
|
1857
|
+
while (current && typeof current === 'object' && current !== null && proxyToObject.has(current)) {
|
|
1858
|
+
current = proxyToObject.get(current);
|
|
1859
|
+
}
|
|
1860
|
+
return current;
|
|
1861
|
+
}
|
|
1862
|
+
function isReactive(obj) {
|
|
1863
|
+
return proxyToObject.has(obj);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const states = new WeakMap();
|
|
1867
|
+
function addState(obj, evolution) {
|
|
1868
|
+
obj = unwrap(obj);
|
|
1869
|
+
const next = {};
|
|
1870
|
+
const state = getState(obj);
|
|
1871
|
+
if (state)
|
|
1872
|
+
Object.assign(state, { evolution, next });
|
|
1873
|
+
states.set(obj, next);
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Gets the current state of a reactive object for evolution tracking
|
|
1877
|
+
* @param obj - The reactive object
|
|
1878
|
+
* @returns The current state object
|
|
1879
|
+
*/
|
|
1880
|
+
function getState(obj) {
|
|
1881
|
+
obj = unwrap(obj);
|
|
1882
|
+
let state = states.get(obj);
|
|
1883
|
+
if (!state) {
|
|
1884
|
+
state = {};
|
|
1885
|
+
states.set(obj, state);
|
|
1886
|
+
}
|
|
1887
|
+
return state;
|
|
1888
|
+
}
|
|
1889
|
+
function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
|
|
1890
|
+
const sourceEffect = getActiveEffect();
|
|
1891
|
+
for (const keys of keyChains)
|
|
1892
|
+
for (const key of keys) {
|
|
1893
|
+
const deps = objectWatchers.get(key);
|
|
1894
|
+
// console.log(`[DEBUG] collectEffects: checking ${String(key)}. Found deps: ${deps ? deps.size : 'none'}`)
|
|
1895
|
+
if (deps)
|
|
1896
|
+
for (const effect of deps) {
|
|
1897
|
+
// console.log(`[DEBUG] collectEffects: found dependency ${effect.name || 'anonymous'} for ${String(key)}`)
|
|
1898
|
+
const runningChain = isRunning(effect);
|
|
1899
|
+
if (runningChain) {
|
|
1900
|
+
optionCall('skipRunningEffect', effect);
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
if (!effects.has(effect)) {
|
|
1904
|
+
effects.add(effect);
|
|
1905
|
+
if (!hasBatched(effect))
|
|
1906
|
+
recordActivation(effect, obj, evolution, key);
|
|
1907
|
+
}
|
|
1908
|
+
recordTriggerLink(sourceEffect, effect, obj, key, evolution);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Triggers effects for a single property change
|
|
1914
|
+
* @param obj - The object that changed
|
|
1915
|
+
* @param evolution - The type of change
|
|
1916
|
+
* @param prop - The property that changed
|
|
1917
|
+
*/
|
|
1918
|
+
function touched1(obj, evolution, prop) {
|
|
1919
|
+
touched(obj, evolution, [prop]);
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Triggers effects for property changes
|
|
1923
|
+
* @param obj - The object that changed
|
|
1924
|
+
* @param evolution - The type of change
|
|
1925
|
+
* @param props - The properties that changed
|
|
1926
|
+
*/
|
|
1927
|
+
function touched(obj, evolution, props) {
|
|
1928
|
+
obj = unwrap(obj);
|
|
1929
|
+
addState(obj, evolution);
|
|
1930
|
+
const objectWatchers = watchers.get(obj);
|
|
1931
|
+
if (objectWatchers) {
|
|
1932
|
+
// Note: we have to collect effects to remove duplicates in the specific case when no batch is running
|
|
1933
|
+
const effects = new Set();
|
|
1934
|
+
if (props)
|
|
1935
|
+
collectEffects(obj, evolution, effects, objectWatchers, [allProps], props);
|
|
1936
|
+
else
|
|
1937
|
+
collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
|
|
1938
|
+
optionCall('touched', obj, evolution, props, effects);
|
|
1939
|
+
batch(Array.from(effects));
|
|
1940
|
+
}
|
|
1941
|
+
// Bubble up changes if this object has deep watchers
|
|
1942
|
+
if (objectsWithDeepWatchers.has(obj)) {
|
|
1943
|
+
bubbleUpChange(obj);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Triggers only opaque effects for property changes
|
|
1948
|
+
* Used by deep-touch to ensure opaque listeners are notified even when deep optimization is active
|
|
1949
|
+
*/
|
|
1950
|
+
function touchedOpaque(obj, evolution, prop) {
|
|
1951
|
+
obj = unwrap(obj);
|
|
1952
|
+
const objectWatchers = watchers.get(obj);
|
|
1953
|
+
if (!objectWatchers)
|
|
1954
|
+
return;
|
|
1955
|
+
const deps = objectWatchers.get(prop);
|
|
1956
|
+
if (!deps)
|
|
1957
|
+
return;
|
|
1958
|
+
const effects = new Set();
|
|
1959
|
+
const sourceEffect = getActiveEffect();
|
|
1960
|
+
for (const effect of deps) {
|
|
1961
|
+
const node = getEffectNode(effect);
|
|
1962
|
+
if (!node.isOpaque)
|
|
1963
|
+
continue;
|
|
1964
|
+
const runningChain = isRunning(effect);
|
|
1965
|
+
if (runningChain) {
|
|
1966
|
+
optionCall('skipRunningEffect', effect);
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
effects.add(effect);
|
|
1970
|
+
recordActivation(effect, obj, evolution, prop);
|
|
1971
|
+
recordTriggerLink(sourceEffect, effect, obj, prop, evolution);
|
|
1972
|
+
}
|
|
1973
|
+
if (effects.size > 0) {
|
|
1974
|
+
optionCall('touched', obj, evolution, [prop], effects);
|
|
1975
|
+
batch(Array.from(effects));
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
const nonReactiveObjects = new WeakSet();
|
|
1980
|
+
const immutables = new Set();
|
|
1981
|
+
const absent = Symbol('absent');
|
|
1982
|
+
function markNonReactive(...obj) {
|
|
1983
|
+
for (const o of obj) {
|
|
1984
|
+
try {
|
|
1985
|
+
Object.defineProperty(o, nonReactiveMark, {
|
|
1986
|
+
value: true,
|
|
1987
|
+
writable: false,
|
|
1988
|
+
enumerable: false,
|
|
1989
|
+
configurable: false,
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
catch { }
|
|
1993
|
+
if (!(nonReactiveMark in o))
|
|
1994
|
+
nonReactiveObjects.add(o);
|
|
1995
|
+
}
|
|
1996
|
+
return obj[0];
|
|
1997
|
+
}
|
|
1998
|
+
function nonReactiveClass(...cls) {
|
|
1999
|
+
for (const c of cls)
|
|
2000
|
+
if (c)
|
|
2001
|
+
c.prototype[nonReactiveMark] = true;
|
|
2002
|
+
return cls[0];
|
|
2003
|
+
}
|
|
2004
|
+
function isNonReactive(obj) {
|
|
2005
|
+
if (obj === null || typeof obj !== 'object')
|
|
2006
|
+
return true;
|
|
2007
|
+
if (nonReactiveObjects.has(obj))
|
|
2008
|
+
return true;
|
|
2009
|
+
if (obj[nonReactiveMark])
|
|
2010
|
+
return true;
|
|
2011
|
+
for (const fn of immutables)
|
|
2012
|
+
if (fn(obj))
|
|
2013
|
+
return true;
|
|
2014
|
+
return false;
|
|
2015
|
+
}
|
|
2016
|
+
function registerNativeReactivity(originalClass, reactiveClass) {
|
|
2017
|
+
originalClass.prototype[nativeReactive] = reactiveClass;
|
|
2018
|
+
nonReactiveClass(reactiveClass);
|
|
2019
|
+
}
|
|
2020
|
+
nonReactiveClass(Date, RegExp, Error, Promise, Function);
|
|
2021
|
+
if (typeof window !== 'undefined') {
|
|
2022
|
+
markNonReactive(window, document);
|
|
2023
|
+
nonReactiveClass(Node, Element, HTMLElement, EventTarget);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function isObject(value) {
|
|
2027
|
+
return typeof value === 'object' && value !== null;
|
|
2028
|
+
}
|
|
2029
|
+
function isObjectLike(value) {
|
|
2030
|
+
return isObject(value);
|
|
2031
|
+
}
|
|
2032
|
+
function getPrototypeToken(value) {
|
|
2033
|
+
if (!isObjectLike(value))
|
|
2034
|
+
return undefined;
|
|
2035
|
+
if (Array.isArray(value))
|
|
2036
|
+
return Array.prototype;
|
|
2037
|
+
try {
|
|
2038
|
+
return value.constructor;
|
|
2039
|
+
}
|
|
2040
|
+
catch {
|
|
2041
|
+
return undefined;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function shouldRecurseTouch(oldValue, newValue) {
|
|
2045
|
+
if (oldValue === newValue)
|
|
2046
|
+
return false;
|
|
2047
|
+
if (!isObjectLike(oldValue) || !isObjectLike(newValue))
|
|
2048
|
+
return false;
|
|
2049
|
+
if (isNonReactive(oldValue) || isNonReactive(newValue))
|
|
2050
|
+
return false;
|
|
2051
|
+
return getPrototypeToken(oldValue) === getPrototypeToken(newValue);
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Centralized function to handle property change notifications with optional recursive touch
|
|
2055
|
+
* @param targetObj - The object whose property changed
|
|
2056
|
+
* @param prop - The property that changed
|
|
2057
|
+
* @param oldValue - The old value (before change)
|
|
2058
|
+
* @param newValue - The new value (after change)
|
|
2059
|
+
* @param hadProperty - Whether the property existed before (for add vs set)
|
|
2060
|
+
*/
|
|
2061
|
+
function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty) {
|
|
2062
|
+
const evolution = { type: hadProperty ? 'set' : 'add', prop };
|
|
2063
|
+
if (options.recursiveTouching &&
|
|
2064
|
+
oldValue !== undefined &&
|
|
2065
|
+
shouldRecurseTouch(oldValue, newValue)) {
|
|
2066
|
+
const unwrappedObj = unwrap(targetObj);
|
|
2067
|
+
const origin = { obj: unwrappedObj, prop };
|
|
2068
|
+
// Deep touch: only notify nested property changes with origin filtering
|
|
2069
|
+
// Don't notify direct property change - the whole point is to avoid parent effects re-running
|
|
2070
|
+
dispatchNotifications(recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
2071
|
+
// Notify opaque listeners (like memoize) that always want to know about identity changes
|
|
2072
|
+
touchedOpaque(targetObj, evolution, prop);
|
|
2073
|
+
}
|
|
2074
|
+
else {
|
|
2075
|
+
touched1(targetObj, evolution, prop);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
function hasVisitedPair(visited, oldObj, newObj) {
|
|
2079
|
+
let mapped = visited.get(oldObj);
|
|
2080
|
+
if (!mapped) {
|
|
2081
|
+
mapped = new WeakSet();
|
|
2082
|
+
visited.set(oldObj, mapped);
|
|
2083
|
+
}
|
|
2084
|
+
if (mapped.has(newObj))
|
|
2085
|
+
return true;
|
|
2086
|
+
mapped.add(newObj);
|
|
2087
|
+
return false;
|
|
2088
|
+
}
|
|
2089
|
+
function collectObjectKeys(obj) {
|
|
2090
|
+
const keys = new Set(Reflect.ownKeys(obj));
|
|
2091
|
+
let proto = Object.getPrototypeOf(obj);
|
|
2092
|
+
// Continue walking while prototype exists and doesn't have its own constructor
|
|
2093
|
+
// This stops at Object.prototype (has own constructor) and class prototypes (have own constructor)
|
|
2094
|
+
// but continues for data prototypes (Object.create({}), Object.create(instance), etc.)
|
|
2095
|
+
while (proto && !Object.hasOwn(proto, 'constructor')) {
|
|
2096
|
+
for (const key of Reflect.ownKeys(proto))
|
|
2097
|
+
keys.add(key);
|
|
2098
|
+
proto = Object.getPrototypeOf(proto);
|
|
2099
|
+
}
|
|
2100
|
+
return keys;
|
|
2101
|
+
}
|
|
2102
|
+
function recursiveTouch(oldValue, newValue, visited = new WeakMap(), notifications = [], origin) {
|
|
2103
|
+
if (!shouldRecurseTouch(oldValue, newValue))
|
|
2104
|
+
return notifications;
|
|
2105
|
+
if (!isObjectLike(oldValue) || !isObjectLike(newValue))
|
|
2106
|
+
return notifications;
|
|
2107
|
+
if (hasVisitedPair(visited, oldValue, newValue))
|
|
2108
|
+
return notifications;
|
|
2109
|
+
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
|
2110
|
+
diffArrayElements(oldValue, newValue, visited, notifications, origin);
|
|
2111
|
+
return notifications;
|
|
2112
|
+
}
|
|
2113
|
+
diffObjectProperties(oldValue, newValue, visited, notifications, origin);
|
|
2114
|
+
return notifications;
|
|
2115
|
+
}
|
|
2116
|
+
function diffArrayElements(oldArray, newArray, _visited, notifications, origin) {
|
|
2117
|
+
const local = [];
|
|
2118
|
+
const oldLength = oldArray.length;
|
|
2119
|
+
const newLength = newArray.length;
|
|
2120
|
+
const max = Math.max(oldLength, newLength);
|
|
2121
|
+
for (let index = 0; index < max; index++) {
|
|
2122
|
+
const hasOld = index < oldLength;
|
|
2123
|
+
const hasNew = index < newLength;
|
|
2124
|
+
if (hasOld && !hasNew) {
|
|
2125
|
+
local.push({ target: oldArray, evolution: { type: 'del', prop: index }, prop: index, origin });
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
if (!hasOld && hasNew) {
|
|
2129
|
+
local.push({ target: oldArray, evolution: { type: 'add', prop: index }, prop: index, origin });
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
if (!hasOld || !hasNew)
|
|
2133
|
+
continue;
|
|
2134
|
+
const oldEntry = unwrap(oldArray[index]);
|
|
2135
|
+
const newEntry = unwrap(newArray[index]);
|
|
2136
|
+
if (!Object.is(oldEntry, newEntry)) {
|
|
2137
|
+
local.push({ target: oldArray, evolution: { type: 'set', prop: index }, prop: index, origin });
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
if (oldLength !== newLength)
|
|
2141
|
+
local.push({
|
|
2142
|
+
target: oldArray,
|
|
2143
|
+
evolution: { type: 'set', prop: 'length' },
|
|
2144
|
+
prop: 'length',
|
|
2145
|
+
origin,
|
|
2146
|
+
});
|
|
2147
|
+
notifications.push(...local);
|
|
2148
|
+
}
|
|
2149
|
+
function diffObjectProperties(oldObj, newObj, visited, notifications, origin) {
|
|
2150
|
+
const oldKeys = collectObjectKeys(oldObj);
|
|
2151
|
+
const newKeys = collectObjectKeys(newObj);
|
|
2152
|
+
const local = [];
|
|
2153
|
+
for (const key of oldKeys)
|
|
2154
|
+
if (!newKeys.has(key))
|
|
2155
|
+
local.push({ target: oldObj, evolution: { type: 'del', prop: key }, prop: key, origin });
|
|
2156
|
+
for (const key of newKeys)
|
|
2157
|
+
if (!oldKeys.has(key))
|
|
2158
|
+
local.push({ target: oldObj, evolution: { type: 'add', prop: key }, prop: key, origin });
|
|
2159
|
+
for (const key of newKeys) {
|
|
2160
|
+
if (!oldKeys.has(key))
|
|
2161
|
+
continue;
|
|
2162
|
+
const oldEntry = unwrap(oldObj[key]);
|
|
2163
|
+
const newEntry = unwrap(newObj[key]);
|
|
2164
|
+
if (shouldRecurseTouch(oldEntry, newEntry)) {
|
|
2165
|
+
recursiveTouch(oldEntry, newEntry, visited, notifications, origin);
|
|
2166
|
+
}
|
|
2167
|
+
else if (!Object.is(oldEntry, newEntry)) {
|
|
2168
|
+
local.push({ target: oldObj, evolution: { type: 'set', prop: key }, prop: key, origin });
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
notifications.push(...local);
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Checks if an effect or any of its ancestors is in the allowed set
|
|
2175
|
+
*/
|
|
2176
|
+
function hasAncestorInSet(effect, allowedSet) {
|
|
2177
|
+
let current = effect;
|
|
2178
|
+
const visited = new WeakSet();
|
|
2179
|
+
while (current && !visited.has(current)) {
|
|
2180
|
+
visited.add(current);
|
|
2181
|
+
if (allowedSet.has(current))
|
|
2182
|
+
return true;
|
|
2183
|
+
const node = getEffectNode(current);
|
|
2184
|
+
current = node.parent;
|
|
2185
|
+
}
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
function dispatchNotifications(notifications) {
|
|
2189
|
+
if (!notifications.length)
|
|
2190
|
+
return;
|
|
2191
|
+
const combinedEffects = new Set();
|
|
2192
|
+
// Extract origin from first notification (all should have the same origin from a single deep touch)
|
|
2193
|
+
const origin = notifications[0]?.origin;
|
|
2194
|
+
let allowedEffects;
|
|
2195
|
+
// If origin exists, compute allowed effects (those that depend on origin.obj[origin.prop])
|
|
2196
|
+
if (origin) {
|
|
2197
|
+
allowedEffects = new Set();
|
|
2198
|
+
const originWatchers = watchers.get(origin.obj);
|
|
2199
|
+
if (originWatchers) {
|
|
2200
|
+
const originEffects = new Set();
|
|
2201
|
+
collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
|
|
2202
|
+
for (const effect of originEffects)
|
|
2203
|
+
allowedEffects.add(effect);
|
|
2204
|
+
}
|
|
2205
|
+
// If no allowed effects, skip all notifications (no one should be notified)
|
|
2206
|
+
if (allowedEffects.size === 0)
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
for (const { target, evolution, prop } of notifications) {
|
|
2210
|
+
if (!isObjectLike(target))
|
|
2211
|
+
continue;
|
|
2212
|
+
const obj = unwrap(target);
|
|
2213
|
+
addState(obj, evolution);
|
|
2214
|
+
const objectWatchers = watchers.get(obj);
|
|
2215
|
+
let currentEffects;
|
|
2216
|
+
const propsArray = [prop];
|
|
2217
|
+
if (objectWatchers) {
|
|
2218
|
+
// console.log(`[DEBUG] dispatchNotifications: processing ${obj.constructor.name} (has watchers)`)
|
|
2219
|
+
currentEffects = new Set();
|
|
2220
|
+
collectEffects(obj, evolution, currentEffects, objectWatchers, [allProps], propsArray);
|
|
2221
|
+
// Filter effects by ancestor chain if origin exists
|
|
2222
|
+
// Include effects that either directly depend on origin or have an ancestor that does
|
|
2223
|
+
if (origin && allowedEffects) {
|
|
2224
|
+
const filteredEffects = new Set();
|
|
2225
|
+
for (const effect of currentEffects) {
|
|
2226
|
+
// Check if effect itself is allowed OR has an ancestor that is allowed
|
|
2227
|
+
if (allowedEffects.has(effect) || hasAncestorInSet(effect, allowedEffects)) {
|
|
2228
|
+
filteredEffects.add(effect);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
currentEffects = filteredEffects;
|
|
2232
|
+
}
|
|
2233
|
+
for (const effect of currentEffects)
|
|
2234
|
+
combinedEffects.add(effect);
|
|
2235
|
+
}
|
|
2236
|
+
optionCall('touched', obj, evolution, propsArray, currentEffects);
|
|
2237
|
+
if (objectsWithDeepWatchers.has(obj))
|
|
2238
|
+
bubbleUpChange(obj);
|
|
2239
|
+
}
|
|
2240
|
+
if (combinedEffects.size)
|
|
2241
|
+
batch([...combinedEffects]);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* Marks a property as a dependency of the current effect
|
|
2246
|
+
* @param obj - The object containing the property
|
|
2247
|
+
* @param prop - The property name (defaults to allProps)
|
|
2248
|
+
*/
|
|
2249
|
+
function dependant(obj, prop = allProps) {
|
|
2250
|
+
obj = unwrap(obj);
|
|
2251
|
+
const currentActiveEffect = getActiveEffect();
|
|
2252
|
+
// Early return if no active effect, tracking disabled, or invalid prop
|
|
2253
|
+
if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps))
|
|
2254
|
+
return;
|
|
2255
|
+
if ('dependencyHook' in currentActiveEffect) {
|
|
2256
|
+
// @ts-expect-error We declared it nowhere - it's okay as it's really internal and for edge-case debug purpose only
|
|
2257
|
+
currentActiveEffect.dependencyHook(obj, prop);
|
|
2258
|
+
}
|
|
2259
|
+
let objectWatchers = watchers.get(obj);
|
|
2260
|
+
if (!objectWatchers) {
|
|
2261
|
+
objectWatchers = new Map();
|
|
2262
|
+
watchers.set(obj, objectWatchers);
|
|
2263
|
+
}
|
|
2264
|
+
let deps = objectWatchers.get(prop);
|
|
2265
|
+
if (!deps) {
|
|
2266
|
+
deps = new Set();
|
|
2267
|
+
objectWatchers.set(prop, deps);
|
|
2268
|
+
}
|
|
2269
|
+
deps.add(currentActiveEffect);
|
|
2270
|
+
// Track which reactive objects this effect is watching
|
|
2271
|
+
const effectObjects = effectToReactiveObjects.get(currentActiveEffect);
|
|
2272
|
+
if (effectObjects) {
|
|
2273
|
+
effectObjects.add(obj);
|
|
2274
|
+
}
|
|
2275
|
+
else {
|
|
2276
|
+
effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
const metaProtos = new WeakMap();
|
|
2281
|
+
const hasReentry = [];
|
|
2282
|
+
const reactiveHandlers = {
|
|
2283
|
+
[Symbol.toStringTag]: 'MutTs Reactive',
|
|
2284
|
+
get(obj, prop, receiver) {
|
|
2285
|
+
if (obj && typeof obj === 'object' && prop !== Symbol.toStringTag) {
|
|
2286
|
+
const metaProto = metaProtos.get(obj.constructor);
|
|
2287
|
+
if (metaProto && Object.hasOwn(metaProto, prop)) {
|
|
2288
|
+
const desc = Object.getOwnPropertyDescriptor(metaProto, prop);
|
|
2289
|
+
if (desc.get) {
|
|
2290
|
+
if (!Object.hasOwn(obj, prop))
|
|
2291
|
+
return desc.get.call(obj);
|
|
2292
|
+
// For own properties (e.g., array length): only override if writable/configurable
|
|
2293
|
+
const ownDesc = Object.getOwnPropertyDescriptor(obj, prop);
|
|
2294
|
+
if (ownDesc.configurable || ownDesc.writable || ownDesc.get)
|
|
2295
|
+
return desc.get.call(obj);
|
|
2296
|
+
}
|
|
2297
|
+
else if (!Object.hasOwn(obj, prop))
|
|
2298
|
+
return (...args) => desc.value.apply(obj, args);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
if (prop === nonReactiveMark)
|
|
2302
|
+
return false;
|
|
2303
|
+
const unwrappedObj = unwrap(obj);
|
|
2304
|
+
// Check if this property is marked as unreactive
|
|
2305
|
+
if (unwrappedObj[unreactiveProperties]?.has(prop) || typeof prop === 'symbol')
|
|
2306
|
+
return FoolProof.get(obj, prop, receiver);
|
|
2307
|
+
// Check if property exists and if it's an own property (cached for later use)
|
|
2308
|
+
const hasProp = Reflect.has(receiver, prop);
|
|
2309
|
+
const isOwnProp = hasProp && Object.hasOwn(receiver, prop);
|
|
2310
|
+
const isInheritedAccess = hasProp && !isOwnProp;
|
|
2311
|
+
// For accessor properties, check the unwrapped object to see if it's an accessor
|
|
2312
|
+
// This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
|
|
2313
|
+
const shouldIgnoreAccessor = options.ignoreAccessors &&
|
|
2314
|
+
isOwnProp &&
|
|
2315
|
+
(isOwnAccessor(receiver, prop) || isOwnAccessor(unwrappedObj, prop));
|
|
2316
|
+
// Depend if...
|
|
2317
|
+
if (!hasProp ||
|
|
2318
|
+
(!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
|
|
2319
|
+
!shouldIgnoreAccessor))
|
|
2320
|
+
dependant(obj, prop);
|
|
2321
|
+
// Watch the whole prototype chain when requested or for null-proto objects
|
|
2322
|
+
if (isInheritedAccess && (!options.instanceMembers || !(obj instanceof Object))) {
|
|
2323
|
+
let current = reactiveObject(Object.getPrototypeOf(obj));
|
|
2324
|
+
while (current && current !== Object.prototype) {
|
|
2325
|
+
dependant(current, prop);
|
|
2326
|
+
if (Object.hasOwn(current, prop))
|
|
2327
|
+
break;
|
|
2328
|
+
let next = reactiveObject(Object.getPrototypeOf(current));
|
|
2329
|
+
if (next === current) {
|
|
2330
|
+
next = reactiveObject(Object.getPrototypeOf(unwrap(current)));
|
|
2331
|
+
}
|
|
2332
|
+
current = next;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
const value = FoolProof.get(obj, prop, receiver);
|
|
2336
|
+
if (typeof value === 'object' && value !== null) {
|
|
2337
|
+
const reactiveValue = reactiveObject(value);
|
|
2338
|
+
// Only create back-references if this object needs them
|
|
2339
|
+
if (needsBackReferences(obj)) {
|
|
2340
|
+
addBackReference(reactiveValue, obj, prop);
|
|
2341
|
+
}
|
|
2342
|
+
return reactiveValue;
|
|
2343
|
+
}
|
|
2344
|
+
return value;
|
|
2345
|
+
},
|
|
2346
|
+
set(obj, prop, value, receiver) {
|
|
2347
|
+
// Read old value directly from unwrapped object to avoid triggering dependency tracking
|
|
2348
|
+
const unwrappedObj = unwrap(obj);
|
|
2349
|
+
const unwrappedReceiver = unwrap(receiver);
|
|
2350
|
+
// Check if this property is marked as unreactive
|
|
2351
|
+
if (unwrappedObj[unreactiveProperties]?.has(prop) || unwrappedObj !== unwrappedReceiver)
|
|
2352
|
+
return FoolProof.set(obj, prop, value, receiver);
|
|
2353
|
+
const newValue = unwrap(value);
|
|
2354
|
+
// metaProto setter dispatch (e.g., reactive array length)
|
|
2355
|
+
if (obj && typeof obj === 'object' && prop !== Symbol.toStringTag) {
|
|
2356
|
+
const metaProto = obj.constructor && metaProtos.get(obj.constructor);
|
|
2357
|
+
if (metaProto && Object.hasOwn(metaProto, prop)) {
|
|
2358
|
+
const desc = Object.getOwnPropertyDescriptor(metaProto, prop);
|
|
2359
|
+
if (desc.set) {
|
|
2360
|
+
desc.set.call(obj, newValue);
|
|
2361
|
+
return true;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
// Read old value, using withEffect(undefined, ...) for getter-only accessors to avoid
|
|
2366
|
+
// breaking memoization dependency tracking during SET operations
|
|
2367
|
+
let oldVal = absent;
|
|
2368
|
+
if (Reflect.has(unwrappedReceiver, prop)) {
|
|
2369
|
+
// Check descriptor on both receiver and target to handle proxy cases
|
|
2370
|
+
const receiverDesc = Object.getOwnPropertyDescriptor(unwrappedReceiver, prop);
|
|
2371
|
+
const targetDesc = Object.getOwnPropertyDescriptor(unwrappedObj, prop);
|
|
2372
|
+
const desc = receiverDesc || targetDesc;
|
|
2373
|
+
// We *need* to use `receiver` and not `unwrappedObj` here, otherwise we break
|
|
2374
|
+
// the dependency tracking for memoized getters
|
|
2375
|
+
if (desc?.get && !desc?.set) {
|
|
2376
|
+
oldVal = untracked(() => Reflect.get(unwrappedObj, prop, receiver));
|
|
2377
|
+
}
|
|
2378
|
+
else {
|
|
2379
|
+
oldVal = untracked(() => Reflect.get(unwrappedObj, prop, receiver));
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
if (objectsWithDeepWatchers.has(obj)) {
|
|
2383
|
+
if (typeof oldVal === 'object' && oldVal !== null) {
|
|
2384
|
+
removeBackReference(oldVal, obj, prop);
|
|
2385
|
+
}
|
|
2386
|
+
if (typeof newValue === 'object' && newValue !== null) {
|
|
2387
|
+
const reactiveValue = reactiveObject(newValue);
|
|
2388
|
+
addBackReference(reactiveValue, obj, prop);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
if (oldVal !== newValue) {
|
|
2392
|
+
// For getter-only accessors, Reflect.set() may fail, but we still return true
|
|
2393
|
+
// to avoid throwing errors. Only proceed with change notifications if set succeeded.
|
|
2394
|
+
if (FoolProof.set(obj, prop, newValue, receiver)) {
|
|
2395
|
+
notifyPropertyChange(obj, prop, oldVal, newValue, oldVal !== absent);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
return true;
|
|
2399
|
+
},
|
|
2400
|
+
has(obj, prop) {
|
|
2401
|
+
if (hasReentry.includes(obj))
|
|
2402
|
+
throw new ReactiveError(`[reactive] Circular dependency detected in 'has' check for property '${String(prop)}'`, {
|
|
2403
|
+
code: ReactiveErrorCode.CycleDetected,
|
|
2404
|
+
cycle: [], // We don't have the full cycle here, but we know it involves obj
|
|
2405
|
+
});
|
|
2406
|
+
hasReentry.push(obj);
|
|
2407
|
+
dependant(obj, prop);
|
|
2408
|
+
const rv = Reflect.has(obj, prop);
|
|
2409
|
+
hasReentry.pop();
|
|
2410
|
+
return rv;
|
|
2411
|
+
},
|
|
2412
|
+
deleteProperty(obj, prop) {
|
|
2413
|
+
if (!Object.hasOwn(obj, prop))
|
|
2414
|
+
return false;
|
|
2415
|
+
const oldVal = obj[prop];
|
|
2416
|
+
// Remove back-references if this object has deep watchers
|
|
2417
|
+
if (objectsWithDeepWatchers.has(obj) && typeof oldVal === 'object' && oldVal !== null) {
|
|
2418
|
+
removeBackReference(oldVal, obj, prop);
|
|
2419
|
+
}
|
|
2420
|
+
delete obj[prop];
|
|
2421
|
+
touched1(obj, { type: 'del', prop }, prop);
|
|
2422
|
+
// Bubble up changes if this object has deep watchers
|
|
2423
|
+
if (objectsWithDeepWatchers.has(obj)) {
|
|
2424
|
+
bubbleUpChange(obj);
|
|
2425
|
+
}
|
|
2426
|
+
return true;
|
|
2427
|
+
},
|
|
2428
|
+
ownKeys(obj) {
|
|
2429
|
+
dependant(obj, allProps);
|
|
2430
|
+
return Reflect.ownKeys(obj);
|
|
2431
|
+
},
|
|
2432
|
+
};
|
|
2433
|
+
const reactiveClasses = new WeakSet();
|
|
2434
|
+
// Create the ReactiveBase mixin
|
|
2435
|
+
/**
|
|
2436
|
+
* Base mixin for reactive classes that provides proper constructor reactivity
|
|
2437
|
+
* Solves constructor reactivity issues in complex inheritance trees
|
|
2438
|
+
*/
|
|
2439
|
+
const ReactiveBase = mixin((base) => {
|
|
2440
|
+
class ReactiveMixin extends base {
|
|
2441
|
+
constructor(...args) {
|
|
2442
|
+
super(...args);
|
|
2443
|
+
// Only apply reactive transformation if the class is marked with @reactive
|
|
2444
|
+
// This allows the mixin to work properly with method inheritance
|
|
2445
|
+
// biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
|
|
2446
|
+
return reactiveClasses.has(new.target) ? reactive(this) : this;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
return ReactiveMixin;
|
|
2450
|
+
});
|
|
2451
|
+
function reactiveObject(anyTarget) {
|
|
2452
|
+
if (!anyTarget || typeof anyTarget !== 'object')
|
|
2453
|
+
return anyTarget;
|
|
2454
|
+
const target = anyTarget;
|
|
2455
|
+
// If target is already a proxy, return it
|
|
2456
|
+
if (isNonReactive(target))
|
|
2457
|
+
return target;
|
|
2458
|
+
const isProxy = proxyToObject.has(target);
|
|
2459
|
+
if (isProxy)
|
|
2460
|
+
return target;
|
|
2461
|
+
// If we already have a proxy for this object, return it (optimized: get returns undefined if not found)
|
|
2462
|
+
const existing = getExistingProxy(target);
|
|
2463
|
+
if (existing !== undefined)
|
|
2464
|
+
return existing;
|
|
2465
|
+
const proxied = nativeReactive in target && !(target instanceof target[nativeReactive])
|
|
2466
|
+
? new target[nativeReactive](target)
|
|
2467
|
+
: target;
|
|
2468
|
+
if (proxied !== target)
|
|
2469
|
+
trackProxyObject(proxied, target);
|
|
2470
|
+
const proxy = new Proxy(proxied, reactiveHandlers);
|
|
2471
|
+
// Store the relationships
|
|
2472
|
+
storeProxyRelationship(target, proxy);
|
|
2473
|
+
return proxy;
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Main decorator for making classes reactive
|
|
2477
|
+
* Automatically makes class instances reactive when created
|
|
2478
|
+
*/
|
|
2479
|
+
const reactive = decorator({
|
|
2480
|
+
class(original) {
|
|
2481
|
+
if (original.prototype instanceof ReactiveBase) {
|
|
2482
|
+
reactiveClasses.add(original);
|
|
2483
|
+
return original;
|
|
2484
|
+
}
|
|
2485
|
+
class Reactive extends original {
|
|
2486
|
+
constructor(...args) {
|
|
2487
|
+
super(...args);
|
|
2488
|
+
if (new.target !== Reactive && !reactiveClasses.has(new.target))
|
|
2489
|
+
options.warn(`${original.name} has been inherited by ${this.constructor.name} that is not reactive.
|
|
2490
|
+
@reactive decorator must be applied to the leaf class OR classes have to extend ReactiveBase.`);
|
|
2491
|
+
// biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
|
|
2492
|
+
return reactive(this);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
Object.defineProperty(Reactive, 'name', {
|
|
2496
|
+
value: `Reactive<${original.name}>`,
|
|
2497
|
+
});
|
|
2498
|
+
return Reactive;
|
|
2499
|
+
},
|
|
2500
|
+
get(original) {
|
|
2501
|
+
return reactiveObject(original);
|
|
2502
|
+
},
|
|
2503
|
+
default: reactiveObject,
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
/**
|
|
2507
|
+
* Displays a floating reactive panel in the browser showing the lineage of triggered effects.
|
|
2508
|
+
*/
|
|
2509
|
+
function showLineagePanel() {
|
|
2510
|
+
if (typeof document === 'undefined')
|
|
2511
|
+
return;
|
|
2512
|
+
// Detect color scheme preference
|
|
2513
|
+
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
2514
|
+
// Color schemes
|
|
2515
|
+
const colors = isDarkMode ? {
|
|
2516
|
+
panelBg: 'rgba(30, 30, 30, 0.95)',
|
|
2517
|
+
panelBorder: '#444',
|
|
2518
|
+
headerBg: '#8B4513',
|
|
2519
|
+
headerText: '#ffffff',
|
|
2520
|
+
closeBtn: '#ffffff',
|
|
2521
|
+
placeholderText: '#999',
|
|
2522
|
+
segmentBorder: '#8B4513',
|
|
2523
|
+
titleText: '#ffffff',
|
|
2524
|
+
frameText: '#ccc',
|
|
2525
|
+
atText: '#888',
|
|
2526
|
+
linkText: '#58a6ff',
|
|
2527
|
+
} : {
|
|
2528
|
+
panelBg: 'rgba(255, 255, 255, 0.95)',
|
|
2529
|
+
panelBorder: '#ddd',
|
|
2530
|
+
headerBg: '#704214',
|
|
2531
|
+
headerText: '#ffffff',
|
|
2532
|
+
closeBtn: '#ffffff',
|
|
2533
|
+
placeholderText: '#666',
|
|
2534
|
+
segmentBorder: '#704214',
|
|
2535
|
+
titleText: '#222',
|
|
2536
|
+
frameText: '#555',
|
|
2537
|
+
atText: '#999',
|
|
2538
|
+
linkText: '#005cc5',
|
|
2539
|
+
};
|
|
2540
|
+
const state = reactive({
|
|
2541
|
+
activeLineage: [],
|
|
2542
|
+
isVisible: true,
|
|
2543
|
+
});
|
|
2544
|
+
// Track the last triggered effect
|
|
2545
|
+
/* TODO: this is really not how effect triggers work - `onEffectTrigger` is called when "this effect" (here nothing) is triggered
|
|
2546
|
+
What we could try is to add events or such in zones... but it's really cosmetic and the whole panel is buggy - not worth doing yet (260206).
|
|
2547
|
+
onEffectTrigger((_obj, _evol, _prop, effectTrigger) => {
|
|
2548
|
+
state.activeLineage = getLineage(effectTrigger)
|
|
2549
|
+
})*/
|
|
2550
|
+
// UI Creation
|
|
2551
|
+
const panel = document.createElement('div');
|
|
2552
|
+
panel.id = 'mutts-lineage-panel';
|
|
2553
|
+
Object.assign(panel.style, {
|
|
2554
|
+
position: 'fixed',
|
|
2555
|
+
bottom: '20px',
|
|
2556
|
+
right: '20px',
|
|
2557
|
+
width: '400px',
|
|
2558
|
+
maxHeight: '80vh',
|
|
2559
|
+
backgroundColor: colors.panelBg,
|
|
2560
|
+
backdropFilter: 'blur(10px)',
|
|
2561
|
+
border: `1px solid ${colors.panelBorder}`,
|
|
2562
|
+
borderRadius: '12px',
|
|
2563
|
+
boxShadow: '0 10px 25px rgba(0,0,0,0.15)',
|
|
2564
|
+
zIndex: '999999',
|
|
2565
|
+
fontFamily: 'Inter, system-ui, sans-serif',
|
|
2566
|
+
fontSize: '12px',
|
|
2567
|
+
display: 'flex',
|
|
2568
|
+
flexDirection: 'column',
|
|
2569
|
+
overflow: 'hidden',
|
|
2570
|
+
transition: 'opacity 0.3s, transform 0.3s',
|
|
2571
|
+
});
|
|
2572
|
+
const header = document.createElement('div');
|
|
2573
|
+
Object.assign(header.style, {
|
|
2574
|
+
padding: '12px 16px',
|
|
2575
|
+
background: colors.headerBg,
|
|
2576
|
+
color: colors.headerText,
|
|
2577
|
+
fontWeight: '600',
|
|
2578
|
+
display: 'flex',
|
|
2579
|
+
justifyContent: 'space-between',
|
|
2580
|
+
alignItems: 'center',
|
|
2581
|
+
cursor: 'grab',
|
|
2582
|
+
});
|
|
2583
|
+
header.innerHTML = '<span><span style="font-size: 14px; margin-right: 5px;">🦴</span> Mutts Lineage</span>';
|
|
2584
|
+
const closeBtn = document.createElement('button');
|
|
2585
|
+
closeBtn.innerText = '×';
|
|
2586
|
+
Object.assign(closeBtn.style, {
|
|
2587
|
+
background: 'none',
|
|
2588
|
+
border: 'none',
|
|
2589
|
+
color: colors.closeBtn,
|
|
2590
|
+
fontSize: '20px',
|
|
2591
|
+
cursor: 'pointer',
|
|
2592
|
+
padding: '0 5px',
|
|
2593
|
+
});
|
|
2594
|
+
closeBtn.onclick = () => {
|
|
2595
|
+
state.isVisible = false;
|
|
2596
|
+
};
|
|
2597
|
+
header.appendChild(closeBtn);
|
|
2598
|
+
panel.appendChild(header);
|
|
2599
|
+
const content = document.createElement('div');
|
|
2600
|
+
Object.assign(content.style, {
|
|
2601
|
+
padding: '16px',
|
|
2602
|
+
overflowY: 'auto',
|
|
2603
|
+
flex: '1',
|
|
2604
|
+
});
|
|
2605
|
+
panel.appendChild(content);
|
|
2606
|
+
document.body.appendChild(panel);
|
|
2607
|
+
// Reactivity: Update Visibility
|
|
2608
|
+
effect(() => {
|
|
2609
|
+
panel.style.opacity = state.isVisible ? '1' : '0';
|
|
2610
|
+
panel.style.pointerEvents = state.isVisible ? 'all' : 'none';
|
|
2611
|
+
panel.style.transform = state.isVisible ? 'translateY(0)' : 'translateY(20px)';
|
|
2612
|
+
});
|
|
2613
|
+
// Reactivity: Update Content
|
|
2614
|
+
effect(() => {
|
|
2615
|
+
if (state.activeLineage.length === 0) {
|
|
2616
|
+
content.innerHTML = `<div style="color: ${colors.placeholderText}; font-style: italic; text-align: center; margin-top: 20px;">No effect triggered yet...</div>`;
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
content.innerHTML = '';
|
|
2620
|
+
state.activeLineage.forEach((segment, i) => {
|
|
2621
|
+
const segDiv = document.createElement('div');
|
|
2622
|
+
Object.assign(segDiv.style, {
|
|
2623
|
+
marginBottom: '16px',
|
|
2624
|
+
borderLeft: `2px solid ${colors.segmentBorder}`,
|
|
2625
|
+
paddingLeft: '12px',
|
|
2626
|
+
});
|
|
2627
|
+
const title = document.createElement('div');
|
|
2628
|
+
Object.assign(title.style, {
|
|
2629
|
+
fontWeight: 'bold',
|
|
2630
|
+
marginBottom: '6px',
|
|
2631
|
+
color: colors.titleText,
|
|
2632
|
+
});
|
|
2633
|
+
title.innerText = i === 0 ? `📍 Current: ${segment.effectName}` : `↖ Effect: ${segment.effectName}`;
|
|
2634
|
+
segDiv.appendChild(title);
|
|
2635
|
+
segment.stack.forEach(frame => {
|
|
2636
|
+
const frameDiv = document.createElement('div');
|
|
2637
|
+
Object.assign(frameDiv.style, {
|
|
2638
|
+
color: colors.frameText,
|
|
2639
|
+
marginBottom: '3px',
|
|
2640
|
+
fontSize: '11px',
|
|
2641
|
+
whiteSpace: 'nowrap',
|
|
2642
|
+
overflow: 'hidden',
|
|
2643
|
+
textOverflow: 'ellipsis',
|
|
2644
|
+
cursor: 'pointer',
|
|
2645
|
+
});
|
|
2646
|
+
frameDiv.title = frame.raw;
|
|
2647
|
+
frameDiv.innerHTML = `<span style="color: ${colors.atText}">at</span> ${frame.functionName} <span style="color: ${colors.linkText}; text-decoration: underline;">(${frame.fileName.split('/').pop()}:${frame.lineNumber})</span>`;
|
|
2648
|
+
segDiv.appendChild(frameDiv);
|
|
2649
|
+
});
|
|
2650
|
+
content.appendChild(segDiv);
|
|
2651
|
+
});
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
/**
|
|
2656
|
+
* Debug utilities for the reactivity system
|
|
2657
|
+
* - Captures effect metadata (names, parent relationships)
|
|
2658
|
+
* - Records cause → consequence edges with object/prop labels
|
|
2659
|
+
* - Provides graph data for tooling (DevTools panel, etc.)
|
|
2660
|
+
*/
|
|
2661
|
+
/**
|
|
2662
|
+
* Log an error with detailed context if error logging is enabled
|
|
2663
|
+
*/
|
|
2664
|
+
function logError(error, context, effect) {
|
|
2665
|
+
if (!debugOptions.logErrors)
|
|
2666
|
+
return;
|
|
2667
|
+
const contextStr = context === 'throw' ? '🔴 Thrown' : '🟡 Caught';
|
|
2668
|
+
console.group(`${contextStr} Error: ${error.message}`);
|
|
2669
|
+
console.error('Error:', error);
|
|
2670
|
+
// Try to get effect name if available
|
|
2671
|
+
if (effect && typeof effect === 'object' && effect.name) {
|
|
2672
|
+
console.log('Effect:', effect.name);
|
|
2673
|
+
}
|
|
2674
|
+
console.groupEnd();
|
|
2675
|
+
}
|
|
2676
|
+
// Log initialization for debugging
|
|
2677
|
+
const EXTERNAL_SOURCE = Symbol('external-source');
|
|
2678
|
+
let devtoolsEnabled = false;
|
|
2679
|
+
// Registry for debugging (populated lazily when DevTools are enabled)
|
|
2680
|
+
const debugEffectRegistry = new Set();
|
|
2681
|
+
const debugObjectRegistry = new Set();
|
|
2682
|
+
// Human-friendly names
|
|
2683
|
+
const effectNames = new WeakMap();
|
|
2684
|
+
const objectNames = new WeakMap();
|
|
2685
|
+
let effectCounter = 0;
|
|
2686
|
+
let objectCounter = 0;
|
|
2687
|
+
// Debug options system
|
|
2688
|
+
const debugOptions = {
|
|
2689
|
+
/**
|
|
2690
|
+
* Whether DevTools are enabled
|
|
2691
|
+
*/
|
|
2692
|
+
get enabled() {
|
|
2693
|
+
return devtoolsEnabled;
|
|
2694
|
+
},
|
|
2695
|
+
logErrors: false
|
|
2696
|
+
};
|
|
2697
|
+
const triggerGraph = new Map();
|
|
2698
|
+
function ensureEffectName(effect) {
|
|
2699
|
+
let name = effectNames.get(effect);
|
|
2700
|
+
if (!name) {
|
|
2701
|
+
const root = getRoot(effect);
|
|
2702
|
+
name = root?.name?.trim() || `effect_${++effectCounter}`;
|
|
2703
|
+
effectNames.set(effect, name);
|
|
2704
|
+
}
|
|
2705
|
+
return name;
|
|
2706
|
+
}
|
|
2707
|
+
function ensureObjectName(obj) {
|
|
2708
|
+
let name = objectNames.get(obj);
|
|
2709
|
+
if (!name) {
|
|
2710
|
+
const ctorName = obj?.constructor?.name;
|
|
2711
|
+
const base = ctorName && ctorName !== 'Object' ? ctorName : 'object';
|
|
2712
|
+
name = `${base}_${++objectCounter}`;
|
|
2713
|
+
objectNames.set(obj, name);
|
|
2714
|
+
}
|
|
2715
|
+
return name;
|
|
2716
|
+
}
|
|
2717
|
+
function describeProp(obj, prop) {
|
|
2718
|
+
const objectName = ensureObjectName(obj);
|
|
2719
|
+
if (prop === allProps)
|
|
2720
|
+
return `${objectName}.*`;
|
|
2721
|
+
if (typeof prop === 'symbol')
|
|
2722
|
+
return `${objectName}.${prop.description ?? prop.toString()}`;
|
|
2723
|
+
return `${objectName}.${String(prop)}`;
|
|
2724
|
+
}
|
|
2725
|
+
function addEffectToRegistry(effect) {
|
|
2726
|
+
if (!effect || debugEffectRegistry.has(effect))
|
|
2727
|
+
return;
|
|
2728
|
+
debugEffectRegistry.add(effect);
|
|
2729
|
+
const deps = effectToReactiveObjects.get(effect);
|
|
2730
|
+
if (deps) {
|
|
2731
|
+
for (const obj of deps) {
|
|
2732
|
+
documentObject(obj);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
function documentObject(obj) {
|
|
2737
|
+
if (!debugObjectRegistry.has(obj)) {
|
|
2738
|
+
dbRegisterObject(obj);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
function dbRegisterObject(obj) {
|
|
2742
|
+
debugObjectRegistry.add(obj);
|
|
2743
|
+
ensureObjectName(obj);
|
|
2744
|
+
}
|
|
2745
|
+
function ensureParentChains(effects) {
|
|
2746
|
+
const queue = Array.from(effects);
|
|
2747
|
+
for (let i = 0; i < queue.length; i++) {
|
|
2748
|
+
const effect = queue[i];
|
|
2749
|
+
const parent = getEffectNode(effect).parent;
|
|
2750
|
+
if (parent && !effects.has(parent)) {
|
|
2751
|
+
effects.add(parent);
|
|
2752
|
+
queue.push(parent);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
function ensureTriggerContainers(source) {
|
|
2757
|
+
let targetMap = triggerGraph.get(source);
|
|
2758
|
+
if (!targetMap) {
|
|
2759
|
+
targetMap = new Map();
|
|
2760
|
+
triggerGraph.set(source, targetMap);
|
|
2761
|
+
}
|
|
2762
|
+
return targetMap;
|
|
2763
|
+
}
|
|
2764
|
+
function ensureTriggerRecord(source, target, label, obj, prop, evolution) {
|
|
2765
|
+
const targetMap = ensureTriggerContainers(source);
|
|
2766
|
+
let labelMap = targetMap.get(target);
|
|
2767
|
+
if (!labelMap) {
|
|
2768
|
+
labelMap = new Map();
|
|
2769
|
+
targetMap.set(target, labelMap);
|
|
2770
|
+
}
|
|
2771
|
+
let record = labelMap.get(label);
|
|
2772
|
+
if (!record) {
|
|
2773
|
+
record = { label, object: obj, prop, evolution, count: 0, lastTriggered: Date.now() };
|
|
2774
|
+
labelMap.set(label, record);
|
|
2775
|
+
}
|
|
2776
|
+
return record;
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Assign a debug-friendly name to an effect (shown in DevTools)
|
|
2780
|
+
*/
|
|
2781
|
+
function setEffectName(effect, name) {
|
|
2782
|
+
effectNames.set(effect, name);
|
|
2783
|
+
}
|
|
2784
|
+
/**
|
|
2785
|
+
* Assign a debug-friendly name to a reactive object
|
|
2786
|
+
*/
|
|
2787
|
+
function setObjectName(obj, name) {
|
|
2788
|
+
objectNames.set(obj, name);
|
|
2789
|
+
debugObjectRegistry.add(obj);
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Register an effect so it appears in the DevTools graph
|
|
2793
|
+
*/
|
|
2794
|
+
function registerEffectForDebug(effect) {
|
|
2795
|
+
if (!effect || !devtoolsEnabled)
|
|
2796
|
+
return;
|
|
2797
|
+
addEffectToRegistry(effect);
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* Register a reactive object so it appears in the DevTools graph
|
|
2801
|
+
*/
|
|
2802
|
+
function registerObjectForDebug(obj) {
|
|
2803
|
+
if (!devtoolsEnabled)
|
|
2804
|
+
return;
|
|
2805
|
+
documentObject(obj);
|
|
2806
|
+
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Records a cause → consequence relationship between effects.
|
|
2809
|
+
* @param source - The effect performing the write (undefined if external/user input)
|
|
2810
|
+
* @param target - The effect that re-ran because of the write
|
|
2811
|
+
* @param obj - The reactive object that changed
|
|
2812
|
+
* @param prop - The property that changed
|
|
2813
|
+
* @param evolution - The type of change (set/add/del/bunch)
|
|
2814
|
+
*/
|
|
2815
|
+
function recordTriggerLink(source, target, obj, prop, evolution) {
|
|
2816
|
+
raiseEffectTrackers(target, obj, evolution, prop);
|
|
2817
|
+
if (options.introspection.enableHistory) {
|
|
2818
|
+
addToMutationHistory(source, target, obj, prop, evolution);
|
|
2819
|
+
}
|
|
2820
|
+
if (!devtoolsEnabled)
|
|
2821
|
+
return;
|
|
2822
|
+
addEffectToRegistry(target);
|
|
2823
|
+
if (source)
|
|
2824
|
+
addEffectToRegistry(source);
|
|
2825
|
+
const descriptor = describeProp(obj, prop);
|
|
2826
|
+
const record = ensureTriggerRecord(source ?? EXTERNAL_SOURCE, target, descriptor, obj, prop, evolution);
|
|
2827
|
+
record.count += 1;
|
|
2828
|
+
record.lastTriggered = Date.now();
|
|
2829
|
+
documentObject(obj);
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Traces back the chain of triggers that led to a specific effect
|
|
2833
|
+
* @param effect The effect to trace back
|
|
2834
|
+
* @param limit Max depth
|
|
2835
|
+
*/
|
|
2836
|
+
function getTriggerChain(effect, limit = 5) {
|
|
2837
|
+
const chain = [];
|
|
2838
|
+
let current = effect;
|
|
2839
|
+
for (let i = 0; i < limit; i++) {
|
|
2840
|
+
// Find who triggered 'current'
|
|
2841
|
+
// We need to reverse search the triggerGraph (source -> target)
|
|
2842
|
+
// This is expensive O(Edges) but okay for error reporting
|
|
2843
|
+
let foundSource;
|
|
2844
|
+
let foundReason = '';
|
|
2845
|
+
search: for (const [source, targetMap] of triggerGraph) {
|
|
2846
|
+
for (const [target, labelMap] of targetMap) {
|
|
2847
|
+
if (target === current) {
|
|
2848
|
+
// Found a source! Use the most recent trigger record
|
|
2849
|
+
let lastTime = 0;
|
|
2850
|
+
for (const record of labelMap.values()) {
|
|
2851
|
+
if (record.lastTriggered > lastTime) {
|
|
2852
|
+
lastTime = record.lastTriggered;
|
|
2853
|
+
foundReason = record.label;
|
|
2854
|
+
foundSource = source === EXTERNAL_SOURCE ? undefined : source;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
if (foundSource || foundReason)
|
|
2858
|
+
break search;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
if (foundSource) {
|
|
2863
|
+
chain.push(`${ensureEffectName(foundSource)} -> (${foundReason}) -> ${ensureEffectName(current)}`);
|
|
2864
|
+
current = foundSource;
|
|
2865
|
+
}
|
|
2866
|
+
else if (foundReason) {
|
|
2867
|
+
chain.push(`External -> (${foundReason}) -> ${ensureEffectName(current)}`);
|
|
2868
|
+
break;
|
|
2869
|
+
}
|
|
2870
|
+
else {
|
|
2871
|
+
break;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
return chain.reverse();
|
|
2875
|
+
}
|
|
2876
|
+
function buildEffectNodes(allEffects) {
|
|
2877
|
+
const nodes = [];
|
|
2878
|
+
const nodeByEffect = new Map();
|
|
2879
|
+
const ordered = Array.from(allEffects);
|
|
2880
|
+
for (const effect of ordered) {
|
|
2881
|
+
const label = ensureEffectName(effect);
|
|
2882
|
+
const node = {
|
|
2883
|
+
id: `effect_${nodes.length}`,
|
|
2884
|
+
label,
|
|
2885
|
+
type: 'effect',
|
|
2886
|
+
depth: 0,
|
|
2887
|
+
debugName: label,
|
|
2888
|
+
};
|
|
2889
|
+
nodes.push(node);
|
|
2890
|
+
nodeByEffect.set(effect, node);
|
|
2891
|
+
}
|
|
2892
|
+
const depthCache = new Map();
|
|
2893
|
+
const computeDepth = (effect) => {
|
|
2894
|
+
if (!effect)
|
|
2895
|
+
return 0;
|
|
2896
|
+
const cached = depthCache.get(effect);
|
|
2897
|
+
if (cached !== undefined)
|
|
2898
|
+
return cached;
|
|
2899
|
+
const parent = getEffectNode(effect).parent;
|
|
2900
|
+
const depth = computeDepth(parent) + (parent ? 1 : 0);
|
|
2901
|
+
depthCache.set(effect, depth);
|
|
2902
|
+
return depth;
|
|
2903
|
+
};
|
|
2904
|
+
for (const [effect, node] of nodeByEffect) {
|
|
2905
|
+
node.depth = computeDepth(effect);
|
|
2906
|
+
const parent = getEffectNode(effect).parent;
|
|
2907
|
+
if (parent) {
|
|
2908
|
+
const parentNode = nodeByEffect.get(parent);
|
|
2909
|
+
if (parentNode) {
|
|
2910
|
+
node.parentId = parentNode.id;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
return { nodes, nodeByEffect };
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Builds a graph representing current reactive state (effects, objects, and trigger edges)
|
|
2918
|
+
*/
|
|
2919
|
+
function buildReactivityGraph() {
|
|
2920
|
+
const nodes = [];
|
|
2921
|
+
const edges = [];
|
|
2922
|
+
const nodeIds = new Map();
|
|
2923
|
+
const allEffects = new Set(debugEffectRegistry);
|
|
2924
|
+
ensureParentChains(allEffects);
|
|
2925
|
+
const { nodes: effectNodes, nodeByEffect } = buildEffectNodes(allEffects);
|
|
2926
|
+
for (const node of effectNodes)
|
|
2927
|
+
nodes.push(node);
|
|
2928
|
+
for (const [effect, node] of nodeByEffect) {
|
|
2929
|
+
nodeIds.set(effect, node.id);
|
|
2930
|
+
}
|
|
2931
|
+
// Object nodes (optional, used for dependency inspection)
|
|
2932
|
+
for (const obj of debugObjectRegistry) {
|
|
2933
|
+
const id = `object_${nodes.length}`;
|
|
2934
|
+
nodes.push({ id, label: ensureObjectName(obj), type: 'state', debugName: objectNames.get(obj) });
|
|
2935
|
+
nodeIds.set(obj, id);
|
|
2936
|
+
}
|
|
2937
|
+
// External source node (user/system outside of effects)
|
|
2938
|
+
if (triggerGraph.has(EXTERNAL_SOURCE)) {
|
|
2939
|
+
const externalId = `effect_external`;
|
|
2940
|
+
nodes.push({ id: externalId, label: 'External', type: 'external', depth: 0 });
|
|
2941
|
+
nodeIds.set(EXTERNAL_SOURCE, externalId);
|
|
2942
|
+
}
|
|
2943
|
+
// Dependency edges (effect → object)
|
|
2944
|
+
for (const effect of allEffects) {
|
|
2945
|
+
const effectId = nodeIds.get(effect);
|
|
2946
|
+
if (!effectId)
|
|
2947
|
+
continue;
|
|
2948
|
+
const deps = effectToReactiveObjects.get(effect);
|
|
2949
|
+
if (!deps)
|
|
2950
|
+
continue;
|
|
2951
|
+
for (const obj of deps) {
|
|
2952
|
+
const objId = nodeIds.get(obj);
|
|
2953
|
+
if (!objId)
|
|
2954
|
+
continue;
|
|
2955
|
+
edges.push({
|
|
2956
|
+
id: `${effectId}->${objId}`,
|
|
2957
|
+
source: effectId,
|
|
2958
|
+
target: objId,
|
|
2959
|
+
type: 'dependency',
|
|
2960
|
+
label: 'depends',
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
// Cause edges (effect/object/prop → effect)
|
|
2965
|
+
for (const [source, targetMap] of triggerGraph) {
|
|
2966
|
+
for (const [targetEffect, labelMap] of targetMap) {
|
|
2967
|
+
const targetId = nodeIds.get(targetEffect);
|
|
2968
|
+
if (!targetId)
|
|
2969
|
+
continue;
|
|
2970
|
+
const sourceId = nodeIds.get(source);
|
|
2971
|
+
if (!sourceId)
|
|
2972
|
+
continue;
|
|
2973
|
+
for (const record of labelMap.values()) {
|
|
2974
|
+
edges.push({
|
|
2975
|
+
id: `${sourceId}->${targetId}:${record.label}`,
|
|
2976
|
+
source: sourceId,
|
|
2977
|
+
target: targetId,
|
|
2978
|
+
type: 'cause',
|
|
2979
|
+
label: record.count > 1 ? `${record.label} (${record.count})` : record.label,
|
|
2980
|
+
count: record.count,
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
return {
|
|
2986
|
+
nodes,
|
|
2987
|
+
edges,
|
|
2988
|
+
meta: {
|
|
2989
|
+
generatedAt: Date.now(),
|
|
2990
|
+
devtoolsEnabled,
|
|
2991
|
+
},
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2995
|
+
* Enables the DevTools bridge and exposes the debug API on window/global.
|
|
2996
|
+
* Call as early as possible in development builds.
|
|
2997
|
+
*/
|
|
2998
|
+
function enableDevTools() {
|
|
2999
|
+
const globalScope = (typeof globalThis !== 'undefined'
|
|
3000
|
+
? globalThis
|
|
3001
|
+
: typeof window !== 'undefined'
|
|
3002
|
+
? window
|
|
3003
|
+
: typeof global !== 'undefined'
|
|
3004
|
+
? global
|
|
3005
|
+
: undefined);
|
|
3006
|
+
if (!globalScope)
|
|
3007
|
+
return;
|
|
3008
|
+
if (devtoolsEnabled)
|
|
3009
|
+
return;
|
|
3010
|
+
devtoolsEnabled = true;
|
|
3011
|
+
globalScope.__MUTTS_DEVTOOLS__ = {
|
|
3012
|
+
getGraph: buildReactivityGraph,
|
|
3013
|
+
nodeLineage(tag) {
|
|
3014
|
+
nodeLineage(getLineage());
|
|
3015
|
+
return '🦴 Effect Lineage Trace' + (tag ? ` (${tag})` : '');
|
|
3016
|
+
},
|
|
3017
|
+
nodeLineageLegacy() {
|
|
3018
|
+
console.groupCollapsed('nodeLineageLegacy');
|
|
3019
|
+
console.log(nodeLineageLegacy(getLineage()));
|
|
3020
|
+
console.groupEnd();
|
|
3021
|
+
},
|
|
3022
|
+
get browserLineage() {
|
|
3023
|
+
return wrapLineageForDebug(getLineage());
|
|
3024
|
+
},
|
|
3025
|
+
getLineage,
|
|
3026
|
+
captureLineage: getStackFrame,
|
|
3027
|
+
formatLineage,
|
|
3028
|
+
showLineagePanel,
|
|
3029
|
+
setEffectName,
|
|
3030
|
+
setObjectName,
|
|
3031
|
+
registerEffect: registerEffectForDebug,
|
|
3032
|
+
registerObject: registerObjectForDebug,
|
|
3033
|
+
// Debug options for controlling runtime behavior
|
|
3034
|
+
debug: debugOptions
|
|
3035
|
+
};
|
|
3036
|
+
// @ts-ignore - devtoolsFormatters is a Chrome-specific array
|
|
3037
|
+
if (globalScope.devtoolsFormatters) {
|
|
3038
|
+
globalScope.devtoolsFormatters.push(lineageFormatter);
|
|
3039
|
+
}
|
|
3040
|
+
else {
|
|
3041
|
+
globalScope.devtoolsFormatters = [lineageFormatter];
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
function forceEnableGraphTracking() {
|
|
3045
|
+
devtoolsEnabled = true;
|
|
3046
|
+
}
|
|
3047
|
+
function isDevtoolsEnabled() {
|
|
3048
|
+
return devtoolsEnabled;
|
|
3049
|
+
}
|
|
3050
|
+
// --- Introspection API ---
|
|
3051
|
+
/**
|
|
3052
|
+
* Returns the raw dependency graph data structure.
|
|
3053
|
+
* This is useful for programmatic analysis of the reactive system.
|
|
3054
|
+
*/
|
|
3055
|
+
function getDependencyGraph() {
|
|
3056
|
+
return {
|
|
3057
|
+
nodes: buildReactivityGraph().nodes,
|
|
3058
|
+
edges: buildReactivityGraph().edges,
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Returns a list of effects that depend on the given object.
|
|
3063
|
+
*/
|
|
3064
|
+
function getDependents(obj) {
|
|
3065
|
+
const dependents = [];
|
|
3066
|
+
// Scan the trigger graph for effects triggered by this object
|
|
3067
|
+
// This is O(E) where E is the number of edges, might need optimization for large graphs
|
|
3068
|
+
// but acceptable for introspection
|
|
3069
|
+
for (const [_source, targetMap] of triggerGraph) {
|
|
3070
|
+
for (const [targetEffect, labelMap] of targetMap) {
|
|
3071
|
+
for (const record of labelMap.values()) {
|
|
3072
|
+
if (record.object === obj) {
|
|
3073
|
+
dependents.push(targetEffect);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
// Also check direct dependencies (dependency graph)
|
|
3079
|
+
// We don't have a direct obj -> effect map without walking all effects
|
|
3080
|
+
// unless we use `watchers` from tracking.ts but that's internal
|
|
3081
|
+
return [...new Set(dependents)];
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Returns a list of objects that the given effect depends on.
|
|
3085
|
+
*/
|
|
3086
|
+
function getDependencies(effect) {
|
|
3087
|
+
const deps = effectToReactiveObjects.get(effect);
|
|
3088
|
+
return deps ? Array.from(deps) : [];
|
|
3089
|
+
}
|
|
3090
|
+
const mutationHistory = [];
|
|
3091
|
+
let mutationCounter = 0;
|
|
3092
|
+
function addToMutationHistory(source, target, obj, prop, evolution) {
|
|
3093
|
+
const record = {
|
|
3094
|
+
id: ++mutationCounter,
|
|
3095
|
+
timestamp: Date.now(),
|
|
3096
|
+
source: source ? ensureEffectName(source) : 'External',
|
|
3097
|
+
target: ensureEffectName(target),
|
|
3098
|
+
objectName: ensureObjectName(obj),
|
|
3099
|
+
prop: String(prop),
|
|
3100
|
+
type: evolution.type,
|
|
3101
|
+
};
|
|
3102
|
+
mutationHistory.push(record);
|
|
3103
|
+
if (mutationHistory.length > options.introspection.historySize) {
|
|
3104
|
+
mutationHistory.shift();
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Get the recent mutation history
|
|
3109
|
+
*/
|
|
3110
|
+
function getMutationHistory() {
|
|
3111
|
+
return [...mutationHistory];
|
|
3112
|
+
}
|
|
3113
|
+
// --- Auto DevTools Initialization ---
|
|
3114
|
+
// Automatically enable devtools in development environments
|
|
3115
|
+
/**
|
|
3116
|
+
* Checks if we're in a development environment
|
|
3117
|
+
* Detection order:
|
|
3118
|
+
* 1. process.env.NODE_ENV (Node.js)
|
|
3119
|
+
* 2. import.meta.env.DEV (Vite)
|
|
3120
|
+
* 3. import.meta.env.PROD === false (Vite alternative)
|
|
3121
|
+
* 4. Assumes development if none of the above are set (safe default)
|
|
3122
|
+
*/
|
|
3123
|
+
function isDevelopmentMode() {
|
|
3124
|
+
// Check for explicit production flag first
|
|
3125
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
|
|
3126
|
+
return false;
|
|
3127
|
+
}
|
|
3128
|
+
// Check for Vite's DEV flag
|
|
3129
|
+
if (typeof import.meta !== 'undefined') {
|
|
3130
|
+
const meta = import.meta;
|
|
3131
|
+
if (meta.env?.PROD === true) {
|
|
3132
|
+
return false;
|
|
3133
|
+
}
|
|
3134
|
+
if (meta.env?.DEV === true) {
|
|
3135
|
+
return true;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
// Check for custom global override
|
|
3139
|
+
if (typeof globalThis !== 'undefined' && '__MUTTS_DEV_MODE__' in globalThis) {
|
|
3140
|
+
return globalThis.__MUTTS_DEV_MODE__ !== false;
|
|
3141
|
+
}
|
|
3142
|
+
// Default to development (safer to enable in dev than disable in prod)
|
|
3143
|
+
return true;
|
|
3144
|
+
}
|
|
3145
|
+
// Auto-enable devtools when the module loads in development
|
|
3146
|
+
if (isDevelopmentMode() && !devtoolsEnabled) {
|
|
3147
|
+
enableDevTools();
|
|
3148
|
+
// Optional: Log that devtools were enabled (only in development)
|
|
3149
|
+
if (typeof console !== 'undefined' && console.info) {
|
|
3150
|
+
// Do not log these in when mode === 'test'
|
|
3151
|
+
console.info('🦴 Mutts DevTools enabled automatically in development mode');
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
/**
|
|
3156
|
+
* Finds a cycle in a sequence of functions by looking for the first repetition
|
|
3157
|
+
*/
|
|
3158
|
+
function findCycleInChain(roots) {
|
|
3159
|
+
const seen = new Map();
|
|
3160
|
+
for (let i = 0; i < roots.length; i++) {
|
|
3161
|
+
const root = roots[i];
|
|
3162
|
+
if (seen.has(root)) {
|
|
3163
|
+
return roots.slice(seen.get(root));
|
|
3164
|
+
}
|
|
3165
|
+
seen.set(root, i);
|
|
3166
|
+
}
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Formats a list of function roots into a readable trace
|
|
3171
|
+
*/
|
|
3172
|
+
function formatRoots(roots, limit = 20) {
|
|
3173
|
+
const names = roots.map((r) => r.name || '<anonymous>');
|
|
3174
|
+
if (names.length <= limit)
|
|
3175
|
+
return names.join(' → ');
|
|
3176
|
+
const start = names.slice(0, 5);
|
|
3177
|
+
const end = names.slice(-10);
|
|
3178
|
+
return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
|
|
3179
|
+
}
|
|
3180
|
+
// Nested map structure for efficient counting and batch cleanup
|
|
3181
|
+
// batchId -> effect root -> obj -> prop -> count
|
|
3182
|
+
let activationRegistry;
|
|
3183
|
+
const activationLog = new Array(100);
|
|
3184
|
+
/**
|
|
3185
|
+
* Returns the activation log containing recent effect activations for debugging.
|
|
3186
|
+
* The log is a circular buffer of the last 100 activations.
|
|
3187
|
+
*
|
|
3188
|
+
* @returns Array of activation records
|
|
3189
|
+
*/
|
|
3190
|
+
function getActivationLog() {
|
|
3191
|
+
return activationLog;
|
|
3192
|
+
}
|
|
3193
|
+
function recordActivation(effect, obj, evolution, prop) {
|
|
3194
|
+
const root = getRoot(effect);
|
|
3195
|
+
if (!activationRegistry)
|
|
3196
|
+
return;
|
|
3197
|
+
let effectData = activationRegistry.get(root);
|
|
3198
|
+
if (!effectData) {
|
|
3199
|
+
effectData = new Map();
|
|
3200
|
+
activationRegistry.set(root, effectData);
|
|
3201
|
+
}
|
|
3202
|
+
let objData = effectData.get(obj);
|
|
3203
|
+
if (!objData) {
|
|
3204
|
+
objData = new Map();
|
|
3205
|
+
effectData.set(obj, objData);
|
|
3206
|
+
}
|
|
3207
|
+
const count = (objData.get(prop) ?? 0) + 1;
|
|
3208
|
+
objData.set(prop, count);
|
|
3209
|
+
// Keep a limited history for diagnostics
|
|
3210
|
+
activationLog.unshift({
|
|
3211
|
+
effect,
|
|
3212
|
+
obj,
|
|
3213
|
+
evolution,
|
|
3214
|
+
prop,
|
|
3215
|
+
});
|
|
3216
|
+
activationLog.pop();
|
|
3217
|
+
if (count >= options.maxTriggerPerBatch) {
|
|
3218
|
+
const effectName = root?.name || 'anonymous';
|
|
3219
|
+
const message = `Aggressive trigger detected: effect "${effectName}" triggered ${count} times in the batch by the same cause.`;
|
|
3220
|
+
if (options.maxEffectReaction === 'throw') {
|
|
3221
|
+
throw new ReactiveError(message, {
|
|
3222
|
+
code: ReactiveErrorCode.MaxReactionExceeded,
|
|
3223
|
+
count,
|
|
3224
|
+
effect: effectName,
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
options.warn(`[reactive] ${message}`);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
/**
|
|
3231
|
+
* Registers a debug callback that is called when the current effect is triggered by a dependency change
|
|
3232
|
+
*
|
|
3233
|
+
* This function is useful for debugging purposes as it pin-points exactly which reactive property
|
|
3234
|
+
* change triggered the effect. The callback receives information about:
|
|
3235
|
+
* - The object that changed
|
|
3236
|
+
* - The type of change (evolution)
|
|
3237
|
+
* - The specific property that changed
|
|
3238
|
+
*
|
|
3239
|
+
* **Note:** The tracker callback is automatically removed after being called once. If you need
|
|
3240
|
+
* to track multiple triggers, call `onEffectTrigger` again within the effect.
|
|
3241
|
+
*
|
|
3242
|
+
* @param onTouch - Callback function that receives (obj, evolution, prop) when the effect is triggered
|
|
3243
|
+
* @throws {Error} If called outside of an effect context
|
|
3244
|
+
*
|
|
3245
|
+
* @example
|
|
3246
|
+
* ```typescript
|
|
3247
|
+
* const state = reactive({ count: 0, name: 'John' })
|
|
3248
|
+
*
|
|
3249
|
+
* effect(() => {
|
|
3250
|
+
* // Register a tracker to see what triggers this effect
|
|
3251
|
+
* onEffectTrigger((obj, evolution, prop) => {
|
|
3252
|
+
* console.log(`Effect triggered by:`, {
|
|
3253
|
+
* object: obj,
|
|
3254
|
+
* change: evolution.type,
|
|
3255
|
+
* property: prop
|
|
3256
|
+
* })
|
|
3257
|
+
* })
|
|
3258
|
+
*
|
|
3259
|
+
* // Access reactive properties
|
|
3260
|
+
* console.log(state.count, state.name)
|
|
3261
|
+
* })
|
|
3262
|
+
*
|
|
3263
|
+
* state.count = 5
|
|
3264
|
+
* ```
|
|
3265
|
+
*/
|
|
3266
|
+
function onEffectTrigger(onTouch, effect) {
|
|
3267
|
+
effect ?? (effect = getActiveEffect());
|
|
3268
|
+
if (!effect)
|
|
3269
|
+
throw new Error('Tracking an effect trigger while not in an effect');
|
|
3270
|
+
const node = getEffectNode(effect);
|
|
3271
|
+
if (!node.trackers)
|
|
3272
|
+
node.trackers = [onTouch];
|
|
3273
|
+
else
|
|
3274
|
+
node.trackers.push(onTouch);
|
|
3275
|
+
}
|
|
3276
|
+
function raiseEffectTrackers(effect, obj, evolution, prop) {
|
|
3277
|
+
const node = getEffectNode(effect);
|
|
3278
|
+
const trackers = node.trackers;
|
|
3279
|
+
if (trackers) {
|
|
3280
|
+
for (const tracker of trackers)
|
|
3281
|
+
tracker(obj, evolution, prop, effect);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
function onEffectThrow(onThrow, effect) {
|
|
3285
|
+
effect ?? (effect = getActiveEffect());
|
|
3286
|
+
if (!effect)
|
|
3287
|
+
throw new Error('Tracking an effect throw while not in an effect');
|
|
3288
|
+
const node = getEffectNode(effect);
|
|
3289
|
+
if (!node.catchers)
|
|
3290
|
+
node.catchers = [onThrow];
|
|
3291
|
+
else
|
|
3292
|
+
node.catchers.push(onThrow);
|
|
3293
|
+
}
|
|
3294
|
+
// Dependency graph: tracks which effects trigger which other effects
|
|
3295
|
+
// Uses roots (Function) as keys for consistency
|
|
3296
|
+
let effectTriggers = new WeakMap();
|
|
3297
|
+
let effectTriggeredBy = new WeakMap();
|
|
3298
|
+
// Transitive closures: track all indirect relationships
|
|
3299
|
+
// causesClosure: for each effect, all effects that trigger it (directly or indirectly)
|
|
3300
|
+
// consequencesClosure: for each effect, all effects that it triggers (directly or indirectly)
|
|
3301
|
+
let causesClosure = new WeakMap();
|
|
3302
|
+
let consequencesClosure = new WeakMap();
|
|
3303
|
+
let broken = false;
|
|
3304
|
+
/**
|
|
3305
|
+
* Gets or creates an IterableWeakSet for a closure map
|
|
3306
|
+
*/
|
|
3307
|
+
function getOrCreateClosure(closure, root) {
|
|
3308
|
+
let set = closure.get(root);
|
|
3309
|
+
if (!set) {
|
|
3310
|
+
set = new IterableWeakSet();
|
|
3311
|
+
closure.set(root, set);
|
|
3312
|
+
}
|
|
3313
|
+
return set;
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Adds an edge to the dependency graph: callerRoot → targetRoot
|
|
3317
|
+
* Also maintains transitive closures
|
|
3318
|
+
* @param callerRoot - Root function of the effect that triggers
|
|
3319
|
+
* @param targetRoot - Root function of the effect being triggered
|
|
3320
|
+
*/
|
|
3321
|
+
function addGraphEdge(callerRoot, targetRoot) {
|
|
3322
|
+
if (options.cycleHandling === 'production')
|
|
3323
|
+
return;
|
|
3324
|
+
// Add to forward graph: callerRoot → targetRoot
|
|
3325
|
+
const triggers = effectTriggers.get(callerRoot);
|
|
3326
|
+
if (!triggers) {
|
|
3327
|
+
const newTriggers = new IterableWeakSet();
|
|
3328
|
+
newTriggers.add(targetRoot);
|
|
3329
|
+
effectTriggers.set(callerRoot, newTriggers);
|
|
3330
|
+
}
|
|
3331
|
+
else {
|
|
3332
|
+
triggers.add(targetRoot);
|
|
3333
|
+
}
|
|
3334
|
+
// Add to reverse graph: targetRoot ← callerRoot
|
|
3335
|
+
let triggeredBy = effectTriggeredBy.get(targetRoot);
|
|
3336
|
+
if (!triggeredBy) {
|
|
3337
|
+
triggeredBy = new IterableWeakSet();
|
|
3338
|
+
effectTriggeredBy.set(targetRoot, triggeredBy);
|
|
3339
|
+
}
|
|
3340
|
+
triggeredBy.add(callerRoot);
|
|
3341
|
+
// Update transitive closures
|
|
3342
|
+
// When U→V is added, we need to propagate the relationship:
|
|
3343
|
+
// 1. Add U to causesClosure(V) and V to consequencesClosure(U) (direct relationship)
|
|
3344
|
+
// 2. For each X in causesClosure(U): add V to consequencesClosure(X) and X to causesClosure(V)
|
|
3345
|
+
// 3. For each Y in consequencesClosure(V): add U to causesClosure(Y) and Y to consequencesClosure(U)
|
|
3346
|
+
// Note: Self-loops (U→U) are not added to closures - if an effect appears in its own closure,
|
|
3347
|
+
// it means there's an indirect cycle that should be detected
|
|
3348
|
+
// Self-loops are explicitly ignored - an effect reading and writing the same property
|
|
3349
|
+
// (e.g., obj.prop++) should not create a dependency relationship or appear in closures
|
|
3350
|
+
if (callerRoot === targetRoot) {
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
const uConsequences = getOrCreateClosure(consequencesClosure, callerRoot);
|
|
3354
|
+
const vCauses = getOrCreateClosure(causesClosure, targetRoot);
|
|
3355
|
+
// 1. Add direct relationship
|
|
3356
|
+
uConsequences.add(targetRoot);
|
|
3357
|
+
vCauses.add(callerRoot);
|
|
3358
|
+
// 2. For each X in causesClosure(U): X→U→V means X→V
|
|
3359
|
+
const uCausesSet = causesClosure.get(callerRoot);
|
|
3360
|
+
if (uCausesSet) {
|
|
3361
|
+
for (const x of uCausesSet) {
|
|
3362
|
+
// Skip if this would create a self-loop
|
|
3363
|
+
if (x === targetRoot)
|
|
3364
|
+
continue;
|
|
3365
|
+
const xConsequences = getOrCreateClosure(consequencesClosure, x);
|
|
3366
|
+
xConsequences.add(targetRoot);
|
|
3367
|
+
vCauses.add(x);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
// 3. For each Y in consequencesClosure(V): U→V→Y means U→Y
|
|
3371
|
+
const vConsequencesSet = consequencesClosure.get(targetRoot);
|
|
3372
|
+
if (vConsequencesSet) {
|
|
3373
|
+
for (const y of vConsequencesSet) {
|
|
3374
|
+
// Skip if this would create a self-loop
|
|
3375
|
+
if (y === callerRoot)
|
|
3376
|
+
continue;
|
|
3377
|
+
const yCauses = getOrCreateClosure(causesClosure, y);
|
|
3378
|
+
yCauses.add(callerRoot);
|
|
3379
|
+
uConsequences.add(y);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
// 4. Cross-product: for each X in causesClosure(U) and Y in consequencesClosure(V): X→Y
|
|
3383
|
+
if (uCausesSet?.size && vConsequencesSet?.size) {
|
|
3384
|
+
for (const x of uCausesSet) {
|
|
3385
|
+
const xConsequences = getOrCreateClosure(consequencesClosure, x);
|
|
3386
|
+
for (const y of vConsequencesSet) {
|
|
3387
|
+
// Skip if this would create a self-loop
|
|
3388
|
+
if (x === y)
|
|
3389
|
+
continue;
|
|
3390
|
+
xConsequences.add(y);
|
|
3391
|
+
const yCauses = getOrCreateClosure(causesClosure, y);
|
|
3392
|
+
yCauses.add(x);
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
/**
|
|
3398
|
+
* Checks if there's a path from start to end in the dependency graph, excluding a specific node
|
|
3399
|
+
* Uses BFS to find any path that doesn't go through the excluded node
|
|
3400
|
+
* @param start - Starting node
|
|
3401
|
+
* @param end - Target node
|
|
3402
|
+
* @param exclude - Node to exclude from the path
|
|
3403
|
+
* @returns true if a path exists without going through the excluded node
|
|
3404
|
+
*/
|
|
3405
|
+
function hasPathExcluding(start, end, exclude) {
|
|
3406
|
+
if (start === end)
|
|
3407
|
+
return true;
|
|
3408
|
+
if (start === exclude)
|
|
3409
|
+
return false;
|
|
3410
|
+
const visited = new Set();
|
|
3411
|
+
const queue = [start];
|
|
3412
|
+
visited.add(start);
|
|
3413
|
+
visited.add(exclude); // Pre-mark excluded node as visited to skip it
|
|
3414
|
+
while (queue.length > 0) {
|
|
3415
|
+
const current = queue.shift();
|
|
3416
|
+
const triggers = effectTriggers.get(current);
|
|
3417
|
+
if (!triggers)
|
|
3418
|
+
continue;
|
|
3419
|
+
for (const next of triggers) {
|
|
3420
|
+
if (next === end)
|
|
3421
|
+
return true;
|
|
3422
|
+
if (!visited.has(next)) {
|
|
3423
|
+
visited.add(next);
|
|
3424
|
+
queue.push(next);
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
return false;
|
|
3429
|
+
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Removes all edges involving the given effect from the dependency graph
|
|
3432
|
+
* Also cleans up transitive closures by propagating cleanup to all affected effects
|
|
3433
|
+
* Called when an effect is stopped/cleaned up
|
|
3434
|
+
* @param effect - The effect being cleaned up
|
|
3435
|
+
*/
|
|
3436
|
+
function cleanupEffectFromGraph(effect) {
|
|
3437
|
+
if (options.cycleHandling === 'production')
|
|
3438
|
+
return;
|
|
3439
|
+
const root = getRoot(effect);
|
|
3440
|
+
// Get closures before removing direct edges (needed for propagation)
|
|
3441
|
+
const rootCauses = causesClosure.get(root);
|
|
3442
|
+
const rootConsequences = consequencesClosure.get(root);
|
|
3443
|
+
// Remove from effectTriggers (outgoing edges)
|
|
3444
|
+
const triggers = effectTriggers.get(root);
|
|
3445
|
+
if (triggers) {
|
|
3446
|
+
// Remove this root from all targets' effectTriggeredBy sets
|
|
3447
|
+
for (const targetRoot of triggers) {
|
|
3448
|
+
const triggeredBy = effectTriggeredBy.get(targetRoot);
|
|
3449
|
+
triggeredBy?.delete(root);
|
|
3450
|
+
}
|
|
3451
|
+
effectTriggers.delete(root);
|
|
3452
|
+
}
|
|
3453
|
+
// Remove from effectTriggeredBy (incoming edges)
|
|
3454
|
+
const triggeredBy = effectTriggeredBy.get(root);
|
|
3455
|
+
if (triggeredBy) {
|
|
3456
|
+
// Remove this root from all sources' effectTriggers sets
|
|
3457
|
+
for (const sourceRoot of triggeredBy) {
|
|
3458
|
+
const triggers = effectTriggers.get(sourceRoot);
|
|
3459
|
+
triggers?.delete(root);
|
|
3460
|
+
}
|
|
3461
|
+
effectTriggeredBy.delete(root);
|
|
3462
|
+
}
|
|
3463
|
+
// Propagate closure cleanup to all affected effects
|
|
3464
|
+
// When removing B from A → B → C:
|
|
3465
|
+
// - Remove B from causesClosure(C) and consequencesClosure(A)
|
|
3466
|
+
// - For each X in causesClosure(B): remove C from consequencesClosure(X) if B was the only path
|
|
3467
|
+
// - For each Y in consequencesClosure(B): remove A from causesClosure(Y) if B was the only path
|
|
3468
|
+
// - Remove transitive relationships that depended on B
|
|
3469
|
+
if (rootCauses) {
|
|
3470
|
+
// For each X that triggers root: remove root from X's consequences
|
|
3471
|
+
// Only remove root's consequences if no alternate path exists
|
|
3472
|
+
for (const causeRoot of rootCauses) {
|
|
3473
|
+
const causeConsequences = consequencesClosure.get(causeRoot);
|
|
3474
|
+
if (causeConsequences) {
|
|
3475
|
+
// Remove root itself (it's being cleaned up)
|
|
3476
|
+
causeConsequences.delete(root);
|
|
3477
|
+
// Only remove consequences of root if there's no alternate path from causeRoot to them
|
|
3478
|
+
if (rootConsequences) {
|
|
3479
|
+
for (const consequence of rootConsequences) {
|
|
3480
|
+
// Check if causeRoot can still reach consequence without going through root
|
|
3481
|
+
if (!hasPathExcluding(causeRoot, consequence, root)) {
|
|
3482
|
+
causeConsequences.delete(consequence);
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
if (rootConsequences) {
|
|
3490
|
+
// For each Y that root triggers: remove root from Y's causes
|
|
3491
|
+
// Only remove root's causes if no alternate path exists
|
|
3492
|
+
for (const consequenceRoot of rootConsequences) {
|
|
3493
|
+
const consequenceCauses = causesClosure.get(consequenceRoot);
|
|
3494
|
+
if (consequenceCauses) {
|
|
3495
|
+
// Remove root itself (it's being cleaned up)
|
|
3496
|
+
consequenceCauses.delete(root);
|
|
3497
|
+
// Only remove causes of root if there's no alternate path from them to consequenceRoot
|
|
3498
|
+
if (rootCauses) {
|
|
3499
|
+
for (const cause of rootCauses) {
|
|
3500
|
+
// Check if cause can still reach consequenceRoot without going through root
|
|
3501
|
+
if (!hasPathExcluding(cause, consequenceRoot, root)) {
|
|
3502
|
+
consequenceCauses.delete(cause);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
// Cross-product cleanup: for each X in causesClosure(B) and Y in consequencesClosure(B),
|
|
3510
|
+
// remove X→Y if B was the only path connecting them
|
|
3511
|
+
if (rootCauses && rootConsequences) {
|
|
3512
|
+
for (const x of rootCauses) {
|
|
3513
|
+
const xConsequences = consequencesClosure.get(x);
|
|
3514
|
+
if (xConsequences) {
|
|
3515
|
+
for (const y of rootConsequences) {
|
|
3516
|
+
// Check if there's still a path from X to Y without going through root
|
|
3517
|
+
// Use BFS to find any path that doesn't include root
|
|
3518
|
+
if (!hasPathExcluding(x, y, root)) {
|
|
3519
|
+
xConsequences.delete(y);
|
|
3520
|
+
const yCauses = causesClosure.get(y);
|
|
3521
|
+
yCauses?.delete(x);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
// Finally, delete the closures for this effect
|
|
3528
|
+
causesClosure.delete(root);
|
|
3529
|
+
consequencesClosure.delete(root);
|
|
3530
|
+
}
|
|
3531
|
+
// Track currently executing effects to prevent re-execution
|
|
3532
|
+
// These are all the effects triggered under `activeEffect`
|
|
3533
|
+
let batchQueue;
|
|
3534
|
+
function hasBatched(effect) {
|
|
3535
|
+
return batchQueue?.all.has(getRoot(effect));
|
|
3536
|
+
}
|
|
3537
|
+
const batchCleanups = new Set();
|
|
3538
|
+
/**
|
|
3539
|
+
* Computes and caches in-degrees for all effects in the batch
|
|
3540
|
+
* Called once when batch starts or when new effects are added
|
|
3541
|
+
*/
|
|
3542
|
+
function computeAllInDegrees(batch) {
|
|
3543
|
+
if (options.cycleHandling === 'production')
|
|
3544
|
+
return;
|
|
3545
|
+
const activeEffect = getActiveEffect();
|
|
3546
|
+
const activeRoot = activeEffect ? getRoot(activeEffect) : null;
|
|
3547
|
+
// Reset all in-degrees
|
|
3548
|
+
batch.inDegrees.clear();
|
|
3549
|
+
for (const [root] of batch.all) {
|
|
3550
|
+
let inDegree = 0;
|
|
3551
|
+
const causes = causesClosure.get(root);
|
|
3552
|
+
if (causes) {
|
|
3553
|
+
for (const causeRoot of causes) {
|
|
3554
|
+
// Only count if it's in the batch and not the active/self effect
|
|
3555
|
+
if (batch.all.has(causeRoot) && causeRoot !== activeRoot && causeRoot !== root) {
|
|
3556
|
+
inDegree++;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
batch.inDegrees.set(root, inDegree);
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
/**
|
|
3564
|
+
* Decrements in-degrees of all effects that depend on the executed effect
|
|
3565
|
+
* Called after an effect is executed to update the cached in-degrees
|
|
3566
|
+
*/
|
|
3567
|
+
function decrementInDegreesForExecuted(batch, executedRoot) {
|
|
3568
|
+
// Get all effects that this executed effect triggers
|
|
3569
|
+
const consequences = consequencesClosure.get(executedRoot);
|
|
3570
|
+
if (!consequences)
|
|
3571
|
+
return;
|
|
3572
|
+
for (const consequenceRoot of consequences) {
|
|
3573
|
+
// Only update if it's still in the batch
|
|
3574
|
+
if (batch.all.has(consequenceRoot)) {
|
|
3575
|
+
const currentDegree = batch.inDegrees.get(consequenceRoot) ?? 0;
|
|
3576
|
+
if (currentDegree > 0) {
|
|
3577
|
+
batch.inDegrees.set(consequenceRoot, currentDegree - 1);
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Finds a path from startRoot to endRoot in the dependency graph
|
|
3584
|
+
* Uses DFS to find the path through direct edges
|
|
3585
|
+
* @param startRoot - Starting effect root
|
|
3586
|
+
* @param endRoot - Target effect root
|
|
3587
|
+
* @param visited - Set of visited nodes (for recursion)
|
|
3588
|
+
* @param path - Current path being explored
|
|
3589
|
+
* @returns Path from startRoot to endRoot, or empty array if no path exists
|
|
3590
|
+
*/
|
|
3591
|
+
function findPath(startRoot, endRoot, visited = new Set(), path = []) {
|
|
3592
|
+
if (startRoot === endRoot) {
|
|
3593
|
+
return [...path, endRoot];
|
|
3594
|
+
}
|
|
3595
|
+
if (visited.has(startRoot)) {
|
|
3596
|
+
return [];
|
|
3597
|
+
}
|
|
3598
|
+
visited.add(startRoot);
|
|
3599
|
+
const newPath = [...path, startRoot];
|
|
3600
|
+
const triggers = effectTriggers.get(startRoot);
|
|
3601
|
+
if (triggers) {
|
|
3602
|
+
for (const targetRoot of triggers) {
|
|
3603
|
+
const result = findPath(targetRoot, endRoot, visited, newPath);
|
|
3604
|
+
if (result.length > 0) {
|
|
3605
|
+
return result;
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
return [];
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
3612
|
+
* Gets the cycle path when adding an edge would create a cycle
|
|
3613
|
+
* @param callerRoot - Root of the effect that triggers
|
|
3614
|
+
* @param targetRoot - Root of the effect being triggered
|
|
3615
|
+
* @returns Array of effect roots forming the cycle, or empty array if no cycle
|
|
3616
|
+
*/
|
|
3617
|
+
function getCyclePathForEdge(callerRoot, targetRoot) {
|
|
3618
|
+
// Find path from targetRoot back to callerRoot (this is the existing path)
|
|
3619
|
+
// Then adding callerRoot -> targetRoot completes the cycle
|
|
3620
|
+
const path = findPath(targetRoot, callerRoot);
|
|
3621
|
+
if (path.length > 0) {
|
|
3622
|
+
// The cycle is: callerRoot -> targetRoot -> ... -> callerRoot
|
|
3623
|
+
return [callerRoot, ...path];
|
|
3624
|
+
}
|
|
3625
|
+
return [];
|
|
3626
|
+
}
|
|
3627
|
+
/**
|
|
3628
|
+
* Checks if adding an edge would create a cycle
|
|
3629
|
+
* Uses causesClosure to check if callerRoot is already a cause of targetRoot
|
|
3630
|
+
* Self-loops (callerRoot === targetRoot) are explicitly ignored and return false
|
|
3631
|
+
*
|
|
3632
|
+
* **Note**: This is the primary optimization benefit of the transitive closure system.
|
|
3633
|
+
* It allows detecting cycles in O(1) time before they are executed.
|
|
3634
|
+
*
|
|
3635
|
+
* @param callerRoot - Root of the effect that triggers
|
|
3636
|
+
* @param targetRoot - Root of the effect being triggered
|
|
3637
|
+
* @returns true if adding this edge would create a cycle
|
|
3638
|
+
*/
|
|
3639
|
+
function wouldCreateCycle(callerRoot, targetRoot) {
|
|
3640
|
+
// Self-loops are explicitly ignored - an effect reading and writing the same property
|
|
3641
|
+
// (e.g., obj.prop++) should not create a dependency relationship
|
|
3642
|
+
if (callerRoot === targetRoot) {
|
|
3643
|
+
return false;
|
|
3644
|
+
}
|
|
3645
|
+
// Check if targetRoot already triggers callerRoot (directly or indirectly)
|
|
3646
|
+
// This would create a cycle: callerRoot -> targetRoot -> ... -> callerRoot
|
|
3647
|
+
// Using consequencesClosure: if targetRoot triggers callerRoot, then callerRoot is in consequencesClosure(targetRoot)
|
|
3648
|
+
const targetConsequences = consequencesClosure.get(targetRoot);
|
|
3649
|
+
if (targetConsequences?.has(callerRoot)) {
|
|
3650
|
+
return true; // Cycle detected: targetRoot -> ... -> callerRoot, and we're adding callerRoot -> targetRoot
|
|
3651
|
+
}
|
|
3652
|
+
return false;
|
|
3653
|
+
}
|
|
3654
|
+
/**
|
|
3655
|
+
* Adds an effect to the batch queue
|
|
3656
|
+
* @param effect - The effect to add
|
|
3657
|
+
* @param caller - The active effect that triggered this one (optional)
|
|
3658
|
+
* @param immediate - If true, don't create edges in the dependency graph
|
|
3659
|
+
*/
|
|
3660
|
+
function addToBatch(effect, caller, immediate) {
|
|
3661
|
+
const node = getEffectNode(effect);
|
|
3662
|
+
node.cleanup?.();
|
|
3663
|
+
// If the effect was stopped during cleanup (e.g. lazy memoization), don't add it to the batch
|
|
3664
|
+
if (node.stopped) {
|
|
3665
|
+
// console.log(`[DEBUG] addToBatch: ${effect.name} is stopped`)
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
if (!batchQueue) {
|
|
3669
|
+
// console.log(`[DEBUG] addToBatch: no batchQueue`)
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
const root = getRoot(effect);
|
|
3673
|
+
// 1. Add to batch first (needed for cycle detection)
|
|
3674
|
+
if (options.cycleHandling === 'production') {
|
|
3675
|
+
// Production mode: FIFO (delete and re-add to move to end)
|
|
3676
|
+
if (batchQueue.all.has(root)) {
|
|
3677
|
+
batchQueue.all.delete(root);
|
|
3678
|
+
}
|
|
3679
|
+
batchQueue.all.set(root, effect);
|
|
3680
|
+
}
|
|
3681
|
+
else {
|
|
3682
|
+
// Dev/Debug mode: Set if not present (preserve order?)
|
|
3683
|
+
// actually, we might want to update it?
|
|
3684
|
+
// For now, simple set
|
|
3685
|
+
if (batchQueue.all.has(root)) {
|
|
3686
|
+
// console.log(`[DEBUG] ${root.name} already in batch`)
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
batchQueue.all.set(root, effect);
|
|
3690
|
+
// console.log(`[DEBUG] ${root.name} added to batch`)
|
|
3691
|
+
}
|
|
3692
|
+
// batchQueue.all.set(root, effect)
|
|
3693
|
+
// console.log(`[DEBUG] addToBatch: ${root.name} added to batch`)
|
|
3694
|
+
if (caller && !immediate && options.cycleHandling !== 'production') {
|
|
3695
|
+
const callerRoot = getRoot(caller);
|
|
3696
|
+
// const root = getRoot(effect) // Already have root
|
|
3697
|
+
// Check for cycle BEFORE adding edge
|
|
3698
|
+
if (wouldCreateCycle(callerRoot, root)) {
|
|
3699
|
+
const cyclePath = getCyclePathForEdge(callerRoot, root);
|
|
3700
|
+
const cycleMessage = cyclePath.length > 0
|
|
3701
|
+
? `Cycle detected: ${cyclePath.map((r) => r.name || r.toString()).join(' → ')}`
|
|
3702
|
+
: `Cycle detected: ${callerRoot.name || callerRoot.toString()} → ${root.name || root.toString()} (and back)`;
|
|
3703
|
+
batchQueue.all.delete(root);
|
|
3704
|
+
const causalChain = getTriggerChain(effect);
|
|
3705
|
+
const creationStack = getEffectNode(effect).creationStack;
|
|
3706
|
+
throw new ReactiveError(`[reactive] ${cycleMessage}`, {
|
|
3707
|
+
code: ReactiveErrorCode.CycleDetected,
|
|
3708
|
+
cycle: cyclePath.map((r) => r.name || r.toString()),
|
|
3709
|
+
details: cycleMessage,
|
|
3710
|
+
causalChain,
|
|
3711
|
+
creationStack,
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
addGraphEdge(callerRoot, root);
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
/**
|
|
3718
|
+
* Adds a cleanup function to be called when the current batch of effects completes
|
|
3719
|
+
* @param cleanup - The cleanup function to add
|
|
3720
|
+
*/
|
|
3721
|
+
function addBatchCleanup(cleanup) {
|
|
3722
|
+
if (!batchQueue)
|
|
3723
|
+
cleanup();
|
|
3724
|
+
else
|
|
3725
|
+
batchCleanups.add(cleanup);
|
|
3726
|
+
}
|
|
3727
|
+
/**
|
|
3728
|
+
* Semantic alias for `addBatchCleanup` - defers work to the end of the current reactive batch.
|
|
3729
|
+
*
|
|
3730
|
+
* Use this when an effect needs to perform an action that would modify state the effect depends on,
|
|
3731
|
+
* which would create a reactive cycle. The deferred callback runs after all effects complete.
|
|
3732
|
+
*
|
|
3733
|
+
* @param callback - The callback to defer until after the current batch completes
|
|
3734
|
+
*
|
|
3735
|
+
* @example
|
|
3736
|
+
* ```typescript
|
|
3737
|
+
* effect(() => {
|
|
3738
|
+
* processData()
|
|
3739
|
+
*
|
|
3740
|
+
* // Defer to avoid cycle (createMovement modifies state this effect reads)
|
|
3741
|
+
* defer(() => {
|
|
3742
|
+
* createMovement(data)
|
|
3743
|
+
* })
|
|
3744
|
+
* })
|
|
3745
|
+
* ```
|
|
3746
|
+
*/
|
|
3747
|
+
const defer = addBatchCleanup;
|
|
3748
|
+
/**
|
|
3749
|
+
* Gets a cycle path for debugging
|
|
3750
|
+
* Uses DFS to find cycles in the batch
|
|
3751
|
+
* @param batchQueue - The batch queue
|
|
3752
|
+
* @returns Array of effect roots forming a cycle
|
|
3753
|
+
*/
|
|
3754
|
+
function getCyclePath(batchQueue) {
|
|
3755
|
+
// If all effects have in-degree > 0, there must be a cycle
|
|
3756
|
+
// Use DFS to find it
|
|
3757
|
+
const visited = new Set();
|
|
3758
|
+
const recursionStack = new Set();
|
|
3759
|
+
const path = [];
|
|
3760
|
+
for (const [root] of batchQueue.all) {
|
|
3761
|
+
if (visited.has(root))
|
|
3762
|
+
continue;
|
|
3763
|
+
const cycle = findCycle(root, visited, recursionStack, path, batchQueue);
|
|
3764
|
+
if (cycle.length > 0) {
|
|
3765
|
+
return cycle;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
return [];
|
|
3769
|
+
}
|
|
3770
|
+
function findCycle(root, visited, recursionStack, path, batchQueue) {
|
|
3771
|
+
if (recursionStack.has(root)) {
|
|
3772
|
+
// Found a cycle! Return the path from the cycle start to root
|
|
3773
|
+
const cycleStart = path.indexOf(root);
|
|
3774
|
+
return path.slice(cycleStart).concat([root]);
|
|
3775
|
+
}
|
|
3776
|
+
if (visited.has(root)) {
|
|
3777
|
+
return [];
|
|
3778
|
+
}
|
|
3779
|
+
visited.add(root);
|
|
3780
|
+
recursionStack.add(root);
|
|
3781
|
+
path.push(root);
|
|
3782
|
+
// Follow edges to effects in the batch
|
|
3783
|
+
// Use direct edges (effectTriggers) for cycle detection
|
|
3784
|
+
const triggers = effectTriggers.get(root);
|
|
3785
|
+
if (triggers) {
|
|
3786
|
+
for (const targetRoot of triggers) {
|
|
3787
|
+
if (batchQueue.all.has(targetRoot)) {
|
|
3788
|
+
const cycle = findCycle(targetRoot, visited, recursionStack, path, batchQueue);
|
|
3789
|
+
if (cycle.length > 0) {
|
|
3790
|
+
return cycle;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
path.pop();
|
|
3796
|
+
recursionStack.delete(root);
|
|
3797
|
+
return [];
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Executes the next effect in dependency order (using cached in-degrees)
|
|
3801
|
+
* Finds an effect with in-degree 0 and executes it
|
|
3802
|
+
* @returns The return value of the executed effect, or null if batch is complete
|
|
3803
|
+
*/
|
|
3804
|
+
function executeNext(effectuatedRoots) {
|
|
3805
|
+
// Find an effect with in-degree 0 using cached values
|
|
3806
|
+
let nextEffect = null;
|
|
3807
|
+
let nextRoot = null;
|
|
3808
|
+
if (options.cycleHandling === 'production') {
|
|
3809
|
+
// In flat mode, we just take the first effect in the queue (FIFO)
|
|
3810
|
+
const first = batchQueue.all.entries().next().value;
|
|
3811
|
+
if (first) {
|
|
3812
|
+
[nextRoot, nextEffect] = first;
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
else {
|
|
3816
|
+
// Find an effect with in-degree 0 (no dependencies in batch that still need execution)
|
|
3817
|
+
// Using cached in-degrees for O(n) lookup instead of O(n²)
|
|
3818
|
+
for (const [root, effect] of batchQueue.all) {
|
|
3819
|
+
const inDegree = batchQueue.inDegrees.get(root) ?? 0;
|
|
3820
|
+
// console.log(`[DEBUG] executeNext: checking ${root.name}, inDegree: ${inDegree}`)
|
|
3821
|
+
if (inDegree === 0) {
|
|
3822
|
+
nextEffect = effect;
|
|
3823
|
+
nextRoot = root;
|
|
3824
|
+
break;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
if (!nextEffect) {
|
|
3829
|
+
// No effect with in-degree 0 - there must be a cycle
|
|
3830
|
+
// If all effects have dependencies, it means there's a circular dependency
|
|
3831
|
+
if (batchQueue.all.size > 0) {
|
|
3832
|
+
let cycle = getCyclePath(batchQueue);
|
|
3833
|
+
// If we couldn't find a cycle path using direct edges, try using closures
|
|
3834
|
+
// (transitive relationships) - if all effects have in-degree > 0, there must be a cycle
|
|
3835
|
+
if (cycle.length === 0) {
|
|
3836
|
+
// Try to find a cycle using consequencesClosure (transitive relationships)
|
|
3837
|
+
// Note: Self-loops are ignored - we only look for cycles between different effects
|
|
3838
|
+
for (const [root] of batchQueue.all) {
|
|
3839
|
+
const consequences = consequencesClosure.get(root);
|
|
3840
|
+
if (consequences) {
|
|
3841
|
+
// Check if any consequence in the batch also has root as a consequence
|
|
3842
|
+
for (const consequence of consequences) {
|
|
3843
|
+
// Skip self-loops - they are ignored
|
|
3844
|
+
if (consequence === root)
|
|
3845
|
+
continue;
|
|
3846
|
+
if (batchQueue.all.has(consequence)) {
|
|
3847
|
+
const consequenceConsequences = consequencesClosure.get(consequence);
|
|
3848
|
+
if (consequenceConsequences?.has(root)) {
|
|
3849
|
+
// Found cycle: root -> consequence -> root
|
|
3850
|
+
cycle = [root, consequence, root];
|
|
3851
|
+
break;
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
if (cycle.length > 0)
|
|
3856
|
+
break;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
const cycleMessage = cycle.length > 0
|
|
3861
|
+
? `Cycle detected: ${cycle.map((r) => r.name || '<anonymous>').join(' → ')}`
|
|
3862
|
+
: 'Cycle detected in effect batch - all effects have dependencies that prevent execution';
|
|
3863
|
+
throw new ReactiveError(`[reactive] ${cycleMessage}`, {
|
|
3864
|
+
code: ReactiveErrorCode.CycleDetected,
|
|
3865
|
+
cycle: cycle.map((r) => r.name || r.toString()),
|
|
3866
|
+
details: cycleMessage,
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
return null; // Batch complete
|
|
3870
|
+
}
|
|
3871
|
+
effectuatedRoots.push(getRoot(nextEffect));
|
|
3872
|
+
// Execute the effect
|
|
3873
|
+
const result = nextEffect();
|
|
3874
|
+
// Remove from batch and update in-degrees of dependents
|
|
3875
|
+
batchQueue.all.delete(nextRoot);
|
|
3876
|
+
batchQueue.inDegrees.delete(nextRoot);
|
|
3877
|
+
decrementInDegreesForExecuted(batchQueue, nextRoot);
|
|
3878
|
+
return result;
|
|
3879
|
+
}
|
|
3880
|
+
// Track which sub-effects have been executed to prevent infinite loops
|
|
3881
|
+
// These are all the effects triggered under `activeEffect` and all their sub-effects
|
|
3882
|
+
function batch(effect, immediate) {
|
|
3883
|
+
if (broken) {
|
|
3884
|
+
throw new ReactiveError('[reactive] Reactive system is broken after an unrecoverable error. Call reset() to recover.', { code: ReactiveErrorCode.BrokenEffects });
|
|
3885
|
+
}
|
|
3886
|
+
if (!Array.isArray(effect))
|
|
3887
|
+
effect = [effect];
|
|
3888
|
+
const roots = effect.map(getRoot);
|
|
3889
|
+
try {
|
|
3890
|
+
if (batchQueue) {
|
|
3891
|
+
// Nested batch - add to existing
|
|
3892
|
+
options?.chain(roots, getRoot(getActiveEffect()));
|
|
3893
|
+
const caller = getActiveEffect();
|
|
3894
|
+
for (let i = 0; i < effect.length; i++)
|
|
3895
|
+
addToBatch(effect[i], caller, immediate === 'immediate');
|
|
3896
|
+
if (immediate) {
|
|
3897
|
+
const firstReturn = {};
|
|
3898
|
+
// Execute immediately (before batch returns)
|
|
3899
|
+
for (let i = 0; i < effect.length; i++) {
|
|
3900
|
+
try {
|
|
3901
|
+
const rv = effect[i]();
|
|
3902
|
+
if (rv !== undefined && !('value' in firstReturn))
|
|
3903
|
+
firstReturn.value = rv;
|
|
3904
|
+
}
|
|
3905
|
+
finally {
|
|
3906
|
+
const root = getRoot(effect[i]);
|
|
3907
|
+
batchQueue.all.delete(root);
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
return firstReturn.value;
|
|
3911
|
+
}
|
|
3912
|
+
// Otherwise, effects will be picked up in next executeNext() call
|
|
3913
|
+
}
|
|
3914
|
+
else {
|
|
3915
|
+
// New batch - initialize
|
|
3916
|
+
if (!activationRegistry)
|
|
3917
|
+
activationRegistry = new Map();
|
|
3918
|
+
else
|
|
3919
|
+
throw new Error('Batch already in progress');
|
|
3920
|
+
optionCall('beginChain', roots);
|
|
3921
|
+
batchQueue = {
|
|
3922
|
+
all: new Map(),
|
|
3923
|
+
inDegrees: new Map(),
|
|
3924
|
+
};
|
|
3925
|
+
const caller = getActiveEffect();
|
|
3926
|
+
const effectuatedRoots = [];
|
|
3927
|
+
const firstReturn = {};
|
|
3928
|
+
let success = false;
|
|
3929
|
+
try {
|
|
3930
|
+
if (immediate) {
|
|
3931
|
+
// Execute initial effects in providing order
|
|
3932
|
+
for (let i = 0; i < effect.length; i++) {
|
|
3933
|
+
try {
|
|
3934
|
+
const rv = effect[i]();
|
|
3935
|
+
if (rv !== undefined && !('value' in firstReturn))
|
|
3936
|
+
firstReturn.value = rv;
|
|
3937
|
+
}
|
|
3938
|
+
finally {
|
|
3939
|
+
batchQueue.all.delete(getRoot(effect[i]));
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
else {
|
|
3944
|
+
// Add initial effects to batch and compute dependencies
|
|
3945
|
+
const len = effect.length;
|
|
3946
|
+
for (let i = 0; i < len; i++) {
|
|
3947
|
+
addToBatch(effect[i], caller, false);
|
|
3948
|
+
}
|
|
3949
|
+
computeAllInDegrees(batchQueue);
|
|
3950
|
+
}
|
|
3951
|
+
// Process the batch
|
|
3952
|
+
while (batchQueue.all.size > 0 || batchCleanups.size > 0) {
|
|
3953
|
+
if (batchQueue.all.size > 0) {
|
|
3954
|
+
if (effectuatedRoots.length > options.maxEffectChain) {
|
|
3955
|
+
const cycle = findCycleInChain(effectuatedRoots);
|
|
3956
|
+
const trace = formatRoots(effectuatedRoots);
|
|
3957
|
+
const message = cycle
|
|
3958
|
+
? `Max effect chain reached (cycle detected: ${formatRoots(cycle)})`
|
|
3959
|
+
: `Max effect chain reached (trace: ${trace})`;
|
|
3960
|
+
const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : [];
|
|
3961
|
+
const queued = queuedRoots.map((r) => r.name || '<anonymous>');
|
|
3962
|
+
const debugInfo = {
|
|
3963
|
+
code: ReactiveErrorCode.MaxDepthExceeded,
|
|
3964
|
+
effectuatedRoots,
|
|
3965
|
+
cycle,
|
|
3966
|
+
trace,
|
|
3967
|
+
maxEffectChain: options.maxEffectChain,
|
|
3968
|
+
queued: queued.slice(0, 50),
|
|
3969
|
+
queuedCount: queued.length,
|
|
3970
|
+
// Try to get causation for the last effect
|
|
3971
|
+
causalChain: effectuatedRoots.length > 0
|
|
3972
|
+
? getTriggerChain(batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1]))
|
|
3973
|
+
: [],
|
|
3974
|
+
};
|
|
3975
|
+
switch (options.maxEffectReaction) {
|
|
3976
|
+
case 'throw':
|
|
3977
|
+
throw new ReactiveError(`[reactive] ${message}`, debugInfo);
|
|
3978
|
+
case 'debug':
|
|
3979
|
+
// biome-ignore lint/suspicious/noDebugger: This is the whole point here
|
|
3980
|
+
debugger;
|
|
3981
|
+
throw new ReactiveError(`[reactive] ${message}`, debugInfo);
|
|
3982
|
+
case 'warn':
|
|
3983
|
+
options.warn(`[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`);
|
|
3984
|
+
break;
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
const rv = executeNext(effectuatedRoots);
|
|
3988
|
+
if (rv !== undefined && !('value' in firstReturn))
|
|
3989
|
+
firstReturn.value = rv;
|
|
3990
|
+
}
|
|
3991
|
+
else {
|
|
3992
|
+
// Process cleanups. If they trigger more effects, they will be caught in the next iteration.
|
|
3993
|
+
const cleanups = Array.from(batchCleanups);
|
|
3994
|
+
batchCleanups.clear();
|
|
3995
|
+
for (const cleanup of cleanups)
|
|
3996
|
+
cleanup();
|
|
3997
|
+
// In immediate mode, we traditionally don't process recursive effects from cleanups.
|
|
3998
|
+
// If we want to keep that behavior: if (immediate) break
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
success = true;
|
|
4002
|
+
}
|
|
4003
|
+
finally {
|
|
4004
|
+
if (!success)
|
|
4005
|
+
panicThrow();
|
|
4006
|
+
activationRegistry = undefined;
|
|
4007
|
+
batchQueue = undefined;
|
|
4008
|
+
batchCleanups.clear();
|
|
4009
|
+
optionCall('endChain');
|
|
4010
|
+
}
|
|
4011
|
+
return firstReturn.value;
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
finally {
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
function panicThrow() {
|
|
4018
|
+
broken = true;
|
|
4019
|
+
}
|
|
4020
|
+
/**
|
|
4021
|
+
* Resets the reactive system to a consistent state.
|
|
4022
|
+
* Call this after an unrecoverable error has set the system to "broken".
|
|
4023
|
+
* This clears all batch state, effect dependency graphs, and watcher registrations.
|
|
4024
|
+
* All existing effects become orphaned and must be recreated.
|
|
4025
|
+
*/
|
|
4026
|
+
function reset() {
|
|
4027
|
+
broken = false;
|
|
4028
|
+
activationRegistry = undefined;
|
|
4029
|
+
batchQueue = undefined;
|
|
4030
|
+
batchCleanups.clear();
|
|
4031
|
+
effectTriggers = new WeakMap();
|
|
4032
|
+
effectTriggeredBy = new WeakMap();
|
|
4033
|
+
causesClosure = new WeakMap();
|
|
4034
|
+
consequencesClosure = new WeakMap();
|
|
4035
|
+
resetRegistry();
|
|
4036
|
+
effectHistory.present.active = undefined;
|
|
4037
|
+
}
|
|
4038
|
+
// Inject batch function to allow atomic game loops in requestAnimationFrame/setTimeout/...
|
|
4039
|
+
// Note: Automatic batching of async callbacks (setTimeout, Promise.then, etc.) is NOT implemented.
|
|
4040
|
+
// Rationale: (1) asyncHooks.addHook API doesn't support knowing when callbacks complete (needed for batching),
|
|
4041
|
+
// (2) hooking all callback-creating functions adds overhead without guaranteed benefit,
|
|
4042
|
+
// (3) incomplete coverage in Node (async_hooks misses user-land patterns).
|
|
4043
|
+
// Solution: Use explicit @atomic decorator or manual batch() calls where optimization is needed.
|
|
4044
|
+
/**
|
|
4045
|
+
* Decorator that makes methods atomic - batches all effects triggered within the method
|
|
4046
|
+
*/
|
|
4047
|
+
const atomic = decorator({
|
|
4048
|
+
method(original) {
|
|
4049
|
+
return function (...args) {
|
|
4050
|
+
const atomicEffect = () => original.apply(this, args);
|
|
4051
|
+
// Debug: helpful to have a name
|
|
4052
|
+
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
|
|
4053
|
+
return batch(atomicEffect, 'immediate');
|
|
4054
|
+
};
|
|
4055
|
+
},
|
|
4056
|
+
default(original) {
|
|
4057
|
+
return function (...args) {
|
|
4058
|
+
const atomicEffect = () => original.apply(this, args);
|
|
4059
|
+
// Debug: helpful to have a name
|
|
4060
|
+
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
|
|
4061
|
+
return batch(atomicEffect, 'immediate');
|
|
4062
|
+
};
|
|
4063
|
+
},
|
|
4064
|
+
});
|
|
4065
|
+
const fr = new FinalizationRegistry((f) => f());
|
|
4066
|
+
/**
|
|
4067
|
+
* @param fn - The effect function to run - provides the cleaner
|
|
4068
|
+
* @returns The cleanup function
|
|
4069
|
+
*/
|
|
4070
|
+
/**
|
|
4071
|
+
* Creates a reactive effect that automatically re-runs when dependencies change
|
|
4072
|
+
* @param fn - The effect function that provides dependencies and may return a cleanup function or Promise
|
|
4073
|
+
* @param options - Options for effect execution
|
|
4074
|
+
* @returns A cleanup function to stop the effect
|
|
4075
|
+
*/
|
|
4076
|
+
const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions) {
|
|
4077
|
+
if (effectOptions?.name)
|
|
4078
|
+
Object.defineProperty(fn, 'name', { value: effectOptions.name });
|
|
4079
|
+
// Use per-effect asyncMode or fall back to global option
|
|
4080
|
+
const asyncMode = effectOptions?.asyncMode ?? options.asyncMode ?? 'cancel';
|
|
4081
|
+
// Create the effect function - naming it for debug
|
|
4082
|
+
const runEffect = () => {
|
|
4083
|
+
const node = getEffectNode(runEffect);
|
|
4084
|
+
// Clear previous dependencies
|
|
4085
|
+
if (node.cleanup) {
|
|
4086
|
+
const prevCleanup = node.cleanup;
|
|
4087
|
+
node.cleanup = undefined;
|
|
4088
|
+
try {
|
|
4089
|
+
untracked(() => prevCleanup());
|
|
4090
|
+
}
|
|
4091
|
+
catch (error) {
|
|
4092
|
+
// If we want to report them, we could use options.warn or similar
|
|
4093
|
+
options.warn('Error during effect cleanup', error);
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
// Handle async modes when effect is retriggered
|
|
4097
|
+
if (runningPromise) {
|
|
4098
|
+
if (asyncMode === 'cancel' && cancelPrevious) {
|
|
4099
|
+
// Cancel previous execution
|
|
4100
|
+
cancelPrevious();
|
|
4101
|
+
cancelPrevious = null;
|
|
4102
|
+
runningPromise = null;
|
|
4103
|
+
}
|
|
4104
|
+
else if (asyncMode === 'ignore') {
|
|
4105
|
+
// Ignore new execution while async work is running
|
|
4106
|
+
return;
|
|
4107
|
+
}
|
|
4108
|
+
// Note: 'queue' mode not yet implemented
|
|
4109
|
+
}
|
|
4110
|
+
// The effect has been stopped after having been planned
|
|
4111
|
+
if (effectStopped)
|
|
4112
|
+
return;
|
|
4113
|
+
optionCall('enter', getRoot(fn));
|
|
4114
|
+
let reactionCleanup;
|
|
4115
|
+
let result;
|
|
4116
|
+
let caught = 0;
|
|
4117
|
+
// Default thrower (self)
|
|
4118
|
+
let thrower = (error) => {
|
|
4119
|
+
throw error;
|
|
4120
|
+
};
|
|
4121
|
+
node.forwardThrow = thrower;
|
|
4122
|
+
let errorToThrow;
|
|
4123
|
+
try {
|
|
4124
|
+
result = tracked(named(effectMarker.enter, () => fn.call(null, access)));
|
|
4125
|
+
optionCall('leave', fn);
|
|
4126
|
+
if (result &&
|
|
4127
|
+
typeof result !== 'function' &&
|
|
4128
|
+
(typeof result !== 'object' || !('then' in result)))
|
|
4129
|
+
throw new ReactiveError(`[reactive] Effect returned a non-function value: ${result}`);
|
|
4130
|
+
// Check if result is a Promise (async effect)
|
|
4131
|
+
if (result && typeof result === 'object' && typeof result.then === 'function') {
|
|
4132
|
+
const originalPromise = result;
|
|
4133
|
+
// Create a cancellation promise that we can reject
|
|
4134
|
+
let cancelReject = null;
|
|
4135
|
+
const cancelPromise = new Promise((_, reject) => {
|
|
4136
|
+
cancelReject = reject;
|
|
4137
|
+
});
|
|
4138
|
+
const cancelError = new ReactiveError('[reactive] Effect canceled due to dependency change');
|
|
4139
|
+
// Race between the actual promise and cancellation
|
|
4140
|
+
// If canceled, the race rejects, which will propagate through any promise chain
|
|
4141
|
+
runningPromise = Promise.race([originalPromise, cancelPromise]);
|
|
4142
|
+
// Store the cancellation function
|
|
4143
|
+
cancelPrevious = () => {
|
|
4144
|
+
if (cancelReject) {
|
|
4145
|
+
cancelReject(cancelError);
|
|
4146
|
+
}
|
|
4147
|
+
};
|
|
4148
|
+
// Wrap the original promise chain so cancellation propagates
|
|
4149
|
+
// This ensures that when we cancel, the original promise's .catch() handlers are triggered
|
|
4150
|
+
// We do this by rejecting the race promise, which makes the original promise chain see the rejection
|
|
4151
|
+
// through the zone-wrapped .then()/.catch() handlers
|
|
4152
|
+
runningPromise = runningPromise.catch((error) => {
|
|
4153
|
+
// Propagate async errors to the effect's error handler
|
|
4154
|
+
// This ensures onEffectThrow handlers are triggered for async errors
|
|
4155
|
+
if (error !== cancelError) {
|
|
4156
|
+
thrower(error);
|
|
4157
|
+
}
|
|
4158
|
+
// If thrower didn't throw (handled), we absorb the error.
|
|
4159
|
+
// If thrower threw (unhandled), it propagates as a new unhandled rejection, which is correct.
|
|
4160
|
+
});
|
|
4161
|
+
}
|
|
4162
|
+
else {
|
|
4163
|
+
// Synchronous result - treat as cleanup function
|
|
4164
|
+
reactionCleanup = result;
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
catch (error) {
|
|
4168
|
+
// catcher:self`
|
|
4169
|
+
errorToThrow = error;
|
|
4170
|
+
}
|
|
4171
|
+
finally {
|
|
4172
|
+
access.reaction = true;
|
|
4173
|
+
}
|
|
4174
|
+
// Create cleanup function for next run
|
|
4175
|
+
node.cleanup = () => {
|
|
4176
|
+
node.cleanup = undefined;
|
|
4177
|
+
reactionCleanup?.();
|
|
4178
|
+
reactionCleanup = undefined;
|
|
4179
|
+
delete node.trackers;
|
|
4180
|
+
delete node.catchers;
|
|
4181
|
+
// Remove this effect from all reactive objects it's watching
|
|
4182
|
+
const effectObjects = effectToReactiveObjects.get(runEffect);
|
|
4183
|
+
if (effectObjects) {
|
|
4184
|
+
for (const reactiveObj of effectObjects) {
|
|
4185
|
+
const objectWatchers = watchers.get(reactiveObj);
|
|
4186
|
+
if (objectWatchers) {
|
|
4187
|
+
for (const [prop, deps] of objectWatchers.entries()) {
|
|
4188
|
+
deps.delete(runEffect);
|
|
4189
|
+
if (deps.size === 0) {
|
|
4190
|
+
objectWatchers.delete(prop);
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
if (objectWatchers.size === 0) {
|
|
4194
|
+
watchers.delete(reactiveObj);
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
effectToReactiveObjects.delete(runEffect);
|
|
4199
|
+
}
|
|
4200
|
+
// Invoke all child stops (recursive via subEffectCleanup calling its own mainCleanup)
|
|
4201
|
+
const children = node.children;
|
|
4202
|
+
if (children) {
|
|
4203
|
+
for (const childCleanup of children)
|
|
4204
|
+
childCleanup();
|
|
4205
|
+
delete node.children;
|
|
4206
|
+
}
|
|
4207
|
+
};
|
|
4208
|
+
// Define bubbling thrower
|
|
4209
|
+
thrower = (error) => {
|
|
4210
|
+
const catches = node.catchers;
|
|
4211
|
+
if (catches)
|
|
4212
|
+
while (caught < catches.length) {
|
|
4213
|
+
reactionCleanup?.(error);
|
|
4214
|
+
reactionCleanup = undefined;
|
|
4215
|
+
try {
|
|
4216
|
+
reactionCleanup = catches[caught](error);
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4219
|
+
catch (e) {
|
|
4220
|
+
caught++;
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
if (parent) {
|
|
4224
|
+
const parentNode = getEffectNode(parent);
|
|
4225
|
+
if (parentNode.forwardThrow)
|
|
4226
|
+
parentNode.forwardThrow(error);
|
|
4227
|
+
else
|
|
4228
|
+
throw error;
|
|
4229
|
+
}
|
|
4230
|
+
else
|
|
4231
|
+
throw error;
|
|
4232
|
+
};
|
|
4233
|
+
// Update the node's forwardThrow to the bubbling one
|
|
4234
|
+
node.forwardThrow = thrower;
|
|
4235
|
+
if (errorToThrow)
|
|
4236
|
+
thrower(errorToThrow);
|
|
4237
|
+
};
|
|
4238
|
+
// Initialize metadata node
|
|
4239
|
+
const node = getEffectNode(runEffect);
|
|
4240
|
+
if (isDevtoolsEnabled()) {
|
|
4241
|
+
const stack = getStackFrame(); // Robustly skips internal mutts frames
|
|
4242
|
+
if (stack.length > 0) {
|
|
4243
|
+
node.creationStack = stack;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
const tracked = named(effectMarker.leave, effectHistory.present.with(runEffect, () => named(effectMarker.leave, effectAggregator.zoned)));
|
|
4247
|
+
const ascended = named(effectMarker.leave, effectHistory.zoned);
|
|
4248
|
+
const parent = effectHistory.present.active;
|
|
4249
|
+
// Set parent relationship in node
|
|
4250
|
+
node.parent = parent;
|
|
4251
|
+
// let thrower: CatchFunction | undefined // Moved inside runEffect
|
|
4252
|
+
let effectStopped = false;
|
|
4253
|
+
const access = {
|
|
4254
|
+
tracked,
|
|
4255
|
+
ascend: named(effectMarker.leave, (fn) => ascended(named(effectMarker.enter, () => fn.call(null)))),
|
|
4256
|
+
//named(effectMarker.enter, (fn) => ascended(fn)),
|
|
4257
|
+
reaction: false,
|
|
4258
|
+
};
|
|
4259
|
+
let runningPromise = null;
|
|
4260
|
+
let cancelPrevious = null;
|
|
4261
|
+
if (effectOptions?.dependencyHook) {
|
|
4262
|
+
node.dependencyHook = effectOptions.dependencyHook;
|
|
4263
|
+
}
|
|
4264
|
+
// Mark the runEffect callback with the original function as its root
|
|
4265
|
+
markWithRoot(runEffect, fn);
|
|
4266
|
+
function augmentedRv(rv) {
|
|
4267
|
+
return Object.defineProperties(rv, {
|
|
4268
|
+
[stopped]: {
|
|
4269
|
+
get: () => effectStopped,
|
|
4270
|
+
},
|
|
4271
|
+
});
|
|
4272
|
+
}
|
|
4273
|
+
// Register strict mode if enabled
|
|
4274
|
+
if (effectOptions?.opaque) {
|
|
4275
|
+
node.isOpaque = true;
|
|
4276
|
+
}
|
|
4277
|
+
if (isDevtoolsEnabled()) {
|
|
4278
|
+
registerEffectForDebug(runEffect);
|
|
4279
|
+
}
|
|
4280
|
+
// Store parent relationship for hierarchy traversal - ALREADY DONE ABOVE via getEffectNode
|
|
4281
|
+
batch(runEffect, 'immediate');
|
|
4282
|
+
// Only ROOT effects are registered for GC cleanup and zone tracking
|
|
4283
|
+
const isRootEffect = !parent;
|
|
4284
|
+
const stopEffect = () => {
|
|
4285
|
+
if (effectStopped)
|
|
4286
|
+
return;
|
|
4287
|
+
effectStopped = true;
|
|
4288
|
+
node.stopped = true;
|
|
4289
|
+
// Cancel any running async work
|
|
4290
|
+
if (cancelPrevious) {
|
|
4291
|
+
cancelPrevious();
|
|
4292
|
+
cancelPrevious = null;
|
|
4293
|
+
runningPromise = null;
|
|
4294
|
+
}
|
|
4295
|
+
try {
|
|
4296
|
+
node.cleanup?.();
|
|
4297
|
+
}
|
|
4298
|
+
catch (error) {
|
|
4299
|
+
// Cleanup errors should basically be ignored or at least not stop the world
|
|
4300
|
+
// If we want to report them, we could use options.warn or similar
|
|
4301
|
+
options.warn('Error during effect cleanup', error);
|
|
4302
|
+
}
|
|
4303
|
+
// Clean up dependency graph edges
|
|
4304
|
+
cleanupEffectFromGraph(runEffect);
|
|
4305
|
+
fr.unregister(stopEffect);
|
|
4306
|
+
};
|
|
4307
|
+
if (isRootEffect) {
|
|
4308
|
+
const callIfCollected = augmentedRv(() => stopEffect());
|
|
4309
|
+
fr.register(callIfCollected, () => {
|
|
4310
|
+
stopEffect();
|
|
4311
|
+
optionCall('garbageCollected', fn);
|
|
4312
|
+
}, stopEffect);
|
|
4313
|
+
return callIfCollected;
|
|
4314
|
+
}
|
|
4315
|
+
// Register this effect to be stopped when the parent effect is cleaned up
|
|
4316
|
+
if (parent) {
|
|
4317
|
+
const parentNode = getEffectNode(parent);
|
|
4318
|
+
if (!parentNode.children) {
|
|
4319
|
+
parentNode.children = new Set();
|
|
4320
|
+
}
|
|
4321
|
+
const children = parentNode.children;
|
|
4322
|
+
const subEffectCleanup = augmentedRv(() => {
|
|
4323
|
+
children.delete(subEffectCleanup);
|
|
4324
|
+
// Execute this child effect cleanup (which triggers its own mainCleanup)
|
|
4325
|
+
stopEffect();
|
|
4326
|
+
});
|
|
4327
|
+
children.add(subEffectCleanup);
|
|
4328
|
+
return subEffectCleanup;
|
|
4329
|
+
}
|
|
4330
|
+
// Should not be reachable given isRootEffect check, but for type safety
|
|
4331
|
+
return augmentedRv(stopEffect);
|
|
4332
|
+
}, {
|
|
4333
|
+
get opaque() {
|
|
4334
|
+
return flavorOptions(this, { opaque: true }, 'opaque');
|
|
4335
|
+
},
|
|
4336
|
+
named(name) {
|
|
4337
|
+
return flavorOptions(this, { name }, 'named');
|
|
4338
|
+
},
|
|
4339
|
+
}));
|
|
4340
|
+
/**
|
|
4341
|
+
* Executes a function without tracking dependencies but maintains parent cleanup relationship
|
|
4342
|
+
* Effects created inside will still be cleaned up when the parent effect is destroyed
|
|
4343
|
+
* @param fn - The function to execute
|
|
4344
|
+
*/
|
|
4345
|
+
function untracked(fn) {
|
|
4346
|
+
return effectHistory.present.root(fn);
|
|
4347
|
+
}
|
|
4348
|
+
/**
|
|
4349
|
+
* Executes a function from a virgin/root context - no parent effect, no tracking
|
|
4350
|
+
* Creates completely independent effects that won't be cleaned up by any parent
|
|
4351
|
+
* @param fn - The function to execute
|
|
4352
|
+
*/
|
|
4353
|
+
function root(fn) {
|
|
4354
|
+
return effectHistory.root(fn);
|
|
4355
|
+
}
|
|
4356
|
+
function biDi(received, get, set) {
|
|
4357
|
+
if (typeof get !== 'function') {
|
|
4358
|
+
set = get.set;
|
|
4359
|
+
get = get.get;
|
|
4360
|
+
}
|
|
4361
|
+
let programmaticallySetValue = Symbol();
|
|
4362
|
+
effect(markWithRoot(() => {
|
|
4363
|
+
const newValue = get();
|
|
4364
|
+
if (unwrap(newValue) !== programmaticallySetValue)
|
|
4365
|
+
received(newValue);
|
|
4366
|
+
}, received));
|
|
4367
|
+
return set
|
|
4368
|
+
? atomic((value) => {
|
|
4369
|
+
programmaticallySetValue = unwrap(value);
|
|
4370
|
+
set(value);
|
|
4371
|
+
})
|
|
4372
|
+
: () => { };
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
export { touched1 as $, AZone as A, getActiveEffect as B, getState as C, DecoratorError as D, immutables as E, FoolProof as F, isConstructor as G, isNonReactive as H, IterableWeakMap as I, isOwnAccessor as J, isReactive as K, legacyDecorator as L, mixin as M, modernDecorator as N, named as O, onEffectThrow as P, onEffectTrigger as Q, ReactiveBase as R, reactive as S, options as T, registerNativeReactivity as U, renamed as V, reset as W, root as X, tag as Y, Zone as Z, touched as _, asyncHooks as a, untracked as a0, unwrap as a1, zip as a2, markWithRoot as a3, rootFunction as a4, getRoot as a5, dependant as a6, optionCall as a7, __runInitializers as a8, __esDecorate as a9, formatLineage as aA, getDependencies as aB, getDependencyGraph as aC, getDependents as aD, getLineage as aE, getMutationHistory as aF, getStackFrame as aG, getTriggerChain as aH, isDevtoolsEnabled as aI, lineageFormatter as aJ, logError as aK, nodeLineage as aL, nodeLineageLegacy as aM, parseStackLine as aN, recordTriggerLink as aO, registerEffectForDebug as aP, registerObjectForDebug as aQ, setObjectName as aR, showLineagePanel as aS, wrapLineageForDebug as aT, objectsWithDeepWatchers as aa, effectToDeepWatchedObjects as ab, deepWatchers as ac, stopped as ad, nonReactiveClass as ae, unreactiveProperties as af, nonReactiveMark as ag, nonReactiveObjects as ah, __setFunctionName as ai, __classPrivateFieldGet as aj, __classPrivateFieldSet as ak, setEffectName as al, projectionInfo as am, notifyPropertyChange as an, metaProtos as ao, objectParents as ap, watchers as aq, effectToReactiveObjects as ar, proxyToObject as as, objectToProxy as at, buildReactivityGraph as au, captureNodeLineage as av, debugOptions as aw, effectMarker as ax, enableDevTools as ay, forceEnableGraphTracking as az, IterableWeakSet as b, ReactiveError as c, ReactiveErrorCode as d, ZoneAggregator as e, ZoneHistory as f, addBatchCleanup as g, hooks as h, arrayEquals as i, asyncHook as j, asyncZone as k, atomic as l, biDi as m, cleanedBy as n, cleanup as o, contentRef as p, createFlavor as q, decorator as r, deepCompare as s, defer as t, derived as u, effect as v, effectAggregator as w, flavorOptions as x, flavored as y, getActivationLog as z };
|
|
4376
|
+
//# sourceMappingURL=index-CtA2AWl3.esm.js.map
|