11import { inject } from '@angular/core' ;
2- import { Events } from '@ngrx/signals/events' ;
2+ import { Dispatcher , Events } from '@ngrx/signals/events' ;
33import { createDevtoolsFeature } from '../internal/devtools-feature' ;
4+ import { GlitchTrackerService } from '../internal/glitch-tracker.service' ;
45
56/**
67 * Automatically infers DevTools action names from NgRx SignalStore events.
@@ -9,13 +10,59 @@ import { createDevtoolsFeature } from '../internal/devtools-feature';
910 * the event's type as the upcoming DevTools action name. When the corresponding
1011 * reducer mutates state, the DevTools sync will use that name instead of
1112 * the default "Store Update".
13+ *
14+ * By default (withGlitchTracking = true), the `GlitchTrackerService` is used to capture
15+ * all intermediate updates (glitched states). To use the default, glitch-free tracker
16+ * and synchronize only stable state transitions, set `withGlitchTracking` to `false`.
17+ *
18+ * @param {{ withGlitchTracking?: boolean } } [options] Options to configure tracking behavior.
19+ * @param {boolean } [options.withGlitchTracking=true] Enable capturing intermediate (glitched) state updates.
20+ * @returns Devtools feature enabling events-based action naming; glitched tracking is enabled by default.
21+ * Set `withGlitchTracking: false` to use glitch-free tracking instead.
22+ * @example
23+ * // Capture intermediate updates (default)
24+ * withDevtools('counter', withEventsTracking());
25+ * @example
26+ * // Glitch-free tracking (only stable transitions)
27+ * withDevtools('counter', withEventsTracking({ withGlitchTracking: false }));
28+ * @see withGlitchTracking
1229 */
13- export function withEventsTracking ( ) {
30+ export function withEventsTracking (
31+ options : { withGlitchTracking : boolean } = { withGlitchTracking : true } ,
32+ ) {
33+ const useGlitchTracking = options . withGlitchTracking === true ;
1434 return createDevtoolsFeature ( {
35+ tracker : useGlitchTracking ? GlitchTrackerService : undefined ,
1536 eventsTracking : true ,
1637 onInit : ( { trackEvents } ) => {
17- const events = inject ( Events ) ;
18- trackEvents ( events . on ( ) ) ;
38+ if ( useGlitchTracking ) {
39+ trackEvents ( getReducerEvents ( ) . on ( ) ) ;
40+ } else {
41+ trackEvents ( inject ( Events ) . on ( ) ) ;
42+ }
1943 } ,
2044 } ) ;
2145}
46+
47+ /**
48+ * Returns the synchronous reducer event stream exposed by the dispatcher.
49+ *
50+ * NgRx's `Dispatcher` delivers events to `ReducerEvents` immediately but feeds
51+ * the public `Events` stream via `queueScheduler`. When `GlitchTrackerService`
52+ * captures the state change synchronously, the queued `Events` emission arrives
53+ * too late and DevTools records the update as `Store Update`. Tapping into the
54+ * reducer stream keeps event names and state changes aligned on the same tick.
55+ *
56+ * TODO(@ngrx): expose a synchronous events API (similar to what `withReducer` uses)
57+ * so consumers do not need to reach into dispatcher internals.
58+ */
59+ function getReducerEvents ( ) {
60+ type ReducerEventsLike = {
61+ on ( ) : ReturnType < Dispatcher [ 'events' ] [ 'on' ] > ;
62+ } ;
63+
64+ const dispatcher = inject ( Dispatcher ) as unknown as {
65+ reducerEvents : ReducerEventsLike ;
66+ } ;
67+ return dispatcher . reducerEvents ;
68+ }
0 commit comments