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,60 @@ 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+ const reducerEvents = getReducerEvents ( ) ;
39+ if ( useGlitchTracking ) {
40+ trackEvents ( reducerEvents . on ( ) ) ;
41+ } else {
42+ trackEvents ( inject ( Events ) . on ( ) ) ;
43+ }
1944 } ,
2045 } ) ;
2146}
47+
48+ /**
49+ * Returns the synchronous reducer event stream exposed by the dispatcher.
50+ *
51+ * NgRx's `Dispatcher` delivers events to `ReducerEvents` immediately but feeds
52+ * the public `Events` stream via `queueScheduler`. When `GlitchTrackerService`
53+ * captures the state change synchronously, the queued `Events` emission arrives
54+ * too late and DevTools records the update as `Store Update`. Tapping into the
55+ * reducer stream keeps event names and state changes aligned on the same tick.
56+ *
57+ * TODO(@ngrx): expose a synchronous events API (similar to what `withReducer` uses)
58+ * so consumers do not need to reach into dispatcher internals.
59+ */
60+ function getReducerEvents ( ) {
61+ type ReducerEventsLike = {
62+ on ( ) : ReturnType < Dispatcher [ 'events' ] [ 'on' ] > ;
63+ } ;
64+
65+ const dispatcher = inject ( Dispatcher ) as unknown as {
66+ reducerEvents : ReducerEventsLike ;
67+ } ;
68+ return dispatcher . reducerEvents ;
69+ }
0 commit comments