From 34fec1dd61499cfbed15af8dfa3a69c2a647044c Mon Sep 17 00:00:00 2001 From: danielwiehl <daniel.wiehl@yahoo.com> Date: Tue, 6 Dec 2022 11:44:22 +0100 Subject: [PATCH] deps(workbench): update @scion/microfrontend-platform to version 1.0.0-rc.11 BREAKING CHANGE: Updating @scion/microfrontend-platform to version 1.0.0-rc.11 introduced a breaking change. More information on how to migrate can be found in the [changelog](https://github.com/SchweizerischeBundesbahnen/scion-microfrontend-platform/blob/master/docs/site/changelog/changelog.md) of the SCION Microfrontend Platform. *For Angular applications, we strongly recommend replacing zone-specific decorators for `MessageClient` and `IntentClient` with an `ObservableDecorator`. Otherwise, you may experience performance degradation due to frequent change detection cycles.* To migrate: - Remove decorators for `MessageClient` and `IntentClient`, including their registration in the bean manager (e.g., `NgZoneMessageClientDecorator` and `NgZoneIntentClientDecorator`). - Provide a `NgZoneObservableDecorator` and register it in the bean manager before starting the platform. Note to register it as a bean, not as a decorator. #### Example of an `ObservableDecorator` for Angular ```ts export class NgZoneObservableDecorator implements ObservableDecorator { constructor(private zone: NgZone) { } public decorate$<T>(source$: Observable<T>): Observable<T> { return new Observable<T>(observer => { const insideAngular = NgZone.isInAngularZone(); const subscription = source$ .pipe( subscribeInside(fn => this.zone.runOutsideAngular(fn)), observeInside(fn => insideAngular ? this.zone.run(fn) : this.zone.runOutsideAngular(fn)), ) .subscribe(observer); return () => subscription.unsubscribe(); }); } } ``` #### Example of Registering an `ObservableDecorator` in Angular ```ts const zone: NgZone = ...; // Register decorator Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(zone)}); // Connect to the host zone.runOutsideAngular(() => WorkbenchClient.connect(...)); ``` --- .../workbench-client/ng-zone-decorators.ts | 90 ------------------ .../ng-zone-observable-decorator.ts | 36 +++++++ .../workbench-microfrontend-support.ts | 16 ++-- ...devtools-capability-interceptor.service.ts | 2 +- .../src/environments/environment.ts | 2 +- .../src/environments/environment.vercel.ts | 2 +- package-lock.json | 14 +-- package.json | 2 +- projects/scion/workbench-client/package.json | 2 +- projects/scion/workbench/package.json | 2 +- ...rofrontend-platform-initializer.service.ts | 12 +-- .../initialization/ng-zone-decorators.ts | 94 ------------------- .../ng-zone-observable-decorator.ts | 37 ++++++++ .../workbench-microfrontend-support.ts | 5 +- 14 files changed, 99 insertions(+), 217 deletions(-) delete mode 100644 apps/workbench-client-testing-app/src/app/workbench-client/ng-zone-decorators.ts create mode 100644 apps/workbench-client-testing-app/src/app/workbench-client/ng-zone-observable-decorator.ts delete mode 100644 projects/scion/workbench/src/lib/microfrontend-platform/initialization/ng-zone-decorators.ts create mode 100644 projects/scion/workbench/src/lib/microfrontend-platform/initialization/ng-zone-observable-decorator.ts diff --git a/apps/workbench-client-testing-app/src/app/workbench-client/ng-zone-decorators.ts b/apps/workbench-client-testing-app/src/app/workbench-client/ng-zone-decorators.ts deleted file mode 100644 index 638ce94e6..000000000 --- a/apps/workbench-client-testing-app/src/app/workbench-client/ng-zone-decorators.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2018-2022 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {BeanDecorator} from '@scion/toolkit/bean-manager'; -import {Intent, IntentClient, IntentMessage, IntentSelector, MessageClient, PublishOptions, RequestOptions, TopicMessage} from '@scion/microfrontend-platform'; -import {Injectable, NgZone} from '@angular/core'; -import {MonoTypeOperatorFunction, Observable, pipe, Subscription} from 'rxjs'; -import {observeInside, subscribeInside} from '@scion/toolkit/operators'; - -/** - * Synchronizes Observable emissions of the {@link MessageClient} with the Angular zone. - */ -@Injectable() -export class NgZoneMessageClientDecorator implements BeanDecorator<MessageClient> { - - constructor(private _zone: NgZone) { - } - - public decorate(messageClient: MessageClient): MessageClient { - const zone = this._zone; - return new class implements MessageClient { - - public publish<T = any>(topic: string, message?: T, options?: PublishOptions): Promise<void> { - return messageClient.publish(topic, message, options); - } - - public request$<T>(topic: string, request?: any, options?: RequestOptions): Observable<TopicMessage<T>> { - return messageClient.request$<T>(topic, request, options).pipe(synchronizeWithAngular(zone)); - } - - public observe$<T>(topic: string): Observable<TopicMessage<T>> { - return messageClient.observe$<T>(topic).pipe(synchronizeWithAngular(zone)); - } - - public onMessage<IN = any, OUT = any>(topic: string, callback: (message: TopicMessage<IN>) => Observable<OUT> | Promise<OUT> | OUT | void): Subscription { - return messageClient.onMessage(topic, callback); - } - - public subscriberCount$(topic: string): Observable<number> { - return messageClient.subscriberCount$(topic).pipe(synchronizeWithAngular(zone)); - } - }; - } -} - -/** - * Synchronizes Observable emissions of the {@link IntentClient} with the Angular zone. - */ -@Injectable() -export class NgZoneIntentClientDecorator implements BeanDecorator<IntentClient> { - - constructor(private _zone: NgZone) { - } - - public decorate(intentClient: IntentClient): IntentClient { - const zone = this._zone; - return new class implements IntentClient { - - public publish<T = any>(intent: Intent, body?: T, options?: PublishOptions): Promise<void> { - return intentClient.publish(intent, body, options); - } - - public request$<T>(intent: Intent, body?: any, options?: RequestOptions): Observable<TopicMessage<T>> { - return intentClient.request$<T>(intent, body, options).pipe(synchronizeWithAngular(zone)); - } - - public observe$<T>(selector?: Intent): Observable<IntentMessage<T>> { - return intentClient.observe$<T>(selector).pipe(synchronizeWithAngular(zone)); - } - - public onIntent<IN = any, OUT = any>(selector: IntentSelector, callback: (intentMessage: IntentMessage<IN>) => Observable<OUT> | Promise<OUT> | OUT | void): Subscription { - return intentClient.onIntent(selector, callback); - } - }; - } -} - -function synchronizeWithAngular<T>(zone: NgZone): MonoTypeOperatorFunction<T> { - return pipe( - subscribeInside(continueFn => zone.runOutsideAngular(continueFn)), - observeInside(continueFn => zone.run(continueFn)), - ); -} diff --git a/apps/workbench-client-testing-app/src/app/workbench-client/ng-zone-observable-decorator.ts b/apps/workbench-client-testing-app/src/app/workbench-client/ng-zone-observable-decorator.ts new file mode 100644 index 000000000..e39b50935 --- /dev/null +++ b/apps/workbench-client-testing-app/src/app/workbench-client/ng-zone-observable-decorator.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018-2022 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Observable} from 'rxjs'; +import {NgZone} from '@angular/core'; +import {observeInside, subscribeInside} from '@scion/toolkit/operators'; +import {ObservableDecorator} from '@scion/microfrontend-platform'; + +/** + * Mirrors the source, but ensures subscription and emission {@link NgZone} to be identical. + */ +export class NgZoneObservableDecorator implements ObservableDecorator { + + constructor(private _zone: NgZone) { + } + + public decorate$<T>(source$: Observable<T>): Observable<T> { + return new Observable<T>(observer => { + const insideAngular = NgZone.isInAngularZone(); + const subscription = source$ + .pipe( + subscribeInside(fn => this._zone.runOutsideAngular(fn)), + observeInside(fn => insideAngular ? this._zone.run(fn) : this._zone.runOutsideAngular(fn)), + ) + .subscribe(observer); + return () => subscription.unsubscribe(); + }); + } +} diff --git a/apps/workbench-client-testing-app/src/app/workbench-client/workbench-microfrontend-support.ts b/apps/workbench-client-testing-app/src/app/workbench-client/workbench-microfrontend-support.ts index 014bf1308..22387cc7f 100644 --- a/apps/workbench-client-testing-app/src/app/workbench-client/workbench-microfrontend-support.ts +++ b/apps/workbench-client-testing-app/src/app/workbench-client/workbench-microfrontend-support.ts @@ -8,10 +8,10 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {APP_INITIALIZER, NgZone, Provider} from '@angular/core'; -import {APP_IDENTITY, ContextService, FocusMonitor, IntentClient, ManifestService, MessageClient, OutletRouter, PlatformPropertyService, PreferredSizeService} from '@scion/microfrontend-platform'; +import {APP_INITIALIZER, inject, NgZone, Provider} from '@angular/core'; +import {APP_IDENTITY, ContextService, FocusMonitor, IntentClient, ManifestService, MessageClient, ObservableDecorator, OutletRouter, PlatformPropertyService, PreferredSizeService} from '@scion/microfrontend-platform'; import {WorkbenchClient, WorkbenchMessageBoxService, WorkbenchNotificationService, WorkbenchPopup, WorkbenchPopupService, WorkbenchRouter, WorkbenchView} from '@scion/workbench-client'; -import {NgZoneIntentClientDecorator, NgZoneMessageClientDecorator} from './ng-zone-decorators'; +import {NgZoneObservableDecorator} from './ng-zone-observable-decorator'; import {Beans} from '@scion/toolkit/bean-manager'; import {environment} from '../../environments/environment'; @@ -27,11 +27,8 @@ export function provideWorkbenchClientInitializer(): Provider[] { { provide: APP_INITIALIZER, useFactory: connectToWorkbenchFn, - deps: [NgZoneMessageClientDecorator, NgZoneIntentClientDecorator, NgZone], multi: true, }, - NgZoneMessageClientDecorator, - NgZoneIntentClientDecorator, {provide: APP_IDENTITY, useFactory: () => Beans.get(APP_IDENTITY)}, {provide: MessageClient, useFactory: () => Beans.get(MessageClient)}, {provide: IntentClient, useFactory: () => Beans.get(IntentClient)}, @@ -53,11 +50,10 @@ export function provideWorkbenchClientInitializer(): Provider[] { /** * Connects this app to the workbench in the host app. */ -export function connectToWorkbenchFn(ngZoneMessageClientDecorator: NgZoneMessageClientDecorator, ngZoneIntentClientDecorator: NgZoneIntentClientDecorator, zone: NgZone): () => Promise<void> { +export function connectToWorkbenchFn(): () => Promise<void> { + const zone = inject(NgZone); return (): Promise<void> => { - Beans.registerDecorator(MessageClient, {useValue: ngZoneMessageClientDecorator}); - Beans.registerDecorator(IntentClient, {useValue: ngZoneIntentClientDecorator}); - // We connect to the host outside the Angular zone in order to avoid excessive change detection cycles of platform-internal subscriptions to global DOM events. + Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(zone)}); return zone.runOutsideAngular(() => WorkbenchClient.connect(determineAppSymbolicName())); }; } diff --git a/apps/workbench-testing-app/src/app/devtools/devtools-capability-interceptor.service.ts b/apps/workbench-testing-app/src/app/devtools/devtools-capability-interceptor.service.ts index 883cacd5e..bce503ddb 100644 --- a/apps/workbench-testing-app/src/app/devtools/devtools-capability-interceptor.service.ts +++ b/apps/workbench-testing-app/src/app/devtools/devtools-capability-interceptor.service.ts @@ -17,7 +17,7 @@ import {Beans} from '@scion/toolkit/bean-manager'; /** * Qualifier of the SCION DevTools view capability. */ -const DEVTOOLS_QUALIFIER_MATCHER = new QualifierMatcher({component: 'devtools', vendor: 'scion'}, {evalAsterisk: false, evalOptional: false}); +const DEVTOOLS_QUALIFIER_MATCHER = new QualifierMatcher({component: 'devtools', vendor: 'scion'}); /** * Intercepts the DevTools view capability to pin it to the start page. diff --git a/apps/workbench-testing-app/src/environments/environment.ts b/apps/workbench-testing-app/src/environments/environment.ts index 8e429712d..df11088f8 100644 --- a/apps/workbench-testing-app/src/environments/environment.ts +++ b/apps/workbench-testing-app/src/environments/environment.ts @@ -31,7 +31,7 @@ const microfrontendPlatformConfig: MicrofrontendPlatformConfig = { applications: [ {symbolicName: 'workbench-client-testing-app1', manifestUrl: 'http://localhost:4201/assets/manifest-app1.json', intentionRegisterApiDisabled: false}, {symbolicName: 'workbench-client-testing-app2', manifestUrl: 'http://localhost:4202/assets/manifest-app2.json', intentionRegisterApiDisabled: false}, - {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools-v1-0-0-rc-10.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, + {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools-v1-0-0-rc-11.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, ], properties: { 'workbench-client-testing-app1': { diff --git a/apps/workbench-testing-app/src/environments/environment.vercel.ts b/apps/workbench-testing-app/src/environments/environment.vercel.ts index 592dad864..7ff7a777a 100644 --- a/apps/workbench-testing-app/src/environments/environment.vercel.ts +++ b/apps/workbench-testing-app/src/environments/environment.vercel.ts @@ -22,7 +22,7 @@ const microfrontendPlatformConfig: MicrofrontendPlatformConfig = { applications: [ {symbolicName: 'workbench-client-testing-app1', manifestUrl: 'https://scion-workbench-client-testing-app1.vercel.app/assets/manifest-app1.json', intentionRegisterApiDisabled: false}, {symbolicName: 'workbench-client-testing-app2', manifestUrl: 'https://scion-workbench-client-testing-app2.vercel.app/assets/manifest-app2.json', intentionRegisterApiDisabled: false}, - {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools-v1-0-0-rc-10.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, + {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools-v1-0-0-rc-11.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, ], properties: { 'workbench-client-testing-app1': { diff --git a/package-lock.json b/package-lock.json index 7c3ffb6c1..1bbe7f9e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@angular/router": "14.2.3", "@scion/components": "14.0.2", "@scion/components.internal": "14.0.1", - "@scion/microfrontend-platform": "1.0.0-rc.10", + "@scion/microfrontend-platform": "1.0.0-rc.11", "@scion/toolkit": "1.3.1", "rxjs": "7.5.7", "tslib": "2.4.0", @@ -3305,9 +3305,9 @@ } }, "node_modules/@scion/microfrontend-platform": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@scion/microfrontend-platform/-/microfrontend-platform-1.0.0-rc.10.tgz", - "integrity": "sha512-+dMddXkRVhSZBBM9faQsCTxKsG1W8E35+h4EWcEilin5BYKoWeasENQXqf2ZHZcFc+Xlt4IDF5Iig+47vkvX1g==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@scion/microfrontend-platform/-/microfrontend-platform-1.0.0-rc.11.tgz", + "integrity": "sha512-hLVlBdVtsl5daTo+3yhKWwdOaDWFCrbAN/KUyRoMaaT4bVV2cxgQoWW2yf3zijBxZ9WsSnwMQPe9ZUzb4cX+yw==", "dependencies": { "tslib": "^2.3.0" }, @@ -20015,9 +20015,9 @@ } }, "@scion/microfrontend-platform": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@scion/microfrontend-platform/-/microfrontend-platform-1.0.0-rc.10.tgz", - "integrity": "sha512-+dMddXkRVhSZBBM9faQsCTxKsG1W8E35+h4EWcEilin5BYKoWeasENQXqf2ZHZcFc+Xlt4IDF5Iig+47vkvX1g==", + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@scion/microfrontend-platform/-/microfrontend-platform-1.0.0-rc.11.tgz", + "integrity": "sha512-hLVlBdVtsl5daTo+3yhKWwdOaDWFCrbAN/KUyRoMaaT4bVV2cxgQoWW2yf3zijBxZ9WsSnwMQPe9ZUzb4cX+yw==", "requires": { "tslib": "^2.3.0" } diff --git a/package.json b/package.json index 62856a67a..9453a5f6e 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@angular/router": "14.2.3", "@scion/components": "14.0.2", "@scion/components.internal": "14.0.1", - "@scion/microfrontend-platform": "1.0.0-rc.10", + "@scion/microfrontend-platform": "1.0.0-rc.11", "@scion/toolkit": "1.3.1", "rxjs": "7.5.7", "tslib": "2.4.0", diff --git a/projects/scion/workbench-client/package.json b/projects/scion/workbench-client/package.json index 82d5a704f..591237317 100644 --- a/projects/scion/workbench-client/package.json +++ b/projects/scion/workbench-client/package.json @@ -20,7 +20,7 @@ "peerDependencies": { "rxjs": "^7.5.0", "@scion/toolkit": "^1.0.0-0", - "@scion/microfrontend-platform": "^1.0.0-0" + "@scion/microfrontend-platform": "^1.0.0-rc.11" }, "keywords": [ "scion", diff --git a/projects/scion/workbench/package.json b/projects/scion/workbench/package.json index de1cfbd83..bd188c2d6 100644 --- a/projects/scion/workbench/package.json +++ b/projects/scion/workbench/package.json @@ -31,7 +31,7 @@ "@angular/router": "^14.0.0", "@scion/components": "^14.0.0", "@scion/toolkit": "^1.3.0", - "@scion/microfrontend-platform": "^1.0.0-rc.9", + "@scion/microfrontend-platform": "^1.0.0-rc.11", "@scion/workbench-client": "^1.0.0-0", "rxjs": "^7.5.0" }, diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/initialization/microfrontend-platform-initializer.service.ts b/projects/scion/workbench/src/lib/microfrontend-platform/initialization/microfrontend-platform-initializer.service.ts index cae3d1605..cbac74742 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/initialization/microfrontend-platform-initializer.service.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/initialization/microfrontend-platform-initializer.service.ts @@ -9,10 +9,10 @@ */ import {Injectable, Injector, NgZone, OnDestroy} from '@angular/core'; -import {CapabilityInterceptor, HostManifestInterceptor, IntentClient, IntentInterceptor, MessageClient, MicrofrontendPlatform, MicrofrontendPlatformConfig, Runlevel} from '@scion/microfrontend-platform'; +import {CapabilityInterceptor, HostManifestInterceptor, IntentInterceptor, MicrofrontendPlatform, MicrofrontendPlatformConfig, ObservableDecorator, Runlevel} from '@scion/microfrontend-platform'; import {Beans} from '@scion/toolkit/bean-manager'; import {Logger, LoggerNames} from '../../logging'; -import {NgZoneIntentClientDecorator, NgZoneMessageClientDecorator} from './ng-zone-decorators'; +import {NgZoneObservableDecorator} from './ng-zone-observable-decorator'; import {MICROFRONTEND_PLATFORM_POST_STARTUP, MICROFRONTEND_PLATFORM_PRE_STARTUP, runWorkbenchInitializers, WorkbenchInitializer} from '../../startup/workbench-initializer'; import {MicrofrontendPlatformConfigLoader} from '../microfrontend-platform-config-loader'; import {MicrofrontendViewIntentInterceptor} from '../routing/microfrontend-view-intent-interceptor.service'; @@ -29,8 +29,7 @@ export class MicrofrontendPlatformInitializer implements WorkbenchInitializer, O constructor(private _microfrontendPlatformConfigLoader: MicrofrontendPlatformConfigLoader, private _hostManifestInterceptor: WorkbenchHostManifestInterceptor, - private _ngZoneMessageClientDecorator: NgZoneMessageClientDecorator, - private _ngZoneIntentClientDecorator: NgZoneIntentClientDecorator, + private _ngZoneObservableDecorator: NgZoneObservableDecorator, private _microfrontendViewIntentInterceptor: MicrofrontendViewIntentInterceptor, private _microfrontendPopupIntentInterceptor: MicrofrontendPopupIntentInterceptor, private _microfrontendViewCapabilityInterceptor: MicrofrontendViewCapabilityInterceptor, @@ -61,9 +60,8 @@ export class MicrofrontendPlatformInitializer implements WorkbenchInitializer, O // Register host manifest interceptor for the workbench to register workbench-specific intentions and capabilities. Beans.register(HostManifestInterceptor, {useValue: this._hostManifestInterceptor, multi: true}); - // Synchronize emissions of messaging Observables with the Angular zone. - Beans.registerDecorator(MessageClient, {useValue: this._ngZoneMessageClientDecorator}); - Beans.registerDecorator(IntentClient, {useValue: this._ngZoneIntentClientDecorator}); + // Synchronize emissions of Observables exposed by the SCION Microfrontend Platform with the Angular zone. + Beans.register(ObservableDecorator, {useValue: this._ngZoneObservableDecorator}); // Register view intent interceptor to translate view intents into workbench router commands. Beans.register(IntentInterceptor, {useValue: this._microfrontendViewIntentInterceptor, multi: true}); diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/initialization/ng-zone-decorators.ts b/projects/scion/workbench/src/lib/microfrontend-platform/initialization/ng-zone-decorators.ts deleted file mode 100644 index d74272b3f..000000000 --- a/projects/scion/workbench/src/lib/microfrontend-platform/initialization/ng-zone-decorators.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2018-2022 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {BeanDecorator} from '@scion/toolkit/bean-manager'; -import {Intent, IntentClient, IntentMessage, IntentSelector, MessageClient, PublishOptions, RequestOptions, TopicMessage} from '@scion/microfrontend-platform'; -import {Injectable, NgZone} from '@angular/core'; -import {MonoTypeOperatorFunction, Observable, pipe, Subscription} from 'rxjs'; -import {observeInside, subscribeInside} from '@scion/toolkit/operators'; - -/** - * Synchronizes Observable emissions of the {@link MessageClient} with the Angular zone. - * - * Refer to https://scion-microfrontend-platform-developer-guide.vercel.app/#chapter:angular_integration_guide for more information. - */ -@Injectable() -export class NgZoneMessageClientDecorator implements BeanDecorator<MessageClient> { - - constructor(private _zone: NgZone) { - } - - public decorate(messageClient: MessageClient): MessageClient { - const zone = this._zone; - return new class implements MessageClient { - - public publish<T = any>(topic: string, message?: T, options?: PublishOptions): Promise<void> { - return messageClient.publish(topic, message, options); - } - - public request$<T>(topic: string, request?: any, options?: RequestOptions): Observable<TopicMessage<T>> { - return messageClient.request$<T>(topic, request, options).pipe(synchronizeWithAngular(zone)); - } - - public observe$<T>(topic: string): Observable<TopicMessage<T>> { - return messageClient.observe$<T>(topic).pipe(synchronizeWithAngular(zone)); - } - - public onMessage<IN = any, OUT = any>(topic: string, callback: (message: TopicMessage<IN>) => Observable<OUT> | Promise<OUT> | OUT | void): Subscription { - return messageClient.onMessage(topic, callback); - } - - public subscriberCount$(topic: string): Observable<number> { - return messageClient.subscriberCount$(topic).pipe(synchronizeWithAngular(zone)); - } - }; - } -} - -/** - * Synchronizes Observable emissions of the {@link IntentClient} with the Angular zone. - * - * Refer to https://scion-microfrontend-platform-developer-guide.vercel.app/#chapter:angular_integration_guide for more information. - */ -@Injectable() -export class NgZoneIntentClientDecorator implements BeanDecorator<IntentClient> { - - constructor(private _zone: NgZone) { - } - - public decorate(intentClient: IntentClient): IntentClient { - const zone = this._zone; - return new class implements IntentClient { - - public publish<T = any>(intent: Intent, body?: T, options?: PublishOptions): Promise<void> { - return intentClient.publish(intent, body, options); - } - - public request$<T>(intent: Intent, body?: any, options?: RequestOptions): Observable<TopicMessage<T>> { - return intentClient.request$<T>(intent, body, options).pipe(synchronizeWithAngular(zone)); - } - - public observe$<T>(selector?: Intent): Observable<IntentMessage<T>> { - return intentClient.observe$<T>(selector).pipe(synchronizeWithAngular(zone)); - } - - public onIntent<IN = any, OUT = any>(selector: IntentSelector, callback: (intentMessage: IntentMessage<IN>) => Observable<OUT> | Promise<OUT> | OUT | void): Subscription { - return intentClient.onIntent(selector, callback); - } - }; - } -} - -function synchronizeWithAngular<T>(zone: NgZone): MonoTypeOperatorFunction<T> { - return pipe( - subscribeInside(continueFn => zone.runOutsideAngular(continueFn)), - observeInside(continueFn => zone.run(continueFn)), - ); -} diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/initialization/ng-zone-observable-decorator.ts b/projects/scion/workbench/src/lib/microfrontend-platform/initialization/ng-zone-observable-decorator.ts new file mode 100644 index 000000000..c85445256 --- /dev/null +++ b/projects/scion/workbench/src/lib/microfrontend-platform/initialization/ng-zone-observable-decorator.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-2022 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Observable} from 'rxjs'; +import {Injectable, NgZone} from '@angular/core'; +import {observeInside, subscribeInside} from '@scion/toolkit/operators'; +import {ObservableDecorator} from '@scion/microfrontend-platform'; + +/** + * Mirrors the source, but ensures subscription and emission {@link NgZone} to be identical. + */ +@Injectable() +export class NgZoneObservableDecorator implements ObservableDecorator { + + constructor(private _zone: NgZone) { + } + + public decorate$<T>(source$: Observable<T>): Observable<T> { + return new Observable<T>(observer => { + const insideAngular = NgZone.isInAngularZone(); + const subscription = source$ + .pipe( + subscribeInside(fn => this._zone.runOutsideAngular(fn)), + observeInside(fn => insideAngular ? this._zone.run(fn) : this._zone.runOutsideAngular(fn)), + ) + .subscribe(observer); + return () => subscription.unsubscribe(); + }); + } +} diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/workbench-microfrontend-support.ts b/projects/scion/workbench/src/lib/microfrontend-platform/workbench-microfrontend-support.ts index 600eb9bc0..776e1b2a6 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/workbench-microfrontend-support.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/workbench-microfrontend-support.ts @@ -15,7 +15,7 @@ import {APP_IDENTITY, IntentClient, ManifestService, MessageClient, Microfronten import {MICROFRONTEND_PLATFORM_POST_STARTUP, WORKBENCH_STARTUP} from '../startup/workbench-initializer'; import {Beans} from '@scion/toolkit/bean-manager'; import {WorkbenchMessageBoxService, WorkbenchNotificationService, WorkbenchPopupService, WorkbenchRouter, ɵMicrofrontendRouteParams} from '@scion/workbench-client'; -import {NgZoneIntentClientDecorator, NgZoneMessageClientDecorator} from './initialization/ng-zone-decorators'; +import {NgZoneObservableDecorator} from './initialization/ng-zone-observable-decorator'; import {WorkbenchModuleConfig} from '../workbench-module-config'; import {MicrofrontendViewCommandHandler} from './microfrontend-view/microfrontend-view-command-handler.service'; import {MicrofrontendMessageBoxIntentHandler} from './microfrontend-message-box/microfrontend-message-box-intent-handler.service'; @@ -71,8 +71,7 @@ export function provideWorkbenchMicrofrontendSupport(workbenchModuleConfig: Work WorkbenchPopupService, WorkbenchMessageBoxService, WorkbenchNotificationService, - NgZoneMessageClientDecorator, - NgZoneIntentClientDecorator, + NgZoneObservableDecorator, WorkbenchHostManifestInterceptor, provideMicrofrontendRoutes(), provideMicrofrontendPlatformBeans(),