mutts 1.0.13 → 1.0.14

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 (54) hide show
  1. package/BROWSER_ASYNC_POLYFILL.md +79 -0
  2. package/README.md +2 -2
  3. package/dist/browser.cjs +145 -26
  4. package/dist/browser.cjs.map +1 -1
  5. package/dist/browser.d.ts +42 -9
  6. package/dist/browser.dev.cjs +12 -2
  7. package/dist/browser.dev.cjs.map +1 -1
  8. package/dist/browser.dev.d.ts +2 -2
  9. package/dist/browser.dev.esm.js +2 -2
  10. package/dist/browser.esm.js +137 -28
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{index-CAdnMJev.cjs → index-BnTNC9eC.cjs} +158 -90
  13. package/dist/chunks/index-BnTNC9eC.cjs.map +1 -0
  14. package/dist/chunks/{index-XsYTUhHx.esm.js → index-CAWVZL7P.esm.js} +156 -88
  15. package/dist/chunks/index-CAWVZL7P.esm.js.map +1 -0
  16. package/dist/chunks/node-Df_5r_WA.cjs +187 -0
  17. package/dist/chunks/node-Df_5r_WA.cjs.map +1 -0
  18. package/dist/chunks/node-DuIduHw3.esm.js +185 -0
  19. package/dist/chunks/node-DuIduHw3.esm.js.map +1 -0
  20. package/dist/chunks/{proxy-BtmPFjSr.esm.js → proxy-C2lnvvbx.esm.js} +652 -222
  21. package/dist/chunks/proxy-C2lnvvbx.esm.js.map +1 -0
  22. package/dist/chunks/{proxy-DBHj3kGK.cjs → proxy-HA_QQnd5.cjs} +662 -223
  23. package/dist/chunks/proxy-HA_QQnd5.cjs.map +1 -0
  24. package/dist/debug.cjs +37 -10
  25. package/dist/debug.cjs.map +1 -1
  26. package/dist/debug.esm.js +37 -10
  27. package/dist/debug.esm.js.map +1 -1
  28. package/dist/mutts.umd.js +4086 -3469
  29. package/dist/mutts.umd.js.map +1 -1
  30. package/dist/mutts.umd.min.js +1 -1
  31. package/dist/mutts.umd.min.js.map +1 -1
  32. package/dist/node.cjs +13 -3
  33. package/dist/node.cjs.map +1 -1
  34. package/dist/node.d.ts +2 -2
  35. package/dist/node.dev.cjs +13 -3
  36. package/dist/node.dev.cjs.map +1 -1
  37. package/dist/node.dev.d.ts +2 -2
  38. package/dist/node.dev.esm.js +3 -3
  39. package/dist/node.esm.js +3 -3
  40. package/dist/types.d.ts +30 -15
  41. package/docs/ai/api-reference.md +3 -1
  42. package/docs/ai/manual.md +17 -5
  43. package/docs/reactive/advanced.md +169 -10
  44. package/docs/reactive/debugging.md +15 -13
  45. package/docs/reactive.md +2 -1
  46. package/package.json +12 -7
  47. package/dist/chunks/index-CAdnMJev.cjs.map +0 -1
  48. package/dist/chunks/index-XsYTUhHx.esm.js.map +0 -1
  49. package/dist/chunks/node-DrrphEPf.cjs +0 -98
  50. package/dist/chunks/node-DrrphEPf.cjs.map +0 -1
  51. package/dist/chunks/node-NEZvVo4M.esm.js +0 -96
  52. package/dist/chunks/node-NEZvVo4M.esm.js.map +0 -1
  53. package/dist/chunks/proxy-BtmPFjSr.esm.js.map +0 -1
  54. package/dist/chunks/proxy-DBHj3kGK.cjs.map +0 -1
@@ -1,26 +1,3 @@
1
- // Queue for hooks registered before the environment is ready (circular dependency fix)
2
- const hooks = new Set();
3
- const asyncHooks = {
4
- addHook(hook) {
5
- hooks.add(hook);
6
- return () => hooks.delete(hook);
7
- },
8
- /**
9
- * [Hack] Sanitize a promise (or value) to prevent context leaks.
10
- * Default: Identity function.
11
- * Browser: Uses Macrotask wrapping to break microtask chains.
12
- */
13
- sanitizePromise(p) {
14
- return p;
15
- },
16
- };
17
- /**
18
- * Register a hook that will be called whenever an asynchronous operation is initiated.
19
- * The hook should return a restorer function which will be called just before the async callback runs.
20
- * That restorer should in turn return an undoer function which will be called just after the async callback finishes.
21
- */
22
- const asyncHook = (hook) => asyncHooks.addHook(hook);
23
-
24
1
  /**
25
2
  * Yields tuples containing elements from each input array, stopping at the longest array length
26
3
  * @param args - Arrays to zip together
@@ -782,7 +759,13 @@ class IterableWeakSet {
782
759
  [Symbol.iterator]() {
783
760
  return this.keys();
784
761
  }
785
- union(other) {
762
+ union(other, ...sets) {
763
+ if (sets.length > 0) {
764
+ for (const set of [other, ...sets])
765
+ for (const value of set)
766
+ this.add(value);
767
+ return this;
768
+ }
786
769
  const others = {
787
770
  [Symbol.iterator]() {
788
771
  return other.keys();
@@ -854,100 +837,6 @@ class IterableWeakSet {
854
837
  }
855
838
  _b = Symbol.toStringTag;
856
839
 
857
- /**
858
- * Creates a mixin that can be used both as a class (extends) and as a function (mixin)
859
- *
860
- * This function supports:
861
- * - Using mixins as base classes: `class MyClass extends MyMixin`
862
- * - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
863
- * - Composing mixins: `const Composed = MixinA(MixinB)`
864
- * - Type-safe property inference for all patterns
865
- *
866
- * @param mixinFunction - The function that creates the mixin
867
- * @param unwrapFunction - Optional function to unwrap reactive objects for method calls
868
- * @returns A mixin that can be used both as a class and as a function
869
- */
870
- function mixin(mixinFunction, unwrapFunction) {
871
- /**
872
- * Cache for mixin results to ensure the same base class always returns the same mixed class
873
- */
874
- const mixinCache = new WeakMap();
875
- // Apply the mixin to Object as the base class
876
- const MixedBase = mixinFunction(Object);
877
- mixinCache.set(Object, MixedBase);
878
- // Create the proxy that handles both constructor and function calls
879
- return new Proxy(MixedBase, {
880
- // Handle `MixinClass(SomeBase)` - use as mixin function
881
- apply(_target, _thisArg, args) {
882
- if (args.length === 0) {
883
- throw new Error('Mixin requires a base class');
884
- }
885
- const baseClass = args[0];
886
- if (typeof baseClass !== 'function') {
887
- throw new Error('Mixin requires a constructor function');
888
- }
889
- // Check if it's a valid constructor or a mixin
890
- if (!isConstructor(baseClass) &&
891
- !(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
892
- throw new Error('Mixin requires a valid constructor');
893
- }
894
- // Check cache first
895
- const cached = mixinCache.get(baseClass);
896
- if (cached) {
897
- return cached;
898
- }
899
- let usedBase = baseClass;
900
- if (unwrapFunction) {
901
- // Create a proxied base class that handles method unwrapping
902
- const ProxiedBaseClass = class extends baseClass {
903
- };
904
- // Proxy the prototype methods to handle unwrapping
905
- const originalPrototype = baseClass.prototype;
906
- const proxiedPrototype = new Proxy(originalPrototype, {
907
- get(target, prop, receiver) {
908
- const value = FoolProof.get(target, prop, receiver);
909
- // Only wrap methods that are likely to access private fields
910
- // Skip symbols and special properties that the reactive system needs
911
- if (typeof value === 'function' &&
912
- typeof prop === 'string' &&
913
- !['constructor', 'toString', 'valueOf'].includes(prop)) {
914
- // Return a wrapped version that uses unwrapped context
915
- return function (...args) {
916
- // Use the unwrapping function if provided, otherwise use this
917
- const context = unwrapFunction(this);
918
- return value.apply(context, args);
919
- };
920
- }
921
- return value;
922
- },
923
- });
924
- // Set the proxied prototype
925
- Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
926
- usedBase = ProxiedBaseClass;
927
- }
928
- // Create the mixed class using the proxied base class
929
- const mixedClass = mixinFunction(usedBase);
930
- // Cache the result
931
- mixinCache.set(baseClass, mixedClass);
932
- return mixedClass;
933
- },
934
- });
935
- }
936
-
937
- const debugHooks = {
938
- isDevtoolsEnabled: () => false,
939
- registerEffect: () => { },
940
- getTriggerChain: () => [],
941
- captureStack: () => [],
942
- captureLineage: () => new Error().stack,
943
- formatStack: (stack) => [stack],
944
- recordTriggerLink: () => { },
945
- decorateError: () => { },
946
- };
947
- function setDebugHooks(hooks) {
948
- Object.assign(debugHooks, hooks);
949
- }
950
-
951
840
  /******************************************************************************
952
841
  Copyright (c) Microsoft Corporation.
953
842
 
@@ -1010,6 +899,29 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
1010
899
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1011
900
  };
1012
901
 
902
+ // Queue for hooks registered before the environment is ready (circular dependency fix)
903
+ const hooks = new Set();
904
+ const asyncHooks = {
905
+ addHook(hook) {
906
+ hooks.add(hook);
907
+ return () => hooks.delete(hook);
908
+ },
909
+ /**
910
+ * [Hack] Sanitize a promise (or value) to prevent context leaks.
911
+ * Default: Identity function.
912
+ * Browser: Uses Macrotask wrapping to break microtask chains.
913
+ */
914
+ sanitizePromise(p) {
915
+ return p;
916
+ },
917
+ };
918
+ /**
919
+ * Register a hook that will be called whenever an asynchronous operation is initiated.
920
+ * The hook should return a restorer function which will be called just before the async callback runs.
921
+ * That restorer should in turn return an undoer function which will be called just after the async callback finishes.
922
+ */
923
+ const asyncHook = (hook) => asyncHooks.addHook(hook);
924
+
1013
925
  var _ZoneAggregator_zones;
1014
926
  function isu(z) {
1015
927
  return z;
@@ -1345,6 +1257,85 @@ function unlink(obj, reason) {
1345
1257
  }
1346
1258
  }
1347
1259
 
1260
+ function extractRawStack(error = new Error()) {
1261
+ if (typeof error === 'string')
1262
+ return error;
1263
+ if (error && typeof error === 'object' && 'stack' in error) {
1264
+ const stack = error.stack;
1265
+ return typeof stack === 'string' ? stack : undefined;
1266
+ }
1267
+ return undefined;
1268
+ }
1269
+ function trimStack(stack) {
1270
+ const raw = extractRawStack(stack);
1271
+ if (!raw)
1272
+ return [];
1273
+ const lines = raw
1274
+ .split('\n')
1275
+ .map((line) => line.trim())
1276
+ .filter(Boolean);
1277
+ if (lines[0]?.startsWith('Error'))
1278
+ lines.shift();
1279
+ while (lines[0] &&
1280
+ (lines[0].includes('captureLineage') ||
1281
+ lines[0].includes('captureDeferredLineage') ||
1282
+ lines[0].includes('debug-hooks.ts')))
1283
+ lines.shift();
1284
+ return lines;
1285
+ }
1286
+ function digestDeferredLineage(lineage) {
1287
+ if (lineage.segments)
1288
+ return lineage.segments;
1289
+ const segments = [];
1290
+ let effect = lineage.effect;
1291
+ let stack = trimStack(lineage.stack);
1292
+ if (!effect) {
1293
+ lineage.segments = [{ effectName: 'root', stack }];
1294
+ return lineage.segments;
1295
+ }
1296
+ while (effect) {
1297
+ const root = getRoot(effect);
1298
+ segments.push({
1299
+ effectName: root.name || 'anonymous',
1300
+ stack,
1301
+ });
1302
+ const node = getEffectNode(effect);
1303
+ effect = node.parent;
1304
+ stack = trimStack(node.creationStack);
1305
+ }
1306
+ if (stack.length)
1307
+ segments.push({ effectName: 'root', stack });
1308
+ lineage.segments = segments;
1309
+ return segments;
1310
+ }
1311
+ function formatDeferredLineage(lineage) {
1312
+ return digestDeferredLineage(lineage)
1313
+ .map((segment) => [`${segment.effectName}:`, ...segment.stack.map((line) => ` ${line}`)].join('\n'))
1314
+ .join('\n');
1315
+ }
1316
+ function captureDeferredLineage(effect = getActiveEffect(), stack = new Error()) {
1317
+ return {
1318
+ effect,
1319
+ stack,
1320
+ toString() {
1321
+ return formatDeferredLineage(this);
1322
+ },
1323
+ };
1324
+ }
1325
+ const debugHooks = {
1326
+ isDevtoolsEnabled: () => false,
1327
+ registerEffect: () => { },
1328
+ getTriggerChain: () => [],
1329
+ captureStack: (error) => extractRawStack(error ?? new Error()),
1330
+ captureLineage: captureDeferredLineage,
1331
+ formatStack: (stack) => [stack],
1332
+ recordTriggerLink: () => { },
1333
+ decorateError: () => { },
1334
+ };
1335
+ function setDebugHooks(hooks) {
1336
+ Object.assign(debugHooks, hooks);
1337
+ }
1338
+
1348
1339
  const effectMarker = {
1349
1340
  enter: 'effect:enter',
1350
1341
  leave: 'effect:leave',
@@ -1495,6 +1486,17 @@ class ReactiveError extends Error {
1495
1486
  return this.debugInfo?.cause;
1496
1487
  }
1497
1488
  }
1489
+ function normalizeSchedulerMode(mode) {
1490
+ switch (mode) {
1491
+ case 'production':
1492
+ return 'raw';
1493
+ case 'development':
1494
+ return 'ordered';
1495
+ default:
1496
+ return mode;
1497
+ }
1498
+ }
1499
+ let schedulerMode = 'ordered';
1498
1500
  // biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
1499
1501
  /**
1500
1502
  * Global options for the reactive system
@@ -1585,22 +1587,42 @@ const options = {
1585
1587
  */
1586
1588
  onMemoizationDiscrepancy: undefined,
1587
1589
  /**
1588
- * How to handle cycles detected in effect batches.
1590
+ * Effect scheduler mode.
1589
1591
  *
1590
- * - `'production'` (Default): High-performance mode. Disables dependency graph maintenance and
1591
- * Topological Sorting in favor of a simple FIFO queue. Use this for trustworthy, acyclic UI code.
1592
- * Cycle detection is heuristic (uses maxEffectChain execution counts).
1592
+ * - `'ordered'` (Default): maintains the causal effect graph so effects that are already
1593
+ * queued together can run in dependency order. It also preserves parent/child effect
1594
+ * lifecycle ordering and catches cycles eagerly when edges are created.
1593
1595
  *
1594
- * - `'development'`: Maintains direct dependency graph for early cycle detection during edge creation.
1595
- * Catches cycles before effects execute via DFS check when adding edges. Throws immediately with
1596
- * basic path information. Good balance of debugging help with moderate overhead.
1596
+ * - `'raw'`: fastest FIFO scheduler. It does not maintain the effect graph.
1597
+ * Cycle detection is heuristic, using maxEffectChain execution counts.
1597
1598
  *
1598
- * - `'debug'`: Full diagnostic mode with transitive closures and topological sorting.
1599
- * Provides detailed cycle path reporting. Highest overhead but most informative for bug hunting.
1599
+ * - `'debug'`: ordered scheduling plus the most detailed graph diagnostics. Highest overhead,
1600
+ * best for investigation.
1600
1601
  *
1601
- * @default 'production'
1602
+ * @default 'ordered'
1602
1603
  */
1603
- cycleHandling: 'development',
1604
+ get scheduler() {
1605
+ return schedulerMode;
1606
+ },
1607
+ set scheduler(mode) {
1608
+ schedulerMode = mode;
1609
+ },
1610
+ /**
1611
+ * @deprecated Use `scheduler` instead.
1612
+ *
1613
+ * Backward-compatible alias for older names:
1614
+ * - `'production'` maps to `scheduler = 'raw'`
1615
+ * - `'development'` maps to `scheduler = 'ordered'`
1616
+ * - `'debug'` maps to `scheduler = 'debug'`
1617
+ *
1618
+ * The new names describe scheduler behavior rather than runtime environment.
1619
+ */
1620
+ get cycleHandling() {
1621
+ return schedulerMode;
1622
+ },
1623
+ set cycleHandling(mode) {
1624
+ schedulerMode = normalizeSchedulerMode(mode);
1625
+ },
1604
1626
  /**
1605
1627
  * Internal flag used by memoization discrepancy detector to avoid counting calls in tests
1606
1628
  * @warning Do not modify this flag manually, this flag is given by the engine
@@ -1690,14 +1712,14 @@ function optionCall(name, ...args) {
1690
1712
  /** Production preset: no introspection, heuristic cycle detection, minimal overhead */
1691
1713
  const prodPreset = {
1692
1714
  maxEffectReaction: 'throw',
1693
- cycleHandling: 'production',
1715
+ scheduler: 'raw',
1694
1716
  introspection: null,
1695
1717
  onMemoizationDiscrepancy: undefined,
1696
1718
  };
1697
- /** Development preset (default): introspection on, early cycle detection, warnings */
1719
+ /** Development preset: introspection on, early cycle detection, warnings */
1698
1720
  const devPreset = {
1699
1721
  maxEffectReaction: 'warn',
1700
- cycleHandling: 'development',
1722
+ scheduler: 'ordered',
1701
1723
  introspection: {
1702
1724
  gatherReasons: { lineages: 'touch' },
1703
1725
  logErrors: true,
@@ -1709,7 +1731,7 @@ const devPreset = {
1709
1731
  /** Debug preset: full diagnostics, throws on violations, rich lineage capture */
1710
1732
  const debugPreset = {
1711
1733
  maxEffectReaction: 'debug',
1712
- cycleHandling: 'debug',
1734
+ scheduler: 'debug',
1713
1735
  introspection: {
1714
1736
  gatherReasons: { lineages: 'both' },
1715
1737
  logErrors: true,
@@ -1732,6 +1754,7 @@ function unwrap(obj) {
1732
1754
  return obj;
1733
1755
  return proxyToObject.get(obj) || obj;
1734
1756
  }
1757
+ const toRaw = unwrap;
1735
1758
  function isReactive(obj) {
1736
1759
  return proxyToObject.has(obj);
1737
1760
  }
@@ -1779,8 +1802,9 @@ function dependant(obj, prop = allProps) {
1779
1802
  if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps && prop !== keysOf))
1780
1803
  return;
1781
1804
  const node = getEffectNode(currentActiveEffect);
1782
- if ('dependencyHook' in node) {
1783
- node.dependencyHook?.(obj, prop);
1805
+ const hasDependencyHook = node.dependencyHook !== undefined;
1806
+ if (hasDependencyHook) {
1807
+ node.dependencyHook(obj, prop);
1784
1808
  }
1785
1809
  let objectWatchers = watchers.get(obj);
1786
1810
  if (!objectWatchers) {
@@ -1802,25 +1826,25 @@ function dependant(obj, prop = allProps) {
1802
1826
  effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
1803
1827
  }
1804
1828
  // Store dependency stack if introspection is enabled
1805
- const gatherReasons = options.introspection?.gatherReasons;
1806
- if (gatherReasons) {
1807
- const lineageConfig = gatherReasons.lineages;
1808
- if (lineageConfig === 'dependency' || lineageConfig === 'both') {
1809
- let objStacks = dependencyStacks.get(obj);
1810
- if (!objStacks) {
1811
- objStacks = new Map();
1812
- dependencyStacks.set(obj, objStacks);
1813
- }
1814
- let propStacks = objStacks.get(prop);
1815
- if (!propStacks) {
1816
- propStacks = new Map();
1817
- objStacks.set(prop, propStacks);
1818
- }
1819
- propStacks.set(currentActiveEffect, debugHooks.captureLineage());
1829
+ const lineageMode = options.introspection?.gatherReasons?.lineages;
1830
+ const shouldGatherDependencyLineage = lineageMode === 'dependency' || lineageMode === 'both';
1831
+ if (shouldGatherDependencyLineage) {
1832
+ let objStacks = dependencyStacks.get(obj);
1833
+ if (!objStacks) {
1834
+ objStacks = new Map();
1835
+ dependencyStacks.set(obj, objStacks);
1836
+ }
1837
+ let propStacks = objStacks.get(prop);
1838
+ if (!propStacks) {
1839
+ propStacks = new Map();
1840
+ objStacks.set(prop, propStacks);
1820
1841
  }
1842
+ propStacks.set(currentActiveEffect, debugHooks.captureLineage());
1821
1843
  }
1822
1844
  }
1823
1845
 
1846
+ // Simple module to manage inert state without circular dependencies
1847
+ let inertDepth = 0;
1824
1848
  /**
1825
1849
  * Finds a cycle in a sequence of functions by looking for the first repetition
1826
1850
  */
@@ -1922,6 +1946,40 @@ let causesClosure = new WeakMap();
1922
1946
  let consequencesClosure = new WeakMap();
1923
1947
  // Batch re-entrance depth and broken state
1924
1948
  let broken = false;
1949
+ /** True after an unrecoverable reactive failure until `reset()`. */
1950
+ function isReactiveBroken() {
1951
+ return broken;
1952
+ }
1953
+ const reactiveBrokenHandlers = new Set();
1954
+ const reactiveResetHandlers = new Set();
1955
+ function onReactiveBroken(handler) {
1956
+ reactiveBrokenHandlers.add(handler);
1957
+ return () => reactiveBrokenHandlers.delete(handler);
1958
+ }
1959
+ function onReactiveReset(handler) {
1960
+ reactiveResetHandlers.add(handler);
1961
+ return () => reactiveResetHandlers.delete(handler);
1962
+ }
1963
+ function notifyReactiveBroken(error) {
1964
+ for (const handler of Array.from(reactiveBrokenHandlers)) {
1965
+ try {
1966
+ handler(error);
1967
+ }
1968
+ catch (handlerError) {
1969
+ options.warn('[reactive] onReactiveBroken handler threw', handlerError);
1970
+ }
1971
+ }
1972
+ }
1973
+ function notifyReactiveReset() {
1974
+ for (const handler of Array.from(reactiveResetHandlers)) {
1975
+ try {
1976
+ handler();
1977
+ }
1978
+ catch (handlerError) {
1979
+ options.warn('[reactive] onReactiveReset handler threw', handlerError);
1980
+ }
1981
+ }
1982
+ }
1925
1983
  /**
1926
1984
  * Gets or creates an IterableWeakSet for a closure map
1927
1985
  */
@@ -1940,7 +1998,7 @@ function getOrCreateClosure(closure, root) {
1940
1998
  * @param targetRoot - Root function of the effect being triggered
1941
1999
  */
1942
2000
  function addGraphEdge(callerRoot, targetRoot) {
1943
- if (options.cycleHandling === 'production')
2001
+ if (options.scheduler === 'raw')
1944
2002
  return;
1945
2003
  // Add to forward graph: callerRoot → targetRoot
1946
2004
  const triggers = effectTriggers.get(callerRoot);
@@ -2055,7 +2113,7 @@ function hasPathExcluding(start, end, exclude) {
2055
2113
  * @param effect - The effect being cleaned up
2056
2114
  */
2057
2115
  function cleanupEffectFromGraph(effect) {
2058
- if (options.cycleHandling === 'production')
2116
+ if (options.scheduler === 'raw')
2059
2117
  return;
2060
2118
  const root = getRoot(effect);
2061
2119
  // Get closures before removing direct edges (needed for propagation)
@@ -2164,7 +2222,7 @@ const executingStack = [];
2164
2222
  * Called once when batch starts or when new effects are added
2165
2223
  */
2166
2224
  function computeAllInDegrees(batch) {
2167
- if (options.cycleHandling === 'production')
2225
+ if (options.scheduler === 'raw')
2168
2226
  return;
2169
2227
  const activeEffect = getActiveEffect();
2170
2228
  const activeRoot = activeEffect ? getRoot(activeEffect) : null;
@@ -2172,6 +2230,16 @@ function computeAllInDegrees(batch) {
2172
2230
  batch.inDegrees.clear();
2173
2231
  for (const [root] of batch.all) {
2174
2232
  let inDegree = 0;
2233
+ // Make sure parents are executed before children: if parent is in batch, count it as a dependency
2234
+ const effect = batch.all.get(root);
2235
+ const parent = getEffectNode(effect).parent;
2236
+ const parentRoot = parent ? getRoot(parent) : undefined;
2237
+ if (parentRoot &&
2238
+ batch.all.has(parentRoot) &&
2239
+ parentRoot !== activeRoot &&
2240
+ parentRoot !== root) {
2241
+ inDegree++;
2242
+ }
2175
2243
  const causes = causesClosure.get(root);
2176
2244
  if (causes) {
2177
2245
  for (const causeRoot of causes) {
@@ -2191,17 +2259,26 @@ function computeAllInDegrees(batch) {
2191
2259
  function decrementInDegreesForExecuted(batch, executedRoot) {
2192
2260
  // Get all effects that this executed effect triggers
2193
2261
  const consequences = consequencesClosure.get(executedRoot);
2194
- if (!consequences)
2195
- return;
2196
- for (const consequenceRoot of consequences) {
2197
- // Only update if it's still in the batch
2198
- if (batch.all.has(consequenceRoot)) {
2199
- const currentDegree = batch.inDegrees.get(consequenceRoot) ?? 0;
2200
- if (currentDegree > 0) {
2201
- batch.inDegrees.set(consequenceRoot, currentDegree - 1);
2262
+ if (consequences) {
2263
+ for (const consequenceRoot of consequences) {
2264
+ // Only update if it's still in the batch
2265
+ if (batch.all.has(consequenceRoot)) {
2266
+ const currentDegree = batch.inDegrees.get(consequenceRoot) ?? 0;
2267
+ if (currentDegree > 0) {
2268
+ batch.inDegrees.set(consequenceRoot, currentDegree - 1);
2269
+ }
2202
2270
  }
2203
2271
  }
2204
2272
  }
2273
+ for (const [root, effect] of batch.all) {
2274
+ const parent = getEffectNode(effect).parent;
2275
+ if (!parent || getRoot(parent) !== executedRoot)
2276
+ continue;
2277
+ const currentDegree = batch.inDegrees.get(root) ?? 0;
2278
+ if (currentDegree > 0) {
2279
+ batch.inDegrees.set(root, currentDegree - 1);
2280
+ }
2281
+ }
2205
2282
  }
2206
2283
  /**
2207
2284
  * Finds a path from startRoot to endRoot in the dependency graph
@@ -2340,15 +2417,15 @@ function addToBatch(effect, caller, immediate, reason) {
2340
2417
  }
2341
2418
  }
2342
2419
  // 1. Add to batch first (needed for cycle detection)
2343
- // TODO: Check if it's the correct way to do (these different behavior in function of dev/production)
2344
- if (options.cycleHandling === 'production') {
2345
- // Production mode: FIFO (delete and re-add to move to end)
2420
+ // TODO: Check if this difference between raw and graph-backed scheduling is the right tradeoff.
2421
+ if (options.scheduler === 'raw') {
2422
+ // Raw mode: FIFO (delete and re-add to move to end)
2346
2423
  if (currentBatch.all.has(root)) {
2347
2424
  currentBatch.all.delete(root);
2348
2425
  }
2349
2426
  }
2350
2427
  else {
2351
- // Dev mode: skip if already queued — the existing entry will re-run
2428
+ // Graph-backed modes: skip if already queued — the existing entry will re-run
2352
2429
  if (currentBatch.all.has(root)) {
2353
2430
  return;
2354
2431
  }
@@ -2357,7 +2434,7 @@ function addToBatch(effect, caller, immediate, reason) {
2357
2434
  if (node.stopped)
2358
2435
  return;
2359
2436
  currentBatch.all.set(root, effect);
2360
- if (caller && true && options.cycleHandling !== 'production') {
2437
+ if (caller && true && options.scheduler !== 'raw') {
2361
2438
  const callerRoot = getRoot(caller);
2362
2439
  // const root = getRoot(effect) // Already have root
2363
2440
  // Check for cycle BEFORE adding edge
@@ -2379,6 +2456,9 @@ function addToBatch(effect, caller, immediate, reason) {
2379
2456
  }
2380
2457
  addGraphEdge(callerRoot, root);
2381
2458
  }
2459
+ if (options.scheduler !== 'raw') {
2460
+ computeAllInDegrees(currentBatch);
2461
+ }
2382
2462
  }
2383
2463
  /**
2384
2464
  * Adds a cleanup function to be called when the current batch of effects completes
@@ -2475,7 +2555,7 @@ function executeNext(effectuatedRoots) {
2475
2555
  // Find an effect with in-degree 0 using cached values
2476
2556
  let nextEffect = null;
2477
2557
  let nextRoot = null;
2478
- if (options.cycleHandling === 'production') {
2558
+ if (options.scheduler === 'raw') {
2479
2559
  // In flat mode, we just take the first effect in the queue (FIFO)
2480
2560
  const first = currentBatch.all.entries().next().value;
2481
2561
  if (first) {
@@ -2565,9 +2645,7 @@ function executeNext(effectuatedRoots) {
2565
2645
  }
2566
2646
  return result;
2567
2647
  }
2568
- // Track which sub-effects have been executed to prevent infinite loops
2569
- // These are all the effects triggered under `activeEffect` and all their sub-effects
2570
- function batch(effect, immediate, caller) {
2648
+ function batch(effect, batchOptions) {
2571
2649
  if (broken) {
2572
2650
  throw new ReactiveError('[reactive] Reactive system is broken after an unrecoverable error. Call reset() to recover.', { code: ReactiveErrorCode.BrokenEffects });
2573
2651
  }
@@ -2582,16 +2660,38 @@ function batch(effect, immediate, caller) {
2582
2660
  throw new Error('Activation registry already exists');
2583
2661
  optionCall('beginChain', roots);
2584
2662
  }
2585
- // TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
2586
- const activeCaller = getActiveEffect();
2587
- const callerToUse = caller || activeCaller;
2663
+ const immediate = batchOptions?.immediate === true;
2664
+ const contained = batchOptions?.contained === true;
2665
+ const callerToUse = batchOptions?.caller || getActiveEffect();
2588
2666
  // Optimization: If nested and NOT immediate, just join the existing batch
2589
- if (!isNewBatch && !immediate) {
2667
+ if (!isNewBatch && !contained && !immediate) {
2590
2668
  for (let i = 0; i < effect.length; i++) {
2591
2669
  addToBatch(effect[i], callerToUse);
2592
2670
  }
2593
2671
  return;
2594
2672
  }
2673
+ if (!isNewBatch && !contained && immediate) {
2674
+ const firstReturn = {};
2675
+ for (let i = 0; i < effect.length; i++) {
2676
+ executingStack.push(effect[i]);
2677
+ try {
2678
+ const node = getEffectNode(effect[i]);
2679
+ const reason = node.nextReason;
2680
+ if (node.cleanup) {
2681
+ const cleanup = node.cleanup;
2682
+ node.cleanup = undefined;
2683
+ cleanup(reason);
2684
+ }
2685
+ const rv = effect[i]();
2686
+ if (rv !== undefined && !('value' in firstReturn))
2687
+ firstReturn.value = rv;
2688
+ }
2689
+ finally {
2690
+ executingStack.pop();
2691
+ }
2692
+ }
2693
+ return firstReturn.value;
2694
+ }
2595
2695
  const currentBatch = {
2596
2696
  all: new Map(),
2597
2697
  inDegrees: new Map(),
@@ -2599,9 +2699,11 @@ function batch(effect, immediate, caller) {
2599
2699
  };
2600
2700
  batchStack.push(currentBatch);
2601
2701
  let success = false;
2702
+ let failure;
2602
2703
  try {
2603
2704
  const effectuatedRoots = [];
2604
2705
  const firstReturn = {};
2706
+ let initialError;
2605
2707
  if (immediate) {
2606
2708
  // Execute initial effects in providing order
2607
2709
  for (let i = 0; i < effect.length; i++) {
@@ -2618,11 +2720,17 @@ function batch(effect, immediate, caller) {
2618
2720
  if (rv !== undefined && !('value' in firstReturn))
2619
2721
  firstReturn.value = rv;
2620
2722
  }
2723
+ catch (error) {
2724
+ initialError = error;
2725
+ break;
2726
+ }
2621
2727
  finally {
2622
2728
  executingStack.pop();
2623
2729
  currentBatch.all.delete(getRoot(effect[i]));
2624
2730
  }
2625
2731
  }
2732
+ if (initialError)
2733
+ throw initialError;
2626
2734
  }
2627
2735
  else {
2628
2736
  // Add initial effects to batch and compute dependencies
@@ -2682,13 +2790,17 @@ function batch(effect, immediate, caller) {
2682
2790
  return firstReturn.value;
2683
2791
  }
2684
2792
  catch (error) {
2793
+ failure = error;
2685
2794
  if (batchStack.length === 1)
2686
2795
  optionCall('error', '[reactive] Root batch failure before broken state:', error);
2687
2796
  throw error;
2688
2797
  }
2689
2798
  finally {
2690
2799
  if (!success && batchStack.length === 1) {
2800
+ const wasBroken = broken;
2691
2801
  broken = true;
2802
+ if (!wasBroken)
2803
+ notifyReactiveBroken(failure);
2692
2804
  }
2693
2805
  batchStack.pop();
2694
2806
  if (batchStack.length === 0) {
@@ -2704,6 +2816,7 @@ function batch(effect, immediate, caller) {
2704
2816
  * All existing effects become orphaned and must be recreated.
2705
2817
  */
2706
2818
  function reset() {
2819
+ const wasBroken = broken;
2707
2820
  broken = false;
2708
2821
  activationRegistry = undefined;
2709
2822
  batchStack.length = 0;
@@ -2714,6 +2827,8 @@ function reset() {
2714
2827
  resetRegistry();
2715
2828
  resetTracking();
2716
2829
  effectHistory.present.active = undefined;
2830
+ if (wasBroken)
2831
+ notifyReactiveReset();
2717
2832
  }
2718
2833
  // Inject batch function to allow atomic game loops in requestAnimationFrame/setTimeout/...
2719
2834
  // Note: Automatic batching of async callbacks (setTimeout, Promise.then, etc.) is NOT implemented.
@@ -2730,7 +2845,7 @@ const atomic = decorator({
2730
2845
  const atomicEffect = () => original.apply(this, args);
2731
2846
  // Debug: helpful to have a name
2732
2847
  Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
2733
- return batch(atomicEffect, 'immediate');
2848
+ return batch(atomicEffect, { immediate: true });
2734
2849
  };
2735
2850
  },
2736
2851
  default(original) {
@@ -2738,7 +2853,7 @@ const atomic = decorator({
2738
2853
  const atomicEffect = () => original.apply(this, args);
2739
2854
  // Debug: helpful to have a name
2740
2855
  Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
2741
- return batch(atomicEffect, 'immediate');
2856
+ return batch(atomicEffect, { immediate: true });
2742
2857
  };
2743
2858
  },
2744
2859
  });
@@ -2777,7 +2892,7 @@ function captured(prev, fn) {
2777
2892
  * ```
2778
2893
  */
2779
2894
  function atom(fn) {
2780
- return batch(fn, 'immediate');
2895
+ return batch(fn, { immediate: true });
2781
2896
  }
2782
2897
  const fr = new FinalizationRegistry((f) => f());
2783
2898
  /**
@@ -3021,7 +3136,7 @@ const effect = captioned(named(effectMarker.leave, flavored(function effect(fn,
3021
3136
  abortController = undefined;
3022
3137
  }
3023
3138
  };
3024
- batch(runEffect, 'immediate');
3139
+ batch(runEffect, { immediate: true });
3025
3140
  // Only ROOT effects are registered for GC cleanup and zone tracking
3026
3141
  const isRootEffect = !parent;
3027
3142
  const stopEffect = (reason) => {
@@ -3091,16 +3206,53 @@ const untracked = captioned(function untracked(fn) {
3091
3206
  ? externalReason.with(external, () => effectHistory.present.root(fn))
3092
3207
  : effectHistory.present.root(fn);
3093
3208
  });
3209
+ function runInert(fn) {
3210
+ // Increment the counter
3211
+ const originalDepth = inertDepth;
3212
+ inertDepth = originalDepth + 1;
3213
+ try {
3214
+ return fn();
3215
+ }
3216
+ finally {
3217
+ inertDepth = originalDepth;
3218
+ }
3219
+ }
3220
+ function wrapInert(fn) {
3221
+ function inertEffect(...args) {
3222
+ return runInert(() => fn.apply(this, args));
3223
+ }
3224
+ Object.defineProperty(inertEffect, 'name', { value: `inert(${fn.name})` });
3225
+ return inertEffect;
3226
+ }
3227
+ /**
3228
+ * Executes a function with fast-path reads that bypass proxy overhead and dependency tracking.
3229
+ * Writes remain fully reactive. Uses a counter for safe nesting.
3230
+ * Can also decorate methods so the whole method body runs inertly.
3231
+ * @param fn - The function to execute
3232
+ */
3233
+ const inert = decorator({
3234
+ method(original) {
3235
+ return wrapInert(original);
3236
+ },
3237
+ default(fn) {
3238
+ if (typeof fn !== 'function')
3239
+ throw new Error('inert() expects a function');
3240
+ return runInert(fn);
3241
+ },
3242
+ });
3094
3243
  /**
3095
3244
  * Executes a function from a virgin/root context - no parent effect, no tracking
3096
3245
  * Creates completely independent effects that won't be cleaned up by any parent
3097
3246
  * @param fn - The function to execute
3098
3247
  */
3099
3248
  const root = captioned(function root(fn) {
3249
+ // When broken, `atomic`/`batch` throws immediately. DOM wrappers (e.g. Sursaut
3250
+ // `root\`event:…\``) still need to run listener code for inspection UI; skip batching.
3251
+ const runner = broken ? fn : atomic(fn);
3100
3252
  const external = externalReasonFrom(fn);
3101
3253
  return external
3102
- ? externalReason.with(external, () => effectHistory.root(fn))
3103
- : effectHistory.root(fn);
3254
+ ? externalReason.with(external, () => effectHistory.root(runner))
3255
+ : effectHistory.root(runner);
3104
3256
  });
3105
3257
  function biDi(received, get, set) {
3106
3258
  if (typeof get !== 'function') {
@@ -3123,6 +3275,86 @@ function biDi(received, get, set) {
3123
3275
  : () => { };
3124
3276
  }
3125
3277
 
3278
+ /**
3279
+ * Creates a mixin that can be used both as a class (extends) and as a function (mixin)
3280
+ *
3281
+ * This function supports:
3282
+ * - Using mixins as base classes: `class MyClass extends MyMixin`
3283
+ * - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
3284
+ * - Composing mixins: `const Composed = MixinA(MixinB)`
3285
+ * - Type-safe property inference for all patterns
3286
+ *
3287
+ * @param mixinFunction - The function that creates the mixin
3288
+ * @param unwrapFunction - Optional function to unwrap reactive objects for method calls
3289
+ * @returns A mixin that can be used both as a class and as a function
3290
+ */
3291
+ function mixin(mixinFunction, unwrapFunction) {
3292
+ /**
3293
+ * Cache for mixin results to ensure the same base class always returns the same mixed class
3294
+ */
3295
+ const mixinCache = new WeakMap();
3296
+ // Apply the mixin to Object as the base class
3297
+ const MixedBase = mixinFunction(Object);
3298
+ mixinCache.set(Object, MixedBase);
3299
+ // Create the proxy that handles both constructor and function calls
3300
+ return new Proxy(MixedBase, {
3301
+ // Handle `MixinClass(SomeBase)` - use as mixin function
3302
+ apply(_target, _thisArg, args) {
3303
+ if (args.length === 0) {
3304
+ throw new Error('Mixin requires a base class');
3305
+ }
3306
+ const baseClass = args[0];
3307
+ if (typeof baseClass !== 'function') {
3308
+ throw new Error('Mixin requires a constructor function');
3309
+ }
3310
+ // Check if it's a valid constructor or a mixin
3311
+ if (!isConstructor(baseClass) &&
3312
+ !(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
3313
+ throw new Error('Mixin requires a valid constructor');
3314
+ }
3315
+ // Check cache first
3316
+ const cached = mixinCache.get(baseClass);
3317
+ if (cached) {
3318
+ return cached;
3319
+ }
3320
+ let usedBase = baseClass;
3321
+ if (unwrapFunction) {
3322
+ // Create a proxied base class that handles method unwrapping
3323
+ const ProxiedBaseClass = class extends baseClass {
3324
+ };
3325
+ // Proxy the prototype methods to handle unwrapping
3326
+ const originalPrototype = baseClass.prototype;
3327
+ const proxiedPrototype = new Proxy(originalPrototype, {
3328
+ get(target, prop, receiver) {
3329
+ const value = FoolProof.get(target, prop, receiver);
3330
+ // Only wrap methods that are likely to access private fields
3331
+ // Skip symbols and special properties that the reactive system needs
3332
+ if (typeof value === 'function' &&
3333
+ typeof prop === 'string' &&
3334
+ !['constructor', 'toString', 'valueOf'].includes(prop)) {
3335
+ // Return a wrapped version that uses unwrapped context
3336
+ return function (...args) {
3337
+ // Use the unwrapping function if provided, otherwise use this
3338
+ const context = unwrapFunction(this);
3339
+ return value.apply(context, args);
3340
+ };
3341
+ }
3342
+ return value;
3343
+ },
3344
+ });
3345
+ // Set the proxied prototype
3346
+ Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
3347
+ usedBase = ProxiedBaseClass;
3348
+ }
3349
+ // Create the mixed class using the proxied base class
3350
+ const mixedClass = mixinFunction(usedBase);
3351
+ // Cache the result
3352
+ mixinCache.set(baseClass, mixedClass);
3353
+ return mixedClass;
3354
+ },
3355
+ });
3356
+ }
3357
+
3126
3358
  // Track which objects contain which other objects (back-references)
3127
3359
  const objectParents = new WeakMap();
3128
3360
  // Track which objects have deep watchers
@@ -3252,7 +3484,7 @@ function getState(obj) {
3252
3484
  }
3253
3485
  return state;
3254
3486
  }
3255
- function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
3487
+ function collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, ...keyChains) {
3256
3488
  const sourceEffect = getActiveEffect();
3257
3489
  for (const keys of keyChains)
3258
3490
  for (const key of keys) {
@@ -3268,7 +3500,7 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
3268
3500
  continue;
3269
3501
  }
3270
3502
  if (!effects.has(effect)) {
3271
- effects.set(effect, getDependencyStack(effect, obj, key));
3503
+ effects.set(effect, collectDependencyStack ? getDependencyStack(effect, obj, key) : undefined);
3272
3504
  if (!hasBatched(effect))
3273
3505
  recordActivation(effect, obj, evolution, key);
3274
3506
  }
@@ -3301,17 +3533,18 @@ function touched(obj, evolution, props) {
3301
3533
  const effects = new Map();
3302
3534
  const structural = !['set', 'invalidate'].includes(evolution.type);
3303
3535
  const broad = structural ? [allProps, keysOf] : [allProps];
3536
+ const gatherReasons = options.introspection?.gatherReasons;
3537
+ const lineageConfig = gatherReasons?.lineages;
3538
+ const collectDependencyStack = lineageConfig === 'dependency' || lineageConfig === 'both';
3304
3539
  if (props)
3305
- collectEffects(obj, evolution, effects, objectWatchers, broad, props);
3540
+ collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, broad, props);
3306
3541
  else
3307
- collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
3542
+ collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, objectWatchers.keys());
3308
3543
  const triggers = Array.from(effects.keys());
3309
3544
  const sourceEffect = getActiveEffect();
3310
3545
  optionCall('touched', obj, evolution, props, triggers);
3311
3546
  // Store pending triggers for CleanupReason before batching
3312
- if (options.introspection?.gatherReasons) {
3313
- const gatherReasons = options.introspection.gatherReasons;
3314
- const lineageConfig = gatherReasons.lineages;
3547
+ if (gatherReasons && effects.size > 0) {
3315
3548
  let touchLineage;
3316
3549
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3317
3550
  touchLineage = debugHooks.captureLineage();
@@ -3328,7 +3561,7 @@ function touched(obj, evolution, props) {
3328
3561
  });
3329
3562
  }
3330
3563
  }
3331
- batch(triggers, undefined, sourceEffect);
3564
+ batch(triggers, { caller: sourceEffect });
3332
3565
  }
3333
3566
  // Bubble up changes if this object has deep watchers
3334
3567
  if (objectsWithDeepWatchers.has(obj)) {
@@ -3352,6 +3585,7 @@ function touchedOpaque(obj, evolution, prop) {
3352
3585
  const gather = options.introspection?.gatherReasons;
3353
3586
  if (gather) {
3354
3587
  const lineageConfig = gather.lineages;
3588
+ let touchLineage;
3355
3589
  for (const effect of deps) {
3356
3590
  const node = getEffectNode(effect);
3357
3591
  if (!node.isOpaque)
@@ -3363,10 +3597,9 @@ function touchedOpaque(obj, evolution, prop) {
3363
3597
  }
3364
3598
  effects.add(effect);
3365
3599
  if (gather) {
3366
- let touchLineage;
3367
3600
  let dependencyStack;
3368
3601
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3369
- touchLineage = debugHooks.captureLineage();
3602
+ touchLineage ?? (touchLineage = debugHooks.captureLineage());
3370
3603
  }
3371
3604
  if (lineageConfig === 'dependency' || lineageConfig === 'both') {
3372
3605
  dependencyStack = getDependencyStack(effect, obj, prop);
@@ -3402,7 +3635,7 @@ function touchedOpaque(obj, evolution, prop) {
3402
3635
  }
3403
3636
  if (effects.size > 0) {
3404
3637
  optionCall('touched', obj, evolution, [prop], Array.from(effects));
3405
- batch(Array.from(effects), undefined, sourceEffect);
3638
+ batch(Array.from(effects), { caller: sourceEffect });
3406
3639
  }
3407
3640
  }
3408
3641
 
@@ -3449,6 +3682,8 @@ function nonReactive(...obj) {
3449
3682
  }
3450
3683
  return obj[0];
3451
3684
  }
3685
+ const markRaw = nonReactive;
3686
+ const markRawProps = addUnreactiveProps;
3452
3687
  function nonReactiveClass(...cls) {
3453
3688
  for (const c of cls)
3454
3689
  if (c)
@@ -3666,7 +3901,7 @@ function dispatchNotifications(notifications) {
3666
3901
  const originWatchers = watchers.get(origin.obj);
3667
3902
  if (originWatchers) {
3668
3903
  const originEffects = new Map();
3669
- collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
3904
+ collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, false, [allProps], [origin.prop]);
3670
3905
  allowedEffects = new Set(originEffects.keys());
3671
3906
  }
3672
3907
  // If no allowed effects, skip all notifications (no one should be notified)
@@ -3685,7 +3920,7 @@ function dispatchNotifications(notifications) {
3685
3920
  if (objectWatchers) {
3686
3921
  currentEffects = new Map();
3687
3922
  const broad = evolution.type !== 'set' ? [allProps, keysOf] : [allProps];
3688
- collectEffects(obj, evolution, currentEffects, objectWatchers, broad, propsArray);
3923
+ collectEffects(obj, evolution, currentEffects, objectWatchers, false, broad, propsArray);
3689
3924
  // Filter effects by ancestor chain if origin exists
3690
3925
  // Include effects that either directly depend on origin or have an ancestor that does
3691
3926
  if (origin && allowedEffects) {
@@ -3747,12 +3982,34 @@ const metaProtos = new WeakMap();
3747
3982
  const wrapProtos = new WeakMap();
3748
3983
  const arrayLengths = new WeakMap();
3749
3984
  const hasReentry = new Set();
3985
+ const accessAnalysisCache = new WeakMap();
3986
+ const readonlyObjectToProxy = new WeakMap();
3987
+ const shallowObjectToProxy = new WeakMap();
3988
+ const readonlyMutators = new Set([
3989
+ 'copyWithin',
3990
+ 'fill',
3991
+ 'pop',
3992
+ 'push',
3993
+ 'reverse',
3994
+ 'shift',
3995
+ 'sort',
3996
+ 'splice',
3997
+ 'unshift',
3998
+ 'add',
3999
+ 'clear',
4000
+ 'delete',
4001
+ 'set',
4002
+ ]);
3750
4003
  // Sub-proxy registration for custom reactive behaviors
3751
4004
  const subsRegister = new WeakMap();
3752
4005
  // Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
3753
4006
  // TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
3754
4007
  let internalUntracked = false;
3755
4008
  function wrapReactiveValue(obj, prop, value) {
4009
+ // Optional fast-path for inert reads - skips reactive wrapping
4010
+ // Disabled by default for safety, can be enabled for performance-critical read-only contexts
4011
+ if (inertDepth > 0)
4012
+ return value;
3756
4013
  if (!isReactive(value) && typeof value === 'object' && value !== null) {
3757
4014
  const reactiveValue = reactiveObject(value);
3758
4015
  // Only create back-references if this object needs them
@@ -3763,6 +4020,56 @@ function wrapReactiveValue(obj, prop, value) {
3763
4020
  }
3764
4021
  return value;
3765
4022
  }
4023
+ function computeAccessAnalysis(obj, prop, receiver) {
4024
+ const proto = Object.getPrototypeOf(obj);
4025
+ const isOwnProp = Object.hasOwn(obj, prop);
4026
+ const shouldIgnoreAccessor = options.ignoreAccessors &&
4027
+ isOwnProp &&
4028
+ proto !== null &&
4029
+ (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
4030
+ let hasProp = isOwnProp;
4031
+ let owner = isOwnProp ? obj : undefined;
4032
+ if (!isOwnProp) {
4033
+ let raw = proto;
4034
+ while (raw && raw !== Object.prototype) {
4035
+ if (Object.hasOwn(raw, prop)) {
4036
+ hasProp = true;
4037
+ owner = raw;
4038
+ break;
4039
+ }
4040
+ raw = Object.getPrototypeOf(raw);
4041
+ }
4042
+ }
4043
+ return {
4044
+ hasProp,
4045
+ owner,
4046
+ isInheritedAccess: hasProp && !isOwnProp,
4047
+ shouldIgnoreAccessor,
4048
+ ignoreAccessors: options.ignoreAccessors,
4049
+ instanceMembers: options.instanceMembers,
4050
+ };
4051
+ }
4052
+ function analyzeAccess(obj, prop, receiver) {
4053
+ const proto = Object.getPrototypeOf(obj);
4054
+ if (Object.hasOwn(obj, prop))
4055
+ return computeAccessAnalysis(obj, prop, receiver);
4056
+ if (proto === null || Array.isArray(obj))
4057
+ return computeAccessAnalysis(obj, prop, receiver);
4058
+ let propCache = accessAnalysisCache.get(proto);
4059
+ if (!propCache) {
4060
+ propCache = new Map();
4061
+ accessAnalysisCache.set(proto, propCache);
4062
+ }
4063
+ const cached = propCache.get(prop);
4064
+ if (cached &&
4065
+ cached.ignoreAccessors === options.ignoreAccessors &&
4066
+ cached.instanceMembers === options.instanceMembers)
4067
+ return cached;
4068
+ const analysis = computeAccessAnalysis(obj, prop, receiver);
4069
+ if (analysis.hasProp)
4070
+ propCache.set(prop, analysis);
4071
+ return analysis;
4072
+ }
3766
4073
  const reactiveHandlers = {
3767
4074
  [Symbol.toStringTag]: 'MutTs Reactive',
3768
4075
  get(obj, prop, receiver) {
@@ -3790,36 +4097,29 @@ const reactiveHandlers = {
3790
4097
  // Symbols: fast-path — no reactivity tracking
3791
4098
  if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
3792
4099
  return FoolProof.get(obj, prop, receiver);
3793
- if (!getActiveEffect()) {
3794
- const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
4100
+ const subProxy = subsRegister.get(obj);
4101
+ if (inertDepth > 0) {
4102
+ const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
3795
4103
  return wrapReactiveValue(obj, prop, value);
3796
4104
  }
3797
- // Check if property exists using a trap-free walk to avoid triggering
3798
- // the has-trap cascade on prototype chains of reactive proxies.
3799
- const isOwnProp = Object.hasOwn(obj, prop);
3800
- // For accessor properties, check the unwrapped object to see if it's an accessor
3801
- // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
3802
- // Skip for null-proto objects (pounce scopes) — they never have accessors
3803
- const shouldIgnoreAccessor = options.ignoreAccessors &&
3804
- isOwnProp &&
3805
- Object.getPrototypeOf(obj) !== null &&
3806
- (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
3807
- // Check if property exists using a trap-free walk to avoid triggering
3808
- // the has-trap cascade on prototype chains of reactive proxies.
3809
- let hasProp = isOwnProp;
3810
- let owner = isOwnProp ? obj : undefined;
3811
- if (!isOwnProp) {
3812
- let raw = Object.getPrototypeOf(obj);
3813
- while (raw && raw !== Object.prototype) {
3814
- if (Object.hasOwn(raw, prop)) {
3815
- hasProp = true;
3816
- owner = raw;
3817
- break;
4105
+ const activeEffect = getActiveEffect();
4106
+ if (!activeEffect) {
4107
+ const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
4108
+ return wrapReactiveValue(obj, prop, value);
4109
+ }
4110
+ if (!subProxy && !Array.isArray(obj)) {
4111
+ const proto = Object.getPrototypeOf(obj);
4112
+ if (proto === Object.prototype || proto === null) {
4113
+ const ownDesc = Object.getOwnPropertyDescriptor(obj, prop);
4114
+ if (ownDesc && 'value' in ownDesc) {
4115
+ dependant(obj, prop);
4116
+ return wrapReactiveValue(obj, prop, ownDesc.value);
3818
4117
  }
3819
- raw = Object.getPrototypeOf(raw);
3820
4118
  }
3821
4119
  }
3822
- const isInheritedAccess = hasProp && !isOwnProp;
4120
+ // Check if property exists using a trap-free walk to avoid triggering
4121
+ // the has-trap cascade on prototype chains of reactive proxies.
4122
+ const { hasProp, owner, isInheritedAccess, shouldIgnoreAccessor } = analyzeAccess(obj, prop, receiver);
3823
4123
  // Depend if...
3824
4124
  if (!hasProp ||
3825
4125
  (!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
@@ -3832,7 +4132,7 @@ const reactiveHandlers = {
3832
4132
  }
3833
4133
  // For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
3834
4134
  // For all other objects, inline Reflect.get directly (skips 3 function calls).
3835
- const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
4135
+ const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
3836
4136
  return wrapReactiveValue(obj, prop, value);
3837
4137
  },
3838
4138
  set(obj, prop, value, receiver) {
@@ -3906,6 +4206,11 @@ const reactiveHandlers = {
3906
4206
  cycle: [], // We don't have the full cycle here, but we know it involves obj
3907
4207
  });
3908
4208
  hasReentry.add(obj);
4209
+ if (inertDepth > 0) {
4210
+ const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
4211
+ hasReentry.delete(obj);
4212
+ return rv;
4213
+ }
3909
4214
  if (!internalUntracked && !isUnreactiveProp(obj, prop))
3910
4215
  dependant(obj, prop);
3911
4216
  const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
@@ -3914,7 +4219,7 @@ const reactiveHandlers = {
3914
4219
  },
3915
4220
  deleteProperty(obj, prop) {
3916
4221
  if (!Object.hasOwn(obj, prop))
3917
- return false;
4222
+ return true;
3918
4223
  const oldVal = obj[prop];
3919
4224
  // Remove back-references if this object has deep watchers
3920
4225
  if (objectsWithDeepWatchers.has(obj) && typeof oldVal === 'object' && oldVal !== null) {
@@ -3929,6 +4234,9 @@ const reactiveHandlers = {
3929
4234
  return true;
3930
4235
  },
3931
4236
  ownKeys(obj) {
4237
+ if (inertDepth > 0) {
4238
+ return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
4239
+ }
3932
4240
  dependant(obj, keysOf);
3933
4241
  return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
3934
4242
  },
@@ -3937,6 +4245,98 @@ const reactiveHandlers = {
3937
4245
  Reflect.getOwnPropertyDescriptor(obj, prop));
3938
4246
  },
3939
4247
  };
4248
+ function readonlyError(prop) {
4249
+ return new ReactiveError(`[reactive] Cannot mutate readonly reactive property '${String(prop)}'`, {
4250
+ code: ReactiveErrorCode.WriteInComputed,
4251
+ });
4252
+ }
4253
+ function readonlyValue(value) {
4254
+ if (!value || typeof value !== 'object')
4255
+ return value;
4256
+ return readonlyReactive(value);
4257
+ }
4258
+ const shallowReactiveHandlers = {
4259
+ get(obj, prop, receiver) {
4260
+ if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
4261
+ return Reflect.get(obj, prop, receiver);
4262
+ if (getActiveEffect())
4263
+ dependant(obj, prop);
4264
+ return Reflect.get(obj, prop, receiver);
4265
+ },
4266
+ set(obj, prop, value, receiver) {
4267
+ const unwrapped = unwrap(receiver);
4268
+ if (obj !== unwrapped)
4269
+ return Object.defineProperty(unwrapped, prop, {
4270
+ value,
4271
+ configurable: true,
4272
+ writable: true,
4273
+ enumerable: true,
4274
+ });
4275
+ if (isUnreactiveProp(obj, prop))
4276
+ return FoolProof.set(obj, prop, value, receiver);
4277
+ const hadProperty = Reflect.has(obj, prop);
4278
+ const oldVal = hadProperty ? Reflect.get(obj, prop, receiver) : absent;
4279
+ const newValue = unwrap(value);
4280
+ if (oldVal !== newValue && FoolProof.set(obj, prop, newValue, receiver)) {
4281
+ touched1(obj, { type: hadProperty ? 'set' : 'add', prop }, prop);
4282
+ }
4283
+ return true;
4284
+ },
4285
+ has(obj, prop) {
4286
+ return reactiveHandlers.has(obj, prop);
4287
+ },
4288
+ deleteProperty(obj, prop) {
4289
+ if (!Object.hasOwn(obj, prop))
4290
+ return true;
4291
+ delete obj[prop];
4292
+ touched1(obj, { type: 'del', prop }, prop);
4293
+ return true;
4294
+ },
4295
+ ownKeys(obj) {
4296
+ return reactiveHandlers.ownKeys(obj);
4297
+ },
4298
+ getOwnPropertyDescriptor(obj, prop) {
4299
+ return Reflect.getOwnPropertyDescriptor(obj, prop);
4300
+ },
4301
+ };
4302
+ const readonlyReactiveHandlers = {
4303
+ get(obj, prop, receiver) {
4304
+ if (readonlyMutators.has(prop)) {
4305
+ return () => {
4306
+ throw readonlyError(prop);
4307
+ };
4308
+ }
4309
+ const reactiveTarget = reactiveObject(obj);
4310
+ const value = FoolProof.get(reactiveTarget, prop, receiver);
4311
+ if (typeof value === 'function') {
4312
+ return (...args) => readonlyValue(value.apply(reactiveTarget, args));
4313
+ }
4314
+ return readonlyValue(value);
4315
+ },
4316
+ set(_obj, prop) {
4317
+ throw readonlyError(prop);
4318
+ },
4319
+ deleteProperty(_obj, prop) {
4320
+ throw readonlyError(prop);
4321
+ },
4322
+ defineProperty(_obj, prop) {
4323
+ throw readonlyError(prop);
4324
+ },
4325
+ setPrototypeOf() {
4326
+ throw readonlyError('[[Prototype]]');
4327
+ },
4328
+ has(obj, prop) {
4329
+ const reactiveTarget = reactiveObject(obj);
4330
+ return Reflect.has(reactiveTarget, prop);
4331
+ },
4332
+ ownKeys(obj) {
4333
+ const reactiveTarget = reactiveObject(obj);
4334
+ return Reflect.ownKeys(reactiveTarget);
4335
+ },
4336
+ getOwnPropertyDescriptor(obj, prop) {
4337
+ return Reflect.getOwnPropertyDescriptor(obj, prop);
4338
+ },
4339
+ };
3940
4340
  const reactiveClasses = new WeakSet();
3941
4341
  // Create the ReactiveBase mixin
3942
4342
  /**
@@ -3978,6 +4378,34 @@ function reactiveObject(anyTarget, subProxy) {
3978
4378
  storeProxyRelationship(target, proxy);
3979
4379
  return proxy;
3980
4380
  }
4381
+ function shallowReactiveObject(anyTarget) {
4382
+ if (!anyTarget || typeof anyTarget !== 'object')
4383
+ return anyTarget;
4384
+ const target = unwrap(anyTarget);
4385
+ if (isNonReactive(target))
4386
+ return target;
4387
+ const existing = shallowObjectToProxy.get(target);
4388
+ if (existing)
4389
+ return existing;
4390
+ const proxy = new Proxy(target, shallowReactiveHandlers);
4391
+ shallowObjectToProxy.set(target, proxy);
4392
+ proxyToObject.set(proxy, target);
4393
+ return proxy;
4394
+ }
4395
+ function readonlyReactiveObject(anyTarget) {
4396
+ if (!anyTarget || typeof anyTarget !== 'object')
4397
+ return anyTarget;
4398
+ const target = unwrap(anyTarget);
4399
+ if (isNonReactive(target))
4400
+ return target;
4401
+ const existing = readonlyObjectToProxy.get(target);
4402
+ if (existing)
4403
+ return existing;
4404
+ const proxy = new Proxy(target, readonlyReactiveHandlers);
4405
+ readonlyObjectToProxy.set(target, proxy);
4406
+ proxyToObject.set(proxy, target);
4407
+ return proxy;
4408
+ }
3981
4409
  /**
3982
4410
  * Main decorator for making classes reactive
3983
4411
  * Automatically makes class instances reactive when created
@@ -4008,6 +4436,8 @@ const reactive = decorator({
4008
4436
  },
4009
4437
  default: reactiveObject,
4010
4438
  });
4439
+ const shallowReactive = shallowReactiveObject;
4440
+ const readonlyReactive = readonlyReactiveObject;
4011
4441
 
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
4442
+ export { markRawProps 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, inert as M, inheritCaption as N, isConstructor as O, isDev as P, isNonReactive as Q, ReactiveBase as R, isObject as S, isProd as T, isReactive as U, isReactiveBroken as V, isTest as W, legacyDecorator as X, link as Y, Zone as Z, markRaw as _, IterableWeakSet as a, mixin as a0, modernDecorator as a1, named as a2, objectToProxy as a3, onEffectThrow as a4, onReactiveBroken as a5, onReactiveReset as a6, prodPreset as a7, proxyToObject as a8, reactive as a9, inertDepth as aA, optionCall as aB, FoolProof as aC, effectHistory as aD, unreactiveProperties as aE, __runInitializers as aF, __esDecorate as aG, contentRef as aH, notifyPropertyChange as aI, metaProtos as aJ, wrapProtos as aK, objectParents as aL, watchers as aM, effectToReactiveObjects as aN, effectMarker as aO, setDebugHooks as aP, allProps as aQ, options as aa, readonlyReactive as ab, reset as ac, root as ad, shallowReactive as ae, tag as af, toRaw as ag, touched as ah, touched1 as ai, unlink as aj, untracked as ak, unwrap as al, withEffectContext as am, wrapInert as an, zip as ao, markWithRoot as ap, dependant as aq, getEffectNode as ar, chainExternalReason as as, keysOf as at, objectsWithDeepWatchers as au, effectToDeepWatchedObjects as av, deepWatchers as aw, registerDeepWatcher as ax, rootFunctionSymbol as ay, getRoot 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 };
4443
+ //# sourceMappingURL=proxy-C2lnvvbx.esm.js.map