mutts 1.0.10 → 1.0.12

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 (61) hide show
  1. package/README.md +3 -3
  2. package/dist/browser.cjs +244 -3199
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +2 -2
  5. package/dist/browser.dev.cjs +51 -50
  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/{index-CaaQQlPJ.esm.js → index-BUop6B2U.esm.js} +384 -981
  12. package/dist/chunks/index-BUop6B2U.esm.js.map +1 -0
  13. package/dist/chunks/index-yK0HVxHv.cjs +2612 -0
  14. package/dist/chunks/index-yK0HVxHv.cjs.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/{async-node-3PrbVAbB.cjs → node-Dd0esp5F.cjs} +4 -4
  18. package/dist/chunks/node-Dd0esp5F.cjs.map +1 -0
  19. package/dist/chunks/{proxy-Dtg-bJ3T.cjs → proxy-BvM4yewA.cjs} +441 -342
  20. package/dist/chunks/proxy-BvM4yewA.cjs.map +1 -0
  21. package/dist/chunks/{proxy-r7lARftl.esm.js → proxy-D2C49sXH.esm.js} +401 -330
  22. package/dist/chunks/proxy-D2C49sXH.esm.js.map +1 -0
  23. package/dist/debug.cjs +17 -4
  24. package/dist/debug.cjs.map +1 -1
  25. package/dist/debug.d.ts +2 -2
  26. package/dist/debug.esm.js +17 -3
  27. package/dist/debug.esm.js.map +1 -1
  28. package/dist/index.d.ts +84 -209
  29. package/dist/mutts.umd.js +842 -1362
  30. package/dist/mutts.umd.js.map +1 -1
  31. package/dist/mutts.umd.min.js +1 -1
  32. package/dist/mutts.umd.min.js.map +1 -1
  33. package/dist/node.cjs +51 -49
  34. package/dist/node.cjs.map +1 -1
  35. package/dist/node.d.ts +2 -2
  36. package/dist/node.dev.cjs +51 -49
  37. package/dist/node.dev.cjs.map +1 -1
  38. package/dist/node.dev.d.ts +2 -2
  39. package/dist/node.dev.esm.js +3 -3
  40. package/dist/node.esm.js +3 -3
  41. package/dist/{types-W5vD6m2n.d.ts → types-Bx2PhORg.d.ts} +38 -47
  42. package/docs/ai/api-reference.md +0 -14
  43. package/docs/ai/manual.md +2 -22
  44. package/docs/reactive/advanced.md +13 -14
  45. package/docs/reactive/attend.md +1 -2
  46. package/docs/reactive/collections.md +2 -149
  47. package/docs/reactive/core.md +218 -96
  48. package/docs/reactive/debugging.md +2 -2
  49. package/docs/reactive/resource.md +1 -1
  50. package/docs/reactive.md +1 -3
  51. package/docs/zone.md +1 -1
  52. package/package.json +18 -9
  53. package/dist/chunks/async-browser-BU_IfxYD.cjs +0 -216
  54. package/dist/chunks/async-browser-BU_IfxYD.cjs.map +0 -1
  55. package/dist/chunks/async-core-CRLKP3l-.cjs +0 -29
  56. package/dist/chunks/async-core-CRLKP3l-.cjs.map +0 -1
  57. package/dist/chunks/async-node-3PrbVAbB.cjs.map +0 -1
  58. package/dist/chunks/index-CaaQQlPJ.esm.js.map +0 -1
  59. package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +0 -1
  60. package/dist/chunks/proxy-r7lARftl.esm.js.map +0 -1
  61. package/docs/reactive/scan.md +0 -324
@@ -1,6 +1,28 @@
1
1
  'use strict';
2
2
 
3
- var asyncCore = require('./async-core-CRLKP3l-.cjs');
3
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
4
+ // Queue for hooks registered before the environment is ready (circular dependency fix)
5
+ const hooks = new Set();
6
+ const asyncHooks = {
7
+ addHook(hook) {
8
+ hooks.add(hook);
9
+ return () => hooks.delete(hook);
10
+ },
11
+ /**
12
+ * [Hack] Sanitize a promise (or value) to prevent context leaks.
13
+ * Default: Identity function.
14
+ * Browser: Uses Macrotask wrapping to break microtask chains.
15
+ */
16
+ sanitizePromise(p) {
17
+ return p;
18
+ },
19
+ };
20
+ /**
21
+ * Register a hook that will be called whenever an asynchronous operation is initiated.
22
+ * The hook should return a restorer function which will be called just before the async callback runs.
23
+ * That restorer should in turn return an undoer function which will be called just after the async callback finishes.
24
+ */
25
+ const asyncHook = (hook) => asyncHooks.addHook(hook);
4
26
 
5
27
  /**
6
28
  * Yields tuples containing elements from each input array, stopping at the longest array length
@@ -99,16 +121,16 @@ const FoolProof = {
99
121
  if (hasNode && obj instanceof Node) {
100
122
  obj[prop] = value;
101
123
  return true;
102
- }
103
- if (!(obj instanceof Object) && !Reflect.has(obj, prop)) {
124
+ } /*
125
+ if (!(obj instanceof Object) && !Object.hasOwn(obj, prop)) {
104
126
  Object.defineProperty(obj, prop, {
105
127
  value,
106
128
  configurable: true,
107
129
  writable: true,
108
130
  enumerable: true,
109
- });
110
- return true;
111
- }
131
+ })
132
+ return true
133
+ }*/
112
134
  return Reflect.set(obj, prop, value, receiver);
113
135
  },
114
136
  };
@@ -132,15 +154,8 @@ function deepCompare(a, b, cache = new Map()) {
132
154
  return a === b;
133
155
  }
134
156
  // 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
- });
157
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
142
158
  return false;
143
- }
144
159
  // Circular reference protection
145
160
  let compared = cache.get(a);
146
161
  if (compared?.has(b))
@@ -152,39 +167,21 @@ function deepCompare(a, b, cache = new Map()) {
152
167
  compared.add(b);
153
168
  // Handle specific object types
154
169
  if (Array.isArray(a)) {
155
- if (!Array.isArray(b)) {
156
- console.warn(`[deepCompare] B is not an array`);
170
+ if (!Array.isArray(b) || a.length !== b.length)
157
171
  return false;
158
- }
159
- if (a.length !== b.length) {
160
- console.warn(`[deepCompare] array length mismatch:`, { lenA: a.length, lenB: b.length });
161
- return false;
162
- }
163
172
  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}`);
173
+ if (!deepCompare(a[i], b[i], cache))
166
174
  return false;
167
- }
168
175
  }
169
176
  return true;
170
177
  }
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
- }
178
+ if (a instanceof Date)
179
+ return b instanceof Date && a.getTime() === b.getTime();
180
+ if (a instanceof RegExp)
181
+ return b instanceof RegExp && a.toString() === b.toString();
183
182
  if (a instanceof Set) {
184
- if (!(b instanceof Set) || a.size !== b.size) {
185
- console.warn(`[deepCompare] Set size mismatch`);
183
+ if (!(b instanceof Set) || a.size !== b.size)
186
184
  return false;
187
- }
188
185
  for (const val of a) {
189
186
  let found = false;
190
187
  for (const bVal of b) {
@@ -193,18 +190,14 @@ function deepCompare(a, b, cache = new Map()) {
193
190
  break;
194
191
  }
195
192
  }
196
- if (!found) {
197
- console.warn(`[deepCompare] missing Set element`);
193
+ if (!found)
198
194
  return false;
199
- }
200
195
  }
201
196
  return true;
202
197
  }
203
198
  if (a instanceof Map) {
204
- if (!(b instanceof Map) || a.size !== b.size) {
205
- console.warn(`[deepCompare] Map size mismatch`);
199
+ if (!(b instanceof Map) || a.size !== b.size)
206
200
  return false;
207
- }
208
201
  for (const [key, val] of a) {
209
202
  if (!b.has(key)) {
210
203
  let foundMatch = false;
@@ -214,16 +207,11 @@ function deepCompare(a, b, cache = new Map()) {
214
207
  break;
215
208
  }
216
209
  }
217
- if (!foundMatch) {
218
- console.warn(`[deepCompare] missing Map key`);
210
+ if (!foundMatch)
219
211
  return false;
220
- }
221
212
  }
222
- else {
223
- if (!deepCompare(val, b.get(key), cache)) {
224
- console.warn(`[deepCompare] Map value mismatch for key`);
225
- return false;
226
- }
213
+ else if (!deepCompare(val, b.get(key), cache)) {
214
+ return false;
227
215
  }
228
216
  }
229
217
  return true;
@@ -231,29 +219,11 @@ function deepCompare(a, b, cache = new Map()) {
231
219
  // Compare own properties
232
220
  const keysA = Object.keys(a);
233
221
  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
- });
222
+ if (keysA.length !== keysB.length)
243
223
  return false;
244
- }
245
224
  for (const key of keysA) {
246
- if (!Object.hasOwn(b, key)) {
247
- console.warn(`[deepCompare] missing key ${String(key)} in B`);
248
- 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
- });
225
+ if (!Object.hasOwn(b, key) || !deepCompare(a[key], b[key], cache))
255
226
  return false;
256
- }
257
227
  }
258
228
  return true;
259
229
  }
@@ -301,6 +271,12 @@ function named(name, fn) {
301
271
  });
302
272
  return fn;
303
273
  }
274
+ const _mode = (typeof process !== 'undefined' && process.env?.NODE_ENV) ||
275
+ (typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('chunks/proxy-BvM4yewA.cjs', document.baseURI).href)) }) !== 'undefined' && undefined?.MODE) ||
276
+ 'production';
277
+ const isDev = _mode === 'development';
278
+ const isProd = _mode === 'production';
279
+ const isTest = _mode === 'test';
304
280
 
305
281
  // biome-ignore-all lint/suspicious/noConfusingVoidType: We *love* voids
306
282
  // Standardized decorator system that works with both Legacy and Modern decorators
@@ -528,9 +504,7 @@ function flavorOptions(fn, defaultOptions, opts = {}) {
528
504
  const isObject = currentOptions !== null &&
529
505
  typeof currentOptions === 'object' &&
530
506
  !Array.isArray(currentOptions);
531
- newArgs[targetIndex] = isObject
532
- ? { ...defaultOptions, ...currentOptions }
533
- : defaultOptions;
507
+ newArgs[targetIndex] = isObject ? { ...defaultOptions, ...currentOptions } : defaultOptions;
534
508
  return fn.apply(this, newArgs);
535
509
  };
536
510
  if (opts.name)
@@ -871,6 +845,20 @@ function mixin(mixinFunction, unwrapFunction) {
871
845
  });
872
846
  }
873
847
 
848
+ const debugHooks = {
849
+ isDevtoolsEnabled: () => false,
850
+ registerEffect: () => { },
851
+ getTriggerChain: () => [],
852
+ captureStack: () => [],
853
+ captureLineage: () => new Error().stack,
854
+ formatStack: (stack) => [stack],
855
+ recordTriggerLink: () => { },
856
+ decorateError: () => { },
857
+ };
858
+ function setDebugHooks(hooks) {
859
+ Object.assign(debugHooks, hooks);
860
+ }
861
+
874
862
  /******************************************************************************
875
863
  Copyright (c) Microsoft Corporation.
876
864
 
@@ -922,41 +910,17 @@ function __runInitializers(thisArg, initializers, value) {
922
910
  }
923
911
  return useValue ? value : void 0;
924
912
  }
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
913
  function __classPrivateFieldGet(receiver, state, kind, f) {
930
914
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
931
915
  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
916
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
933
917
  }
934
918
 
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
919
  typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
943
920
  var e = new Error(message);
944
921
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
945
922
  };
946
923
 
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
924
  var _ZoneAggregator_zones;
961
925
  function isu(z) {
962
926
  return z;
@@ -981,7 +945,7 @@ class AZone {
981
945
  }
982
946
  // [HACK]: Sanitization
983
947
  // See BROWSER_ASYNC_POLYFILL.md
984
- return asyncCore.asyncHooks.sanitizePromise(res);
948
+ return asyncHooks.sanitizePromise(res);
985
949
  }
986
950
  root(fn) {
987
951
  const prev = this.enter();
@@ -1084,13 +1048,27 @@ class ZoneAggregator extends AZone {
1084
1048
  }
1085
1049
  }
1086
1050
  _ZoneAggregator_zones = new WeakMap();
1051
+ /**
1052
+ * Aggregator of zones that should be preserved across async boundaries.
1053
+ * If you add a zone here, it will be preserved across async boundaries.
1054
+ *
1055
+ * @example
1056
+ * ```ts
1057
+ * import { Zone, asyncZone } from 'mutts'
1058
+ * const userZone = new Zone<User>()
1059
+ * asyncZone.add(userZone)
1060
+ * ```
1061
+ */
1087
1062
  const asyncZone = tag('async', new ZoneAggregator());
1088
- asyncCore.asyncHooks.addHook(() => {
1063
+ asyncHooks.addHook(() => {
1064
+ // capture state before async boundary
1089
1065
  const zone = asyncZone.active;
1090
1066
  return () => {
1067
+ // restore state after async boundary, temporarily
1091
1068
  const prev = asyncZone.active;
1092
1069
  asyncZone.active = zone;
1093
1070
  return () => {
1071
+ // restore previous state from before our restore
1094
1072
  asyncZone.active = prev;
1095
1073
  };
1096
1074
  };
@@ -1160,6 +1138,75 @@ function getRoot(fn) {
1160
1138
  return fn;
1161
1139
  }
1162
1140
 
1141
+ const effectHistory = tag('effectHistory', new ZoneHistory());
1142
+ tag('effectHistory.present', effectHistory.present);
1143
+ asyncZone.add(effectHistory);
1144
+ /**
1145
+ * Aggregator for zones that need to be tracked along effects.
1146
+ * ie. in each effect, the active zone of the given zoning will be the one active at effect's definition
1147
+ */
1148
+ const effectAggregator = tag('effectAggregator', new ZoneAggregator(effectHistory.present));
1149
+ function isRunning(effect) {
1150
+ const root = getRoot(effect);
1151
+ return effectHistory.some((e) => getRoot(e) === root);
1152
+ }
1153
+ function getActiveEffect() {
1154
+ return effectHistory.present.active;
1155
+ }
1156
+ const cleanups = new WeakMap();
1157
+ /**
1158
+ * Attach cleanup dependencies to an object. When `unlink(obj)` is called,
1159
+ * each dependency is disposed: functions are invoked with the cleanup reason,
1160
+ * objects are recursively `unlink`ed. This forms a cleanup tree.
1161
+ *
1162
+ * @param obj - The owner object
1163
+ * @param cleanupFns - Cleanup callbacks and/or child objects to unlink recursively
1164
+ * @returns The owner object (for chaining)
1165
+ *
1166
+ * @example
1167
+ * ```ts
1168
+ * // Functions are called with CleanupReason
1169
+ * link(parent, () => console.log('disposed'))
1170
+ *
1171
+ * // Objects are recursively unlinked
1172
+ * link(parent, childA, childB)
1173
+ *
1174
+ * // Mixed
1175
+ * link(parent, childObj, () => timer.clear())
1176
+ *
1177
+ * unlink(parent) // disposes childA, childB, calls the function
1178
+ * ```
1179
+ */
1180
+ function link(obj, ...cleanupFns) {
1181
+ const set = cleanups.get(obj);
1182
+ if (!set)
1183
+ cleanups.set(obj, new Set(cleanupFns.filter(Boolean)));
1184
+ else
1185
+ for (const fn of cleanupFns)
1186
+ if (fn)
1187
+ set.add(fn);
1188
+ return obj;
1189
+ }
1190
+ /**
1191
+ * Dispose an object's cleanup dependencies. Functions are called with the
1192
+ * reason; linked objects are recursively unlinked. The cleanup set is removed
1193
+ * so calling `unlink` twice is safe (second call is a no-op).
1194
+ *
1195
+ * @param obj - The object to dispose
1196
+ * @param reason - Optional cleanup reason propagated to callbacks
1197
+ */
1198
+ function unlink(obj, reason) {
1199
+ const set = cleanups.get(obj);
1200
+ if (set) {
1201
+ cleanups.delete(obj);
1202
+ for (const fn of set)
1203
+ if (typeof fn === 'function')
1204
+ fn(reason);
1205
+ else
1206
+ unlink(fn, reason);
1207
+ }
1208
+ }
1209
+
1163
1210
  const effectMarker = {
1164
1211
  enter: 'effect:enter',
1165
1212
  leave: 'effect:leave',
@@ -1222,6 +1269,11 @@ function formatCleanupReason(reason, depth = 0) {
1222
1269
  }
1223
1270
  }
1224
1271
  }
1272
+ // Track native reactivity
1273
+ /**
1274
+ * Symbol to mark class properties as non-reactive
1275
+ */
1276
+ const unreactiveProperties = Symbol('unreactive-properties');
1225
1277
  /**
1226
1278
  * Symbol representing all properties in reactive tracking
1227
1279
  */
@@ -1231,14 +1283,6 @@ const allProps = Symbol('all-props');
1231
1283
  * Used by ownKeys proxy trap — Object.keys(), for..in, Map.keys() depend on this.
1232
1284
  */
1233
1285
  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
1286
  /**
1243
1287
  * Structured error codes for machine-readable diagnosis
1244
1288
  */
@@ -1312,6 +1356,12 @@ const options = {
1312
1356
  * @param runningChain - The array of effects from the detected one to the currently running one
1313
1357
  */
1314
1358
  skipRunningEffect: (_effect) => { },
1359
+ /**
1360
+ * Debug purpose: called when an effect starts executing.
1361
+ * @param effect - The effect being executed (root function)
1362
+ * @param reaction - false for initial creation, true/CleanupReason for subsequent runs
1363
+ */
1364
+ effectRun: (_effect, _reaction) => { },
1315
1365
  /**
1316
1366
  * Debug purpose: maximum effect chain (like call stack max depth)
1317
1367
  * Used to prevent infinite loops
@@ -1438,34 +1488,6 @@ const options = {
1438
1488
  enableHistory: true,
1439
1489
  historySize: 50,
1440
1490
  },
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
1491
  };
1470
1492
  function optionCall(name, ...args) {
1471
1493
  const fn = options[name];
@@ -1479,6 +1501,36 @@ function optionCall(name, ...args) {
1479
1501
  options.warn(`options.${name} threw`, error);
1480
1502
  }
1481
1503
  }
1504
+ /** Production preset: no introspection, heuristic cycle detection, minimal overhead */
1505
+ const prodPreset = {
1506
+ maxEffectReaction: 'throw',
1507
+ cycleHandling: 'production',
1508
+ introspection: null,
1509
+ onMemoizationDiscrepancy: undefined,
1510
+ };
1511
+ /** Development preset (default): introspection on, early cycle detection, warnings */
1512
+ const devPreset = {
1513
+ maxEffectReaction: 'warn',
1514
+ cycleHandling: 'development',
1515
+ introspection: {
1516
+ gatherReasons: { lineages: 'touch' },
1517
+ logErrors: true,
1518
+ enableHistory: true,
1519
+ historySize: 50,
1520
+ },
1521
+ onMemoizationDiscrepancy: undefined,
1522
+ };
1523
+ /** Debug preset: full diagnostics, throws on violations, rich lineage capture */
1524
+ const debugPreset = {
1525
+ maxEffectReaction: 'debug',
1526
+ cycleHandling: 'debug',
1527
+ introspection: {
1528
+ gatherReasons: { lineages: 'both' },
1529
+ logErrors: true,
1530
+ enableHistory: true,
1531
+ historySize: 200,
1532
+ },
1533
+ };
1482
1534
  // --- Proxy State (Merged from proxy-state.ts) ---
1483
1535
  const objectToProxy = new WeakMap();
1484
1536
  const proxyToObject = new WeakMap();
@@ -1498,54 +1550,28 @@ function isReactive(obj) {
1498
1550
  return proxyToObject.has(obj);
1499
1551
  }
1500
1552
 
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
1553
  // Track dependency stacks per (obj, prop, effect)
1545
1554
  let dependencyStacks = new WeakMap();
1555
+ let assertUntrackedFlag = false;
1546
1556
  function resetTracking() {
1547
1557
  dependencyStacks = new WeakMap();
1548
1558
  }
1559
+ /**
1560
+ * Executes a function and throws if any reactive dependencies are tracked during execution.
1561
+ * Used to assert that code runs in an untracked context.
1562
+ */
1563
+ function assertUntracked(fn) {
1564
+ if (assertUntrackedFlag) {
1565
+ throw new Error('assertUntracked: nested calls are not supported');
1566
+ }
1567
+ assertUntrackedFlag = true;
1568
+ try {
1569
+ return fn();
1570
+ }
1571
+ finally {
1572
+ assertUntrackedFlag = false;
1573
+ }
1574
+ }
1549
1575
  function getDependencyStack(effect, obj, prop) {
1550
1576
  const objStacks = dependencyStacks.get(obj);
1551
1577
  if (!objStacks)
@@ -1558,14 +1584,17 @@ function getDependencyStack(effect, obj, prop) {
1558
1584
  * @param prop - The property name (defaults to allProps)
1559
1585
  */
1560
1586
  function dependant(obj, prop = allProps) {
1587
+ if (assertUntrackedFlag) {
1588
+ throw new Error(`Reactive dependency tracking detected in assertUntracked context: ${String(prop)} on ${obj}`);
1589
+ }
1561
1590
  obj = unwrap(obj);
1562
1591
  const currentActiveEffect = getActiveEffect();
1563
1592
  // Early return if no active effect, tracking disabled, or invalid prop
1564
1593
  if (!currentActiveEffect || (typeof prop === 'symbol' && prop !== allProps && prop !== keysOf))
1565
1594
  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);
1595
+ const node = getEffectNode(currentActiveEffect);
1596
+ if ('dependencyHook' in node) {
1597
+ node.dependencyHook(obj, prop);
1569
1598
  }
1570
1599
  let objectWatchers = exports.watchers.get(obj);
1571
1600
  if (!objectWatchers) {
@@ -1702,6 +1731,7 @@ let effectTriggeredBy = new WeakMap();
1702
1731
  // consequencesClosure: for each effect, all effects that it triggers (directly or indirectly)
1703
1732
  let causesClosure = new WeakMap();
1704
1733
  let consequencesClosure = new WeakMap();
1734
+ // Batch re-entrance depth and broken state
1705
1735
  let broken = false;
1706
1736
  /**
1707
1737
  * Gets or creates an IterableWeakSet for a closure map
@@ -2355,6 +2385,7 @@ function batch(effect, immediate) {
2355
2385
  throw new Error('Activation registry already exists');
2356
2386
  optionCall('beginChain', roots);
2357
2387
  }
2388
+ // TODO: Consider this has been produced but was useless - it might be more correct ?const caller = executingStack.length > 0 ? getActiveEffect() : undefined
2358
2389
  const caller = getActiveEffect();
2359
2390
  // Optimization: If nested and NOT immediate, just join the existing batch
2360
2391
  if (!isNewBatch && !immediate) {
@@ -2508,6 +2539,22 @@ const atomic = decorator({
2508
2539
  };
2509
2540
  },
2510
2541
  });
2542
+ /**
2543
+ * Wraps `fn` so it runs within `effect`'s zone context when invoked later.
2544
+ *
2545
+ * Useful for deferred callbacks (event listeners, `DOMContentLoaded`, etc.)
2546
+ * that need sub-effects parented to the original effect.
2547
+ *
2548
+ * @param prev - The effect whose context should be restored, or `undefined` for root context
2549
+ * @param fn - The function to wrap
2550
+ * @returns A function with the same signature that restores the effect context before calling `fn`
2551
+ */
2552
+ function captured(prev, fn) {
2553
+ prev ?? (prev = effectHistory.active);
2554
+ return named(effectMarker.leave, (...args) => {
2555
+ return effectHistory.with(prev, () => fn(...args));
2556
+ });
2557
+ }
2511
2558
  /**
2512
2559
  * Runs `fn` atomically and **always immediately**, batching all reactive effects
2513
2560
  * triggered inside it so they fire only once after `fn` completes.
@@ -2530,19 +2577,13 @@ function atom(fn) {
2530
2577
  return batch(fn, 'immediate');
2531
2578
  }
2532
2579
  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
2580
  /**
2538
2581
  * Creates a reactive effect that automatically re-runs when dependencies change
2539
2582
  * @param fn - The effect function that provides dependencies and may return a cleanup function or Promise
2540
2583
  * @param options - Options for effect execution
2541
2584
  * @returns A cleanup function to stop the effect
2542
2585
  */
2543
- const effect = named(effectMarker.leave, flavored(function effect(
2544
- // biome-ignore lint/suspicious/noConfusingVoidType: Effect callbacks commonly return void
2545
- fn, effectOptions = {}) {
2586
+ const effect = named(effectMarker.leave, flavored(function effect(fn, effectOptions = {}) {
2546
2587
  if (effectOptions?.name)
2547
2588
  Object.defineProperty(fn, 'name', { value: effectOptions.name });
2548
2589
  // Use per-effect asyncMode or fall back to global option
@@ -2566,6 +2607,7 @@ fn, effectOptions = {}) {
2566
2607
  if (runningPromise) {
2567
2608
  if (asyncMode === 'cancel' && cancelPrevious) {
2568
2609
  // Cancel previous execution
2610
+ abort();
2569
2611
  cancelPrevious();
2570
2612
  cancelPrevious = null;
2571
2613
  runningPromise = null;
@@ -2589,11 +2631,33 @@ fn, effectOptions = {}) {
2589
2631
  access.reaction = node.nextReason || access.reaction;
2590
2632
  node.nextReason = undefined;
2591
2633
  optionCall('enter', getRoot(fn));
2634
+ optionCall('effectRun', getRoot(fn), access.reaction);
2592
2635
  let result;
2593
2636
  let caught = 0;
2594
- // Default thrower (self)
2595
- let thrower = (error) => {
2596
- throw error;
2637
+ // Define bubbling thrower
2638
+ const thrower = (error) => {
2639
+ const catches = node.catchers;
2640
+ const reason = { type: 'error', error };
2641
+ if (catches)
2642
+ while (caught < catches.length) {
2643
+ cleanupReaction(reason);
2644
+ try {
2645
+ reactionCleanup = catches[caught](error);
2646
+ return;
2647
+ }
2648
+ catch (_e) {
2649
+ caught++;
2650
+ }
2651
+ }
2652
+ if (parent) {
2653
+ const parentNode = getEffectNode(parent);
2654
+ if (parentNode.forwardThrow)
2655
+ parentNode.forwardThrow(error);
2656
+ else
2657
+ throw error;
2658
+ }
2659
+ else
2660
+ throw error;
2597
2661
  };
2598
2662
  node.forwardThrow = thrower;
2599
2663
  let errorToThrow;
@@ -2643,12 +2707,14 @@ fn, effectOptions = {}) {
2643
2707
  }
2644
2708
  }
2645
2709
  catch (error) {
2710
+ debugHooks.decorateError(error, runEffect);
2646
2711
  // catcher:self`
2647
2712
  errorToThrow = error;
2648
2713
  }
2649
2714
  // Create cleanup function for next run
2650
2715
  node.cleanup = (reason) => {
2651
2716
  node.cleanup = undefined;
2717
+ abort();
2652
2718
  cleanupReaction(reason);
2653
2719
  delete node.catchers;
2654
2720
  // Remove this effect from all reactive objects it's watching
@@ -2659,13 +2725,11 @@ fn, effectOptions = {}) {
2659
2725
  if (objectWatchers) {
2660
2726
  for (const [prop, deps] of objectWatchers.entries()) {
2661
2727
  deps.delete(runEffect);
2662
- if (deps.size === 0) {
2728
+ if (deps.size === 0)
2663
2729
  objectWatchers.delete(prop);
2664
- }
2665
2730
  }
2666
- if (objectWatchers.size === 0) {
2731
+ if (objectWatchers.size === 0)
2667
2732
  exports.watchers.delete(reactiveObj);
2668
- }
2669
2733
  }
2670
2734
  }
2671
2735
  exports.effectToReactiveObjects.delete(runEffect);
@@ -2683,33 +2747,6 @@ fn, effectOptions = {}) {
2683
2747
  delete node.children;
2684
2748
  }
2685
2749
  };
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
2750
  if (errorToThrow)
2714
2751
  thrower(errorToThrow);
2715
2752
  };
@@ -2728,26 +2765,25 @@ fn, effectOptions = {}) {
2728
2765
  node.parent = parent;
2729
2766
  // let thrower: CatchFunction | undefined // Moved inside runEffect
2730
2767
  let effectStopped = false;
2768
+ let abortController;
2731
2769
  const access = {
2732
2770
  tracked,
2733
2771
  ascend: named(effectMarker.leave, (fn) => ascended(named(effectMarker.enter, () => fn.call(null)))),
2734
2772
  //named(effectMarker.enter, (fn) => ascended(fn)),
2735
2773
  reaction: false,
2774
+ get signal() {
2775
+ if (!abortController) {
2776
+ abortController = new AbortController();
2777
+ }
2778
+ return abortController.signal;
2779
+ },
2736
2780
  };
2737
2781
  let runningPromise = null;
2738
2782
  let cancelPrevious = null;
2739
- if (effectOptions?.dependencyHook) {
2783
+ if (effectOptions?.dependencyHook)
2740
2784
  node.dependencyHook = effectOptions.dependencyHook;
2741
- }
2742
2785
  // Mark the runEffect callback with the original function as its root
2743
2786
  markWithRoot(runEffect, fn);
2744
- function augmentedRv(rv) {
2745
- return Object.defineProperties(rv, {
2746
- [stopped]: {
2747
- get: () => effectStopped,
2748
- },
2749
- });
2750
- }
2751
2787
  // Register strict mode if enabled
2752
2788
  if (effectOptions?.opaque) {
2753
2789
  node.isOpaque = true;
@@ -2756,6 +2792,12 @@ fn, effectOptions = {}) {
2756
2792
  debugHooks.registerEffect(runEffect);
2757
2793
  }
2758
2794
  // Store parent relationship for hierarchy traversal - ALREADY DONE ABOVE via getEffectNode
2795
+ const abort = () => {
2796
+ if (abortController) {
2797
+ abortController.abort(new ReactiveError('[reactive] Effect aborted due to dependency change or stop'));
2798
+ abortController = undefined;
2799
+ }
2800
+ };
2759
2801
  batch(runEffect, 'immediate');
2760
2802
  // Only ROOT effects are registered for GC cleanup and zone tracking
2761
2803
  const isRootEffect = !parent;
@@ -2765,6 +2807,7 @@ fn, effectOptions = {}) {
2765
2807
  effectStopped = true;
2766
2808
  node.stopped = true;
2767
2809
  // Cancel any running async work
2810
+ abort();
2768
2811
  if (cancelPrevious) {
2769
2812
  cancelPrevious();
2770
2813
  cancelPrevious = null;
@@ -2783,7 +2826,7 @@ fn, effectOptions = {}) {
2783
2826
  fr.unregister(stopEffect);
2784
2827
  };
2785
2828
  if (isRootEffect) {
2786
- const callIfCollected = augmentedRv((reason) => stopEffect(reason));
2829
+ const callIfCollected = (reason) => stopEffect(reason);
2787
2830
  fr.register(callIfCollected, () => {
2788
2831
  stopEffect({ type: 'gc' });
2789
2832
  optionCall('garbageCollected', fn);
@@ -2797,16 +2840,16 @@ fn, effectOptions = {}) {
2797
2840
  parentNode.children = new Set();
2798
2841
  }
2799
2842
  const children = parentNode.children;
2800
- const subEffectCleanup = augmentedRv((reason) => {
2843
+ const subEffectCleanup = (reason) => {
2801
2844
  children.delete(subEffectCleanup);
2802
2845
  // Execute this child effect cleanup (which triggers its own mainCleanup)
2803
2846
  stopEffect(reason);
2804
- });
2847
+ };
2805
2848
  children.add(subEffectCleanup);
2806
2849
  return subEffectCleanup;
2807
2850
  }
2808
2851
  // Should not be reachable given isRootEffect check, but for type safety
2809
- return augmentedRv((reason) => stopEffect(reason));
2852
+ return (reason) => stopEffect(reason);
2810
2853
  }, {
2811
2854
  get opaque() {
2812
2855
  return flavorOptions(this, { opaque: true }, { name: 'opaque' });
@@ -2837,9 +2880,11 @@ function biDi(received, get, set) {
2837
2880
  get = get.get;
2838
2881
  }
2839
2882
  let programmaticallySetValue = Symbol();
2840
- effect(markWithRoot(() => {
2883
+ effect.named('biDi')(markWithRoot(() => {
2841
2884
  const newValue = get();
2842
- if (unwrap(newValue) !== programmaticallySetValue)
2885
+ const pValue = programmaticallySetValue;
2886
+ programmaticallySetValue = Symbol();
2887
+ if (unwrap(newValue) !== pValue)
2843
2888
  received(newValue);
2844
2889
  }, received));
2845
2890
  return set
@@ -2910,16 +2955,16 @@ function bubbleUpChange(changedObject, evolution) {
2910
2955
  const parents = objectParents.get(changedObject);
2911
2956
  if (!parents)
2912
2957
  return;
2913
- for (const { parent, prop } of parents) {
2958
+ for (const { parent } of parents) {
2914
2959
  // Trigger deep watchers on parent
2915
2960
  const parentDeepWatchers = deepWatchers.get(parent);
2916
2961
  if (parentDeepWatchers) {
2917
2962
  if (options.introspection?.gatherReasons) {
2918
2963
  const gatherReasons = options.introspection.gatherReasons;
2919
2964
  const lineageConfig = gatherReasons.lineages;
2920
- let touchStack;
2965
+ let touchLineage;
2921
2966
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
2922
- touchStack = debugHooks.captureLineage();
2967
+ touchLineage = debugHooks.captureLineage();
2923
2968
  }
2924
2969
  for (const watcher of parentDeepWatchers) {
2925
2970
  const dependencyStack = lineageConfig === 'dependency' || lineageConfig === 'both'
@@ -2932,7 +2977,7 @@ function bubbleUpChange(changedObject, evolution) {
2932
2977
  obj: parent,
2933
2978
  evolution,
2934
2979
  dependency: dependencyStack,
2935
- touch: touchStack,
2980
+ touch: touchLineage,
2936
2981
  });
2937
2982
  }
2938
2983
  }
@@ -2985,6 +3030,8 @@ function collectEffects(obj, evolution, effects, objectWatchers, ...keyChains) {
2985
3030
  for (const key of keys) {
2986
3031
  const deps = objectWatchers.get(key);
2987
3032
  if (deps) {
3033
+ // Make sure `some.prop++` does not keep a dependency to `some.props`
3034
+ deps.delete(sourceEffect);
2988
3035
  for (const effect of deps) {
2989
3036
  const runningChain = isRunning(effect);
2990
3037
  if (runningChain) {
@@ -3035,9 +3082,9 @@ function touched(obj, evolution, props) {
3035
3082
  if (options.introspection?.gatherReasons) {
3036
3083
  const gatherReasons = options.introspection.gatherReasons;
3037
3084
  const lineageConfig = gatherReasons.lineages;
3038
- let touchStack;
3085
+ let touchLineage;
3039
3086
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3040
- touchStack = debugHooks.captureLineage();
3087
+ touchLineage = debugHooks.captureLineage();
3041
3088
  }
3042
3089
  for (const [effect, dependencyStack] of effects) {
3043
3090
  const node = getEffectNode(effect);
@@ -3047,7 +3094,7 @@ function touched(obj, evolution, props) {
3047
3094
  obj,
3048
3095
  evolution,
3049
3096
  dependency: dependencyStack,
3050
- touch: touchStack,
3097
+ touch: touchLineage,
3051
3098
  });
3052
3099
  }
3053
3100
  }
@@ -3086,10 +3133,10 @@ function touchedOpaque(obj, evolution, prop) {
3086
3133
  }
3087
3134
  effects.add(effect);
3088
3135
  if (gather) {
3089
- let touchStack;
3136
+ let touchLineage;
3090
3137
  let dependencyStack;
3091
3138
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3092
- touchStack = debugHooks.captureLineage();
3139
+ touchLineage = debugHooks.captureLineage();
3093
3140
  }
3094
3141
  if (lineageConfig === 'dependency' || lineageConfig === 'both') {
3095
3142
  dependencyStack = getDependencyStack(effect, obj, prop);
@@ -3100,7 +3147,7 @@ function touchedOpaque(obj, evolution, prop) {
3100
3147
  obj,
3101
3148
  evolution,
3102
3149
  dependency: dependencyStack,
3103
- touch: touchStack,
3150
+ touch: touchLineage,
3104
3151
  });
3105
3152
  }
3106
3153
  recordActivation(effect, obj, evolution, prop);
@@ -3129,59 +3176,60 @@ function touchedOpaque(obj, evolution, prop) {
3129
3176
  }
3130
3177
  }
3131
3178
 
3132
- const nonReactiveObjects = new WeakSet();
3133
- const nonReactiveClasses = new WeakSet();
3134
- const unreactiveProps = new WeakMap();
3135
- let unreactivePropsCount = 0;
3179
+ const absent = Symbol('absent');
3180
+ /**
3181
+ * Add unreactive properties to a prototype.
3182
+ * If no set is provided, marks the entire object/prototype as non-reactive (sets [unreactiveProperties] = true).
3183
+ * If a set is provided, merges with existing unreactive properties (never overrides true).
3184
+ */
3136
3185
  function addUnreactiveProps(proto, set) {
3137
- unreactiveProps.set(proto, set);
3138
- unreactivePropsCount++;
3186
+ if (unreactiveProperties in proto) {
3187
+ const existing = proto[unreactiveProperties];
3188
+ // If already fully unreactive, don't change
3189
+ if (existing === true)
3190
+ return proto;
3191
+ // If no set provided, upgrade to fully unreactive
3192
+ if (!set) {
3193
+ proto[unreactiveProperties] = true;
3194
+ return proto;
3195
+ }
3196
+ // Merge sets
3197
+ set = proto[unreactiveProperties] = new Set(proto[unreactiveProperties]);
3198
+ for (const p of set)
3199
+ existing.add(p);
3200
+ }
3201
+ // If no set, mark as fully unreactive, otherwise create set
3202
+ else
3203
+ proto[unreactiveProperties] = set ? new Set(set) : true;
3204
+ return proto;
3139
3205
  }
3140
3206
  /** Check if a property is marked unreactive on obj or any of its prototypes (trap-free) */
3141
3207
  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;
3208
+ if (typeof prop === 'symbol' || prop === 'constructor')
3209
+ return true;
3210
+ const marker = obj[unreactiveProperties];
3211
+ return (marker === true || // Fully unreactive
3212
+ marker?.has?.(prop) || // Property is unreactive
3213
+ false);
3151
3214
  }
3152
- const immutables = new Set();
3153
- const absent = Symbol('absent');
3154
- function markNonReactive(...obj) {
3155
- for (const o of obj)
3156
- nonReactiveObjects.add(o);
3215
+ function nonReactive(...obj) {
3216
+ for (const o of obj) {
3217
+ o[unreactiveProperties] = true;
3218
+ }
3157
3219
  return obj[0];
3158
3220
  }
3159
3221
  function nonReactiveClass(...cls) {
3160
3222
  for (const c of cls)
3161
3223
  if (c)
3162
- nonReactiveClasses.add(c.prototype);
3224
+ c.prototype[unreactiveProperties] = true;
3163
3225
  return cls[0];
3164
3226
  }
3165
3227
  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;
3228
+ return !obj || obj[unreactiveProperties] === true;
3181
3229
  }
3182
3230
  nonReactiveClass(Date, RegExp, Error, Promise, Function);
3183
3231
  if (typeof window !== 'undefined') {
3184
- markNonReactive(window, document);
3232
+ nonReactive(window, document);
3185
3233
  nonReactiveClass(Node, Element, HTMLElement, EventTarget, HTMLCollection, NodeList);
3186
3234
  }
3187
3235
 
@@ -3203,10 +3251,33 @@ function shouldRecurseTouch(oldValue, newValue) {
3203
3251
  if ((typeof oldValue !== 'object' && !Array.isArray(oldValue)) ||
3204
3252
  (typeof newValue !== 'object' && !Array.isArray(newValue)))
3205
3253
  return false;
3206
- if (isNonReactive(oldValue) || isNonReactive(newValue))
3254
+ if (isNonReactive(oldValue) /*|| isNonReactive(newValue)*/)
3207
3255
  return false;
3208
3256
  return getPrototypeToken(oldValue) === getPrototypeToken(newValue);
3209
3257
  }
3258
+ /**
3259
+ * Migrate all watcher registrations from oldRef to newRef.
3260
+ * Called when deep touch replaces an object identity without any child value differences,
3261
+ * to prevent watcher orphaning (effects still pointing at the discarded old object).
3262
+ */
3263
+ function migrateWatchers(oldRef, newRef) {
3264
+ const oldMap = exports.watchers.get(oldRef);
3265
+ if (!oldMap)
3266
+ return;
3267
+ // Move the entire watcher map
3268
+ exports.watchers.set(newRef, oldMap);
3269
+ exports.watchers.delete(oldRef);
3270
+ // Update the reverse map (effect → objects it watches)
3271
+ for (const deps of oldMap.values()) {
3272
+ for (const effect of deps) {
3273
+ const objects = exports.effectToReactiveObjects.get(effect);
3274
+ if (objects) {
3275
+ objects.delete(oldRef);
3276
+ objects.add(newRef);
3277
+ }
3278
+ }
3279
+ }
3280
+ }
3210
3281
  /**
3211
3282
  * Centralized function to handle property change notifications with optional recursive touch
3212
3283
  * @param targetObj - The object whose property changed
@@ -3224,7 +3295,15 @@ function notifyPropertyChange(targetObj, prop, oldValue, newValue, hadProperty)
3224
3295
  const origin = { obj: unwrappedObj, prop };
3225
3296
  // Deep touch: only notify nested property changes with origin filtering
3226
3297
  // 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)));
3298
+ const changes = untracked(() => recursiveTouch(oldValue, newValue, new WeakMap(), [], origin));
3299
+ // When deep touch found no child differences, the object identity still changed.
3300
+ // Migrate watchers from old → new so the dependency chain is preserved.
3301
+ if (changes.length === 0) {
3302
+ migrateWatchers(unwrap(oldValue), unwrap(newValue));
3303
+ }
3304
+ else {
3305
+ dispatchNotifications(changes);
3306
+ }
3228
3307
  // Notify opaque listeners (like memoize) that always want to know about identity changes
3229
3308
  touchedOpaque(targetObj, evolution, prop);
3230
3309
  }
@@ -3311,12 +3390,11 @@ function diffObjectProperties(oldObj, newObj, visited, notifications, origin) {
3311
3390
  for (const key of oldKeys)
3312
3391
  if (!newKeys.has(key))
3313
3392
  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
3393
  for (const key of newKeys) {
3318
- if (!oldKeys.has(key))
3394
+ if (!oldKeys.has(key)) {
3395
+ local.push({ target: oldObj, evolution: { type: 'add', prop: key }, prop: key, origin });
3319
3396
  continue;
3397
+ }
3320
3398
  const oldEntry = unwrap(oldObj[key]);
3321
3399
  const newEntry = unwrap(newObj[key]);
3322
3400
  if (shouldRecurseTouch(oldEntry, newEntry)) {
@@ -3358,11 +3436,10 @@ function dispatchNotifications(notifications) {
3358
3436
  if (originWatchers) {
3359
3437
  const originEffects = new Map();
3360
3438
  collectEffects(origin.obj, { type: 'set', prop: origin.prop }, originEffects, originWatchers, [allProps], [origin.prop]);
3361
- for (const effect of originEffects.keys())
3362
- allowedEffects.add(effect);
3439
+ allowedEffects = new Set(originEffects.keys());
3363
3440
  }
3364
3441
  // If no allowed effects, skip all notifications (no one should be notified)
3365
- if (allowedEffects.size === 0)
3442
+ if (!allowedEffects?.size)
3366
3443
  return;
3367
3444
  }
3368
3445
  for (const notification of notifications) {
@@ -3375,7 +3452,6 @@ function dispatchNotifications(notifications) {
3375
3452
  let currentEffects;
3376
3453
  const propsArray = [prop];
3377
3454
  if (objectWatchers) {
3378
- // console.log(`[DEBUG] dispatchNotifications: processing ${obj.constructor.name} (has watchers)`)
3379
3455
  currentEffects = new Map();
3380
3456
  const broad = evolution.type !== 'set' ? [allProps, keysOf] : [allProps];
3381
3457
  collectEffects(obj, evolution, currentEffects, objectWatchers, broad, propsArray);
@@ -3411,9 +3487,9 @@ function dispatchNotifications(notifications) {
3411
3487
  if (options.introspection?.gatherReasons) {
3412
3488
  const gatherReasons = options.introspection.gatherReasons;
3413
3489
  const lineageConfig = gatherReasons.lineages;
3414
- let touchStack;
3490
+ let touchLineage;
3415
3491
  if (lineageConfig === 'touch' || lineageConfig === 'both') {
3416
- touchStack = debugHooks.captureLineage();
3492
+ touchLineage = debugHooks.captureLineage();
3417
3493
  }
3418
3494
  for (const effect of combinedEffects) {
3419
3495
  const node = getEffectNode(effect);
@@ -3427,7 +3503,7 @@ function dispatchNotifications(notifications) {
3427
3503
  obj: unwrap(target),
3428
3504
  evolution,
3429
3505
  dependency: dependencyStack,
3430
- touch: touchStack,
3506
+ touch: touchLineage,
3431
3507
  });
3432
3508
  }
3433
3509
  }
@@ -3440,11 +3516,16 @@ const metaProtos = new WeakMap();
3440
3516
  const wrapProtos = new WeakMap();
3441
3517
  const arrayLengths = new WeakMap();
3442
3518
  const hasReentry = new Set();
3443
- const subsRegister = new WeakMap();
3444
3519
  // Sub-proxy registration for custom reactive behaviors
3520
+ const subsRegister = new WeakMap();
3521
+ // Internal untracked flag for setter/getter operations - only used when testing oldValue while setting a value
3522
+ // TODO: `touched` trigger also compares to old value and should use the internalUntracked flag
3523
+ let internalUntracked = false;
3445
3524
  const reactiveHandlers = {
3446
3525
  [Symbol.toStringTag]: 'MutTs Reactive',
3447
3526
  get(obj, prop, receiver) {
3527
+ if (internalUntracked)
3528
+ return FoolProof.get(obj, prop, receiver);
3448
3529
  if (obj && typeof obj === 'object' && prop !== Symbol.toStringTag) {
3449
3530
  const metaProto = metaProtos.get(obj.constructor);
3450
3531
  if (metaProto && Object.hasOwn(metaProto, prop)) {
@@ -3464,15 +3545,21 @@ const reactiveHandlers = {
3464
3545
  if (wrapProto && Object.hasOwn(wrapProto, prop))
3465
3546
  return wrapProto[prop];
3466
3547
  }
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))
3548
+ // Symbols: fast-path — no reactivity tracking
3549
+ if (typeof prop === 'symbol' || prop === 'constructor' || isUnreactiveProp(obj, prop))
3472
3550
  return FoolProof.get(obj, prop, receiver);
3473
3551
  // Check if property exists using a trap-free walk to avoid triggering
3474
3552
  // the has-trap cascade on prototype chains of reactive proxies.
3475
3553
  const isOwnProp = Object.hasOwn(obj, prop);
3554
+ // For accessor properties, check the unwrapped object to see if it's an accessor
3555
+ // This ensures ignoreAccessors works correctly even after operations like Object.setPrototypeOf
3556
+ // Skip for null-proto objects (pounce scopes) — they never have accessors
3557
+ const shouldIgnoreAccessor = options.ignoreAccessors &&
3558
+ isOwnProp &&
3559
+ Object.getPrototypeOf(obj) !== null &&
3560
+ (isOwnAccessor(receiver, prop) || isOwnAccessor(obj, prop));
3561
+ // Check if property exists using a trap-free walk to avoid triggering
3562
+ // the has-trap cascade on prototype chains of reactive proxies.
3476
3563
  let hasProp = isOwnProp;
3477
3564
  let owner = isOwnProp ? obj : undefined;
3478
3565
  if (!isOwnProp) {
@@ -3487,29 +3574,20 @@ const reactiveHandlers = {
3487
3574
  }
3488
3575
  }
3489
3576
  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
3577
  // Depend if...
3498
3578
  if (!hasProp ||
3499
3579
  (!(options.instanceMembers && isInheritedAccess && obj instanceof Object) &&
3500
3580
  !shouldIgnoreAccessor))
3501
3581
  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.
3582
+ // Two-Point Tracking: for inherited access on null-proto chains, also track
3583
+ // the owning ancestor so that writing directly to it triggers dependent effects.
3506
3584
  if (isInheritedAccess && owner && (!options.instanceMembers || !(obj instanceof Object))) {
3507
3585
  dependant(owner, prop);
3508
3586
  }
3509
3587
  // For arrays, use FoolProof.get (Indexer path) for numeric index reactivity.
3510
3588
  // For all other objects, inline Reflect.get directly (skips 3 function calls).
3511
3589
  const value = (subsRegister.get(obj)?.get || FoolProof.get)(obj, prop, receiver);
3512
- if (typeof value === 'object' && value !== null) {
3590
+ if (!isReactive(value) && typeof value === 'object' && value !== null) {
3513
3591
  const reactiveValue = reactiveObject(value);
3514
3592
  // Only create back-references if this object needs them
3515
3593
  if (needsBackReferences(obj)) {
@@ -3520,9 +3598,19 @@ const reactiveHandlers = {
3520
3598
  return value;
3521
3599
  },
3522
3600
  set(obj, prop, value, receiver) {
3523
- const unwrappedReceiver = unwrap(receiver);
3601
+ const unwrapped = unwrap(receiver);
3602
+ if (obj !== unwrapped)
3603
+ return Object.defineProperty(unwrapped, prop, {
3604
+ value,
3605
+ configurable: true,
3606
+ writable: true,
3607
+ enumerable: true,
3608
+ });
3609
+ if (internalUntracked)
3610
+ throw new Error('Internal untracked: setting a value in an getter in a set operation');
3611
+ //return FoolProof.set(obj, prop, value, receiver)
3524
3612
  // Check if this property is marked as unreactive
3525
- if (isUnreactiveProp(obj, prop) || obj !== unwrappedReceiver)
3613
+ if (isUnreactiveProp(obj, prop))
3526
3614
  return FoolProof.set(obj, prop, value, receiver);
3527
3615
  const newValue = unwrap(value);
3528
3616
  // metaProto setter dispatch (e.g., reactive array length)
@@ -3539,16 +3627,19 @@ const reactiveHandlers = {
3539
3627
  // Read old value, using withEffect(undefined, ...) for getter-only accessors to avoid
3540
3628
  // breaking memoization dependency tracking during SET operations
3541
3629
  let oldVal = absent;
3542
- // TODO: Pffft... Find a way to "generalize" this case?
3543
3630
  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));
3631
+ internalUntracked = true;
3632
+ try {
3633
+ if (Reflect.has(obj, prop)) {
3634
+ oldVal = isArrayLength
3635
+ ? arrayLengths.get(obj) === newValue
3636
+ ? newValue
3637
+ : absent
3638
+ : Reflect.get(obj, prop, receiver);
3639
+ }
3640
+ }
3641
+ finally {
3642
+ internalUntracked = false;
3552
3643
  }
3553
3644
  if (objectsWithDeepWatchers.has(obj)) {
3554
3645
  if (typeof oldVal === 'object' && oldVal !== null) {
@@ -3577,7 +3668,8 @@ const reactiveHandlers = {
3577
3668
  cycle: [], // We don't have the full cycle here, but we know it involves obj
3578
3669
  });
3579
3670
  hasReentry.add(obj);
3580
- dependant(obj, prop);
3671
+ if (!internalUntracked && !isUnreactiveProp(obj, prop))
3672
+ dependant(obj, prop);
3581
3673
  const rv = (subsRegister.get(obj)?.has || Reflect.has)(obj, prop);
3582
3674
  hasReentry.delete(obj);
3583
3675
  return rv;
@@ -3603,7 +3695,8 @@ const reactiveHandlers = {
3603
3695
  return subsRegister.get(obj)?.ownKeys?.(obj) || Reflect.ownKeys(obj);
3604
3696
  },
3605
3697
  getOwnPropertyDescriptor(obj, prop) {
3606
- return subsRegister.get(obj)?.getOwnPropertyDescriptor?.(obj, prop) || Reflect.getOwnPropertyDescriptor(obj, prop);
3698
+ return (subsRegister.get(obj)?.getOwnPropertyDescriptor?.(obj, prop) ||
3699
+ Reflect.getOwnPropertyDescriptor(obj, prop));
3607
3700
  },
3608
3701
  };
3609
3702
  const reactiveClasses = new WeakSet();
@@ -3688,31 +3781,34 @@ exports.ReactiveError = ReactiveError;
3688
3781
  exports.Zone = Zone;
3689
3782
  exports.ZoneAggregator = ZoneAggregator;
3690
3783
  exports.ZoneHistory = ZoneHistory;
3691
- exports.__classPrivateFieldGet = __classPrivateFieldGet;
3692
- exports.__classPrivateFieldSet = __classPrivateFieldSet;
3693
3784
  exports.__esDecorate = __esDecorate;
3694
3785
  exports.__runInitializers = __runInitializers;
3695
- exports.__setFunctionName = __setFunctionName;
3696
3786
  exports.addBatchCleanup = addBatchCleanup;
3697
3787
  exports.addUnreactiveProps = addUnreactiveProps;
3698
3788
  exports.allProps = allProps;
3699
3789
  exports.arrayEquals = arrayEquals;
3790
+ exports.assertUntracked = assertUntracked;
3791
+ exports.asyncHook = asyncHook;
3792
+ exports.asyncHooks = asyncHooks;
3700
3793
  exports.asyncZone = asyncZone;
3701
3794
  exports.atom = atom;
3702
3795
  exports.atomic = atomic;
3796
+ exports.batch = batch;
3703
3797
  exports.biDi = biDi;
3798
+ exports.captured = captured;
3704
3799
  exports.caught = caught;
3705
- exports.cleanedBy = cleanedBy;
3706
- exports.cleanup = cleanup;
3707
3800
  exports.contentRef = contentRef;
3708
3801
  exports.createFlavor = createFlavor;
3802
+ exports.debugPreset = debugPreset;
3709
3803
  exports.decorator = decorator;
3710
3804
  exports.deepCompare = deepCompare;
3711
3805
  exports.deepWatchers = deepWatchers;
3712
3806
  exports.defer = defer;
3713
3807
  exports.dependant = dependant;
3808
+ exports.devPreset = devPreset;
3714
3809
  exports.effect = effect;
3715
3810
  exports.effectAggregator = effectAggregator;
3811
+ exports.effectHistory = effectHistory;
3716
3812
  exports.effectMarker = effectMarker;
3717
3813
  exports.effectToDeepWatchedObjects = effectToDeepWatchedObjects;
3718
3814
  exports.flavorOptions = flavorOptions;
@@ -3723,20 +3819,22 @@ exports.getActiveEffect = getActiveEffect;
3723
3819
  exports.getEffectNode = getEffectNode;
3724
3820
  exports.getRoot = getRoot;
3725
3821
  exports.getState = getState;
3726
- exports.immutables = immutables;
3822
+ exports.hooks = hooks;
3727
3823
  exports.isConstructor = isConstructor;
3824
+ exports.isDev = isDev;
3728
3825
  exports.isNonReactive = isNonReactive;
3729
3826
  exports.isObject = isObject;
3827
+ exports.isProd = isProd;
3730
3828
  exports.isReactive = isReactive;
3829
+ exports.isTest = isTest;
3731
3830
  exports.keysOf = keysOf;
3732
3831
  exports.legacyDecorator = legacyDecorator;
3832
+ exports.link = link;
3733
3833
  exports.markWithRoot = markWithRoot;
3734
3834
  exports.metaProtos = metaProtos;
3735
3835
  exports.mixin = mixin;
3736
3836
  exports.modernDecorator = modernDecorator;
3737
3837
  exports.named = named;
3738
- exports.nonReactiveClass = nonReactiveClass;
3739
- exports.nonReactiveObjects = nonReactiveObjects;
3740
3838
  exports.notifyPropertyChange = notifyPropertyChange;
3741
3839
  exports.objectParents = objectParents;
3742
3840
  exports.objectToProxy = objectToProxy;
@@ -3744,6 +3842,7 @@ exports.objectsWithDeepWatchers = objectsWithDeepWatchers;
3744
3842
  exports.onEffectThrow = onEffectThrow;
3745
3843
  exports.optionCall = optionCall;
3746
3844
  exports.options = options;
3845
+ exports.prodPreset = prodPreset;
3747
3846
  exports.proxyToObject = proxyToObject;
3748
3847
  exports.reactive = reactive;
3749
3848
  exports.registerDeepWatcher = registerDeepWatcher;
@@ -3751,13 +3850,13 @@ exports.reset = reset;
3751
3850
  exports.root = root;
3752
3851
  exports.rootFunctionSymbol = rootFunctionSymbol;
3753
3852
  exports.setDebugHooks = setDebugHooks;
3754
- exports.stopped = stopped;
3755
3853
  exports.tag = tag;
3756
3854
  exports.touched = touched;
3757
3855
  exports.touched1 = touched1;
3758
- exports.unreactiveProps = unreactiveProps;
3856
+ exports.unlink = unlink;
3857
+ exports.unreactiveProperties = unreactiveProperties;
3759
3858
  exports.untracked = untracked;
3760
3859
  exports.unwrap = unwrap;
3761
3860
  exports.wrapProtos = wrapProtos;
3762
3861
  exports.zip = zip;
3763
- //# sourceMappingURL=proxy-Dtg-bJ3T.cjs.map
3862
+ //# sourceMappingURL=proxy-BvM4yewA.cjs.map