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
@@ -118,16 +118,16 @@ const FoolProof = {
118
118
  if (hasNode && obj instanceof Node) {
119
119
  obj[prop] = value;
120
120
  return true;
121
- }
122
- if (!(obj instanceof Object) && !Reflect.has(obj, prop)) {
121
+ } /*
122
+ if (!(obj instanceof Object) && !Object.hasOwn(obj, prop)) {
123
123
  Object.defineProperty(obj, prop, {
124
124
  value,
125
125
  configurable: true,
126
126
  writable: true,
127
127
  enumerable: true,
128
- });
129
- return true;
130
- }
128
+ })
129
+ return true
130
+ }*/
131
131
  return Reflect.set(obj, prop, value, receiver);
132
132
  },
133
133
  };
@@ -151,15 +151,8 @@ function deepCompare(a, b, cache = new Map()) {
151
151
  return a === b;
152
152
  }
153
153
  // Prototype check
154
- const protoA = Object.getPrototypeOf(a);
155
- const protoB = Object.getPrototypeOf(b);
156
- if (protoA !== protoB) {
157
- console.warn(`[deepCompare] prototype mismatch:`, {
158
- nameA: a?.constructor?.name,
159
- nameB: b?.constructor?.name,
160
- });
154
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
161
155
  return false;
162
- }
163
156
  // Circular reference protection
164
157
  let compared = cache.get(a);
165
158
  if (compared?.has(b))
@@ -171,39 +164,21 @@ function deepCompare(a, b, cache = new Map()) {
171
164
  compared.add(b);
172
165
  // Handle specific object types
173
166
  if (Array.isArray(a)) {
174
- if (!Array.isArray(b)) {
175
- console.warn(`[deepCompare] B is not an array`);
176
- return false;
177
- }
178
- if (a.length !== b.length) {
179
- console.warn(`[deepCompare] array length mismatch:`, { lenA: a.length, lenB: b.length });
167
+ if (!Array.isArray(b) || a.length !== b.length)
180
168
  return false;
181
- }
182
169
  for (let i = 0; i < a.length; i++) {
183
- if (!deepCompare(a[i], b[i], cache)) {
184
- console.warn(`[deepCompare] array element mismatch at index ${i}`);
170
+ if (!deepCompare(a[i], b[i], cache))
185
171
  return false;
186
- }
187
172
  }
188
173
  return true;
189
174
  }
190
- if (a instanceof Date) {
191
- const match = b instanceof Date && a.getTime() === b.getTime();
192
- if (!match)
193
- console.warn(`[deepCompare] Date mismatch`);
194
- return match;
195
- }
196
- if (a instanceof RegExp) {
197
- const match = b instanceof RegExp && a.toString() === b.toString();
198
- if (!match)
199
- console.warn(`[deepCompare] RegExp mismatch`);
200
- return match;
201
- }
175
+ if (a instanceof Date)
176
+ return b instanceof Date && a.getTime() === b.getTime();
177
+ if (a instanceof RegExp)
178
+ return b instanceof RegExp && a.toString() === b.toString();
202
179
  if (a instanceof Set) {
203
- if (!(b instanceof Set) || a.size !== b.size) {
204
- console.warn(`[deepCompare] Set size mismatch`);
180
+ if (!(b instanceof Set) || a.size !== b.size)
205
181
  return false;
206
- }
207
182
  for (const val of a) {
208
183
  let found = false;
209
184
  for (const bVal of b) {
@@ -212,18 +187,14 @@ function deepCompare(a, b, cache = new Map()) {
212
187
  break;
213
188
  }
214
189
  }
215
- if (!found) {
216
- console.warn(`[deepCompare] missing Set element`);
190
+ if (!found)
217
191
  return false;
218
- }
219
192
  }
220
193
  return true;
221
194
  }
222
195
  if (a instanceof Map) {
223
- if (!(b instanceof Map) || a.size !== b.size) {
224
- console.warn(`[deepCompare] Map size mismatch`);
196
+ if (!(b instanceof Map) || a.size !== b.size)
225
197
  return false;
226
- }
227
198
  for (const [key, val] of a) {
228
199
  if (!b.has(key)) {
229
200
  let foundMatch = false;
@@ -233,16 +204,11 @@ function deepCompare(a, b, cache = new Map()) {
233
204
  break;
234
205
  }
235
206
  }
236
- if (!foundMatch) {
237
- console.warn(`[deepCompare] missing Map key`);
207
+ if (!foundMatch)
238
208
  return false;
239
- }
240
209
  }
241
- else {
242
- if (!deepCompare(val, b.get(key), cache)) {
243
- console.warn(`[deepCompare] Map value mismatch for key`);
244
- return false;
245
- }
210
+ else if (!deepCompare(val, b.get(key), cache)) {
211
+ return false;
246
212
  }
247
213
  }
248
214
  return true;
@@ -250,29 +216,11 @@ function deepCompare(a, b, cache = new Map()) {
250
216
  // Compare own properties
251
217
  const keysA = Object.keys(a);
252
218
  const keysB = Object.keys(b);
253
- if (keysA.length !== keysB.length) {
254
- console.warn(`[deepCompare] keys length mismatch:`, {
255
- lenA: keysA.length,
256
- lenB: keysB.length,
257
- keysA,
258
- keysB,
259
- a,
260
- b,
261
- });
219
+ if (keysA.length !== keysB.length)
262
220
  return false;
263
- }
264
221
  for (const key of keysA) {
265
- if (!Object.hasOwn(b, key)) {
266
- console.warn(`[deepCompare] missing key ${String(key)} in B`);
222
+ if (!Object.hasOwn(b, key) || !deepCompare(a[key], b[key], cache))
267
223
  return false;
268
- }
269
- if (!deepCompare(a[key], b[key], cache)) {
270
- console.warn(`[deepCompare] value mismatch for key ${String(key)}:`, {
271
- valA: a[key],
272
- valB: b[key],
273
- });
274
- return false;
275
- }
276
224
  }
277
225
  return true;
278
226
  }
@@ -320,6 +268,12 @@ function named(name, fn) {
320
268
  });
321
269
  return fn;
322
270
  }
271
+ const _mode = (typeof process !== 'undefined' && process.env?.NODE_ENV) ||
272
+ (typeof import.meta !== 'undefined' && import.meta.env?.MODE) ||
273
+ 'production';
274
+ const isDev = _mode === 'development';
275
+ const isProd = _mode === 'production';
276
+ const isTest = _mode === 'test';
323
277
 
324
278
  // biome-ignore-all lint/suspicious/noConfusingVoidType: We *love* voids
325
279
  // Standardized decorator system that works with both Legacy and Modern decorators
@@ -547,9 +501,7 @@ function flavorOptions(fn, defaultOptions, opts = {}) {
547
501
  const isObject = currentOptions !== null &&
548
502
  typeof currentOptions === 'object' &&
549
503
  !Array.isArray(currentOptions);
550
- newArgs[targetIndex] = isObject
551
- ? { ...defaultOptions, ...currentOptions }
552
- : defaultOptions;
504
+ newArgs[targetIndex] = isObject ? { ...defaultOptions, ...currentOptions } : defaultOptions;
553
505
  return fn.apply(this, newArgs);
554
506
  };
555
507
  if (opts.name)
@@ -890,6 +842,20 @@ function mixin(mixinFunction, unwrapFunction) {
890
842
  });
891
843
  }
892
844
 
845
+ const debugHooks = {
846
+ isDevtoolsEnabled: () => false,
847
+ registerEffect: () => { },
848
+ getTriggerChain: () => [],
849
+ captureStack: () => [],
850
+ captureLineage: () => new Error().stack,
851
+ formatStack: (stack) => [stack],
852
+ recordTriggerLink: () => { },
853
+ decorateError: () => { },
854
+ };
855
+ function setDebugHooks(hooks) {
856
+ Object.assign(debugHooks, hooks);
857
+ }
858
+
893
859
  /******************************************************************************
894
860
  Copyright (c) Microsoft Corporation.
895
861
 
@@ -941,41 +907,17 @@ function __runInitializers(thisArg, initializers, value) {
941
907
  }
942
908
  return useValue ? value : void 0;
943
909
  }
944
- function __setFunctionName(f, name, prefix) {
945
- if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
946
- return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
947
- }
948
910
  function __classPrivateFieldGet(receiver, state, kind, f) {
949
911
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
950
912
  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");
951
913
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
952
914
  }
953
915
 
954
- function __classPrivateFieldSet(receiver, state, value, kind, f) {
955
- if (kind === "m") throw new TypeError("Private method is not writable");
956
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
957
- 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");
958
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
959
- }
960
-
961
916
  typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
962
917
  var e = new Error(message);
963
918
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
964
919
  };
965
920
 
966
- const debugHooks = {
967
- isDevtoolsEnabled: () => false,
968
- registerEffect: () => { },
969
- getTriggerChain: () => [],
970
- captureStack: () => [],
971
- captureLineage: () => new Error().stack,
972
- formatStack: (stack) => [stack],
973
- recordTriggerLink: () => { },
974
- };
975
- function setDebugHooks(hooks) {
976
- Object.assign(debugHooks, hooks);
977
- }
978
-
979
921
  var _ZoneAggregator_zones;
980
922
  function isu(z) {
981
923
  return z;
@@ -1103,13 +1045,27 @@ class ZoneAggregator extends AZone {
1103
1045
  }
1104
1046
  }
1105
1047
  _ZoneAggregator_zones = new WeakMap();
1048
+ /**
1049
+ * Aggregator of zones that should be preserved across async boundaries.
1050
+ * If you add a zone here, it will be preserved across async boundaries.
1051
+ *
1052
+ * @example
1053
+ * ```ts
1054
+ * import { Zone, asyncZone } from 'mutts'
1055
+ * const userZone = new Zone<User>()
1056
+ * asyncZone.add(userZone)
1057
+ * ```
1058
+ */
1106
1059
  const asyncZone = tag('async', new ZoneAggregator());
1107
1060
  asyncHooks.addHook(() => {
1061
+ // capture state before async boundary
1108
1062
  const zone = asyncZone.active;
1109
1063
  return () => {
1064
+ // restore state after async boundary, temporarily
1110
1065
  const prev = asyncZone.active;
1111
1066
  asyncZone.active = zone;
1112
1067
  return () => {
1068
+ // restore previous state from before our restore
1113
1069
  asyncZone.active = prev;
1114
1070
  };
1115
1071
  };
@@ -1179,6 +1135,75 @@ function getRoot(fn) {
1179
1135
  return fn;
1180
1136
  }
1181
1137
 
1138
+ const effectHistory = tag('effectHistory', new ZoneHistory());
1139
+ tag('effectHistory.present', effectHistory.present);
1140
+ asyncZone.add(effectHistory);
1141
+ /**
1142
+ * Aggregator for zones that need to be tracked along effects.
1143
+ * ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
1144
+ */
1145
+ const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
1146
+ function isRunning(effect) {
1147
+ const root = getRoot(effect);
1148
+ return effectHistory.some((e) => getRoot(e) === root);
1149
+ }
1150
+ function getActiveEffect() {
1151
+ return effectHistory.present.active;
1152
+ }
1153
+ const cleanups = new WeakMap();
1154
+ /**
1155
+ * Attach cleanup dependencies to an object. When `unlink(obj)` is called,
1156
+ * each dependency is disposed: functions are invoked with the cleanup reason,
1157
+ * objects are recursively `unlink`ed. This forms a cleanup tree.
1158
+ *
1159
+ * @param obj - The owner object
1160
+ * @param cleanupFns - Cleanup callbacks and/or child objects to unlink recursively
1161
+ * @returns The owner object (for chaining)
1162
+ *
1163
+ * @example
1164
+ * ```ts
1165
+ * // Functions are called with CleanupReason
1166
+ * link(parent, () => console.log('disposed'))
1167
+ *
1168
+ * // Objects are recursively unlinked
1169
+ * link(parent, childA, childB)
1170
+ *
1171
+ * // Mixed
1172
+ * link(parent, childObj, () => timer.clear())
1173
+ *
1174
+ * unlink(parent) // disposes childA, childB, calls the function
1175
+ * ```
1176
+ */
1177
+ function link(obj, ...cleanupFns) {
1178
+ const set = cleanups.get(obj);
1179
+ if (!set)
1180
+ cleanups.set(obj, new Set(cleanupFns.filter(Boolean)));
1181
+ else
1182
+ for (const fn of cleanupFns)
1183
+ if (fn)
1184
+ set.add(fn);
1185
+ return obj;
1186
+ }
1187
+ /**
1188
+ * Dispose an object's cleanup dependencies. Functions are called with the
1189
+ * reason; linked objects are recursively unlinked. The cleanup set is removed
1190
+ * so calling `unlink` twice is safe (second call is a no-op).
1191
+ *
1192
+ * @param obj - The object to dispose
1193
+ * @param reason - Optional cleanup reason propagated to callbacks
1194
+ */
1195
+ function unlink(obj, reason) {
1196
+ const set = cleanups.get(obj);
1197
+ if (set) {
1198
+ cleanups.delete(obj);
1199
+ for (const fn of set)
1200
+ if (typeof fn === 'function')
1201
+ fn(reason);
1202
+ else
1203
+ unlink(fn, reason);
1204
+ }
1205
+ }
1206
+
1182
1207
  const effectMarker = {
1183
1208
  enter: 'effect:enter',
1184
1209
  leave: 'effect:leave',
@@ -1241,6 +1266,11 @@ function formatCleanupReason(reason, depth = 0) {
1241
1266
  }
1242
1267
  }
1243
1268
  }
1269
+ // Track native reactivity
1270
+ /**
1271
+ * Symbol to mark class properties as non-reactive
1272
+ */
1273
+ const unreactiveProperties = Symbol('unreactive-properties');
1244
1274
  /**
1245
1275
  * Symbol representing all properties in reactive tracking
1246
1276
  */
@@ -1250,14 +1280,6 @@ const allProps = Symbol('all-props');
1250
1280
  * Used by ownKeys proxy trap — Object.keys(), for..in, Map.keys() depend on this.
1251
1281
  */
1252
1282
  const keysOf = Symbol('keys-of');
1253
- /**
1254
- * Symbol to check if an effect is stopped
1255
- */
1256
- const stopped = Symbol('stopped');
1257
- /**
1258
- * Symbol to access effect cleanup function
1259
- */
1260
- const cleanup = Symbol('cleanup');
1261
1283
  /**
1262
1284
  * Structured error codes for machine-readable diagnosis
1263
1285
  */
@@ -1331,6 +1353,12 @@ const options = {
1331
1353
  * @param runningChain - The array of effects from the detected one to the currently running one
1332
1354
  */
1333
1355
  skipRunningEffect: (_effect) => { },
1356
+ /**
1357
+ * Debug purpose: called when an effect starts executing.
1358
+ * @param effect - The effect being executed (root function)
1359
+ * @param reaction - false for initial creation, true/CleanupReason for subsequent runs
1360
+ */
1361
+ effectRun: (_effect, _reaction) => { },
1334
1362
  /**
1335
1363
  * Debug purpose: maximum effect chain (like call stack max depth)
1336
1364
  * Used to prevent infinite loops
@@ -1457,34 +1485,6 @@ const options = {
1457
1485
  enableHistory: true,
1458
1486
  historySize: 50,
1459
1487
  },
1460
- /**
1461
- * Configuration for zone hooks - control which async APIs are hooked
1462
- * Each option controls whether the corresponding async API is wrapped to preserve effect context
1463
- * Only applies when asyncMode is enabled (truthy)
1464
- * @deprecated Should take all when we made sure PIXI.create, Game.create, ... are -> .root()
1465
- */
1466
- zones: {
1467
- /**
1468
- * Hook setTimeout to preserve effect context
1469
- * @default true
1470
- */
1471
- setTimeout: true,
1472
- /**
1473
- * Hook setInterval to preserve effect context
1474
- * @default true
1475
- */
1476
- setInterval: true,
1477
- /**
1478
- * Hook requestAnimationFrame (runs in untracked context when hooked)
1479
- * @default true
1480
- */
1481
- requestAnimationFrame: true,
1482
- /**
1483
- * Hook queueMicrotask to preserve effect context
1484
- * @default true
1485
- */
1486
- queueMicrotask: true,
1487
- },
1488
1488
  };
1489
1489
  function optionCall(name, ...args) {
1490
1490
  const fn = options[name];
@@ -1498,6 +1498,36 @@ function optionCall(name, ...args) {
1498
1498
  options.warn(`options.${name} threw`, error);
1499
1499
  }
1500
1500
  }
1501
+ /** Production preset: no introspection, heuristic cycle detection, minimal overhead */
1502
+ const prodPreset = {
1503
+ maxEffectReaction: 'throw',
1504
+ cycleHandling: 'production',
1505
+ introspection: null,
1506
+ onMemoizationDiscrepancy: undefined,
1507
+ };
1508
+ /** Development preset (default): introspection on, early cycle detection, warnings */
1509
+ const devPreset = {
1510
+ maxEffectReaction: 'warn',
1511
+ cycleHandling: 'development',
1512
+ introspection: {
1513
+ gatherReasons: { lineages: 'touch' },
1514
+ logErrors: true,
1515
+ enableHistory: true,
1516
+ historySize: 50,
1517
+ },
1518
+ onMemoizationDiscrepancy: undefined,
1519
+ };
1520
+ /** Debug preset: full diagnostics, throws on violations, rich lineage capture */
1521
+ const debugPreset = {
1522
+ maxEffectReaction: 'debug',
1523
+ cycleHandling: 'debug',
1524
+ introspection: {
1525
+ gatherReasons: { lineages: 'both' },
1526
+ logErrors: true,
1527
+ enableHistory: true,
1528
+ historySize: 200,
1529
+ },
1530
+ };
1501
1531
  // --- Proxy State (Merged from proxy-state.ts) ---
1502
1532
  const objectToProxy = new WeakMap();
1503
1533
  const proxyToObject = new WeakMap();
@@ -1517,54 +1547,28 @@ function isReactive(obj) {
1517
1547
  return proxyToObject.has(obj);
1518
1548
  }
1519
1549
 
1520
- const effectHistory = tag('effectHistory', new ZoneHistory());
1521
- tag('effectHistory.present', effectHistory.present);
1522
- asyncZone.add(effectHistory);
1523
- /**
1524
- * Aggregator for zones that need to be tracked along effects.
1525
- * ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
1526
- */
1527
- const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
1528
- function isRunning(effect) {
1529
- const root = getRoot(effect);
1530
- return effectHistory.some((e) => getRoot(e) === root);
1531
- }
1532
- function getActiveEffect() {
1533
- return effectHistory.present.active;
1534
- }
1535
- /**
1536
- * ADD a cleanup function to an object using the cleanup symbol.
1537
- * The cleanup function will be called when the object needs to be disposed.
1538
- *
1539
- * Note: most of the time, you don't need to use this function directly.
1540
- * 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.
1541
- *
1542
- * @param obj - The object to attach the cleanup function to
1543
- * @param cleanupFn - The cleanup function to attach
1544
- * @returns The object with the cleanup function attached
1545
- */
1546
- function cleanedBy(obj, cleanupFn) {
1547
- const oldCleanup = obj[cleanup];
1548
- return Object.defineProperty(obj, cleanup, {
1549
- value: oldCleanup
1550
- ? Object.defineProperties((reason) => {
1551
- oldCleanup(reason);
1552
- cleanupFn(reason);
1553
- }, {
1554
- [stopped]: { get: () => oldCleanup[stopped] || cleanupFn[stopped] },
1555
- })
1556
- : cleanupFn,
1557
- writable: false,
1558
- enumerable: false,
1559
- configurable: true,
1560
- });
1561
- }
1562
-
1563
1550
  // Track dependency stacks per (obj, prop, effect)
1564
1551
  let dependencyStacks = new WeakMap();
1552
+ let assertUntrackedFlag = false;
1565
1553
  function resetTracking() {
1566
1554
  dependencyStacks = new WeakMap();
1567
1555
  }
1556
+ /**
1557
+ * Executes a function and throws if any reactive dependencies are tracked during execution.
1558
+ * Used to assert that code runs in an untracked context.
1559
+ */
1560
+ function assertUntracked(fn) {
1561
+ if (assertUntrackedFlag) {
1562
+ throw new Error('assertUntracked: nested calls are not supported');
1563
+ }
1564
+ assertUntrackedFlag = true;
1565
+ try {
1566
+ return fn();
1567
+ }
1568
+ finally {
1569
+ assertUntrackedFlag = false;
1570
+ }
1571
+ }
1568
1572
  function getDependencyStack(effect, obj, prop) {
1569
1573
  const objStacks = dependencyStacks.get(obj);
1570
1574
  if (!objStacks)
@@ -1577,14 +1581,17 @@ function getDependencyStack(effect, obj, prop) {
1577
1581
  * @param prop - The property name (defaults to allProps)
1578
1582
  */
1579
1583
  function dependant(obj, prop = allProps) {
1584
+ if (assertUntrackedFlag) {
1585
+ throw new Error(`Reactive dependency tracking detected in assertUntracked context: ${String(prop)} on ${obj}`);
1586
+ }
1580
1587
  obj = unwrap(obj);
1581
1588
  const currentActiveEffect = getActiveEffect();
1582
1589
  // Early return if no active effect, tracking disabled, or invalid prop
1583
1590
  if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps && prop !== keysOf))
1584
1591
  return;
1585
- if ('dependencyHook' in currentActiveEffect) {
1586
- // @ts-expect-error We declared it nowhere - it's okay as it's really internal and for edge-case debug purpose only
1587
- currentActiveEffect.dependencyHook(obj, prop);
1592
+ const node = getEffectNode(currentActiveEffect);
1593
+ if ('dependencyHook' in node) {
1594
+ node.dependencyHook(obj, prop);
1588
1595
  }
1589
1596
  let objectWatchers = watchers.get(obj);
1590
1597
  if (!objectWatchers) {
@@ -1721,6 +1728,7 @@ let effectTriggeredBy = new WeakMap();
1721
1728
  // consequencesClosure: for each effect, all effects that it triggers (directly or indirectly)
1722
1729
  let causesClosure = new WeakMap();
1723
1730
  let consequencesClosure = new WeakMap();
1731
+ // Batch re-entrance depth and broken state
1724
1732
  let broken = false;
1725
1733
  /**
1726
1734
  * Gets or creates an IterableWeakSet for a closure map
@@ -2374,6 +2382,7 @@ function batch(effect, immediate) {
2374
2382
  throw new Error('Activation registry already exists');
2375
2383
  optionCall('beginChain', roots);
2376
2384
  }
2385
+ // TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
2377
2386
  const caller = getActiveEffect();
2378
2387
  // Optimization: If nested and NOT immediate, just join the existing batch
2379
2388
  if (!isNewBatch && !immediate) {
@@ -2527,6 +2536,22 @@ const atomic = decorator({
2527
2536
  };
2528
2537
  },
2529
2538
  });
2539
+ /**
2540
+ * Wraps `fn` so it runs within `effect`'s zone context when invoked later.
2541
+ *
2542
+ * Useful for deferred callbacks (event listeners, `DOMContentLoaded`, etc.)
2543
+ * that need sub-effects parented to the original effect.
2544
+ *
2545
+ * @param prev - The effect whose context should be restored, or `undefined` for root context
2546
+ * @param fn - The function to wrap
2547
+ * @returns A function with the same signature that restores the effect context before calling `fn`
2548
+ */
2549
+ function captured(prev, fn) {
2550
+ prev ?? (prev = effectHistory.active);
2551
+ return named(effectMarker.leave, (...args) => {
2552
+ return effectHistory.with(prev, () => fn(...args));
2553
+ });
2554
+ }
2530
2555
  /**
2531
2556
  * Runs `fn` atomically and **always immediately**, batching all reactive effects
2532
2557
  * triggered inside it so they fire only once after `fn` completes.
@@ -2549,19 +2574,13 @@ function atom(fn) {
2549
2574
  return batch(fn, 'immediate');
2550
2575
  }
2551
2576
  const fr = new FinalizationRegistry((f) => f());
2552
- /**
2553
- * @param fn - The effect function to run - provides the cleaner
2554
- * @returns The cleanup function
2555
- */
2556
2577
  /**
2557
2578
  * Creates a reactive effect that automatically re-runs when dependencies change
2558
2579
  * @param fn - The effect function that provides dependencies and may return a cleanup function or Promise
2559
2580
  * @param options - Options for effect execution
2560
2581
  * @returns A cleanup function to stop the effect
2561
2582
  */
2562
- const effect = named(effectMarker.leave, flavored(function effect(
2563
- // biome-ignore lint/suspicious/noConfusingVoidType: Effect callbacks commonly return void
2564
- fn, effectOptions = {}) {
2583
+ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
2565
2584
  if (effectOptions?.name)
2566
2585
  Object.defineProperty(fn, 'name', { value: effectOptions.name });
2567
2586
  // Use per-effect asyncMode or fall back to global option
@@ -2585,6 +2604,7 @@ fn, effectOptions = {}) {
2585
2604
  if (runningPromise) {
2586
2605
  if (asyncMode === 'cancel' && cancelPrevious) {
2587
2606
  // Cancel previous execution
2607
+ abort();
2588
2608
  cancelPrevious();
2589
2609
  cancelPrevious = null;
2590
2610
  runningPromise = null;
@@ -2608,11 +2628,33 @@ fn, effectOptions = {}) {
2608
2628
  access.reaction = node.nextReason || access.reaction;
2609
2629
  node.nextReason = undefined;
2610
2630
  optionCall('enter', getRoot(fn));
2631
+ optionCall('effectRun', getRoot(fn), access.reaction);
2611
2632
  let result;
2612
2633
  let caught = 0;
2613
- // Default thrower (self)
2614
- let thrower = (error) => {
2615
- throw error;
2634
+ // Define bubbling thrower
2635
+ const thrower = (error) => {
2636
+ const catches = node.catchers;
2637
+ const reason = { type: 'error', error };
2638
+ if (catches)
2639
+ while (caught < catches.length) {
2640
+ cleanupReaction(reason);
2641
+ try {
2642
+ reactionCleanup = catches[caught](error);
2643
+ return;
2644
+ }
2645
+ catch (_e) {
2646
+ caught++;
2647
+ }
2648
+ }
2649
+ if (parent) {
2650
+ const parentNode = getEffectNode(parent);
2651
+ if (parentNode.forwardThrow)
2652
+ parentNode.forwardThrow(error);
2653
+ else
2654
+ throw error;
2655
+ }
2656
+ else
2657
+ throw error;
2616
2658
  };
2617
2659
  node.forwardThrow = thrower;
2618
2660
  let errorToThrow;
@@ -2662,12 +2704,14 @@ fn, effectOptions = {}) {
2662
2704
  }
2663
2705
  }
2664
2706
  catch (error) {
2707
+ debugHooks.decorateError(error, runEffect);
2665
2708
  // catcher:self`
2666
2709
  errorToThrow = error;
2667
2710
  }
2668
2711
  // Create cleanup function for next run
2669
2712
  node.cleanup = (reason) => {
2670
2713
  node.cleanup = undefined;
2714
+ abort();
2671
2715
  cleanupReaction(reason);
2672
2716
  delete node.catchers;
2673
2717
  // Remove this effect from all reactive objects it's watching
@@ -2678,13 +2722,11 @@ fn, effectOptions = {}) {
2678
2722
  if (objectWatchers) {
2679
2723
  for (const [prop, deps] of objectWatchers.entries()) {
2680
2724
  deps.delete(runEffect);
2681
- if (deps.size === 0) {
2725
+ if (deps.size === 0)
2682
2726
  objectWatchers.delete(prop);
2683
- }
2684
2727
  }
2685
- if (objectWatchers.size === 0) {
2728
+ if (objectWatchers.size === 0)
2686
2729
  watchers.delete(reactiveObj);
2687
- }
2688
2730
  }
2689
2731
  }
2690
2732
  effectToReactiveObjects.delete(runEffect);
@@ -2702,33 +2744,6 @@ fn, effectOptions = {}) {
2702
2744
  delete node.children;
2703
2745
  }
2704
2746
  };
2705
- // Define bubbling thrower
2706
- thrower = (error) => {
2707
- const catches = node.catchers;
2708
- const reason = { type: 'error', error };
2709
- if (catches)
2710
- while (caught < catches.length) {
2711
- cleanupReaction(reason);
2712
- try {
2713
- reactionCleanup = catches[caught](error);
2714
- return;
2715
- }
2716
- catch (e) {
2717
- caught++;
2718
- }
2719
- }
2720
- if (parent) {
2721
- const parentNode = getEffectNode(parent);
2722
- if (parentNode.forwardThrow)
2723
- parentNode.forwardThrow(error);
2724
- else
2725
- throw error;
2726
- }
2727
- else
2728
- throw error;
2729
- };
2730
- // Update the node's forwardThrow to the bubbling one
2731
- node.forwardThrow = thrower;
2732
2747
  if (errorToThrow)
2733
2748
  thrower(errorToThrow);
2734
2749
  };
@@ -2747,26 +2762,25 @@ fn, effectOptions = {}) {
2747
2762
  node.parent = parent;
2748
2763
  // let thrower: CatchFunction | undefined // Moved inside runEffect
2749
2764
  let effectStopped = false;
2765
+ let abortController;
2750
2766
  const access = {
2751
2767
  tracked,
2752
2768
  ascend: named(effectMarker.leave, (fn) => ascended(named(effectMarker.enter, () => fn.call(null)))),
2753
2769
  //named(effectMarker.enter, (fn) => ascended(fn)),
2754
2770
  reaction: false,
2771
+ get signal() {
2772
+ if (!abortController) {
2773
+ abortController = new AbortController();
2774
+ }
2775
+ return abortController.signal;
2776
+ },
2755
2777
  };
2756
2778
  let runningPromise = null;
2757
2779
  let cancelPrevious = null;
2758
- if (effectOptions?.dependencyHook) {
2780
+ if (effectOptions?.dependencyHook)
2759
2781
  node.dependencyHook = effectOptions.dependencyHook;
2760
- }
2761
2782
  // Mark the runEffect callback with the original function as its root
2762
2783
  markWithRoot(runEffect, fn);
2763
- function augmentedRv(rv) {
2764
- return Object.defineProperties(rv, {
2765
- [stopped]: {
2766
- get: () => effectStopped,
2767
- },
2768
- });
2769
- }
2770
2784
  // Register strict mode if enabled
2771
2785
  if (effectOptions?.opaque) {
2772
2786
  node.isOpaque = true;
@@ -2775,6 +2789,12 @@ fn, effectOptions = {}) {
2775
2789
  debugHooks.registerEffect(runEffect);
2776
2790
  }
2777
2791
  // Store parent relationship for hierarchy traversal - ALREADY DONE ABOVE via getEffectNode
2792
+ const abort = () => {
2793
+ if (abortController) {
2794
+ abortController.abort(new ReactiveError('[reactive] Effect aborted due to dependency change or stop'));
2795
+ abortController = undefined;
2796
+ }
2797
+ };
2778
2798
  batch(runEffect, 'immediate');
2779
2799
  // Only ROOT effects are registered for GC cleanup and zone tracking
2780
2800
  const isRootEffect = !parent;
@@ -2784,6 +2804,7 @@ fn, effectOptions = {}) {
2784
2804
  effectStopped = true;
2785
2805
  node.stopped = true;
2786
2806
  // Cancel any running async work
2807
+ abort();
2787
2808
  if (cancelPrevious) {
2788
2809
  cancelPrevious();
2789
2810
  cancelPrevious = null;
@@ -2802,7 +2823,7 @@ fn, effectOptions = {}) {
2802
2823
  fr.unregister(stopEffect);
2803
2824
  };
2804
2825
  if (isRootEffect) {
2805
- const callIfCollected = augmentedRv((reason) => stopEffect(reason));
2826
+ const callIfCollected = (reason) => stopEffect(reason);
2806
2827
  fr.register(callIfCollected, () => {
2807
2828
  stopEffect({ type: 'gc' });
2808
2829
  optionCall('garbageCollected', fn);
@@ -2816,16 +2837,16 @@ fn, effectOptions = {}) {
2816
2837
  parentNode.children = new Set();
2817
2838
  }
2818
2839
  const children = parentNode.children;
2819
- const subEffectCleanup = augmentedRv((reason) => {
2840
+ const subEffectCleanup = (reason) => {
2820
2841
  children.delete(subEffectCleanup);
2821
2842
  // Execute this child effect cleanup (which triggers its own mainCleanup)
2822
2843
  stopEffect(reason);
2823
- });
2844
+ };
2824
2845
  children.add(subEffectCleanup);
2825
2846
  return subEffectCleanup;
2826
2847
  }
2827
2848
  // Should not be reachable given isRootEffect check, but for type safety
2828
- return augmentedRv((reason) => stopEffect(reason));
2849
+ return (reason) => stopEffect(reason);
2829
2850
  }, {
2830
2851
  get opaque() {
2831
2852
  return flavorOptions(this, { opaque: true }, { name: 'opaque' });
@@ -2856,9 +2877,11 @@ function biDi(received, get, set) {
2856
2877
  get = get.get;
2857
2878
  }
2858
2879
  let programmaticallySetValue = Symbol();
2859
- effect(markWithRoot(() => {
2880
+ effect.named('biDi')(markWithRoot(() => {
2860
2881
  const newValue = get();
2861
- if (unwrap(newValue) !== programmaticallySetValue)
2882
+ const pValue = programmaticallySetValue;
2883
+ programmaticallySetValue = Symbol();
2884
+ if (unwrap(newValue) !== pValue)
2862
2885
  received(newValue);
2863
2886
  }, received));
2864
2887
  return set
@@ -2929,16 +2952,16 @@ function bubbleUpChange(changedObject, evolution) {
2929
2952
  const parents = objectParents.get(changedObject);
2930
2953
  if (!parents)
2931
2954
  return;
2932
- for (const { parent, prop } of parents) {
2955
+ for (const { parent } of parents) {
2933
2956
  // Trigger deep watchers on parent
2934
2957
  const parentDeepWatchers = deepWatchers.get(parent);
2935
2958
  if (parentDeepWatchers) {
2936
2959
  if (options.introspection?.gatherReasons) {
2937
2960
  const gatherReasons = options.introspection.gatherReasons;
2938
2961
  const lineageConfig = gatherReasons.lineages;
2939
- let touchStack;
2962
+ let touchLineage;
2940
2963
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
2941
- touchStack = debugHooks.captureLineage();
2964
+ touchLineage = debugHooks.captureLineage();
2942
2965
  }
2943
2966
  for (const watcher of parentDeepWatchers) {
2944
2967
  const dependencyStack = lineageConfig === 'dependency' || lineageConfig === 'both'
@@ -2951,7 +2974,7 @@ function bubbleUpChange(changedObject, evolution) {
2951
2974
  obj: parent,
2952
2975
  evolution,
2953
2976
  dependency: dependencyStack,
2954
- touch: touchStack,
2977
+ touch: touchLineage,
2955
2978
  });
2956
2979
  }
2957
2980
  }
@@ -3004,6 +3027,8 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
3004
3027
  for (const key of keys) {
3005
3028
  const deps = objectWatchers.get(key);
3006
3029
  if (deps) {
3030
+ // Make sure `some.prop++` does not keep a dependency to `some.props`
3031
+ deps.delete(sourceEffect);
3007
3032
  for (const effect of deps) {
3008
3033
  const runningChain = isRunning(effect);
3009
3034
  if (runningChain) {
@@ -3054,9 +3079,9 @@ function touched(obj, evolution, props) {
3054
3079
  if (options.introspection?.gatherReasons) {
3055
3080
  const gatherReasons = options.introspection.gatherReasons;
3056
3081
  const lineageConfig = gatherReasons.lineages;
3057
- let touchStack;
3082
+ let touchLineage;
3058
3083
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3059
- touchStack = debugHooks.captureLineage();
3084
+ touchLineage = debugHooks.captureLineage();
3060
3085
  }
3061
3086
  for (const [effect, dependencyStack] of effects) {
3062
3087
  const node = getEffectNode(effect);
@@ -3066,7 +3091,7 @@ function touched(obj, evolution, props) {
3066
3091
  obj,
3067
3092
  evolution,
3068
3093
  dependency: dependencyStack,
3069
- touch: touchStack,
3094
+ touch: touchLineage,
3070
3095
  });
3071
3096
  }
3072
3097
  }
@@ -3105,10 +3130,10 @@ function touchedOpaque(obj, evolution, prop) {
3105
3130
  }
3106
3131
  effects.add(effect);
3107
3132
  if (gather) {
3108
- let touchStack;
3133
+ let touchLineage;
3109
3134
  let dependencyStack;
3110
3135
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3111
- touchStack = debugHooks.captureLineage();
3136
+ touchLineage = debugHooks.captureLineage();
3112
3137
  }
3113
3138
  if (lineageConfig === 'dependency' || lineageConfig === 'both') {
3114
3139
  dependencyStack = getDependencyStack(effect, obj, prop);
@@ -3119,7 +3144,7 @@ function touchedOpaque(obj, evolution, prop) {
3119
3144
  obj,
3120
3145
  evolution,
3121
3146
  dependency: dependencyStack,
3122
- touch: touchStack,
3147
+ touch: touchLineage,
3123
3148
  });
3124
3149
  }
3125
3150
  recordActivation(effect, obj, evolution, prop);
@@ -3148,59 +3173,60 @@ function touchedOpaque(obj, evolution, prop) {
3148
3173
  }
3149
3174
  }
3150
3175
 
3151
- const nonReactiveObjects = new WeakSet();
3152
- const nonReactiveClasses = new WeakSet();
3153
- const unreactiveProps = new WeakMap();
3154
- let unreactivePropsCount = 0;
3176
+ const absent = Symbol('absent');
3177
+ /**
3178
+ * Add unreactive properties to a prototype.
3179
+ * If no set is provided, marks the entire object/prototype as non-reactive (sets [unreactiveProperties] = true).
3180
+ * If a set is provided, merges with existing unreactive properties (never overrides true).
3181
+ */
3155
3182
  function addUnreactiveProps(proto, set) {
3156
- unreactiveProps.set(proto, set);
3157
- unreactivePropsCount++;
3183
+ if (unreactiveProperties in proto) {
3184
+ const existing = proto[unreactiveProperties];
3185
+ // If already fully unreactive, don't change
3186
+ if (existing === true)
3187
+ return proto;
3188
+ // If no set provided, upgrade to fully unreactive
3189
+ if (!set) {
3190
+ proto[unreactiveProperties] = true;
3191
+ return proto;
3192
+ }
3193
+ // Merge sets
3194
+ set = proto[unreactiveProperties] = new Set(proto[unreactiveProperties]);
3195
+ for (const p of set)
3196
+ existing.add(p);
3197
+ }
3198
+ // If no set, mark as fully unreactive, otherwise create set
3199
+ else
3200
+ proto[unreactiveProperties] = set ? new Set(set) : true;
3201
+ return proto;
3158
3202
  }
3159
3203
  /** Check if a property is marked unreactive on obj or any of its prototypes (trap-free) */
3160
3204
  function isUnreactiveProp(obj, prop) {
3161
- if (!unreactivePropsCount)
3162
- return false;
3163
- let target = obj;
3164
- while (target) {
3165
- if (unreactiveProps.get(target)?.has(prop))
3166
- return true;
3167
- target = Object.getPrototypeOf(target);
3168
- }
3169
- return false;
3205
+ if (typeof prop === 'symbol' || prop === 'constructor')
3206
+ return true;
3207
+ const marker = obj[unreactiveProperties];
3208
+ return (marker === true || // Fully unreactive
3209
+ marker?.has?.(prop) || // Property is unreactive
3210
+ false);
3170
3211
  }
3171
- const immutables = new Set();
3172
- const absent = Symbol('absent');
3173
- function markNonReactive(...obj) {
3174
- for (const o of obj)
3175
- nonReactiveObjects.add(o);
3212
+ function nonReactive(...obj) {
3213
+ for (const o of obj) {
3214
+ o[unreactiveProperties] = true;
3215
+ }
3176
3216
  return obj[0];
3177
3217
  }
3178
3218
  function nonReactiveClass(...cls) {
3179
3219
  for (const c of cls)
3180
3220
  if (c)
3181
- nonReactiveClasses.add(c.prototype);
3221
+ c.prototype[unreactiveProperties] = true;
3182
3222
  return cls[0];
3183
3223
  }
3184
3224
  function isNonReactive(obj) {
3185
- if (obj === null || typeof obj !== 'object')
3186
- return true;
3187
- if (nonReactiveObjects.has(obj))
3188
- return true;
3189
- // Walk the prototype chain on the raw object to check for non-reactive classes
3190
- let proto = Object.getPrototypeOf(obj);
3191
- while (proto) {
3192
- if (nonReactiveClasses.has(proto))
3193
- return true;
3194
- proto = Object.getPrototypeOf(proto);
3195
- }
3196
- for (const fn of immutables)
3197
- if (fn(obj))
3198
- return true;
3199
- return false;
3225
+ return !obj || obj[unreactiveProperties] === true;
3200
3226
  }
3201
3227
  nonReactiveClass(Date, RegExp, Error, Promise, Function);
3202
3228
  if (typeof window !== 'undefined') {
3203
- markNonReactive(window, document);
3229
+ nonReactive(window, document);
3204
3230
  nonReactiveClass(Node, Element, HTMLElement, EventTarget, HTMLCollection, NodeList);
3205
3231
  }
3206
3232
 
@@ -3222,10 +3248,33 @@ function shouldRecurseTouch(oldValue, newValue) {
3222
3248
  if ((typeof oldValue !== 'object' && !Array.isArray(oldValue)) ||
3223
3249
  (typeof newValue !== 'object' && !Array.isArray(newValue)))
3224
3250
  return false;
3225
- if (isNonReactive(oldValue) || isNonReactive(newValue))
3251
+ if (isNonReactive(oldValue) /*|| isNonReactive(newValue)*/)
3226
3252
  return false;
3227
3253
  return getPrototypeToken(oldValue) === getPrototypeToken(newValue);
3228
3254
  }
3255
+ /**
3256
+ * Migrate all watcher registrations from oldRef to newRef.
3257
+ * Called when deep touch replaces an object identity without any child value differences,
3258
+ * to prevent watcher orphaning (effects still pointing at the discarded old object).
3259
+ */
3260
+ function migrateWatchers(oldRef, newRef) {
3261
+ const oldMap = watchers.get(oldRef);
3262
+ if (!oldMap)
3263
+ return;
3264
+ // Move the entire watcher map
3265
+ watchers.set(newRef, oldMap);
3266
+ watchers.delete(oldRef);
3267
+ // Update the reverse map (effect → objects it watches)
3268
+ for (const deps of oldMap.values()) {
3269
+ for (const effect of deps) {
3270
+ const objects = effectToReactiveObjects.get(effect);
3271
+ if (objects) {
3272
+ objects.delete(oldRef);
3273
+ objects.add(newRef);
3274
+ }
3275
+ }
3276
+ }
3277
+ }
3229
3278
  /**
3230
3279
  * Centralized function to handle property change notifications with optional recursive touch
3231
3280
  * @param targetObj - The object whose property changed
@@ -3243,7 +3292,15 @@ function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty)
3243
3292
  const origin = { obj: unwrappedObj, prop };
3244
3293
  // Deep touch: only notify nested property changes with origin filtering
3245
3294
  // Don't notify direct property change - the whole point is to avoid parent effects re-running
3246
- dispatchNotifications(untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin)));
3295
+ const changes = untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
3296
+ // When deep touch found no child differences, the object identity still changed.
3297
+ // Migrate watchers from old → new so the dependency chain is preserved.
3298
+ if (changes.length === 0) {
3299
+ migrateWatchers(unwrap(oldValue), unwrap(newValue));
3300
+ }
3301
+ else {
3302
+ dispatchNotifications(changes);
3303
+ }
3247
3304
  // Notify opaque listeners (like memoize) that always want to know about identity changes
3248
3305
  touchedOpaque(targetObj, evolution, prop);
3249
3306
  }
@@ -3330,12 +3387,11 @@ function diffObjectProperties(oldObj, newObj, visited, notifications, origin) {
3330
3387
  for (const key of oldKeys)
3331
3388
  if (!newKeys.has(key))
3332
3389
  local.push({ target: oldObj, evolution: { type: 'del', prop: key }, prop: key, origin });
3333
- for (const key of newKeys)
3334
- if (!oldKeys.has(key))
3335
- local.push({ target: oldObj, evolution: { type: 'add', prop: key }, prop: key, origin });
3336
3390
  for (const key of newKeys) {
3337
- if (!oldKeys.has(key))
3391
+ if (!oldKeys.has(key)) {
3392
+ local.push({ target: oldObj, evolution: { type: 'add', prop: key }, prop: key, origin });
3338
3393
  continue;
3394
+ }
3339
3395
  const oldEntry = unwrap(oldObj[key]);
3340
3396
  const newEntry = unwrap(newObj[key]);
3341
3397
  if (shouldRecurseTouch(oldEntry, newEntry)) {
@@ -3377,11 +3433,10 @@ function dispatchNotifications(notifications) {
3377
3433
  if (originWatchers) {
3378
3434
  const originEffects = new Map();
3379
3435
  collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
3380
- for (const effect of originEffects.keys())
3381
- allowedEffects.add(effect);
3436
+ allowedEffects = new Set(originEffects.keys());
3382
3437
  }
3383
3438
  // If no allowed effects, skip all notifications (no one should be notified)
3384
- if (allowedEffects.size === 0)
3439
+ if (!allowedEffects?.size)
3385
3440
  return;
3386
3441
  }
3387
3442
  for (const notification of notifications) {
@@ -3394,7 +3449,6 @@ function dispatchNotifications(notifications) {
3394
3449
  let currentEffects;
3395
3450
  const propsArray = [prop];
3396
3451
  if (objectWatchers) {
3397
- // console.log(`[DEBUG] dispatchNotifications: processing ${obj.constructor.name} (has watchers)`)
3398
3452
  currentEffects = new Map();
3399
3453
  const broad = evolution.type !== 'set' ? [allProps, keysOf] : [allProps];
3400
3454
  collectEffects(obj, evolution, currentEffects, objectWatchers, broad, propsArray);
@@ -3430,9 +3484,9 @@ function dispatchNotifications(notifications) {
3430
3484
  if (options.introspection?.gatherReasons) {
3431
3485
  const gatherReasons = options.introspection.gatherReasons;
3432
3486
  const lineageConfig = gatherReasons.lineages;
3433
- let touchStack;
3487
+ let touchLineage;
3434
3488
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3435
- touchStack = debugHooks.captureLineage();
3489
+ touchLineage = debugHooks.captureLineage();
3436
3490
  }
3437
3491
  for (const effect of combinedEffects) {
3438
3492
  const node = getEffectNode(effect);
@@ -3446,7 +3500,7 @@ function dispatchNotifications(notifications) {
3446
3500
  obj: unwrap(target),
3447
3501
  evolution,
3448
3502
  dependency: dependencyStack,
3449
- touch: touchStack,
3503
+ touch: touchLineage,
3450
3504
  });
3451
3505
  }
3452
3506
  }
@@ -3459,11 +3513,16 @@ const metaProtos = new WeakMap();
3459
3513
  const wrapProtos = new WeakMap();
3460
3514
  const arrayLengths = new WeakMap();
3461
3515
  const hasReentry = new Set();
3462
- const subsRegister = new WeakMap();
3463
3516
  // Sub-proxy registration for custom reactive behaviors
3517
+ const subsRegister = new WeakMap();
3518
+ // Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
3519
+ // TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
3520
+ let internalUntracked = false;
3464
3521
  const reactiveHandlers = {
3465
3522
  [Symbol.toStringTag]: 'MutTs Reactive',
3466
3523
  get(obj, prop, receiver) {
3524
+ if (internalUntracked)
3525
+ return FoolProof.get(obj, prop, receiver);
3467
3526
  if (obj && typeof obj === 'object' && prop !== Symbol.toStringTag) {
3468
3527
  const metaProto = metaProtos.get(obj.constructor);
3469
3528
  if (metaProto && Object.hasOwn(metaProto, prop)) {
@@ -3483,15 +3542,21 @@ const reactiveHandlers = {
3483
3542
  if (wrapProto && Object.hasOwn(wrapProto, prop))
3484
3543
  return wrapProto[prop];
3485
3544
  }
3486
- // Symbols: fast-path — no reactivity tracking, no unreactive check needed
3487
- if (typeof prop === 'symbol')
3488
- return FoolProof.get(obj, prop, receiver);
3489
- // Check if this property is marked as unreactive (WeakMap lookup — no proxy traps)
3490
- if (isUnreactiveProp(obj, prop))
3545
+ // Symbols: fast-path — no reactivity tracking
3546
+ if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
3491
3547
  return FoolProof.get(obj, prop, receiver);
3492
3548
  // Check if property exists using a trap-free walk to avoid triggering
3493
3549
  // the has-trap cascade on prototype chains of reactive proxies.
3494
3550
  const isOwnProp = Object.hasOwn(obj, prop);
3551
+ // For accessor properties, check the unwrapped object to see if it's an accessor
3552
+ // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
3553
+ // Skip for null-proto objects (pounce scopes) — they never have accessors
3554
+ const shouldIgnoreAccessor = options.ignoreAccessors &&
3555
+ isOwnProp &&
3556
+ Object.getPrototypeOf(obj) !== null &&
3557
+ (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
3558
+ // Check if property exists using a trap-free walk to avoid triggering
3559
+ // the has-trap cascade on prototype chains of reactive proxies.
3495
3560
  let hasProp = isOwnProp;
3496
3561
  let owner = isOwnProp ? obj : undefined;
3497
3562
  if (!isOwnProp) {
@@ -3506,29 +3571,20 @@ const reactiveHandlers = {
3506
3571
  }
3507
3572
  }
3508
3573
  const isInheritedAccess = hasProp && !isOwnProp;
3509
- // For accessor properties, check the unwrapped object to see if it's an accessor
3510
- // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
3511
- // Skip for null-proto objects (pounce scopes) — they never have accessors
3512
- const shouldIgnoreAccessor = options.ignoreAccessors &&
3513
- isOwnProp &&
3514
- Object.getPrototypeOf(obj) !== null &&
3515
- (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
3516
3574
  // Depend if...
3517
3575
  if (!hasProp ||
3518
3576
  (!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
3519
3577
  !shouldIgnoreAccessor))
3520
3578
  dependant(obj, prop);
3521
- // Two-Point Tracking: for inherited access on null-proto chains, only track
3522
- // the owning ancestor not every intermediate level. This relies on the
3523
- // "structural stability" contract: key presence in the chain is fixed at
3524
- // creation time, so intermediate levels never gain/lose shadowing properties.
3579
+ // Two-Point Tracking: for inherited access on null-proto chains, also track
3580
+ // the owning ancestor so that writing directly to it triggers dependent effects.
3525
3581
  if (isInheritedAccess && owner && (!options.instanceMembers || !(obj instanceof Object))) {
3526
3582
  dependant(owner, prop);
3527
3583
  }
3528
3584
  // For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
3529
3585
  // For all other objects, inline Reflect.get directly (skips 3 function calls).
3530
3586
  const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
3531
- if (typeof value === 'object' && value !== null) {
3587
+ if (!isReactive(value) && typeof value === 'object' && value !== null) {
3532
3588
  const reactiveValue = reactiveObject(value);
3533
3589
  // Only create back-references if this object needs them
3534
3590
  if (needsBackReferences(obj)) {
@@ -3539,9 +3595,19 @@ const reactiveHandlers = {
3539
3595
  return value;
3540
3596
  },
3541
3597
  set(obj, prop, value, receiver) {
3542
- const unwrappedReceiver = unwrap(receiver);
3598
+ const unwrapped = unwrap(receiver);
3599
+ if (obj !== unwrapped)
3600
+ return Object.defineProperty(unwrapped, prop, {
3601
+ value,
3602
+ configurable: true,
3603
+ writable: true,
3604
+ enumerable: true,
3605
+ });
3606
+ if (internalUntracked)
3607
+ throw new Error('Internal untracked: setting a value in an getter in a set operation');
3608
+ //return FoolProof.set(obj, prop, value, receiver)
3543
3609
  // Check if this property is marked as unreactive
3544
- if (isUnreactiveProp(obj, prop) || obj !== unwrappedReceiver)
3610
+ if (isUnreactiveProp(obj, prop))
3545
3611
  return FoolProof.set(obj, prop, value, receiver);
3546
3612
  const newValue = unwrap(value);
3547
3613
  // metaProto setter dispatch (e.g., reactive array length)
@@ -3558,16 +3624,19 @@ const reactiveHandlers = {
3558
3624
  // Read old value, using withEffect(undefined, ...) for getter-only accessors to avoid
3559
3625
  // breaking memoization dependency tracking during SET operations
3560
3626
  let oldVal = absent;
3561
- // TODO: Pffft... Find a way to "generalize" this case?
3562
3627
  const isArrayLength = prop === 'length' && Array.isArray(obj);
3563
- if (Reflect.has(unwrappedReceiver, prop)) {
3564
- // We *need* to use `receiver` and not `obj` here, otherwise we break
3565
- // the dependency tracking for memoized getters
3566
- oldVal = isArrayLength
3567
- ? arrayLengths.get(obj) === newValue
3568
- ? newValue
3569
- : absent
3570
- : untracked(() => Reflect.get(obj, prop, receiver));
3628
+ internalUntracked = true;
3629
+ try {
3630
+ if (Reflect.has(obj, prop)) {
3631
+ oldVal = isArrayLength
3632
+ ? arrayLengths.get(obj) === newValue
3633
+ ? newValue
3634
+ : absent
3635
+ : Reflect.get(obj, prop, receiver);
3636
+ }
3637
+ }
3638
+ finally {
3639
+ internalUntracked = false;
3571
3640
  }
3572
3641
  if (objectsWithDeepWatchers.has(obj)) {
3573
3642
  if (typeof oldVal === 'object' && oldVal !== null) {
@@ -3596,7 +3665,8 @@ const reactiveHandlers = {
3596
3665
  cycle: [], // We don't have the full cycle here, but we know it involves obj
3597
3666
  });
3598
3667
  hasReentry.add(obj);
3599
- dependant(obj, prop);
3668
+ if (!internalUntracked && !isUnreactiveProp(obj, prop))
3669
+ dependant(obj, prop);
3600
3670
  const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
3601
3671
  hasReentry.delete(obj);
3602
3672
  return rv;
@@ -3622,7 +3692,8 @@ const reactiveHandlers = {
3622
3692
  return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
3623
3693
  },
3624
3694
  getOwnPropertyDescriptor(obj, prop) {
3625
- return subsRegister.get(obj)?.getOwnPropertyDescriptor?.(obj, prop) || Reflect.getOwnPropertyDescriptor(obj, prop);
3695
+ return (subsRegister.get(obj)?.getOwnPropertyDescriptor?.(obj, prop) ||
3696
+ Reflect.getOwnPropertyDescriptor(obj, prop));
3626
3697
  },
3627
3698
  };
3628
3699
  const reactiveClasses = new WeakSet();
@@ -3697,5 +3768,5 @@ const reactive = decorator({
3697
3768
  default: reactiveObject,
3698
3769
  });
3699
3770
 
3700
- export { touched1 as $, AZone as A, getActiveEffect as B, getState as C, DecoratorError as D, hooks as E, immutables as F, isConstructor as G, isNonReactive as H, IterableWeakMap as I, isObject as J, isReactive as K, legacyDecorator as L, mixin as M, modernDecorator as N, named as O, objectToProxy as P, onEffectThrow as Q, ReactiveBase as R, proxyToObject as S, reactive as T, options as U, reset as V, root as W, stopped as X, tag as Y, Zone as Z, touched as _, IterableWeakSet as a, untracked as a0, unwrap as a1, zip as a2, markWithRoot as a3, rootFunctionSymbol as a4, getRoot as a5, dependant as a6, optionCall as a7, keysOf as a8, __runInitializers as a9, __esDecorate as aa, FoolProof as ab, objectsWithDeepWatchers as ac, effectToDeepWatchedObjects as ad, deepWatchers as ae, registerDeepWatcher as af, nonReactiveClass as ag, unreactiveProps as ah, addUnreactiveProps as ai, nonReactiveObjects as aj, __setFunctionName as ak, __classPrivateFieldGet as al, __classPrivateFieldSet as am, contentRef as an, notifyPropertyChange as ao, metaProtos as ap, wrapProtos as aq, objectParents as ar, watchers as as, effectToReactiveObjects as at, effectMarker as au, getEffectNode as av, setDebugHooks as aw, allProps as ax, ReactiveError as b, ReactiveErrorCode as c, ZoneAggregator as d, ZoneHistory as e, addBatchCleanup as f, arrayEquals as g, asyncHook as h, asyncHooks as i, asyncZone as j, atom as k, atomic as l, biDi as m, caught as n, cleanedBy as o, cleanup as p, createFlavor as q, decorator as r, deepCompare as s, defer as t, effect as u, effectAggregator as v, flavorOptions as w, flavored as x, formatCleanupReason as y, getActivationLog as z };
3701
- //# sourceMappingURL=proxy-r7lARftl.esm.js.map
3771
+ export { reactive as $, AZone as A, flavored as B, formatCleanupReason as C, DecoratorError as D, getActivationLog as E, getActiveEffect as F, getState as G, hooks as H, IterableWeakMap as I, isConstructor as J, isDev as K, isNonReactive as L, isObject as M, isProd as N, isReactive as O, isTest as P, legacyDecorator as Q, ReactiveBase as R, link as S, mixin as T, modernDecorator as U, named as V, objectToProxy as W, onEffectThrow as X, prodPreset as Y, Zone as Z, proxyToObject as _, IterableWeakSet as a, options as a0, reset as a1, root as a2, tag as a3, touched as a4, touched1 as a5, unlink as a6, untracked as a7, unwrap as a8, zip as a9, allProps as aA, dependant as aa, keysOf as ab, markWithRoot as ac, objectsWithDeepWatchers as ad, effectToDeepWatchedObjects as ae, deepWatchers as af, registerDeepWatcher as ag, rootFunctionSymbol as ah, getRoot as ai, optionCall as aj, FoolProof as ak, effectHistory as al, unreactiveProperties as am, __runInitializers as an, __esDecorate as ao, batch as ap, contentRef as aq, notifyPropertyChange as ar, metaProtos as as, wrapProtos as at, objectParents as au, watchers as av, effectToReactiveObjects as aw, effectMarker as ax, getEffectNode as ay, setDebugHooks as az, ReactiveError as b, ReactiveErrorCode as c, ZoneAggregator as d, ZoneHistory as e, addBatchCleanup as f, addUnreactiveProps as g, arrayEquals as h, assertUntracked as i, asyncHook as j, asyncHooks as k, asyncZone as l, atom as m, atomic as n, biDi as o, captured as p, caught as q, createFlavor as r, debugPreset as s, decorator as t, deepCompare as u, defer as v, devPreset as w, effect as x, effectAggregator as y, flavorOptions as z };
3772
+ //# sourceMappingURL=proxy-D2C49sXH.esm.js.map