mutts 1.0.2 → 1.0.3
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 +14 -6
- package/dist/chunks/{_tslib-C-cuVLvZ.js → _tslib-BgjropY9.js} +9 -1
- package/dist/chunks/_tslib-BgjropY9.js.map +1 -0
- package/dist/chunks/{_tslib-CMEnd0VE.esm.js → _tslib-Mzh1rNsX.esm.js} +9 -2
- package/dist/chunks/_tslib-Mzh1rNsX.esm.js.map +1 -0
- package/dist/chunks/{decorator-D4DU97Zg.js → decorator-DLvrD0UF.js} +42 -19
- package/dist/chunks/decorator-DLvrD0UF.js.map +1 -0
- package/dist/chunks/{decorator-GnHw1Az7.esm.js → decorator-DqiszP7i.esm.js} +42 -19
- package/dist/chunks/decorator-DqiszP7i.esm.js.map +1 -0
- package/dist/chunks/index-DzUDtFc7.esm.js +4841 -0
- package/dist/chunks/index-DzUDtFc7.esm.js.map +1 -0
- package/dist/chunks/index-HNVqPzjz.js +4891 -0
- package/dist/chunks/index-HNVqPzjz.js.map +1 -0
- package/dist/decorator.esm.js +1 -1
- package/dist/decorator.js +1 -1
- package/dist/destroyable.d.ts +1 -1
- package/dist/destroyable.esm.js +1 -1
- package/dist/destroyable.esm.js.map +1 -1
- package/dist/destroyable.js +1 -1
- package/dist/destroyable.js.map +1 -1
- package/dist/devtools/devtools.html +9 -0
- package/dist/devtools/devtools.js +5 -0
- package/dist/devtools/devtools.js.map +1 -0
- package/dist/devtools/manifest.json +8 -0
- package/dist/devtools/panel.css +72 -0
- package/dist/devtools/panel.html +31 -0
- package/dist/devtools/panel.js +13048 -0
- package/dist/devtools/panel.js.map +1 -0
- package/dist/eventful.esm.js +1 -1
- package/dist/eventful.js +1 -1
- package/dist/index.d.ts +18 -63
- package/dist/index.esm.js +4 -4
- package/dist/index.js +36 -11
- package/dist/index.js.map +1 -1
- package/dist/indexable.d.ts +187 -1
- package/dist/indexable.esm.js +197 -3
- package/dist/indexable.esm.js.map +1 -1
- package/dist/indexable.js +198 -2
- package/dist/indexable.js.map +1 -1
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/promiseChain.esm.js.map +1 -1
- package/dist/promiseChain.js.map +1 -1
- package/dist/reactive.d.ts +601 -97
- package/dist/reactive.esm.js +3 -3
- package/dist/reactive.js +31 -10
- package/dist/reactive.js.map +1 -1
- package/dist/std-decorators.esm.js +1 -1
- package/dist/std-decorators.js +1 -1
- package/docs/ai/api-reference.md +133 -0
- package/docs/ai/manual.md +105 -0
- package/docs/iterableWeak.md +646 -0
- package/docs/reactive/advanced.md +1280 -0
- package/docs/reactive/collections.md +767 -0
- package/docs/reactive/core.md +973 -0
- package/docs/reactive.md +21 -9545
- package/package.json +18 -5
- package/src/decorator.ts +266 -0
- package/src/destroyable.ts +199 -0
- package/src/eventful.ts +77 -0
- package/src/index.d.ts +9 -0
- package/src/index.ts +9 -0
- package/src/indexable.ts +484 -0
- package/src/introspection.ts +59 -0
- package/src/iterableWeak.ts +233 -0
- package/src/mixins.ts +123 -0
- package/src/promiseChain.ts +110 -0
- package/src/reactive/array.ts +414 -0
- package/src/reactive/change.ts +134 -0
- package/src/reactive/debug.ts +517 -0
- package/src/reactive/deep-touch.ts +268 -0
- package/src/reactive/deep-watch-state.ts +82 -0
- package/src/reactive/deep-watch.ts +168 -0
- package/src/reactive/effect-context.ts +94 -0
- package/src/reactive/effects.ts +1333 -0
- package/src/reactive/index.ts +75 -0
- package/src/reactive/interface.ts +223 -0
- package/src/reactive/map.ts +171 -0
- package/src/reactive/mapped.ts +130 -0
- package/src/reactive/memoize.ts +107 -0
- package/src/reactive/non-reactive-state.ts +49 -0
- package/src/reactive/non-reactive.ts +43 -0
- package/src/reactive/project.project.md +93 -0
- package/src/reactive/project.ts +335 -0
- package/src/reactive/proxy-state.ts +27 -0
- package/src/reactive/proxy.ts +285 -0
- package/src/reactive/record.ts +196 -0
- package/src/reactive/register.ts +421 -0
- package/src/reactive/set.ts +144 -0
- package/src/reactive/tracking.ts +101 -0
- package/src/reactive/types.ts +358 -0
- package/src/reactive/zone.ts +208 -0
- package/src/std-decorators.ts +217 -0
- package/src/utils.ts +117 -0
- package/dist/chunks/_tslib-C-cuVLvZ.js.map +0 -1
- package/dist/chunks/_tslib-CMEnd0VE.esm.js.map +0 -1
- package/dist/chunks/decorator-D4DU97Zg.js.map +0 -1
- package/dist/chunks/decorator-GnHw1Az7.esm.js.map +0 -1
- package/dist/chunks/index-DBScoeCX.esm.js +0 -1960
- package/dist/chunks/index-DBScoeCX.esm.js.map +0 -1
- package/dist/chunks/index-DOTmXL89.js +0 -1983
- package/dist/chunks/index-DOTmXL89.js.map +0 -1
|
@@ -0,0 +1,4891 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var decorator = require('./decorator-DLvrD0UF.js');
|
|
4
|
+
var indexable = require('../indexable.js');
|
|
5
|
+
var _tslib = require('./_tslib-BgjropY9.js');
|
|
6
|
+
|
|
7
|
+
/// <reference lib="esnext.collection" />
|
|
8
|
+
var _a, _b;
|
|
9
|
+
/**
|
|
10
|
+
* Uses weak references but still may iterate through them
|
|
11
|
+
* Note: The behavior is highly dependant on the garbage collector - some entries are perhaps deemed to be collected: don't resuscitate them
|
|
12
|
+
*/
|
|
13
|
+
class IterableWeakMap {
|
|
14
|
+
constructor(entries) {
|
|
15
|
+
this.uuids = new WeakMap();
|
|
16
|
+
this.refs = {};
|
|
17
|
+
this[_a] = 'IterableWeakMap';
|
|
18
|
+
// Create a FinalizationRegistry to clean up refs when keys are garbage collected
|
|
19
|
+
this.registry = new FinalizationRegistry((uuid) => {
|
|
20
|
+
delete this.refs[uuid];
|
|
21
|
+
});
|
|
22
|
+
if (entries)
|
|
23
|
+
for (const [k, v] of entries)
|
|
24
|
+
this.set(k, v);
|
|
25
|
+
}
|
|
26
|
+
createIterator(cb) {
|
|
27
|
+
const { refs } = this;
|
|
28
|
+
return (function* () {
|
|
29
|
+
for (const uuid of Object.keys(refs)) {
|
|
30
|
+
const [keyRef, value] = refs[uuid];
|
|
31
|
+
const key = keyRef.deref();
|
|
32
|
+
if (key)
|
|
33
|
+
yield cb(key, value);
|
|
34
|
+
else
|
|
35
|
+
delete refs[uuid];
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
})();
|
|
39
|
+
}
|
|
40
|
+
clear() {
|
|
41
|
+
// Unregister all keys from the FinalizationRegistry
|
|
42
|
+
for (const uuid of Object.keys(this.refs)) {
|
|
43
|
+
const key = this.refs[uuid][0].deref();
|
|
44
|
+
if (key)
|
|
45
|
+
this.registry.unregister(key);
|
|
46
|
+
}
|
|
47
|
+
this.uuids = new WeakMap();
|
|
48
|
+
this.refs = {};
|
|
49
|
+
}
|
|
50
|
+
delete(key) {
|
|
51
|
+
const uuid = this.uuids.get(key);
|
|
52
|
+
if (!uuid)
|
|
53
|
+
return false;
|
|
54
|
+
delete this.refs[uuid];
|
|
55
|
+
this.uuids.delete(key);
|
|
56
|
+
this.registry.unregister(key);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
forEach(callbackfn, thisArg) {
|
|
60
|
+
for (const [k, v] of this)
|
|
61
|
+
callbackfn.call(thisArg ?? this, v, k, thisArg ?? this);
|
|
62
|
+
}
|
|
63
|
+
get(key) {
|
|
64
|
+
const uuid = this.uuids.get(key);
|
|
65
|
+
if (!uuid)
|
|
66
|
+
return undefined;
|
|
67
|
+
return this.refs[uuid][1];
|
|
68
|
+
}
|
|
69
|
+
has(key) {
|
|
70
|
+
return this.uuids.has(key);
|
|
71
|
+
}
|
|
72
|
+
set(key, value) {
|
|
73
|
+
let uuid = this.uuids.get(key);
|
|
74
|
+
if (uuid) {
|
|
75
|
+
this.refs[uuid][1] = value;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
uuid = crypto.randomUUID();
|
|
79
|
+
this.uuids.set(key, uuid);
|
|
80
|
+
this.refs[uuid] = [new WeakRef(key), value];
|
|
81
|
+
// Register key for cleanup when garbage collected
|
|
82
|
+
this.registry.register(key, uuid, key);
|
|
83
|
+
}
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
get size() {
|
|
87
|
+
return [...this].length;
|
|
88
|
+
}
|
|
89
|
+
entries() {
|
|
90
|
+
return this.createIterator((key, value) => [key, value]);
|
|
91
|
+
}
|
|
92
|
+
keys() {
|
|
93
|
+
return this.createIterator((key, _value) => key);
|
|
94
|
+
}
|
|
95
|
+
values() {
|
|
96
|
+
return this.createIterator((_key, value) => value);
|
|
97
|
+
}
|
|
98
|
+
[Symbol.iterator]() {
|
|
99
|
+
return this.entries();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
_a = Symbol.toStringTag;
|
|
103
|
+
/**
|
|
104
|
+
* Uses weak references but still may iterate through them
|
|
105
|
+
* Note: The behavior is highly dependant on the garbage collector - some entries are perhaps deemed to be collected: don't resuscitate them
|
|
106
|
+
*/
|
|
107
|
+
class IterableWeakSet {
|
|
108
|
+
constructor(entries) {
|
|
109
|
+
this.uuids = new WeakMap();
|
|
110
|
+
this.refs = {};
|
|
111
|
+
this[_b] = 'IterableWeakSet';
|
|
112
|
+
// Create a FinalizationRegistry to clean up refs when values are garbage collected
|
|
113
|
+
this.registry = new FinalizationRegistry((uuid) => {
|
|
114
|
+
delete this.refs[uuid];
|
|
115
|
+
});
|
|
116
|
+
if (entries)
|
|
117
|
+
for (const k of entries)
|
|
118
|
+
this.add(k);
|
|
119
|
+
}
|
|
120
|
+
createIterator(cb) {
|
|
121
|
+
const { refs } = this;
|
|
122
|
+
return (function* () {
|
|
123
|
+
for (const uuid of Object.keys(refs)) {
|
|
124
|
+
const key = refs[uuid].deref();
|
|
125
|
+
if (key)
|
|
126
|
+
yield cb(key);
|
|
127
|
+
else
|
|
128
|
+
delete refs[uuid];
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
})();
|
|
132
|
+
}
|
|
133
|
+
clear() {
|
|
134
|
+
// Unregister all values from the FinalizationRegistry
|
|
135
|
+
for (const uuid of Object.keys(this.refs)) {
|
|
136
|
+
const value = this.refs[uuid].deref();
|
|
137
|
+
if (value)
|
|
138
|
+
this.registry.unregister(value);
|
|
139
|
+
}
|
|
140
|
+
this.uuids = new WeakMap();
|
|
141
|
+
this.refs = {};
|
|
142
|
+
}
|
|
143
|
+
add(value) {
|
|
144
|
+
let uuid = this.uuids.get(value);
|
|
145
|
+
if (!uuid) {
|
|
146
|
+
uuid = crypto.randomUUID();
|
|
147
|
+
this.uuids.set(value, uuid);
|
|
148
|
+
this.refs[uuid] = new WeakRef(value);
|
|
149
|
+
// Register value for cleanup when garbage collected
|
|
150
|
+
this.registry.register(value, uuid, value);
|
|
151
|
+
}
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
delete(value) {
|
|
155
|
+
const uuid = this.uuids.get(value);
|
|
156
|
+
if (!uuid)
|
|
157
|
+
return false;
|
|
158
|
+
delete this.refs[uuid];
|
|
159
|
+
this.uuids.delete(value);
|
|
160
|
+
this.registry.unregister(value);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
forEach(callbackfn, thisArg) {
|
|
164
|
+
for (const value of this)
|
|
165
|
+
callbackfn.call(thisArg ?? this, value, value, thisArg ?? this);
|
|
166
|
+
}
|
|
167
|
+
has(value) {
|
|
168
|
+
return this.uuids.has(value);
|
|
169
|
+
}
|
|
170
|
+
get size() {
|
|
171
|
+
return [...this].length;
|
|
172
|
+
}
|
|
173
|
+
entries() {
|
|
174
|
+
return this.createIterator((key) => [key, key]);
|
|
175
|
+
}
|
|
176
|
+
keys() {
|
|
177
|
+
return this.createIterator((key) => key);
|
|
178
|
+
}
|
|
179
|
+
values() {
|
|
180
|
+
return this.createIterator((key) => key);
|
|
181
|
+
}
|
|
182
|
+
[Symbol.iterator]() {
|
|
183
|
+
return this.keys();
|
|
184
|
+
}
|
|
185
|
+
union(other) {
|
|
186
|
+
const others = {
|
|
187
|
+
[Symbol.iterator]() {
|
|
188
|
+
return other.keys();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const that = this;
|
|
192
|
+
return new Set((function* () {
|
|
193
|
+
yield* that;
|
|
194
|
+
for (const value of others)
|
|
195
|
+
if (!that.has(value))
|
|
196
|
+
yield value;
|
|
197
|
+
})());
|
|
198
|
+
}
|
|
199
|
+
intersection(other) {
|
|
200
|
+
const that = this;
|
|
201
|
+
return new Set((function* () {
|
|
202
|
+
for (const value of that)
|
|
203
|
+
if (other.has(value))
|
|
204
|
+
yield value;
|
|
205
|
+
})());
|
|
206
|
+
}
|
|
207
|
+
difference(other) {
|
|
208
|
+
const that = this;
|
|
209
|
+
return new Set((function* () {
|
|
210
|
+
for (const value of that)
|
|
211
|
+
if (!other.has(value))
|
|
212
|
+
yield value;
|
|
213
|
+
})());
|
|
214
|
+
}
|
|
215
|
+
symmetricDifference(other) {
|
|
216
|
+
const others = {
|
|
217
|
+
[Symbol.iterator]() {
|
|
218
|
+
return other.keys();
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const that = this;
|
|
222
|
+
return new Set((function* () {
|
|
223
|
+
for (const value of that)
|
|
224
|
+
if (!other.has(value))
|
|
225
|
+
yield value;
|
|
226
|
+
for (const value of others)
|
|
227
|
+
if (!that.has(value))
|
|
228
|
+
yield value;
|
|
229
|
+
})());
|
|
230
|
+
}
|
|
231
|
+
isSubsetOf(other) {
|
|
232
|
+
for (const value of this)
|
|
233
|
+
if (!other.has(value))
|
|
234
|
+
return false;
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
isSupersetOf(other) {
|
|
238
|
+
const others = {
|
|
239
|
+
[Symbol.iterator]() {
|
|
240
|
+
return other.keys();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
for (const value of others)
|
|
244
|
+
if (!this.has(value))
|
|
245
|
+
return false;
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
isDisjointFrom(other) {
|
|
249
|
+
for (const value of this)
|
|
250
|
+
if (other.has(value))
|
|
251
|
+
return false;
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
_b = Symbol.toStringTag;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Creates a mixin that can be used both as a class (extends) and as a function (mixin)
|
|
259
|
+
*
|
|
260
|
+
* This function supports:
|
|
261
|
+
* - Using mixins as base classes: `class MyClass extends MyMixin`
|
|
262
|
+
* - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
|
|
263
|
+
* - Composing mixins: `const Composed = MixinA(MixinB)`
|
|
264
|
+
* - Type-safe property inference for all patterns
|
|
265
|
+
*
|
|
266
|
+
* @param mixinFunction - The function that creates the mixin
|
|
267
|
+
* @param unwrapFunction - Optional function to unwrap reactive objects for method calls
|
|
268
|
+
* @returns A mixin that can be used both as a class and as a function
|
|
269
|
+
*/
|
|
270
|
+
function mixin(mixinFunction, unwrapFunction) {
|
|
271
|
+
/**
|
|
272
|
+
* Cache for mixin results to ensure the same base class always returns the same mixed class
|
|
273
|
+
*/
|
|
274
|
+
const mixinCache = new WeakMap();
|
|
275
|
+
// Apply the mixin to Object as the base class
|
|
276
|
+
const MixedBase = mixinFunction(Object);
|
|
277
|
+
mixinCache.set(Object, MixedBase);
|
|
278
|
+
// Create the proxy that handles both constructor and function calls
|
|
279
|
+
return new Proxy(MixedBase, {
|
|
280
|
+
// Handle `MixinClass(SomeBase)` - use as mixin function
|
|
281
|
+
apply(_target, _thisArg, args) {
|
|
282
|
+
if (args.length === 0) {
|
|
283
|
+
throw new Error('Mixin requires a base class');
|
|
284
|
+
}
|
|
285
|
+
const baseClass = args[0];
|
|
286
|
+
if (typeof baseClass !== 'function') {
|
|
287
|
+
throw new Error('Mixin requires a constructor function');
|
|
288
|
+
}
|
|
289
|
+
// Check if it's a valid constructor or a mixin
|
|
290
|
+
if (!decorator.isConstructor(baseClass) &&
|
|
291
|
+
!(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
|
|
292
|
+
throw new Error('Mixin requires a valid constructor');
|
|
293
|
+
}
|
|
294
|
+
// Check cache first
|
|
295
|
+
const cached = mixinCache.get(baseClass);
|
|
296
|
+
if (cached) {
|
|
297
|
+
return cached;
|
|
298
|
+
}
|
|
299
|
+
let usedBase = baseClass;
|
|
300
|
+
if (unwrapFunction) {
|
|
301
|
+
// Create a proxied base class that handles method unwrapping
|
|
302
|
+
const ProxiedBaseClass = class extends baseClass {
|
|
303
|
+
};
|
|
304
|
+
// Proxy the prototype methods to handle unwrapping
|
|
305
|
+
const originalPrototype = baseClass.prototype;
|
|
306
|
+
const proxiedPrototype = new Proxy(originalPrototype, {
|
|
307
|
+
get(target, prop, receiver) {
|
|
308
|
+
const value = decorator.ReflectGet(target, prop, receiver);
|
|
309
|
+
// Only wrap methods that are likely to access private fields
|
|
310
|
+
// Skip symbols and special properties that the reactive system needs
|
|
311
|
+
if (typeof value === 'function' &&
|
|
312
|
+
typeof prop === 'string' &&
|
|
313
|
+
!['constructor', 'toString', 'valueOf'].includes(prop)) {
|
|
314
|
+
// Return a wrapped version that uses unwrapped context
|
|
315
|
+
return function (...args) {
|
|
316
|
+
// Use the unwrapping function if provided, otherwise use this
|
|
317
|
+
const context = unwrapFunction(this);
|
|
318
|
+
return value.apply(context, args);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return value;
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
// Set the proxied prototype
|
|
325
|
+
Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
|
|
326
|
+
usedBase = ProxiedBaseClass;
|
|
327
|
+
}
|
|
328
|
+
// Create the mixed class using the proxied base class
|
|
329
|
+
const mixedClass = mixinFunction(usedBase);
|
|
330
|
+
// Cache the result
|
|
331
|
+
mixinCache.set(baseClass, mixedClass);
|
|
332
|
+
return mixedClass;
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// biome-ignore-all lint/suspicious/noConfusingVoidType: Type 'void' is not assignable to type 'ScopedCallback | undefined'.
|
|
338
|
+
// Argument of type '() => void' is not assignable to parameter of type '(dep: DependencyFunction) => ScopedCallback | undefined'.
|
|
339
|
+
// Track native reactivity
|
|
340
|
+
const nativeReactive = Symbol('native-reactive');
|
|
341
|
+
/**
|
|
342
|
+
* Symbol to mark individual objects as non-reactive
|
|
343
|
+
*/
|
|
344
|
+
const nonReactiveMark = Symbol('non-reactive');
|
|
345
|
+
/**
|
|
346
|
+
* Symbol to mark class properties as non-reactive
|
|
347
|
+
*/
|
|
348
|
+
const unreactiveProperties = Symbol('unreactive-properties');
|
|
349
|
+
/**
|
|
350
|
+
* Symbol for prototype forwarding in reactive objects
|
|
351
|
+
*/
|
|
352
|
+
const prototypeForwarding = Symbol('prototype-forwarding');
|
|
353
|
+
/**
|
|
354
|
+
* Symbol representing all properties in reactive tracking
|
|
355
|
+
*/
|
|
356
|
+
const allProps = Symbol('all-props');
|
|
357
|
+
// Symbol to mark functions with their root function
|
|
358
|
+
const rootFunction = Symbol('root-function');
|
|
359
|
+
/**
|
|
360
|
+
* Structured error codes for machine-readable diagnosis
|
|
361
|
+
*/
|
|
362
|
+
var ReactiveErrorCode;
|
|
363
|
+
(function (ReactiveErrorCode) {
|
|
364
|
+
ReactiveErrorCode["CycleDetected"] = "CYCLE_DETECTED";
|
|
365
|
+
ReactiveErrorCode["MaxDepthExceeded"] = "MAX_DEPTH_EXCEEDED";
|
|
366
|
+
ReactiveErrorCode["MaxReactionExceeded"] = "MAX_REACTION_EXCEEDED";
|
|
367
|
+
ReactiveErrorCode["WriteInComputed"] = "WRITE_IN_COMPUTED";
|
|
368
|
+
ReactiveErrorCode["TrackingError"] = "TRACKING_ERROR";
|
|
369
|
+
})(ReactiveErrorCode || (ReactiveErrorCode = {}));
|
|
370
|
+
/**
|
|
371
|
+
* Error class for reactive system errors
|
|
372
|
+
*/
|
|
373
|
+
class ReactiveError extends Error {
|
|
374
|
+
constructor(message, debugInfo) {
|
|
375
|
+
super(message);
|
|
376
|
+
this.debugInfo = debugInfo;
|
|
377
|
+
this.name = 'ReactiveError';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
|
|
381
|
+
/**
|
|
382
|
+
* Global options for the reactive system
|
|
383
|
+
*/
|
|
384
|
+
const options = {
|
|
385
|
+
/**
|
|
386
|
+
* Debug purpose: called when an effect is entered
|
|
387
|
+
* @param effect - The effect that is entered
|
|
388
|
+
*/
|
|
389
|
+
enter: (_effect) => { },
|
|
390
|
+
/**
|
|
391
|
+
* Debug purpose: called when an effect is left
|
|
392
|
+
* @param effect - The effect that is left
|
|
393
|
+
*/
|
|
394
|
+
leave: (_effect) => { },
|
|
395
|
+
/**
|
|
396
|
+
* Debug purpose: called when an effect is chained
|
|
397
|
+
* @param target - The effect that is being triggered
|
|
398
|
+
* @param caller - The effect that is calling the target
|
|
399
|
+
*/
|
|
400
|
+
chain: (_targets, _caller) => { },
|
|
401
|
+
/**
|
|
402
|
+
* Debug purpose: called when an effect chain is started
|
|
403
|
+
* @param target - The effect that is being triggered
|
|
404
|
+
*/
|
|
405
|
+
beginChain: (_targets) => { },
|
|
406
|
+
/**
|
|
407
|
+
* Debug purpose: called when an effect chain is ended
|
|
408
|
+
*/
|
|
409
|
+
endChain: () => { },
|
|
410
|
+
garbageCollected: (_fn) => { },
|
|
411
|
+
/**
|
|
412
|
+
* Debug purpose: called when an object is touched
|
|
413
|
+
* @param obj - The object that is touched
|
|
414
|
+
* @param evolution - The type of change
|
|
415
|
+
* @param props - The properties that changed
|
|
416
|
+
* @param deps - The dependencies that changed
|
|
417
|
+
*/
|
|
418
|
+
touched: (_obj, _evolution, _props, _deps) => { },
|
|
419
|
+
/**
|
|
420
|
+
* Debug purpose: called when an effect is skipped because it's already running
|
|
421
|
+
* @param effect - The effect that is already running
|
|
422
|
+
* @param runningChain - The array of effects from the detected one to the currently running one
|
|
423
|
+
*/
|
|
424
|
+
skipRunningEffect: (_effect, _runningChain) => { },
|
|
425
|
+
/**
|
|
426
|
+
* Debug purpose: maximum effect chain (like call stack max depth)
|
|
427
|
+
* Used to prevent infinite loops
|
|
428
|
+
* @default 100
|
|
429
|
+
*/
|
|
430
|
+
maxEffectChain: 100,
|
|
431
|
+
/**
|
|
432
|
+
* Debug purpose: maximum effect reaction (like call stack max depth)
|
|
433
|
+
* Used to prevent infinite loops
|
|
434
|
+
* @default 'throw'
|
|
435
|
+
*/
|
|
436
|
+
maxEffectReaction: 'throw',
|
|
437
|
+
/**
|
|
438
|
+
* How to handle cycles detected in effect batches
|
|
439
|
+
* - 'throw': Throw an error with cycle information (default, recommended for development)
|
|
440
|
+
* - 'warn': Log a warning and break the cycle by executing one effect
|
|
441
|
+
* - 'break': Silently break the cycle by executing one effect (recommended for production)
|
|
442
|
+
* - 'strict': Prevent cycle creation by checking graph before execution (throws error)
|
|
443
|
+
* @default 'throw'
|
|
444
|
+
*/
|
|
445
|
+
cycleHandling: 'throw',
|
|
446
|
+
/**
|
|
447
|
+
* Maximum depth for deep watching traversal
|
|
448
|
+
* Used to prevent infinite recursion in circular references
|
|
449
|
+
* @default 100
|
|
450
|
+
*/
|
|
451
|
+
maxDeepWatchDepth: 100,
|
|
452
|
+
/**
|
|
453
|
+
* Only react on instance members modification (not inherited properties)
|
|
454
|
+
* For instance, do not track class methods
|
|
455
|
+
* @default true
|
|
456
|
+
*/
|
|
457
|
+
instanceMembers: true,
|
|
458
|
+
/**
|
|
459
|
+
* Ignore accessors (getters and setters) and only track direct properties
|
|
460
|
+
* @default true
|
|
461
|
+
*/
|
|
462
|
+
ignoreAccessors: true,
|
|
463
|
+
/**
|
|
464
|
+
* Enable recursive touching when objects with the same prototype are replaced
|
|
465
|
+
* When enabled, replacing an object with another of the same prototype triggers
|
|
466
|
+
* recursive diffing instead of notifying parent effects
|
|
467
|
+
* @default true
|
|
468
|
+
*/
|
|
469
|
+
recursiveTouching: true,
|
|
470
|
+
/**
|
|
471
|
+
* Default async execution mode for effects that return Promises
|
|
472
|
+
* - 'cancel': Cancel previous async execution when dependencies change (default, enables async zone)
|
|
473
|
+
* - 'queue': Queue next execution to run after current completes (enables async zone)
|
|
474
|
+
* - 'ignore': Ignore new executions while async work is running (enables async zone)
|
|
475
|
+
* - false: Disable async zone and async mode handling (effects run concurrently)
|
|
476
|
+
*
|
|
477
|
+
* **When truthy:** Enables async zone (Promise.prototype wrapping) for automatic context
|
|
478
|
+
* preservation in Promise callbacks. Warning: This modifies Promise.prototype globally.
|
|
479
|
+
* Only enable if no other library modifies Promise.prototype.
|
|
480
|
+
*
|
|
481
|
+
* **When false:** Async zone is disabled. Use `tracked()` manually in Promise callbacks.
|
|
482
|
+
*
|
|
483
|
+
* Can be overridden per-effect via EffectOptions
|
|
484
|
+
* @default 'cancel'
|
|
485
|
+
*/
|
|
486
|
+
asyncMode: 'cancel',
|
|
487
|
+
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
488
|
+
warn: (...args) => console.warn(...args),
|
|
489
|
+
/**
|
|
490
|
+
* Configuration for the introspection system
|
|
491
|
+
*/
|
|
492
|
+
introspection: {
|
|
493
|
+
/**
|
|
494
|
+
* Whether to keep a history of mutations for debugging
|
|
495
|
+
* @default false
|
|
496
|
+
*/
|
|
497
|
+
enableHistory: false,
|
|
498
|
+
/**
|
|
499
|
+
* Number of mutations to keep in history
|
|
500
|
+
* @default 50
|
|
501
|
+
*/
|
|
502
|
+
historySize: 50,
|
|
503
|
+
},
|
|
504
|
+
/**
|
|
505
|
+
* Configuration for zone hooks - control which async APIs are hooked
|
|
506
|
+
* Each option controls whether the corresponding async API is wrapped to preserve effect context
|
|
507
|
+
* Only applies when asyncMode is enabled (truthy)
|
|
508
|
+
*/
|
|
509
|
+
zones: {
|
|
510
|
+
/**
|
|
511
|
+
* Hook setTimeout to preserve effect context
|
|
512
|
+
* @default true
|
|
513
|
+
*/
|
|
514
|
+
setTimeout: true,
|
|
515
|
+
/**
|
|
516
|
+
* Hook setInterval to preserve effect context
|
|
517
|
+
* @default true
|
|
518
|
+
*/
|
|
519
|
+
setInterval: true,
|
|
520
|
+
/**
|
|
521
|
+
* Hook requestAnimationFrame (runs in untracked context when hooked)
|
|
522
|
+
* @default true
|
|
523
|
+
*/
|
|
524
|
+
requestAnimationFrame: true,
|
|
525
|
+
/**
|
|
526
|
+
* Hook queueMicrotask to preserve effect context
|
|
527
|
+
* @default true
|
|
528
|
+
*/
|
|
529
|
+
queueMicrotask: true,
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Effect context stack for nested tracking (front = active, next = parent)
|
|
535
|
+
*/
|
|
536
|
+
const stack = [];
|
|
537
|
+
function captureEffectStack() {
|
|
538
|
+
return stack.slice();
|
|
539
|
+
}
|
|
540
|
+
function isRunning(effect) {
|
|
541
|
+
const rootEffect = getRoot(effect);
|
|
542
|
+
// Check if the effect is directly in the stack
|
|
543
|
+
const rootIndex = stack.indexOf(rootEffect);
|
|
544
|
+
if (rootIndex !== -1) {
|
|
545
|
+
return stack.slice(0, rootIndex + 1).reverse();
|
|
546
|
+
}
|
|
547
|
+
// Check if any effect in the stack is a descendant of this effect
|
|
548
|
+
// (i.e., walk up the parent chain from each stack effect to see if we reach this effect)
|
|
549
|
+
for (let i = 0; i < stack.length; i++) {
|
|
550
|
+
const stackEffect = stack[i];
|
|
551
|
+
let current = stackEffect;
|
|
552
|
+
const visited = new WeakSet();
|
|
553
|
+
const ancestorChain = [];
|
|
554
|
+
// TODO: That's perhaps a lot of computations for an `assert`
|
|
555
|
+
// Walk up the parent chain to find if this effect is an ancestor
|
|
556
|
+
while (current && !visited.has(current)) {
|
|
557
|
+
visited.add(current);
|
|
558
|
+
const currentRoot = getRoot(current);
|
|
559
|
+
ancestorChain.push(currentRoot);
|
|
560
|
+
if (currentRoot === rootEffect) {
|
|
561
|
+
// Found a descendant - build the full chain from ancestor to active
|
|
562
|
+
// The ancestorChain contains [descendant, parent, ..., ancestor] (walking up)
|
|
563
|
+
// We need [ancestor (effect), ..., parent, descendant, ...stack from descendant to active]
|
|
564
|
+
const chainFromAncestor = ancestorChain.reverse(); // [ancestor, ..., descendant]
|
|
565
|
+
// Prepend the actual effect we're checking (in case current is a wrapper)
|
|
566
|
+
if (chainFromAncestor[0] !== rootEffect) {
|
|
567
|
+
chainFromAncestor.unshift(rootEffect);
|
|
568
|
+
}
|
|
569
|
+
// Append the rest of the stack from the descendant to the active effect
|
|
570
|
+
const stackFromDescendant = stack.slice(0, i + 1).reverse(); // [descendant, ..., active]
|
|
571
|
+
// Remove duplicate descendant (it's both at end of chainFromAncestor and start of stackFromDescendant)
|
|
572
|
+
if (chainFromAncestor.length > 0 && stackFromDescendant.length > 0) {
|
|
573
|
+
stackFromDescendant.shift(); // Remove duplicate descendant
|
|
574
|
+
}
|
|
575
|
+
return [...chainFromAncestor, ...stackFromDescendant];
|
|
576
|
+
}
|
|
577
|
+
current = effectParent.get(current);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
function withEffectStack(snapshot, fn) {
|
|
583
|
+
const previousStack = stack.slice();
|
|
584
|
+
assignStack(snapshot);
|
|
585
|
+
try {
|
|
586
|
+
return fn();
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
assignStack(previousStack);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function getActiveEffect() {
|
|
593
|
+
return stack[0];
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Executes a function with a specific effect context
|
|
597
|
+
* @param effect - The effect to use as context
|
|
598
|
+
* @param fn - The function to execute
|
|
599
|
+
* @param keepParent - Whether to keep the parent effect context
|
|
600
|
+
* @returns The result of the function
|
|
601
|
+
*/
|
|
602
|
+
function withEffect(effect, fn) {
|
|
603
|
+
// console.log('[Mutts] withEffect', effect ? 'Active' : 'NULL');
|
|
604
|
+
if (getRoot(effect) === getRoot(getActiveEffect()))
|
|
605
|
+
return fn();
|
|
606
|
+
stack.unshift(effect);
|
|
607
|
+
try {
|
|
608
|
+
return fn();
|
|
609
|
+
}
|
|
610
|
+
finally {
|
|
611
|
+
const recoveredEffect = stack.shift();
|
|
612
|
+
if (recoveredEffect !== effect)
|
|
613
|
+
throw new ReactiveError('[reactive] Effect stack mismatch');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function assignStack(values) {
|
|
617
|
+
stack.length = 0;
|
|
618
|
+
stack.push(...values);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const objectToProxy = new WeakMap();
|
|
622
|
+
const proxyToObject = new WeakMap();
|
|
623
|
+
function storeProxyRelationship(target, proxy) {
|
|
624
|
+
objectToProxy.set(target, proxy);
|
|
625
|
+
proxyToObject.set(proxy, target);
|
|
626
|
+
}
|
|
627
|
+
function getExistingProxy(target) {
|
|
628
|
+
return objectToProxy.get(target);
|
|
629
|
+
}
|
|
630
|
+
function trackProxyObject(proxy, target) {
|
|
631
|
+
proxyToObject.set(proxy, target);
|
|
632
|
+
}
|
|
633
|
+
function unwrap(obj) {
|
|
634
|
+
let current = obj;
|
|
635
|
+
while (current && typeof current === 'object' && current !== null && proxyToObject.has(current)) {
|
|
636
|
+
current = proxyToObject.get(current);
|
|
637
|
+
}
|
|
638
|
+
return current;
|
|
639
|
+
}
|
|
640
|
+
function isReactive(obj) {
|
|
641
|
+
return proxyToObject.has(obj);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Track which effects are watching which reactive objects for cleanup
|
|
645
|
+
const effectToReactiveObjects = new WeakMap();
|
|
646
|
+
// Track effects per reactive object and property
|
|
647
|
+
const watchers = new WeakMap();
|
|
648
|
+
// runEffect -> set<stop>
|
|
649
|
+
const effectChildren = new WeakMap();
|
|
650
|
+
// Track parent effect relationships for hierarchy traversal (used in deep touch filtering)
|
|
651
|
+
const effectParent = new WeakMap();
|
|
652
|
+
/**
|
|
653
|
+
* Marks a function with its root function for effect tracking
|
|
654
|
+
* @param fn - The function to mark
|
|
655
|
+
* @param root - The root function
|
|
656
|
+
* @returns The marked function
|
|
657
|
+
*/
|
|
658
|
+
function markWithRoot(fn, root) {
|
|
659
|
+
// Mark fn with the new root
|
|
660
|
+
return Object.defineProperty(fn, rootFunction, {
|
|
661
|
+
value: getRoot(root),
|
|
662
|
+
writable: false,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Gets the root function of a function for effect tracking
|
|
667
|
+
* @param fn - The function to get the root of
|
|
668
|
+
* @returns The root function
|
|
669
|
+
*/
|
|
670
|
+
function getRoot(fn) {
|
|
671
|
+
return fn?.[rootFunction] || fn;
|
|
672
|
+
}
|
|
673
|
+
// Flag to disable dependency tracking for the current active effect (not globally)
|
|
674
|
+
const trackingDisabledEffects = new WeakSet();
|
|
675
|
+
let globalTrackingDisabled = false;
|
|
676
|
+
function getTrackingDisabled() {
|
|
677
|
+
const active = getActiveEffect();
|
|
678
|
+
if (!active)
|
|
679
|
+
return globalTrackingDisabled;
|
|
680
|
+
return trackingDisabledEffects.has(getRoot(active));
|
|
681
|
+
}
|
|
682
|
+
function setTrackingDisabled(value) {
|
|
683
|
+
const active = getActiveEffect();
|
|
684
|
+
if (!active) {
|
|
685
|
+
globalTrackingDisabled = value;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const root = getRoot(active);
|
|
689
|
+
if (value)
|
|
690
|
+
trackingDisabledEffects.add(root);
|
|
691
|
+
else
|
|
692
|
+
trackingDisabledEffects.delete(root);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Marks a property as a dependency of the current effect
|
|
696
|
+
* @param obj - The object containing the property
|
|
697
|
+
* @param prop - The property name (defaults to allProps)
|
|
698
|
+
*/
|
|
699
|
+
function dependant(obj, prop = allProps) {
|
|
700
|
+
obj = unwrap(obj);
|
|
701
|
+
const currentActiveEffect = getActiveEffect();
|
|
702
|
+
// Early return if no active effect, tracking disabled, or invalid prop
|
|
703
|
+
if (!currentActiveEffect ||
|
|
704
|
+
getTrackingDisabled() ||
|
|
705
|
+
(typeof prop === 'symbol' && prop !== allProps))
|
|
706
|
+
return;
|
|
707
|
+
registerDependency(obj, prop, currentActiveEffect);
|
|
708
|
+
}
|
|
709
|
+
function registerDependency(obj, prop, currentActiveEffect) {
|
|
710
|
+
let objectWatchers = watchers.get(obj);
|
|
711
|
+
if (!objectWatchers) {
|
|
712
|
+
objectWatchers = new Map();
|
|
713
|
+
watchers.set(obj, objectWatchers);
|
|
714
|
+
}
|
|
715
|
+
let deps = objectWatchers.get(prop);
|
|
716
|
+
if (!deps) {
|
|
717
|
+
deps = new Set();
|
|
718
|
+
objectWatchers.set(prop, deps);
|
|
719
|
+
}
|
|
720
|
+
deps.add(currentActiveEffect);
|
|
721
|
+
// Track which reactive objects this effect is watching
|
|
722
|
+
const effectObjects = effectToReactiveObjects.get(currentActiveEffect);
|
|
723
|
+
if (effectObjects) {
|
|
724
|
+
effectObjects.add(obj);
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Debug utilities for the reactivity system
|
|
733
|
+
* - Captures effect metadata (names, parent relationships)
|
|
734
|
+
* - Records cause → consequence edges with object/prop labels
|
|
735
|
+
* - Provides graph data for tooling (DevTools panel, etc.)
|
|
736
|
+
*/
|
|
737
|
+
const EXTERNAL_SOURCE = Symbol('external-source');
|
|
738
|
+
let devtoolsEnabled = false;
|
|
739
|
+
// Registry for debugging (populated lazily when DevTools are enabled)
|
|
740
|
+
const debugEffectRegistry = new Set();
|
|
741
|
+
const debugObjectRegistry = new Set();
|
|
742
|
+
// Human-friendly names
|
|
743
|
+
const effectNames = new WeakMap();
|
|
744
|
+
const objectNames = new WeakMap();
|
|
745
|
+
let effectCounter = 0;
|
|
746
|
+
let objectCounter = 0;
|
|
747
|
+
const triggerGraph = new Map();
|
|
748
|
+
function ensureEffectName(effect) {
|
|
749
|
+
let name = effectNames.get(effect);
|
|
750
|
+
if (!name) {
|
|
751
|
+
const root = getRoot(effect);
|
|
752
|
+
name = root?.name?.trim() || `effect_${++effectCounter}`;
|
|
753
|
+
effectNames.set(effect, name);
|
|
754
|
+
}
|
|
755
|
+
return name;
|
|
756
|
+
}
|
|
757
|
+
function ensureObjectName(obj) {
|
|
758
|
+
let name = objectNames.get(obj);
|
|
759
|
+
if (!name) {
|
|
760
|
+
const ctorName = obj?.constructor?.name;
|
|
761
|
+
const base = ctorName && ctorName !== 'Object' ? ctorName : 'object';
|
|
762
|
+
name = `${base}_${++objectCounter}`;
|
|
763
|
+
objectNames.set(obj, name);
|
|
764
|
+
}
|
|
765
|
+
return name;
|
|
766
|
+
}
|
|
767
|
+
function describeProp(obj, prop) {
|
|
768
|
+
const objectName = ensureObjectName(obj);
|
|
769
|
+
if (prop === allProps)
|
|
770
|
+
return `${objectName}.*`;
|
|
771
|
+
if (typeof prop === 'symbol')
|
|
772
|
+
return `${objectName}.${prop.description ?? prop.toString()}`;
|
|
773
|
+
return `${objectName}.${String(prop)}`;
|
|
774
|
+
}
|
|
775
|
+
function addEffectToRegistry(effect) {
|
|
776
|
+
if (!effect || debugEffectRegistry.has(effect))
|
|
777
|
+
return;
|
|
778
|
+
debugEffectRegistry.add(effect);
|
|
779
|
+
const deps = effectToReactiveObjects.get(effect);
|
|
780
|
+
if (deps) {
|
|
781
|
+
for (const obj of deps) {
|
|
782
|
+
documentObject(obj);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function documentObject(obj) {
|
|
787
|
+
if (!debugObjectRegistry.has(obj)) {
|
|
788
|
+
dbRegisterObject(obj);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function dbRegisterObject(obj) {
|
|
792
|
+
debugObjectRegistry.add(obj);
|
|
793
|
+
ensureObjectName(obj);
|
|
794
|
+
}
|
|
795
|
+
function ensureParentChains(effects) {
|
|
796
|
+
const queue = Array.from(effects);
|
|
797
|
+
for (let i = 0; i < queue.length; i++) {
|
|
798
|
+
const effect = queue[i];
|
|
799
|
+
const parent = effectParent.get(effect);
|
|
800
|
+
if (parent && !effects.has(parent)) {
|
|
801
|
+
effects.add(parent);
|
|
802
|
+
queue.push(parent);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function ensureTriggerContainers(source) {
|
|
807
|
+
let targetMap = triggerGraph.get(source);
|
|
808
|
+
if (!targetMap) {
|
|
809
|
+
targetMap = new Map();
|
|
810
|
+
triggerGraph.set(source, targetMap);
|
|
811
|
+
}
|
|
812
|
+
return targetMap;
|
|
813
|
+
}
|
|
814
|
+
function ensureTriggerRecord(source, target, label, obj, prop, evolution) {
|
|
815
|
+
const targetMap = ensureTriggerContainers(source);
|
|
816
|
+
let labelMap = targetMap.get(target);
|
|
817
|
+
if (!labelMap) {
|
|
818
|
+
labelMap = new Map();
|
|
819
|
+
targetMap.set(target, labelMap);
|
|
820
|
+
}
|
|
821
|
+
let record = labelMap.get(label);
|
|
822
|
+
if (!record) {
|
|
823
|
+
record = { label, object: obj, prop, evolution, count: 0, lastTriggered: Date.now() };
|
|
824
|
+
labelMap.set(label, record);
|
|
825
|
+
}
|
|
826
|
+
return record;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Assign a debug-friendly name to an effect (shown in DevTools)
|
|
830
|
+
*/
|
|
831
|
+
function setEffectName(effect, name) {
|
|
832
|
+
effectNames.set(effect, name);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Assign a debug-friendly name to a reactive object
|
|
836
|
+
*/
|
|
837
|
+
function setObjectName(obj, name) {
|
|
838
|
+
objectNames.set(obj, name);
|
|
839
|
+
debugObjectRegistry.add(obj);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Register an effect so it appears in the DevTools graph
|
|
843
|
+
*/
|
|
844
|
+
function registerEffectForDebug(effect) {
|
|
845
|
+
if (!effect || !devtoolsEnabled)
|
|
846
|
+
return;
|
|
847
|
+
addEffectToRegistry(effect);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Register a reactive object so it appears in the DevTools graph
|
|
851
|
+
*/
|
|
852
|
+
function registerObjectForDebug(obj) {
|
|
853
|
+
if (!devtoolsEnabled)
|
|
854
|
+
return;
|
|
855
|
+
documentObject(obj);
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Records a cause → consequence relationship between effects.
|
|
859
|
+
* @param source - The effect performing the write (undefined if external/user input)
|
|
860
|
+
* @param target - The effect that re-ran because of the write
|
|
861
|
+
* @param obj - The reactive object that changed
|
|
862
|
+
* @param prop - The property that changed
|
|
863
|
+
* @param evolution - The type of change (set/add/del/bunch)
|
|
864
|
+
*/
|
|
865
|
+
function recordTriggerLink(source, target, obj, prop, evolution) {
|
|
866
|
+
if (options.introspection.enableHistory) {
|
|
867
|
+
addToMutationHistory(source, target, obj, prop, evolution);
|
|
868
|
+
}
|
|
869
|
+
if (!devtoolsEnabled)
|
|
870
|
+
return;
|
|
871
|
+
addEffectToRegistry(target);
|
|
872
|
+
if (source)
|
|
873
|
+
addEffectToRegistry(source);
|
|
874
|
+
const descriptor = describeProp(obj, prop);
|
|
875
|
+
const record = ensureTriggerRecord(source ?? EXTERNAL_SOURCE, target, descriptor, obj, prop, evolution);
|
|
876
|
+
record.count += 1;
|
|
877
|
+
record.lastTriggered = Date.now();
|
|
878
|
+
documentObject(obj);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Traces back the chain of triggers that led to a specific effect
|
|
882
|
+
* @param effect The effect to trace back
|
|
883
|
+
* @param limit Max depth
|
|
884
|
+
*/
|
|
885
|
+
function getTriggerChain(effect, limit = 5) {
|
|
886
|
+
const chain = [];
|
|
887
|
+
let current = effect;
|
|
888
|
+
for (let i = 0; i < limit; i++) {
|
|
889
|
+
// Find who triggered 'current'
|
|
890
|
+
// We need to reverse search the triggerGraph (source -> target)
|
|
891
|
+
// This is expensive O(Edges) but okay for error reporting
|
|
892
|
+
let foundSource;
|
|
893
|
+
let foundReason = '';
|
|
894
|
+
search: for (const [source, targetMap] of triggerGraph) {
|
|
895
|
+
for (const [target, labelMap] of targetMap) {
|
|
896
|
+
if (target === current) {
|
|
897
|
+
// Found a source! Use the most recent trigger record
|
|
898
|
+
let lastTime = 0;
|
|
899
|
+
for (const record of labelMap.values()) {
|
|
900
|
+
if (record.lastTriggered > lastTime) {
|
|
901
|
+
lastTime = record.lastTriggered;
|
|
902
|
+
foundReason = record.label;
|
|
903
|
+
foundSource = source === EXTERNAL_SOURCE ? undefined : source;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (foundSource || foundReason)
|
|
907
|
+
break search;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (foundSource) {
|
|
912
|
+
chain.push(`${ensureEffectName(foundSource)} -> (${foundReason}) -> ${ensureEffectName(current)}`);
|
|
913
|
+
current = foundSource;
|
|
914
|
+
}
|
|
915
|
+
else if (foundReason) {
|
|
916
|
+
chain.push(`External -> (${foundReason}) -> ${ensureEffectName(current)}`);
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return chain.reverse();
|
|
924
|
+
}
|
|
925
|
+
function buildEffectNodes(allEffects) {
|
|
926
|
+
const nodes = [];
|
|
927
|
+
const nodeByEffect = new Map();
|
|
928
|
+
const ordered = Array.from(allEffects);
|
|
929
|
+
for (const effect of ordered) {
|
|
930
|
+
const label = ensureEffectName(effect);
|
|
931
|
+
const node = {
|
|
932
|
+
id: `effect_${nodes.length}`,
|
|
933
|
+
label,
|
|
934
|
+
type: 'effect',
|
|
935
|
+
depth: 0,
|
|
936
|
+
debugName: label,
|
|
937
|
+
};
|
|
938
|
+
nodes.push(node);
|
|
939
|
+
nodeByEffect.set(effect, node);
|
|
940
|
+
}
|
|
941
|
+
const depthCache = new Map();
|
|
942
|
+
const computeDepth = (effect) => {
|
|
943
|
+
if (!effect)
|
|
944
|
+
return 0;
|
|
945
|
+
const cached = depthCache.get(effect);
|
|
946
|
+
if (cached !== undefined)
|
|
947
|
+
return cached;
|
|
948
|
+
const parent = effectParent.get(effect);
|
|
949
|
+
const depth = computeDepth(parent) + (parent ? 1 : 0);
|
|
950
|
+
depthCache.set(effect, depth);
|
|
951
|
+
return depth;
|
|
952
|
+
};
|
|
953
|
+
for (const [effect, node] of nodeByEffect) {
|
|
954
|
+
node.depth = computeDepth(effect);
|
|
955
|
+
const parent = effectParent.get(effect);
|
|
956
|
+
if (parent) {
|
|
957
|
+
const parentNode = nodeByEffect.get(parent);
|
|
958
|
+
if (parentNode) {
|
|
959
|
+
node.parentId = parentNode.id;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return { nodes, nodeByEffect };
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Builds a graph representing current reactive state (effects, objects, and trigger edges)
|
|
967
|
+
*/
|
|
968
|
+
function buildReactivityGraph() {
|
|
969
|
+
const nodes = [];
|
|
970
|
+
const edges = [];
|
|
971
|
+
const nodeIds = new Map();
|
|
972
|
+
const allEffects = new Set(debugEffectRegistry);
|
|
973
|
+
ensureParentChains(allEffects);
|
|
974
|
+
const { nodes: effectNodes, nodeByEffect } = buildEffectNodes(allEffects);
|
|
975
|
+
for (const node of effectNodes)
|
|
976
|
+
nodes.push(node);
|
|
977
|
+
for (const [effect, node] of nodeByEffect) {
|
|
978
|
+
nodeIds.set(effect, node.id);
|
|
979
|
+
}
|
|
980
|
+
// Object nodes (optional, used for dependency inspection)
|
|
981
|
+
for (const obj of debugObjectRegistry) {
|
|
982
|
+
const id = `object_${nodes.length}`;
|
|
983
|
+
nodes.push({ id, label: ensureObjectName(obj), type: 'state', debugName: objectNames.get(obj) });
|
|
984
|
+
nodeIds.set(obj, id);
|
|
985
|
+
}
|
|
986
|
+
// External source node (user/system outside of effects)
|
|
987
|
+
if (triggerGraph.has(EXTERNAL_SOURCE)) {
|
|
988
|
+
const externalId = `effect_external`;
|
|
989
|
+
nodes.push({ id: externalId, label: 'External', type: 'external', depth: 0 });
|
|
990
|
+
nodeIds.set(EXTERNAL_SOURCE, externalId);
|
|
991
|
+
}
|
|
992
|
+
// Dependency edges (effect → object)
|
|
993
|
+
for (const effect of allEffects) {
|
|
994
|
+
const effectId = nodeIds.get(effect);
|
|
995
|
+
if (!effectId)
|
|
996
|
+
continue;
|
|
997
|
+
const deps = effectToReactiveObjects.get(effect);
|
|
998
|
+
if (!deps)
|
|
999
|
+
continue;
|
|
1000
|
+
for (const obj of deps) {
|
|
1001
|
+
const objId = nodeIds.get(obj);
|
|
1002
|
+
if (!objId)
|
|
1003
|
+
continue;
|
|
1004
|
+
edges.push({
|
|
1005
|
+
id: `${effectId}->${objId}`,
|
|
1006
|
+
source: effectId,
|
|
1007
|
+
target: objId,
|
|
1008
|
+
type: 'dependency',
|
|
1009
|
+
label: 'depends',
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// Cause edges (effect/object/prop → effect)
|
|
1014
|
+
for (const [source, targetMap] of triggerGraph) {
|
|
1015
|
+
for (const [targetEffect, labelMap] of targetMap) {
|
|
1016
|
+
const targetId = nodeIds.get(targetEffect);
|
|
1017
|
+
if (!targetId)
|
|
1018
|
+
continue;
|
|
1019
|
+
const sourceId = nodeIds.get(source);
|
|
1020
|
+
if (!sourceId)
|
|
1021
|
+
continue;
|
|
1022
|
+
for (const record of labelMap.values()) {
|
|
1023
|
+
edges.push({
|
|
1024
|
+
id: `${sourceId}->${targetId}:${record.label}`,
|
|
1025
|
+
source: sourceId,
|
|
1026
|
+
target: targetId,
|
|
1027
|
+
type: 'cause',
|
|
1028
|
+
label: record.count > 1 ? `${record.label} (${record.count})` : record.label,
|
|
1029
|
+
count: record.count,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
nodes,
|
|
1036
|
+
edges,
|
|
1037
|
+
meta: {
|
|
1038
|
+
generatedAt: Date.now(),
|
|
1039
|
+
devtoolsEnabled,
|
|
1040
|
+
},
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Enables the DevTools bridge and exposes the debug API on window.
|
|
1045
|
+
* Call as early as possible in development builds.
|
|
1046
|
+
*/
|
|
1047
|
+
function enableDevTools() {
|
|
1048
|
+
if (typeof window === 'undefined')
|
|
1049
|
+
return;
|
|
1050
|
+
if (devtoolsEnabled)
|
|
1051
|
+
return;
|
|
1052
|
+
devtoolsEnabled = true;
|
|
1053
|
+
// @ts-expect-error - global window extension
|
|
1054
|
+
window.__MUTTS_DEVTOOLS__ = {
|
|
1055
|
+
getGraph: buildReactivityGraph,
|
|
1056
|
+
setEffectName,
|
|
1057
|
+
setObjectName,
|
|
1058
|
+
registerEffect: registerEffectForDebug,
|
|
1059
|
+
registerObject: registerObjectForDebug,
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function isDevtoolsEnabled() {
|
|
1063
|
+
return devtoolsEnabled;
|
|
1064
|
+
}
|
|
1065
|
+
const mutationHistory = [];
|
|
1066
|
+
let mutationCounter = 0;
|
|
1067
|
+
function addToMutationHistory(source, target, obj, prop, evolution) {
|
|
1068
|
+
const record = {
|
|
1069
|
+
id: ++mutationCounter,
|
|
1070
|
+
timestamp: Date.now(),
|
|
1071
|
+
source: source ? ensureEffectName(source) : 'External',
|
|
1072
|
+
target: ensureEffectName(target),
|
|
1073
|
+
objectName: ensureObjectName(obj),
|
|
1074
|
+
prop: String(prop),
|
|
1075
|
+
type: evolution.type,
|
|
1076
|
+
};
|
|
1077
|
+
mutationHistory.push(record);
|
|
1078
|
+
if (mutationHistory.length > options.introspection.historySize) {
|
|
1079
|
+
mutationHistory.shift();
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Zone-like async context preservation for reactive effects
|
|
1085
|
+
*
|
|
1086
|
+
* Automatically preserves effect context across async boundaries:
|
|
1087
|
+
* - Promise methods: .then(), .catch(), .finally()
|
|
1088
|
+
* - Timers: setTimeout(), setInterval()
|
|
1089
|
+
* - Animation: requestAnimationFrame() (if available) - runs in untracked context
|
|
1090
|
+
* - Microtasks: queueMicrotask() (if available)
|
|
1091
|
+
*
|
|
1092
|
+
* **IMPORTANT:** This module is opt-in via `reactiveOptions.asyncMode` (truthy = enabled, false = disabled).
|
|
1093
|
+
* By default, async zone is ENABLED with 'cancel' mode.
|
|
1094
|
+
*
|
|
1095
|
+
* When disabled (asyncMode = false), use `tracked()` manually in async callbacks.
|
|
1096
|
+
* When enabled (asyncMode = 'cancel' | 'queue' | 'ignore'), async entry points are wrapped ONCE.
|
|
1097
|
+
*/
|
|
1098
|
+
let zoneHooked = false;
|
|
1099
|
+
// Store original Promise methods at module load time (before any wrapping)
|
|
1100
|
+
// This ensures we always have the true originals, even if wrapping happens multiple times
|
|
1101
|
+
const originalPromiseThen = Object.getOwnPropertyDescriptor(Promise.prototype, 'then')?.value || Promise.prototype.then;
|
|
1102
|
+
const originalPromiseCatch = Object.getOwnPropertyDescriptor(Promise.prototype, 'catch')?.value || Promise.prototype.catch;
|
|
1103
|
+
const originalPromiseFinally = Object.getOwnPropertyDescriptor(Promise.prototype, 'finally')?.value || Promise.prototype.finally;
|
|
1104
|
+
// Store original timer functions at module load time
|
|
1105
|
+
const originalSetTimeout = globalThis.setTimeout;
|
|
1106
|
+
const originalSetInterval = globalThis.setInterval;
|
|
1107
|
+
const originalRequestAnimationFrame = typeof globalThis.requestAnimationFrame !== 'undefined'
|
|
1108
|
+
? globalThis.requestAnimationFrame
|
|
1109
|
+
: undefined;
|
|
1110
|
+
const originalQueueMicrotask = typeof globalThis.queueMicrotask !== 'undefined' ? globalThis.queueMicrotask : undefined;
|
|
1111
|
+
// Store batch function to avoid circular dependency
|
|
1112
|
+
let batchFn;
|
|
1113
|
+
/**
|
|
1114
|
+
* Check the asyncMode option and hook Promise.prototype once if enabled
|
|
1115
|
+
* Called lazily on first effect creation
|
|
1116
|
+
* asyncMode being truthy enables async zone, false disables it
|
|
1117
|
+
*
|
|
1118
|
+
* @param batch - Optional batch function injection from effects.ts to avoid circular dependency
|
|
1119
|
+
*/
|
|
1120
|
+
function ensureZoneHooked(batch) {
|
|
1121
|
+
if (batch)
|
|
1122
|
+
batchFn = batch;
|
|
1123
|
+
if (zoneHooked || !options.asyncMode)
|
|
1124
|
+
return;
|
|
1125
|
+
hookZone();
|
|
1126
|
+
zoneHooked = true;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Hook Promise.prototype methods to preserve effect context
|
|
1130
|
+
*/
|
|
1131
|
+
function hookZone() {
|
|
1132
|
+
// biome-ignore lint/suspicious/noThenProperty: Intentional wrapping for zone functionality
|
|
1133
|
+
Promise.prototype.then = function (onFulfilled, onRejected) {
|
|
1134
|
+
const capturedStack = captureEffectStack();
|
|
1135
|
+
return originalPromiseThen.call(this, wrapCallback(onFulfilled, capturedStack), wrapCallback(onRejected, capturedStack));
|
|
1136
|
+
};
|
|
1137
|
+
Promise.prototype.catch = function (onRejected) {
|
|
1138
|
+
const capturedStack = captureEffectStack();
|
|
1139
|
+
return originalPromiseCatch.call(this, wrapCallback(onRejected, capturedStack));
|
|
1140
|
+
};
|
|
1141
|
+
Promise.prototype.finally = function (onFinally) {
|
|
1142
|
+
const capturedStack = captureEffectStack();
|
|
1143
|
+
return originalPromiseFinally.call(this, wrapCallback(onFinally, capturedStack));
|
|
1144
|
+
};
|
|
1145
|
+
// Hook setTimeout - preserve original function properties for Node.js compatibility
|
|
1146
|
+
const wrappedSetTimeout = ((callback, delay, ...args) => {
|
|
1147
|
+
const capturedStack = options.zones.setTimeout ? captureEffectStack() : undefined;
|
|
1148
|
+
return originalSetTimeout.apply(globalThis, [
|
|
1149
|
+
wrapCallback(callback, capturedStack),
|
|
1150
|
+
delay,
|
|
1151
|
+
...args,
|
|
1152
|
+
]);
|
|
1153
|
+
});
|
|
1154
|
+
Object.assign(wrappedSetTimeout, originalSetTimeout);
|
|
1155
|
+
globalThis.setTimeout = wrappedSetTimeout;
|
|
1156
|
+
// Hook setInterval - preserve original function properties for Node.js compatibility
|
|
1157
|
+
const wrappedSetInterval = ((callback, delay, ...args) => {
|
|
1158
|
+
const capturedStack = options.zones.setInterval ? captureEffectStack() : undefined;
|
|
1159
|
+
return originalSetInterval.apply(globalThis, [
|
|
1160
|
+
wrapCallback(callback, capturedStack),
|
|
1161
|
+
delay,
|
|
1162
|
+
...args,
|
|
1163
|
+
]);
|
|
1164
|
+
});
|
|
1165
|
+
Object.assign(wrappedSetInterval, originalSetInterval);
|
|
1166
|
+
globalThis.setInterval = wrappedSetInterval;
|
|
1167
|
+
// Hook requestAnimationFrame if available
|
|
1168
|
+
if (originalRequestAnimationFrame) {
|
|
1169
|
+
globalThis.requestAnimationFrame = ((callback) => {
|
|
1170
|
+
const capturedStack = options.zones.requestAnimationFrame ? captureEffectStack() : undefined;
|
|
1171
|
+
return originalRequestAnimationFrame.call(globalThis, wrapCallback(callback, capturedStack));
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
// Hook queueMicrotask if available
|
|
1175
|
+
if (originalQueueMicrotask) {
|
|
1176
|
+
globalThis.queueMicrotask = ((callback) => {
|
|
1177
|
+
const capturedStack = options.zones.queueMicrotask ? captureEffectStack() : undefined;
|
|
1178
|
+
originalQueueMicrotask.call(globalThis, wrapCallback(callback, capturedStack));
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Wraps a callback to restore effect context and ensure batching
|
|
1184
|
+
*/
|
|
1185
|
+
function wrapCallback(callback, capturedStack) {
|
|
1186
|
+
if (!callback)
|
|
1187
|
+
return undefined;
|
|
1188
|
+
// If no stack to restore and no batch function, direct call (optimization)
|
|
1189
|
+
if ((!capturedStack || !capturedStack.length) && !batchFn) {
|
|
1190
|
+
return callback;
|
|
1191
|
+
}
|
|
1192
|
+
return ((...args) => {
|
|
1193
|
+
const execute = () => {
|
|
1194
|
+
if (capturedStack?.length) {
|
|
1195
|
+
return withEffectStack(capturedStack, () => callback(...args));
|
|
1196
|
+
}
|
|
1197
|
+
return callback(...args);
|
|
1198
|
+
};
|
|
1199
|
+
if (batchFn) {
|
|
1200
|
+
return batchFn(execute, 'immediate');
|
|
1201
|
+
}
|
|
1202
|
+
return execute();
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Manually enable/disable the zone (for testing)
|
|
1207
|
+
*/
|
|
1208
|
+
function setZoneEnabled(enabled) {
|
|
1209
|
+
if (enabled && !zoneHooked) {
|
|
1210
|
+
hookZone();
|
|
1211
|
+
zoneHooked = true;
|
|
1212
|
+
}
|
|
1213
|
+
else if (!enabled && zoneHooked) {
|
|
1214
|
+
// Restore original Promise methods
|
|
1215
|
+
// biome-ignore lint/suspicious/noThenProperty: Restoring original methods
|
|
1216
|
+
Promise.prototype.then = originalPromiseThen;
|
|
1217
|
+
Promise.prototype.catch = originalPromiseCatch;
|
|
1218
|
+
Promise.prototype.finally = originalPromiseFinally;
|
|
1219
|
+
// Restore original timer functions
|
|
1220
|
+
globalThis.setTimeout = originalSetTimeout;
|
|
1221
|
+
globalThis.setInterval = originalSetInterval;
|
|
1222
|
+
if (originalRequestAnimationFrame) {
|
|
1223
|
+
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
|
|
1224
|
+
}
|
|
1225
|
+
if (originalQueueMicrotask) {
|
|
1226
|
+
globalThis.queueMicrotask = originalQueueMicrotask;
|
|
1227
|
+
}
|
|
1228
|
+
zoneHooked = false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Check if zone is currently hooked
|
|
1233
|
+
*/
|
|
1234
|
+
function isZoneEnabled() {
|
|
1235
|
+
return zoneHooked;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Finds a cycle in a sequence of functions by looking for the first repetition
|
|
1240
|
+
*/
|
|
1241
|
+
function findCycleInChain(roots) {
|
|
1242
|
+
const seen = new Map();
|
|
1243
|
+
for (let i = 0; i < roots.length; i++) {
|
|
1244
|
+
const root = roots[i];
|
|
1245
|
+
if (seen.has(root)) {
|
|
1246
|
+
return roots.slice(seen.get(root));
|
|
1247
|
+
}
|
|
1248
|
+
seen.set(root, i);
|
|
1249
|
+
}
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Formats a list of function roots into a readable trace
|
|
1254
|
+
*/
|
|
1255
|
+
function formatRoots(roots, limit = 20) {
|
|
1256
|
+
const names = roots.map((r) => r.name || '<anonymous>');
|
|
1257
|
+
if (names.length <= limit)
|
|
1258
|
+
return names.join(' → ');
|
|
1259
|
+
const start = names.slice(0, 5);
|
|
1260
|
+
const end = names.slice(-10);
|
|
1261
|
+
return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Registers a debug callback that is called when the current effect is triggered by a dependency change
|
|
1265
|
+
*
|
|
1266
|
+
* This function is useful for debugging purposes as it pin-points exactly which reactive property
|
|
1267
|
+
* change triggered the effect. The callback receives information about:
|
|
1268
|
+
* - The object that changed
|
|
1269
|
+
* - The type of change (evolution)
|
|
1270
|
+
* - The specific property that changed
|
|
1271
|
+
*
|
|
1272
|
+
* **Note:** The tracker callback is automatically removed after being called once. If you need
|
|
1273
|
+
* to track multiple triggers, call `trackEffect` again within the effect.
|
|
1274
|
+
*
|
|
1275
|
+
* @param onTouch - Callback function that receives (obj, evolution, prop) when the effect is triggered
|
|
1276
|
+
* @throws {Error} If called outside of an effect context
|
|
1277
|
+
*
|
|
1278
|
+
* @example
|
|
1279
|
+
* ```typescript
|
|
1280
|
+
* const state = reactive({ count: 0, name: 'John' })
|
|
1281
|
+
*
|
|
1282
|
+
* effect(() => {
|
|
1283
|
+
* // Register a tracker to see what triggers this effect
|
|
1284
|
+
* trackEffect((obj, evolution, prop) => {
|
|
1285
|
+
* console.log(`Effect triggered by:`, {
|
|
1286
|
+
* object: obj,
|
|
1287
|
+
* change: evolution.type,
|
|
1288
|
+
* property: prop
|
|
1289
|
+
* })
|
|
1290
|
+
* })
|
|
1291
|
+
*
|
|
1292
|
+
* // Access reactive properties
|
|
1293
|
+
* console.log(state.count, state.name)
|
|
1294
|
+
* })
|
|
1295
|
+
*
|
|
1296
|
+
* state.count = 5
|
|
1297
|
+
* // Logs: Effect triggered by: { object: state, change: 'set', property: 'count' }
|
|
1298
|
+
* ```
|
|
1299
|
+
*/
|
|
1300
|
+
function trackEffect(onTouch) {
|
|
1301
|
+
const activeEffect = getActiveEffect();
|
|
1302
|
+
if (!activeEffect)
|
|
1303
|
+
throw new Error('Not in an effect');
|
|
1304
|
+
if (!effectTrackers.has(activeEffect))
|
|
1305
|
+
effectTrackers.set(activeEffect, new Set([onTouch]));
|
|
1306
|
+
else
|
|
1307
|
+
effectTrackers.get(activeEffect).add(onTouch);
|
|
1308
|
+
}
|
|
1309
|
+
const effectTrackers = new WeakMap();
|
|
1310
|
+
const opaqueEffects = new WeakSet();
|
|
1311
|
+
// Dependency graph: tracks which effects trigger which other effects
|
|
1312
|
+
// Uses roots (Function) as keys for consistency
|
|
1313
|
+
const effectTriggers = new WeakMap();
|
|
1314
|
+
const effectTriggeredBy = new WeakMap();
|
|
1315
|
+
// Transitive closures: track all indirect relationships
|
|
1316
|
+
// causesClosure: for each effect, all effects that trigger it (directly or indirectly)
|
|
1317
|
+
// consequencesClosure: for each effect, all effects that it triggers (directly or indirectly)
|
|
1318
|
+
const causesClosure = new WeakMap();
|
|
1319
|
+
const consequencesClosure = new WeakMap();
|
|
1320
|
+
// Debug: Capture where an effect was created
|
|
1321
|
+
const effectCreationStacks = new WeakMap();
|
|
1322
|
+
/**
|
|
1323
|
+
* Gets or creates an IterableWeakSet for a closure map
|
|
1324
|
+
*/
|
|
1325
|
+
function getOrCreateClosure(closure, root) {
|
|
1326
|
+
let set = closure.get(root);
|
|
1327
|
+
if (!set) {
|
|
1328
|
+
set = new IterableWeakSet();
|
|
1329
|
+
closure.set(root, set);
|
|
1330
|
+
}
|
|
1331
|
+
return set;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Adds an edge to the dependency graph: callerRoot → targetRoot
|
|
1335
|
+
* Also maintains transitive closures
|
|
1336
|
+
* @param callerRoot - Root function of the effect that triggers
|
|
1337
|
+
* @param targetRoot - Root function of the effect being triggered
|
|
1338
|
+
*/
|
|
1339
|
+
function addGraphEdge(callerRoot, targetRoot) {
|
|
1340
|
+
// Skip if edge already exists
|
|
1341
|
+
const triggers = effectTriggers.get(callerRoot);
|
|
1342
|
+
if (triggers?.has(targetRoot)) {
|
|
1343
|
+
return; // Edge already exists
|
|
1344
|
+
}
|
|
1345
|
+
// Add to forward graph: callerRoot → targetRoot
|
|
1346
|
+
if (!triggers) {
|
|
1347
|
+
const newTriggers = new IterableWeakSet();
|
|
1348
|
+
newTriggers.add(targetRoot);
|
|
1349
|
+
effectTriggers.set(callerRoot, newTriggers);
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
triggers.add(targetRoot);
|
|
1353
|
+
}
|
|
1354
|
+
// Add to reverse graph: targetRoot ← callerRoot
|
|
1355
|
+
let triggeredBy = effectTriggeredBy.get(targetRoot);
|
|
1356
|
+
if (!triggeredBy) {
|
|
1357
|
+
triggeredBy = new IterableWeakSet();
|
|
1358
|
+
effectTriggeredBy.set(targetRoot, triggeredBy);
|
|
1359
|
+
}
|
|
1360
|
+
triggeredBy.add(callerRoot);
|
|
1361
|
+
// Update transitive closures
|
|
1362
|
+
// When U→V is added, we need to propagate the relationship:
|
|
1363
|
+
// 1. Add U to causesClosure(V) and V to consequencesClosure(U) (direct relationship)
|
|
1364
|
+
// 2. For each X in causesClosure(U): add V to consequencesClosure(X) and X to causesClosure(V)
|
|
1365
|
+
// 3. For each Y in consequencesClosure(V): add U to causesClosure(Y) and Y to consequencesClosure(U)
|
|
1366
|
+
// Note: Self-loops (U→U) are not added to closures - if an effect appears in its own closure,
|
|
1367
|
+
// it means there's an indirect cycle that should be detected
|
|
1368
|
+
// Self-loops are explicitly ignored - an effect reading and writing the same property
|
|
1369
|
+
// (e.g., obj.prop++) should not create a dependency relationship or appear in closures
|
|
1370
|
+
if (callerRoot === targetRoot) {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const uConsequences = getOrCreateClosure(consequencesClosure, callerRoot);
|
|
1374
|
+
const vCauses = getOrCreateClosure(causesClosure, targetRoot);
|
|
1375
|
+
// 1. Add direct relationship
|
|
1376
|
+
uConsequences.add(targetRoot);
|
|
1377
|
+
vCauses.add(callerRoot);
|
|
1378
|
+
// 2. For each X in causesClosure(U): X→U→V means X→V
|
|
1379
|
+
const uCausesSet = causesClosure.get(callerRoot);
|
|
1380
|
+
if (uCausesSet) {
|
|
1381
|
+
for (const x of uCausesSet) {
|
|
1382
|
+
// Skip if this would create a self-loop
|
|
1383
|
+
if (x === targetRoot)
|
|
1384
|
+
continue;
|
|
1385
|
+
const xConsequences = getOrCreateClosure(consequencesClosure, x);
|
|
1386
|
+
xConsequences.add(targetRoot);
|
|
1387
|
+
vCauses.add(x);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
// 3. For each Y in consequencesClosure(V): U→V→Y means U→Y
|
|
1391
|
+
const vConsequencesSet = consequencesClosure.get(targetRoot);
|
|
1392
|
+
if (vConsequencesSet) {
|
|
1393
|
+
for (const y of vConsequencesSet) {
|
|
1394
|
+
// Skip if this would create a self-loop
|
|
1395
|
+
if (y === callerRoot)
|
|
1396
|
+
continue;
|
|
1397
|
+
const yCauses = getOrCreateClosure(causesClosure, y);
|
|
1398
|
+
yCauses.add(callerRoot);
|
|
1399
|
+
uConsequences.add(y);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
// 4. Cross-product: for each X in causesClosure(U) and Y in consequencesClosure(V): X→Y
|
|
1403
|
+
if (uCausesSet && vConsequencesSet) {
|
|
1404
|
+
for (const x of uCausesSet) {
|
|
1405
|
+
const xConsequences = getOrCreateClosure(consequencesClosure, x);
|
|
1406
|
+
for (const y of vConsequencesSet) {
|
|
1407
|
+
// Skip if this would create a self-loop
|
|
1408
|
+
if (x === y)
|
|
1409
|
+
continue;
|
|
1410
|
+
xConsequences.add(y);
|
|
1411
|
+
const yCauses = getOrCreateClosure(causesClosure, y);
|
|
1412
|
+
yCauses.add(x);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Checks if there's a path from start to end in the dependency graph, excluding a specific node
|
|
1419
|
+
* Uses BFS to find any path that doesn't go through the excluded node
|
|
1420
|
+
* @param start - Starting node
|
|
1421
|
+
* @param end - Target node
|
|
1422
|
+
* @param exclude - Node to exclude from the path
|
|
1423
|
+
* @returns true if a path exists without going through the excluded node
|
|
1424
|
+
*/
|
|
1425
|
+
function hasPathExcluding(start, end, exclude) {
|
|
1426
|
+
if (start === end)
|
|
1427
|
+
return true;
|
|
1428
|
+
if (start === exclude)
|
|
1429
|
+
return false;
|
|
1430
|
+
const visited = new Set();
|
|
1431
|
+
const queue = [start];
|
|
1432
|
+
visited.add(start);
|
|
1433
|
+
visited.add(exclude); // Pre-mark excluded node as visited to skip it
|
|
1434
|
+
while (queue.length > 0) {
|
|
1435
|
+
const current = queue.shift();
|
|
1436
|
+
const triggers = effectTriggers.get(current);
|
|
1437
|
+
if (!triggers)
|
|
1438
|
+
continue;
|
|
1439
|
+
for (const next of triggers) {
|
|
1440
|
+
if (next === end)
|
|
1441
|
+
return true;
|
|
1442
|
+
if (!visited.has(next)) {
|
|
1443
|
+
visited.add(next);
|
|
1444
|
+
queue.push(next);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Removes all edges involving the given effect from the dependency graph
|
|
1452
|
+
* Also cleans up transitive closures by propagating cleanup to all affected effects
|
|
1453
|
+
* Called when an effect is stopped/cleaned up
|
|
1454
|
+
* @param effect - The effect being cleaned up
|
|
1455
|
+
*/
|
|
1456
|
+
function cleanupEffectFromGraph(effect) {
|
|
1457
|
+
const root = getRoot(effect);
|
|
1458
|
+
// Get closures before removing direct edges (needed for propagation)
|
|
1459
|
+
const rootCauses = causesClosure.get(root);
|
|
1460
|
+
const rootConsequences = consequencesClosure.get(root);
|
|
1461
|
+
// Remove from effectTriggers (outgoing edges)
|
|
1462
|
+
const triggers = effectTriggers.get(root);
|
|
1463
|
+
if (triggers) {
|
|
1464
|
+
// Remove this root from all targets' effectTriggeredBy sets
|
|
1465
|
+
for (const targetRoot of triggers) {
|
|
1466
|
+
const triggeredBy = effectTriggeredBy.get(targetRoot);
|
|
1467
|
+
triggeredBy?.delete(root);
|
|
1468
|
+
}
|
|
1469
|
+
effectTriggers.delete(root);
|
|
1470
|
+
}
|
|
1471
|
+
// Remove from effectTriggeredBy (incoming edges)
|
|
1472
|
+
const triggeredBy = effectTriggeredBy.get(root);
|
|
1473
|
+
if (triggeredBy) {
|
|
1474
|
+
// Remove this root from all sources' effectTriggers sets
|
|
1475
|
+
for (const sourceRoot of triggeredBy) {
|
|
1476
|
+
const triggers = effectTriggers.get(sourceRoot);
|
|
1477
|
+
triggers?.delete(root);
|
|
1478
|
+
}
|
|
1479
|
+
effectTriggeredBy.delete(root);
|
|
1480
|
+
}
|
|
1481
|
+
// Propagate closure cleanup to all affected effects
|
|
1482
|
+
// When removing B from A → B → C:
|
|
1483
|
+
// - Remove B from causesClosure(C) and consequencesClosure(A)
|
|
1484
|
+
// - For each X in causesClosure(B): remove C from consequencesClosure(X) if B was the only path
|
|
1485
|
+
// - For each Y in consequencesClosure(B): remove A from causesClosure(Y) if B was the only path
|
|
1486
|
+
// - Remove transitive relationships that depended on B
|
|
1487
|
+
if (rootCauses) {
|
|
1488
|
+
// For each X that triggers root: remove root from X's consequences
|
|
1489
|
+
// Only remove root's consequences if no alternate path exists
|
|
1490
|
+
for (const causeRoot of rootCauses) {
|
|
1491
|
+
const causeConsequences = consequencesClosure.get(causeRoot);
|
|
1492
|
+
if (causeConsequences) {
|
|
1493
|
+
// Remove root itself (it's being cleaned up)
|
|
1494
|
+
causeConsequences.delete(root);
|
|
1495
|
+
// Only remove consequences of root if there's no alternate path from causeRoot to them
|
|
1496
|
+
if (rootConsequences) {
|
|
1497
|
+
for (const consequence of rootConsequences) {
|
|
1498
|
+
// Check if causeRoot can still reach consequence without going through root
|
|
1499
|
+
if (!hasPathExcluding(causeRoot, consequence, root)) {
|
|
1500
|
+
causeConsequences.delete(consequence);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
if (rootConsequences) {
|
|
1508
|
+
// For each Y that root triggers: remove root from Y's causes
|
|
1509
|
+
// Only remove root's causes if no alternate path exists
|
|
1510
|
+
for (const consequenceRoot of rootConsequences) {
|
|
1511
|
+
const consequenceCauses = causesClosure.get(consequenceRoot);
|
|
1512
|
+
if (consequenceCauses) {
|
|
1513
|
+
// Remove root itself (it's being cleaned up)
|
|
1514
|
+
consequenceCauses.delete(root);
|
|
1515
|
+
// Only remove causes of root if there's no alternate path from them to consequenceRoot
|
|
1516
|
+
if (rootCauses) {
|
|
1517
|
+
for (const cause of rootCauses) {
|
|
1518
|
+
// Check if cause can still reach consequenceRoot without going through root
|
|
1519
|
+
if (!hasPathExcluding(cause, consequenceRoot, root)) {
|
|
1520
|
+
consequenceCauses.delete(cause);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
// Cross-product cleanup: for each X in causesClosure(B) and Y in consequencesClosure(B),
|
|
1528
|
+
// remove X→Y if B was the only path connecting them
|
|
1529
|
+
if (rootCauses && rootConsequences) {
|
|
1530
|
+
for (const x of rootCauses) {
|
|
1531
|
+
const xConsequences = consequencesClosure.get(x);
|
|
1532
|
+
if (xConsequences) {
|
|
1533
|
+
for (const y of rootConsequences) {
|
|
1534
|
+
// Check if there's still a path from X to Y without going through root
|
|
1535
|
+
// Use BFS to find any path that doesn't include root
|
|
1536
|
+
if (!hasPathExcluding(x, y, root)) {
|
|
1537
|
+
xConsequences.delete(y);
|
|
1538
|
+
const yCauses = causesClosure.get(y);
|
|
1539
|
+
yCauses?.delete(x);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
// Finally, delete the closures for this effect
|
|
1546
|
+
causesClosure.delete(root);
|
|
1547
|
+
consequencesClosure.delete(root);
|
|
1548
|
+
}
|
|
1549
|
+
// Track currently executing effects to prevent re-execution
|
|
1550
|
+
// These are all the effects triggered under `activeEffect`
|
|
1551
|
+
let batchQueue;
|
|
1552
|
+
const batchCleanups = new Set();
|
|
1553
|
+
/**
|
|
1554
|
+
* Computes and caches in-degrees for all effects in the batch
|
|
1555
|
+
* Called once when batch starts or when new effects are added
|
|
1556
|
+
*/
|
|
1557
|
+
function computeAllInDegrees(batch) {
|
|
1558
|
+
const activeEffect = getActiveEffect();
|
|
1559
|
+
const activeRoot = activeEffect ? getRoot(activeEffect) : null;
|
|
1560
|
+
// Reset all in-degrees
|
|
1561
|
+
batch.inDegrees.clear();
|
|
1562
|
+
for (const [root] of batch.all) {
|
|
1563
|
+
let inDegree = 0;
|
|
1564
|
+
const causes = causesClosure.get(root);
|
|
1565
|
+
if (causes) {
|
|
1566
|
+
for (const causeRoot of causes) {
|
|
1567
|
+
// Only count if it's in the batch and not the active/self effect
|
|
1568
|
+
if (batch.all.has(causeRoot) && causeRoot !== activeRoot && causeRoot !== root) {
|
|
1569
|
+
inDegree++;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
batch.inDegrees.set(root, inDegree);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Decrements in-degrees of all effects that depend on the executed effect
|
|
1578
|
+
* Called after an effect is executed to update the cached in-degrees
|
|
1579
|
+
*/
|
|
1580
|
+
function decrementInDegreesForExecuted(batch, executedRoot) {
|
|
1581
|
+
// Get all effects that this executed effect triggers
|
|
1582
|
+
const consequences = consequencesClosure.get(executedRoot);
|
|
1583
|
+
if (!consequences)
|
|
1584
|
+
return;
|
|
1585
|
+
for (const consequenceRoot of consequences) {
|
|
1586
|
+
// Only update if it's still in the batch
|
|
1587
|
+
if (batch.all.has(consequenceRoot)) {
|
|
1588
|
+
const currentDegree = batch.inDegrees.get(consequenceRoot) ?? 0;
|
|
1589
|
+
if (currentDegree > 0) {
|
|
1590
|
+
batch.inDegrees.set(consequenceRoot, currentDegree - 1);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Computes the in-degree (number of dependencies) for an effect in the current batch
|
|
1597
|
+
* Uses causesClosure to count all effects (directly or indirectly) that trigger this effect
|
|
1598
|
+
* @param root - Root function of the effect
|
|
1599
|
+
* @param batchEffects - Map of all effects in current batch (todos - effects that still need execution)
|
|
1600
|
+
* @returns Number of effects in batch that trigger this effect (directly or indirectly)
|
|
1601
|
+
*
|
|
1602
|
+
* TODO: Optimization - For large graphs with small batches, iterating over all causes in the closure
|
|
1603
|
+
* can be expensive. Consider maintaining a separate "batch causes" set or caching in-degrees.
|
|
1604
|
+
*/
|
|
1605
|
+
/* function computeInDegreeInBatch(
|
|
1606
|
+
root: Function,
|
|
1607
|
+
batchEffects: Map<Function, ScopedCallback>
|
|
1608
|
+
): number {
|
|
1609
|
+
let inDegree = 0
|
|
1610
|
+
const activeEffect = getActiveEffect()
|
|
1611
|
+
const activeRoot = activeEffect ? getRoot(activeEffect) : null
|
|
1612
|
+
|
|
1613
|
+
// Count effects in batch that trigger this effect (directly or indirectly)
|
|
1614
|
+
// Using causesClosure which contains all transitive causes
|
|
1615
|
+
// Note: batchEffects only contains effects that still need execution (todos),
|
|
1616
|
+
// so we don't need to check if causes have been executed - they're not in the map if executed
|
|
1617
|
+
const causes = causesClosure.get(root)
|
|
1618
|
+
if (causes) {
|
|
1619
|
+
for (const causeRoot of causes) {
|
|
1620
|
+
// Only count if it's in the batch (still needs execution)
|
|
1621
|
+
// BUT: don't count the currently executing effect (active effect)
|
|
1622
|
+
// This handles the case where an effect is triggered during another effect's execution
|
|
1623
|
+
// Note: Self-loops are ignored - they should not appear in closures, but we check to be safe
|
|
1624
|
+
if (batchEffects.has(causeRoot) && causeRoot !== activeRoot && causeRoot !== root) {
|
|
1625
|
+
inDegree++
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
return inDegree
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Finds a path from startRoot to endRoot in the dependency graph
|
|
1635
|
+
* Uses DFS to find the path through direct edges
|
|
1636
|
+
* @param startRoot - Starting effect root
|
|
1637
|
+
* @param endRoot - Target effect root
|
|
1638
|
+
* @param visited - Set of visited nodes (for recursion)
|
|
1639
|
+
* @param path - Current path being explored
|
|
1640
|
+
* @returns Path from startRoot to endRoot, or empty array if no path exists
|
|
1641
|
+
*/
|
|
1642
|
+
function findPath(startRoot, endRoot, visited = new Set(), path = []) {
|
|
1643
|
+
if (startRoot === endRoot) {
|
|
1644
|
+
return [...path, endRoot];
|
|
1645
|
+
}
|
|
1646
|
+
if (visited.has(startRoot)) {
|
|
1647
|
+
return [];
|
|
1648
|
+
}
|
|
1649
|
+
visited.add(startRoot);
|
|
1650
|
+
const newPath = [...path, startRoot];
|
|
1651
|
+
const triggers = effectTriggers.get(startRoot);
|
|
1652
|
+
if (triggers) {
|
|
1653
|
+
for (const targetRoot of triggers) {
|
|
1654
|
+
const result = findPath(targetRoot, endRoot, visited, newPath);
|
|
1655
|
+
if (result.length > 0) {
|
|
1656
|
+
return result;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return [];
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Gets the cycle path when adding an edge would create a cycle
|
|
1664
|
+
* @param callerRoot - Root of the effect that triggers
|
|
1665
|
+
* @param targetRoot - Root of the effect being triggered
|
|
1666
|
+
* @returns Array of effect roots forming the cycle, or empty array if no cycle
|
|
1667
|
+
*/
|
|
1668
|
+
function getCyclePathForEdge(callerRoot, targetRoot) {
|
|
1669
|
+
// Find path from targetRoot back to callerRoot (this is the existing path)
|
|
1670
|
+
// Then adding callerRoot -> targetRoot completes the cycle
|
|
1671
|
+
const path = findPath(targetRoot, callerRoot);
|
|
1672
|
+
if (path.length > 0) {
|
|
1673
|
+
// The cycle is: callerRoot -> targetRoot -> ... -> callerRoot
|
|
1674
|
+
return [callerRoot, ...path];
|
|
1675
|
+
}
|
|
1676
|
+
return [];
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Checks if adding an edge would create a cycle
|
|
1680
|
+
* Uses causesClosure to check if callerRoot is already a cause of targetRoot
|
|
1681
|
+
* Self-loops (callerRoot === targetRoot) are explicitly ignored and return false
|
|
1682
|
+
* @param callerRoot - Root of the effect that triggers
|
|
1683
|
+
* @param targetRoot - Root of the effect being triggered
|
|
1684
|
+
* @returns true if adding this edge would create a cycle
|
|
1685
|
+
*/
|
|
1686
|
+
function wouldCreateCycle(callerRoot, targetRoot) {
|
|
1687
|
+
// Self-loops are explicitly ignored - an effect reading and writing the same property
|
|
1688
|
+
// (e.g., obj.prop++) should not create a dependency relationship
|
|
1689
|
+
if (callerRoot === targetRoot) {
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
// Check if targetRoot already triggers callerRoot (directly or indirectly)
|
|
1693
|
+
// This would create a cycle: callerRoot -> targetRoot -> ... -> callerRoot
|
|
1694
|
+
// Using consequencesClosure: if targetRoot triggers callerRoot, then callerRoot is in consequencesClosure(targetRoot)
|
|
1695
|
+
const targetConsequences = consequencesClosure.get(targetRoot);
|
|
1696
|
+
if (targetConsequences?.has(callerRoot)) {
|
|
1697
|
+
return true; // Cycle detected: targetRoot -> ... -> callerRoot, and we're adding callerRoot -> targetRoot
|
|
1698
|
+
}
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Adds an effect to the batch queue
|
|
1703
|
+
* @param effect - The effect to add
|
|
1704
|
+
* @param caller - The active effect that triggered this one (optional)
|
|
1705
|
+
* @param immediate - If true, don't create edges in the dependency graph
|
|
1706
|
+
*/
|
|
1707
|
+
function addToBatch(effect, caller, immediate) {
|
|
1708
|
+
if (!batchQueue)
|
|
1709
|
+
return;
|
|
1710
|
+
const root = getRoot(effect);
|
|
1711
|
+
// 1. Add to batch first (needed for cycle detection)
|
|
1712
|
+
batchQueue.all.set(root, effect);
|
|
1713
|
+
// 2. Add to global graph (if caller exists and not immediate) - USE ROOTS ONLY
|
|
1714
|
+
// When immediate is true, don't create edges - the effect is not considered as a consequence
|
|
1715
|
+
if (caller && !immediate) {
|
|
1716
|
+
const callerRoot = getRoot(caller);
|
|
1717
|
+
// Check for cycle BEFORE adding edge
|
|
1718
|
+
// We check if adding callerRoot -> root would create a cycle
|
|
1719
|
+
// This means checking if root already triggers callerRoot (directly or transitively)
|
|
1720
|
+
if (wouldCreateCycle(callerRoot, root)) {
|
|
1721
|
+
// Cycle detected! Get the full cycle path for debugging
|
|
1722
|
+
const cyclePath = getCyclePathForEdge(callerRoot, root);
|
|
1723
|
+
const cycleMessage = cyclePath.length > 0
|
|
1724
|
+
? `Cycle detected: ${cyclePath.map((r) => r.name || r.toString()).join(' → ')}`
|
|
1725
|
+
: `Cycle detected: ${callerRoot.name || callerRoot.toString()} → ${root.name || root.toString()} (and back)`;
|
|
1726
|
+
const cycleHandling = options.cycleHandling;
|
|
1727
|
+
// In strict mode, we throw immediately on detection
|
|
1728
|
+
if (cycleHandling === 'strict') {
|
|
1729
|
+
batchQueue.all.delete(root);
|
|
1730
|
+
const causalChain = getTriggerChain(effect);
|
|
1731
|
+
const creationStack = effectCreationStacks.get(root);
|
|
1732
|
+
throw new ReactiveError(`[reactive] Strict Cycle Prevention: ${cycleMessage}`, {
|
|
1733
|
+
code: ReactiveErrorCode.CycleDetected,
|
|
1734
|
+
cycle: cyclePath.map((r) => r.name || r.toString()),
|
|
1735
|
+
details: cycleMessage,
|
|
1736
|
+
causalChain,
|
|
1737
|
+
creationStack,
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
switch (cycleHandling) {
|
|
1741
|
+
case 'throw': {
|
|
1742
|
+
// Remove from batch before throwing
|
|
1743
|
+
batchQueue.all.delete(root);
|
|
1744
|
+
const causalChain = getTriggerChain(effect);
|
|
1745
|
+
const creationStack = effectCreationStacks.get(root);
|
|
1746
|
+
throw new ReactiveError(`[reactive] ${cycleMessage}`, {
|
|
1747
|
+
code: ReactiveErrorCode.CycleDetected,
|
|
1748
|
+
cycle: cyclePath.map((r) => r.name || r.toString()),
|
|
1749
|
+
details: cycleMessage,
|
|
1750
|
+
causalChain,
|
|
1751
|
+
creationStack,
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
case 'warn':
|
|
1755
|
+
options.warn(`[reactive] ${cycleMessage}`);
|
|
1756
|
+
// Don't add the edge, break the cycle
|
|
1757
|
+
batchQueue.all.delete(root);
|
|
1758
|
+
return;
|
|
1759
|
+
case 'break':
|
|
1760
|
+
// Silently break cycle, don't add the edge
|
|
1761
|
+
batchQueue.all.delete(root);
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
addGraphEdge(callerRoot, root); // Add to persistent graph using roots
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Adds a cleanup function to be called when the current batch of effects completes
|
|
1770
|
+
* @param cleanup - The cleanup function to add
|
|
1771
|
+
*/
|
|
1772
|
+
function addBatchCleanup(cleanup) {
|
|
1773
|
+
if (!batchQueue)
|
|
1774
|
+
cleanup();
|
|
1775
|
+
else
|
|
1776
|
+
batchCleanups.add(cleanup);
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Semantic alias for `addBatchCleanup` - defers work to the end of the current reactive batch.
|
|
1780
|
+
*
|
|
1781
|
+
* Use this when an effect needs to perform an action that would modify state the effect depends on,
|
|
1782
|
+
* which would create a reactive cycle. The deferred callback runs after all effects complete.
|
|
1783
|
+
*
|
|
1784
|
+
* @param callback - The callback to defer until after the current batch completes
|
|
1785
|
+
*
|
|
1786
|
+
* @example
|
|
1787
|
+
* ```typescript
|
|
1788
|
+
* effect(() => {
|
|
1789
|
+
* processData()
|
|
1790
|
+
*
|
|
1791
|
+
* // Defer to avoid cycle (createMovement modifies state this effect reads)
|
|
1792
|
+
* defer(() => {
|
|
1793
|
+
* createMovement(data)
|
|
1794
|
+
* })
|
|
1795
|
+
* })
|
|
1796
|
+
* ```
|
|
1797
|
+
*/
|
|
1798
|
+
const defer = addBatchCleanup;
|
|
1799
|
+
/**
|
|
1800
|
+
* Gets a cycle path for debugging
|
|
1801
|
+
* Uses DFS to find cycles in the batch
|
|
1802
|
+
* @param batchQueue - The batch queue
|
|
1803
|
+
* @returns Array of effect roots forming a cycle
|
|
1804
|
+
*/
|
|
1805
|
+
function getCyclePath(batchQueue) {
|
|
1806
|
+
// If all effects have in-degree > 0, there must be a cycle
|
|
1807
|
+
// Use DFS to find it
|
|
1808
|
+
const visited = new Set();
|
|
1809
|
+
const recursionStack = new Set();
|
|
1810
|
+
const path = [];
|
|
1811
|
+
for (const [root] of batchQueue.all) {
|
|
1812
|
+
if (visited.has(root))
|
|
1813
|
+
continue;
|
|
1814
|
+
const cycle = findCycle(root, visited, recursionStack, path, batchQueue);
|
|
1815
|
+
if (cycle.length > 0) {
|
|
1816
|
+
return cycle;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return [];
|
|
1820
|
+
}
|
|
1821
|
+
function findCycle(root, visited, recursionStack, path, batchQueue) {
|
|
1822
|
+
if (recursionStack.has(root)) {
|
|
1823
|
+
// Found a cycle! Return the path from the cycle start to root
|
|
1824
|
+
const cycleStart = path.indexOf(root);
|
|
1825
|
+
return path.slice(cycleStart).concat([root]);
|
|
1826
|
+
}
|
|
1827
|
+
if (visited.has(root)) {
|
|
1828
|
+
return [];
|
|
1829
|
+
}
|
|
1830
|
+
visited.add(root);
|
|
1831
|
+
recursionStack.add(root);
|
|
1832
|
+
path.push(root);
|
|
1833
|
+
// Follow edges to effects in the batch
|
|
1834
|
+
// Use direct edges (effectTriggers) for cycle detection
|
|
1835
|
+
const triggers = effectTriggers.get(root);
|
|
1836
|
+
if (triggers) {
|
|
1837
|
+
for (const targetRoot of triggers) {
|
|
1838
|
+
if (batchQueue.all.has(targetRoot)) {
|
|
1839
|
+
const cycle = findCycle(targetRoot, visited, recursionStack, path, batchQueue);
|
|
1840
|
+
if (cycle.length > 0) {
|
|
1841
|
+
return cycle;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
path.pop();
|
|
1847
|
+
recursionStack.delete(root);
|
|
1848
|
+
return [];
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Executes the next effect in dependency order (using cached in-degrees)
|
|
1852
|
+
* Finds an effect with in-degree 0 and executes it
|
|
1853
|
+
* @returns The return value of the executed effect, or null if batch is complete
|
|
1854
|
+
*/
|
|
1855
|
+
function executeNext(effectuatedRoots) {
|
|
1856
|
+
// Find an effect with in-degree 0 using cached values
|
|
1857
|
+
let nextEffect = null;
|
|
1858
|
+
let nextRoot = null;
|
|
1859
|
+
// Find an effect with in-degree 0 (no dependencies in batch that still need execution)
|
|
1860
|
+
// Using cached in-degrees for O(n) lookup instead of O(n²)
|
|
1861
|
+
for (const [root, effect] of batchQueue.all) {
|
|
1862
|
+
const inDegree = batchQueue.inDegrees.get(root) ?? 0;
|
|
1863
|
+
if (inDegree === 0) {
|
|
1864
|
+
nextEffect = effect;
|
|
1865
|
+
nextRoot = root;
|
|
1866
|
+
break;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (!nextEffect) {
|
|
1870
|
+
// No effect with in-degree 0 - there must be a cycle
|
|
1871
|
+
// If all effects have dependencies, it means there's a circular dependency
|
|
1872
|
+
if (batchQueue.all.size > 0) {
|
|
1873
|
+
let cycle = getCyclePath(batchQueue);
|
|
1874
|
+
// If we couldn't find a cycle path using direct edges, try using closures
|
|
1875
|
+
// (transitive relationships) - if all effects have in-degree > 0, there must be a cycle
|
|
1876
|
+
if (cycle.length === 0) {
|
|
1877
|
+
// Try to find a cycle using consequencesClosure (transitive relationships)
|
|
1878
|
+
// Note: Self-loops are ignored - we only look for cycles between different effects
|
|
1879
|
+
for (const [root] of batchQueue.all) {
|
|
1880
|
+
const consequences = consequencesClosure.get(root);
|
|
1881
|
+
if (consequences) {
|
|
1882
|
+
// Check if any consequence in the batch also has root as a consequence
|
|
1883
|
+
for (const consequence of consequences) {
|
|
1884
|
+
// Skip self-loops - they are ignored
|
|
1885
|
+
if (consequence === root)
|
|
1886
|
+
continue;
|
|
1887
|
+
if (batchQueue.all.has(consequence)) {
|
|
1888
|
+
const consequenceConsequences = consequencesClosure.get(consequence);
|
|
1889
|
+
if (consequenceConsequences?.has(root)) {
|
|
1890
|
+
// Found cycle: root -> consequence -> root
|
|
1891
|
+
cycle = [root, consequence, root];
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
if (cycle.length > 0)
|
|
1897
|
+
break;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
const cycleMessage = cycle.length > 0
|
|
1902
|
+
? `Cycle detected: ${cycle.map((r) => r.name || '<anonymous>').join(' → ')}`
|
|
1903
|
+
: 'Cycle detected in effect batch - all effects have dependencies that prevent execution';
|
|
1904
|
+
const cycleHandling = options.cycleHandling;
|
|
1905
|
+
switch (cycleHandling) {
|
|
1906
|
+
case 'throw':
|
|
1907
|
+
throw new ReactiveError(`[reactive] ${cycleMessage}`);
|
|
1908
|
+
case 'warn': {
|
|
1909
|
+
options.warn(`[reactive] ${cycleMessage}`);
|
|
1910
|
+
// Break the cycle by executing one effect anyway
|
|
1911
|
+
const firstEffect = batchQueue.all.values().next().value;
|
|
1912
|
+
if (firstEffect) {
|
|
1913
|
+
const firstRoot = getRoot(firstEffect);
|
|
1914
|
+
batchQueue.all.delete(firstRoot);
|
|
1915
|
+
batchQueue.inDegrees.delete(firstRoot);
|
|
1916
|
+
return firstEffect();
|
|
1917
|
+
}
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
case 'break': {
|
|
1921
|
+
// Silently break cycle
|
|
1922
|
+
const firstEffect2 = batchQueue.all.values().next().value;
|
|
1923
|
+
if (firstEffect2) {
|
|
1924
|
+
const firstRoot2 = getRoot(firstEffect2);
|
|
1925
|
+
batchQueue.all.delete(firstRoot2);
|
|
1926
|
+
batchQueue.inDegrees.delete(firstRoot2);
|
|
1927
|
+
return firstEffect2();
|
|
1928
|
+
}
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
return null; // Batch complete
|
|
1934
|
+
}
|
|
1935
|
+
effectuatedRoots.push(getRoot(nextEffect));
|
|
1936
|
+
// Execute the effect
|
|
1937
|
+
const result = nextEffect();
|
|
1938
|
+
// Remove from batch and update in-degrees of dependents
|
|
1939
|
+
batchQueue.all.delete(nextRoot);
|
|
1940
|
+
batchQueue.inDegrees.delete(nextRoot);
|
|
1941
|
+
decrementInDegreesForExecuted(batchQueue, nextRoot);
|
|
1942
|
+
return result;
|
|
1943
|
+
}
|
|
1944
|
+
// Track which sub-effects have been executed to prevent infinite loops
|
|
1945
|
+
// These are all the effects triggered under `activeEffect` and all their sub-effects
|
|
1946
|
+
function batch(effect, immediate) {
|
|
1947
|
+
if (!Array.isArray(effect))
|
|
1948
|
+
effect = [effect];
|
|
1949
|
+
const roots = effect.map(getRoot);
|
|
1950
|
+
if (batchQueue) {
|
|
1951
|
+
// Nested batch - add to existing
|
|
1952
|
+
options?.chain(roots, getRoot(getActiveEffect()));
|
|
1953
|
+
const caller = getActiveEffect();
|
|
1954
|
+
for (let i = 0; i < effect.length; i++) {
|
|
1955
|
+
addToBatch(effect[i], caller, immediate === 'immediate');
|
|
1956
|
+
}
|
|
1957
|
+
if (immediate) {
|
|
1958
|
+
// Execute immediately (before batch returns)
|
|
1959
|
+
for (let i = 0; i < effect.length; i++) {
|
|
1960
|
+
try {
|
|
1961
|
+
effect[i]();
|
|
1962
|
+
}
|
|
1963
|
+
finally {
|
|
1964
|
+
const root = getRoot(effect[i]);
|
|
1965
|
+
batchQueue.all.delete(root);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
// Otherwise, effects will be picked up in next executeNext() call
|
|
1970
|
+
}
|
|
1971
|
+
else {
|
|
1972
|
+
// New batch - initialize
|
|
1973
|
+
options.beginChain(roots);
|
|
1974
|
+
batchQueue = {
|
|
1975
|
+
all: new Map(),
|
|
1976
|
+
inDegrees: new Map(),
|
|
1977
|
+
};
|
|
1978
|
+
// Add initial effects
|
|
1979
|
+
const caller = getActiveEffect();
|
|
1980
|
+
for (let i = 0; i < effect.length; i++) {
|
|
1981
|
+
addToBatch(effect[i], caller, immediate === 'immediate');
|
|
1982
|
+
}
|
|
1983
|
+
const effectuatedRoots = [];
|
|
1984
|
+
computeAllInDegrees(batchQueue);
|
|
1985
|
+
if (immediate) {
|
|
1986
|
+
// Execute immediately (before batch returns)
|
|
1987
|
+
const firstReturn = {};
|
|
1988
|
+
try {
|
|
1989
|
+
for (let i = 0; i < effect.length; i++) {
|
|
1990
|
+
try {
|
|
1991
|
+
const rv = effect[i]();
|
|
1992
|
+
if (rv !== undefined && !('value' in firstReturn))
|
|
1993
|
+
firstReturn.value = rv;
|
|
1994
|
+
}
|
|
1995
|
+
finally {
|
|
1996
|
+
const root = getRoot(effect[i]);
|
|
1997
|
+
batchQueue.all.delete(root);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
// After immediate execution, execute any effects that were triggered during execution
|
|
2001
|
+
// This is important for @atomic decorator - effects triggered inside should still run
|
|
2002
|
+
while (batchQueue.all.size > 0) {
|
|
2003
|
+
if (effectuatedRoots.length > options.maxEffectChain) {
|
|
2004
|
+
const cycle = findCycleInChain(effectuatedRoots);
|
|
2005
|
+
const trace = formatRoots(effectuatedRoots);
|
|
2006
|
+
const message = cycle
|
|
2007
|
+
? `Max effect chain reached (cycle detected: ${formatRoots(cycle)})`
|
|
2008
|
+
: `Max effect chain reached (trace: ${trace})`;
|
|
2009
|
+
const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : [];
|
|
2010
|
+
const queued = queuedRoots.map((r) => r.name || '<anonymous>');
|
|
2011
|
+
const debugInfo = {
|
|
2012
|
+
code: ReactiveErrorCode.MaxDepthExceeded,
|
|
2013
|
+
effectuatedRoots,
|
|
2014
|
+
cycle,
|
|
2015
|
+
trace,
|
|
2016
|
+
maxEffectChain: options.maxEffectChain,
|
|
2017
|
+
queued: queued.slice(0, 50),
|
|
2018
|
+
queuedCount: queued.length,
|
|
2019
|
+
// Try to get causation for the last effect
|
|
2020
|
+
causalChain: effectuatedRoots.length > 0
|
|
2021
|
+
? getTriggerChain(batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1]))
|
|
2022
|
+
: [],
|
|
2023
|
+
};
|
|
2024
|
+
switch (options.maxEffectReaction) {
|
|
2025
|
+
case 'throw':
|
|
2026
|
+
throw new ReactiveError(`[reactive] ${message}`, debugInfo);
|
|
2027
|
+
case 'debug':
|
|
2028
|
+
// biome-ignore lint/suspicious/noDebugger: This is the whole point here
|
|
2029
|
+
debugger;
|
|
2030
|
+
throw new ReactiveError(`[reactive] ${message}`, debugInfo);
|
|
2031
|
+
case 'warn':
|
|
2032
|
+
options.warn(`[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`);
|
|
2033
|
+
break;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (!batchQueue || batchQueue.all.size === 0)
|
|
2037
|
+
break;
|
|
2038
|
+
const rv = executeNext(effectuatedRoots);
|
|
2039
|
+
// If executeNext() returned null but batch is not empty, it means a cycle was detected
|
|
2040
|
+
// and an error was thrown, so we won't reach here
|
|
2041
|
+
if (rv !== undefined && !('value' in firstReturn))
|
|
2042
|
+
firstReturn.value = rv;
|
|
2043
|
+
}
|
|
2044
|
+
const cleanups = Array.from(batchCleanups);
|
|
2045
|
+
batchCleanups.clear();
|
|
2046
|
+
for (const cleanup of cleanups)
|
|
2047
|
+
cleanup();
|
|
2048
|
+
return firstReturn.value;
|
|
2049
|
+
}
|
|
2050
|
+
finally {
|
|
2051
|
+
batchQueue = undefined;
|
|
2052
|
+
options.endChain();
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
else {
|
|
2056
|
+
// Execute in dependency order
|
|
2057
|
+
const firstReturn = {};
|
|
2058
|
+
try {
|
|
2059
|
+
while (batchQueue.all.size > 0) {
|
|
2060
|
+
if (effectuatedRoots.length > options.maxEffectChain) {
|
|
2061
|
+
const cycle = findCycleInChain(effectuatedRoots);
|
|
2062
|
+
const trace = formatRoots(effectuatedRoots);
|
|
2063
|
+
const message = cycle
|
|
2064
|
+
? `Max effect chain reached (cycle detected: ${formatRoots(cycle)})`
|
|
2065
|
+
: `Max effect chain reached (trace: ${trace})`;
|
|
2066
|
+
const queuedRoots = batchQueue ? Array.from(batchQueue.all.keys()) : [];
|
|
2067
|
+
const queued = queuedRoots.map((r) => r.name || '<anonymous>');
|
|
2068
|
+
const debugInfo = {
|
|
2069
|
+
code: ReactiveErrorCode.MaxDepthExceeded,
|
|
2070
|
+
effectuatedRoots,
|
|
2071
|
+
cycle,
|
|
2072
|
+
trace,
|
|
2073
|
+
maxEffectChain: options.maxEffectChain,
|
|
2074
|
+
queued: queued.slice(0, 50),
|
|
2075
|
+
queuedCount: queued.length,
|
|
2076
|
+
// Try to get causation for the last effect
|
|
2077
|
+
causalChain: effectuatedRoots.length > 0
|
|
2078
|
+
? getTriggerChain(batchQueue.all.get(effectuatedRoots[effectuatedRoots.length - 1]))
|
|
2079
|
+
: [],
|
|
2080
|
+
};
|
|
2081
|
+
switch (options.maxEffectReaction) {
|
|
2082
|
+
case 'throw':
|
|
2083
|
+
throw new ReactiveError(`[reactive] ${message}`, debugInfo);
|
|
2084
|
+
case 'debug':
|
|
2085
|
+
// biome-ignore lint/suspicious/noDebugger: This is the whole point here
|
|
2086
|
+
debugger;
|
|
2087
|
+
throw new ReactiveError(`[reactive] ${message}`, debugInfo);
|
|
2088
|
+
case 'warn':
|
|
2089
|
+
options.warn(`[reactive] ${message} (queued: ${queued.slice(0, 10).join(', ')}${queued.length > 10 ? ', …' : ''})`);
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
const rv = executeNext(effectuatedRoots);
|
|
2094
|
+
// executeNext() returns null when batch is complete or cycle detected (throws error)
|
|
2095
|
+
// But functions can legitimately return null, so we check batchQueue.all.size instead
|
|
2096
|
+
if (batchQueue.all.size === 0) {
|
|
2097
|
+
// Batch complete
|
|
2098
|
+
break;
|
|
2099
|
+
}
|
|
2100
|
+
// If executeNext() returned null but batch is not empty, it means a cycle was detected
|
|
2101
|
+
// and an error was thrown, so we won't reach here
|
|
2102
|
+
if (rv !== undefined && !('value' in firstReturn))
|
|
2103
|
+
firstReturn.value = rv;
|
|
2104
|
+
// Note: executeNext() already removed it from batchQueue, so we track by count
|
|
2105
|
+
}
|
|
2106
|
+
const cleanups = Array.from(batchCleanups);
|
|
2107
|
+
batchCleanups.clear();
|
|
2108
|
+
for (const cleanup of cleanups)
|
|
2109
|
+
cleanup();
|
|
2110
|
+
return firstReturn.value;
|
|
2111
|
+
}
|
|
2112
|
+
finally {
|
|
2113
|
+
batchQueue = undefined;
|
|
2114
|
+
options.endChain();
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Decorator that makes methods atomic - batches all effects triggered within the method
|
|
2121
|
+
*/
|
|
2122
|
+
const atomic = decorator.decorator({
|
|
2123
|
+
method(original) {
|
|
2124
|
+
return function (...args) {
|
|
2125
|
+
return batch(markWithRoot(() => original.apply(this, args), original), 'immediate');
|
|
2126
|
+
};
|
|
2127
|
+
},
|
|
2128
|
+
default(original) {
|
|
2129
|
+
return function (...args) {
|
|
2130
|
+
return batch(markWithRoot(() => original.apply(this, args), original), 'immediate');
|
|
2131
|
+
};
|
|
2132
|
+
},
|
|
2133
|
+
});
|
|
2134
|
+
const fr = new FinalizationRegistry((f) => f());
|
|
2135
|
+
/**
|
|
2136
|
+
* @param fn - The effect function to run - provides the cleaner
|
|
2137
|
+
* @returns The cleanup function
|
|
2138
|
+
*/
|
|
2139
|
+
/**
|
|
2140
|
+
* Creates a reactive effect that automatically re-runs when dependencies change
|
|
2141
|
+
* @param fn - The effect function that provides dependencies and may return a cleanup function or Promise
|
|
2142
|
+
* @param options - Options for effect execution
|
|
2143
|
+
* @returns A cleanup function to stop the effect
|
|
2144
|
+
*/
|
|
2145
|
+
function effect(
|
|
2146
|
+
//biome-ignore lint/suspicious/noConfusingVoidType: We have to
|
|
2147
|
+
fn, effectOptions) {
|
|
2148
|
+
// Ensure zone is hooked if asyncZone option is enabled (lazy initialization)
|
|
2149
|
+
// Inject batch function to allow atomic game loops in requestAnimationFrame
|
|
2150
|
+
ensureZoneHooked(batch);
|
|
2151
|
+
// Use per-effect asyncMode or fall back to global option
|
|
2152
|
+
const asyncMode = effectOptions?.asyncMode ?? options.asyncMode ?? 'cancel';
|
|
2153
|
+
if (options.introspection.enableHistory) {
|
|
2154
|
+
const stack = new Error().stack;
|
|
2155
|
+
if (stack) {
|
|
2156
|
+
// Clean up the stack trace to remove internal frames
|
|
2157
|
+
const cleanStack = stack.split('\n').slice(2).join('\n');
|
|
2158
|
+
effectCreationStacks.set(getRoot(fn), cleanStack);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
let cleanup = null;
|
|
2162
|
+
// capture the parent effect at creation time for ascend
|
|
2163
|
+
const parentsForAscend = captureEffectStack();
|
|
2164
|
+
const tracked = markWithRoot((cb) => withEffect(runEffect, cb), fn);
|
|
2165
|
+
const ascend = (cb) => withEffectStack(parentsForAscend, cb);
|
|
2166
|
+
let effectStopped = false;
|
|
2167
|
+
let hasReacted = false;
|
|
2168
|
+
let runningPromise = null;
|
|
2169
|
+
let cancelPrevious = null;
|
|
2170
|
+
function runEffect() {
|
|
2171
|
+
// Clear previous dependencies
|
|
2172
|
+
if (cleanup) {
|
|
2173
|
+
const prevCleanup = cleanup;
|
|
2174
|
+
cleanup = null;
|
|
2175
|
+
withEffect(undefined, () => prevCleanup());
|
|
2176
|
+
}
|
|
2177
|
+
// Handle async modes when effect is retriggered
|
|
2178
|
+
if (runningPromise) {
|
|
2179
|
+
if (asyncMode === 'cancel' && cancelPrevious) {
|
|
2180
|
+
// Cancel previous execution
|
|
2181
|
+
cancelPrevious();
|
|
2182
|
+
cancelPrevious = null;
|
|
2183
|
+
runningPromise = null;
|
|
2184
|
+
}
|
|
2185
|
+
else if (asyncMode === 'ignore') {
|
|
2186
|
+
// Ignore new execution while async work is running
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
// Note: 'queue' mode not yet implemented
|
|
2190
|
+
}
|
|
2191
|
+
// The effect has been stopped after having been planned
|
|
2192
|
+
if (effectStopped)
|
|
2193
|
+
return;
|
|
2194
|
+
options.enter(getRoot(fn));
|
|
2195
|
+
let reactionCleanup;
|
|
2196
|
+
let result;
|
|
2197
|
+
try {
|
|
2198
|
+
result = withEffect(runEffect, () => fn({ tracked, ascend, reaction: hasReacted }));
|
|
2199
|
+
if (result &&
|
|
2200
|
+
typeof result !== 'function' &&
|
|
2201
|
+
(typeof result !== 'object' || !('then' in result)))
|
|
2202
|
+
throw new ReactiveError(`[reactive] Effect returned a non-function value: ${result}`);
|
|
2203
|
+
// Check if result is a Promise (async effect)
|
|
2204
|
+
if (result && typeof result === 'object' && typeof result.then === 'function') {
|
|
2205
|
+
const originalPromise = result;
|
|
2206
|
+
// Create a cancellation promise that we can reject
|
|
2207
|
+
let cancelReject = null;
|
|
2208
|
+
const cancelPromise = new Promise((_, reject) => {
|
|
2209
|
+
cancelReject = reject;
|
|
2210
|
+
});
|
|
2211
|
+
const cancelError = new ReactiveError('[reactive] Effect canceled due to dependency change');
|
|
2212
|
+
// Race between the actual promise and cancellation
|
|
2213
|
+
// If canceled, the race rejects, which will propagate through any promise chain
|
|
2214
|
+
runningPromise = Promise.race([originalPromise, cancelPromise]);
|
|
2215
|
+
// Store the cancellation function
|
|
2216
|
+
cancelPrevious = () => {
|
|
2217
|
+
if (cancelReject) {
|
|
2218
|
+
cancelReject(cancelError);
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
// Wrap the original promise chain so cancellation propagates
|
|
2222
|
+
// This ensures that when we cancel, the original promise's .catch() handlers are triggered
|
|
2223
|
+
// We do this by rejecting the race promise, which makes the original promise chain see the rejection
|
|
2224
|
+
// through the zone-wrapped .then()/.catch() handlers
|
|
2225
|
+
}
|
|
2226
|
+
else {
|
|
2227
|
+
// Synchronous result - treat as cleanup function
|
|
2228
|
+
reactionCleanup = result;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
finally {
|
|
2232
|
+
hasReacted = true;
|
|
2233
|
+
options.leave(fn);
|
|
2234
|
+
}
|
|
2235
|
+
// Create cleanup function for next run
|
|
2236
|
+
cleanup = () => {
|
|
2237
|
+
cleanup = null;
|
|
2238
|
+
reactionCleanup?.();
|
|
2239
|
+
// Remove this effect from all reactive objects it's watching
|
|
2240
|
+
const effectObjects = effectToReactiveObjects.get(runEffect);
|
|
2241
|
+
if (effectObjects) {
|
|
2242
|
+
for (const reactiveObj of effectObjects) {
|
|
2243
|
+
const objectWatchers = watchers.get(reactiveObj);
|
|
2244
|
+
if (objectWatchers) {
|
|
2245
|
+
for (const [prop, deps] of objectWatchers.entries()) {
|
|
2246
|
+
deps.delete(runEffect);
|
|
2247
|
+
if (deps.size === 0) {
|
|
2248
|
+
objectWatchers.delete(prop);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
if (objectWatchers.size === 0) {
|
|
2252
|
+
watchers.delete(reactiveObj);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
effectToReactiveObjects.delete(runEffect);
|
|
2257
|
+
}
|
|
2258
|
+
// Invoke all child stops (recursive via subEffectCleanup calling its own mainCleanup)
|
|
2259
|
+
const children = effectChildren.get(runEffect);
|
|
2260
|
+
if (children) {
|
|
2261
|
+
for (const childCleanup of children)
|
|
2262
|
+
childCleanup();
|
|
2263
|
+
effectChildren.delete(runEffect);
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
// Mark the runEffect callback with the original function as its root
|
|
2268
|
+
markWithRoot(runEffect, fn);
|
|
2269
|
+
// Register strict mode if enabled
|
|
2270
|
+
if (effectOptions?.opaque) {
|
|
2271
|
+
opaqueEffects.add(runEffect);
|
|
2272
|
+
}
|
|
2273
|
+
if (isDevtoolsEnabled()) {
|
|
2274
|
+
registerEffectForDebug(runEffect);
|
|
2275
|
+
}
|
|
2276
|
+
batch(runEffect, 'immediate');
|
|
2277
|
+
const parent = parentsForAscend[0];
|
|
2278
|
+
// Store parent relationship for hierarchy traversal
|
|
2279
|
+
effectParent.set(runEffect, parent);
|
|
2280
|
+
// Only ROOT effects are registered for GC cleanup and zone tracking
|
|
2281
|
+
const isRootEffect = !parent;
|
|
2282
|
+
const stopEffect = () => {
|
|
2283
|
+
if (effectStopped)
|
|
2284
|
+
return;
|
|
2285
|
+
effectStopped = true;
|
|
2286
|
+
// Cancel any running async work
|
|
2287
|
+
if (cancelPrevious) {
|
|
2288
|
+
cancelPrevious();
|
|
2289
|
+
cancelPrevious = null;
|
|
2290
|
+
runningPromise = null;
|
|
2291
|
+
}
|
|
2292
|
+
cleanup?.();
|
|
2293
|
+
// Clean up dependency graph edges
|
|
2294
|
+
cleanupEffectFromGraph(runEffect);
|
|
2295
|
+
fr.unregister(stopEffect);
|
|
2296
|
+
};
|
|
2297
|
+
if (isRootEffect) {
|
|
2298
|
+
const callIfCollected = () => stopEffect();
|
|
2299
|
+
fr.register(callIfCollected, () => {
|
|
2300
|
+
stopEffect();
|
|
2301
|
+
options.garbageCollected(fn);
|
|
2302
|
+
}, stopEffect);
|
|
2303
|
+
return callIfCollected;
|
|
2304
|
+
}
|
|
2305
|
+
// Register this effect to be stopped when the parent effect is cleaned up
|
|
2306
|
+
let children = effectChildren.get(parent);
|
|
2307
|
+
if (!children) {
|
|
2308
|
+
children = new Set();
|
|
2309
|
+
effectChildren.set(parent, children);
|
|
2310
|
+
}
|
|
2311
|
+
const subEffectCleanup = () => {
|
|
2312
|
+
children.delete(subEffectCleanup);
|
|
2313
|
+
if (children.size === 0) {
|
|
2314
|
+
effectChildren.delete(parent);
|
|
2315
|
+
}
|
|
2316
|
+
// Execute this child effect cleanup (which triggers its own mainCleanup)
|
|
2317
|
+
stopEffect();
|
|
2318
|
+
};
|
|
2319
|
+
children.add(subEffectCleanup);
|
|
2320
|
+
return subEffectCleanup;
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Executes a function without tracking dependencies but maintains parent cleanup relationship
|
|
2324
|
+
* Effects created inside will still be cleaned up when the parent effect is destroyed
|
|
2325
|
+
* @param fn - The function to execute
|
|
2326
|
+
*/
|
|
2327
|
+
function untracked(fn) {
|
|
2328
|
+
// Store current tracking state and temporarily disable it
|
|
2329
|
+
// This prevents the parent effect from tracking dependencies during fn execution
|
|
2330
|
+
const wasTrackingDisabled = getTrackingDisabled();
|
|
2331
|
+
setTrackingDisabled(true);
|
|
2332
|
+
try {
|
|
2333
|
+
return fn();
|
|
2334
|
+
}
|
|
2335
|
+
finally {
|
|
2336
|
+
// Restore tracking state
|
|
2337
|
+
setTrackingDisabled(wasTrackingDisabled);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Executes a function from a virgin/root context - no parent effect, no tracking
|
|
2342
|
+
* Creates completely independent effects that won't be cleaned up by any parent
|
|
2343
|
+
* @param fn - The function to execute
|
|
2344
|
+
*/
|
|
2345
|
+
function root(fn) {
|
|
2346
|
+
let rv;
|
|
2347
|
+
withEffect(undefined, () => {
|
|
2348
|
+
rv = fn();
|
|
2349
|
+
});
|
|
2350
|
+
return rv;
|
|
2351
|
+
}
|
|
2352
|
+
function biDi(received, get, set) {
|
|
2353
|
+
if (typeof get !== 'function') {
|
|
2354
|
+
set = get.set;
|
|
2355
|
+
get = get.get;
|
|
2356
|
+
}
|
|
2357
|
+
const root = getRoot(received);
|
|
2358
|
+
effect(markWithRoot(() => {
|
|
2359
|
+
received(get());
|
|
2360
|
+
}, root));
|
|
2361
|
+
return atomic((value) => {
|
|
2362
|
+
set(value);
|
|
2363
|
+
if (batchQueue?.all.has(root)) {
|
|
2364
|
+
// Remove the effect from the batch queue so it doesn't execute
|
|
2365
|
+
// This prevents circular updates in bidirectional bindings
|
|
2366
|
+
batchQueue.all.delete(root);
|
|
2367
|
+
}
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// Track which objects contain which other objects (back-references)
|
|
2372
|
+
const objectParents = new WeakMap();
|
|
2373
|
+
// Track which objects have deep watchers
|
|
2374
|
+
const objectsWithDeepWatchers = new WeakSet();
|
|
2375
|
+
// Track deep watchers per object
|
|
2376
|
+
const deepWatchers = new WeakMap();
|
|
2377
|
+
// Track which effects are doing deep watching
|
|
2378
|
+
const effectToDeepWatchedObjects = new WeakMap();
|
|
2379
|
+
/**
|
|
2380
|
+
* Add a back-reference from child to parent
|
|
2381
|
+
*/
|
|
2382
|
+
function addBackReference(child, parent, prop) {
|
|
2383
|
+
let parents = objectParents.get(child);
|
|
2384
|
+
if (!parents) {
|
|
2385
|
+
parents = new Set();
|
|
2386
|
+
objectParents.set(child, parents);
|
|
2387
|
+
}
|
|
2388
|
+
parents.add({ parent, prop });
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Remove a back-reference from child to parent
|
|
2392
|
+
*/
|
|
2393
|
+
function removeBackReference(child, parent, prop) {
|
|
2394
|
+
const parents = objectParents.get(child);
|
|
2395
|
+
if (parents) {
|
|
2396
|
+
for (const entry of parents) {
|
|
2397
|
+
if (entry.parent === parent && entry.prop === prop) {
|
|
2398
|
+
parents.delete(entry);
|
|
2399
|
+
break;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
if (parents.size === 0) {
|
|
2403
|
+
objectParents.delete(child);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Check if an object needs back-references (has deep watchers or parents with deep watchers)
|
|
2409
|
+
*/
|
|
2410
|
+
function needsBackReferences(obj) {
|
|
2411
|
+
// Fast path: check if object itself has deep watchers
|
|
2412
|
+
if (objectsWithDeepWatchers.has(obj))
|
|
2413
|
+
return true;
|
|
2414
|
+
// Slow path: check if any parent has deep watchers (recursive)
|
|
2415
|
+
return hasParentWithDeepWatchers(obj);
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Bubble up changes through the back-reference chain
|
|
2419
|
+
*/
|
|
2420
|
+
function bubbleUpChange(changedObject, evolution) {
|
|
2421
|
+
const parents = objectParents.get(changedObject);
|
|
2422
|
+
if (!parents)
|
|
2423
|
+
return;
|
|
2424
|
+
for (const { parent } of parents) {
|
|
2425
|
+
// Trigger deep watchers on parent
|
|
2426
|
+
const parentDeepWatchers = deepWatchers.get(parent);
|
|
2427
|
+
if (parentDeepWatchers)
|
|
2428
|
+
for (const watcher of parentDeepWatchers)
|
|
2429
|
+
batch(watcher);
|
|
2430
|
+
// Continue bubbling up
|
|
2431
|
+
bubbleUpChange(parent);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function hasParentWithDeepWatchers(obj) {
|
|
2435
|
+
const parents = objectParents.get(obj);
|
|
2436
|
+
if (!parents)
|
|
2437
|
+
return false;
|
|
2438
|
+
for (const { parent } of parents) {
|
|
2439
|
+
if (objectsWithDeepWatchers.has(parent))
|
|
2440
|
+
return true;
|
|
2441
|
+
if (hasParentWithDeepWatchers(parent))
|
|
2442
|
+
return true;
|
|
2443
|
+
}
|
|
2444
|
+
return false;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
const states = new WeakMap();
|
|
2448
|
+
function addState(obj, evolution) {
|
|
2449
|
+
obj = unwrap(obj);
|
|
2450
|
+
const next = {};
|
|
2451
|
+
const state = getState(obj);
|
|
2452
|
+
if (state)
|
|
2453
|
+
Object.assign(state, { evolution, next });
|
|
2454
|
+
states.set(obj, next);
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Gets the current state of a reactive object for evolution tracking
|
|
2458
|
+
* @param obj - The reactive object
|
|
2459
|
+
* @returns The current state object
|
|
2460
|
+
*/
|
|
2461
|
+
function getState(obj) {
|
|
2462
|
+
obj = unwrap(obj);
|
|
2463
|
+
let state = states.get(obj);
|
|
2464
|
+
if (!state) {
|
|
2465
|
+
state = {};
|
|
2466
|
+
states.set(obj, state);
|
|
2467
|
+
}
|
|
2468
|
+
return state;
|
|
2469
|
+
}
|
|
2470
|
+
function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
|
|
2471
|
+
const sourceEffect = getActiveEffect();
|
|
2472
|
+
for (const keys of keyChains)
|
|
2473
|
+
for (const key of keys) {
|
|
2474
|
+
const deps = objectWatchers.get(key);
|
|
2475
|
+
if (deps)
|
|
2476
|
+
for (const effect of deps) {
|
|
2477
|
+
const runningChain = isRunning(effect);
|
|
2478
|
+
if (runningChain) {
|
|
2479
|
+
options.skipRunningEffect(effect, runningChain);
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
effects.add(effect);
|
|
2483
|
+
const trackers = effectTrackers.get(effect);
|
|
2484
|
+
recordTriggerLink(sourceEffect, effect, obj, key, evolution);
|
|
2485
|
+
if (trackers) {
|
|
2486
|
+
for (const tracker of trackers)
|
|
2487
|
+
tracker(obj, evolution, key);
|
|
2488
|
+
trackers.delete(effect);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Triggers effects for a single property change
|
|
2495
|
+
* @param obj - The object that changed
|
|
2496
|
+
* @param evolution - The type of change
|
|
2497
|
+
* @param prop - The property that changed
|
|
2498
|
+
*/
|
|
2499
|
+
function touched1(obj, evolution, prop) {
|
|
2500
|
+
touched(obj, evolution, [prop]);
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Triggers effects for property changes
|
|
2504
|
+
* @param obj - The object that changed
|
|
2505
|
+
* @param evolution - The type of change
|
|
2506
|
+
* @param props - The properties that changed
|
|
2507
|
+
*/
|
|
2508
|
+
function touched(obj, evolution, props) {
|
|
2509
|
+
obj = unwrap(obj);
|
|
2510
|
+
addState(obj, evolution);
|
|
2511
|
+
const objectWatchers = watchers.get(obj);
|
|
2512
|
+
if (objectWatchers) {
|
|
2513
|
+
// Note: we have to collect effects to remove duplicates in the specific case when no batch is running
|
|
2514
|
+
const effects = new Set();
|
|
2515
|
+
if (props)
|
|
2516
|
+
collectEffects(obj, evolution, effects, objectWatchers, [allProps], props);
|
|
2517
|
+
else
|
|
2518
|
+
collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
|
|
2519
|
+
options.touched(obj, evolution, props, effects);
|
|
2520
|
+
batch(Array.from(effects));
|
|
2521
|
+
}
|
|
2522
|
+
// Bubble up changes if this object has deep watchers
|
|
2523
|
+
if (objectsWithDeepWatchers.has(obj)) {
|
|
2524
|
+
bubbleUpChange(obj);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* Triggers only opaque effects for property changes
|
|
2529
|
+
* Used by deep-touch to ensure opaque listeners are notified even when deep optimization is active
|
|
2530
|
+
*/
|
|
2531
|
+
function touchedOpaque(obj, evolution, prop) {
|
|
2532
|
+
obj = unwrap(obj);
|
|
2533
|
+
const objectWatchers = watchers.get(obj);
|
|
2534
|
+
if (!objectWatchers)
|
|
2535
|
+
return;
|
|
2536
|
+
const deps = objectWatchers.get(prop);
|
|
2537
|
+
if (!deps)
|
|
2538
|
+
return;
|
|
2539
|
+
const effects = new Set();
|
|
2540
|
+
const sourceEffect = getActiveEffect();
|
|
2541
|
+
for (const effect of deps) {
|
|
2542
|
+
if (!opaqueEffects.has(effect))
|
|
2543
|
+
continue;
|
|
2544
|
+
const runningChain = isRunning(effect);
|
|
2545
|
+
if (runningChain) {
|
|
2546
|
+
options.skipRunningEffect(effect, runningChain);
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
effects.add(effect);
|
|
2550
|
+
const trackers = effectTrackers.get(effect);
|
|
2551
|
+
recordTriggerLink(sourceEffect, effect, obj, prop, evolution);
|
|
2552
|
+
if (trackers) {
|
|
2553
|
+
for (const tracker of trackers)
|
|
2554
|
+
tracker(obj, evolution, prop);
|
|
2555
|
+
trackers.delete(effect);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
if (effects.size > 0) {
|
|
2559
|
+
options.touched(obj, evolution, [prop], effects);
|
|
2560
|
+
batch(Array.from(effects));
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
const nonReactiveObjects = new WeakSet();
|
|
2565
|
+
const immutables = new Set();
|
|
2566
|
+
const absent = Symbol('absent');
|
|
2567
|
+
function markNonReactive(...obj) {
|
|
2568
|
+
for (const o of obj) {
|
|
2569
|
+
try {
|
|
2570
|
+
Object.defineProperty(o, nonReactiveMark, {
|
|
2571
|
+
value: true,
|
|
2572
|
+
writable: false,
|
|
2573
|
+
enumerable: false,
|
|
2574
|
+
configurable: false,
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
catch { }
|
|
2578
|
+
if (!(nonReactiveMark in o))
|
|
2579
|
+
nonReactiveObjects.add(o);
|
|
2580
|
+
}
|
|
2581
|
+
return obj[0];
|
|
2582
|
+
}
|
|
2583
|
+
function nonReactiveClass(...cls) {
|
|
2584
|
+
for (const c of cls)
|
|
2585
|
+
if (c)
|
|
2586
|
+
c.prototype[nonReactiveMark] = true;
|
|
2587
|
+
return cls[0];
|
|
2588
|
+
}
|
|
2589
|
+
function isNonReactive(obj) {
|
|
2590
|
+
if (obj === null || typeof obj !== 'object')
|
|
2591
|
+
return true;
|
|
2592
|
+
if (nonReactiveObjects.has(obj))
|
|
2593
|
+
return true;
|
|
2594
|
+
if (obj[nonReactiveMark])
|
|
2595
|
+
return true;
|
|
2596
|
+
for (const fn of immutables)
|
|
2597
|
+
if (fn(obj))
|
|
2598
|
+
return true;
|
|
2599
|
+
return false;
|
|
2600
|
+
}
|
|
2601
|
+
function registerNativeReactivity(originalClass, reactiveClass) {
|
|
2602
|
+
originalClass.prototype[nativeReactive] = reactiveClass;
|
|
2603
|
+
nonReactiveClass(reactiveClass);
|
|
2604
|
+
}
|
|
2605
|
+
nonReactiveClass(Date, RegExp, Error, Promise, Function);
|
|
2606
|
+
if (typeof window !== 'undefined') {
|
|
2607
|
+
markNonReactive(window, document);
|
|
2608
|
+
nonReactiveClass(Node, Element, HTMLElement, EventTarget);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
function isObject$1(value) {
|
|
2612
|
+
return typeof value === 'object' && value !== null;
|
|
2613
|
+
}
|
|
2614
|
+
function isObjectLike(value) {
|
|
2615
|
+
return isObject$1(value);
|
|
2616
|
+
}
|
|
2617
|
+
function getPrototypeToken(value) {
|
|
2618
|
+
if (!isObjectLike(value))
|
|
2619
|
+
return undefined;
|
|
2620
|
+
if (Array.isArray(value))
|
|
2621
|
+
return Array.prototype;
|
|
2622
|
+
try {
|
|
2623
|
+
return value.constructor;
|
|
2624
|
+
}
|
|
2625
|
+
catch {
|
|
2626
|
+
return undefined;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
function shouldRecurseTouch(oldValue, newValue) {
|
|
2630
|
+
if (oldValue === newValue)
|
|
2631
|
+
return false;
|
|
2632
|
+
if (!isObjectLike(oldValue) || !isObjectLike(newValue))
|
|
2633
|
+
return false;
|
|
2634
|
+
if (isNonReactive(oldValue) || isNonReactive(newValue))
|
|
2635
|
+
return false;
|
|
2636
|
+
return getPrototypeToken(oldValue) === getPrototypeToken(newValue);
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Centralized function to handle property change notifications with optional recursive touch
|
|
2640
|
+
* @param targetObj - The object whose property changed
|
|
2641
|
+
* @param prop - The property that changed
|
|
2642
|
+
* @param oldValue - The old value (before change)
|
|
2643
|
+
* @param newValue - The new value (after change)
|
|
2644
|
+
* @param hadProperty - Whether the property existed before (for add vs set)
|
|
2645
|
+
*/
|
|
2646
|
+
function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty) {
|
|
2647
|
+
const evolution = { type: hadProperty ? 'set' : 'add', prop };
|
|
2648
|
+
if (options.recursiveTouching &&
|
|
2649
|
+
oldValue !== undefined &&
|
|
2650
|
+
shouldRecurseTouch(oldValue, newValue)) {
|
|
2651
|
+
const unwrappedObj = unwrap(targetObj);
|
|
2652
|
+
const origin = { obj: unwrappedObj, prop };
|
|
2653
|
+
// Deep touch: only notify nested property changes with origin filtering
|
|
2654
|
+
// Don't notify direct property change - the whole point is to avoid parent effects re-running
|
|
2655
|
+
dispatchNotifications(recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
2656
|
+
// Notify opaque listeners (like memoize) that always want to know about identity changes
|
|
2657
|
+
touchedOpaque(targetObj, evolution, prop);
|
|
2658
|
+
}
|
|
2659
|
+
else {
|
|
2660
|
+
touched1(targetObj, evolution, prop);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
function hasVisitedPair(visited, oldObj, newObj) {
|
|
2664
|
+
let mapped = visited.get(oldObj);
|
|
2665
|
+
if (!mapped) {
|
|
2666
|
+
mapped = new WeakSet();
|
|
2667
|
+
visited.set(oldObj, mapped);
|
|
2668
|
+
}
|
|
2669
|
+
if (mapped.has(newObj))
|
|
2670
|
+
return true;
|
|
2671
|
+
mapped.add(newObj);
|
|
2672
|
+
return false;
|
|
2673
|
+
}
|
|
2674
|
+
function collectObjectKeys(obj) {
|
|
2675
|
+
const keys = new Set(Reflect.ownKeys(obj));
|
|
2676
|
+
let proto = Object.getPrototypeOf(obj);
|
|
2677
|
+
// Continue walking while prototype exists and doesn't have its own constructor
|
|
2678
|
+
// This stops at Object.prototype (has own constructor) and class prototypes (have own constructor)
|
|
2679
|
+
// but continues for data prototypes (Object.create({}), Object.create(instance), etc.)
|
|
2680
|
+
while (proto && !Object.hasOwn(proto, 'constructor')) {
|
|
2681
|
+
for (const key of Reflect.ownKeys(proto))
|
|
2682
|
+
keys.add(key);
|
|
2683
|
+
proto = Object.getPrototypeOf(proto);
|
|
2684
|
+
}
|
|
2685
|
+
return keys;
|
|
2686
|
+
}
|
|
2687
|
+
function recursiveTouch(oldValue, newValue, visited = new WeakMap(), notifications = [], origin) {
|
|
2688
|
+
if (!shouldRecurseTouch(oldValue, newValue))
|
|
2689
|
+
return notifications;
|
|
2690
|
+
if (!isObjectLike(oldValue) || !isObjectLike(newValue))
|
|
2691
|
+
return notifications;
|
|
2692
|
+
if (hasVisitedPair(visited, oldValue, newValue))
|
|
2693
|
+
return notifications;
|
|
2694
|
+
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
|
2695
|
+
diffArrayElements(oldValue, newValue, visited, notifications, origin);
|
|
2696
|
+
return notifications;
|
|
2697
|
+
}
|
|
2698
|
+
diffObjectProperties(oldValue, newValue, visited, notifications, origin);
|
|
2699
|
+
return notifications;
|
|
2700
|
+
}
|
|
2701
|
+
function diffArrayElements(oldArray, newArray, _visited, notifications, origin) {
|
|
2702
|
+
const local = [];
|
|
2703
|
+
const oldLength = oldArray.length;
|
|
2704
|
+
const newLength = newArray.length;
|
|
2705
|
+
const max = Math.max(oldLength, newLength);
|
|
2706
|
+
for (let index = 0; index < max; index++) {
|
|
2707
|
+
const hasOld = index < oldLength;
|
|
2708
|
+
const hasNew = index < newLength;
|
|
2709
|
+
if (hasOld && !hasNew) {
|
|
2710
|
+
local.push({ target: oldArray, evolution: { type: 'del', prop: index }, prop: index, origin });
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
if (!hasOld && hasNew) {
|
|
2714
|
+
local.push({ target: oldArray, evolution: { type: 'add', prop: index }, prop: index, origin });
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
if (!hasOld || !hasNew)
|
|
2718
|
+
continue;
|
|
2719
|
+
const oldEntry = unwrap(oldArray[index]);
|
|
2720
|
+
const newEntry = unwrap(newArray[index]);
|
|
2721
|
+
if (!Object.is(oldEntry, newEntry)) {
|
|
2722
|
+
local.push({ target: oldArray, evolution: { type: 'set', prop: index }, prop: index, origin });
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
if (oldLength !== newLength)
|
|
2726
|
+
local.push({
|
|
2727
|
+
target: oldArray,
|
|
2728
|
+
evolution: { type: 'set', prop: 'length' },
|
|
2729
|
+
prop: 'length',
|
|
2730
|
+
origin,
|
|
2731
|
+
});
|
|
2732
|
+
notifications.push(...local);
|
|
2733
|
+
}
|
|
2734
|
+
function diffObjectProperties(oldObj, newObj, visited, notifications, origin) {
|
|
2735
|
+
const oldKeys = collectObjectKeys(oldObj);
|
|
2736
|
+
const newKeys = collectObjectKeys(newObj);
|
|
2737
|
+
const local = [];
|
|
2738
|
+
for (const key of oldKeys)
|
|
2739
|
+
if (!newKeys.has(key))
|
|
2740
|
+
local.push({ target: oldObj, evolution: { type: 'del', prop: key }, prop: key, origin });
|
|
2741
|
+
for (const key of newKeys)
|
|
2742
|
+
if (!oldKeys.has(key))
|
|
2743
|
+
local.push({ target: oldObj, evolution: { type: 'add', prop: key }, prop: key, origin });
|
|
2744
|
+
for (const key of newKeys) {
|
|
2745
|
+
if (!oldKeys.has(key))
|
|
2746
|
+
continue;
|
|
2747
|
+
const oldEntry = unwrap(oldObj[key]);
|
|
2748
|
+
const newEntry = unwrap(newObj[key]);
|
|
2749
|
+
if (shouldRecurseTouch(oldEntry, newEntry)) {
|
|
2750
|
+
recursiveTouch(oldEntry, newEntry, visited, notifications, origin);
|
|
2751
|
+
}
|
|
2752
|
+
else if (!Object.is(oldEntry, newEntry)) {
|
|
2753
|
+
local.push({ target: oldObj, evolution: { type: 'set', prop: key }, prop: key, origin });
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
notifications.push(...local);
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Checks if an effect or any of its ancestors is in the allowed set
|
|
2760
|
+
*/
|
|
2761
|
+
function hasAncestorInSet(effect, allowedSet) {
|
|
2762
|
+
let current = effect;
|
|
2763
|
+
const visited = new WeakSet();
|
|
2764
|
+
while (current && !visited.has(current)) {
|
|
2765
|
+
visited.add(current);
|
|
2766
|
+
if (allowedSet.has(current))
|
|
2767
|
+
return true;
|
|
2768
|
+
current = effectParent.get(current);
|
|
2769
|
+
}
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
function dispatchNotifications(notifications) {
|
|
2773
|
+
if (!notifications.length)
|
|
2774
|
+
return;
|
|
2775
|
+
const combinedEffects = new Set();
|
|
2776
|
+
// Extract origin from first notification (all should have the same origin from a single deep touch)
|
|
2777
|
+
const origin = notifications[0]?.origin;
|
|
2778
|
+
let allowedEffects;
|
|
2779
|
+
// If origin exists, compute allowed effects (those that depend on origin.obj[origin.prop])
|
|
2780
|
+
if (origin) {
|
|
2781
|
+
allowedEffects = new Set();
|
|
2782
|
+
const originWatchers = watchers.get(origin.obj);
|
|
2783
|
+
if (originWatchers) {
|
|
2784
|
+
const originEffects = new Set();
|
|
2785
|
+
collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
|
|
2786
|
+
for (const effect of originEffects)
|
|
2787
|
+
allowedEffects.add(effect);
|
|
2788
|
+
}
|
|
2789
|
+
// If no allowed effects, skip all notifications (no one should be notified)
|
|
2790
|
+
if (allowedEffects.size === 0)
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
for (const { target, evolution, prop } of notifications) {
|
|
2794
|
+
if (!isObjectLike(target))
|
|
2795
|
+
continue;
|
|
2796
|
+
const obj = unwrap(target);
|
|
2797
|
+
addState(obj, evolution);
|
|
2798
|
+
const objectWatchers = watchers.get(obj);
|
|
2799
|
+
let currentEffects;
|
|
2800
|
+
const propsArray = [prop];
|
|
2801
|
+
if (objectWatchers) {
|
|
2802
|
+
currentEffects = new Set();
|
|
2803
|
+
collectEffects(obj, evolution, currentEffects, objectWatchers, [allProps], propsArray);
|
|
2804
|
+
// Filter effects by ancestor chain if origin exists
|
|
2805
|
+
// Include effects that either directly depend on origin or have an ancestor that does
|
|
2806
|
+
if (origin && allowedEffects) {
|
|
2807
|
+
const filteredEffects = new Set();
|
|
2808
|
+
for (const effect of currentEffects) {
|
|
2809
|
+
// Check if effect itself is allowed OR has an ancestor that is allowed
|
|
2810
|
+
if (allowedEffects.has(effect) || hasAncestorInSet(effect, allowedEffects)) {
|
|
2811
|
+
filteredEffects.add(effect);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
currentEffects = filteredEffects;
|
|
2815
|
+
}
|
|
2816
|
+
for (const effect of currentEffects)
|
|
2817
|
+
combinedEffects.add(effect);
|
|
2818
|
+
}
|
|
2819
|
+
options.touched(obj, evolution, propsArray, currentEffects);
|
|
2820
|
+
if (objectsWithDeepWatchers.has(obj))
|
|
2821
|
+
bubbleUpChange(obj);
|
|
2822
|
+
}
|
|
2823
|
+
if (combinedEffects.size)
|
|
2824
|
+
batch([...combinedEffects]);
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
const hasReentry = [];
|
|
2828
|
+
const reactiveHandlers = {
|
|
2829
|
+
[Symbol.toStringTag]: 'MutTs Reactive',
|
|
2830
|
+
get(obj, prop, receiver) {
|
|
2831
|
+
if (prop === nonReactiveMark)
|
|
2832
|
+
return false;
|
|
2833
|
+
const unwrappedObj = unwrap(obj);
|
|
2834
|
+
// Check if this property is marked as unreactive
|
|
2835
|
+
if (unwrappedObj[unreactiveProperties]?.has(prop) || typeof prop === 'symbol')
|
|
2836
|
+
return decorator.ReflectGet(obj, prop, receiver);
|
|
2837
|
+
// Special-case: array wrappers use prototype forwarding + numeric accessors.
|
|
2838
|
+
// With options.instanceMembers=true, inherited reads are normally not tracked, which breaks
|
|
2839
|
+
// reactivity for array indices/length (they appear inherited on the proxy).
|
|
2840
|
+
const isArrayCase = prototypeForwarding in obj &&
|
|
2841
|
+
// biome-ignore lint/suspicious/useIsArray: This is the whole point here
|
|
2842
|
+
obj[prototypeForwarding] instanceof Array &&
|
|
2843
|
+
typeof prop === 'string' &&
|
|
2844
|
+
(prop === 'length' || !Number.isNaN(Number(prop)));
|
|
2845
|
+
if (isArrayCase) {
|
|
2846
|
+
dependant(obj, prop === 'length' ? 'length' : Number(prop));
|
|
2847
|
+
}
|
|
2848
|
+
// Check if property exists and if it's an own property (cached for later use)
|
|
2849
|
+
const hasProp = Reflect.has(receiver, prop);
|
|
2850
|
+
const isOwnProp = hasProp && Object.hasOwn(receiver, prop);
|
|
2851
|
+
const isInheritedAccess = hasProp && !isOwnProp;
|
|
2852
|
+
// For accessor properties, check the unwrapped object to see if it's an accessor
|
|
2853
|
+
// This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
|
|
2854
|
+
const shouldIgnoreAccessor = options.ignoreAccessors &&
|
|
2855
|
+
isOwnProp &&
|
|
2856
|
+
(decorator.isOwnAccessor(receiver, prop) || decorator.isOwnAccessor(unwrappedObj, prop));
|
|
2857
|
+
// Depend if...
|
|
2858
|
+
if (!hasProp ||
|
|
2859
|
+
(!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
|
|
2860
|
+
!shouldIgnoreAccessor))
|
|
2861
|
+
dependant(obj, prop);
|
|
2862
|
+
// Watch the whole prototype chain when requested or for null-proto objects
|
|
2863
|
+
if (isInheritedAccess && (!options.instanceMembers || !(obj instanceof Object))) {
|
|
2864
|
+
let current = reactiveObject(Object.getPrototypeOf(obj));
|
|
2865
|
+
while (current && current !== Object.prototype) {
|
|
2866
|
+
dependant(current, prop);
|
|
2867
|
+
if (Object.hasOwn(current, prop))
|
|
2868
|
+
break;
|
|
2869
|
+
current = reactiveObject(Object.getPrototypeOf(current));
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
const value = decorator.ReflectGet(obj, prop, receiver);
|
|
2873
|
+
if (typeof value === 'object' && value !== null) {
|
|
2874
|
+
const reactiveValue = reactiveObject(value);
|
|
2875
|
+
// Only create back-references if this object needs them
|
|
2876
|
+
if (needsBackReferences(obj)) {
|
|
2877
|
+
addBackReference(reactiveValue, obj, prop);
|
|
2878
|
+
}
|
|
2879
|
+
return reactiveValue;
|
|
2880
|
+
}
|
|
2881
|
+
return value;
|
|
2882
|
+
},
|
|
2883
|
+
set(obj, prop, value, receiver) {
|
|
2884
|
+
// Read old value directly from unwrapped object to avoid triggering dependency tracking
|
|
2885
|
+
const unwrappedObj = unwrap(obj);
|
|
2886
|
+
const unwrappedReceiver = unwrap(receiver);
|
|
2887
|
+
// Check if this property is marked as unreactive
|
|
2888
|
+
if (unwrappedObj[unreactiveProperties]?.has(prop) || unwrappedObj !== unwrappedReceiver)
|
|
2889
|
+
return decorator.ReflectSet(obj, prop, value, receiver);
|
|
2890
|
+
// Really specific case for when Array is forwarder, in order to let it manage the reactivity
|
|
2891
|
+
const isArrayCase = prototypeForwarding in obj &&
|
|
2892
|
+
// biome-ignore lint/suspicious/useIsArray: This is the whole point here
|
|
2893
|
+
obj[prototypeForwarding] instanceof Array &&
|
|
2894
|
+
(!Number.isNaN(Number(prop)) || prop === 'length');
|
|
2895
|
+
const newValue = unwrap(value);
|
|
2896
|
+
if (isArrayCase) {
|
|
2897
|
+
obj[prop] = newValue;
|
|
2898
|
+
return true;
|
|
2899
|
+
}
|
|
2900
|
+
// Read old value, using withEffect(undefined, ...) for getter-only accessors to avoid
|
|
2901
|
+
// breaking memoization dependency tracking during SET operations
|
|
2902
|
+
let oldVal = absent;
|
|
2903
|
+
if (Reflect.has(unwrappedReceiver, prop)) {
|
|
2904
|
+
// Check descriptor on both receiver and target to handle proxy cases
|
|
2905
|
+
const receiverDesc = Object.getOwnPropertyDescriptor(unwrappedReceiver, prop);
|
|
2906
|
+
const targetDesc = Object.getOwnPropertyDescriptor(unwrappedObj, prop);
|
|
2907
|
+
const desc = receiverDesc || targetDesc;
|
|
2908
|
+
// If it's a getter-only accessor (has getter but no setter), read without tracking
|
|
2909
|
+
// to avoid breaking memoization invalidation when the getter calls memoized functions
|
|
2910
|
+
if (desc?.get && !desc?.set) {
|
|
2911
|
+
oldVal = withEffect(undefined, () => Reflect.get(unwrappedObj, prop, unwrappedReceiver));
|
|
2912
|
+
}
|
|
2913
|
+
else {
|
|
2914
|
+
oldVal = Reflect.get(unwrappedObj, prop, unwrappedReceiver);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
if (objectsWithDeepWatchers.has(obj)) {
|
|
2918
|
+
if (typeof oldVal === 'object' && oldVal !== null) {
|
|
2919
|
+
removeBackReference(oldVal, obj, prop);
|
|
2920
|
+
}
|
|
2921
|
+
if (typeof newValue === 'object' && newValue !== null) {
|
|
2922
|
+
const reactiveValue = reactiveObject(newValue);
|
|
2923
|
+
addBackReference(reactiveValue, obj, prop);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
if (oldVal !== newValue) {
|
|
2927
|
+
// For getter-only accessors, Reflect.set() may fail, but we still return true
|
|
2928
|
+
// to avoid throwing errors. Only proceed with change notifications if set succeeded.
|
|
2929
|
+
if (decorator.ReflectSet(obj, prop, newValue, receiver)) {
|
|
2930
|
+
notifyPropertyChange(obj, prop, oldVal, newValue, oldVal !== absent);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return true;
|
|
2934
|
+
},
|
|
2935
|
+
has(obj, prop) {
|
|
2936
|
+
if (hasReentry.includes(obj))
|
|
2937
|
+
throw new ReactiveError(`[reactive] Circular dependency detected in 'has' check for property '${String(prop)}'`, {
|
|
2938
|
+
code: ReactiveErrorCode.CycleDetected,
|
|
2939
|
+
cycle: [], // We don't have the full cycle here, but we know it involves obj
|
|
2940
|
+
});
|
|
2941
|
+
hasReentry.push(obj);
|
|
2942
|
+
dependant(obj, prop);
|
|
2943
|
+
const rv = Reflect.has(obj, prop);
|
|
2944
|
+
hasReentry.pop();
|
|
2945
|
+
return rv;
|
|
2946
|
+
},
|
|
2947
|
+
deleteProperty(obj, prop) {
|
|
2948
|
+
if (!Object.hasOwn(obj, prop))
|
|
2949
|
+
return false;
|
|
2950
|
+
const oldVal = obj[prop];
|
|
2951
|
+
// Remove back-references if this object has deep watchers
|
|
2952
|
+
if (objectsWithDeepWatchers.has(obj) && typeof oldVal === 'object' && oldVal !== null) {
|
|
2953
|
+
removeBackReference(oldVal, obj, prop);
|
|
2954
|
+
}
|
|
2955
|
+
delete obj[prop];
|
|
2956
|
+
touched1(obj, { type: 'del', prop }, prop);
|
|
2957
|
+
// Bubble up changes if this object has deep watchers
|
|
2958
|
+
if (objectsWithDeepWatchers.has(obj)) {
|
|
2959
|
+
bubbleUpChange(obj);
|
|
2960
|
+
}
|
|
2961
|
+
return true;
|
|
2962
|
+
},
|
|
2963
|
+
getPrototypeOf(obj) {
|
|
2964
|
+
if (prototypeForwarding in obj)
|
|
2965
|
+
return obj[prototypeForwarding];
|
|
2966
|
+
return Object.getPrototypeOf(obj);
|
|
2967
|
+
},
|
|
2968
|
+
setPrototypeOf(obj, proto) {
|
|
2969
|
+
if (prototypeForwarding in obj)
|
|
2970
|
+
return false;
|
|
2971
|
+
Object.setPrototypeOf(obj, proto);
|
|
2972
|
+
return true;
|
|
2973
|
+
},
|
|
2974
|
+
ownKeys(obj) {
|
|
2975
|
+
dependant(obj, allProps);
|
|
2976
|
+
return Reflect.ownKeys(obj);
|
|
2977
|
+
},
|
|
2978
|
+
};
|
|
2979
|
+
const reactiveClasses = new WeakSet();
|
|
2980
|
+
// Create the ReactiveBase mixin
|
|
2981
|
+
/**
|
|
2982
|
+
* Base mixin for reactive classes that provides proper constructor reactivity
|
|
2983
|
+
* Solves constructor reactivity issues in complex inheritance trees
|
|
2984
|
+
*/
|
|
2985
|
+
const ReactiveBase = mixin((base) => {
|
|
2986
|
+
class ReactiveMixin extends base {
|
|
2987
|
+
constructor(...args) {
|
|
2988
|
+
super(...args);
|
|
2989
|
+
// Only apply reactive transformation if the class is marked with @reactive
|
|
2990
|
+
// This allows the mixin to work properly with method inheritance
|
|
2991
|
+
// biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
|
|
2992
|
+
return reactiveClasses.has(new.target) ? reactive(this) : this;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
return ReactiveMixin;
|
|
2996
|
+
});
|
|
2997
|
+
function reactiveObject(anyTarget) {
|
|
2998
|
+
if (!anyTarget || typeof anyTarget !== 'object')
|
|
2999
|
+
return anyTarget;
|
|
3000
|
+
const target = anyTarget;
|
|
3001
|
+
// If target is already a proxy, return it
|
|
3002
|
+
if (isNonReactive(target))
|
|
3003
|
+
return target;
|
|
3004
|
+
const isProxy = proxyToObject.has(target);
|
|
3005
|
+
if (isProxy)
|
|
3006
|
+
return target;
|
|
3007
|
+
// If we already have a proxy for this object, return it (optimized: get returns undefined if not found)
|
|
3008
|
+
const existing = getExistingProxy(target);
|
|
3009
|
+
if (existing !== undefined)
|
|
3010
|
+
return existing;
|
|
3011
|
+
const proxied = nativeReactive in target && !(target instanceof target[nativeReactive])
|
|
3012
|
+
? new target[nativeReactive](target)
|
|
3013
|
+
: target;
|
|
3014
|
+
if (proxied !== target)
|
|
3015
|
+
trackProxyObject(proxied, target);
|
|
3016
|
+
const proxy = new Proxy(proxied, reactiveHandlers);
|
|
3017
|
+
// Store the relationships
|
|
3018
|
+
storeProxyRelationship(target, proxy);
|
|
3019
|
+
return proxy;
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* Main decorator for making classes reactive
|
|
3023
|
+
* Automatically makes class instances reactive when created
|
|
3024
|
+
*/
|
|
3025
|
+
const reactive = decorator.decorator({
|
|
3026
|
+
class(original) {
|
|
3027
|
+
if (original.prototype instanceof ReactiveBase) {
|
|
3028
|
+
reactiveClasses.add(original);
|
|
3029
|
+
return original;
|
|
3030
|
+
}
|
|
3031
|
+
class Reactive extends original {
|
|
3032
|
+
constructor(...args) {
|
|
3033
|
+
super(...args);
|
|
3034
|
+
if (new.target !== Reactive && !reactiveClasses.has(new.target))
|
|
3035
|
+
options.warn(`${original.name} has been inherited by ${this.constructor.name} that is not reactive.
|
|
3036
|
+
@reactive decorator must be applied to the leaf class OR classes have to extend ReactiveBase.`);
|
|
3037
|
+
// biome-ignore lint/correctness/noConstructorReturn: This is the whole point here
|
|
3038
|
+
return reactive(this);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
Object.defineProperty(Reactive, 'name', {
|
|
3042
|
+
value: `Reactive<${original.name}>`,
|
|
3043
|
+
});
|
|
3044
|
+
return Reactive;
|
|
3045
|
+
},
|
|
3046
|
+
get(original) {
|
|
3047
|
+
return reactiveObject(original);
|
|
3048
|
+
},
|
|
3049
|
+
default: reactiveObject,
|
|
3050
|
+
});
|
|
3051
|
+
|
|
3052
|
+
function isObject(value) {
|
|
3053
|
+
return typeof value === 'object' && value !== null;
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Deep watch an object and all its nested properties
|
|
3057
|
+
* @param target - The object to watch deeply
|
|
3058
|
+
* @param callback - The callback to call when any nested property changes
|
|
3059
|
+
* @param options - Options for the deep watch
|
|
3060
|
+
* @returns A cleanup function to stop watching
|
|
3061
|
+
*/
|
|
3062
|
+
/**
|
|
3063
|
+
* Sets up deep watching for an object, tracking all nested property changes
|
|
3064
|
+
* @param target - The object to watch
|
|
3065
|
+
* @param callback - The callback to call when changes occur
|
|
3066
|
+
* @param options - Options for deep watching
|
|
3067
|
+
* @returns A cleanup function to stop deep watching
|
|
3068
|
+
*/
|
|
3069
|
+
function deepWatch(target, callback, { immediate = false } = {}) {
|
|
3070
|
+
if (target === null || target === undefined)
|
|
3071
|
+
return undefined;
|
|
3072
|
+
if (typeof target !== 'object')
|
|
3073
|
+
throw new Error('Target of deep watching must be an object');
|
|
3074
|
+
// Create a wrapper callback that matches ScopedCallback signature
|
|
3075
|
+
const wrappedCallback = markWithRoot(() => callback(target), callback);
|
|
3076
|
+
// Use the existing effect system to register dependencies
|
|
3077
|
+
return effect(() => {
|
|
3078
|
+
// Mark the target object as having deep watchers
|
|
3079
|
+
objectsWithDeepWatchers.add(target);
|
|
3080
|
+
// Track which objects this effect is watching for cleanup
|
|
3081
|
+
let effectObjects = effectToDeepWatchedObjects.get(wrappedCallback);
|
|
3082
|
+
if (!effectObjects) {
|
|
3083
|
+
effectObjects = new Set();
|
|
3084
|
+
effectToDeepWatchedObjects.set(wrappedCallback, effectObjects);
|
|
3085
|
+
}
|
|
3086
|
+
effectObjects.add(target);
|
|
3087
|
+
// Traverse the object graph and register dependencies
|
|
3088
|
+
// This will re-run every time the effect runs, ensuring we catch all changes
|
|
3089
|
+
const visited = new WeakSet();
|
|
3090
|
+
function traverseAndTrack(obj, depth = 0) {
|
|
3091
|
+
// Prevent infinite recursion and excessive depth
|
|
3092
|
+
if (!obj || visited.has(obj) || !isObject(obj) || depth > options.maxDeepWatchDepth)
|
|
3093
|
+
return;
|
|
3094
|
+
// Do not traverse into unreactive objects
|
|
3095
|
+
if (isNonReactive(obj))
|
|
3096
|
+
return;
|
|
3097
|
+
visited.add(obj);
|
|
3098
|
+
// Mark this object as having deep watchers
|
|
3099
|
+
objectsWithDeepWatchers.add(obj);
|
|
3100
|
+
effectObjects.add(obj);
|
|
3101
|
+
// Traverse all properties to register dependencies
|
|
3102
|
+
// unwrap to avoid kicking dependency
|
|
3103
|
+
for (const key in unwrap(obj)) {
|
|
3104
|
+
if (Object.hasOwn(obj, key)) {
|
|
3105
|
+
// Access the property to register dependency
|
|
3106
|
+
const value = obj[key];
|
|
3107
|
+
// Make the value reactive if it's an object
|
|
3108
|
+
const reactiveValue = typeof value === 'object' && value !== null ? reactive(value) : value;
|
|
3109
|
+
traverseAndTrack(reactiveValue, depth + 1);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
// Also handle array indices and length
|
|
3113
|
+
// biome-ignore lint/suspicious/useIsArray: Check for both native arrays and reactive arrays
|
|
3114
|
+
if (Array.isArray(obj) || obj instanceof Array) {
|
|
3115
|
+
// Access array length to register dependency on length changes
|
|
3116
|
+
const length = obj.length;
|
|
3117
|
+
// Access all current array elements to register dependencies
|
|
3118
|
+
for (let i = 0; i < length; i++) {
|
|
3119
|
+
// Access the array element to register dependency
|
|
3120
|
+
const value = obj[i];
|
|
3121
|
+
// Make the value reactive if it's an object
|
|
3122
|
+
const reactiveValue = typeof value === 'object' && value !== null ? reactive(value) : value;
|
|
3123
|
+
traverseAndTrack(reactiveValue, depth + 1);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
// Handle Set values (deep watch values only, not keys since Sets don't have separate keys)
|
|
3127
|
+
else if (obj instanceof Set) {
|
|
3128
|
+
// Access all Set values to register dependencies
|
|
3129
|
+
for (const value of obj) {
|
|
3130
|
+
// Make the value reactive if it's an object
|
|
3131
|
+
const reactiveValue = typeof value === 'object' && value !== null ? reactive(value) : value;
|
|
3132
|
+
traverseAndTrack(reactiveValue, depth + 1);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
// Handle Map values (deep watch values only, not keys)
|
|
3136
|
+
else if (obj instanceof Map) {
|
|
3137
|
+
// Access all Map values to register dependencies
|
|
3138
|
+
for (const [_key, value] of obj) {
|
|
3139
|
+
// Make the value reactive if it's an object
|
|
3140
|
+
const reactiveValue = typeof value === 'object' && value !== null ? reactive(value) : value;
|
|
3141
|
+
traverseAndTrack(reactiveValue, depth + 1);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
// Note: WeakSet and WeakMap cannot be iterated, so we can't deep watch their contents
|
|
3145
|
+
// They will only trigger when the collection itself is replaced
|
|
3146
|
+
}
|
|
3147
|
+
// Traverse the target object to register all dependencies
|
|
3148
|
+
// This will register dependencies on all current properties and array elements
|
|
3149
|
+
traverseAndTrack(target);
|
|
3150
|
+
// Only call the callback if immediate is true or if it's not the first run
|
|
3151
|
+
if (immediate)
|
|
3152
|
+
callback(target);
|
|
3153
|
+
immediate = true;
|
|
3154
|
+
// Return a cleanup function that properly removes deep watcher tracking
|
|
3155
|
+
return () => {
|
|
3156
|
+
// Get the objects this effect was watching
|
|
3157
|
+
const effectObjects = effectToDeepWatchedObjects.get(wrappedCallback);
|
|
3158
|
+
if (effectObjects) {
|
|
3159
|
+
// Remove deep watcher tracking from all objects this effect was watching
|
|
3160
|
+
for (const obj of effectObjects) {
|
|
3161
|
+
// Check if this object still has other deep watchers
|
|
3162
|
+
const watchers = deepWatchers.get(obj);
|
|
3163
|
+
if (watchers) {
|
|
3164
|
+
// Remove this effect's callback from the watchers
|
|
3165
|
+
watchers.delete(wrappedCallback);
|
|
3166
|
+
// If no more watchers, remove the object from deep watchers tracking
|
|
3167
|
+
if (watchers.size === 0) {
|
|
3168
|
+
deepWatchers.delete(obj);
|
|
3169
|
+
objectsWithDeepWatchers.delete(obj);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
else {
|
|
3173
|
+
// No watchers found, remove from deep watchers tracking
|
|
3174
|
+
objectsWithDeepWatchers.delete(obj);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
// Clean up the tracking data
|
|
3178
|
+
effectToDeepWatchedObjects.delete(wrappedCallback);
|
|
3179
|
+
}
|
|
3180
|
+
};
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
/**
|
|
3185
|
+
* Symbol for accessing the cleanup function on cleaned objects
|
|
3186
|
+
*/
|
|
3187
|
+
const cleanup = Symbol('cleanup');
|
|
3188
|
+
//#region watch
|
|
3189
|
+
const unsetYet = Symbol('unset-yet');
|
|
3190
|
+
function watch(value, //object | ((dep: DependencyAccess) => object),
|
|
3191
|
+
changed, options = {}) {
|
|
3192
|
+
return typeof value === 'function'
|
|
3193
|
+
? watchCallBack(value, changed, options)
|
|
3194
|
+
: typeof value === 'object' && value !== null
|
|
3195
|
+
? watchObject(value, changed, options)
|
|
3196
|
+
: (() => {
|
|
3197
|
+
throw new Error('watch: value must be a function or an object');
|
|
3198
|
+
})();
|
|
3199
|
+
}
|
|
3200
|
+
function watchObject(value, changed, { immediate = false, deep = false } = {}) {
|
|
3201
|
+
const myParentEffect = getActiveEffect();
|
|
3202
|
+
if (deep)
|
|
3203
|
+
return deepWatch(value, changed, { immediate });
|
|
3204
|
+
return effect(markWithRoot(function watchObjectEffect() {
|
|
3205
|
+
dependant(value);
|
|
3206
|
+
if (immediate)
|
|
3207
|
+
withEffect(myParentEffect, () => changed(value));
|
|
3208
|
+
immediate = true;
|
|
3209
|
+
}, changed));
|
|
3210
|
+
}
|
|
3211
|
+
function watchCallBack(value, changed, { immediate = false, deep = false } = {}) {
|
|
3212
|
+
const myParentEffect = getActiveEffect();
|
|
3213
|
+
let oldValue = unsetYet;
|
|
3214
|
+
let deepCleanup;
|
|
3215
|
+
const cbCleanup = effect(markWithRoot(function watchCallBackEffect(access) {
|
|
3216
|
+
const newValue = value(access);
|
|
3217
|
+
if (oldValue !== newValue)
|
|
3218
|
+
withEffect(myParentEffect, markWithRoot(() => {
|
|
3219
|
+
if (oldValue === unsetYet) {
|
|
3220
|
+
if (immediate)
|
|
3221
|
+
changed(newValue);
|
|
3222
|
+
}
|
|
3223
|
+
else
|
|
3224
|
+
changed(newValue, oldValue);
|
|
3225
|
+
oldValue = newValue;
|
|
3226
|
+
if (deep) {
|
|
3227
|
+
if (deepCleanup)
|
|
3228
|
+
deepCleanup();
|
|
3229
|
+
deepCleanup = deepWatch(newValue, markWithRoot((value) => changed(value, value), changed));
|
|
3230
|
+
}
|
|
3231
|
+
}, changed));
|
|
3232
|
+
}, value));
|
|
3233
|
+
return () => {
|
|
3234
|
+
cbCleanup();
|
|
3235
|
+
if (deepCleanup)
|
|
3236
|
+
deepCleanup();
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
//#endregion
|
|
3240
|
+
//#region nonReactive
|
|
3241
|
+
/**
|
|
3242
|
+
* Mark an object as non-reactive. This object and all its properties will never be made reactive.
|
|
3243
|
+
* @param obj - The object to mark as non-reactive
|
|
3244
|
+
*/
|
|
3245
|
+
function deepNonReactive(obj) {
|
|
3246
|
+
obj = unwrap(obj);
|
|
3247
|
+
if (isNonReactive(obj))
|
|
3248
|
+
return obj;
|
|
3249
|
+
try {
|
|
3250
|
+
Object.defineProperty(obj, nonReactiveMark, {
|
|
3251
|
+
value: true,
|
|
3252
|
+
writable: false,
|
|
3253
|
+
enumerable: false,
|
|
3254
|
+
configurable: true,
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
catch { }
|
|
3258
|
+
if (!(nonReactiveMark in obj))
|
|
3259
|
+
nonReactiveObjects.add(obj);
|
|
3260
|
+
//for (const key in obj) deepNonReactive(obj[key])
|
|
3261
|
+
return obj;
|
|
3262
|
+
}
|
|
3263
|
+
function unreactiveApplication(arg1, ...args) {
|
|
3264
|
+
return typeof arg1 === 'object'
|
|
3265
|
+
? deepNonReactive(arg1)
|
|
3266
|
+
: ((original) => {
|
|
3267
|
+
// Copy the parent's unreactive properties if they exist
|
|
3268
|
+
original.prototype[unreactiveProperties] = new Set(original.prototype[unreactiveProperties] || []);
|
|
3269
|
+
// Add all arguments (including the first one)
|
|
3270
|
+
original.prototype[unreactiveProperties].add(arg1);
|
|
3271
|
+
for (const arg of args)
|
|
3272
|
+
original.prototype[unreactiveProperties].add(arg);
|
|
3273
|
+
return original; // Return the class
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Decorator that marks classes or properties as non-reactive
|
|
3278
|
+
* Prevents objects from being made reactive
|
|
3279
|
+
*/
|
|
3280
|
+
const unreactive = decorator.decorator({
|
|
3281
|
+
class(original) {
|
|
3282
|
+
// Called without arguments, mark entire class as non-reactive
|
|
3283
|
+
nonReactiveClass(original);
|
|
3284
|
+
},
|
|
3285
|
+
default: unreactiveApplication,
|
|
3286
|
+
});
|
|
3287
|
+
//#endregion
|
|
3288
|
+
function cleanedBy(obj, cleanupFn) {
|
|
3289
|
+
return Object.defineProperty(obj, cleanup, {
|
|
3290
|
+
value: cleanupFn,
|
|
3291
|
+
writable: false,
|
|
3292
|
+
enumerable: false,
|
|
3293
|
+
configurable: true,
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
//#region greedy caching
|
|
3297
|
+
/**
|
|
3298
|
+
* Creates a derived value that automatically recomputes when dependencies change
|
|
3299
|
+
* @param compute - Function that computes the derived value
|
|
3300
|
+
* @returns Object with value and cleanup function
|
|
3301
|
+
*/
|
|
3302
|
+
function derived(compute) {
|
|
3303
|
+
const rv = { value: undefined };
|
|
3304
|
+
return cleanedBy(rv, untracked(() => effect(markWithRoot(function derivedEffect(access) {
|
|
3305
|
+
rv.value = compute(access);
|
|
3306
|
+
}, compute))));
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
/**
|
|
3310
|
+
* Converts an iterator to a generator that yields reactive values
|
|
3311
|
+
*/
|
|
3312
|
+
function* makeReactiveIterator(iterator) {
|
|
3313
|
+
let result = iterator.next();
|
|
3314
|
+
while (!result.done) {
|
|
3315
|
+
yield reactive(result.value);
|
|
3316
|
+
result = iterator.next();
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Converts an iterator of key-value pairs to a generator that yields reactive key-value pairs
|
|
3321
|
+
*/
|
|
3322
|
+
function* makeReactiveEntriesIterator(iterator) {
|
|
3323
|
+
let result = iterator.next();
|
|
3324
|
+
while (!result.done) {
|
|
3325
|
+
const [key, value] = result.value;
|
|
3326
|
+
yield [reactive(key), reactive(value)];
|
|
3327
|
+
result = iterator.next();
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
const native$2 = Symbol('native');
|
|
3332
|
+
const isArray = Array.isArray;
|
|
3333
|
+
Array.isArray = ((value) => isArray(value) ||
|
|
3334
|
+
// biome-ignore lint/suspicious/useIsArray: We are defining it
|
|
3335
|
+
(value &&
|
|
3336
|
+
typeof value === 'object' &&
|
|
3337
|
+
prototypeForwarding in value &&
|
|
3338
|
+
Array.isArray(value[prototypeForwarding])));
|
|
3339
|
+
class ReactiveBaseArray {
|
|
3340
|
+
// Safe array access with negative indices
|
|
3341
|
+
at(index) {
|
|
3342
|
+
const actualIndex = index < 0 ? this[native$2].length + index : index;
|
|
3343
|
+
dependant(this, actualIndex);
|
|
3344
|
+
if (actualIndex < 0 || actualIndex >= this[native$2].length)
|
|
3345
|
+
return undefined;
|
|
3346
|
+
return reactive(this[native$2][actualIndex]);
|
|
3347
|
+
}
|
|
3348
|
+
// Immutable versions of mutator methods
|
|
3349
|
+
toReversed() {
|
|
3350
|
+
dependant(this);
|
|
3351
|
+
return reactive(this[native$2].toReversed());
|
|
3352
|
+
}
|
|
3353
|
+
toSorted(compareFn) {
|
|
3354
|
+
dependant(this);
|
|
3355
|
+
return reactive(this[native$2].toSorted(compareFn));
|
|
3356
|
+
}
|
|
3357
|
+
toSpliced(start, deleteCount, ...items) {
|
|
3358
|
+
dependant(this);
|
|
3359
|
+
return deleteCount === undefined
|
|
3360
|
+
? this[native$2].toSpliced(start)
|
|
3361
|
+
: this[native$2].toSpliced(start, deleteCount, ...items);
|
|
3362
|
+
}
|
|
3363
|
+
with(index, value) {
|
|
3364
|
+
dependant(this);
|
|
3365
|
+
return reactive(this[native$2].with(index, value));
|
|
3366
|
+
}
|
|
3367
|
+
// Iterator methods with reactivity tracking
|
|
3368
|
+
entries() {
|
|
3369
|
+
dependant(this);
|
|
3370
|
+
return makeReactiveEntriesIterator(this[native$2].entries());
|
|
3371
|
+
}
|
|
3372
|
+
keys() {
|
|
3373
|
+
dependant(this, 'length');
|
|
3374
|
+
return this[native$2].keys();
|
|
3375
|
+
}
|
|
3376
|
+
values() {
|
|
3377
|
+
dependant(this);
|
|
3378
|
+
return makeReactiveIterator(this[native$2].values());
|
|
3379
|
+
}
|
|
3380
|
+
[Symbol.iterator]() {
|
|
3381
|
+
dependant(this);
|
|
3382
|
+
const nativeIterator = this[native$2][Symbol.iterator]();
|
|
3383
|
+
return {
|
|
3384
|
+
next() {
|
|
3385
|
+
const result = nativeIterator.next();
|
|
3386
|
+
if (result.done) {
|
|
3387
|
+
return result;
|
|
3388
|
+
}
|
|
3389
|
+
return { value: reactive(result.value), done: false };
|
|
3390
|
+
},
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
indexOf(searchElement, fromIndex) {
|
|
3394
|
+
dependant(this);
|
|
3395
|
+
const unwrappedSearch = unwrap(searchElement);
|
|
3396
|
+
// Check both wrapped and unwrapped versions since array may contain either
|
|
3397
|
+
const index = this[native$2].indexOf(unwrappedSearch, fromIndex);
|
|
3398
|
+
if (index !== -1)
|
|
3399
|
+
return index;
|
|
3400
|
+
// If not found with unwrapped, try with wrapped (in case array contains wrapped version)
|
|
3401
|
+
return this[native$2].indexOf(searchElement, fromIndex);
|
|
3402
|
+
}
|
|
3403
|
+
lastIndexOf(searchElement, fromIndex) {
|
|
3404
|
+
dependant(this);
|
|
3405
|
+
const unwrappedSearch = unwrap(searchElement);
|
|
3406
|
+
// Check both wrapped and unwrapped versions since array may contain either
|
|
3407
|
+
const index = this[native$2].lastIndexOf(unwrappedSearch, fromIndex);
|
|
3408
|
+
if (index !== -1)
|
|
3409
|
+
return index;
|
|
3410
|
+
// If not found with unwrapped, try with wrapped (in case array contains wrapped version)
|
|
3411
|
+
return this[native$2].lastIndexOf(searchElement, fromIndex);
|
|
3412
|
+
}
|
|
3413
|
+
includes(searchElement, fromIndex) {
|
|
3414
|
+
dependant(this);
|
|
3415
|
+
const unwrappedSearch = unwrap(searchElement);
|
|
3416
|
+
// Check both wrapped and unwrapped versions since array may contain either
|
|
3417
|
+
return (this[native$2].includes(unwrappedSearch, fromIndex) ||
|
|
3418
|
+
this[native$2].includes(searchElement, fromIndex));
|
|
3419
|
+
}
|
|
3420
|
+
find(predicateOrElement, thisArg) {
|
|
3421
|
+
dependant(this);
|
|
3422
|
+
if (typeof predicateOrElement === 'function') {
|
|
3423
|
+
const predicate = predicateOrElement;
|
|
3424
|
+
return reactive(this[native$2].find((value, index, array) => predicate.call(thisArg, reactive(value), index, array), thisArg));
|
|
3425
|
+
}
|
|
3426
|
+
const fromIndex = typeof thisArg === 'number' ? thisArg : undefined;
|
|
3427
|
+
const index = this[native$2].indexOf(predicateOrElement, fromIndex);
|
|
3428
|
+
if (index === -1)
|
|
3429
|
+
return undefined;
|
|
3430
|
+
return reactive(this[native$2][index]);
|
|
3431
|
+
}
|
|
3432
|
+
findIndex(predicateOrElement, thisArg) {
|
|
3433
|
+
dependant(this);
|
|
3434
|
+
if (typeof predicateOrElement === 'function') {
|
|
3435
|
+
const predicate = predicateOrElement;
|
|
3436
|
+
return this[native$2].findIndex((value, index, array) => predicate.call(thisArg, reactive(value), index, array), thisArg);
|
|
3437
|
+
}
|
|
3438
|
+
const fromIndex = typeof thisArg === 'number' ? thisArg : undefined;
|
|
3439
|
+
return this[native$2].indexOf(predicateOrElement, fromIndex);
|
|
3440
|
+
}
|
|
3441
|
+
flat() {
|
|
3442
|
+
dependant(this);
|
|
3443
|
+
return reactive(this[native$2].flat());
|
|
3444
|
+
}
|
|
3445
|
+
flatMap(callbackfn, thisArg) {
|
|
3446
|
+
dependant(this);
|
|
3447
|
+
return reactive(this[native$2].flatMap(callbackfn, thisArg));
|
|
3448
|
+
}
|
|
3449
|
+
filter(callbackfn, thisArg) {
|
|
3450
|
+
dependant(this);
|
|
3451
|
+
return reactive(this[native$2].filter((item, index, array) => callbackfn(reactive(item), index, array), thisArg));
|
|
3452
|
+
}
|
|
3453
|
+
map(callbackfn, thisArg) {
|
|
3454
|
+
dependant(this);
|
|
3455
|
+
return reactive(this[native$2].map((item, index, array) => callbackfn(reactive(item), index, array), thisArg));
|
|
3456
|
+
}
|
|
3457
|
+
reduce(callbackfn, initialValue) {
|
|
3458
|
+
dependant(this);
|
|
3459
|
+
const result = initialValue === undefined
|
|
3460
|
+
? this[native$2].reduce(callbackfn)
|
|
3461
|
+
: this[native$2].reduce(callbackfn, initialValue);
|
|
3462
|
+
return reactive(result);
|
|
3463
|
+
}
|
|
3464
|
+
reduceRight(callbackfn, initialValue) {
|
|
3465
|
+
dependant(this);
|
|
3466
|
+
const result = initialValue !== undefined
|
|
3467
|
+
? this[native$2].reduceRight(callbackfn, initialValue)
|
|
3468
|
+
: this[native$2].reduceRight(callbackfn);
|
|
3469
|
+
return reactive(result);
|
|
3470
|
+
}
|
|
3471
|
+
slice(start, end) {
|
|
3472
|
+
for (const i of range(start || 0, end || this[native$2].length - 1))
|
|
3473
|
+
dependant(this, i);
|
|
3474
|
+
return start === undefined
|
|
3475
|
+
? this[native$2].slice()
|
|
3476
|
+
: end === undefined
|
|
3477
|
+
? this[native$2].slice(start)
|
|
3478
|
+
: this[native$2].slice(start, end);
|
|
3479
|
+
}
|
|
3480
|
+
concat(...items) {
|
|
3481
|
+
dependant(this);
|
|
3482
|
+
return reactive(this[native$2].concat(...items));
|
|
3483
|
+
}
|
|
3484
|
+
join(separator) {
|
|
3485
|
+
dependant(this);
|
|
3486
|
+
return this[native$2].join(separator);
|
|
3487
|
+
}
|
|
3488
|
+
forEach(callbackfn, thisArg) {
|
|
3489
|
+
dependant(this);
|
|
3490
|
+
this[native$2].forEach((value, index, array) => {
|
|
3491
|
+
callbackfn.call(thisArg, reactive(value), index, array);
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
// TODO: re-implement for fun dependencies? (eg - every only check the first ones until it find some),
|
|
3495
|
+
// no need to make it dependant on indexes after the found one
|
|
3496
|
+
every(callbackfn, thisArg) {
|
|
3497
|
+
dependant(this);
|
|
3498
|
+
return this[native$2].every((value, index, array) => callbackfn.call(thisArg, reactive(value), index, array), thisArg);
|
|
3499
|
+
}
|
|
3500
|
+
some(callbackfn, thisArg) {
|
|
3501
|
+
dependant(this);
|
|
3502
|
+
return this[native$2].some((value, index, array) => callbackfn.call(thisArg, reactive(value), index, array), thisArg);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
function* index(i, { length = true } = {}) {
|
|
3506
|
+
if (length)
|
|
3507
|
+
yield 'length';
|
|
3508
|
+
yield i;
|
|
3509
|
+
}
|
|
3510
|
+
function* range(a, b, { length = false } = {}) {
|
|
3511
|
+
const start = Math.min(a, b);
|
|
3512
|
+
const end = Math.max(a, b);
|
|
3513
|
+
if (length)
|
|
3514
|
+
yield 'length';
|
|
3515
|
+
for (let i = start; i <= end; i++)
|
|
3516
|
+
yield i;
|
|
3517
|
+
}
|
|
3518
|
+
/**
|
|
3519
|
+
* Reactive wrapper around JavaScript's Array class with full array method support
|
|
3520
|
+
* Tracks length changes, individual index operations, and collection-wide operations
|
|
3521
|
+
*/
|
|
3522
|
+
class ReactiveArray extends indexable.Indexable(ReactiveBaseArray, {
|
|
3523
|
+
get(i) {
|
|
3524
|
+
dependant(this, i);
|
|
3525
|
+
return reactive(this[native$2][i]);
|
|
3526
|
+
},
|
|
3527
|
+
set(i, value) {
|
|
3528
|
+
const added = i >= this[native$2].length;
|
|
3529
|
+
this[native$2][i] = value;
|
|
3530
|
+
touched(this, { type: 'set', prop: i }, index(i, { length: added }));
|
|
3531
|
+
},
|
|
3532
|
+
getLength() {
|
|
3533
|
+
dependant(this, 'length');
|
|
3534
|
+
return this[native$2].length;
|
|
3535
|
+
},
|
|
3536
|
+
setLength(value) {
|
|
3537
|
+
const oldLength = this[native$2].length;
|
|
3538
|
+
try {
|
|
3539
|
+
this[native$2].length = value;
|
|
3540
|
+
}
|
|
3541
|
+
finally {
|
|
3542
|
+
touched(this, { type: 'set', prop: 'length' }, range(oldLength, value, { length: true }));
|
|
3543
|
+
}
|
|
3544
|
+
},
|
|
3545
|
+
}) {
|
|
3546
|
+
constructor(original) {
|
|
3547
|
+
super();
|
|
3548
|
+
Object.defineProperties(this, {
|
|
3549
|
+
// We have to make it double, as [native] must be `unique symbol` - impossible through import
|
|
3550
|
+
[native$2]: { value: original },
|
|
3551
|
+
[prototypeForwarding]: { value: original },
|
|
3552
|
+
});
|
|
3553
|
+
}
|
|
3554
|
+
push(...items) {
|
|
3555
|
+
const oldLength = this[native$2].length;
|
|
3556
|
+
try {
|
|
3557
|
+
return this[native$2].push(...items);
|
|
3558
|
+
}
|
|
3559
|
+
finally {
|
|
3560
|
+
touched(this, { type: 'bunch', method: 'push' }, range(oldLength, oldLength + items.length - 1, { length: true }));
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
pop() {
|
|
3564
|
+
if (this[native$2].length === 0)
|
|
3565
|
+
return undefined;
|
|
3566
|
+
try {
|
|
3567
|
+
return reactive(this[native$2].pop());
|
|
3568
|
+
}
|
|
3569
|
+
finally {
|
|
3570
|
+
touched(this, { type: 'bunch', method: 'pop' }, index(this[native$2].length));
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
shift() {
|
|
3574
|
+
if (this[native$2].length === 0)
|
|
3575
|
+
return undefined;
|
|
3576
|
+
try {
|
|
3577
|
+
return reactive(this[native$2].shift());
|
|
3578
|
+
}
|
|
3579
|
+
finally {
|
|
3580
|
+
touched(this, { type: 'bunch', method: 'shift' }, range(0, this[native$2].length + 1, { length: true }));
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
unshift(...items) {
|
|
3584
|
+
try {
|
|
3585
|
+
return this[native$2].unshift(...items);
|
|
3586
|
+
}
|
|
3587
|
+
finally {
|
|
3588
|
+
touched(this, { type: 'bunch', method: 'unshift' }, range(0, this[native$2].length - items.length, { length: true }));
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
splice(start, deleteCount, ...items) {
|
|
3592
|
+
const oldLength = this[native$2].length;
|
|
3593
|
+
if (deleteCount === undefined)
|
|
3594
|
+
deleteCount = oldLength - start;
|
|
3595
|
+
try {
|
|
3596
|
+
if (deleteCount === undefined)
|
|
3597
|
+
return reactive(this[native$2].splice(start));
|
|
3598
|
+
return reactive(this[native$2].splice(start, deleteCount, ...items));
|
|
3599
|
+
}
|
|
3600
|
+
finally {
|
|
3601
|
+
touched(this, { type: 'bunch', method: 'splice' },
|
|
3602
|
+
// TODO: edge cases
|
|
3603
|
+
deleteCount === items.length
|
|
3604
|
+
? range(start, start + deleteCount)
|
|
3605
|
+
: range(start, oldLength + Math.max(items.length - deleteCount, 0), {
|
|
3606
|
+
length: true,
|
|
3607
|
+
}));
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
reverse() {
|
|
3611
|
+
try {
|
|
3612
|
+
return this[native$2].reverse();
|
|
3613
|
+
}
|
|
3614
|
+
finally {
|
|
3615
|
+
touched(this, { type: 'bunch', method: 'reverse' }, range(0, this[native$2].length - 1));
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
sort(compareFn) {
|
|
3619
|
+
compareFn = compareFn || ((a, b) => a.toString().localeCompare(b.toString()));
|
|
3620
|
+
try {
|
|
3621
|
+
return this[native$2].sort((a, b) => compareFn(reactive(a), reactive(b)));
|
|
3622
|
+
}
|
|
3623
|
+
finally {
|
|
3624
|
+
touched(this, { type: 'bunch', method: 'sort' }, range(0, this[native$2].length - 1));
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
fill(value, start, end) {
|
|
3628
|
+
try {
|
|
3629
|
+
if (start === undefined)
|
|
3630
|
+
return this[native$2].fill(value);
|
|
3631
|
+
if (end === undefined)
|
|
3632
|
+
return this[native$2].fill(value, start);
|
|
3633
|
+
return this[native$2].fill(value, start, end);
|
|
3634
|
+
}
|
|
3635
|
+
finally {
|
|
3636
|
+
touched(this, { type: 'bunch', method: 'fill' }, range(0, this[native$2].length - 1));
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
copyWithin(target, start, end) {
|
|
3640
|
+
try {
|
|
3641
|
+
if (end === undefined)
|
|
3642
|
+
return this[native$2].copyWithin(target, start);
|
|
3643
|
+
return this[native$2].copyWithin(target, start, end);
|
|
3644
|
+
}
|
|
3645
|
+
finally {
|
|
3646
|
+
touched(this, { type: 'bunch', method: 'copyWithin' },
|
|
3647
|
+
// TODO: calculate the range properly
|
|
3648
|
+
range(0, this[native$2].length - 1));
|
|
3649
|
+
}
|
|
3650
|
+
// Touch all affected indices with a single allProps call
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
// TODO: Lazy reactivity ?
|
|
3655
|
+
class ReadOnlyError extends Error {
|
|
3656
|
+
}
|
|
3657
|
+
/**
|
|
3658
|
+
* Reactive wrapper around JavaScript's Array class with full array method support
|
|
3659
|
+
* Tracks length changes, individual index operations, and collection-wide operations
|
|
3660
|
+
*/
|
|
3661
|
+
class ReactiveReadOnlyArrayClass extends indexable.Indexable(ReactiveBaseArray, {
|
|
3662
|
+
get(i) {
|
|
3663
|
+
dependant(this, i);
|
|
3664
|
+
return reactive(this[native$2][i]);
|
|
3665
|
+
},
|
|
3666
|
+
set(i, _value) {
|
|
3667
|
+
throw new ReadOnlyError(`Setting index ${i} on a read-only array`);
|
|
3668
|
+
},
|
|
3669
|
+
getLength() {
|
|
3670
|
+
dependant(this, 'length');
|
|
3671
|
+
return this[native$2].length;
|
|
3672
|
+
},
|
|
3673
|
+
setLength(value) {
|
|
3674
|
+
throw new ReadOnlyError(`Setting length to ${value} on a read-only array`);
|
|
3675
|
+
},
|
|
3676
|
+
}) {
|
|
3677
|
+
constructor(original) {
|
|
3678
|
+
super();
|
|
3679
|
+
Object.defineProperties(this, {
|
|
3680
|
+
// We have to make it double, as [native] must be `unique symbol` - impossible through import
|
|
3681
|
+
[native$2]: { value: original },
|
|
3682
|
+
[prototypeForwarding]: { value: original },
|
|
3683
|
+
});
|
|
3684
|
+
}
|
|
3685
|
+
push(..._items) {
|
|
3686
|
+
throw new ReadOnlyError(`Pushing items to a read-only array`);
|
|
3687
|
+
}
|
|
3688
|
+
pop() {
|
|
3689
|
+
throw new ReadOnlyError(`Popping from a read-only array`);
|
|
3690
|
+
}
|
|
3691
|
+
shift() {
|
|
3692
|
+
throw new ReadOnlyError(`Shifting from a read-only array`);
|
|
3693
|
+
}
|
|
3694
|
+
unshift(..._items) {
|
|
3695
|
+
throw new ReadOnlyError(`Unshifting items to a read-only array`);
|
|
3696
|
+
}
|
|
3697
|
+
splice(_start, _deleteCount, ..._items) {
|
|
3698
|
+
throw new ReadOnlyError(`Splice from a read-only array`);
|
|
3699
|
+
}
|
|
3700
|
+
reverse() {
|
|
3701
|
+
throw new ReadOnlyError(`Reversing a read-only array`);
|
|
3702
|
+
}
|
|
3703
|
+
sort(_compareFn) {
|
|
3704
|
+
throw new ReadOnlyError(`Sorting a read-only array`);
|
|
3705
|
+
}
|
|
3706
|
+
fill(_value, _start, _end) {
|
|
3707
|
+
throw new ReadOnlyError(`Filling a read-only array`);
|
|
3708
|
+
}
|
|
3709
|
+
copyWithin(_target, _start, _end) {
|
|
3710
|
+
throw new ReadOnlyError(`Copying within a read-only array`);
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
const ReactiveReadOnlyArray = reactive(ReactiveReadOnlyArrayClass);
|
|
3714
|
+
function mapped(inputs, compute, resize) {
|
|
3715
|
+
const result = [];
|
|
3716
|
+
const resultReactive = new ReactiveReadOnlyArray(result);
|
|
3717
|
+
const cleanups = [];
|
|
3718
|
+
function input(index) {
|
|
3719
|
+
return effect(function computedIndexedMapInputEffect() {
|
|
3720
|
+
result[index] = compute(inputs[index], index, resultReactive);
|
|
3721
|
+
touched1(resultReactive, { type: 'set', prop: index }, index);
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
const cleanupLength = effect(function computedMapLengthEffect({ ascend }) {
|
|
3725
|
+
const length = inputs.length;
|
|
3726
|
+
const resultLength = untracked(() => result.length);
|
|
3727
|
+
resize?.(length, resultLength);
|
|
3728
|
+
touched1(resultReactive, { type: 'set', prop: 'length' }, 'length');
|
|
3729
|
+
if (length < resultLength) {
|
|
3730
|
+
const toCleanup = cleanups.splice(length);
|
|
3731
|
+
for (const cleanup of toCleanup)
|
|
3732
|
+
cleanup();
|
|
3733
|
+
result.length = length;
|
|
3734
|
+
}
|
|
3735
|
+
else if (length > resultLength)
|
|
3736
|
+
// the input effects will be registered as the call's children, so they will remain not cleaned with this effect on length
|
|
3737
|
+
ascend(function computedMapNewElements() {
|
|
3738
|
+
for (let i = resultLength; i < length; i++)
|
|
3739
|
+
cleanups.push(input(i));
|
|
3740
|
+
});
|
|
3741
|
+
});
|
|
3742
|
+
return cleanedBy(resultReactive, () => {
|
|
3743
|
+
for (const cleanup of cleanups)
|
|
3744
|
+
cleanup();
|
|
3745
|
+
cleanups.length = 0;
|
|
3746
|
+
cleanupLength();
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
function reduced(inputs, compute) {
|
|
3750
|
+
const result = [];
|
|
3751
|
+
const resultReactive = new ReactiveReadOnlyArray(result);
|
|
3752
|
+
const cleanupFactor = effect(function computedReducedFactorEffect() {
|
|
3753
|
+
const factor = {};
|
|
3754
|
+
result.length = 0;
|
|
3755
|
+
for (const input of inputs)
|
|
3756
|
+
result.push(...compute(input, factor));
|
|
3757
|
+
touched(resultReactive, { type: 'invalidate', prop: 'reduced' });
|
|
3758
|
+
});
|
|
3759
|
+
return cleanedBy(resultReactive, cleanupFactor);
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
const memoizedRegistry = new WeakMap();
|
|
3763
|
+
function getBranch(tree, key) {
|
|
3764
|
+
tree.branches ?? (tree.branches = new WeakMap());
|
|
3765
|
+
let branch = tree.branches.get(key);
|
|
3766
|
+
if (!branch) {
|
|
3767
|
+
branch = {};
|
|
3768
|
+
tree.branches.set(key, branch);
|
|
3769
|
+
}
|
|
3770
|
+
return branch;
|
|
3771
|
+
}
|
|
3772
|
+
function memoizeFunction(fn) {
|
|
3773
|
+
const fnRoot = getRoot(fn);
|
|
3774
|
+
const existing = memoizedRegistry.get(fnRoot);
|
|
3775
|
+
if (existing)
|
|
3776
|
+
return existing;
|
|
3777
|
+
const cacheRoot = {};
|
|
3778
|
+
const memoized = markWithRoot((...args) => {
|
|
3779
|
+
const localArgs = args; //: Args = maxArgs !== undefined ? (args.slice(0, maxArgs) as Args) : args
|
|
3780
|
+
if (localArgs.some((arg) => !(arg && ['object', 'symbol', 'function'].includes(typeof arg))))
|
|
3781
|
+
throw new Error('memoize expects non-null object arguments');
|
|
3782
|
+
let node = cacheRoot;
|
|
3783
|
+
for (const arg of localArgs) {
|
|
3784
|
+
node = getBranch(node, arg);
|
|
3785
|
+
}
|
|
3786
|
+
dependant(node, 'memoize');
|
|
3787
|
+
if ('result' in node)
|
|
3788
|
+
return node.result;
|
|
3789
|
+
// Create memoize internal effect to track dependencies and invalidate cache
|
|
3790
|
+
// Use untracked to prevent the effect creation from being affected by parent effects
|
|
3791
|
+
node.cleanup = root(() => effect(markWithRoot(() => {
|
|
3792
|
+
// Execute the function and track its dependencies
|
|
3793
|
+
// The function execution will automatically track dependencies on reactive objects
|
|
3794
|
+
node.result = fn(...localArgs);
|
|
3795
|
+
return () => {
|
|
3796
|
+
// When dependencies change, clear the cache and notify consumers
|
|
3797
|
+
delete node.result;
|
|
3798
|
+
touched1(node, { type: 'invalidate', prop: localArgs }, 'memoize');
|
|
3799
|
+
};
|
|
3800
|
+
}, fnRoot), { opaque: true }));
|
|
3801
|
+
return node.result;
|
|
3802
|
+
}, fn);
|
|
3803
|
+
memoizedRegistry.set(fnRoot, memoized);
|
|
3804
|
+
memoizedRegistry.set(memoized, memoized);
|
|
3805
|
+
return memoized;
|
|
3806
|
+
}
|
|
3807
|
+
const memoize = decorator.decorator({
|
|
3808
|
+
getter(original, propertyKey) {
|
|
3809
|
+
const memoized = memoizeFunction(markWithRoot(decorator.renamed((that) => {
|
|
3810
|
+
return original.call(that);
|
|
3811
|
+
}, `${String(this.constructor.name)}.${String(propertyKey)}`), original));
|
|
3812
|
+
return function () {
|
|
3813
|
+
return memoized(this);
|
|
3814
|
+
};
|
|
3815
|
+
},
|
|
3816
|
+
method(original, name) {
|
|
3817
|
+
const memoized = memoizeFunction(markWithRoot(decorator.renamed((that, ...args) => {
|
|
3818
|
+
return original.call(that, ...args);
|
|
3819
|
+
}, `${String(this.constructor.name)}.${String(name)}`), original));
|
|
3820
|
+
return function (...args) {
|
|
3821
|
+
return memoized(this, ...args);
|
|
3822
|
+
};
|
|
3823
|
+
},
|
|
3824
|
+
default: memoizeFunction,
|
|
3825
|
+
});
|
|
3826
|
+
|
|
3827
|
+
// Helper to work around TypeScript limitation: base class expressions cannot reference class type parameters
|
|
3828
|
+
function getRegisterBase() {
|
|
3829
|
+
class RegisterBase extends indexable.Indexable(indexable.ArrayReadForward, {
|
|
3830
|
+
get(index) {
|
|
3831
|
+
return this[indexable.getAt](index);
|
|
3832
|
+
},
|
|
3833
|
+
set(index, value) {
|
|
3834
|
+
this[indexable.setAt](index, value);
|
|
3835
|
+
},
|
|
3836
|
+
getLength() {
|
|
3837
|
+
return this.length;
|
|
3838
|
+
},
|
|
3839
|
+
setLength(value) {
|
|
3840
|
+
this.length = value;
|
|
3841
|
+
},
|
|
3842
|
+
}) {
|
|
3843
|
+
toArray() {
|
|
3844
|
+
return Array.from(this);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return RegisterBase;
|
|
3848
|
+
}
|
|
3849
|
+
let RegisterClass = (() => {
|
|
3850
|
+
var _RegisterClass_instances, _RegisterClass_keyFn, _RegisterClass_keys, _RegisterClass_values, _RegisterClass_usage, _RegisterClass_valueInfo, _RegisterClass_keyEffects, _RegisterClass_ascend, _RegisterClass_rekeyValue;
|
|
3851
|
+
let _classDecorators = [unreactive];
|
|
3852
|
+
let _classDescriptor;
|
|
3853
|
+
let _classExtraInitializers = [];
|
|
3854
|
+
let _classThis;
|
|
3855
|
+
let _classSuper = getRegisterBase();
|
|
3856
|
+
_classThis = class extends _classSuper {
|
|
3857
|
+
get [(_RegisterClass_keyFn = new WeakMap(), _RegisterClass_keys = new WeakMap(), _RegisterClass_values = new WeakMap(), _RegisterClass_usage = new WeakMap(), _RegisterClass_valueInfo = new WeakMap(), _RegisterClass_keyEffects = new WeakMap(), _RegisterClass_ascend = new WeakMap(), _RegisterClass_instances = new WeakSet(), indexable.forwardArray)]() {
|
|
3858
|
+
return this.toArray();
|
|
3859
|
+
}
|
|
3860
|
+
constructor(keyFn, initial) {
|
|
3861
|
+
super();
|
|
3862
|
+
_RegisterClass_instances.add(this);
|
|
3863
|
+
_RegisterClass_keyFn.set(this, void 0);
|
|
3864
|
+
_RegisterClass_keys.set(this, void 0);
|
|
3865
|
+
_RegisterClass_values.set(this, void 0);
|
|
3866
|
+
_RegisterClass_usage.set(this, new Map());
|
|
3867
|
+
_RegisterClass_valueInfo.set(this, new Map());
|
|
3868
|
+
_RegisterClass_keyEffects.set(this, new Set());
|
|
3869
|
+
_RegisterClass_ascend.set(this, void 0);
|
|
3870
|
+
/* Moved below initialization */
|
|
3871
|
+
let ascendGet;
|
|
3872
|
+
effect(({ ascend }) => {
|
|
3873
|
+
ascendGet = ascend;
|
|
3874
|
+
});
|
|
3875
|
+
_tslib.__classPrivateFieldSet(this, _RegisterClass_ascend, ascendGet, "f");
|
|
3876
|
+
if (typeof keyFn !== 'function')
|
|
3877
|
+
throw new Error('Register requires a key function');
|
|
3878
|
+
_tslib.__classPrivateFieldSet(this, _RegisterClass_keyFn, keyFn, "f");
|
|
3879
|
+
_tslib.__classPrivateFieldSet(this, _RegisterClass_keys, reactive([]), "f");
|
|
3880
|
+
_tslib.__classPrivateFieldSet(this, _RegisterClass_values, reactive(new Map()), "f");
|
|
3881
|
+
Object.defineProperties(this, {
|
|
3882
|
+
[prototypeForwarding]: { value: _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f") },
|
|
3883
|
+
});
|
|
3884
|
+
if (initial)
|
|
3885
|
+
this.push(...initial);
|
|
3886
|
+
}
|
|
3887
|
+
ensureKey(value) {
|
|
3888
|
+
let info = _tslib.__classPrivateFieldGet(this, _RegisterClass_valueInfo, "f").get(value);
|
|
3889
|
+
if (info)
|
|
3890
|
+
return info.key;
|
|
3891
|
+
info = { key: undefined };
|
|
3892
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_valueInfo, "f").set(value, info);
|
|
3893
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_ascend, "f").call(this, () => {
|
|
3894
|
+
const stop = effect(({ reaction }) => {
|
|
3895
|
+
const nextKey = _tslib.__classPrivateFieldGet(this, _RegisterClass_keyFn, "f").call(this, value);
|
|
3896
|
+
this.assertValidKey(nextKey);
|
|
3897
|
+
const previousKey = info.key;
|
|
3898
|
+
if (reaction && previousKey !== undefined && !Object.is(nextKey, previousKey))
|
|
3899
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_instances, "m", _RegisterClass_rekeyValue).call(this, value, previousKey, nextKey);
|
|
3900
|
+
info.key = nextKey;
|
|
3901
|
+
});
|
|
3902
|
+
info.stop = stop;
|
|
3903
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keyEffects, "f").add(stop);
|
|
3904
|
+
});
|
|
3905
|
+
if (info.key === undefined)
|
|
3906
|
+
throw new Error('Register key function must return a property key');
|
|
3907
|
+
return info.key;
|
|
3908
|
+
}
|
|
3909
|
+
assertValidKey(key) {
|
|
3910
|
+
const type = typeof key;
|
|
3911
|
+
if (type !== 'string' && type !== 'number' && type !== 'symbol')
|
|
3912
|
+
throw new Error('Register key function must return a property key');
|
|
3913
|
+
}
|
|
3914
|
+
setKeyValue(key, value) {
|
|
3915
|
+
const existing = _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(key);
|
|
3916
|
+
if (existing !== undefined && existing !== value)
|
|
3917
|
+
this.cleanupValue(existing);
|
|
3918
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").set(key, value);
|
|
3919
|
+
}
|
|
3920
|
+
cleanupValue(value) {
|
|
3921
|
+
const info = _tslib.__classPrivateFieldGet(this, _RegisterClass_valueInfo, "f").get(value);
|
|
3922
|
+
if (!info)
|
|
3923
|
+
return;
|
|
3924
|
+
const stop = info.stop;
|
|
3925
|
+
if (stop) {
|
|
3926
|
+
info.stop = undefined;
|
|
3927
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keyEffects, "f").delete(stop);
|
|
3928
|
+
stop();
|
|
3929
|
+
}
|
|
3930
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_valueInfo, "f").delete(value);
|
|
3931
|
+
}
|
|
3932
|
+
disposeKeyEffects() {
|
|
3933
|
+
for (const value of Array.from(_tslib.__classPrivateFieldGet(this, _RegisterClass_valueInfo, "f").keys()))
|
|
3934
|
+
this.cleanupValue(value);
|
|
3935
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keyEffects, "f").clear();
|
|
3936
|
+
}
|
|
3937
|
+
incrementUsage(key) {
|
|
3938
|
+
const count = _tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").get(key) ?? 0;
|
|
3939
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").set(key, count + 1);
|
|
3940
|
+
}
|
|
3941
|
+
decrementUsage(key) {
|
|
3942
|
+
const count = _tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").get(key);
|
|
3943
|
+
if (!count)
|
|
3944
|
+
return;
|
|
3945
|
+
if (count <= 1) {
|
|
3946
|
+
const value = _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(key);
|
|
3947
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").delete(key);
|
|
3948
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").delete(key);
|
|
3949
|
+
if (value !== undefined)
|
|
3950
|
+
this.cleanupValue(value);
|
|
3951
|
+
}
|
|
3952
|
+
else {
|
|
3953
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").set(key, count - 1);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
normalizeIndex(index, allowEnd = false) {
|
|
3957
|
+
const length = this.length;
|
|
3958
|
+
let resolved = index;
|
|
3959
|
+
if (resolved < 0)
|
|
3960
|
+
resolved = Math.max(length + resolved, 0);
|
|
3961
|
+
if (resolved > length) {
|
|
3962
|
+
if (allowEnd)
|
|
3963
|
+
resolved = length;
|
|
3964
|
+
else
|
|
3965
|
+
throw new RangeError('Index out of bounds');
|
|
3966
|
+
}
|
|
3967
|
+
if (!allowEnd && resolved === length)
|
|
3968
|
+
throw new RangeError('Index out of bounds');
|
|
3969
|
+
return resolved;
|
|
3970
|
+
}
|
|
3971
|
+
assignAt(index, key, value) {
|
|
3972
|
+
const oldKey = _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f")[index];
|
|
3973
|
+
if (oldKey !== undefined && Object.is(oldKey, key)) {
|
|
3974
|
+
this.setKeyValue(key, value);
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
if (oldKey !== undefined)
|
|
3978
|
+
this.decrementUsage(oldKey);
|
|
3979
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f")[index] = key;
|
|
3980
|
+
this.incrementUsage(key);
|
|
3981
|
+
this.setKeyValue(key, value);
|
|
3982
|
+
}
|
|
3983
|
+
insertKeyValue(index, key, value) {
|
|
3984
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").splice(index, 0, key);
|
|
3985
|
+
this.incrementUsage(key);
|
|
3986
|
+
this.setKeyValue(key, value);
|
|
3987
|
+
}
|
|
3988
|
+
rebuildFrom(values) {
|
|
3989
|
+
this.disposeKeyEffects();
|
|
3990
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").splice(0, _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").length);
|
|
3991
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").clear();
|
|
3992
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").clear();
|
|
3993
|
+
for (const value of values) {
|
|
3994
|
+
const key = this.ensureKey(value);
|
|
3995
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").push(key);
|
|
3996
|
+
this.incrementUsage(key);
|
|
3997
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").set(key, value);
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
get length() {
|
|
4001
|
+
return _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").length;
|
|
4002
|
+
}
|
|
4003
|
+
[(_RegisterClass_rekeyValue = function _RegisterClass_rekeyValue(value, oldKey, newKey) {
|
|
4004
|
+
if (Object.is(oldKey, newKey))
|
|
4005
|
+
return;
|
|
4006
|
+
const existingValue = _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(newKey);
|
|
4007
|
+
if (existingValue !== undefined && existingValue !== value)
|
|
4008
|
+
throw new Error(`Register key collision for key ${String(newKey)}`);
|
|
4009
|
+
const count = _tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").get(oldKey);
|
|
4010
|
+
if (!count)
|
|
4011
|
+
return;
|
|
4012
|
+
const existingCount = _tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").get(newKey) ?? 0;
|
|
4013
|
+
this.setKeyValue(newKey, value);
|
|
4014
|
+
for (let i = 0; i < _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").length; i++)
|
|
4015
|
+
if (Object.is(_tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f")[i], oldKey))
|
|
4016
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f")[i] = newKey;
|
|
4017
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").set(newKey, existingCount + count);
|
|
4018
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").delete(oldKey);
|
|
4019
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").delete(oldKey);
|
|
4020
|
+
const updatedInfo = _tslib.__classPrivateFieldGet(this, _RegisterClass_valueInfo, "f").get(value);
|
|
4021
|
+
if (updatedInfo)
|
|
4022
|
+
updatedInfo.key = newKey;
|
|
4023
|
+
}, indexable.getAt)](index) {
|
|
4024
|
+
const key = _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f")[index];
|
|
4025
|
+
return key === undefined ? undefined : _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(key);
|
|
4026
|
+
}
|
|
4027
|
+
[indexable.setAt](index, value) {
|
|
4028
|
+
const key = this.ensureKey(value);
|
|
4029
|
+
if (index === this.length) {
|
|
4030
|
+
this.insertKeyValue(index, key, value);
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
const normalized = this.normalizeIndex(index);
|
|
4034
|
+
this.assignAt(normalized, key, value);
|
|
4035
|
+
}
|
|
4036
|
+
push(...items) {
|
|
4037
|
+
for (const item of items) {
|
|
4038
|
+
const key = this.ensureKey(item);
|
|
4039
|
+
this.insertKeyValue(this.length, key, item);
|
|
4040
|
+
}
|
|
4041
|
+
return this.length;
|
|
4042
|
+
}
|
|
4043
|
+
pop() {
|
|
4044
|
+
if (!this.length)
|
|
4045
|
+
return undefined;
|
|
4046
|
+
return this.removeAt(this.length - 1);
|
|
4047
|
+
}
|
|
4048
|
+
shift() {
|
|
4049
|
+
if (!this.length)
|
|
4050
|
+
return undefined;
|
|
4051
|
+
return this.removeAt(0);
|
|
4052
|
+
}
|
|
4053
|
+
unshift(...items) {
|
|
4054
|
+
let index = 0;
|
|
4055
|
+
for (const item of items) {
|
|
4056
|
+
const key = this.ensureKey(item);
|
|
4057
|
+
this.insertKeyValue(index++, key, item);
|
|
4058
|
+
}
|
|
4059
|
+
return this.length;
|
|
4060
|
+
}
|
|
4061
|
+
splice(start, deleteCount, ...items) {
|
|
4062
|
+
const normalizedStart = this.normalizeIndex(start, true);
|
|
4063
|
+
const maxDeletions = this.length - normalizedStart;
|
|
4064
|
+
const actualDelete = Math.min(deleteCount === undefined ? maxDeletions : Math.max(deleteCount, 0), maxDeletions);
|
|
4065
|
+
const keysToInsert = [];
|
|
4066
|
+
for (const item of items)
|
|
4067
|
+
keysToInsert.push(this.ensureKey(item));
|
|
4068
|
+
const removedKeys = _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").splice(normalizedStart, actualDelete, ...keysToInsert);
|
|
4069
|
+
const removedValues = [];
|
|
4070
|
+
for (const key of removedKeys) {
|
|
4071
|
+
if (key === undefined)
|
|
4072
|
+
continue;
|
|
4073
|
+
const value = _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(key);
|
|
4074
|
+
this.decrementUsage(key);
|
|
4075
|
+
removedValues.push(value);
|
|
4076
|
+
}
|
|
4077
|
+
for (let i = 0; i < keysToInsert.length; i++) {
|
|
4078
|
+
const key = keysToInsert[i];
|
|
4079
|
+
const value = items[i];
|
|
4080
|
+
this.incrementUsage(key);
|
|
4081
|
+
this.setKeyValue(key, value);
|
|
4082
|
+
}
|
|
4083
|
+
return removedValues;
|
|
4084
|
+
}
|
|
4085
|
+
clear() {
|
|
4086
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").length = 0;
|
|
4087
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").clear();
|
|
4088
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").clear();
|
|
4089
|
+
this.disposeKeyEffects();
|
|
4090
|
+
}
|
|
4091
|
+
get(key) {
|
|
4092
|
+
return _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(key);
|
|
4093
|
+
}
|
|
4094
|
+
set(key, value) {
|
|
4095
|
+
if (_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").has(key))
|
|
4096
|
+
this.setKeyValue(key, value);
|
|
4097
|
+
}
|
|
4098
|
+
remove(key) {
|
|
4099
|
+
let index = this.indexOfKey(key);
|
|
4100
|
+
while (index !== -1) {
|
|
4101
|
+
this.removeAt(index);
|
|
4102
|
+
index = this.indexOfKey(key);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
removeAt(index) {
|
|
4106
|
+
const [key] = _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").splice(index, 1);
|
|
4107
|
+
if (key === undefined)
|
|
4108
|
+
return undefined;
|
|
4109
|
+
const value = _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(key);
|
|
4110
|
+
this.decrementUsage(key);
|
|
4111
|
+
return value;
|
|
4112
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* Keep only the items for which the predicate returns true.
|
|
4115
|
+
* Items for which the predicate returns false are removed.
|
|
4116
|
+
*
|
|
4117
|
+
* The predicate is evaluated once per distinct key; duplicate keys
|
|
4118
|
+
* will follow the same keep/remove decision.
|
|
4119
|
+
*/
|
|
4120
|
+
keep(predicate) {
|
|
4121
|
+
const decisions = new Map();
|
|
4122
|
+
for (const [index, key] of _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").entries()) {
|
|
4123
|
+
if (decisions.has(key)) {
|
|
4124
|
+
if (!decisions.get(key))
|
|
4125
|
+
this.removeAt(index);
|
|
4126
|
+
continue;
|
|
4127
|
+
}
|
|
4128
|
+
const value = _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(key);
|
|
4129
|
+
const shouldKeep = predicate(value);
|
|
4130
|
+
decisions.set(key, shouldKeep);
|
|
4131
|
+
if (!shouldKeep)
|
|
4132
|
+
this.removeAt(index);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
hasKey(key) {
|
|
4136
|
+
return _tslib.__classPrivateFieldGet(this, _RegisterClass_usage, "f").has(key);
|
|
4137
|
+
}
|
|
4138
|
+
indexOfKey(key) {
|
|
4139
|
+
return _tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").indexOf(key);
|
|
4140
|
+
}
|
|
4141
|
+
mapKeys() {
|
|
4142
|
+
return _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").keys();
|
|
4143
|
+
}
|
|
4144
|
+
update(...values) {
|
|
4145
|
+
for (const value of values) {
|
|
4146
|
+
const key = this.ensureKey(value);
|
|
4147
|
+
if (_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").has(key))
|
|
4148
|
+
this.setKeyValue(key, value);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
upsert(insert, ...values) {
|
|
4152
|
+
for (const value of values) {
|
|
4153
|
+
const key = this.ensureKey(value);
|
|
4154
|
+
if (_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").has(key))
|
|
4155
|
+
this.setKeyValue(key, value);
|
|
4156
|
+
else
|
|
4157
|
+
insert(value);
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
entries() {
|
|
4161
|
+
const self = this;
|
|
4162
|
+
function* iterator() {
|
|
4163
|
+
for (let i = 0; i < _tslib.__classPrivateFieldGet(self, _RegisterClass_keys, "f").length; i++) {
|
|
4164
|
+
const val = _tslib.__classPrivateFieldGet(self, _RegisterClass_values, "f").get(_tslib.__classPrivateFieldGet(self, _RegisterClass_keys, "f")[i]);
|
|
4165
|
+
if (val !== undefined)
|
|
4166
|
+
yield [i, val];
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
return iterator();
|
|
4170
|
+
}
|
|
4171
|
+
[Symbol.iterator]() {
|
|
4172
|
+
const self = this;
|
|
4173
|
+
function* iterator() {
|
|
4174
|
+
for (const key of _tslib.__classPrivateFieldGet(self, _RegisterClass_keys, "f")) {
|
|
4175
|
+
const value = _tslib.__classPrivateFieldGet(self, _RegisterClass_values, "f").get(key);
|
|
4176
|
+
if (value !== undefined)
|
|
4177
|
+
yield value;
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
return iterator();
|
|
4181
|
+
}
|
|
4182
|
+
toString() {
|
|
4183
|
+
return `[Register length=${this.length}]`;
|
|
4184
|
+
}
|
|
4185
|
+
at(index) {
|
|
4186
|
+
const resolved = index < 0 ? this.length + index : index;
|
|
4187
|
+
if (resolved < 0 || resolved >= this.length)
|
|
4188
|
+
return undefined;
|
|
4189
|
+
return this[indexable.getAt](resolved);
|
|
4190
|
+
}
|
|
4191
|
+
reverse() {
|
|
4192
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").reverse();
|
|
4193
|
+
return this;
|
|
4194
|
+
}
|
|
4195
|
+
sort(compareFn) {
|
|
4196
|
+
const fwdCompareFn = compareFn
|
|
4197
|
+
? (a, b) => compareFn(_tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(a), _tslib.__classPrivateFieldGet(this, _RegisterClass_values, "f").get(b))
|
|
4198
|
+
: undefined;
|
|
4199
|
+
_tslib.__classPrivateFieldGet(this, _RegisterClass_keys, "f").sort(fwdCompareFn);
|
|
4200
|
+
return this;
|
|
4201
|
+
}
|
|
4202
|
+
fill(value, start = 0, end = this.length) {
|
|
4203
|
+
const values = this.toArray();
|
|
4204
|
+
values.fill(value, start, end);
|
|
4205
|
+
this.rebuildFrom(values);
|
|
4206
|
+
return this;
|
|
4207
|
+
}
|
|
4208
|
+
copyWithin(target, start, end) {
|
|
4209
|
+
const values = this.toArray();
|
|
4210
|
+
values.copyWithin(target, start, end);
|
|
4211
|
+
this.rebuildFrom(values);
|
|
4212
|
+
return this;
|
|
4213
|
+
}
|
|
4214
|
+
};
|
|
4215
|
+
_tslib.__setFunctionName(_classThis, "RegisterClass");
|
|
4216
|
+
(() => {
|
|
4217
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
4218
|
+
_tslib.__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
4219
|
+
_classThis = _classDescriptor.value;
|
|
4220
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
4221
|
+
_tslib.__runInitializers(_classThis, _classExtraInitializers);
|
|
4222
|
+
})();
|
|
4223
|
+
return _classThis;
|
|
4224
|
+
})();
|
|
4225
|
+
const Register = RegisterClass;
|
|
4226
|
+
function register(keyFn, initial) {
|
|
4227
|
+
return new RegisterClass(keyFn, initial);
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
function defineAccessValue(access) {
|
|
4231
|
+
Object.defineProperty(access, 'value', {
|
|
4232
|
+
get: access.get,
|
|
4233
|
+
set: access.set,
|
|
4234
|
+
configurable: true,
|
|
4235
|
+
enumerable: true,
|
|
4236
|
+
});
|
|
4237
|
+
}
|
|
4238
|
+
function makeCleanup(target, effectMap, onDispose) {
|
|
4239
|
+
return cleanedBy(target, () => {
|
|
4240
|
+
onDispose();
|
|
4241
|
+
for (const stop of effectMap.values())
|
|
4242
|
+
stop?.();
|
|
4243
|
+
effectMap.clear();
|
|
4244
|
+
});
|
|
4245
|
+
}
|
|
4246
|
+
function projectArray(source, apply) {
|
|
4247
|
+
const observedSource = reactive(source);
|
|
4248
|
+
const target = reactive([]);
|
|
4249
|
+
const indexEffects = new Map();
|
|
4250
|
+
function normalizeTargetLength(length) {
|
|
4251
|
+
decorator.ReflectSet(target, 'length', length, target);
|
|
4252
|
+
}
|
|
4253
|
+
function disposeIndex(index) {
|
|
4254
|
+
const stopEffect = indexEffects.get(index);
|
|
4255
|
+
if (stopEffect) {
|
|
4256
|
+
indexEffects.delete(index);
|
|
4257
|
+
stopEffect();
|
|
4258
|
+
Reflect.deleteProperty(target, index);
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
const cleanupLength = effect(function projectArrayLengthEffect({ ascend }) {
|
|
4262
|
+
const length = observedSource.length;
|
|
4263
|
+
normalizeTargetLength(length);
|
|
4264
|
+
const existing = Array.from(indexEffects.keys());
|
|
4265
|
+
for (let i = 0; i < length; i++) {
|
|
4266
|
+
if (indexEffects.has(i))
|
|
4267
|
+
continue;
|
|
4268
|
+
ascend(() => {
|
|
4269
|
+
const index = i;
|
|
4270
|
+
const stop = effect(function projectArrayIndexEffect() {
|
|
4271
|
+
const previous = untracked(() => target[index]);
|
|
4272
|
+
const accessBase = {
|
|
4273
|
+
key: index,
|
|
4274
|
+
source: observedSource,
|
|
4275
|
+
get: () => decorator.ReflectGet(observedSource, index, observedSource),
|
|
4276
|
+
set: (value) => decorator.ReflectSet(observedSource, index, value, observedSource),
|
|
4277
|
+
old: previous,
|
|
4278
|
+
};
|
|
4279
|
+
defineAccessValue(accessBase);
|
|
4280
|
+
const produced = apply(accessBase, target);
|
|
4281
|
+
target[index] = produced;
|
|
4282
|
+
});
|
|
4283
|
+
indexEffects.set(i, stop);
|
|
4284
|
+
});
|
|
4285
|
+
}
|
|
4286
|
+
for (const index of existing)
|
|
4287
|
+
if (index >= length)
|
|
4288
|
+
disposeIndex(index);
|
|
4289
|
+
});
|
|
4290
|
+
return makeCleanup(target, indexEffects, () => cleanupLength());
|
|
4291
|
+
}
|
|
4292
|
+
function projectRegister(source, apply) {
|
|
4293
|
+
const observedSource = reactive(source);
|
|
4294
|
+
const rawTarget = new Map();
|
|
4295
|
+
const target = reactive(rawTarget);
|
|
4296
|
+
const keyEffects = new Map();
|
|
4297
|
+
function disposeKey(key) {
|
|
4298
|
+
const stopEffect = keyEffects.get(key);
|
|
4299
|
+
if (stopEffect) {
|
|
4300
|
+
stopEffect();
|
|
4301
|
+
keyEffects.delete(key);
|
|
4302
|
+
target.delete(key);
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
const cleanupKeys = effect(function projectRegisterEffect({ ascend }) {
|
|
4306
|
+
const keys = new Set();
|
|
4307
|
+
for (const key of observedSource.mapKeys())
|
|
4308
|
+
keys.add(key);
|
|
4309
|
+
for (const key of keys) {
|
|
4310
|
+
if (keyEffects.has(key))
|
|
4311
|
+
continue;
|
|
4312
|
+
ascend(() => {
|
|
4313
|
+
const stop = effect(function projectRegisterKeyEffect() {
|
|
4314
|
+
const previous = untracked(() => target.get(key));
|
|
4315
|
+
const accessBase = {
|
|
4316
|
+
key,
|
|
4317
|
+
source: observedSource,
|
|
4318
|
+
get: () => observedSource.get(key),
|
|
4319
|
+
set: (value) => {
|
|
4320
|
+
observedSource.set(key, value);
|
|
4321
|
+
return true;
|
|
4322
|
+
},
|
|
4323
|
+
old: previous,
|
|
4324
|
+
};
|
|
4325
|
+
defineAccessValue(accessBase);
|
|
4326
|
+
const produced = apply(accessBase, target);
|
|
4327
|
+
target.set(key, produced);
|
|
4328
|
+
});
|
|
4329
|
+
keyEffects.set(key, stop);
|
|
4330
|
+
});
|
|
4331
|
+
}
|
|
4332
|
+
for (const key of Array.from(keyEffects.keys()))
|
|
4333
|
+
if (!keys.has(key))
|
|
4334
|
+
disposeKey(key);
|
|
4335
|
+
});
|
|
4336
|
+
return makeCleanup(target, keyEffects, () => cleanupKeys());
|
|
4337
|
+
}
|
|
4338
|
+
function projectRecord(source, apply) {
|
|
4339
|
+
const observedSource = reactive(source);
|
|
4340
|
+
const target = reactive({});
|
|
4341
|
+
const keyEffects = new Map();
|
|
4342
|
+
function disposeKey(key) {
|
|
4343
|
+
const stopEffect = keyEffects.get(key);
|
|
4344
|
+
if (stopEffect) {
|
|
4345
|
+
stopEffect();
|
|
4346
|
+
keyEffects.delete(key);
|
|
4347
|
+
Reflect.deleteProperty(target, key);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
const cleanupKeys = effect(function projectRecordEffect({ ascend }) {
|
|
4351
|
+
const keys = new Set();
|
|
4352
|
+
for (const key in observedSource)
|
|
4353
|
+
keys.add(key);
|
|
4354
|
+
const observed = Reflect.ownKeys(observedSource);
|
|
4355
|
+
for (const key of observed)
|
|
4356
|
+
keys.add(key);
|
|
4357
|
+
for (const key of keys) {
|
|
4358
|
+
if (keyEffects.has(key))
|
|
4359
|
+
continue;
|
|
4360
|
+
ascend(() => {
|
|
4361
|
+
const stop = effect(function projectRecordKeyEffect() {
|
|
4362
|
+
const sourceKey = key;
|
|
4363
|
+
const previous = untracked(() => target[key]);
|
|
4364
|
+
const accessBase = {
|
|
4365
|
+
key: sourceKey,
|
|
4366
|
+
source: observedSource,
|
|
4367
|
+
get: () => decorator.ReflectGet(observedSource, sourceKey, observedSource),
|
|
4368
|
+
set: (value) => decorator.ReflectSet(observedSource, sourceKey, value, observedSource),
|
|
4369
|
+
old: previous,
|
|
4370
|
+
};
|
|
4371
|
+
defineAccessValue(accessBase);
|
|
4372
|
+
const produced = apply(accessBase, target);
|
|
4373
|
+
target[sourceKey] = produced;
|
|
4374
|
+
});
|
|
4375
|
+
keyEffects.set(key, stop);
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
for (const key of Array.from(keyEffects.keys()))
|
|
4379
|
+
if (!keys.has(key))
|
|
4380
|
+
disposeKey(key);
|
|
4381
|
+
});
|
|
4382
|
+
return makeCleanup(target, keyEffects, () => cleanupKeys());
|
|
4383
|
+
}
|
|
4384
|
+
function projectMap(source, apply) {
|
|
4385
|
+
const observedSource = reactive(source);
|
|
4386
|
+
const rawTarget = new Map();
|
|
4387
|
+
const target = reactive(rawTarget);
|
|
4388
|
+
const keyEffects = new Map();
|
|
4389
|
+
function disposeKey(key) {
|
|
4390
|
+
const stopEffect = keyEffects.get(key);
|
|
4391
|
+
if (stopEffect) {
|
|
4392
|
+
stopEffect();
|
|
4393
|
+
keyEffects.delete(key);
|
|
4394
|
+
target.delete(key);
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
const cleanupKeys = effect(function projectMapEffect({ ascend }) {
|
|
4398
|
+
const keys = new Set();
|
|
4399
|
+
for (const key of observedSource.keys())
|
|
4400
|
+
keys.add(key);
|
|
4401
|
+
for (const key of keys) {
|
|
4402
|
+
if (keyEffects.has(key))
|
|
4403
|
+
continue;
|
|
4404
|
+
ascend(() => {
|
|
4405
|
+
const stop = effect(function projectMapKeyEffect() {
|
|
4406
|
+
const previous = untracked(() => target.get(key));
|
|
4407
|
+
const accessBase = {
|
|
4408
|
+
key,
|
|
4409
|
+
source: observedSource,
|
|
4410
|
+
get: () => observedSource.get(key),
|
|
4411
|
+
set: (value) => {
|
|
4412
|
+
observedSource.set(key, value);
|
|
4413
|
+
return true;
|
|
4414
|
+
},
|
|
4415
|
+
old: previous,
|
|
4416
|
+
};
|
|
4417
|
+
defineAccessValue(accessBase);
|
|
4418
|
+
const produced = apply(accessBase, target);
|
|
4419
|
+
target.set(key, produced);
|
|
4420
|
+
});
|
|
4421
|
+
keyEffects.set(key, stop);
|
|
4422
|
+
});
|
|
4423
|
+
}
|
|
4424
|
+
for (const key of Array.from(keyEffects.keys()))
|
|
4425
|
+
if (!keys.has(key))
|
|
4426
|
+
disposeKey(key);
|
|
4427
|
+
});
|
|
4428
|
+
return makeCleanup(target, keyEffects, () => cleanupKeys());
|
|
4429
|
+
}
|
|
4430
|
+
function projectCore(source, apply) {
|
|
4431
|
+
if (Array.isArray(source))
|
|
4432
|
+
return projectArray(source, apply);
|
|
4433
|
+
if (source instanceof Map)
|
|
4434
|
+
return projectMap(source, apply);
|
|
4435
|
+
if (source instanceof Register)
|
|
4436
|
+
return projectRegister(source, apply);
|
|
4437
|
+
if (source && (source.constructor === Object || source.constructor === undefined))
|
|
4438
|
+
return projectRecord(source, apply);
|
|
4439
|
+
throw new Error('Unsupported source type');
|
|
4440
|
+
}
|
|
4441
|
+
const project = Object.assign(projectCore, {
|
|
4442
|
+
array: projectArray,
|
|
4443
|
+
register: projectRegister,
|
|
4444
|
+
record: projectRecord,
|
|
4445
|
+
map: projectMap,
|
|
4446
|
+
});
|
|
4447
|
+
|
|
4448
|
+
/**
|
|
4449
|
+
* Organizes a source object's properties into a target object using a callback function.
|
|
4450
|
+
* This creates a reactive mapping between source properties and a target object,
|
|
4451
|
+
* automatically handling property additions, updates, and removals.
|
|
4452
|
+
*
|
|
4453
|
+
* @template Source - The type of the source object
|
|
4454
|
+
* @template Target - The type of the target object (defaults to Record<PropertyKey, any>)
|
|
4455
|
+
*
|
|
4456
|
+
* @param {Source} source - The source object to organize
|
|
4457
|
+
* @param {OrganizedCallback<Source, Target>} apply - Callback function that defines how each source property is mapped to the target
|
|
4458
|
+
* @param {Target} [baseTarget={}] - Optional base target object to use (will be made reactive if not already)
|
|
4459
|
+
*
|
|
4460
|
+
* @returns {OrganizedResult<Target>} The target object with cleanup capability
|
|
4461
|
+
*
|
|
4462
|
+
* @example
|
|
4463
|
+
* // Organize user permissions into role-based access
|
|
4464
|
+
* const user = reactive({ isAdmin: true, canEdit: false });
|
|
4465
|
+
* const permissions = organized(
|
|
4466
|
+
* user,
|
|
4467
|
+
* (access, target) => {
|
|
4468
|
+
* if (access.key === 'isAdmin') {
|
|
4469
|
+
* target.hasFullAccess = access.value;
|
|
4470
|
+
* }
|
|
4471
|
+
* target[`can${access.key.charAt(0).toUpperCase() + access.key.slice(1)}`] = access.value;
|
|
4472
|
+
* }
|
|
4473
|
+
* );
|
|
4474
|
+
*
|
|
4475
|
+
* @example
|
|
4476
|
+
* // Transform object structure with cleanup
|
|
4477
|
+
* const source = reactive({ firstName: 'John', lastName: 'Doe' });
|
|
4478
|
+
* const formatted = organized(
|
|
4479
|
+
* source,
|
|
4480
|
+
* (access, target) => {
|
|
4481
|
+
* if (access.key === 'firstName' || access.key === 'lastName') {
|
|
4482
|
+
* target.fullName = `${source.firstName} ${source.lastName}`.trim();
|
|
4483
|
+
* }
|
|
4484
|
+
* }
|
|
4485
|
+
* );
|
|
4486
|
+
*
|
|
4487
|
+
* @example
|
|
4488
|
+
* // Using with cleanup in a component
|
|
4489
|
+
* effect(() => {
|
|
4490
|
+
* const data = fetchData();
|
|
4491
|
+
* const organizedData = organized(data, (access, target) => {
|
|
4492
|
+
* // Transform data
|
|
4493
|
+
* });
|
|
4494
|
+
*
|
|
4495
|
+
* // The cleanup will be called automatically when the effect is disposed
|
|
4496
|
+
* return () => organizedData[cleanup]();
|
|
4497
|
+
* });
|
|
4498
|
+
*/
|
|
4499
|
+
function organized(source, apply, baseTarget = {}) {
|
|
4500
|
+
const observedSource = reactive(source);
|
|
4501
|
+
const target = reactive(baseTarget);
|
|
4502
|
+
const keyEffects = new Map();
|
|
4503
|
+
function disposeKey(key) {
|
|
4504
|
+
const stopEffect = keyEffects.get(key);
|
|
4505
|
+
if (stopEffect) {
|
|
4506
|
+
keyEffects.delete(key);
|
|
4507
|
+
stopEffect();
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
const cleanupKeys = effect(function organizedKeysEffect({ ascend }) {
|
|
4511
|
+
//const keys = Reflect.ownKeys(observedSource) as PropertyKey[]
|
|
4512
|
+
const keys = new Set();
|
|
4513
|
+
for (const key in observedSource)
|
|
4514
|
+
keys.add(key);
|
|
4515
|
+
for (const key of keys) {
|
|
4516
|
+
if (keyEffects.has(key))
|
|
4517
|
+
continue;
|
|
4518
|
+
ascend(() => {
|
|
4519
|
+
const stop = effect(function organizedKeyEffect() {
|
|
4520
|
+
const sourceKey = key;
|
|
4521
|
+
const accessBase = {
|
|
4522
|
+
key: sourceKey,
|
|
4523
|
+
get: () => decorator.ReflectGet(observedSource, sourceKey, observedSource),
|
|
4524
|
+
set: (value) => decorator.ReflectSet(observedSource, sourceKey, value, observedSource),
|
|
4525
|
+
};
|
|
4526
|
+
Object.defineProperty(accessBase, 'value', {
|
|
4527
|
+
get: accessBase.get,
|
|
4528
|
+
set: accessBase.set,
|
|
4529
|
+
configurable: true,
|
|
4530
|
+
enumerable: true,
|
|
4531
|
+
});
|
|
4532
|
+
return apply(accessBase, target);
|
|
4533
|
+
});
|
|
4534
|
+
keyEffects.set(key, stop);
|
|
4535
|
+
});
|
|
4536
|
+
}
|
|
4537
|
+
for (const key of Array.from(keyEffects.keys()))
|
|
4538
|
+
if (!keys.has(key))
|
|
4539
|
+
disposeKey(key);
|
|
4540
|
+
});
|
|
4541
|
+
return cleanedBy(target, () => {
|
|
4542
|
+
cleanupKeys();
|
|
4543
|
+
for (const key of Array.from(keyEffects.keys()))
|
|
4544
|
+
disposeKey(key);
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
/**
|
|
4548
|
+
* Organizes a property on a target object
|
|
4549
|
+
* Shortcut for defineProperty/delete with touched signal
|
|
4550
|
+
* @param target - The target object
|
|
4551
|
+
* @param property - The property to organize
|
|
4552
|
+
* @param access - The access object
|
|
4553
|
+
* @returns The property descriptor
|
|
4554
|
+
*/
|
|
4555
|
+
function organize(target, property, access) {
|
|
4556
|
+
Object.defineProperty(target, property, {
|
|
4557
|
+
get: access.get,
|
|
4558
|
+
set: access.set,
|
|
4559
|
+
configurable: true,
|
|
4560
|
+
enumerable: true,
|
|
4561
|
+
});
|
|
4562
|
+
touched1(target, { type: 'set', prop: property }, property);
|
|
4563
|
+
return () => delete target[property];
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
const native$1 = Symbol('native');
|
|
4567
|
+
/**
|
|
4568
|
+
* Reactive wrapper around JavaScript's WeakMap class
|
|
4569
|
+
* Only tracks individual key operations, no size tracking (WeakMap limitation)
|
|
4570
|
+
*/
|
|
4571
|
+
class ReactiveWeakMap {
|
|
4572
|
+
constructor(original) {
|
|
4573
|
+
Object.defineProperties(this, {
|
|
4574
|
+
[native$1]: { value: original },
|
|
4575
|
+
[prototypeForwarding]: { value: original },
|
|
4576
|
+
content: { value: Symbol('content') },
|
|
4577
|
+
[Symbol.toStringTag]: { value: 'ReactiveWeakMap' },
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
// Implement WeakMap interface methods with reactivity
|
|
4581
|
+
delete(key) {
|
|
4582
|
+
const hadKey = this[native$1].has(key);
|
|
4583
|
+
const result = this[native$1].delete(key);
|
|
4584
|
+
if (hadKey)
|
|
4585
|
+
touched1(this.content, { type: 'del', prop: key }, key);
|
|
4586
|
+
return result;
|
|
4587
|
+
}
|
|
4588
|
+
get(key) {
|
|
4589
|
+
dependant(this.content, key);
|
|
4590
|
+
return reactive(this[native$1].get(key));
|
|
4591
|
+
}
|
|
4592
|
+
has(key) {
|
|
4593
|
+
dependant(this.content, key);
|
|
4594
|
+
return this[native$1].has(key);
|
|
4595
|
+
}
|
|
4596
|
+
set(key, value) {
|
|
4597
|
+
const hadKey = this[native$1].has(key);
|
|
4598
|
+
const oldValue = this[native$1].get(key);
|
|
4599
|
+
const reactiveValue = reactive(value);
|
|
4600
|
+
this[native$1].set(key, reactiveValue);
|
|
4601
|
+
if (!hadKey || oldValue !== reactiveValue) {
|
|
4602
|
+
notifyPropertyChange(this.content, key, oldValue, reactiveValue, hadKey);
|
|
4603
|
+
}
|
|
4604
|
+
return this;
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
/**
|
|
4608
|
+
* Reactive wrapper around JavaScript's Map class
|
|
4609
|
+
* Tracks size changes, individual key operations, and collection-wide operations
|
|
4610
|
+
*/
|
|
4611
|
+
class ReactiveMap {
|
|
4612
|
+
constructor(original) {
|
|
4613
|
+
Object.defineProperties(this, {
|
|
4614
|
+
[native$1]: { value: original },
|
|
4615
|
+
[prototypeForwarding]: { value: original },
|
|
4616
|
+
content: { value: Symbol('content') },
|
|
4617
|
+
[Symbol.toStringTag]: { value: 'ReactiveMap' },
|
|
4618
|
+
});
|
|
4619
|
+
}
|
|
4620
|
+
// Implement Map interface methods with reactivity
|
|
4621
|
+
get size() {
|
|
4622
|
+
dependant(this, 'size'); // The ReactiveMap instance still goes through proxy
|
|
4623
|
+
return this[native$1].size;
|
|
4624
|
+
}
|
|
4625
|
+
clear() {
|
|
4626
|
+
const hadEntries = this[native$1].size > 0;
|
|
4627
|
+
this[native$1].clear();
|
|
4628
|
+
if (hadEntries) {
|
|
4629
|
+
const evolution = { type: 'bunch', method: 'clear' };
|
|
4630
|
+
// Clear triggers all effects since all keys are affected
|
|
4631
|
+
touched1(this, evolution, 'size');
|
|
4632
|
+
touched(this.content, evolution);
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
entries() {
|
|
4636
|
+
dependant(this.content);
|
|
4637
|
+
return makeReactiveEntriesIterator(this[native$1].entries());
|
|
4638
|
+
}
|
|
4639
|
+
forEach(callbackfn, thisArg) {
|
|
4640
|
+
dependant(this.content);
|
|
4641
|
+
this[native$1].forEach(callbackfn, thisArg);
|
|
4642
|
+
}
|
|
4643
|
+
keys() {
|
|
4644
|
+
dependant(this.content);
|
|
4645
|
+
return this[native$1].keys();
|
|
4646
|
+
}
|
|
4647
|
+
values() {
|
|
4648
|
+
dependant(this.content);
|
|
4649
|
+
return makeReactiveIterator(this[native$1].values());
|
|
4650
|
+
}
|
|
4651
|
+
[Symbol.iterator]() {
|
|
4652
|
+
dependant(this.content);
|
|
4653
|
+
const nativeIterator = this[native$1][Symbol.iterator]();
|
|
4654
|
+
return {
|
|
4655
|
+
next() {
|
|
4656
|
+
const result = nativeIterator.next();
|
|
4657
|
+
if (result.done) {
|
|
4658
|
+
return result;
|
|
4659
|
+
}
|
|
4660
|
+
return {
|
|
4661
|
+
value: [result.value[0], reactive(result.value[1])],
|
|
4662
|
+
done: false,
|
|
4663
|
+
};
|
|
4664
|
+
},
|
|
4665
|
+
};
|
|
4666
|
+
}
|
|
4667
|
+
// Implement Map methods with reactivity
|
|
4668
|
+
delete(key) {
|
|
4669
|
+
const hadKey = this[native$1].has(key);
|
|
4670
|
+
const result = this[native$1].delete(key);
|
|
4671
|
+
if (hadKey) {
|
|
4672
|
+
const evolution = { type: 'del', prop: key };
|
|
4673
|
+
touched1(this.content, evolution, key);
|
|
4674
|
+
touched1(this, evolution, 'size');
|
|
4675
|
+
}
|
|
4676
|
+
return result;
|
|
4677
|
+
}
|
|
4678
|
+
get(key) {
|
|
4679
|
+
dependant(this.content, key);
|
|
4680
|
+
return reactive(this[native$1].get(key));
|
|
4681
|
+
}
|
|
4682
|
+
has(key) {
|
|
4683
|
+
dependant(this.content, key);
|
|
4684
|
+
return this[native$1].has(key);
|
|
4685
|
+
}
|
|
4686
|
+
set(key, value) {
|
|
4687
|
+
const hadKey = this[native$1].has(key);
|
|
4688
|
+
const oldValue = this[native$1].get(key);
|
|
4689
|
+
const reactiveValue = reactive(value);
|
|
4690
|
+
this[native$1].set(key, reactiveValue);
|
|
4691
|
+
if (!hadKey || oldValue !== reactiveValue) {
|
|
4692
|
+
notifyPropertyChange(this.content, key, oldValue, reactiveValue, hadKey);
|
|
4693
|
+
// Also notify size change for Map (WeakMap doesn't track size)
|
|
4694
|
+
const evolution = { type: hadKey ? 'set' : 'add', prop: key };
|
|
4695
|
+
touched1(this, evolution, 'size');
|
|
4696
|
+
}
|
|
4697
|
+
return this;
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
const native = Symbol('native');
|
|
4702
|
+
/**
|
|
4703
|
+
* Reactive wrapper around JavaScript's WeakSet class
|
|
4704
|
+
* Only tracks individual value operations, no size tracking (WeakSet limitation)
|
|
4705
|
+
*/
|
|
4706
|
+
class ReactiveWeakSet {
|
|
4707
|
+
constructor(original) {
|
|
4708
|
+
Object.defineProperties(this, {
|
|
4709
|
+
[native]: { value: original },
|
|
4710
|
+
[prototypeForwarding]: { value: original },
|
|
4711
|
+
content: { value: Symbol('content') },
|
|
4712
|
+
[Symbol.toStringTag]: { value: 'ReactiveWeakSet' },
|
|
4713
|
+
});
|
|
4714
|
+
}
|
|
4715
|
+
add(value) {
|
|
4716
|
+
const had = this[native].has(value);
|
|
4717
|
+
this[native].add(value);
|
|
4718
|
+
if (!had) {
|
|
4719
|
+
// touch the specific value and the collection view
|
|
4720
|
+
touched1(this.content, { type: 'add', prop: value }, value);
|
|
4721
|
+
// no size/allProps for WeakSet
|
|
4722
|
+
}
|
|
4723
|
+
return this;
|
|
4724
|
+
}
|
|
4725
|
+
delete(value) {
|
|
4726
|
+
const had = this[native].has(value);
|
|
4727
|
+
const res = this[native].delete(value);
|
|
4728
|
+
if (had)
|
|
4729
|
+
touched1(this.content, { type: 'del', prop: value }, value);
|
|
4730
|
+
return res;
|
|
4731
|
+
}
|
|
4732
|
+
has(value) {
|
|
4733
|
+
dependant(this.content, value);
|
|
4734
|
+
return this[native].has(value);
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
/**
|
|
4738
|
+
* Reactive wrapper around JavaScript's Set class
|
|
4739
|
+
* Tracks size changes, individual value operations, and collection-wide operations
|
|
4740
|
+
*/
|
|
4741
|
+
class ReactiveSet {
|
|
4742
|
+
constructor(original) {
|
|
4743
|
+
Object.defineProperties(this, {
|
|
4744
|
+
[native]: { value: original },
|
|
4745
|
+
[prototypeForwarding]: { value: original },
|
|
4746
|
+
content: { value: Symbol('content') },
|
|
4747
|
+
[Symbol.toStringTag]: { value: 'ReactiveSet' },
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
get size() {
|
|
4751
|
+
// size depends on the wrapper instance, like Map counterpart
|
|
4752
|
+
dependant(this, 'size');
|
|
4753
|
+
return this[native].size;
|
|
4754
|
+
}
|
|
4755
|
+
add(value) {
|
|
4756
|
+
const had = this[native].has(value);
|
|
4757
|
+
const reactiveValue = reactive(value);
|
|
4758
|
+
this[native].add(reactiveValue);
|
|
4759
|
+
if (!had) {
|
|
4760
|
+
const evolution = { type: 'add', prop: reactiveValue };
|
|
4761
|
+
// touch for value-specific and aggregate dependencies
|
|
4762
|
+
touched1(this.content, evolution, reactiveValue);
|
|
4763
|
+
touched1(this, evolution, 'size');
|
|
4764
|
+
}
|
|
4765
|
+
return this;
|
|
4766
|
+
}
|
|
4767
|
+
clear() {
|
|
4768
|
+
const hadEntries = this[native].size > 0;
|
|
4769
|
+
this[native].clear();
|
|
4770
|
+
if (hadEntries) {
|
|
4771
|
+
const evolution = { type: 'bunch', method: 'clear' };
|
|
4772
|
+
touched1(this, evolution, 'size');
|
|
4773
|
+
touched(this.content, evolution);
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
delete(value) {
|
|
4777
|
+
const had = this[native].has(value);
|
|
4778
|
+
const res = this[native].delete(value);
|
|
4779
|
+
if (had) {
|
|
4780
|
+
const evolution = { type: 'del', prop: value };
|
|
4781
|
+
touched1(this.content, evolution, value);
|
|
4782
|
+
touched1(this, evolution, 'size');
|
|
4783
|
+
}
|
|
4784
|
+
return res;
|
|
4785
|
+
}
|
|
4786
|
+
has(value) {
|
|
4787
|
+
dependant(this.content, value);
|
|
4788
|
+
return this[native].has(value);
|
|
4789
|
+
}
|
|
4790
|
+
entries() {
|
|
4791
|
+
dependant(this.content);
|
|
4792
|
+
return makeReactiveEntriesIterator(this[native].entries());
|
|
4793
|
+
}
|
|
4794
|
+
forEach(callbackfn, thisArg) {
|
|
4795
|
+
dependant(this.content);
|
|
4796
|
+
this[native].forEach(callbackfn, thisArg);
|
|
4797
|
+
}
|
|
4798
|
+
keys() {
|
|
4799
|
+
dependant(this.content);
|
|
4800
|
+
return makeReactiveIterator(this[native].keys());
|
|
4801
|
+
}
|
|
4802
|
+
values() {
|
|
4803
|
+
dependant(this.content);
|
|
4804
|
+
return makeReactiveIterator(this[native].values());
|
|
4805
|
+
}
|
|
4806
|
+
[Symbol.iterator]() {
|
|
4807
|
+
dependant(this.content);
|
|
4808
|
+
const nativeIterator = this[native][Symbol.iterator]();
|
|
4809
|
+
return {
|
|
4810
|
+
next() {
|
|
4811
|
+
const result = nativeIterator.next();
|
|
4812
|
+
if (result.done) {
|
|
4813
|
+
return result;
|
|
4814
|
+
}
|
|
4815
|
+
return { value: reactive(result.value), done: false };
|
|
4816
|
+
},
|
|
4817
|
+
};
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
// Register native collection types to use specialized reactive wrappers
|
|
4822
|
+
registerNativeReactivity(WeakMap, ReactiveWeakMap);
|
|
4823
|
+
registerNativeReactivity(Map, ReactiveMap);
|
|
4824
|
+
registerNativeReactivity(WeakSet, ReactiveWeakSet);
|
|
4825
|
+
registerNativeReactivity(Set, ReactiveSet);
|
|
4826
|
+
registerNativeReactivity(Array, ReactiveArray);
|
|
4827
|
+
/**
|
|
4828
|
+
* Object containing internal reactive system state for debugging and profiling
|
|
4829
|
+
*/
|
|
4830
|
+
const profileInfo = {
|
|
4831
|
+
objectToProxy,
|
|
4832
|
+
proxyToObject,
|
|
4833
|
+
effectToReactiveObjects,
|
|
4834
|
+
watchers,
|
|
4835
|
+
objectParents,
|
|
4836
|
+
objectsWithDeepWatchers,
|
|
4837
|
+
deepWatchers,
|
|
4838
|
+
effectToDeepWatchedObjects,
|
|
4839
|
+
nonReactiveObjects,
|
|
4840
|
+
};
|
|
4841
|
+
|
|
4842
|
+
exports.IterableWeakMap = IterableWeakMap;
|
|
4843
|
+
exports.IterableWeakSet = IterableWeakSet;
|
|
4844
|
+
exports.ReactiveBase = ReactiveBase;
|
|
4845
|
+
exports.ReactiveError = ReactiveError;
|
|
4846
|
+
exports.ReadOnlyError = ReadOnlyError;
|
|
4847
|
+
exports.Register = Register;
|
|
4848
|
+
exports.addBatchCleanup = addBatchCleanup;
|
|
4849
|
+
exports.atomic = atomic;
|
|
4850
|
+
exports.biDi = biDi;
|
|
4851
|
+
exports.buildReactivityGraph = buildReactivityGraph;
|
|
4852
|
+
exports.cleanedBy = cleanedBy;
|
|
4853
|
+
exports.cleanup = cleanup;
|
|
4854
|
+
exports.deepWatch = deepWatch;
|
|
4855
|
+
exports.defer = defer;
|
|
4856
|
+
exports.derived = derived;
|
|
4857
|
+
exports.effect = effect;
|
|
4858
|
+
exports.enableDevTools = enableDevTools;
|
|
4859
|
+
exports.getActiveEffect = getActiveEffect;
|
|
4860
|
+
exports.getState = getState;
|
|
4861
|
+
exports.immutables = immutables;
|
|
4862
|
+
exports.isDevtoolsEnabled = isDevtoolsEnabled;
|
|
4863
|
+
exports.isNonReactive = isNonReactive;
|
|
4864
|
+
exports.isReactive = isReactive;
|
|
4865
|
+
exports.isZoneEnabled = isZoneEnabled;
|
|
4866
|
+
exports.mapped = mapped;
|
|
4867
|
+
exports.memoize = memoize;
|
|
4868
|
+
exports.mixin = mixin;
|
|
4869
|
+
exports.options = options;
|
|
4870
|
+
exports.organize = organize;
|
|
4871
|
+
exports.organized = organized;
|
|
4872
|
+
exports.profileInfo = profileInfo;
|
|
4873
|
+
exports.project = project;
|
|
4874
|
+
exports.reactive = reactive;
|
|
4875
|
+
exports.reduced = reduced;
|
|
4876
|
+
exports.register = register;
|
|
4877
|
+
exports.registerEffectForDebug = registerEffectForDebug;
|
|
4878
|
+
exports.registerNativeReactivity = registerNativeReactivity;
|
|
4879
|
+
exports.registerObjectForDebug = registerObjectForDebug;
|
|
4880
|
+
exports.root = root;
|
|
4881
|
+
exports.setEffectName = setEffectName;
|
|
4882
|
+
exports.setObjectName = setObjectName;
|
|
4883
|
+
exports.setZoneEnabled = setZoneEnabled;
|
|
4884
|
+
exports.touched = touched;
|
|
4885
|
+
exports.touched1 = touched1;
|
|
4886
|
+
exports.trackEffect = trackEffect;
|
|
4887
|
+
exports.unreactive = unreactive;
|
|
4888
|
+
exports.untracked = untracked;
|
|
4889
|
+
exports.unwrap = unwrap;
|
|
4890
|
+
exports.watch = watch;
|
|
4891
|
+
//# sourceMappingURL=index-HNVqPzjz.js.map
|