mutts 1.0.10 → 1.0.11

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 (55) hide show
  1. package/README.md +3 -3
  2. package/dist/browser.cjs +395 -987
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +2 -2
  5. package/dist/browser.dev.cjs +13 -9
  6. package/dist/browser.dev.cjs.map +1 -1
  7. package/dist/browser.dev.d.ts +2 -2
  8. package/dist/browser.dev.esm.js +2 -2
  9. package/dist/browser.esm.js +15 -13
  10. package/dist/browser.esm.js.map +1 -1
  11. package/dist/chunks/{async-browser-BU_IfxYD.cjs → async-browser-Dgr5CreQ.cjs} +13 -11
  12. package/dist/chunks/async-browser-Dgr5CreQ.cjs.map +1 -0
  13. package/dist/chunks/{index-CaaQQlPJ.esm.js → index-Sf74wXTV.esm.js} +384 -981
  14. package/dist/chunks/index-Sf74wXTV.esm.js.map +1 -0
  15. package/dist/chunks/{node-nKJBk8iJ.esm.js → node-Bo7WU5S2.esm.js} +2 -2
  16. package/dist/chunks/{node-nKJBk8iJ.esm.js.map → node-Bo7WU5S2.esm.js.map} +1 -1
  17. package/dist/chunks/{proxy-Dtg-bJ3T.cjs → proxy-Cc79Lrzj.cjs} +414 -339
  18. package/dist/chunks/proxy-Cc79Lrzj.cjs.map +1 -0
  19. package/dist/chunks/{proxy-r7lARftl.esm.js → proxy-D2C49sXH.esm.js} +401 -330
  20. package/dist/chunks/proxy-D2C49sXH.esm.js.map +1 -0
  21. package/dist/debug.cjs +17 -3
  22. package/dist/debug.cjs.map +1 -1
  23. package/dist/debug.d.ts +2 -2
  24. package/dist/debug.esm.js +17 -3
  25. package/dist/debug.esm.js.map +1 -1
  26. package/dist/index.d.ts +84 -209
  27. package/dist/mutts.umd.js +842 -1362
  28. package/dist/mutts.umd.js.map +1 -1
  29. package/dist/mutts.umd.min.js +1 -1
  30. package/dist/mutts.umd.min.js.map +1 -1
  31. package/dist/node.cjs +13 -9
  32. package/dist/node.cjs.map +1 -1
  33. package/dist/node.d.ts +2 -2
  34. package/dist/node.dev.cjs +13 -9
  35. package/dist/node.dev.cjs.map +1 -1
  36. package/dist/node.dev.d.ts +2 -2
  37. package/dist/node.dev.esm.js +3 -3
  38. package/dist/node.esm.js +3 -3
  39. package/dist/{types-W5vD6m2n.d.ts → types-Bx2PhORg.d.ts} +38 -47
  40. package/docs/ai/api-reference.md +0 -14
  41. package/docs/ai/manual.md +2 -22
  42. package/docs/reactive/advanced.md +13 -14
  43. package/docs/reactive/attend.md +1 -2
  44. package/docs/reactive/collections.md +2 -149
  45. package/docs/reactive/core.md +218 -96
  46. package/docs/reactive/debugging.md +2 -2
  47. package/docs/reactive/resource.md +1 -1
  48. package/docs/reactive.md +1 -3
  49. package/docs/zone.md +1 -1
  50. package/package.json +18 -9
  51. package/dist/chunks/async-browser-BU_IfxYD.cjs.map +0 -1
  52. package/dist/chunks/index-CaaQQlPJ.esm.js.map +0 -1
  53. package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +0 -1
  54. package/dist/chunks/proxy-r7lARftl.esm.js.map +0 -1
  55. package/docs/reactive/scan.md +0 -324
@@ -2,6 +2,7 @@
2
2
 
3
3
  var asyncCore = require('./async-core-CRLKP3l-.cjs');
4
4
 
5
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
5
6
  /**
6
7
  * Yields tuples containing elements from each input array, stopping at the longest array length
7
8
  * @param args - Arrays to zip together
@@ -99,16 +100,16 @@ const FoolProof = {
99
100
  if (hasNode && obj instanceof Node) {
100
101
  obj[prop] = value;
101
102
  return true;
102
- }
103
- if (!(obj instanceof Object) && !Reflect.has(obj, prop)) {
103
+ } /*
104
+ if (!(obj instanceof Object) && !Object.hasOwn(obj, prop)) {
104
105
  Object.defineProperty(obj, prop, {
105
106
  value,
106
107
  configurable: true,
107
108
  writable: true,
108
109
  enumerable: true,
109
- });
110
- return true;
111
- }
110
+ })
111
+ return true
112
+ }*/
112
113
  return Reflect.set(obj, prop, value, receiver);
113
114
  },
114
115
  };
@@ -132,15 +133,8 @@ function deepCompare(a, b, cache = new Map()) {
132
133
  return a === b;
133
134
  }
134
135
  // Prototype check
135
- const protoA = Object.getPrototypeOf(a);
136
- const protoB = Object.getPrototypeOf(b);
137
- if (protoA !== protoB) {
138
- console.warn(`[deepCompare] prototype mismatch:`, {
139
- nameA: a?.constructor?.name,
140
- nameB: b?.constructor?.name,
141
- });
136
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
142
137
  return false;
143
- }
144
138
  // Circular reference protection
145
139
  let compared = cache.get(a);
146
140
  if (compared?.has(b))
@@ -152,39 +146,21 @@ function deepCompare(a, b, cache = new Map()) {
152
146
  compared.add(b);
153
147
  // Handle specific object types
154
148
  if (Array.isArray(a)) {
155
- if (!Array.isArray(b)) {
156
- console.warn(`[deepCompare] B is not an array`);
157
- return false;
158
- }
159
- if (a.length !== b.length) {
160
- console.warn(`[deepCompare] array length mismatch:`, { lenA: a.length, lenB: b.length });
149
+ if (!Array.isArray(b) || a.length !== b.length)
161
150
  return false;
162
- }
163
151
  for (let i = 0; i < a.length; i++) {
164
- if (!deepCompare(a[i], b[i], cache)) {
165
- console.warn(`[deepCompare] array element mismatch at index ${i}`);
152
+ if (!deepCompare(a[i], b[i], cache))
166
153
  return false;
167
- }
168
154
  }
169
155
  return true;
170
156
  }
171
- if (a instanceof Date) {
172
- const match = b instanceof Date && a.getTime() === b.getTime();
173
- if (!match)
174
- console.warn(`[deepCompare] Date mismatch`);
175
- return match;
176
- }
177
- if (a instanceof RegExp) {
178
- const match = b instanceof RegExp && a.toString() === b.toString();
179
- if (!match)
180
- console.warn(`[deepCompare] RegExp mismatch`);
181
- return match;
182
- }
157
+ if (a instanceof Date)
158
+ return b instanceof Date && a.getTime() === b.getTime();
159
+ if (a instanceof RegExp)
160
+ return b instanceof RegExp && a.toString() === b.toString();
183
161
  if (a instanceof Set) {
184
- if (!(b instanceof Set) || a.size !== b.size) {
185
- console.warn(`[deepCompare] Set size mismatch`);
162
+ if (!(b instanceof Set) || a.size !== b.size)
186
163
  return false;
187
- }
188
164
  for (const val of a) {
189
165
  let found = false;
190
166
  for (const bVal of b) {
@@ -193,18 +169,14 @@ function deepCompare(a, b, cache = new Map()) {
193
169
  break;
194
170
  }
195
171
  }
196
- if (!found) {
197
- console.warn(`[deepCompare] missing Set element`);
172
+ if (!found)
198
173
  return false;
199
- }
200
174
  }
201
175
  return true;
202
176
  }
203
177
  if (a instanceof Map) {
204
- if (!(b instanceof Map) || a.size !== b.size) {
205
- console.warn(`[deepCompare] Map size mismatch`);
178
+ if (!(b instanceof Map) || a.size !== b.size)
206
179
  return false;
207
- }
208
180
  for (const [key, val] of a) {
209
181
  if (!b.has(key)) {
210
182
  let foundMatch = false;
@@ -214,16 +186,11 @@ function deepCompare(a, b, cache = new Map()) {
214
186
  break;
215
187
  }
216
188
  }
217
- if (!foundMatch) {
218
- console.warn(`[deepCompare] missing Map key`);
189
+ if (!foundMatch)
219
190
  return false;
220
- }
221
191
  }
222
- else {
223
- if (!deepCompare(val, b.get(key), cache)) {
224
- console.warn(`[deepCompare] Map value mismatch for key`);
225
- return false;
226
- }
192
+ else if (!deepCompare(val, b.get(key), cache)) {
193
+ return false;
227
194
  }
228
195
  }
229
196
  return true;
@@ -231,29 +198,11 @@ function deepCompare(a, b, cache = new Map()) {
231
198
  // Compare own properties
232
199
  const keysA = Object.keys(a);
233
200
  const keysB = Object.keys(b);
234
- if (keysA.length !== keysB.length) {
235
- console.warn(`[deepCompare] keys length mismatch:`, {
236
- lenA: keysA.length,
237
- lenB: keysB.length,
238
- keysA,
239
- keysB,
240
- a,
241
- b,
242
- });
201
+ if (keysA.length !== keysB.length)
243
202
  return false;
244
- }
245
203
  for (const key of keysA) {
246
- if (!Object.hasOwn(b, key)) {
247
- console.warn(`[deepCompare] missing key ${String(key)} in B`);
204
+ if (!Object.hasOwn(b, key) || !deepCompare(a[key], b[key], cache))
248
205
  return false;
249
- }
250
- if (!deepCompare(a[key], b[key], cache)) {
251
- console.warn(`[deepCompare] value mismatch for key ${String(key)}:`, {
252
- valA: a[key],
253
- valB: b[key],
254
- });
255
- return false;
256
- }
257
206
  }
258
207
  return true;
259
208
  }
@@ -301,6 +250,12 @@ function named(name, fn) {
301
250
  });
302
251
  return fn;
303
252
  }
253
+ const _mode = (typeof process !== 'undefined' && process.env?.NODE_ENV) ||
254
+ (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunks/proxy-Cc79Lrzj.cjs', document.baseURI).href)) }) !== 'undefined' && undefined?.MODE) ||
255
+ 'production';
256
+ const isDev = _mode === 'development';
257
+ const isProd = _mode === 'production';
258
+ const isTest = _mode === 'test';
304
259
 
305
260
  // biome-ignore-all lint/suspicious/noConfusingVoidType: We *love* voids
306
261
  // Standardized decorator system that works with both Legacy and Modern decorators
@@ -528,9 +483,7 @@ function flavorOptions(fn, defaultOptions, opts = {}) {
528
483
  const isObject = currentOptions !== null &&
529
484
  typeof currentOptions === 'object' &&
530
485
  !Array.isArray(currentOptions);
531
- newArgs[targetIndex] = isObject
532
- ? { ...defaultOptions, ...currentOptions }
533
- : defaultOptions;
486
+ newArgs[targetIndex] = isObject ? { ...defaultOptions, ...currentOptions } : defaultOptions;
534
487
  return fn.apply(this, newArgs);
535
488
  };
536
489
  if (opts.name)
@@ -871,6 +824,20 @@ function mixin(mixinFunction, unwrapFunction) {
871
824
  });
872
825
  }
873
826
 
827
+ const debugHooks = {
828
+ isDevtoolsEnabled: () => false,
829
+ registerEffect: () => { },
830
+ getTriggerChain: () => [],
831
+ captureStack: () => [],
832
+ captureLineage: () => new Error().stack,
833
+ formatStack: (stack) => [stack],
834
+ recordTriggerLink: () => { },
835
+ decorateError: () => { },
836
+ };
837
+ function setDebugHooks(hooks) {
838
+ Object.assign(debugHooks, hooks);
839
+ }
840
+
874
841
  /******************************************************************************
875
842
  Copyright (c) Microsoft Corporation.
876
843
 
@@ -922,41 +889,17 @@ function __runInitializers(thisArg, initializers, value) {
922
889
  }
923
890
  return useValue ? value : void 0;
924
891
  }
925
- function __setFunctionName(f, name, prefix) {
926
- if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
927
- return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
928
- }
929
892
  function __classPrivateFieldGet(receiver, state, kind, f) {
930
893
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
931
894
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
932
895
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
933
896
  }
934
897
 
935
- function __classPrivateFieldSet(receiver, state, value, kind, f) {
936
- if (kind === "m") throw new TypeError("Private method is not writable");
937
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
938
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
939
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
940
- }
941
-
942
898
  typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
943
899
  var e = new Error(message);
944
900
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
945
901
  };
946
902
 
947
- const debugHooks = {
948
- isDevtoolsEnabled: () => false,
949
- registerEffect: () => { },
950
- getTriggerChain: () => [],
951
- captureStack: () => [],
952
- captureLineage: () => new Error().stack,
953
- formatStack: (stack) => [stack],
954
- recordTriggerLink: () => { },
955
- };
956
- function setDebugHooks(hooks) {
957
- Object.assign(debugHooks, hooks);
958
- }
959
-
960
903
  var _ZoneAggregator_zones;
961
904
  function isu(z) {
962
905
  return z;
@@ -1084,13 +1027,27 @@ class ZoneAggregator extends AZone {
1084
1027
  }
1085
1028
  }
1086
1029
  _ZoneAggregator_zones = new WeakMap();
1030
+ /**
1031
+ * Aggregator of zones that should be preserved across async boundaries.
1032
+ * If you add a zone here, it will be preserved across async boundaries.
1033
+ *
1034
+ * @example
1035
+ * ```ts
1036
+ * import { Zone, asyncZone } from 'mutts'
1037
+ * const userZone = new Zone<User>()
1038
+ * asyncZone.add(userZone)
1039
+ * ```
1040
+ */
1087
1041
  const asyncZone = tag('async', new ZoneAggregator());
1088
1042
  asyncCore.asyncHooks.addHook(() => {
1043
+ // capture state before async boundary
1089
1044
  const zone = asyncZone.active;
1090
1045
  return () => {
1046
+ // restore state after async boundary, temporarily
1091
1047
  const prev = asyncZone.active;
1092
1048
  asyncZone.active = zone;
1093
1049
  return () => {
1050
+ // restore previous state from before our restore
1094
1051
  asyncZone.active = prev;
1095
1052
  };
1096
1053
  };
@@ -1160,6 +1117,75 @@ function getRoot(fn) {
1160
1117
  return fn;
1161
1118
  }
1162
1119
 
1120
+ const effectHistory = tag('effectHistory', new ZoneHistory());
1121
+ tag('effectHistory.present', effectHistory.present);
1122
+ asyncZone.add(effectHistory);
1123
+ /**
1124
+ * Aggregator for zones that need to be tracked along effects.
1125
+ * ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
1126
+ */
1127
+ const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
1128
+ function isRunning(effect) {
1129
+ const root = getRoot(effect);
1130
+ return effectHistory.some((e) => getRoot(e) === root);
1131
+ }
1132
+ function getActiveEffect() {
1133
+ return effectHistory.present.active;
1134
+ }
1135
+ const cleanups = new WeakMap();
1136
+ /**
1137
+ * Attach cleanup dependencies to an object. When `unlink(obj)` is called,
1138
+ * each dependency is disposed: functions are invoked with the cleanup reason,
1139
+ * objects are recursively `unlink`ed. This forms a cleanup tree.
1140
+ *
1141
+ * @param obj - The owner object
1142
+ * @param cleanupFns - Cleanup callbacks and/or child objects to unlink recursively
1143
+ * @returns The owner object (for chaining)
1144
+ *
1145
+ * @example
1146
+ * ```ts
1147
+ * // Functions are called with CleanupReason
1148
+ * link(parent, () => console.log('disposed'))
1149
+ *
1150
+ * // Objects are recursively unlinked
1151
+ * link(parent, childA, childB)
1152
+ *
1153
+ * // Mixed
1154
+ * link(parent, childObj, () => timer.clear())
1155
+ *
1156
+ * unlink(parent) // disposes childA, childB, calls the function
1157
+ * ```
1158
+ */
1159
+ function link(obj, ...cleanupFns) {
1160
+ const set = cleanups.get(obj);
1161
+ if (!set)
1162
+ cleanups.set(obj, new Set(cleanupFns.filter(Boolean)));
1163
+ else
1164
+ for (const fn of cleanupFns)
1165
+ if (fn)
1166
+ set.add(fn);
1167
+ return obj;
1168
+ }
1169
+ /**
1170
+ * Dispose an object's cleanup dependencies. Functions are called with the
1171
+ * reason; linked objects are recursively unlinked. The cleanup set is removed
1172
+ * so calling `unlink` twice is safe (second call is a no-op).
1173
+ *
1174
+ * @param obj - The object to dispose
1175
+ * @param reason - Optional cleanup reason propagated to callbacks
1176
+ */
1177
+ function unlink(obj, reason) {
1178
+ const set = cleanups.get(obj);
1179
+ if (set) {
1180
+ cleanups.delete(obj);
1181
+ for (const fn of set)
1182
+ if (typeof fn === 'function')
1183
+ fn(reason);
1184
+ else
1185
+ unlink(fn, reason);
1186
+ }
1187
+ }
1188
+
1163
1189
  const effectMarker = {
1164
1190
  enter: 'effect:enter',
1165
1191
  leave: 'effect:leave',
@@ -1222,6 +1248,11 @@ function formatCleanupReason(reason, depth = 0) {
1222
1248
  }
1223
1249
  }
1224
1250
  }
1251
+ // Track native reactivity
1252
+ /**
1253
+ * Symbol to mark class properties as non-reactive
1254
+ */
1255
+ const unreactiveProperties = Symbol('unreactive-properties');
1225
1256
  /**
1226
1257
  * Symbol representing all properties in reactive tracking
1227
1258
  */
@@ -1231,14 +1262,6 @@ const allProps = Symbol('all-props');
1231
1262
  * Used by ownKeys proxy trap — Object.keys(), for..in, Map.keys() depend on this.
1232
1263
  */
1233
1264
  const keysOf = Symbol('keys-of');
1234
- /**
1235
- * Symbol to check if an effect is stopped
1236
- */
1237
- const stopped = Symbol('stopped');
1238
- /**
1239
- * Symbol to access effect cleanup function
1240
- */
1241
- const cleanup = Symbol('cleanup');
1242
1265
  /**
1243
1266
  * Structured error codes for machine-readable diagnosis
1244
1267
  */
@@ -1312,6 +1335,12 @@ const options = {
1312
1335
  * @param runningChain - The array of effects from the detected one to the currently running one
1313
1336
  */
1314
1337
  skipRunningEffect: (_effect) => { },
1338
+ /**
1339
+ * Debug purpose: called when an effect starts executing.
1340
+ * @param effect - The effect being executed (root function)
1341
+ * @param reaction - false for initial creation, true/CleanupReason for subsequent runs
1342
+ */
1343
+ effectRun: (_effect, _reaction) => { },
1315
1344
  /**
1316
1345
  * Debug purpose: maximum effect chain (like call stack max depth)
1317
1346
  * Used to prevent infinite loops
@@ -1438,34 +1467,6 @@ const options = {
1438
1467
  enableHistory: true,
1439
1468
  historySize: 50,
1440
1469
  },
1441
- /**
1442
- * Configuration for zone hooks - control which async APIs are hooked
1443
- * Each option controls whether the corresponding async API is wrapped to preserve effect context
1444
- * Only applies when asyncMode is enabled (truthy)
1445
- * @deprecated Should take all when we made sure PIXI.create, Game.create, ... are -> .root()
1446
- */
1447
- zones: {
1448
- /**
1449
- * Hook setTimeout to preserve effect context
1450
- * @default true
1451
- */
1452
- setTimeout: true,
1453
- /**
1454
- * Hook setInterval to preserve effect context
1455
- * @default true
1456
- */
1457
- setInterval: true,
1458
- /**
1459
- * Hook requestAnimationFrame (runs in untracked context when hooked)
1460
- * @default true
1461
- */
1462
- requestAnimationFrame: true,
1463
- /**
1464
- * Hook queueMicrotask to preserve effect context
1465
- * @default true
1466
- */
1467
- queueMicrotask: true,
1468
- },
1469
1470
  };
1470
1471
  function optionCall(name, ...args) {
1471
1472
  const fn = options[name];
@@ -1479,6 +1480,36 @@ function optionCall(name, ...args) {
1479
1480
  options.warn(`options.${name} threw`, error);
1480
1481
  }
1481
1482
  }
1483
+ /** Production preset: no introspection, heuristic cycle detection, minimal overhead */
1484
+ const prodPreset = {
1485
+ maxEffectReaction: 'throw',
1486
+ cycleHandling: 'production',
1487
+ introspection: null,
1488
+ onMemoizationDiscrepancy: undefined,
1489
+ };
1490
+ /** Development preset (default): introspection on, early cycle detection, warnings */
1491
+ const devPreset = {
1492
+ maxEffectReaction: 'warn',
1493
+ cycleHandling: 'development',
1494
+ introspection: {
1495
+ gatherReasons: { lineages: 'touch' },
1496
+ logErrors: true,
1497
+ enableHistory: true,
1498
+ historySize: 50,
1499
+ },
1500
+ onMemoizationDiscrepancy: undefined,
1501
+ };
1502
+ /** Debug preset: full diagnostics, throws on violations, rich lineage capture */
1503
+ const debugPreset = {
1504
+ maxEffectReaction: 'debug',
1505
+ cycleHandling: 'debug',
1506
+ introspection: {
1507
+ gatherReasons: { lineages: 'both' },
1508
+ logErrors: true,
1509
+ enableHistory: true,
1510
+ historySize: 200,
1511
+ },
1512
+ };
1482
1513
  // --- Proxy State (Merged from proxy-state.ts) ---
1483
1514
  const objectToProxy = new WeakMap();
1484
1515
  const proxyToObject = new WeakMap();
@@ -1498,54 +1529,28 @@ function isReactive(obj) {
1498
1529
  return proxyToObject.has(obj);
1499
1530
  }
1500
1531
 
1501
- const effectHistory = tag('effectHistory', new ZoneHistory());
1502
- tag('effectHistory.present', effectHistory.present);
1503
- asyncZone.add(effectHistory);
1504
- /**
1505
- * Aggregator for zones that need to be tracked along effects.
1506
- * ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
1507
- */
1508
- const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
1509
- function isRunning(effect) {
1510
- const root = getRoot(effect);
1511
- return effectHistory.some((e) => getRoot(e) === root);
1512
- }
1513
- function getActiveEffect() {
1514
- return effectHistory.present.active;
1515
- }
1516
- /**
1517
- * ADD a cleanup function to an object using the cleanup symbol.
1518
- * The cleanup function will be called when the object needs to be disposed.
1519
- *
1520
- * Note: most of the time, you don't need to use this function directly.
1521
- * The main use if for the cleanup function to be stored with the object, as GC calls the cleanup function when the *function* is garbage collected.
1522
- *
1523
- * @param obj - The object to attach the cleanup function to
1524
- * @param cleanupFn - The cleanup function to attach
1525
- * @returns The object with the cleanup function attached
1526
- */
1527
- function cleanedBy(obj, cleanupFn) {
1528
- const oldCleanup = obj[cleanup];
1529
- return Object.defineProperty(obj, cleanup, {
1530
- value: oldCleanup
1531
- ? Object.defineProperties((reason) => {
1532
- oldCleanup(reason);
1533
- cleanupFn(reason);
1534
- }, {
1535
- [stopped]: { get: () => oldCleanup[stopped] || cleanupFn[stopped] },
1536
- })
1537
- : cleanupFn,
1538
- writable: false,
1539
- enumerable: false,
1540
- configurable: true,
1541
- });
1542
- }
1543
-
1544
1532
  // Track dependency stacks per (obj, prop, effect)
1545
1533
  let dependencyStacks = new WeakMap();
1534
+ let assertUntrackedFlag = false;
1546
1535
  function resetTracking() {
1547
1536
  dependencyStacks = new WeakMap();
1548
1537
  }
1538
+ /**
1539
+ * Executes a function and throws if any reactive dependencies are tracked during execution.
1540
+ * Used to assert that code runs in an untracked context.
1541
+ */
1542
+ function assertUntracked(fn) {
1543
+ if (assertUntrackedFlag) {
1544
+ throw new Error('assertUntracked: nested calls are not supported');
1545
+ }
1546
+ assertUntrackedFlag = true;
1547
+ try {
1548
+ return fn();
1549
+ }
1550
+ finally {
1551
+ assertUntrackedFlag = false;
1552
+ }
1553
+ }
1549
1554
  function getDependencyStack(effect, obj, prop) {
1550
1555
  const objStacks = dependencyStacks.get(obj);
1551
1556
  if (!objStacks)
@@ -1558,14 +1563,17 @@ function getDependencyStack(effect, obj, prop) {
1558
1563
  * @param prop - The property name (defaults to allProps)
1559
1564
  */
1560
1565
  function dependant(obj, prop = allProps) {
1566
+ if (assertUntrackedFlag) {
1567
+ throw new Error(`Reactive dependency tracking detected in assertUntracked context: ${String(prop)} on ${obj}`);
1568
+ }
1561
1569
  obj = unwrap(obj);
1562
1570
  const currentActiveEffect = getActiveEffect();
1563
1571
  // Early return if no active effect, tracking disabled, or invalid prop
1564
1572
  if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps && prop !== keysOf))
1565
1573
  return;
1566
- if ('dependencyHook' in currentActiveEffect) {
1567
- // @ts-expect-error We declared it nowhere - it's okay as it's really internal and for edge-case debug purpose only
1568
- currentActiveEffect.dependencyHook(obj, prop);
1574
+ const node = getEffectNode(currentActiveEffect);
1575
+ if ('dependencyHook' in node) {
1576
+ node.dependencyHook(obj, prop);
1569
1577
  }
1570
1578
  let objectWatchers = exports.watchers.get(obj);
1571
1579
  if (!objectWatchers) {
@@ -1702,6 +1710,7 @@ let effectTriggeredBy = new WeakMap();
1702
1710
  // consequencesClosure: for each effect, all effects that it triggers (directly or indirectly)
1703
1711
  let causesClosure = new WeakMap();
1704
1712
  let consequencesClosure = new WeakMap();
1713
+ // Batch re-entrance depth and broken state
1705
1714
  let broken = false;
1706
1715
  /**
1707
1716
  * Gets or creates an IterableWeakSet for a closure map
@@ -2355,6 +2364,7 @@ function batch(effect, immediate) {
2355
2364
  throw new Error('Activation registry already exists');
2356
2365
  optionCall('beginChain', roots);
2357
2366
  }
2367
+ // TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
2358
2368
  const caller = getActiveEffect();
2359
2369
  // Optimization: If nested and NOT immediate, just join the existing batch
2360
2370
  if (!isNewBatch && !immediate) {
@@ -2508,6 +2518,22 @@ const atomic = decorator({
2508
2518
  };
2509
2519
  },
2510
2520
  });
2521
+ /**
2522
+ * Wraps `fn` so it runs within `effect`'s zone context when invoked later.
2523
+ *
2524
+ * Useful for deferred callbacks (event listeners, `DOMContentLoaded`, etc.)
2525
+ * that need sub-effects parented to the original effect.
2526
+ *
2527
+ * @param prev - The effect whose context should be restored, or `undefined` for root context
2528
+ * @param fn - The function to wrap
2529
+ * @returns A function with the same signature that restores the effect context before calling `fn`
2530
+ */
2531
+ function captured(prev, fn) {
2532
+ prev ?? (prev = effectHistory.active);
2533
+ return named(effectMarker.leave, (...args) => {
2534
+ return effectHistory.with(prev, () => fn(...args));
2535
+ });
2536
+ }
2511
2537
  /**
2512
2538
  * Runs `fn` atomically and **always immediately**, batching all reactive effects
2513
2539
  * triggered inside it so they fire only once after `fn` completes.
@@ -2530,19 +2556,13 @@ function atom(fn) {
2530
2556
  return batch(fn, 'immediate');
2531
2557
  }
2532
2558
  const fr = new FinalizationRegistry((f) => f());
2533
- /**
2534
- * @param fn - The effect function to run - provides the cleaner
2535
- * @returns The cleanup function
2536
- */
2537
2559
  /**
2538
2560
  * Creates a reactive effect that automatically re-runs when dependencies change
2539
2561
  * @param fn - The effect function that provides dependencies and may return a cleanup function or Promise
2540
2562
  * @param options - Options for effect execution
2541
2563
  * @returns A cleanup function to stop the effect
2542
2564
  */
2543
- const effect = named(effectMarker.leave, flavored(function effect(
2544
- // biome-ignore lint/suspicious/noConfusingVoidType: Effect callbacks commonly return void
2545
- fn, effectOptions = {}) {
2565
+ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
2546
2566
  if (effectOptions?.name)
2547
2567
  Object.defineProperty(fn, 'name', { value: effectOptions.name });
2548
2568
  // Use per-effect asyncMode or fall back to global option
@@ -2566,6 +2586,7 @@ fn, effectOptions = {}) {
2566
2586
  if (runningPromise) {
2567
2587
  if (asyncMode === 'cancel' && cancelPrevious) {
2568
2588
  // Cancel previous execution
2589
+ abort();
2569
2590
  cancelPrevious();
2570
2591
  cancelPrevious = null;
2571
2592
  runningPromise = null;
@@ -2589,11 +2610,33 @@ fn, effectOptions = {}) {
2589
2610
  access.reaction = node.nextReason || access.reaction;
2590
2611
  node.nextReason = undefined;
2591
2612
  optionCall('enter', getRoot(fn));
2613
+ optionCall('effectRun', getRoot(fn), access.reaction);
2592
2614
  let result;
2593
2615
  let caught = 0;
2594
- // Default thrower (self)
2595
- let thrower = (error) => {
2596
- throw error;
2616
+ // Define bubbling thrower
2617
+ const thrower = (error) => {
2618
+ const catches = node.catchers;
2619
+ const reason = { type: 'error', error };
2620
+ if (catches)
2621
+ while (caught < catches.length) {
2622
+ cleanupReaction(reason);
2623
+ try {
2624
+ reactionCleanup = catches[caught](error);
2625
+ return;
2626
+ }
2627
+ catch (_e) {
2628
+ caught++;
2629
+ }
2630
+ }
2631
+ if (parent) {
2632
+ const parentNode = getEffectNode(parent);
2633
+ if (parentNode.forwardThrow)
2634
+ parentNode.forwardThrow(error);
2635
+ else
2636
+ throw error;
2637
+ }
2638
+ else
2639
+ throw error;
2597
2640
  };
2598
2641
  node.forwardThrow = thrower;
2599
2642
  let errorToThrow;
@@ -2643,12 +2686,14 @@ fn, effectOptions = {}) {
2643
2686
  }
2644
2687
  }
2645
2688
  catch (error) {
2689
+ debugHooks.decorateError(error, runEffect);
2646
2690
  // catcher:self`
2647
2691
  errorToThrow = error;
2648
2692
  }
2649
2693
  // Create cleanup function for next run
2650
2694
  node.cleanup = (reason) => {
2651
2695
  node.cleanup = undefined;
2696
+ abort();
2652
2697
  cleanupReaction(reason);
2653
2698
  delete node.catchers;
2654
2699
  // Remove this effect from all reactive objects it's watching
@@ -2659,13 +2704,11 @@ fn, effectOptions = {}) {
2659
2704
  if (objectWatchers) {
2660
2705
  for (const [prop, deps] of objectWatchers.entries()) {
2661
2706
  deps.delete(runEffect);
2662
- if (deps.size === 0) {
2707
+ if (deps.size === 0)
2663
2708
  objectWatchers.delete(prop);
2664
- }
2665
2709
  }
2666
- if (objectWatchers.size === 0) {
2710
+ if (objectWatchers.size === 0)
2667
2711
  exports.watchers.delete(reactiveObj);
2668
- }
2669
2712
  }
2670
2713
  }
2671
2714
  exports.effectToReactiveObjects.delete(runEffect);
@@ -2683,33 +2726,6 @@ fn, effectOptions = {}) {
2683
2726
  delete node.children;
2684
2727
  }
2685
2728
  };
2686
- // Define bubbling thrower
2687
- thrower = (error) => {
2688
- const catches = node.catchers;
2689
- const reason = { type: 'error', error };
2690
- if (catches)
2691
- while (caught < catches.length) {
2692
- cleanupReaction(reason);
2693
- try {
2694
- reactionCleanup = catches[caught](error);
2695
- return;
2696
- }
2697
- catch (e) {
2698
- caught++;
2699
- }
2700
- }
2701
- if (parent) {
2702
- const parentNode = getEffectNode(parent);
2703
- if (parentNode.forwardThrow)
2704
- parentNode.forwardThrow(error);
2705
- else
2706
- throw error;
2707
- }
2708
- else
2709
- throw error;
2710
- };
2711
- // Update the node's forwardThrow to the bubbling one
2712
- node.forwardThrow = thrower;
2713
2729
  if (errorToThrow)
2714
2730
  thrower(errorToThrow);
2715
2731
  };
@@ -2728,26 +2744,25 @@ fn, effectOptions = {}) {
2728
2744
  node.parent = parent;
2729
2745
  // let thrower: CatchFunction | undefined // Moved inside runEffect
2730
2746
  let effectStopped = false;
2747
+ let abortController;
2731
2748
  const access = {
2732
2749
  tracked,
2733
2750
  ascend: named(effectMarker.leave, (fn) => ascended(named(effectMarker.enter, () => fn.call(null)))),
2734
2751
  //named(effectMarker.enter, (fn) => ascended(fn)),
2735
2752
  reaction: false,
2753
+ get signal() {
2754
+ if (!abortController) {
2755
+ abortController = new AbortController();
2756
+ }
2757
+ return abortController.signal;
2758
+ },
2736
2759
  };
2737
2760
  let runningPromise = null;
2738
2761
  let cancelPrevious = null;
2739
- if (effectOptions?.dependencyHook) {
2762
+ if (effectOptions?.dependencyHook)
2740
2763
  node.dependencyHook = effectOptions.dependencyHook;
2741
- }
2742
2764
  // Mark the runEffect callback with the original function as its root
2743
2765
  markWithRoot(runEffect, fn);
2744
- function augmentedRv(rv) {
2745
- return Object.defineProperties(rv, {
2746
- [stopped]: {
2747
- get: () => effectStopped,
2748
- },
2749
- });
2750
- }
2751
2766
  // Register strict mode if enabled
2752
2767
  if (effectOptions?.opaque) {
2753
2768
  node.isOpaque = true;
@@ -2756,6 +2771,12 @@ fn, effectOptions = {}) {
2756
2771
  debugHooks.registerEffect(runEffect);
2757
2772
  }
2758
2773
  // Store parent relationship for hierarchy traversal - ALREADY DONE ABOVE via getEffectNode
2774
+ const abort = () => {
2775
+ if (abortController) {
2776
+ abortController.abort(new ReactiveError('[reactive] Effect aborted due to dependency change or stop'));
2777
+ abortController = undefined;
2778
+ }
2779
+ };
2759
2780
  batch(runEffect, 'immediate');
2760
2781
  // Only ROOT effects are registered for GC cleanup and zone tracking
2761
2782
  const isRootEffect = !parent;
@@ -2765,6 +2786,7 @@ fn, effectOptions = {}) {
2765
2786
  effectStopped = true;
2766
2787
  node.stopped = true;
2767
2788
  // Cancel any running async work
2789
+ abort();
2768
2790
  if (cancelPrevious) {
2769
2791
  cancelPrevious();
2770
2792
  cancelPrevious = null;
@@ -2783,7 +2805,7 @@ fn, effectOptions = {}) {
2783
2805
  fr.unregister(stopEffect);
2784
2806
  };
2785
2807
  if (isRootEffect) {
2786
- const callIfCollected = augmentedRv((reason) => stopEffect(reason));
2808
+ const callIfCollected = (reason) => stopEffect(reason);
2787
2809
  fr.register(callIfCollected, () => {
2788
2810
  stopEffect({ type: 'gc' });
2789
2811
  optionCall('garbageCollected', fn);
@@ -2797,16 +2819,16 @@ fn, effectOptions = {}) {
2797
2819
  parentNode.children = new Set();
2798
2820
  }
2799
2821
  const children = parentNode.children;
2800
- const subEffectCleanup = augmentedRv((reason) => {
2822
+ const subEffectCleanup = (reason) => {
2801
2823
  children.delete(subEffectCleanup);
2802
2824
  // Execute this child effect cleanup (which triggers its own mainCleanup)
2803
2825
  stopEffect(reason);
2804
- });
2826
+ };
2805
2827
  children.add(subEffectCleanup);
2806
2828
  return subEffectCleanup;
2807
2829
  }
2808
2830
  // Should not be reachable given isRootEffect check, but for type safety
2809
- return augmentedRv((reason) => stopEffect(reason));
2831
+ return (reason) => stopEffect(reason);
2810
2832
  }, {
2811
2833
  get opaque() {
2812
2834
  return flavorOptions(this, { opaque: true }, { name: 'opaque' });
@@ -2837,9 +2859,11 @@ function biDi(received, get, set) {
2837
2859
  get = get.get;
2838
2860
  }
2839
2861
  let programmaticallySetValue = Symbol();
2840
- effect(markWithRoot(() => {
2862
+ effect.named('biDi')(markWithRoot(() => {
2841
2863
  const newValue = get();
2842
- if (unwrap(newValue) !== programmaticallySetValue)
2864
+ const pValue = programmaticallySetValue;
2865
+ programmaticallySetValue = Symbol();
2866
+ if (unwrap(newValue) !== pValue)
2843
2867
  received(newValue);
2844
2868
  }, received));
2845
2869
  return set
@@ -2910,16 +2934,16 @@ function bubbleUpChange(changedObject, evolution) {
2910
2934
  const parents = objectParents.get(changedObject);
2911
2935
  if (!parents)
2912
2936
  return;
2913
- for (const { parent, prop } of parents) {
2937
+ for (const { parent } of parents) {
2914
2938
  // Trigger deep watchers on parent
2915
2939
  const parentDeepWatchers = deepWatchers.get(parent);
2916
2940
  if (parentDeepWatchers) {
2917
2941
  if (options.introspection?.gatherReasons) {
2918
2942
  const gatherReasons = options.introspection.gatherReasons;
2919
2943
  const lineageConfig = gatherReasons.lineages;
2920
- let touchStack;
2944
+ let touchLineage;
2921
2945
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
2922
- touchStack = debugHooks.captureLineage();
2946
+ touchLineage = debugHooks.captureLineage();
2923
2947
  }
2924
2948
  for (const watcher of parentDeepWatchers) {
2925
2949
  const dependencyStack = lineageConfig === 'dependency' || lineageConfig === 'both'
@@ -2932,7 +2956,7 @@ function bubbleUpChange(changedObject, evolution) {
2932
2956
  obj: parent,
2933
2957
  evolution,
2934
2958
  dependency: dependencyStack,
2935
- touch: touchStack,
2959
+ touch: touchLineage,
2936
2960
  });
2937
2961
  }
2938
2962
  }
@@ -2985,6 +3009,8 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
2985
3009
  for (const key of keys) {
2986
3010
  const deps = objectWatchers.get(key);
2987
3011
  if (deps) {
3012
+ // Make sure `some.prop++` does not keep a dependency to `some.props`
3013
+ deps.delete(sourceEffect);
2988
3014
  for (const effect of deps) {
2989
3015
  const runningChain = isRunning(effect);
2990
3016
  if (runningChain) {
@@ -3035,9 +3061,9 @@ function touched(obj, evolution, props) {
3035
3061
  if (options.introspection?.gatherReasons) {
3036
3062
  const gatherReasons = options.introspection.gatherReasons;
3037
3063
  const lineageConfig = gatherReasons.lineages;
3038
- let touchStack;
3064
+ let touchLineage;
3039
3065
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3040
- touchStack = debugHooks.captureLineage();
3066
+ touchLineage = debugHooks.captureLineage();
3041
3067
  }
3042
3068
  for (const [effect, dependencyStack] of effects) {
3043
3069
  const node = getEffectNode(effect);
@@ -3047,7 +3073,7 @@ function touched(obj, evolution, props) {
3047
3073
  obj,
3048
3074
  evolution,
3049
3075
  dependency: dependencyStack,
3050
- touch: touchStack,
3076
+ touch: touchLineage,
3051
3077
  });
3052
3078
  }
3053
3079
  }
@@ -3086,10 +3112,10 @@ function touchedOpaque(obj, evolution, prop) {
3086
3112
  }
3087
3113
  effects.add(effect);
3088
3114
  if (gather) {
3089
- let touchStack;
3115
+ let touchLineage;
3090
3116
  let dependencyStack;
3091
3117
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3092
- touchStack = debugHooks.captureLineage();
3118
+ touchLineage = debugHooks.captureLineage();
3093
3119
  }
3094
3120
  if (lineageConfig === 'dependency' || lineageConfig === 'both') {
3095
3121
  dependencyStack = getDependencyStack(effect, obj, prop);
@@ -3100,7 +3126,7 @@ function touchedOpaque(obj, evolution, prop) {
3100
3126
  obj,
3101
3127
  evolution,
3102
3128
  dependency: dependencyStack,
3103
- touch: touchStack,
3129
+ touch: touchLineage,
3104
3130
  });
3105
3131
  }
3106
3132
  recordActivation(effect, obj, evolution, prop);
@@ -3129,59 +3155,60 @@ function touchedOpaque(obj, evolution, prop) {
3129
3155
  }
3130
3156
  }
3131
3157
 
3132
- const nonReactiveObjects = new WeakSet();
3133
- const nonReactiveClasses = new WeakSet();
3134
- const unreactiveProps = new WeakMap();
3135
- let unreactivePropsCount = 0;
3158
+ const absent = Symbol('absent');
3159
+ /**
3160
+ * Add unreactive properties to a prototype.
3161
+ * If no set is provided, marks the entire object/prototype as non-reactive (sets [unreactiveProperties] = true).
3162
+ * If a set is provided, merges with existing unreactive properties (never overrides true).
3163
+ */
3136
3164
  function addUnreactiveProps(proto, set) {
3137
- unreactiveProps.set(proto, set);
3138
- unreactivePropsCount++;
3165
+ if (unreactiveProperties in proto) {
3166
+ const existing = proto[unreactiveProperties];
3167
+ // If already fully unreactive, don't change
3168
+ if (existing === true)
3169
+ return proto;
3170
+ // If no set provided, upgrade to fully unreactive
3171
+ if (!set) {
3172
+ proto[unreactiveProperties] = true;
3173
+ return proto;
3174
+ }
3175
+ // Merge sets
3176
+ set = proto[unreactiveProperties] = new Set(proto[unreactiveProperties]);
3177
+ for (const p of set)
3178
+ existing.add(p);
3179
+ }
3180
+ // If no set, mark as fully unreactive, otherwise create set
3181
+ else
3182
+ proto[unreactiveProperties] = set ? new Set(set) : true;
3183
+ return proto;
3139
3184
  }
3140
3185
  /** Check if a property is marked unreactive on obj or any of its prototypes (trap-free) */
3141
3186
  function isUnreactiveProp(obj, prop) {
3142
- if (!unreactivePropsCount)
3143
- return false;
3144
- let target = obj;
3145
- while (target) {
3146
- if (unreactiveProps.get(target)?.has(prop))
3147
- return true;
3148
- target = Object.getPrototypeOf(target);
3149
- }
3150
- return false;
3187
+ if (typeof prop === 'symbol' || prop === 'constructor')
3188
+ return true;
3189
+ const marker = obj[unreactiveProperties];
3190
+ return (marker === true || // Fully unreactive
3191
+ marker?.has?.(prop) || // Property is unreactive
3192
+ false);
3151
3193
  }
3152
- const immutables = new Set();
3153
- const absent = Symbol('absent');
3154
- function markNonReactive(...obj) {
3155
- for (const o of obj)
3156
- nonReactiveObjects.add(o);
3194
+ function nonReactive(...obj) {
3195
+ for (const o of obj) {
3196
+ o[unreactiveProperties] = true;
3197
+ }
3157
3198
  return obj[0];
3158
3199
  }
3159
3200
  function nonReactiveClass(...cls) {
3160
3201
  for (const c of cls)
3161
3202
  if (c)
3162
- nonReactiveClasses.add(c.prototype);
3203
+ c.prototype[unreactiveProperties] = true;
3163
3204
  return cls[0];
3164
3205
  }
3165
3206
  function isNonReactive(obj) {
3166
- if (obj === null || typeof obj !== 'object')
3167
- return true;
3168
- if (nonReactiveObjects.has(obj))
3169
- return true;
3170
- // Walk the prototype chain on the raw object to check for non-reactive classes
3171
- let proto = Object.getPrototypeOf(obj);
3172
- while (proto) {
3173
- if (nonReactiveClasses.has(proto))
3174
- return true;
3175
- proto = Object.getPrototypeOf(proto);
3176
- }
3177
- for (const fn of immutables)
3178
- if (fn(obj))
3179
- return true;
3180
- return false;
3207
+ return !obj || obj[unreactiveProperties] === true;
3181
3208
  }
3182
3209
  nonReactiveClass(Date, RegExp, Error, Promise, Function);
3183
3210
  if (typeof window !== 'undefined') {
3184
- markNonReactive(window, document);
3211
+ nonReactive(window, document);
3185
3212
  nonReactiveClass(Node, Element, HTMLElement, EventTarget, HTMLCollection, NodeList);
3186
3213
  }
3187
3214
 
@@ -3203,10 +3230,33 @@ function shouldRecurseTouch(oldValue, newValue) {
3203
3230
  if ((typeof oldValue !== 'object' && !Array.isArray(oldValue)) ||
3204
3231
  (typeof newValue !== 'object' && !Array.isArray(newValue)))
3205
3232
  return false;
3206
- if (isNonReactive(oldValue) || isNonReactive(newValue))
3233
+ if (isNonReactive(oldValue) /*|| isNonReactive(newValue)*/)
3207
3234
  return false;
3208
3235
  return getPrototypeToken(oldValue) === getPrototypeToken(newValue);
3209
3236
  }
3237
+ /**
3238
+ * Migrate all watcher registrations from oldRef to newRef.
3239
+ * Called when deep touch replaces an object identity without any child value differences,
3240
+ * to prevent watcher orphaning (effects still pointing at the discarded old object).
3241
+ */
3242
+ function migrateWatchers(oldRef, newRef) {
3243
+ const oldMap = exports.watchers.get(oldRef);
3244
+ if (!oldMap)
3245
+ return;
3246
+ // Move the entire watcher map
3247
+ exports.watchers.set(newRef, oldMap);
3248
+ exports.watchers.delete(oldRef);
3249
+ // Update the reverse map (effect → objects it watches)
3250
+ for (const deps of oldMap.values()) {
3251
+ for (const effect of deps) {
3252
+ const objects = exports.effectToReactiveObjects.get(effect);
3253
+ if (objects) {
3254
+ objects.delete(oldRef);
3255
+ objects.add(newRef);
3256
+ }
3257
+ }
3258
+ }
3259
+ }
3210
3260
  /**
3211
3261
  * Centralized function to handle property change notifications with optional recursive touch
3212
3262
  * @param targetObj - The object whose property changed
@@ -3224,7 +3274,15 @@ function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty)
3224
3274
  const origin = { obj: unwrappedObj, prop };
3225
3275
  // Deep touch: only notify nested property changes with origin filtering
3226
3276
  // Don't notify direct property change - the whole point is to avoid parent effects re-running
3227
- dispatchNotifications(untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin)));
3277
+ const changes = untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
3278
+ // When deep touch found no child differences, the object identity still changed.
3279
+ // Migrate watchers from old → new so the dependency chain is preserved.
3280
+ if (changes.length === 0) {
3281
+ migrateWatchers(unwrap(oldValue), unwrap(newValue));
3282
+ }
3283
+ else {
3284
+ dispatchNotifications(changes);
3285
+ }
3228
3286
  // Notify opaque listeners (like memoize) that always want to know about identity changes
3229
3287
  touchedOpaque(targetObj, evolution, prop);
3230
3288
  }
@@ -3311,12 +3369,11 @@ function diffObjectProperties(oldObj, newObj, visited, notifications, origin) {
3311
3369
  for (const key of oldKeys)
3312
3370
  if (!newKeys.has(key))
3313
3371
  local.push({ target: oldObj, evolution: { type: 'del', prop: key }, prop: key, origin });
3314
- for (const key of newKeys)
3315
- if (!oldKeys.has(key))
3316
- local.push({ target: oldObj, evolution: { type: 'add', prop: key }, prop: key, origin });
3317
3372
  for (const key of newKeys) {
3318
- if (!oldKeys.has(key))
3373
+ if (!oldKeys.has(key)) {
3374
+ local.push({ target: oldObj, evolution: { type: 'add', prop: key }, prop: key, origin });
3319
3375
  continue;
3376
+ }
3320
3377
  const oldEntry = unwrap(oldObj[key]);
3321
3378
  const newEntry = unwrap(newObj[key]);
3322
3379
  if (shouldRecurseTouch(oldEntry, newEntry)) {
@@ -3358,11 +3415,10 @@ function dispatchNotifications(notifications) {
3358
3415
  if (originWatchers) {
3359
3416
  const originEffects = new Map();
3360
3417
  collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
3361
- for (const effect of originEffects.keys())
3362
- allowedEffects.add(effect);
3418
+ allowedEffects = new Set(originEffects.keys());
3363
3419
  }
3364
3420
  // If no allowed effects, skip all notifications (no one should be notified)
3365
- if (allowedEffects.size === 0)
3421
+ if (!allowedEffects?.size)
3366
3422
  return;
3367
3423
  }
3368
3424
  for (const notification of notifications) {
@@ -3375,7 +3431,6 @@ function dispatchNotifications(notifications) {
3375
3431
  let currentEffects;
3376
3432
  const propsArray = [prop];
3377
3433
  if (objectWatchers) {
3378
- // console.log(`[DEBUG] dispatchNotifications: processing ${obj.constructor.name} (has watchers)`)
3379
3434
  currentEffects = new Map();
3380
3435
  const broad = evolution.type !== 'set' ? [allProps, keysOf] : [allProps];
3381
3436
  collectEffects(obj, evolution, currentEffects, objectWatchers, broad, propsArray);
@@ -3411,9 +3466,9 @@ function dispatchNotifications(notifications) {
3411
3466
  if (options.introspection?.gatherReasons) {
3412
3467
  const gatherReasons = options.introspection.gatherReasons;
3413
3468
  const lineageConfig = gatherReasons.lineages;
3414
- let touchStack;
3469
+ let touchLineage;
3415
3470
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3416
- touchStack = debugHooks.captureLineage();
3471
+ touchLineage = debugHooks.captureLineage();
3417
3472
  }
3418
3473
  for (const effect of combinedEffects) {
3419
3474
  const node = getEffectNode(effect);
@@ -3427,7 +3482,7 @@ function dispatchNotifications(notifications) {
3427
3482
  obj: unwrap(target),
3428
3483
  evolution,
3429
3484
  dependency: dependencyStack,
3430
- touch: touchStack,
3485
+ touch: touchLineage,
3431
3486
  });
3432
3487
  }
3433
3488
  }
@@ -3440,11 +3495,16 @@ const metaProtos = new WeakMap();
3440
3495
  const wrapProtos = new WeakMap();
3441
3496
  const arrayLengths = new WeakMap();
3442
3497
  const hasReentry = new Set();
3443
- const subsRegister = new WeakMap();
3444
3498
  // Sub-proxy registration for custom reactive behaviors
3499
+ const subsRegister = new WeakMap();
3500
+ // Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
3501
+ // TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
3502
+ let internalUntracked = false;
3445
3503
  const reactiveHandlers = {
3446
3504
  [Symbol.toStringTag]: 'MutTs Reactive',
3447
3505
  get(obj, prop, receiver) {
3506
+ if (internalUntracked)
3507
+ return FoolProof.get(obj, prop, receiver);
3448
3508
  if (obj && typeof obj === 'object' && prop !== Symbol.toStringTag) {
3449
3509
  const metaProto = metaProtos.get(obj.constructor);
3450
3510
  if (metaProto && Object.hasOwn(metaProto, prop)) {
@@ -3464,15 +3524,21 @@ const reactiveHandlers = {
3464
3524
  if (wrapProto && Object.hasOwn(wrapProto, prop))
3465
3525
  return wrapProto[prop];
3466
3526
  }
3467
- // Symbols: fast-path — no reactivity tracking, no unreactive check needed
3468
- if (typeof prop === 'symbol')
3469
- return FoolProof.get(obj, prop, receiver);
3470
- // Check if this property is marked as unreactive (WeakMap lookup — no proxy traps)
3471
- if (isUnreactiveProp(obj, prop))
3527
+ // Symbols: fast-path — no reactivity tracking
3528
+ if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
3472
3529
  return FoolProof.get(obj, prop, receiver);
3473
3530
  // Check if property exists using a trap-free walk to avoid triggering
3474
3531
  // the has-trap cascade on prototype chains of reactive proxies.
3475
3532
  const isOwnProp = Object.hasOwn(obj, prop);
3533
+ // For accessor properties, check the unwrapped object to see if it's an accessor
3534
+ // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
3535
+ // Skip for null-proto objects (pounce scopes) — they never have accessors
3536
+ const shouldIgnoreAccessor = options.ignoreAccessors &&
3537
+ isOwnProp &&
3538
+ Object.getPrototypeOf(obj) !== null &&
3539
+ (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
3540
+ // Check if property exists using a trap-free walk to avoid triggering
3541
+ // the has-trap cascade on prototype chains of reactive proxies.
3476
3542
  let hasProp = isOwnProp;
3477
3543
  let owner = isOwnProp ? obj : undefined;
3478
3544
  if (!isOwnProp) {
@@ -3487,29 +3553,20 @@ const reactiveHandlers = {
3487
3553
  }
3488
3554
  }
3489
3555
  const isInheritedAccess = hasProp && !isOwnProp;
3490
- // For accessor properties, check the unwrapped object to see if it's an accessor
3491
- // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
3492
- // Skip for null-proto objects (pounce scopes) — they never have accessors
3493
- const shouldIgnoreAccessor = options.ignoreAccessors &&
3494
- isOwnProp &&
3495
- Object.getPrototypeOf(obj) !== null &&
3496
- (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
3497
3556
  // Depend if...
3498
3557
  if (!hasProp ||
3499
3558
  (!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
3500
3559
  !shouldIgnoreAccessor))
3501
3560
  dependant(obj, prop);
3502
- // Two-Point Tracking: for inherited access on null-proto chains, only track
3503
- // the owning ancestor not every intermediate level. This relies on the
3504
- // "structural stability" contract: key presence in the chain is fixed at
3505
- // creation time, so intermediate levels never gain/lose shadowing properties.
3561
+ // Two-Point Tracking: for inherited access on null-proto chains, also track
3562
+ // the owning ancestor so that writing directly to it triggers dependent effects.
3506
3563
  if (isInheritedAccess && owner && (!options.instanceMembers || !(obj instanceof Object))) {
3507
3564
  dependant(owner, prop);
3508
3565
  }
3509
3566
  // For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
3510
3567
  // For all other objects, inline Reflect.get directly (skips 3 function calls).
3511
3568
  const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
3512
- if (typeof value === 'object' && value !== null) {
3569
+ if (!isReactive(value) && typeof value === 'object' && value !== null) {
3513
3570
  const reactiveValue = reactiveObject(value);
3514
3571
  // Only create back-references if this object needs them
3515
3572
  if (needsBackReferences(obj)) {
@@ -3520,9 +3577,19 @@ const reactiveHandlers = {
3520
3577
  return value;
3521
3578
  },
3522
3579
  set(obj, prop, value, receiver) {
3523
- const unwrappedReceiver = unwrap(receiver);
3580
+ const unwrapped = unwrap(receiver);
3581
+ if (obj !== unwrapped)
3582
+ return Object.defineProperty(unwrapped, prop, {
3583
+ value,
3584
+ configurable: true,
3585
+ writable: true,
3586
+ enumerable: true,
3587
+ });
3588
+ if (internalUntracked)
3589
+ throw new Error('Internal untracked: setting a value in an getter in a set operation');
3590
+ //return FoolProof.set(obj, prop, value, receiver)
3524
3591
  // Check if this property is marked as unreactive
3525
- if (isUnreactiveProp(obj, prop) || obj !== unwrappedReceiver)
3592
+ if (isUnreactiveProp(obj, prop))
3526
3593
  return FoolProof.set(obj, prop, value, receiver);
3527
3594
  const newValue = unwrap(value);
3528
3595
  // metaProto setter dispatch (e.g., reactive array length)
@@ -3539,16 +3606,19 @@ const reactiveHandlers = {
3539
3606
  // Read old value, using withEffect(undefined, ...) for getter-only accessors to avoid
3540
3607
  // breaking memoization dependency tracking during SET operations
3541
3608
  let oldVal = absent;
3542
- // TODO: Pffft... Find a way to "generalize" this case?
3543
3609
  const isArrayLength = prop === 'length' && Array.isArray(obj);
3544
- if (Reflect.has(unwrappedReceiver, prop)) {
3545
- // We *need* to use `receiver` and not `obj` here, otherwise we break
3546
- // the dependency tracking for memoized getters
3547
- oldVal = isArrayLength
3548
- ? arrayLengths.get(obj) === newValue
3549
- ? newValue
3550
- : absent
3551
- : untracked(() => Reflect.get(obj, prop, receiver));
3610
+ internalUntracked = true;
3611
+ try {
3612
+ if (Reflect.has(obj, prop)) {
3613
+ oldVal = isArrayLength
3614
+ ? arrayLengths.get(obj) === newValue
3615
+ ? newValue
3616
+ : absent
3617
+ : Reflect.get(obj, prop, receiver);
3618
+ }
3619
+ }
3620
+ finally {
3621
+ internalUntracked = false;
3552
3622
  }
3553
3623
  if (objectsWithDeepWatchers.has(obj)) {
3554
3624
  if (typeof oldVal === 'object' && oldVal !== null) {
@@ -3577,7 +3647,8 @@ const reactiveHandlers = {
3577
3647
  cycle: [], // We don't have the full cycle here, but we know it involves obj
3578
3648
  });
3579
3649
  hasReentry.add(obj);
3580
- dependant(obj, prop);
3650
+ if (!internalUntracked && !isUnreactiveProp(obj, prop))
3651
+ dependant(obj, prop);
3581
3652
  const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
3582
3653
  hasReentry.delete(obj);
3583
3654
  return rv;
@@ -3603,7 +3674,8 @@ const reactiveHandlers = {
3603
3674
  return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
3604
3675
  },
3605
3676
  getOwnPropertyDescriptor(obj, prop) {
3606
- return subsRegister.get(obj)?.getOwnPropertyDescriptor?.(obj, prop) || Reflect.getOwnPropertyDescriptor(obj, prop);
3677
+ return (subsRegister.get(obj)?.getOwnPropertyDescriptor?.(obj, prop) ||
3678
+ Reflect.getOwnPropertyDescriptor(obj, prop));
3607
3679
  },
3608
3680
  };
3609
3681
  const reactiveClasses = new WeakSet();
@@ -3688,31 +3760,32 @@ exports.ReactiveError = ReactiveError;
3688
3760
  exports.Zone = Zone;
3689
3761
  exports.ZoneAggregator = ZoneAggregator;
3690
3762
  exports.ZoneHistory = ZoneHistory;
3691
- exports.__classPrivateFieldGet = __classPrivateFieldGet;
3692
- exports.__classPrivateFieldSet = __classPrivateFieldSet;
3693
3763
  exports.__esDecorate = __esDecorate;
3694
3764
  exports.__runInitializers = __runInitializers;
3695
- exports.__setFunctionName = __setFunctionName;
3696
3765
  exports.addBatchCleanup = addBatchCleanup;
3697
3766
  exports.addUnreactiveProps = addUnreactiveProps;
3698
3767
  exports.allProps = allProps;
3699
3768
  exports.arrayEquals = arrayEquals;
3769
+ exports.assertUntracked = assertUntracked;
3700
3770
  exports.asyncZone = asyncZone;
3701
3771
  exports.atom = atom;
3702
3772
  exports.atomic = atomic;
3773
+ exports.batch = batch;
3703
3774
  exports.biDi = biDi;
3775
+ exports.captured = captured;
3704
3776
  exports.caught = caught;
3705
- exports.cleanedBy = cleanedBy;
3706
- exports.cleanup = cleanup;
3707
3777
  exports.contentRef = contentRef;
3708
3778
  exports.createFlavor = createFlavor;
3779
+ exports.debugPreset = debugPreset;
3709
3780
  exports.decorator = decorator;
3710
3781
  exports.deepCompare = deepCompare;
3711
3782
  exports.deepWatchers = deepWatchers;
3712
3783
  exports.defer = defer;
3713
3784
  exports.dependant = dependant;
3785
+ exports.devPreset = devPreset;
3714
3786
  exports.effect = effect;
3715
3787
  exports.effectAggregator = effectAggregator;
3788
+ exports.effectHistory = effectHistory;
3716
3789
  exports.effectMarker = effectMarker;
3717
3790
  exports.effectToDeepWatchedObjects = effectToDeepWatchedObjects;
3718
3791
  exports.flavorOptions = flavorOptions;
@@ -3723,20 +3796,21 @@ exports.getActiveEffect = getActiveEffect;
3723
3796
  exports.getEffectNode = getEffectNode;
3724
3797
  exports.getRoot = getRoot;
3725
3798
  exports.getState = getState;
3726
- exports.immutables = immutables;
3727
3799
  exports.isConstructor = isConstructor;
3800
+ exports.isDev = isDev;
3728
3801
  exports.isNonReactive = isNonReactive;
3729
3802
  exports.isObject = isObject;
3803
+ exports.isProd = isProd;
3730
3804
  exports.isReactive = isReactive;
3805
+ exports.isTest = isTest;
3731
3806
  exports.keysOf = keysOf;
3732
3807
  exports.legacyDecorator = legacyDecorator;
3808
+ exports.link = link;
3733
3809
  exports.markWithRoot = markWithRoot;
3734
3810
  exports.metaProtos = metaProtos;
3735
3811
  exports.mixin = mixin;
3736
3812
  exports.modernDecorator = modernDecorator;
3737
3813
  exports.named = named;
3738
- exports.nonReactiveClass = nonReactiveClass;
3739
- exports.nonReactiveObjects = nonReactiveObjects;
3740
3814
  exports.notifyPropertyChange = notifyPropertyChange;
3741
3815
  exports.objectParents = objectParents;
3742
3816
  exports.objectToProxy = objectToProxy;
@@ -3744,6 +3818,7 @@ exports.objectsWithDeepWatchers = objectsWithDeepWatchers;
3744
3818
  exports.onEffectThrow = onEffectThrow;
3745
3819
  exports.optionCall = optionCall;
3746
3820
  exports.options = options;
3821
+ exports.prodPreset = prodPreset;
3747
3822
  exports.proxyToObject = proxyToObject;
3748
3823
  exports.reactive = reactive;
3749
3824
  exports.registerDeepWatcher = registerDeepWatcher;
@@ -3751,13 +3826,13 @@ exports.reset = reset;
3751
3826
  exports.root = root;
3752
3827
  exports.rootFunctionSymbol = rootFunctionSymbol;
3753
3828
  exports.setDebugHooks = setDebugHooks;
3754
- exports.stopped = stopped;
3755
3829
  exports.tag = tag;
3756
3830
  exports.touched = touched;
3757
3831
  exports.touched1 = touched1;
3758
- exports.unreactiveProps = unreactiveProps;
3832
+ exports.unlink = unlink;
3833
+ exports.unreactiveProperties = unreactiveProperties;
3759
3834
  exports.untracked = untracked;
3760
3835
  exports.unwrap = unwrap;
3761
3836
  exports.wrapProtos = wrapProtos;
3762
3837
  exports.zip = zip;
3763
- //# sourceMappingURL=proxy-Dtg-bJ3T.cjs.map
3838
+ //# sourceMappingURL=proxy-Cc79Lrzj.cjs.map