mutts 1.0.12 → 1.0.14
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/BROWSER_ASYNC_POLYFILL.md +79 -0
- package/README.md +7 -4
- package/dist/browser.cjs +150 -27
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +1440 -2
- package/dist/browser.dev.cjs +17 -3
- package/dist/browser.dev.cjs.map +1 -1
- package/dist/browser.dev.d.ts +2 -2
- package/dist/browser.dev.esm.js +2 -2
- package/dist/browser.esm.js +137 -28
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{index-yK0HVxHv.cjs → index-BnTNC9eC.cjs} +347 -156
- package/dist/chunks/index-BnTNC9eC.cjs.map +1 -0
- package/dist/chunks/{index-BUop6B2U.esm.js → index-CAWVZL7P.esm.js} +345 -154
- package/dist/chunks/index-CAWVZL7P.esm.js.map +1 -0
- package/dist/chunks/node-Df_5r_WA.cjs +187 -0
- package/dist/chunks/node-Df_5r_WA.cjs.map +1 -0
- package/dist/chunks/node-DuIduHw3.esm.js +185 -0
- package/dist/chunks/node-DuIduHw3.esm.js.map +1 -0
- package/dist/chunks/{proxy-D2C49sXH.esm.js → proxy-C2lnvvbx.esm.js} +943 -272
- package/dist/chunks/proxy-C2lnvvbx.esm.js.map +1 -0
- package/dist/chunks/{proxy-BvM4yewA.cjs → proxy-HA_QQnd5.cjs} +959 -273
- package/dist/chunks/proxy-HA_QQnd5.cjs.map +1 -0
- package/dist/debug.cjs +571 -173
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.ts +96 -80
- package/dist/debug.esm.js +567 -173
- package/dist/debug.esm.js.map +1 -1
- package/dist/devtools/panel.js.map +1 -1
- package/dist/mutts.umd.js +4351 -3366
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/node.cjs +18 -4
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.dev.cjs +18 -4
- package/dist/node.dev.cjs.map +1 -1
- package/dist/node.dev.d.ts +2 -2
- package/dist/node.dev.esm.js +3 -3
- package/dist/node.esm.js +3 -3
- package/dist/{types-Bx2PhORg.d.ts → types.d.ts} +42 -15
- package/docs/ai/api-reference.md +105 -13
- package/docs/ai/manual.md +77 -29
- package/docs/debug-getReason.md +161 -0
- package/docs/flavored.md +98 -1
- package/docs/reactive/advanced.md +184 -12
- package/docs/reactive/attend.md +32 -0
- package/docs/reactive/core.md +40 -6
- package/docs/reactive/debugging.md +40 -15
- package/docs/reactive.md +4 -1
- package/package.json +13 -9
- package/dist/chunks/index-BUop6B2U.esm.js.map +0 -1
- package/dist/chunks/index-yK0HVxHv.cjs.map +0 -1
- package/dist/chunks/node-Bo7WU5S2.esm.js +0 -96
- package/dist/chunks/node-Bo7WU5S2.esm.js.map +0 -1
- package/dist/chunks/node-Dd0esp5F.cjs +0 -98
- package/dist/chunks/node-Dd0esp5F.cjs.map +0 -1
- package/dist/chunks/proxy-BvM4yewA.cjs.map +0 -1
- package/dist/chunks/proxy-D2C49sXH.esm.js.map +0 -1
- package/dist/index.d.ts +0 -1322
|
@@ -1,26 +1,3 @@
|
|
|
1
|
-
// Queue for hooks registered before the environment is ready (circular dependency fix)
|
|
2
|
-
const hooks = new Set();
|
|
3
|
-
const asyncHooks = {
|
|
4
|
-
addHook(hook) {
|
|
5
|
-
hooks.add(hook);
|
|
6
|
-
return () => hooks.delete(hook);
|
|
7
|
-
},
|
|
8
|
-
/**
|
|
9
|
-
* [Hack] Sanitize a promise (or value) to prevent context leaks.
|
|
10
|
-
* Default: Identity function.
|
|
11
|
-
* Browser: Uses Macrotask wrapping to break microtask chains.
|
|
12
|
-
*/
|
|
13
|
-
sanitizePromise(p) {
|
|
14
|
-
return p;
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Register a hook that will be called whenever an asynchronous operation is initiated.
|
|
19
|
-
* The hook should return a restorer function which will be called just before the async callback runs.
|
|
20
|
-
* That restorer should in turn return an undoer function which will be called just after the async callback finishes.
|
|
21
|
-
*/
|
|
22
|
-
const asyncHook = (hook) => asyncHooks.addHook(hook);
|
|
23
|
-
|
|
24
1
|
/**
|
|
25
2
|
* Yields tuples containing elements from each input array, stopping at the longest array length
|
|
26
3
|
* @param args - Arrays to zip together
|
|
@@ -136,13 +113,10 @@ function isOwnAccessor(obj, prop) {
|
|
|
136
113
|
return !!(opd?.get || opd?.set);
|
|
137
114
|
}
|
|
138
115
|
/**
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* @param b - Second value
|
|
144
|
-
* @param cache - Map for circular reference protection (internal use)
|
|
145
|
-
* @returns True if values are deeply equal
|
|
116
|
+
* Symbol used to provide custom comparison logic for an object.
|
|
117
|
+
*/
|
|
118
|
+
const CompareSymbol = Symbol.for('mutts.compare');
|
|
119
|
+
/**
|
|
146
120
|
*/
|
|
147
121
|
function deepCompare(a, b, cache = new Map()) {
|
|
148
122
|
if (a === b)
|
|
@@ -150,6 +124,13 @@ function deepCompare(a, b, cache = new Map()) {
|
|
|
150
124
|
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
|
|
151
125
|
return a === b;
|
|
152
126
|
}
|
|
127
|
+
// Custom comparison support
|
|
128
|
+
if (typeof a[CompareSymbol] === 'function') {
|
|
129
|
+
return a[CompareSymbol](b, (x, y) => deepCompare(x, y, cache));
|
|
130
|
+
}
|
|
131
|
+
if (typeof b[CompareSymbol] === 'function') {
|
|
132
|
+
return b[CompareSymbol](a, (x, y) => deepCompare(x, y, cache));
|
|
133
|
+
}
|
|
153
134
|
// Prototype check
|
|
154
135
|
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
|
|
155
136
|
return false;
|
|
@@ -268,7 +249,8 @@ function named(name, fn) {
|
|
|
268
249
|
});
|
|
269
250
|
return fn;
|
|
270
251
|
}
|
|
271
|
-
const
|
|
252
|
+
const runtimeGlobals = globalThis;
|
|
253
|
+
const _mode = runtimeGlobals.process?.env?.NODE_ENV ||
|
|
272
254
|
(typeof import.meta !== 'undefined' && import.meta.env?.MODE) ||
|
|
273
255
|
'production';
|
|
274
256
|
const isDev = _mode === 'development';
|
|
@@ -444,6 +426,93 @@ const decorator = (description) => {
|
|
|
444
426
|
* flavoredGreet.loud('World') // "HELLO, WORLD!"
|
|
445
427
|
* ```
|
|
446
428
|
*/
|
|
429
|
+
const captionedOptionsSymbol = Symbol('mutts.captioned.options');
|
|
430
|
+
function isTemplateStringsArray(value) {
|
|
431
|
+
return (Array.isArray(value) &&
|
|
432
|
+
Object.hasOwn(value, 'raw') &&
|
|
433
|
+
Array.isArray(value.raw));
|
|
434
|
+
}
|
|
435
|
+
function renderTemplate(strings, values) {
|
|
436
|
+
let result = strings[0] ?? '';
|
|
437
|
+
for (let i = 0; i < values.length; i++)
|
|
438
|
+
result += String(values[i]) + (strings[i + 1] ?? '');
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
function renameCallback(caption, callback) {
|
|
442
|
+
Object.defineProperty(callback, 'name', {
|
|
443
|
+
value: caption,
|
|
444
|
+
writable: false,
|
|
445
|
+
configurable: true,
|
|
446
|
+
});
|
|
447
|
+
return callback;
|
|
448
|
+
}
|
|
449
|
+
function isAnonymousCallback(callback) {
|
|
450
|
+
return !callback.name || callback.name === 'anonymous';
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Wraps a callback-first function so it also accepts a tagged-template call form.
|
|
454
|
+
*
|
|
455
|
+
* The template caption is applied to one callback argument before the base
|
|
456
|
+
* function runs. By default, `captioned` targets the first argument, but
|
|
457
|
+
* `callbackIndex` can point to any callback position.
|
|
458
|
+
*
|
|
459
|
+
* This is intended for APIs such as `effect`, `lift`, or `watch` where naming
|
|
460
|
+
* is useful but should remain separate from the flavor system.
|
|
461
|
+
*
|
|
462
|
+
* Plain calls still work:
|
|
463
|
+
* `run(callback)`
|
|
464
|
+
*
|
|
465
|
+
* Captioned calls add a runtime name to the first callback:
|
|
466
|
+
* `` run`task:${id}`(callback) ``
|
|
467
|
+
*
|
|
468
|
+
* Anonymous uncaptioned callbacks may trigger a warning depending on
|
|
469
|
+
* `shouldWarnAnonymous`.
|
|
470
|
+
*/
|
|
471
|
+
function captioned(fn, options = {}) {
|
|
472
|
+
const settings = {
|
|
473
|
+
callbackIndex: options.callbackIndex ?? 0,
|
|
474
|
+
name: options.name ?? (fn.name || 'callback'),
|
|
475
|
+
rename: options.rename ?? ((caption, callback) => renameCallback(caption, callback)),
|
|
476
|
+
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
477
|
+
warn: options.warn ?? ((message) => console.warn(message)),
|
|
478
|
+
shouldWarnAnonymous: options.shouldWarnAnonymous,
|
|
479
|
+
};
|
|
480
|
+
fn[captionedOptionsSymbol] = settings;
|
|
481
|
+
return new Proxy(fn, {
|
|
482
|
+
get(target, prop, receiver) {
|
|
483
|
+
if (prop === captionedOptionsSymbol)
|
|
484
|
+
return settings;
|
|
485
|
+
return Reflect.get(target, prop, receiver);
|
|
486
|
+
},
|
|
487
|
+
apply(target, thisArg, args) {
|
|
488
|
+
if (isTemplateStringsArray(args[0])) {
|
|
489
|
+
const caption = renderTemplate(args[0], args.slice(1));
|
|
490
|
+
return function captionedCall(...callArgs) {
|
|
491
|
+
const callback = callArgs[settings.callbackIndex];
|
|
492
|
+
if (typeof callback !== 'function')
|
|
493
|
+
throw new TypeError(`${settings.name} template calls require a callback at argument index ${settings.callbackIndex}`);
|
|
494
|
+
const nextArgs = [...callArgs];
|
|
495
|
+
nextArgs[settings.callbackIndex] = settings.rename(caption, callback);
|
|
496
|
+
return Reflect.apply(target, this, nextArgs);
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const callback = args[settings.callbackIndex];
|
|
500
|
+
if (typeof callback === 'function' && isAnonymousCallback(callback)) {
|
|
501
|
+
const shouldWarn = settings.shouldWarnAnonymous?.(callback, args) ?? true;
|
|
502
|
+
if (shouldWarn)
|
|
503
|
+
settings.warn(`${settings.name}: anonymous callback detected. Use template syntax for automatic naming:\n` +
|
|
504
|
+
` Current: ${settings.name}(() => { ... })\n` +
|
|
505
|
+
` Fix: ${settings.name}\`descriptive-name\`(() => { ... })\n` +
|
|
506
|
+
`The captioned system uses the template literal as the effect name for better debugging.`);
|
|
507
|
+
}
|
|
508
|
+
return Reflect.apply(target, thisArg, args);
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function inheritCaption(source, target) {
|
|
513
|
+
const settings = source[captionedOptionsSymbol];
|
|
514
|
+
return settings ? captioned(target, settings) : target;
|
|
515
|
+
}
|
|
447
516
|
/**
|
|
448
517
|
* Creates a flavored (extensible) version of a function with chainable property modifiers.
|
|
449
518
|
*/
|
|
@@ -476,7 +545,7 @@ function createFlavor(fn, transform, name) {
|
|
|
476
545
|
};
|
|
477
546
|
if (name)
|
|
478
547
|
named(name, fct);
|
|
479
|
-
return flavored(fct, fn.flavors || {});
|
|
548
|
+
return flavored(inheritCaption(fn, fct), fn.flavors || {});
|
|
480
549
|
}
|
|
481
550
|
/**
|
|
482
551
|
* Creates a new flavored function that merges options objects at a specific index.
|
|
@@ -509,7 +578,7 @@ function flavorOptions(fn, defaultOptions, opts = {}) {
|
|
|
509
578
|
// Preserve arity and options track
|
|
510
579
|
Object.defineProperty(fct, 'length', { value: fn.length });
|
|
511
580
|
fct.optionsIndex = targetIndex;
|
|
512
|
-
return flavored(fct, fn.flavors || {});
|
|
581
|
+
return flavored(inheritCaption(fn, fct), fn.flavors || {});
|
|
513
582
|
}
|
|
514
583
|
|
|
515
584
|
/// <reference lib="esnext.collection" />
|
|
@@ -690,7 +759,13 @@ class IterableWeakSet {
|
|
|
690
759
|
[Symbol.iterator]() {
|
|
691
760
|
return this.keys();
|
|
692
761
|
}
|
|
693
|
-
union(other) {
|
|
762
|
+
union(other, ...sets) {
|
|
763
|
+
if (sets.length > 0) {
|
|
764
|
+
for (const set of [other, ...sets])
|
|
765
|
+
for (const value of set)
|
|
766
|
+
this.add(value);
|
|
767
|
+
return this;
|
|
768
|
+
}
|
|
694
769
|
const others = {
|
|
695
770
|
[Symbol.iterator]() {
|
|
696
771
|
return other.keys();
|
|
@@ -762,100 +837,6 @@ class IterableWeakSet {
|
|
|
762
837
|
}
|
|
763
838
|
_b = Symbol.toStringTag;
|
|
764
839
|
|
|
765
|
-
/**
|
|
766
|
-
* Creates a mixin that can be used both as a class (extends) and as a function (mixin)
|
|
767
|
-
*
|
|
768
|
-
* This function supports:
|
|
769
|
-
* - Using mixins as base classes: `class MyClass extends MyMixin`
|
|
770
|
-
* - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
|
|
771
|
-
* - Composing mixins: `const Composed = MixinA(MixinB)`
|
|
772
|
-
* - Type-safe property inference for all patterns
|
|
773
|
-
*
|
|
774
|
-
* @param mixinFunction - The function that creates the mixin
|
|
775
|
-
* @param unwrapFunction - Optional function to unwrap reactive objects for method calls
|
|
776
|
-
* @returns A mixin that can be used both as a class and as a function
|
|
777
|
-
*/
|
|
778
|
-
function mixin(mixinFunction, unwrapFunction) {
|
|
779
|
-
/**
|
|
780
|
-
* Cache for mixin results to ensure the same base class always returns the same mixed class
|
|
781
|
-
*/
|
|
782
|
-
const mixinCache = new WeakMap();
|
|
783
|
-
// Apply the mixin to Object as the base class
|
|
784
|
-
const MixedBase = mixinFunction(Object);
|
|
785
|
-
mixinCache.set(Object, MixedBase);
|
|
786
|
-
// Create the proxy that handles both constructor and function calls
|
|
787
|
-
return new Proxy(MixedBase, {
|
|
788
|
-
// Handle `MixinClass(SomeBase)` - use as mixin function
|
|
789
|
-
apply(_target, _thisArg, args) {
|
|
790
|
-
if (args.length === 0) {
|
|
791
|
-
throw new Error('Mixin requires a base class');
|
|
792
|
-
}
|
|
793
|
-
const baseClass = args[0];
|
|
794
|
-
if (typeof baseClass !== 'function') {
|
|
795
|
-
throw new Error('Mixin requires a constructor function');
|
|
796
|
-
}
|
|
797
|
-
// Check if it's a valid constructor or a mixin
|
|
798
|
-
if (!isConstructor(baseClass) &&
|
|
799
|
-
!(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
|
|
800
|
-
throw new Error('Mixin requires a valid constructor');
|
|
801
|
-
}
|
|
802
|
-
// Check cache first
|
|
803
|
-
const cached = mixinCache.get(baseClass);
|
|
804
|
-
if (cached) {
|
|
805
|
-
return cached;
|
|
806
|
-
}
|
|
807
|
-
let usedBase = baseClass;
|
|
808
|
-
if (unwrapFunction) {
|
|
809
|
-
// Create a proxied base class that handles method unwrapping
|
|
810
|
-
const ProxiedBaseClass = class extends baseClass {
|
|
811
|
-
};
|
|
812
|
-
// Proxy the prototype methods to handle unwrapping
|
|
813
|
-
const originalPrototype = baseClass.prototype;
|
|
814
|
-
const proxiedPrototype = new Proxy(originalPrototype, {
|
|
815
|
-
get(target, prop, receiver) {
|
|
816
|
-
const value = FoolProof.get(target, prop, receiver);
|
|
817
|
-
// Only wrap methods that are likely to access private fields
|
|
818
|
-
// Skip symbols and special properties that the reactive system needs
|
|
819
|
-
if (typeof value === 'function' &&
|
|
820
|
-
typeof prop === 'string' &&
|
|
821
|
-
!['constructor', 'toString', 'valueOf'].includes(prop)) {
|
|
822
|
-
// Return a wrapped version that uses unwrapped context
|
|
823
|
-
return function (...args) {
|
|
824
|
-
// Use the unwrapping function if provided, otherwise use this
|
|
825
|
-
const context = unwrapFunction(this);
|
|
826
|
-
return value.apply(context, args);
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
return value;
|
|
830
|
-
},
|
|
831
|
-
});
|
|
832
|
-
// Set the proxied prototype
|
|
833
|
-
Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
|
|
834
|
-
usedBase = ProxiedBaseClass;
|
|
835
|
-
}
|
|
836
|
-
// Create the mixed class using the proxied base class
|
|
837
|
-
const mixedClass = mixinFunction(usedBase);
|
|
838
|
-
// Cache the result
|
|
839
|
-
mixinCache.set(baseClass, mixedClass);
|
|
840
|
-
return mixedClass;
|
|
841
|
-
},
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
const debugHooks = {
|
|
846
|
-
isDevtoolsEnabled: () => false,
|
|
847
|
-
registerEffect: () => { },
|
|
848
|
-
getTriggerChain: () => [],
|
|
849
|
-
captureStack: () => [],
|
|
850
|
-
captureLineage: () => new Error().stack,
|
|
851
|
-
formatStack: (stack) => [stack],
|
|
852
|
-
recordTriggerLink: () => { },
|
|
853
|
-
decorateError: () => { },
|
|
854
|
-
};
|
|
855
|
-
function setDebugHooks(hooks) {
|
|
856
|
-
Object.assign(debugHooks, hooks);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
840
|
/******************************************************************************
|
|
860
841
|
Copyright (c) Microsoft Corporation.
|
|
861
842
|
|
|
@@ -918,6 +899,29 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
918
899
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
919
900
|
};
|
|
920
901
|
|
|
902
|
+
// Queue for hooks registered before the environment is ready (circular dependency fix)
|
|
903
|
+
const hooks = new Set();
|
|
904
|
+
const asyncHooks = {
|
|
905
|
+
addHook(hook) {
|
|
906
|
+
hooks.add(hook);
|
|
907
|
+
return () => hooks.delete(hook);
|
|
908
|
+
},
|
|
909
|
+
/**
|
|
910
|
+
* [Hack] Sanitize a promise (or value) to prevent context leaks.
|
|
911
|
+
* Default: Identity function.
|
|
912
|
+
* Browser: Uses Macrotask wrapping to break microtask chains.
|
|
913
|
+
*/
|
|
914
|
+
sanitizePromise(p) {
|
|
915
|
+
return p;
|
|
916
|
+
},
|
|
917
|
+
};
|
|
918
|
+
/**
|
|
919
|
+
* Register a hook that will be called whenever an asynchronous operation is initiated.
|
|
920
|
+
* The hook should return a restorer function which will be called just before the async callback runs.
|
|
921
|
+
* That restorer should in turn return an undoer function which will be called just after the async callback finishes.
|
|
922
|
+
*/
|
|
923
|
+
const asyncHook = (hook) => asyncHooks.addHook(hook);
|
|
924
|
+
|
|
921
925
|
var _ZoneAggregator_zones;
|
|
922
926
|
function isu(z) {
|
|
923
927
|
return z;
|
|
@@ -1103,6 +1107,7 @@ function resetRegistry() {
|
|
|
1103
1107
|
* @returns The marked function
|
|
1104
1108
|
*/
|
|
1105
1109
|
function markWithRoot(fn, root) {
|
|
1110
|
+
const marked = fn;
|
|
1106
1111
|
// Check for collision
|
|
1107
1112
|
const existingRef = reverseRoots.get(root);
|
|
1108
1113
|
const existing = existingRef?.deref();
|
|
@@ -1117,8 +1122,8 @@ function markWithRoot(fn, root) {
|
|
|
1117
1122
|
// (Last writer wins for the check)
|
|
1118
1123
|
reverseRoots.set(root, new WeakRef(fn));
|
|
1119
1124
|
// Store root mapping as symbol property on the function
|
|
1120
|
-
|
|
1121
|
-
return
|
|
1125
|
+
marked[rootFunctionSymbol] = getRoot(root);
|
|
1126
|
+
return marked;
|
|
1122
1127
|
}
|
|
1123
1128
|
/**
|
|
1124
1129
|
* Gets the root function of a function for effect tracking
|
|
@@ -1138,11 +1143,30 @@ function getRoot(fn) {
|
|
|
1138
1143
|
const effectHistory = tag('effectHistory', new ZoneHistory());
|
|
1139
1144
|
tag('effectHistory.present', effectHistory.present);
|
|
1140
1145
|
asyncZone.add(effectHistory);
|
|
1146
|
+
const externalReason = tag('externalReason', new Zone());
|
|
1147
|
+
asyncZone.add(externalReason);
|
|
1141
1148
|
/**
|
|
1142
1149
|
* Aggregator for zones that need to be tracked along effects.
|
|
1143
1150
|
* ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
|
|
1144
1151
|
*/
|
|
1145
1152
|
const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
|
|
1153
|
+
effectAggregator.add(externalReason);
|
|
1154
|
+
function chainExternalReason(reason) {
|
|
1155
|
+
const external = externalReason.active;
|
|
1156
|
+
if (!external)
|
|
1157
|
+
return reason;
|
|
1158
|
+
if (!reason)
|
|
1159
|
+
return external;
|
|
1160
|
+
let current = reason;
|
|
1161
|
+
while (current) {
|
|
1162
|
+
if (current.type === 'external' &&
|
|
1163
|
+
external.type === 'external' &&
|
|
1164
|
+
current.detail === external.detail)
|
|
1165
|
+
return reason;
|
|
1166
|
+
current = current.chain;
|
|
1167
|
+
}
|
|
1168
|
+
return { ...reason, chain: chainExternalReason(reason.chain) };
|
|
1169
|
+
}
|
|
1146
1170
|
function isRunning(effect) {
|
|
1147
1171
|
const root = getRoot(effect);
|
|
1148
1172
|
return effectHistory.some((e) => getRoot(e) === root);
|
|
@@ -1150,6 +1174,35 @@ function isRunning(effect) {
|
|
|
1150
1174
|
function getActiveEffect() {
|
|
1151
1175
|
return effectHistory.present.active;
|
|
1152
1176
|
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Captures the current effect context so that deferred code can later
|
|
1179
|
+
* create child effects parented to this point in the effect tree.
|
|
1180
|
+
*
|
|
1181
|
+
* @returns An opaque token to pass to `withEffectContext()`
|
|
1182
|
+
*
|
|
1183
|
+
* @example
|
|
1184
|
+
* ```ts
|
|
1185
|
+
* const ctx = effectContext() // inside an effect or root()
|
|
1186
|
+
* // later, in a deferred callback:
|
|
1187
|
+
* withEffectContext(ctx, () => {
|
|
1188
|
+
* effect(() => { /* child of the captured context */ })
|
|
1189
|
+
* })
|
|
1190
|
+
* ```
|
|
1191
|
+
*/
|
|
1192
|
+
function effectContext() {
|
|
1193
|
+
return effectHistory.active;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Runs `fn` within a previously captured effect context.
|
|
1197
|
+
* Any effects created inside `fn` become children of the captured parent.
|
|
1198
|
+
*
|
|
1199
|
+
* @param ctx - The context token from `effectContext()`, or `undefined` for root context
|
|
1200
|
+
* @param fn - The function to execute within the restored context
|
|
1201
|
+
* @returns The return value of `fn`
|
|
1202
|
+
*/
|
|
1203
|
+
function withEffectContext(ctx, fn) {
|
|
1204
|
+
return effectHistory.with(ctx, fn);
|
|
1205
|
+
}
|
|
1153
1206
|
const cleanups = new WeakMap();
|
|
1154
1207
|
/**
|
|
1155
1208
|
* Attach cleanup dependencies to an object. When `unlink(obj)` is called,
|
|
@@ -1177,7 +1230,7 @@ const cleanups = new WeakMap();
|
|
|
1177
1230
|
function link(obj, ...cleanupFns) {
|
|
1178
1231
|
const set = cleanups.get(obj);
|
|
1179
1232
|
if (!set)
|
|
1180
|
-
cleanups.set(obj, new Set(cleanupFns.filter(
|
|
1233
|
+
cleanups.set(obj, new Set(cleanupFns.filter((fn) => fn !== undefined)));
|
|
1181
1234
|
else
|
|
1182
1235
|
for (const fn of cleanupFns)
|
|
1183
1236
|
if (fn)
|
|
@@ -1204,6 +1257,85 @@ function unlink(obj, reason) {
|
|
|
1204
1257
|
}
|
|
1205
1258
|
}
|
|
1206
1259
|
|
|
1260
|
+
function extractRawStack(error = new Error()) {
|
|
1261
|
+
if (typeof error === 'string')
|
|
1262
|
+
return error;
|
|
1263
|
+
if (error && typeof error === 'object' && 'stack' in error) {
|
|
1264
|
+
const stack = error.stack;
|
|
1265
|
+
return typeof stack === 'string' ? stack : undefined;
|
|
1266
|
+
}
|
|
1267
|
+
return undefined;
|
|
1268
|
+
}
|
|
1269
|
+
function trimStack(stack) {
|
|
1270
|
+
const raw = extractRawStack(stack);
|
|
1271
|
+
if (!raw)
|
|
1272
|
+
return [];
|
|
1273
|
+
const lines = raw
|
|
1274
|
+
.split('\n')
|
|
1275
|
+
.map((line) => line.trim())
|
|
1276
|
+
.filter(Boolean);
|
|
1277
|
+
if (lines[0]?.startsWith('Error'))
|
|
1278
|
+
lines.shift();
|
|
1279
|
+
while (lines[0] &&
|
|
1280
|
+
(lines[0].includes('captureLineage') ||
|
|
1281
|
+
lines[0].includes('captureDeferredLineage') ||
|
|
1282
|
+
lines[0].includes('debug-hooks.ts')))
|
|
1283
|
+
lines.shift();
|
|
1284
|
+
return lines;
|
|
1285
|
+
}
|
|
1286
|
+
function digestDeferredLineage(lineage) {
|
|
1287
|
+
if (lineage.segments)
|
|
1288
|
+
return lineage.segments;
|
|
1289
|
+
const segments = [];
|
|
1290
|
+
let effect = lineage.effect;
|
|
1291
|
+
let stack = trimStack(lineage.stack);
|
|
1292
|
+
if (!effect) {
|
|
1293
|
+
lineage.segments = [{ effectName: 'root', stack }];
|
|
1294
|
+
return lineage.segments;
|
|
1295
|
+
}
|
|
1296
|
+
while (effect) {
|
|
1297
|
+
const root = getRoot(effect);
|
|
1298
|
+
segments.push({
|
|
1299
|
+
effectName: root.name || 'anonymous',
|
|
1300
|
+
stack,
|
|
1301
|
+
});
|
|
1302
|
+
const node = getEffectNode(effect);
|
|
1303
|
+
effect = node.parent;
|
|
1304
|
+
stack = trimStack(node.creationStack);
|
|
1305
|
+
}
|
|
1306
|
+
if (stack.length)
|
|
1307
|
+
segments.push({ effectName: 'root', stack });
|
|
1308
|
+
lineage.segments = segments;
|
|
1309
|
+
return segments;
|
|
1310
|
+
}
|
|
1311
|
+
function formatDeferredLineage(lineage) {
|
|
1312
|
+
return digestDeferredLineage(lineage)
|
|
1313
|
+
.map((segment) => [`${segment.effectName}:`, ...segment.stack.map((line) => ` ${line}`)].join('\n'))
|
|
1314
|
+
.join('\n');
|
|
1315
|
+
}
|
|
1316
|
+
function captureDeferredLineage(effect = getActiveEffect(), stack = new Error()) {
|
|
1317
|
+
return {
|
|
1318
|
+
effect,
|
|
1319
|
+
stack,
|
|
1320
|
+
toString() {
|
|
1321
|
+
return formatDeferredLineage(this);
|
|
1322
|
+
},
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
const debugHooks = {
|
|
1326
|
+
isDevtoolsEnabled: () => false,
|
|
1327
|
+
registerEffect: () => { },
|
|
1328
|
+
getTriggerChain: () => [],
|
|
1329
|
+
captureStack: (error) => extractRawStack(error ?? new Error()),
|
|
1330
|
+
captureLineage: captureDeferredLineage,
|
|
1331
|
+
formatStack: (stack) => [stack],
|
|
1332
|
+
recordTriggerLink: () => { },
|
|
1333
|
+
decorateError: () => { },
|
|
1334
|
+
};
|
|
1335
|
+
function setDebugHooks(hooks) {
|
|
1336
|
+
Object.assign(debugHooks, hooks);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1207
1339
|
const effectMarker = {
|
|
1208
1340
|
enter: 'effect:enter',
|
|
1209
1341
|
leave: 'effect:leave',
|
|
@@ -1243,18 +1375,61 @@ function formatCleanupReason(reason, depth = 0) {
|
|
|
1243
1375
|
parts.push(',');
|
|
1244
1376
|
parts.push(...formatTrigger(reason.triggers[i]));
|
|
1245
1377
|
}
|
|
1378
|
+
if (reason.chain) {
|
|
1379
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1380
|
+
}
|
|
1381
|
+
return parts;
|
|
1382
|
+
}
|
|
1383
|
+
case 'stopped': {
|
|
1384
|
+
const parts = [`${indent}stopped`];
|
|
1385
|
+
if (reason.detail)
|
|
1386
|
+
parts.push(`(${reason.detail})`);
|
|
1387
|
+
if (reason.chain) {
|
|
1388
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1389
|
+
}
|
|
1390
|
+
return parts;
|
|
1391
|
+
}
|
|
1392
|
+
case 'external': {
|
|
1393
|
+
const parts = [`${indent}external:`, reason.detail];
|
|
1394
|
+
if (reason.chain) {
|
|
1395
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1396
|
+
}
|
|
1397
|
+
return parts;
|
|
1398
|
+
}
|
|
1399
|
+
case 'gc': {
|
|
1400
|
+
const parts = [`${indent}gc`];
|
|
1401
|
+
if (reason.chain) {
|
|
1402
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1403
|
+
}
|
|
1404
|
+
return parts;
|
|
1405
|
+
}
|
|
1406
|
+
case 'error': {
|
|
1407
|
+
const parts = [`${indent}error:`, reason.error];
|
|
1408
|
+
if (reason.chain) {
|
|
1409
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1410
|
+
}
|
|
1411
|
+
return parts;
|
|
1412
|
+
}
|
|
1413
|
+
case 'lineage': {
|
|
1414
|
+
const parts = [
|
|
1415
|
+
`${indent}lineage ←\n`,
|
|
1416
|
+
...formatCleanupReason(reason.parent, depth + 1),
|
|
1417
|
+
];
|
|
1418
|
+
if (reason.chain) {
|
|
1419
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1420
|
+
}
|
|
1421
|
+
return parts;
|
|
1422
|
+
}
|
|
1423
|
+
case 'invalidate': {
|
|
1424
|
+
const parts = [
|
|
1425
|
+
`${indent}invalidate ←\n`,
|
|
1426
|
+
...formatCleanupReason(reason.cause, depth + 1),
|
|
1427
|
+
];
|
|
1428
|
+
if (reason.chain) {
|
|
1429
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1430
|
+
}
|
|
1246
1431
|
return parts;
|
|
1247
1432
|
}
|
|
1248
|
-
case 'stopped':
|
|
1249
|
-
return [`${indent}stopped`];
|
|
1250
|
-
case 'gc':
|
|
1251
|
-
return [`${indent}gc`];
|
|
1252
|
-
case 'error':
|
|
1253
|
-
return [`${indent}error:`, reason.error];
|
|
1254
|
-
case 'lineage':
|
|
1255
|
-
return [`${indent}lineage ←\n`, ...formatCleanupReason(reason.parent, depth + 1)];
|
|
1256
|
-
case 'invalidate':
|
|
1257
|
-
return [`${indent}invalidate ←\n`, ...formatCleanupReason(reason.cause, depth + 1)];
|
|
1258
1433
|
case 'multiple': {
|
|
1259
1434
|
const parts = [];
|
|
1260
1435
|
for (let i = 0; i < reason.reasons.length; i++) {
|
|
@@ -1262,6 +1437,9 @@ function formatCleanupReason(reason, depth = 0) {
|
|
|
1262
1437
|
parts.push('\n');
|
|
1263
1438
|
parts.push(...formatCleanupReason(reason.reasons[i], depth));
|
|
1264
1439
|
}
|
|
1440
|
+
if (reason.chain) {
|
|
1441
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1442
|
+
}
|
|
1265
1443
|
return parts;
|
|
1266
1444
|
}
|
|
1267
1445
|
}
|
|
@@ -1308,6 +1486,17 @@ class ReactiveError extends Error {
|
|
|
1308
1486
|
return this.debugInfo?.cause;
|
|
1309
1487
|
}
|
|
1310
1488
|
}
|
|
1489
|
+
function normalizeSchedulerMode(mode) {
|
|
1490
|
+
switch (mode) {
|
|
1491
|
+
case 'production':
|
|
1492
|
+
return 'raw';
|
|
1493
|
+
case 'development':
|
|
1494
|
+
return 'ordered';
|
|
1495
|
+
default:
|
|
1496
|
+
return mode;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
let schedulerMode = 'ordered';
|
|
1311
1500
|
// biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
|
|
1312
1501
|
/**
|
|
1313
1502
|
* Global options for the reactive system
|
|
@@ -1398,22 +1587,42 @@ const options = {
|
|
|
1398
1587
|
*/
|
|
1399
1588
|
onMemoizationDiscrepancy: undefined,
|
|
1400
1589
|
/**
|
|
1401
|
-
*
|
|
1590
|
+
* Effect scheduler mode.
|
|
1402
1591
|
*
|
|
1403
|
-
* - `'
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1592
|
+
* - `'ordered'` (Default): maintains the causal effect graph so effects that are already
|
|
1593
|
+
* queued together can run in dependency order. It also preserves parent/child effect
|
|
1594
|
+
* lifecycle ordering and catches cycles eagerly when edges are created.
|
|
1406
1595
|
*
|
|
1407
|
-
* - `'
|
|
1408
|
-
*
|
|
1409
|
-
* basic path information. Good balance of debugging help with moderate overhead.
|
|
1596
|
+
* - `'raw'`: fastest FIFO scheduler. It does not maintain the effect graph.
|
|
1597
|
+
* Cycle detection is heuristic, using maxEffectChain execution counts.
|
|
1410
1598
|
*
|
|
1411
|
-
* - `'debug'`:
|
|
1412
|
-
*
|
|
1599
|
+
* - `'debug'`: ordered scheduling plus the most detailed graph diagnostics. Highest overhead,
|
|
1600
|
+
* best for investigation.
|
|
1413
1601
|
*
|
|
1414
|
-
* @default '
|
|
1602
|
+
* @default 'ordered'
|
|
1415
1603
|
*/
|
|
1416
|
-
|
|
1604
|
+
get scheduler() {
|
|
1605
|
+
return schedulerMode;
|
|
1606
|
+
},
|
|
1607
|
+
set scheduler(mode) {
|
|
1608
|
+
schedulerMode = mode;
|
|
1609
|
+
},
|
|
1610
|
+
/**
|
|
1611
|
+
* @deprecated Use `scheduler` instead.
|
|
1612
|
+
*
|
|
1613
|
+
* Backward-compatible alias for older names:
|
|
1614
|
+
* - `'production'` maps to `scheduler = 'raw'`
|
|
1615
|
+
* - `'development'` maps to `scheduler = 'ordered'`
|
|
1616
|
+
* - `'debug'` maps to `scheduler = 'debug'`
|
|
1617
|
+
*
|
|
1618
|
+
* The new names describe scheduler behavior rather than runtime environment.
|
|
1619
|
+
*/
|
|
1620
|
+
get cycleHandling() {
|
|
1621
|
+
return schedulerMode;
|
|
1622
|
+
},
|
|
1623
|
+
set cycleHandling(mode) {
|
|
1624
|
+
schedulerMode = normalizeSchedulerMode(mode);
|
|
1625
|
+
},
|
|
1417
1626
|
/**
|
|
1418
1627
|
* Internal flag used by memoization discrepancy detector to avoid counting calls in tests
|
|
1419
1628
|
* @warning Do not modify this flag manually, this flag is given by the engine
|
|
@@ -1462,6 +1671,8 @@ const options = {
|
|
|
1462
1671
|
asyncMode: 'cancel',
|
|
1463
1672
|
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
1464
1673
|
warn: (...args) => console.warn(...args),
|
|
1674
|
+
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
1675
|
+
error: (...args) => console.error(...args),
|
|
1465
1676
|
/**
|
|
1466
1677
|
* Introspection and debug aids. Set to `null` to disable all debug overhead in production.
|
|
1467
1678
|
*
|
|
@@ -1501,14 +1712,14 @@ function optionCall(name, ...args) {
|
|
|
1501
1712
|
/** Production preset: no introspection, heuristic cycle detection, minimal overhead */
|
|
1502
1713
|
const prodPreset = {
|
|
1503
1714
|
maxEffectReaction: 'throw',
|
|
1504
|
-
|
|
1715
|
+
scheduler: 'raw',
|
|
1505
1716
|
introspection: null,
|
|
1506
1717
|
onMemoizationDiscrepancy: undefined,
|
|
1507
1718
|
};
|
|
1508
|
-
/** Development preset
|
|
1719
|
+
/** Development preset: introspection on, early cycle detection, warnings */
|
|
1509
1720
|
const devPreset = {
|
|
1510
1721
|
maxEffectReaction: 'warn',
|
|
1511
|
-
|
|
1722
|
+
scheduler: 'ordered',
|
|
1512
1723
|
introspection: {
|
|
1513
1724
|
gatherReasons: { lineages: 'touch' },
|
|
1514
1725
|
logErrors: true,
|
|
@@ -1520,7 +1731,7 @@ const devPreset = {
|
|
|
1520
1731
|
/** Debug preset: full diagnostics, throws on violations, rich lineage capture */
|
|
1521
1732
|
const debugPreset = {
|
|
1522
1733
|
maxEffectReaction: 'debug',
|
|
1523
|
-
|
|
1734
|
+
scheduler: 'debug',
|
|
1524
1735
|
introspection: {
|
|
1525
1736
|
gatherReasons: { lineages: 'both' },
|
|
1526
1737
|
logErrors: true,
|
|
@@ -1543,6 +1754,7 @@ function unwrap(obj) {
|
|
|
1543
1754
|
return obj;
|
|
1544
1755
|
return proxyToObject.get(obj) || obj;
|
|
1545
1756
|
}
|
|
1757
|
+
const toRaw = unwrap;
|
|
1546
1758
|
function isReactive(obj) {
|
|
1547
1759
|
return proxyToObject.has(obj);
|
|
1548
1760
|
}
|
|
@@ -1590,7 +1802,8 @@ function dependant(obj, prop = allProps) {
|
|
|
1590
1802
|
if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps && prop !== keysOf))
|
|
1591
1803
|
return;
|
|
1592
1804
|
const node = getEffectNode(currentActiveEffect);
|
|
1593
|
-
|
|
1805
|
+
const hasDependencyHook = node.dependencyHook !== undefined;
|
|
1806
|
+
if (hasDependencyHook) {
|
|
1594
1807
|
node.dependencyHook(obj, prop);
|
|
1595
1808
|
}
|
|
1596
1809
|
let objectWatchers = watchers.get(obj);
|
|
@@ -1613,25 +1826,25 @@ function dependant(obj, prop = allProps) {
|
|
|
1613
1826
|
effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
|
|
1614
1827
|
}
|
|
1615
1828
|
// Store dependency stack if introspection is enabled
|
|
1616
|
-
const
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
objStacks.set(prop, propStacks);
|
|
1629
|
-
}
|
|
1630
|
-
propStacks.set(currentActiveEffect, debugHooks.captureLineage());
|
|
1829
|
+
const lineageMode = options.introspection?.gatherReasons?.lineages;
|
|
1830
|
+
const shouldGatherDependencyLineage = lineageMode === 'dependency' || lineageMode === 'both';
|
|
1831
|
+
if (shouldGatherDependencyLineage) {
|
|
1832
|
+
let objStacks = dependencyStacks.get(obj);
|
|
1833
|
+
if (!objStacks) {
|
|
1834
|
+
objStacks = new Map();
|
|
1835
|
+
dependencyStacks.set(obj, objStacks);
|
|
1836
|
+
}
|
|
1837
|
+
let propStacks = objStacks.get(prop);
|
|
1838
|
+
if (!propStacks) {
|
|
1839
|
+
propStacks = new Map();
|
|
1840
|
+
objStacks.set(prop, propStacks);
|
|
1631
1841
|
}
|
|
1842
|
+
propStacks.set(currentActiveEffect, debugHooks.captureLineage());
|
|
1632
1843
|
}
|
|
1633
1844
|
}
|
|
1634
1845
|
|
|
1846
|
+
// Simple module to manage inert state without circular dependencies
|
|
1847
|
+
let inertDepth = 0;
|
|
1635
1848
|
/**
|
|
1636
1849
|
* Finds a cycle in a sequence of functions by looking for the first repetition
|
|
1637
1850
|
*/
|
|
@@ -1657,6 +1870,9 @@ function formatRoots(roots, limit = 20) {
|
|
|
1657
1870
|
const end = names.slice(-10);
|
|
1658
1871
|
return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
|
|
1659
1872
|
}
|
|
1873
|
+
function externalReasonFrom(fn) {
|
|
1874
|
+
return fn.name ? { type: 'external', detail: fn.name } : undefined;
|
|
1875
|
+
}
|
|
1660
1876
|
// Nested map structure for efficient counting and batch cleanup
|
|
1661
1877
|
// batchId -> effect root -> obj -> prop -> count
|
|
1662
1878
|
let activationRegistry;
|
|
@@ -1730,6 +1946,40 @@ let causesClosure = new WeakMap();
|
|
|
1730
1946
|
let consequencesClosure = new WeakMap();
|
|
1731
1947
|
// Batch re-entrance depth and broken state
|
|
1732
1948
|
let broken = false;
|
|
1949
|
+
/** True after an unrecoverable reactive failure until `reset()`. */
|
|
1950
|
+
function isReactiveBroken() {
|
|
1951
|
+
return broken;
|
|
1952
|
+
}
|
|
1953
|
+
const reactiveBrokenHandlers = new Set();
|
|
1954
|
+
const reactiveResetHandlers = new Set();
|
|
1955
|
+
function onReactiveBroken(handler) {
|
|
1956
|
+
reactiveBrokenHandlers.add(handler);
|
|
1957
|
+
return () => reactiveBrokenHandlers.delete(handler);
|
|
1958
|
+
}
|
|
1959
|
+
function onReactiveReset(handler) {
|
|
1960
|
+
reactiveResetHandlers.add(handler);
|
|
1961
|
+
return () => reactiveResetHandlers.delete(handler);
|
|
1962
|
+
}
|
|
1963
|
+
function notifyReactiveBroken(error) {
|
|
1964
|
+
for (const handler of Array.from(reactiveBrokenHandlers)) {
|
|
1965
|
+
try {
|
|
1966
|
+
handler(error);
|
|
1967
|
+
}
|
|
1968
|
+
catch (handlerError) {
|
|
1969
|
+
options.warn('[reactive] onReactiveBroken handler threw', handlerError);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
function notifyReactiveReset() {
|
|
1974
|
+
for (const handler of Array.from(reactiveResetHandlers)) {
|
|
1975
|
+
try {
|
|
1976
|
+
handler();
|
|
1977
|
+
}
|
|
1978
|
+
catch (handlerError) {
|
|
1979
|
+
options.warn('[reactive] onReactiveReset handler threw', handlerError);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1733
1983
|
/**
|
|
1734
1984
|
* Gets or creates an IterableWeakSet for a closure map
|
|
1735
1985
|
*/
|
|
@@ -1748,7 +1998,7 @@ function getOrCreateClosure(closure, root) {
|
|
|
1748
1998
|
* @param targetRoot - Root function of the effect being triggered
|
|
1749
1999
|
*/
|
|
1750
2000
|
function addGraphEdge(callerRoot, targetRoot) {
|
|
1751
|
-
if (options.
|
|
2001
|
+
if (options.scheduler === 'raw')
|
|
1752
2002
|
return;
|
|
1753
2003
|
// Add to forward graph: callerRoot → targetRoot
|
|
1754
2004
|
const triggers = effectTriggers.get(callerRoot);
|
|
@@ -1863,7 +2113,7 @@ function hasPathExcluding(start, end, exclude) {
|
|
|
1863
2113
|
* @param effect - The effect being cleaned up
|
|
1864
2114
|
*/
|
|
1865
2115
|
function cleanupEffectFromGraph(effect) {
|
|
1866
|
-
if (options.
|
|
2116
|
+
if (options.scheduler === 'raw')
|
|
1867
2117
|
return;
|
|
1868
2118
|
const root = getRoot(effect);
|
|
1869
2119
|
// Get closures before removing direct edges (needed for propagation)
|
|
@@ -1972,7 +2222,7 @@ const executingStack = [];
|
|
|
1972
2222
|
* Called once when batch starts or when new effects are added
|
|
1973
2223
|
*/
|
|
1974
2224
|
function computeAllInDegrees(batch) {
|
|
1975
|
-
if (options.
|
|
2225
|
+
if (options.scheduler === 'raw')
|
|
1976
2226
|
return;
|
|
1977
2227
|
const activeEffect = getActiveEffect();
|
|
1978
2228
|
const activeRoot = activeEffect ? getRoot(activeEffect) : null;
|
|
@@ -1980,6 +2230,16 @@ function computeAllInDegrees(batch) {
|
|
|
1980
2230
|
batch.inDegrees.clear();
|
|
1981
2231
|
for (const [root] of batch.all) {
|
|
1982
2232
|
let inDegree = 0;
|
|
2233
|
+
// Make sure parents are executed before children: if parent is in batch, count it as a dependency
|
|
2234
|
+
const effect = batch.all.get(root);
|
|
2235
|
+
const parent = getEffectNode(effect).parent;
|
|
2236
|
+
const parentRoot = parent ? getRoot(parent) : undefined;
|
|
2237
|
+
if (parentRoot &&
|
|
2238
|
+
batch.all.has(parentRoot) &&
|
|
2239
|
+
parentRoot !== activeRoot &&
|
|
2240
|
+
parentRoot !== root) {
|
|
2241
|
+
inDegree++;
|
|
2242
|
+
}
|
|
1983
2243
|
const causes = causesClosure.get(root);
|
|
1984
2244
|
if (causes) {
|
|
1985
2245
|
for (const causeRoot of causes) {
|
|
@@ -1999,17 +2259,26 @@ function computeAllInDegrees(batch) {
|
|
|
1999
2259
|
function decrementInDegreesForExecuted(batch, executedRoot) {
|
|
2000
2260
|
// Get all effects that this executed effect triggers
|
|
2001
2261
|
const consequences = consequencesClosure.get(executedRoot);
|
|
2002
|
-
if (
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2262
|
+
if (consequences) {
|
|
2263
|
+
for (const consequenceRoot of consequences) {
|
|
2264
|
+
// Only update if it's still in the batch
|
|
2265
|
+
if (batch.all.has(consequenceRoot)) {
|
|
2266
|
+
const currentDegree = batch.inDegrees.get(consequenceRoot) ?? 0;
|
|
2267
|
+
if (currentDegree > 0) {
|
|
2268
|
+
batch.inDegrees.set(consequenceRoot, currentDegree - 1);
|
|
2269
|
+
}
|
|
2010
2270
|
}
|
|
2011
2271
|
}
|
|
2012
2272
|
}
|
|
2273
|
+
for (const [root, effect] of batch.all) {
|
|
2274
|
+
const parent = getEffectNode(effect).parent;
|
|
2275
|
+
if (!parent || getRoot(parent) !== executedRoot)
|
|
2276
|
+
continue;
|
|
2277
|
+
const currentDegree = batch.inDegrees.get(root) ?? 0;
|
|
2278
|
+
if (currentDegree > 0) {
|
|
2279
|
+
batch.inDegrees.set(root, currentDegree - 1);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2013
2282
|
}
|
|
2014
2283
|
/**
|
|
2015
2284
|
* Finds a path from startRoot to endRoot in the dependency graph
|
|
@@ -2099,6 +2368,14 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2099
2368
|
// Build reason from pending triggers if not provided
|
|
2100
2369
|
if (!reason && node.pendingTriggers) {
|
|
2101
2370
|
reason = { type: 'propChange', triggers: node.pendingTriggers };
|
|
2371
|
+
// Add chain: if this is being triggered from another effect, get its reason
|
|
2372
|
+
if (caller) {
|
|
2373
|
+
const callerNode = getEffectNode(caller);
|
|
2374
|
+
if (callerNode.currentReason) {
|
|
2375
|
+
reason.chain = callerNode.currentReason;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
reason = chainExternalReason(reason);
|
|
2102
2379
|
}
|
|
2103
2380
|
node.pendingTriggers = undefined;
|
|
2104
2381
|
if (reason) {
|
|
@@ -2140,15 +2417,15 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2140
2417
|
}
|
|
2141
2418
|
}
|
|
2142
2419
|
// 1. Add to batch first (needed for cycle detection)
|
|
2143
|
-
// TODO: Check if
|
|
2144
|
-
if (options.
|
|
2145
|
-
//
|
|
2420
|
+
// TODO: Check if this difference between raw and graph-backed scheduling is the right tradeoff.
|
|
2421
|
+
if (options.scheduler === 'raw') {
|
|
2422
|
+
// Raw mode: FIFO (delete and re-add to move to end)
|
|
2146
2423
|
if (currentBatch.all.has(root)) {
|
|
2147
2424
|
currentBatch.all.delete(root);
|
|
2148
2425
|
}
|
|
2149
2426
|
}
|
|
2150
2427
|
else {
|
|
2151
|
-
//
|
|
2428
|
+
// Graph-backed modes: skip if already queued — the existing entry will re-run
|
|
2152
2429
|
if (currentBatch.all.has(root)) {
|
|
2153
2430
|
return;
|
|
2154
2431
|
}
|
|
@@ -2157,7 +2434,7 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2157
2434
|
if (node.stopped)
|
|
2158
2435
|
return;
|
|
2159
2436
|
currentBatch.all.set(root, effect);
|
|
2160
|
-
if (caller && true && options.
|
|
2437
|
+
if (caller && true && options.scheduler !== 'raw') {
|
|
2161
2438
|
const callerRoot = getRoot(caller);
|
|
2162
2439
|
// const root = getRoot(effect) // Already have root
|
|
2163
2440
|
// Check for cycle BEFORE adding edge
|
|
@@ -2179,6 +2456,9 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2179
2456
|
}
|
|
2180
2457
|
addGraphEdge(callerRoot, root);
|
|
2181
2458
|
}
|
|
2459
|
+
if (options.scheduler !== 'raw') {
|
|
2460
|
+
computeAllInDegrees(currentBatch);
|
|
2461
|
+
}
|
|
2182
2462
|
}
|
|
2183
2463
|
/**
|
|
2184
2464
|
* Adds a cleanup function to be called when the current batch of effects completes
|
|
@@ -2275,7 +2555,7 @@ function executeNext(effectuatedRoots) {
|
|
|
2275
2555
|
// Find an effect with in-degree 0 using cached values
|
|
2276
2556
|
let nextEffect = null;
|
|
2277
2557
|
let nextRoot = null;
|
|
2278
|
-
if (options.
|
|
2558
|
+
if (options.scheduler === 'raw') {
|
|
2279
2559
|
// In flat mode, we just take the first effect in the queue (FIFO)
|
|
2280
2560
|
const first = currentBatch.all.entries().next().value;
|
|
2281
2561
|
if (first) {
|
|
@@ -2365,9 +2645,7 @@ function executeNext(effectuatedRoots) {
|
|
|
2365
2645
|
}
|
|
2366
2646
|
return result;
|
|
2367
2647
|
}
|
|
2368
|
-
|
|
2369
|
-
// These are all the effects triggered under `activeEffect` and all their sub-effects
|
|
2370
|
-
function batch(effect, immediate) {
|
|
2648
|
+
function batch(effect, batchOptions) {
|
|
2371
2649
|
if (broken) {
|
|
2372
2650
|
throw new ReactiveError('[reactive] Reactive system is broken after an unrecoverable error. Call reset() to recover.', { code: ReactiveErrorCode.BrokenEffects });
|
|
2373
2651
|
}
|
|
@@ -2382,15 +2660,38 @@ function batch(effect, immediate) {
|
|
|
2382
2660
|
throw new Error('Activation registry already exists');
|
|
2383
2661
|
optionCall('beginChain', roots);
|
|
2384
2662
|
}
|
|
2385
|
-
|
|
2386
|
-
const
|
|
2663
|
+
const immediate = batchOptions?.immediate === true;
|
|
2664
|
+
const contained = batchOptions?.contained === true;
|
|
2665
|
+
const callerToUse = batchOptions?.caller || getActiveEffect();
|
|
2387
2666
|
// Optimization: If nested and NOT immediate, just join the existing batch
|
|
2388
|
-
if (!isNewBatch && !immediate) {
|
|
2667
|
+
if (!isNewBatch && !contained && !immediate) {
|
|
2389
2668
|
for (let i = 0; i < effect.length; i++) {
|
|
2390
|
-
addToBatch(effect[i],
|
|
2669
|
+
addToBatch(effect[i], callerToUse);
|
|
2391
2670
|
}
|
|
2392
2671
|
return;
|
|
2393
2672
|
}
|
|
2673
|
+
if (!isNewBatch && !contained && immediate) {
|
|
2674
|
+
const firstReturn = {};
|
|
2675
|
+
for (let i = 0; i < effect.length; i++) {
|
|
2676
|
+
executingStack.push(effect[i]);
|
|
2677
|
+
try {
|
|
2678
|
+
const node = getEffectNode(effect[i]);
|
|
2679
|
+
const reason = node.nextReason;
|
|
2680
|
+
if (node.cleanup) {
|
|
2681
|
+
const cleanup = node.cleanup;
|
|
2682
|
+
node.cleanup = undefined;
|
|
2683
|
+
cleanup(reason);
|
|
2684
|
+
}
|
|
2685
|
+
const rv = effect[i]();
|
|
2686
|
+
if (rv !== undefined && !('value' in firstReturn))
|
|
2687
|
+
firstReturn.value = rv;
|
|
2688
|
+
}
|
|
2689
|
+
finally {
|
|
2690
|
+
executingStack.pop();
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
return firstReturn.value;
|
|
2694
|
+
}
|
|
2394
2695
|
const currentBatch = {
|
|
2395
2696
|
all: new Map(),
|
|
2396
2697
|
inDegrees: new Map(),
|
|
@@ -2398,9 +2699,11 @@ function batch(effect, immediate) {
|
|
|
2398
2699
|
};
|
|
2399
2700
|
batchStack.push(currentBatch);
|
|
2400
2701
|
let success = false;
|
|
2702
|
+
let failure;
|
|
2401
2703
|
try {
|
|
2402
2704
|
const effectuatedRoots = [];
|
|
2403
2705
|
const firstReturn = {};
|
|
2706
|
+
let initialError;
|
|
2404
2707
|
if (immediate) {
|
|
2405
2708
|
// Execute initial effects in providing order
|
|
2406
2709
|
for (let i = 0; i < effect.length; i++) {
|
|
@@ -2417,16 +2720,22 @@ function batch(effect, immediate) {
|
|
|
2417
2720
|
if (rv !== undefined && !('value' in firstReturn))
|
|
2418
2721
|
firstReturn.value = rv;
|
|
2419
2722
|
}
|
|
2723
|
+
catch (error) {
|
|
2724
|
+
initialError = error;
|
|
2725
|
+
break;
|
|
2726
|
+
}
|
|
2420
2727
|
finally {
|
|
2421
2728
|
executingStack.pop();
|
|
2422
2729
|
currentBatch.all.delete(getRoot(effect[i]));
|
|
2423
2730
|
}
|
|
2424
2731
|
}
|
|
2732
|
+
if (initialError)
|
|
2733
|
+
throw initialError;
|
|
2425
2734
|
}
|
|
2426
2735
|
else {
|
|
2427
2736
|
// Add initial effects to batch and compute dependencies
|
|
2428
2737
|
for (let i = 0; i < effect.length; i++) {
|
|
2429
|
-
addToBatch(effect[i],
|
|
2738
|
+
addToBatch(effect[i], callerToUse, false);
|
|
2430
2739
|
}
|
|
2431
2740
|
computeAllInDegrees(currentBatch);
|
|
2432
2741
|
}
|
|
@@ -2480,9 +2789,18 @@ function batch(effect, immediate) {
|
|
|
2480
2789
|
success = true;
|
|
2481
2790
|
return firstReturn.value;
|
|
2482
2791
|
}
|
|
2792
|
+
catch (error) {
|
|
2793
|
+
failure = error;
|
|
2794
|
+
if (batchStack.length === 1)
|
|
2795
|
+
optionCall('error', '[reactive] Root batch failure before broken state:', error);
|
|
2796
|
+
throw error;
|
|
2797
|
+
}
|
|
2483
2798
|
finally {
|
|
2484
2799
|
if (!success && batchStack.length === 1) {
|
|
2800
|
+
const wasBroken = broken;
|
|
2485
2801
|
broken = true;
|
|
2802
|
+
if (!wasBroken)
|
|
2803
|
+
notifyReactiveBroken(failure);
|
|
2486
2804
|
}
|
|
2487
2805
|
batchStack.pop();
|
|
2488
2806
|
if (batchStack.length === 0) {
|
|
@@ -2498,6 +2816,7 @@ function batch(effect, immediate) {
|
|
|
2498
2816
|
* All existing effects become orphaned and must be recreated.
|
|
2499
2817
|
*/
|
|
2500
2818
|
function reset() {
|
|
2819
|
+
const wasBroken = broken;
|
|
2501
2820
|
broken = false;
|
|
2502
2821
|
activationRegistry = undefined;
|
|
2503
2822
|
batchStack.length = 0;
|
|
@@ -2508,6 +2827,8 @@ function reset() {
|
|
|
2508
2827
|
resetRegistry();
|
|
2509
2828
|
resetTracking();
|
|
2510
2829
|
effectHistory.present.active = undefined;
|
|
2830
|
+
if (wasBroken)
|
|
2831
|
+
notifyReactiveReset();
|
|
2511
2832
|
}
|
|
2512
2833
|
// Inject batch function to allow atomic game loops in requestAnimationFrame/setTimeout/...
|
|
2513
2834
|
// Note: Automatic batching of async callbacks (setTimeout, Promise.then, etc.) is NOT implemented.
|
|
@@ -2524,7 +2845,7 @@ const atomic = decorator({
|
|
|
2524
2845
|
const atomicEffect = () => original.apply(this, args);
|
|
2525
2846
|
// Debug: helpful to have a name
|
|
2526
2847
|
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
|
|
2527
|
-
return batch(atomicEffect,
|
|
2848
|
+
return batch(atomicEffect, { immediate: true });
|
|
2528
2849
|
};
|
|
2529
2850
|
},
|
|
2530
2851
|
default(original) {
|
|
@@ -2532,7 +2853,7 @@ const atomic = decorator({
|
|
|
2532
2853
|
const atomicEffect = () => original.apply(this, args);
|
|
2533
2854
|
// Debug: helpful to have a name
|
|
2534
2855
|
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
|
|
2535
|
-
return batch(atomicEffect,
|
|
2856
|
+
return batch(atomicEffect, { immediate: true });
|
|
2536
2857
|
};
|
|
2537
2858
|
},
|
|
2538
2859
|
});
|
|
@@ -2571,7 +2892,7 @@ function captured(prev, fn) {
|
|
|
2571
2892
|
* ```
|
|
2572
2893
|
*/
|
|
2573
2894
|
function atom(fn) {
|
|
2574
|
-
return batch(fn,
|
|
2895
|
+
return batch(fn, { immediate: true });
|
|
2575
2896
|
}
|
|
2576
2897
|
const fr = new FinalizationRegistry((f) => f());
|
|
2577
2898
|
/**
|
|
@@ -2580,7 +2901,7 @@ const fr = new FinalizationRegistry((f) => f());
|
|
|
2580
2901
|
* @param options - Options for effect execution
|
|
2581
2902
|
* @returns A cleanup function to stop the effect
|
|
2582
2903
|
*/
|
|
2583
|
-
const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
|
|
2904
|
+
const effect = captioned(named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
|
|
2584
2905
|
if (effectOptions?.name)
|
|
2585
2906
|
Object.defineProperty(fn, 'name', { value: effectOptions.name });
|
|
2586
2907
|
// Use per-effect asyncMode or fall back to global option
|
|
@@ -2593,7 +2914,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2593
2914
|
const prevCleanup = node.cleanup;
|
|
2594
2915
|
node.cleanup = undefined;
|
|
2595
2916
|
try {
|
|
2596
|
-
untracked(() => prevCleanup(node.nextReason || {
|
|
2917
|
+
untracked `effect:cleanup`(() => prevCleanup(chainExternalReason(node.nextReason || {
|
|
2918
|
+
type: 'stopped',
|
|
2919
|
+
chain: node.currentReason,
|
|
2920
|
+
})));
|
|
2597
2921
|
}
|
|
2598
2922
|
catch (error) {
|
|
2599
2923
|
// If we want to report them, we could use options.warn or similar
|
|
@@ -2626,6 +2950,9 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2626
2950
|
}
|
|
2627
2951
|
// Set reaction reason for the upcoming run
|
|
2628
2952
|
access.reaction = node.nextReason || access.reaction;
|
|
2953
|
+
node.currentReason =
|
|
2954
|
+
node.nextReason ||
|
|
2955
|
+
(access.reaction && access.reaction !== true ? access.reaction : undefined);
|
|
2629
2956
|
node.nextReason = undefined;
|
|
2630
2957
|
optionCall('enter', getRoot(fn));
|
|
2631
2958
|
optionCall('effectRun', getRoot(fn), access.reaction);
|
|
@@ -2688,7 +3015,8 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2688
3015
|
// This ensures that when we cancel, the original promise's .catch() handlers are triggered
|
|
2689
3016
|
// We do this by rejecting the race promise, which makes the original promise chain see the rejection
|
|
2690
3017
|
// through the zone-wrapped .then()/.catch() handlers
|
|
2691
|
-
runningPromise = runningPromise
|
|
3018
|
+
runningPromise = runningPromise
|
|
3019
|
+
.catch((error) => {
|
|
2692
3020
|
// Propagate async errors to the effect's error handler
|
|
2693
3021
|
// This ensures onEffectThrow handlers are triggered for async errors
|
|
2694
3022
|
if (error !== cancelError) {
|
|
@@ -2696,6 +3024,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2696
3024
|
}
|
|
2697
3025
|
// If thrower didn't throw (handled), we absorb the error.
|
|
2698
3026
|
// If thrower threw (unhandled), it propagates as a new unhandled rejection, which is correct.
|
|
3027
|
+
})
|
|
3028
|
+
.finally(() => {
|
|
3029
|
+
// Clear currentReason when async effect completes
|
|
3030
|
+
node.currentReason = undefined;
|
|
2699
3031
|
});
|
|
2700
3032
|
}
|
|
2701
3033
|
else {
|
|
@@ -2706,7 +3038,13 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2706
3038
|
catch (error) {
|
|
2707
3039
|
debugHooks.decorateError(error, runEffect);
|
|
2708
3040
|
// catcher:self`
|
|
2709
|
-
errorToThrow = error;
|
|
3041
|
+
errorToThrow = error instanceof Error ? error : new Error(String(error));
|
|
3042
|
+
}
|
|
3043
|
+
finally {
|
|
3044
|
+
// Clear currentReason for synchronous effects
|
|
3045
|
+
if (!runningPromise) {
|
|
3046
|
+
node.currentReason = undefined;
|
|
3047
|
+
}
|
|
2710
3048
|
}
|
|
2711
3049
|
// Create cleanup function for next run
|
|
2712
3050
|
node.cleanup = (reason) => {
|
|
@@ -2737,8 +3075,11 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2737
3075
|
const childReason = reason
|
|
2738
3076
|
? reason.type === 'lineage'
|
|
2739
3077
|
? reason
|
|
2740
|
-
: { type: 'lineage', parent: reason }
|
|
2741
|
-
: { type: 'stopped' }
|
|
3078
|
+
: { type: 'lineage', parent: reason, chain: node.currentReason }
|
|
3079
|
+
: (chainExternalReason({ type: 'stopped', chain: node.currentReason }) ?? {
|
|
3080
|
+
type: 'stopped',
|
|
3081
|
+
chain: node.currentReason,
|
|
3082
|
+
});
|
|
2742
3083
|
for (const childCleanup of children)
|
|
2743
3084
|
childCleanup(childReason);
|
|
2744
3085
|
delete node.children;
|
|
@@ -2751,7 +3092,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2751
3092
|
const node = getEffectNode(runEffect);
|
|
2752
3093
|
if (debugHooks.isDevtoolsEnabled()) {
|
|
2753
3094
|
const stack = debugHooks.captureStack(); // Robustly skips internal mutts frames
|
|
2754
|
-
if (
|
|
3095
|
+
if (stack) {
|
|
2755
3096
|
node.creationStack = stack;
|
|
2756
3097
|
}
|
|
2757
3098
|
}
|
|
@@ -2795,7 +3136,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2795
3136
|
abortController = undefined;
|
|
2796
3137
|
}
|
|
2797
3138
|
};
|
|
2798
|
-
batch(runEffect,
|
|
3139
|
+
batch(runEffect, { immediate: true });
|
|
2799
3140
|
// Only ROOT effects are registered for GC cleanup and zone tracking
|
|
2800
3141
|
const isRootEffect = !parent;
|
|
2801
3142
|
const stopEffect = (reason) => {
|
|
@@ -2811,7 +3152,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2811
3152
|
runningPromise = null;
|
|
2812
3153
|
}
|
|
2813
3154
|
try {
|
|
2814
|
-
node.cleanup?.(reason || { type: 'stopped' });
|
|
3155
|
+
node.cleanup?.(chainExternalReason(reason || { type: 'stopped', chain: node.currentReason }));
|
|
2815
3156
|
}
|
|
2816
3157
|
catch (error) {
|
|
2817
3158
|
// Cleanup errors should basically be ignored or at least not stop the world
|
|
@@ -2854,30 +3195,72 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2854
3195
|
named(name) {
|
|
2855
3196
|
return flavorOptions(this, { name }, { name: 'named' });
|
|
2856
3197
|
},
|
|
2857
|
-
}))
|
|
3198
|
+
})), {
|
|
3199
|
+
name: 'effect',
|
|
3200
|
+
warn: (message) => options.warn(`[reactive] ${message}`),
|
|
3201
|
+
shouldWarnAnonymous: (_callback, args) => !(args[1] && typeof args[1] === 'object' && 'name' in args[1]),
|
|
3202
|
+
});
|
|
3203
|
+
const untracked = captioned(function untracked(fn) {
|
|
3204
|
+
const external = externalReasonFrom(fn);
|
|
3205
|
+
return external
|
|
3206
|
+
? externalReason.with(external, () => effectHistory.present.root(fn))
|
|
3207
|
+
: effectHistory.present.root(fn);
|
|
3208
|
+
});
|
|
3209
|
+
function runInert(fn) {
|
|
3210
|
+
// Increment the counter
|
|
3211
|
+
const originalDepth = inertDepth;
|
|
3212
|
+
inertDepth = originalDepth + 1;
|
|
3213
|
+
try {
|
|
3214
|
+
return fn();
|
|
3215
|
+
}
|
|
3216
|
+
finally {
|
|
3217
|
+
inertDepth = originalDepth;
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
function wrapInert(fn) {
|
|
3221
|
+
function inertEffect(...args) {
|
|
3222
|
+
return runInert(() => fn.apply(this, args));
|
|
3223
|
+
}
|
|
3224
|
+
Object.defineProperty(inertEffect, 'name', { value: `inert(${fn.name})` });
|
|
3225
|
+
return inertEffect;
|
|
3226
|
+
}
|
|
2858
3227
|
/**
|
|
2859
|
-
* Executes a function
|
|
2860
|
-
*
|
|
3228
|
+
* Executes a function with fast-path reads that bypass proxy overhead and dependency tracking.
|
|
3229
|
+
* Writes remain fully reactive. Uses a counter for safe nesting.
|
|
3230
|
+
* Can also decorate methods so the whole method body runs inertly.
|
|
2861
3231
|
* @param fn - The function to execute
|
|
2862
3232
|
*/
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
3233
|
+
const inert = decorator({
|
|
3234
|
+
method(original) {
|
|
3235
|
+
return wrapInert(original);
|
|
3236
|
+
},
|
|
3237
|
+
default(fn) {
|
|
3238
|
+
if (typeof fn !== 'function')
|
|
3239
|
+
throw new Error('inert() expects a function');
|
|
3240
|
+
return runInert(fn);
|
|
3241
|
+
},
|
|
3242
|
+
});
|
|
2866
3243
|
/**
|
|
2867
3244
|
* Executes a function from a virgin/root context - no parent effect, no tracking
|
|
2868
3245
|
* Creates completely independent effects that won't be cleaned up by any parent
|
|
2869
3246
|
* @param fn - The function to execute
|
|
2870
3247
|
*/
|
|
2871
|
-
function root(fn) {
|
|
2872
|
-
|
|
2873
|
-
|
|
3248
|
+
const root = captioned(function root(fn) {
|
|
3249
|
+
// When broken, `atomic`/`batch` throws immediately. DOM wrappers (e.g. Sursaut
|
|
3250
|
+
// `root\`event:…\``) still need to run listener code for inspection UI; skip batching.
|
|
3251
|
+
const runner = broken ? fn : atomic(fn);
|
|
3252
|
+
const external = externalReasonFrom(fn);
|
|
3253
|
+
return external
|
|
3254
|
+
? externalReason.with(external, () => effectHistory.root(runner))
|
|
3255
|
+
: effectHistory.root(runner);
|
|
3256
|
+
});
|
|
2874
3257
|
function biDi(received, get, set) {
|
|
2875
3258
|
if (typeof get !== 'function') {
|
|
2876
3259
|
set = get.set;
|
|
2877
3260
|
get = get.get;
|
|
2878
3261
|
}
|
|
2879
3262
|
let programmaticallySetValue = Symbol();
|
|
2880
|
-
effect
|
|
3263
|
+
effect `biDi`(markWithRoot(() => {
|
|
2881
3264
|
const newValue = get();
|
|
2882
3265
|
const pValue = programmaticallySetValue;
|
|
2883
3266
|
programmaticallySetValue = Symbol();
|
|
@@ -2892,6 +3275,86 @@ function biDi(received, get, set) {
|
|
|
2892
3275
|
: () => { };
|
|
2893
3276
|
}
|
|
2894
3277
|
|
|
3278
|
+
/**
|
|
3279
|
+
* Creates a mixin that can be used both as a class (extends) and as a function (mixin)
|
|
3280
|
+
*
|
|
3281
|
+
* This function supports:
|
|
3282
|
+
* - Using mixins as base classes: `class MyClass extends MyMixin`
|
|
3283
|
+
* - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
|
|
3284
|
+
* - Composing mixins: `const Composed = MixinA(MixinB)`
|
|
3285
|
+
* - Type-safe property inference for all patterns
|
|
3286
|
+
*
|
|
3287
|
+
* @param mixinFunction - The function that creates the mixin
|
|
3288
|
+
* @param unwrapFunction - Optional function to unwrap reactive objects for method calls
|
|
3289
|
+
* @returns A mixin that can be used both as a class and as a function
|
|
3290
|
+
*/
|
|
3291
|
+
function mixin(mixinFunction, unwrapFunction) {
|
|
3292
|
+
/**
|
|
3293
|
+
* Cache for mixin results to ensure the same base class always returns the same mixed class
|
|
3294
|
+
*/
|
|
3295
|
+
const mixinCache = new WeakMap();
|
|
3296
|
+
// Apply the mixin to Object as the base class
|
|
3297
|
+
const MixedBase = mixinFunction(Object);
|
|
3298
|
+
mixinCache.set(Object, MixedBase);
|
|
3299
|
+
// Create the proxy that handles both constructor and function calls
|
|
3300
|
+
return new Proxy(MixedBase, {
|
|
3301
|
+
// Handle `MixinClass(SomeBase)` - use as mixin function
|
|
3302
|
+
apply(_target, _thisArg, args) {
|
|
3303
|
+
if (args.length === 0) {
|
|
3304
|
+
throw new Error('Mixin requires a base class');
|
|
3305
|
+
}
|
|
3306
|
+
const baseClass = args[0];
|
|
3307
|
+
if (typeof baseClass !== 'function') {
|
|
3308
|
+
throw new Error('Mixin requires a constructor function');
|
|
3309
|
+
}
|
|
3310
|
+
// Check if it's a valid constructor or a mixin
|
|
3311
|
+
if (!isConstructor(baseClass) &&
|
|
3312
|
+
!(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
|
|
3313
|
+
throw new Error('Mixin requires a valid constructor');
|
|
3314
|
+
}
|
|
3315
|
+
// Check cache first
|
|
3316
|
+
const cached = mixinCache.get(baseClass);
|
|
3317
|
+
if (cached) {
|
|
3318
|
+
return cached;
|
|
3319
|
+
}
|
|
3320
|
+
let usedBase = baseClass;
|
|
3321
|
+
if (unwrapFunction) {
|
|
3322
|
+
// Create a proxied base class that handles method unwrapping
|
|
3323
|
+
const ProxiedBaseClass = class extends baseClass {
|
|
3324
|
+
};
|
|
3325
|
+
// Proxy the prototype methods to handle unwrapping
|
|
3326
|
+
const originalPrototype = baseClass.prototype;
|
|
3327
|
+
const proxiedPrototype = new Proxy(originalPrototype, {
|
|
3328
|
+
get(target, prop, receiver) {
|
|
3329
|
+
const value = FoolProof.get(target, prop, receiver);
|
|
3330
|
+
// Only wrap methods that are likely to access private fields
|
|
3331
|
+
// Skip symbols and special properties that the reactive system needs
|
|
3332
|
+
if (typeof value === 'function' &&
|
|
3333
|
+
typeof prop === 'string' &&
|
|
3334
|
+
!['constructor', 'toString', 'valueOf'].includes(prop)) {
|
|
3335
|
+
// Return a wrapped version that uses unwrapped context
|
|
3336
|
+
return function (...args) {
|
|
3337
|
+
// Use the unwrapping function if provided, otherwise use this
|
|
3338
|
+
const context = unwrapFunction(this);
|
|
3339
|
+
return value.apply(context, args);
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
return value;
|
|
3343
|
+
},
|
|
3344
|
+
});
|
|
3345
|
+
// Set the proxied prototype
|
|
3346
|
+
Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
|
|
3347
|
+
usedBase = ProxiedBaseClass;
|
|
3348
|
+
}
|
|
3349
|
+
// Create the mixed class using the proxied base class
|
|
3350
|
+
const mixedClass = mixinFunction(usedBase);
|
|
3351
|
+
// Cache the result
|
|
3352
|
+
mixinCache.set(baseClass, mixedClass);
|
|
3353
|
+
return mixedClass;
|
|
3354
|
+
},
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
|
|
2895
3358
|
// Track which objects contain which other objects (back-references)
|
|
2896
3359
|
const objectParents = new WeakMap();
|
|
2897
3360
|
// Track which objects have deep watchers
|
|
@@ -3021,14 +3484,15 @@ function getState(obj) {
|
|
|
3021
3484
|
}
|
|
3022
3485
|
return state;
|
|
3023
3486
|
}
|
|
3024
|
-
function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
|
|
3487
|
+
function collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, ...keyChains) {
|
|
3025
3488
|
const sourceEffect = getActiveEffect();
|
|
3026
3489
|
for (const keys of keyChains)
|
|
3027
3490
|
for (const key of keys) {
|
|
3028
3491
|
const deps = objectWatchers.get(key);
|
|
3029
3492
|
if (deps) {
|
|
3030
3493
|
// Make sure `some.prop++` does not keep a dependency to `some.props`
|
|
3031
|
-
|
|
3494
|
+
if (sourceEffect)
|
|
3495
|
+
deps.delete(sourceEffect);
|
|
3032
3496
|
for (const effect of deps) {
|
|
3033
3497
|
const runningChain = isRunning(effect);
|
|
3034
3498
|
if (runningChain) {
|
|
@@ -3036,7 +3500,7 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
|
|
|
3036
3500
|
continue;
|
|
3037
3501
|
}
|
|
3038
3502
|
if (!effects.has(effect)) {
|
|
3039
|
-
effects.set(effect, getDependencyStack(effect, obj, key));
|
|
3503
|
+
effects.set(effect, collectDependencyStack ? getDependencyStack(effect, obj, key) : undefined);
|
|
3040
3504
|
if (!hasBatched(effect))
|
|
3041
3505
|
recordActivation(effect, obj, evolution, key);
|
|
3042
3506
|
}
|
|
@@ -3069,16 +3533,18 @@ function touched(obj, evolution, props) {
|
|
|
3069
3533
|
const effects = new Map();
|
|
3070
3534
|
const structural = !['set', 'invalidate'].includes(evolution.type);
|
|
3071
3535
|
const broad = structural ? [allProps, keysOf] : [allProps];
|
|
3536
|
+
const gatherReasons = options.introspection?.gatherReasons;
|
|
3537
|
+
const lineageConfig = gatherReasons?.lineages;
|
|
3538
|
+
const collectDependencyStack = lineageConfig === 'dependency' || lineageConfig === 'both';
|
|
3072
3539
|
if (props)
|
|
3073
|
-
collectEffects(obj, evolution, effects, objectWatchers, broad, props);
|
|
3540
|
+
collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, broad, props);
|
|
3074
3541
|
else
|
|
3075
|
-
collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
|
|
3542
|
+
collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, objectWatchers.keys());
|
|
3076
3543
|
const triggers = Array.from(effects.keys());
|
|
3544
|
+
const sourceEffect = getActiveEffect();
|
|
3077
3545
|
optionCall('touched', obj, evolution, props, triggers);
|
|
3078
3546
|
// Store pending triggers for CleanupReason before batching
|
|
3079
|
-
if (
|
|
3080
|
-
const gatherReasons = options.introspection.gatherReasons;
|
|
3081
|
-
const lineageConfig = gatherReasons.lineages;
|
|
3547
|
+
if (gatherReasons && effects.size > 0) {
|
|
3082
3548
|
let touchLineage;
|
|
3083
3549
|
if (lineageConfig === 'touch' || lineageConfig === 'both') {
|
|
3084
3550
|
touchLineage = debugHooks.captureLineage();
|
|
@@ -3095,7 +3561,7 @@ function touched(obj, evolution, props) {
|
|
|
3095
3561
|
});
|
|
3096
3562
|
}
|
|
3097
3563
|
}
|
|
3098
|
-
batch(triggers);
|
|
3564
|
+
batch(triggers, { caller: sourceEffect });
|
|
3099
3565
|
}
|
|
3100
3566
|
// Bubble up changes if this object has deep watchers
|
|
3101
3567
|
if (objectsWithDeepWatchers.has(obj)) {
|
|
@@ -3119,6 +3585,7 @@ function touchedOpaque(obj, evolution, prop) {
|
|
|
3119
3585
|
const gather = options.introspection?.gatherReasons;
|
|
3120
3586
|
if (gather) {
|
|
3121
3587
|
const lineageConfig = gather.lineages;
|
|
3588
|
+
let touchLineage;
|
|
3122
3589
|
for (const effect of deps) {
|
|
3123
3590
|
const node = getEffectNode(effect);
|
|
3124
3591
|
if (!node.isOpaque)
|
|
@@ -3130,10 +3597,9 @@ function touchedOpaque(obj, evolution, prop) {
|
|
|
3130
3597
|
}
|
|
3131
3598
|
effects.add(effect);
|
|
3132
3599
|
if (gather) {
|
|
3133
|
-
let touchLineage;
|
|
3134
3600
|
let dependencyStack;
|
|
3135
3601
|
if (lineageConfig === 'touch' || lineageConfig === 'both') {
|
|
3136
|
-
touchLineage = debugHooks.captureLineage();
|
|
3602
|
+
touchLineage ?? (touchLineage = debugHooks.captureLineage());
|
|
3137
3603
|
}
|
|
3138
3604
|
if (lineageConfig === 'dependency' || lineageConfig === 'both') {
|
|
3139
3605
|
dependencyStack = getDependencyStack(effect, obj, prop);
|
|
@@ -3169,7 +3635,7 @@ function touchedOpaque(obj, evolution, prop) {
|
|
|
3169
3635
|
}
|
|
3170
3636
|
if (effects.size > 0) {
|
|
3171
3637
|
optionCall('touched', obj, evolution, [prop], Array.from(effects));
|
|
3172
|
-
batch(Array.from(effects));
|
|
3638
|
+
batch(Array.from(effects), { caller: sourceEffect });
|
|
3173
3639
|
}
|
|
3174
3640
|
}
|
|
3175
3641
|
|
|
@@ -3191,9 +3657,10 @@ function addUnreactiveProps(proto, set) {
|
|
|
3191
3657
|
return proto;
|
|
3192
3658
|
}
|
|
3193
3659
|
// Merge sets
|
|
3194
|
-
|
|
3660
|
+
const merged = new Set(existing);
|
|
3661
|
+
proto[unreactiveProperties] = merged;
|
|
3195
3662
|
for (const p of set)
|
|
3196
|
-
|
|
3663
|
+
merged.add(p);
|
|
3197
3664
|
}
|
|
3198
3665
|
// If no set, mark as fully unreactive, otherwise create set
|
|
3199
3666
|
else
|
|
@@ -3215,6 +3682,8 @@ function nonReactive(...obj) {
|
|
|
3215
3682
|
}
|
|
3216
3683
|
return obj[0];
|
|
3217
3684
|
}
|
|
3685
|
+
const markRaw = nonReactive;
|
|
3686
|
+
const markRawProps = addUnreactiveProps;
|
|
3218
3687
|
function nonReactiveClass(...cls) {
|
|
3219
3688
|
for (const c of cls)
|
|
3220
3689
|
if (c)
|
|
@@ -3292,7 +3761,7 @@ function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty)
|
|
|
3292
3761
|
const origin = { obj: unwrappedObj, prop };
|
|
3293
3762
|
// Deep touch: only notify nested property changes with origin filtering
|
|
3294
3763
|
// Don't notify direct property change - the whole point is to avoid parent effects re-running
|
|
3295
|
-
const changes = untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
3764
|
+
const changes = untracked `deepTouch:recursive`(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
3296
3765
|
// When deep touch found no child differences, the object identity still changed.
|
|
3297
3766
|
// Migrate watchers from old → new so the dependency chain is preserved.
|
|
3298
3767
|
if (changes.length === 0) {
|
|
@@ -3432,7 +3901,7 @@ function dispatchNotifications(notifications) {
|
|
|
3432
3901
|
const originWatchers = watchers.get(origin.obj);
|
|
3433
3902
|
if (originWatchers) {
|
|
3434
3903
|
const originEffects = new Map();
|
|
3435
|
-
collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
|
|
3904
|
+
collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, false, [allProps], [origin.prop]);
|
|
3436
3905
|
allowedEffects = new Set(originEffects.keys());
|
|
3437
3906
|
}
|
|
3438
3907
|
// If no allowed effects, skip all notifications (no one should be notified)
|
|
@@ -3451,7 +3920,7 @@ function dispatchNotifications(notifications) {
|
|
|
3451
3920
|
if (objectWatchers) {
|
|
3452
3921
|
currentEffects = new Map();
|
|
3453
3922
|
const broad = evolution.type !== 'set' ? [allProps, keysOf] : [allProps];
|
|
3454
|
-
collectEffects(obj, evolution, currentEffects, objectWatchers, broad, propsArray);
|
|
3923
|
+
collectEffects(obj, evolution, currentEffects, objectWatchers, false, broad, propsArray);
|
|
3455
3924
|
// Filter effects by ancestor chain if origin exists
|
|
3456
3925
|
// Include effects that either directly depend on origin or have an ancestor that does
|
|
3457
3926
|
if (origin && allowedEffects) {
|
|
@@ -3513,11 +3982,94 @@ const metaProtos = new WeakMap();
|
|
|
3513
3982
|
const wrapProtos = new WeakMap();
|
|
3514
3983
|
const arrayLengths = new WeakMap();
|
|
3515
3984
|
const hasReentry = new Set();
|
|
3985
|
+
const accessAnalysisCache = new WeakMap();
|
|
3986
|
+
const readonlyObjectToProxy = new WeakMap();
|
|
3987
|
+
const shallowObjectToProxy = new WeakMap();
|
|
3988
|
+
const readonlyMutators = new Set([
|
|
3989
|
+
'copyWithin',
|
|
3990
|
+
'fill',
|
|
3991
|
+
'pop',
|
|
3992
|
+
'push',
|
|
3993
|
+
'reverse',
|
|
3994
|
+
'shift',
|
|
3995
|
+
'sort',
|
|
3996
|
+
'splice',
|
|
3997
|
+
'unshift',
|
|
3998
|
+
'add',
|
|
3999
|
+
'clear',
|
|
4000
|
+
'delete',
|
|
4001
|
+
'set',
|
|
4002
|
+
]);
|
|
3516
4003
|
// Sub-proxy registration for custom reactive behaviors
|
|
3517
4004
|
const subsRegister = new WeakMap();
|
|
3518
4005
|
// Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
|
|
3519
4006
|
// TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
|
|
3520
4007
|
let internalUntracked = false;
|
|
4008
|
+
function wrapReactiveValue(obj, prop, value) {
|
|
4009
|
+
// Optional fast-path for inert reads - skips reactive wrapping
|
|
4010
|
+
// Disabled by default for safety, can be enabled for performance-critical read-only contexts
|
|
4011
|
+
if (inertDepth > 0)
|
|
4012
|
+
return value;
|
|
4013
|
+
if (!isReactive(value) && typeof value === 'object' && value !== null) {
|
|
4014
|
+
const reactiveValue = reactiveObject(value);
|
|
4015
|
+
// Only create back-references if this object needs them
|
|
4016
|
+
if (needsBackReferences(obj)) {
|
|
4017
|
+
addBackReference(reactiveValue, obj, prop);
|
|
4018
|
+
}
|
|
4019
|
+
return reactiveValue;
|
|
4020
|
+
}
|
|
4021
|
+
return value;
|
|
4022
|
+
}
|
|
4023
|
+
function computeAccessAnalysis(obj, prop, receiver) {
|
|
4024
|
+
const proto = Object.getPrototypeOf(obj);
|
|
4025
|
+
const isOwnProp = Object.hasOwn(obj, prop);
|
|
4026
|
+
const shouldIgnoreAccessor = options.ignoreAccessors &&
|
|
4027
|
+
isOwnProp &&
|
|
4028
|
+
proto !== null &&
|
|
4029
|
+
(isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
|
|
4030
|
+
let hasProp = isOwnProp;
|
|
4031
|
+
let owner = isOwnProp ? obj : undefined;
|
|
4032
|
+
if (!isOwnProp) {
|
|
4033
|
+
let raw = proto;
|
|
4034
|
+
while (raw && raw !== Object.prototype) {
|
|
4035
|
+
if (Object.hasOwn(raw, prop)) {
|
|
4036
|
+
hasProp = true;
|
|
4037
|
+
owner = raw;
|
|
4038
|
+
break;
|
|
4039
|
+
}
|
|
4040
|
+
raw = Object.getPrototypeOf(raw);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
return {
|
|
4044
|
+
hasProp,
|
|
4045
|
+
owner,
|
|
4046
|
+
isInheritedAccess: hasProp && !isOwnProp,
|
|
4047
|
+
shouldIgnoreAccessor,
|
|
4048
|
+
ignoreAccessors: options.ignoreAccessors,
|
|
4049
|
+
instanceMembers: options.instanceMembers,
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
function analyzeAccess(obj, prop, receiver) {
|
|
4053
|
+
const proto = Object.getPrototypeOf(obj);
|
|
4054
|
+
if (Object.hasOwn(obj, prop))
|
|
4055
|
+
return computeAccessAnalysis(obj, prop, receiver);
|
|
4056
|
+
if (proto === null || Array.isArray(obj))
|
|
4057
|
+
return computeAccessAnalysis(obj, prop, receiver);
|
|
4058
|
+
let propCache = accessAnalysisCache.get(proto);
|
|
4059
|
+
if (!propCache) {
|
|
4060
|
+
propCache = new Map();
|
|
4061
|
+
accessAnalysisCache.set(proto, propCache);
|
|
4062
|
+
}
|
|
4063
|
+
const cached = propCache.get(prop);
|
|
4064
|
+
if (cached &&
|
|
4065
|
+
cached.ignoreAccessors === options.ignoreAccessors &&
|
|
4066
|
+
cached.instanceMembers === options.instanceMembers)
|
|
4067
|
+
return cached;
|
|
4068
|
+
const analysis = computeAccessAnalysis(obj, prop, receiver);
|
|
4069
|
+
if (analysis.hasProp)
|
|
4070
|
+
propCache.set(prop, analysis);
|
|
4071
|
+
return analysis;
|
|
4072
|
+
}
|
|
3521
4073
|
const reactiveHandlers = {
|
|
3522
4074
|
[Symbol.toStringTag]: 'MutTs Reactive',
|
|
3523
4075
|
get(obj, prop, receiver) {
|
|
@@ -3545,32 +4097,29 @@ const reactiveHandlers = {
|
|
|
3545
4097
|
// Symbols: fast-path — no reactivity tracking
|
|
3546
4098
|
if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
|
|
3547
4099
|
return FoolProof.get(obj, prop, receiver);
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
if (Object.hasOwn(raw, prop)) {
|
|
3566
|
-
hasProp = true;
|
|
3567
|
-
owner = raw;
|
|
3568
|
-
break;
|
|
4100
|
+
const subProxy = subsRegister.get(obj);
|
|
4101
|
+
if (inertDepth > 0) {
|
|
4102
|
+
const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
|
|
4103
|
+
return wrapReactiveValue(obj, prop, value);
|
|
4104
|
+
}
|
|
4105
|
+
const activeEffect = getActiveEffect();
|
|
4106
|
+
if (!activeEffect) {
|
|
4107
|
+
const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
|
|
4108
|
+
return wrapReactiveValue(obj, prop, value);
|
|
4109
|
+
}
|
|
4110
|
+
if (!subProxy && !Array.isArray(obj)) {
|
|
4111
|
+
const proto = Object.getPrototypeOf(obj);
|
|
4112
|
+
if (proto === Object.prototype || proto === null) {
|
|
4113
|
+
const ownDesc = Object.getOwnPropertyDescriptor(obj, prop);
|
|
4114
|
+
if (ownDesc && 'value' in ownDesc) {
|
|
4115
|
+
dependant(obj, prop);
|
|
4116
|
+
return wrapReactiveValue(obj, prop, ownDesc.value);
|
|
3569
4117
|
}
|
|
3570
|
-
raw = Object.getPrototypeOf(raw);
|
|
3571
4118
|
}
|
|
3572
4119
|
}
|
|
3573
|
-
|
|
4120
|
+
// Check if property exists using a trap-free walk to avoid triggering
|
|
4121
|
+
// the has-trap cascade on prototype chains of reactive proxies.
|
|
4122
|
+
const { hasProp, owner, isInheritedAccess, shouldIgnoreAccessor } = analyzeAccess(obj, prop, receiver);
|
|
3574
4123
|
// Depend if...
|
|
3575
4124
|
if (!hasProp ||
|
|
3576
4125
|
(!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
|
|
@@ -3583,16 +4132,8 @@ const reactiveHandlers = {
|
|
|
3583
4132
|
}
|
|
3584
4133
|
// For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
|
|
3585
4134
|
// For all other objects, inline Reflect.get directly (skips 3 function calls).
|
|
3586
|
-
const value = (
|
|
3587
|
-
|
|
3588
|
-
const reactiveValue = reactiveObject(value);
|
|
3589
|
-
// Only create back-references if this object needs them
|
|
3590
|
-
if (needsBackReferences(obj)) {
|
|
3591
|
-
addBackReference(reactiveValue, obj, prop);
|
|
3592
|
-
}
|
|
3593
|
-
return reactiveValue;
|
|
3594
|
-
}
|
|
3595
|
-
return value;
|
|
4135
|
+
const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
|
|
4136
|
+
return wrapReactiveValue(obj, prop, value);
|
|
3596
4137
|
},
|
|
3597
4138
|
set(obj, prop, value, receiver) {
|
|
3598
4139
|
const unwrapped = unwrap(receiver);
|
|
@@ -3665,6 +4206,11 @@ const reactiveHandlers = {
|
|
|
3665
4206
|
cycle: [], // We don't have the full cycle here, but we know it involves obj
|
|
3666
4207
|
});
|
|
3667
4208
|
hasReentry.add(obj);
|
|
4209
|
+
if (inertDepth > 0) {
|
|
4210
|
+
const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
|
|
4211
|
+
hasReentry.delete(obj);
|
|
4212
|
+
return rv;
|
|
4213
|
+
}
|
|
3668
4214
|
if (!internalUntracked && !isUnreactiveProp(obj, prop))
|
|
3669
4215
|
dependant(obj, prop);
|
|
3670
4216
|
const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
|
|
@@ -3673,7 +4219,7 @@ const reactiveHandlers = {
|
|
|
3673
4219
|
},
|
|
3674
4220
|
deleteProperty(obj, prop) {
|
|
3675
4221
|
if (!Object.hasOwn(obj, prop))
|
|
3676
|
-
return
|
|
4222
|
+
return true;
|
|
3677
4223
|
const oldVal = obj[prop];
|
|
3678
4224
|
// Remove back-references if this object has deep watchers
|
|
3679
4225
|
if (objectsWithDeepWatchers.has(obj) && typeof oldVal === 'object' && oldVal !== null) {
|
|
@@ -3688,6 +4234,9 @@ const reactiveHandlers = {
|
|
|
3688
4234
|
return true;
|
|
3689
4235
|
},
|
|
3690
4236
|
ownKeys(obj) {
|
|
4237
|
+
if (inertDepth > 0) {
|
|
4238
|
+
return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
|
|
4239
|
+
}
|
|
3691
4240
|
dependant(obj, keysOf);
|
|
3692
4241
|
return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
|
|
3693
4242
|
},
|
|
@@ -3696,6 +4245,98 @@ const reactiveHandlers = {
|
|
|
3696
4245
|
Reflect.getOwnPropertyDescriptor(obj, prop));
|
|
3697
4246
|
},
|
|
3698
4247
|
};
|
|
4248
|
+
function readonlyError(prop) {
|
|
4249
|
+
return new ReactiveError(`[reactive] Cannot mutate readonly reactive property '${String(prop)}'`, {
|
|
4250
|
+
code: ReactiveErrorCode.WriteInComputed,
|
|
4251
|
+
});
|
|
4252
|
+
}
|
|
4253
|
+
function readonlyValue(value) {
|
|
4254
|
+
if (!value || typeof value !== 'object')
|
|
4255
|
+
return value;
|
|
4256
|
+
return readonlyReactive(value);
|
|
4257
|
+
}
|
|
4258
|
+
const shallowReactiveHandlers = {
|
|
4259
|
+
get(obj, prop, receiver) {
|
|
4260
|
+
if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
|
|
4261
|
+
return Reflect.get(obj, prop, receiver);
|
|
4262
|
+
if (getActiveEffect())
|
|
4263
|
+
dependant(obj, prop);
|
|
4264
|
+
return Reflect.get(obj, prop, receiver);
|
|
4265
|
+
},
|
|
4266
|
+
set(obj, prop, value, receiver) {
|
|
4267
|
+
const unwrapped = unwrap(receiver);
|
|
4268
|
+
if (obj !== unwrapped)
|
|
4269
|
+
return Object.defineProperty(unwrapped, prop, {
|
|
4270
|
+
value,
|
|
4271
|
+
configurable: true,
|
|
4272
|
+
writable: true,
|
|
4273
|
+
enumerable: true,
|
|
4274
|
+
});
|
|
4275
|
+
if (isUnreactiveProp(obj, prop))
|
|
4276
|
+
return FoolProof.set(obj, prop, value, receiver);
|
|
4277
|
+
const hadProperty = Reflect.has(obj, prop);
|
|
4278
|
+
const oldVal = hadProperty ? Reflect.get(obj, prop, receiver) : absent;
|
|
4279
|
+
const newValue = unwrap(value);
|
|
4280
|
+
if (oldVal !== newValue && FoolProof.set(obj, prop, newValue, receiver)) {
|
|
4281
|
+
touched1(obj, { type: hadProperty ? 'set' : 'add', prop }, prop);
|
|
4282
|
+
}
|
|
4283
|
+
return true;
|
|
4284
|
+
},
|
|
4285
|
+
has(obj, prop) {
|
|
4286
|
+
return reactiveHandlers.has(obj, prop);
|
|
4287
|
+
},
|
|
4288
|
+
deleteProperty(obj, prop) {
|
|
4289
|
+
if (!Object.hasOwn(obj, prop))
|
|
4290
|
+
return true;
|
|
4291
|
+
delete obj[prop];
|
|
4292
|
+
touched1(obj, { type: 'del', prop }, prop);
|
|
4293
|
+
return true;
|
|
4294
|
+
},
|
|
4295
|
+
ownKeys(obj) {
|
|
4296
|
+
return reactiveHandlers.ownKeys(obj);
|
|
4297
|
+
},
|
|
4298
|
+
getOwnPropertyDescriptor(obj, prop) {
|
|
4299
|
+
return Reflect.getOwnPropertyDescriptor(obj, prop);
|
|
4300
|
+
},
|
|
4301
|
+
};
|
|
4302
|
+
const readonlyReactiveHandlers = {
|
|
4303
|
+
get(obj, prop, receiver) {
|
|
4304
|
+
if (readonlyMutators.has(prop)) {
|
|
4305
|
+
return () => {
|
|
4306
|
+
throw readonlyError(prop);
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
const reactiveTarget = reactiveObject(obj);
|
|
4310
|
+
const value = FoolProof.get(reactiveTarget, prop, receiver);
|
|
4311
|
+
if (typeof value === 'function') {
|
|
4312
|
+
return (...args) => readonlyValue(value.apply(reactiveTarget, args));
|
|
4313
|
+
}
|
|
4314
|
+
return readonlyValue(value);
|
|
4315
|
+
},
|
|
4316
|
+
set(_obj, prop) {
|
|
4317
|
+
throw readonlyError(prop);
|
|
4318
|
+
},
|
|
4319
|
+
deleteProperty(_obj, prop) {
|
|
4320
|
+
throw readonlyError(prop);
|
|
4321
|
+
},
|
|
4322
|
+
defineProperty(_obj, prop) {
|
|
4323
|
+
throw readonlyError(prop);
|
|
4324
|
+
},
|
|
4325
|
+
setPrototypeOf() {
|
|
4326
|
+
throw readonlyError('[[Prototype]]');
|
|
4327
|
+
},
|
|
4328
|
+
has(obj, prop) {
|
|
4329
|
+
const reactiveTarget = reactiveObject(obj);
|
|
4330
|
+
return Reflect.has(reactiveTarget, prop);
|
|
4331
|
+
},
|
|
4332
|
+
ownKeys(obj) {
|
|
4333
|
+
const reactiveTarget = reactiveObject(obj);
|
|
4334
|
+
return Reflect.ownKeys(reactiveTarget);
|
|
4335
|
+
},
|
|
4336
|
+
getOwnPropertyDescriptor(obj, prop) {
|
|
4337
|
+
return Reflect.getOwnPropertyDescriptor(obj, prop);
|
|
4338
|
+
},
|
|
4339
|
+
};
|
|
3699
4340
|
const reactiveClasses = new WeakSet();
|
|
3700
4341
|
// Create the ReactiveBase mixin
|
|
3701
4342
|
/**
|
|
@@ -3737,6 +4378,34 @@ function reactiveObject(anyTarget, subProxy) {
|
|
|
3737
4378
|
storeProxyRelationship(target, proxy);
|
|
3738
4379
|
return proxy;
|
|
3739
4380
|
}
|
|
4381
|
+
function shallowReactiveObject(anyTarget) {
|
|
4382
|
+
if (!anyTarget || typeof anyTarget !== 'object')
|
|
4383
|
+
return anyTarget;
|
|
4384
|
+
const target = unwrap(anyTarget);
|
|
4385
|
+
if (isNonReactive(target))
|
|
4386
|
+
return target;
|
|
4387
|
+
const existing = shallowObjectToProxy.get(target);
|
|
4388
|
+
if (existing)
|
|
4389
|
+
return existing;
|
|
4390
|
+
const proxy = new Proxy(target, shallowReactiveHandlers);
|
|
4391
|
+
shallowObjectToProxy.set(target, proxy);
|
|
4392
|
+
proxyToObject.set(proxy, target);
|
|
4393
|
+
return proxy;
|
|
4394
|
+
}
|
|
4395
|
+
function readonlyReactiveObject(anyTarget) {
|
|
4396
|
+
if (!anyTarget || typeof anyTarget !== 'object')
|
|
4397
|
+
return anyTarget;
|
|
4398
|
+
const target = unwrap(anyTarget);
|
|
4399
|
+
if (isNonReactive(target))
|
|
4400
|
+
return target;
|
|
4401
|
+
const existing = readonlyObjectToProxy.get(target);
|
|
4402
|
+
if (existing)
|
|
4403
|
+
return existing;
|
|
4404
|
+
const proxy = new Proxy(target, readonlyReactiveHandlers);
|
|
4405
|
+
readonlyObjectToProxy.set(target, proxy);
|
|
4406
|
+
proxyToObject.set(proxy, target);
|
|
4407
|
+
return proxy;
|
|
4408
|
+
}
|
|
3740
4409
|
/**
|
|
3741
4410
|
* Main decorator for making classes reactive
|
|
3742
4411
|
* Automatically makes class instances reactive when created
|
|
@@ -3767,6 +4436,8 @@ const reactive = decorator({
|
|
|
3767
4436
|
},
|
|
3768
4437
|
default: reactiveObject,
|
|
3769
4438
|
});
|
|
4439
|
+
const shallowReactive = shallowReactiveObject;
|
|
4440
|
+
const readonlyReactive = readonlyReactiveObject;
|
|
3770
4441
|
|
|
3771
|
-
export {
|
|
3772
|
-
//# sourceMappingURL=proxy-
|
|
4442
|
+
export { markRawProps as $, AZone as A, effectContext as B, CompareSymbol as C, DecoratorError as D, flavorOptions as E, flavored as F, formatCleanupReason as G, getActivationLog as H, IterableWeakMap as I, getActiveEffect as J, getState as K, hooks as L, inert as M, inheritCaption as N, isConstructor as O, isDev as P, isNonReactive as Q, ReactiveBase as R, isObject as S, isProd as T, isReactive as U, isReactiveBroken as V, isTest as W, legacyDecorator as X, link as Y, Zone as Z, markRaw as _, IterableWeakSet as a, mixin as a0, modernDecorator as a1, named as a2, objectToProxy as a3, onEffectThrow as a4, onReactiveBroken as a5, onReactiveReset as a6, prodPreset as a7, proxyToObject as a8, reactive as a9, inertDepth as aA, optionCall as aB, FoolProof as aC, effectHistory as aD, unreactiveProperties as aE, __runInitializers as aF, __esDecorate as aG, contentRef as aH, notifyPropertyChange as aI, metaProtos as aJ, wrapProtos as aK, objectParents as aL, watchers as aM, effectToReactiveObjects as aN, effectMarker as aO, setDebugHooks as aP, allProps as aQ, options as aa, readonlyReactive as ab, reset as ac, root as ad, shallowReactive as ae, tag as af, toRaw as ag, touched as ah, touched1 as ai, unlink as aj, untracked as ak, unwrap as al, withEffectContext as am, wrapInert as an, zip as ao, markWithRoot as ap, dependant as aq, getEffectNode as ar, chainExternalReason as as, keysOf as at, objectsWithDeepWatchers as au, effectToDeepWatchedObjects as av, deepWatchers as aw, registerDeepWatcher as ax, rootFunctionSymbol as ay, getRoot as az, ReactiveError as b, ReactiveErrorCode as c, ZoneAggregator as d, ZoneHistory as e, addBatchCleanup as f, addUnreactiveProps as g, arrayEquals as h, assertUntracked as i, asyncHook as j, asyncHooks as k, asyncZone as l, atom as m, atomic as n, biDi as o, captioned as p, captured as q, caught as r, createFlavor as s, debugPreset as t, decorator as u, deepCompare as v, defer as w, devPreset as x, effect as y, effectAggregator as z };
|
|
4443
|
+
//# sourceMappingURL=proxy-C2lnvvbx.esm.js.map
|