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.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, az as getRoot, ar as getEffectNode, aO as effectMarker, G as formatCleanupReason, a9 as reactive, y as effect, U as isReactive, al as unwrap, aa as options, x as devPreset, aP as setDebugHooks, aN as effectToReactiveObjects, aQ as allProps } from './chunks/proxy-C2lnvvbx.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 [
252
585
  '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 = [
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 [
259
599
  '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',
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 [
623
+ 'div',
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.
@@ -377,13 +728,13 @@ function showLineagePanel() {
377
728
  panel.appendChild(content);
378
729
  document.body.appendChild(panel);
379
730
  // Reactivity: Update Visibility
380
- effect(() => {
731
+ effect `lineage-panel:visibility`(() => {
381
732
  panel.style.opacity = state.isVisible ? '1' : '0';
382
733
  panel.style.pointerEvents = state.isVisible ? 'all' : 'none';
383
734
  panel.style.transform = state.isVisible ? 'translateY(0)' : 'translateY(20px)';
384
735
  });
385
736
  // Reactivity: Update Content
386
- effect(() => {
737
+ effect `lineage-panel:content`(() => {
387
738
  if (state.activeLineage.length === 0) {
388
739
  content.innerHTML = `<div style="color: ${colors.placeholderText}; font-style: italic; text-align: center; margin-top: 20px;">No effect triggered yet...</div>`;
389
740
  return;
@@ -425,6 +776,35 @@ function showLineagePanel() {
425
776
  });
426
777
  }
427
778
 
779
+ function getColors() {
780
+ const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
781
+ return {
782
+ accent: isDark ? '#b392f0' : '#6f42c1',
783
+ meta: isDark ? '#8b949e' : '#57606a',
784
+ };
785
+ }
786
+ function getReactiveTarget(obj) {
787
+ if (!obj || typeof obj !== 'object' || !isReactive(obj))
788
+ return undefined;
789
+ return unwrap(obj);
790
+ }
791
+ const reactiveFormatter = {
792
+ header: (obj) => {
793
+ const target = getReactiveTarget(obj);
794
+ if (!target)
795
+ return null;
796
+ const colors = getColors();
797
+ return [
798
+ 'span',
799
+ {},
800
+ ['span', { style: `color: ${colors.accent}; font-weight: bold;` }, 'Reactive<'],
801
+ ['object', { object: target }],
802
+ ['span', { style: `color: ${colors.accent}; font-weight: bold;` }, '>'],
803
+ ];
804
+ },
805
+ hasBody: () => false,
806
+ };
807
+
428
808
  /**
429
809
  * Debug utilities for the reactivity system
430
810
  * - Captures effect metadata (names, parent relationships)
@@ -755,7 +1135,6 @@ function buildReactivityGraph() {
755
1135
  },
756
1136
  };
757
1137
  }
758
- const captureLineage = () => wrapLineageForDebug(getLineage());
759
1138
  /**
760
1139
  * Enables the DevTools bridge and exposes the debug API on window/global.
761
1140
  * Call as early as possible in development builds.
@@ -782,25 +1161,38 @@ function enableDevTools() {
782
1161
  logLineage(getLineage());
783
1162
  return '🦴 Effect Lineage Trace' + (tag ? ` (${tag})` : '');
784
1163
  },
785
- get browserLineage() {
786
- return captureLineage();
1164
+ logReason(reason, tag) {
1165
+ const reasonToLog = reason ?? globalScope.__MUTTS_DEBUG__.reason;
1166
+ if (!reasonToLog)
1167
+ return '🧹 Cleanup Reason';
1168
+ return logReason(reasonToLog, tag);
1169
+ },
1170
+ get lineage() {
1171
+ return getLineage();
787
1172
  },
788
- getLineage,
789
- captureLineage: getStackFrame,
790
- formatLineage,
791
1173
  showLineagePanel,
792
1174
  setEffectName,
793
1175
  setObjectName,
794
1176
  registerEffect: registerEffectForDebug,
795
1177
  registerObject: registerObjectForDebug,
1178
+ /**
1179
+ * Returns the reason why the current effect is being executed.
1180
+ * Returns undefined if not in an effect or if this is the first run.
1181
+ * @type {() => import('../src/reactive/types').CleanupReason | undefined}
1182
+ */
1183
+ get reason() {
1184
+ const activeEffect = getActiveEffect();
1185
+ if (!activeEffect)
1186
+ return undefined;
1187
+ const node = getEffectNode(activeEffect);
1188
+ return node?.currentReason;
1189
+ },
796
1190
  };
797
1191
  // @ts-ignore - devtoolsFormatters is a Chrome-specific array
798
- if (globalScope.devtoolsFormatters) {
799
- globalScope.devtoolsFormatters.push(lineageFormatter);
800
- }
801
- else {
802
- globalScope.devtoolsFormatters = [lineageFormatter];
803
- }
1192
+ if (globalScope.devtoolsFormatters)
1193
+ globalScope.devtoolsFormatters.push(lineageFormatter, reasonFormatter, reactiveFormatter);
1194
+ else
1195
+ globalScope.devtoolsFormatters = [lineageFormatter, reasonFormatter, reactiveFormatter];
804
1196
  setDebugHooks({
805
1197
  isDevtoolsEnabled: () => devtoolsEnabled,
806
1198
  registerEffect: registerEffectForDebug,
@@ -808,6 +1200,8 @@ function enableDevTools() {
808
1200
  captureStack: getStackFrame,
809
1201
  captureLineage,
810
1202
  formatStack: (stack) => {
1203
+ if (isCleanupReason(stack))
1204
+ return [stack];
811
1205
  if (typeof stack === 'string') {
812
1206
  return [
813
1207
  `\n ${stack
@@ -829,8 +1223,7 @@ function enableDevTools() {
829
1223
  typeof error === 'object' &&
830
1224
  'stack' in error &&
831
1225
  !('lineage' in error)) {
832
- const lineage = getLineage(trigger, getStackFrame(error));
833
- error.lineage = wrapLineageForDebug(lineage);
1226
+ error.lineage = wrapLineageForDebug(getLineage(trigger, error));
834
1227
  }
835
1228
  },
836
1229
  });
@@ -952,6 +1345,7 @@ function isDevelopmentMode() {
952
1345
  }
953
1346
  // Auto-enable devtools when the module loads in development
954
1347
  if (isDevelopmentMode() && !devtoolsEnabled) {
1348
+ Object.assign(options, devPreset);
955
1349
  enableDevTools();
956
1350
  // Optional: Log that devtools were enabled (only in development)
957
1351
  if (typeof console !== 'undefined' && console.info) {
@@ -960,5 +1354,5 @@ if (isDevelopmentMode() && !devtoolsEnabled) {
960
1354
  }
961
1355
  }
962
1356
 
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 };
1357
+ 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
1358
  //# sourceMappingURL=debug.esm.js.map