mutts 1.0.4 → 1.0.5

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{index-GRBSx0mB.js → index-Cvxdw6Ax.js} +164 -12
  3. package/dist/chunks/index-Cvxdw6Ax.js.map +1 -0
  4. package/dist/chunks/{index-79Kk8D6e.esm.js → index-qiWwozOc.esm.js} +163 -13
  5. package/dist/chunks/index-qiWwozOc.esm.js.map +1 -0
  6. package/dist/destroyable.esm.js.map +1 -1
  7. package/dist/destroyable.js.map +1 -1
  8. package/dist/index.esm.js +1 -1
  9. package/dist/index.js +3 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/mutts.umd.js +1 -1
  12. package/dist/mutts.umd.js.map +1 -1
  13. package/dist/mutts.umd.min.js +1 -1
  14. package/dist/mutts.umd.min.js.map +1 -1
  15. package/dist/reactive.d.ts +29 -1
  16. package/dist/reactive.esm.js +1 -1
  17. package/dist/reactive.js +3 -1
  18. package/dist/reactive.js.map +1 -1
  19. package/dist/std-decorators.esm.js.map +1 -1
  20. package/dist/std-decorators.js.map +1 -1
  21. package/docs/reactive/core.md +16 -16
  22. package/docs/reactive.md +7 -0
  23. package/package.json +1 -1
  24. package/src/destroyable.ts +2 -2
  25. package/src/reactive/array.ts +3 -5
  26. package/src/reactive/change.ts +6 -2
  27. package/src/reactive/effects.ts +70 -1
  28. package/src/reactive/index.ts +2 -1
  29. package/src/reactive/interface.ts +1 -1
  30. package/src/reactive/map.ts +6 -6
  31. package/src/reactive/mapped.ts +2 -3
  32. package/src/reactive/project.ts +103 -6
  33. package/src/reactive/set.ts +6 -6
  34. package/src/reactive/types.ts +22 -0
  35. package/src/reactive/zone.ts +1 -1
  36. package/src/std-decorators.ts +1 -1
  37. package/dist/chunks/index-79Kk8D6e.esm.js.map +0 -1
  38. package/dist/chunks/index-GRBSx0mB.js.map +0 -1
  39. /package/{src/reactive/project.project.md → docs/reactive/project.md} +0 -0
package/README.md CHANGED
@@ -16,7 +16,7 @@ npm install mutts
16
16
 
17
17
  > [!TIP]
18
18
  > **Are you an AI Agent?**
19
- > If you are an LLM or autonomous agent trying to fix bugs or understand this codebase, please read the **[AI Agent Manual](./docs/ai.md)**.
19
+ > If you are an LLM or autonomous agent trying to fix bugs or understand this codebase, please read the **[AI Agent Manual](./docs/ai/manual.md)**.
20
20
  > It contains structured protocols, error code definitions, and introspection API details designed specifically for you.
21
21
  > A precise **[API Reference](./docs/ai/api-reference.md)** is also available for type lookups.
22
22
 
@@ -354,6 +354,10 @@ const prototypeForwarding = Symbol('prototype-forwarding');
354
354
  * Symbol representing all properties in reactive tracking
355
355
  */
356
356
  const allProps = Symbol('all-props');
357
+ /**
358
+ * Symbol for accessing projection information on reactive objects
359
+ */
360
+ const projectionInfo = Symbol('projection-info');
357
361
  // Symbol to mark functions with their root function
358
362
  const rootFunction = Symbol('root-function');
359
363
  /**
@@ -428,6 +432,12 @@ const options = {
428
432
  * @default 100
429
433
  */
430
434
  maxEffectChain: 100,
435
+ /**
436
+ * Maximum number of times an effect can be triggered by the same cause in a single batch
437
+ * Used to detect aggressive re-computation or infinite loops
438
+ * @default 10
439
+ */
440
+ maxTriggerPerBatch: 10,
431
441
  /**
432
442
  * Debug purpose: maximum effect reaction (like call stack max depth)
433
443
  * Used to prevent infinite loops
@@ -1260,6 +1270,50 @@ function formatRoots(roots, limit = 20) {
1260
1270
  const end = names.slice(-10);
1261
1271
  return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
1262
1272
  }
1273
+ // Nested map structure for efficient counting and batch cleanup
1274
+ // batchId -> effect root -> obj -> prop -> count
1275
+ let activationRegistry;
1276
+ const activationLog = new Array(100);
1277
+ function getActivationLog() {
1278
+ return activationLog;
1279
+ }
1280
+ function recordActivation(effect, obj, evolution, prop) {
1281
+ const root = getRoot(effect);
1282
+ if (!activationRegistry)
1283
+ return;
1284
+ let effectData = activationRegistry.get(root);
1285
+ if (!effectData) {
1286
+ effectData = new Map();
1287
+ activationRegistry.set(root, effectData);
1288
+ }
1289
+ let objData = effectData.get(obj);
1290
+ if (!objData) {
1291
+ objData = new Map();
1292
+ effectData.set(obj, objData);
1293
+ }
1294
+ const count = (objData.get(prop) ?? 0) + 1;
1295
+ objData.set(prop, count);
1296
+ // Keep a limited history for diagnostics
1297
+ activationLog.unshift({
1298
+ effect,
1299
+ obj,
1300
+ evolution,
1301
+ prop,
1302
+ });
1303
+ activationLog.pop();
1304
+ if (count >= options.maxTriggerPerBatch) {
1305
+ const effectName = root?.name || 'anonymous';
1306
+ const message = `Aggressive trigger detected: effect "${effectName}" triggered ${count} times in the batch by the same cause.`;
1307
+ if (options.maxEffectReaction === 'throw') {
1308
+ throw new ReactiveError(message, {
1309
+ code: ReactiveErrorCode.MaxReactionExceeded,
1310
+ count,
1311
+ effect: effectName,
1312
+ });
1313
+ }
1314
+ options.warn(`[reactive] ${message}`);
1315
+ }
1316
+ }
1263
1317
  /**
1264
1318
  * Registers a debug callback that is called when the current effect is triggered by a dependency change
1265
1319
  *
@@ -1549,6 +1603,9 @@ function cleanupEffectFromGraph(effect) {
1549
1603
  // Track currently executing effects to prevent re-execution
1550
1604
  // These are all the effects triggered under `activeEffect`
1551
1605
  let batchQueue;
1606
+ function hasBatched(effect) {
1607
+ return batchQueue?.all.has(getRoot(effect));
1608
+ }
1552
1609
  const batchCleanups = new Set();
1553
1610
  /**
1554
1611
  * Computes and caches in-degrees for all effects in the batch
@@ -1974,6 +2031,10 @@ function batch(effect, immediate) {
1974
2031
  }
1975
2032
  else {
1976
2033
  // New batch - initialize
2034
+ if (!activationRegistry)
2035
+ activationRegistry = new Map();
2036
+ else
2037
+ throw new Error('Batch already in progress');
1977
2038
  options.beginChain(roots);
1978
2039
  batchQueue = {
1979
2040
  all: new Map(),
@@ -2052,6 +2113,7 @@ function batch(effect, immediate) {
2052
2113
  return firstReturn.value;
2053
2114
  }
2054
2115
  finally {
2116
+ activationRegistry = undefined;
2055
2117
  batchQueue = undefined;
2056
2118
  options.endChain();
2057
2119
  }
@@ -2122,6 +2184,7 @@ function batch(effect, immediate) {
2122
2184
  return firstReturn.value;
2123
2185
  }
2124
2186
  finally {
2187
+ activationRegistry = undefined;
2125
2188
  batchQueue = undefined;
2126
2189
  options.endChain();
2127
2190
  }
@@ -2491,7 +2554,11 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
2491
2554
  options.skipRunningEffect(effect, runningChain);
2492
2555
  continue;
2493
2556
  }
2494
- effects.add(effect);
2557
+ if (!effects.has(effect)) {
2558
+ effects.add(effect);
2559
+ if (!hasBatched(effect))
2560
+ recordActivation(effect, obj, evolution, key);
2561
+ }
2495
2562
  const trackers = effectTrackers.get(effect);
2496
2563
  recordTriggerLink(sourceEffect, effect, obj, key, evolution);
2497
2564
  if (trackers) {
@@ -2559,6 +2626,7 @@ function touchedOpaque(obj, evolution, prop) {
2559
2626
  continue;
2560
2627
  }
2561
2628
  effects.add(effect);
2629
+ recordActivation(effect, obj, evolution, prop);
2562
2630
  const trackers = effectTrackers.get(effect);
2563
2631
  recordTriggerLink(sourceEffect, effect, obj, prop, evolution);
2564
2632
  if (trackers) {
@@ -3347,7 +3415,6 @@ function* makeReactiveEntriesIterator(iterator) {
3347
3415
  const native$2 = Symbol('native');
3348
3416
  const isArray = Array.isArray;
3349
3417
  Array.isArray = ((value) => isArray(value) ||
3350
- // biome-ignore lint/suspicious/useIsArray: We are defining it
3351
3418
  (value &&
3352
3419
  typeof value === 'object' &&
3353
3420
  prototypeForwarding in value &&
@@ -4243,6 +4310,17 @@ function register(keyFn, initial) {
4243
4310
  return new RegisterClass(keyFn, initial);
4244
4311
  }
4245
4312
 
4313
+ /**
4314
+ * Maps projection effects (item effects) to their projection context
4315
+ */
4316
+ const effectProjectionMetadata = new WeakMap();
4317
+ /**
4318
+ * Returns the projection context of the currently running effect, if any.
4319
+ */
4320
+ function getActiveProjection() {
4321
+ const active = getActiveEffect();
4322
+ return active ? effectProjectionMetadata.get(active) : undefined;
4323
+ }
4246
4324
  function defineAccessValue(access) {
4247
4325
  Object.defineProperty(access, 'value', {
4248
4326
  get: access.get,
@@ -4251,7 +4329,15 @@ function defineAccessValue(access) {
4251
4329
  enumerable: true,
4252
4330
  });
4253
4331
  }
4254
- function makeCleanup(target, effectMap, onDispose) {
4332
+ function makeCleanup(target, effectMap, onDispose, metadata) {
4333
+ if (metadata) {
4334
+ Object.defineProperty(target, projectionInfo, {
4335
+ value: metadata,
4336
+ writable: false,
4337
+ enumerable: false,
4338
+ configurable: true,
4339
+ });
4340
+ }
4255
4341
  return cleanedBy(target, () => {
4256
4342
  onDispose();
4257
4343
  for (const stop of effectMap.values())
@@ -4274,6 +4360,8 @@ function projectArray(source, apply) {
4274
4360
  Reflect.deleteProperty(target, index);
4275
4361
  }
4276
4362
  }
4363
+ const parent = getActiveProjection();
4364
+ const depth = parent ? parent.depth + 1 : 0;
4277
4365
  const cleanupLength = effect(function projectArrayLengthEffect({ ascend }) {
4278
4366
  const length = observedSource.length;
4279
4367
  normalizeTargetLength(length);
@@ -4296,6 +4384,14 @@ function projectArray(source, apply) {
4296
4384
  const produced = apply(accessBase, target);
4297
4385
  target[index] = produced;
4298
4386
  });
4387
+ setEffectName(stop, `project[${depth}]:${index}`);
4388
+ effectProjectionMetadata.set(stop, {
4389
+ source: observedSource,
4390
+ key: index,
4391
+ target,
4392
+ depth,
4393
+ parent,
4394
+ });
4299
4395
  indexEffects.set(i, stop);
4300
4396
  });
4301
4397
  }
@@ -4303,7 +4399,13 @@ function projectArray(source, apply) {
4303
4399
  if (index >= length)
4304
4400
  disposeIndex(index);
4305
4401
  });
4306
- return makeCleanup(target, indexEffects, () => cleanupLength());
4402
+ return makeCleanup(target, indexEffects, () => cleanupLength(), {
4403
+ source: observedSource,
4404
+ target,
4405
+ apply,
4406
+ depth,
4407
+ parent,
4408
+ });
4307
4409
  }
4308
4410
  function projectRegister(source, apply) {
4309
4411
  const observedSource = reactive(source);
@@ -4318,6 +4420,8 @@ function projectRegister(source, apply) {
4318
4420
  target.delete(key);
4319
4421
  }
4320
4422
  }
4423
+ const parent = getActiveProjection();
4424
+ const depth = parent ? parent.depth + 1 : 0;
4321
4425
  const cleanupKeys = effect(function projectRegisterEffect({ ascend }) {
4322
4426
  const keys = new Set();
4323
4427
  for (const key of observedSource.mapKeys())
@@ -4342,6 +4446,14 @@ function projectRegister(source, apply) {
4342
4446
  const produced = apply(accessBase, target);
4343
4447
  target.set(key, produced);
4344
4448
  });
4449
+ setEffectName(stop, `project[${depth}]:${String(key)}`);
4450
+ effectProjectionMetadata.set(stop, {
4451
+ source: observedSource,
4452
+ key,
4453
+ target,
4454
+ depth,
4455
+ parent,
4456
+ });
4345
4457
  keyEffects.set(key, stop);
4346
4458
  });
4347
4459
  }
@@ -4349,7 +4461,13 @@ function projectRegister(source, apply) {
4349
4461
  if (!keys.has(key))
4350
4462
  disposeKey(key);
4351
4463
  });
4352
- return makeCleanup(target, keyEffects, () => cleanupKeys());
4464
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
4465
+ source: observedSource,
4466
+ target,
4467
+ apply,
4468
+ depth,
4469
+ parent,
4470
+ });
4353
4471
  }
4354
4472
  function projectRecord(source, apply) {
4355
4473
  const observedSource = reactive(source);
@@ -4363,6 +4481,8 @@ function projectRecord(source, apply) {
4363
4481
  Reflect.deleteProperty(target, key);
4364
4482
  }
4365
4483
  }
4484
+ const parent = getActiveProjection();
4485
+ const depth = parent ? parent.depth + 1 : 0;
4366
4486
  const cleanupKeys = effect(function projectRecordEffect({ ascend }) {
4367
4487
  const keys = new Set();
4368
4488
  for (const key in observedSource)
@@ -4388,6 +4508,14 @@ function projectRecord(source, apply) {
4388
4508
  const produced = apply(accessBase, target);
4389
4509
  target[sourceKey] = produced;
4390
4510
  });
4511
+ setEffectName(stop, `project[${depth}]:${String(key)}`);
4512
+ effectProjectionMetadata.set(stop, {
4513
+ source: observedSource,
4514
+ key,
4515
+ target,
4516
+ depth,
4517
+ parent,
4518
+ });
4391
4519
  keyEffects.set(key, stop);
4392
4520
  });
4393
4521
  }
@@ -4395,7 +4523,13 @@ function projectRecord(source, apply) {
4395
4523
  if (!keys.has(key))
4396
4524
  disposeKey(key);
4397
4525
  });
4398
- return makeCleanup(target, keyEffects, () => cleanupKeys());
4526
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
4527
+ source: observedSource,
4528
+ target,
4529
+ apply,
4530
+ depth,
4531
+ parent,
4532
+ });
4399
4533
  }
4400
4534
  function projectMap(source, apply) {
4401
4535
  const observedSource = reactive(source);
@@ -4410,6 +4544,8 @@ function projectMap(source, apply) {
4410
4544
  target.delete(key);
4411
4545
  }
4412
4546
  }
4547
+ const parent = getActiveProjection();
4548
+ const depth = parent ? parent.depth + 1 : 0;
4413
4549
  const cleanupKeys = effect(function projectMapEffect({ ascend }) {
4414
4550
  const keys = new Set();
4415
4551
  for (const key of observedSource.keys())
@@ -4434,6 +4570,14 @@ function projectMap(source, apply) {
4434
4570
  const produced = apply(accessBase, target);
4435
4571
  target.set(key, produced);
4436
4572
  });
4573
+ setEffectName(stop, `project[${depth}]:${String(key)}`);
4574
+ effectProjectionMetadata.set(stop, {
4575
+ source: observedSource,
4576
+ key,
4577
+ target,
4578
+ depth,
4579
+ parent,
4580
+ });
4437
4581
  keyEffects.set(key, stop);
4438
4582
  });
4439
4583
  }
@@ -4441,7 +4585,13 @@ function projectMap(source, apply) {
4441
4585
  if (!keys.has(key))
4442
4586
  disposeKey(key);
4443
4587
  });
4444
- return makeCleanup(target, keyEffects, () => cleanupKeys());
4588
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
4589
+ source: observedSource,
4590
+ target,
4591
+ apply,
4592
+ depth,
4593
+ parent,
4594
+ });
4445
4595
  }
4446
4596
  function projectCore(source, apply) {
4447
4597
  if (Array.isArray(source))
@@ -4589,7 +4739,7 @@ class ReactiveWeakMap {
4589
4739
  Object.defineProperties(this, {
4590
4740
  [native$1]: { value: original },
4591
4741
  [prototypeForwarding]: { value: original },
4592
- content: { value: Symbol('content') },
4742
+ content: { value: Symbol('WeakMapContent') },
4593
4743
  [Symbol.toStringTag]: { value: 'ReactiveWeakMap' },
4594
4744
  });
4595
4745
  }
@@ -4629,7 +4779,7 @@ class ReactiveMap {
4629
4779
  Object.defineProperties(this, {
4630
4780
  [native$1]: { value: original },
4631
4781
  [prototypeForwarding]: { value: original },
4632
- content: { value: Symbol('content') },
4782
+ content: { value: Symbol('MapContent') },
4633
4783
  [Symbol.toStringTag]: { value: 'ReactiveMap' },
4634
4784
  });
4635
4785
  }
@@ -4724,7 +4874,7 @@ class ReactiveWeakSet {
4724
4874
  Object.defineProperties(this, {
4725
4875
  [native]: { value: original },
4726
4876
  [prototypeForwarding]: { value: original },
4727
- content: { value: Symbol('content') },
4877
+ content: { value: Symbol('WeakSetContent') },
4728
4878
  [Symbol.toStringTag]: { value: 'ReactiveWeakSet' },
4729
4879
  });
4730
4880
  }
@@ -4759,7 +4909,7 @@ class ReactiveSet {
4759
4909
  Object.defineProperties(this, {
4760
4910
  [native]: { value: original },
4761
4911
  [prototypeForwarding]: { value: original },
4762
- content: { value: Symbol('content') },
4912
+ content: { value: Symbol('SetContent') },
4763
4913
  [Symbol.toStringTag]: { value: 'ReactiveSet' },
4764
4914
  });
4765
4915
  }
@@ -4873,7 +5023,9 @@ exports.defer = defer;
4873
5023
  exports.derived = derived;
4874
5024
  exports.effect = effect;
4875
5025
  exports.enableDevTools = enableDevTools;
5026
+ exports.getActivationLog = getActivationLog;
4876
5027
  exports.getActiveEffect = getActiveEffect;
5028
+ exports.getActiveProjection = getActiveProjection;
4877
5029
  exports.getState = getState;
4878
5030
  exports.immutables = immutables;
4879
5031
  exports.isDevtoolsEnabled = isDevtoolsEnabled;
@@ -4905,4 +5057,4 @@ exports.unreactive = unreactive;
4905
5057
  exports.untracked = untracked;
4906
5058
  exports.unwrap = unwrap;
4907
5059
  exports.watch = watch;
4908
- //# sourceMappingURL=index-GRBSx0mB.js.map
5060
+ //# sourceMappingURL=index-Cvxdw6Ax.js.map