Skip to content

Commit

Permalink
feat(store-devtools): provide the ability to connect extension outsid…
Browse files Browse the repository at this point in the history
…e of Angular zone (#3970)

Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>

Closes #3839
  • Loading branch information
arturovt authored Jul 28, 2023
1 parent b8d9aaa commit 1ee80e5
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 6 deletions.
5 changes: 5 additions & 0 deletions modules/store-devtools/spec/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('StoreDevtoolsOptions', () => {
features: defaultFeatures,
trace: false,
traceLimit: 75,
connectOutsideZone: false,
});
});

Expand Down Expand Up @@ -67,6 +68,7 @@ describe('StoreDevtoolsOptions', () => {
features: {
test: true,
},
connectOutsideZone: false,
});
});

Expand All @@ -84,6 +86,7 @@ describe('StoreDevtoolsOptions', () => {
trace: false,
traceLimit: 75,
features: defaultFeatures,
connectOutsideZone: false,
});
});

Expand All @@ -107,6 +110,7 @@ describe('StoreDevtoolsOptions', () => {
export: true,
test: true,
},
connectOutsideZone: false,
});
});

Expand All @@ -133,6 +137,7 @@ describe('StoreDevtoolsOptions', () => {
},
trace: false,
traceLimit: 75,
connectOutsideZone: false,
});
});
});
7 changes: 7 additions & 0 deletions modules/store-devtools/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ export class StoreDevtoolsConfig {
* Maximum stack trace frames to be stored (in case trace option was provided as true).
*/
traceLimit?: number;

/**
* The property determines whether the extension connection is established within the
* Angular zone or not. It is set to `false` by default.
*/
connectOutsideZone?: boolean;
}

export const STORE_DEVTOOLS_CONFIG = new InjectionToken<StoreDevtoolsConfig>(
Expand Down Expand Up @@ -165,6 +171,7 @@ export function createConfig(
dispatch: true, // Dispatch custom actions or action creators
test: true, // Generate tests for the selected actions
},
connectOutsideZone: false,
};

const options =
Expand Down
39 changes: 36 additions & 3 deletions modules/store-devtools/src/devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@ngrx/store';
import {
merge,
MonoTypeOperatorFunction,
Observable,
Observer,
queueScheduler,
Expand All @@ -31,6 +32,7 @@ import {
} from './utils';
import { DevtoolsDispatcher } from './devtools-dispatcher';
import { PERFORM_ACTION } from './actions';
import { ZoneConfig, injectZoneConfig } from './zone-config';

@Injectable()
export class StoreDevtools implements Observer<any>, OnDestroy {
Expand Down Expand Up @@ -69,11 +71,20 @@ export class StoreDevtools implements Observer<any>, OnDestroy {

const liftedReducer$ = reducers$.pipe(map(liftReducer));

const zoneConfig = injectZoneConfig(config.connectOutsideZone!);

const liftedStateSubject = new ReplaySubject<LiftedState>(1);

this.liftedStateSubscription = liftedAction$
.pipe(
withLatestFrom(liftedReducer$),
// The extension would post messages back outside of the Angular zone
// because we call `connect()` wrapped with `runOutsideAngular`. We run change
// detection only once at the end after all the required asynchronous tasks have
// been processed (for instance, `setInterval` scheduled by the `timeout` operator).
// We have to re-enter the Angular zone before the `scan` since it runs the reducer
// which must be run within the Angular zone.
emitInZone(zoneConfig),
scan<
[any, ActionReducer<LiftedState, Actions.All>],
{
Expand Down Expand Up @@ -110,9 +121,11 @@ export class StoreDevtools implements Observer<any>, OnDestroy {
}
});

this.extensionStartSubscription = extension.start$.subscribe(() => {
this.refresh();
});
this.extensionStartSubscription = extension.start$
.pipe(emitInZone(zoneConfig))
.subscribe(() => {
this.refresh();
});

const liftedState$ =
liftedStateSubject.asObservable() as Observable<LiftedState>;
Expand Down Expand Up @@ -196,3 +209,23 @@ export class StoreDevtools implements Observer<any>, OnDestroy {
this.dispatch(new Actions.PauseRecording(status));
}
}

/**
* If the devtools extension is connected out of the Angular zone,
* this operator will emit all events within the zone.
*/
function emitInZone<T>({
ngZone,
connectOutsideZone,
}: ZoneConfig): MonoTypeOperatorFunction<T> {
return (source) =>
connectOutsideZone
? new Observable<T>((subscriber) =>
source.subscribe({
next: (value) => ngZone.run(() => subscriber.next(value)),
error: (error) => ngZone.run(() => subscriber.error(error)),
complete: () => ngZone.run(() => subscriber.complete()),
})
)
: source;
}
16 changes: 13 additions & 3 deletions modules/store-devtools/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
shouldFilterActions,
unliftState,
} from './utils';
import { injectZoneConfig } from './zone-config';

export const ExtensionActionTypes = {
START: 'START',
Expand Down Expand Up @@ -77,6 +78,8 @@ export class DevtoolsExtension {
actions$!: Observable<any>;
start$!: Observable<any>;

private zoneConfig = injectZoneConfig(this.config.connectOutsideZone!);

constructor(
@Inject(REDUX_DEVTOOLS_EXTENSION) devtoolsExtension: ReduxDevtoolsExtension,
@Inject(STORE_DEVTOOLS_CONFIG) private config: StoreDevtoolsConfig,
Expand Down Expand Up @@ -168,9 +171,16 @@ export class DevtoolsExtension {
}

return new Observable((subscriber) => {
const connection = this.devtoolsExtension.connect(
this.getExtensionConfig(this.config)
);
const connection = this.zoneConfig.connectOutsideZone
? // To reduce change detection cycles, we need to run the `connect` method
// outside of the Angular zone. The `connect` method adds a `message`
// event listener to communicate with an extension using `window.postMessage`
// and handle message events.
this.zoneConfig.ngZone.runOutsideAngular(() =>
this.devtoolsExtension.connect(this.getExtensionConfig(this.config))
)
: this.devtoolsExtension.connect(this.getExtensionConfig(this.config));

this.extensionConnection = connection;
connection.init();

Expand Down
10 changes: 10 additions & 0 deletions modules/store-devtools/src/zone-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NgZone, inject } from '@angular/core';

export type ZoneConfig =
| { connectOutsideZone: true; ngZone: NgZone }
| { connectOutsideZone: false; ngZone: null };

export function injectZoneConfig(connectOutsideZone: boolean) {
const ngZone = connectOutsideZone ? inject(NgZone) : null;
return { ngZone, connectOutsideZone } as ZoneConfig;
}

0 comments on commit 1ee80e5

Please sign in to comment.