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,29 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
4
- // Queue for hooks registered before the environment is ready (circular dependency fix)
5
- const hooks = new Set();
6
- const asyncHooks = {
7
- addHook(hook) {
8
- hooks.add(hook);
9
- return () => hooks.delete(hook);
10
- },
11
- /**
12
- * [Hack] Sanitize a promise (or value) to prevent context leaks.
13
- * Default: Identity function.
14
- * Browser: Uses Macrotask wrapping to break microtask chains.
15
- */
16
- sanitizePromise(p) {
17
- return p;
18
- },
19
- };
20
- /**
21
- * Register a hook that will be called whenever an asynchronous operation is initiated.
22
- * The hook should return a restorer function which will be called just before the async callback runs.
23
- * That restorer should in turn return an undoer function which will be called just after the async callback finishes.
24
- */
25
- const asyncHook = (hook) => asyncHooks.addHook(hook);
26
-
27
4
  /**
28
5
  * Yields tuples containing elements from each input array, stopping at the longest array length
29
6
  * @param args - Arrays to zip together
@@ -277,7 +254,7 @@ function named(name, fn) {
277
254
  }
278
255
  const runtimeGlobals = globalThis;
279
256
  const _mode = runtimeGlobals.process?.env?.NODE_ENV ||
280
- (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunks/proxy-DBHj3kGK.cjs', document.baseURI).href)) }) !== 'undefined' && undefined?.MODE) ||
257
+ (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunks/proxy-HA_QQnd5.cjs', document.baseURI).href)) }) !== 'undefined' && undefined?.MODE) ||
281
258
  'production';
282
259
  const isDev = _mode === 'development';
283
260
  const isProd = _mode === 'production';
@@ -785,7 +762,13 @@ class IterableWeakSet {
785
762
  [Symbol.iterator]() {
786
763
  return this.keys();
787
764
  }
788
- union(other) {
765
+ union(other, ...sets) {
766
+ if (sets.length > 0) {
767
+ for (const set of [other, ...sets])
768
+ for (const value of set)
769
+ this.add(value);
770
+ return this;
771
+ }
789
772
  const others = {
790
773
  [Symbol.iterator]() {
791
774
  return other.keys();
@@ -857,100 +840,6 @@ class IterableWeakSet {
857
840
  }
858
841
  _b = Symbol.toStringTag;
859
842
 
860
- /**
861
- * Creates a mixin that can be used both as a class (extends) and as a function (mixin)
862
- *
863
- * This function supports:
864
- * - Using mixins as base classes: `class MyClass extends MyMixin`
865
- * - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
866
- * - Composing mixins: `const Composed = MixinA(MixinB)`
867
- * - Type-safe property inference for all patterns
868
- *
869
- * @param mixinFunction - The function that creates the mixin
870
- * @param unwrapFunction - Optional function to unwrap reactive objects for method calls
871
- * @returns A mixin that can be used both as a class and as a function
872
- */
873
- function mixin(mixinFunction, unwrapFunction) {
874
- /**
875
- * Cache for mixin results to ensure the same base class always returns the same mixed class
876
- */
877
- const mixinCache = new WeakMap();
878
- // Apply the mixin to Object as the base class
879
- const MixedBase = mixinFunction(Object);
880
- mixinCache.set(Object, MixedBase);
881
- // Create the proxy that handles both constructor and function calls
882
- return new Proxy(MixedBase, {
883
- // Handle `MixinClass(SomeBase)` - use as mixin function
884
- apply(_target, _thisArg, args) {
885
- if (args.length === 0) {
886
- throw new Error('Mixin requires a base class');
887
- }
888
- const baseClass = args[0];
889
- if (typeof baseClass !== 'function') {
890
- throw new Error('Mixin requires a constructor function');
891
- }
892
- // Check if it's a valid constructor or a mixin
893
- if (!isConstructor(baseClass) &&
894
- !(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
895
- throw new Error('Mixin requires a valid constructor');
896
- }
897
- // Check cache first
898
- const cached = mixinCache.get(baseClass);
899
- if (cached) {
900
- return cached;
901
- }
902
- let usedBase = baseClass;
903
- if (unwrapFunction) {
904
- // Create a proxied base class that handles method unwrapping
905
- const ProxiedBaseClass = class extends baseClass {
906
- };
907
- // Proxy the prototype methods to handle unwrapping
908
- const originalPrototype = baseClass.prototype;
909
- const proxiedPrototype = new Proxy(originalPrototype, {
910
- get(target, prop, receiver) {
911
- const value = FoolProof.get(target, prop, receiver);
912
- // Only wrap methods that are likely to access private fields
913
- // Skip symbols and special properties that the reactive system needs
914
- if (typeof value === 'function' &&
915
- typeof prop === 'string' &&
916
- !['constructor', 'toString', 'valueOf'].includes(prop)) {
917
- // Return a wrapped version that uses unwrapped context
918
- return function (...args) {
919
- // Use the unwrapping function if provided, otherwise use this
920
- const context = unwrapFunction(this);
921
- return value.apply(context, args);
922
- };
923
- }
924
- return value;
925
- },
926
- });
927
- // Set the proxied prototype
928
- Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
929
- usedBase = ProxiedBaseClass;
930
- }
931
- // Create the mixed class using the proxied base class
932
- const mixedClass = mixinFunction(usedBase);
933
- // Cache the result
934
- mixinCache.set(baseClass, mixedClass);
935
- return mixedClass;
936
- },
937
- });
938
- }
939
-
940
- const debugHooks = {
941
- isDevtoolsEnabled: () => false,
942
- registerEffect: () => { },
943
- getTriggerChain: () => [],
944
- captureStack: () => [],
945
- captureLineage: () => new Error().stack,
946
- formatStack: (stack) => [stack],
947
- recordTriggerLink: () => { },
948
- decorateError: () => { },
949
- };
950
- function setDebugHooks(hooks) {
951
- Object.assign(debugHooks, hooks);
952
- }
953
-
954
843
  /******************************************************************************
955
844
  Copyright (c) Microsoft Corporation.
956
845
 
@@ -1013,6 +902,29 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
1013
902
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1014
903
  };
1015
904
 
905
+ // Queue for hooks registered before the environment is ready (circular dependency fix)
906
+ const hooks = new Set();
907
+ const asyncHooks = {
908
+ addHook(hook) {
909
+ hooks.add(hook);
910
+ return () => hooks.delete(hook);
911
+ },
912
+ /**
913
+ * [Hack] Sanitize a promise (or value) to prevent context leaks.
914
+ * Default: Identity function.
915
+ * Browser: Uses Macrotask wrapping to break microtask chains.
916
+ */
917
+ sanitizePromise(p) {
918
+ return p;
919
+ },
920
+ };
921
+ /**
922
+ * Register a hook that will be called whenever an asynchronous operation is initiated.
923
+ * The hook should return a restorer function which will be called just before the async callback runs.
924
+ * That restorer should in turn return an undoer function which will be called just after the async callback finishes.
925
+ */
926
+ const asyncHook = (hook) => asyncHooks.addHook(hook);
927
+
1016
928
  var _ZoneAggregator_zones;
1017
929
  function isu(z) {
1018
930
  return z;
@@ -1348,6 +1260,85 @@ function unlink(obj, reason) {
1348
1260
  }
1349
1261
  }
1350
1262
 
1263
+ function extractRawStack(error = new Error()) {
1264
+ if (typeof error === 'string')
1265
+ return error;
1266
+ if (error && typeof error === 'object' && 'stack' in error) {
1267
+ const stack = error.stack;
1268
+ return typeof stack === 'string' ? stack : undefined;
1269
+ }
1270
+ return undefined;
1271
+ }
1272
+ function trimStack(stack) {
1273
+ const raw = extractRawStack(stack);
1274
+ if (!raw)
1275
+ return [];
1276
+ const lines = raw
1277
+ .split('\n')
1278
+ .map((line) => line.trim())
1279
+ .filter(Boolean);
1280
+ if (lines[0]?.startsWith('Error'))
1281
+ lines.shift();
1282
+ while (lines[0] &&
1283
+ (lines[0].includes('captureLineage') ||
1284
+ lines[0].includes('captureDeferredLineage') ||
1285
+ lines[0].includes('debug-hooks.ts')))
1286
+ lines.shift();
1287
+ return lines;
1288
+ }
1289
+ function digestDeferredLineage(lineage) {
1290
+ if (lineage.segments)
1291
+ return lineage.segments;
1292
+ const segments = [];
1293
+ let effect = lineage.effect;
1294
+ let stack = trimStack(lineage.stack);
1295
+ if (!effect) {
1296
+ lineage.segments = [{ effectName: 'root', stack }];
1297
+ return lineage.segments;
1298
+ }
1299
+ while (effect) {
1300
+ const root = getRoot(effect);
1301
+ segments.push({
1302
+ effectName: root.name || 'anonymous',
1303
+ stack,
1304
+ });
1305
+ const node = getEffectNode(effect);
1306
+ effect = node.parent;
1307
+ stack = trimStack(node.creationStack);
1308
+ }
1309
+ if (stack.length)
1310
+ segments.push({ effectName: 'root', stack });
1311
+ lineage.segments = segments;
1312
+ return segments;
1313
+ }
1314
+ function formatDeferredLineage(lineage) {
1315
+ return digestDeferredLineage(lineage)
1316
+ .map((segment) => [`${segment.effectName}:`, ...segment.stack.map((line) => ` ${line}`)].join('\n'))
1317
+ .join('\n');
1318
+ }
1319
+ function captureDeferredLineage(effect = getActiveEffect(), stack = new Error()) {
1320
+ return {
1321
+ effect,
1322
+ stack,
1323
+ toString() {
1324
+ return formatDeferredLineage(this);
1325
+ },
1326
+ };
1327
+ }
1328
+ const debugHooks = {
1329
+ isDevtoolsEnabled: () => false,
1330
+ registerEffect: () => { },
1331
+ getTriggerChain: () => [],
1332
+ captureStack: (error) => extractRawStack(error ?? new Error()),
1333
+ captureLineage: captureDeferredLineage,
1334
+ formatStack: (stack) => [stack],
1335
+ recordTriggerLink: () => { },
1336
+ decorateError: () => { },
1337
+ };
1338
+ function setDebugHooks(hooks) {
1339
+ Object.assign(debugHooks, hooks);
1340
+ }
1341
+
1351
1342
  const effectMarker = {
1352
1343
  enter: 'effect:enter',
1353
1344
  leave: 'effect:leave',
@@ -1498,6 +1489,17 @@ class ReactiveError extends Error {
1498
1489
  return this.debugInfo?.cause;
1499
1490
  }
1500
1491
  }
1492
+ function normalizeSchedulerMode(mode) {
1493
+ switch (mode) {
1494
+ case 'production':
1495
+ return 'raw';
1496
+ case 'development':
1497
+ return 'ordered';
1498
+ default:
1499
+ return mode;
1500
+ }
1501
+ }
1502
+ let schedulerMode = 'ordered';
1501
1503
  // biome-ignore-start lint/correctness/noUnusedFunctionParameters: Interface declaration with empty defaults
1502
1504
  /**
1503
1505
  * Global options for the reactive system
@@ -1588,22 +1590,42 @@ const options = {
1588
1590
  */
1589
1591
  onMemoizationDiscrepancy: undefined,
1590
1592
  /**
1591
- * How to handle cycles detected in effect batches.
1593
+ * Effect scheduler mode.
1592
1594
  *
1593
- * - `'production'` (Default): High-performance mode. Disables dependency graph maintenance and
1594
- * Topological Sorting in favor of a simple FIFO queue. Use this for trustworthy, acyclic UI code.
1595
- * Cycle detection is heuristic (uses maxEffectChain execution counts).
1595
+ * - `'ordered'` (Default): maintains the causal effect graph so effects that are already
1596
+ * queued together can run in dependency order. It also preserves parent/child effect
1597
+ * lifecycle ordering and catches cycles eagerly when edges are created.
1596
1598
  *
1597
- * - `'development'`: Maintains direct dependency graph for early cycle detection during edge creation.
1598
- * Catches cycles before effects execute via DFS check when adding edges. Throws immediately with
1599
- * basic path information. Good balance of debugging help with moderate overhead.
1599
+ * - `'raw'`: fastest FIFO scheduler. It does not maintain the effect graph.
1600
+ * Cycle detection is heuristic, using maxEffectChain execution counts.
1600
1601
  *
1601
- * - `'debug'`: Full diagnostic mode with transitive closures and topological sorting.
1602
- * Provides detailed cycle path reporting. Highest overhead but most informative for bug hunting.
1602
+ * - `'debug'`: ordered scheduling plus the most detailed graph diagnostics. Highest overhead,
1603
+ * best for investigation.
1603
1604
  *
1604
- * @default 'production'
1605
+ * @default 'ordered'
1605
1606
  */
1606
- cycleHandling: 'development',
1607
+ get scheduler() {
1608
+ return schedulerMode;
1609
+ },
1610
+ set scheduler(mode) {
1611
+ schedulerMode = mode;
1612
+ },
1613
+ /**
1614
+ * @deprecated Use `scheduler` instead.
1615
+ *
1616
+ * Backward-compatible alias for older names:
1617
+ * - `'production'` maps to `scheduler = 'raw'`
1618
+ * - `'development'` maps to `scheduler = 'ordered'`
1619
+ * - `'debug'` maps to `scheduler = 'debug'`
1620
+ *
1621
+ * The new names describe scheduler behavior rather than runtime environment.
1622
+ */
1623
+ get cycleHandling() {
1624
+ return schedulerMode;
1625
+ },
1626
+ set cycleHandling(mode) {
1627
+ schedulerMode = normalizeSchedulerMode(mode);
1628
+ },
1607
1629
  /**
1608
1630
  * Internal flag used by memoization discrepancy detector to avoid counting calls in tests
1609
1631
  * @warning Do not modify this flag manually, this flag is given by the engine
@@ -1693,14 +1715,14 @@ function optionCall(name, ...args) {
1693
1715
  /** Production preset: no introspection, heuristic cycle detection, minimal overhead */
1694
1716
  const prodPreset = {
1695
1717
  maxEffectReaction: 'throw',
1696
- cycleHandling: 'production',
1718
+ scheduler: 'raw',
1697
1719
  introspection: null,
1698
1720
  onMemoizationDiscrepancy: undefined,
1699
1721
  };
1700
- /** Development preset (default): introspection on, early cycle detection, warnings */
1722
+ /** Development preset: introspection on, early cycle detection, warnings */
1701
1723
  const devPreset = {
1702
1724
  maxEffectReaction: 'warn',
1703
- cycleHandling: 'development',
1725
+ scheduler: 'ordered',
1704
1726
  introspection: {
1705
1727
  gatherReasons: { lineages: 'touch' },
1706
1728
  logErrors: true,
@@ -1712,7 +1734,7 @@ const devPreset = {
1712
1734
  /** Debug preset: full diagnostics, throws on violations, rich lineage capture */
1713
1735
  const debugPreset = {
1714
1736
  maxEffectReaction: 'debug',
1715
- cycleHandling: 'debug',
1737
+ scheduler: 'debug',
1716
1738
  introspection: {
1717
1739
  gatherReasons: { lineages: 'both' },
1718
1740
  logErrors: true,
@@ -1735,6 +1757,7 @@ function unwrap(obj) {
1735
1757
  return obj;
1736
1758
  return proxyToObject.get(obj) || obj;
1737
1759
  }
1760
+ const toRaw = unwrap;
1738
1761
  function isReactive(obj) {
1739
1762
  return proxyToObject.has(obj);
1740
1763
  }
@@ -1782,8 +1805,9 @@ function dependant(obj, prop = allProps) {
1782
1805
  if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps && prop !== keysOf))
1783
1806
  return;
1784
1807
  const node = getEffectNode(currentActiveEffect);
1785
- if ('dependencyHook' in node) {
1786
- node.dependencyHook?.(obj, prop);
1808
+ const hasDependencyHook = node.dependencyHook !== undefined;
1809
+ if (hasDependencyHook) {
1810
+ node.dependencyHook(obj, prop);
1787
1811
  }
1788
1812
  let objectWatchers = exports.watchers.get(obj);
1789
1813
  if (!objectWatchers) {
@@ -1805,25 +1829,25 @@ function dependant(obj, prop = allProps) {
1805
1829
  exports.effectToReactiveObjects.set(currentActiveEffect, new Set([obj]));
1806
1830
  }
1807
1831
  // Store dependency stack if introspection is enabled
1808
- const gatherReasons = options.introspection?.gatherReasons;
1809
- if (gatherReasons) {
1810
- const lineageConfig = gatherReasons.lineages;
1811
- if (lineageConfig === 'dependency' || lineageConfig === 'both') {
1812
- let objStacks = dependencyStacks.get(obj);
1813
- if (!objStacks) {
1814
- objStacks = new Map();
1815
- dependencyStacks.set(obj, objStacks);
1816
- }
1817
- let propStacks = objStacks.get(prop);
1818
- if (!propStacks) {
1819
- propStacks = new Map();
1820
- objStacks.set(prop, propStacks);
1821
- }
1822
- propStacks.set(currentActiveEffect, debugHooks.captureLineage());
1832
+ const lineageMode = options.introspection?.gatherReasons?.lineages;
1833
+ const shouldGatherDependencyLineage = lineageMode === 'dependency' || lineageMode === 'both';
1834
+ if (shouldGatherDependencyLineage) {
1835
+ let objStacks = dependencyStacks.get(obj);
1836
+ if (!objStacks) {
1837
+ objStacks = new Map();
1838
+ dependencyStacks.set(obj, objStacks);
1839
+ }
1840
+ let propStacks = objStacks.get(prop);
1841
+ if (!propStacks) {
1842
+ propStacks = new Map();
1843
+ objStacks.set(prop, propStacks);
1823
1844
  }
1845
+ propStacks.set(currentActiveEffect, debugHooks.captureLineage());
1824
1846
  }
1825
1847
  }
1826
1848
 
1849
+ // Simple module to manage inert state without circular dependencies
1850
+ exports.inertDepth = 0;
1827
1851
  /**
1828
1852
  * Finds a cycle in a sequence of functions by looking for the first repetition
1829
1853
  */
@@ -1925,6 +1949,40 @@ let causesClosure = new WeakMap();
1925
1949
  let consequencesClosure = new WeakMap();
1926
1950
  // Batch re-entrance depth and broken state
1927
1951
  let broken = false;
1952
+ /** True after an unrecoverable reactive failure until `reset()`. */
1953
+ function isReactiveBroken() {
1954
+ return broken;
1955
+ }
1956
+ const reactiveBrokenHandlers = new Set();
1957
+ const reactiveResetHandlers = new Set();
1958
+ function onReactiveBroken(handler) {
1959
+ reactiveBrokenHandlers.add(handler);
1960
+ return () => reactiveBrokenHandlers.delete(handler);
1961
+ }
1962
+ function onReactiveReset(handler) {
1963
+ reactiveResetHandlers.add(handler);
1964
+ return () => reactiveResetHandlers.delete(handler);
1965
+ }
1966
+ function notifyReactiveBroken(error) {
1967
+ for (const handler of Array.from(reactiveBrokenHandlers)) {
1968
+ try {
1969
+ handler(error);
1970
+ }
1971
+ catch (handlerError) {
1972
+ options.warn('[reactive] onReactiveBroken handler threw', handlerError);
1973
+ }
1974
+ }
1975
+ }
1976
+ function notifyReactiveReset() {
1977
+ for (const handler of Array.from(reactiveResetHandlers)) {
1978
+ try {
1979
+ handler();
1980
+ }
1981
+ catch (handlerError) {
1982
+ options.warn('[reactive] onReactiveReset handler threw', handlerError);
1983
+ }
1984
+ }
1985
+ }
1928
1986
  /**
1929
1987
  * Gets or creates an IterableWeakSet for a closure map
1930
1988
  */
@@ -1943,7 +2001,7 @@ function getOrCreateClosure(closure, root) {
1943
2001
  * @param targetRoot - Root function of the effect being triggered
1944
2002
  */
1945
2003
  function addGraphEdge(callerRoot, targetRoot) {
1946
- if (options.cycleHandling === 'production')
2004
+ if (options.scheduler === 'raw')
1947
2005
  return;
1948
2006
  // Add to forward graph: callerRoot → targetRoot
1949
2007
  const triggers = effectTriggers.get(callerRoot);
@@ -2058,7 +2116,7 @@ function hasPathExcluding(start, end, exclude) {
2058
2116
  * @param effect - The effect being cleaned up
2059
2117
  */
2060
2118
  function cleanupEffectFromGraph(effect) {
2061
- if (options.cycleHandling === 'production')
2119
+ if (options.scheduler === 'raw')
2062
2120
  return;
2063
2121
  const root = getRoot(effect);
2064
2122
  // Get closures before removing direct edges (needed for propagation)
@@ -2167,7 +2225,7 @@ const executingStack = [];
2167
2225
  * Called once when batch starts or when new effects are added
2168
2226
  */
2169
2227
  function computeAllInDegrees(batch) {
2170
- if (options.cycleHandling === 'production')
2228
+ if (options.scheduler === 'raw')
2171
2229
  return;
2172
2230
  const activeEffect = getActiveEffect();
2173
2231
  const activeRoot = activeEffect ? getRoot(activeEffect) : null;
@@ -2175,6 +2233,16 @@ function computeAllInDegrees(batch) {
2175
2233
  batch.inDegrees.clear();
2176
2234
  for (const [root] of batch.all) {
2177
2235
  let inDegree = 0;
2236
+ // Make sure parents are executed before children: if parent is in batch, count it as a dependency
2237
+ const effect = batch.all.get(root);
2238
+ const parent = getEffectNode(effect).parent;
2239
+ const parentRoot = parent ? getRoot(parent) : undefined;
2240
+ if (parentRoot &&
2241
+ batch.all.has(parentRoot) &&
2242
+ parentRoot !== activeRoot &&
2243
+ parentRoot !== root) {
2244
+ inDegree++;
2245
+ }
2178
2246
  const causes = causesClosure.get(root);
2179
2247
  if (causes) {
2180
2248
  for (const causeRoot of causes) {
@@ -2194,17 +2262,26 @@ function computeAllInDegrees(batch) {
2194
2262
  function decrementInDegreesForExecuted(batch, executedRoot) {
2195
2263
  // Get all effects that this executed effect triggers
2196
2264
  const consequences = consequencesClosure.get(executedRoot);
2197
- if (!consequences)
2198
- return;
2199
- for (const consequenceRoot of consequences) {
2200
- // Only update if it's still in the batch
2201
- if (batch.all.has(consequenceRoot)) {
2202
- const currentDegree = batch.inDegrees.get(consequenceRoot) ?? 0;
2203
- if (currentDegree > 0) {
2204
- batch.inDegrees.set(consequenceRoot, currentDegree - 1);
2265
+ if (consequences) {
2266
+ for (const consequenceRoot of consequences) {
2267
+ // Only update if it's still in the batch
2268
+ if (batch.all.has(consequenceRoot)) {
2269
+ const currentDegree = batch.inDegrees.get(consequenceRoot) ?? 0;
2270
+ if (currentDegree > 0) {
2271
+ batch.inDegrees.set(consequenceRoot, currentDegree - 1);
2272
+ }
2205
2273
  }
2206
2274
  }
2207
2275
  }
2276
+ for (const [root, effect] of batch.all) {
2277
+ const parent = getEffectNode(effect).parent;
2278
+ if (!parent || getRoot(parent) !== executedRoot)
2279
+ continue;
2280
+ const currentDegree = batch.inDegrees.get(root) ?? 0;
2281
+ if (currentDegree > 0) {
2282
+ batch.inDegrees.set(root, currentDegree - 1);
2283
+ }
2284
+ }
2208
2285
  }
2209
2286
  /**
2210
2287
  * Finds a path from startRoot to endRoot in the dependency graph
@@ -2343,15 +2420,15 @@ function addToBatch(effect, caller, immediate, reason) {
2343
2420
  }
2344
2421
  }
2345
2422
  // 1. Add to batch first (needed for cycle detection)
2346
- // TODO: Check if it's the correct way to do (these different behavior in function of dev/production)
2347
- if (options.cycleHandling === 'production') {
2348
- // Production mode: FIFO (delete and re-add to move to end)
2423
+ // TODO: Check if this difference between raw and graph-backed scheduling is the right tradeoff.
2424
+ if (options.scheduler === 'raw') {
2425
+ // Raw mode: FIFO (delete and re-add to move to end)
2349
2426
  if (currentBatch.all.has(root)) {
2350
2427
  currentBatch.all.delete(root);
2351
2428
  }
2352
2429
  }
2353
2430
  else {
2354
- // Dev mode: skip if already queued — the existing entry will re-run
2431
+ // Graph-backed modes: skip if already queued — the existing entry will re-run
2355
2432
  if (currentBatch.all.has(root)) {
2356
2433
  return;
2357
2434
  }
@@ -2360,7 +2437,7 @@ function addToBatch(effect, caller, immediate, reason) {
2360
2437
  if (node.stopped)
2361
2438
  return;
2362
2439
  currentBatch.all.set(root, effect);
2363
- if (caller && true && options.cycleHandling !== 'production') {
2440
+ if (caller && true && options.scheduler !== 'raw') {
2364
2441
  const callerRoot = getRoot(caller);
2365
2442
  // const root = getRoot(effect) // Already have root
2366
2443
  // Check for cycle BEFORE adding edge
@@ -2382,6 +2459,9 @@ function addToBatch(effect, caller, immediate, reason) {
2382
2459
  }
2383
2460
  addGraphEdge(callerRoot, root);
2384
2461
  }
2462
+ if (options.scheduler !== 'raw') {
2463
+ computeAllInDegrees(currentBatch);
2464
+ }
2385
2465
  }
2386
2466
  /**
2387
2467
  * Adds a cleanup function to be called when the current batch of effects completes
@@ -2478,7 +2558,7 @@ function executeNext(effectuatedRoots) {
2478
2558
  // Find an effect with in-degree 0 using cached values
2479
2559
  let nextEffect = null;
2480
2560
  let nextRoot = null;
2481
- if (options.cycleHandling === 'production') {
2561
+ if (options.scheduler === 'raw') {
2482
2562
  // In flat mode, we just take the first effect in the queue (FIFO)
2483
2563
  const first = currentBatch.all.entries().next().value;
2484
2564
  if (first) {
@@ -2568,9 +2648,7 @@ function executeNext(effectuatedRoots) {
2568
2648
  }
2569
2649
  return result;
2570
2650
  }
2571
- // Track which sub-effects have been executed to prevent infinite loops
2572
- // These are all the effects triggered under `activeEffect` and all their sub-effects
2573
- function batch(effect, immediate, caller) {
2651
+ function batch(effect, batchOptions) {
2574
2652
  if (broken) {
2575
2653
  throw new ReactiveError('[reactive] Reactive system is broken after an unrecoverable error. Call reset() to recover.', { code: exports.ReactiveErrorCode.BrokenEffects });
2576
2654
  }
@@ -2585,16 +2663,38 @@ function batch(effect, immediate, caller) {
2585
2663
  throw new Error('Activation registry already exists');
2586
2664
  optionCall('beginChain', roots);
2587
2665
  }
2588
- // TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
2589
- const activeCaller = getActiveEffect();
2590
- const callerToUse = caller || activeCaller;
2666
+ const immediate = batchOptions?.immediate === true;
2667
+ const contained = batchOptions?.contained === true;
2668
+ const callerToUse = batchOptions?.caller || getActiveEffect();
2591
2669
  // Optimization: If nested and NOT immediate, just join the existing batch
2592
- if (!isNewBatch && !immediate) {
2670
+ if (!isNewBatch && !contained && !immediate) {
2593
2671
  for (let i = 0; i < effect.length; i++) {
2594
2672
  addToBatch(effect[i], callerToUse);
2595
2673
  }
2596
2674
  return;
2597
2675
  }
2676
+ if (!isNewBatch && !contained && immediate) {
2677
+ const firstReturn = {};
2678
+ for (let i = 0; i < effect.length; i++) {
2679
+ executingStack.push(effect[i]);
2680
+ try {
2681
+ const node = getEffectNode(effect[i]);
2682
+ const reason = node.nextReason;
2683
+ if (node.cleanup) {
2684
+ const cleanup = node.cleanup;
2685
+ node.cleanup = undefined;
2686
+ cleanup(reason);
2687
+ }
2688
+ const rv = effect[i]();
2689
+ if (rv !== undefined && !('value' in firstReturn))
2690
+ firstReturn.value = rv;
2691
+ }
2692
+ finally {
2693
+ executingStack.pop();
2694
+ }
2695
+ }
2696
+ return firstReturn.value;
2697
+ }
2598
2698
  const currentBatch = {
2599
2699
  all: new Map(),
2600
2700
  inDegrees: new Map(),
@@ -2602,9 +2702,11 @@ function batch(effect, immediate, caller) {
2602
2702
  };
2603
2703
  batchStack.push(currentBatch);
2604
2704
  let success = false;
2705
+ let failure;
2605
2706
  try {
2606
2707
  const effectuatedRoots = [];
2607
2708
  const firstReturn = {};
2709
+ let initialError;
2608
2710
  if (immediate) {
2609
2711
  // Execute initial effects in providing order
2610
2712
  for (let i = 0; i < effect.length; i++) {
@@ -2621,11 +2723,17 @@ function batch(effect, immediate, caller) {
2621
2723
  if (rv !== undefined && !('value' in firstReturn))
2622
2724
  firstReturn.value = rv;
2623
2725
  }
2726
+ catch (error) {
2727
+ initialError = error;
2728
+ break;
2729
+ }
2624
2730
  finally {
2625
2731
  executingStack.pop();
2626
2732
  currentBatch.all.delete(getRoot(effect[i]));
2627
2733
  }
2628
2734
  }
2735
+ if (initialError)
2736
+ throw initialError;
2629
2737
  }
2630
2738
  else {
2631
2739
  // Add initial effects to batch and compute dependencies
@@ -2685,13 +2793,17 @@ function batch(effect, immediate, caller) {
2685
2793
  return firstReturn.value;
2686
2794
  }
2687
2795
  catch (error) {
2796
+ failure = error;
2688
2797
  if (batchStack.length === 1)
2689
2798
  optionCall('error', '[reactive] Root batch failure before broken state:', error);
2690
2799
  throw error;
2691
2800
  }
2692
2801
  finally {
2693
2802
  if (!success && batchStack.length === 1) {
2803
+ const wasBroken = broken;
2694
2804
  broken = true;
2805
+ if (!wasBroken)
2806
+ notifyReactiveBroken(failure);
2695
2807
  }
2696
2808
  batchStack.pop();
2697
2809
  if (batchStack.length === 0) {
@@ -2707,6 +2819,7 @@ function batch(effect, immediate, caller) {
2707
2819
  * All existing effects become orphaned and must be recreated.
2708
2820
  */
2709
2821
  function reset() {
2822
+ const wasBroken = broken;
2710
2823
  broken = false;
2711
2824
  activationRegistry = undefined;
2712
2825
  batchStack.length = 0;
@@ -2717,6 +2830,8 @@ function reset() {
2717
2830
  resetRegistry();
2718
2831
  resetTracking();
2719
2832
  effectHistory.present.active = undefined;
2833
+ if (wasBroken)
2834
+ notifyReactiveReset();
2720
2835
  }
2721
2836
  // Inject batch function to allow atomic game loops in requestAnimationFrame/setTimeout/...
2722
2837
  // Note: Automatic batching of async callbacks (setTimeout, Promise.then, etc.) is NOT implemented.
@@ -2733,7 +2848,7 @@ const atomic = decorator({
2733
2848
  const atomicEffect = () => original.apply(this, args);
2734
2849
  // Debug: helpful to have a name
2735
2850
  Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
2736
- return batch(atomicEffect, 'immediate');
2851
+ return batch(atomicEffect, { immediate: true });
2737
2852
  };
2738
2853
  },
2739
2854
  default(original) {
@@ -2741,7 +2856,7 @@ const atomic = decorator({
2741
2856
  const atomicEffect = () => original.apply(this, args);
2742
2857
  // Debug: helpful to have a name
2743
2858
  Object.defineProperty(atomicEffect, 'name', { value: `atomic(${original.name})` });
2744
- return batch(atomicEffect, 'immediate');
2859
+ return batch(atomicEffect, { immediate: true });
2745
2860
  };
2746
2861
  },
2747
2862
  });
@@ -2780,7 +2895,7 @@ function captured(prev, fn) {
2780
2895
  * ```
2781
2896
  */
2782
2897
  function atom(fn) {
2783
- return batch(fn, 'immediate');
2898
+ return batch(fn, { immediate: true });
2784
2899
  }
2785
2900
  const fr = new FinalizationRegistry((f) => f());
2786
2901
  /**
@@ -3024,7 +3139,7 @@ const effect = captioned(named(effectMarker.leave, flavored(function effect(fn,
3024
3139
  abortController = undefined;
3025
3140
  }
3026
3141
  };
3027
- batch(runEffect, 'immediate');
3142
+ batch(runEffect, { immediate: true });
3028
3143
  // Only ROOT effects are registered for GC cleanup and zone tracking
3029
3144
  const isRootEffect = !parent;
3030
3145
  const stopEffect = (reason) => {
@@ -3094,16 +3209,53 @@ const untracked = captioned(function untracked(fn) {
3094
3209
  ? externalReason.with(external, () => effectHistory.present.root(fn))
3095
3210
  : effectHistory.present.root(fn);
3096
3211
  });
3212
+ function runInert(fn) {
3213
+ // Increment the counter
3214
+ const originalDepth = exports.inertDepth;
3215
+ exports.inertDepth = originalDepth + 1;
3216
+ try {
3217
+ return fn();
3218
+ }
3219
+ finally {
3220
+ exports.inertDepth = originalDepth;
3221
+ }
3222
+ }
3223
+ function wrapInert(fn) {
3224
+ function inertEffect(...args) {
3225
+ return runInert(() => fn.apply(this, args));
3226
+ }
3227
+ Object.defineProperty(inertEffect, 'name', { value: `inert(${fn.name})` });
3228
+ return inertEffect;
3229
+ }
3230
+ /**
3231
+ * Executes a function with fast-path reads that bypass proxy overhead and dependency tracking.
3232
+ * Writes remain fully reactive. Uses a counter for safe nesting.
3233
+ * Can also decorate methods so the whole method body runs inertly.
3234
+ * @param fn - The function to execute
3235
+ */
3236
+ const inert = decorator({
3237
+ method(original) {
3238
+ return wrapInert(original);
3239
+ },
3240
+ default(fn) {
3241
+ if (typeof fn !== 'function')
3242
+ throw new Error('inert() expects a function');
3243
+ return runInert(fn);
3244
+ },
3245
+ });
3097
3246
  /**
3098
3247
  * Executes a function from a virgin/root context - no parent effect, no tracking
3099
3248
  * Creates completely independent effects that won't be cleaned up by any parent
3100
3249
  * @param fn - The function to execute
3101
3250
  */
3102
3251
  const root = captioned(function root(fn) {
3252
+ // When broken, `atomic`/`batch` throws immediately. DOM wrappers (e.g. Sursaut
3253
+ // `root\`event:…\``) still need to run listener code for inspection UI; skip batching.
3254
+ const runner = broken ? fn : atomic(fn);
3103
3255
  const external = externalReasonFrom(fn);
3104
3256
  return external
3105
- ? externalReason.with(external, () => effectHistory.root(fn))
3106
- : effectHistory.root(fn);
3257
+ ? externalReason.with(external, () => effectHistory.root(runner))
3258
+ : effectHistory.root(runner);
3107
3259
  });
3108
3260
  function biDi(received, get, set) {
3109
3261
  if (typeof get !== 'function') {
@@ -3126,6 +3278,86 @@ function biDi(received, get, set) {
3126
3278
  : () => { };
3127
3279
  }
3128
3280
 
3281
+ /**
3282
+ * Creates a mixin that can be used both as a class (extends) and as a function (mixin)
3283
+ *
3284
+ * This function supports:
3285
+ * - Using mixins as base classes: `class MyClass extends MyMixin`
3286
+ * - Using mixins as functions: `class MyClass extends MyMixin(SomeBase)`
3287
+ * - Composing mixins: `const Composed = MixinA(MixinB)`
3288
+ * - Type-safe property inference for all patterns
3289
+ *
3290
+ * @param mixinFunction - The function that creates the mixin
3291
+ * @param unwrapFunction - Optional function to unwrap reactive objects for method calls
3292
+ * @returns A mixin that can be used both as a class and as a function
3293
+ */
3294
+ function mixin(mixinFunction, unwrapFunction) {
3295
+ /**
3296
+ * Cache for mixin results to ensure the same base class always returns the same mixed class
3297
+ */
3298
+ const mixinCache = new WeakMap();
3299
+ // Apply the mixin to Object as the base class
3300
+ const MixedBase = mixinFunction(Object);
3301
+ mixinCache.set(Object, MixedBase);
3302
+ // Create the proxy that handles both constructor and function calls
3303
+ return new Proxy(MixedBase, {
3304
+ // Handle `MixinClass(SomeBase)` - use as mixin function
3305
+ apply(_target, _thisArg, args) {
3306
+ if (args.length === 0) {
3307
+ throw new Error('Mixin requires a base class');
3308
+ }
3309
+ const baseClass = args[0];
3310
+ if (typeof baseClass !== 'function') {
3311
+ throw new Error('Mixin requires a constructor function');
3312
+ }
3313
+ // Check if it's a valid constructor or a mixin
3314
+ if (!isConstructor(baseClass) &&
3315
+ !(baseClass && typeof baseClass === 'function' && baseClass.prototype)) {
3316
+ throw new Error('Mixin requires a valid constructor');
3317
+ }
3318
+ // Check cache first
3319
+ const cached = mixinCache.get(baseClass);
3320
+ if (cached) {
3321
+ return cached;
3322
+ }
3323
+ let usedBase = baseClass;
3324
+ if (unwrapFunction) {
3325
+ // Create a proxied base class that handles method unwrapping
3326
+ const ProxiedBaseClass = class extends baseClass {
3327
+ };
3328
+ // Proxy the prototype methods to handle unwrapping
3329
+ const originalPrototype = baseClass.prototype;
3330
+ const proxiedPrototype = new Proxy(originalPrototype, {
3331
+ get(target, prop, receiver) {
3332
+ const value = FoolProof.get(target, prop, receiver);
3333
+ // Only wrap methods that are likely to access private fields
3334
+ // Skip symbols and special properties that the reactive system needs
3335
+ if (typeof value === 'function' &&
3336
+ typeof prop === 'string' &&
3337
+ !['constructor', 'toString', 'valueOf'].includes(prop)) {
3338
+ // Return a wrapped version that uses unwrapped context
3339
+ return function (...args) {
3340
+ // Use the unwrapping function if provided, otherwise use this
3341
+ const context = unwrapFunction(this);
3342
+ return value.apply(context, args);
3343
+ };
3344
+ }
3345
+ return value;
3346
+ },
3347
+ });
3348
+ // Set the proxied prototype
3349
+ Object.setPrototypeOf(ProxiedBaseClass.prototype, proxiedPrototype);
3350
+ usedBase = ProxiedBaseClass;
3351
+ }
3352
+ // Create the mixed class using the proxied base class
3353
+ const mixedClass = mixinFunction(usedBase);
3354
+ // Cache the result
3355
+ mixinCache.set(baseClass, mixedClass);
3356
+ return mixedClass;
3357
+ },
3358
+ });
3359
+ }
3360
+
3129
3361
  // Track which objects contain which other objects (back-references)
3130
3362
  const objectParents = new WeakMap();
3131
3363
  // Track which objects have deep watchers
@@ -3255,7 +3487,7 @@ function getState(obj) {
3255
3487
  }
3256
3488
  return state;
3257
3489
  }
3258
- function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
3490
+ function collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, ...keyChains) {
3259
3491
  const sourceEffect = getActiveEffect();
3260
3492
  for (const keys of keyChains)
3261
3493
  for (const key of keys) {
@@ -3271,7 +3503,7 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
3271
3503
  continue;
3272
3504
  }
3273
3505
  if (!effects.has(effect)) {
3274
- effects.set(effect, getDependencyStack(effect, obj, key));
3506
+ effects.set(effect, collectDependencyStack ? getDependencyStack(effect, obj, key) : undefined);
3275
3507
  if (!hasBatched(effect))
3276
3508
  recordActivation(effect, obj, evolution, key);
3277
3509
  }
@@ -3304,17 +3536,18 @@ function touched(obj, evolution, props) {
3304
3536
  const effects = new Map();
3305
3537
  const structural = !['set', 'invalidate'].includes(evolution.type);
3306
3538
  const broad = structural ? [allProps, keysOf] : [allProps];
3539
+ const gatherReasons = options.introspection?.gatherReasons;
3540
+ const lineageConfig = gatherReasons?.lineages;
3541
+ const collectDependencyStack = lineageConfig === 'dependency' || lineageConfig === 'both';
3307
3542
  if (props)
3308
- collectEffects(obj, evolution, effects, objectWatchers, broad, props);
3543
+ collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, broad, props);
3309
3544
  else
3310
- collectEffects(obj, evolution, effects, objectWatchers, objectWatchers.keys());
3545
+ collectEffects(obj, evolution, effects, objectWatchers, collectDependencyStack, objectWatchers.keys());
3311
3546
  const triggers = Array.from(effects.keys());
3312
3547
  const sourceEffect = getActiveEffect();
3313
3548
  optionCall('touched', obj, evolution, props, triggers);
3314
3549
  // Store pending triggers for CleanupReason before batching
3315
- if (options.introspection?.gatherReasons) {
3316
- const gatherReasons = options.introspection.gatherReasons;
3317
- const lineageConfig = gatherReasons.lineages;
3550
+ if (gatherReasons && effects.size > 0) {
3318
3551
  let touchLineage;
3319
3552
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3320
3553
  touchLineage = debugHooks.captureLineage();
@@ -3331,7 +3564,7 @@ function touched(obj, evolution, props) {
3331
3564
  });
3332
3565
  }
3333
3566
  }
3334
- batch(triggers, undefined, sourceEffect);
3567
+ batch(triggers, { caller: sourceEffect });
3335
3568
  }
3336
3569
  // Bubble up changes if this object has deep watchers
3337
3570
  if (objectsWithDeepWatchers.has(obj)) {
@@ -3355,6 +3588,7 @@ function touchedOpaque(obj, evolution, prop) {
3355
3588
  const gather = options.introspection?.gatherReasons;
3356
3589
  if (gather) {
3357
3590
  const lineageConfig = gather.lineages;
3591
+ let touchLineage;
3358
3592
  for (const effect of deps) {
3359
3593
  const node = getEffectNode(effect);
3360
3594
  if (!node.isOpaque)
@@ -3366,10 +3600,9 @@ function touchedOpaque(obj, evolution, prop) {
3366
3600
  }
3367
3601
  effects.add(effect);
3368
3602
  if (gather) {
3369
- let touchLineage;
3370
3603
  let dependencyStack;
3371
3604
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3372
- touchLineage = debugHooks.captureLineage();
3605
+ touchLineage ?? (touchLineage = debugHooks.captureLineage());
3373
3606
  }
3374
3607
  if (lineageConfig === 'dependency' || lineageConfig === 'both') {
3375
3608
  dependencyStack = getDependencyStack(effect, obj, prop);
@@ -3405,7 +3638,7 @@ function touchedOpaque(obj, evolution, prop) {
3405
3638
  }
3406
3639
  if (effects.size > 0) {
3407
3640
  optionCall('touched', obj, evolution, [prop], Array.from(effects));
3408
- batch(Array.from(effects), undefined, sourceEffect);
3641
+ batch(Array.from(effects), { caller: sourceEffect });
3409
3642
  }
3410
3643
  }
3411
3644
 
@@ -3452,6 +3685,8 @@ function nonReactive(...obj) {
3452
3685
  }
3453
3686
  return obj[0];
3454
3687
  }
3688
+ const markRaw = nonReactive;
3689
+ const markRawProps = addUnreactiveProps;
3455
3690
  function nonReactiveClass(...cls) {
3456
3691
  for (const c of cls)
3457
3692
  if (c)
@@ -3669,7 +3904,7 @@ function dispatchNotifications(notifications) {
3669
3904
  const originWatchers = exports.watchers.get(origin.obj);
3670
3905
  if (originWatchers) {
3671
3906
  const originEffects = new Map();
3672
- collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
3907
+ collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, false, [allProps], [origin.prop]);
3673
3908
  allowedEffects = new Set(originEffects.keys());
3674
3909
  }
3675
3910
  // If no allowed effects, skip all notifications (no one should be notified)
@@ -3688,7 +3923,7 @@ function dispatchNotifications(notifications) {
3688
3923
  if (objectWatchers) {
3689
3924
  currentEffects = new Map();
3690
3925
  const broad = evolution.type !== 'set' ? [allProps, keysOf] : [allProps];
3691
- collectEffects(obj, evolution, currentEffects, objectWatchers, broad, propsArray);
3926
+ collectEffects(obj, evolution, currentEffects, objectWatchers, false, broad, propsArray);
3692
3927
  // Filter effects by ancestor chain if origin exists
3693
3928
  // Include effects that either directly depend on origin or have an ancestor that does
3694
3929
  if (origin && allowedEffects) {
@@ -3750,12 +3985,34 @@ const metaProtos = new WeakMap();
3750
3985
  const wrapProtos = new WeakMap();
3751
3986
  const arrayLengths = new WeakMap();
3752
3987
  const hasReentry = new Set();
3988
+ const accessAnalysisCache = new WeakMap();
3989
+ const readonlyObjectToProxy = new WeakMap();
3990
+ const shallowObjectToProxy = new WeakMap();
3991
+ const readonlyMutators = new Set([
3992
+ 'copyWithin',
3993
+ 'fill',
3994
+ 'pop',
3995
+ 'push',
3996
+ 'reverse',
3997
+ 'shift',
3998
+ 'sort',
3999
+ 'splice',
4000
+ 'unshift',
4001
+ 'add',
4002
+ 'clear',
4003
+ 'delete',
4004
+ 'set',
4005
+ ]);
3753
4006
  // Sub-proxy registration for custom reactive behaviors
3754
4007
  const subsRegister = new WeakMap();
3755
4008
  // Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
3756
4009
  // TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
3757
4010
  let internalUntracked = false;
3758
4011
  function wrapReactiveValue(obj, prop, value) {
4012
+ // Optional fast-path for inert reads - skips reactive wrapping
4013
+ // Disabled by default for safety, can be enabled for performance-critical read-only contexts
4014
+ if (exports.inertDepth > 0)
4015
+ return value;
3759
4016
  if (!isReactive(value) && typeof value === 'object' && value !== null) {
3760
4017
  const reactiveValue = reactiveObject(value);
3761
4018
  // Only create back-references if this object needs them
@@ -3766,6 +4023,56 @@ function wrapReactiveValue(obj, prop, value) {
3766
4023
  }
3767
4024
  return value;
3768
4025
  }
4026
+ function computeAccessAnalysis(obj, prop, receiver) {
4027
+ const proto = Object.getPrototypeOf(obj);
4028
+ const isOwnProp = Object.hasOwn(obj, prop);
4029
+ const shouldIgnoreAccessor = options.ignoreAccessors &&
4030
+ isOwnProp &&
4031
+ proto !== null &&
4032
+ (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
4033
+ let hasProp = isOwnProp;
4034
+ let owner = isOwnProp ? obj : undefined;
4035
+ if (!isOwnProp) {
4036
+ let raw = proto;
4037
+ while (raw && raw !== Object.prototype) {
4038
+ if (Object.hasOwn(raw, prop)) {
4039
+ hasProp = true;
4040
+ owner = raw;
4041
+ break;
4042
+ }
4043
+ raw = Object.getPrototypeOf(raw);
4044
+ }
4045
+ }
4046
+ return {
4047
+ hasProp,
4048
+ owner,
4049
+ isInheritedAccess: hasProp && !isOwnProp,
4050
+ shouldIgnoreAccessor,
4051
+ ignoreAccessors: options.ignoreAccessors,
4052
+ instanceMembers: options.instanceMembers,
4053
+ };
4054
+ }
4055
+ function analyzeAccess(obj, prop, receiver) {
4056
+ const proto = Object.getPrototypeOf(obj);
4057
+ if (Object.hasOwn(obj, prop))
4058
+ return computeAccessAnalysis(obj, prop, receiver);
4059
+ if (proto === null || Array.isArray(obj))
4060
+ return computeAccessAnalysis(obj, prop, receiver);
4061
+ let propCache = accessAnalysisCache.get(proto);
4062
+ if (!propCache) {
4063
+ propCache = new Map();
4064
+ accessAnalysisCache.set(proto, propCache);
4065
+ }
4066
+ const cached = propCache.get(prop);
4067
+ if (cached &&
4068
+ cached.ignoreAccessors === options.ignoreAccessors &&
4069
+ cached.instanceMembers === options.instanceMembers)
4070
+ return cached;
4071
+ const analysis = computeAccessAnalysis(obj, prop, receiver);
4072
+ if (analysis.hasProp)
4073
+ propCache.set(prop, analysis);
4074
+ return analysis;
4075
+ }
3769
4076
  const reactiveHandlers = {
3770
4077
  [Symbol.toStringTag]: 'MutTs Reactive',
3771
4078
  get(obj, prop, receiver) {
@@ -3793,36 +4100,29 @@ const reactiveHandlers = {
3793
4100
  // Symbols: fast-path — no reactivity tracking
3794
4101
  if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
3795
4102
  return FoolProof.get(obj, prop, receiver);
3796
- if (!getActiveEffect()) {
3797
- const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
4103
+ const subProxy = subsRegister.get(obj);
4104
+ if (exports.inertDepth > 0) {
4105
+ const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
3798
4106
  return wrapReactiveValue(obj, prop, value);
3799
4107
  }
3800
- // Check if property exists using a trap-free walk to avoid triggering
3801
- // the has-trap cascade on prototype chains of reactive proxies.
3802
- const isOwnProp = Object.hasOwn(obj, prop);
3803
- // For accessor properties, check the unwrapped object to see if it's an accessor
3804
- // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
3805
- // Skip for null-proto objects (pounce scopes) — they never have accessors
3806
- const shouldIgnoreAccessor = options.ignoreAccessors &&
3807
- isOwnProp &&
3808
- Object.getPrototypeOf(obj) !== null &&
3809
- (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
3810
- // Check if property exists using a trap-free walk to avoid triggering
3811
- // the has-trap cascade on prototype chains of reactive proxies.
3812
- let hasProp = isOwnProp;
3813
- let owner = isOwnProp ? obj : undefined;
3814
- if (!isOwnProp) {
3815
- let raw = Object.getPrototypeOf(obj);
3816
- while (raw && raw !== Object.prototype) {
3817
- if (Object.hasOwn(raw, prop)) {
3818
- hasProp = true;
3819
- owner = raw;
3820
- break;
4108
+ const activeEffect = getActiveEffect();
4109
+ if (!activeEffect) {
4110
+ const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
4111
+ return wrapReactiveValue(obj, prop, value);
4112
+ }
4113
+ if (!subProxy && !Array.isArray(obj)) {
4114
+ const proto = Object.getPrototypeOf(obj);
4115
+ if (proto === Object.prototype || proto === null) {
4116
+ const ownDesc = Object.getOwnPropertyDescriptor(obj, prop);
4117
+ if (ownDesc && 'value' in ownDesc) {
4118
+ dependant(obj, prop);
4119
+ return wrapReactiveValue(obj, prop, ownDesc.value);
3821
4120
  }
3822
- raw = Object.getPrototypeOf(raw);
3823
4121
  }
3824
4122
  }
3825
- const isInheritedAccess = hasProp && !isOwnProp;
4123
+ // Check if property exists using a trap-free walk to avoid triggering
4124
+ // the has-trap cascade on prototype chains of reactive proxies.
4125
+ const { hasProp, owner, isInheritedAccess, shouldIgnoreAccessor } = analyzeAccess(obj, prop, receiver);
3826
4126
  // Depend if...
3827
4127
  if (!hasProp ||
3828
4128
  (!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
@@ -3835,7 +4135,7 @@ const reactiveHandlers = {
3835
4135
  }
3836
4136
  // For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
3837
4137
  // For all other objects, inline Reflect.get directly (skips 3 function calls).
3838
- const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
4138
+ const value = (subProxy?.get || FoolProof.get)(obj, prop, receiver);
3839
4139
  return wrapReactiveValue(obj, prop, value);
3840
4140
  },
3841
4141
  set(obj, prop, value, receiver) {
@@ -3909,6 +4209,11 @@ const reactiveHandlers = {
3909
4209
  cycle: [], // We don't have the full cycle here, but we know it involves obj
3910
4210
  });
3911
4211
  hasReentry.add(obj);
4212
+ if (exports.inertDepth > 0) {
4213
+ const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
4214
+ hasReentry.delete(obj);
4215
+ return rv;
4216
+ }
3912
4217
  if (!internalUntracked && !isUnreactiveProp(obj, prop))
3913
4218
  dependant(obj, prop);
3914
4219
  const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
@@ -3917,7 +4222,7 @@ const reactiveHandlers = {
3917
4222
  },
3918
4223
  deleteProperty(obj, prop) {
3919
4224
  if (!Object.hasOwn(obj, prop))
3920
- return false;
4225
+ return true;
3921
4226
  const oldVal = obj[prop];
3922
4227
  // Remove back-references if this object has deep watchers
3923
4228
  if (objectsWithDeepWatchers.has(obj) && typeof oldVal === 'object' && oldVal !== null) {
@@ -3932,6 +4237,9 @@ const reactiveHandlers = {
3932
4237
  return true;
3933
4238
  },
3934
4239
  ownKeys(obj) {
4240
+ if (exports.inertDepth > 0) {
4241
+ return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
4242
+ }
3935
4243
  dependant(obj, keysOf);
3936
4244
  return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
3937
4245
  },
@@ -3940,6 +4248,98 @@ const reactiveHandlers = {
3940
4248
  Reflect.getOwnPropertyDescriptor(obj, prop));
3941
4249
  },
3942
4250
  };
4251
+ function readonlyError(prop) {
4252
+ return new ReactiveError(`[reactive] Cannot mutate readonly reactive property '${String(prop)}'`, {
4253
+ code: exports.ReactiveErrorCode.WriteInComputed,
4254
+ });
4255
+ }
4256
+ function readonlyValue(value) {
4257
+ if (!value || typeof value !== 'object')
4258
+ return value;
4259
+ return readonlyReactive(value);
4260
+ }
4261
+ const shallowReactiveHandlers = {
4262
+ get(obj, prop, receiver) {
4263
+ if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
4264
+ return Reflect.get(obj, prop, receiver);
4265
+ if (getActiveEffect())
4266
+ dependant(obj, prop);
4267
+ return Reflect.get(obj, prop, receiver);
4268
+ },
4269
+ set(obj, prop, value, receiver) {
4270
+ const unwrapped = unwrap(receiver);
4271
+ if (obj !== unwrapped)
4272
+ return Object.defineProperty(unwrapped, prop, {
4273
+ value,
4274
+ configurable: true,
4275
+ writable: true,
4276
+ enumerable: true,
4277
+ });
4278
+ if (isUnreactiveProp(obj, prop))
4279
+ return FoolProof.set(obj, prop, value, receiver);
4280
+ const hadProperty = Reflect.has(obj, prop);
4281
+ const oldVal = hadProperty ? Reflect.get(obj, prop, receiver) : absent;
4282
+ const newValue = unwrap(value);
4283
+ if (oldVal !== newValue && FoolProof.set(obj, prop, newValue, receiver)) {
4284
+ touched1(obj, { type: hadProperty ? 'set' : 'add', prop }, prop);
4285
+ }
4286
+ return true;
4287
+ },
4288
+ has(obj, prop) {
4289
+ return reactiveHandlers.has(obj, prop);
4290
+ },
4291
+ deleteProperty(obj, prop) {
4292
+ if (!Object.hasOwn(obj, prop))
4293
+ return true;
4294
+ delete obj[prop];
4295
+ touched1(obj, { type: 'del', prop }, prop);
4296
+ return true;
4297
+ },
4298
+ ownKeys(obj) {
4299
+ return reactiveHandlers.ownKeys(obj);
4300
+ },
4301
+ getOwnPropertyDescriptor(obj, prop) {
4302
+ return Reflect.getOwnPropertyDescriptor(obj, prop);
4303
+ },
4304
+ };
4305
+ const readonlyReactiveHandlers = {
4306
+ get(obj, prop, receiver) {
4307
+ if (readonlyMutators.has(prop)) {
4308
+ return () => {
4309
+ throw readonlyError(prop);
4310
+ };
4311
+ }
4312
+ const reactiveTarget = reactiveObject(obj);
4313
+ const value = FoolProof.get(reactiveTarget, prop, receiver);
4314
+ if (typeof value === 'function') {
4315
+ return (...args) => readonlyValue(value.apply(reactiveTarget, args));
4316
+ }
4317
+ return readonlyValue(value);
4318
+ },
4319
+ set(_obj, prop) {
4320
+ throw readonlyError(prop);
4321
+ },
4322
+ deleteProperty(_obj, prop) {
4323
+ throw readonlyError(prop);
4324
+ },
4325
+ defineProperty(_obj, prop) {
4326
+ throw readonlyError(prop);
4327
+ },
4328
+ setPrototypeOf() {
4329
+ throw readonlyError('[[Prototype]]');
4330
+ },
4331
+ has(obj, prop) {
4332
+ const reactiveTarget = reactiveObject(obj);
4333
+ return Reflect.has(reactiveTarget, prop);
4334
+ },
4335
+ ownKeys(obj) {
4336
+ const reactiveTarget = reactiveObject(obj);
4337
+ return Reflect.ownKeys(reactiveTarget);
4338
+ },
4339
+ getOwnPropertyDescriptor(obj, prop) {
4340
+ return Reflect.getOwnPropertyDescriptor(obj, prop);
4341
+ },
4342
+ };
3943
4343
  const reactiveClasses = new WeakSet();
3944
4344
  // Create the ReactiveBase mixin
3945
4345
  /**
@@ -3981,6 +4381,34 @@ function reactiveObject(anyTarget, subProxy) {
3981
4381
  storeProxyRelationship(target, proxy);
3982
4382
  return proxy;
3983
4383
  }
4384
+ function shallowReactiveObject(anyTarget) {
4385
+ if (!anyTarget || typeof anyTarget !== 'object')
4386
+ return anyTarget;
4387
+ const target = unwrap(anyTarget);
4388
+ if (isNonReactive(target))
4389
+ return target;
4390
+ const existing = shallowObjectToProxy.get(target);
4391
+ if (existing)
4392
+ return existing;
4393
+ const proxy = new Proxy(target, shallowReactiveHandlers);
4394
+ shallowObjectToProxy.set(target, proxy);
4395
+ proxyToObject.set(proxy, target);
4396
+ return proxy;
4397
+ }
4398
+ function readonlyReactiveObject(anyTarget) {
4399
+ if (!anyTarget || typeof anyTarget !== 'object')
4400
+ return anyTarget;
4401
+ const target = unwrap(anyTarget);
4402
+ if (isNonReactive(target))
4403
+ return target;
4404
+ const existing = readonlyObjectToProxy.get(target);
4405
+ if (existing)
4406
+ return existing;
4407
+ const proxy = new Proxy(target, readonlyReactiveHandlers);
4408
+ readonlyObjectToProxy.set(target, proxy);
4409
+ proxyToObject.set(proxy, target);
4410
+ return proxy;
4411
+ }
3984
4412
  /**
3985
4413
  * Main decorator for making classes reactive
3986
4414
  * Automatically makes class instances reactive when created
@@ -4011,6 +4439,8 @@ const reactive = decorator({
4011
4439
  },
4012
4440
  default: reactiveObject,
4013
4441
  });
4442
+ const shallowReactive = shallowReactiveObject;
4443
+ const readonlyReactive = readonlyReactiveObject;
4014
4444
 
4015
4445
  exports.AZone = AZone;
4016
4446
  exports.CompareSymbol = CompareSymbol;
@@ -4035,7 +4465,6 @@ exports.asyncHooks = asyncHooks;
4035
4465
  exports.asyncZone = asyncZone;
4036
4466
  exports.atom = atom;
4037
4467
  exports.atomic = atomic;
4038
- exports.batch = batch;
4039
4468
  exports.biDi = biDi;
4040
4469
  exports.captioned = captioned;
4041
4470
  exports.captured = captured;
@@ -4065,6 +4494,7 @@ exports.getEffectNode = getEffectNode;
4065
4494
  exports.getRoot = getRoot;
4066
4495
  exports.getState = getState;
4067
4496
  exports.hooks = hooks;
4497
+ exports.inert = inert;
4068
4498
  exports.inheritCaption = inheritCaption;
4069
4499
  exports.isConstructor = isConstructor;
4070
4500
  exports.isDev = isDev;
@@ -4072,10 +4502,13 @@ exports.isNonReactive = isNonReactive;
4072
4502
  exports.isObject = isObject;
4073
4503
  exports.isProd = isProd;
4074
4504
  exports.isReactive = isReactive;
4505
+ exports.isReactiveBroken = isReactiveBroken;
4075
4506
  exports.isTest = isTest;
4076
4507
  exports.keysOf = keysOf;
4077
4508
  exports.legacyDecorator = legacyDecorator;
4078
4509
  exports.link = link;
4510
+ exports.markRaw = markRaw;
4511
+ exports.markRawProps = markRawProps;
4079
4512
  exports.markWithRoot = markWithRoot;
4080
4513
  exports.metaProtos = metaProtos;
4081
4514
  exports.mixin = mixin;
@@ -4086,17 +4519,22 @@ exports.objectParents = objectParents;
4086
4519
  exports.objectToProxy = objectToProxy;
4087
4520
  exports.objectsWithDeepWatchers = objectsWithDeepWatchers;
4088
4521
  exports.onEffectThrow = onEffectThrow;
4522
+ exports.onReactiveBroken = onReactiveBroken;
4523
+ exports.onReactiveReset = onReactiveReset;
4089
4524
  exports.optionCall = optionCall;
4090
4525
  exports.options = options;
4091
4526
  exports.prodPreset = prodPreset;
4092
4527
  exports.proxyToObject = proxyToObject;
4093
4528
  exports.reactive = reactive;
4529
+ exports.readonlyReactive = readonlyReactive;
4094
4530
  exports.registerDeepWatcher = registerDeepWatcher;
4095
4531
  exports.reset = reset;
4096
4532
  exports.root = root;
4097
4533
  exports.rootFunctionSymbol = rootFunctionSymbol;
4098
4534
  exports.setDebugHooks = setDebugHooks;
4535
+ exports.shallowReactive = shallowReactive;
4099
4536
  exports.tag = tag;
4537
+ exports.toRaw = toRaw;
4100
4538
  exports.touched = touched;
4101
4539
  exports.touched1 = touched1;
4102
4540
  exports.unlink = unlink;
@@ -4104,6 +4542,7 @@ exports.unreactiveProperties = unreactiveProperties;
4104
4542
  exports.untracked = untracked;
4105
4543
  exports.unwrap = unwrap;
4106
4544
  exports.withEffectContext = withEffectContext;
4545
+ exports.wrapInert = wrapInert;
4107
4546
  exports.wrapProtos = wrapProtos;
4108
4547
  exports.zip = zip;
4109
- //# sourceMappingURL=proxy-DBHj3kGK.cjs.map
4548
+ //# sourceMappingURL=proxy-HA_QQnd5.cjs.map