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
@@ -352,6 +352,10 @@ const prototypeForwarding = Symbol('prototype-forwarding');
352
352
  * Symbol representing all properties in reactive tracking
353
353
  */
354
354
  const allProps = Symbol('all-props');
355
+ /**
356
+ * Symbol for accessing projection information on reactive objects
357
+ */
358
+ const projectionInfo = Symbol('projection-info');
355
359
  // Symbol to mark functions with their root function
356
360
  const rootFunction = Symbol('root-function');
357
361
  /**
@@ -426,6 +430,12 @@ const options = {
426
430
  * @default 100
427
431
  */
428
432
  maxEffectChain: 100,
433
+ /**
434
+ * Maximum number of times an effect can be triggered by the same cause in a single batch
435
+ * Used to detect aggressive re-computation or infinite loops
436
+ * @default 10
437
+ */
438
+ maxTriggerPerBatch: 10,
429
439
  /**
430
440
  * Debug purpose: maximum effect reaction (like call stack max depth)
431
441
  * Used to prevent infinite loops
@@ -1258,6 +1268,50 @@ function formatRoots(roots, limit = 20) {
1258
1268
  const end = names.slice(-10);
1259
1269
  return `${start.join(' → ')} ... (${names.length - 15} more) ... ${end.join(' → ')}`;
1260
1270
  }
1271
+ // Nested map structure for efficient counting and batch cleanup
1272
+ // batchId -> effect root -> obj -> prop -> count
1273
+ let activationRegistry;
1274
+ const activationLog = new Array(100);
1275
+ function getActivationLog() {
1276
+ return activationLog;
1277
+ }
1278
+ function recordActivation(effect, obj, evolution, prop) {
1279
+ const root = getRoot(effect);
1280
+ if (!activationRegistry)
1281
+ return;
1282
+ let effectData = activationRegistry.get(root);
1283
+ if (!effectData) {
1284
+ effectData = new Map();
1285
+ activationRegistry.set(root, effectData);
1286
+ }
1287
+ let objData = effectData.get(obj);
1288
+ if (!objData) {
1289
+ objData = new Map();
1290
+ effectData.set(obj, objData);
1291
+ }
1292
+ const count = (objData.get(prop) ?? 0) + 1;
1293
+ objData.set(prop, count);
1294
+ // Keep a limited history for diagnostics
1295
+ activationLog.unshift({
1296
+ effect,
1297
+ obj,
1298
+ evolution,
1299
+ prop,
1300
+ });
1301
+ activationLog.pop();
1302
+ if (count >= options.maxTriggerPerBatch) {
1303
+ const effectName = root?.name || 'anonymous';
1304
+ const message = `Aggressive trigger detected: effect "${effectName}" triggered ${count} times in the batch by the same cause.`;
1305
+ if (options.maxEffectReaction === 'throw') {
1306
+ throw new ReactiveError(message, {
1307
+ code: ReactiveErrorCode.MaxReactionExceeded,
1308
+ count,
1309
+ effect: effectName,
1310
+ });
1311
+ }
1312
+ options.warn(`[reactive] ${message}`);
1313
+ }
1314
+ }
1261
1315
  /**
1262
1316
  * Registers a debug callback that is called when the current effect is triggered by a dependency change
1263
1317
  *
@@ -1547,6 +1601,9 @@ function cleanupEffectFromGraph(effect) {
1547
1601
  // Track currently executing effects to prevent re-execution
1548
1602
  // These are all the effects triggered under `activeEffect`
1549
1603
  let batchQueue;
1604
+ function hasBatched(effect) {
1605
+ return batchQueue?.all.has(getRoot(effect));
1606
+ }
1550
1607
  const batchCleanups = new Set();
1551
1608
  /**
1552
1609
  * Computes and caches in-degrees for all effects in the batch
@@ -1972,6 +2029,10 @@ function batch(effect, immediate) {
1972
2029
  }
1973
2030
  else {
1974
2031
  // New batch - initialize
2032
+ if (!activationRegistry)
2033
+ activationRegistry = new Map();
2034
+ else
2035
+ throw new Error('Batch already in progress');
1975
2036
  options.beginChain(roots);
1976
2037
  batchQueue = {
1977
2038
  all: new Map(),
@@ -2050,6 +2111,7 @@ function batch(effect, immediate) {
2050
2111
  return firstReturn.value;
2051
2112
  }
2052
2113
  finally {
2114
+ activationRegistry = undefined;
2053
2115
  batchQueue = undefined;
2054
2116
  options.endChain();
2055
2117
  }
@@ -2120,6 +2182,7 @@ function batch(effect, immediate) {
2120
2182
  return firstReturn.value;
2121
2183
  }
2122
2184
  finally {
2185
+ activationRegistry = undefined;
2123
2186
  batchQueue = undefined;
2124
2187
  options.endChain();
2125
2188
  }
@@ -2489,7 +2552,11 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
2489
2552
  options.skipRunningEffect(effect, runningChain);
2490
2553
  continue;
2491
2554
  }
2492
- effects.add(effect);
2555
+ if (!effects.has(effect)) {
2556
+ effects.add(effect);
2557
+ if (!hasBatched(effect))
2558
+ recordActivation(effect, obj, evolution, key);
2559
+ }
2493
2560
  const trackers = effectTrackers.get(effect);
2494
2561
  recordTriggerLink(sourceEffect, effect, obj, key, evolution);
2495
2562
  if (trackers) {
@@ -2557,6 +2624,7 @@ function touchedOpaque(obj, evolution, prop) {
2557
2624
  continue;
2558
2625
  }
2559
2626
  effects.add(effect);
2627
+ recordActivation(effect, obj, evolution, prop);
2560
2628
  const trackers = effectTrackers.get(effect);
2561
2629
  recordTriggerLink(sourceEffect, effect, obj, prop, evolution);
2562
2630
  if (trackers) {
@@ -3345,7 +3413,6 @@ function* makeReactiveEntriesIterator(iterator) {
3345
3413
  const native$2 = Symbol('native');
3346
3414
  const isArray = Array.isArray;
3347
3415
  Array.isArray = ((value) => isArray(value) ||
3348
- // biome-ignore lint/suspicious/useIsArray: We are defining it
3349
3416
  (value &&
3350
3417
  typeof value === 'object' &&
3351
3418
  prototypeForwarding in value &&
@@ -4241,6 +4308,17 @@ function register(keyFn, initial) {
4241
4308
  return new RegisterClass(keyFn, initial);
4242
4309
  }
4243
4310
 
4311
+ /**
4312
+ * Maps projection effects (item effects) to their projection context
4313
+ */
4314
+ const effectProjectionMetadata = new WeakMap();
4315
+ /**
4316
+ * Returns the projection context of the currently running effect, if any.
4317
+ */
4318
+ function getActiveProjection() {
4319
+ const active = getActiveEffect();
4320
+ return active ? effectProjectionMetadata.get(active) : undefined;
4321
+ }
4244
4322
  function defineAccessValue(access) {
4245
4323
  Object.defineProperty(access, 'value', {
4246
4324
  get: access.get,
@@ -4249,7 +4327,15 @@ function defineAccessValue(access) {
4249
4327
  enumerable: true,
4250
4328
  });
4251
4329
  }
4252
- function makeCleanup(target, effectMap, onDispose) {
4330
+ function makeCleanup(target, effectMap, onDispose, metadata) {
4331
+ if (metadata) {
4332
+ Object.defineProperty(target, projectionInfo, {
4333
+ value: metadata,
4334
+ writable: false,
4335
+ enumerable: false,
4336
+ configurable: true,
4337
+ });
4338
+ }
4253
4339
  return cleanedBy(target, () => {
4254
4340
  onDispose();
4255
4341
  for (const stop of effectMap.values())
@@ -4272,6 +4358,8 @@ function projectArray(source, apply) {
4272
4358
  Reflect.deleteProperty(target, index);
4273
4359
  }
4274
4360
  }
4361
+ const parent = getActiveProjection();
4362
+ const depth = parent ? parent.depth + 1 : 0;
4275
4363
  const cleanupLength = effect(function projectArrayLengthEffect({ ascend }) {
4276
4364
  const length = observedSource.length;
4277
4365
  normalizeTargetLength(length);
@@ -4294,6 +4382,14 @@ function projectArray(source, apply) {
4294
4382
  const produced = apply(accessBase, target);
4295
4383
  target[index] = produced;
4296
4384
  });
4385
+ setEffectName(stop, `project[${depth}]:${index}`);
4386
+ effectProjectionMetadata.set(stop, {
4387
+ source: observedSource,
4388
+ key: index,
4389
+ target,
4390
+ depth,
4391
+ parent,
4392
+ });
4297
4393
  indexEffects.set(i, stop);
4298
4394
  });
4299
4395
  }
@@ -4301,7 +4397,13 @@ function projectArray(source, apply) {
4301
4397
  if (index >= length)
4302
4398
  disposeIndex(index);
4303
4399
  });
4304
- return makeCleanup(target, indexEffects, () => cleanupLength());
4400
+ return makeCleanup(target, indexEffects, () => cleanupLength(), {
4401
+ source: observedSource,
4402
+ target,
4403
+ apply,
4404
+ depth,
4405
+ parent,
4406
+ });
4305
4407
  }
4306
4408
  function projectRegister(source, apply) {
4307
4409
  const observedSource = reactive(source);
@@ -4316,6 +4418,8 @@ function projectRegister(source, apply) {
4316
4418
  target.delete(key);
4317
4419
  }
4318
4420
  }
4421
+ const parent = getActiveProjection();
4422
+ const depth = parent ? parent.depth + 1 : 0;
4319
4423
  const cleanupKeys = effect(function projectRegisterEffect({ ascend }) {
4320
4424
  const keys = new Set();
4321
4425
  for (const key of observedSource.mapKeys())
@@ -4340,6 +4444,14 @@ function projectRegister(source, apply) {
4340
4444
  const produced = apply(accessBase, target);
4341
4445
  target.set(key, produced);
4342
4446
  });
4447
+ setEffectName(stop, `project[${depth}]:${String(key)}`);
4448
+ effectProjectionMetadata.set(stop, {
4449
+ source: observedSource,
4450
+ key,
4451
+ target,
4452
+ depth,
4453
+ parent,
4454
+ });
4343
4455
  keyEffects.set(key, stop);
4344
4456
  });
4345
4457
  }
@@ -4347,7 +4459,13 @@ function projectRegister(source, apply) {
4347
4459
  if (!keys.has(key))
4348
4460
  disposeKey(key);
4349
4461
  });
4350
- return makeCleanup(target, keyEffects, () => cleanupKeys());
4462
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
4463
+ source: observedSource,
4464
+ target,
4465
+ apply,
4466
+ depth,
4467
+ parent,
4468
+ });
4351
4469
  }
4352
4470
  function projectRecord(source, apply) {
4353
4471
  const observedSource = reactive(source);
@@ -4361,6 +4479,8 @@ function projectRecord(source, apply) {
4361
4479
  Reflect.deleteProperty(target, key);
4362
4480
  }
4363
4481
  }
4482
+ const parent = getActiveProjection();
4483
+ const depth = parent ? parent.depth + 1 : 0;
4364
4484
  const cleanupKeys = effect(function projectRecordEffect({ ascend }) {
4365
4485
  const keys = new Set();
4366
4486
  for (const key in observedSource)
@@ -4386,6 +4506,14 @@ function projectRecord(source, apply) {
4386
4506
  const produced = apply(accessBase, target);
4387
4507
  target[sourceKey] = produced;
4388
4508
  });
4509
+ setEffectName(stop, `project[${depth}]:${String(key)}`);
4510
+ effectProjectionMetadata.set(stop, {
4511
+ source: observedSource,
4512
+ key,
4513
+ target,
4514
+ depth,
4515
+ parent,
4516
+ });
4389
4517
  keyEffects.set(key, stop);
4390
4518
  });
4391
4519
  }
@@ -4393,7 +4521,13 @@ function projectRecord(source, apply) {
4393
4521
  if (!keys.has(key))
4394
4522
  disposeKey(key);
4395
4523
  });
4396
- return makeCleanup(target, keyEffects, () => cleanupKeys());
4524
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
4525
+ source: observedSource,
4526
+ target,
4527
+ apply,
4528
+ depth,
4529
+ parent,
4530
+ });
4397
4531
  }
4398
4532
  function projectMap(source, apply) {
4399
4533
  const observedSource = reactive(source);
@@ -4408,6 +4542,8 @@ function projectMap(source, apply) {
4408
4542
  target.delete(key);
4409
4543
  }
4410
4544
  }
4545
+ const parent = getActiveProjection();
4546
+ const depth = parent ? parent.depth + 1 : 0;
4411
4547
  const cleanupKeys = effect(function projectMapEffect({ ascend }) {
4412
4548
  const keys = new Set();
4413
4549
  for (const key of observedSource.keys())
@@ -4432,6 +4568,14 @@ function projectMap(source, apply) {
4432
4568
  const produced = apply(accessBase, target);
4433
4569
  target.set(key, produced);
4434
4570
  });
4571
+ setEffectName(stop, `project[${depth}]:${String(key)}`);
4572
+ effectProjectionMetadata.set(stop, {
4573
+ source: observedSource,
4574
+ key,
4575
+ target,
4576
+ depth,
4577
+ parent,
4578
+ });
4435
4579
  keyEffects.set(key, stop);
4436
4580
  });
4437
4581
  }
@@ -4439,7 +4583,13 @@ function projectMap(source, apply) {
4439
4583
  if (!keys.has(key))
4440
4584
  disposeKey(key);
4441
4585
  });
4442
- return makeCleanup(target, keyEffects, () => cleanupKeys());
4586
+ return makeCleanup(target, keyEffects, () => cleanupKeys(), {
4587
+ source: observedSource,
4588
+ target,
4589
+ apply,
4590
+ depth,
4591
+ parent,
4592
+ });
4443
4593
  }
4444
4594
  function projectCore(source, apply) {
4445
4595
  if (Array.isArray(source))
@@ -4587,7 +4737,7 @@ class ReactiveWeakMap {
4587
4737
  Object.defineProperties(this, {
4588
4738
  [native$1]: { value: original },
4589
4739
  [prototypeForwarding]: { value: original },
4590
- content: { value: Symbol('content') },
4740
+ content: { value: Symbol('WeakMapContent') },
4591
4741
  [Symbol.toStringTag]: { value: 'ReactiveWeakMap' },
4592
4742
  });
4593
4743
  }
@@ -4627,7 +4777,7 @@ class ReactiveMap {
4627
4777
  Object.defineProperties(this, {
4628
4778
  [native$1]: { value: original },
4629
4779
  [prototypeForwarding]: { value: original },
4630
- content: { value: Symbol('content') },
4780
+ content: { value: Symbol('MapContent') },
4631
4781
  [Symbol.toStringTag]: { value: 'ReactiveMap' },
4632
4782
  });
4633
4783
  }
@@ -4722,7 +4872,7 @@ class ReactiveWeakSet {
4722
4872
  Object.defineProperties(this, {
4723
4873
  [native]: { value: original },
4724
4874
  [prototypeForwarding]: { value: original },
4725
- content: { value: Symbol('content') },
4875
+ content: { value: Symbol('WeakSetContent') },
4726
4876
  [Symbol.toStringTag]: { value: 'ReactiveWeakSet' },
4727
4877
  });
4728
4878
  }
@@ -4757,7 +4907,7 @@ class ReactiveSet {
4757
4907
  Object.defineProperties(this, {
4758
4908
  [native]: { value: original },
4759
4909
  [prototypeForwarding]: { value: original },
4760
- content: { value: Symbol('content') },
4910
+ content: { value: Symbol('SetContent') },
4761
4911
  [Symbol.toStringTag]: { value: 'ReactiveSet' },
4762
4912
  });
4763
4913
  }
@@ -4853,5 +5003,5 @@ const profileInfo = {
4853
5003
  nonReactiveObjects,
4854
5004
  };
4855
5005
 
4856
- export { derived as A, unreactive as B, watch as C, mapped as D, reduced as E, memoize as F, immutables as G, isNonReactive as H, IterableWeakMap as I, registerNativeReactivity as J, project as K, isReactive as L, ReactiveBase as M, reactive as N, unwrap as O, organize as P, organized as Q, ReadOnlyError as R, Register as S, register as T, options as U, ReactiveError as V, isZoneEnabled as W, setZoneEnabled as X, IterableWeakSet as a, touched1 as b, buildReactivityGraph as c, registerObjectForDebug as d, enableDevTools as e, setObjectName as f, getState as g, deepWatch as h, isDevtoolsEnabled as i, addBatchCleanup as j, atomic as k, batch as l, mixin as m, biDi as n, defer as o, profileInfo as p, effect as q, registerEffectForDebug as r, setEffectName as s, touched as t, getActiveEffect as u, root as v, trackEffect as w, untracked as x, cleanedBy as y, cleanup as z };
4857
- //# sourceMappingURL=index-79Kk8D6e.esm.js.map
5006
+ export { cleanup as A, derived as B, unreactive as C, watch as D, mapped as E, reduced as F, memoize as G, immutables as H, IterableWeakMap as I, isNonReactive as J, registerNativeReactivity as K, getActiveProjection as L, project as M, isReactive as N, ReactiveBase as O, reactive as P, unwrap as Q, ReadOnlyError as R, organize as S, organized as T, Register as U, register as V, options as W, ReactiveError as X, isZoneEnabled as Y, setZoneEnabled as Z, IterableWeakSet as a, touched1 as b, buildReactivityGraph as c, registerObjectForDebug as d, enableDevTools as e, setObjectName as f, getState as g, deepWatch as h, isDevtoolsEnabled as i, addBatchCleanup as j, atomic as k, batch as l, mixin as m, biDi as n, defer as o, profileInfo as p, effect as q, registerEffectForDebug as r, setEffectName as s, touched as t, getActivationLog as u, getActiveEffect as v, root as w, trackEffect as x, untracked as y, cleanedBy as z };
5007
+ //# sourceMappingURL=index-qiWwozOc.esm.js.map