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.
- package/README.md +5 -2
- package/dist/browser.cjs +7 -3
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +1407 -2
- package/dist/browser.dev.cjs +7 -3
- package/dist/browser.dev.cjs.map +1 -1
- package/dist/browser.dev.d.ts +2 -2
- package/dist/browser.dev.esm.js +2 -2
- package/dist/browser.esm.js +3 -3
- package/dist/chunks/{index-yK0HVxHv.cjs → index-CAdnMJev.cjs} +202 -79
- package/dist/chunks/index-CAdnMJev.cjs.map +1 -0
- package/dist/chunks/{index-BUop6B2U.esm.js → index-XsYTUhHx.esm.js} +200 -77
- package/dist/chunks/index-XsYTUhHx.esm.js.map +1 -0
- package/dist/chunks/{node-Dd0esp5F.cjs → node-DrrphEPf.cjs} +2 -2
- package/dist/chunks/{node-Dd0esp5F.cjs.map → node-DrrphEPf.cjs.map} +1 -1
- package/dist/chunks/{node-Bo7WU5S2.esm.js → node-NEZvVo4M.esm.js} +2 -2
- package/dist/chunks/{node-Bo7WU5S2.esm.js.map → node-NEZvVo4M.esm.js.map} +1 -1
- package/dist/chunks/{proxy-D2C49sXH.esm.js → proxy-BtmPFjSr.esm.js} +307 -66
- package/dist/chunks/proxy-BtmPFjSr.esm.js.map +1 -0
- package/dist/chunks/{proxy-BvM4yewA.cjs → proxy-DBHj3kGK.cjs} +313 -66
- package/dist/chunks/proxy-DBHj3kGK.cjs.map +1 -0
- package/dist/debug.cjs +537 -166
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.d.ts +96 -80
- package/dist/debug.esm.js +533 -166
- package/dist/debug.esm.js.map +1 -1
- package/dist/devtools/panel.js.map +1 -1
- package/dist/mutts.umd.js +508 -140
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/node.cjs +8 -4
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.dev.cjs +8 -4
- package/dist/node.dev.cjs.map +1 -1
- package/dist/node.dev.d.ts +2 -2
- package/dist/node.dev.esm.js +3 -3
- package/dist/node.esm.js +3 -3
- package/dist/{types-Bx2PhORg.d.ts → types.d.ts} +12 -0
- package/docs/ai/api-reference.md +102 -12
- package/docs/ai/manual.md +60 -24
- package/docs/debug-getReason.md +161 -0
- package/docs/flavored.md +98 -1
- package/docs/reactive/advanced.md +15 -2
- package/docs/reactive/attend.md +32 -0
- package/docs/reactive/core.md +40 -6
- package/docs/reactive/debugging.md +25 -2
- package/docs/reactive.md +2 -0
- package/package.json +2 -3
- package/dist/chunks/index-BUop6B2U.esm.js.map +0 -1
- package/dist/chunks/index-yK0HVxHv.cjs.map +0 -1
- package/dist/chunks/proxy-BvM4yewA.cjs.map +0 -1
- package/dist/chunks/proxy-D2C49sXH.esm.js.map +0 -1
- package/dist/index.d.ts +0 -1322
|
@@ -139,13 +139,10 @@ function isOwnAccessor(obj, prop) {
|
|
|
139
139
|
return !!(opd?.get || opd?.set);
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* @param b - Second value
|
|
147
|
-
* @param cache - Map for circular reference protection (internal use)
|
|
148
|
-
* @returns True if values are deeply equal
|
|
142
|
+
* Symbol used to provide custom comparison logic for an object.
|
|
143
|
+
*/
|
|
144
|
+
const CompareSymbol = Symbol.for('mutts.compare');
|
|
145
|
+
/**
|
|
149
146
|
*/
|
|
150
147
|
function deepCompare(a, b, cache = new Map()) {
|
|
151
148
|
if (a === b)
|
|
@@ -153,6 +150,13 @@ function deepCompare(a, b, cache = new Map()) {
|
|
|
153
150
|
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
|
|
154
151
|
return a === b;
|
|
155
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
|
+
}
|
|
156
160
|
// Prototype check
|
|
157
161
|
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
|
|
158
162
|
return false;
|
|
@@ -271,8 +275,9 @@ function named(name, fn) {
|
|
|
271
275
|
});
|
|
272
276
|
return fn;
|
|
273
277
|
}
|
|
274
|
-
const
|
|
275
|
-
|
|
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) ||
|
|
276
281
|
'production';
|
|
277
282
|
const isDev = _mode === 'development';
|
|
278
283
|
const isProd = _mode === 'production';
|
|
@@ -447,6 +452,93 @@ const decorator = (description) => {
|
|
|
447
452
|
* flavoredGreet.loud('World') // "HELLO, WORLD!"
|
|
448
453
|
* ```
|
|
449
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
|
+
}
|
|
450
542
|
/**
|
|
451
543
|
* Creates a flavored (extensible) version of a function with chainable property modifiers.
|
|
452
544
|
*/
|
|
@@ -479,7 +571,7 @@ function createFlavor(fn, transform, name) {
|
|
|
479
571
|
};
|
|
480
572
|
if (name)
|
|
481
573
|
named(name, fct);
|
|
482
|
-
return flavored(fct, fn.flavors || {});
|
|
574
|
+
return flavored(inheritCaption(fn, fct), fn.flavors || {});
|
|
483
575
|
}
|
|
484
576
|
/**
|
|
485
577
|
* Creates a new flavored function that merges options objects at a specific index.
|
|
@@ -512,7 +604,7 @@ function flavorOptions(fn, defaultOptions, opts = {}) {
|
|
|
512
604
|
// Preserve arity and options track
|
|
513
605
|
Object.defineProperty(fct, 'length', { value: fn.length });
|
|
514
606
|
fct.optionsIndex = targetIndex;
|
|
515
|
-
return flavored(fct, fn.flavors || {});
|
|
607
|
+
return flavored(inheritCaption(fn, fct), fn.flavors || {});
|
|
516
608
|
}
|
|
517
609
|
|
|
518
610
|
/// <reference lib="esnext.collection" />
|
|
@@ -1106,6 +1198,7 @@ function resetRegistry() {
|
|
|
1106
1198
|
* @returns The marked function
|
|
1107
1199
|
*/
|
|
1108
1200
|
function markWithRoot(fn, root) {
|
|
1201
|
+
const marked = fn;
|
|
1109
1202
|
// Check for collision
|
|
1110
1203
|
const existingRef = reverseRoots.get(root);
|
|
1111
1204
|
const existing = existingRef?.deref();
|
|
@@ -1120,8 +1213,8 @@ function markWithRoot(fn, root) {
|
|
|
1120
1213
|
// (Last writer wins for the check)
|
|
1121
1214
|
reverseRoots.set(root, new WeakRef(fn));
|
|
1122
1215
|
// Store root mapping as symbol property on the function
|
|
1123
|
-
|
|
1124
|
-
return
|
|
1216
|
+
marked[rootFunctionSymbol] = getRoot(root);
|
|
1217
|
+
return marked;
|
|
1125
1218
|
}
|
|
1126
1219
|
/**
|
|
1127
1220
|
* Gets the root function of a function for effect tracking
|
|
@@ -1141,11 +1234,30 @@ function getRoot(fn) {
|
|
|
1141
1234
|
const effectHistory = tag('effectHistory', new ZoneHistory());
|
|
1142
1235
|
tag('effectHistory.present', effectHistory.present);
|
|
1143
1236
|
asyncZone.add(effectHistory);
|
|
1237
|
+
const externalReason = tag('externalReason', new Zone());
|
|
1238
|
+
asyncZone.add(externalReason);
|
|
1144
1239
|
/**
|
|
1145
1240
|
* Aggregator for zones that need to be tracked along effects.
|
|
1146
1241
|
* ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
|
|
1147
1242
|
*/
|
|
1148
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
|
+
}
|
|
1149
1261
|
function isRunning(effect) {
|
|
1150
1262
|
const root = getRoot(effect);
|
|
1151
1263
|
return effectHistory.some((e) => getRoot(e) === root);
|
|
@@ -1153,6 +1265,35 @@ function isRunning(effect) {
|
|
|
1153
1265
|
function getActiveEffect() {
|
|
1154
1266
|
return effectHistory.present.active;
|
|
1155
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
|
+
}
|
|
1156
1297
|
const cleanups = new WeakMap();
|
|
1157
1298
|
/**
|
|
1158
1299
|
* Attach cleanup dependencies to an object. When `unlink(obj)` is called,
|
|
@@ -1180,7 +1321,7 @@ const cleanups = new WeakMap();
|
|
|
1180
1321
|
function link(obj, ...cleanupFns) {
|
|
1181
1322
|
const set = cleanups.get(obj);
|
|
1182
1323
|
if (!set)
|
|
1183
|
-
cleanups.set(obj, new Set(cleanupFns.filter(
|
|
1324
|
+
cleanups.set(obj, new Set(cleanupFns.filter((fn) => fn !== undefined)));
|
|
1184
1325
|
else
|
|
1185
1326
|
for (const fn of cleanupFns)
|
|
1186
1327
|
if (fn)
|
|
@@ -1246,18 +1387,61 @@ function formatCleanupReason(reason, depth = 0) {
|
|
|
1246
1387
|
parts.push(',');
|
|
1247
1388
|
parts.push(...formatTrigger(reason.triggers[i]));
|
|
1248
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
|
+
}
|
|
1249
1443
|
return parts;
|
|
1250
1444
|
}
|
|
1251
|
-
case 'stopped':
|
|
1252
|
-
return [`${indent}stopped`];
|
|
1253
|
-
case 'gc':
|
|
1254
|
-
return [`${indent}gc`];
|
|
1255
|
-
case 'error':
|
|
1256
|
-
return [`${indent}error:`, reason.error];
|
|
1257
|
-
case 'lineage':
|
|
1258
|
-
return [`${indent}lineage ←\n`, ...formatCleanupReason(reason.parent, depth + 1)];
|
|
1259
|
-
case 'invalidate':
|
|
1260
|
-
return [`${indent}invalidate ←\n`, ...formatCleanupReason(reason.cause, depth + 1)];
|
|
1261
1445
|
case 'multiple': {
|
|
1262
1446
|
const parts = [];
|
|
1263
1447
|
for (let i = 0; i < reason.reasons.length; i++) {
|
|
@@ -1265,6 +1449,9 @@ function formatCleanupReason(reason, depth = 0) {
|
|
|
1265
1449
|
parts.push('\n');
|
|
1266
1450
|
parts.push(...formatCleanupReason(reason.reasons[i], depth));
|
|
1267
1451
|
}
|
|
1452
|
+
if (reason.chain) {
|
|
1453
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1454
|
+
}
|
|
1268
1455
|
return parts;
|
|
1269
1456
|
}
|
|
1270
1457
|
}
|
|
@@ -1465,6 +1652,8 @@ const options = {
|
|
|
1465
1652
|
asyncMode: 'cancel',
|
|
1466
1653
|
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
1467
1654
|
warn: (...args) => console.warn(...args),
|
|
1655
|
+
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
1656
|
+
error: (...args) => console.error(...args),
|
|
1468
1657
|
/**
|
|
1469
1658
|
* Introspection and debug aids. Set to `null` to disable all debug overhead in production.
|
|
1470
1659
|
*
|
|
@@ -1594,7 +1783,7 @@ function dependant(obj, prop = allProps) {
|
|
|
1594
1783
|
return;
|
|
1595
1784
|
const node = getEffectNode(currentActiveEffect);
|
|
1596
1785
|
if ('dependencyHook' in node) {
|
|
1597
|
-
node.dependencyHook(obj, prop);
|
|
1786
|
+
node.dependencyHook?.(obj, prop);
|
|
1598
1787
|
}
|
|
1599
1788
|
let objectWatchers = exports.watchers.get(obj);
|
|
1600
1789
|
if (!objectWatchers) {
|
|
@@ -1660,6 +1849,9 @@ function formatRoots(roots, limit = 20) {
|
|
|
1660
1849
|
const end = names.slice(-10);
|
|
1661
1850
|
return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
|
|
1662
1851
|
}
|
|
1852
|
+
function externalReasonFrom(fn) {
|
|
1853
|
+
return fn.name ? { type: 'external', detail: fn.name } : undefined;
|
|
1854
|
+
}
|
|
1663
1855
|
// Nested map structure for efficient counting and batch cleanup
|
|
1664
1856
|
// batchId -> effect root -> obj -> prop -> count
|
|
1665
1857
|
let activationRegistry;
|
|
@@ -2102,6 +2294,14 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2102
2294
|
// Build reason from pending triggers if not provided
|
|
2103
2295
|
if (!reason && node.pendingTriggers) {
|
|
2104
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);
|
|
2105
2305
|
}
|
|
2106
2306
|
node.pendingTriggers = undefined;
|
|
2107
2307
|
if (reason) {
|
|
@@ -2370,7 +2570,7 @@ function executeNext(effectuatedRoots) {
|
|
|
2370
2570
|
}
|
|
2371
2571
|
// Track which sub-effects have been executed to prevent infinite loops
|
|
2372
2572
|
// These are all the effects triggered under `activeEffect` and all their sub-effects
|
|
2373
|
-
function batch(effect, immediate) {
|
|
2573
|
+
function batch(effect, immediate, caller) {
|
|
2374
2574
|
if (broken) {
|
|
2375
2575
|
throw new ReactiveError('[reactive] Reactive system is broken after an unrecoverable error. Call reset() to recover.', { code: exports.ReactiveErrorCode.BrokenEffects });
|
|
2376
2576
|
}
|
|
@@ -2386,11 +2586,12 @@ function batch(effect, immediate) {
|
|
|
2386
2586
|
optionCall('beginChain', roots);
|
|
2387
2587
|
}
|
|
2388
2588
|
// TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
|
|
2389
|
-
const
|
|
2589
|
+
const activeCaller = getActiveEffect();
|
|
2590
|
+
const callerToUse = caller || activeCaller;
|
|
2390
2591
|
// Optimization: If nested and NOT immediate, just join the existing batch
|
|
2391
2592
|
if (!isNewBatch && !immediate) {
|
|
2392
2593
|
for (let i = 0; i < effect.length; i++) {
|
|
2393
|
-
addToBatch(effect[i],
|
|
2594
|
+
addToBatch(effect[i], callerToUse);
|
|
2394
2595
|
}
|
|
2395
2596
|
return;
|
|
2396
2597
|
}
|
|
@@ -2429,7 +2630,7 @@ function batch(effect, immediate) {
|
|
|
2429
2630
|
else {
|
|
2430
2631
|
// Add initial effects to batch and compute dependencies
|
|
2431
2632
|
for (let i = 0; i < effect.length; i++) {
|
|
2432
|
-
addToBatch(effect[i],
|
|
2633
|
+
addToBatch(effect[i], callerToUse, false);
|
|
2433
2634
|
}
|
|
2434
2635
|
computeAllInDegrees(currentBatch);
|
|
2435
2636
|
}
|
|
@@ -2483,6 +2684,11 @@ function batch(effect, immediate) {
|
|
|
2483
2684
|
success = true;
|
|
2484
2685
|
return firstReturn.value;
|
|
2485
2686
|
}
|
|
2687
|
+
catch (error) {
|
|
2688
|
+
if (batchStack.length === 1)
|
|
2689
|
+
optionCall('error', '[reactive] Root batch failure before broken state:', error);
|
|
2690
|
+
throw error;
|
|
2691
|
+
}
|
|
2486
2692
|
finally {
|
|
2487
2693
|
if (!success && batchStack.length === 1) {
|
|
2488
2694
|
broken = true;
|
|
@@ -2583,7 +2789,7 @@ const fr = new FinalizationRegistry((f) => f());
|
|
|
2583
2789
|
* @param options - Options for effect execution
|
|
2584
2790
|
* @returns A cleanup function to stop the effect
|
|
2585
2791
|
*/
|
|
2586
|
-
const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
|
|
2792
|
+
const effect = captioned(named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
|
|
2587
2793
|
if (effectOptions?.name)
|
|
2588
2794
|
Object.defineProperty(fn, 'name', { value: effectOptions.name });
|
|
2589
2795
|
// Use per-effect asyncMode or fall back to global option
|
|
@@ -2596,7 +2802,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2596
2802
|
const prevCleanup = node.cleanup;
|
|
2597
2803
|
node.cleanup = undefined;
|
|
2598
2804
|
try {
|
|
2599
|
-
untracked(() => prevCleanup(node.nextReason || {
|
|
2805
|
+
untracked `effect:cleanup`(() => prevCleanup(chainExternalReason(node.nextReason || {
|
|
2806
|
+
type: 'stopped',
|
|
2807
|
+
chain: node.currentReason,
|
|
2808
|
+
})));
|
|
2600
2809
|
}
|
|
2601
2810
|
catch (error) {
|
|
2602
2811
|
// If we want to report them, we could use options.warn or similar
|
|
@@ -2629,6 +2838,9 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2629
2838
|
}
|
|
2630
2839
|
// Set reaction reason for the upcoming run
|
|
2631
2840
|
access.reaction = node.nextReason || access.reaction;
|
|
2841
|
+
node.currentReason =
|
|
2842
|
+
node.nextReason ||
|
|
2843
|
+
(access.reaction && access.reaction !== true ? access.reaction : undefined);
|
|
2632
2844
|
node.nextReason = undefined;
|
|
2633
2845
|
optionCall('enter', getRoot(fn));
|
|
2634
2846
|
optionCall('effectRun', getRoot(fn), access.reaction);
|
|
@@ -2691,7 +2903,8 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2691
2903
|
// This ensures that when we cancel, the original promise's .catch() handlers are triggered
|
|
2692
2904
|
// We do this by rejecting the race promise, which makes the original promise chain see the rejection
|
|
2693
2905
|
// through the zone-wrapped .then()/.catch() handlers
|
|
2694
|
-
runningPromise = runningPromise
|
|
2906
|
+
runningPromise = runningPromise
|
|
2907
|
+
.catch((error) => {
|
|
2695
2908
|
// Propagate async errors to the effect's error handler
|
|
2696
2909
|
// This ensures onEffectThrow handlers are triggered for async errors
|
|
2697
2910
|
if (error !== cancelError) {
|
|
@@ -2699,6 +2912,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2699
2912
|
}
|
|
2700
2913
|
// If thrower didn't throw (handled), we absorb the error.
|
|
2701
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;
|
|
2702
2919
|
});
|
|
2703
2920
|
}
|
|
2704
2921
|
else {
|
|
@@ -2709,7 +2926,13 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2709
2926
|
catch (error) {
|
|
2710
2927
|
debugHooks.decorateError(error, runEffect);
|
|
2711
2928
|
// catcher:self`
|
|
2712
|
-
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
|
+
}
|
|
2713
2936
|
}
|
|
2714
2937
|
// Create cleanup function for next run
|
|
2715
2938
|
node.cleanup = (reason) => {
|
|
@@ -2740,8 +2963,11 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2740
2963
|
const childReason = reason
|
|
2741
2964
|
? reason.type === 'lineage'
|
|
2742
2965
|
? reason
|
|
2743
|
-
: { type: 'lineage', parent: reason }
|
|
2744
|
-
: { 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
|
+
});
|
|
2745
2971
|
for (const childCleanup of children)
|
|
2746
2972
|
childCleanup(childReason);
|
|
2747
2973
|
delete node.children;
|
|
@@ -2754,7 +2980,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2754
2980
|
const node = getEffectNode(runEffect);
|
|
2755
2981
|
if (debugHooks.isDevtoolsEnabled()) {
|
|
2756
2982
|
const stack = debugHooks.captureStack(); // Robustly skips internal mutts frames
|
|
2757
|
-
if (
|
|
2983
|
+
if (stack) {
|
|
2758
2984
|
node.creationStack = stack;
|
|
2759
2985
|
}
|
|
2760
2986
|
}
|
|
@@ -2814,7 +3040,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2814
3040
|
runningPromise = null;
|
|
2815
3041
|
}
|
|
2816
3042
|
try {
|
|
2817
|
-
node.cleanup?.(reason || { type: 'stopped' });
|
|
3043
|
+
node.cleanup?.(chainExternalReason(reason || { type: 'stopped', chain: node.currentReason }));
|
|
2818
3044
|
}
|
|
2819
3045
|
catch (error) {
|
|
2820
3046
|
// Cleanup errors should basically be ignored or at least not stop the world
|
|
@@ -2857,30 +3083,35 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2857
3083
|
named(name) {
|
|
2858
3084
|
return flavorOptions(this, { name }, { name: 'named' });
|
|
2859
3085
|
},
|
|
2860
|
-
}))
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
return
|
|
2868
|
-
|
|
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
|
+
});
|
|
2869
3097
|
/**
|
|
2870
3098
|
* Executes a function from a virgin/root context - no parent effect, no tracking
|
|
2871
3099
|
* Creates completely independent effects that won't be cleaned up by any parent
|
|
2872
3100
|
* @param fn - The function to execute
|
|
2873
3101
|
*/
|
|
2874
|
-
function root(fn) {
|
|
2875
|
-
|
|
2876
|
-
|
|
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
|
+
});
|
|
2877
3108
|
function biDi(received, get, set) {
|
|
2878
3109
|
if (typeof get !== 'function') {
|
|
2879
3110
|
set = get.set;
|
|
2880
3111
|
get = get.get;
|
|
2881
3112
|
}
|
|
2882
3113
|
let programmaticallySetValue = Symbol();
|
|
2883
|
-
effect
|
|
3114
|
+
effect `biDi`(markWithRoot(() => {
|
|
2884
3115
|
const newValue = get();
|
|
2885
3116
|
const pValue = programmaticallySetValue;
|
|
2886
3117
|
programmaticallySetValue = Symbol();
|
|
@@ -3031,7 +3262,8 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
|
|
|
3031
3262
|
const deps = objectWatchers.get(key);
|
|
3032
3263
|
if (deps) {
|
|
3033
3264
|
// Make sure `some.prop++` does not keep a dependency to `some.props`
|
|
3034
|
-
|
|
3265
|
+
if (sourceEffect)
|
|
3266
|
+
deps.delete(sourceEffect);
|
|
3035
3267
|
for (const effect of deps) {
|
|
3036
3268
|
const runningChain = isRunning(effect);
|
|
3037
3269
|
if (runningChain) {
|
|
@@ -3077,6 +3309,7 @@ function touched(obj, evolution, props) {
|
|
|
3077
3309
|
else
|
|
3078
3310
|
collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
|
|
3079
3311
|
const triggers = Array.from(effects.keys());
|
|
3312
|
+
const sourceEffect = getActiveEffect();
|
|
3080
3313
|
optionCall('touched', obj, evolution, props, triggers);
|
|
3081
3314
|
// Store pending triggers for CleanupReason before batching
|
|
3082
3315
|
if (options.introspection?.gatherReasons) {
|
|
@@ -3098,7 +3331,7 @@ function touched(obj, evolution, props) {
|
|
|
3098
3331
|
});
|
|
3099
3332
|
}
|
|
3100
3333
|
}
|
|
3101
|
-
batch(triggers);
|
|
3334
|
+
batch(triggers, undefined, sourceEffect);
|
|
3102
3335
|
}
|
|
3103
3336
|
// Bubble up changes if this object has deep watchers
|
|
3104
3337
|
if (objectsWithDeepWatchers.has(obj)) {
|
|
@@ -3172,7 +3405,7 @@ function touchedOpaque(obj, evolution, prop) {
|
|
|
3172
3405
|
}
|
|
3173
3406
|
if (effects.size > 0) {
|
|
3174
3407
|
optionCall('touched', obj, evolution, [prop], Array.from(effects));
|
|
3175
|
-
batch(Array.from(effects));
|
|
3408
|
+
batch(Array.from(effects), undefined, sourceEffect);
|
|
3176
3409
|
}
|
|
3177
3410
|
}
|
|
3178
3411
|
|
|
@@ -3194,9 +3427,10 @@ function addUnreactiveProps(proto, set) {
|
|
|
3194
3427
|
return proto;
|
|
3195
3428
|
}
|
|
3196
3429
|
// Merge sets
|
|
3197
|
-
|
|
3430
|
+
const merged = new Set(existing);
|
|
3431
|
+
proto[unreactiveProperties] = merged;
|
|
3198
3432
|
for (const p of set)
|
|
3199
|
-
|
|
3433
|
+
merged.add(p);
|
|
3200
3434
|
}
|
|
3201
3435
|
// If no set, mark as fully unreactive, otherwise create set
|
|
3202
3436
|
else
|
|
@@ -3295,7 +3529,7 @@ function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty)
|
|
|
3295
3529
|
const origin = { obj: unwrappedObj, prop };
|
|
3296
3530
|
// Deep touch: only notify nested property changes with origin filtering
|
|
3297
3531
|
// Don't notify direct property change - the whole point is to avoid parent effects re-running
|
|
3298
|
-
const changes = untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
3532
|
+
const changes = untracked `deepTouch:recursive`(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
3299
3533
|
// When deep touch found no child differences, the object identity still changed.
|
|
3300
3534
|
// Migrate watchers from old → new so the dependency chain is preserved.
|
|
3301
3535
|
if (changes.length === 0) {
|
|
@@ -3521,6 +3755,17 @@ const subsRegister = new WeakMap();
|
|
|
3521
3755
|
// Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
|
|
3522
3756
|
// TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
|
|
3523
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
|
+
}
|
|
3524
3769
|
const reactiveHandlers = {
|
|
3525
3770
|
[Symbol.toStringTag]: 'MutTs Reactive',
|
|
3526
3771
|
get(obj, prop, receiver) {
|
|
@@ -3548,6 +3793,10 @@ const reactiveHandlers = {
|
|
|
3548
3793
|
// Symbols: fast-path — no reactivity tracking
|
|
3549
3794
|
if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
|
|
3550
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
|
+
}
|
|
3551
3800
|
// Check if property exists using a trap-free walk to avoid triggering
|
|
3552
3801
|
// the has-trap cascade on prototype chains of reactive proxies.
|
|
3553
3802
|
const isOwnProp = Object.hasOwn(obj, prop);
|
|
@@ -3587,15 +3836,7 @@ const reactiveHandlers = {
|
|
|
3587
3836
|
// For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
|
|
3588
3837
|
// For all other objects, inline Reflect.get directly (skips 3 function calls).
|
|
3589
3838
|
const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
|
|
3590
|
-
|
|
3591
|
-
const reactiveValue = reactiveObject(value);
|
|
3592
|
-
// Only create back-references if this object needs them
|
|
3593
|
-
if (needsBackReferences(obj)) {
|
|
3594
|
-
addBackReference(reactiveValue, obj, prop);
|
|
3595
|
-
}
|
|
3596
|
-
return reactiveValue;
|
|
3597
|
-
}
|
|
3598
|
-
return value;
|
|
3839
|
+
return wrapReactiveValue(obj, prop, value);
|
|
3599
3840
|
},
|
|
3600
3841
|
set(obj, prop, value, receiver) {
|
|
3601
3842
|
const unwrapped = unwrap(receiver);
|
|
@@ -3772,6 +4013,7 @@ const reactive = decorator({
|
|
|
3772
4013
|
});
|
|
3773
4014
|
|
|
3774
4015
|
exports.AZone = AZone;
|
|
4016
|
+
exports.CompareSymbol = CompareSymbol;
|
|
3775
4017
|
exports.DecoratorError = DecoratorError;
|
|
3776
4018
|
exports.FoolProof = FoolProof;
|
|
3777
4019
|
exports.IterableWeakMap = IterableWeakMap;
|
|
@@ -3795,8 +4037,10 @@ exports.atom = atom;
|
|
|
3795
4037
|
exports.atomic = atomic;
|
|
3796
4038
|
exports.batch = batch;
|
|
3797
4039
|
exports.biDi = biDi;
|
|
4040
|
+
exports.captioned = captioned;
|
|
3798
4041
|
exports.captured = captured;
|
|
3799
4042
|
exports.caught = caught;
|
|
4043
|
+
exports.chainExternalReason = chainExternalReason;
|
|
3800
4044
|
exports.contentRef = contentRef;
|
|
3801
4045
|
exports.createFlavor = createFlavor;
|
|
3802
4046
|
exports.debugPreset = debugPreset;
|
|
@@ -3808,6 +4052,7 @@ exports.dependant = dependant;
|
|
|
3808
4052
|
exports.devPreset = devPreset;
|
|
3809
4053
|
exports.effect = effect;
|
|
3810
4054
|
exports.effectAggregator = effectAggregator;
|
|
4055
|
+
exports.effectContext = effectContext;
|
|
3811
4056
|
exports.effectHistory = effectHistory;
|
|
3812
4057
|
exports.effectMarker = effectMarker;
|
|
3813
4058
|
exports.effectToDeepWatchedObjects = effectToDeepWatchedObjects;
|
|
@@ -3820,6 +4065,7 @@ exports.getEffectNode = getEffectNode;
|
|
|
3820
4065
|
exports.getRoot = getRoot;
|
|
3821
4066
|
exports.getState = getState;
|
|
3822
4067
|
exports.hooks = hooks;
|
|
4068
|
+
exports.inheritCaption = inheritCaption;
|
|
3823
4069
|
exports.isConstructor = isConstructor;
|
|
3824
4070
|
exports.isDev = isDev;
|
|
3825
4071
|
exports.isNonReactive = isNonReactive;
|
|
@@ -3857,6 +4103,7 @@ exports.unlink = unlink;
|
|
|
3857
4103
|
exports.unreactiveProperties = unreactiveProperties;
|
|
3858
4104
|
exports.untracked = untracked;
|
|
3859
4105
|
exports.unwrap = unwrap;
|
|
4106
|
+
exports.withEffectContext = withEffectContext;
|
|
3860
4107
|
exports.wrapProtos = wrapProtos;
|
|
3861
4108
|
exports.zip = zip;
|
|
3862
|
-
//# sourceMappingURL=proxy-
|
|
4109
|
+
//# sourceMappingURL=proxy-DBHj3kGK.cjs.map
|