mutts 1.0.12 → 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 (55) hide show
  1. package/README.md +5 -2
  2. package/dist/browser.cjs +7 -3
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +1407 -2
  5. package/dist/browser.dev.cjs +7 -3
  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-yK0HVxHv.cjs → index-CAdnMJev.cjs} +202 -79
  11. package/dist/chunks/index-CAdnMJev.cjs.map +1 -0
  12. package/dist/chunks/{index-BUop6B2U.esm.js → index-XsYTUhHx.esm.js} +200 -77
  13. package/dist/chunks/index-XsYTUhHx.esm.js.map +1 -0
  14. package/dist/chunks/{node-Dd0esp5F.cjs → node-DrrphEPf.cjs} +2 -2
  15. package/dist/chunks/{node-Dd0esp5F.cjs.map → node-DrrphEPf.cjs.map} +1 -1
  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-BvM4yewA.cjs → proxy-DBHj3kGK.cjs} +313 -66
  21. package/dist/chunks/proxy-DBHj3kGK.cjs.map +1 -0
  22. package/dist/debug.cjs +537 -166
  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 +8 -4
  33. package/dist/node.cjs.map +1 -1
  34. package/dist/node.d.ts +2 -2
  35. package/dist/node.dev.cjs +8 -4
  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/index-BUop6B2U.esm.js.map +0 -1
  52. package/dist/chunks/index-yK0HVxHv.cjs.map +0 -1
  53. package/dist/chunks/proxy-BvM4yewA.cjs.map +0 -1
  54. package/dist/chunks/proxy-D2C49sXH.esm.js.map +0 -1
  55. package/dist/index.d.ts +0 -1322
package/dist/mutts.umd.js CHANGED
@@ -357,13 +357,10 @@
357
357
  return !!(opd?.get || opd?.set);
358
358
  }
359
359
  /**
360
- * Deeply compares two values.
361
- * For objects, compares prototypes with === and then own properties recursively.
362
- * Uses a cache to handle circular references.
363
- * @param a - First value
364
- * @param b - Second value
365
- * @param cache - Map for circular reference protection (internal use)
366
- * @returns True if values are deeply equal
360
+ * Symbol used to provide custom comparison logic for an object.
361
+ */
362
+ const CompareSymbol = Symbol.for('mutts.compare');
363
+ /**
367
364
  */
368
365
  function deepCompare(a, b, cache = new Map()) {
369
366
  if (a === b)
@@ -371,6 +368,13 @@
371
368
  if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
372
369
  return a === b;
373
370
  }
371
+ // Custom comparison support
372
+ if (typeof a[CompareSymbol] === 'function') {
373
+ return a[CompareSymbol](b, (x, y) => deepCompare(x, y, cache));
374
+ }
375
+ if (typeof b[CompareSymbol] === 'function') {
376
+ return b[CompareSymbol](a, (x, y) => deepCompare(x, y, cache));
377
+ }
374
378
  // Prototype check
375
379
  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
376
380
  return false;
@@ -489,7 +493,8 @@
489
493
  });
490
494
  return fn;
491
495
  }
492
- const _mode = (typeof process !== 'undefined' && process.env?.NODE_ENV) ||
496
+ const runtimeGlobals$1 = globalThis;
497
+ const _mode = runtimeGlobals$1.process?.env?.NODE_ENV ||
493
498
  (typeof ({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('mutts.umd.js', document.baseURI).href)) }) !== 'undefined' && undefined?.MODE) ||
494
499
  'production';
495
500
  const isDev = _mode === 'development';
@@ -893,46 +898,57 @@
893
898
  var _a$1, _b$1;
894
899
  const events = Symbol('events');
895
900
  const hooks = Symbol('hooks');
901
+ function getEventMap(target) {
902
+ return target[events];
903
+ }
904
+ function getHookSet(target) {
905
+ return target[hooks];
906
+ }
896
907
  const eventBehavior = {
897
908
  on(eventOrEvents, cb) {
909
+ const self = this;
910
+ const eventMap = getEventMap(self);
898
911
  if (typeof eventOrEvents === 'object') {
899
912
  for (const e of Object.keys(eventOrEvents)) {
900
913
  this.on(e, eventOrEvents[e]);
901
914
  }
902
915
  }
903
916
  else if (cb !== undefined) {
904
- const callbacks = this[events].get(eventOrEvents) ?? new Set();
917
+ const callbacks = eventMap.get(eventOrEvents) ?? new Set();
905
918
  if (!callbacks.has(cb))
906
919
  callbacks.add(cb);
907
- this[events].set(eventOrEvents, callbacks);
920
+ eventMap.set(eventOrEvents, callbacks);
908
921
  }
909
922
  return () => this.off(eventOrEvents, cb);
910
923
  },
911
924
  off(eventOrEvents, cb) {
925
+ const self = this;
926
+ const eventMap = getEventMap(self);
912
927
  if (typeof eventOrEvents === 'object') {
913
928
  for (const e of Object.keys(eventOrEvents)) {
914
929
  this.off(e, eventOrEvents[e]);
915
930
  }
916
931
  }
917
932
  else if (cb !== null && cb !== undefined) {
918
- const callbacks = this[events].get(eventOrEvents);
933
+ const callbacks = eventMap.get(eventOrEvents);
919
934
  if (callbacks) {
920
935
  callbacks.delete(cb);
921
936
  if (!callbacks.size)
922
- this[events].delete(eventOrEvents);
937
+ eventMap.delete(eventOrEvents);
923
938
  }
924
939
  }
925
940
  else {
926
941
  // Remove all listeners for this event
927
- this[events].delete(eventOrEvents);
942
+ eventMap.delete(eventOrEvents);
928
943
  }
929
944
  },
930
945
  emit(event, ...args) {
931
- const callbacks = this[events].get(event);
946
+ const self = this;
947
+ const callbacks = getEventMap(self).get(event);
932
948
  if (callbacks)
933
949
  for (const cb of callbacks)
934
950
  cb.apply(this, args);
935
- for (const cb of this[hooks])
951
+ for (const cb of getHookSet(self))
936
952
  cb.call(this, event, ...args);
937
953
  },
938
954
  };
@@ -942,7 +958,7 @@
942
958
  get(target, prop) {
943
959
  if (typeof prop !== 'string')
944
960
  return target[prop];
945
- if (use && !eventful[events].has(prop) && !eventful[hooks].size)
961
+ if (use && !getEventMap(eventful).has(prop) && !getHookSet(eventful).size)
946
962
  return () => { };
947
963
  // Return cached function or create and cache
948
964
  let cached = cache.get(prop);
@@ -1003,6 +1019,93 @@
1003
1019
  * flavoredGreet.loud('World') // "HELLO, WORLD!"
1004
1020
  * ```
1005
1021
  */
1022
+ const captionedOptionsSymbol = Symbol('mutts.captioned.options');
1023
+ function isTemplateStringsArray(value) {
1024
+ return (Array.isArray(value) &&
1025
+ Object.hasOwn(value, 'raw') &&
1026
+ Array.isArray(value.raw));
1027
+ }
1028
+ function renderTemplate(strings, values) {
1029
+ let result = strings[0] ?? '';
1030
+ for (let i = 0; i < values.length; i++)
1031
+ result += String(values[i]) + (strings[i + 1] ?? '');
1032
+ return result;
1033
+ }
1034
+ function renameCallback(caption, callback) {
1035
+ Object.defineProperty(callback, 'name', {
1036
+ value: caption,
1037
+ writable: false,
1038
+ configurable: true,
1039
+ });
1040
+ return callback;
1041
+ }
1042
+ function isAnonymousCallback(callback) {
1043
+ return !callback.name || callback.name === 'anonymous';
1044
+ }
1045
+ /**
1046
+ * Wraps a callback-first function so it also accepts a tagged-template call form.
1047
+ *
1048
+ * The template caption is applied to one callback argument before the base
1049
+ * function runs. By default, `captioned` targets the first argument, but
1050
+ * `callbackIndex` can point to any callback position.
1051
+ *
1052
+ * This is intended for APIs such as `effect`, `lift`, or `watch` where naming
1053
+ * is useful but should remain separate from the flavor system.
1054
+ *
1055
+ * Plain calls still work:
1056
+ * `run(callback)`
1057
+ *
1058
+ * Captioned calls add a runtime name to the first callback:
1059
+ * `` run`task:${id}`(callback) ``
1060
+ *
1061
+ * Anonymous uncaptioned callbacks may trigger a warning depending on
1062
+ * `shouldWarnAnonymous`.
1063
+ */
1064
+ function captioned(fn, options = {}) {
1065
+ const settings = {
1066
+ callbackIndex: options.callbackIndex ?? 0,
1067
+ name: options.name ?? (fn.name || 'callback'),
1068
+ rename: options.rename ?? ((caption, callback) => renameCallback(caption, callback)),
1069
+ // biome-ignore lint/suspicious/noConsole: This is the whole point here
1070
+ warn: options.warn ?? ((message) => console.warn(message)),
1071
+ shouldWarnAnonymous: options.shouldWarnAnonymous,
1072
+ };
1073
+ fn[captionedOptionsSymbol] = settings;
1074
+ return new Proxy(fn, {
1075
+ get(target, prop, receiver) {
1076
+ if (prop === captionedOptionsSymbol)
1077
+ return settings;
1078
+ return Reflect.get(target, prop, receiver);
1079
+ },
1080
+ apply(target, thisArg, args) {
1081
+ if (isTemplateStringsArray(args[0])) {
1082
+ const caption = renderTemplate(args[0], args.slice(1));
1083
+ return function captionedCall(...callArgs) {
1084
+ const callback = callArgs[settings.callbackIndex];
1085
+ if (typeof callback !== 'function')
1086
+ throw new TypeError(`${settings.name} template calls require a callback at argument index ${settings.callbackIndex}`);
1087
+ const nextArgs = [...callArgs];
1088
+ nextArgs[settings.callbackIndex] = settings.rename(caption, callback);
1089
+ return Reflect.apply(target, this, nextArgs);
1090
+ };
1091
+ }
1092
+ const callback = args[settings.callbackIndex];
1093
+ if (typeof callback === 'function' && isAnonymousCallback(callback)) {
1094
+ const shouldWarn = settings.shouldWarnAnonymous?.(callback, args) ?? true;
1095
+ if (shouldWarn)
1096
+ settings.warn(`${settings.name}: anonymous callback detected. Use template syntax for automatic naming:\n` +
1097
+ ` Current: ${settings.name}(() => { ... })\n` +
1098
+ ` Fix: ${settings.name}\`descriptive-name\`(() => { ... })\n` +
1099
+ `The captioned system uses the template literal as the effect name for better debugging.`);
1100
+ }
1101
+ return Reflect.apply(target, thisArg, args);
1102
+ },
1103
+ });
1104
+ }
1105
+ function inheritCaption(source, target) {
1106
+ const settings = source[captionedOptionsSymbol];
1107
+ return settings ? captioned(target, settings) : target;
1108
+ }
1006
1109
  /**
1007
1110
  * Creates a flavored (extensible) version of a function with chainable property modifiers.
1008
1111
  */
@@ -1035,7 +1138,7 @@
1035
1138
  };
1036
1139
  if (name)
1037
1140
  named(name, fct);
1038
- return flavored(fct, fn.flavors || {});
1141
+ return flavored(inheritCaption(fn, fct), fn.flavors || {});
1039
1142
  }
1040
1143
  /**
1041
1144
  * Creates a new flavored function that merges options objects at a specific index.
@@ -1068,7 +1171,7 @@
1068
1171
  // Preserve arity and options track
1069
1172
  Object.defineProperty(fct, 'length', { value: fn.length });
1070
1173
  fct.optionsIndex = targetIndex;
1071
- return flavored(fct, fn.flavors || {});
1174
+ return flavored(inheritCaption(fn, fct), fn.flavors || {});
1072
1175
  }
1073
1176
 
1074
1177
  /**
@@ -2065,6 +2168,7 @@
2065
2168
  * @returns The marked function
2066
2169
  */
2067
2170
  function markWithRoot(fn, root) {
2171
+ const marked = fn;
2068
2172
  // Check for collision
2069
2173
  const existingRef = reverseRoots.get(root);
2070
2174
  const existing = existingRef?.deref();
@@ -2079,8 +2183,8 @@
2079
2183
  // (Last writer wins for the check)
2080
2184
  reverseRoots.set(root, new WeakRef(fn));
2081
2185
  // Store root mapping as symbol property on the function
2082
- fn[rootFunctionSymbol] = getRoot(root);
2083
- return fn;
2186
+ marked[rootFunctionSymbol] = getRoot(root);
2187
+ return marked;
2084
2188
  }
2085
2189
  /**
2086
2190
  * Gets the root function of a function for effect tracking
@@ -2100,11 +2204,30 @@
2100
2204
  const effectHistory = tag('effectHistory', new ZoneHistory());
2101
2205
  tag('effectHistory.present', effectHistory.present);
2102
2206
  asyncZone.add(effectHistory);
2207
+ const externalReason = tag('externalReason', new Zone());
2208
+ asyncZone.add(externalReason);
2103
2209
  /**
2104
2210
  * Aggregator for zones that need to be tracked along effects.
2105
2211
  * ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
2106
2212
  */
2107
2213
  const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
2214
+ effectAggregator.add(externalReason);
2215
+ function chainExternalReason(reason) {
2216
+ const external = externalReason.active;
2217
+ if (!external)
2218
+ return reason;
2219
+ if (!reason)
2220
+ return external;
2221
+ let current = reason;
2222
+ while (current) {
2223
+ if (current.type === 'external' &&
2224
+ external.type === 'external' &&
2225
+ current.detail === external.detail)
2226
+ return reason;
2227
+ current = current.chain;
2228
+ }
2229
+ return { ...reason, chain: chainExternalReason(reason.chain) };
2230
+ }
2108
2231
  function isRunning(effect) {
2109
2232
  const root = getRoot(effect);
2110
2233
  return effectHistory.some((e) => getRoot(e) === root);
@@ -2112,6 +2235,35 @@
2112
2235
  function getActiveEffect() {
2113
2236
  return effectHistory.present.active;
2114
2237
  }
2238
+ /**
2239
+ * Captures the current effect context so that deferred code can later
2240
+ * create child effects parented to this point in the effect tree.
2241
+ *
2242
+ * @returns An opaque token to pass to `withEffectContext()`
2243
+ *
2244
+ * @example
2245
+ * ```ts
2246
+ * const ctx = effectContext() // inside an effect or root()
2247
+ * // later, in a deferred callback:
2248
+ * withEffectContext(ctx, () => {
2249
+ * effect(() => { /* child of the captured context *​/ })
2250
+ * })
2251
+ * ```
2252
+ */
2253
+ function effectContext() {
2254
+ return effectHistory.active;
2255
+ }
2256
+ /**
2257
+ * Runs `fn` within a previously captured effect context.
2258
+ * Any effects created inside `fn` become children of the captured parent.
2259
+ *
2260
+ * @param ctx - The context token from `effectContext()`, or `undefined` for root context
2261
+ * @param fn - The function to execute within the restored context
2262
+ * @returns The return value of `fn`
2263
+ */
2264
+ function withEffectContext(ctx, fn) {
2265
+ return effectHistory.with(ctx, fn);
2266
+ }
2115
2267
  const cleanups = new WeakMap();
2116
2268
  /**
2117
2269
  * Attach cleanup dependencies to an object. When `unlink(obj)` is called,
@@ -2139,7 +2291,7 @@
2139
2291
  function link(obj, ...cleanupFns) {
2140
2292
  const set = cleanups.get(obj);
2141
2293
  if (!set)
2142
- cleanups.set(obj, new Set(cleanupFns.filter(Boolean)));
2294
+ cleanups.set(obj, new Set(cleanupFns.filter((fn) => fn !== undefined)));
2143
2295
  else
2144
2296
  for (const fn of cleanupFns)
2145
2297
  if (fn)
@@ -2205,18 +2357,61 @@
2205
2357
  parts.push(',');
2206
2358
  parts.push(...formatTrigger(reason.triggers[i]));
2207
2359
  }
2360
+ if (reason.chain) {
2361
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
2362
+ }
2363
+ return parts;
2364
+ }
2365
+ case 'stopped': {
2366
+ const parts = [`${indent}stopped`];
2367
+ if (reason.detail)
2368
+ parts.push(`(${reason.detail})`);
2369
+ if (reason.chain) {
2370
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
2371
+ }
2372
+ return parts;
2373
+ }
2374
+ case 'external': {
2375
+ const parts = [`${indent}external:`, reason.detail];
2376
+ if (reason.chain) {
2377
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
2378
+ }
2379
+ return parts;
2380
+ }
2381
+ case 'gc': {
2382
+ const parts = [`${indent}gc`];
2383
+ if (reason.chain) {
2384
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
2385
+ }
2386
+ return parts;
2387
+ }
2388
+ case 'error': {
2389
+ const parts = [`${indent}error:`, reason.error];
2390
+ if (reason.chain) {
2391
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
2392
+ }
2393
+ return parts;
2394
+ }
2395
+ case 'lineage': {
2396
+ const parts = [
2397
+ `${indent}lineage ←\n`,
2398
+ ...formatCleanupReason(reason.parent, depth + 1),
2399
+ ];
2400
+ if (reason.chain) {
2401
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
2402
+ }
2403
+ return parts;
2404
+ }
2405
+ case 'invalidate': {
2406
+ const parts = [
2407
+ `${indent}invalidate ←\n`,
2408
+ ...formatCleanupReason(reason.cause, depth + 1),
2409
+ ];
2410
+ if (reason.chain) {
2411
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
2412
+ }
2208
2413
  return parts;
2209
2414
  }
2210
- case 'stopped':
2211
- return [`${indent}stopped`];
2212
- case 'gc':
2213
- return [`${indent}gc`];
2214
- case 'error':
2215
- return [`${indent}error:`, reason.error];
2216
- case 'lineage':
2217
- return [`${indent}lineage ←\n`, ...formatCleanupReason(reason.parent, depth + 1)];
2218
- case 'invalidate':
2219
- return [`${indent}invalidate ←\n`, ...formatCleanupReason(reason.cause, depth + 1)];
2220
2415
  case 'multiple': {
2221
2416
  const parts = [];
2222
2417
  for (let i = 0; i < reason.reasons.length; i++) {
@@ -2224,6 +2419,9 @@
2224
2419
  parts.push('\n');
2225
2420
  parts.push(...formatCleanupReason(reason.reasons[i], depth));
2226
2421
  }
2422
+ if (reason.chain) {
2423
+ parts.push('\n', ...formatCleanupReason(reason.chain, depth));
2424
+ }
2227
2425
  return parts;
2228
2426
  }
2229
2427
  }
@@ -2424,6 +2622,8 @@
2424
2622
  asyncMode: 'cancel',
2425
2623
  // biome-ignore lint/suspicious/noConsole: This is the whole point here
2426
2624
  warn: (...args) => console.warn(...args),
2625
+ // biome-ignore lint/suspicious/noConsole: This is the whole point here
2626
+ error: (...args) => console.error(...args),
2427
2627
  /**
2428
2628
  * Introspection and debug aids. Set to `null` to disable all debug overhead in production.
2429
2629
  *
@@ -2553,7 +2753,7 @@
2553
2753
  return;
2554
2754
  const node = getEffectNode(currentActiveEffect);
2555
2755
  if ('dependencyHook' in node) {
2556
- node.dependencyHook(obj, prop);
2756
+ node.dependencyHook?.(obj, prop);
2557
2757
  }
2558
2758
  let objectWatchers = watchers.get(obj);
2559
2759
  if (!objectWatchers) {
@@ -2619,6 +2819,9 @@
2619
2819
  const end = names.slice(-10);
2620
2820
  return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
2621
2821
  }
2822
+ function externalReasonFrom(fn) {
2823
+ return fn.name ? { type: 'external', detail: fn.name } : undefined;
2824
+ }
2622
2825
  // Nested map structure for efficient counting and batch cleanup
2623
2826
  // batchId -> effect root -> obj -> prop -> count
2624
2827
  let activationRegistry;
@@ -3061,6 +3264,14 @@
3061
3264
  // Build reason from pending triggers if not provided
3062
3265
  if (!reason && node.pendingTriggers) {
3063
3266
  reason = { type: 'propChange', triggers: node.pendingTriggers };
3267
+ // Add chain: if this is being triggered from another effect, get its reason
3268
+ if (caller) {
3269
+ const callerNode = getEffectNode(caller);
3270
+ if (callerNode.currentReason) {
3271
+ reason.chain = callerNode.currentReason;
3272
+ }
3273
+ }
3274
+ reason = chainExternalReason(reason);
3064
3275
  }
3065
3276
  node.pendingTriggers = undefined;
3066
3277
  if (reason) {
@@ -3329,7 +3540,7 @@
3329
3540
  }
3330
3541
  // Track which sub-effects have been executed to prevent infinite loops
3331
3542
  // These are all the effects triggered under `activeEffect` and all their sub-effects
3332
- function batch(effect, immediate) {
3543
+ function batch(effect, immediate, caller) {
3333
3544
  if (broken) {
3334
3545
  throw new ReactiveError('[reactive] Reactive system is broken after an unrecoverable error. Call reset() to recover.', { code: exports.ReactiveErrorCode.BrokenEffects });
3335
3546
  }
@@ -3345,11 +3556,12 @@
3345
3556
  optionCall('beginChain', roots);
3346
3557
  }
3347
3558
  // TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
3348
- const caller = getActiveEffect();
3559
+ const activeCaller = getActiveEffect();
3560
+ const callerToUse = caller || activeCaller;
3349
3561
  // Optimization: If nested and NOT immediate, just join the existing batch
3350
3562
  if (!isNewBatch && !immediate) {
3351
3563
  for (let i = 0; i < effect.length; i++) {
3352
- addToBatch(effect[i], caller);
3564
+ addToBatch(effect[i], callerToUse);
3353
3565
  }
3354
3566
  return;
3355
3567
  }
@@ -3388,7 +3600,7 @@
3388
3600
  else {
3389
3601
  // Add initial effects to batch and compute dependencies
3390
3602
  for (let i = 0; i < effect.length; i++) {
3391
- addToBatch(effect[i], caller, false);
3603
+ addToBatch(effect[i], callerToUse, false);
3392
3604
  }
3393
3605
  computeAllInDegrees(currentBatch);
3394
3606
  }
@@ -3442,6 +3654,11 @@
3442
3654
  success = true;
3443
3655
  return firstReturn.value;
3444
3656
  }
3657
+ catch (error) {
3658
+ if (batchStack.length === 1)
3659
+ optionCall('error', '[reactive] Root batch failure before broken state:', error);
3660
+ throw error;
3661
+ }
3445
3662
  finally {
3446
3663
  if (!success && batchStack.length === 1) {
3447
3664
  broken = true;
@@ -3542,7 +3759,7 @@
3542
3759
  * @param options - Options for effect execution
3543
3760
  * @returns A cleanup function to stop the effect
3544
3761
  */
3545
- const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
3762
+ const effect = captioned(named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
3546
3763
  if (effectOptions?.name)
3547
3764
  Object.defineProperty(fn, 'name', { value: effectOptions.name });
3548
3765
  // Use per-effect asyncMode or fall back to global option
@@ -3555,7 +3772,10 @@
3555
3772
  const prevCleanup = node.cleanup;
3556
3773
  node.cleanup = undefined;
3557
3774
  try {
3558
- untracked(() => prevCleanup(node.nextReason || { type: 'stopped' }));
3775
+ untracked `effect:cleanup`(() => prevCleanup(chainExternalReason(node.nextReason || {
3776
+ type: 'stopped',
3777
+ chain: node.currentReason,
3778
+ })));
3559
3779
  }
3560
3780
  catch (error) {
3561
3781
  // If we want to report them, we could use options.warn or similar
@@ -3588,6 +3808,9 @@
3588
3808
  }
3589
3809
  // Set reaction reason for the upcoming run
3590
3810
  access.reaction = node.nextReason || access.reaction;
3811
+ node.currentReason =
3812
+ node.nextReason ||
3813
+ (access.reaction && access.reaction !== true ? access.reaction : undefined);
3591
3814
  node.nextReason = undefined;
3592
3815
  optionCall('enter', getRoot(fn));
3593
3816
  optionCall('effectRun', getRoot(fn), access.reaction);
@@ -3650,7 +3873,8 @@
3650
3873
  // This ensures that when we cancel, the original promise's .catch() handlers are triggered
3651
3874
  // We do this by rejecting the race promise, which makes the original promise chain see the rejection
3652
3875
  // through the zone-wrapped .then()/.catch() handlers
3653
- runningPromise = runningPromise.catch((error) => {
3876
+ runningPromise = runningPromise
3877
+ .catch((error) => {
3654
3878
  // Propagate async errors to the effect's error handler
3655
3879
  // This ensures onEffectThrow handlers are triggered for async errors
3656
3880
  if (error !== cancelError) {
@@ -3658,6 +3882,10 @@
3658
3882
  }
3659
3883
  // If thrower didn't throw (handled), we absorb the error.
3660
3884
  // If thrower threw (unhandled), it propagates as a new unhandled rejection, which is correct.
3885
+ })
3886
+ .finally(() => {
3887
+ // Clear currentReason when async effect completes
3888
+ node.currentReason = undefined;
3661
3889
  });
3662
3890
  }
3663
3891
  else {
@@ -3667,7 +3895,13 @@
3667
3895
  }
3668
3896
  catch (error) {
3669
3897
  // catcher:self`
3670
- errorToThrow = error;
3898
+ errorToThrow = error instanceof Error ? error : new Error(String(error));
3899
+ }
3900
+ finally {
3901
+ // Clear currentReason for synchronous effects
3902
+ if (!runningPromise) {
3903
+ node.currentReason = undefined;
3904
+ }
3671
3905
  }
3672
3906
  // Create cleanup function for next run
3673
3907
  node.cleanup = (reason) => {
@@ -3698,8 +3932,11 @@
3698
3932
  const childReason = reason
3699
3933
  ? reason.type === 'lineage'
3700
3934
  ? reason
3701
- : { type: 'lineage', parent: reason }
3702
- : { type: 'stopped' };
3935
+ : { type: 'lineage', parent: reason, chain: node.currentReason }
3936
+ : (chainExternalReason({ type: 'stopped', chain: node.currentReason }) ?? {
3937
+ type: 'stopped',
3938
+ chain: node.currentReason,
3939
+ });
3703
3940
  for (const childCleanup of children)
3704
3941
  childCleanup(childReason);
3705
3942
  delete node.children;
@@ -3763,7 +4000,7 @@
3763
4000
  runningPromise = null;
3764
4001
  }
3765
4002
  try {
3766
- node.cleanup?.(reason || { type: 'stopped' });
4003
+ node.cleanup?.(chainExternalReason(reason || { type: 'stopped', chain: node.currentReason }));
3767
4004
  }
3768
4005
  catch (error) {
3769
4006
  // Cleanup errors should basically be ignored or at least not stop the world
@@ -3806,30 +4043,35 @@
3806
4043
  named(name) {
3807
4044
  return flavorOptions(this, { name }, { name: 'named' });
3808
4045
  },
3809
- }));
3810
- /**
3811
- * Executes a function without tracking dependencies but maintains parent cleanup relationship
3812
- * Effects created inside will still be cleaned up when the parent effect is destroyed
3813
- * @param fn - The function to execute
3814
- */
3815
- function untracked(fn) {
3816
- return effectHistory.present.root(fn);
3817
- }
4046
+ })), {
4047
+ name: 'effect',
4048
+ warn: (message) => options.warn(`[reactive] ${message}`),
4049
+ shouldWarnAnonymous: (_callback, args) => !(args[1] && typeof args[1] === 'object' && 'name' in args[1]),
4050
+ });
4051
+ const untracked = captioned(function untracked(fn) {
4052
+ const external = externalReasonFrom(fn);
4053
+ return external
4054
+ ? externalReason.with(external, () => effectHistory.present.root(fn))
4055
+ : effectHistory.present.root(fn);
4056
+ });
3818
4057
  /**
3819
4058
  * Executes a function from a virgin/root context - no parent effect, no tracking
3820
4059
  * Creates completely independent effects that won't be cleaned up by any parent
3821
4060
  * @param fn - The function to execute
3822
4061
  */
3823
- function root(fn) {
3824
- return effectHistory.root(fn);
3825
- }
4062
+ const root = captioned(function root(fn) {
4063
+ const external = externalReasonFrom(fn);
4064
+ return external
4065
+ ? externalReason.with(external, () => effectHistory.root(fn))
4066
+ : effectHistory.root(fn);
4067
+ });
3826
4068
  function biDi(received, get, set) {
3827
4069
  if (typeof get !== 'function') {
3828
4070
  set = get.set;
3829
4071
  get = get.get;
3830
4072
  }
3831
4073
  let programmaticallySetValue = Symbol();
3832
- effect.named('biDi')(markWithRoot(() => {
4074
+ effect `biDi`(markWithRoot(() => {
3833
4075
  const newValue = get();
3834
4076
  const pValue = programmaticallySetValue;
3835
4077
  programmaticallySetValue = Symbol();
@@ -3980,7 +4222,8 @@
3980
4222
  const deps = objectWatchers.get(key);
3981
4223
  if (deps) {
3982
4224
  // Make sure `some.prop++` does not keep a dependency to `some.props`
3983
- deps.delete(sourceEffect);
4225
+ if (sourceEffect)
4226
+ deps.delete(sourceEffect);
3984
4227
  for (const effect of deps) {
3985
4228
  const runningChain = isRunning(effect);
3986
4229
  if (runningChain) {
@@ -4025,6 +4268,7 @@
4025
4268
  else
4026
4269
  collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
4027
4270
  const triggers = Array.from(effects.keys());
4271
+ const sourceEffect = getActiveEffect();
4028
4272
  optionCall('touched', obj, evolution, props, triggers);
4029
4273
  // Store pending triggers for CleanupReason before batching
4030
4274
  if (options.introspection?.gatherReasons) {
@@ -4046,7 +4290,7 @@
4046
4290
  });
4047
4291
  }
4048
4292
  }
4049
- batch(triggers);
4293
+ batch(triggers, undefined, sourceEffect);
4050
4294
  }
4051
4295
  // Bubble up changes if this object has deep watchers
4052
4296
  if (objectsWithDeepWatchers.has(obj)) {
@@ -4066,7 +4310,7 @@
4066
4310
  if (!deps)
4067
4311
  return;
4068
4312
  const effects = new Set();
4069
- getActiveEffect();
4313
+ const sourceEffect = getActiveEffect();
4070
4314
  const gather = options.introspection?.gatherReasons;
4071
4315
  if (gather) {
4072
4316
  const lineageConfig = gather.lineages;
@@ -4118,7 +4362,7 @@
4118
4362
  }
4119
4363
  if (effects.size > 0) {
4120
4364
  optionCall('touched', obj, evolution, [prop], Array.from(effects));
4121
- batch(Array.from(effects));
4365
+ batch(Array.from(effects), undefined, sourceEffect);
4122
4366
  }
4123
4367
  }
4124
4368
 
@@ -4140,9 +4384,10 @@
4140
4384
  return proto;
4141
4385
  }
4142
4386
  // Merge sets
4143
- set = proto[unreactiveProperties] = new Set(proto[unreactiveProperties]);
4387
+ const merged = new Set(existing);
4388
+ proto[unreactiveProperties] = merged;
4144
4389
  for (const p of set)
4145
- existing.add(p);
4390
+ merged.add(p);
4146
4391
  }
4147
4392
  // If no set, mark as fully unreactive, otherwise create set
4148
4393
  else
@@ -4241,7 +4486,7 @@
4241
4486
  const origin = { obj: unwrappedObj, prop };
4242
4487
  // Deep touch: only notify nested property changes with origin filtering
4243
4488
  // Don't notify direct property change - the whole point is to avoid parent effects re-running
4244
- const changes = untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
4489
+ const changes = untracked `deepTouch:recursive`(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
4245
4490
  // When deep touch found no child differences, the object identity still changed.
4246
4491
  // Migrate watchers from old → new so the dependency chain is preserved.
4247
4492
  if (changes.length === 0) {
@@ -4467,6 +4712,17 @@
4467
4712
  // Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
4468
4713
  // TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
4469
4714
  let internalUntracked = false;
4715
+ function wrapReactiveValue(obj, prop, value) {
4716
+ if (!isReactive(value) && typeof value === 'object' && value !== null) {
4717
+ const reactiveValue = reactiveObject(value);
4718
+ // Only create back-references if this object needs them
4719
+ if (needsBackReferences(obj)) {
4720
+ addBackReference(reactiveValue, obj, prop);
4721
+ }
4722
+ return reactiveValue;
4723
+ }
4724
+ return value;
4725
+ }
4470
4726
  const reactiveHandlers = {
4471
4727
  [Symbol.toStringTag]: 'MutTs Reactive',
4472
4728
  get(obj, prop, receiver) {
@@ -4494,6 +4750,10 @@
4494
4750
  // Symbols: fast-path — no reactivity tracking
4495
4751
  if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
4496
4752
  return FoolProof.get(obj, prop, receiver);
4753
+ if (!getActiveEffect()) {
4754
+ const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
4755
+ return wrapReactiveValue(obj, prop, value);
4756
+ }
4497
4757
  // Check if property exists using a trap-free walk to avoid triggering
4498
4758
  // the has-trap cascade on prototype chains of reactive proxies.
4499
4759
  const isOwnProp = Object.hasOwn(obj, prop);
@@ -4533,15 +4793,7 @@
4533
4793
  // For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
4534
4794
  // For all other objects, inline Reflect.get directly (skips 3 function calls).
4535
4795
  const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
4536
- if (!isReactive(value) && typeof value === 'object' && value !== null) {
4537
- const reactiveValue = reactiveObject(value);
4538
- // Only create back-references if this object needs them
4539
- if (needsBackReferences(obj)) {
4540
- addBackReference(reactiveValue, obj, prop);
4541
- }
4542
- return reactiveValue;
4543
- }
4544
- return value;
4796
+ return wrapReactiveValue(obj, prop, value);
4545
4797
  },
4546
4798
  set(obj, prop, value, receiver) {
4547
4799
  const unwrapped = unwrap(receiver);
@@ -4717,7 +4969,7 @@
4717
4969
  default: reactiveObject,
4718
4970
  });
4719
4971
 
4720
- function attend(source, callback) {
4972
+ const attend = captioned(function attend(source, callback) {
4721
4973
  const enumerate = typeof source === 'function'
4722
4974
  ? source
4723
4975
  : Array.isArray(source)
@@ -4728,14 +4980,16 @@
4728
4980
  ? () => source.values()
4729
4981
  : () => Object.keys(source);
4730
4982
  const keyEffects = new Map();
4731
- const outer = effect.named('attend')(({ ascend }) => {
4983
+ const callbackLabel = callback.name ? callback.name : '';
4984
+ const outer = effect `attend`(({ ascend }) => {
4732
4985
  const keys = new Set();
4733
4986
  for (const key of enumerate())
4734
4987
  keys.add(key);
4735
4988
  for (const key of keys) {
4736
4989
  if (keyEffects.has(key))
4737
4990
  continue;
4738
- keyEffects.set(key, ascend(() => effect.named(`attend:${key}`)((access) => callback(key, access))));
4991
+ const indexRef = { value: key };
4992
+ keyEffects.set(key, ascend(() => effect `attend${callbackLabel ? `:${callbackLabel}` : ''}:${key}`((access) => callback(indexRef.value, access))));
4739
4993
  }
4740
4994
  for (const key of Array.from(keyEffects.keys())) {
4741
4995
  if (!keys.has(key)) {
@@ -4750,17 +5004,50 @@
4750
5004
  stop(reason);
4751
5005
  keyEffects.clear();
4752
5006
  };
4753
- }
4754
- function lift(cb) {
5007
+ }, {
5008
+ name: 'attend',
5009
+ callbackIndex: 1,
5010
+ warn: (message) => options.warn(`[reactive] ${message}`),
5011
+ });
5012
+ /**
5013
+ * Lifts a callback that returns an object into a reactive object that automatically
5014
+ * synchronizes with the source object returned by the callback.
5015
+ *
5016
+ * The returned reactive object will update whenever the callback's dependencies change,
5017
+ * efficiently syncing only the properties that differ from the previous result using
5018
+ * Object.assign(). Properties that no longer exist in the source are automatically removed.
5019
+ *
5020
+ * @example
5021
+ * ```typescript
5022
+ * const user = reactive({ name: 'John', age: 30 })
5023
+ * const profile = lift(() => ({
5024
+ * displayName: user.name.toUpperCase(),
5025
+ * isAdult: user.age >= 18,
5026
+ * description: `${user.name} is ${user.age} years old`
5027
+ * }))
5028
+ *
5029
+ * console.log(profile.displayName) // JOHN
5030
+ * console.log(profile.isAdult) // true
5031
+ *
5032
+ * user.name = 'Jane'
5033
+ * console.log(profile.displayName) // JANE
5034
+ * console.log(profile.description) // Jane is 30 years old
5035
+ * ```
5036
+ *
5037
+ * @param cb Callback function that returns an object
5038
+ * @returns A reactive object synchronized with the callback's result, with a [cleanup] property to stop tracking
5039
+ */
5040
+ const lift = captioned(function lift(cb) {
4755
5041
  let result;
4756
5042
  let rawResult;
4757
- const liftCleanup = effect.named(`lift:${cb.name}`)(markWithRoot((access) => {
5043
+ const resultName = `lift:${cb.name || 'anonymous'}`;
5044
+ const liftCleanup = effect `lift:${cb.name}`(markWithRoot((access) => {
4758
5045
  const source = cb(access);
4759
5046
  if (!source || typeof source !== 'object')
4760
5047
  throw new Error('lift callback must return an array or object');
4761
5048
  const sourceProto = Object.getPrototypeOf(source);
4762
5049
  if (!result) {
4763
- rawResult = Array.isArray(source) ? [] : Object.create(sourceProto);
5050
+ rawResult = tag(resultName, Array.isArray(source) ? [] : Object.create(sourceProto));
4764
5051
  result = reactive(rawResult);
4765
5052
  }
4766
5053
  if (sourceProto !== Object.getPrototypeOf(result))
@@ -4771,6 +5058,7 @@
4771
5058
  res.splice(indexA, sliceA.length, ...sliceB);
4772
5059
  }
4773
5060
  else {
5061
+ const recordResult = rawResult;
4774
5062
  for (const key of Object.keys(source)) {
4775
5063
  const had = key in rawResult;
4776
5064
  const newDesc = Object.getOwnPropertyDescriptor(source, key);
@@ -4779,7 +5067,7 @@
4779
5067
  const sameAccessor = oldDesc && newDesc.get && oldDesc.get === newDesc.get;
4780
5068
  Object.defineProperty(rawResult, key, newDesc);
4781
5069
  if (!sameAccessor &&
4782
- rawResult[key] !==
5070
+ recordResult[key] !==
4783
5071
  (oldDesc ? (oldDesc.get ? oldDesc.get() : oldDesc.value) : undefined))
4784
5072
  touched1(rawResult, { type: 'set', prop: key }, key);
4785
5073
  }
@@ -4790,13 +5078,16 @@
4790
5078
  }
4791
5079
  for (const key of Object.keys(rawResult))
4792
5080
  if (!(key in source)) {
4793
- delete rawResult[key];
5081
+ delete recordResult[key];
4794
5082
  touched1(rawResult, { type: 'del', prop: key }, key);
4795
5083
  }
4796
5084
  }
4797
5085
  }, cb));
4798
5086
  return link(result, liftCleanup);
4799
- }
5087
+ }, {
5088
+ name: 'lift',
5089
+ warn: (message) => options.warn(`[reactive] ${message}`),
5090
+ });
4800
5091
  /**
4801
5092
  * Reactively maps an array source through `fn`, producing a lazy reactive output array.
4802
5093
  *
@@ -4820,12 +5111,18 @@
4820
5111
  }
4821
5112
  let track;
4822
5113
  const itemEffects = new Map();
4823
- const cache = [];
5114
+ const cache = tag(`morph:${fn.name || 'anonymous'}`, []);
4824
5115
  let input = [];
4825
5116
  function stopItem(key) {
4826
5117
  const entry = itemEffects.get(key);
4827
5118
  if (entry) {
4828
- entry.stop({ type: 'stopped' });
5119
+ const activeEffect = getActiveEffect();
5120
+ let chain;
5121
+ if (activeEffect) {
5122
+ const node = getEffectNode(activeEffect);
5123
+ chain = node.currentReason;
5124
+ }
5125
+ entry.stop(chainExternalReason({ type: 'stopped', chain }));
4829
5126
  itemEffects.delete(key);
4830
5127
  }
4831
5128
  }
@@ -4838,12 +5135,22 @@
4838
5135
  }
4839
5136
  else {
4840
5137
  const indexRef = { value: key };
4841
- const stop = track(() => effect.named(`morph:${fn.name}:${key}`).opaque((access) => {
5138
+ const stop = track(() => effect.opaque `morph:${fn.name}:${key}`((access) => {
4842
5139
  cache[indexRef.value] = fn(input, access);
4843
5140
  return (reason) => {
4844
5141
  delete cache[indexRef.value];
4845
5142
  touched1(cache, { type: 'invalidate', prop: 'morph' }, String(key));
4846
- stop?.({ type: 'invalidate', cause: reason });
5143
+ const activeEffect = getActiveEffect();
5144
+ let chain;
5145
+ if (activeEffect) {
5146
+ const node = getEffectNode(activeEffect);
5147
+ chain = node.currentReason;
5148
+ }
5149
+ stop?.({
5150
+ type: 'invalidate',
5151
+ cause: chainExternalReason(reason ?? { type: 'stopped', chain }),
5152
+ chain: chainExternalReason(chain),
5153
+ });
4847
5154
  };
4848
5155
  }));
4849
5156
  itemEffects.set(key, { stop, index: indexRef });
@@ -4862,7 +5169,7 @@
4862
5169
  return Reflect.has(input, prop);
4863
5170
  },
4864
5171
  });
4865
- const stopMain = effect.named(`morph:${fn.name}`)(({ ascend }) => {
5172
+ const stopMain = effect `morph:${fn.name}`(({ ascend }) => {
4866
5173
  track = ascend;
4867
5174
  const newInput = [...(typeof source === 'function' ? source() : source)];
4868
5175
  const diffs = arrayDiff(input, newInput).toSorted((a, b) => b.indexA - a.indexA);
@@ -4936,12 +5243,18 @@
4936
5243
  }
4937
5244
  let track;
4938
5245
  const itemEffects = new Map();
4939
- const cache = new Map();
5246
+ const cache = tag(`morph:${fn.name || 'anonymous'}`, new Map());
4940
5247
  Object.defineProperty(cache, 'constructor', { value: Object, enumerable: false });
4941
5248
  function stopItem(key) {
4942
5249
  const stop = itemEffects.get(key);
4943
5250
  if (stop) {
4944
- stop({ type: 'stopped' });
5251
+ const activeEffect = getActiveEffect();
5252
+ let chain;
5253
+ if (activeEffect) {
5254
+ const node = getEffectNode(activeEffect);
5255
+ chain = node.currentReason;
5256
+ }
5257
+ stop({ type: 'stopped', chain });
4945
5258
  itemEffects.delete(key);
4946
5259
  }
4947
5260
  }
@@ -4951,12 +5264,25 @@
4951
5264
  cache.set(key, track(() => fn(val, key)));
4952
5265
  }
4953
5266
  else {
4954
- const stop = track(() => effect.named(`morph:${fn.name}:${key}`).opaque((access) => {
4955
- cache.set(key, fn(source.get(key), key, access));
5267
+ const stop = track(() => effect.opaque `morph:${fn.name}:${key}`((access) => {
5268
+ const next = source.get(key);
5269
+ if (next === undefined && !source.has(key))
5270
+ return;
5271
+ cache.set(key, fn(next, key, access));
4956
5272
  return (reason) => {
4957
5273
  cache.delete(key);
4958
5274
  touched1(cache, { type: 'invalidate', prop: 'morph' }, String(key));
4959
- stop?.({ type: 'invalidate', cause: reason });
5275
+ const activeEffect = getActiveEffect();
5276
+ let chain;
5277
+ if (activeEffect) {
5278
+ const node = getEffectNode(activeEffect);
5279
+ chain = node.currentReason;
5280
+ }
5281
+ stop?.({
5282
+ type: 'invalidate',
5283
+ cause: chainExternalReason(reason ?? { type: 'stopped', chain }),
5284
+ chain: chainExternalReason(chain),
5285
+ });
4960
5286
  };
4961
5287
  }));
4962
5288
  itemEffects.set(key, stop);
@@ -5000,7 +5326,7 @@
5000
5326
  },
5001
5327
  });
5002
5328
  let stateSnapshot = getState(source);
5003
- const stopMain = effect.named(`morph:${fn.name}`)(({ ascend }) => {
5329
+ const stopMain = effect `morph:${fn.name}`(({ ascend }) => {
5004
5330
  track = ascend;
5005
5331
  dependant(source, keysOf);
5006
5332
  while ('evolution' in stateSnapshot) {
@@ -5047,7 +5373,13 @@
5047
5373
  function stopItem(key) {
5048
5374
  const stop = itemEffects.get(key);
5049
5375
  if (stop) {
5050
- stop({ type: 'stopped' });
5376
+ const activeEffect = getActiveEffect();
5377
+ let chain;
5378
+ if (activeEffect) {
5379
+ const node = getEffectNode(activeEffect);
5380
+ chain = node.currentReason;
5381
+ }
5382
+ stop({ type: 'stopped', chain });
5051
5383
  itemEffects.delete(key);
5052
5384
  }
5053
5385
  }
@@ -5057,12 +5389,22 @@
5057
5389
  cache[key] = track(() => fn(val, key));
5058
5390
  }
5059
5391
  else {
5060
- const stop = track(() => effect.named(`morph:${fn.name}:${key}`).opaque((access) => {
5392
+ const stop = track(() => effect.opaque `morph:${fn.name}:${key}`((access) => {
5061
5393
  cache[key] = fn(source[key], key, access);
5062
5394
  return (reason) => {
5063
5395
  delete cache[key];
5064
5396
  touched1(cache, { type: 'invalidate', prop: 'morph' }, String(key));
5065
- stop?.({ type: 'invalidate', cause: reason });
5397
+ const activeEffect = getActiveEffect();
5398
+ let chain;
5399
+ if (activeEffect) {
5400
+ const node = getEffectNode(activeEffect);
5401
+ chain = node.currentReason;
5402
+ }
5403
+ stop?.({
5404
+ type: 'invalidate',
5405
+ cause: chainExternalReason(reason ?? { type: 'stopped', chain }),
5406
+ chain: chainExternalReason(chain),
5407
+ });
5066
5408
  };
5067
5409
  }));
5068
5410
  itemEffects.set(key, stop);
@@ -5089,7 +5431,7 @@
5089
5431
  },
5090
5432
  });
5091
5433
  let stateSnapshot = getState(source);
5092
- const stopMain = effect.named(`morph:${fn.name}`)(({ ascend }) => {
5434
+ const stopMain = effect `morph:${fn.name}`(({ ascend }) => {
5093
5435
  track = ascend;
5094
5436
  // Track only structural changes on source
5095
5437
  dependant(source, keysOf);
@@ -5130,7 +5472,7 @@
5130
5472
  * // Changing users[0].name only recomputes names[0]
5131
5473
  * ```
5132
5474
  */
5133
- const morph = flavored(function morph(source, fn, options) {
5475
+ const morph = captioned(flavored(function morph(source, fn, options) {
5134
5476
  if (Array.isArray(source) || typeof source === 'function')
5135
5477
  return morphArray(source, fn, options);
5136
5478
  if (source instanceof Map)
@@ -5140,6 +5482,10 @@
5140
5482
  get pure() {
5141
5483
  return (source, fn, _opt) => this(source, fn, { pure: true });
5142
5484
  },
5485
+ }), {
5486
+ name: 'morph',
5487
+ callbackIndex: 1,
5488
+ warn: (message) => options.warn(`[reactive] ${message}`),
5143
5489
  });
5144
5490
 
5145
5491
  /**
@@ -5165,7 +5511,7 @@
5165
5511
  const wrappedCallback = markWithRoot((() => callback(target)), callback);
5166
5512
  registerDeepWatcher();
5167
5513
  // Use the existing effect system to register dependencies
5168
- return effect.named('deepWatch')(() => {
5514
+ return effect `deepWatch`(() => {
5169
5515
  // Mark the target object as having deep watchers
5170
5516
  objectsWithDeepWatchers.add(target);
5171
5517
  // Track which objects this effect is watching for cleanup
@@ -5229,7 +5575,7 @@
5229
5575
  traverseAndTrack(target);
5230
5576
  // Only call the callback if immediate is true or if it's not the first run
5231
5577
  if (immediate) {
5232
- untracked(() => callback(target));
5578
+ untracked `deepWatch:callback`(() => callback(target));
5233
5579
  }
5234
5580
  immediate = true;
5235
5581
  // Return a cleanup function that properly removes deep watcher tracking
@@ -5296,7 +5642,7 @@
5296
5642
  const wasVerification = options.isVerificationRun;
5297
5643
  options.isVerificationRun = true;
5298
5644
  try {
5299
- const fresh = untracked(() => fn.apply(this, args));
5645
+ const fresh = untracked `memoize:verify-calculation`(() => fn.apply(this, args));
5300
5646
  if (!deepCompare(node.result, fresh)) {
5301
5647
  optionCall('onMemoizationDiscrepancy', node.result, fresh, fn, args, 'calculation');
5302
5648
  }
@@ -5309,7 +5655,7 @@
5309
5655
  }
5310
5656
  // Create memoize internal effect to track dependencies and invalidate cache
5311
5657
  // Use untracked to prevent the effect creation from being affected by parent effects
5312
- node.cleanup = root(() => effect.named('memoize')(() => {
5658
+ node.cleanup = root `memoize:root`(() => effect `memoize`(() => {
5313
5659
  // Execute the function and track its dependencies
5314
5660
  // The function execution will automatically track dependencies on reactive objects
5315
5661
  node.result = fn.apply(this, args);
@@ -5320,7 +5666,17 @@
5320
5666
  // Lazy memoization: stop the effect so it doesn't re-run immediately.
5321
5667
  // It will be re-created on next access.
5322
5668
  if (node.cleanup) {
5323
- node.cleanup({ type: 'invalidate', cause: reason });
5669
+ const activeEffect = getActiveEffect();
5670
+ let chain;
5671
+ if (activeEffect) {
5672
+ const effectNode = getEffectNode(activeEffect);
5673
+ chain = effectNode.currentReason;
5674
+ }
5675
+ node.cleanup({
5676
+ type: 'invalidate',
5677
+ cause: chainExternalReason(reason ?? { type: 'stopped', chain }),
5678
+ chain: chainExternalReason(chain),
5679
+ });
5324
5680
  node.cleanup = undefined;
5325
5681
  }
5326
5682
  };
@@ -5329,7 +5685,7 @@
5329
5685
  const wasVerification = options.isVerificationRun;
5330
5686
  options.isVerificationRun = true;
5331
5687
  try {
5332
- const fresh = untracked(() => fn.apply(this, args));
5688
+ const fresh = untracked `memoize:verify-comparison`(() => fn.apply(this, args));
5333
5689
  if (!deepCompare(node.result, fresh)) {
5334
5690
  optionCall('onMemoizationDiscrepancy', node.result, fresh, fn, args, 'comparison');
5335
5691
  }
@@ -5521,12 +5877,12 @@
5521
5877
  function organized(source, apply, baseTarget = {}) {
5522
5878
  const observedSource = reactive(source);
5523
5879
  const target = reactive(baseTarget);
5524
- const stop = attend(() => {
5880
+ const stop = attend `organized:entries`(function enumerateObservedSourceKeys() {
5525
5881
  const keys = [];
5526
5882
  for (const key in observedSource)
5527
5883
  keys.push(key);
5528
5884
  return keys;
5529
- }, (key) => {
5885
+ }, function applyObservedSourceKey(key) {
5530
5886
  const sourceKey = key;
5531
5887
  const accessBase = {
5532
5888
  key: sourceKey,
@@ -5564,7 +5920,7 @@
5564
5920
 
5565
5921
  //#region watch
5566
5922
  const unsetYet = Symbol('unset-yet');
5567
- const watch = flavored(function watch(value, //object | ((dep: DependencyAccess) => object),
5923
+ const watch = captioned(flavored(function watch(value, //object | ((dep: DependencyAccess) => object),
5568
5924
  changed, options = {}) {
5569
5925
  return typeof value === 'function'
5570
5926
  ? watchCallBack(value, changed, options)
@@ -5580,11 +5936,14 @@
5580
5936
  get immediate() {
5581
5937
  return flavorOptions(this, { immediate: true });
5582
5938
  },
5939
+ }), {
5940
+ name: 'watch',
5941
+ warn: (message) => options.warn(`[reactive] ${message}`),
5583
5942
  });
5584
5943
  function watchObject(value, changed, { immediate = false, deep = false } = {}) {
5585
5944
  if (deep)
5586
5945
  return deepWatch(value, changed, { immediate });
5587
- return effect.named('watch:object')(() => {
5946
+ return effect `watch:object`(() => {
5588
5947
  dependant(value);
5589
5948
  if (immediate)
5590
5949
  changed(value);
@@ -5594,16 +5953,16 @@
5594
5953
  function watchCallBack(value, changed, { immediate = false, deep = false } = {}) {
5595
5954
  let oldValue = unsetYet;
5596
5955
  let deepCleanup;
5597
- const cbCleanup = effect.named('watch:callback')(markWithRoot((access) => {
5956
+ const cbCleanup = effect `watch:callback`(markWithRoot((access) => {
5598
5957
  const newValue = value(access);
5599
5958
  if (oldValue !== newValue) {
5600
5959
  const old = oldValue;
5601
5960
  if (old === unsetYet) {
5602
5961
  if (immediate)
5603
- untracked(() => changed(newValue));
5962
+ untracked `watch:changed`(() => changed(newValue));
5604
5963
  }
5605
5964
  else
5606
- untracked(() => changed(newValue, old));
5965
+ untracked `watch:changed`(() => changed(newValue, old));
5607
5966
  }
5608
5967
  oldValue = newValue;
5609
5968
  if (deep) {
@@ -5630,7 +5989,7 @@
5630
5989
  function when(predicate, timeout) {
5631
5990
  return new Promise((resolve, reject) => {
5632
5991
  let timer;
5633
- const stop = effect.named('watch:when')((access) => {
5992
+ const stop = effect `watch:when`((access) => {
5634
5993
  try {
5635
5994
  const value = predicate(access);
5636
5995
  if (value) {
@@ -5737,7 +6096,7 @@
5737
6096
  // Solve race conditions: make sure a new fast request is not overloaded by a slow old one
5738
6097
  let counter = 0;
5739
6098
  return lazyInit(resource, () => {
5740
- link(resource, effect.named('watch:resource')((access) => {
6099
+ link(resource, effect `watch:resource`((access) => {
5741
6100
  // Track reload signal to enable manual reloading
5742
6101
  void reloadSignal.value;
5743
6102
  const id = ++counter;
@@ -5903,6 +6262,7 @@
5903
6262
  return super.findLastIndex((v, i, a) => predicate.call(thisArg, reactive(v), i, a), thisArg);
5904
6263
  }
5905
6264
  flat(depth) {
6265
+ dependant(this, keysOf);
5906
6266
  return reactive(super.flat(depth));
5907
6267
  }
5908
6268
  flatMap(callbackfn, thisArg) {
@@ -6329,59 +6689,62 @@
6329
6689
  function cache(object, propertyKey, value) {
6330
6690
  Object.defineProperty(object, propertyKey, { value });
6331
6691
  }
6692
+ function descriptorBase(descriptor) {
6693
+ return function descriptorDecorator(...properties) {
6694
+ return (Base) => {
6695
+ return class extends Base {
6696
+ constructor(...args) {
6697
+ super(...args);
6698
+ for (const key of properties) {
6699
+ const existing = Object.getOwnPropertyDescriptor(this, key);
6700
+ Object.defineProperty(this, key, Object.assign(existing || {}, descriptor));
6701
+ }
6702
+ }
6703
+ };
6704
+ };
6705
+ };
6706
+ }
6332
6707
  /**
6333
6708
  * Creates a decorator that modifies property descriptors for specified properties
6334
6709
  * @param descriptor - The descriptor properties to apply
6335
6710
  * @returns A class decorator that applies the descriptor to specified properties
6336
6711
  */
6337
- const descriptor = flavored(function descriptor(descriptor) {
6338
- return (...properties) => (Base) => {
6339
- return class extends Base {
6340
- constructor(...args) {
6341
- super(...args);
6342
- for (const key of properties) {
6343
- const existing = Object.getOwnPropertyDescriptor(this, key);
6344
- Object.defineProperty(this, key, Object.assign(existing || {}, descriptor));
6345
- }
6346
- }
6347
- };
6348
- };
6349
- }, {
6712
+ const descriptor = Object.assign(descriptorBase, {
6350
6713
  /**
6351
6714
  * enumerable: true
6352
6715
  */
6353
6716
  get enumerable() {
6354
- return descriptor({ enumerable: true });
6717
+ return descriptorBase({ enumerable: true });
6355
6718
  },
6356
6719
  /**
6357
6720
  * enumerable: false
6358
6721
  */
6359
6722
  get hidden() {
6360
- return descriptor({ enumerable: false });
6723
+ return descriptorBase({ enumerable: false });
6361
6724
  },
6362
6725
  /**
6363
6726
  * configurable: true
6364
6727
  */
6365
6728
  get configurable() {
6366
- return descriptor({ configurable: true });
6729
+ return descriptorBase({ configurable: true });
6367
6730
  },
6368
6731
  /**
6369
6732
  * configurable: false
6370
6733
  */
6371
6734
  get frozen() {
6372
- return descriptor({ configurable: false });
6735
+ return descriptorBase({ configurable: false });
6373
6736
  },
6374
6737
  /**
6375
6738
  * writable: true
6376
6739
  */
6377
6740
  get writable() {
6378
- return descriptor({ writable: true });
6741
+ return descriptorBase({ writable: true });
6379
6742
  },
6380
6743
  /**
6381
6744
  * writable: false
6382
6745
  */
6383
6746
  get readonly() {
6384
- return descriptor({ writable: false });
6747
+ return descriptorBase({ writable: false });
6385
6748
  },
6386
6749
  });
6387
6750
  /**
@@ -6510,24 +6873,25 @@
6510
6873
  });
6511
6874
  }
6512
6875
 
6513
- var version$1 = "1.0.12";
6876
+ var version$1 = "1.0.13";
6514
6877
  var pkg = {
6515
6878
  version: version$1};
6516
6879
 
6517
6880
  const { version } = pkg;
6518
6881
  const GLOBAL_MUTTS_KEY = '__MUTTS_INSTANCE__';
6882
+ const runtimeGlobals = globalThis;
6519
6883
  const globalScope = (typeof globalThis !== 'undefined'
6520
6884
  ? globalThis
6521
- : typeof window !== 'undefined'
6522
- ? window
6523
- : typeof global !== 'undefined'
6524
- ? global
6885
+ : runtimeGlobals.window
6886
+ ? runtimeGlobals.window
6887
+ : runtimeGlobals.global
6888
+ ? runtimeGlobals.global
6525
6889
  : false);
6526
6890
  if (globalScope) {
6527
6891
  let source = 'mutts/index';
6528
6892
  try {
6529
- if (typeof __filename !== 'undefined')
6530
- source = __filename;
6893
+ if (runtimeGlobals.__filename)
6894
+ source = runtimeGlobals.__filename;
6531
6895
  else if (typeof ({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('mutts.umd.js', document.baseURI).href)) }) !== 'undefined' && (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('mutts.umd.js', document.baseURI).href))) {
6532
6896
  source = (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('mutts.umd.js', document.baseURI).href));
6533
6897
  }
@@ -6548,6 +6912,7 @@
6548
6912
 
6549
6913
  exports.AZone = AZone;
6550
6914
  exports.ArrayReadForward = ArrayReadForward;
6915
+ exports.CompareSymbol = CompareSymbol;
6551
6916
  exports.DecoratorError = DecoratorError;
6552
6917
  exports.Destroyable = Destroyable;
6553
6918
  exports.DestructionError = DestructionError;
@@ -6577,6 +6942,7 @@
6577
6942
  exports.cache = cache;
6578
6943
  exports.cached = cached;
6579
6944
  exports.callOnGC = callOnGC;
6945
+ exports.captioned = captioned;
6580
6946
  exports.captured = captured;
6581
6947
  exports.caught = caught;
6582
6948
  exports.chainPromise = chainPromise;
@@ -6593,6 +6959,7 @@
6593
6959
  exports.devPreset = devPreset;
6594
6960
  exports.effect = effect;
6595
6961
  exports.effectAggregator = effectAggregator;
6962
+ exports.effectContext = effectContext;
6596
6963
  exports.flavorOptions = flavorOptions;
6597
6964
  exports.flavored = flavored;
6598
6965
  exports.formatCleanupReason = formatCleanupReason;
@@ -6602,6 +6969,7 @@
6602
6969
  exports.getAt = getAt;
6603
6970
  exports.getState = getState;
6604
6971
  exports.hooks = hooks$1;
6972
+ exports.inheritCaption = inheritCaption;
6605
6973
  exports.isCached = isCached;
6606
6974
  exports.isConstructor = isConstructor;
6607
6975
  exports.isDev = isDev;
@@ -6624,7 +6992,6 @@
6624
6992
  exports.organized = organized;
6625
6993
  exports.prodPreset = prodPreset;
6626
6994
  exports.profileInfo = profileInfo;
6627
- exports.project = morph;
6628
6995
  exports.proxyToObject = proxyToObject;
6629
6996
  exports.reactive = reactive;
6630
6997
  exports.reactiveOptions = options;
@@ -6642,6 +7009,7 @@
6642
7009
  exports.unwrap = unwrap;
6643
7010
  exports.watch = watch;
6644
7011
  exports.when = when;
7012
+ exports.withEffectContext = withEffectContext;
6645
7013
  exports.zip = zip;
6646
7014
 
6647
7015
  }));