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.
Files changed (61) hide show
  1. package/BROWSER_ASYNC_POLYFILL.md +79 -0
  2. package/README.md +7 -4
  3. package/dist/browser.cjs +150 -27
  4. package/dist/browser.cjs.map +1 -1
  5. package/dist/browser.d.ts +1440 -2
  6. package/dist/browser.dev.cjs +17 -3
  7. package/dist/browser.dev.cjs.map +1 -1
  8. package/dist/browser.dev.d.ts +2 -2
  9. package/dist/browser.dev.esm.js +2 -2
  10. package/dist/browser.esm.js +137 -28
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{index-yK0HVxHv.cjs → index-BnTNC9eC.cjs} +347 -156
  13. package/dist/chunks/index-BnTNC9eC.cjs.map +1 -0
  14. package/dist/chunks/{index-BUop6B2U.esm.js → index-CAWVZL7P.esm.js} +345 -154
  15. package/dist/chunks/index-CAWVZL7P.esm.js.map +1 -0
  16. package/dist/chunks/node-Df_5r_WA.cjs +187 -0
  17. package/dist/chunks/node-Df_5r_WA.cjs.map +1 -0
  18. package/dist/chunks/node-DuIduHw3.esm.js +185 -0
  19. package/dist/chunks/node-DuIduHw3.esm.js.map +1 -0
  20. package/dist/chunks/{proxy-D2C49sXH.esm.js → proxy-C2lnvvbx.esm.js} +943 -272
  21. package/dist/chunks/proxy-C2lnvvbx.esm.js.map +1 -0
  22. package/dist/chunks/{proxy-BvM4yewA.cjs → proxy-HA_QQnd5.cjs} +959 -273
  23. package/dist/chunks/proxy-HA_QQnd5.cjs.map +1 -0
  24. package/dist/debug.cjs +571 -173
  25. package/dist/debug.cjs.map +1 -1
  26. package/dist/debug.d.ts +96 -80
  27. package/dist/debug.esm.js +567 -173
  28. package/dist/debug.esm.js.map +1 -1
  29. package/dist/devtools/panel.js.map +1 -1
  30. package/dist/mutts.umd.js +4351 -3366
  31. package/dist/mutts.umd.js.map +1 -1
  32. package/dist/mutts.umd.min.js +1 -1
  33. package/dist/mutts.umd.min.js.map +1 -1
  34. package/dist/node.cjs +18 -4
  35. package/dist/node.cjs.map +1 -1
  36. package/dist/node.d.ts +2 -2
  37. package/dist/node.dev.cjs +18 -4
  38. package/dist/node.dev.cjs.map +1 -1
  39. package/dist/node.dev.d.ts +2 -2
  40. package/dist/node.dev.esm.js +3 -3
  41. package/dist/node.esm.js +3 -3
  42. package/dist/{types-Bx2PhORg.d.ts → types.d.ts} +42 -15
  43. package/docs/ai/api-reference.md +105 -13
  44. package/docs/ai/manual.md +77 -29
  45. package/docs/debug-getReason.md +161 -0
  46. package/docs/flavored.md +98 -1
  47. package/docs/reactive/advanced.md +184 -12
  48. package/docs/reactive/attend.md +32 -0
  49. package/docs/reactive/core.md +40 -6
  50. package/docs/reactive/debugging.md +40 -15
  51. package/docs/reactive.md +4 -1
  52. package/package.json +13 -9
  53. package/dist/chunks/index-BUop6B2U.esm.js.map +0 -1
  54. package/dist/chunks/index-yK0HVxHv.cjs.map +0 -1
  55. package/dist/chunks/node-Bo7WU5S2.esm.js +0 -96
  56. package/dist/chunks/node-Bo7WU5S2.esm.js.map +0 -1
  57. package/dist/chunks/node-Dd0esp5F.cjs +0 -98
  58. package/dist/chunks/node-Dd0esp5F.cjs.map +0 -1
  59. package/dist/chunks/proxy-BvM4yewA.cjs.map +0 -1
  60. package/dist/chunks/proxy-D2C49sXH.esm.js.map +0 -1
  61. 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
- * Deeply compares two values.
143
- * For objects, compares prototypes with === and then own properties recursively.
144
- * Uses a cache to handle circular references.
145
- * @param a - First value
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 _mode = (typeof process !== 'undefined' && process.env?.NODE_ENV) ||
275
- (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunks/proxy-BvM4yewA.cjs', document.baseURI).href)) }) !== 'undefined' && undefined?.MODE) ||
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
- fn[rootFunctionSymbol] = getRoot(root);
1124
- return fn;
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(Boolean)));
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
- * How to handle cycles detected in effect batches.
1593
+ * Effect scheduler mode.
1405
1594
  *
1406
- * - `'production'` (Default): High-performance mode. Disables dependency graph maintenance and
1407
- * Topological Sorting in favor of a simple FIFO queue. Use this for trustworthy, acyclic UI code.
1408
- * Cycle detection is heuristic (uses maxEffectChain execution counts).
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
- * - `'development'`: Maintains direct dependency graph for early cycle detection during edge creation.
1411
- * Catches cycles before effects execute via DFS check when adding edges. Throws immediately with
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'`: Full diagnostic mode with transitive closures and topological sorting.
1415
- * Provides detailed cycle path reporting. Highest overhead but most informative for bug hunting.
1602
+ * - `'debug'`: ordered scheduling plus the most detailed graph diagnostics. Highest overhead,
1603
+ * best for investigation.
1416
1604
  *
1417
- * @default 'production'
1605
+ * @default 'ordered'
1418
1606
  */
1419
- cycleHandling: 'development',
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
- cycleHandling: 'production',
1718
+ scheduler: 'raw',
1508
1719
  introspection: null,
1509
1720
  onMemoizationDiscrepancy: undefined,
1510
1721
  };
1511
- /** Development preset (default): introspection on, early cycle detection, warnings */
1722
+ /** Development preset: introspection on, early cycle detection, warnings */
1512
1723
  const devPreset = {
1513
1724
  maxEffectReaction: 'warn',
1514
- cycleHandling: 'development',
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
- cycleHandling: 'debug',
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
- if ('dependencyHook' in node) {
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 gatherReasons = options.introspection?.gatherReasons;
1620
- if (gatherReasons) {
1621
- const lineageConfig = gatherReasons.lineages;
1622
- if (lineageConfig === 'dependency' || lineageConfig === 'both') {
1623
- let objStacks = dependencyStacks.get(obj);
1624
- if (!objStacks) {
1625
- objStacks = new Map();
1626
- dependencyStacks.set(obj, objStacks);
1627
- }
1628
- let propStacks = objStacks.get(prop);
1629
- if (!propStacks) {
1630
- propStacks = new Map();
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.cycleHandling === 'production')
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.cycleHandling === 'production')
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.cycleHandling === 'production')
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 (!consequences)
2006
- return;
2007
- for (const consequenceRoot of consequences) {
2008
- // Only update if it's still in the batch
2009
- if (batch.all.has(consequenceRoot)) {
2010
- const currentDegree = batch.inDegrees.get(consequenceRoot) ?? 0;
2011
- if (currentDegree > 0) {
2012
- batch.inDegrees.set(consequenceRoot, currentDegree - 1);
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 it's the correct way to do (these different behavior in function of dev/production)
2147
- if (options.cycleHandling === 'production') {
2148
- // Production mode: FIFO (delete and re-add to move to end)
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
- // Dev mode: skip if already queued — the existing entry will re-run
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.cycleHandling !== 'production') {
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.cycleHandling === 'production') {
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
- // Track which sub-effects have been executed to prevent infinite loops
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
- // TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
2389
- const caller = getActiveEffect();
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], caller);
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], caller, false);
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, 'immediate');
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, 'immediate');
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, 'immediate');
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 || { type: 'stopped' }));
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.catch((error) => {
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 (Array.isArray(stack) && stack.length > 0) {
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, 'immediate');
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 without tracking dependencies but maintains parent cleanup relationship
2863
- * Effects created inside will still be cleaned up when the parent effect is destroyed
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
- function untracked(fn) {
2867
- return effectHistory.present.root(fn);
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
- return effectHistory.root(fn);
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.named('biDi')(markWithRoot(() => {
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
- deps.delete(sourceEffect);
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 (options.introspection?.gatherReasons) {
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
- set = proto[unreactiveProperties] = new Set(proto[unreactiveProperties]);
3663
+ const merged = new Set(existing);
3664
+ proto[unreactiveProperties] = merged;
3198
3665
  for (const p of set)
3199
- existing.add(p);
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
- // Check if property exists using a trap-free walk to avoid triggering
3552
- // the has-trap cascade on prototype chains of reactive proxies.
3553
- const isOwnProp = Object.hasOwn(obj, prop);
3554
- // For accessor properties, check the unwrapped object to see if it's an accessor
3555
- // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
3556
- // Skip for null-proto objects (pounce scopes) — they never have accessors
3557
- const shouldIgnoreAccessor = options.ignoreAccessors &&
3558
- isOwnProp &&
3559
- Object.getPrototypeOf(obj) !== null &&
3560
- (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
3561
- // Check if property exists using a trap-free walk to avoid triggering
3562
- // the has-trap cascade on prototype chains of reactive proxies.
3563
- let hasProp = isOwnProp;
3564
- let owner = isOwnProp ? obj : undefined;
3565
- if (!isOwnProp) {
3566
- let raw = Object.getPrototypeOf(obj);
3567
- while (raw && raw !== Object.prototype) {
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
- const isInheritedAccess = hasProp && !isOwnProp;
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 = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
3590
- if (!isReactive(value) && typeof value === 'object' && value !== null) {
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 false;
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-BvM4yewA.cjs.map
4548
+ //# sourceMappingURL=proxy-HA_QQnd5.cjs.map