mutts 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/BROWSER_ASYNC_POLYFILL.md +79 -0
  2. package/README.md +7 -4
  3. package/dist/browser.cjs +150 -27
  4. package/dist/browser.cjs.map +1 -1
  5. package/dist/browser.d.ts +1440 -2
  6. package/dist/browser.dev.cjs +17 -3
  7. package/dist/browser.dev.cjs.map +1 -1
  8. package/dist/browser.dev.d.ts +2 -2
  9. package/dist/browser.dev.esm.js +2 -2
  10. package/dist/browser.esm.js +137 -28
  11. package/dist/browser.esm.js.map +1 -1
  12. package/dist/chunks/{index-yK0HVxHv.cjs → index-BnTNC9eC.cjs} +347 -156
  13. package/dist/chunks/index-BnTNC9eC.cjs.map +1 -0
  14. package/dist/chunks/{index-BUop6B2U.esm.js → index-CAWVZL7P.esm.js} +345 -154
  15. package/dist/chunks/index-CAWVZL7P.esm.js.map +1 -0
  16. package/dist/chunks/node-Df_5r_WA.cjs +187 -0
  17. package/dist/chunks/node-Df_5r_WA.cjs.map +1 -0
  18. package/dist/chunks/node-DuIduHw3.esm.js +185 -0
  19. package/dist/chunks/node-DuIduHw3.esm.js.map +1 -0
  20. package/dist/chunks/{proxy-D2C49sXH.esm.js → proxy-C2lnvvbx.esm.js} +943 -272
  21. package/dist/chunks/proxy-C2lnvvbx.esm.js.map +1 -0
  22. package/dist/chunks/{proxy-BvM4yewA.cjs → proxy-HA_QQnd5.cjs} +959 -273
  23. package/dist/chunks/proxy-HA_QQnd5.cjs.map +1 -0
  24. package/dist/debug.cjs +571 -173
  25. package/dist/debug.cjs.map +1 -1
  26. package/dist/debug.d.ts +96 -80
  27. package/dist/debug.esm.js +567 -173
  28. package/dist/debug.esm.js.map +1 -1
  29. package/dist/devtools/panel.js.map +1 -1
  30. package/dist/mutts.umd.js +4351 -3366
  31. package/dist/mutts.umd.js.map +1 -1
  32. package/dist/mutts.umd.min.js +1 -1
  33. package/dist/mutts.umd.min.js.map +1 -1
  34. package/dist/node.cjs +18 -4
  35. package/dist/node.cjs.map +1 -1
  36. package/dist/node.d.ts +2 -2
  37. package/dist/node.dev.cjs +18 -4
  38. package/dist/node.dev.cjs.map +1 -1
  39. package/dist/node.dev.d.ts +2 -2
  40. package/dist/node.dev.esm.js +3 -3
  41. package/dist/node.esm.js +3 -3
  42. package/dist/{types-Bx2PhORg.d.ts → types.d.ts} +42 -15
  43. package/docs/ai/api-reference.md +105 -13
  44. package/docs/ai/manual.md +77 -29
  45. package/docs/debug-getReason.md +161 -0
  46. package/docs/flavored.md +98 -1
  47. package/docs/reactive/advanced.md +184 -12
  48. package/docs/reactive/attend.md +32 -0
  49. package/docs/reactive/core.md +40 -6
  50. package/docs/reactive/debugging.md +40 -15
  51. package/docs/reactive.md +4 -1
  52. package/package.json +13 -9
  53. package/dist/chunks/index-BUop6B2U.esm.js.map +0 -1
  54. package/dist/chunks/index-yK0HVxHv.cjs.map +0 -1
  55. package/dist/chunks/node-Bo7WU5S2.esm.js +0 -96
  56. package/dist/chunks/node-Bo7WU5S2.esm.js.map +0 -1
  57. package/dist/chunks/node-Dd0esp5F.cjs +0 -98
  58. package/dist/chunks/node-Dd0esp5F.cjs.map +0 -1
  59. package/dist/chunks/proxy-BvM4yewA.cjs.map +0 -1
  60. package/dist/chunks/proxy-D2C49sXH.esm.js.map +0 -1
  61. package/dist/index.d.ts +0 -1322
package/dist/debug.cjs CHANGED
@@ -1,25 +1,23 @@
1
1
  'use strict';
2
2
 
3
- var proxy = require('./chunks/proxy-BvM4yewA.cjs');
3
+ var proxy = require('./chunks/proxy-HA_QQnd5.cjs');
4
4
 
5
5
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
6
6
  const lineageObjects = new WeakSet();
7
- /**
8
- * Checks if an object is a lineage object
9
- */
7
+ const lineageSegmentObjects = new WeakSet();
8
+ const lineageFrameObjects = new WeakSet();
10
9
  function isLineage(obj) {
11
- return obj && typeof obj === 'object' && lineageObjects.has(obj);
10
+ return !!obj && typeof obj === 'object' && lineageObjects.has(obj);
12
11
  }
13
- /**
14
- * Parses a single stack line into a structured frame.
15
- * @param line - A line from Error.stack
16
- */
12
+ function isLineageSegmentView(obj) {
13
+ return !!obj && typeof obj === 'object' && lineageSegmentObjects.has(obj);
14
+ }
15
+ function isLineageFrameView(obj) {
16
+ return !!obj && typeof obj === 'object' && lineageFrameObjects.has(obj);
17
+ }
18
+ let internalFile;
17
19
  function parseStackLine(line) {
18
- // Node.js format: " at functionName (file:line:column)"
19
- // or " at file:line:column"
20
20
  const nodeMatch = line.match(/^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+))\)?$/);
21
- // Browser format (Firefox/Safari): "functionName@file:line:column" or "file:line:column"
22
- // Chrome/Edge use Node-like format but without "at" sometimes or with different prefixes
23
21
  const browserMatch = line.match(/(?:(.+?)(?:\@|\(?))?(?:(.+?):(\d+):(\d+))(?:\@|\)?)$/);
24
22
  const match = nodeMatch || browserMatch;
25
23
  if (!match)
@@ -33,16 +31,23 @@ function parseStackLine(line) {
33
31
  raw: line.trim(),
34
32
  };
35
33
  }
36
- let internalFile;
37
- /**
38
- * Gets the current call stack
39
- * @param skipFrames - Number of frames to skip
40
- * @param error - Optional error to use as source of stack
41
- */
42
- function getStackFrame(error = new Error()) {
43
- if (!error.stack)
34
+ function extractRawStack(error = new Error()) {
35
+ if (typeof error === 'string')
36
+ return error;
37
+ if (error && typeof error === 'object' && 'stack' in error) {
38
+ const stack = error.stack;
39
+ return typeof stack === 'string' ? stack : undefined;
40
+ }
41
+ return undefined;
42
+ }
43
+ function getStackFrame(error) {
44
+ return extractRawStack(error);
45
+ }
46
+ function parseStackFrames(stack) {
47
+ const rawStack = extractRawStack(stack);
48
+ if (!rawStack)
44
49
  return [];
45
- const lines = error.stack.split('\n');
50
+ const lines = rawStack.split('\n');
46
51
  const lastLine = lines.findIndex((line) => line.includes(proxy.effectMarker.enter));
47
52
  if (lastLine !== -1)
48
53
  lines.splice(lastLine);
@@ -50,150 +55,164 @@ function getStackFrame(error = new Error()) {
50
55
  if (firstLine !== -1)
51
56
  lines.splice(0, firstLine + 1);
52
57
  else {
53
- // Dynamically identify the library's internal files if not already done
54
58
  if (!internalFile && lines[1]) {
55
59
  const selfFrame = parseStackLine(lines[1]);
56
- if (selfFrame) {
60
+ if (selfFrame)
57
61
  internalFile = selfFrame.fileName;
58
- }
59
62
  }
60
- // Determine the "base" directory of the library to skip other internal files
61
- // We look for "src" or "dist" to be more specific than just the project root
62
- const srcIndex = internalFile ? internalFile.lastIndexOf('/src/') : -1;
63
- const distIndex = internalFile ? internalFile.lastIndexOf('/dist/') : -1;
64
- internalFile ? (srcIndex !== -1 ? internalFile.substring(0, srcIndex + 5) :
65
- (distIndex !== -1 ? internalFile.substring(0, distIndex + 6) :
66
- internalFile.substring(0, internalFile.lastIndexOf('/') + 1))) : undefined;
67
- let l;
68
- for (l = 1; l < lines.length; l++) {
63
+ let l = 1;
64
+ for (; l < lines.length; l++) {
69
65
  const frame = parseStackLine(lines[l]);
70
66
  if (!frame)
71
67
  continue;
72
- // Robust skipping: if we are still in the internal area, skip it.
73
68
  const isInternal = /Lineage$/.test(frame.functionName) ||
74
69
  frame.functionName === 'eval' ||
75
- [`getStackFrame`, `captureLineage`].includes(frame.functionName);
70
+ [`getStackFrame`, `captureLineage`, `getLineage`, `digestLineage`, `formatLineage`].includes(frame.functionName);
76
71
  if (!isInternal)
77
72
  break;
78
73
  }
79
74
  lines.splice(0, l);
80
75
  }
81
- return lines.map(parseStackLine).filter(Boolean);
76
+ return filterNodeModules(lines.map(parseStackLine).filter((frame) => !!frame));
82
77
  }
83
- /**
84
- * Traces the lineage of the current execution through nested effects
85
- * @param effect - Starting effect (defaults to active effect)
86
- */
87
- function getLineage(effect, currentStack = getStackFrame()) {
88
- const currentEffect = effect ?? proxy.getActiveEffect();
78
+ function filterNodeModules(frames) {
79
+ const result = [];
80
+ let inNodeModules = false;
81
+ for (const frame of frames) {
82
+ const isNodeModule = frame.fileName.includes('/node_modules/');
83
+ if (isNodeModule && !inNodeModules) {
84
+ inNodeModules = true;
85
+ result.push({
86
+ functionName: '...node_modules...',
87
+ fileName: '[filtered]',
88
+ lineNumber: 0,
89
+ columnNumber: 0,
90
+ raw: 'at ...node_modules...',
91
+ });
92
+ continue;
93
+ }
94
+ if (!isNodeModule && inNodeModules)
95
+ inNodeModules = false;
96
+ if (!isNodeModule)
97
+ result.push(frame);
98
+ }
99
+ return result;
100
+ }
101
+ function formatEffectPreview(effect) {
102
+ if (!effect)
103
+ return 'root';
104
+ return `[effect ${effect.name || 'anonymous'}]`;
105
+ }
106
+ function formatSegmentTitle(segment, index, total) {
107
+ const isLast = index === total - 1;
108
+ const prefix = index === 0 ? '📍' : isLast ? '└─' : '├─';
109
+ return `${prefix} Effect: ${segment.effect?.name || 'root'}`;
110
+ }
111
+ function wrapLineageSegmentView(segment, index, total) {
112
+ const view = { index, total, segment };
113
+ lineageSegmentObjects.add(view);
114
+ return view;
115
+ }
116
+ function wrapLineageFrameView(frame) {
117
+ const view = { frame };
118
+ lineageFrameObjects.add(view);
119
+ return view;
120
+ }
121
+ function formatLocationHref(frame) {
122
+ return `${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`;
123
+ }
124
+ function ensureSignature(signature) {
125
+ if (!isLineage(signature)) {
126
+ Object.defineProperty(signature, 'toString', {
127
+ value: () => formatLineage(signature),
128
+ enumerable: false,
129
+ configurable: true,
130
+ });
131
+ lineageObjects.add(signature);
132
+ }
133
+ return signature;
134
+ }
135
+ function getLineage(effect, currentStack = new Error()) {
136
+ return ensureSignature({
137
+ effect: effect ?? proxy.getActiveEffect(),
138
+ stack: currentStack,
139
+ toString() {
140
+ return formatLineage(this);
141
+ },
142
+ });
143
+ }
144
+ function captureLineage(effect, currentStack) {
145
+ return getLineage(effect, currentStack ?? new Error());
146
+ }
147
+ function digestLineage(lineage) {
148
+ if (lineage.digested)
149
+ return lineage.digested;
150
+ const currentEffect = lineage.effect;
151
+ const currentStack = parseStackFrames(lineage.stack);
89
152
  const segments = [];
90
153
  if (!currentEffect) {
91
- segments.push({
92
- effect: undefined,
93
- effectName: 'root',
94
- stack: filterNodeModules(currentStack),
95
- });
96
- return segments;
154
+ lineage.digested = [
155
+ {
156
+ effect: undefined,
157
+ effectName: 'root',
158
+ stack: currentStack,
159
+ },
160
+ ];
161
+ return lineage.digested;
97
162
  }
98
163
  let current = currentEffect;
99
164
  let lastStack = currentStack;
100
165
  while (current) {
101
166
  const rootFn = proxy.getRoot(current);
102
- // Too aggressive for now
103
- //if(!rootFn.name) debugger
104
- const filteredStack = filterNodeModules(lastStack);
105
167
  segments.push({
106
168
  effect: rootFn,
107
169
  effectName: rootFn.name || 'anonymous',
108
- stack: filteredStack,
170
+ stack: lastStack,
109
171
  });
110
- // Move to parent
111
172
  const node = proxy.getEffectNode(current);
112
173
  const parent = node.parent;
113
- const creationStack = node.creationStack;
174
+ const creationStack = parseStackFrames(node.creationStack);
114
175
  if (parent) {
115
176
  current = parent;
116
- lastStack = creationStack ?? [];
177
+ lastStack = creationStack;
117
178
  }
118
- else if (creationStack) {
179
+ else if (node.creationStack) {
119
180
  segments.push({
120
181
  effect: undefined,
121
182
  effectName: 'root',
122
- stack: filterNodeModules(creationStack),
183
+ stack: creationStack,
123
184
  });
124
185
  break;
125
186
  }
126
- else {
187
+ else
127
188
  break;
128
- }
129
189
  }
190
+ lineage.digested = segments;
130
191
  return segments;
131
192
  }
132
- /**
133
- * Filters out node_modules frames and groups them
134
- * @param frames - Array of stack frames
135
- */
136
- function filterNodeModules(frames) {
137
- const result = [];
138
- let inNodeModules = false;
139
- for (const frame of frames) {
140
- const isNodeModule = frame.fileName.includes('/node_modules/'); //|| frame.fileName.includes('/dist/')
141
- if (isNodeModule && !inNodeModules) {
142
- // Start of node_modules block
143
- inNodeModules = true;
144
- result.push({
145
- functionName: '...node_modules...',
146
- fileName: '[filtered]',
147
- lineNumber: 0,
148
- columnNumber: 0,
149
- raw: ' at ...node_modules...'
150
- });
151
- }
152
- else if (!isNodeModule && inNodeModules) {
153
- // End of node_modules block
154
- inNodeModules = false;
155
- result.push(frame);
156
- }
157
- else if (!isNodeModule) {
158
- // Regular frame
159
- result.push(frame);
160
- }
161
- // Skip frames inside node_modules
162
- }
163
- return result;
164
- }
165
- /**
166
- * Formats lineage segments into a single stack-like string
167
- * @param segments - Lineage segments
168
- */
169
- function formatLineage(segments) {
193
+ function formatLineage(lineage) {
194
+ const segments = Array.isArray(lineage) ? lineage : digestLineage(lineage);
170
195
  const result = [];
171
196
  for (let i = 0; i < segments.length; i++) {
172
197
  const segment = segments[i];
173
198
  if (i > 0) {
174
199
  const triggerEffect = segments[i - 1].effect;
175
- const triggerName = triggerEffect ? (proxy.getRoot(triggerEffect).name || 'anonymous') : 'root';
200
+ const triggerName = triggerEffect ? proxy.getRoot(triggerEffect).name || 'anonymous' : 'root';
176
201
  result.push(` --- effect: ${triggerName} ---`);
177
202
  }
178
- for (const frame of segment.stack) {
203
+ for (const frame of segment.stack)
179
204
  result.push(` ${frame.raw}`);
180
- }
181
205
  }
182
206
  return result.join('\n');
183
207
  }
184
- /**
185
- * Logs lineage segments to console with grouping
186
- * @param segments - Lineage segments
187
- */
188
- function logLineage(segments) {
208
+ function logLineage(lineage) {
209
+ const segments = Array.isArray(lineage) ? lineage : digestLineage(lineage);
189
210
  console.groupCollapsed(`🦴 Effect Lineage Trace (${segments.length} segment${segments.length === 1 ? '' : 's'})`);
190
211
  for (let i = 0; i < segments.length; i++) {
191
212
  const segment = segments[i];
192
- // Add segment header
193
213
  const isLast = i === segments.length - 1;
194
214
  const prefix = i === 0 ? '📍' : isLast ? '└─' : '├─';
195
215
  console.groupCollapsed(`${prefix} Effect:`, segment.effect ? segment.effect.name || 'anonymous' : 'root');
196
- // Add stack frames
197
216
  for (let j = 0; j < segment.stack.length; j++) {
198
217
  const frame = segment.stack[j];
199
218
  const isLastFrame = j === segment.stack.length - 1;
@@ -211,76 +230,408 @@ function logLineage(segments) {
211
230
  }
212
231
  console.groupEnd();
213
232
  }
214
- /**
215
- * Captures and logs lineage to console
216
- */
217
233
  function captureNodeLineage() {
218
234
  logLineage(getLineage());
219
235
  }
220
- /**
221
- * Custom formatter for Chrome DevTools to render lineage data nicely.
222
- */
223
236
  const lineageFormatter = {
224
237
  header: (obj) => {
225
- if (isLineage(obj)) {
226
- // Try to detect DevTools theme - default to dark colors if uncertain
238
+ if (isLineageSegmentView(obj)) {
227
239
  const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
228
- const headerColor = isDark ? '#cd9d5d' : '#704214';
240
+ const frameColor = isDark ? '#8b949e' : '#57606a';
229
241
  return [
230
242
  'span',
231
- { style: `color: ${headerColor}; font-weight: bold;` },
232
- `🦴 Effect Lineage (${obj.segments.length} segments)`,
243
+ { style: 'font-weight: bold;' },
244
+ formatSegmentTitle(obj.segment, obj.index, obj.total),
245
+ ['span', { style: `color: ${frameColor}; margin-left: 6px; font-weight: normal;` }, formatEffectPreview(obj.segment.effect)],
246
+ [
247
+ 'span',
248
+ { style: `color: ${frameColor}; margin-left: 6px; font-weight: normal;` },
249
+ `${obj.segment.stack.length} frame${obj.segment.stack.length === 1 ? '' : 's'}`,
250
+ ],
233
251
  ];
234
252
  }
235
- return null;
253
+ if (isLineageFrameView(obj)) {
254
+ const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
255
+ const fnColor = obj.frame.functionName === 'anonymous' ? '#8b949e' : isDark ? '#ffffff' : '#222';
256
+ const linkColor = isDark ? '#58a6ff' : '#005cc5';
257
+ return [
258
+ 'div',
259
+ { style: 'display: block;' },
260
+ ['span', { style: `color: ${fnColor}; font-weight: bold;` }, obj.frame.functionName],
261
+ ['span', { style: 'color: #8b949e; margin: 0 6px;' }, '@'],
262
+ [
263
+ 'span',
264
+ { style: `color: ${linkColor}; text-decoration: underline;` },
265
+ formatLocationHref(obj.frame),
266
+ ],
267
+ ];
268
+ }
269
+ if (!isLineage(obj))
270
+ return null;
271
+ const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
272
+ const headerColor = isDark ? '#cd9d5d' : '#704214';
273
+ return [
274
+ 'span',
275
+ { style: `color: ${headerColor}; font-weight: bold;` },
276
+ obj.digested
277
+ ? `🦴 Effect Lineage (${obj.digested.length} segments)`
278
+ : '🦴 Effect Lineage (lazy)',
279
+ ];
236
280
  },
237
- hasBody: (obj) => isLineage(obj),
281
+ hasBody: (obj) => isLineage(obj) || isLineageSegmentView(obj),
238
282
  body: (obj) => {
283
+ if (isLineageSegmentView(obj)) {
284
+ return [
285
+ 'div',
286
+ { style: 'padding: 4px 0;' },
287
+ ...obj.segment.stack.map((frame) => [
288
+ 'div',
289
+ { style: 'display: block; margin-top: 2px;' },
290
+ ['object', { object: wrapLineageFrameView(frame) }],
291
+ ]),
292
+ ];
293
+ }
239
294
  if (!isLineage(obj))
240
295
  return null;
241
- const segments = obj.segments;
242
- // Try to detect DevTools theme
296
+ const segments = digestLineage(obj);
297
+ return [
298
+ 'div',
299
+ { style: 'padding: 5px; line-height: 1.4;' },
300
+ ...segments.map((segment, index) => [
301
+ 'div',
302
+ { style: 'display: block; margin-top: 4px;' },
303
+ ['object', { object: wrapLineageSegmentView(segment, index, segments.length) }],
304
+ ]),
305
+ ];
306
+ },
307
+ };
308
+ function wrapLineageForDebug(lineage) {
309
+ return ensureSignature(lineage ?? getLineage());
310
+ }
311
+
312
+ const reasonTypes = new Set(['propChange', 'invalidate', 'external', 'stopped', 'gc', 'lineage', 'error', 'multiple']);
313
+ const reasonCauseLists = new WeakSet();
314
+ function hasOwn(value, key) {
315
+ return Object.prototype.hasOwnProperty.call(value, key);
316
+ }
317
+ function isCleanupReason(value) {
318
+ if (!value || typeof value !== 'object')
319
+ return false;
320
+ if (!hasOwn(value, 'type'))
321
+ return false;
322
+ const type = value.type;
323
+ if (typeof type !== 'string' || !reasonTypes.has(type))
324
+ return false;
325
+ switch (type) {
326
+ case 'propChange':
327
+ return Array.isArray(value.triggers);
328
+ case 'invalidate':
329
+ return hasOwn(value, 'cause');
330
+ case 'lineage':
331
+ return hasOwn(value, 'parent');
332
+ case 'error':
333
+ return hasOwn(value, 'error');
334
+ case 'multiple':
335
+ return Array.isArray(value.reasons);
336
+ default:
337
+ return true;
338
+ }
339
+ }
340
+ const reasonCauseEntries = new WeakSet();
341
+ const reasonCauseDetails = new WeakSet();
342
+ function isReasonCauseList(value) {
343
+ return !!value && typeof value === 'object' && reasonCauseLists.has(value);
344
+ }
345
+ function isReasonCauseEntry(value) {
346
+ return !!value && typeof value === 'object' && reasonCauseEntries.has(value);
347
+ }
348
+ function isReasonCauseDetail(value) {
349
+ return !!value && typeof value === 'object' && reasonCauseDetails.has(value);
350
+ }
351
+ function wrapReasonCauseEntry(entry) {
352
+ reasonCauseEntries.add(entry);
353
+ return entry;
354
+ }
355
+ function wrapReasonCauseDetail(detail) {
356
+ reasonCauseDetails.add(detail);
357
+ return detail;
358
+ }
359
+ function wrapReasonCauseList(list) {
360
+ reasonCauseLists.add(list);
361
+ return list;
362
+ }
363
+ function getCleanupReasonChain(reason) {
364
+ const chain = [];
365
+ let current = reason;
366
+ while (current) {
367
+ chain.push(current);
368
+ current = current.chain;
369
+ }
370
+ return chain;
371
+ }
372
+ function formatReasonSummary(reason) {
373
+ switch (reason.type) {
374
+ case 'propChange':
375
+ return `propChange: ${formatTriggerSummary(reason)}`;
376
+ case 'invalidate':
377
+ return `invalidate ← ${formatReasonSummary(reason.cause)}`;
378
+ case 'stopped':
379
+ return reason.detail ? `stopped (${reason.detail})` : 'stopped';
380
+ case 'external':
381
+ return `external: ${reason.detail}`;
382
+ case 'gc':
383
+ return 'gc';
384
+ case 'lineage':
385
+ return `lineage ← ${formatReasonSummary(reason.parent)}`;
386
+ case 'error':
387
+ return `error: ${formatUnknownSummary(reason.error)}`;
388
+ case 'multiple':
389
+ return `multiple: ${reason.reasons.map(formatReasonSummary).join(' | ')}`;
390
+ }
391
+ }
392
+ function formatTriggerSummary(reason) {
393
+ const groups = new Map();
394
+ const ordered = [];
395
+ for (const { obj, evolution } of reason.triggers) {
396
+ let group = groups.get(obj);
397
+ if (!group) {
398
+ group = { target: describeTarget(obj), parts: [] };
399
+ groups.set(obj, group);
400
+ ordered.push(group);
401
+ }
402
+ group.parts.push(evolution.type === 'bunch'
403
+ ? `${evolution.type} ${String(evolution.method)}`
404
+ : `${evolution.type} ${String(evolution.prop)}`);
405
+ }
406
+ return ordered.map(({ target, parts }) => `${target}: ${parts.join(', ')}`).join(' | ');
407
+ }
408
+ function formatUnknownSummary(value) {
409
+ if (typeof value === 'string')
410
+ return value;
411
+ if (typeof value === 'number' || typeof value === 'boolean' || value == null)
412
+ return String(value);
413
+ if (value instanceof Error)
414
+ return value.message;
415
+ if (typeof value === 'function')
416
+ return value.name || 'anonymous';
417
+ if (typeof value === 'object')
418
+ return value.constructor?.name || 'object';
419
+ return typeof value;
420
+ }
421
+ function describeTarget(value) {
422
+ if (!value || typeof value !== 'object')
423
+ return formatUnknownSummary(value);
424
+ const tag = value[Symbol.toStringTag];
425
+ if (typeof tag === 'string' && tag && tag !== 'Object' && tag !== 'Array')
426
+ return tag;
427
+ if (Array.isArray(value))
428
+ return 'Array';
429
+ if (value instanceof Map)
430
+ return 'Map';
431
+ if (value instanceof Set)
432
+ return 'Set';
433
+ return value.constructor?.name || 'object';
434
+ }
435
+ function formatPreview(value) {
436
+ if (typeof value === 'string')
437
+ return value;
438
+ if (typeof value === 'number' || typeof value === 'boolean' || value == null)
439
+ return String(value);
440
+ if (typeof value === 'function')
441
+ return `[function ${value.name || 'anonymous'}]`;
442
+ if (value instanceof Error)
443
+ return `[Error: ${value.message}]`;
444
+ if (value && typeof value === 'object' && 'stack' in value)
445
+ return '[lineage]';
446
+ if (typeof value === 'object')
447
+ return `[${value.constructor?.name || 'object'}]`;
448
+ return `[${typeof value}]`;
449
+ }
450
+ function renderDetailValue(value, color) {
451
+ if (value && typeof value === 'object' && 'stack' in value) {
452
+ return ['object', { object: value }];
453
+ }
454
+ return ['span', { style: `color: ${color}; white-space: pre-wrap;` }, formatPreview(value)];
455
+ }
456
+ function getReasonLineageDetails(reason) {
457
+ switch (reason.type) {
458
+ case 'propChange': {
459
+ const details = [];
460
+ for (const trigger of reason.triggers) {
461
+ const target = trigger.evolution.type === 'bunch'
462
+ ? String(trigger.evolution.method)
463
+ : String(trigger.evolution.prop);
464
+ const label = trigger.evolution.type === 'bunch'
465
+ ? `${trigger.evolution.type} ${String(trigger.evolution.method)}`
466
+ : `${trigger.evolution.type} ${String(trigger.evolution.prop)}`;
467
+ if (trigger.touch)
468
+ details.push({ kind: 'touch', target, label, value: trigger.touch });
469
+ if (trigger.dependency)
470
+ details.push({ kind: 'dependency', target, label, value: trigger.dependency });
471
+ }
472
+ return details;
473
+ }
474
+ case 'invalidate':
475
+ return getReasonLineageDetails(reason.cause);
476
+ case 'lineage':
477
+ return getReasonLineageDetails(reason.parent);
478
+ case 'multiple':
479
+ return reason.reasons.flatMap(getReasonLineageDetails);
480
+ case 'external':
481
+ default:
482
+ return [];
483
+ }
484
+ }
485
+ function renderLineageList(details, colors) {
486
+ const grouped = new Map();
487
+ for (const { kind, label, value } of details) {
488
+ let entry = grouped.get(label);
489
+ if (!entry) {
490
+ entry = { label };
491
+ grouped.set(label, entry);
492
+ }
493
+ if (kind === 'touch')
494
+ entry.touch = [...(entry.touch ?? []), value];
495
+ if (kind === 'dependency')
496
+ entry.dependency = [...(entry.dependency ?? []), value];
497
+ }
498
+ const listObject = wrapReasonCauseList({
499
+ count: grouped.size,
500
+ entries: Array.from(grouped.values(), wrapReasonCauseEntry),
501
+ });
502
+ return [
503
+ 'div',
504
+ {
505
+ style: `margin-top: 4px; padding: 3px 5px; background: ${colors.block}; border-radius: 3px;`,
506
+ },
507
+ ['object', { object: listObject }],
508
+ ];
509
+ }
510
+ function formatReasonWithoutChain(reason) {
511
+ switch (reason.type) {
512
+ case 'propChange':
513
+ return proxy.formatCleanupReason({ type: 'propChange', triggers: reason.triggers });
514
+ case 'invalidate':
515
+ return proxy.formatCleanupReason({ type: 'invalidate', cause: reason.cause });
516
+ case 'stopped':
517
+ return proxy.formatCleanupReason(reason.detail ? { type: 'stopped', detail: reason.detail } : { type: 'stopped' });
518
+ case 'external':
519
+ return proxy.formatCleanupReason({ type: 'external', detail: reason.detail });
520
+ case 'gc':
521
+ return proxy.formatCleanupReason({ type: 'gc' });
522
+ case 'lineage':
523
+ return proxy.formatCleanupReason({ type: 'lineage', parent: reason.parent });
524
+ case 'error':
525
+ return proxy.formatCleanupReason({ type: 'error', error: reason.error });
526
+ case 'multiple':
527
+ return proxy.formatCleanupReason({ type: 'multiple', reasons: reason.reasons });
528
+ }
529
+ }
530
+ function logReason(reason, tag) {
531
+ const chain = getCleanupReasonChain(reason);
532
+ console.groupCollapsed(`🧹 Cleanup Reason${tag ? ` (${tag})` : ''}`);
533
+ for (let i = 0; i < chain.length; i++) {
534
+ console.groupCollapsed(`${i === 0 ? '📍' : '↖'} ${formatReasonSummary(chain[i])}`);
535
+ const details = getReasonLineageDetails(chain[i]);
536
+ if (details.length > 0) {
537
+ for (const { kind, label, value } of details)
538
+ console.log(`${kind} ${label}`, value);
539
+ }
540
+ else {
541
+ console.log(...formatReasonWithoutChain(chain[i]));
542
+ }
543
+ console.groupEnd();
544
+ }
545
+ console.groupEnd();
546
+ return `🧹 Cleanup Reason${tag ? ` (${tag})` : ''}`;
547
+ }
548
+ const reasonFormatter = {
549
+ header: (obj) => {
550
+ if (isReasonCauseList(obj)) {
551
+ return ['span', { style: 'font-weight: bold;' }, `causes: ${obj.count}`];
552
+ }
553
+ if (isReasonCauseEntry(obj)) {
554
+ return [
555
+ 'span',
556
+ { style: 'font-weight: bold;' },
557
+ `${obj.label}:`,
558
+ ...(obj.touch?.length
559
+ ? [
560
+ ['span', { style: 'margin-left: 6px;' }, ['object', { object: wrapReasonCauseDetail({ kind: 'touch', values: obj.touch }) }]],
561
+ ]
562
+ : []),
563
+ ...(obj.dependency?.length
564
+ ? [
565
+ ['span', { style: 'margin-left: 6px;' }, ['object', { object: wrapReasonCauseDetail({ kind: 'dependency', values: obj.dependency }) }]],
566
+ ]
567
+ : []),
568
+ ];
569
+ }
570
+ if (isReasonCauseDetail(obj)) {
571
+ return [
572
+ 'span',
573
+ { style: 'font-weight: normal;' },
574
+ obj.values.length === 1 ? obj.kind : `${obj.values.length} ${obj.kind}`,
575
+ ];
576
+ }
577
+ if (!isCleanupReason(obj))
578
+ return null;
579
+ const chain = getCleanupReasonChain(obj);
243
580
  const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
244
- const colors = isDark ? {
245
- frameText: '#ccc',
246
- functionName: '#ffffff',
247
- segmentBg: '#2d2d2d',
248
- } : {
249
- frameText: '#555',
250
- functionName: '#222',
251
- segmentBg: '#eee',
252
- };
253
- const children = segments.map((segment, i) => {
254
- const frames = segment.stack.map((frame) => [
581
+ const color = isDark ? '#8fb3ff' : '#0b5fff';
582
+ return ['span', { style: `color: ${color}; font-weight: bold;` }, `🧹 ${formatReasonSummary(obj)}${chain.length > 1 ? ` ×${chain.length}` : ''}`];
583
+ },
584
+ hasBody: (obj) => isCleanupReason(obj) || isReasonCauseList(obj) || isReasonCauseDetail(obj),
585
+ body: (obj) => {
586
+ if (isReasonCauseList(obj)) {
587
+ return [
255
588
  'div',
256
- { style: `margin-left: 20px; color: ${colors.frameText}; font-family: monospace; font-size: 11px;` },
257
- ['span', { style: `color: ${colors.functionName};` }, `at ${frame.functionName} `],
258
- ['span', {}, `${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`],
259
- ]);
260
- segment.effect;
261
- const segmentHeader = [
589
+ { style: 'padding: 4px 0;' },
590
+ ...obj.entries.map((entry) => [
591
+ 'div',
592
+ { style: 'display: block; margin-top: 2px;' },
593
+ ['object', { object: entry }],
594
+ ]),
595
+ ];
596
+ }
597
+ if (isReasonCauseDetail(obj)) {
598
+ const detailValue = obj.values.length === 1
599
+ ? obj.values[0]
600
+ : Object.fromEntries(obj.values.map((value, index) => [`${obj.kind} ${index + 1}`, value]));
601
+ return [
262
602
  'div',
263
- { style: `margin-top: 5px; padding: 2px 5px; background: ${colors.segmentBg}; border-radius: 3px; font-weight: bold;` },
264
- i === 0 ? '📍 Current: ' : '↖ Effect: ',
265
- segment.effect?.name || 'anonymous',
266
- segment.effect ? ['object', { object: segment.effect }] : 'root',
603
+ { style: 'padding: 4px 0; line-height: 1.4;' },
604
+ [
605
+ 'div',
606
+ { style: 'margin-top: 2px;' },
607
+ renderDetailValue(detailValue, 'inherit'),
608
+ ],
609
+ ];
610
+ }
611
+ if (!isCleanupReason(obj))
612
+ return null;
613
+ const chain = getCleanupReasonChain(obj);
614
+ const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
615
+ const colors = isDark
616
+ ? { block: '#2d2d2d', title: '#ffffff', meta: '#8b949e' }
617
+ : { block: '#eee', title: '#222', meta: '#57606a' };
618
+ const children = chain.map((reason, index) => {
619
+ const lineageDetails = getReasonLineageDetails(reason);
620
+ const renderedDetails = lineageDetails.length > 0
621
+ ? [renderLineageList(lineageDetails, colors)]
622
+ : formatReasonWithoutChain(reason).map((part) => typeof part === 'string'
623
+ ? ['span', { style: `color: ${colors.meta}; white-space: pre-wrap;` }, part]
624
+ : renderDetailValue(part, colors.meta));
625
+ return [
626
+ 'div',
627
+ { style: `margin-top: 5px; padding: 4px 6px; background: ${colors.block}; border-radius: 3px;` },
628
+ ['div', { style: `font-weight: bold; color: ${colors.title}; margin-bottom: 3px;` }, `${index === 0 ? '📍' : '↖'} ${formatReasonSummary(reason)}`],
629
+ ['div', { style: 'display: inline-block;' }, ...renderedDetails],
267
630
  ];
268
- return ['div', {}, segmentHeader, ...frames];
269
631
  });
270
632
  return ['div', { style: 'padding: 5px; line-height: 1.4;' }, ...children];
271
633
  },
272
634
  };
273
- /**
274
- * Wraps lineage data in a way that the Chrome Formatter can recognize.
275
- */
276
- function wrapLineageForDebug(segments) {
277
- const lineage = {
278
- segments,
279
- toString: () => formatLineage(segments ?? getLineage()),
280
- };
281
- lineageObjects.add(lineage);
282
- return lineage;
283
- }
284
635
 
285
636
  /**
286
637
  * Displays a floating reactive panel in the browser showing the lineage of triggered effects.
@@ -380,13 +731,13 @@ function showLineagePanel() {
380
731
  panel.appendChild(content);
381
732
  document.body.appendChild(panel);
382
733
  // Reactivity: Update Visibility
383
- proxy.effect(() => {
734
+ proxy.effect `lineage-panel:visibility`(() => {
384
735
  panel.style.opacity = state.isVisible ? '1' : '0';
385
736
  panel.style.pointerEvents = state.isVisible ? 'all' : 'none';
386
737
  panel.style.transform = state.isVisible ? 'translateY(0)' : 'translateY(20px)';
387
738
  });
388
739
  // Reactivity: Update Content
389
- proxy.effect(() => {
740
+ proxy.effect `lineage-panel:content`(() => {
390
741
  if (state.activeLineage.length === 0) {
391
742
  content.innerHTML = `<div style="color: ${colors.placeholderText}; font-style: italic; text-align: center; margin-top: 20px;">No effect triggered yet...</div>`;
392
743
  return;
@@ -428,6 +779,35 @@ function showLineagePanel() {
428
779
  });
429
780
  }
430
781
 
782
+ function getColors() {
783
+ const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
784
+ return {
785
+ accent: isDark ? '#b392f0' : '#6f42c1',
786
+ meta: isDark ? '#8b949e' : '#57606a',
787
+ };
788
+ }
789
+ function getReactiveTarget(obj) {
790
+ if (!obj || typeof obj !== 'object' || !proxy.isReactive(obj))
791
+ return undefined;
792
+ return proxy.unwrap(obj);
793
+ }
794
+ const reactiveFormatter = {
795
+ header: (obj) => {
796
+ const target = getReactiveTarget(obj);
797
+ if (!target)
798
+ return null;
799
+ const colors = getColors();
800
+ return [
801
+ 'span',
802
+ {},
803
+ ['span', { style: `color: ${colors.accent}; font-weight: bold;` }, 'Reactive<'],
804
+ ['object', { object: target }],
805
+ ['span', { style: `color: ${colors.accent}; font-weight: bold;` }, '>'],
806
+ ];
807
+ },
808
+ hasBody: () => false,
809
+ };
810
+
431
811
  /**
432
812
  * Debug utilities for the reactivity system
433
813
  * - Captures effect metadata (names, parent relationships)
@@ -758,7 +1138,6 @@ function buildReactivityGraph() {
758
1138
  },
759
1139
  };
760
1140
  }
761
- const captureLineage = () => wrapLineageForDebug(getLineage());
762
1141
  /**
763
1142
  * Enables the DevTools bridge and exposes the debug API on window/global.
764
1143
  * Call as early as possible in development builds.
@@ -785,25 +1164,38 @@ function enableDevTools() {
785
1164
  logLineage(getLineage());
786
1165
  return '🦴 Effect Lineage Trace' + (tag ? ` (${tag})` : '');
787
1166
  },
788
- get browserLineage() {
789
- return captureLineage();
1167
+ logReason(reason, tag) {
1168
+ const reasonToLog = reason ?? globalScope.__MUTTS_DEBUG__.reason;
1169
+ if (!reasonToLog)
1170
+ return '🧹 Cleanup Reason';
1171
+ return logReason(reasonToLog, tag);
1172
+ },
1173
+ get lineage() {
1174
+ return getLineage();
790
1175
  },
791
- getLineage,
792
- captureLineage: getStackFrame,
793
- formatLineage,
794
1176
  showLineagePanel,
795
1177
  setEffectName,
796
1178
  setObjectName,
797
1179
  registerEffect: registerEffectForDebug,
798
1180
  registerObject: registerObjectForDebug,
1181
+ /**
1182
+ * Returns the reason why the current effect is being executed.
1183
+ * Returns undefined if not in an effect or if this is the first run.
1184
+ * @type {() => import('../src/reactive/types').CleanupReason | undefined}
1185
+ */
1186
+ get reason() {
1187
+ const activeEffect = proxy.getActiveEffect();
1188
+ if (!activeEffect)
1189
+ return undefined;
1190
+ const node = proxy.getEffectNode(activeEffect);
1191
+ return node?.currentReason;
1192
+ },
799
1193
  };
800
1194
  // @ts-ignore - devtoolsFormatters is a Chrome-specific array
801
- if (globalScope.devtoolsFormatters) {
802
- globalScope.devtoolsFormatters.push(lineageFormatter);
803
- }
804
- else {
805
- globalScope.devtoolsFormatters = [lineageFormatter];
806
- }
1195
+ if (globalScope.devtoolsFormatters)
1196
+ globalScope.devtoolsFormatters.push(lineageFormatter, reasonFormatter, reactiveFormatter);
1197
+ else
1198
+ globalScope.devtoolsFormatters = [lineageFormatter, reasonFormatter, reactiveFormatter];
807
1199
  proxy.setDebugHooks({
808
1200
  isDevtoolsEnabled: () => devtoolsEnabled,
809
1201
  registerEffect: registerEffectForDebug,
@@ -811,6 +1203,8 @@ function enableDevTools() {
811
1203
  captureStack: getStackFrame,
812
1204
  captureLineage,
813
1205
  formatStack: (stack) => {
1206
+ if (isCleanupReason(stack))
1207
+ return [stack];
814
1208
  if (typeof stack === 'string') {
815
1209
  return [
816
1210
  `\n ${stack
@@ -832,8 +1226,7 @@ function enableDevTools() {
832
1226
  typeof error === 'object' &&
833
1227
  'stack' in error &&
834
1228
  !('lineage' in error)) {
835
- const lineage = getLineage(trigger, getStackFrame(error));
836
- error.lineage = wrapLineageForDebug(lineage);
1229
+ error.lineage = wrapLineageForDebug(getLineage(trigger, error));
837
1230
  }
838
1231
  },
839
1232
  });
@@ -955,6 +1348,7 @@ function isDevelopmentMode() {
955
1348
  }
956
1349
  // Auto-enable devtools when the module loads in development
957
1350
  if (isDevelopmentMode() && !devtoolsEnabled) {
1351
+ Object.assign(proxy.options, proxy.devPreset);
958
1352
  enableDevTools();
959
1353
  // Optional: Log that devtools were enabled (only in development)
960
1354
  if (typeof console !== 'undefined' && console.info) {
@@ -966,9 +1360,11 @@ if (isDevelopmentMode() && !devtoolsEnabled) {
966
1360
  exports.buildReactivityGraph = buildReactivityGraph;
967
1361
  exports.captureLineage = captureLineage;
968
1362
  exports.captureNodeLineage = captureNodeLineage;
1363
+ exports.digestLineage = digestLineage;
969
1364
  exports.enableDevTools = enableDevTools;
970
1365
  exports.forceEnableGraphTracking = forceEnableGraphTracking;
971
1366
  exports.formatLineage = formatLineage;
1367
+ exports.getCleanupReasonChain = getCleanupReasonChain;
972
1368
  exports.getDependencies = getDependencies;
973
1369
  exports.getDependencyGraph = getDependencyGraph;
974
1370
  exports.getDependents = getDependents;
@@ -976,12 +1372,14 @@ exports.getLineage = getLineage;
976
1372
  exports.getMutationHistory = getMutationHistory;
977
1373
  exports.getStackFrame = getStackFrame;
978
1374
  exports.getTriggerChain = getTriggerChain;
1375
+ exports.isCleanupReason = isCleanupReason;
979
1376
  exports.isDevtoolsEnabled = isDevtoolsEnabled;
980
1377
  exports.isLineage = isLineage;
981
1378
  exports.lineageFormatter = lineageFormatter;
982
1379
  exports.logError = logError;
983
1380
  exports.logLineage = logLineage;
984
- exports.parseStackLine = parseStackLine;
1381
+ exports.logReason = logReason;
1382
+ exports.reasonFormatter = reasonFormatter;
985
1383
  exports.recordTriggerLink = recordTriggerLink;
986
1384
  exports.registerEffectForDebug = registerEffectForDebug;
987
1385
  exports.registerObjectForDebug = registerObjectForDebug;