pulse-js-framework 1.7.9 → 1.7.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.
@@ -7,460 +7,81 @@
7
7
  * - Time-travel debugging with state snapshots
8
8
  * - Performance monitoring
9
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}
10
+ * - Accessibility auditing
77
11
  *
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}>}
12
+ * Architecture:
13
+ * This module re-exports from specialized sub-modules:
14
+ * - devtools/diagnostics.js: Graph inspection, tracking, profiling
15
+ * - devtools/time-travel.js: State snapshots and history navigation
16
+ * - devtools/a11y-audit.js: Accessibility validation and reporting
111
17
  */
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
18
 
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
- // =============================================================================
19
+ import { createLogger } from './logger.js';
140
20
 
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
- }
21
+ // Import from sub-modules
22
+ import {
23
+ config,
24
+ getDiagnostics,
25
+ getEffectStats,
26
+ getPulseList,
27
+ getDependencyGraph,
28
+ exportGraphAsDot,
29
+ trackedPulse as basedTrackedPulse,
30
+ trackedEffect,
31
+ profile,
32
+ mark,
33
+ resetDiagnostics,
34
+ _setSnapshotCountFn
35
+ } from './devtools/diagnostics.js';
36
+
37
+ import {
38
+ timeTravelConfig,
39
+ getIsTimeTraveling,
40
+ takeSnapshot,
41
+ getHistory,
42
+ getHistoryIndex,
43
+ getSnapshotCount,
44
+ travelTo,
45
+ back,
46
+ forward,
47
+ clearHistory
48
+ } from './devtools/time-travel.js';
49
+
50
+ // Wire up snapshot count for diagnostics
51
+ _setSnapshotCountFn(getSnapshotCount);
52
+
53
+ import {
54
+ a11yAuditConfig,
55
+ runA11yAudit,
56
+ getA11yIssues,
57
+ getA11yStats,
58
+ enableA11yAudit,
59
+ disableA11yAudit,
60
+ toggleA11yHighlights,
61
+ exportA11yReport,
62
+ resetA11yAudit
63
+ } from './devtools/a11y-audit.js';
64
+
65
+ const log = createLogger('DevTools');
370
66
 
371
67
  // =============================================================================
372
- // PULSE & EFFECT TRACKING
68
+ // TRACKED PULSE WITH SNAPSHOT INTEGRATION
373
69
  // =============================================================================
374
70
 
375
- let pulseIdCounter = 0;
376
- let trackedEffectIdCounter = 0;
377
-
378
71
  /**
379
- * Create a tracked pulse (for dev tools)
72
+ * Create a tracked pulse with automatic snapshot on change
380
73
  * @param {any} initialValue - Initial value
381
74
  * @param {string} [name] - Display name for debugging
382
75
  * @returns {Pulse} Tracked pulse
383
76
  */
384
77
  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)`);
78
+ return basedTrackedPulse(initialValue, name, {
79
+ onSnapshot: (action) => {
80
+ if (!getIsTimeTraveling()) {
81
+ takeSnapshot(action);
82
+ }
450
83
  }
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
- };
84
+ });
464
85
  }
465
86
 
466
87
  // =============================================================================
@@ -474,24 +95,47 @@ export function trackedEffect(fn, name) {
474
95
  export function enableDevTools(options = {}) {
475
96
  Object.assign(config, options, { enabled: true });
476
97
 
98
+ if (options.maxSnapshots) {
99
+ timeTravelConfig.maxSnapshots = options.maxSnapshots;
100
+ }
101
+
477
102
  if (typeof window !== 'undefined') {
478
103
  // Expose to window for browser dev tools
479
104
  window.__PULSE_DEVTOOLS__ = {
105
+ // Diagnostics
480
106
  getDiagnostics,
481
107
  getEffectStats,
482
108
  getPulseList,
483
109
  getDependencyGraph,
484
110
  exportGraphAsDot,
111
+ profile,
112
+ mark,
113
+
114
+ // Time-travel
485
115
  takeSnapshot,
486
116
  getHistory,
487
117
  travelTo,
488
118
  back,
489
119
  forward,
490
120
  clearHistory,
491
- config
121
+
122
+ // A11y Audit
123
+ runA11yAudit,
124
+ getA11yIssues,
125
+ getA11yStats,
126
+ enableA11yAudit,
127
+ disableA11yAudit,
128
+ toggleA11yHighlights,
129
+ exportA11yReport,
130
+ resetA11yAudit,
131
+
132
+ // Config
133
+ config,
134
+ timeTravelConfig,
135
+ a11yAuditConfig
492
136
  };
493
137
 
494
- console.log('[Pulse DevTools] Enabled. Access via window.__PULSE_DEVTOOLS__');
138
+ log.info('Enabled. Access via window.__PULSE_DEVTOOLS__');
495
139
  }
496
140
  }
497
141
 
@@ -504,6 +148,8 @@ export function disableDevTools() {
504
148
  if (typeof window !== 'undefined') {
505
149
  delete window.__PULSE_DEVTOOLS__;
506
150
  }
151
+
152
+ log.info('Disabled');
507
153
  }
508
154
 
509
155
  /**
@@ -520,67 +166,58 @@ export function isDevToolsEnabled() {
520
166
  */
521
167
  export function configureDevTools(options) {
522
168
  Object.assign(config, options);
169
+
170
+ if (options.maxSnapshots) {
171
+ timeTravelConfig.maxSnapshots = options.maxSnapshots;
172
+ }
523
173
  }
524
174
 
525
175
  /**
526
176
  * Clear all dev tools data
527
177
  */
528
178
  export function resetDevTools() {
529
- pulseRegistry.clear();
530
- effectRegistry.clear();
531
- stateHistory.length = 0;
532
- historyIndex = -1;
533
- pulseIdCounter = 0;
534
- trackedEffectIdCounter = 0;
179
+ resetDiagnostics();
180
+ clearHistory();
181
+ resetA11yAudit();
535
182
  }
536
183
 
537
184
  // =============================================================================
538
- // PERFORMANCE PROFILING
185
+ // RE-EXPORTS
539
186
  // =============================================================================
540
187
 
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
- }
188
+ export {
189
+ // Diagnostics
190
+ getDiagnostics,
191
+ getEffectStats,
192
+ getPulseList,
193
+ getDependencyGraph,
194
+ exportGraphAsDot,
195
+ trackedEffect,
196
+ profile,
197
+ mark,
562
198
 
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
- }
199
+ // Time-travel
200
+ takeSnapshot,
201
+ getHistory,
202
+ getHistoryIndex,
203
+ travelTo,
204
+ back,
205
+ forward,
206
+ clearHistory,
207
+
208
+ // A11y Audit
209
+ runA11yAudit,
210
+ getA11yIssues,
211
+ getA11yStats,
212
+ enableA11yAudit,
213
+ disableA11yAudit,
214
+ toggleA11yHighlights,
215
+ exportA11yReport,
216
+ resetA11yAudit
217
+ };
581
218
 
582
219
  // =============================================================================
583
- // EXPORTS
220
+ // DEFAULT EXPORT
584
221
  // =============================================================================
585
222
 
586
223
  export default {
@@ -588,10 +225,12 @@ export default {
588
225
  getDiagnostics,
589
226
  getEffectStats,
590
227
  getPulseList,
591
-
592
- // Graph
593
228
  getDependencyGraph,
594
229
  exportGraphAsDot,
230
+ trackedPulse,
231
+ trackedEffect,
232
+ profile,
233
+ mark,
595
234
 
596
235
  // Time-travel
597
236
  takeSnapshot,
@@ -602,10 +241,6 @@ export default {
602
241
  forward,
603
242
  clearHistory,
604
243
 
605
- // Tracking
606
- trackedPulse,
607
- trackedEffect,
608
-
609
244
  // Configuration
610
245
  enableDevTools,
611
246
  disableDevTools,
@@ -613,7 +248,13 @@ export default {
613
248
  configureDevTools,
614
249
  resetDevTools,
615
250
 
616
- // Profiling
617
- profile,
618
- mark
251
+ // Accessibility Audit
252
+ runA11yAudit,
253
+ getA11yIssues,
254
+ getA11yStats,
255
+ enableA11yAudit,
256
+ disableA11yAudit,
257
+ toggleA11yHighlights,
258
+ exportA11yReport,
259
+ resetA11yAudit
619
260
  };