mutts 1.0.12 → 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 (55) hide show
  1. package/README.md +5 -2
  2. package/dist/browser.cjs +7 -3
  3. package/dist/browser.cjs.map +1 -1
  4. package/dist/browser.d.ts +1407 -2
  5. package/dist/browser.dev.cjs +7 -3
  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-yK0HVxHv.cjs → index-CAdnMJev.cjs} +202 -79
  11. package/dist/chunks/index-CAdnMJev.cjs.map +1 -0
  12. package/dist/chunks/{index-BUop6B2U.esm.js → index-XsYTUhHx.esm.js} +200 -77
  13. package/dist/chunks/index-XsYTUhHx.esm.js.map +1 -0
  14. package/dist/chunks/{node-Dd0esp5F.cjs → node-DrrphEPf.cjs} +2 -2
  15. package/dist/chunks/{node-Dd0esp5F.cjs.map → node-DrrphEPf.cjs.map} +1 -1
  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-BvM4yewA.cjs → proxy-DBHj3kGK.cjs} +313 -66
  21. package/dist/chunks/proxy-DBHj3kGK.cjs.map +1 -0
  22. package/dist/debug.cjs +537 -166
  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 +8 -4
  33. package/dist/node.cjs.map +1 -1
  34. package/dist/node.d.ts +2 -2
  35. package/dist/node.dev.cjs +8 -4
  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/index-BUop6B2U.esm.js.map +0 -1
  52. package/dist/chunks/index-yK0HVxHv.cjs.map +0 -1
  53. package/dist/chunks/proxy-BvM4yewA.cjs.map +0 -1
  54. package/dist/chunks/proxy-D2C49sXH.esm.js.map +0 -1
  55. package/dist/index.d.ts +0 -1322
package/dist/debug.esm.js CHANGED
@@ -1,22 +1,20 @@
1
- import { ai as getRoot, ax as effectMarker, F as getActiveEffect, ay as getEffectNode, $ as reactive, x as effect, a0 as options, az as setDebugHooks, aw as effectToReactiveObjects, aA as allProps } from './chunks/proxy-D2C49sXH.esm.js';
1
+ import { J as getActiveEffect, ap as getRoot, ah as getEffectNode, aE as effectMarker, G as formatCleanupReason, a3 as reactive, y as effect, a4 as options, aF as setDebugHooks, aD as effectToReactiveObjects, aG as allProps } from './chunks/proxy-BtmPFjSr.esm.js';
2
2
 
3
3
  const lineageObjects = new WeakSet();
4
- /**
5
- * Checks if an object is a lineage object
6
- */
4
+ const lineageSegmentObjects = new WeakSet();
5
+ const lineageFrameObjects = new WeakSet();
7
6
  function isLineage(obj) {
8
- return obj && typeof obj === 'object' && lineageObjects.has(obj);
7
+ return !!obj && typeof obj === 'object' && lineageObjects.has(obj);
9
8
  }
10
- /**
11
- * Parses a single stack line into a structured frame.
12
- * @param line - A line from Error.stack
13
- */
9
+ function isLineageSegmentView(obj) {
10
+ return !!obj && typeof obj === 'object' && lineageSegmentObjects.has(obj);
11
+ }
12
+ function isLineageFrameView(obj) {
13
+ return !!obj && typeof obj === 'object' && lineageFrameObjects.has(obj);
14
+ }
15
+ let internalFile;
14
16
  function parseStackLine(line) {
15
- // Node.js format: " at functionName (file:line:column)"
16
- // or " at file:line:column"
17
17
  const nodeMatch = line.match(/^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+))\)?$/);
18
- // Browser format (Firefox/Safari): "functionName@file:line:column" or "file:line:column"
19
- // Chrome/Edge use Node-like format but without "at" sometimes or with different prefixes
20
18
  const browserMatch = line.match(/(?:(.+?)(?:\@|\(?))?(?:(.+?):(\d+):(\d+))(?:\@|\)?)$/);
21
19
  const match = nodeMatch || browserMatch;
22
20
  if (!match)
@@ -30,16 +28,23 @@ function parseStackLine(line) {
30
28
  raw: line.trim(),
31
29
  };
32
30
  }
33
- let internalFile;
34
- /**
35
- * Gets the current call stack
36
- * @param skipFrames - Number of frames to skip
37
- * @param error - Optional error to use as source of stack
38
- */
39
- function getStackFrame(error = new Error()) {
40
- if (!error.stack)
31
+ function extractRawStack(error = new Error()) {
32
+ if (typeof error === 'string')
33
+ return error;
34
+ if (error && typeof error === 'object' && 'stack' in error) {
35
+ const stack = error.stack;
36
+ return typeof stack === 'string' ? stack : undefined;
37
+ }
38
+ return undefined;
39
+ }
40
+ function getStackFrame(error) {
41
+ return extractRawStack(error);
42
+ }
43
+ function parseStackFrames(stack) {
44
+ const rawStack = extractRawStack(stack);
45
+ if (!rawStack)
41
46
  return [];
42
- const lines = error.stack.split('\n');
47
+ const lines = rawStack.split('\n');
43
48
  const lastLine = lines.findIndex((line) => line.includes(effectMarker.enter));
44
49
  if (lastLine !== -1)
45
50
  lines.splice(lastLine);
@@ -47,150 +52,164 @@ function getStackFrame(error = new Error()) {
47
52
  if (firstLine !== -1)
48
53
  lines.splice(0, firstLine + 1);
49
54
  else {
50
- // Dynamically identify the library's internal files if not already done
51
55
  if (!internalFile && lines[1]) {
52
56
  const selfFrame = parseStackLine(lines[1]);
53
- if (selfFrame) {
57
+ if (selfFrame)
54
58
  internalFile = selfFrame.fileName;
55
- }
56
59
  }
57
- // Determine the "base" directory of the library to skip other internal files
58
- // We look for "src" or "dist" to be more specific than just the project root
59
- const srcIndex = internalFile ? internalFile.lastIndexOf('/src/') : -1;
60
- const distIndex = internalFile ? internalFile.lastIndexOf('/dist/') : -1;
61
- internalFile ? (srcIndex !== -1 ? internalFile.substring(0, srcIndex + 5) :
62
- (distIndex !== -1 ? internalFile.substring(0, distIndex + 6) :
63
- internalFile.substring(0, internalFile.lastIndexOf('/') + 1))) : undefined;
64
- let l;
65
- for (l = 1; l < lines.length; l++) {
60
+ let l = 1;
61
+ for (; l < lines.length; l++) {
66
62
  const frame = parseStackLine(lines[l]);
67
63
  if (!frame)
68
64
  continue;
69
- // Robust skipping: if we are still in the internal area, skip it.
70
65
  const isInternal = /Lineage$/.test(frame.functionName) ||
71
66
  frame.functionName === 'eval' ||
72
- [`getStackFrame`, `captureLineage`].includes(frame.functionName);
67
+ [`getStackFrame`, `captureLineage`, `getLineage`, `digestLineage`, `formatLineage`].includes(frame.functionName);
73
68
  if (!isInternal)
74
69
  break;
75
70
  }
76
71
  lines.splice(0, l);
77
72
  }
78
- return lines.map(parseStackLine).filter(Boolean);
73
+ return filterNodeModules(lines.map(parseStackLine).filter((frame) => !!frame));
79
74
  }
80
- /**
81
- * Traces the lineage of the current execution through nested effects
82
- * @param effect - Starting effect (defaults to active effect)
83
- */
84
- function getLineage(effect, currentStack = getStackFrame()) {
85
- const currentEffect = effect ?? getActiveEffect();
75
+ function filterNodeModules(frames) {
76
+ const result = [];
77
+ let inNodeModules = false;
78
+ for (const frame of frames) {
79
+ const isNodeModule = frame.fileName.includes('/node_modules/');
80
+ if (isNodeModule && !inNodeModules) {
81
+ inNodeModules = true;
82
+ result.push({
83
+ functionName: '...node_modules...',
84
+ fileName: '[filtered]',
85
+ lineNumber: 0,
86
+ columnNumber: 0,
87
+ raw: 'at ...node_modules...',
88
+ });
89
+ continue;
90
+ }
91
+ if (!isNodeModule && inNodeModules)
92
+ inNodeModules = false;
93
+ if (!isNodeModule)
94
+ result.push(frame);
95
+ }
96
+ return result;
97
+ }
98
+ function formatEffectPreview(effect) {
99
+ if (!effect)
100
+ return 'root';
101
+ return `[effect ${effect.name || 'anonymous'}]`;
102
+ }
103
+ function formatSegmentTitle(segment, index, total) {
104
+ const isLast = index === total - 1;
105
+ const prefix = index === 0 ? '📍' : isLast ? '└─' : '├─';
106
+ return `${prefix} Effect: ${segment.effect?.name || 'root'}`;
107
+ }
108
+ function wrapLineageSegmentView(segment, index, total) {
109
+ const view = { index, total, segment };
110
+ lineageSegmentObjects.add(view);
111
+ return view;
112
+ }
113
+ function wrapLineageFrameView(frame) {
114
+ const view = { frame };
115
+ lineageFrameObjects.add(view);
116
+ return view;
117
+ }
118
+ function formatLocationHref(frame) {
119
+ return `${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`;
120
+ }
121
+ function ensureSignature(signature) {
122
+ if (!isLineage(signature)) {
123
+ Object.defineProperty(signature, 'toString', {
124
+ value: () => formatLineage(signature),
125
+ enumerable: false,
126
+ configurable: true,
127
+ });
128
+ lineageObjects.add(signature);
129
+ }
130
+ return signature;
131
+ }
132
+ function getLineage(effect, currentStack = new Error()) {
133
+ return ensureSignature({
134
+ effect: effect ?? getActiveEffect(),
135
+ stack: currentStack,
136
+ toString() {
137
+ return formatLineage(this);
138
+ },
139
+ });
140
+ }
141
+ function captureLineage(effect, currentStack) {
142
+ return getLineage(effect, currentStack ?? new Error());
143
+ }
144
+ function digestLineage(lineage) {
145
+ if (lineage.digested)
146
+ return lineage.digested;
147
+ const currentEffect = lineage.effect;
148
+ const currentStack = parseStackFrames(lineage.stack);
86
149
  const segments = [];
87
150
  if (!currentEffect) {
88
- segments.push({
89
- effect: undefined,
90
- effectName: 'root',
91
- stack: filterNodeModules(currentStack),
92
- });
93
- return segments;
151
+ lineage.digested = [
152
+ {
153
+ effect: undefined,
154
+ effectName: 'root',
155
+ stack: currentStack,
156
+ },
157
+ ];
158
+ return lineage.digested;
94
159
  }
95
160
  let current = currentEffect;
96
161
  let lastStack = currentStack;
97
162
  while (current) {
98
163
  const rootFn = getRoot(current);
99
- // Too aggressive for now
100
- //if(!rootFn.name) debugger
101
- const filteredStack = filterNodeModules(lastStack);
102
164
  segments.push({
103
165
  effect: rootFn,
104
166
  effectName: rootFn.name || 'anonymous',
105
- stack: filteredStack,
167
+ stack: lastStack,
106
168
  });
107
- // Move to parent
108
169
  const node = getEffectNode(current);
109
170
  const parent = node.parent;
110
- const creationStack = node.creationStack;
171
+ const creationStack = parseStackFrames(node.creationStack);
111
172
  if (parent) {
112
173
  current = parent;
113
- lastStack = creationStack ?? [];
174
+ lastStack = creationStack;
114
175
  }
115
- else if (creationStack) {
176
+ else if (node.creationStack) {
116
177
  segments.push({
117
178
  effect: undefined,
118
179
  effectName: 'root',
119
- stack: filterNodeModules(creationStack),
180
+ stack: creationStack,
120
181
  });
121
182
  break;
122
183
  }
123
- else {
184
+ else
124
185
  break;
125
- }
126
186
  }
187
+ lineage.digested = segments;
127
188
  return segments;
128
189
  }
129
- /**
130
- * Filters out node_modules frames and groups them
131
- * @param frames - Array of stack frames
132
- */
133
- function filterNodeModules(frames) {
134
- const result = [];
135
- let inNodeModules = false;
136
- for (const frame of frames) {
137
- const isNodeModule = frame.fileName.includes('/node_modules/'); //|| frame.fileName.includes('/dist/')
138
- if (isNodeModule && !inNodeModules) {
139
- // Start of node_modules block
140
- inNodeModules = true;
141
- result.push({
142
- functionName: '...node_modules...',
143
- fileName: '[filtered]',
144
- lineNumber: 0,
145
- columnNumber: 0,
146
- raw: ' at ...node_modules...'
147
- });
148
- }
149
- else if (!isNodeModule && inNodeModules) {
150
- // End of node_modules block
151
- inNodeModules = false;
152
- result.push(frame);
153
- }
154
- else if (!isNodeModule) {
155
- // Regular frame
156
- result.push(frame);
157
- }
158
- // Skip frames inside node_modules
159
- }
160
- return result;
161
- }
162
- /**
163
- * Formats lineage segments into a single stack-like string
164
- * @param segments - Lineage segments
165
- */
166
- function formatLineage(segments) {
190
+ function formatLineage(lineage) {
191
+ const segments = Array.isArray(lineage) ? lineage : digestLineage(lineage);
167
192
  const result = [];
168
193
  for (let i = 0; i < segments.length; i++) {
169
194
  const segment = segments[i];
170
195
  if (i > 0) {
171
196
  const triggerEffect = segments[i - 1].effect;
172
- const triggerName = triggerEffect ? (getRoot(triggerEffect).name || 'anonymous') : 'root';
197
+ const triggerName = triggerEffect ? getRoot(triggerEffect).name || 'anonymous' : 'root';
173
198
  result.push(` --- effect: ${triggerName} ---`);
174
199
  }
175
- for (const frame of segment.stack) {
200
+ for (const frame of segment.stack)
176
201
  result.push(` ${frame.raw}`);
177
- }
178
202
  }
179
203
  return result.join('\n');
180
204
  }
181
- /**
182
- * Logs lineage segments to console with grouping
183
- * @param segments - Lineage segments
184
- */
185
- function logLineage(segments) {
205
+ function logLineage(lineage) {
206
+ const segments = Array.isArray(lineage) ? lineage : digestLineage(lineage);
186
207
  console.groupCollapsed(`🦴 Effect Lineage Trace (${segments.length} segment${segments.length === 1 ? '' : 's'})`);
187
208
  for (let i = 0; i < segments.length; i++) {
188
209
  const segment = segments[i];
189
- // Add segment header
190
210
  const isLast = i === segments.length - 1;
191
211
  const prefix = i === 0 ? '📍' : isLast ? '└─' : '├─';
192
212
  console.groupCollapsed(`${prefix} Effect:`, segment.effect ? segment.effect.name || 'anonymous' : 'root');
193
- // Add stack frames
194
213
  for (let j = 0; j < segment.stack.length; j++) {
195
214
  const frame = segment.stack[j];
196
215
  const isLastFrame = j === segment.stack.length - 1;
@@ -208,76 +227,408 @@ function logLineage(segments) {
208
227
  }
209
228
  console.groupEnd();
210
229
  }
211
- /**
212
- * Captures and logs lineage to console
213
- */
214
230
  function captureNodeLineage() {
215
231
  logLineage(getLineage());
216
232
  }
217
- /**
218
- * Custom formatter for Chrome DevTools to render lineage data nicely.
219
- */
220
233
  const lineageFormatter = {
221
234
  header: (obj) => {
222
- if (isLineage(obj)) {
223
- // Try to detect DevTools theme - default to dark colors if uncertain
235
+ if (isLineageSegmentView(obj)) {
224
236
  const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
225
- const headerColor = isDark ? '#cd9d5d' : '#704214';
237
+ const frameColor = isDark ? '#8b949e' : '#57606a';
226
238
  return [
227
239
  'span',
228
- { style: `color: ${headerColor}; font-weight: bold;` },
229
- `🦴 Effect Lineage (${obj.segments.length} segments)`,
240
+ { style: 'font-weight: bold;' },
241
+ formatSegmentTitle(obj.segment, obj.index, obj.total),
242
+ ['span', { style: `color: ${frameColor}; margin-left: 6px; font-weight: normal;` }, formatEffectPreview(obj.segment.effect)],
243
+ [
244
+ 'span',
245
+ { style: `color: ${frameColor}; margin-left: 6px; font-weight: normal;` },
246
+ `${obj.segment.stack.length} frame${obj.segment.stack.length === 1 ? '' : 's'}`,
247
+ ],
230
248
  ];
231
249
  }
232
- return null;
250
+ if (isLineageFrameView(obj)) {
251
+ const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
252
+ const fnColor = obj.frame.functionName === 'anonymous' ? '#8b949e' : isDark ? '#ffffff' : '#222';
253
+ const linkColor = isDark ? '#58a6ff' : '#005cc5';
254
+ return [
255
+ 'div',
256
+ { style: 'display: block;' },
257
+ ['span', { style: `color: ${fnColor}; font-weight: bold;` }, obj.frame.functionName],
258
+ ['span', { style: 'color: #8b949e; margin: 0 6px;' }, '@'],
259
+ [
260
+ 'span',
261
+ { style: `color: ${linkColor}; text-decoration: underline;` },
262
+ formatLocationHref(obj.frame),
263
+ ],
264
+ ];
265
+ }
266
+ if (!isLineage(obj))
267
+ return null;
268
+ const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
269
+ const headerColor = isDark ? '#cd9d5d' : '#704214';
270
+ return [
271
+ 'span',
272
+ { style: `color: ${headerColor}; font-weight: bold;` },
273
+ obj.digested
274
+ ? `🦴 Effect Lineage (${obj.digested.length} segments)`
275
+ : '🦴 Effect Lineage (lazy)',
276
+ ];
233
277
  },
234
- hasBody: (obj) => isLineage(obj),
278
+ hasBody: (obj) => isLineage(obj) || isLineageSegmentView(obj),
235
279
  body: (obj) => {
280
+ if (isLineageSegmentView(obj)) {
281
+ return [
282
+ 'div',
283
+ { style: 'padding: 4px 0;' },
284
+ ...obj.segment.stack.map((frame) => [
285
+ 'div',
286
+ { style: 'display: block; margin-top: 2px;' },
287
+ ['object', { object: wrapLineageFrameView(frame) }],
288
+ ]),
289
+ ];
290
+ }
236
291
  if (!isLineage(obj))
237
292
  return null;
238
- const segments = obj.segments;
239
- // Try to detect DevTools theme
293
+ const segments = digestLineage(obj);
294
+ return [
295
+ 'div',
296
+ { style: 'padding: 5px; line-height: 1.4;' },
297
+ ...segments.map((segment, index) => [
298
+ 'div',
299
+ { style: 'display: block; margin-top: 4px;' },
300
+ ['object', { object: wrapLineageSegmentView(segment, index, segments.length) }],
301
+ ]),
302
+ ];
303
+ },
304
+ };
305
+ function wrapLineageForDebug(lineage) {
306
+ return ensureSignature(lineage ?? getLineage());
307
+ }
308
+
309
+ const reasonTypes = new Set(['propChange', 'invalidate', 'external', 'stopped', 'gc', 'lineage', 'error', 'multiple']);
310
+ const reasonCauseLists = new WeakSet();
311
+ function hasOwn(value, key) {
312
+ return Object.prototype.hasOwnProperty.call(value, key);
313
+ }
314
+ function isCleanupReason(value) {
315
+ if (!value || typeof value !== 'object')
316
+ return false;
317
+ if (!hasOwn(value, 'type'))
318
+ return false;
319
+ const type = value.type;
320
+ if (typeof type !== 'string' || !reasonTypes.has(type))
321
+ return false;
322
+ switch (type) {
323
+ case 'propChange':
324
+ return Array.isArray(value.triggers);
325
+ case 'invalidate':
326
+ return hasOwn(value, 'cause');
327
+ case 'lineage':
328
+ return hasOwn(value, 'parent');
329
+ case 'error':
330
+ return hasOwn(value, 'error');
331
+ case 'multiple':
332
+ return Array.isArray(value.reasons);
333
+ default:
334
+ return true;
335
+ }
336
+ }
337
+ const reasonCauseEntries = new WeakSet();
338
+ const reasonCauseDetails = new WeakSet();
339
+ function isReasonCauseList(value) {
340
+ return !!value && typeof value === 'object' && reasonCauseLists.has(value);
341
+ }
342
+ function isReasonCauseEntry(value) {
343
+ return !!value && typeof value === 'object' && reasonCauseEntries.has(value);
344
+ }
345
+ function isReasonCauseDetail(value) {
346
+ return !!value && typeof value === 'object' && reasonCauseDetails.has(value);
347
+ }
348
+ function wrapReasonCauseEntry(entry) {
349
+ reasonCauseEntries.add(entry);
350
+ return entry;
351
+ }
352
+ function wrapReasonCauseDetail(detail) {
353
+ reasonCauseDetails.add(detail);
354
+ return detail;
355
+ }
356
+ function wrapReasonCauseList(list) {
357
+ reasonCauseLists.add(list);
358
+ return list;
359
+ }
360
+ function getCleanupReasonChain(reason) {
361
+ const chain = [];
362
+ let current = reason;
363
+ while (current) {
364
+ chain.push(current);
365
+ current = current.chain;
366
+ }
367
+ return chain;
368
+ }
369
+ function formatReasonSummary(reason) {
370
+ switch (reason.type) {
371
+ case 'propChange':
372
+ return `propChange: ${formatTriggerSummary(reason)}`;
373
+ case 'invalidate':
374
+ return `invalidate ← ${formatReasonSummary(reason.cause)}`;
375
+ case 'stopped':
376
+ return reason.detail ? `stopped (${reason.detail})` : 'stopped';
377
+ case 'external':
378
+ return `external: ${reason.detail}`;
379
+ case 'gc':
380
+ return 'gc';
381
+ case 'lineage':
382
+ return `lineage ← ${formatReasonSummary(reason.parent)}`;
383
+ case 'error':
384
+ return `error: ${formatUnknownSummary(reason.error)}`;
385
+ case 'multiple':
386
+ return `multiple: ${reason.reasons.map(formatReasonSummary).join(' | ')}`;
387
+ }
388
+ }
389
+ function formatTriggerSummary(reason) {
390
+ const groups = new Map();
391
+ const ordered = [];
392
+ for (const { obj, evolution } of reason.triggers) {
393
+ let group = groups.get(obj);
394
+ if (!group) {
395
+ group = { target: describeTarget(obj), parts: [] };
396
+ groups.set(obj, group);
397
+ ordered.push(group);
398
+ }
399
+ group.parts.push(evolution.type === 'bunch'
400
+ ? `${evolution.type} ${String(evolution.method)}`
401
+ : `${evolution.type} ${String(evolution.prop)}`);
402
+ }
403
+ return ordered.map(({ target, parts }) => `${target}: ${parts.join(', ')}`).join(' | ');
404
+ }
405
+ function formatUnknownSummary(value) {
406
+ if (typeof value === 'string')
407
+ return value;
408
+ if (typeof value === 'number' || typeof value === 'boolean' || value == null)
409
+ return String(value);
410
+ if (value instanceof Error)
411
+ return value.message;
412
+ if (typeof value === 'function')
413
+ return value.name || 'anonymous';
414
+ if (typeof value === 'object')
415
+ return value.constructor?.name || 'object';
416
+ return typeof value;
417
+ }
418
+ function describeTarget(value) {
419
+ if (!value || typeof value !== 'object')
420
+ return formatUnknownSummary(value);
421
+ const tag = value[Symbol.toStringTag];
422
+ if (typeof tag === 'string' && tag && tag !== 'Object' && tag !== 'Array')
423
+ return tag;
424
+ if (Array.isArray(value))
425
+ return 'Array';
426
+ if (value instanceof Map)
427
+ return 'Map';
428
+ if (value instanceof Set)
429
+ return 'Set';
430
+ return value.constructor?.name || 'object';
431
+ }
432
+ function formatPreview(value) {
433
+ if (typeof value === 'string')
434
+ return value;
435
+ if (typeof value === 'number' || typeof value === 'boolean' || value == null)
436
+ return String(value);
437
+ if (typeof value === 'function')
438
+ return `[function ${value.name || 'anonymous'}]`;
439
+ if (value instanceof Error)
440
+ return `[Error: ${value.message}]`;
441
+ if (value && typeof value === 'object' && 'stack' in value)
442
+ return '[lineage]';
443
+ if (typeof value === 'object')
444
+ return `[${value.constructor?.name || 'object'}]`;
445
+ return `[${typeof value}]`;
446
+ }
447
+ function renderDetailValue(value, color) {
448
+ if (value && typeof value === 'object' && 'stack' in value) {
449
+ return ['object', { object: value }];
450
+ }
451
+ return ['span', { style: `color: ${color}; white-space: pre-wrap;` }, formatPreview(value)];
452
+ }
453
+ function getReasonLineageDetails(reason) {
454
+ switch (reason.type) {
455
+ case 'propChange': {
456
+ const details = [];
457
+ for (const trigger of reason.triggers) {
458
+ const target = trigger.evolution.type === 'bunch'
459
+ ? String(trigger.evolution.method)
460
+ : String(trigger.evolution.prop);
461
+ const label = trigger.evolution.type === 'bunch'
462
+ ? `${trigger.evolution.type} ${String(trigger.evolution.method)}`
463
+ : `${trigger.evolution.type} ${String(trigger.evolution.prop)}`;
464
+ if (trigger.touch)
465
+ details.push({ kind: 'touch', target, label, value: trigger.touch });
466
+ if (trigger.dependency)
467
+ details.push({ kind: 'dependency', target, label, value: trigger.dependency });
468
+ }
469
+ return details;
470
+ }
471
+ case 'invalidate':
472
+ return getReasonLineageDetails(reason.cause);
473
+ case 'lineage':
474
+ return getReasonLineageDetails(reason.parent);
475
+ case 'multiple':
476
+ return reason.reasons.flatMap(getReasonLineageDetails);
477
+ case 'external':
478
+ default:
479
+ return [];
480
+ }
481
+ }
482
+ function renderLineageList(details, colors) {
483
+ const grouped = new Map();
484
+ for (const { kind, label, value } of details) {
485
+ let entry = grouped.get(label);
486
+ if (!entry) {
487
+ entry = { label };
488
+ grouped.set(label, entry);
489
+ }
490
+ if (kind === 'touch')
491
+ entry.touch = [...(entry.touch ?? []), value];
492
+ if (kind === 'dependency')
493
+ entry.dependency = [...(entry.dependency ?? []), value];
494
+ }
495
+ const listObject = wrapReasonCauseList({
496
+ count: grouped.size,
497
+ entries: Array.from(grouped.values(), wrapReasonCauseEntry),
498
+ });
499
+ return [
500
+ 'div',
501
+ {
502
+ style: `margin-top: 4px; padding: 3px 5px; background: ${colors.block}; border-radius: 3px;`,
503
+ },
504
+ ['object', { object: listObject }],
505
+ ];
506
+ }
507
+ function formatReasonWithoutChain(reason) {
508
+ switch (reason.type) {
509
+ case 'propChange':
510
+ return formatCleanupReason({ type: 'propChange', triggers: reason.triggers });
511
+ case 'invalidate':
512
+ return formatCleanupReason({ type: 'invalidate', cause: reason.cause });
513
+ case 'stopped':
514
+ return formatCleanupReason(reason.detail ? { type: 'stopped', detail: reason.detail } : { type: 'stopped' });
515
+ case 'external':
516
+ return formatCleanupReason({ type: 'external', detail: reason.detail });
517
+ case 'gc':
518
+ return formatCleanupReason({ type: 'gc' });
519
+ case 'lineage':
520
+ return formatCleanupReason({ type: 'lineage', parent: reason.parent });
521
+ case 'error':
522
+ return formatCleanupReason({ type: 'error', error: reason.error });
523
+ case 'multiple':
524
+ return formatCleanupReason({ type: 'multiple', reasons: reason.reasons });
525
+ }
526
+ }
527
+ function logReason(reason, tag) {
528
+ const chain = getCleanupReasonChain(reason);
529
+ console.groupCollapsed(`🧹 Cleanup Reason${tag ? ` (${tag})` : ''}`);
530
+ for (let i = 0; i < chain.length; i++) {
531
+ console.groupCollapsed(`${i === 0 ? '📍' : '↖'} ${formatReasonSummary(chain[i])}`);
532
+ const details = getReasonLineageDetails(chain[i]);
533
+ if (details.length > 0) {
534
+ for (const { kind, label, value } of details)
535
+ console.log(`${kind} ${label}`, value);
536
+ }
537
+ else {
538
+ console.log(...formatReasonWithoutChain(chain[i]));
539
+ }
540
+ console.groupEnd();
541
+ }
542
+ console.groupEnd();
543
+ return `🧹 Cleanup Reason${tag ? ` (${tag})` : ''}`;
544
+ }
545
+ const reasonFormatter = {
546
+ header: (obj) => {
547
+ if (isReasonCauseList(obj)) {
548
+ return ['span', { style: 'font-weight: bold;' }, `causes: ${obj.count}`];
549
+ }
550
+ if (isReasonCauseEntry(obj)) {
551
+ return [
552
+ 'span',
553
+ { style: 'font-weight: bold;' },
554
+ `${obj.label}:`,
555
+ ...(obj.touch?.length
556
+ ? [
557
+ ['span', { style: 'margin-left: 6px;' }, ['object', { object: wrapReasonCauseDetail({ kind: 'touch', values: obj.touch }) }]],
558
+ ]
559
+ : []),
560
+ ...(obj.dependency?.length
561
+ ? [
562
+ ['span', { style: 'margin-left: 6px;' }, ['object', { object: wrapReasonCauseDetail({ kind: 'dependency', values: obj.dependency }) }]],
563
+ ]
564
+ : []),
565
+ ];
566
+ }
567
+ if (isReasonCauseDetail(obj)) {
568
+ return [
569
+ 'span',
570
+ { style: 'font-weight: normal;' },
571
+ obj.values.length === 1 ? obj.kind : `${obj.values.length} ${obj.kind}`,
572
+ ];
573
+ }
574
+ if (!isCleanupReason(obj))
575
+ return null;
576
+ const chain = getCleanupReasonChain(obj);
240
577
  const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
241
- const colors = isDark ? {
242
- frameText: '#ccc',
243
- functionName: '#ffffff',
244
- segmentBg: '#2d2d2d',
245
- } : {
246
- frameText: '#555',
247
- functionName: '#222',
248
- segmentBg: '#eee',
249
- };
250
- const children = segments.map((segment, i) => {
251
- const frames = segment.stack.map((frame) => [
578
+ const color = isDark ? '#8fb3ff' : '#0b5fff';
579
+ return ['span', { style: `color: ${color}; font-weight: bold;` }, `🧹 ${formatReasonSummary(obj)}${chain.length > 1 ? ` ×${chain.length}` : ''}`];
580
+ },
581
+ hasBody: (obj) => isCleanupReason(obj) || isReasonCauseList(obj) || isReasonCauseDetail(obj),
582
+ body: (obj) => {
583
+ if (isReasonCauseList(obj)) {
584
+ return [
585
+ 'div',
586
+ { style: 'padding: 4px 0;' },
587
+ ...obj.entries.map((entry) => [
588
+ 'div',
589
+ { style: 'display: block; margin-top: 2px;' },
590
+ ['object', { object: entry }],
591
+ ]),
592
+ ];
593
+ }
594
+ if (isReasonCauseDetail(obj)) {
595
+ const detailValue = obj.values.length === 1
596
+ ? obj.values[0]
597
+ : Object.fromEntries(obj.values.map((value, index) => [`${obj.kind} ${index + 1}`, value]));
598
+ return [
252
599
  'div',
253
- { style: `margin-left: 20px; color: ${colors.frameText}; font-family: monospace; font-size: 11px;` },
254
- ['span', { style: `color: ${colors.functionName};` }, `at ${frame.functionName} `],
255
- ['span', {}, `${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`],
256
- ]);
257
- segment.effect;
258
- const segmentHeader = [
600
+ { style: 'padding: 4px 0; line-height: 1.4;' },
601
+ [
602
+ 'div',
603
+ { style: 'margin-top: 2px;' },
604
+ renderDetailValue(detailValue, 'inherit'),
605
+ ],
606
+ ];
607
+ }
608
+ if (!isCleanupReason(obj))
609
+ return null;
610
+ const chain = getCleanupReasonChain(obj);
611
+ const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
612
+ const colors = isDark
613
+ ? { block: '#2d2d2d', title: '#ffffff', meta: '#8b949e' }
614
+ : { block: '#eee', title: '#222', meta: '#57606a' };
615
+ const children = chain.map((reason, index) => {
616
+ const lineageDetails = getReasonLineageDetails(reason);
617
+ const renderedDetails = lineageDetails.length > 0
618
+ ? [renderLineageList(lineageDetails, colors)]
619
+ : formatReasonWithoutChain(reason).map((part) => typeof part === 'string'
620
+ ? ['span', { style: `color: ${colors.meta}; white-space: pre-wrap;` }, part]
621
+ : renderDetailValue(part, colors.meta));
622
+ return [
259
623
  'div',
260
- { style: `margin-top: 5px; padding: 2px 5px; background: ${colors.segmentBg}; border-radius: 3px; font-weight: bold;` },
261
- i === 0 ? '📍 Current: ' : '↖ Effect: ',
262
- segment.effect?.name || 'anonymous',
263
- segment.effect ? ['object', { object: segment.effect }] : 'root',
624
+ { style: `margin-top: 5px; padding: 4px 6px; background: ${colors.block}; border-radius: 3px;` },
625
+ ['div', { style: `font-weight: bold; color: ${colors.title}; margin-bottom: 3px;` }, `${index === 0 ? '📍' : '↖'} ${formatReasonSummary(reason)}`],
626
+ ['div', { style: 'display: inline-block;' }, ...renderedDetails],
264
627
  ];
265
- return ['div', {}, segmentHeader, ...frames];
266
628
  });
267
629
  return ['div', { style: 'padding: 5px; line-height: 1.4;' }, ...children];
268
630
  },
269
631
  };
270
- /**
271
- * Wraps lineage data in a way that the Chrome Formatter can recognize.
272
- */
273
- function wrapLineageForDebug(segments) {
274
- const lineage = {
275
- segments,
276
- toString: () => formatLineage(segments ?? getLineage()),
277
- };
278
- lineageObjects.add(lineage);
279
- return lineage;
280
- }
281
632
 
282
633
  /**
283
634
  * Displays a floating reactive panel in the browser showing the lineage of triggered effects.
@@ -755,7 +1106,6 @@ function buildReactivityGraph() {
755
1106
  },
756
1107
  };
757
1108
  }
758
- const captureLineage = () => wrapLineageForDebug(getLineage());
759
1109
  /**
760
1110
  * Enables the DevTools bridge and exposes the debug API on window/global.
761
1111
  * Call as early as possible in development builds.
@@ -782,24 +1132,40 @@ function enableDevTools() {
782
1132
  logLineage(getLineage());
783
1133
  return '🦴 Effect Lineage Trace' + (tag ? ` (${tag})` : '');
784
1134
  },
785
- get browserLineage() {
786
- return captureLineage();
1135
+ logReason(reason, tag) {
1136
+ const reasonToLog = reason ?? globalScope.__MUTTS_DEBUG__.reason;
1137
+ if (!reasonToLog)
1138
+ return '🧹 Cleanup Reason';
1139
+ return logReason(reasonToLog, tag);
1140
+ },
1141
+ get lineage() {
1142
+ return getLineage();
787
1143
  },
788
- getLineage,
789
- captureLineage: getStackFrame,
790
- formatLineage,
791
1144
  showLineagePanel,
792
1145
  setEffectName,
793
1146
  setObjectName,
794
1147
  registerEffect: registerEffectForDebug,
795
1148
  registerObject: registerObjectForDebug,
1149
+ /**
1150
+ * Returns the reason why the current effect is being executed.
1151
+ * Returns undefined if not in an effect or if this is the first run.
1152
+ * @type {() => import('../src/reactive/types').CleanupReason | undefined}
1153
+ */
1154
+ get reason() {
1155
+ const activeEffect = getActiveEffect();
1156
+ if (!activeEffect)
1157
+ return undefined;
1158
+ const node = getEffectNode(activeEffect);
1159
+ return node?.currentReason;
1160
+ },
796
1161
  };
797
1162
  // @ts-ignore - devtoolsFormatters is a Chrome-specific array
798
1163
  if (globalScope.devtoolsFormatters) {
799
1164
  globalScope.devtoolsFormatters.push(lineageFormatter);
1165
+ globalScope.devtoolsFormatters.push(reasonFormatter);
800
1166
  }
801
1167
  else {
802
- globalScope.devtoolsFormatters = [lineageFormatter];
1168
+ globalScope.devtoolsFormatters = [lineageFormatter, reasonFormatter];
803
1169
  }
804
1170
  setDebugHooks({
805
1171
  isDevtoolsEnabled: () => devtoolsEnabled,
@@ -808,6 +1174,8 @@ function enableDevTools() {
808
1174
  captureStack: getStackFrame,
809
1175
  captureLineage,
810
1176
  formatStack: (stack) => {
1177
+ if (isCleanupReason(stack))
1178
+ return [stack];
811
1179
  if (typeof stack === 'string') {
812
1180
  return [
813
1181
  `\n ${stack
@@ -829,8 +1197,7 @@ function enableDevTools() {
829
1197
  typeof error === 'object' &&
830
1198
  'stack' in error &&
831
1199
  !('lineage' in error)) {
832
- const lineage = getLineage(trigger, getStackFrame(error));
833
- error.lineage = wrapLineageForDebug(lineage);
1200
+ error.lineage = wrapLineageForDebug(getLineage(trigger, error));
834
1201
  }
835
1202
  },
836
1203
  });
@@ -960,5 +1327,5 @@ if (isDevelopmentMode() && !devtoolsEnabled) {
960
1327
  }
961
1328
  }
962
1329
 
963
- export { buildReactivityGraph, captureLineage, captureNodeLineage, enableDevTools, forceEnableGraphTracking, formatLineage, getDependencies, getDependencyGraph, getDependents, getLineage, getMutationHistory, getStackFrame, getTriggerChain, isDevtoolsEnabled, isLineage, lineageFormatter, logError, logLineage, parseStackLine, recordTriggerLink, registerEffectForDebug, registerObjectForDebug, setEffectName, setObjectName, showLineagePanel, snapshot, wrapLineageForDebug };
1330
+ export { buildReactivityGraph, captureLineage, captureNodeLineage, digestLineage, enableDevTools, forceEnableGraphTracking, formatLineage, getCleanupReasonChain, getDependencies, getDependencyGraph, getDependents, getLineage, getMutationHistory, getStackFrame, getTriggerChain, isCleanupReason, isDevtoolsEnabled, isLineage, lineageFormatter, logError, logLineage, logReason, reasonFormatter, recordTriggerLink, registerEffectForDebug, registerObjectForDebug, setEffectName, setObjectName, showLineagePanel, snapshot, wrapLineageForDebug };
964
1331
  //# sourceMappingURL=debug.esm.js.map