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
|
@@ -136,13 +136,10 @@ function isOwnAccessor(obj, prop) {
|
|
|
136
136
|
return !!(opd?.get || opd?.set);
|
|
137
137
|
}
|
|
138
138
|
/**
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* @param b - Second value
|
|
144
|
-
* @param cache - Map for circular reference protection (internal use)
|
|
145
|
-
* @returns True if values are deeply equal
|
|
139
|
+
* Symbol used to provide custom comparison logic for an object.
|
|
140
|
+
*/
|
|
141
|
+
const CompareSymbol = Symbol.for('mutts.compare');
|
|
142
|
+
/**
|
|
146
143
|
*/
|
|
147
144
|
function deepCompare(a, b, cache = new Map()) {
|
|
148
145
|
if (a === b)
|
|
@@ -150,6 +147,13 @@ function deepCompare(a, b, cache = new Map()) {
|
|
|
150
147
|
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
|
|
151
148
|
return a === b;
|
|
152
149
|
}
|
|
150
|
+
// Custom comparison support
|
|
151
|
+
if (typeof a[CompareSymbol] === 'function') {
|
|
152
|
+
return a[CompareSymbol](b, (x, y) => deepCompare(x, y, cache));
|
|
153
|
+
}
|
|
154
|
+
if (typeof b[CompareSymbol] === 'function') {
|
|
155
|
+
return b[CompareSymbol](a, (x, y) => deepCompare(x, y, cache));
|
|
156
|
+
}
|
|
153
157
|
// Prototype check
|
|
154
158
|
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
|
|
155
159
|
return false;
|
|
@@ -268,7 +272,8 @@ function named(name, fn) {
|
|
|
268
272
|
});
|
|
269
273
|
return fn;
|
|
270
274
|
}
|
|
271
|
-
const
|
|
275
|
+
const runtimeGlobals = globalThis;
|
|
276
|
+
const _mode = runtimeGlobals.process?.env?.NODE_ENV ||
|
|
272
277
|
(typeof import.meta !== 'undefined' && import.meta.env?.MODE) ||
|
|
273
278
|
'production';
|
|
274
279
|
const isDev = _mode === 'development';
|
|
@@ -444,6 +449,93 @@ const decorator = (description) => {
|
|
|
444
449
|
* flavoredGreet.loud('World') // "HELLO, WORLD!"
|
|
445
450
|
* ```
|
|
446
451
|
*/
|
|
452
|
+
const captionedOptionsSymbol = Symbol('mutts.captioned.options');
|
|
453
|
+
function isTemplateStringsArray(value) {
|
|
454
|
+
return (Array.isArray(value) &&
|
|
455
|
+
Object.hasOwn(value, 'raw') &&
|
|
456
|
+
Array.isArray(value.raw));
|
|
457
|
+
}
|
|
458
|
+
function renderTemplate(strings, values) {
|
|
459
|
+
let result = strings[0] ?? '';
|
|
460
|
+
for (let i = 0; i < values.length; i++)
|
|
461
|
+
result += String(values[i]) + (strings[i + 1] ?? '');
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
function renameCallback(caption, callback) {
|
|
465
|
+
Object.defineProperty(callback, 'name', {
|
|
466
|
+
value: caption,
|
|
467
|
+
writable: false,
|
|
468
|
+
configurable: true,
|
|
469
|
+
});
|
|
470
|
+
return callback;
|
|
471
|
+
}
|
|
472
|
+
function isAnonymousCallback(callback) {
|
|
473
|
+
return !callback.name || callback.name === 'anonymous';
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Wraps a callback-first function so it also accepts a tagged-template call form.
|
|
477
|
+
*
|
|
478
|
+
* The template caption is applied to one callback argument before the base
|
|
479
|
+
* function runs. By default, `captioned` targets the first argument, but
|
|
480
|
+
* `callbackIndex` can point to any callback position.
|
|
481
|
+
*
|
|
482
|
+
* This is intended for APIs such as `effect`, `lift`, or `watch` where naming
|
|
483
|
+
* is useful but should remain separate from the flavor system.
|
|
484
|
+
*
|
|
485
|
+
* Plain calls still work:
|
|
486
|
+
* `run(callback)`
|
|
487
|
+
*
|
|
488
|
+
* Captioned calls add a runtime name to the first callback:
|
|
489
|
+
* `` run`task:${id}`(callback) ``
|
|
490
|
+
*
|
|
491
|
+
* Anonymous uncaptioned callbacks may trigger a warning depending on
|
|
492
|
+
* `shouldWarnAnonymous`.
|
|
493
|
+
*/
|
|
494
|
+
function captioned(fn, options = {}) {
|
|
495
|
+
const settings = {
|
|
496
|
+
callbackIndex: options.callbackIndex ?? 0,
|
|
497
|
+
name: options.name ?? (fn.name || 'callback'),
|
|
498
|
+
rename: options.rename ?? ((caption, callback) => renameCallback(caption, callback)),
|
|
499
|
+
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
500
|
+
warn: options.warn ?? ((message) => console.warn(message)),
|
|
501
|
+
shouldWarnAnonymous: options.shouldWarnAnonymous,
|
|
502
|
+
};
|
|
503
|
+
fn[captionedOptionsSymbol] = settings;
|
|
504
|
+
return new Proxy(fn, {
|
|
505
|
+
get(target, prop, receiver) {
|
|
506
|
+
if (prop === captionedOptionsSymbol)
|
|
507
|
+
return settings;
|
|
508
|
+
return Reflect.get(target, prop, receiver);
|
|
509
|
+
},
|
|
510
|
+
apply(target, thisArg, args) {
|
|
511
|
+
if (isTemplateStringsArray(args[0])) {
|
|
512
|
+
const caption = renderTemplate(args[0], args.slice(1));
|
|
513
|
+
return function captionedCall(...callArgs) {
|
|
514
|
+
const callback = callArgs[settings.callbackIndex];
|
|
515
|
+
if (typeof callback !== 'function')
|
|
516
|
+
throw new TypeError(`${settings.name} template calls require a callback at argument index ${settings.callbackIndex}`);
|
|
517
|
+
const nextArgs = [...callArgs];
|
|
518
|
+
nextArgs[settings.callbackIndex] = settings.rename(caption, callback);
|
|
519
|
+
return Reflect.apply(target, this, nextArgs);
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const callback = args[settings.callbackIndex];
|
|
523
|
+
if (typeof callback === 'function' && isAnonymousCallback(callback)) {
|
|
524
|
+
const shouldWarn = settings.shouldWarnAnonymous?.(callback, args) ?? true;
|
|
525
|
+
if (shouldWarn)
|
|
526
|
+
settings.warn(`${settings.name}: anonymous callback detected. Use template syntax for automatic naming:\n` +
|
|
527
|
+
` Current: ${settings.name}(() => { ... })\n` +
|
|
528
|
+
` Fix: ${settings.name}\`descriptive-name\`(() => { ... })\n` +
|
|
529
|
+
`The captioned system uses the template literal as the effect name for better debugging.`);
|
|
530
|
+
}
|
|
531
|
+
return Reflect.apply(target, thisArg, args);
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
function inheritCaption(source, target) {
|
|
536
|
+
const settings = source[captionedOptionsSymbol];
|
|
537
|
+
return settings ? captioned(target, settings) : target;
|
|
538
|
+
}
|
|
447
539
|
/**
|
|
448
540
|
* Creates a flavored (extensible) version of a function with chainable property modifiers.
|
|
449
541
|
*/
|
|
@@ -476,7 +568,7 @@ function createFlavor(fn, transform, name) {
|
|
|
476
568
|
};
|
|
477
569
|
if (name)
|
|
478
570
|
named(name, fct);
|
|
479
|
-
return flavored(fct, fn.flavors || {});
|
|
571
|
+
return flavored(inheritCaption(fn, fct), fn.flavors || {});
|
|
480
572
|
}
|
|
481
573
|
/**
|
|
482
574
|
* Creates a new flavored function that merges options objects at a specific index.
|
|
@@ -509,7 +601,7 @@ function flavorOptions(fn, defaultOptions, opts = {}) {
|
|
|
509
601
|
// Preserve arity and options track
|
|
510
602
|
Object.defineProperty(fct, 'length', { value: fn.length });
|
|
511
603
|
fct.optionsIndex = targetIndex;
|
|
512
|
-
return flavored(fct, fn.flavors || {});
|
|
604
|
+
return flavored(inheritCaption(fn, fct), fn.flavors || {});
|
|
513
605
|
}
|
|
514
606
|
|
|
515
607
|
/// <reference lib="esnext.collection" />
|
|
@@ -1103,6 +1195,7 @@ function resetRegistry() {
|
|
|
1103
1195
|
* @returns The marked function
|
|
1104
1196
|
*/
|
|
1105
1197
|
function markWithRoot(fn, root) {
|
|
1198
|
+
const marked = fn;
|
|
1106
1199
|
// Check for collision
|
|
1107
1200
|
const existingRef = reverseRoots.get(root);
|
|
1108
1201
|
const existing = existingRef?.deref();
|
|
@@ -1117,8 +1210,8 @@ function markWithRoot(fn, root) {
|
|
|
1117
1210
|
// (Last writer wins for the check)
|
|
1118
1211
|
reverseRoots.set(root, new WeakRef(fn));
|
|
1119
1212
|
// Store root mapping as symbol property on the function
|
|
1120
|
-
|
|
1121
|
-
return
|
|
1213
|
+
marked[rootFunctionSymbol] = getRoot(root);
|
|
1214
|
+
return marked;
|
|
1122
1215
|
}
|
|
1123
1216
|
/**
|
|
1124
1217
|
* Gets the root function of a function for effect tracking
|
|
@@ -1138,11 +1231,30 @@ function getRoot(fn) {
|
|
|
1138
1231
|
const effectHistory = tag('effectHistory', new ZoneHistory());
|
|
1139
1232
|
tag('effectHistory.present', effectHistory.present);
|
|
1140
1233
|
asyncZone.add(effectHistory);
|
|
1234
|
+
const externalReason = tag('externalReason', new Zone());
|
|
1235
|
+
asyncZone.add(externalReason);
|
|
1141
1236
|
/**
|
|
1142
1237
|
* Aggregator for zones that need to be tracked along effects.
|
|
1143
1238
|
* ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
|
|
1144
1239
|
*/
|
|
1145
1240
|
const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
|
|
1241
|
+
effectAggregator.add(externalReason);
|
|
1242
|
+
function chainExternalReason(reason) {
|
|
1243
|
+
const external = externalReason.active;
|
|
1244
|
+
if (!external)
|
|
1245
|
+
return reason;
|
|
1246
|
+
if (!reason)
|
|
1247
|
+
return external;
|
|
1248
|
+
let current = reason;
|
|
1249
|
+
while (current) {
|
|
1250
|
+
if (current.type === 'external' &&
|
|
1251
|
+
external.type === 'external' &&
|
|
1252
|
+
current.detail === external.detail)
|
|
1253
|
+
return reason;
|
|
1254
|
+
current = current.chain;
|
|
1255
|
+
}
|
|
1256
|
+
return { ...reason, chain: chainExternalReason(reason.chain) };
|
|
1257
|
+
}
|
|
1146
1258
|
function isRunning(effect) {
|
|
1147
1259
|
const root = getRoot(effect);
|
|
1148
1260
|
return effectHistory.some((e) => getRoot(e) === root);
|
|
@@ -1150,6 +1262,35 @@ function isRunning(effect) {
|
|
|
1150
1262
|
function getActiveEffect() {
|
|
1151
1263
|
return effectHistory.present.active;
|
|
1152
1264
|
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Captures the current effect context so that deferred code can later
|
|
1267
|
+
* create child effects parented to this point in the effect tree.
|
|
1268
|
+
*
|
|
1269
|
+
* @returns An opaque token to pass to `withEffectContext()`
|
|
1270
|
+
*
|
|
1271
|
+
* @example
|
|
1272
|
+
* ```ts
|
|
1273
|
+
* const ctx = effectContext() // inside an effect or root()
|
|
1274
|
+
* // later, in a deferred callback:
|
|
1275
|
+
* withEffectContext(ctx, () => {
|
|
1276
|
+
* effect(() => { /* child of the captured context */ })
|
|
1277
|
+
* })
|
|
1278
|
+
* ```
|
|
1279
|
+
*/
|
|
1280
|
+
function effectContext() {
|
|
1281
|
+
return effectHistory.active;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Runs `fn` within a previously captured effect context.
|
|
1285
|
+
* Any effects created inside `fn` become children of the captured parent.
|
|
1286
|
+
*
|
|
1287
|
+
* @param ctx - The context token from `effectContext()`, or `undefined` for root context
|
|
1288
|
+
* @param fn - The function to execute within the restored context
|
|
1289
|
+
* @returns The return value of `fn`
|
|
1290
|
+
*/
|
|
1291
|
+
function withEffectContext(ctx, fn) {
|
|
1292
|
+
return effectHistory.with(ctx, fn);
|
|
1293
|
+
}
|
|
1153
1294
|
const cleanups = new WeakMap();
|
|
1154
1295
|
/**
|
|
1155
1296
|
* Attach cleanup dependencies to an object. When `unlink(obj)` is called,
|
|
@@ -1177,7 +1318,7 @@ const cleanups = new WeakMap();
|
|
|
1177
1318
|
function link(obj, ...cleanupFns) {
|
|
1178
1319
|
const set = cleanups.get(obj);
|
|
1179
1320
|
if (!set)
|
|
1180
|
-
cleanups.set(obj, new Set(cleanupFns.filter(
|
|
1321
|
+
cleanups.set(obj, new Set(cleanupFns.filter((fn) => fn !== undefined)));
|
|
1181
1322
|
else
|
|
1182
1323
|
for (const fn of cleanupFns)
|
|
1183
1324
|
if (fn)
|
|
@@ -1243,18 +1384,61 @@ function formatCleanupReason(reason, depth = 0) {
|
|
|
1243
1384
|
parts.push(',');
|
|
1244
1385
|
parts.push(...formatTrigger(reason.triggers[i]));
|
|
1245
1386
|
}
|
|
1387
|
+
if (reason.chain) {
|
|
1388
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1389
|
+
}
|
|
1390
|
+
return parts;
|
|
1391
|
+
}
|
|
1392
|
+
case 'stopped': {
|
|
1393
|
+
const parts = [`${indent}stopped`];
|
|
1394
|
+
if (reason.detail)
|
|
1395
|
+
parts.push(`(${reason.detail})`);
|
|
1396
|
+
if (reason.chain) {
|
|
1397
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1398
|
+
}
|
|
1399
|
+
return parts;
|
|
1400
|
+
}
|
|
1401
|
+
case 'external': {
|
|
1402
|
+
const parts = [`${indent}external:`, reason.detail];
|
|
1403
|
+
if (reason.chain) {
|
|
1404
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1405
|
+
}
|
|
1406
|
+
return parts;
|
|
1407
|
+
}
|
|
1408
|
+
case 'gc': {
|
|
1409
|
+
const parts = [`${indent}gc`];
|
|
1410
|
+
if (reason.chain) {
|
|
1411
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1412
|
+
}
|
|
1413
|
+
return parts;
|
|
1414
|
+
}
|
|
1415
|
+
case 'error': {
|
|
1416
|
+
const parts = [`${indent}error:`, reason.error];
|
|
1417
|
+
if (reason.chain) {
|
|
1418
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1419
|
+
}
|
|
1420
|
+
return parts;
|
|
1421
|
+
}
|
|
1422
|
+
case 'lineage': {
|
|
1423
|
+
const parts = [
|
|
1424
|
+
`${indent}lineage ←\n`,
|
|
1425
|
+
...formatCleanupReason(reason.parent, depth + 1),
|
|
1426
|
+
];
|
|
1427
|
+
if (reason.chain) {
|
|
1428
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1429
|
+
}
|
|
1430
|
+
return parts;
|
|
1431
|
+
}
|
|
1432
|
+
case 'invalidate': {
|
|
1433
|
+
const parts = [
|
|
1434
|
+
`${indent}invalidate ←\n`,
|
|
1435
|
+
...formatCleanupReason(reason.cause, depth + 1),
|
|
1436
|
+
];
|
|
1437
|
+
if (reason.chain) {
|
|
1438
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1439
|
+
}
|
|
1246
1440
|
return parts;
|
|
1247
1441
|
}
|
|
1248
|
-
case 'stopped':
|
|
1249
|
-
return [`${indent}stopped`];
|
|
1250
|
-
case 'gc':
|
|
1251
|
-
return [`${indent}gc`];
|
|
1252
|
-
case 'error':
|
|
1253
|
-
return [`${indent}error:`, reason.error];
|
|
1254
|
-
case 'lineage':
|
|
1255
|
-
return [`${indent}lineage ←\n`, ...formatCleanupReason(reason.parent, depth + 1)];
|
|
1256
|
-
case 'invalidate':
|
|
1257
|
-
return [`${indent}invalidate ←\n`, ...formatCleanupReason(reason.cause, depth + 1)];
|
|
1258
1442
|
case 'multiple': {
|
|
1259
1443
|
const parts = [];
|
|
1260
1444
|
for (let i = 0; i < reason.reasons.length; i++) {
|
|
@@ -1262,6 +1446,9 @@ function formatCleanupReason(reason, depth = 0) {
|
|
|
1262
1446
|
parts.push('\n');
|
|
1263
1447
|
parts.push(...formatCleanupReason(reason.reasons[i], depth));
|
|
1264
1448
|
}
|
|
1449
|
+
if (reason.chain) {
|
|
1450
|
+
parts.push('\n', ...formatCleanupReason(reason.chain, depth));
|
|
1451
|
+
}
|
|
1265
1452
|
return parts;
|
|
1266
1453
|
}
|
|
1267
1454
|
}
|
|
@@ -1462,6 +1649,8 @@ const options = {
|
|
|
1462
1649
|
asyncMode: 'cancel',
|
|
1463
1650
|
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
1464
1651
|
warn: (...args) => console.warn(...args),
|
|
1652
|
+
// biome-ignore lint/suspicious/noConsole: This is the whole point here
|
|
1653
|
+
error: (...args) => console.error(...args),
|
|
1465
1654
|
/**
|
|
1466
1655
|
* Introspection and debug aids. Set to `null` to disable all debug overhead in production.
|
|
1467
1656
|
*
|
|
@@ -1591,7 +1780,7 @@ function dependant(obj, prop = allProps) {
|
|
|
1591
1780
|
return;
|
|
1592
1781
|
const node = getEffectNode(currentActiveEffect);
|
|
1593
1782
|
if ('dependencyHook' in node) {
|
|
1594
|
-
node.dependencyHook(obj, prop);
|
|
1783
|
+
node.dependencyHook?.(obj, prop);
|
|
1595
1784
|
}
|
|
1596
1785
|
let objectWatchers = watchers.get(obj);
|
|
1597
1786
|
if (!objectWatchers) {
|
|
@@ -1657,6 +1846,9 @@ function formatRoots(roots, limit = 20) {
|
|
|
1657
1846
|
const end = names.slice(-10);
|
|
1658
1847
|
return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
|
|
1659
1848
|
}
|
|
1849
|
+
function externalReasonFrom(fn) {
|
|
1850
|
+
return fn.name ? { type: 'external', detail: fn.name } : undefined;
|
|
1851
|
+
}
|
|
1660
1852
|
// Nested map structure for efficient counting and batch cleanup
|
|
1661
1853
|
// batchId -> effect root -> obj -> prop -> count
|
|
1662
1854
|
let activationRegistry;
|
|
@@ -2099,6 +2291,14 @@ function addToBatch(effect, caller, immediate, reason) {
|
|
|
2099
2291
|
// Build reason from pending triggers if not provided
|
|
2100
2292
|
if (!reason && node.pendingTriggers) {
|
|
2101
2293
|
reason = { type: 'propChange', triggers: node.pendingTriggers };
|
|
2294
|
+
// Add chain: if this is being triggered from another effect, get its reason
|
|
2295
|
+
if (caller) {
|
|
2296
|
+
const callerNode = getEffectNode(caller);
|
|
2297
|
+
if (callerNode.currentReason) {
|
|
2298
|
+
reason.chain = callerNode.currentReason;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
reason = chainExternalReason(reason);
|
|
2102
2302
|
}
|
|
2103
2303
|
node.pendingTriggers = undefined;
|
|
2104
2304
|
if (reason) {
|
|
@@ -2367,7 +2567,7 @@ function executeNext(effectuatedRoots) {
|
|
|
2367
2567
|
}
|
|
2368
2568
|
// Track which sub-effects have been executed to prevent infinite loops
|
|
2369
2569
|
// These are all the effects triggered under `activeEffect` and all their sub-effects
|
|
2370
|
-
function batch(effect, immediate) {
|
|
2570
|
+
function batch(effect, immediate, caller) {
|
|
2371
2571
|
if (broken) {
|
|
2372
2572
|
throw new ReactiveError('[reactive] Reactive system is broken after an unrecoverable error. Call reset() to recover.', { code: ReactiveErrorCode.BrokenEffects });
|
|
2373
2573
|
}
|
|
@@ -2383,11 +2583,12 @@ function batch(effect, immediate) {
|
|
|
2383
2583
|
optionCall('beginChain', roots);
|
|
2384
2584
|
}
|
|
2385
2585
|
// TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
|
|
2386
|
-
const
|
|
2586
|
+
const activeCaller = getActiveEffect();
|
|
2587
|
+
const callerToUse = caller || activeCaller;
|
|
2387
2588
|
// Optimization: If nested and NOT immediate, just join the existing batch
|
|
2388
2589
|
if (!isNewBatch && !immediate) {
|
|
2389
2590
|
for (let i = 0; i < effect.length; i++) {
|
|
2390
|
-
addToBatch(effect[i],
|
|
2591
|
+
addToBatch(effect[i], callerToUse);
|
|
2391
2592
|
}
|
|
2392
2593
|
return;
|
|
2393
2594
|
}
|
|
@@ -2426,7 +2627,7 @@ function batch(effect, immediate) {
|
|
|
2426
2627
|
else {
|
|
2427
2628
|
// Add initial effects to batch and compute dependencies
|
|
2428
2629
|
for (let i = 0; i < effect.length; i++) {
|
|
2429
|
-
addToBatch(effect[i],
|
|
2630
|
+
addToBatch(effect[i], callerToUse, false);
|
|
2430
2631
|
}
|
|
2431
2632
|
computeAllInDegrees(currentBatch);
|
|
2432
2633
|
}
|
|
@@ -2480,6 +2681,11 @@ function batch(effect, immediate) {
|
|
|
2480
2681
|
success = true;
|
|
2481
2682
|
return firstReturn.value;
|
|
2482
2683
|
}
|
|
2684
|
+
catch (error) {
|
|
2685
|
+
if (batchStack.length === 1)
|
|
2686
|
+
optionCall('error', '[reactive] Root batch failure before broken state:', error);
|
|
2687
|
+
throw error;
|
|
2688
|
+
}
|
|
2483
2689
|
finally {
|
|
2484
2690
|
if (!success && batchStack.length === 1) {
|
|
2485
2691
|
broken = true;
|
|
@@ -2580,7 +2786,7 @@ const fr = new FinalizationRegistry((f) => f());
|
|
|
2580
2786
|
* @param options - Options for effect execution
|
|
2581
2787
|
* @returns A cleanup function to stop the effect
|
|
2582
2788
|
*/
|
|
2583
|
-
const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
|
|
2789
|
+
const effect = captioned(named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
|
|
2584
2790
|
if (effectOptions?.name)
|
|
2585
2791
|
Object.defineProperty(fn, 'name', { value: effectOptions.name });
|
|
2586
2792
|
// Use per-effect asyncMode or fall back to global option
|
|
@@ -2593,7 +2799,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2593
2799
|
const prevCleanup = node.cleanup;
|
|
2594
2800
|
node.cleanup = undefined;
|
|
2595
2801
|
try {
|
|
2596
|
-
untracked(() => prevCleanup(node.nextReason || {
|
|
2802
|
+
untracked `effect:cleanup`(() => prevCleanup(chainExternalReason(node.nextReason || {
|
|
2803
|
+
type: 'stopped',
|
|
2804
|
+
chain: node.currentReason,
|
|
2805
|
+
})));
|
|
2597
2806
|
}
|
|
2598
2807
|
catch (error) {
|
|
2599
2808
|
// If we want to report them, we could use options.warn or similar
|
|
@@ -2626,6 +2835,9 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2626
2835
|
}
|
|
2627
2836
|
// Set reaction reason for the upcoming run
|
|
2628
2837
|
access.reaction = node.nextReason || access.reaction;
|
|
2838
|
+
node.currentReason =
|
|
2839
|
+
node.nextReason ||
|
|
2840
|
+
(access.reaction && access.reaction !== true ? access.reaction : undefined);
|
|
2629
2841
|
node.nextReason = undefined;
|
|
2630
2842
|
optionCall('enter', getRoot(fn));
|
|
2631
2843
|
optionCall('effectRun', getRoot(fn), access.reaction);
|
|
@@ -2688,7 +2900,8 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2688
2900
|
// This ensures that when we cancel, the original promise's .catch() handlers are triggered
|
|
2689
2901
|
// We do this by rejecting the race promise, which makes the original promise chain see the rejection
|
|
2690
2902
|
// through the zone-wrapped .then()/.catch() handlers
|
|
2691
|
-
runningPromise = runningPromise
|
|
2903
|
+
runningPromise = runningPromise
|
|
2904
|
+
.catch((error) => {
|
|
2692
2905
|
// Propagate async errors to the effect's error handler
|
|
2693
2906
|
// This ensures onEffectThrow handlers are triggered for async errors
|
|
2694
2907
|
if (error !== cancelError) {
|
|
@@ -2696,6 +2909,10 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2696
2909
|
}
|
|
2697
2910
|
// If thrower didn't throw (handled), we absorb the error.
|
|
2698
2911
|
// If thrower threw (unhandled), it propagates as a new unhandled rejection, which is correct.
|
|
2912
|
+
})
|
|
2913
|
+
.finally(() => {
|
|
2914
|
+
// Clear currentReason when async effect completes
|
|
2915
|
+
node.currentReason = undefined;
|
|
2699
2916
|
});
|
|
2700
2917
|
}
|
|
2701
2918
|
else {
|
|
@@ -2706,7 +2923,13 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2706
2923
|
catch (error) {
|
|
2707
2924
|
debugHooks.decorateError(error, runEffect);
|
|
2708
2925
|
// catcher:self`
|
|
2709
|
-
errorToThrow = error;
|
|
2926
|
+
errorToThrow = error instanceof Error ? error : new Error(String(error));
|
|
2927
|
+
}
|
|
2928
|
+
finally {
|
|
2929
|
+
// Clear currentReason for synchronous effects
|
|
2930
|
+
if (!runningPromise) {
|
|
2931
|
+
node.currentReason = undefined;
|
|
2932
|
+
}
|
|
2710
2933
|
}
|
|
2711
2934
|
// Create cleanup function for next run
|
|
2712
2935
|
node.cleanup = (reason) => {
|
|
@@ -2737,8 +2960,11 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2737
2960
|
const childReason = reason
|
|
2738
2961
|
? reason.type === 'lineage'
|
|
2739
2962
|
? reason
|
|
2740
|
-
: { type: 'lineage', parent: reason }
|
|
2741
|
-
: { type: 'stopped' }
|
|
2963
|
+
: { type: 'lineage', parent: reason, chain: node.currentReason }
|
|
2964
|
+
: (chainExternalReason({ type: 'stopped', chain: node.currentReason }) ?? {
|
|
2965
|
+
type: 'stopped',
|
|
2966
|
+
chain: node.currentReason,
|
|
2967
|
+
});
|
|
2742
2968
|
for (const childCleanup of children)
|
|
2743
2969
|
childCleanup(childReason);
|
|
2744
2970
|
delete node.children;
|
|
@@ -2751,7 +2977,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2751
2977
|
const node = getEffectNode(runEffect);
|
|
2752
2978
|
if (debugHooks.isDevtoolsEnabled()) {
|
|
2753
2979
|
const stack = debugHooks.captureStack(); // Robustly skips internal mutts frames
|
|
2754
|
-
if (
|
|
2980
|
+
if (stack) {
|
|
2755
2981
|
node.creationStack = stack;
|
|
2756
2982
|
}
|
|
2757
2983
|
}
|
|
@@ -2811,7 +3037,7 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2811
3037
|
runningPromise = null;
|
|
2812
3038
|
}
|
|
2813
3039
|
try {
|
|
2814
|
-
node.cleanup?.(reason || { type: 'stopped' });
|
|
3040
|
+
node.cleanup?.(chainExternalReason(reason || { type: 'stopped', chain: node.currentReason }));
|
|
2815
3041
|
}
|
|
2816
3042
|
catch (error) {
|
|
2817
3043
|
// Cleanup errors should basically be ignored or at least not stop the world
|
|
@@ -2854,30 +3080,35 @@ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOpti
|
|
|
2854
3080
|
named(name) {
|
|
2855
3081
|
return flavorOptions(this, { name }, { name: 'named' });
|
|
2856
3082
|
},
|
|
2857
|
-
}))
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
return
|
|
2865
|
-
|
|
3083
|
+
})), {
|
|
3084
|
+
name: 'effect',
|
|
3085
|
+
warn: (message) => options.warn(`[reactive] ${message}`),
|
|
3086
|
+
shouldWarnAnonymous: (_callback, args) => !(args[1] && typeof args[1] === 'object' && 'name' in args[1]),
|
|
3087
|
+
});
|
|
3088
|
+
const untracked = captioned(function untracked(fn) {
|
|
3089
|
+
const external = externalReasonFrom(fn);
|
|
3090
|
+
return external
|
|
3091
|
+
? externalReason.with(external, () => effectHistory.present.root(fn))
|
|
3092
|
+
: effectHistory.present.root(fn);
|
|
3093
|
+
});
|
|
2866
3094
|
/**
|
|
2867
3095
|
* Executes a function from a virgin/root context - no parent effect, no tracking
|
|
2868
3096
|
* Creates completely independent effects that won't be cleaned up by any parent
|
|
2869
3097
|
* @param fn - The function to execute
|
|
2870
3098
|
*/
|
|
2871
|
-
function root(fn) {
|
|
2872
|
-
|
|
2873
|
-
|
|
3099
|
+
const root = captioned(function root(fn) {
|
|
3100
|
+
const external = externalReasonFrom(fn);
|
|
3101
|
+
return external
|
|
3102
|
+
? externalReason.with(external, () => effectHistory.root(fn))
|
|
3103
|
+
: effectHistory.root(fn);
|
|
3104
|
+
});
|
|
2874
3105
|
function biDi(received, get, set) {
|
|
2875
3106
|
if (typeof get !== 'function') {
|
|
2876
3107
|
set = get.set;
|
|
2877
3108
|
get = get.get;
|
|
2878
3109
|
}
|
|
2879
3110
|
let programmaticallySetValue = Symbol();
|
|
2880
|
-
effect
|
|
3111
|
+
effect `biDi`(markWithRoot(() => {
|
|
2881
3112
|
const newValue = get();
|
|
2882
3113
|
const pValue = programmaticallySetValue;
|
|
2883
3114
|
programmaticallySetValue = Symbol();
|
|
@@ -3028,7 +3259,8 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
|
|
|
3028
3259
|
const deps = objectWatchers.get(key);
|
|
3029
3260
|
if (deps) {
|
|
3030
3261
|
// Make sure `some.prop++` does not keep a dependency to `some.props`
|
|
3031
|
-
|
|
3262
|
+
if (sourceEffect)
|
|
3263
|
+
deps.delete(sourceEffect);
|
|
3032
3264
|
for (const effect of deps) {
|
|
3033
3265
|
const runningChain = isRunning(effect);
|
|
3034
3266
|
if (runningChain) {
|
|
@@ -3074,6 +3306,7 @@ function touched(obj, evolution, props) {
|
|
|
3074
3306
|
else
|
|
3075
3307
|
collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
|
|
3076
3308
|
const triggers = Array.from(effects.keys());
|
|
3309
|
+
const sourceEffect = getActiveEffect();
|
|
3077
3310
|
optionCall('touched', obj, evolution, props, triggers);
|
|
3078
3311
|
// Store pending triggers for CleanupReason before batching
|
|
3079
3312
|
if (options.introspection?.gatherReasons) {
|
|
@@ -3095,7 +3328,7 @@ function touched(obj, evolution, props) {
|
|
|
3095
3328
|
});
|
|
3096
3329
|
}
|
|
3097
3330
|
}
|
|
3098
|
-
batch(triggers);
|
|
3331
|
+
batch(triggers, undefined, sourceEffect);
|
|
3099
3332
|
}
|
|
3100
3333
|
// Bubble up changes if this object has deep watchers
|
|
3101
3334
|
if (objectsWithDeepWatchers.has(obj)) {
|
|
@@ -3169,7 +3402,7 @@ function touchedOpaque(obj, evolution, prop) {
|
|
|
3169
3402
|
}
|
|
3170
3403
|
if (effects.size > 0) {
|
|
3171
3404
|
optionCall('touched', obj, evolution, [prop], Array.from(effects));
|
|
3172
|
-
batch(Array.from(effects));
|
|
3405
|
+
batch(Array.from(effects), undefined, sourceEffect);
|
|
3173
3406
|
}
|
|
3174
3407
|
}
|
|
3175
3408
|
|
|
@@ -3191,9 +3424,10 @@ function addUnreactiveProps(proto, set) {
|
|
|
3191
3424
|
return proto;
|
|
3192
3425
|
}
|
|
3193
3426
|
// Merge sets
|
|
3194
|
-
|
|
3427
|
+
const merged = new Set(existing);
|
|
3428
|
+
proto[unreactiveProperties] = merged;
|
|
3195
3429
|
for (const p of set)
|
|
3196
|
-
|
|
3430
|
+
merged.add(p);
|
|
3197
3431
|
}
|
|
3198
3432
|
// If no set, mark as fully unreactive, otherwise create set
|
|
3199
3433
|
else
|
|
@@ -3292,7 +3526,7 @@ function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty)
|
|
|
3292
3526
|
const origin = { obj: unwrappedObj, prop };
|
|
3293
3527
|
// Deep touch: only notify nested property changes with origin filtering
|
|
3294
3528
|
// Don't notify direct property change - the whole point is to avoid parent effects re-running
|
|
3295
|
-
const changes = untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
3529
|
+
const changes = untracked `deepTouch:recursive`(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
|
|
3296
3530
|
// When deep touch found no child differences, the object identity still changed.
|
|
3297
3531
|
// Migrate watchers from old → new so the dependency chain is preserved.
|
|
3298
3532
|
if (changes.length === 0) {
|
|
@@ -3518,6 +3752,17 @@ const subsRegister = new WeakMap();
|
|
|
3518
3752
|
// Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
|
|
3519
3753
|
// TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
|
|
3520
3754
|
let internalUntracked = false;
|
|
3755
|
+
function wrapReactiveValue(obj, prop, value) {
|
|
3756
|
+
if (!isReactive(value) && typeof value === 'object' && value !== null) {
|
|
3757
|
+
const reactiveValue = reactiveObject(value);
|
|
3758
|
+
// Only create back-references if this object needs them
|
|
3759
|
+
if (needsBackReferences(obj)) {
|
|
3760
|
+
addBackReference(reactiveValue, obj, prop);
|
|
3761
|
+
}
|
|
3762
|
+
return reactiveValue;
|
|
3763
|
+
}
|
|
3764
|
+
return value;
|
|
3765
|
+
}
|
|
3521
3766
|
const reactiveHandlers = {
|
|
3522
3767
|
[Symbol.toStringTag]: 'MutTs Reactive',
|
|
3523
3768
|
get(obj, prop, receiver) {
|
|
@@ -3545,6 +3790,10 @@ const reactiveHandlers = {
|
|
|
3545
3790
|
// Symbols: fast-path — no reactivity tracking
|
|
3546
3791
|
if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
|
|
3547
3792
|
return FoolProof.get(obj, prop, receiver);
|
|
3793
|
+
if (!getActiveEffect()) {
|
|
3794
|
+
const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
|
|
3795
|
+
return wrapReactiveValue(obj, prop, value);
|
|
3796
|
+
}
|
|
3548
3797
|
// Check if property exists using a trap-free walk to avoid triggering
|
|
3549
3798
|
// the has-trap cascade on prototype chains of reactive proxies.
|
|
3550
3799
|
const isOwnProp = Object.hasOwn(obj, prop);
|
|
@@ -3584,15 +3833,7 @@ const reactiveHandlers = {
|
|
|
3584
3833
|
// For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
|
|
3585
3834
|
// For all other objects, inline Reflect.get directly (skips 3 function calls).
|
|
3586
3835
|
const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
|
|
3587
|
-
|
|
3588
|
-
const reactiveValue = reactiveObject(value);
|
|
3589
|
-
// Only create back-references if this object needs them
|
|
3590
|
-
if (needsBackReferences(obj)) {
|
|
3591
|
-
addBackReference(reactiveValue, obj, prop);
|
|
3592
|
-
}
|
|
3593
|
-
return reactiveValue;
|
|
3594
|
-
}
|
|
3595
|
-
return value;
|
|
3836
|
+
return wrapReactiveValue(obj, prop, value);
|
|
3596
3837
|
},
|
|
3597
3838
|
set(obj, prop, value, receiver) {
|
|
3598
3839
|
const unwrapped = unwrap(receiver);
|
|
@@ -3768,5 +4009,5 @@ const reactive = decorator({
|
|
|
3768
4009
|
default: reactiveObject,
|
|
3769
4010
|
});
|
|
3770
4011
|
|
|
3771
|
-
export {
|
|
3772
|
-
//# sourceMappingURL=proxy-
|
|
4012
|
+
export { objectToProxy as $, AZone as A, effectContext as B, CompareSymbol as C, DecoratorError as D, flavorOptions as E, flavored as F, formatCleanupReason as G, getActivationLog as H, IterableWeakMap as I, getActiveEffect as J, getState as K, hooks as L, inheritCaption as M, isConstructor as N, isDev as O, isNonReactive as P, isObject as Q, ReactiveBase as R, isProd as S, isReactive as T, isTest as U, legacyDecorator as V, link as W, mixin as X, modernDecorator as Y, Zone as Z, named as _, IterableWeakSet as a, onEffectThrow as a0, prodPreset as a1, proxyToObject as a2, reactive as a3, options as a4, reset as a5, root as a6, tag as a7, touched as a8, touched1 as a9, wrapProtos as aA, objectParents as aB, watchers as aC, effectToReactiveObjects as aD, effectMarker as aE, setDebugHooks as aF, allProps as aG, unlink as aa, untracked as ab, unwrap as ac, withEffectContext as ad, zip as ae, markWithRoot as af, dependant as ag, getEffectNode as ah, chainExternalReason as ai, keysOf as aj, objectsWithDeepWatchers as ak, effectToDeepWatchedObjects as al, deepWatchers as am, registerDeepWatcher as an, rootFunctionSymbol as ao, getRoot as ap, optionCall as aq, FoolProof as ar, effectHistory as as, unreactiveProperties as at, __runInitializers as au, __esDecorate as av, batch as aw, contentRef as ax, notifyPropertyChange as ay, metaProtos as az, ReactiveError as b, ReactiveErrorCode as c, ZoneAggregator as d, ZoneHistory as e, addBatchCleanup as f, addUnreactiveProps as g, arrayEquals as h, assertUntracked as i, asyncHook as j, asyncHooks as k, asyncZone as l, atom as m, atomic as n, biDi as o, captioned as p, captured as q, caught as r, createFlavor as s, debugPreset as t, decorator as u, deepCompare as v, defer as w, devPreset as x, effect as y, effectAggregator as z };
|
|
4013
|
+
//# sourceMappingURL=proxy-BtmPFjSr.esm.js.map
|