mutts 1.0.11 → 1.0.13

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 (59) hide show
  1. package/README.md +5 -2
  2. package/dist/browser.cjs +237 -2596
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +1407 -2
  5. package/dist/browser.dev.cjs +44 -43
  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 +3 -3
  10. package/dist/chunks/index-CAdnMJev.cjs +2735 -0
  11. package/dist/chunks/index-CAdnMJev.cjs.map +1 -0
  12. package/dist/chunks/{index-Sf74wXTV.esm.js → index-XsYTUhHx.esm.js} +200 -77
  13. package/dist/chunks/index-XsYTUhHx.esm.js.map +1 -0
  14. package/dist/chunks/{async-node-3PrbVAbB.cjs → node-DrrphEPf.cjs} +4 -4
  15. package/dist/chunks/node-DrrphEPf.cjs.map +1 -0
  16. package/dist/chunks/{node-Bo7WU5S2.esm.js → node-NEZvVo4M.esm.js} +2 -2
  17. package/dist/chunks/{node-Bo7WU5S2.esm.js.map → node-NEZvVo4M.esm.js.map} +1 -1
  18. package/dist/chunks/{proxy-D2C49sXH.esm.js → proxy-BtmPFjSr.esm.js} +307 -66
  19. package/dist/chunks/proxy-BtmPFjSr.esm.js.map +1 -0
  20. package/dist/chunks/{proxy-Cc79Lrzj.cjs → proxy-DBHj3kGK.cjs} +341 -70
  21. package/dist/chunks/proxy-DBHj3kGK.cjs.map +1 -0
  22. package/dist/debug.cjs +537 -167
  23. package/dist/debug.cjs.map +1 -1
  24. package/dist/debug.d.ts +96 -80
  25. package/dist/debug.esm.js +533 -166
  26. package/dist/debug.esm.js.map +1 -1
  27. package/dist/devtools/panel.js.map +1 -1
  28. package/dist/mutts.umd.js +508 -140
  29. package/dist/mutts.umd.js.map +1 -1
  30. package/dist/mutts.umd.min.js +1 -1
  31. package/dist/mutts.umd.min.js.map +1 -1
  32. package/dist/node.cjs +44 -42
  33. package/dist/node.cjs.map +1 -1
  34. package/dist/node.d.ts +2 -2
  35. package/dist/node.dev.cjs +44 -42
  36. package/dist/node.dev.cjs.map +1 -1
  37. package/dist/node.dev.d.ts +2 -2
  38. package/dist/node.dev.esm.js +3 -3
  39. package/dist/node.esm.js +3 -3
  40. package/dist/{types-Bx2PhORg.d.ts → types.d.ts} +12 -0
  41. package/docs/ai/api-reference.md +102 -12
  42. package/docs/ai/manual.md +60 -24
  43. package/docs/debug-getReason.md +161 -0
  44. package/docs/flavored.md +98 -1
  45. package/docs/reactive/advanced.md +15 -2
  46. package/docs/reactive/attend.md +32 -0
  47. package/docs/reactive/core.md +40 -6
  48. package/docs/reactive/debugging.md +25 -2
  49. package/docs/reactive.md +2 -0
  50. package/package.json +2 -3
  51. package/dist/chunks/async-browser-Dgr5CreQ.cjs +0 -218
  52. package/dist/chunks/async-browser-Dgr5CreQ.cjs.map +0 -1
  53. package/dist/chunks/async-core-CRLKP3l-.cjs +0 -29
  54. package/dist/chunks/async-core-CRLKP3l-.cjs.map +0 -1
  55. package/dist/chunks/async-node-3PrbVAbB.cjs.map +0 -1
  56. package/dist/chunks/index-Sf74wXTV.esm.js.map +0 -1
  57. package/dist/chunks/proxy-Cc79Lrzj.cjs.map +0 -1
  58. package/dist/chunks/proxy-D2C49sXH.esm.js.map +0 -1
  59. package/dist/index.d.ts +0 -1322
package/dist/debug.cjs CHANGED
@@ -1,26 +1,23 @@
1
1
  'use strict';
2
2
 
3
- var proxy = require('./chunks/proxy-Cc79Lrzj.cjs');
4
- require('./chunks/async-core-CRLKP3l-.cjs');
3
+ var proxy = require('./chunks/proxy-DBHj3kGK.cjs');
5
4
 
6
5
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
7
6
  const lineageObjects = new WeakSet();
8
- /**
9
- * Checks if an object is a lineage object
10
- */
7
+ const lineageSegmentObjects = new WeakSet();
8
+ const lineageFrameObjects = new WeakSet();
11
9
  function isLineage(obj) {
12
- return obj && typeof obj === 'object' && lineageObjects.has(obj);
10
+ return !!obj && typeof obj === 'object' && lineageObjects.has(obj);
13
11
  }
14
- /**
15
- * Parses a single stack line into a structured frame.
16
- * @param line - A line from Error.stack
17
- */
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;
18
19
  function parseStackLine(line) {
19
- // Node.js format: " at functionName (file:line:column)"
20
- // or " at file:line:column"
21
20
  const nodeMatch = line.match(/^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+))\)?$/);
22
- // Browser format (Firefox/Safari): "functionName@file:line:column" or "file:line:column"
23
- // Chrome/Edge use Node-like format but without "at" sometimes or with different prefixes
24
21
  const browserMatch = line.match(/(?:(.+?)(?:\@|\(?))?(?:(.+?):(\d+):(\d+))(?:\@|\)?)$/);
25
22
  const match = nodeMatch || browserMatch;
26
23
  if (!match)
@@ -34,16 +31,23 @@ function parseStackLine(line) {
34
31
  raw: line.trim(),
35
32
  };
36
33
  }
37
- let internalFile;
38
- /**
39
- * Gets the current call stack
40
- * @param skipFrames - Number of frames to skip
41
- * @param error - Optional error to use as source of stack
42
- */
43
- function getStackFrame(error = new Error()) {
44
- 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)
45
49
  return [];
46
- const lines = error.stack.split('\n');
50
+ const lines = rawStack.split('\n');
47
51
  const lastLine = lines.findIndex((line) => line.includes(proxy.effectMarker.enter));
48
52
  if (lastLine !== -1)
49
53
  lines.splice(lastLine);
@@ -51,150 +55,164 @@ function getStackFrame(error = new Error()) {
51
55
  if (firstLine !== -1)
52
56
  lines.splice(0, firstLine + 1);
53
57
  else {
54
- // Dynamically identify the library's internal files if not already done
55
58
  if (!internalFile && lines[1]) {
56
59
  const selfFrame = parseStackLine(lines[1]);
57
- if (selfFrame) {
60
+ if (selfFrame)
58
61
  internalFile = selfFrame.fileName;
59
- }
60
62
  }
61
- // Determine the "base" directory of the library to skip other internal files
62
- // We look for "src" or "dist" to be more specific than just the project root
63
- const srcIndex = internalFile ? internalFile.lastIndexOf('/src/') : -1;
64
- const distIndex = internalFile ? internalFile.lastIndexOf('/dist/') : -1;
65
- internalFile ? (srcIndex !== -1 ? internalFile.substring(0, srcIndex + 5) :
66
- (distIndex !== -1 ? internalFile.substring(0, distIndex + 6) :
67
- internalFile.substring(0, internalFile.lastIndexOf('/') + 1))) : undefined;
68
- let l;
69
- for (l = 1; l < lines.length; l++) {
63
+ let l = 1;
64
+ for (; l < lines.length; l++) {
70
65
  const frame = parseStackLine(lines[l]);
71
66
  if (!frame)
72
67
  continue;
73
- // Robust skipping: if we are still in the internal area, skip it.
74
68
  const isInternal = /Lineage$/.test(frame.functionName) ||
75
69
  frame.functionName === 'eval' ||
76
- [`getStackFrame`, `captureLineage`].includes(frame.functionName);
70
+ [`getStackFrame`, `captureLineage`, `getLineage`, `digestLineage`, `formatLineage`].includes(frame.functionName);
77
71
  if (!isInternal)
78
72
  break;
79
73
  }
80
74
  lines.splice(0, l);
81
75
  }
82
- return lines.map(parseStackLine).filter(Boolean);
76
+ return filterNodeModules(lines.map(parseStackLine).filter((frame) => !!frame));
83
77
  }
84
- /**
85
- * Traces the lineage of the current execution through nested effects
86
- * @param effect - Starting effect (defaults to active effect)
87
- */
88
- function getLineage(effect, currentStack = getStackFrame()) {
89
- 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);
90
152
  const segments = [];
91
153
  if (!currentEffect) {
92
- segments.push({
93
- effect: undefined,
94
- effectName: 'root',
95
- stack: filterNodeModules(currentStack),
96
- });
97
- return segments;
154
+ lineage.digested = [
155
+ {
156
+ effect: undefined,
157
+ effectName: 'root',
158
+ stack: currentStack,
159
+ },
160
+ ];
161
+ return lineage.digested;
98
162
  }
99
163
  let current = currentEffect;
100
164
  let lastStack = currentStack;
101
165
  while (current) {
102
166
  const rootFn = proxy.getRoot(current);
103
- // Too aggressive for now
104
- //if(!rootFn.name) debugger
105
- const filteredStack = filterNodeModules(lastStack);
106
167
  segments.push({
107
168
  effect: rootFn,
108
169
  effectName: rootFn.name || 'anonymous',
109
- stack: filteredStack,
170
+ stack: lastStack,
110
171
  });
111
- // Move to parent
112
172
  const node = proxy.getEffectNode(current);
113
173
  const parent = node.parent;
114
- const creationStack = node.creationStack;
174
+ const creationStack = parseStackFrames(node.creationStack);
115
175
  if (parent) {
116
176
  current = parent;
117
- lastStack = creationStack ?? [];
177
+ lastStack = creationStack;
118
178
  }
119
- else if (creationStack) {
179
+ else if (node.creationStack) {
120
180
  segments.push({
121
181
  effect: undefined,
122
182
  effectName: 'root',
123
- stack: filterNodeModules(creationStack),
183
+ stack: creationStack,
124
184
  });
125
185
  break;
126
186
  }
127
- else {
187
+ else
128
188
  break;
129
- }
130
189
  }
190
+ lineage.digested = segments;
131
191
  return segments;
132
192
  }
133
- /**
134
- * Filters out node_modules frames and groups them
135
- * @param frames - Array of stack frames
136
- */
137
- function filterNodeModules(frames) {
138
- const result = [];
139
- let inNodeModules = false;
140
- for (const frame of frames) {
141
- const isNodeModule = frame.fileName.includes('/node_modules/'); //|| frame.fileName.includes('/dist/')
142
- if (isNodeModule && !inNodeModules) {
143
- // Start of node_modules block
144
- inNodeModules = true;
145
- result.push({
146
- functionName: '...node_modules...',
147
- fileName: '[filtered]',
148
- lineNumber: 0,
149
- columnNumber: 0,
150
- raw: ' at ...node_modules...'
151
- });
152
- }
153
- else if (!isNodeModule && inNodeModules) {
154
- // End of node_modules block
155
- inNodeModules = false;
156
- result.push(frame);
157
- }
158
- else if (!isNodeModule) {
159
- // Regular frame
160
- result.push(frame);
161
- }
162
- // Skip frames inside node_modules
163
- }
164
- return result;
165
- }
166
- /**
167
- * Formats lineage segments into a single stack-like string
168
- * @param segments - Lineage segments
169
- */
170
- function formatLineage(segments) {
193
+ function formatLineage(lineage) {
194
+ const segments = Array.isArray(lineage) ? lineage : digestLineage(lineage);
171
195
  const result = [];
172
196
  for (let i = 0; i < segments.length; i++) {
173
197
  const segment = segments[i];
174
198
  if (i > 0) {
175
199
  const triggerEffect = segments[i - 1].effect;
176
- const triggerName = triggerEffect ? (proxy.getRoot(triggerEffect).name || 'anonymous') : 'root';
200
+ const triggerName = triggerEffect ? proxy.getRoot(triggerEffect).name || 'anonymous' : 'root';
177
201
  result.push(` --- effect: ${triggerName} ---`);
178
202
  }
179
- for (const frame of segment.stack) {
203
+ for (const frame of segment.stack)
180
204
  result.push(` ${frame.raw}`);
181
- }
182
205
  }
183
206
  return result.join('\n');
184
207
  }
185
- /**
186
- * Logs lineage segments to console with grouping
187
- * @param segments - Lineage segments
188
- */
189
- function logLineage(segments) {
208
+ function logLineage(lineage) {
209
+ const segments = Array.isArray(lineage) ? lineage : digestLineage(lineage);
190
210
  console.groupCollapsed(`🦴 Effect Lineage Trace (${segments.length} segment${segments.length === 1 ? '' : 's'})`);
191
211
  for (let i = 0; i < segments.length; i++) {
192
212
  const segment = segments[i];
193
- // Add segment header
194
213
  const isLast = i === segments.length - 1;
195
214
  const prefix = i === 0 ? '📍' : isLast ? '└─' : '├─';
196
215
  console.groupCollapsed(`${prefix} Effect:`, segment.effect ? segment.effect.name || 'anonymous' : 'root');
197
- // Add stack frames
198
216
  for (let j = 0; j < segment.stack.length; j++) {
199
217
  const frame = segment.stack[j];
200
218
  const isLastFrame = j === segment.stack.length - 1;
@@ -212,76 +230,408 @@ function logLineage(segments) {
212
230
  }
213
231
  console.groupEnd();
214
232
  }
215
- /**
216
- * Captures and logs lineage to console
217
- */
218
233
  function captureNodeLineage() {
219
234
  logLineage(getLineage());
220
235
  }
221
- /**
222
- * Custom formatter for Chrome DevTools to render lineage data nicely.
223
- */
224
236
  const lineageFormatter = {
225
237
  header: (obj) => {
226
- if (isLineage(obj)) {
227
- // Try to detect DevTools theme - default to dark colors if uncertain
238
+ if (isLineageSegmentView(obj)) {
228
239
  const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
229
- const headerColor = isDark ? '#cd9d5d' : '#704214';
240
+ const frameColor = isDark ? '#8b949e' : '#57606a';
230
241
  return [
231
242
  'span',
232
- { style: `color: ${headerColor}; font-weight: bold;` },
233
- `🦴 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
+ ],
234
251
  ];
235
252
  }
236
- 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
+ ];
237
280
  },
238
- hasBody: (obj) => isLineage(obj),
281
+ hasBody: (obj) => isLineage(obj) || isLineageSegmentView(obj),
239
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
+ }
240
294
  if (!isLineage(obj))
241
295
  return null;
242
- const segments = obj.segments;
243
- // 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);
244
580
  const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
245
- const colors = isDark ? {
246
- frameText: '#ccc',
247
- functionName: '#ffffff',
248
- segmentBg: '#2d2d2d',
249
- } : {
250
- frameText: '#555',
251
- functionName: '#222',
252
- segmentBg: '#eee',
253
- };
254
- const children = segments.map((segment, i) => {
255
- 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 [
588
+ 'div',
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 [
256
602
  'div',
257
- { style: `margin-left: 20px; color: ${colors.frameText}; font-family: monospace; font-size: 11px;` },
258
- ['span', { style: `color: ${colors.functionName};` }, `at ${frame.functionName} `],
259
- ['span', {}, `${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`],
260
- ]);
261
- segment.effect;
262
- const segmentHeader = [
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 [
263
626
  'div',
264
- { style: `margin-top: 5px; padding: 2px 5px; background: ${colors.segmentBg}; border-radius: 3px; font-weight: bold;` },
265
- i === 0 ? '📍 Current: ' : '↖ Effect: ',
266
- segment.effect?.name || 'anonymous',
267
- segment.effect ? ['object', { object: segment.effect }] : 'root',
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],
268
630
  ];
269
- return ['div', {}, segmentHeader, ...frames];
270
631
  });
271
632
  return ['div', { style: 'padding: 5px; line-height: 1.4;' }, ...children];
272
633
  },
273
634
  };
274
- /**
275
- * Wraps lineage data in a way that the Chrome Formatter can recognize.
276
- */
277
- function wrapLineageForDebug(segments) {
278
- const lineage = {
279
- segments,
280
- toString: () => formatLineage(segments ?? getLineage()),
281
- };
282
- lineageObjects.add(lineage);
283
- return lineage;
284
- }
285
635
 
286
636
  /**
287
637
  * Displays a floating reactive panel in the browser showing the lineage of triggered effects.
@@ -759,7 +1109,6 @@ function buildReactivityGraph() {
759
1109
  },
760
1110
  };
761
1111
  }
762
- const captureLineage = () => wrapLineageForDebug(getLineage());
763
1112
  /**
764
1113
  * Enables the DevTools bridge and exposes the debug API on window/global.
765
1114
  * Call as early as possible in development builds.
@@ -786,24 +1135,40 @@ function enableDevTools() {
786
1135
  logLineage(getLineage());
787
1136
  return '🦴 Effect Lineage Trace' + (tag ? ` (${tag})` : '');
788
1137
  },
789
- get browserLineage() {
790
- return captureLineage();
1138
+ logReason(reason, tag) {
1139
+ const reasonToLog = reason ?? globalScope.__MUTTS_DEBUG__.reason;
1140
+ if (!reasonToLog)
1141
+ return '🧹 Cleanup Reason';
1142
+ return logReason(reasonToLog, tag);
1143
+ },
1144
+ get lineage() {
1145
+ return getLineage();
791
1146
  },
792
- getLineage,
793
- captureLineage: getStackFrame,
794
- formatLineage,
795
1147
  showLineagePanel,
796
1148
  setEffectName,
797
1149
  setObjectName,
798
1150
  registerEffect: registerEffectForDebug,
799
1151
  registerObject: registerObjectForDebug,
1152
+ /**
1153
+ * Returns the reason why the current effect is being executed.
1154
+ * Returns undefined if not in an effect or if this is the first run.
1155
+ * @type {() => import('../src/reactive/types').CleanupReason | undefined}
1156
+ */
1157
+ get reason() {
1158
+ const activeEffect = proxy.getActiveEffect();
1159
+ if (!activeEffect)
1160
+ return undefined;
1161
+ const node = proxy.getEffectNode(activeEffect);
1162
+ return node?.currentReason;
1163
+ },
800
1164
  };
801
1165
  // @ts-ignore - devtoolsFormatters is a Chrome-specific array
802
1166
  if (globalScope.devtoolsFormatters) {
803
1167
  globalScope.devtoolsFormatters.push(lineageFormatter);
1168
+ globalScope.devtoolsFormatters.push(reasonFormatter);
804
1169
  }
805
1170
  else {
806
- globalScope.devtoolsFormatters = [lineageFormatter];
1171
+ globalScope.devtoolsFormatters = [lineageFormatter, reasonFormatter];
807
1172
  }
808
1173
  proxy.setDebugHooks({
809
1174
  isDevtoolsEnabled: () => devtoolsEnabled,
@@ -812,6 +1177,8 @@ function enableDevTools() {
812
1177
  captureStack: getStackFrame,
813
1178
  captureLineage,
814
1179
  formatStack: (stack) => {
1180
+ if (isCleanupReason(stack))
1181
+ return [stack];
815
1182
  if (typeof stack === 'string') {
816
1183
  return [
817
1184
  `\n ${stack
@@ -833,8 +1200,7 @@ function enableDevTools() {
833
1200
  typeof error === 'object' &&
834
1201
  'stack' in error &&
835
1202
  !('lineage' in error)) {
836
- const lineage = getLineage(trigger, getStackFrame(error));
837
- error.lineage = wrapLineageForDebug(lineage);
1203
+ error.lineage = wrapLineageForDebug(getLineage(trigger, error));
838
1204
  }
839
1205
  },
840
1206
  });
@@ -967,9 +1333,11 @@ if (isDevelopmentMode() && !devtoolsEnabled) {
967
1333
  exports.buildReactivityGraph = buildReactivityGraph;
968
1334
  exports.captureLineage = captureLineage;
969
1335
  exports.captureNodeLineage = captureNodeLineage;
1336
+ exports.digestLineage = digestLineage;
970
1337
  exports.enableDevTools = enableDevTools;
971
1338
  exports.forceEnableGraphTracking = forceEnableGraphTracking;
972
1339
  exports.formatLineage = formatLineage;
1340
+ exports.getCleanupReasonChain = getCleanupReasonChain;
973
1341
  exports.getDependencies = getDependencies;
974
1342
  exports.getDependencyGraph = getDependencyGraph;
975
1343
  exports.getDependents = getDependents;
@@ -977,12 +1345,14 @@ exports.getLineage = getLineage;
977
1345
  exports.getMutationHistory = getMutationHistory;
978
1346
  exports.getStackFrame = getStackFrame;
979
1347
  exports.getTriggerChain = getTriggerChain;
1348
+ exports.isCleanupReason = isCleanupReason;
980
1349
  exports.isDevtoolsEnabled = isDevtoolsEnabled;
981
1350
  exports.isLineage = isLineage;
982
1351
  exports.lineageFormatter = lineageFormatter;
983
1352
  exports.logError = logError;
984
1353
  exports.logLineage = logLineage;
985
- exports.parseStackLine = parseStackLine;
1354
+ exports.logReason = logReason;
1355
+ exports.reasonFormatter = reasonFormatter;
986
1356
  exports.recordTriggerLink = recordTriggerLink;
987
1357
  exports.registerEffectForDebug = registerEffectForDebug;
988
1358
  exports.registerObjectForDebug = registerObjectForDebug;