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,29 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
4
|
-
// Queue for hooks registered before the environment is ready (circular dependency fix)
|
|
5
|
-
const hooks = new Set();
|
|
6
|
-
const asyncHooks = {
|
|
7
|
-
addHook(hook) {
|
|
8
|
-
hooks.add(hook);
|
|
9
|
-
return () => hooks.delete(hook);
|
|
10
|
-
},
|
|
11
|
-
/**
|
|
12
|
-
* [Hack] Sanitize a promise (or value) to prevent context leaks.
|
|
13
|
-
* Default: Identity function.
|
|
14
|
-
* Browser: Uses Macrotask wrapping to break microtask chains.
|
|
15
|
-
*/
|
|
16
|
-
sanitizePromise(p) {
|
|
17
|
-
return p;
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Register a hook that will be called whenever an asynchronous operation is initiated.
|
|
22
|
-
* The hook should return a restorer function which will be called just before the async callback runs.
|
|
23
|
-
* That restorer should in turn return an undoer function which will be called just after the async callback finishes.
|
|
24
|
-
*/
|
|
25
|
-
const asyncHook = (hook) => asyncHooks.addHook(hook);
|
|
26
|
-
|
|
27
4
|
/**
|
|
28
5
|
* Yields tuples containing elements from each input array, stopping at the longest array length
|
|
29
6
|
* @param args - Arrays to zip together
|
|
@@ -139,13 +116,10 @@ function isOwnAccessor(obj, prop) {
|
|
|
139
116
|
return !!(opd?.get || opd?.set);
|
|
140
117
|
}
|
|
141
118
|
/**
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* @param b - Second value
|
|
147
|
-
* @param cache - Map for circular reference protection (internal use)
|
|
148
|
-
* @returns True if values are deeply equal
|
|
119
|
+
* Symbol used to provide custom comparison logic for an object.
|
|
120
|
+
*/
|
|
121
|
+
const CompareSymbol = Symbol.for('mutts.compare');
|
|
122
|
+
/**
|
|
149
123
|
*/
|
|
150
124
|
function deepCompare(a, b, cache = new Map()) {
|
|
151
125
|
if (a === b)
|
|
@@ -153,6 +127,13 @@ function deepCompare(a, b, cache = new Map()) {
|
|
|
153
127
|
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
|
|
154
128
|
return a === b;
|
|
155
129
|
}
|
|
130
|
+
// Custom comparison support
|
|
131
|
+
if (typeof a[CompareSymbol] === 'function') {
|
|
132
|
+
return a[CompareSymbol](b, (x, y) => deepCompare(x, y, cache));
|
|
133
|
+
}
|
|
134
|
+
if (typeof b[CompareSymbol] === 'function') {
|
|
135
|
+
return b[CompareSymbol](a, (x, y) => deepCompare(x, y, cache));
|
|
136
|
+
}
|
|
156
137
|
// Prototype check
|
|
157
138
|
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
|
|
158
139
|
return false;
|
|
@@ -271,8 +252,9 @@ function named(name, fn) {
|
|
|
271
252
|
});
|
|
272
253
|
return fn;
|
|
273
254
|
}
|
|
274
|
-
const
|
|
275
|
-
|
|
255
|
+
const runtimeGlobals = globalThis;
|
|
256
|
+
const _mode = runtimeGlobals.process?.env?.NODE_ENV ||
|
|
257
|
+
(typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunks/proxy-HA_QQnd5.cjs', document.baseURI).href)) }) !== 'undefined' && undefined?.MODE) ||
|
|
276
258
|
'production';
|
|
277
259
|
const isDev = _mode === 'development';
|
|
278
260
|
const isProd = _mode === 'production';
|
|
@@ -447,6 +429,93 @@ const decorator = (description) => {
|
|
|
447
429
|
* flavoredGreet.loud('World') // "HELLO, WORLD!"
|
|
448
430
|
* ```
|
|
449
431
|
*/
|
|
432
|
+
const captionedOptionsSymbol = Symbol('mutts.captioned.options');
|
|
433
|
+
function isTemplateStringsArray(value) {
|
|
434
|
+
return (Array.isArray(value) &&
|
|
435
|
+
Object.hasOwn(value, 'raw') &&
|
|
436
|
+
Array.isArray(value.raw));
|
|
437
|
+
}
|
|
438
|
+
function renderTemplate(strings, values) {
|
|
439
|
+
let result = strings[0] ?? '';
|
|
440
|
+
for (let i = 0; i < values.length; i++)
|
|
441
|
+
result += String(values[i]) + (strings[i + 1] ?? '');
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
function renameCallback(caption, callback) {
|
|
445
|
+
Object.defineProperty(callback, 'name', {
|
|
446
|
+
value: caption,
|
|
447
|
+
writable: false,
|
|
448
|
+
configurable: true,
|
|
449
|
+
});
|
|
450
|
+
return callback;
|
|
451
|
+
}
|
|
452
|
+
function isAnonymousCallback(callback) {
|
|
453
|
+
return !callback.name || callback.name === 'anonymous';
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Wraps a callback-first function so it also accepts a tagged-template call form.
|
|
457
|
+
*
|
|
458
|
+
* The template caption is applied to one callback argument before the base
|
|
459
|
+
* function runs. By default, `captioned` targets the first argument, but
|
|
460
|
+
* `callbackIndex` can point to any callback position.
|
|
461
|
+
*
|
|
462
|
+
* This is intended for APIs such as `effect`, `lift`, or `watch` where naming
|
|
463
|
+
* is useful but should remain separate from the flavor system.
|
|
464
|
+
*
|
|
465
|
+
* Plain calls still work:
|
|
466
|
+
* `run(callback)`
|
|
467
|
+
*
|
|
468
|
+
* Captioned calls add a runtime name to the first callback:
|
|
469
|
+
* `` run`task:${id}`(callback) ``
|
|
470
|
+
*
|
|
471
|
+
* Anonymous uncaptioned callbacks may trigger a warning depending on
|
|
472
|
+
* `shouldWarnAnonymous`.
|
|
473
|
+
*/
|
|
474
|
+
function captioned(fn, options = {}) {
|
|
475
|
+
const settings = {
|
|
476
|
+
callbackIndex: options.callbackIndex ?? 0,
|
|
477
|
+
name: options.name ?? (fn.name || 'callback'),
|
|
478
|
+
rename: options.rename ?? ((caption, callback) => renameCallback(caption, callback)),
|
|
479
|
+
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
480
|
+
warn: options.warn ?? ((message) => console.warn(message)),
|
|
481
|
+
shouldWarnAnonymous: options.shouldWarnAnonymous,
|
|
482
|
+
};
|
|
483
|
+
fn[captionedOptionsSymbol] = settings;
|
|
484
|
+
return new Proxy(fn, {
|
|
485
|
+
get(target, prop, receiver) {
|
|
486
|
+
if (prop === captionedOptionsSymbol)
|
|
487
|
+
return settings;
|
|
488
|
+
return Reflect.get(target, prop, receiver);
|
|
489
|
+
},
|
|
490
|
+
apply(target, thisArg, args) {
|
|
491
|
+
if (isTemplateStringsArray(args[0])) {
|
|
492
|
+
const caption = renderTemplate(args[0], args.slice(1));
|
|
493
|
+
return function captionedCall(...callArgs) {
|
|
494
|
+
const callback = callArgs[settings.callbackIndex];
|
|
495
|
+
if (typeof callback !== 'function')
|
|
496
|
+
throw new TypeError(`${settings.name} template calls require a callback at argument index ${settings.callbackIndex}`);
|
|
497
|
+
const nextArgs = [...callArgs];
|
|
498
|
+
nextArgs[settings.callbackIndex] = settings.rename(caption, callback);
|
|
499
|
+
return Reflect.apply(target, this, nextArgs);
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const callback = args[settings.callbackIndex];
|
|
503
|
+
if (typeof callback === 'function' && isAnonymousCallback(callback)) {
|
|
504
|
+
const shouldWarn = settings.shouldWarnAnonymous?.(callback, args) ?? true;
|
|
505
|
+
if (shouldWarn)
|
|
506
|
+
settings.warn(`${settings.name}: anonymous callback detected. Use template syntax for automatic naming:\n` +
|
|
507
|
+
` Current: ${settings.name}(() => { ... })\n` +
|
|
508
|
+
` Fix: ${settings.name}\`descriptive-name\`(() => { ... })\n` +
|
|
509
|
+
`The captioned system uses the template literal as the effect name for better debugging.`);
|
|
510
|
+
}
|
|
511
|
+
return Reflect.apply(target, thisArg, args);
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function inheritCaption(source, target) {
|
|
516
|
+
const settings = source[captionedOptionsSymbol];
|
|
517
|
+
return settings ? captioned(target, settings) : target;
|
|
518
|
+
}
|
|
450
519
|
/**
|
|
451
520
|
* Creates a flavored (extensible) version of a function with chainable property modifiers.
|
|
452
521
|
*/
|
|
@@ -479,7 +548,7 @@ function createFlavor(fn, transform, name) {
|
|
|
479
548
|
};
|
|
480
549
|
if (name)
|
|
481
550
|
named(name, fct);
|
|
482
|
-
return flavored(fct, fn.flavors || {});
|
|
551
|
+
return flavored(inheritCaption(fn, fct), fn.flavors || {});
|
|
483
552
|
}
|
|
484
553
|
/**
|
|
485
554
|
* Creates a new flavored function that merges options objects at a specific index.
|
|
@@ -512,7 +581,7 @@ function flavorOptions(fn, defaultOptions, opts = {}) {
|
|
|
512
581
|
// Preserve arity and options track
|
|
513
582
|
Object.defineProperty(fct, 'length', { value: fn.length });
|
|
514
583
|
fct.optionsIndex = targetIndex;
|
|
515
|
-
return flavored(fct, fn.flavors || {});
|
|
584
|
+
return flavored(inheritCaption(fn, fct), fn.flavors || {});
|
|
516
585
|
}
|
|
517
586
|
|
|
518
587
|
/// <reference lib="esnext.collection" />
|
|
@@ -693,7 +762,13 @@ class IterableWeakSet {
|
|
|
693
762
|
[Symbol.iterator]() {
|
|
694
763
|
return this.keys();
|
|
695
764
|
}
|
|
696
|
-
union(other) {
|
|
765
|
+
union(other, ...sets) {
|
|
766
|
+
if (sets.length > 0) {
|
|
767
|
+
for (const set of [other, ...sets])
|
|
768
|
+
for (const value of set)
|
|
769
|
+
this.add(value);
|
|
770
|
+
return this;
|
|
771
|
+
}
|
|
697
772
|
const others = {
|
|
698
773
|
[Symbol.iterator]() {
|
|
699
774
|
return other.keys();
|
|
@@ -765,100 +840,6 @@ class IterableWeakSet {
|
|
|
765
840
|
}
|
|
766
841
|
_b = Symbol.toStringTag;
|
|
767
842
|
|
|
768
|
-
/**
|
|
769
|
-
* Creates a mixin that can be used both as a class (extends) and as a function (mixin)
|
|
770
|
-
*
|
|
771
|
-
* This function supports:
|
|
772
|
-
* - Using mixins as base classes: `class MyClass extends MyMixin`
|
|
773
|
-
* - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
|
|
774
|
-
* - Composing mixins: `const Composed = MixinA(MixinB)`
|
|
775
|
-
* - Type-safe property inference for all patterns
|
|
776
|
-
*
|
|
777
|
-
* @param mixinFunction - The function that creates the mixin
|
|
778
|
-
* @param unwrapFunction - Optional function to unwrap reactive objects for method calls
|
|
779
|
-
* @returns A mixin that can be used both as a class and as a function
|
|
780
|
-
*/
|
|
781
|
-
function mixin(mixinFunction, unwrapFunction) {
|
|
782
|
-
/**
|
|
783
|
-
* Cache for mixin results to ensure the same base class always returns the same mixed class
|
|
784
|
-
*/
|
|
785
|
-
const mixinCache = new WeakMap();
|
|
786
|
-
// Apply the mixin to Object as the base class
|
|
787
|
-
const MixedBase = mixinFunction(Object);
|
|
788
|
-
mixinCache.set(Object, MixedBase);
|
|
789
|
-
// Create the proxy that handles both constructor and function calls
|
|
790
|
-
return new Proxy(MixedBase, {
|
|
791
|
-
// Handle `MixinClass(SomeBase)` - use as mixin function
|
|
792
|
-
apply(_target, _thisArg, args) {
|
|
793
|
-
if (args.length === 0) {
|
|
794
|
-
throw new Error('Mixin requires a base class');
|
|
795
|
-
}
|
|
796
|
-
const baseClass = args[0];
|
|
797
|
-
if (typeof baseClass !== 'function') {
|
|
798
|
-
throw new Error('Mixin requires a constructor function');
|
|
799
|
-
}
|
|
800
|
-
// Check if it's a valid constructor or a mixin
|
|
801
|
-
if (!isConstructor(baseClass) &&
|
|
802
|
-
!(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
|
|
803
|
-
throw new Error('Mixin requires a valid constructor');
|
|
804
|
-
}
|
|
805
|
-
// Check cache first
|
|
806
|
-
const cached = mixinCache.get(baseClass);
|
|
807
|
-
if (cached) {
|
|
808
|
-
return cached;
|
|
809
|
-
}
|
|
810
|
-
let usedBase = baseClass;
|
|
811
|
-
if (unwrapFunction) {
|
|
812
|
-
// Create a proxied base class that handles method unwrapping
|
|
813
|
-
const ProxiedBaseClass = class extends baseClass {
|
|
814
|
-
};
|
|
815
|
-
// Proxy the prototype methods to handle unwrapping
|
|
816
|
-
const originalPrototype = baseClass.prototype;
|
|
817
|
-
const proxiedPrototype = new Proxy(originalPrototype, {
|
|
818
|
-
get(target, prop, receiver) {
|
|
819
|
-
const value = FoolProof.get(target, prop, receiver);
|
|
820
|
-
// Only wrap methods that are likely to access private fields
|
|
821
|
-
// Skip symbols and special properties that the reactive system needs
|
|
822
|
-
if (typeof value === 'function' &&
|
|
823
|
-
typeof prop === 'string' &&
|
|
824
|
-
!['constructor', 'toString', 'valueOf'].includes(prop)) {
|
|
825
|
-
// Return a wrapped version that uses unwrapped context
|
|
826
|
-
return function (...args) {
|
|
827
|
-
// Use the unwrapping function if provided, otherwise use this
|
|
828
|
-
const context = unwrapFunction(this);
|
|
829
|
-
return value.apply(context, args);
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
return value;
|
|
833
|
-
},
|
|
834
|
-
});
|
|
835
|
-
// Set the proxied prototype
|
|
836
|
-
Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
|
|
837
|
-
usedBase = ProxiedBaseClass;
|
|
838
|
-
}
|
|
839
|
-
// Create the mixed class using the proxied base class
|
|
840
|
-
const mixedClass = mixinFunction(usedBase);
|
|
841
|
-
// Cache the result
|
|
842
|
-
mixinCache.set(baseClass, mixedClass);
|
|
843
|
-
return mixedClass;
|
|
844
|
-
},
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
const debugHooks = {
|
|
849
|
-
isDevtoolsEnabled: () => false,
|
|
850
|
-
registerEffect: () => { },
|
|
851
|
-
getTriggerChain: () => [],
|
|
852
|
-
captureStack: () => [],
|
|
853
|
-
captureLineage: () => new Error().stack,
|
|
854
|
-
formatStack: (stack) => [stack],
|
|
855
|
-
recordTriggerLink: () => { },
|
|
856
|
-
decorateError: () => { },
|
|
857
|
-
};
|
|
858
|
-
function setDebugHooks(hooks) {
|
|
859
|
-
Object.assign(debugHooks, hooks);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
843
|
/******************************************************************************
|
|
863
844
|
Copyright (c) Microsoft Corporation.
|
|
864
845
|
|
|
@@ -921,6 +902,29 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
921
902
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
922
903
|
};
|
|
923
904
|
|
|
905
|
+
// Queue for hooks registered before the environment is ready (circular dependency fix)
|
|
906
|
+
const hooks = new Set();
|
|
907
|
+
const asyncHooks = {
|
|
908
|
+
addHook(hook) {
|
|
909
|
+
hooks.add(hook);
|
|
910
|
+
return () => hooks.delete(hook);
|
|
911
|
+
},
|
|
912
|
+
/**
|
|
913
|
+
* [Hack] Sanitize a promise (or value) to prevent context leaks.
|
|
914
|
+
* Default: Identity function.
|
|
915
|
+
* Browser: Uses Macrotask wrapping to break microtask chains.
|
|
916
|
+
*/
|
|
917
|
+
sanitizePromise(p) {
|
|
918
|
+
return p;
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
/**
|
|
922
|
+
* Register a hook that will be called whenever an asynchronous operation is initiated.
|
|
923
|
+
* The hook should return a restorer function which will be called just before the async callback runs.
|
|
924
|
+
* That restorer should in turn return an undoer function which will be called just after the async callback finishes.
|
|
925
|
+
*/
|
|
926
|
+
const asyncHook = (hook) => asyncHooks.addHook(hook);
|
|
927
|
+
|
|
924
928
|
var _ZoneAggregator_zones;
|
|
925
929
|
function isu(z) {
|
|
926
930
|
return z;
|
|
@@ -1106,6 +1110,7 @@ function resetRegistry() {
|
|
|
1106
1110
|
* @returns The marked function
|
|
1107
1111
|
*/
|
|
1108
1112
|
function markWithRoot(fn, root) {
|
|
1113
|
+
const marked = fn;
|
|
1109
1114
|
// Check for collision
|
|
1110
1115
|
const existingRef = reverseRoots.get(root);
|
|
1111
1116
|
const existing = existingRef?.deref();
|
|
@@ -1120,8 +1125,8 @@ function markWithRoot(fn, root) {
|
|
|
1120
1125
|
// (Last writer wins for the check)
|
|
1121
1126
|
reverseRoots.set(root, new WeakRef(fn));
|
|
1122
1127
|
// Store root mapping as symbol property on the function
|
|
1123
|
-
|
|
1124
|
-
return
|
|
1128
|
+
marked[rootFunctionSymbol] = getRoot(root);
|
|
1129
|
+
return marked;
|
|
1125
1130
|
}
|
|
1126
1131
|
/**
|
|
1127
1132
|
* Gets the root function of a function for effect tracking
|
|
@@ -1141,11 +1146,30 @@ function getRoot(fn) {
|
|
|
1141
1146
|
const effectHistory = tag('effectHistory', new ZoneHistory());
|
|
1142
1147
|
tag('effectHistory.present', effectHistory.present);
|
|
1143
1148
|
asyncZone.add(effectHistory);
|
|
1149
|
+
const externalReason = tag('externalReason', new Zone());
|
|
1150
|
+
asyncZone.add(externalReason);
|
|
1144
1151
|
/**
|
|
1145
1152
|
* Aggregator for zones that need to be tracked along effects.
|
|
1146
1153
|
* ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
|
|
1147
1154
|
*/
|
|
1148
1155
|
const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
|
|
1156
|
+
effectAggregator.add(externalReason);
|
|
1157
|
+
function chainExternalReason(reason) {
|
|
1158
|
+
const external = externalReason.active;
|
|
1159
|
+
if (!external)
|
|
1160
|
+
return reason;
|
|
1161
|
+
if (!reason)
|
|
1162
|
+
return external;
|
|
1163
|
+
let current = reason;
|
|
1164
|
+
while (current) {
|
|
1165
|
+
if (current.type === 'external' &&
|
|
1166
|
+
external.type === 'external' &&
|
|
1167
|
+
current.detail === external.detail)
|
|
1168
|
+
return reason;
|
|
1169
|
+
current = current.chain;
|
|
1170
|
+
}
|
|
1171
|
+
return { ...reason, chain: chainExternalReason(reason.chain) };
|
|
1172
|
+
}
|
|
1149
1173
|
function isRunning(effect) {
|
|
1150
1174
|
const root = getRoot(effect);
|
|
1151
1175
|
return effectHistory.some((e) => getRoot(e) === root);
|
|
@@ -1153,6 +1177,35 @@ function isRunning(effect) {
|
|
|
1153
1177
|
function getActiveEffect() {
|
|
1154
1178
|
return effectHistory.present.active;
|
|
1155
1179
|
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Captures the current effect context so that deferred code can later
|
|
1182
|
+
* create child effects parented to this point in the effect tree.
|
|
1183
|
+
*
|
|
1184
|
+
* @returns An opaque token to pass to `withEffectContext()`
|
|
1185
|
+
*
|
|
1186
|
+
* @example
|
|
1187
|
+
* ```ts
|
|
1188
|
+
* const ctx = effectContext() // inside an effect or root()
|
|
1189
|
+
* // later, in a deferred callback:
|
|
1190
|
+
* withEffectContext(ctx, () => {
|
|
1191
|
+
* effect(() => { /* child of the captured context */ })
|
|
1192
|
+
* })
|
|
1193
|
+
* ```
|
|
1194
|
+
*/
|
|
1195
|
+
function effectContext() {
|
|
1196
|
+
return effectHistory.active;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Runs `fn` within a previously captured effect context.
|
|
1200
|
+
* Any effects created inside `fn` become children of the captured parent.
|
|
1201
|
+
*
|
|
1202
|
+
* @param ctx - The context token from `effectContext()`, or `undefined` for root context
|
|
1203
|
+
* @param fn - The function to execute within the restored context
|
|
1204
|
+
* @returns The return value of `fn`
|
|
1205
|
+
*/
|
|
1206
|
+
function withEffectContext(ctx, fn) {
|
|
1207
|
+
return effectHistory.with(ctx, fn);
|
|
1208
|
+
}
|
|
1156
1209
|
const cleanups = new WeakMap();
|
|
1157
1210
|
/**
|
|
1158
1211
|
* Attach cleanup dependencies to an object. When `unlink(obj)` is called,
|
|
@@ -1180,7 +1233,7 @@ const cleanups = new WeakMap();
|
|
|
1180
1233
|
function link(obj, ...cleanupFns) {
|
|
1181
1234
|
const set = cleanups.get(obj);
|
|
1182
1235
|
if (!set)
|
|
1183
|
-
cleanups.set(obj, new Set(cleanupFns.filter(
|
|
1236
|
+
cleanups.set(obj, new Set(cleanupFns.filter((fn) => fn !== undefined)));
|
|
1184
1237
|
else
|
|
1185
1238
|
for (const fn of cleanupFns)
|
|
1186
1239
|
if (fn)
|
|
@@ -1207,6 +1260,85 @@ function unlink(obj, reason) {
|
|
|
1207
1260
|
}
|
|
1208
1261
|
}
|
|
1209
1262
|
|
|
1263
|
+
function extractRawStack(error = new Error()) {
|
|
1264
|
+
if (typeof error === 'string')
|
|
1265
|
+
return error;
|
|
1266
|
+
if (error && typeof error === 'object' && 'stack' in error) {
|
|
1267
|
+
const stack = error.stack;
|
|
1268
|
+
return typeof stack === 'string' ? stack : undefined;
|
|
1269
|
+
}
|
|
1270
|
+
return undefined;
|
|
1271
|
+
}
|
|
1272
|
+
function trimStack(stack) {
|
|
1273
|
+
const raw = extractRawStack(stack);
|
|
1274
|
+
if (!raw)
|
|
1275
|
+
return [];
|
|
1276
|
+
const lines = raw
|
|
1277
|
+
.split('\n')
|
|
1278
|
+
.map((line) => line.trim())
|
|
1279
|
+
.filter(Boolean);
|
|
1280
|
+
if (lines[0]?.startsWith('Error'))
|
|
1281
|
+
lines.shift();
|
|
1282
|
+
while (lines[0] &&
|
|
1283
|
+
(lines[0].includes('captureLineage') ||
|
|
1284
|
+
lines[0].includes('captureDeferredLineage') ||
|
|
1285
|
+
lines[0].includes('debug-hooks.ts')))
|
|
1286
|
+
lines.shift();
|
|
1287
|
+
return lines;
|
|
1288
|
+
}
|
|
1289
|
+
function digestDeferredLineage(lineage) {
|
|
1290
|
+
if (lineage.segments)
|
|
1291
|
+
return lineage.segments;
|
|
1292
|
+
const segments = [];
|
|
1293
|
+
let effect = lineage.effect;
|
|
1294
|
+
let stack = trimStack(lineage.stack);
|
|
1295
|
+
if (!effect) {
|
|
1296
|
+
lineage.segments = [{ effectName: 'root', stack }];
|
|
1297
|
+
return lineage.segments;
|
|
1298
|
+
}
|
|
1299
|
+
while (effect) {
|
|
1300
|
+
const root = getRoot(effect);
|
|
1301
|
+
segments.push({
|
|
1302
|
+
effectName: root.name || 'anonymous',
|
|
1303
|
+
stack,
|
|
1304
|
+
});
|
|
1305
|
+
const node = getEffectNode(effect);
|
|
1306
|
+
effect = node.parent;
|
|
1307
|
+
stack = trimStack(node.creationStack);
|
|
1308
|
+
}
|
|
1309
|
+
if (stack.length)
|
|
1310
|
+
segments.push({ effectName: 'root', stack });
|
|
1311
|
+
lineage.segments = segments;
|
|
1312
|
+
return segments;
|
|
1313
|
+
}
|
|
1314
|
+
function formatDeferredLineage(lineage) {
|
|
1315
|
+
return digestDeferredLineage(lineage)
|
|
1316
|
+
.map((segment) => [`${segment.effectName}:`, ...segment.stack.map((line) => ` ${line}`)].join('\n'))
|
|
1317
|
+
.join('\n');
|
|
1318
|
+
}
|
|
1319
|
+
function captureDeferredLineage(effect = getActiveEffect(), stack = new Error()) {
|
|
1320
|
+
return {
|
|
1321
|
+
effect,
|
|
1322
|
+
stack,
|
|
1323
|
+
toString() {
|
|
1324
|
+
return formatDeferredLineage(this);
|
|
1325
|
+
},
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
const debugHooks = {
|
|
1329
|
+
isDevtoolsEnabled: () => false,
|
|
1330
|
+
registerEffect: () => { },
|
|
1331
|
+
getTriggerChain: () => [],
|
|
1332
|
+
captureStack: (error) => extractRawStack(error ?? new Error()),
|
|
1333
|
+
captureLineage: captureDeferredLineage,
|
|
1334
|
+
formatStack: (stack) => [stack],
|
|
1335
|
+
recordTriggerLink: () => { },
|
|
1336
|
+
decorateError: () => { },
|
|
1337
|
+
};
|
|
1338
|
+
function setDebugHooks(hooks) {
|
|
1339
|
+
Object.assign(debugHooks, hooks);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1210
1342
|
const effectMarker = {
|
|
1211
1343
|
enter: 'effect:enter',
|
|
1212
1344
|
leave: 'effect:leave',
|
|
@@ -1246,18 +1378,61 @@ function formatCleanupReason(reason, depth = 0) {
|
|
|
1246
1378
|
parts.push(',');
|
|
1247
1379
|
parts.push(...formatTrigger(reason.triggers[i]));
|
|
1248
1380
|
}
|
|
1381
|
+
if (reason.chain) {
|
|
1382
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1383
|
+
}
|
|
1384
|
+
return parts;
|
|
1385
|
+
}
|
|
1386
|
+
case 'stopped': {
|
|
1387
|
+
const parts = [`${indent}stopped`];
|
|
1388
|
+
if (reason.detail)
|
|
1389
|
+
parts.push(`(${reason.detail})`);
|
|
1390
|
+
if (reason.chain) {
|
|
1391
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1392
|
+
}
|
|
1393
|
+
return parts;
|
|
1394
|
+
}
|
|
1395
|
+
case 'external': {
|
|
1396
|
+
const parts = [`${indent}external:`, reason.detail];
|
|
1397
|
+
if (reason.chain) {
|
|
1398
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1399
|
+
}
|
|
1400
|
+
return parts;
|
|
1401
|
+
}
|
|
1402
|
+
case 'gc': {
|
|
1403
|
+
const parts = [`${indent}gc`];
|
|
1404
|
+
if (reason.chain) {
|
|
1405
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1406
|
+
}
|
|
1407
|
+
return parts;
|
|
1408
|
+
}
|
|
1409
|
+
case 'error': {
|
|
1410
|
+
const parts = [`${indent}error:`, reason.error];
|
|
1411
|
+
if (reason.chain) {
|
|
1412
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1413
|
+
}
|
|
1414
|
+
return parts;
|
|
1415
|
+
}
|
|
1416
|
+
case 'lineage': {
|
|
1417
|
+
const parts = [
|
|
1418
|
+
`${indent}lineage ←\n`,
|
|
1419
|
+
...formatCleanupReason(reason.parent, depth + 1),
|
|
1420
|
+
];
|
|
1421
|
+
if (reason.chain) {
|
|
1422
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1423
|
+
}
|
|
1424
|
+
return parts;
|
|
1425
|
+
}
|
|
1426
|
+
case 'invalidate': {
|
|
1427
|
+
const parts = [
|
|
1428
|
+
`${indent}invalidate ←\n`,
|
|
1429
|
+
...formatCleanupReason(reason.cause, depth + 1),
|
|
1430
|
+
];
|
|
1431
|
+
if (reason.chain) {
|
|
1432
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1433
|
+
}
|
|
1249
1434
|
return parts;
|
|
1250
1435
|
}
|
|
1251
|
-
case 'stopped':
|
|
1252
|
-
return [`${indent}stopped`];
|
|
1253
|
-
case 'gc':
|
|
1254
|
-
return [`${indent}gc`];
|
|
1255
|
-
case 'error':
|
|
1256
|
-
return [`${indent}error:`, reason.error];
|
|
1257
|
-
case 'lineage':
|
|
1258
|
-
return [`${indent}lineage ←\n`, ...formatCleanupReason(reason.parent, depth + 1)];
|
|
1259
|
-
case 'invalidate':
|
|
1260
|
-
return [`${indent}invalidate ←\n`, ...formatCleanupReason(reason.cause, depth + 1)];
|
|
1261
1436
|
case 'multiple': {
|
|
1262
1437
|
const parts = [];
|
|
1263
1438
|
for (let i = 0; i < reason.reasons.length; i++) {
|
|
@@ -1265,6 +1440,9 @@ function formatCleanupReason(reason, depth = 0) {
|
|
|
1265
1440
|
parts.push('\n');
|
|
1266
1441
|
parts.push(...formatCleanupReason(reason.reasons[i], depth));
|
|
1267
1442
|
}
|
|
1443
|
+
if (reason.chain) {
|
|
1444
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1445
|
+
}
|
|
1268
1446
|
return parts;
|
|
1269
1447
|
}
|
|
1270
1448
|
}
|
|
@@ -1311,6 +1489,17 @@ class ReactiveError extends Error {
|
|
|
1311
1489
|
return this.debugInfo?.cause;
|
|
1312
1490
|
}
|
|
1313
1491
|
}
|
|
1492
|
+
function normalizeSchedulerMode(mode) {
|
|
1493
|
+
switch (mode) {
|
|
1494
|
+
case 'production':
|
|
1495
|
+
return 'raw';
|
|
1496
|
+
case 'development':
|
|
1497
|
+
return 'ordered';
|
|
1498
|
+
default:
|
|
1499
|
+
return mode;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
let schedulerMode = 'ordered';
|
|
1314
1503
|
// biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
|
|
1315
1504
|
/**
|
|
1316
1505
|
* Global options for the reactive system
|
|
@@ -1401,22 +1590,42 @@ const options = {
|
|
|
1401
1590
|
*/
|
|
1402
1591
|
onMemoizationDiscrepancy: undefined,
|
|
1403
1592
|
/**
|
|
1404
|
-
*
|
|
1593
|
+
* Effect scheduler mode.
|
|
1405
1594
|
*
|
|
1406
|
-
* - `'
|
|
1407
|
-
*
|
|
1408
|
-
*
|
|
1595
|
+
* - `'ordered'` (Default): maintains the causal effect graph so effects that are already
|
|
1596
|
+
* queued together can run in dependency order. It also preserves parent/child effect
|
|
1597
|
+
* lifecycle ordering and catches cycles eagerly when edges are created.
|
|
1409
1598
|
*
|
|
1410
|
-
* - `'
|
|
1411
|
-
*
|
|
1412
|
-
* basic path information. Good balance of debugging help with moderate overhead.
|
|
1599
|
+
* - `'raw'`: fastest FIFO scheduler. It does not maintain the effect graph.
|
|
1600
|
+
* Cycle detection is heuristic, using maxEffectChain execution counts.
|
|
1413
1601
|
*
|
|
1414
|
-
* - `'debug'`:
|
|
1415
|
-
*
|
|
1602
|
+
* - `'debug'`: ordered scheduling plus the most detailed graph diagnostics. Highest overhead,
|
|
1603
|
+
* best for investigation.
|
|
1416
1604
|
*
|
|
1417
|
-
* @default '
|
|
1605
|
+
* @default 'ordered'
|
|
1418
1606
|
*/
|
|
1419
|
-
|
|
1607
|
+
get scheduler() {
|
|
1608
|
+
return schedulerMode;
|
|
1609
|
+
},
|
|
1610
|
+
set scheduler(mode) {
|
|
1611
|
+
schedulerMode = mode;
|
|
1612
|
+
},
|
|
1613
|
+
/**
|
|
1614
|
+
* @deprecated Use `scheduler` instead.
|
|
1615
|
+
*
|
|
1616
|
+
* Backward-compatible alias for older names:
|
|
1617
|
+
* - `'production'` maps to `scheduler = 'raw'`
|
|
1618
|
+
* - `'development'` maps to `scheduler = 'ordered'`
|
|
1619
|
+
* - `'debug'` maps to `scheduler = 'debug'`
|
|
1620
|
+
*
|
|
1621
|
+
* The new names describe scheduler behavior rather than runtime environment.
|
|
1622
|
+
*/
|
|
1623
|
+
get cycleHandling() {
|
|
1624
|
+
return schedulerMode;
|
|
1625
|
+
},
|
|
1626
|
+
set cycleHandling(mode) {
|
|
1627
|
+
schedulerMode = normalizeSchedulerMode(mode);
|
|
1628
|
+
},
|
|
1420
1629
|
/**
|
|
1421
1630
|
* Internal flag used by memoization discrepancy detector to avoid counting calls in tests
|
|
1422
1631
|
* @warning Do not modify this flag manually, this flag is given by the engine
|
|
@@ -1465,6 +1674,8 @@ const options = {
|
|
|
1465
1674
|
asyncMode: 'cancel',
|
|
1466
1675
|
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
1467
1676
|
warn: (...args) => console.warn(...args),
|
|
1677
|
+
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
1678
|
+
error: (...args) => console.error(...args),
|
|
1468
1679
|
/**
|
|
1469
1680
|
* Introspection and debug aids. Set to `null` to disable all debug overhead in production.
|
|
1470
1681
|
*
|
|
@@ -1504,14 +1715,14 @@ function optionCall(name, ...args) {
|
|
|
1504
1715
|
/** Production preset: no introspection, heuristic cycle detection, minimal overhead */
|
|
1505
1716
|
const prodPreset = {
|
|
1506
1717
|
maxEffectReaction: 'throw',
|
|
1507
|
-
|
|
1718
|
+
scheduler: 'raw',
|
|
1508
1719
|
introspection: null,
|
|
1509
1720
|
onMemoizationDiscrepancy: undefined,
|
|
1510
1721
|
};
|
|
1511
|
-
/** Development preset
|
|
1722
|
+
/** Development preset: introspection on, early cycle detection, warnings */
|
|
1512
1723
|
const devPreset = {
|
|
1513
1724
|
maxEffectReaction: 'warn',
|
|
1514
|
-
|
|
1725
|
+
scheduler: 'ordered',
|
|
1515
1726
|
introspection: {
|
|
1516
1727
|
gatherReasons: { lineages: 'touch' },
|
|
1517
1728
|
logErrors: true,
|
|
@@ -1523,7 +1734,7 @@ const devPreset = {
|
|
|
1523
1734
|
/** Debug preset: full diagnostics, throws on violations, rich lineage capture */
|
|
1524
1735
|
const debugPreset = {
|
|
1525
1736
|
maxEffectReaction: 'debug',
|
|
1526
|
-
|
|
1737
|
+
scheduler: 'debug',
|
|
1527
1738
|
introspection: {
|
|
1528
1739
|
gatherReasons: { lineages: 'both' },
|
|
1529
1740
|
logErrors: true,
|
|
@@ -1546,6 +1757,7 @@ function unwrap(obj) {
|
|
|
1546
1757
|
return obj;
|
|
1547
1758
|
return proxyToObject.get(obj) || obj;
|
|
1548
1759
|
}
|
|
1760
|
+
const toRaw = unwrap;
|
|
1549
1761
|
function isReactive(obj) {
|
|
1550
1762
|
return proxyToObject.has(obj);
|
|
1551
1763
|
}
|
|
@@ -1593,7 +1805,8 @@ function dependant(obj, prop = allProps) {
|
|
|
1593
1805
|
if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps && prop !== keysOf))
|
|
1594
1806
|
return;
|
|
1595
1807
|
const node = getEffectNode(currentActiveEffect);
|
|
1596
|
-
|
|
1808
|
+
const hasDependencyHook = node.dependencyHook !== undefined;
|
|
1809
|
+
if (hasDependencyHook) {
|
|
1597
1810
|
node.dependencyHook(obj, prop);
|
|
1598
1811
|
}
|
|
1599
1812
|
let objectWatchers = exports.watchers.get(obj);
|
|
@@ -1616,25 +1829,25 @@ function dependant(obj, prop = allProps) {
|
|
|
1616
1829
|
exports.effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
|
|
1617
1830
|
}
|
|
1618
1831
|
// Store dependency stack if introspection is enabled
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
objStacks.set(prop, propStacks);
|
|
1632
|
-
}
|
|
1633
|
-
propStacks.set(currentActiveEffect, debugHooks.captureLineage());
|
|
1832
|
+
const lineageMode = options.introspection?.gatherReasons?.lineages;
|
|
1833
|
+
const shouldGatherDependencyLineage = lineageMode === 'dependency' || lineageMode === 'both';
|
|
1834
|
+
if (shouldGatherDependencyLineage) {
|
|
1835
|
+
let objStacks = dependencyStacks.get(obj);
|
|
1836
|
+
if (!objStacks) {
|
|
1837
|
+
objStacks = new Map();
|
|
1838
|
+
dependencyStacks.set(obj, objStacks);
|
|
1839
|
+
}
|
|
1840
|
+
let propStacks = objStacks.get(prop);
|
|
1841
|
+
if (!propStacks) {
|
|
1842
|
+
propStacks = new Map();
|
|
1843
|
+
objStacks.set(prop, propStacks);
|
|
1634
1844
|
}
|
|
1845
|
+
propStacks.set(currentActiveEffect, debugHooks.captureLineage());
|
|
1635
1846
|
}
|
|
1636
1847
|
}
|
|
1637
1848
|
|
|
1849
|
+
// Simple module to manage inert state without circular dependencies
|
|
1850
|
+
exports.inertDepth = 0;
|
|
1638
1851
|
/**
|
|
1639
1852
|
* Finds a cycle in a sequence of functions by looking for the first repetition
|
|
1640
1853
|
*/
|
|
@@ -1660,6 +1873,9 @@ function formatRoots(roots, limit = 20) {
|
|
|
1660
1873
|
const end = names.slice(-10);
|
|
1661
1874
|
return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
|
|
1662
1875
|
}
|
|
1876
|
+
function externalReasonFrom(fn) {
|
|
1877
|
+
return fn.name ? { type: 'external', detail: fn.name } : undefined;
|
|
1878
|
+
}
|
|
1663
1879
|
// Nested map structure for efficient counting and batch cleanup
|
|
1664
1880
|
// batchId -> effect root -> obj -> prop -> count
|
|
1665
1881
|
let activationRegistry;
|
|
@@ -1733,6 +1949,40 @@ let causesClosure = new WeakMap();
|
|
|
1733
1949
|
let consequencesClosure = new WeakMap();
|
|
1734
1950
|
// Batch re-entrance depth and broken state
|
|
1735
1951
|
let broken = false;
|
|
1952
|
+
/** True after an unrecoverable reactive failure until `reset()`. */
|
|
1953
|
+
function isReactiveBroken() {
|
|
1954
|
+
return broken;
|
|
1955
|
+
}
|
|
1956
|
+
const reactiveBrokenHandlers = new Set();
|
|
1957
|
+
const reactiveResetHandlers = new Set();
|
|
1958
|
+
function onReactiveBroken(handler) {
|
|
1959
|
+
reactiveBrokenHandlers.add(handler);
|
|
1960
|
+
return () => reactiveBrokenHandlers.delete(handler);
|
|
1961
|
+
}
|
|
1962
|
+
function onReactiveReset(handler) {
|
|
1963
|
+
reactiveResetHandlers.add(handler);
|
|
1964
|
+
return () => reactiveResetHandlers.delete(handler);
|
|
1965
|
+
}
|
|
1966
|
+
function notifyReactiveBroken(error) {
|
|
1967
|
+
for (const handler of Array.from(reactiveBrokenHandlers)) {
|
|
1968
|
+
try {
|
|
1969
|
+
handler(error);
|
|
1970
|
+
}
|
|
1971
|
+
catch (handlerError) {
|
|
1972
|
+
options.warn('[reactive] onReactiveBroken handler threw', handlerError);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function notifyReactiveReset() {
|
|
1977
|
+
for (const handler of Array.from(reactiveResetHandlers)) {
|
|
1978
|
+
try {
|
|
1979
|
+
handler();
|
|
1980
|
+
}
|
|
1981
|
+
catch (handlerError) {
|
|
1982
|
+
options.warn('[reactive] onReactiveReset handler threw', handlerError);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1736
1986
|
/**
|
|
1737
1987
|
* Gets or creates an IterableWeakSet for a closure map
|
|
1738
1988
|
*/
|
|
@@ -1751,7 +2001,7 @@ function getOrCreateClosure(closure, root) {
|
|
|
1751
2001
|
* @param targetRoot - Root function of the effect being triggered
|
|
1752
2002
|
*/
|
|
1753
2003
|
function addGraphEdge(callerRoot, targetRoot) {
|
|
1754
|
-
if (options.
|
|
2004
|
+
if (options.scheduler === 'raw')
|
|
1755
2005
|
return;
|
|
1756
2006
|
// Add to forward graph: callerRoot → targetRoot
|
|
1757
2007
|
const triggers = effectTriggers.get(callerRoot);
|
|
@@ -1866,7 +2116,7 @@ function hasPathExcluding(start, end, exclude) {
|
|
|
1866
2116
|
* @param effect - The effect being cleaned up
|
|
1867
2117
|
*/
|
|
1868
2118
|
function cleanupEffectFromGraph(effect) {
|
|
1869
|
-
if (options.
|
|
2119
|
+
if (options.scheduler === 'raw')
|
|
1870
2120
|
return;
|
|
1871
2121
|
const root = getRoot(effect);
|
|
1872
2122
|
// Get closures before removing direct edges (needed for propagation)
|
|
@@ -1975,7 +2225,7 @@ const executingStack = [];
|
|
|
1975
2225
|
* Called once when batch starts or when new effects are added
|
|
1976
2226
|
*/
|
|
1977
2227
|
function computeAllInDegrees(batch) {
|
|
1978
|
-
if (options.
|
|
2228
|
+
if (options.scheduler === 'raw')
|
|
1979
2229
|
return;
|
|
1980
2230
|
const activeEffect = getActiveEffect();
|
|
1981
2231
|
const activeRoot = activeEffect ? getRoot(activeEffect) : null;
|
|
@@ -1983,6 +2233,16 @@ function computeAllInDegrees(batch) {
|
|
|
1983
2233
|
batch.inDegrees.clear();
|
|
1984
2234
|
for (const [root] of batch.all) {
|
|
1985
2235
|
let inDegree = 0;
|
|
2236
|
+
// Make sure parents are executed before children: if parent is in batch, count it as a dependency
|
|
2237
|
+
const effect = batch.all.get(root);
|
|
2238
|
+
const parent = getEffectNode(effect).parent;
|
|
2239
|
+
const parentRoot = parent ? getRoot(parent) : undefined;
|
|
2240
|
+
if (parentRoot &&
|
|
2241
|
+
batch.all.has(parentRoot) &&
|
|
2242
|
+
parentRoot !== activeRoot &&
|
|
2243
|
+
parentRoot !== root) {
|
|
2244
|
+
inDegree++;
|
|
2245
|
+
}
|
|
1986
2246
|
const causes = causesClosure.get(root);
|
|
1987
2247
|
if (causes) {
|
|
1988
2248
|
for (const causeRoot of causes) {
|
|
@@ -2002,17 +2262,26 @@ function computeAllInDegrees(batch) {
|
|
|
2002
2262
|
function decrementInDegreesForExecuted(batch, executedRoot) {
|
|
2003
2263
|
// Get all effects that this executed effect triggers
|
|
2004
2264
|
const consequences = consequencesClosure.get(executedRoot);
|
|
2005
|
-
if (
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2265
|
+
if (consequences) {
|
|
2266
|
+
for (const consequenceRoot of consequences) {
|
|
2267
|
+
// Only update if it's still in the batch
|
|
2268
|
+
if (batch.all.has(consequenceRoot)) {
|
|
2269
|
+
const currentDegree = batch.inDegrees.get(consequenceRoot) ?? 0;
|
|
2270
|
+
if (currentDegree > 0) {
|
|
2271
|
+
batch.inDegrees.set(consequenceRoot, currentDegree - 1);
|
|
2272
|
+
}
|
|
2013
2273
|
}
|
|
2014
2274
|
}
|
|
2015
2275
|
}
|
|
2276
|
+
for (const [root, effect] of batch.all) {
|
|
2277
|
+
const parent = getEffectNode(effect).parent;
|
|
2278
|
+
if (!parent || getRoot(parent) !== executedRoot)
|
|
2279
|
+
continue;
|
|
2280
|
+
const currentDegree = batch.inDegrees.get(root) ?? 0;
|
|
2281
|
+
if (currentDegree > 0) {
|
|
2282
|
+
batch.inDegrees.set(root, currentDegree - 1);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2016
2285
|
}
|
|
2017
2286
|
/**
|
|
2018
2287
|
* Finds a path from startRoot to endRoot in the dependency graph
|
|
@@ -2102,6 +2371,14 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2102
2371
|
// Build reason from pending triggers if not provided
|
|
2103
2372
|
if (!reason && node.pendingTriggers) {
|
|
2104
2373
|
reason = { type: 'propChange', triggers: node.pendingTriggers };
|
|
2374
|
+
// Add chain: if this is being triggered from another effect, get its reason
|
|
2375
|
+
if (caller) {
|
|
2376
|
+
const callerNode = getEffectNode(caller);
|
|
2377
|
+
if (callerNode.currentReason) {
|
|
2378
|
+
reason.chain = callerNode.currentReason;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
reason = chainExternalReason(reason);
|
|
2105
2382
|
}
|
|
2106
2383
|
node.pendingTriggers = undefined;
|
|
2107
2384
|
if (reason) {
|
|
@@ -2143,15 +2420,15 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2143
2420
|
}
|
|
2144
2421
|
}
|
|
2145
2422
|
// 1. Add to batch first (needed for cycle detection)
|
|
2146
|
-
// TODO: Check if
|
|
2147
|
-
if (options.
|
|
2148
|
-
//
|
|
2423
|
+
// TODO: Check if this difference between raw and graph-backed scheduling is the right tradeoff.
|
|
2424
|
+
if (options.scheduler === 'raw') {
|
|
2425
|
+
// Raw mode: FIFO (delete and re-add to move to end)
|
|
2149
2426
|
if (currentBatch.all.has(root)) {
|
|
2150
2427
|
currentBatch.all.delete(root);
|
|
2151
2428
|
}
|
|
2152
2429
|
}
|
|
2153
2430
|
else {
|
|
2154
|
-
//
|
|
2431
|
+
// Graph-backed modes: skip if already queued — the existing entry will re-run
|
|
2155
2432
|
if (currentBatch.all.has(root)) {
|
|
2156
2433
|
return;
|
|
2157
2434
|
}
|
|
@@ -2160,7 +2437,7 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2160
2437
|
if (node.stopped)
|
|
2161
2438
|
return;
|
|
2162
2439
|
currentBatch.all.set(root, effect);
|
|
2163
|
-
if (caller && true && options.
|
|
2440
|
+
if (caller && true && options.scheduler !== 'raw') {
|
|
2164
2441
|
const callerRoot = getRoot(caller);
|
|
2165
2442
|
// const root = getRoot(effect) // Already have root
|
|
2166
2443
|
// Check for cycle BEFORE adding edge
|
|
@@ -2182,6 +2459,9 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2182
2459
|
}
|
|
2183
2460
|
addGraphEdge(callerRoot, root);
|
|
2184
2461
|
}
|
|
2462
|
+
if (options.scheduler !== 'raw') {
|
|
2463
|
+
computeAllInDegrees(currentBatch);
|
|
2464
|
+
}
|
|
2185
2465
|
}
|
|
2186
2466
|
/**
|
|
2187
2467
|
* Adds a cleanup function to be called when the current batch of effects completes
|
|
@@ -2278,7 +2558,7 @@ function executeNext(effectuatedRoots) {
|
|
|
2278
2558
|
// Find an effect with in-degree 0 using cached values
|
|
2279
2559
|
let nextEffect = null;
|
|
2280
2560
|
let nextRoot = null;
|
|
2281
|
-
if (options.
|
|
2561
|
+
if (options.scheduler === 'raw') {
|
|
2282
2562
|
// In flat mode, we just take the first effect in the queue (FIFO)
|
|
2283
2563
|
const first = currentBatch.all.entries().next().value;
|
|
2284
2564
|
if (first) {
|
|
@@ -2368,9 +2648,7 @@ function executeNext(effectuatedRoots) {
|
|
|
2368
2648
|
}
|
|
2369
2649
|
return result;
|
|
2370
2650
|
}
|
|
2371
|
-
|
|
2372
|
-
// These are all the effects triggered under `activeEffect` and all their sub-effects
|
|
2373
|
-
function batch(effect, immediate) {
|
|
2651
|
+
function batch(effect, batchOptions) {
|
|
2374
2652
|
if (broken) {
|
|
2375
2653
|
throw new ReactiveError('[reactive] Reactive system is broken after an unrecoverable error. Call reset() to recover.', { code: exports.ReactiveErrorCode.BrokenEffects });
|
|
2376
2654
|
}
|
|
@@ -2385,15 +2663,38 @@ function batch(effect, immediate) {
|
|
|
2385
2663
|
throw new Error('Activation registry already exists');
|
|
2386
2664
|
optionCall('beginChain', roots);
|
|
2387
2665
|
}
|
|
2388
|
-
|
|
2389
|
-
const
|
|
2666
|
+
const immediate = batchOptions?.immediate === true;
|
|
2667
|
+
const contained = batchOptions?.contained === true;
|
|
2668
|
+
const callerToUse = batchOptions?.caller || getActiveEffect();
|
|
2390
2669
|
// Optimization: If nested and NOT immediate, just join the existing batch
|
|
2391
|
-
if (!isNewBatch && !immediate) {
|
|
2670
|
+
if (!isNewBatch && !contained && !immediate) {
|
|
2392
2671
|
for (let i = 0; i < effect.length; i++) {
|
|
2393
|
-
addToBatch(effect[i],
|
|
2672
|
+
addToBatch(effect[i], callerToUse);
|
|
2394
2673
|
}
|
|
2395
2674
|
return;
|
|
2396
2675
|
}
|
|
2676
|
+
if (!isNewBatch && !contained && immediate) {
|
|
2677
|
+
const firstReturn = {};
|
|
2678
|
+
for (let i = 0; i < effect.length; i++) {
|
|
2679
|
+
executingStack.push(effect[i]);
|
|
2680
|
+
try {
|
|
2681
|
+
const node = getEffectNode(effect[i]);
|
|
2682
|
+
const reason = node.nextReason;
|
|
2683
|
+
if (node.cleanup) {
|
|
2684
|
+
const cleanup = node.cleanup;
|
|
2685
|
+
node.cleanup = undefined;
|
|
2686
|
+
cleanup(reason);
|
|
2687
|
+
}
|
|
2688
|
+
const rv = effect[i]();
|
|
2689
|
+
if (rv !== undefined && !('value' in firstReturn))
|
|
2690
|
+
firstReturn.value = rv;
|
|
2691
|
+
}
|
|
2692
|
+
finally {
|
|
2693
|
+
executingStack.pop();
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return firstReturn.value;
|
|
2697
|
+
}
|
|
2397
2698
|
const currentBatch = {
|
|
2398
2699
|
all: new Map(),
|
|
2399
2700
|
inDegrees: new Map(),
|
|
@@ -2401,9 +2702,11 @@ function batch(effect, immediate) {
|
|
|
2401
2702
|
};
|
|
2402
2703
|
batchStack.push(currentBatch);
|
|
2403
2704
|
let success = false;
|
|
2705
|
+
let failure;
|
|
2404
2706
|
try {
|
|
2405
2707
|
const effectuatedRoots = [];
|
|
2406
2708
|
const firstReturn = {};
|
|
2709
|
+
let initialError;
|
|
2407
2710
|
if (immediate) {
|
|
2408
2711
|
// Execute initial effects in providing order
|
|
2409
2712
|
for (let i = 0; i < effect.length; i++) {
|
|
@@ -2420,16 +2723,22 @@ function batch(effect, immediate) {
|
|
|
2420
2723
|
if (rv !== undefined && !('value' in firstReturn))
|
|
2421
2724
|
firstReturn.value = rv;
|
|
2422
2725
|
}
|
|
2726
|
+
catch (error) {
|
|
2727
|
+
initialError = error;
|
|
2728
|
+
break;
|
|
2729
|
+
}
|
|
2423
2730
|
finally {
|
|
2424
2731
|
executingStack.pop();
|
|
2425
2732
|
currentBatch.all.delete(getRoot(effect[i]));
|
|
2426
2733
|
}
|
|
2427
2734
|
}
|
|
2735
|
+
if (initialError)
|
|
2736
|
+
throw initialError;
|
|
2428
2737
|
}
|
|
2429
2738
|
else {
|
|
2430
2739
|
// Add initial effects to batch and compute dependencies
|
|
2431
2740
|
for (let i = 0; i < effect.length; i++) {
|
|
2432
|
-
addToBatch(effect[i],
|
|
2741
|
+
addToBatch(effect[i], callerToUse, false);
|
|
2433
2742
|
}
|
|
2434
2743
|
computeAllInDegrees(currentBatch);
|
|
2435
2744
|
}
|
|
@@ -2483,9 +2792,18 @@ function batch(effect, immediate) {
|
|
|
2483
2792
|
success = true;
|
|
2484
2793
|
return firstReturn.value;
|
|
2485
2794
|
}
|
|
2795
|
+
catch (error) {
|
|
2796
|
+
failure = error;
|
|
2797
|
+
if (batchStack.length === 1)
|
|
2798
|
+
optionCall('error', '[reactive] Root batch failure before broken state:', error);
|
|
2799
|
+
throw error;
|
|
2800
|
+
}
|
|
2486
2801
|
finally {
|
|
2487
2802
|
if (!success && batchStack.length === 1) {
|
|
2803
|
+
const wasBroken = broken;
|
|
2488
2804
|
broken = true;
|
|
2805
|
+
if (!wasBroken)
|
|
2806
|
+
notifyReactiveBroken(failure);
|
|
2489
2807
|
}
|
|
2490
2808
|
batchStack.pop();
|
|
2491
2809
|
if (batchStack.length === 0) {
|
|
@@ -2501,6 +2819,7 @@ function batch(effect, immediate) {
|
|
|
2501
2819
|
* All existing effects become orphaned and must be recreated.
|
|
2502
2820
|
*/
|
|
2503
2821
|
function reset() {
|
|
2822
|
+
const wasBroken = broken;
|
|
2504
2823
|
broken = false;
|
|
2505
2824
|
activationRegistry = undefined;
|
|
2506
2825
|
batchStack.length = 0;
|
|
@@ -2511,6 +2830,8 @@ function reset() {
|
|
|
2511
2830
|
resetRegistry();
|
|
2512
2831
|
resetTracking();
|
|
2513
2832
|
effectHistory.present.active = undefined;
|
|
2833
|
+
if (wasBroken)
|
|
2834
|
+
notifyReactiveReset();
|
|
2514
2835
|
}
|
|
2515
2836
|
// Inject batch function to allow atomic game loops in requestAnimationFrame/setTimeout/...
|
|
2516
2837
|
// Note: Automatic batching of async callbacks (setTimeout, Promise.then, etc.) is NOT implemented.
|
|
@@ -2527,7 +2848,7 @@ const atomic = decorator({
|
|
|
2527
2848
|
const atomicEffect = () => original.apply(this, args);
|
|
2528
2849
|
// Debug: helpful to have a name
|
|
2529
2850
|
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
|
|
2530
|
-
return batch(atomicEffect,
|
|
2851
|
+
return batch(atomicEffect, { immediate: true });
|
|
2531
2852
|
};
|
|
2532
2853
|
},
|
|
2533
2854
|
default(original) {
|
|
@@ -2535,7 +2856,7 @@ const atomic = decorator({
|
|
|
2535
2856
|
const atomicEffect = () => original.apply(this, args);
|
|
2536
2857
|
// Debug: helpful to have a name
|
|
2537
2858
|
Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
|
|
2538
|
-
return batch(atomicEffect,
|
|
2859
|
+
return batch(atomicEffect, { immediate: true });
|
|
2539
2860
|
};
|
|
2540
2861
|
},
|
|
2541
2862
|
});
|
|
@@ -2574,7 +2895,7 @@ function captured(prev, fn) {
|
|
|
2574
2895
|
* ```
|
|
2575
2896
|
*/
|
|
2576
2897
|
function atom(fn) {
|
|
2577
|
-
return batch(fn,
|
|
2898
|
+
return batch(fn, { immediate: true });
|
|
2578
2899
|
}
|
|
2579
2900
|
const fr = new FinalizationRegistry((f) => f());
|
|
2580
2901
|
/**
|
|
@@ -2583,7 +2904,7 @@ const fr = new FinalizationRegistry((f) => f());
|
|
|
2583
2904
|
* @param options - Options for effect execution
|
|
2584
2905
|
* @returns A cleanup function to stop the effect
|
|
2585
2906
|
*/
|
|
2586
|
-
const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
|
|
2907
|
+
const effect = captioned(named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
|
|
2587
2908
|
if (effectOptions?.name)
|
|
2588
2909
|
Object.defineProperty(fn, 'name', { value: effectOptions.name });
|
|
2589
2910
|
// Use per-effect asyncMode or fall back to global option
|
|
@@ -2596,7 +2917,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2596
2917
|
const prevCleanup = node.cleanup;
|
|
2597
2918
|
node.cleanup = undefined;
|
|
2598
2919
|
try {
|
|
2599
|
-
untracked(() => prevCleanup(node.nextReason || {
|
|
2920
|
+
untracked `effect:cleanup`(() => prevCleanup(chainExternalReason(node.nextReason || {
|
|
2921
|
+
type: 'stopped',
|
|
2922
|
+
chain: node.currentReason,
|
|
2923
|
+
})));
|
|
2600
2924
|
}
|
|
2601
2925
|
catch (error) {
|
|
2602
2926
|
// If we want to report them, we could use options.warn or similar
|
|
@@ -2629,6 +2953,9 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2629
2953
|
}
|
|
2630
2954
|
// Set reaction reason for the upcoming run
|
|
2631
2955
|
access.reaction = node.nextReason || access.reaction;
|
|
2956
|
+
node.currentReason =
|
|
2957
|
+
node.nextReason ||
|
|
2958
|
+
(access.reaction && access.reaction !== true ? access.reaction : undefined);
|
|
2632
2959
|
node.nextReason = undefined;
|
|
2633
2960
|
optionCall('enter', getRoot(fn));
|
|
2634
2961
|
optionCall('effectRun', getRoot(fn), access.reaction);
|
|
@@ -2691,7 +3018,8 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2691
3018
|
// This ensures that when we cancel, the original promise's .catch() handlers are triggered
|
|
2692
3019
|
// We do this by rejecting the race promise, which makes the original promise chain see the rejection
|
|
2693
3020
|
// through the zone-wrapped .then()/.catch() handlers
|
|
2694
|
-
runningPromise = runningPromise
|
|
3021
|
+
runningPromise = runningPromise
|
|
3022
|
+
.catch((error) => {
|
|
2695
3023
|
// Propagate async errors to the effect's error handler
|
|
2696
3024
|
// This ensures onEffectThrow handlers are triggered for async errors
|
|
2697
3025
|
if (error !== cancelError) {
|
|
@@ -2699,6 +3027,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2699
3027
|
}
|
|
2700
3028
|
// If thrower didn't throw (handled), we absorb the error.
|
|
2701
3029
|
// If thrower threw (unhandled), it propagates as a new unhandled rejection, which is correct.
|
|
3030
|
+
})
|
|
3031
|
+
.finally(() => {
|
|
3032
|
+
// Clear currentReason when async effect completes
|
|
3033
|
+
node.currentReason = undefined;
|
|
2702
3034
|
});
|
|
2703
3035
|
}
|
|
2704
3036
|
else {
|
|
@@ -2709,7 +3041,13 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2709
3041
|
catch (error) {
|
|
2710
3042
|
debugHooks.decorateError(error, runEffect);
|
|
2711
3043
|
// catcher:self`
|
|
2712
|
-
errorToThrow = error;
|
|
3044
|
+
errorToThrow = error instanceof Error ? error : new Error(String(error));
|
|
3045
|
+
}
|
|
3046
|
+
finally {
|
|
3047
|
+
// Clear currentReason for synchronous effects
|
|
3048
|
+
if (!runningPromise) {
|
|
3049
|
+
node.currentReason = undefined;
|
|
3050
|
+
}
|
|
2713
3051
|
}
|
|
2714
3052
|
// Create cleanup function for next run
|
|
2715
3053
|
node.cleanup = (reason) => {
|
|
@@ -2740,8 +3078,11 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2740
3078
|
const childReason = reason
|
|
2741
3079
|
? reason.type === 'lineage'
|
|
2742
3080
|
? reason
|
|
2743
|
-
: { type: 'lineage', parent: reason }
|
|
2744
|
-
: { type: 'stopped' }
|
|
3081
|
+
: { type: 'lineage', parent: reason, chain: node.currentReason }
|
|
3082
|
+
: (chainExternalReason({ type: 'stopped', chain: node.currentReason }) ?? {
|
|
3083
|
+
type: 'stopped',
|
|
3084
|
+
chain: node.currentReason,
|
|
3085
|
+
});
|
|
2745
3086
|
for (const childCleanup of children)
|
|
2746
3087
|
childCleanup(childReason);
|
|
2747
3088
|
delete node.children;
|
|
@@ -2754,7 +3095,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2754
3095
|
const node = getEffectNode(runEffect);
|
|
2755
3096
|
if (debugHooks.isDevtoolsEnabled()) {
|
|
2756
3097
|
const stack = debugHooks.captureStack(); // Robustly skips internal mutts frames
|
|
2757
|
-
if (
|
|
3098
|
+
if (stack) {
|
|
2758
3099
|
node.creationStack = stack;
|
|
2759
3100
|
}
|
|
2760
3101
|
}
|
|
@@ -2798,7 +3139,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2798
3139
|
abortController = undefined;
|
|
2799
3140
|
}
|
|
2800
3141
|
};
|
|
2801
|
-
batch(runEffect,
|
|
3142
|
+
batch(runEffect, { immediate: true });
|
|
2802
3143
|
// Only ROOT effects are registered for GC cleanup and zone tracking
|
|
2803
3144
|
const isRootEffect = !parent;
|
|
2804
3145
|
const stopEffect = (reason) => {
|
|
@@ -2814,7 +3155,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2814
3155
|
runningPromise = null;
|
|
2815
3156
|
}
|
|
2816
3157
|
try {
|
|
2817
|
-
node.cleanup?.(reason || { type: 'stopped' });
|
|
3158
|
+
node.cleanup?.(chainExternalReason(reason || { type: 'stopped', chain: node.currentReason }));
|
|
2818
3159
|
}
|
|
2819
3160
|
catch (error) {
|
|
2820
3161
|
// Cleanup errors should basically be ignored or at least not stop the world
|
|
@@ -2857,30 +3198,72 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2857
3198
|
named(name) {
|
|
2858
3199
|
return flavorOptions(this, { name }, { name: 'named' });
|
|
2859
3200
|
},
|
|
2860
|
-
}))
|
|
3201
|
+
})), {
|
|
3202
|
+
name: 'effect',
|
|
3203
|
+
warn: (message) => options.warn(`[reactive] ${message}`),
|
|
3204
|
+
shouldWarnAnonymous: (_callback, args) => !(args[1] && typeof args[1] === 'object' && 'name' in args[1]),
|
|
3205
|
+
});
|
|
3206
|
+
const untracked = captioned(function untracked(fn) {
|
|
3207
|
+
const external = externalReasonFrom(fn);
|
|
3208
|
+
return external
|
|
3209
|
+
? externalReason.with(external, () => effectHistory.present.root(fn))
|
|
3210
|
+
: effectHistory.present.root(fn);
|
|
3211
|
+
});
|
|
3212
|
+
function runInert(fn) {
|
|
3213
|
+
// Increment the counter
|
|
3214
|
+
const originalDepth = exports.inertDepth;
|
|
3215
|
+
exports.inertDepth = originalDepth + 1;
|
|
3216
|
+
try {
|
|
3217
|
+
return fn();
|
|
3218
|
+
}
|
|
3219
|
+
finally {
|
|
3220
|
+
exports.inertDepth = originalDepth;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
function wrapInert(fn) {
|
|
3224
|
+
function inertEffect(...args) {
|
|
3225
|
+
return runInert(() => fn.apply(this, args));
|
|
3226
|
+
}
|
|
3227
|
+
Object.defineProperty(inertEffect, 'name', { value: `inert(${fn.name})` });
|
|
3228
|
+
return inertEffect;
|
|
3229
|
+
}
|
|
2861
3230
|
/**
|
|
2862
|
-
* Executes a function
|
|
2863
|
-
*
|
|
3231
|
+
* Executes a function with fast-path reads that bypass proxy overhead and dependency tracking.
|
|
3232
|
+
* Writes remain fully reactive. Uses a counter for safe nesting.
|
|
3233
|
+
* Can also decorate methods so the whole method body runs inertly.
|
|
2864
3234
|
* @param fn - The function to execute
|
|
2865
3235
|
*/
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
3236
|
+
const inert = decorator({
|
|
3237
|
+
method(original) {
|
|
3238
|
+
return wrapInert(original);
|
|
3239
|
+
},
|
|
3240
|
+
default(fn) {
|
|
3241
|
+
if (typeof fn !== 'function')
|
|
3242
|
+
throw new Error('inert() expects a function');
|
|
3243
|
+
return runInert(fn);
|
|
3244
|
+
},
|
|
3245
|
+
});
|
|
2869
3246
|
/**
|
|
2870
3247
|
* Executes a function from a virgin/root context - no parent effect, no tracking
|
|
2871
3248
|
* Creates completely independent effects that won't be cleaned up by any parent
|
|
2872
3249
|
* @param fn - The function to execute
|
|
2873
3250
|
*/
|
|
2874
|
-
function root(fn) {
|
|
2875
|
-
|
|
2876
|
-
|
|
3251
|
+
const root = captioned(function root(fn) {
|
|
3252
|
+
// When broken, `atomic`/`batch` throws immediately. DOM wrappers (e.g. Sursaut
|
|
3253
|
+
// `root\`event:…\``) still need to run listener code for inspection UI; skip batching.
|
|
3254
|
+
const runner = broken ? fn : atomic(fn);
|
|
3255
|
+
const external = externalReasonFrom(fn);
|
|
3256
|
+
return external
|
|
3257
|
+
? externalReason.with(external, () => effectHistory.root(runner))
|
|
3258
|
+
: effectHistory.root(runner);
|
|
3259
|
+
});
|
|
2877
3260
|
function biDi(received, get, set) {
|
|
2878
3261
|
if (typeof get !== 'function') {
|
|
2879
3262
|
set = get.set;
|
|
2880
3263
|
get = get.get;
|
|
2881
3264
|
}
|
|
2882
3265
|
let programmaticallySetValue = Symbol();
|
|
2883
|
-
effect
|
|
3266
|
+
effect `biDi`(markWithRoot(() => {
|
|
2884
3267
|
const newValue = get();
|
|
2885
3268
|
const pValue = programmaticallySetValue;
|
|
2886
3269
|
programmaticallySetValue = Symbol();
|
|
@@ -2895,6 +3278,86 @@ function biDi(received, get, set) {
|
|
|
2895
3278
|
: () => { };
|
|
2896
3279
|
}
|
|
2897
3280
|
|
|
3281
|
+
/**
|
|
3282
|
+
* Creates a mixin that can be used both as a class (extends) and as a function (mixin)
|
|
3283
|
+
*
|
|
3284
|
+
* This function supports:
|
|
3285
|
+
* - Using mixins as base classes: `class MyClass extends MyMixin`
|
|
3286
|
+
* - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
|
|
3287
|
+
* - Composing mixins: `const Composed = MixinA(MixinB)`
|
|
3288
|
+
* - Type-safe property inference for all patterns
|
|
3289
|
+
*
|
|
3290
|
+
* @param mixinFunction - The function that creates the mixin
|
|
3291
|
+
* @param unwrapFunction - Optional function to unwrap reactive objects for method calls
|
|
3292
|
+
* @returns A mixin that can be used both as a class and as a function
|
|
3293
|
+
*/
|
|
3294
|
+
function mixin(mixinFunction, unwrapFunction) {
|
|
3295
|
+
/**
|
|
3296
|
+
* Cache for mixin results to ensure the same base class always returns the same mixed class
|
|
3297
|
+
*/
|
|
3298
|
+
const mixinCache = new WeakMap();
|
|
3299
|
+
// Apply the mixin to Object as the base class
|
|
3300
|
+
const MixedBase = mixinFunction(Object);
|
|
3301
|
+
mixinCache.set(Object, MixedBase);
|
|
3302
|
+
// Create the proxy that handles both constructor and function calls
|
|
3303
|
+
return new Proxy(MixedBase, {
|
|
3304
|
+
// Handle `MixinClass(SomeBase)` - use as mixin function
|
|
3305
|
+
apply(_target, _thisArg, args) {
|
|
3306
|
+
if (args.length === 0) {
|
|
3307
|
+
throw new Error('Mixin requires a base class');
|
|
3308
|
+
}
|
|
3309
|
+
const baseClass = args[0];
|
|
3310
|
+
if (typeof baseClass !== 'function') {
|
|
3311
|
+
throw new Error('Mixin requires a constructor function');
|
|
3312
|
+
}
|
|
3313
|
+
// Check if it's a valid constructor or a mixin
|
|
3314
|
+
if (!isConstructor(baseClass) &&
|
|
3315
|
+
!(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
|
|
3316
|
+
throw new Error('Mixin requires a valid constructor');
|
|
3317
|
+
}
|
|
3318
|
+
// Check cache first
|
|
3319
|
+
const cached = mixinCache.get(baseClass);
|
|
3320
|
+
if (cached) {
|
|
3321
|
+
return cached;
|
|
3322
|
+
}
|
|
3323
|
+
let usedBase = baseClass;
|
|
3324
|
+
if (unwrapFunction) {
|
|
3325
|
+
// Create a proxied base class that handles method unwrapping
|
|
3326
|
+
const ProxiedBaseClass = class extends baseClass {
|
|
3327
|
+
};
|
|
3328
|
+
// Proxy the prototype methods to handle unwrapping
|
|
3329
|
+
const originalPrototype = baseClass.prototype;
|
|
3330
|
+
const proxiedPrototype = new Proxy(originalPrototype, {
|
|
3331
|
+
get(target, prop, receiver) {
|
|
3332
|
+
const value = FoolProof.get(target, prop, receiver);
|
|
3333
|
+
// Only wrap methods that are likely to access private fields
|
|
3334
|
+
// Skip symbols and special properties that the reactive system needs
|
|
3335
|
+
if (typeof value === 'function' &&
|
|
3336
|
+
typeof prop === 'string' &&
|
|
3337
|
+
!['constructor', 'toString', 'valueOf'].includes(prop)) {
|
|
3338
|
+
// Return a wrapped version that uses unwrapped context
|
|
3339
|
+
return function (...args) {
|
|
3340
|
+
// Use the unwrapping function if provided, otherwise use this
|
|
3341
|
+
const context = unwrapFunction(this);
|
|
3342
|
+
return value.apply(context, args);
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
return value;
|
|
3346
|
+
},
|
|
3347
|
+
});
|
|
3348
|
+
// Set the proxied prototype
|
|
3349
|
+
Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
|
|
3350
|
+
usedBase = ProxiedBaseClass;
|
|
3351
|
+
}
|
|
3352
|
+
// Create the mixed class using the proxied base class
|
|
3353
|
+
const mixedClass = mixinFunction(usedBase);
|
|
3354
|
+
// Cache the result
|
|
3355
|
+
mixinCache.set(baseClass, mixedClass);
|
|
3356
|
+
return mixedClass;
|
|
3357
|
+
},
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
|
|
2898
3361
|
// Track which objects contain which other objects (back-references)
|
|
2899
3362
|
const objectParents = new WeakMap();
|
|
2900
3363
|
// Track which objects have deep watchers
|
|
@@ -3024,14 +3487,15 @@ function getState(obj) {
|
|
|
3024
3487
|
}
|
|
3025
3488
|
return state;
|
|
3026
3489
|
}
|
|
3027
|
-
function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
|
|
3490
|
+
function collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, ...keyChains) {
|
|
3028
3491
|
const sourceEffect = getActiveEffect();
|
|
3029
3492
|
for (const keys of keyChains)
|
|
3030
3493
|
for (const key of keys) {
|
|
3031
3494
|
const deps = objectWatchers.get(key);
|
|
3032
3495
|
if (deps) {
|
|
3033
3496
|
// Make sure `some.prop++` does not keep a dependency to `some.props`
|
|
3034
|
-
|
|
3497
|
+
if (sourceEffect)
|
|
3498
|
+
deps.delete(sourceEffect);
|
|
3035
3499
|
for (const effect of deps) {
|
|
3036
3500
|
const runningChain = isRunning(effect);
|
|
3037
3501
|
if (runningChain) {
|
|
@@ -3039,7 +3503,7 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
|
|
|
3039
3503
|
continue;
|
|
3040
3504
|
}
|
|
3041
3505
|
if (!effects.has(effect)) {
|
|
3042
|
-
effects.set(effect, getDependencyStack(effect, obj, key));
|
|
3506
|
+
effects.set(effect, collectDependencyStack ? getDependencyStack(effect, obj, key) : undefined);
|
|
3043
3507
|
if (!hasBatched(effect))
|
|
3044
3508
|
recordActivation(effect, obj, evolution, key);
|
|
3045
3509
|
}
|
|
@@ -3072,16 +3536,18 @@ function touched(obj, evolution, props) {
|
|
|
3072
3536
|
const effects = new Map();
|
|
3073
3537
|
const structural = !['set', 'invalidate'].includes(evolution.type);
|
|
3074
3538
|
const broad = structural ? [allProps, keysOf] : [allProps];
|
|
3539
|
+
const gatherReasons = options.introspection?.gatherReasons;
|
|
3540
|
+
const lineageConfig = gatherReasons?.lineages;
|
|
3541
|
+
const collectDependencyStack = lineageConfig === 'dependency' || lineageConfig === 'both';
|
|
3075
3542
|
if (props)
|
|
3076
|
-
collectEffects(obj, evolution, effects, objectWatchers, broad, props);
|
|
3543
|
+
collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, broad, props);
|
|
3077
3544
|
else
|
|
3078
|
-
collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
|
|
3545
|
+
collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, objectWatchers.keys());
|
|
3079
3546
|
const triggers = Array.from(effects.keys());
|
|
3547
|
+
const sourceEffect = getActiveEffect();
|
|
3080
3548
|
optionCall('touched', obj, evolution, props, triggers);
|
|
3081
3549
|
// Store pending triggers for CleanupReason before batching
|
|
3082
|
-
if (
|
|
3083
|
-
const gatherReasons = options.introspection.gatherReasons;
|
|
3084
|
-
const lineageConfig = gatherReasons.lineages;
|
|
3550
|
+
if (gatherReasons && effects.size > 0) {
|
|
3085
3551
|
let touchLineage;
|
|
3086
3552
|
if (lineageConfig === 'touch' || lineageConfig === 'both') {
|
|
3087
3553
|
touchLineage = debugHooks.captureLineage();
|
|
@@ -3098,7 +3564,7 @@ function touched(obj, evolution, props) {
|
|
|
3098
3564
|
});
|
|
3099
3565
|
}
|
|
3100
3566
|
}
|
|
3101
|
-
batch(triggers);
|
|
3567
|
+
batch(triggers, { caller: sourceEffect });
|
|
3102
3568
|
}
|
|
3103
3569
|
// Bubble up changes if this object has deep watchers
|
|
3104
3570
|
if (objectsWithDeepWatchers.has(obj)) {
|
|
@@ -3122,6 +3588,7 @@ function touchedOpaque(obj, evolution, prop) {
|
|
|
3122
3588
|
const gather = options.introspection?.gatherReasons;
|
|
3123
3589
|
if (gather) {
|
|
3124
3590
|
const lineageConfig = gather.lineages;
|
|
3591
|
+
let touchLineage;
|
|
3125
3592
|
for (const effect of deps) {
|
|
3126
3593
|
const node = getEffectNode(effect);
|
|
3127
3594
|
if (!node.isOpaque)
|
|
@@ -3133,10 +3600,9 @@ function touchedOpaque(obj, evolution, prop) {
|
|
|
3133
3600
|
}
|
|
3134
3601
|
effects.add(effect);
|
|
3135
3602
|
if (gather) {
|
|
3136
|
-
let touchLineage;
|
|
3137
3603
|
let dependencyStack;
|
|
3138
3604
|
if (lineageConfig === 'touch' || lineageConfig === 'both') {
|
|
3139
|
-
touchLineage = debugHooks.captureLineage();
|
|
3605
|
+
touchLineage ?? (touchLineage = debugHooks.captureLineage());
|
|
3140
3606
|
}
|
|
3141
3607
|
if (lineageConfig === 'dependency' || lineageConfig === 'both') {
|
|
3142
3608
|
dependencyStack = getDependencyStack(effect, obj, prop);
|
|
@@ -3172,7 +3638,7 @@ function touchedOpaque(obj, evolution, prop) {
|
|
|
3172
3638
|
}
|
|
3173
3639
|
if (effects.size > 0) {
|
|
3174
3640
|
optionCall('touched', obj, evolution, [prop], Array.from(effects));
|
|
3175
|
-
batch(Array.from(effects));
|
|
3641
|
+
batch(Array.from(effects), { caller: sourceEffect });
|
|
3176
3642
|
}
|
|
3177
3643
|
}
|
|
3178
3644
|
|
|
@@ -3194,9 +3660,10 @@ function addUnreactiveProps(proto, set) {
|
|
|
3194
3660
|
return proto;
|
|
3195
3661
|
}
|
|
3196
3662
|
// Merge sets
|
|
3197
|
-
|
|
3663
|
+
const merged = new Set(existing);
|
|
3664
|
+
proto[unreactiveProperties] = merged;
|
|
3198
3665
|
for (const p of set)
|
|
3199
|
-
|
|
3666
|
+
merged.add(p);
|
|
3200
3667
|
}
|
|
3201
3668
|
// If no set, mark as fully unreactive, otherwise create set
|
|
3202
3669
|
else
|
|
@@ -3218,6 +3685,8 @@ function nonReactive(...obj) {
|
|
|
3218
3685
|
}
|
|
3219
3686
|
return obj[0];
|
|
3220
3687
|
}
|
|
3688
|
+
const markRaw = nonReactive;
|
|
3689
|
+
const markRawProps = addUnreactiveProps;
|
|
3221
3690
|
function nonReactiveClass(...cls) {
|
|
3222
3691
|
for (const c of cls)
|
|
3223
3692
|
if (c)
|
|
@@ -3295,7 +3764,7 @@ function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty)
|
|
|
3295
3764
|
const origin = { obj: unwrappedObj, prop };
|
|
3296
3765
|
// Deep touch: only notify nested property changes with origin filtering
|
|
3297
3766
|
// Don't notify direct property change - the whole point is to avoid parent effects re-running
|
|
3298
|
-
const changes = untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
3767
|
+
const changes = untracked `deepTouch:recursive`(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
3299
3768
|
// When deep touch found no child differences, the object identity still changed.
|
|
3300
3769
|
// Migrate watchers from old → new so the dependency chain is preserved.
|
|
3301
3770
|
if (changes.length === 0) {
|
|
@@ -3435,7 +3904,7 @@ function dispatchNotifications(notifications) {
|
|
|
3435
3904
|
const originWatchers = exports.watchers.get(origin.obj);
|
|
3436
3905
|
if (originWatchers) {
|
|
3437
3906
|
const originEffects = new Map();
|
|
3438
|
-
collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
|
|
3907
|
+
collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, false, [allProps], [origin.prop]);
|
|
3439
3908
|
allowedEffects = new Set(originEffects.keys());
|
|
3440
3909
|
}
|
|
3441
3910
|
// If no allowed effects, skip all notifications (no one should be notified)
|
|
@@ -3454,7 +3923,7 @@ function dispatchNotifications(notifications) {
|
|
|
3454
3923
|
if (objectWatchers) {
|
|
3455
3924
|
currentEffects = new Map();
|
|
3456
3925
|
const broad = evolution.type !== 'set' ? [allProps, keysOf] : [allProps];
|
|
3457
|
-
collectEffects(obj, evolution, currentEffects, objectWatchers, broad, propsArray);
|
|
3926
|
+
collectEffects(obj, evolution, currentEffects, objectWatchers, false, broad, propsArray);
|
|
3458
3927
|
// Filter effects by ancestor chain if origin exists
|
|
3459
3928
|
// Include effects that either directly depend on origin or have an ancestor that does
|
|
3460
3929
|
if (origin && allowedEffects) {
|
|
@@ -3516,11 +3985,94 @@ const metaProtos = new WeakMap();
|
|
|
3516
3985
|
const wrapProtos = new WeakMap();
|
|
3517
3986
|
const arrayLengths = new WeakMap();
|
|
3518
3987
|
const hasReentry = new Set();
|
|
3988
|
+
const accessAnalysisCache = new WeakMap();
|
|
3989
|
+
const readonlyObjectToProxy = new WeakMap();
|
|
3990
|
+
const shallowObjectToProxy = new WeakMap();
|
|
3991
|
+
const readonlyMutators = new Set([
|
|
3992
|
+
'copyWithin',
|
|
3993
|
+
'fill',
|
|
3994
|
+
'pop',
|
|
3995
|
+
'push',
|
|
3996
|
+
'reverse',
|
|
3997
|
+
'shift',
|
|
3998
|
+
'sort',
|
|
3999
|
+
'splice',
|
|
4000
|
+
'unshift',
|
|
4001
|
+
'add',
|
|
4002
|
+
'clear',
|
|
4003
|
+
'delete',
|
|
4004
|
+
'set',
|
|
4005
|
+
]);
|
|
3519
4006
|
// Sub-proxy registration for custom reactive behaviors
|
|
3520
4007
|
const subsRegister = new WeakMap();
|
|
3521
4008
|
// Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
|
|
3522
4009
|
// TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
|
|
3523
4010
|
let internalUntracked = false;
|
|
4011
|
+
function wrapReactiveValue(obj, prop, value) {
|
|
4012
|
+
// Optional fast-path for inert reads - skips reactive wrapping
|
|
4013
|
+
// Disabled by default for safety, can be enabled for performance-critical read-only contexts
|
|
4014
|
+
if (exports.inertDepth > 0)
|
|
4015
|
+
return value;
|
|
4016
|
+
if (!isReactive(value) && typeof value === 'object' && value !== null) {
|
|
4017
|
+
const reactiveValue = reactiveObject(value);
|
|
4018
|
+
// Only create back-references if this object needs them
|
|
4019
|
+
if (needsBackReferences(obj)) {
|
|
4020
|
+
addBackReference(reactiveValue, obj, prop);
|
|
4021
|
+
}
|
|
4022
|
+
return reactiveValue;
|
|
4023
|
+
}
|
|
4024
|
+
return value;
|
|
4025
|
+
}
|
|
4026
|
+
function computeAccessAnalysis(obj, prop, receiver) {
|
|
4027
|
+
const proto = Object.getPrototypeOf(obj);
|
|
4028
|
+
const isOwnProp = Object.hasOwn(obj, prop);
|
|
4029
|
+
const shouldIgnoreAccessor = options.ignoreAccessors &&
|
|
4030
|
+
isOwnProp &&
|
|
4031
|
+
proto !== null &&
|
|
4032
|
+
(isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
|
|
4033
|
+
let hasProp = isOwnProp;
|
|
4034
|
+
let owner = isOwnProp ? obj : undefined;
|
|
4035
|
+
if (!isOwnProp) {
|
|
4036
|
+
let raw = proto;
|
|
4037
|
+
while (raw && raw !== Object.prototype) {
|
|
4038
|
+
if (Object.hasOwn(raw, prop)) {
|
|
4039
|
+
hasProp = true;
|
|
4040
|
+
owner = raw;
|
|
4041
|
+
break;
|
|
4042
|
+
}
|
|
4043
|
+
raw = Object.getPrototypeOf(raw);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
return {
|
|
4047
|
+
hasProp,
|
|
4048
|
+
owner,
|
|
4049
|
+
isInheritedAccess: hasProp && !isOwnProp,
|
|
4050
|
+
shouldIgnoreAccessor,
|
|
4051
|
+
ignoreAccessors: options.ignoreAccessors,
|
|
4052
|
+
instanceMembers: options.instanceMembers,
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
function analyzeAccess(obj, prop, receiver) {
|
|
4056
|
+
const proto = Object.getPrototypeOf(obj);
|
|
4057
|
+
if (Object.hasOwn(obj, prop))
|
|
4058
|
+
return computeAccessAnalysis(obj, prop, receiver);
|
|
4059
|
+
if (proto === null || Array.isArray(obj))
|
|
4060
|
+
return computeAccessAnalysis(obj, prop, receiver);
|
|
4061
|
+
let propCache = accessAnalysisCache.get(proto);
|
|
4062
|
+
if (!propCache) {
|
|
4063
|
+
propCache = new Map();
|
|
4064
|
+
accessAnalysisCache.set(proto, propCache);
|
|
4065
|
+
}
|
|
4066
|
+
const cached = propCache.get(prop);
|
|
4067
|
+
if (cached &&
|
|
4068
|
+
cached.ignoreAccessors === options.ignoreAccessors &&
|
|
4069
|
+
cached.instanceMembers === options.instanceMembers)
|
|
4070
|
+
return cached;
|
|
4071
|
+
const analysis = computeAccessAnalysis(obj, prop, receiver);
|
|
4072
|
+
if (analysis.hasProp)
|
|
4073
|
+
propCache.set(prop, analysis);
|
|
4074
|
+
return analysis;
|
|
4075
|
+
}
|
|
3524
4076
|
const reactiveHandlers = {
|
|
3525
4077
|
[Symbol.toStringTag]: 'MutTs Reactive',
|
|
3526
4078
|
get(obj, prop, receiver) {
|
|
@@ -3548,32 +4100,29 @@ const reactiveHandlers = {
|
|
|
3548
4100
|
// Symbols: fast-path — no reactivity tracking
|
|
3549
4101
|
if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
|
|
3550
4102
|
return FoolProof.get(obj, prop, receiver);
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
if (Object.hasOwn(raw, prop)) {
|
|
3569
|
-
hasProp = true;
|
|
3570
|
-
owner = raw;
|
|
3571
|
-
break;
|
|
4103
|
+
const subProxy = subsRegister.get(obj);
|
|
4104
|
+
if (exports.inertDepth > 0) {
|
|
4105
|
+
const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
|
|
4106
|
+
return wrapReactiveValue(obj, prop, value);
|
|
4107
|
+
}
|
|
4108
|
+
const activeEffect = getActiveEffect();
|
|
4109
|
+
if (!activeEffect) {
|
|
4110
|
+
const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
|
|
4111
|
+
return wrapReactiveValue(obj, prop, value);
|
|
4112
|
+
}
|
|
4113
|
+
if (!subProxy && !Array.isArray(obj)) {
|
|
4114
|
+
const proto = Object.getPrototypeOf(obj);
|
|
4115
|
+
if (proto === Object.prototype || proto === null) {
|
|
4116
|
+
const ownDesc = Object.getOwnPropertyDescriptor(obj, prop);
|
|
4117
|
+
if (ownDesc && 'value' in ownDesc) {
|
|
4118
|
+
dependant(obj, prop);
|
|
4119
|
+
return wrapReactiveValue(obj, prop, ownDesc.value);
|
|
3572
4120
|
}
|
|
3573
|
-
raw = Object.getPrototypeOf(raw);
|
|
3574
4121
|
}
|
|
3575
4122
|
}
|
|
3576
|
-
|
|
4123
|
+
// Check if property exists using a trap-free walk to avoid triggering
|
|
4124
|
+
// the has-trap cascade on prototype chains of reactive proxies.
|
|
4125
|
+
const { hasProp, owner, isInheritedAccess, shouldIgnoreAccessor } = analyzeAccess(obj, prop, receiver);
|
|
3577
4126
|
// Depend if...
|
|
3578
4127
|
if (!hasProp ||
|
|
3579
4128
|
(!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
|
|
@@ -3586,16 +4135,8 @@ const reactiveHandlers = {
|
|
|
3586
4135
|
}
|
|
3587
4136
|
// For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
|
|
3588
4137
|
// For all other objects, inline Reflect.get directly (skips 3 function calls).
|
|
3589
|
-
const value = (
|
|
3590
|
-
|
|
3591
|
-
const reactiveValue = reactiveObject(value);
|
|
3592
|
-
// Only create back-references if this object needs them
|
|
3593
|
-
if (needsBackReferences(obj)) {
|
|
3594
|
-
addBackReference(reactiveValue, obj, prop);
|
|
3595
|
-
}
|
|
3596
|
-
return reactiveValue;
|
|
3597
|
-
}
|
|
3598
|
-
return value;
|
|
4138
|
+
const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
|
|
4139
|
+
return wrapReactiveValue(obj, prop, value);
|
|
3599
4140
|
},
|
|
3600
4141
|
set(obj, prop, value, receiver) {
|
|
3601
4142
|
const unwrapped = unwrap(receiver);
|
|
@@ -3668,6 +4209,11 @@ const reactiveHandlers = {
|
|
|
3668
4209
|
cycle: [], // We don't have the full cycle here, but we know it involves obj
|
|
3669
4210
|
});
|
|
3670
4211
|
hasReentry.add(obj);
|
|
4212
|
+
if (exports.inertDepth > 0) {
|
|
4213
|
+
const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
|
|
4214
|
+
hasReentry.delete(obj);
|
|
4215
|
+
return rv;
|
|
4216
|
+
}
|
|
3671
4217
|
if (!internalUntracked && !isUnreactiveProp(obj, prop))
|
|
3672
4218
|
dependant(obj, prop);
|
|
3673
4219
|
const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
|
|
@@ -3676,7 +4222,7 @@ const reactiveHandlers = {
|
|
|
3676
4222
|
},
|
|
3677
4223
|
deleteProperty(obj, prop) {
|
|
3678
4224
|
if (!Object.hasOwn(obj, prop))
|
|
3679
|
-
return
|
|
4225
|
+
return true;
|
|
3680
4226
|
const oldVal = obj[prop];
|
|
3681
4227
|
// Remove back-references if this object has deep watchers
|
|
3682
4228
|
if (objectsWithDeepWatchers.has(obj) && typeof oldVal === 'object' && oldVal !== null) {
|
|
@@ -3691,6 +4237,9 @@ const reactiveHandlers = {
|
|
|
3691
4237
|
return true;
|
|
3692
4238
|
},
|
|
3693
4239
|
ownKeys(obj) {
|
|
4240
|
+
if (exports.inertDepth > 0) {
|
|
4241
|
+
return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
|
|
4242
|
+
}
|
|
3694
4243
|
dependant(obj, keysOf);
|
|
3695
4244
|
return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
|
|
3696
4245
|
},
|
|
@@ -3699,6 +4248,98 @@ const reactiveHandlers = {
|
|
|
3699
4248
|
Reflect.getOwnPropertyDescriptor(obj, prop));
|
|
3700
4249
|
},
|
|
3701
4250
|
};
|
|
4251
|
+
function readonlyError(prop) {
|
|
4252
|
+
return new ReactiveError(`[reactive] Cannot mutate readonly reactive property '${String(prop)}'`, {
|
|
4253
|
+
code: exports.ReactiveErrorCode.WriteInComputed,
|
|
4254
|
+
});
|
|
4255
|
+
}
|
|
4256
|
+
function readonlyValue(value) {
|
|
4257
|
+
if (!value || typeof value !== 'object')
|
|
4258
|
+
return value;
|
|
4259
|
+
return readonlyReactive(value);
|
|
4260
|
+
}
|
|
4261
|
+
const shallowReactiveHandlers = {
|
|
4262
|
+
get(obj, prop, receiver) {
|
|
4263
|
+
if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
|
|
4264
|
+
return Reflect.get(obj, prop, receiver);
|
|
4265
|
+
if (getActiveEffect())
|
|
4266
|
+
dependant(obj, prop);
|
|
4267
|
+
return Reflect.get(obj, prop, receiver);
|
|
4268
|
+
},
|
|
4269
|
+
set(obj, prop, value, receiver) {
|
|
4270
|
+
const unwrapped = unwrap(receiver);
|
|
4271
|
+
if (obj !== unwrapped)
|
|
4272
|
+
return Object.defineProperty(unwrapped, prop, {
|
|
4273
|
+
value,
|
|
4274
|
+
configurable: true,
|
|
4275
|
+
writable: true,
|
|
4276
|
+
enumerable: true,
|
|
4277
|
+
});
|
|
4278
|
+
if (isUnreactiveProp(obj, prop))
|
|
4279
|
+
return FoolProof.set(obj, prop, value, receiver);
|
|
4280
|
+
const hadProperty = Reflect.has(obj, prop);
|
|
4281
|
+
const oldVal = hadProperty ? Reflect.get(obj, prop, receiver) : absent;
|
|
4282
|
+
const newValue = unwrap(value);
|
|
4283
|
+
if (oldVal !== newValue && FoolProof.set(obj, prop, newValue, receiver)) {
|
|
4284
|
+
touched1(obj, { type: hadProperty ? 'set' : 'add', prop }, prop);
|
|
4285
|
+
}
|
|
4286
|
+
return true;
|
|
4287
|
+
},
|
|
4288
|
+
has(obj, prop) {
|
|
4289
|
+
return reactiveHandlers.has(obj, prop);
|
|
4290
|
+
},
|
|
4291
|
+
deleteProperty(obj, prop) {
|
|
4292
|
+
if (!Object.hasOwn(obj, prop))
|
|
4293
|
+
return true;
|
|
4294
|
+
delete obj[prop];
|
|
4295
|
+
touched1(obj, { type: 'del', prop }, prop);
|
|
4296
|
+
return true;
|
|
4297
|
+
},
|
|
4298
|
+
ownKeys(obj) {
|
|
4299
|
+
return reactiveHandlers.ownKeys(obj);
|
|
4300
|
+
},
|
|
4301
|
+
getOwnPropertyDescriptor(obj, prop) {
|
|
4302
|
+
return Reflect.getOwnPropertyDescriptor(obj, prop);
|
|
4303
|
+
},
|
|
4304
|
+
};
|
|
4305
|
+
const readonlyReactiveHandlers = {
|
|
4306
|
+
get(obj, prop, receiver) {
|
|
4307
|
+
if (readonlyMutators.has(prop)) {
|
|
4308
|
+
return () => {
|
|
4309
|
+
throw readonlyError(prop);
|
|
4310
|
+
};
|
|
4311
|
+
}
|
|
4312
|
+
const reactiveTarget = reactiveObject(obj);
|
|
4313
|
+
const value = FoolProof.get(reactiveTarget, prop, receiver);
|
|
4314
|
+
if (typeof value === 'function') {
|
|
4315
|
+
return (...args) => readonlyValue(value.apply(reactiveTarget, args));
|
|
4316
|
+
}
|
|
4317
|
+
return readonlyValue(value);
|
|
4318
|
+
},
|
|
4319
|
+
set(_obj, prop) {
|
|
4320
|
+
throw readonlyError(prop);
|
|
4321
|
+
},
|
|
4322
|
+
deleteProperty(_obj, prop) {
|
|
4323
|
+
throw readonlyError(prop);
|
|
4324
|
+
},
|
|
4325
|
+
defineProperty(_obj, prop) {
|
|
4326
|
+
throw readonlyError(prop);
|
|
4327
|
+
},
|
|
4328
|
+
setPrototypeOf() {
|
|
4329
|
+
throw readonlyError('[[Prototype]]');
|
|
4330
|
+
},
|
|
4331
|
+
has(obj, prop) {
|
|
4332
|
+
const reactiveTarget = reactiveObject(obj);
|
|
4333
|
+
return Reflect.has(reactiveTarget, prop);
|
|
4334
|
+
},
|
|
4335
|
+
ownKeys(obj) {
|
|
4336
|
+
const reactiveTarget = reactiveObject(obj);
|
|
4337
|
+
return Reflect.ownKeys(reactiveTarget);
|
|
4338
|
+
},
|
|
4339
|
+
getOwnPropertyDescriptor(obj, prop) {
|
|
4340
|
+
return Reflect.getOwnPropertyDescriptor(obj, prop);
|
|
4341
|
+
},
|
|
4342
|
+
};
|
|
3702
4343
|
const reactiveClasses = new WeakSet();
|
|
3703
4344
|
// Create the ReactiveBase mixin
|
|
3704
4345
|
/**
|
|
@@ -3740,6 +4381,34 @@ function reactiveObject(anyTarget, subProxy) {
|
|
|
3740
4381
|
storeProxyRelationship(target, proxy);
|
|
3741
4382
|
return proxy;
|
|
3742
4383
|
}
|
|
4384
|
+
function shallowReactiveObject(anyTarget) {
|
|
4385
|
+
if (!anyTarget || typeof anyTarget !== 'object')
|
|
4386
|
+
return anyTarget;
|
|
4387
|
+
const target = unwrap(anyTarget);
|
|
4388
|
+
if (isNonReactive(target))
|
|
4389
|
+
return target;
|
|
4390
|
+
const existing = shallowObjectToProxy.get(target);
|
|
4391
|
+
if (existing)
|
|
4392
|
+
return existing;
|
|
4393
|
+
const proxy = new Proxy(target, shallowReactiveHandlers);
|
|
4394
|
+
shallowObjectToProxy.set(target, proxy);
|
|
4395
|
+
proxyToObject.set(proxy, target);
|
|
4396
|
+
return proxy;
|
|
4397
|
+
}
|
|
4398
|
+
function readonlyReactiveObject(anyTarget) {
|
|
4399
|
+
if (!anyTarget || typeof anyTarget !== 'object')
|
|
4400
|
+
return anyTarget;
|
|
4401
|
+
const target = unwrap(anyTarget);
|
|
4402
|
+
if (isNonReactive(target))
|
|
4403
|
+
return target;
|
|
4404
|
+
const existing = readonlyObjectToProxy.get(target);
|
|
4405
|
+
if (existing)
|
|
4406
|
+
return existing;
|
|
4407
|
+
const proxy = new Proxy(target, readonlyReactiveHandlers);
|
|
4408
|
+
readonlyObjectToProxy.set(target, proxy);
|
|
4409
|
+
proxyToObject.set(proxy, target);
|
|
4410
|
+
return proxy;
|
|
4411
|
+
}
|
|
3743
4412
|
/**
|
|
3744
4413
|
* Main decorator for making classes reactive
|
|
3745
4414
|
* Automatically makes class instances reactive when created
|
|
@@ -3770,8 +4439,11 @@ const reactive = decorator({
|
|
|
3770
4439
|
},
|
|
3771
4440
|
default: reactiveObject,
|
|
3772
4441
|
});
|
|
4442
|
+
const shallowReactive = shallowReactiveObject;
|
|
4443
|
+
const readonlyReactive = readonlyReactiveObject;
|
|
3773
4444
|
|
|
3774
4445
|
exports.AZone = AZone;
|
|
4446
|
+
exports.CompareSymbol = CompareSymbol;
|
|
3775
4447
|
exports.DecoratorError = DecoratorError;
|
|
3776
4448
|
exports.FoolProof = FoolProof;
|
|
3777
4449
|
exports.IterableWeakMap = IterableWeakMap;
|
|
@@ -3793,10 +4465,11 @@ exports.asyncHooks = asyncHooks;
|
|
|
3793
4465
|
exports.asyncZone = asyncZone;
|
|
3794
4466
|
exports.atom = atom;
|
|
3795
4467
|
exports.atomic = atomic;
|
|
3796
|
-
exports.batch = batch;
|
|
3797
4468
|
exports.biDi = biDi;
|
|
4469
|
+
exports.captioned = captioned;
|
|
3798
4470
|
exports.captured = captured;
|
|
3799
4471
|
exports.caught = caught;
|
|
4472
|
+
exports.chainExternalReason = chainExternalReason;
|
|
3800
4473
|
exports.contentRef = contentRef;
|
|
3801
4474
|
exports.createFlavor = createFlavor;
|
|
3802
4475
|
exports.debugPreset = debugPreset;
|
|
@@ -3808,6 +4481,7 @@ exports.dependant = dependant;
|
|
|
3808
4481
|
exports.devPreset = devPreset;
|
|
3809
4482
|
exports.effect = effect;
|
|
3810
4483
|
exports.effectAggregator = effectAggregator;
|
|
4484
|
+
exports.effectContext = effectContext;
|
|
3811
4485
|
exports.effectHistory = effectHistory;
|
|
3812
4486
|
exports.effectMarker = effectMarker;
|
|
3813
4487
|
exports.effectToDeepWatchedObjects = effectToDeepWatchedObjects;
|
|
@@ -3820,16 +4494,21 @@ exports.getEffectNode = getEffectNode;
|
|
|
3820
4494
|
exports.getRoot = getRoot;
|
|
3821
4495
|
exports.getState = getState;
|
|
3822
4496
|
exports.hooks = hooks;
|
|
4497
|
+
exports.inert = inert;
|
|
4498
|
+
exports.inheritCaption = inheritCaption;
|
|
3823
4499
|
exports.isConstructor = isConstructor;
|
|
3824
4500
|
exports.isDev = isDev;
|
|
3825
4501
|
exports.isNonReactive = isNonReactive;
|
|
3826
4502
|
exports.isObject = isObject;
|
|
3827
4503
|
exports.isProd = isProd;
|
|
3828
4504
|
exports.isReactive = isReactive;
|
|
4505
|
+
exports.isReactiveBroken = isReactiveBroken;
|
|
3829
4506
|
exports.isTest = isTest;
|
|
3830
4507
|
exports.keysOf = keysOf;
|
|
3831
4508
|
exports.legacyDecorator = legacyDecorator;
|
|
3832
4509
|
exports.link = link;
|
|
4510
|
+
exports.markRaw = markRaw;
|
|
4511
|
+
exports.markRawProps = markRawProps;
|
|
3833
4512
|
exports.markWithRoot = markWithRoot;
|
|
3834
4513
|
exports.metaProtos = metaProtos;
|
|
3835
4514
|
exports.mixin = mixin;
|
|
@@ -3840,23 +4519,30 @@ exports.objectParents = objectParents;
|
|
|
3840
4519
|
exports.objectToProxy = objectToProxy;
|
|
3841
4520
|
exports.objectsWithDeepWatchers = objectsWithDeepWatchers;
|
|
3842
4521
|
exports.onEffectThrow = onEffectThrow;
|
|
4522
|
+
exports.onReactiveBroken = onReactiveBroken;
|
|
4523
|
+
exports.onReactiveReset = onReactiveReset;
|
|
3843
4524
|
exports.optionCall = optionCall;
|
|
3844
4525
|
exports.options = options;
|
|
3845
4526
|
exports.prodPreset = prodPreset;
|
|
3846
4527
|
exports.proxyToObject = proxyToObject;
|
|
3847
4528
|
exports.reactive = reactive;
|
|
4529
|
+
exports.readonlyReactive = readonlyReactive;
|
|
3848
4530
|
exports.registerDeepWatcher = registerDeepWatcher;
|
|
3849
4531
|
exports.reset = reset;
|
|
3850
4532
|
exports.root = root;
|
|
3851
4533
|
exports.rootFunctionSymbol = rootFunctionSymbol;
|
|
3852
4534
|
exports.setDebugHooks = setDebugHooks;
|
|
4535
|
+
exports.shallowReactive = shallowReactive;
|
|
3853
4536
|
exports.tag = tag;
|
|
4537
|
+
exports.toRaw = toRaw;
|
|
3854
4538
|
exports.touched = touched;
|
|
3855
4539
|
exports.touched1 = touched1;
|
|
3856
4540
|
exports.unlink = unlink;
|
|
3857
4541
|
exports.unreactiveProperties = unreactiveProperties;
|
|
3858
4542
|
exports.untracked = untracked;
|
|
3859
4543
|
exports.unwrap = unwrap;
|
|
4544
|
+
exports.withEffectContext = withEffectContext;
|
|
4545
|
+
exports.wrapInert = wrapInert;
|
|
3860
4546
|
exports.wrapProtos = wrapProtos;
|
|
3861
4547
|
exports.zip = zip;
|
|
3862
|
-
//# sourceMappingURL=proxy-
|
|
4548
|
+
//# sourceMappingURL=proxy-HA_QQnd5.cjs.map
|