pulse-js-framework 1.7.1 → 1.7.3
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/cli/dev.js +55 -39
- package/cli/release.js +2 -3
- package/package.json +8 -2
- package/runtime/async.js +619 -0
- package/runtime/devtools.js +619 -0
- package/runtime/dom.js +254 -40
- package/runtime/form.js +659 -0
- package/runtime/pulse.js +36 -3
- package/runtime/router.js +51 -5
- package/runtime/store.js +45 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Dev Tools
|
|
3
|
+
* @module pulse-js-framework/runtime/devtools
|
|
4
|
+
*
|
|
5
|
+
* Development tools for debugging reactive applications:
|
|
6
|
+
* - Reactive dependency graph inspection
|
|
7
|
+
* - Time-travel debugging with state snapshots
|
|
8
|
+
* - Performance monitoring
|
|
9
|
+
* - Effect tracking
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { pulse, effect, batch, context } from './pulse.js';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// DEV TOOLS STATE
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Dev tools configuration
|
|
20
|
+
*/
|
|
21
|
+
const config = {
|
|
22
|
+
enabled: false,
|
|
23
|
+
maxSnapshots: 50,
|
|
24
|
+
logUpdates: false,
|
|
25
|
+
logEffects: false,
|
|
26
|
+
warnOnSlowEffects: true,
|
|
27
|
+
slowEffectThreshold: 16 // ms (one frame at 60fps)
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Registry of all tracked pulses
|
|
32
|
+
* @type {Map<string, {pulse: Pulse, name: string, createdAt: number}>}
|
|
33
|
+
*/
|
|
34
|
+
const pulseRegistry = new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Registry of all tracked effects
|
|
38
|
+
* @type {Map<string, {effect: Object, name: string, createdAt: number, runCount: number, totalTime: number}>}
|
|
39
|
+
*/
|
|
40
|
+
const effectRegistry = new Map();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Time-travel state history
|
|
44
|
+
* @type {Array<{timestamp: number, state: Object, action: string}>}
|
|
45
|
+
*/
|
|
46
|
+
const stateHistory = [];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Current position in history (for time-travel)
|
|
50
|
+
*/
|
|
51
|
+
let historyIndex = -1;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Flag to prevent recording during time-travel
|
|
55
|
+
*/
|
|
56
|
+
let isTimeTraveling = false;
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// DIAGNOSTICS API
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} DiagnosticsStats
|
|
64
|
+
* @property {number} pulseCount - Total number of active pulses
|
|
65
|
+
* @property {number} effectCount - Total number of active effects
|
|
66
|
+
* @property {number} totalEffectRuns - Total effect executions
|
|
67
|
+
* @property {number} avgEffectTime - Average effect execution time (ms)
|
|
68
|
+
* @property {number} pendingEffects - Effects waiting to run
|
|
69
|
+
* @property {number} batchDepth - Current batch nesting depth
|
|
70
|
+
* @property {number} snapshotCount - Number of stored snapshots
|
|
71
|
+
* @property {Object} memoryEstimate - Estimated memory usage
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get current diagnostics statistics
|
|
76
|
+
* @returns {DiagnosticsStats}
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const stats = getDiagnostics();
|
|
80
|
+
* console.log(`Active pulses: ${stats.pulseCount}`);
|
|
81
|
+
* console.log(`Effect runs: ${stats.totalEffectRuns}`);
|
|
82
|
+
*/
|
|
83
|
+
export function getDiagnostics() {
|
|
84
|
+
let totalRuns = 0;
|
|
85
|
+
let totalTime = 0;
|
|
86
|
+
|
|
87
|
+
for (const entry of effectRegistry.values()) {
|
|
88
|
+
totalRuns += entry.runCount;
|
|
89
|
+
totalTime += entry.totalTime;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
pulseCount: pulseRegistry.size,
|
|
94
|
+
effectCount: effectRegistry.size,
|
|
95
|
+
totalEffectRuns: totalRuns,
|
|
96
|
+
avgEffectTime: totalRuns > 0 ? totalTime / totalRuns : 0,
|
|
97
|
+
pendingEffects: context.pendingEffects.size,
|
|
98
|
+
batchDepth: context.batchDepth,
|
|
99
|
+
snapshotCount: stateHistory.length,
|
|
100
|
+
memoryEstimate: {
|
|
101
|
+
pulses: pulseRegistry.size * 100, // rough estimate in bytes
|
|
102
|
+
effects: effectRegistry.size * 200,
|
|
103
|
+
history: stateHistory.length * 500
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get detailed effect statistics
|
|
110
|
+
* @returns {Array<{id: string, name: string, runCount: number, avgTime: number, lastRun: number}>}
|
|
111
|
+
*/
|
|
112
|
+
export function getEffectStats() {
|
|
113
|
+
return [...effectRegistry.entries()].map(([id, entry]) => ({
|
|
114
|
+
id,
|
|
115
|
+
name: entry.name,
|
|
116
|
+
runCount: entry.runCount,
|
|
117
|
+
avgTime: entry.runCount > 0 ? entry.totalTime / entry.runCount : 0,
|
|
118
|
+
totalTime: entry.totalTime,
|
|
119
|
+
createdAt: entry.createdAt
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get list of all tracked pulses
|
|
125
|
+
* @returns {Array<{id: string, name: string, value: any, subscriberCount: number}>}
|
|
126
|
+
*/
|
|
127
|
+
export function getPulseList() {
|
|
128
|
+
return [...pulseRegistry.entries()].map(([id, entry]) => ({
|
|
129
|
+
id,
|
|
130
|
+
name: entry.name,
|
|
131
|
+
value: entry.pulse.peek(),
|
|
132
|
+
subscriberCount: entry.pulse._subscribers?.size || 0,
|
|
133
|
+
createdAt: entry.createdAt
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// REACTIVE GRAPH INSPECTION
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @typedef {Object} DependencyNode
|
|
143
|
+
* @property {string} id - Node identifier
|
|
144
|
+
* @property {string} type - 'pulse' | 'effect' | 'computed'
|
|
145
|
+
* @property {string} name - Display name
|
|
146
|
+
* @property {any} value - Current value (for pulses)
|
|
147
|
+
* @property {string[]} dependencies - IDs of nodes this depends on
|
|
148
|
+
* @property {string[]} dependents - IDs of nodes that depend on this
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build the reactive dependency graph
|
|
153
|
+
* @returns {{nodes: DependencyNode[], edges: Array<{from: string, to: string}>}}
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* const graph = getDependencyGraph();
|
|
157
|
+
* console.log('Nodes:', graph.nodes.length);
|
|
158
|
+
* console.log('Edges:', graph.edges.length);
|
|
159
|
+
* // Visualize with D3.js or similar
|
|
160
|
+
*/
|
|
161
|
+
export function getDependencyGraph() {
|
|
162
|
+
const nodes = [];
|
|
163
|
+
const edges = [];
|
|
164
|
+
const nodeMap = new Map();
|
|
165
|
+
|
|
166
|
+
// Add pulse nodes
|
|
167
|
+
for (const [id, entry] of pulseRegistry) {
|
|
168
|
+
const node = {
|
|
169
|
+
id,
|
|
170
|
+
type: 'pulse',
|
|
171
|
+
name: entry.name,
|
|
172
|
+
value: entry.pulse.peek(),
|
|
173
|
+
dependencies: [],
|
|
174
|
+
dependents: []
|
|
175
|
+
};
|
|
176
|
+
nodes.push(node);
|
|
177
|
+
nodeMap.set(id, node);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Add effect nodes and build edges
|
|
181
|
+
for (const [id, entry] of effectRegistry) {
|
|
182
|
+
const node = {
|
|
183
|
+
id,
|
|
184
|
+
type: 'effect',
|
|
185
|
+
name: entry.name,
|
|
186
|
+
value: null,
|
|
187
|
+
dependencies: [],
|
|
188
|
+
dependents: []
|
|
189
|
+
};
|
|
190
|
+
nodes.push(node);
|
|
191
|
+
nodeMap.set(id, node);
|
|
192
|
+
|
|
193
|
+
// Get effect's dependencies
|
|
194
|
+
if (entry.effect?.dependencies) {
|
|
195
|
+
for (const dep of entry.effect.dependencies) {
|
|
196
|
+
const depId = findPulseId(dep);
|
|
197
|
+
if (depId) {
|
|
198
|
+
node.dependencies.push(depId);
|
|
199
|
+
edges.push({ from: depId, to: id });
|
|
200
|
+
|
|
201
|
+
const depNode = nodeMap.get(depId);
|
|
202
|
+
if (depNode) {
|
|
203
|
+
depNode.dependents.push(id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { nodes, edges };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Find pulse ID from pulse instance
|
|
215
|
+
*/
|
|
216
|
+
function findPulseId(pulseInstance) {
|
|
217
|
+
for (const [id, entry] of pulseRegistry) {
|
|
218
|
+
if (entry.pulse === pulseInstance) {
|
|
219
|
+
return id;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Export graph in DOT format for Graphviz visualization
|
|
227
|
+
* @returns {string} DOT format graph
|
|
228
|
+
*/
|
|
229
|
+
export function exportGraphAsDot() {
|
|
230
|
+
const { nodes, edges } = getDependencyGraph();
|
|
231
|
+
|
|
232
|
+
let dot = 'digraph ReactiveGraph {\n';
|
|
233
|
+
dot += ' rankdir=LR;\n';
|
|
234
|
+
dot += ' node [shape=box];\n\n';
|
|
235
|
+
|
|
236
|
+
// Add nodes with styling
|
|
237
|
+
for (const node of nodes) {
|
|
238
|
+
const color = node.type === 'pulse' ? 'lightblue' : 'lightgreen';
|
|
239
|
+
const label = `${node.name}\\n${node.type}`;
|
|
240
|
+
dot += ` "${node.id}" [label="${label}" fillcolor="${color}" style="filled"];\n`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
dot += '\n';
|
|
244
|
+
|
|
245
|
+
// Add edges
|
|
246
|
+
for (const edge of edges) {
|
|
247
|
+
dot += ` "${edge.from}" -> "${edge.to}";\n`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
dot += '}\n';
|
|
251
|
+
return dot;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// =============================================================================
|
|
255
|
+
// TIME-TRAVEL DEBUGGING
|
|
256
|
+
// =============================================================================
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @typedef {Object} StateSnapshot
|
|
260
|
+
* @property {number} timestamp - When snapshot was taken
|
|
261
|
+
* @property {Object} state - Serialized state
|
|
262
|
+
* @property {string} action - Description of what caused the snapshot
|
|
263
|
+
* @property {number} index - Position in history
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Take a snapshot of current state
|
|
268
|
+
* @param {string} [action='manual'] - Description of the action
|
|
269
|
+
* @returns {StateSnapshot}
|
|
270
|
+
*/
|
|
271
|
+
export function takeSnapshot(action = 'manual') {
|
|
272
|
+
if (isTimeTraveling) return null;
|
|
273
|
+
|
|
274
|
+
const state = {};
|
|
275
|
+
for (const [id, entry] of pulseRegistry) {
|
|
276
|
+
try {
|
|
277
|
+
// Deep clone to prevent mutation
|
|
278
|
+
state[id] = JSON.parse(JSON.stringify(entry.pulse.peek()));
|
|
279
|
+
} catch {
|
|
280
|
+
// Non-serializable value
|
|
281
|
+
state[id] = '[Non-serializable]';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const snapshot = {
|
|
286
|
+
timestamp: Date.now(),
|
|
287
|
+
state,
|
|
288
|
+
action,
|
|
289
|
+
index: stateHistory.length
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Trim history if too long
|
|
293
|
+
if (stateHistory.length >= config.maxSnapshots) {
|
|
294
|
+
stateHistory.shift();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
stateHistory.push(snapshot);
|
|
298
|
+
historyIndex = stateHistory.length - 1;
|
|
299
|
+
|
|
300
|
+
return snapshot;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get state history
|
|
305
|
+
* @returns {StateSnapshot[]}
|
|
306
|
+
*/
|
|
307
|
+
export function getHistory() {
|
|
308
|
+
return [...stateHistory];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get current history position
|
|
313
|
+
* @returns {number}
|
|
314
|
+
*/
|
|
315
|
+
export function getHistoryIndex() {
|
|
316
|
+
return historyIndex;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Travel to a specific point in history
|
|
321
|
+
* @param {number} index - History index to travel to
|
|
322
|
+
* @returns {boolean} Success
|
|
323
|
+
*/
|
|
324
|
+
export function travelTo(index) {
|
|
325
|
+
if (index < 0 || index >= stateHistory.length) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const snapshot = stateHistory[index];
|
|
330
|
+
isTimeTraveling = true;
|
|
331
|
+
|
|
332
|
+
batch(() => {
|
|
333
|
+
for (const [id, value] of Object.entries(snapshot.state)) {
|
|
334
|
+
const entry = pulseRegistry.get(id);
|
|
335
|
+
if (entry && value !== '[Non-serializable]') {
|
|
336
|
+
entry.pulse.set(value);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
historyIndex = index;
|
|
342
|
+
isTimeTraveling = false;
|
|
343
|
+
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Go back one step in history
|
|
349
|
+
* @returns {boolean} Success
|
|
350
|
+
*/
|
|
351
|
+
export function back() {
|
|
352
|
+
return travelTo(historyIndex - 1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Go forward one step in history
|
|
357
|
+
* @returns {boolean} Success
|
|
358
|
+
*/
|
|
359
|
+
export function forward() {
|
|
360
|
+
return travelTo(historyIndex + 1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Clear all history
|
|
365
|
+
*/
|
|
366
|
+
export function clearHistory() {
|
|
367
|
+
stateHistory.length = 0;
|
|
368
|
+
historyIndex = -1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// =============================================================================
|
|
372
|
+
// PULSE & EFFECT TRACKING
|
|
373
|
+
// =============================================================================
|
|
374
|
+
|
|
375
|
+
let pulseIdCounter = 0;
|
|
376
|
+
let trackedEffectIdCounter = 0;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Create a tracked pulse (for dev tools)
|
|
380
|
+
* @param {any} initialValue - Initial value
|
|
381
|
+
* @param {string} [name] - Display name for debugging
|
|
382
|
+
* @returns {Pulse} Tracked pulse
|
|
383
|
+
*/
|
|
384
|
+
export function trackedPulse(initialValue, name) {
|
|
385
|
+
const p = pulse(initialValue);
|
|
386
|
+
const id = `pulse_${++pulseIdCounter}`;
|
|
387
|
+
|
|
388
|
+
pulseRegistry.set(id, {
|
|
389
|
+
pulse: p,
|
|
390
|
+
name: name || id,
|
|
391
|
+
createdAt: Date.now()
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Wrap set to record snapshots
|
|
395
|
+
const originalSet = p.set.bind(p);
|
|
396
|
+
p.set = (value) => {
|
|
397
|
+
const result = originalSet(value);
|
|
398
|
+
if (config.enabled && config.logUpdates) {
|
|
399
|
+
console.log(`[Pulse] ${name || id} updated:`, value);
|
|
400
|
+
}
|
|
401
|
+
if (config.enabled && !isTimeTraveling) {
|
|
402
|
+
takeSnapshot(`${name || id} = ${JSON.stringify(value)}`);
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// Add dispose method
|
|
408
|
+
p.dispose = () => {
|
|
409
|
+
pulseRegistry.delete(id);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return p;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Create a tracked effect (for dev tools)
|
|
417
|
+
* @param {function} fn - Effect function
|
|
418
|
+
* @param {string} [name] - Display name for debugging
|
|
419
|
+
* @returns {function} Dispose function
|
|
420
|
+
*/
|
|
421
|
+
export function trackedEffect(fn, name) {
|
|
422
|
+
const id = `effect_${++trackedEffectIdCounter}`;
|
|
423
|
+
const startTime = Date.now();
|
|
424
|
+
|
|
425
|
+
const entry = {
|
|
426
|
+
effect: null,
|
|
427
|
+
name: name || id,
|
|
428
|
+
createdAt: startTime,
|
|
429
|
+
runCount: 0,
|
|
430
|
+
totalTime: 0
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
effectRegistry.set(id, entry);
|
|
434
|
+
|
|
435
|
+
const wrappedFn = () => {
|
|
436
|
+
const runStart = performance.now();
|
|
437
|
+
|
|
438
|
+
if (config.enabled && config.logEffects) {
|
|
439
|
+
console.log(`[Effect] ${name || id} running...`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const result = fn();
|
|
443
|
+
|
|
444
|
+
const runTime = performance.now() - runStart;
|
|
445
|
+
entry.runCount++;
|
|
446
|
+
entry.totalTime += runTime;
|
|
447
|
+
|
|
448
|
+
if (config.enabled && config.warnOnSlowEffects && runTime > config.slowEffectThreshold) {
|
|
449
|
+
console.warn(`[Effect] ${name || id} took ${runTime.toFixed(2)}ms (slow)`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return result;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const dispose = effect(wrappedFn, { id });
|
|
456
|
+
|
|
457
|
+
// Store reference to effect for graph building
|
|
458
|
+
entry.effect = context.currentEffect;
|
|
459
|
+
|
|
460
|
+
return () => {
|
|
461
|
+
dispose();
|
|
462
|
+
effectRegistry.delete(id);
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// =============================================================================
|
|
467
|
+
// DEV TOOLS API
|
|
468
|
+
// =============================================================================
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Enable dev tools
|
|
472
|
+
* @param {Object} [options] - Configuration options
|
|
473
|
+
*/
|
|
474
|
+
export function enableDevTools(options = {}) {
|
|
475
|
+
Object.assign(config, options, { enabled: true });
|
|
476
|
+
|
|
477
|
+
if (typeof window !== 'undefined') {
|
|
478
|
+
// Expose to window for browser dev tools
|
|
479
|
+
window.__PULSE_DEVTOOLS__ = {
|
|
480
|
+
getDiagnostics,
|
|
481
|
+
getEffectStats,
|
|
482
|
+
getPulseList,
|
|
483
|
+
getDependencyGraph,
|
|
484
|
+
exportGraphAsDot,
|
|
485
|
+
takeSnapshot,
|
|
486
|
+
getHistory,
|
|
487
|
+
travelTo,
|
|
488
|
+
back,
|
|
489
|
+
forward,
|
|
490
|
+
clearHistory,
|
|
491
|
+
config
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
console.log('[Pulse DevTools] Enabled. Access via window.__PULSE_DEVTOOLS__');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Disable dev tools
|
|
500
|
+
*/
|
|
501
|
+
export function disableDevTools() {
|
|
502
|
+
config.enabled = false;
|
|
503
|
+
|
|
504
|
+
if (typeof window !== 'undefined') {
|
|
505
|
+
delete window.__PULSE_DEVTOOLS__;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Check if dev tools are enabled
|
|
511
|
+
* @returns {boolean}
|
|
512
|
+
*/
|
|
513
|
+
export function isDevToolsEnabled() {
|
|
514
|
+
return config.enabled;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Update dev tools configuration
|
|
519
|
+
* @param {Object} options - Configuration options
|
|
520
|
+
*/
|
|
521
|
+
export function configureDevTools(options) {
|
|
522
|
+
Object.assign(config, options);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Clear all dev tools data
|
|
527
|
+
*/
|
|
528
|
+
export function resetDevTools() {
|
|
529
|
+
pulseRegistry.clear();
|
|
530
|
+
effectRegistry.clear();
|
|
531
|
+
stateHistory.length = 0;
|
|
532
|
+
historyIndex = -1;
|
|
533
|
+
pulseIdCounter = 0;
|
|
534
|
+
trackedEffectIdCounter = 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// =============================================================================
|
|
538
|
+
// PERFORMANCE PROFILING
|
|
539
|
+
// =============================================================================
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Profile a section of code
|
|
543
|
+
* @param {string} name - Profile name
|
|
544
|
+
* @param {function} fn - Function to profile
|
|
545
|
+
* @returns {any} Result of fn
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* const result = profile('data-processing', () => {
|
|
549
|
+
* return processLargeDataset();
|
|
550
|
+
* });
|
|
551
|
+
*/
|
|
552
|
+
export function profile(name, fn) {
|
|
553
|
+
const start = performance.now();
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
return fn();
|
|
557
|
+
} finally {
|
|
558
|
+
const duration = performance.now() - start;
|
|
559
|
+
console.log(`[Profile] ${name}: ${duration.toFixed(2)}ms`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Create a performance marker
|
|
565
|
+
* @param {string} name - Marker name
|
|
566
|
+
* @returns {{end: function(): number}} Marker with end method
|
|
567
|
+
*/
|
|
568
|
+
export function mark(name) {
|
|
569
|
+
const start = performance.now();
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
end() {
|
|
573
|
+
const duration = performance.now() - start;
|
|
574
|
+
if (config.enabled) {
|
|
575
|
+
console.log(`[Mark] ${name}: ${duration.toFixed(2)}ms`);
|
|
576
|
+
}
|
|
577
|
+
return duration;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// =============================================================================
|
|
583
|
+
// EXPORTS
|
|
584
|
+
// =============================================================================
|
|
585
|
+
|
|
586
|
+
export default {
|
|
587
|
+
// Diagnostics
|
|
588
|
+
getDiagnostics,
|
|
589
|
+
getEffectStats,
|
|
590
|
+
getPulseList,
|
|
591
|
+
|
|
592
|
+
// Graph
|
|
593
|
+
getDependencyGraph,
|
|
594
|
+
exportGraphAsDot,
|
|
595
|
+
|
|
596
|
+
// Time-travel
|
|
597
|
+
takeSnapshot,
|
|
598
|
+
getHistory,
|
|
599
|
+
getHistoryIndex,
|
|
600
|
+
travelTo,
|
|
601
|
+
back,
|
|
602
|
+
forward,
|
|
603
|
+
clearHistory,
|
|
604
|
+
|
|
605
|
+
// Tracking
|
|
606
|
+
trackedPulse,
|
|
607
|
+
trackedEffect,
|
|
608
|
+
|
|
609
|
+
// Configuration
|
|
610
|
+
enableDevTools,
|
|
611
|
+
disableDevTools,
|
|
612
|
+
isDevToolsEnabled,
|
|
613
|
+
configureDevTools,
|
|
614
|
+
resetDevTools,
|
|
615
|
+
|
|
616
|
+
// Profiling
|
|
617
|
+
profile,
|
|
618
|
+
mark
|
|
619
|
+
};
|