@amadeus-it-group/ngrx-devtool 0.1.2

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.
@@ -0,0 +1,1354 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Injectable, PLATFORM_ID, ErrorHandler, Injector, afterNextRender, APP_INITIALIZER } from '@angular/core';
3
+ import { EffectSources, Actions, EFFECTS_ERROR_HANDLER } from '@ngrx/effects';
4
+ import { Subject, ReplaySubject, BehaviorSubject, takeUntil as takeUntil$1, filter, tap, Observable } from 'rxjs';
5
+ import { takeUntil, tap as tap$1 } from 'rxjs/operators';
6
+ import { isPlatformBrowser } from '@angular/common';
7
+ import { createSelector } from '@ngrx/store';
8
+
9
+ const REPLAY_BUFFER_SIZE$1 = 100;
10
+ const TIMELINE_MAX_SIZE = 1000;
11
+ const TIMELINE_TRIM_SIZE = 500;
12
+ const EFFECT_TIMELINE_MAX_SIZE = 500;
13
+ const CORRELATION_TIMEOUT_MS = 30000;
14
+ class EffectTrackerService {
15
+ effectSources = inject(EffectSources, { optional: true });
16
+ destroy$ = new Subject();
17
+ actionTimeline = [];
18
+ effectTimeline = [];
19
+ effectActionPatterns = new Set();
20
+ pendingCorrelations = new Map();
21
+ correlationCounter = 0;
22
+ lastTriggerAction = null;
23
+ effectEvents$ = new ReplaySubject(REPLAY_BUFFER_SIZE$1);
24
+ constructor() {
25
+ this.subscribeToEffectEvents();
26
+ }
27
+ subscribeToEffectEvents() {
28
+ const devToolsSources = this.effectSources;
29
+ if (devToolsSources?.effectEvents$) {
30
+ devToolsSources.effectEvents$.pipe(takeUntil(this.destroy$)).subscribe((event) => {
31
+ this.handleEffectEvent(event);
32
+ this.effectEvents$.next(event);
33
+ });
34
+ }
35
+ }
36
+ handleEffectEvent(event) {
37
+ switch (event.lifecycle) {
38
+ case 'emitted':
39
+ if (event.action) {
40
+ const trackedEffect = {
41
+ effectName: event.effectName,
42
+ sourceName: event.sourceName,
43
+ propertyName: event.propertyName,
44
+ triggerAction: this.lastTriggerAction?.type,
45
+ resultAction: event.action.type,
46
+ startTime: event.timestamp - (event.duration ?? 0),
47
+ endTime: event.timestamp,
48
+ duration: event.duration,
49
+ status: 'completed',
50
+ };
51
+ this.effectTimeline.push(trackedEffect);
52
+ this.effectActionPatterns.add(event.action.type);
53
+ }
54
+ break;
55
+ case 'error': {
56
+ const erroredEffect = {
57
+ effectName: event.effectName,
58
+ sourceName: event.sourceName,
59
+ propertyName: event.propertyName,
60
+ triggerAction: this.lastTriggerAction?.type,
61
+ startTime: event.timestamp - (event.duration ?? 0),
62
+ endTime: event.timestamp,
63
+ duration: event.duration,
64
+ status: 'error',
65
+ error: event.error,
66
+ };
67
+ this.effectTimeline.push(erroredEffect);
68
+ break;
69
+ }
70
+ }
71
+ if (this.effectTimeline.length > EFFECT_TIMELINE_MAX_SIZE) {
72
+ this.effectTimeline = this.effectTimeline.slice(-EFFECT_TIMELINE_MAX_SIZE / 2);
73
+ }
74
+ }
75
+ isEffectAction(actionType) {
76
+ return this.effectActionPatterns.has(actionType);
77
+ }
78
+ trackAction(action) {
79
+ const isEffect = this.isEffectAction(action.type);
80
+ if (!isEffect) {
81
+ this.lastTriggerAction = action;
82
+ }
83
+ const correlationId = isEffect
84
+ ? this.findCorrelation()
85
+ : this.createCorrelation(action);
86
+ const effectName = this.findEffectNameForAction(action.type);
87
+ const tracked = {
88
+ action: action.type,
89
+ payload: action,
90
+ timestamp: Date.now(),
91
+ source: isEffect ? 'effect' : 'user',
92
+ correlationId,
93
+ effectName,
94
+ };
95
+ this.actionTimeline.push(tracked);
96
+ if (this.actionTimeline.length > TIMELINE_MAX_SIZE) {
97
+ this.actionTimeline = this.actionTimeline.slice(-TIMELINE_TRIM_SIZE);
98
+ }
99
+ return tracked;
100
+ }
101
+ getTimeline() {
102
+ return [...this.actionTimeline];
103
+ }
104
+ clearTimeline() {
105
+ this.actionTimeline = [];
106
+ this.pendingCorrelations.clear();
107
+ }
108
+ createCorrelation(action) {
109
+ const correlationId = `corr_${++this.correlationCounter}_${Date.now()}`;
110
+ this.pendingCorrelations.set(correlationId, {
111
+ action,
112
+ timestamp: Date.now(),
113
+ });
114
+ setTimeout(() => {
115
+ this.pendingCorrelations.delete(correlationId);
116
+ }, CORRELATION_TIMEOUT_MS);
117
+ return correlationId;
118
+ }
119
+ findCorrelation() {
120
+ const entries = Array.from(this.pendingCorrelations.entries());
121
+ return entries.length > 0 ? entries[entries.length - 1][0] : undefined;
122
+ }
123
+ findEffectNameForAction(actionType) {
124
+ for (let i = this.effectTimeline.length - 1; i >= Math.max(0, this.effectTimeline.length - 10); i--) {
125
+ const effect = this.effectTimeline[i];
126
+ if (effect.resultAction === actionType) {
127
+ return effect.effectName;
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+ ngOnDestroy() {
133
+ this.destroy$.next();
134
+ this.destroy$.complete();
135
+ this.effectEvents$.complete();
136
+ }
137
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: EffectTrackerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
138
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: EffectTrackerService, providedIn: 'root' });
139
+ }
140
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: EffectTrackerService, decorators: [{
141
+ type: Injectable,
142
+ args: [{ providedIn: 'root' }]
143
+ }], ctorParameters: () => [] });
144
+
145
+ class WebSocketService {
146
+ platformId = inject(PLATFORM_ID);
147
+ isBrowser = isPlatformBrowser(this.platformId);
148
+ socket = null;
149
+ messageBuffer = [];
150
+ wsUrl = null;
151
+ initialized = false;
152
+ connectionState$ = new BehaviorSubject(false);
153
+ incomingMessages$ = new BehaviorSubject(null);
154
+ get isConnected() {
155
+ return this.connectionState$.getValue();
156
+ }
157
+ get connected$() {
158
+ return this.connectionState$.asObservable();
159
+ }
160
+ get messages$() {
161
+ return this.incomingMessages$.asObservable();
162
+ }
163
+ initialize(wsUrl = 'ws://localhost:4000') {
164
+ if (this.initialized) {
165
+ // If already initialized with same URL, skip
166
+ if (this.wsUrl === wsUrl) {
167
+ return;
168
+ }
169
+ // If different URL, close existing and reinitialize
170
+ this.close();
171
+ }
172
+ this.initialized = true;
173
+ this.wsUrl = wsUrl;
174
+ this.setupWebSocket(wsUrl);
175
+ }
176
+ send(message) {
177
+ const payload = JSON.stringify(message);
178
+ if (this.isConnected && this.socket?.readyState === WebSocket.OPEN) {
179
+ this.socket.send(payload);
180
+ }
181
+ else if (this.isBrowser) {
182
+ this.messageBuffer.push(payload);
183
+ }
184
+ }
185
+ sendRaw(payload) {
186
+ if (this.isConnected && this.socket?.readyState === WebSocket.OPEN) {
187
+ this.socket.send(payload);
188
+ }
189
+ else if (this.isBrowser) {
190
+ this.messageBuffer.push(payload);
191
+ }
192
+ }
193
+ ngOnDestroy() {
194
+ this.close();
195
+ this.connectionState$.complete();
196
+ this.incomingMessages$.complete();
197
+ }
198
+ close() {
199
+ this.socket?.close();
200
+ this.socket = null;
201
+ this.connectionState$.next(false);
202
+ this.initialized = false;
203
+ this.wsUrl = null;
204
+ }
205
+ setupWebSocket(wsUrl) {
206
+ if (!this.isBrowser) {
207
+ return;
208
+ }
209
+ this.socket = new WebSocket(wsUrl);
210
+ this.socket.onopen = () => {
211
+ this.connectionState$.next(true);
212
+ this.flushBuffer();
213
+ };
214
+ this.socket.onclose = () => {
215
+ this.connectionState$.next(false);
216
+ };
217
+ this.socket.onerror = (error) => {
218
+ console.warn('[NgRx DevTool] WebSocket error:', error);
219
+ };
220
+ this.socket.onmessage = (event) => {
221
+ try {
222
+ const message = JSON.parse(event.data);
223
+ this.incomingMessages$.next(message);
224
+ }
225
+ catch {
226
+ // Ignore non-JSON messages
227
+ }
228
+ };
229
+ }
230
+ flushBuffer() {
231
+ while (this.messageBuffer.length > 0 && this.isConnected) {
232
+ const message = this.messageBuffer.shift();
233
+ if (message && this.socket?.readyState === WebSocket.OPEN) {
234
+ this.socket.send(message);
235
+ }
236
+ }
237
+ }
238
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: WebSocketService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
239
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: WebSocketService, providedIn: 'root' });
240
+ }
241
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: WebSocketService, decorators: [{
242
+ type: Injectable,
243
+ args: [{ providedIn: 'root' }]
244
+ }] });
245
+
246
+ class ActionsInterceptorService {
247
+ actions$ = inject(Actions);
248
+ effectTracker = inject(EffectTrackerService);
249
+ webSocketService = inject(WebSocketService);
250
+ destroy$ = new Subject();
251
+ initialized = false;
252
+ initialize(wsUrl = 'ws://localhost:4000') {
253
+ if (this.initialized) {
254
+ return;
255
+ }
256
+ this.initialized = true;
257
+ this.webSocketService.initialize(wsUrl);
258
+ this.setupMessageListener();
259
+ this.setupActionInterception();
260
+ this.setupEffectEventForwarding();
261
+ }
262
+ getTimeline() {
263
+ return this.effectTracker.getTimeline();
264
+ }
265
+ clearTimeline() {
266
+ this.effectTracker.clearTimeline();
267
+ this.sendMessage({
268
+ type: 'TIMELINE_CLEARED',
269
+ timestamp: new Date().toISOString(),
270
+ });
271
+ }
272
+ ngOnDestroy() {
273
+ this.destroy$.next();
274
+ this.destroy$.complete();
275
+ }
276
+ setupMessageListener() {
277
+ this.webSocketService.messages$.pipe(takeUntil$1(this.destroy$), filter((message) => message !== null), tap((message) => {
278
+ if (message['type'] === 'CLEAR_REQUEST') {
279
+ this.effectTracker.clearTimeline();
280
+ }
281
+ })).subscribe();
282
+ }
283
+ setupActionInterception() {
284
+ this.actions$.pipe(takeUntil$1(this.destroy$), tap((action) => {
285
+ const tracked = this.effectTracker.trackAction(action);
286
+ const message = {
287
+ type: 'ACTION_TRACKED',
288
+ action: action.type,
289
+ payload: this.sanitizePayload(action),
290
+ isEffectResult: tracked.source === 'effect',
291
+ effectName: tracked.effectName,
292
+ correlationId: tracked.correlationId,
293
+ timestamp: new Date().toISOString(),
294
+ };
295
+ this.sendMessage(message);
296
+ })).subscribe();
297
+ }
298
+ setupEffectEventForwarding() {
299
+ this.effectTracker.effectEvents$.pipe(takeUntil$1(this.destroy$), tap((event) => {
300
+ const message = {
301
+ type: 'EFFECT_EVENT',
302
+ action: event.action?.type,
303
+ effectName: event.effectName,
304
+ effectEvent: {
305
+ name: event.effectName,
306
+ lifecycle: event.lifecycle,
307
+ duration: event.duration,
308
+ executionId: event.executionId,
309
+ dispatch: event.dispatch,
310
+ },
311
+ timestamp: new Date().toISOString(),
312
+ };
313
+ this.sendMessage(message);
314
+ })).subscribe({
315
+ error: (err) => console.error('[NgRx DevTool] Effect event forwarding error:', err)
316
+ });
317
+ }
318
+ sendMessage(message) {
319
+ this.webSocketService.send(message);
320
+ }
321
+ sanitizePayload(action) {
322
+ try {
323
+ JSON.stringify(action);
324
+ return action;
325
+ }
326
+ catch {
327
+ return { type: action.type, _note: 'Non-serializable payload' };
328
+ }
329
+ }
330
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: ActionsInterceptorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
331
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: ActionsInterceptorService, providedIn: 'root' });
332
+ }
333
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: ActionsInterceptorService, decorators: [{
334
+ type: Injectable,
335
+ args: [{ providedIn: 'root' }]
336
+ }] });
337
+
338
+ /** NgRx internal metadata key for createEffect() */
339
+ const CREATE_EFFECT_METADATA_KEY = '__@ngrx/effects_create__';
340
+ const REPLAY_BUFFER_SIZE = 100;
341
+ class DevToolsEffectSources extends EffectSources {
342
+ effectEvents$ = new ReplaySubject(REPLAY_BUFFER_SIZE);
343
+ registeredEffects = new Map();
344
+ executionCounter = 0;
345
+ constructor() {
346
+ super(inject(ErrorHandler), inject(EFFECTS_ERROR_HANDLER));
347
+ }
348
+ addEffects(effectSourceInstance) {
349
+ const sourceName = this.resolveSourceName(effectSourceInstance);
350
+ if (sourceName) {
351
+ const metadata = this.extractEffectMetadata(effectSourceInstance);
352
+ if (metadata.length > 0) {
353
+ this.registeredEffects.set(sourceName, metadata);
354
+ this.instrumentEffects(effectSourceInstance, sourceName, metadata);
355
+ }
356
+ }
357
+ super.addEffects(effectSourceInstance);
358
+ }
359
+ getRegisteredEffects() {
360
+ return this.registeredEffects;
361
+ }
362
+ ngOnDestroy() {
363
+ this.effectEvents$.complete();
364
+ }
365
+ instrumentEffects(instance, sourceName, metadata) {
366
+ for (const { propertyName, dispatch } of metadata) {
367
+ const original = instance[propertyName];
368
+ if (!original)
369
+ continue;
370
+ const effectName = `${sourceName}.${propertyName}`;
371
+ if (typeof original === 'function') {
372
+ const factory = original;
373
+ const wrapped = () => this.wrapEffectObservable(factory(), effectName, dispatch);
374
+ this.copyEffectMetadata(factory, wrapped);
375
+ instance[propertyName] = wrapped;
376
+ }
377
+ else if (this.isObservable(original)) {
378
+ const wrapped = this.wrapEffectObservable(original, effectName, dispatch);
379
+ this.copyEffectMetadata(original, wrapped);
380
+ instance[propertyName] = wrapped;
381
+ }
382
+ }
383
+ }
384
+ wrapEffectObservable(source$, effectName, dispatch) {
385
+ const [sourceName, propertyName] = this.parseEffectName(effectName);
386
+ return new Observable(subscriber => {
387
+ let lastEmitTime = Date.now();
388
+ const subscription = source$.pipe(tap$1({
389
+ next: (value) => {
390
+ const now = Date.now();
391
+ this.emitEvent({
392
+ effectName,
393
+ sourceName,
394
+ propertyName,
395
+ lifecycle: dispatch ? 'emitted' : 'executed',
396
+ action: dispatch ? value : undefined,
397
+ timestamp: now,
398
+ duration: now - lastEmitTime,
399
+ executionId: this.generateExecutionId(effectName),
400
+ dispatch,
401
+ });
402
+ lastEmitTime = now;
403
+ },
404
+ error: (err) => {
405
+ const now = Date.now();
406
+ this.emitEvent({
407
+ effectName,
408
+ sourceName,
409
+ propertyName,
410
+ lifecycle: 'error',
411
+ error: err,
412
+ timestamp: now,
413
+ duration: now - lastEmitTime,
414
+ executionId: this.generateExecutionId(effectName),
415
+ });
416
+ },
417
+ })).subscribe(subscriber);
418
+ return () => subscription.unsubscribe();
419
+ });
420
+ }
421
+ copyEffectMetadata(original, wrapped) {
422
+ const originalRecord = original;
423
+ const metadata = originalRecord[CREATE_EFFECT_METADATA_KEY];
424
+ if (metadata) {
425
+ Object.defineProperty(wrapped, CREATE_EFFECT_METADATA_KEY, {
426
+ value: metadata,
427
+ configurable: true,
428
+ });
429
+ }
430
+ }
431
+ extractEffectMetadata(instance) {
432
+ const metadata = [];
433
+ for (const propertyName of Object.getOwnPropertyNames(instance)) {
434
+ try {
435
+ const property = instance[propertyName];
436
+ const config = property?.[CREATE_EFFECT_METADATA_KEY];
437
+ if (config) {
438
+ metadata.push({
439
+ propertyName,
440
+ dispatch: config.dispatch !== false,
441
+ functional: config.functional === true,
442
+ useEffectsErrorHandler: config.useEffectsErrorHandler !== false,
443
+ });
444
+ }
445
+ }
446
+ catch {
447
+ // Ignore reflection errors for non-effect properties
448
+ }
449
+ }
450
+ return metadata;
451
+ }
452
+ resolveSourceName(instance) {
453
+ if (!instance)
454
+ return null;
455
+ if (typeof instance.ngrxOnIdentifyEffects === 'function') {
456
+ const id = instance.ngrxOnIdentifyEffects();
457
+ if (id)
458
+ return id;
459
+ }
460
+ const constructorName = instance.constructor?.name;
461
+ if (this.isValidClassName(constructorName)) {
462
+ return constructorName;
463
+ }
464
+ const proto = Object.getPrototypeOf(instance);
465
+ const protoName = proto?.constructor?.name;
466
+ if (this.isValidClassName(protoName)) {
467
+ return protoName;
468
+ }
469
+ const effectProps = Object.getOwnPropertyNames(instance)
470
+ .filter(p => p.endsWith('$') || p.startsWith('load') || p.startsWith('fetch'));
471
+ if (effectProps.length > 0) {
472
+ return `Effects(${effectProps.slice(0, 2).join(', ')}...)`;
473
+ }
474
+ return constructorName ?? 'UnknownEffect';
475
+ }
476
+ isValidClassName(name) {
477
+ return Boolean(name && name.length > 1 && name !== 'Object' && name !== 'Function');
478
+ }
479
+ isObservable(value) {
480
+ return Boolean(value && typeof value.subscribe === 'function');
481
+ }
482
+ parseEffectName(effectName) {
483
+ const [sourceName, propertyName = 'unknown'] = effectName.split('.');
484
+ return [sourceName, propertyName];
485
+ }
486
+ generateExecutionId(effectName) {
487
+ return `${effectName}_${++this.executionCounter}`;
488
+ }
489
+ emitEvent(event) {
490
+ this.effectEvents$.next(event);
491
+ }
492
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: DevToolsEffectSources, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
493
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: DevToolsEffectSources });
494
+ }
495
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: DevToolsEffectSources, decorators: [{
496
+ type: Injectable
497
+ }], ctorParameters: () => [] });
498
+
499
+ // Models
500
+
501
+ const ENTRIES_MAX_SIZE = 1000;
502
+ const ENTRIES_TRIM_SIZE = 500;
503
+ const DEFAULT_THRESHOLDS = {
504
+ maxReducerTime: 16,
505
+ maxStateSize: 5 * 1024 * 1024,
506
+ maxStateChangeSize: 1024 * 1024,
507
+ maxActionsPerSecond: 60,
508
+ maxPayloadSize: 100 * 1024,
509
+ };
510
+ class PerformanceTrackerService {
511
+ entries = [];
512
+ warnings = [];
513
+ platformId = inject(PLATFORM_ID);
514
+ injector = inject(Injector);
515
+ isBrowser = isPlatformBrowser(this.platformId);
516
+ thresholds = DEFAULT_THRESHOLDS;
517
+ firstActionTime = null;
518
+ measureRenderTime(actionType, reducer, callback) {
519
+ const startTime = performance.now();
520
+ const nextState = reducer();
521
+ if (!this.isBrowser) {
522
+ callback(0);
523
+ return nextState;
524
+ }
525
+ afterNextRender(() => {
526
+ const renderTime = parseFloat((performance.now() - startTime).toFixed(2));
527
+ const entry = {
528
+ actionType,
529
+ timestamp: Date.now(),
530
+ renderTime,
531
+ };
532
+ this.entries.push(entry);
533
+ if (this.firstActionTime === null) {
534
+ this.firstActionTime = entry.timestamp;
535
+ }
536
+ if (this.entries.length > ENTRIES_MAX_SIZE) {
537
+ this.entries = this.entries.slice(-ENTRIES_TRIM_SIZE);
538
+ }
539
+ callback(renderTime);
540
+ }, { injector: this.injector });
541
+ return nextState;
542
+ }
543
+ getEntries() {
544
+ return [...this.entries];
545
+ }
546
+ getStats() {
547
+ if (this.entries.length === 0) {
548
+ return {
549
+ avgRenderTime: 0,
550
+ maxRenderTime: 0,
551
+ slowestAction: null,
552
+ totalActions: 0,
553
+ };
554
+ }
555
+ const renderTimes = this.entries.map(e => e.renderTime);
556
+ const avgRenderTime = renderTimes.reduce((a, b) => a + b, 0) / renderTimes.length;
557
+ const maxRenderTime = Math.max(...renderTimes);
558
+ const slowestEntry = this.entries.reduce((prev, curr) => curr.renderTime > prev.renderTime ? curr : prev);
559
+ return {
560
+ avgRenderTime,
561
+ maxRenderTime,
562
+ slowestAction: slowestEntry.actionType,
563
+ totalActions: this.entries.length,
564
+ };
565
+ }
566
+ getAggregatedStats() {
567
+ const stats = this.getStats();
568
+ const actionTypeStats = this.getActionTypeStats();
569
+ const elapsedTime = this.firstActionTime
570
+ ? (Date.now() - this.firstActionTime) / 1000
571
+ : 1;
572
+ return {
573
+ totalActions: stats.totalActions,
574
+ avgReducerTime: stats.avgRenderTime,
575
+ maxReducerTime: stats.maxRenderTime,
576
+ slowestAction: stats.slowestAction,
577
+ currentStateSize: 0,
578
+ actionsPerSecond: stats.totalActions / Math.max(elapsedTime, 1),
579
+ performanceScore: this.calculatePerformanceScore(stats),
580
+ actionTypeStats,
581
+ };
582
+ }
583
+ getThresholds() {
584
+ return this.thresholds;
585
+ }
586
+ setThresholds(thresholds) {
587
+ this.thresholds = { ...this.thresholds, ...thresholds };
588
+ }
589
+ getWarningsSummary() {
590
+ const summaryMap = new Map();
591
+ for (const warning of this.warnings) {
592
+ const existing = summaryMap.get(warning.type);
593
+ if (existing) {
594
+ summaryMap.set(warning.type, {
595
+ ...existing,
596
+ count: existing.count + 1,
597
+ severity: this.maxSeverity(existing.severity, warning.severity),
598
+ });
599
+ }
600
+ else {
601
+ summaryMap.set(warning.type, {
602
+ type: warning.type,
603
+ count: 1,
604
+ severity: warning.severity,
605
+ });
606
+ }
607
+ }
608
+ return Array.from(summaryMap.values());
609
+ }
610
+ getSlowestRenders(limit = 10) {
611
+ return [...this.entries]
612
+ .sort((a, b) => b.renderTime - a.renderTime)
613
+ .slice(0, limit);
614
+ }
615
+ clear() {
616
+ this.entries = [];
617
+ this.warnings = [];
618
+ this.firstActionTime = null;
619
+ }
620
+ getActionTypeStats() {
621
+ const statsMap = new Map();
622
+ for (const entry of this.entries) {
623
+ const existing = statsMap.get(entry.actionType);
624
+ if (existing) {
625
+ existing.count++;
626
+ existing.totalTime += entry.renderTime;
627
+ existing.avgTime = existing.totalTime / existing.count;
628
+ existing.maxTime = Math.max(existing.maxTime, entry.renderTime);
629
+ existing.lastExecuted = entry.timestamp;
630
+ }
631
+ else {
632
+ statsMap.set(entry.actionType, {
633
+ count: 1,
634
+ totalTime: entry.renderTime,
635
+ avgTime: entry.renderTime,
636
+ maxTime: entry.renderTime,
637
+ lastExecuted: entry.timestamp,
638
+ });
639
+ }
640
+ }
641
+ return statsMap;
642
+ }
643
+ calculatePerformanceScore(stats) {
644
+ let score = 100;
645
+ if (stats.avgRenderTime > this.thresholds.maxReducerTime) {
646
+ score -= Math.min(30, (stats.avgRenderTime - this.thresholds.maxReducerTime) * 2);
647
+ }
648
+ if (stats.maxRenderTime > this.thresholds.maxReducerTime * 2) {
649
+ score -= Math.min(20, (stats.maxRenderTime - this.thresholds.maxReducerTime * 2) / 2);
650
+ }
651
+ return Math.max(0, Math.round(score));
652
+ }
653
+ maxSeverity(a, b) {
654
+ const order = { low: 0, medium: 1, high: 2 };
655
+ return order[a] >= order[b] ? a : b;
656
+ }
657
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PerformanceTrackerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
658
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PerformanceTrackerService, providedIn: 'root' });
659
+ }
660
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PerformanceTrackerService, decorators: [{
661
+ type: Injectable,
662
+ args: [{ providedIn: 'root' }]
663
+ }] });
664
+
665
+ var PerformanceWarningType;
666
+ (function (PerformanceWarningType) {
667
+ PerformanceWarningType["SLOW_REDUCER"] = "SLOW_REDUCER";
668
+ PerformanceWarningType["LARGE_STATE"] = "LARGE_STATE";
669
+ PerformanceWarningType["LARGE_STATE_CHANGE"] = "LARGE_STATE_CHANGE";
670
+ PerformanceWarningType["FREQUENT_ACTIONS"] = "FREQUENT_ACTIONS";
671
+ PerformanceWarningType["LARGE_PAYLOAD"] = "LARGE_PAYLOAD";
672
+ PerformanceWarningType["MEMORY_PRESSURE"] = "MEMORY_PRESSURE";
673
+ PerformanceWarningType["HIGH_RENDER_IMPACT"] = "HIGH_RENDER_IMPACT";
674
+ })(PerformanceWarningType || (PerformanceWarningType = {}));
675
+
676
+ class PerformanceAnalyzerService {
677
+ previousStats = null;
678
+ performanceTracker = inject(PerformanceTrackerService);
679
+ generateReport() {
680
+ const stats = this.performanceTracker.getAggregatedStats();
681
+ const recommendations = this.generateRecommendations(stats);
682
+ const hotspots = this.identifyHotspots(stats);
683
+ const trends = this.analyzeTrends(stats);
684
+ const report = {
685
+ timestamp: new Date().toISOString(),
686
+ overallScore: stats.performanceScore,
687
+ scoreGrade: this.getScoreGrade(stats.performanceScore),
688
+ stats,
689
+ recommendations,
690
+ hotspots,
691
+ trends,
692
+ };
693
+ this.previousStats = stats;
694
+ return report;
695
+ }
696
+ getQuickSummary() {
697
+ const stats = this.performanceTracker.getAggregatedStats();
698
+ const warnings = this.performanceTracker.getWarningsSummary();
699
+ let mainIssue = null;
700
+ const highSeverityWarning = warnings.find(w => w.severity === 'high');
701
+ if (highSeverityWarning) {
702
+ mainIssue = this.getWarningDescription(highSeverityWarning.type);
703
+ }
704
+ return {
705
+ score: stats.performanceScore,
706
+ grade: this.getScoreGrade(stats.performanceScore),
707
+ mainIssue,
708
+ actionCount: stats.totalActions,
709
+ avgReducerTime: stats.avgReducerTime,
710
+ };
711
+ }
712
+ analyzeActionType(actionType) {
713
+ const stats = this.performanceTracker.getAggregatedStats();
714
+ const actionStats = stats.actionTypeStats.get(actionType);
715
+ const thresholds = this.performanceTracker.getThresholds();
716
+ const issues = [];
717
+ const suggestions = [];
718
+ if (!actionStats) {
719
+ return { stats: undefined, isProblematic: false, issues, suggestions };
720
+ }
721
+ if (actionStats.avgTime > thresholds.maxReducerTime) {
722
+ issues.push(`Average execution time (${actionStats.avgTime.toFixed(2)}ms) exceeds threshold`);
723
+ suggestions.push('Consider optimizing the reducer logic for this action');
724
+ }
725
+ const totalActions = stats.totalActions;
726
+ const percentOfTotal = (actionStats.count / totalActions) * 100;
727
+ if (percentOfTotal > 30 && actionStats.count > 10) {
728
+ issues.push(`This action represents ${percentOfTotal.toFixed(1)}% of all actions`);
729
+ suggestions.push('Consider debouncing or batching this action if dispatched frequently');
730
+ }
731
+ if (actionStats.maxTime > actionStats.avgTime * 3 && actionStats.maxTime > thresholds.maxReducerTime) {
732
+ issues.push(`Occasional spikes in execution time (max: ${actionStats.maxTime.toFixed(2)}ms)`);
733
+ suggestions.push('Investigate what causes performance spikes - possibly large payload or complex state updates');
734
+ }
735
+ return {
736
+ stats: actionStats,
737
+ isProblematic: issues.length > 0,
738
+ issues,
739
+ suggestions,
740
+ };
741
+ }
742
+ generateRecommendations(stats) {
743
+ const recommendations = [];
744
+ const thresholds = this.performanceTracker.getThresholds();
745
+ const warnings = this.performanceTracker.getWarningsSummary();
746
+ if (stats.avgReducerTime > thresholds.maxReducerTime) {
747
+ recommendations.push({
748
+ category: 'reducer',
749
+ title: 'Optimize Reducer Performance',
750
+ description: `Your reducers are taking an average of ${stats.avgReducerTime.toFixed(2)}ms to execute. For smooth 60fps animations, reducers should complete in under ${thresholds.maxReducerTime}ms.`,
751
+ impact: stats.avgReducerTime > thresholds.maxReducerTime * 2 ? 'high' : 'medium',
752
+ codeExample: `import { createReducer, on } from '@ngrx/store';
753
+ import { produce } from 'immer';
754
+
755
+ on(someAction, (state, { items }) => ({
756
+ ...state,
757
+ items
758
+ }))`,
759
+ learnMoreUrl: 'https://ngrx.io/guide/store/reducers#reducer-functions',
760
+ });
761
+ }
762
+ if (stats.currentStateSize > thresholds.maxStateSize * 0.5) {
763
+ recommendations.push({
764
+ category: 'state',
765
+ title: 'Consider State Normalization',
766
+ description: `Your state size is ${this.formatBytes(stats.currentStateSize)}. Large states can slow down serialization and increase memory usage.`,
767
+ impact: stats.currentStateSize > thresholds.maxStateSize ? 'high' : 'medium',
768
+ codeExample: `{ users: [{ id: 1, posts: [{ id: 1, ... }] }] }
769
+
770
+ {
771
+ users: { ids: [1], entities: { 1: { id: 1 } } },
772
+ posts: { ids: [1], entities: { 1: { id: 1, userId: 1 } } }
773
+ }`,
774
+ learnMoreUrl: 'https://ngrx.io/guide/entity',
775
+ });
776
+ }
777
+ if (stats.actionsPerSecond > thresholds.maxActionsPerSecond * 0.5) {
778
+ recommendations.push({
779
+ category: 'actions',
780
+ title: 'Reduce Action Dispatch Frequency',
781
+ description: `You're dispatching ${stats.actionsPerSecond.toFixed(1)} actions per second. High frequency can cause performance issues.`,
782
+ impact: stats.actionsPerSecond > thresholds.maxActionsPerSecond ? 'high' : 'medium',
783
+ codeExample: `import { debounceTime } from 'rxjs/operators';
784
+
785
+ searchInput$.pipe(
786
+ debounceTime(300)
787
+ ).subscribe(term => {
788
+ this.store.dispatch(searchAction({ term }));
789
+ });
790
+
791
+ this.store.dispatch(batchUpdateAction({ items: allItems }));`,
792
+ });
793
+ }
794
+ if (stats.slowestAction && stats.maxReducerTime > thresholds.maxReducerTime * 2) {
795
+ recommendations.push({
796
+ category: 'reducer',
797
+ title: `Optimize "${stats.slowestAction}"`,
798
+ description: `This action has the highest execution time (${stats.maxReducerTime.toFixed(2)}ms). Focus optimization efforts here for maximum impact. Profile this specific action to identify bottlenecks.`,
799
+ impact: 'high',
800
+ });
801
+ }
802
+ const memoryWarnings = warnings.filter(w => w.type === PerformanceWarningType.MEMORY_PRESSURE);
803
+ if (memoryWarnings.length > 0) {
804
+ recommendations.push({
805
+ category: 'memory',
806
+ title: 'Address Memory Pressure',
807
+ description: 'Your application is using a significant portion of available memory. This can lead to garbage collection pauses.',
808
+ impact: 'high',
809
+ codeExample: `ngOnDestroy() {
810
+ this.store.dispatch(clearTemporaryData());
811
+ }
812
+
813
+ this.store.dispatch(loadPage({ page: 1, pageSize: 50 }));`,
814
+ });
815
+ }
816
+ if (stats.performanceScore >= 80 && recommendations.length === 0) {
817
+ recommendations.push({
818
+ category: 'general',
819
+ title: 'Performance is Good! 🎉',
820
+ description: 'Your NgRx store is performing well. Continue following best practices.',
821
+ impact: 'low',
822
+ });
823
+ }
824
+ return recommendations;
825
+ }
826
+ identifyHotspots(stats) {
827
+ const hotspots = [];
828
+ const totalTime = Array.from(stats.actionTypeStats.values())
829
+ .reduce((sum, s) => sum + s.totalTime, 0);
830
+ stats.actionTypeStats.forEach((actionStats, actionType) => {
831
+ const percentOfTotal = (actionStats.totalTime / totalTime) * 100;
832
+ const thresholds = this.performanceTracker.getThresholds();
833
+ let issue = '';
834
+ if (actionStats.avgTime > thresholds.maxReducerTime) {
835
+ issue = 'Slow reducer execution';
836
+ }
837
+ else if (percentOfTotal > 20 && actionStats.count > 5) {
838
+ issue = 'High time consumption';
839
+ }
840
+ else if (actionStats.maxTime > actionStats.avgTime * 5) {
841
+ issue = 'Inconsistent performance';
842
+ }
843
+ if (issue) {
844
+ hotspots.push({
845
+ actionType,
846
+ issue,
847
+ avgTime: actionStats.avgTime,
848
+ callCount: actionStats.count,
849
+ totalTime: actionStats.totalTime,
850
+ percentOfTotal,
851
+ });
852
+ }
853
+ });
854
+ return hotspots.sort((a, b) => b.totalTime - a.totalTime).slice(0, 5);
855
+ }
856
+ analyzeTrends(currentStats) {
857
+ if (!this.previousStats) {
858
+ return {
859
+ reducerTimesTrend: 'stable',
860
+ stateSizeTrend: 'stable',
861
+ actionFrequencyTrend: 'stable',
862
+ };
863
+ }
864
+ const prev = this.previousStats;
865
+ let reducerTimesTrend = 'stable';
866
+ const timeDiff = currentStats.avgReducerTime - prev.avgReducerTime;
867
+ if (timeDiff > 2)
868
+ reducerTimesTrend = 'degrading';
869
+ else if (timeDiff < -2)
870
+ reducerTimesTrend = 'improving';
871
+ let stateSizeTrend = 'stable';
872
+ const sizeDiff = currentStats.currentStateSize - prev.currentStateSize;
873
+ const sizeChangePercent = Math.abs(sizeDiff) / (prev.currentStateSize || 1) * 100;
874
+ if (sizeChangePercent > 10) {
875
+ stateSizeTrend = sizeDiff > 0 ? 'growing' : 'shrinking';
876
+ }
877
+ let actionFrequencyTrend = 'stable';
878
+ const freqDiff = currentStats.actionsPerSecond - prev.actionsPerSecond;
879
+ if (freqDiff > 5)
880
+ actionFrequencyTrend = 'increasing';
881
+ else if (freqDiff < -5)
882
+ actionFrequencyTrend = 'decreasing';
883
+ return {
884
+ reducerTimesTrend,
885
+ stateSizeTrend,
886
+ actionFrequencyTrend,
887
+ };
888
+ }
889
+ getScoreGrade(score) {
890
+ if (score >= 90)
891
+ return 'A';
892
+ if (score >= 80)
893
+ return 'B';
894
+ if (score >= 70)
895
+ return 'C';
896
+ if (score >= 60)
897
+ return 'D';
898
+ return 'F';
899
+ }
900
+ getWarningDescription(type) {
901
+ const descriptions = {
902
+ SLOW_REDUCER: 'Slow reducer execution',
903
+ LARGE_STATE: 'Large state size',
904
+ LARGE_STATE_CHANGE: 'Large state changes',
905
+ FREQUENT_ACTIONS: 'High action frequency',
906
+ LARGE_PAYLOAD: 'Large action payloads',
907
+ MEMORY_PRESSURE: 'Memory pressure',
908
+ };
909
+ return descriptions[type] || type;
910
+ }
911
+ formatBytes(bytes) {
912
+ if (bytes === 0)
913
+ return '0 B';
914
+ const k = 1024;
915
+ const sizes = ['B', 'KB', 'MB', 'GB'];
916
+ const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
917
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
918
+ }
919
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PerformanceAnalyzerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
920
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PerformanceAnalyzerService, providedIn: 'root' });
921
+ }
922
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PerformanceAnalyzerService, decorators: [{
923
+ type: Injectable,
924
+ args: [{ providedIn: 'root' }]
925
+ }] });
926
+
927
+ function estimateRenderImpact(prevState, nextState) {
928
+ const analysis = analyzeStateChange(prevState, nextState);
929
+ const factors = [];
930
+ let totalScore = 0;
931
+ if (analysis.rootPropertiesChanged > 0) {
932
+ const impact = Math.min(analysis.rootPropertiesChanged * 15, 40);
933
+ totalScore += impact;
934
+ factors.push({
935
+ name: 'Root State Changes',
936
+ description: `${analysis.rootPropertiesChanged} top-level state slice(s) changed`,
937
+ impact,
938
+ details: 'Each root change can trigger re-renders in components selecting that slice',
939
+ });
940
+ }
941
+ if (analysis.arrayChanges.length > 0) {
942
+ const largestArrayChange = Math.max(...analysis.arrayChanges.map(a => Math.abs(a.newLength - a.previousLength)));
943
+ const impact = Math.min(largestArrayChange * 2 + analysis.arrayChanges.length * 5, 50);
944
+ totalScore += impact;
945
+ factors.push({
946
+ name: 'Array Mutations',
947
+ description: `${analysis.arrayChanges.length} array(s) changed`,
948
+ impact,
949
+ details: analysis.arrayChanges.map(a => `${a.path}: ${a.previousLength} → ${a.newLength} items`).join(', '),
950
+ });
951
+ }
952
+ if (analysis.largeObjectChanges.length > 0) {
953
+ const totalSize = analysis.largeObjectChanges.reduce((sum, o) => sum + o.size, 0);
954
+ const impact = Math.min(totalSize / 10000 * 20, 30);
955
+ totalScore += impact;
956
+ factors.push({
957
+ name: 'Large Object Changes',
958
+ description: `${analysis.largeObjectChanges.length} large object(s) changed (${formatBytes(totalSize)})`,
959
+ impact,
960
+ details: 'Large objects can cause expensive diffing and template updates',
961
+ });
962
+ }
963
+ if (analysis.maxChangeDepth > 3) {
964
+ const impact = Math.min((analysis.maxChangeDepth - 3) * 5, 15);
965
+ totalScore += impact;
966
+ factors.push({
967
+ name: 'Deep State Changes',
968
+ description: `Changes at depth ${analysis.maxChangeDepth}`,
969
+ impact,
970
+ details: 'Deeply nested changes may indicate non-normalized state',
971
+ });
972
+ }
973
+ if (analysis.totalPropertiesChanged > 10) {
974
+ const impact = Math.min(analysis.totalPropertiesChanged / 5, 20);
975
+ totalScore += impact;
976
+ factors.push({
977
+ name: 'Property Change Volume',
978
+ description: `${analysis.totalPropertiesChanged} total properties changed`,
979
+ impact,
980
+ });
981
+ }
982
+ totalScore = Math.min(Math.round(totalScore), 100);
983
+ let level;
984
+ if (totalScore < 25)
985
+ level = 'low';
986
+ else if (totalScore < 50)
987
+ level = 'medium';
988
+ else if (totalScore < 75)
989
+ level = 'high';
990
+ else
991
+ level = 'critical';
992
+ const estimatedComponentsAffected = Math.ceil(analysis.rootPropertiesChanged * 2 +
993
+ analysis.arrayChanges.length * 3 +
994
+ analysis.largeObjectChanges.length);
995
+ const recommendations = generateRecommendations(analysis, factors);
996
+ return {
997
+ score: totalScore,
998
+ level,
999
+ estimatedComponentsAffected,
1000
+ factors,
1001
+ recommendations,
1002
+ };
1003
+ }
1004
+ function analyzeStateChange(prevState, nextState) {
1005
+ const analysis = {
1006
+ rootPropertiesChanged: 0,
1007
+ totalPropertiesChanged: 0,
1008
+ arrayChanges: [],
1009
+ largeObjectChanges: [],
1010
+ maxChangeDepth: 0,
1011
+ };
1012
+ if (!prevState || !nextState || typeof prevState !== 'object' || typeof nextState !== 'object') {
1013
+ return analysis;
1014
+ }
1015
+ const prevObj = prevState;
1016
+ const nextObj = nextState;
1017
+ const allKeys = new Set([...Object.keys(prevObj), ...Object.keys(nextObj)]);
1018
+ for (const key of allKeys) {
1019
+ if (prevObj[key] !== nextObj[key]) {
1020
+ analysis.rootPropertiesChanged++;
1021
+ analyzeDeep(prevObj[key], nextObj[key], key, 1, analysis);
1022
+ }
1023
+ }
1024
+ return analysis;
1025
+ }
1026
+ function analyzeDeep(prev, next, path, depth, analysis) {
1027
+ analysis.maxChangeDepth = Math.max(analysis.maxChangeDepth, depth);
1028
+ analysis.totalPropertiesChanged++;
1029
+ if (Array.isArray(prev) || Array.isArray(next)) {
1030
+ const prevArr = Array.isArray(prev) ? prev : [];
1031
+ const nextArr = Array.isArray(next) ? next : [];
1032
+ if (prevArr.length !== nextArr.length || prev !== next) {
1033
+ analysis.arrayChanges.push({
1034
+ path,
1035
+ previousLength: prevArr.length,
1036
+ newLength: nextArr.length,
1037
+ itemsAdded: Math.max(0, nextArr.length - prevArr.length),
1038
+ itemsRemoved: Math.max(0, prevArr.length - nextArr.length),
1039
+ });
1040
+ }
1041
+ return;
1042
+ }
1043
+ if (typeof next === 'object' && next !== null) {
1044
+ const size = estimateSize(next);
1045
+ if (size > 5000) {
1046
+ analysis.largeObjectChanges.push({
1047
+ path,
1048
+ size,
1049
+ changeType: prev === undefined || prev === null ? 'added' : 'modified',
1050
+ });
1051
+ }
1052
+ if (depth < 10) {
1053
+ const prevObj = (prev && typeof prev === 'object' ? prev : {});
1054
+ const nextObj = next;
1055
+ const allKeys = new Set([...Object.keys(prevObj), ...Object.keys(nextObj)]);
1056
+ for (const key of allKeys) {
1057
+ if (prevObj[key] !== nextObj[key]) {
1058
+ analyzeDeep(prevObj[key], nextObj[key], `${path}.${key}`, depth + 1, analysis);
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+ }
1064
+ function estimateSize(obj) {
1065
+ try {
1066
+ return JSON.stringify(obj).length;
1067
+ }
1068
+ catch {
1069
+ return 0;
1070
+ }
1071
+ }
1072
+ function formatBytes(bytes) {
1073
+ if (bytes < 1024)
1074
+ return `${bytes} B`;
1075
+ if (bytes < 1024 * 1024)
1076
+ return `${(bytes / 1024).toFixed(1)} KB`;
1077
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1078
+ }
1079
+ function generateRecommendations(analysis, factors) {
1080
+ const recommendations = [];
1081
+ const largeArrayChanges = analysis.arrayChanges.filter(a => a.newLength > 100);
1082
+ if (largeArrayChanges.length > 0) {
1083
+ recommendations.push('Consider using trackBy in *ngFor for large arrays to reduce DOM updates');
1084
+ recommendations.push('Use virtual scrolling (cdk-virtual-scroll-viewport) for lists with 100+ items');
1085
+ }
1086
+ if (analysis.largeObjectChanges.length > 0) {
1087
+ recommendations.push('Break down large state objects into smaller, normalized entities');
1088
+ recommendations.push('Use OnPush change detection strategy in components displaying this data');
1089
+ }
1090
+ if (analysis.maxChangeDepth > 4) {
1091
+ recommendations.push('Normalize deeply nested state using @ngrx/entity for better performance');
1092
+ }
1093
+ if (analysis.rootPropertiesChanged > 2) {
1094
+ recommendations.push('Consider batching related state updates into a single action');
1095
+ }
1096
+ const totalImpact = factors.reduce((sum, f) => sum + f.impact, 0);
1097
+ if (totalImpact > 50) {
1098
+ recommendations.push('Use memoized selectors to prevent unnecessary recomputations');
1099
+ recommendations.push('Consider using component-level state for frequently changing UI state');
1100
+ }
1101
+ return recommendations;
1102
+ }
1103
+
1104
+ class SelectorTrackerService {
1105
+ selectorMetrics = new Map();
1106
+ recentInvocations = [];
1107
+ pendingAction = null;
1108
+ endToEndTimings = [];
1109
+ markActionDispatch(actionType) {
1110
+ this.pendingAction = {
1111
+ type: actionType,
1112
+ dispatchTime: performance.now(),
1113
+ };
1114
+ }
1115
+ markReducerComplete(reducerTime) {
1116
+ if (this.pendingAction) {
1117
+ this.pendingAction.reducerCompleteTime = performance.now();
1118
+ this.pendingAction.reducerTime = reducerTime;
1119
+ }
1120
+ }
1121
+ recordSelectorInvocation(selectorName, computationTime, wasRecomputed, inputChanged) {
1122
+ const invocation = {
1123
+ selectorName,
1124
+ timestamp: Date.now(),
1125
+ computationTime,
1126
+ wasRecomputed,
1127
+ inputChanged,
1128
+ triggeringAction: this.pendingAction?.type,
1129
+ };
1130
+ this.recentInvocations.push(invocation);
1131
+ const existing = this.selectorMetrics.get(selectorName);
1132
+ if (existing) {
1133
+ existing.invocationCount++;
1134
+ if (wasRecomputed) {
1135
+ existing.recomputationCount++;
1136
+ existing.totalComputationTime += computationTime;
1137
+ existing.avgComputationTime = existing.totalComputationTime / existing.recomputationCount;
1138
+ existing.maxComputationTime = Math.max(existing.maxComputationTime, computationTime);
1139
+ existing.lastComputationTime = computationTime;
1140
+ }
1141
+ existing.cacheHitRate = ((existing.invocationCount - existing.recomputationCount) / existing.invocationCount) * 100;
1142
+ existing.lastInvoked = Date.now();
1143
+ if (this.pendingAction?.type && !existing.triggeringActions.includes(this.pendingAction.type)) {
1144
+ existing.triggeringActions.push(this.pendingAction.type);
1145
+ if (existing.triggeringActions.length > 10) {
1146
+ existing.triggeringActions.shift();
1147
+ }
1148
+ }
1149
+ }
1150
+ else {
1151
+ this.selectorMetrics.set(selectorName, {
1152
+ name: selectorName,
1153
+ invocationCount: 1,
1154
+ recomputationCount: wasRecomputed ? 1 : 0,
1155
+ totalComputationTime: wasRecomputed ? computationTime : 0,
1156
+ avgComputationTime: wasRecomputed ? computationTime : 0,
1157
+ maxComputationTime: wasRecomputed ? computationTime : 0,
1158
+ lastComputationTime: wasRecomputed ? computationTime : 0,
1159
+ cacheHitRate: wasRecomputed ? 0 : 100,
1160
+ lastInvoked: Date.now(),
1161
+ triggeringActions: this.pendingAction?.type ? [this.pendingAction.type] : [],
1162
+ });
1163
+ }
1164
+ if (this.recentInvocations.length > 500) {
1165
+ this.recentInvocations = this.recentInvocations.slice(-250);
1166
+ }
1167
+ }
1168
+ markSelectorsComplete() {
1169
+ if (!this.pendingAction)
1170
+ return null;
1171
+ const now = performance.now();
1172
+ const affectedSelectors = this.recentInvocations
1173
+ .filter(inv => inv.triggeringAction === this.pendingAction?.type)
1174
+ .map(inv => inv.selectorName)
1175
+ .filter((name, index, arr) => arr.indexOf(name) === index);
1176
+ const timing = {
1177
+ actionType: this.pendingAction.type,
1178
+ actionDispatchTime: this.pendingAction.dispatchTime,
1179
+ reducerCompleteTime: this.pendingAction.reducerCompleteTime ?? now,
1180
+ selectorsCompleteTime: now,
1181
+ totalTime: now - this.pendingAction.dispatchTime,
1182
+ reducerTime: this.pendingAction.reducerTime ?? 0,
1183
+ selectorTime: now - (this.pendingAction.reducerCompleteTime ?? this.pendingAction.dispatchTime),
1184
+ affectedSelectors,
1185
+ };
1186
+ this.endToEndTimings.push(timing);
1187
+ if (this.endToEndTimings.length > 100) {
1188
+ this.endToEndTimings = this.endToEndTimings.slice(-50);
1189
+ }
1190
+ this.pendingAction = null;
1191
+ return timing;
1192
+ }
1193
+ getAllMetrics() {
1194
+ return new Map(this.selectorMetrics);
1195
+ }
1196
+ getMetrics(selectorName) {
1197
+ return this.selectorMetrics.get(selectorName);
1198
+ }
1199
+ getSlowestSelectors(limit = 10) {
1200
+ return Array.from(this.selectorMetrics.values())
1201
+ .sort((a, b) => b.totalComputationTime - a.totalComputationTime)
1202
+ .slice(0, limit);
1203
+ }
1204
+ getInefficientSelectors(cacheHitThreshold = 50) {
1205
+ return Array.from(this.selectorMetrics.values())
1206
+ .filter(m => m.cacheHitRate < cacheHitThreshold && m.invocationCount > 5)
1207
+ .sort((a, b) => a.cacheHitRate - b.cacheHitRate);
1208
+ }
1209
+ getEndToEndTimings() {
1210
+ return [...this.endToEndTimings];
1211
+ }
1212
+ getAverageEndToEndTime() {
1213
+ if (this.endToEndTimings.length === 0)
1214
+ return 0;
1215
+ const total = this.endToEndTimings.reduce((sum, t) => sum + t.totalTime, 0);
1216
+ return total / this.endToEndTimings.length;
1217
+ }
1218
+ clear() {
1219
+ this.selectorMetrics.clear();
1220
+ this.recentInvocations = [];
1221
+ this.endToEndTimings = [];
1222
+ this.pendingAction = null;
1223
+ }
1224
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: SelectorTrackerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1225
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: SelectorTrackerService, providedIn: 'root' });
1226
+ }
1227
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: SelectorTrackerService, decorators: [{
1228
+ type: Injectable,
1229
+ args: [{ providedIn: 'root' }]
1230
+ }] });
1231
+
1232
+ let globalSelectorTracker = null;
1233
+ function setSelectorTracker(tracker) {
1234
+ globalSelectorTracker = tracker;
1235
+ }
1236
+ function trackSelector(name, selector) {
1237
+ let lastResult;
1238
+ let lastInput;
1239
+ const trackedSelector = ((state) => {
1240
+ const startTime = performance.now();
1241
+ const result = selector(state);
1242
+ const endTime = performance.now();
1243
+ const computationTime = endTime - startTime;
1244
+ const wasRecomputed = result !== lastResult;
1245
+ const inputChanged = state !== lastInput;
1246
+ lastResult = result;
1247
+ lastInput = state;
1248
+ if (globalSelectorTracker) {
1249
+ globalSelectorTracker.recordSelectorInvocation(name, computationTime, wasRecomputed, inputChanged);
1250
+ }
1251
+ return result;
1252
+ });
1253
+ trackedSelector.release = selector.release;
1254
+ trackedSelector.projector = selector.projector;
1255
+ trackedSelector.setResult = selector.setResult;
1256
+ trackedSelector.clearResult = selector.clearResult;
1257
+ return trackedSelector;
1258
+ }
1259
+ function createTrackedSelector(name, ...args) {
1260
+ const selector = createSelector(...args);
1261
+ return trackSelector(name, selector);
1262
+ }
1263
+ function TrackedSelector(name) {
1264
+ return function (target, propertyKey) {
1265
+ const originalSelector = target[propertyKey];
1266
+ if (originalSelector) {
1267
+ target[propertyKey] = trackSelector(name, originalSelector);
1268
+ }
1269
+ };
1270
+ }
1271
+
1272
+ function createDevToolMetaReducer(wsUrlOrConfig = 'ws://localhost:4000') {
1273
+ const config = typeof wsUrlOrConfig === 'string'
1274
+ ? { wsUrl: wsUrlOrConfig }
1275
+ : wsUrlOrConfig;
1276
+ const wsUrl = config.wsUrl ?? 'ws://localhost:4000';
1277
+ const enablePerf = config.enablePerformanceTracking ?? true;
1278
+ return function devToolMetaReducer(reducer) {
1279
+ const performanceTracker = inject(PerformanceTrackerService);
1280
+ const webSocketService = inject(WebSocketService);
1281
+ // Initialize WebSocket connection (no-op if already initialized)
1282
+ webSocketService.initialize(wsUrl);
1283
+ return function (state, action) {
1284
+ const prevState = state;
1285
+ const timestamp = new Date().toISOString();
1286
+ let nextState;
1287
+ if (enablePerf) {
1288
+ nextState = performanceTracker.measureRenderTime(action.type, () => reducer(state, action), (renderTime) => {
1289
+ const message = {
1290
+ type: 'STATE_CHANGE',
1291
+ action,
1292
+ prevState,
1293
+ nextState,
1294
+ timestamp,
1295
+ renderPerformance: { renderTime }
1296
+ };
1297
+ webSocketService.send(message);
1298
+ });
1299
+ }
1300
+ else {
1301
+ nextState = reducer(state, action);
1302
+ const message = {
1303
+ type: 'STATE_CHANGE',
1304
+ action,
1305
+ prevState,
1306
+ nextState,
1307
+ timestamp,
1308
+ };
1309
+ webSocketService.send(message);
1310
+ }
1311
+ return nextState;
1312
+ };
1313
+ };
1314
+ }
1315
+ function loggerMetaReducer(reducer) {
1316
+ return createDevToolMetaReducer('ws://localhost:4000')(reducer);
1317
+ }
1318
+
1319
+ function provideNgrxDevTool(config = {}) {
1320
+ const providers = [
1321
+ {
1322
+ provide: APP_INITIALIZER,
1323
+ useFactory: (interceptor) => () => {
1324
+ interceptor.initialize(config.wsUrl ?? 'ws://localhost:4000');
1325
+ },
1326
+ deps: [ActionsInterceptorService],
1327
+ multi: true,
1328
+ },
1329
+ ];
1330
+ if (config.trackEffects !== false) {
1331
+ providers.push({
1332
+ provide: EffectSources,
1333
+ useClass: DevToolsEffectSources,
1334
+ });
1335
+ }
1336
+ return providers;
1337
+ }
1338
+ function provideEffectTracking() {
1339
+ return {
1340
+ provide: EffectSources,
1341
+ useClass: DevToolsEffectSources,
1342
+ };
1343
+ }
1344
+
1345
+ /*
1346
+ * Public API Surface of ngrx-devtool
1347
+ */
1348
+
1349
+ /**
1350
+ * Generated bundle index. Do not edit.
1351
+ */
1352
+
1353
+ export { ActionsInterceptorService, DevToolsEffectSources, EffectTrackerService, PerformanceAnalyzerService, PerformanceTrackerService, PerformanceWarningType, SelectorTrackerService, TrackedSelector, WebSocketService, createDevToolMetaReducer, createTrackedSelector, estimateRenderImpact, loggerMetaReducer, provideEffectTracking, provideNgrxDevTool, setSelectorTracker, trackSelector };
1354
+ //# sourceMappingURL=amadeus-it-group-ngrx-devtool.mjs.map