mutts 1.0.11 → 1.0.13

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 (59) hide show
  1. package/README.md +5 -2
  2. package/dist/browser.cjs +237 -2596
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +1407 -2
  5. package/dist/browser.dev.cjs +44 -43
  6. package/dist/browser.dev.cjs.map +1 -1
  7. package/dist/browser.dev.d.ts +2 -2
  8. package/dist/browser.dev.esm.js +2 -2
  9. package/dist/browser.esm.js +3 -3
  10. package/dist/chunks/index-CAdnMJev.cjs +2735 -0
  11. package/dist/chunks/index-CAdnMJev.cjs.map +1 -0
  12. package/dist/chunks/{index-Sf74wXTV.esm.js → index-XsYTUhHx.esm.js} +200 -77
  13. package/dist/chunks/index-XsYTUhHx.esm.js.map +1 -0
  14. package/dist/chunks/{async-node-3PrbVAbB.cjs → node-DrrphEPf.cjs} +4 -4
  15. package/dist/chunks/node-DrrphEPf.cjs.map +1 -0
  16. package/dist/chunks/{node-Bo7WU5S2.esm.js → node-NEZvVo4M.esm.js} +2 -2
  17. package/dist/chunks/{node-Bo7WU5S2.esm.js.map → node-NEZvVo4M.esm.js.map} +1 -1
  18. package/dist/chunks/{proxy-D2C49sXH.esm.js → proxy-BtmPFjSr.esm.js} +307 -66
  19. package/dist/chunks/proxy-BtmPFjSr.esm.js.map +1 -0
  20. package/dist/chunks/{proxy-Cc79Lrzj.cjs → proxy-DBHj3kGK.cjs} +341 -70
  21. package/dist/chunks/proxy-DBHj3kGK.cjs.map +1 -0
  22. package/dist/debug.cjs +537 -167
  23. package/dist/debug.cjs.map +1 -1
  24. package/dist/debug.d.ts +96 -80
  25. package/dist/debug.esm.js +533 -166
  26. package/dist/debug.esm.js.map +1 -1
  27. package/dist/devtools/panel.js.map +1 -1
  28. package/dist/mutts.umd.js +508 -140
  29. package/dist/mutts.umd.js.map +1 -1
  30. package/dist/mutts.umd.min.js +1 -1
  31. package/dist/mutts.umd.min.js.map +1 -1
  32. package/dist/node.cjs +44 -42
  33. package/dist/node.cjs.map +1 -1
  34. package/dist/node.d.ts +2 -2
  35. package/dist/node.dev.cjs +44 -42
  36. package/dist/node.dev.cjs.map +1 -1
  37. package/dist/node.dev.d.ts +2 -2
  38. package/dist/node.dev.esm.js +3 -3
  39. package/dist/node.esm.js +3 -3
  40. package/dist/{types-Bx2PhORg.d.ts → types.d.ts} +12 -0
  41. package/docs/ai/api-reference.md +102 -12
  42. package/docs/ai/manual.md +60 -24
  43. package/docs/debug-getReason.md +161 -0
  44. package/docs/flavored.md +98 -1
  45. package/docs/reactive/advanced.md +15 -2
  46. package/docs/reactive/attend.md +32 -0
  47. package/docs/reactive/core.md +40 -6
  48. package/docs/reactive/debugging.md +25 -2
  49. package/docs/reactive.md +2 -0
  50. package/package.json +2 -3
  51. package/dist/chunks/async-browser-Dgr5CreQ.cjs +0 -218
  52. package/dist/chunks/async-browser-Dgr5CreQ.cjs.map +0 -1
  53. package/dist/chunks/async-core-CRLKP3l-.cjs +0 -29
  54. package/dist/chunks/async-core-CRLKP3l-.cjs.map +0 -1
  55. package/dist/chunks/async-node-3PrbVAbB.cjs.map +0 -1
  56. package/dist/chunks/index-Sf74wXTV.esm.js.map +0 -1
  57. package/dist/chunks/proxy-Cc79Lrzj.cjs.map +0 -1
  58. package/dist/chunks/proxy-D2C49sXH.esm.js.map +0 -1
  59. package/dist/index.d.ts +0 -1322
@@ -1,8 +1,29 @@
1
1
  'use strict';
2
2
 
3
- var asyncCore = require('./async-core-CRLKP3l-.cjs');
4
-
5
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
+
6
27
  /**
7
28
  * Yields tuples containing elements from each input array, stopping at the longest array length
8
29
  * @param args - Arrays to zip together
@@ -118,13 +139,10 @@ function isOwnAccessor(obj, prop) {
118
139
  return !!(opd?.get || opd?.set);
119
140
  }
120
141
  /**
121
- * Deeply compares two values.
122
- * For objects, compares prototypes with === and then own properties recursively.
123
- * Uses a cache to handle circular references.
124
- * @param a - First value
125
- * @param b - Second value
126
- * @param cache - Map for circular reference protection (internal use)
127
- * @returns True if values are deeply equal
142
+ * Symbol used to provide custom comparison logic for an object.
143
+ */
144
+ const CompareSymbol = Symbol.for('mutts.compare');
145
+ /**
128
146
  */
129
147
  function deepCompare(a, b, cache = new Map()) {
130
148
  if (a === b)
@@ -132,6 +150,13 @@ function deepCompare(a, b, cache = new Map()) {
132
150
  if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
133
151
  return a === b;
134
152
  }
153
+ // Custom comparison support
154
+ if (typeof a[CompareSymbol] === 'function') {
155
+ return a[CompareSymbol](b, (x, y) => deepCompare(x, y, cache));
156
+ }
157
+ if (typeof b[CompareSymbol] === 'function') {
158
+ return b[CompareSymbol](a, (x, y) => deepCompare(x, y, cache));
159
+ }
135
160
  // Prototype check
136
161
  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
137
162
  return false;
@@ -250,8 +275,9 @@ function named(name, fn) {
250
275
  });
251
276
  return fn;
252
277
  }
253
- const _mode = (typeof process !== 'undefined' && process.env?.NODE_ENV) ||
254
- (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunks/proxy-Cc79Lrzj.cjs', document.baseURI).href)) }) !== 'undefined' && undefined?.MODE) ||
278
+ const runtimeGlobals = globalThis;
279
+ const _mode = runtimeGlobals.process?.env?.NODE_ENV ||
280
+ (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunks/proxy-DBHj3kGK.cjs', document.baseURI).href)) }) !== 'undefined' && undefined?.MODE) ||
255
281
  'production';
256
282
  const isDev = _mode === 'development';
257
283
  const isProd = _mode === 'production';
@@ -426,6 +452,93 @@ const decorator = (description) => {
426
452
  * flavoredGreet.loud('World') // "HELLO, WORLD!"
427
453
  * ```
428
454
  */
455
+ const captionedOptionsSymbol = Symbol('mutts.captioned.options');
456
+ function isTemplateStringsArray(value) {
457
+ return (Array.isArray(value) &&
458
+ Object.hasOwn(value, 'raw') &&
459
+ Array.isArray(value.raw));
460
+ }
461
+ function renderTemplate(strings, values) {
462
+ let result = strings[0] ?? '';
463
+ for (let i = 0; i < values.length; i++)
464
+ result += String(values[i]) + (strings[i + 1] ?? '');
465
+ return result;
466
+ }
467
+ function renameCallback(caption, callback) {
468
+ Object.defineProperty(callback, 'name', {
469
+ value: caption,
470
+ writable: false,
471
+ configurable: true,
472
+ });
473
+ return callback;
474
+ }
475
+ function isAnonymousCallback(callback) {
476
+ return !callback.name || callback.name === 'anonymous';
477
+ }
478
+ /**
479
+ * Wraps a callback-first function so it also accepts a tagged-template call form.
480
+ *
481
+ * The template caption is applied to one callback argument before the base
482
+ * function runs. By default, `captioned` targets the first argument, but
483
+ * `callbackIndex` can point to any callback position.
484
+ *
485
+ * This is intended for APIs such as `effect`, `lift`, or `watch` where naming
486
+ * is useful but should remain separate from the flavor system.
487
+ *
488
+ * Plain calls still work:
489
+ * `run(callback)`
490
+ *
491
+ * Captioned calls add a runtime name to the first callback:
492
+ * `` run`task:${id}`(callback) ``
493
+ *
494
+ * Anonymous uncaptioned callbacks may trigger a warning depending on
495
+ * `shouldWarnAnonymous`.
496
+ */
497
+ function captioned(fn, options = {}) {
498
+ const settings = {
499
+ callbackIndex: options.callbackIndex ?? 0,
500
+ name: options.name ?? (fn.name || 'callback'),
501
+ rename: options.rename ?? ((caption, callback) => renameCallback(caption, callback)),
502
+ // biome-ignore lint/suspicious/noConsole: This is the whole point here
503
+ warn: options.warn ?? ((message) => console.warn(message)),
504
+ shouldWarnAnonymous: options.shouldWarnAnonymous,
505
+ };
506
+ fn[captionedOptionsSymbol] = settings;
507
+ return new Proxy(fn, {
508
+ get(target, prop, receiver) {
509
+ if (prop === captionedOptionsSymbol)
510
+ return settings;
511
+ return Reflect.get(target, prop, receiver);
512
+ },
513
+ apply(target, thisArg, args) {
514
+ if (isTemplateStringsArray(args[0])) {
515
+ const caption = renderTemplate(args[0], args.slice(1));
516
+ return function captionedCall(...callArgs) {
517
+ const callback = callArgs[settings.callbackIndex];
518
+ if (typeof callback !== 'function')
519
+ throw new TypeError(`${settings.name} template calls require a callback at argument index ${settings.callbackIndex}`);
520
+ const nextArgs = [...callArgs];
521
+ nextArgs[settings.callbackIndex] = settings.rename(caption, callback);
522
+ return Reflect.apply(target, this, nextArgs);
523
+ };
524
+ }
525
+ const callback = args[settings.callbackIndex];
526
+ if (typeof callback === 'function' && isAnonymousCallback(callback)) {
527
+ const shouldWarn = settings.shouldWarnAnonymous?.(callback, args) ?? true;
528
+ if (shouldWarn)
529
+ settings.warn(`${settings.name}: anonymous callback detected. Use template syntax for automatic naming:\n` +
530
+ ` Current: ${settings.name}(() => { ... })\n` +
531
+ ` Fix: ${settings.name}\`descriptive-name\`(() => { ... })\n` +
532
+ `The captioned system uses the template literal as the effect name for better debugging.`);
533
+ }
534
+ return Reflect.apply(target, thisArg, args);
535
+ },
536
+ });
537
+ }
538
+ function inheritCaption(source, target) {
539
+ const settings = source[captionedOptionsSymbol];
540
+ return settings ? captioned(target, settings) : target;
541
+ }
429
542
  /**
430
543
  * Creates a flavored (extensible) version of a function with chainable property modifiers.
431
544
  */
@@ -458,7 +571,7 @@ function createFlavor(fn, transform, name) {
458
571
  };
459
572
  if (name)
460
573
  named(name, fct);
461
- return flavored(fct, fn.flavors || {});
574
+ return flavored(inheritCaption(fn, fct), fn.flavors || {});
462
575
  }
463
576
  /**
464
577
  * Creates a new flavored function that merges options objects at a specific index.
@@ -491,7 +604,7 @@ function flavorOptions(fn, defaultOptions, opts = {}) {
491
604
  // Preserve arity and options track
492
605
  Object.defineProperty(fct, 'length', { value: fn.length });
493
606
  fct.optionsIndex = targetIndex;
494
- return flavored(fct, fn.flavors || {});
607
+ return flavored(inheritCaption(fn, fct), fn.flavors || {});
495
608
  }
496
609
 
497
610
  /// <reference lib="esnext.collection" />
@@ -924,7 +1037,7 @@ class AZone {
924
1037
  }
925
1038
  // [HACK]: Sanitization
926
1039
  // See BROWSER_ASYNC_POLYFILL.md
927
- return asyncCore.asyncHooks.sanitizePromise(res);
1040
+ return asyncHooks.sanitizePromise(res);
928
1041
  }
929
1042
  root(fn) {
930
1043
  const prev = this.enter();
@@ -1039,7 +1152,7 @@ _ZoneAggregator_zones = new WeakMap();
1039
1152
  * ```
1040
1153
  */
1041
1154
  const asyncZone = tag('async', new ZoneAggregator());
1042
- asyncCore.asyncHooks.addHook(() => {
1155
+ asyncHooks.addHook(() => {
1043
1156
  // capture state before async boundary
1044
1157
  const zone = asyncZone.active;
1045
1158
  return () => {
@@ -1085,6 +1198,7 @@ function resetRegistry() {
1085
1198
  * @returns The marked function
1086
1199
  */
1087
1200
  function markWithRoot(fn, root) {
1201
+ const marked = fn;
1088
1202
  // Check for collision
1089
1203
  const existingRef = reverseRoots.get(root);
1090
1204
  const existing = existingRef?.deref();
@@ -1099,8 +1213,8 @@ function markWithRoot(fn, root) {
1099
1213
  // (Last writer wins for the check)
1100
1214
  reverseRoots.set(root, new WeakRef(fn));
1101
1215
  // Store root mapping as symbol property on the function
1102
- fn[rootFunctionSymbol] = getRoot(root);
1103
- return fn;
1216
+ marked[rootFunctionSymbol] = getRoot(root);
1217
+ return marked;
1104
1218
  }
1105
1219
  /**
1106
1220
  * Gets the root function of a function for effect tracking
@@ -1120,11 +1234,30 @@ function getRoot(fn) {
1120
1234
  const effectHistory = tag('effectHistory', new ZoneHistory());
1121
1235
  tag('effectHistory.present', effectHistory.present);
1122
1236
  asyncZone.add(effectHistory);
1237
+ const externalReason = tag('externalReason', new Zone());
1238
+ asyncZone.add(externalReason);
1123
1239
  /**
1124
1240
  * Aggregator for zones that need to be tracked along effects.
1125
1241
  * ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
1126
1242
  */
1127
1243
  const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
1244
+ effectAggregator.add(externalReason);
1245
+ function chainExternalReason(reason) {
1246
+ const external = externalReason.active;
1247
+ if (!external)
1248
+ return reason;
1249
+ if (!reason)
1250
+ return external;
1251
+ let current = reason;
1252
+ while (current) {
1253
+ if (current.type === 'external' &&
1254
+ external.type === 'external' &&
1255
+ current.detail === external.detail)
1256
+ return reason;
1257
+ current = current.chain;
1258
+ }
1259
+ return { ...reason, chain: chainExternalReason(reason.chain) };
1260
+ }
1128
1261
  function isRunning(effect) {
1129
1262
  const root = getRoot(effect);
1130
1263
  return effectHistory.some((e) => getRoot(e) === root);
@@ -1132,6 +1265,35 @@ function isRunning(effect) {
1132
1265
  function getActiveEffect() {
1133
1266
  return effectHistory.present.active;
1134
1267
  }
1268
+ /**
1269
+ * Captures the current effect context so that deferred code can later
1270
+ * create child effects parented to this point in the effect tree.
1271
+ *
1272
+ * @returns An opaque token to pass to `withEffectContext()`
1273
+ *
1274
+ * @example
1275
+ * ```ts
1276
+ * const ctx = effectContext() // inside an effect or root()
1277
+ * // later, in a deferred callback:
1278
+ * withEffectContext(ctx, () => {
1279
+ * effect(() => { /* child of the captured context *​/ })
1280
+ * })
1281
+ * ```
1282
+ */
1283
+ function effectContext() {
1284
+ return effectHistory.active;
1285
+ }
1286
+ /**
1287
+ * Runs `fn` within a previously captured effect context.
1288
+ * Any effects created inside `fn` become children of the captured parent.
1289
+ *
1290
+ * @param ctx - The context token from `effectContext()`, or `undefined` for root context
1291
+ * @param fn - The function to execute within the restored context
1292
+ * @returns The return value of `fn`
1293
+ */
1294
+ function withEffectContext(ctx, fn) {
1295
+ return effectHistory.with(ctx, fn);
1296
+ }
1135
1297
  const cleanups = new WeakMap();
1136
1298
  /**
1137
1299
  * Attach cleanup dependencies to an object. When `unlink(obj)` is called,
@@ -1159,7 +1321,7 @@ const cleanups = new WeakMap();
1159
1321
  function link(obj, ...cleanupFns) {
1160
1322
  const set = cleanups.get(obj);
1161
1323
  if (!set)
1162
- cleanups.set(obj, new Set(cleanupFns.filter(Boolean)));
1324
+ cleanups.set(obj, new Set(cleanupFns.filter((fn) => fn !== undefined)));
1163
1325
  else
1164
1326
  for (const fn of cleanupFns)
1165
1327
  if (fn)
@@ -1225,18 +1387,61 @@ function formatCleanupReason(reason, depth = 0) {
1225
1387
  parts.push(',');
1226
1388
  parts.push(...formatTrigger(reason.triggers[i]));
1227
1389
  }
1390
+ if (reason.chain) {
1391
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
1392
+ }
1393
+ return parts;
1394
+ }
1395
+ case 'stopped': {
1396
+ const parts = [`${indent}stopped`];
1397
+ if (reason.detail)
1398
+ parts.push(`(${reason.detail})`);
1399
+ if (reason.chain) {
1400
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
1401
+ }
1402
+ return parts;
1403
+ }
1404
+ case 'external': {
1405
+ const parts = [`${indent}external:`, reason.detail];
1406
+ if (reason.chain) {
1407
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
1408
+ }
1409
+ return parts;
1410
+ }
1411
+ case 'gc': {
1412
+ const parts = [`${indent}gc`];
1413
+ if (reason.chain) {
1414
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
1415
+ }
1416
+ return parts;
1417
+ }
1418
+ case 'error': {
1419
+ const parts = [`${indent}error:`, reason.error];
1420
+ if (reason.chain) {
1421
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
1422
+ }
1423
+ return parts;
1424
+ }
1425
+ case 'lineage': {
1426
+ const parts = [
1427
+ `${indent}lineage ←\n`,
1428
+ ...formatCleanupReason(reason.parent, depth + 1),
1429
+ ];
1430
+ if (reason.chain) {
1431
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
1432
+ }
1433
+ return parts;
1434
+ }
1435
+ case 'invalidate': {
1436
+ const parts = [
1437
+ `${indent}invalidate ←\n`,
1438
+ ...formatCleanupReason(reason.cause, depth + 1),
1439
+ ];
1440
+ if (reason.chain) {
1441
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
1442
+ }
1228
1443
  return parts;
1229
1444
  }
1230
- case 'stopped':
1231
- return [`${indent}stopped`];
1232
- case 'gc':
1233
- return [`${indent}gc`];
1234
- case 'error':
1235
- return [`${indent}error:`, reason.error];
1236
- case 'lineage':
1237
- return [`${indent}lineage ←\n`, ...formatCleanupReason(reason.parent, depth + 1)];
1238
- case 'invalidate':
1239
- return [`${indent}invalidate ←\n`, ...formatCleanupReason(reason.cause, depth + 1)];
1240
1445
  case 'multiple': {
1241
1446
  const parts = [];
1242
1447
  for (let i = 0; i < reason.reasons.length; i++) {
@@ -1244,6 +1449,9 @@ function formatCleanupReason(reason, depth = 0) {
1244
1449
  parts.push('\n');
1245
1450
  parts.push(...formatCleanupReason(reason.reasons[i], depth));
1246
1451
  }
1452
+ if (reason.chain) {
1453
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
1454
+ }
1247
1455
  return parts;
1248
1456
  }
1249
1457
  }
@@ -1444,6 +1652,8 @@ const options = {
1444
1652
  asyncMode: 'cancel',
1445
1653
  // biome-ignore lint/suspicious/noConsole: This is the whole point here
1446
1654
  warn: (...args) => console.warn(...args),
1655
+ // biome-ignore lint/suspicious/noConsole: This is the whole point here
1656
+ error: (...args) => console.error(...args),
1447
1657
  /**
1448
1658
  * Introspection and debug aids. Set to `null` to disable all debug overhead in production.
1449
1659
  *
@@ -1573,7 +1783,7 @@ function dependant(obj, prop = allProps) {
1573
1783
  return;
1574
1784
  const node = getEffectNode(currentActiveEffect);
1575
1785
  if ('dependencyHook' in node) {
1576
- node.dependencyHook(obj, prop);
1786
+ node.dependencyHook?.(obj, prop);
1577
1787
  }
1578
1788
  let objectWatchers = exports.watchers.get(obj);
1579
1789
  if (!objectWatchers) {
@@ -1639,6 +1849,9 @@ function formatRoots(roots, limit = 20) {
1639
1849
  const end = names.slice(-10);
1640
1850
  return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
1641
1851
  }
1852
+ function externalReasonFrom(fn) {
1853
+ return fn.name ? { type: 'external', detail: fn.name } : undefined;
1854
+ }
1642
1855
  // Nested map structure for efficient counting and batch cleanup
1643
1856
  // batchId -> effect root -> obj -> prop -> count
1644
1857
  let activationRegistry;
@@ -2081,6 +2294,14 @@ function addToBatch(effect, caller, immediate, reason) {
2081
2294
  // Build reason from pending triggers if not provided
2082
2295
  if (!reason && node.pendingTriggers) {
2083
2296
  reason = { type: 'propChange', triggers: node.pendingTriggers };
2297
+ // Add chain: if this is being triggered from another effect, get its reason
2298
+ if (caller) {
2299
+ const callerNode = getEffectNode(caller);
2300
+ if (callerNode.currentReason) {
2301
+ reason.chain = callerNode.currentReason;
2302
+ }
2303
+ }
2304
+ reason = chainExternalReason(reason);
2084
2305
  }
2085
2306
  node.pendingTriggers = undefined;
2086
2307
  if (reason) {
@@ -2349,7 +2570,7 @@ function executeNext(effectuatedRoots) {
2349
2570
  }
2350
2571
  // Track which sub-effects have been executed to prevent infinite loops
2351
2572
  // These are all the effects triggered under `activeEffect` and all their sub-effects
2352
- function batch(effect, immediate) {
2573
+ function batch(effect, immediate, caller) {
2353
2574
  if (broken) {
2354
2575
  throw new ReactiveError('[reactive] Reactive system is broken after an unrecoverable error. Call reset() to recover.', { code: exports.ReactiveErrorCode.BrokenEffects });
2355
2576
  }
@@ -2365,11 +2586,12 @@ function batch(effect, immediate) {
2365
2586
  optionCall('beginChain', roots);
2366
2587
  }
2367
2588
  // TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
2368
- const caller = getActiveEffect();
2589
+ const activeCaller = getActiveEffect();
2590
+ const callerToUse = caller || activeCaller;
2369
2591
  // Optimization: If nested and NOT immediate, just join the existing batch
2370
2592
  if (!isNewBatch && !immediate) {
2371
2593
  for (let i = 0; i < effect.length; i++) {
2372
- addToBatch(effect[i], caller);
2594
+ addToBatch(effect[i], callerToUse);
2373
2595
  }
2374
2596
  return;
2375
2597
  }
@@ -2408,7 +2630,7 @@ function batch(effect, immediate) {
2408
2630
  else {
2409
2631
  // Add initial effects to batch and compute dependencies
2410
2632
  for (let i = 0; i < effect.length; i++) {
2411
- addToBatch(effect[i], caller, false);
2633
+ addToBatch(effect[i], callerToUse, false);
2412
2634
  }
2413
2635
  computeAllInDegrees(currentBatch);
2414
2636
  }
@@ -2462,6 +2684,11 @@ function batch(effect, immediate) {
2462
2684
  success = true;
2463
2685
  return firstReturn.value;
2464
2686
  }
2687
+ catch (error) {
2688
+ if (batchStack.length === 1)
2689
+ optionCall('error', '[reactive] Root batch failure before broken state:', error);
2690
+ throw error;
2691
+ }
2465
2692
  finally {
2466
2693
  if (!success && batchStack.length === 1) {
2467
2694
  broken = true;
@@ -2562,7 +2789,7 @@ const fr = new FinalizationRegistry((f) => f());
2562
2789
  * @param options - Options for effect execution
2563
2790
  * @returns A cleanup function to stop the effect
2564
2791
  */
2565
- const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
2792
+ const effect = captioned(named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
2566
2793
  if (effectOptions?.name)
2567
2794
  Object.defineProperty(fn, 'name', { value: effectOptions.name });
2568
2795
  // Use per-effect asyncMode or fall back to global option
@@ -2575,7 +2802,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
2575
2802
  const prevCleanup = node.cleanup;
2576
2803
  node.cleanup = undefined;
2577
2804
  try {
2578
- untracked(() => prevCleanup(node.nextReason || { type: 'stopped' }));
2805
+ untracked `effect:cleanup`(() => prevCleanup(chainExternalReason(node.nextReason || {
2806
+ type: 'stopped',
2807
+ chain: node.currentReason,
2808
+ })));
2579
2809
  }
2580
2810
  catch (error) {
2581
2811
  // If we want to report them, we could use options.warn or similar
@@ -2608,6 +2838,9 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
2608
2838
  }
2609
2839
  // Set reaction reason for the upcoming run
2610
2840
  access.reaction = node.nextReason || access.reaction;
2841
+ node.currentReason =
2842
+ node.nextReason ||
2843
+ (access.reaction && access.reaction !== true ? access.reaction : undefined);
2611
2844
  node.nextReason = undefined;
2612
2845
  optionCall('enter', getRoot(fn));
2613
2846
  optionCall('effectRun', getRoot(fn), access.reaction);
@@ -2670,7 +2903,8 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
2670
2903
  // This ensures that when we cancel, the original promise's .catch() handlers are triggered
2671
2904
  // We do this by rejecting the race promise, which makes the original promise chain see the rejection
2672
2905
  // through the zone-wrapped .then()/.catch() handlers
2673
- runningPromise = runningPromise.catch((error) => {
2906
+ runningPromise = runningPromise
2907
+ .catch((error) => {
2674
2908
  // Propagate async errors to the effect's error handler
2675
2909
  // This ensures onEffectThrow handlers are triggered for async errors
2676
2910
  if (error !== cancelError) {
@@ -2678,6 +2912,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
2678
2912
  }
2679
2913
  // If thrower didn't throw (handled), we absorb the error.
2680
2914
  // If thrower threw (unhandled), it propagates as a new unhandled rejection, which is correct.
2915
+ })
2916
+ .finally(() => {
2917
+ // Clear currentReason when async effect completes
2918
+ node.currentReason = undefined;
2681
2919
  });
2682
2920
  }
2683
2921
  else {
@@ -2688,7 +2926,13 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
2688
2926
  catch (error) {
2689
2927
  debugHooks.decorateError(error, runEffect);
2690
2928
  // catcher:self`
2691
- errorToThrow = error;
2929
+ errorToThrow = error instanceof Error ? error : new Error(String(error));
2930
+ }
2931
+ finally {
2932
+ // Clear currentReason for synchronous effects
2933
+ if (!runningPromise) {
2934
+ node.currentReason = undefined;
2935
+ }
2692
2936
  }
2693
2937
  // Create cleanup function for next run
2694
2938
  node.cleanup = (reason) => {
@@ -2719,8 +2963,11 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
2719
2963
  const childReason = reason
2720
2964
  ? reason.type === 'lineage'
2721
2965
  ? reason
2722
- : { type: 'lineage', parent: reason }
2723
- : { type: 'stopped' };
2966
+ : { type: 'lineage', parent: reason, chain: node.currentReason }
2967
+ : (chainExternalReason({ type: 'stopped', chain: node.currentReason }) ?? {
2968
+ type: 'stopped',
2969
+ chain: node.currentReason,
2970
+ });
2724
2971
  for (const childCleanup of children)
2725
2972
  childCleanup(childReason);
2726
2973
  delete node.children;
@@ -2733,7 +2980,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
2733
2980
  const node = getEffectNode(runEffect);
2734
2981
  if (debugHooks.isDevtoolsEnabled()) {
2735
2982
  const stack = debugHooks.captureStack(); // Robustly skips internal mutts frames
2736
- if (Array.isArray(stack) && stack.length > 0) {
2983
+ if (stack) {
2737
2984
  node.creationStack = stack;
2738
2985
  }
2739
2986
  }
@@ -2793,7 +3040,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
2793
3040
  runningPromise = null;
2794
3041
  }
2795
3042
  try {
2796
- node.cleanup?.(reason || { type: 'stopped' });
3043
+ node.cleanup?.(chainExternalReason(reason || { type: 'stopped', chain: node.currentReason }));
2797
3044
  }
2798
3045
  catch (error) {
2799
3046
  // Cleanup errors should basically be ignored or at least not stop the world
@@ -2836,30 +3083,35 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
2836
3083
  named(name) {
2837
3084
  return flavorOptions(this, { name }, { name: 'named' });
2838
3085
  },
2839
- }));
2840
- /**
2841
- * Executes a function without tracking dependencies but maintains parent cleanup relationship
2842
- * Effects created inside will still be cleaned up when the parent effect is destroyed
2843
- * @param fn - The function to execute
2844
- */
2845
- function untracked(fn) {
2846
- return effectHistory.present.root(fn);
2847
- }
3086
+ })), {
3087
+ name: 'effect',
3088
+ warn: (message) => options.warn(`[reactive] ${message}`),
3089
+ shouldWarnAnonymous: (_callback, args) => !(args[1] && typeof args[1] === 'object' && 'name' in args[1]),
3090
+ });
3091
+ const untracked = captioned(function untracked(fn) {
3092
+ const external = externalReasonFrom(fn);
3093
+ return external
3094
+ ? externalReason.with(external, () => effectHistory.present.root(fn))
3095
+ : effectHistory.present.root(fn);
3096
+ });
2848
3097
  /**
2849
3098
  * Executes a function from a virgin/root context - no parent effect, no tracking
2850
3099
  * Creates completely independent effects that won't be cleaned up by any parent
2851
3100
  * @param fn - The function to execute
2852
3101
  */
2853
- function root(fn) {
2854
- return effectHistory.root(fn);
2855
- }
3102
+ const root = captioned(function root(fn) {
3103
+ const external = externalReasonFrom(fn);
3104
+ return external
3105
+ ? externalReason.with(external, () => effectHistory.root(fn))
3106
+ : effectHistory.root(fn);
3107
+ });
2856
3108
  function biDi(received, get, set) {
2857
3109
  if (typeof get !== 'function') {
2858
3110
  set = get.set;
2859
3111
  get = get.get;
2860
3112
  }
2861
3113
  let programmaticallySetValue = Symbol();
2862
- effect.named('biDi')(markWithRoot(() => {
3114
+ effect `biDi`(markWithRoot(() => {
2863
3115
  const newValue = get();
2864
3116
  const pValue = programmaticallySetValue;
2865
3117
  programmaticallySetValue = Symbol();
@@ -3010,7 +3262,8 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
3010
3262
  const deps = objectWatchers.get(key);
3011
3263
  if (deps) {
3012
3264
  // Make sure `some.prop++` does not keep a dependency to `some.props`
3013
- deps.delete(sourceEffect);
3265
+ if (sourceEffect)
3266
+ deps.delete(sourceEffect);
3014
3267
  for (const effect of deps) {
3015
3268
  const runningChain = isRunning(effect);
3016
3269
  if (runningChain) {
@@ -3056,6 +3309,7 @@ function touched(obj, evolution, props) {
3056
3309
  else
3057
3310
  collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
3058
3311
  const triggers = Array.from(effects.keys());
3312
+ const sourceEffect = getActiveEffect();
3059
3313
  optionCall('touched', obj, evolution, props, triggers);
3060
3314
  // Store pending triggers for CleanupReason before batching
3061
3315
  if (options.introspection?.gatherReasons) {
@@ -3077,7 +3331,7 @@ function touched(obj, evolution, props) {
3077
3331
  });
3078
3332
  }
3079
3333
  }
3080
- batch(triggers);
3334
+ batch(triggers, undefined, sourceEffect);
3081
3335
  }
3082
3336
  // Bubble up changes if this object has deep watchers
3083
3337
  if (objectsWithDeepWatchers.has(obj)) {
@@ -3151,7 +3405,7 @@ function touchedOpaque(obj, evolution, prop) {
3151
3405
  }
3152
3406
  if (effects.size > 0) {
3153
3407
  optionCall('touched', obj, evolution, [prop], Array.from(effects));
3154
- batch(Array.from(effects));
3408
+ batch(Array.from(effects), undefined, sourceEffect);
3155
3409
  }
3156
3410
  }
3157
3411
 
@@ -3173,9 +3427,10 @@ function addUnreactiveProps(proto, set) {
3173
3427
  return proto;
3174
3428
  }
3175
3429
  // Merge sets
3176
- set = proto[unreactiveProperties] = new Set(proto[unreactiveProperties]);
3430
+ const merged = new Set(existing);
3431
+ proto[unreactiveProperties] = merged;
3177
3432
  for (const p of set)
3178
- existing.add(p);
3433
+ merged.add(p);
3179
3434
  }
3180
3435
  // If no set, mark as fully unreactive, otherwise create set
3181
3436
  else
@@ -3274,7 +3529,7 @@ function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty)
3274
3529
  const origin = { obj: unwrappedObj, prop };
3275
3530
  // Deep touch: only notify nested property changes with origin filtering
3276
3531
  // Don't notify direct property change - the whole point is to avoid parent effects re-running
3277
- const changes = untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
3532
+ const changes = untracked `deepTouch:recursive`(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
3278
3533
  // When deep touch found no child differences, the object identity still changed.
3279
3534
  // Migrate watchers from old → new so the dependency chain is preserved.
3280
3535
  if (changes.length === 0) {
@@ -3500,6 +3755,17 @@ const subsRegister = new WeakMap();
3500
3755
  // Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
3501
3756
  // TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
3502
3757
  let internalUntracked = false;
3758
+ function wrapReactiveValue(obj, prop, value) {
3759
+ if (!isReactive(value) && typeof value === 'object' && value !== null) {
3760
+ const reactiveValue = reactiveObject(value);
3761
+ // Only create back-references if this object needs them
3762
+ if (needsBackReferences(obj)) {
3763
+ addBackReference(reactiveValue, obj, prop);
3764
+ }
3765
+ return reactiveValue;
3766
+ }
3767
+ return value;
3768
+ }
3503
3769
  const reactiveHandlers = {
3504
3770
  [Symbol.toStringTag]: 'MutTs Reactive',
3505
3771
  get(obj, prop, receiver) {
@@ -3527,6 +3793,10 @@ const reactiveHandlers = {
3527
3793
  // Symbols: fast-path — no reactivity tracking
3528
3794
  if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
3529
3795
  return FoolProof.get(obj, prop, receiver);
3796
+ if (!getActiveEffect()) {
3797
+ const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
3798
+ return wrapReactiveValue(obj, prop, value);
3799
+ }
3530
3800
  // Check if property exists using a trap-free walk to avoid triggering
3531
3801
  // the has-trap cascade on prototype chains of reactive proxies.
3532
3802
  const isOwnProp = Object.hasOwn(obj, prop);
@@ -3566,15 +3836,7 @@ const reactiveHandlers = {
3566
3836
  // For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
3567
3837
  // For all other objects, inline Reflect.get directly (skips 3 function calls).
3568
3838
  const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
3569
- if (!isReactive(value) && typeof value === 'object' && value !== null) {
3570
- const reactiveValue = reactiveObject(value);
3571
- // Only create back-references if this object needs them
3572
- if (needsBackReferences(obj)) {
3573
- addBackReference(reactiveValue, obj, prop);
3574
- }
3575
- return reactiveValue;
3576
- }
3577
- return value;
3839
+ return wrapReactiveValue(obj, prop, value);
3578
3840
  },
3579
3841
  set(obj, prop, value, receiver) {
3580
3842
  const unwrapped = unwrap(receiver);
@@ -3751,6 +4013,7 @@ const reactive = decorator({
3751
4013
  });
3752
4014
 
3753
4015
  exports.AZone = AZone;
4016
+ exports.CompareSymbol = CompareSymbol;
3754
4017
  exports.DecoratorError = DecoratorError;
3755
4018
  exports.FoolProof = FoolProof;
3756
4019
  exports.IterableWeakMap = IterableWeakMap;
@@ -3767,13 +4030,17 @@ exports.addUnreactiveProps = addUnreactiveProps;
3767
4030
  exports.allProps = allProps;
3768
4031
  exports.arrayEquals = arrayEquals;
3769
4032
  exports.assertUntracked = assertUntracked;
4033
+ exports.asyncHook = asyncHook;
4034
+ exports.asyncHooks = asyncHooks;
3770
4035
  exports.asyncZone = asyncZone;
3771
4036
  exports.atom = atom;
3772
4037
  exports.atomic = atomic;
3773
4038
  exports.batch = batch;
3774
4039
  exports.biDi = biDi;
4040
+ exports.captioned = captioned;
3775
4041
  exports.captured = captured;
3776
4042
  exports.caught = caught;
4043
+ exports.chainExternalReason = chainExternalReason;
3777
4044
  exports.contentRef = contentRef;
3778
4045
  exports.createFlavor = createFlavor;
3779
4046
  exports.debugPreset = debugPreset;
@@ -3785,6 +4052,7 @@ exports.dependant = dependant;
3785
4052
  exports.devPreset = devPreset;
3786
4053
  exports.effect = effect;
3787
4054
  exports.effectAggregator = effectAggregator;
4055
+ exports.effectContext = effectContext;
3788
4056
  exports.effectHistory = effectHistory;
3789
4057
  exports.effectMarker = effectMarker;
3790
4058
  exports.effectToDeepWatchedObjects = effectToDeepWatchedObjects;
@@ -3796,6 +4064,8 @@ exports.getActiveEffect = getActiveEffect;
3796
4064
  exports.getEffectNode = getEffectNode;
3797
4065
  exports.getRoot = getRoot;
3798
4066
  exports.getState = getState;
4067
+ exports.hooks = hooks;
4068
+ exports.inheritCaption = inheritCaption;
3799
4069
  exports.isConstructor = isConstructor;
3800
4070
  exports.isDev = isDev;
3801
4071
  exports.isNonReactive = isNonReactive;
@@ -3833,6 +4103,7 @@ exports.unlink = unlink;
3833
4103
  exports.unreactiveProperties = unreactiveProperties;
3834
4104
  exports.untracked = untracked;
3835
4105
  exports.unwrap = unwrap;
4106
+ exports.withEffectContext = withEffectContext;
3836
4107
  exports.wrapProtos = wrapProtos;
3837
4108
  exports.zip = zip;
3838
- //# sourceMappingURL=proxy-Cc79Lrzj.cjs.map
4109
+ //# sourceMappingURL=proxy-DBHj3kGK.cjs.map