mutts 1.0.8 → 1.0.10
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.
- package/README.md +77 -35
- package/dist/browser.cjs +2144 -5355
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +2 -1655
- package/dist/browser.dev.cjs +110 -0
- package/dist/browser.dev.cjs.map +1 -0
- package/dist/browser.dev.d.ts +2 -0
- package/dist/browser.dev.esm.js +5 -0
- package/dist/browser.dev.esm.js.map +1 -0
- package/dist/browser.esm.js +20 -107
- package/dist/browser.esm.js.map +1 -1
- package/dist/chunks/{async-browser-CA0jPWIi.cjs → async-browser-BU_IfxYD.cjs} +19 -107
- package/dist/chunks/async-browser-BU_IfxYD.cjs.map +1 -0
- package/dist/chunks/{async-core-UqHzvJ-S.cjs → async-core-CRLKP3l-.cjs} +8 -4
- package/dist/chunks/async-core-CRLKP3l-.cjs.map +1 -0
- package/dist/chunks/{async-node-BYHuGTni.cjs → async-node-3PrbVAbB.cjs} +7 -12
- package/dist/chunks/async-node-3PrbVAbB.cjs.map +1 -0
- package/dist/chunks/index-CaaQQlPJ.esm.js +3174 -0
- package/dist/chunks/index-CaaQQlPJ.esm.js.map +1 -0
- package/dist/chunks/node-nKJBk8iJ.esm.js +96 -0
- package/dist/chunks/node-nKJBk8iJ.esm.js.map +1 -0
- package/dist/chunks/proxy-Dtg-bJ3T.cjs +3763 -0
- package/dist/chunks/proxy-Dtg-bJ3T.cjs.map +1 -0
- package/dist/chunks/proxy-r7lARftl.esm.js +3701 -0
- package/dist/chunks/proxy-r7lARftl.esm.js.map +1 -0
- package/dist/debug.cjs +980 -0
- package/dist/debug.cjs.map +1 -0
- package/dist/debug.d.ts +208 -0
- package/dist/debug.esm.js +950 -0
- package/dist/debug.esm.js.map +1 -0
- package/dist/devtools/manifest.json +1 -1
- package/dist/devtools/panel.html +1 -1
- package/dist/devtools/panel.js +107 -94
- package/dist/devtools/panel.js.map +1 -1
- package/dist/index.d.ts +1447 -13
- package/dist/mutts.umd.js +7167 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/node.cjs +70 -66
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.dev.cjs +110 -0
- package/dist/node.dev.cjs.map +1 -0
- package/dist/node.dev.d.ts +2 -0
- package/dist/node.dev.esm.js +6 -0
- package/dist/node.dev.esm.js.map +1 -0
- package/dist/node.esm.js +4 -103
- package/dist/node.esm.js.map +1 -1
- package/dist/{reactive/types.d.ts → types-W5vD6m2n.d.ts} +186 -93
- package/docs/ai/api-reference.md +12 -19
- package/docs/ai/manual.md +521 -17
- package/docs/events.md +57 -2
- package/docs/flavored.md +210 -0
- package/docs/reactive/advanced.md +314 -5
- package/docs/reactive/attend.md +142 -0
- package/docs/reactive/collections.md +76 -39
- package/docs/reactive/core.md +100 -14
- package/docs/reactive/debugging.md +123 -18
- package/docs/reactive/error-handling.md +121 -0
- package/docs/reactive/resource.md +125 -0
- package/docs/reactive/scan.md +247 -1
- package/docs/reactive.md +4 -1
- package/docs/std-decorators.md +102 -51
- package/docs/utils.md +70 -0
- package/docs/zone.md +3 -7
- package/package.json +64 -30
- package/dist/async/browser.d.ts +0 -2
- package/dist/async/browser.d.ts.map +0 -1
- package/dist/async/index.d.ts +0 -18
- package/dist/async/index.d.ts.map +0 -1
- package/dist/async/node.d.ts +0 -2
- package/dist/async/node.d.ts.map +0 -1
- package/dist/chunks/async-browser-CA0jPWIi.cjs.map +0 -1
- package/dist/chunks/async-core-UqHzvJ-S.cjs.map +0 -1
- package/dist/chunks/async-node-BYHuGTni.cjs.map +0 -1
- package/dist/chunks/index-DhaOVusv.esm.js +0 -6411
- package/dist/chunks/index-DhaOVusv.esm.js.map +0 -1
- package/dist/decorator.d.ts +0 -106
- package/dist/decorator.d.ts.map +0 -1
- package/dist/destroyable.d.ts +0 -87
- package/dist/destroyable.d.ts.map +0 -1
- package/dist/entry-browser.d.ts +0 -3
- package/dist/entry-browser.d.ts.map +0 -1
- package/dist/entry-node.d.ts +0 -3
- package/dist/entry-node.d.ts.map +0 -1
- package/dist/eventful.d.ts +0 -18
- package/dist/eventful.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/indexable.d.ts +0 -243
- package/dist/indexable.d.ts.map +0 -1
- package/dist/introspection.d.ts +0 -27
- package/dist/introspection.d.ts.map +0 -1
- package/dist/iterableWeak.d.ts +0 -53
- package/dist/iterableWeak.d.ts.map +0 -1
- package/dist/mixins.d.ts +0 -25
- package/dist/mixins.d.ts.map +0 -1
- package/dist/promiseChain.d.ts +0 -20
- package/dist/promiseChain.d.ts.map +0 -1
- package/dist/reactive/array.d.ts +0 -49
- package/dist/reactive/array.d.ts.map +0 -1
- package/dist/reactive/buffer.d.ts +0 -44
- package/dist/reactive/buffer.d.ts.map +0 -1
- package/dist/reactive/change.d.ts +0 -29
- package/dist/reactive/change.d.ts.map +0 -1
- package/dist/reactive/debug.d.ts +0 -111
- package/dist/reactive/debug.d.ts.map +0 -1
- package/dist/reactive/deep-touch.d.ts +0 -28
- package/dist/reactive/deep-touch.d.ts.map +0 -1
- package/dist/reactive/deep-watch-state.d.ts +0 -25
- package/dist/reactive/deep-watch-state.d.ts.map +0 -1
- package/dist/reactive/deep-watch.d.ts +0 -19
- package/dist/reactive/deep-watch.d.ts.map +0 -1
- package/dist/reactive/effect-context.d.ts +0 -7
- package/dist/reactive/effect-context.d.ts.map +0 -1
- package/dist/reactive/effects.d.ts +0 -151
- package/dist/reactive/effects.d.ts.map +0 -1
- package/dist/reactive/index.d.ts +0 -20
- package/dist/reactive/index.d.ts.map +0 -1
- package/dist/reactive/interface.d.ts +0 -64
- package/dist/reactive/interface.d.ts.map +0 -1
- package/dist/reactive/map.d.ts +0 -30
- package/dist/reactive/map.d.ts.map +0 -1
- package/dist/reactive/memoize.d.ts +0 -5
- package/dist/reactive/memoize.d.ts.map +0 -1
- package/dist/reactive/non-reactive-state.d.ts +0 -9
- package/dist/reactive/non-reactive-state.d.ts.map +0 -1
- package/dist/reactive/non-reactive.d.ts +0 -11
- package/dist/reactive/non-reactive.d.ts.map +0 -1
- package/dist/reactive/project.d.ts +0 -41
- package/dist/reactive/project.d.ts.map +0 -1
- package/dist/reactive/proxy-state.d.ts +0 -8
- package/dist/reactive/proxy-state.d.ts.map +0 -1
- package/dist/reactive/proxy.d.ts +0 -23
- package/dist/reactive/proxy.d.ts.map +0 -1
- package/dist/reactive/record.d.ts +0 -116
- package/dist/reactive/record.d.ts.map +0 -1
- package/dist/reactive/register.d.ts +0 -64
- package/dist/reactive/register.d.ts.map +0 -1
- package/dist/reactive/registry.d.ts +0 -20
- package/dist/reactive/registry.d.ts.map +0 -1
- package/dist/reactive/set.d.ts +0 -28
- package/dist/reactive/set.d.ts.map +0 -1
- package/dist/reactive/tracking.d.ts +0 -7
- package/dist/reactive/tracking.d.ts.map +0 -1
- package/dist/reactive/types.d.ts.map +0 -1
- package/dist/std-decorators.d.ts +0 -50
- package/dist/std-decorators.d.ts.map +0 -1
- package/dist/utils.d.ts +0 -49
- package/dist/utils.d.ts.map +0 -1
- package/dist/zone.d.ts +0 -40
- package/dist/zone.d.ts.map +0 -1
- package/docs/reactive/project.md +0 -93
- package/src/async/browser.ts +0 -319
- package/src/async/index.ts +0 -23
- package/src/async/node.ts +0 -104
- package/src/decorator.ts +0 -272
- package/src/destroyable.ts +0 -199
- package/src/entry-browser.ts +0 -5
- package/src/entry-node.ts +0 -5
- package/src/eventful.ts +0 -77
- package/src/index.d.ts +0 -12
- package/src/index.ts +0 -64
- package/src/indexable.ts +0 -526
- package/src/introspection.ts +0 -59
- package/src/iterableWeak.ts +0 -233
- package/src/mixins.ts +0 -123
- package/src/promiseChain.ts +0 -110
- package/src/reactive/array.ts +0 -507
- package/src/reactive/buffer.ts +0 -168
- package/src/reactive/change.ts +0 -138
- package/src/reactive/debug.ts +0 -517
- package/src/reactive/deep-touch.ts +0 -268
- package/src/reactive/deep-watch-state.ts +0 -82
- package/src/reactive/deep-watch.ts +0 -168
- package/src/reactive/effect-context.ts +0 -18
- package/src/reactive/effects.ts +0 -1382
- package/src/reactive/index.ts +0 -75
- package/src/reactive/interface.ts +0 -210
- package/src/reactive/map.ts +0 -158
- package/src/reactive/memoize.ts +0 -163
- package/src/reactive/non-reactive-state.ts +0 -49
- package/src/reactive/non-reactive.ts +0 -43
- package/src/reactive/project.ts +0 -453
- package/src/reactive/proxy-state.ts +0 -27
- package/src/reactive/proxy.ts +0 -264
- package/src/reactive/record.ts +0 -196
- package/src/reactive/register.ts +0 -419
- package/src/reactive/registry.ts +0 -59
- package/src/reactive/set.ts +0 -130
- package/src/reactive/tracking.ts +0 -44
- package/src/reactive/types.ts +0 -440
- package/src/std-decorators.ts +0 -217
- package/src/utils.ts +0 -301
- package/src/zone.ts +0 -138
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
import { a5 as getRoot, au as effectMarker, B as getActiveEffect, av as getEffectNode, T as reactive, u as effect, U as options, aw as setDebugHooks, at as effectToReactiveObjects, ax as allProps } from './chunks/proxy-r7lARftl.esm.js';
|
|
2
|
+
|
|
3
|
+
const lineageObjects = new WeakSet();
|
|
4
|
+
/**
|
|
5
|
+
* Checks if an object is a lineage object
|
|
6
|
+
*/
|
|
7
|
+
function isLineage(obj) {
|
|
8
|
+
return obj && typeof obj === 'object' && lineageObjects.has(obj);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parses a single stack line into a structured frame.
|
|
12
|
+
* @param line - A line from Error.stack
|
|
13
|
+
*/
|
|
14
|
+
function parseStackLine(line) {
|
|
15
|
+
// Node.js format: " at functionName (file:line:column)"
|
|
16
|
+
// or " at file:line:column"
|
|
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
|
+
const browserMatch = line.match(/(?:(.+?)(?:\@|\(?))?(?:(.+?):(\d+):(\d+))(?:\@|\)?)$/);
|
|
21
|
+
const match = nodeMatch || browserMatch;
|
|
22
|
+
if (!match)
|
|
23
|
+
return null;
|
|
24
|
+
const [, functionName = 'anonymous', fileName, lineNumber, columnNumber] = match;
|
|
25
|
+
return {
|
|
26
|
+
functionName: functionName.trim(),
|
|
27
|
+
fileName,
|
|
28
|
+
lineNumber: parseInt(lineNumber, 10),
|
|
29
|
+
columnNumber: parseInt(columnNumber, 10),
|
|
30
|
+
raw: line.trim(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
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)
|
|
41
|
+
return [];
|
|
42
|
+
const lines = error.stack.split('\n');
|
|
43
|
+
const lastLine = lines.findIndex((line) => line.includes(effectMarker.enter));
|
|
44
|
+
if (lastLine !== -1)
|
|
45
|
+
lines.splice(lastLine);
|
|
46
|
+
const firstLine = lines.findLastIndex((line) => line.includes(effectMarker.leave));
|
|
47
|
+
if (firstLine !== -1)
|
|
48
|
+
lines.splice(0, firstLine + 1);
|
|
49
|
+
else {
|
|
50
|
+
// Dynamically identify the library's internal files if not already done
|
|
51
|
+
if (!internalFile && lines[1]) {
|
|
52
|
+
const selfFrame = parseStackLine(lines[1]);
|
|
53
|
+
if (selfFrame) {
|
|
54
|
+
internalFile = selfFrame.fileName;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
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++) {
|
|
66
|
+
const frame = parseStackLine(lines[l]);
|
|
67
|
+
if (!frame)
|
|
68
|
+
continue;
|
|
69
|
+
// Robust skipping: if we are still in the internal area, skip it.
|
|
70
|
+
const isInternal = /Lineage$/.test(frame.functionName) ||
|
|
71
|
+
frame.functionName === 'eval' ||
|
|
72
|
+
[`getStackFrame`, `captureLineage`].includes(frame.functionName);
|
|
73
|
+
if (!isInternal)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
lines.splice(0, l);
|
|
77
|
+
}
|
|
78
|
+
return lines.map(parseStackLine).filter(Boolean);
|
|
79
|
+
}
|
|
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) {
|
|
85
|
+
const currentEffect = effect ?? getActiveEffect();
|
|
86
|
+
const currentStack = getStackFrame(); // Robustly skips internal mutts frames
|
|
87
|
+
const segments = [];
|
|
88
|
+
if (!currentEffect) {
|
|
89
|
+
segments.push({
|
|
90
|
+
effect: undefined,
|
|
91
|
+
effectName: 'root',
|
|
92
|
+
stack: filterNodeModules(currentStack),
|
|
93
|
+
});
|
|
94
|
+
return segments;
|
|
95
|
+
}
|
|
96
|
+
let current = currentEffect;
|
|
97
|
+
let lastStack = currentStack;
|
|
98
|
+
while (current) {
|
|
99
|
+
const rootFn = getRoot(current);
|
|
100
|
+
// Too aggressive for now
|
|
101
|
+
//if(!rootFn.name) debugger
|
|
102
|
+
const filteredStack = filterNodeModules(lastStack);
|
|
103
|
+
segments.push({
|
|
104
|
+
effect: rootFn,
|
|
105
|
+
effectName: rootFn.name || 'anonymous',
|
|
106
|
+
stack: filteredStack,
|
|
107
|
+
});
|
|
108
|
+
// Move to parent
|
|
109
|
+
const node = getEffectNode(current);
|
|
110
|
+
const parent = node.parent;
|
|
111
|
+
const creationStack = node.creationStack;
|
|
112
|
+
if (parent) {
|
|
113
|
+
current = parent;
|
|
114
|
+
lastStack = creationStack ?? [];
|
|
115
|
+
}
|
|
116
|
+
else if (creationStack) {
|
|
117
|
+
segments.push({
|
|
118
|
+
effect: undefined,
|
|
119
|
+
effectName: 'root',
|
|
120
|
+
stack: filterNodeModules(creationStack),
|
|
121
|
+
});
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return segments;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Filters out node_modules frames and groups them
|
|
132
|
+
* @param frames - Array of stack frames
|
|
133
|
+
*/
|
|
134
|
+
function filterNodeModules(frames) {
|
|
135
|
+
const result = [];
|
|
136
|
+
let inNodeModules = false;
|
|
137
|
+
for (const frame of frames) {
|
|
138
|
+
const isNodeModule = frame.fileName.includes('/node_modules/'); //|| frame.fileName.includes('/dist/')
|
|
139
|
+
if (isNodeModule && !inNodeModules) {
|
|
140
|
+
// Start of node_modules block
|
|
141
|
+
inNodeModules = true;
|
|
142
|
+
result.push({
|
|
143
|
+
functionName: '...node_modules...',
|
|
144
|
+
fileName: '[filtered]',
|
|
145
|
+
lineNumber: 0,
|
|
146
|
+
columnNumber: 0,
|
|
147
|
+
raw: ' at ...node_modules...'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else if (!isNodeModule && inNodeModules) {
|
|
151
|
+
// End of node_modules block
|
|
152
|
+
inNodeModules = false;
|
|
153
|
+
result.push(frame);
|
|
154
|
+
}
|
|
155
|
+
else if (!isNodeModule) {
|
|
156
|
+
// Regular frame
|
|
157
|
+
result.push(frame);
|
|
158
|
+
}
|
|
159
|
+
// Skip frames inside node_modules
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Formats lineage segments into a single stack-like string
|
|
165
|
+
* @param segments - Lineage segments
|
|
166
|
+
*/
|
|
167
|
+
function formatLineage(segments) {
|
|
168
|
+
const result = [];
|
|
169
|
+
for (let i = 0; i < segments.length; i++) {
|
|
170
|
+
const segment = segments[i];
|
|
171
|
+
if (i > 0) {
|
|
172
|
+
const triggerEffect = segments[i - 1].effect;
|
|
173
|
+
const triggerName = triggerEffect ? (getRoot(triggerEffect).name || 'anonymous') : 'root';
|
|
174
|
+
result.push(` --- effect: ${triggerName} ---`);
|
|
175
|
+
}
|
|
176
|
+
for (const frame of segment.stack) {
|
|
177
|
+
result.push(` ${frame.raw}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return result.join('\n');
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Logs lineage segments to console with grouping
|
|
184
|
+
* @param segments - Lineage segments
|
|
185
|
+
*/
|
|
186
|
+
function logLineage(segments) {
|
|
187
|
+
console.groupCollapsed(`🦴 Effect Lineage Trace (${segments.length} segment${segments.length === 1 ? '' : 's'})`);
|
|
188
|
+
for (let i = 0; i < segments.length; i++) {
|
|
189
|
+
const segment = segments[i];
|
|
190
|
+
// Add segment header
|
|
191
|
+
const isLast = i === segments.length - 1;
|
|
192
|
+
const prefix = i === 0 ? '📍' : isLast ? '└─' : '├─';
|
|
193
|
+
console.groupCollapsed(`${prefix} Effect:`, segment.effect ? segment.effect.name || 'anonymous' : 'root');
|
|
194
|
+
// Add stack frames
|
|
195
|
+
for (let j = 0; j < segment.stack.length; j++) {
|
|
196
|
+
const frame = segment.stack[j];
|
|
197
|
+
const isLastFrame = j === segment.stack.length - 1;
|
|
198
|
+
const framePrefix = isLastFrame ? '└─' : '├─';
|
|
199
|
+
if (frame.functionName === '...node_modules...') {
|
|
200
|
+
console.log(`%c${framePrefix} ${frame.functionName}`, 'color: #888; font-style: italic;');
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const fnStyle = frame.functionName === 'anonymous' ? 'color: #888;' : 'color: #1a7f37; font-weight: bold;';
|
|
204
|
+
const fileStyle = 'color: #0550ae;';
|
|
205
|
+
console.log(`%c${framePrefix} %c${frame.functionName} %c(${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})`, 'color: #888;', fnStyle, fileStyle);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.groupEnd();
|
|
209
|
+
}
|
|
210
|
+
console.groupEnd();
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Captures and logs lineage to console
|
|
214
|
+
*/
|
|
215
|
+
function captureNodeLineage() {
|
|
216
|
+
logLineage(getLineage());
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Custom formatter for Chrome DevTools to render lineage data nicely.
|
|
220
|
+
*/
|
|
221
|
+
const lineageFormatter = {
|
|
222
|
+
header: (obj) => {
|
|
223
|
+
if (isLineage(obj)) {
|
|
224
|
+
// Try to detect DevTools theme - default to dark colors if uncertain
|
|
225
|
+
const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
|
226
|
+
const headerColor = isDark ? '#cd9d5d' : '#704214';
|
|
227
|
+
return [
|
|
228
|
+
'span',
|
|
229
|
+
{ style: `color: ${headerColor}; font-weight: bold;` },
|
|
230
|
+
`🦴 Effect Lineage (${obj.segments.length} segments)`,
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
},
|
|
235
|
+
hasBody: (obj) => isLineage(obj),
|
|
236
|
+
body: (obj) => {
|
|
237
|
+
if (!isLineage(obj))
|
|
238
|
+
return null;
|
|
239
|
+
const segments = obj.segments;
|
|
240
|
+
// Try to detect DevTools theme
|
|
241
|
+
const isDark = typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
|
242
|
+
const colors = isDark ? {
|
|
243
|
+
frameText: '#ccc',
|
|
244
|
+
functionName: '#ffffff',
|
|
245
|
+
segmentBg: '#2d2d2d',
|
|
246
|
+
} : {
|
|
247
|
+
frameText: '#555',
|
|
248
|
+
functionName: '#222',
|
|
249
|
+
segmentBg: '#eee',
|
|
250
|
+
};
|
|
251
|
+
const children = segments.map((segment, i) => {
|
|
252
|
+
const frames = segment.stack.map((frame) => [
|
|
253
|
+
'div',
|
|
254
|
+
{ style: `margin-left: 20px; color: ${colors.frameText}; font-family: monospace; font-size: 11px;` },
|
|
255
|
+
['span', { style: `color: ${colors.functionName};` }, `at ${frame.functionName} `],
|
|
256
|
+
['span', {}, `${frame.fileName}:${frame.lineNumber}:${frame.columnNumber}`],
|
|
257
|
+
]);
|
|
258
|
+
segment.effect;
|
|
259
|
+
const segmentHeader = [
|
|
260
|
+
'div',
|
|
261
|
+
{ style: `margin-top: 5px; padding: 2px 5px; background: ${colors.segmentBg}; border-radius: 3px; font-weight: bold;` },
|
|
262
|
+
i === 0 ? '📍 Current: ' : '↖ Effect: ',
|
|
263
|
+
segment.effect?.name || 'anonymous',
|
|
264
|
+
segment.effect ? ['object', { object: segment.effect }] : 'root',
|
|
265
|
+
];
|
|
266
|
+
return ['div', {}, segmentHeader, ...frames];
|
|
267
|
+
});
|
|
268
|
+
return ['div', { style: 'padding: 5px; line-height: 1.4;' }, ...children];
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Wraps lineage data in a way that the Chrome Formatter can recognize.
|
|
273
|
+
*/
|
|
274
|
+
function wrapLineageForDebug(segments) {
|
|
275
|
+
const lineage = {
|
|
276
|
+
segments,
|
|
277
|
+
toString: () => formatLineage(segments ?? getLineage()),
|
|
278
|
+
};
|
|
279
|
+
lineageObjects.add(lineage);
|
|
280
|
+
return lineage;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Displays a floating reactive panel in the browser showing the lineage of triggered effects.
|
|
285
|
+
*/
|
|
286
|
+
function showLineagePanel() {
|
|
287
|
+
if (typeof document === 'undefined')
|
|
288
|
+
return;
|
|
289
|
+
// Detect color scheme preference
|
|
290
|
+
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
291
|
+
// Color schemes
|
|
292
|
+
const colors = isDarkMode ? {
|
|
293
|
+
panelBg: 'rgba(30, 30, 30, 0.95)',
|
|
294
|
+
panelBorder: '#444',
|
|
295
|
+
headerBg: '#8B4513',
|
|
296
|
+
headerText: '#ffffff',
|
|
297
|
+
closeBtn: '#ffffff',
|
|
298
|
+
placeholderText: '#999',
|
|
299
|
+
segmentBorder: '#8B4513',
|
|
300
|
+
titleText: '#ffffff',
|
|
301
|
+
frameText: '#ccc',
|
|
302
|
+
atText: '#888',
|
|
303
|
+
linkText: '#58a6ff',
|
|
304
|
+
} : {
|
|
305
|
+
panelBg: 'rgba(255, 255, 255, 0.95)',
|
|
306
|
+
panelBorder: '#ddd',
|
|
307
|
+
headerBg: '#704214',
|
|
308
|
+
headerText: '#ffffff',
|
|
309
|
+
closeBtn: '#ffffff',
|
|
310
|
+
placeholderText: '#666',
|
|
311
|
+
segmentBorder: '#704214',
|
|
312
|
+
titleText: '#222',
|
|
313
|
+
frameText: '#555',
|
|
314
|
+
atText: '#999',
|
|
315
|
+
linkText: '#005cc5',
|
|
316
|
+
};
|
|
317
|
+
const state = reactive({
|
|
318
|
+
activeLineage: [],
|
|
319
|
+
isVisible: true,
|
|
320
|
+
});
|
|
321
|
+
// onEffectTrigger tracks "this effect" (here nothing) — not useful for a global panel.
|
|
322
|
+
// Would need zone-based events to track all triggers. Panel is cosmetic/experimental (260206).
|
|
323
|
+
// UI Creation
|
|
324
|
+
const panel = document.createElement('div');
|
|
325
|
+
panel.id = 'mutts-lineage-panel';
|
|
326
|
+
Object.assign(panel.style, {
|
|
327
|
+
position: 'fixed',
|
|
328
|
+
bottom: '20px',
|
|
329
|
+
right: '20px',
|
|
330
|
+
width: '400px',
|
|
331
|
+
maxHeight: '80vh',
|
|
332
|
+
backgroundColor: colors.panelBg,
|
|
333
|
+
backdropFilter: 'blur(10px)',
|
|
334
|
+
border: `1px solid ${colors.panelBorder}`,
|
|
335
|
+
borderRadius: '12px',
|
|
336
|
+
boxShadow: '0 10px 25px rgba(0,0,0,0.15)',
|
|
337
|
+
zIndex: '999999',
|
|
338
|
+
fontFamily: 'Inter, system-ui, sans-serif',
|
|
339
|
+
fontSize: '12px',
|
|
340
|
+
display: 'flex',
|
|
341
|
+
flexDirection: 'column',
|
|
342
|
+
overflow: 'hidden',
|
|
343
|
+
transition: 'opacity 0.3s, transform 0.3s',
|
|
344
|
+
});
|
|
345
|
+
const header = document.createElement('div');
|
|
346
|
+
Object.assign(header.style, {
|
|
347
|
+
padding: '12px 16px',
|
|
348
|
+
background: colors.headerBg,
|
|
349
|
+
color: colors.headerText,
|
|
350
|
+
fontWeight: '600',
|
|
351
|
+
display: 'flex',
|
|
352
|
+
justifyContent: 'space-between',
|
|
353
|
+
alignItems: 'center',
|
|
354
|
+
cursor: 'grab',
|
|
355
|
+
});
|
|
356
|
+
header.innerHTML = '<span><span style="font-size: 14px; margin-right: 5px;">🦴</span> Mutts Lineage</span>';
|
|
357
|
+
const closeBtn = document.createElement('button');
|
|
358
|
+
closeBtn.innerText = '×';
|
|
359
|
+
Object.assign(closeBtn.style, {
|
|
360
|
+
background: 'none',
|
|
361
|
+
border: 'none',
|
|
362
|
+
color: colors.closeBtn,
|
|
363
|
+
fontSize: '20px',
|
|
364
|
+
cursor: 'pointer',
|
|
365
|
+
padding: '0 5px',
|
|
366
|
+
});
|
|
367
|
+
closeBtn.onclick = () => {
|
|
368
|
+
state.isVisible = false;
|
|
369
|
+
};
|
|
370
|
+
header.appendChild(closeBtn);
|
|
371
|
+
panel.appendChild(header);
|
|
372
|
+
const content = document.createElement('div');
|
|
373
|
+
Object.assign(content.style, {
|
|
374
|
+
padding: '16px',
|
|
375
|
+
overflowY: 'auto',
|
|
376
|
+
flex: '1',
|
|
377
|
+
});
|
|
378
|
+
panel.appendChild(content);
|
|
379
|
+
document.body.appendChild(panel);
|
|
380
|
+
// Reactivity: Update Visibility
|
|
381
|
+
effect(() => {
|
|
382
|
+
panel.style.opacity = state.isVisible ? '1' : '0';
|
|
383
|
+
panel.style.pointerEvents = state.isVisible ? 'all' : 'none';
|
|
384
|
+
panel.style.transform = state.isVisible ? 'translateY(0)' : 'translateY(20px)';
|
|
385
|
+
});
|
|
386
|
+
// Reactivity: Update Content
|
|
387
|
+
effect(() => {
|
|
388
|
+
if (state.activeLineage.length === 0) {
|
|
389
|
+
content.innerHTML = `<div style="color: ${colors.placeholderText}; font-style: italic; text-align: center; margin-top: 20px;">No effect triggered yet...</div>`;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
content.innerHTML = '';
|
|
393
|
+
state.activeLineage.forEach((segment, i) => {
|
|
394
|
+
const segDiv = document.createElement('div');
|
|
395
|
+
Object.assign(segDiv.style, {
|
|
396
|
+
marginBottom: '16px',
|
|
397
|
+
borderLeft: `2px solid ${colors.segmentBorder}`,
|
|
398
|
+
paddingLeft: '12px',
|
|
399
|
+
});
|
|
400
|
+
const title = document.createElement('div');
|
|
401
|
+
Object.assign(title.style, {
|
|
402
|
+
fontWeight: 'bold',
|
|
403
|
+
marginBottom: '6px',
|
|
404
|
+
color: colors.titleText,
|
|
405
|
+
});
|
|
406
|
+
const effectName = segment.effect ? (getRoot(segment.effect).name || 'anonymous') : 'root';
|
|
407
|
+
title.innerText = i === 0 ? `📍 Current: ${effectName}` : `↖ Effect: ${effectName}`;
|
|
408
|
+
segDiv.appendChild(title);
|
|
409
|
+
segment.stack.forEach(frame => {
|
|
410
|
+
const frameDiv = document.createElement('div');
|
|
411
|
+
Object.assign(frameDiv.style, {
|
|
412
|
+
color: colors.frameText,
|
|
413
|
+
marginBottom: '3px',
|
|
414
|
+
fontSize: '11px',
|
|
415
|
+
whiteSpace: 'nowrap',
|
|
416
|
+
overflow: 'hidden',
|
|
417
|
+
textOverflow: 'ellipsis',
|
|
418
|
+
cursor: 'pointer',
|
|
419
|
+
});
|
|
420
|
+
frameDiv.title = frame.raw;
|
|
421
|
+
frameDiv.innerHTML = `<span style="color: ${colors.atText}">at</span> ${frame.functionName} <span style="color: ${colors.linkText}; text-decoration: underline;">(${frame.fileName.split('/').pop()}:${frame.lineNumber})</span>`;
|
|
422
|
+
segDiv.appendChild(frameDiv);
|
|
423
|
+
});
|
|
424
|
+
content.appendChild(segDiv);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Debug utilities for the reactivity system
|
|
431
|
+
* - Captures effect metadata (names, parent relationships)
|
|
432
|
+
* - Records cause → consequence edges with object/prop labels
|
|
433
|
+
* - Provides graph data for tooling (DevTools panel, etc.)
|
|
434
|
+
*/
|
|
435
|
+
/**
|
|
436
|
+
* Log an error with detailed context if error logging is enabled
|
|
437
|
+
*/
|
|
438
|
+
function logError(error, context, effect) {
|
|
439
|
+
if (!options.introspection?.logErrors)
|
|
440
|
+
return;
|
|
441
|
+
const contextStr = context === 'throw' ? '🔴 Thrown' : '🟡 Caught';
|
|
442
|
+
console.group(`${contextStr} Error: ${error.message}`);
|
|
443
|
+
console.error('Error:', error);
|
|
444
|
+
// Try to get effect name if available
|
|
445
|
+
if (effect && typeof effect === 'object' && effect.name) {
|
|
446
|
+
console.log('Effect:', effect.name);
|
|
447
|
+
}
|
|
448
|
+
console.groupEnd();
|
|
449
|
+
}
|
|
450
|
+
// Log initialization for debugging
|
|
451
|
+
const EXTERNAL_SOURCE = Symbol('external-source');
|
|
452
|
+
let devtoolsEnabled = false;
|
|
453
|
+
// Registry for debugging (populated lazily when DevTools are enabled)
|
|
454
|
+
const debugEffectRegistry = new Set();
|
|
455
|
+
const debugObjectRegistry = new Set();
|
|
456
|
+
// Human-friendly names
|
|
457
|
+
const effectNames = new WeakMap();
|
|
458
|
+
const objectNames = new WeakMap();
|
|
459
|
+
let effectCounter = 0;
|
|
460
|
+
let objectCounter = 0;
|
|
461
|
+
const triggerGraph = new Map();
|
|
462
|
+
function ensureEffectName(effect) {
|
|
463
|
+
let name = effectNames.get(effect);
|
|
464
|
+
if (!name) {
|
|
465
|
+
const root = getRoot(effect);
|
|
466
|
+
name = root?.name?.trim() || `effect_${++effectCounter}`;
|
|
467
|
+
}
|
|
468
|
+
effectNames.set(effect, name);
|
|
469
|
+
return name;
|
|
470
|
+
}
|
|
471
|
+
function ensureObjectName(obj) {
|
|
472
|
+
let name = objectNames.get(obj);
|
|
473
|
+
if (!name) {
|
|
474
|
+
// Try Symbol.toStringTag first (standard way to customize object string representation)
|
|
475
|
+
const toStringTag = obj?.[Symbol.toStringTag];
|
|
476
|
+
const ctorName = obj?.constructor?.name;
|
|
477
|
+
const base = toStringTag || (ctorName && ctorName !== 'Object' ? ctorName : 'object');
|
|
478
|
+
name = `${base}_${++objectCounter}`;
|
|
479
|
+
objectNames.set(obj, name);
|
|
480
|
+
}
|
|
481
|
+
return name;
|
|
482
|
+
}
|
|
483
|
+
function describeProp(obj, prop) {
|
|
484
|
+
const objectName = ensureObjectName(obj);
|
|
485
|
+
if (prop === allProps)
|
|
486
|
+
return `${objectName}.*`;
|
|
487
|
+
if (typeof prop === 'symbol')
|
|
488
|
+
return `${objectName}.${prop.description ?? prop.toString()}`;
|
|
489
|
+
return `${objectName}.${String(prop)}`;
|
|
490
|
+
}
|
|
491
|
+
function addEffectToRegistry(effect) {
|
|
492
|
+
if (!effect || debugEffectRegistry.has(effect))
|
|
493
|
+
return;
|
|
494
|
+
debugEffectRegistry.add(effect);
|
|
495
|
+
const deps = effectToReactiveObjects.get(effect);
|
|
496
|
+
if (deps) {
|
|
497
|
+
for (const obj of deps) {
|
|
498
|
+
documentObject(obj);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function documentObject(obj) {
|
|
503
|
+
if (!debugObjectRegistry.has(obj)) {
|
|
504
|
+
dbRegisterObject(obj);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function dbRegisterObject(obj) {
|
|
508
|
+
debugObjectRegistry.add(obj);
|
|
509
|
+
ensureObjectName(obj);
|
|
510
|
+
}
|
|
511
|
+
function ensureParentChains(effects) {
|
|
512
|
+
const queue = Array.from(effects);
|
|
513
|
+
for (let i = 0; i < queue.length; i++) {
|
|
514
|
+
const effect = queue[i];
|
|
515
|
+
const parent = getEffectNode(effect).parent;
|
|
516
|
+
if (parent && !effects.has(parent)) {
|
|
517
|
+
effects.add(parent);
|
|
518
|
+
queue.push(parent);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function ensureTriggerContainers(source) {
|
|
523
|
+
let targetMap = triggerGraph.get(source);
|
|
524
|
+
if (!targetMap) {
|
|
525
|
+
targetMap = new Map();
|
|
526
|
+
triggerGraph.set(source, targetMap);
|
|
527
|
+
}
|
|
528
|
+
return targetMap;
|
|
529
|
+
}
|
|
530
|
+
function ensureTriggerRecord(source, target, label, obj, prop, evolution) {
|
|
531
|
+
const targetMap = ensureTriggerContainers(source);
|
|
532
|
+
let labelMap = targetMap.get(target);
|
|
533
|
+
if (!labelMap) {
|
|
534
|
+
labelMap = new Map();
|
|
535
|
+
targetMap.set(target, labelMap);
|
|
536
|
+
}
|
|
537
|
+
let record = labelMap.get(label);
|
|
538
|
+
if (!record) {
|
|
539
|
+
record = { label, object: obj, prop, evolution, count: 0, lastTriggered: Date.now() };
|
|
540
|
+
labelMap.set(label, record);
|
|
541
|
+
}
|
|
542
|
+
return record;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Assign a debug-friendly name to an effect (shown in DevTools)
|
|
546
|
+
*/
|
|
547
|
+
function setEffectName(effect, name) {
|
|
548
|
+
effectNames.set(effect, name);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Assign a debug-friendly name to a reactive object
|
|
552
|
+
*/
|
|
553
|
+
function setObjectName(obj, name) {
|
|
554
|
+
objectNames.set(obj, name);
|
|
555
|
+
debugObjectRegistry.add(obj);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Register an effect so it appears in the DevTools graph
|
|
559
|
+
*/
|
|
560
|
+
function registerEffectForDebug(effect) {
|
|
561
|
+
if (!effect || !devtoolsEnabled)
|
|
562
|
+
return;
|
|
563
|
+
addEffectToRegistry(effect);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Register a reactive object so it appears in the DevTools graph
|
|
567
|
+
*/
|
|
568
|
+
function registerObjectForDebug(obj) {
|
|
569
|
+
if (!devtoolsEnabled)
|
|
570
|
+
return;
|
|
571
|
+
documentObject(obj);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Records a cause → consequence relationship between effects.
|
|
575
|
+
* @param source - The effect performing the write (undefined if external/user input)
|
|
576
|
+
* @param target - The effect that re-ran because of the write
|
|
577
|
+
* @param obj - The reactive object that changed
|
|
578
|
+
* @param prop - The property that changed
|
|
579
|
+
* @param evolution - The type of change (set/add/del/bunch)
|
|
580
|
+
*/
|
|
581
|
+
function recordTriggerLink(source, target, obj, prop, evolution) {
|
|
582
|
+
if (options.introspection?.enableHistory) {
|
|
583
|
+
addToMutationHistory(source, target, obj, prop, evolution);
|
|
584
|
+
}
|
|
585
|
+
if (!devtoolsEnabled)
|
|
586
|
+
return;
|
|
587
|
+
addEffectToRegistry(target);
|
|
588
|
+
if (source)
|
|
589
|
+
addEffectToRegistry(source);
|
|
590
|
+
const descriptor = describeProp(obj, prop);
|
|
591
|
+
const record = ensureTriggerRecord(source ?? EXTERNAL_SOURCE, target, descriptor, obj, prop, evolution);
|
|
592
|
+
record.count += 1;
|
|
593
|
+
record.lastTriggered = Date.now();
|
|
594
|
+
documentObject(obj);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Traces back the chain of triggers that led to a specific effect
|
|
598
|
+
* @param effect The effect to trace back
|
|
599
|
+
* @param limit Max depth
|
|
600
|
+
*/
|
|
601
|
+
function getTriggerChain(effect, limit = 5) {
|
|
602
|
+
const chain = [];
|
|
603
|
+
let current = effect;
|
|
604
|
+
for (let i = 0; i < limit; i++) {
|
|
605
|
+
// Find who triggered 'current'
|
|
606
|
+
// We need to reverse search the triggerGraph (source -> target)
|
|
607
|
+
// This is expensive O(Edges) but okay for error reporting
|
|
608
|
+
let foundSource;
|
|
609
|
+
let foundReason = '';
|
|
610
|
+
search: for (const [source, targetMap] of triggerGraph) {
|
|
611
|
+
for (const [target, labelMap] of targetMap) {
|
|
612
|
+
if (target === current) {
|
|
613
|
+
// Found a source! Use the most recent trigger record
|
|
614
|
+
let lastTime = 0;
|
|
615
|
+
for (const record of labelMap.values()) {
|
|
616
|
+
if (record.lastTriggered > lastTime) {
|
|
617
|
+
lastTime = record.lastTriggered;
|
|
618
|
+
foundReason = record.label;
|
|
619
|
+
foundSource = source === EXTERNAL_SOURCE ? undefined : source;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (foundSource || foundReason)
|
|
623
|
+
break search;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (foundSource) {
|
|
628
|
+
chain.push(`${ensureEffectName(foundSource)} -> (${foundReason}) -> ${ensureEffectName(current)}`);
|
|
629
|
+
current = foundSource;
|
|
630
|
+
}
|
|
631
|
+
else if (foundReason) {
|
|
632
|
+
chain.push(`External -> (${foundReason}) -> ${ensureEffectName(current)}`);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return chain.reverse();
|
|
640
|
+
}
|
|
641
|
+
function buildEffectNodes(allEffects) {
|
|
642
|
+
const nodes = [];
|
|
643
|
+
const nodeByEffect = new Map();
|
|
644
|
+
const ordered = Array.from(allEffects);
|
|
645
|
+
for (const effect of ordered) {
|
|
646
|
+
const label = ensureEffectName(effect);
|
|
647
|
+
const node = {
|
|
648
|
+
id: `effect_${nodes.length}`,
|
|
649
|
+
label,
|
|
650
|
+
type: 'effect',
|
|
651
|
+
depth: 0,
|
|
652
|
+
debugName: label,
|
|
653
|
+
};
|
|
654
|
+
nodes.push(node);
|
|
655
|
+
nodeByEffect.set(effect, node);
|
|
656
|
+
}
|
|
657
|
+
const depthCache = new Map();
|
|
658
|
+
const computeDepth = (effect) => {
|
|
659
|
+
if (!effect)
|
|
660
|
+
return 0;
|
|
661
|
+
const cached = depthCache.get(effect);
|
|
662
|
+
if (cached !== undefined)
|
|
663
|
+
return cached;
|
|
664
|
+
const parent = getEffectNode(effect).parent;
|
|
665
|
+
const depth = computeDepth(parent) + (parent ? 1 : 0);
|
|
666
|
+
depthCache.set(effect, depth);
|
|
667
|
+
return depth;
|
|
668
|
+
};
|
|
669
|
+
for (const [effect, node] of nodeByEffect) {
|
|
670
|
+
node.depth = computeDepth(effect);
|
|
671
|
+
const parent = getEffectNode(effect).parent;
|
|
672
|
+
if (parent) {
|
|
673
|
+
const parentNode = nodeByEffect.get(parent);
|
|
674
|
+
if (parentNode) {
|
|
675
|
+
node.parentId = parentNode.id;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return { nodes, nodeByEffect };
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Builds a graph representing current reactive state (effects, objects, and trigger edges)
|
|
683
|
+
*/
|
|
684
|
+
function buildReactivityGraph() {
|
|
685
|
+
const nodes = [];
|
|
686
|
+
const edges = [];
|
|
687
|
+
const nodeIds = new Map();
|
|
688
|
+
const allEffects = new Set(debugEffectRegistry);
|
|
689
|
+
ensureParentChains(allEffects);
|
|
690
|
+
const { nodes: effectNodes, nodeByEffect } = buildEffectNodes(allEffects);
|
|
691
|
+
for (const node of effectNodes)
|
|
692
|
+
nodes.push(node);
|
|
693
|
+
for (const [effect, node] of nodeByEffect) {
|
|
694
|
+
nodeIds.set(effect, node.id);
|
|
695
|
+
}
|
|
696
|
+
// Object nodes (optional, used for dependency inspection)
|
|
697
|
+
for (const obj of debugObjectRegistry) {
|
|
698
|
+
const id = `object_${nodes.length}`;
|
|
699
|
+
nodes.push({ id, label: ensureObjectName(obj), type: 'state', debugName: objectNames.get(obj) });
|
|
700
|
+
nodeIds.set(obj, id);
|
|
701
|
+
}
|
|
702
|
+
// External source node (user/system outside of effects)
|
|
703
|
+
if (triggerGraph.has(EXTERNAL_SOURCE)) {
|
|
704
|
+
const externalId = `effect_external`;
|
|
705
|
+
nodes.push({ id: externalId, label: 'External', type: 'external', depth: 0 });
|
|
706
|
+
nodeIds.set(EXTERNAL_SOURCE, externalId);
|
|
707
|
+
}
|
|
708
|
+
// Dependency edges (effect → object)
|
|
709
|
+
for (const effect of allEffects) {
|
|
710
|
+
const effectId = nodeIds.get(effect);
|
|
711
|
+
if (!effectId)
|
|
712
|
+
continue;
|
|
713
|
+
const deps = effectToReactiveObjects.get(effect);
|
|
714
|
+
if (!deps)
|
|
715
|
+
continue;
|
|
716
|
+
for (const obj of deps) {
|
|
717
|
+
const objId = nodeIds.get(obj);
|
|
718
|
+
if (!objId)
|
|
719
|
+
continue;
|
|
720
|
+
edges.push({
|
|
721
|
+
id: `${effectId}->${objId}`,
|
|
722
|
+
source: effectId,
|
|
723
|
+
target: objId,
|
|
724
|
+
type: 'dependency',
|
|
725
|
+
label: 'depends',
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Cause edges (effect/object/prop → effect)
|
|
730
|
+
for (const [source, targetMap] of triggerGraph) {
|
|
731
|
+
for (const [targetEffect, labelMap] of targetMap) {
|
|
732
|
+
const targetId = nodeIds.get(targetEffect);
|
|
733
|
+
if (!targetId)
|
|
734
|
+
continue;
|
|
735
|
+
const sourceId = nodeIds.get(source);
|
|
736
|
+
if (!sourceId)
|
|
737
|
+
continue;
|
|
738
|
+
for (const record of labelMap.values()) {
|
|
739
|
+
edges.push({
|
|
740
|
+
id: `${sourceId}->${targetId}:${record.label}`,
|
|
741
|
+
source: sourceId,
|
|
742
|
+
target: targetId,
|
|
743
|
+
type: 'cause',
|
|
744
|
+
label: record.count > 1 ? `${record.label} (${record.count})` : record.label,
|
|
745
|
+
count: record.count,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
nodes,
|
|
752
|
+
edges,
|
|
753
|
+
meta: {
|
|
754
|
+
generatedAt: Date.now(),
|
|
755
|
+
devtoolsEnabled,
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const captureLineage = () => wrapLineageForDebug(getLineage());
|
|
760
|
+
/**
|
|
761
|
+
* Enables the DevTools bridge and exposes the debug API on window/global.
|
|
762
|
+
* Call as early as possible in development builds.
|
|
763
|
+
*/
|
|
764
|
+
function enableDevTools() {
|
|
765
|
+
const globalScope = (typeof globalThis !== 'undefined'
|
|
766
|
+
? globalThis
|
|
767
|
+
: typeof window !== 'undefined'
|
|
768
|
+
? window
|
|
769
|
+
: typeof global !== 'undefined'
|
|
770
|
+
? global
|
|
771
|
+
: undefined);
|
|
772
|
+
if (!globalScope)
|
|
773
|
+
return;
|
|
774
|
+
if (devtoolsEnabled)
|
|
775
|
+
return;
|
|
776
|
+
devtoolsEnabled = true;
|
|
777
|
+
if (options.introspection) {
|
|
778
|
+
options.introspection.logErrors = true;
|
|
779
|
+
}
|
|
780
|
+
globalScope.__MUTTS_DEBUG__ = {
|
|
781
|
+
getGraph: buildReactivityGraph,
|
|
782
|
+
logLineage(tag) {
|
|
783
|
+
logLineage(getLineage());
|
|
784
|
+
return '🦴 Effect Lineage Trace' + (tag ? ` (${tag})` : '');
|
|
785
|
+
},
|
|
786
|
+
get browserLineage() {
|
|
787
|
+
return captureLineage();
|
|
788
|
+
},
|
|
789
|
+
getLineage,
|
|
790
|
+
captureLineage: getStackFrame,
|
|
791
|
+
formatLineage,
|
|
792
|
+
showLineagePanel,
|
|
793
|
+
setEffectName,
|
|
794
|
+
setObjectName,
|
|
795
|
+
registerEffect: registerEffectForDebug,
|
|
796
|
+
registerObject: registerObjectForDebug,
|
|
797
|
+
};
|
|
798
|
+
// @ts-ignore - devtoolsFormatters is a Chrome-specific array
|
|
799
|
+
if (globalScope.devtoolsFormatters) {
|
|
800
|
+
globalScope.devtoolsFormatters.push(lineageFormatter);
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
globalScope.devtoolsFormatters = [lineageFormatter];
|
|
804
|
+
}
|
|
805
|
+
setDebugHooks({
|
|
806
|
+
isDevtoolsEnabled: () => devtoolsEnabled,
|
|
807
|
+
registerEffect: registerEffectForDebug,
|
|
808
|
+
getTriggerChain: getTriggerChain,
|
|
809
|
+
captureStack: getStackFrame,
|
|
810
|
+
captureLineage,
|
|
811
|
+
formatStack: (stack) => {
|
|
812
|
+
if (typeof stack === 'string') {
|
|
813
|
+
return [
|
|
814
|
+
`\n ${stack
|
|
815
|
+
.split('\n')
|
|
816
|
+
.slice(1)
|
|
817
|
+
.map((l) => l.trim())
|
|
818
|
+
.join('\n ')}`,
|
|
819
|
+
];
|
|
820
|
+
}
|
|
821
|
+
if (isLineage(stack)) {
|
|
822
|
+
return ['\n', stack];
|
|
823
|
+
}
|
|
824
|
+
return ['\n', stack];
|
|
825
|
+
},
|
|
826
|
+
recordTriggerLink: recordTriggerLink,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
function forceEnableGraphTracking() {
|
|
830
|
+
devtoolsEnabled = true;
|
|
831
|
+
}
|
|
832
|
+
function isDevtoolsEnabled() {
|
|
833
|
+
return devtoolsEnabled;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Returns the raw dependency graph data structure.
|
|
837
|
+
* This is useful for programmatic analysis of the reactive system.
|
|
838
|
+
*/
|
|
839
|
+
function getDependencyGraph() {
|
|
840
|
+
return {
|
|
841
|
+
nodes: buildReactivityGraph().nodes,
|
|
842
|
+
edges: buildReactivityGraph().edges,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Capture a complete snapshot of the current reactive state
|
|
847
|
+
*/
|
|
848
|
+
function snapshot() {
|
|
849
|
+
return {
|
|
850
|
+
graph: getDependencyGraph(),
|
|
851
|
+
history: getMutationHistory(),
|
|
852
|
+
timestamp: Date.now(),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Returns a list of effects that depend on the given object.
|
|
857
|
+
*/
|
|
858
|
+
function getDependents(obj) {
|
|
859
|
+
const dependents = [];
|
|
860
|
+
// Scan the trigger graph for effects triggered by this object
|
|
861
|
+
// This is O(E) where E is the number of edges, might need optimization for large graphs
|
|
862
|
+
// but acceptable for introspection
|
|
863
|
+
for (const [_source, targetMap] of triggerGraph) {
|
|
864
|
+
for (const [targetEffect, labelMap] of targetMap) {
|
|
865
|
+
for (const record of labelMap.values()) {
|
|
866
|
+
if (record.object === obj) {
|
|
867
|
+
dependents.push(targetEffect);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
// Also check direct dependencies (dependency graph)
|
|
873
|
+
// We don't have a direct obj -> effect map without walking all effects
|
|
874
|
+
// unless we use `watchers` from tracking.ts but that's internal
|
|
875
|
+
return [...new Set(dependents)];
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Returns a list of objects that the given effect depends on.
|
|
879
|
+
*/
|
|
880
|
+
function getDependencies(effect) {
|
|
881
|
+
const deps = effectToReactiveObjects.get(effect);
|
|
882
|
+
return deps ? Array.from(deps) : [];
|
|
883
|
+
}
|
|
884
|
+
const mutationHistory = [];
|
|
885
|
+
let mutationCounter = 0;
|
|
886
|
+
function addToMutationHistory(source, target, obj, prop, evolution) {
|
|
887
|
+
const record = {
|
|
888
|
+
id: ++mutationCounter,
|
|
889
|
+
timestamp: Date.now(),
|
|
890
|
+
source: source ? ensureEffectName(source) : 'External',
|
|
891
|
+
target: ensureEffectName(target),
|
|
892
|
+
objectName: ensureObjectName(obj),
|
|
893
|
+
prop: String(prop),
|
|
894
|
+
type: evolution.type,
|
|
895
|
+
};
|
|
896
|
+
mutationHistory.push(record);
|
|
897
|
+
if (mutationHistory.length > (options.introspection?.historySize ?? 50)) {
|
|
898
|
+
mutationHistory.shift();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Get the recent mutation history
|
|
903
|
+
*/
|
|
904
|
+
function getMutationHistory() {
|
|
905
|
+
return [...mutationHistory];
|
|
906
|
+
}
|
|
907
|
+
// --- Auto DevTools Initialization ---
|
|
908
|
+
// Automatically enable devtools in development environments
|
|
909
|
+
/**
|
|
910
|
+
* Checks if we're in a development environment
|
|
911
|
+
* Detection order:
|
|
912
|
+
* 1. process.env.NODE_ENV (Node.js)
|
|
913
|
+
* 2. import.meta.env.DEV (Vite)
|
|
914
|
+
* 3. import.meta.env.PROD === false (Vite alternative)
|
|
915
|
+
* 4. Assumes development if none of the above are set (safe default)
|
|
916
|
+
*/
|
|
917
|
+
function isDevelopmentMode() {
|
|
918
|
+
// Check for explicit production flag first
|
|
919
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
// Check for Vite's DEV flag
|
|
923
|
+
if (typeof import.meta !== 'undefined') {
|
|
924
|
+
const meta = import.meta;
|
|
925
|
+
if (meta.env?.PROD === true) {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
if (meta.env?.DEV === true) {
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
// Check for custom global override
|
|
933
|
+
if (typeof globalThis !== 'undefined' && '__MUTTS_DEV_MODE__' in globalThis) {
|
|
934
|
+
return globalThis.__MUTTS_DEV_MODE__ !== false;
|
|
935
|
+
}
|
|
936
|
+
// Default to development (safer to enable in dev than disable in prod)
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
// Auto-enable devtools when the module loads in development
|
|
940
|
+
if (isDevelopmentMode() && !devtoolsEnabled) {
|
|
941
|
+
enableDevTools();
|
|
942
|
+
// Optional: Log that devtools were enabled (only in development)
|
|
943
|
+
if (typeof console !== 'undefined' && console.info) {
|
|
944
|
+
// Do not log these in when mode === 'test'
|
|
945
|
+
console.info('🦴 Mutts DevTools enabled automatically in development mode');
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
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 };
|
|
950
|
+
//# sourceMappingURL=debug.esm.js.map
|