Skip to content

Commit

Permalink
feat(platform): separate message and intent communication APIs
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Use `MessageClient` for topic-based messaging, and `IntentClient` for intent-based messaging

Note: The messaging protocol between host and client HAS NOT CHANGED. You can therefore independently upgrade host and clients to the new version.

#### Breaking changes in MessageClient
Moved or renamed the following methods:
- `MessageClient#onMessage$` -> `MessageClient.observe$`
- `MessageClient#issueIntent` -> `IntentClient.publish`
- `MessageClient#requestByIntent$` -> `IntentClient.request$`
- `MessageClient#onIntent$` -> `IntentClient.observe$`
- `MessageClient#isConnected` -> `MicrofrontendPlatform.isConnectedToHost`

Renamed options object of the following methods
- `MessageClient#request`: `MessageOptions` -> `RequestOptions`
- `IntentClient.publish`: `MessageOptions` -> `IntentOptions`
- `IntentClient.request`: `MessageOptions` -> `IntentOptions`

#### Breaking change for decorating MessageClient and IntentClient bean
For Angular developers, see [Preparing the MessageClient and IntentClient for use with Angular](https://scion-microfrontend-platform-developer-guide.now.sh/#chapter:angular-integration-guide:preparing-messaging-for-use-with-angular) how to decorate the `MessageClient` and `IntentClient` for making Observables to emit inside the Angular zone.

#### Breaking change for disabling messaging in tests
Messaging can now be deactivated via options object when starting the platform. Previously you had to register a `NullMessageClient` bean.
```MicrofrontendPlatform.connectToHost({messaging: {enabled: false}}```
  • Loading branch information
danielwiehl authored and efux committed Sep 25, 2020
1 parent 98bf890 commit 7610eb0
Show file tree
Hide file tree
Showing 52 changed files with 1,271 additions and 972 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class ActivatorModule {
const pingReply = `${symbolicName} [primary: ${activationContext.primary}, X-APP-NAME: ${activationContext.activator.properties['X-APP-NAME']}]`;

// Subscribe for ping requests.
Beans.get(MessageClient).onMessage$(TestingAppTopics.ActivatorPing)
Beans.get(MessageClient).observe$(TestingAppTopics.ActivatorPing)
.subscribe(pingRequest => {
Beans.get(MessageClient).publish(pingRequest.headers.get(MessageHeaders.ReplyTo), pingReply);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* SPDX-License-Identifier: EPL-2.0
*/

import { BeanDecorator, Beans, Intent, IntentMessage, MessageClient, MessageOptions, PublishOptions, TopicMessage } from '@scion/microfrontend-platform';
import { BeanDecorator, Beans, Intent, IntentClient, IntentMessage, IntentOptions, MessageClient, PublishOptions, RequestOptions, TopicMessage } from '@scion/microfrontend-platform';
import { MonoTypeOperatorFunction, Observable, Observer, TeardownLogic } from 'rxjs';
import { NgZone } from '@angular/core';

Expand Down Expand Up @@ -36,32 +36,51 @@ export class AngularZoneMessageClientDecorator implements BeanDecorator<MessageC
return messageClient.publish(topic, message, options); // Unlike Observables, Promises correctly synchronize with the Angular zone.
}

public request$<T>(topic: string, request?: any, options?: MessageOptions): Observable<TopicMessage<T>> {
public request$<T>(topic: string, request?: any, options?: RequestOptions): Observable<TopicMessage<T>> {
return messageClient.request$<T>(topic, request, options).pipe(emitInsideAngular(zone));
}

public onMessage$<T>(topic: string): Observable<TopicMessage<T>> {
return messageClient.onMessage$<T>(topic).pipe(emitInsideAngular(zone));
public observe$<T>(topic: string): Observable<TopicMessage<T>> {
return messageClient.observe$<T>(topic).pipe(emitInsideAngular(zone));
}

public issueIntent<T = any>(intent: Intent, body?: T, options?: MessageOptions): Promise<void> {
return messageClient.issueIntent(intent, body, options); // Unlike Observables, Promises correctly synchronize with the Angular zone.
public subscriberCount$(topic: string): Observable<number> {
return messageClient.subscriberCount$(topic).pipe(emitInsideAngular(zone));
}
};
}
}

public requestByIntent$<T>(intent: Intent, body?: any, options?: MessageOptions): Observable<TopicMessage<T>> {
return messageClient.requestByIntent$<T>(intent, body, options).pipe(emitInsideAngular(zone));
}
/**
* Proxies invocations to the {@link IntentClient}, making Observables to emit inside the Angular zone.
*
* Because Angular does not control the {@link Window} of the broker gateway, Angular does not notice when messages
* are received from the gateway, causing the application not being detected for changes.
*
* Alternatively, the patch 'zone-patch-rxjs' can be installed to make sure RxJS subscriptions and operators run in the correct zone.
* For more information, see https://github.com/angular/zone.js/blob/master/NON-STANDARD-APIS.md
*
* You can load the patch in the `app.module.ts` as following:
* ```
* import 'zone.js/dist/zone-patch-rxjs';
* ```
*/
export class AngularZoneIntentClientDecorator implements BeanDecorator<IntentClient> {

public decorate(intentClient: IntentClient): IntentClient {
const zone = Beans.get(NgZone);
return new class implements IntentClient { // tslint:disable-line:new-parens

public onIntent$<T>(selector?: Intent): Observable<IntentMessage<T>> {
return messageClient.onIntent$<T>(selector).pipe(emitInsideAngular(zone));
public publish<T = any>(intent: Intent, body?: T, options?: IntentOptions): Promise<void> {
return intentClient.publish(intent, body, options);
}

public subscriberCount$(topic: string): Observable<number> {
return messageClient.subscriberCount$(topic).pipe(emitInsideAngular(zone));
public request$<T>(intent: Intent, body?: any, options?: IntentOptions): Observable<TopicMessage<T>> {
return intentClient.request$<T>(intent, body, options).pipe(emitInsideAngular(zone)); // <3>
}

public isConnected(): Promise<boolean> {
return zone.run(() => messageClient.isConnected());
public observe$<T>(selector?: Intent): Observable<IntentMessage<T>> {
return intentClient.observe$<T>(selector).pipe(emitInsideAngular(zone)); // <3>
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* SPDX-License-Identifier: EPL-2.0
*/
import { Component, OnDestroy } from '@angular/core';
import { Beans, MessageClient, Qualifier, TopicMessage } from '@scion/microfrontend-platform';
import { Beans, IntentClient, MessageClient, Qualifier, TopicMessage } from '@scion/microfrontend-platform';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, finalize, startWith, takeUntil } from 'rxjs/operators';
Expand Down Expand Up @@ -47,6 +47,7 @@ export class PublishMessageComponent implements OnDestroy {

private _destroy$ = new Subject<void>();
private _messageClient: MessageClient;
private _intentClient: IntentClient;
private _requestResponseSubscription: Subscription;

public form: FormGroup;
Expand All @@ -57,6 +58,7 @@ export class PublishMessageComponent implements OnDestroy {

constructor(private _formBuilder: FormBuilder) {
this._messageClient = Beans.get(MessageClient);
this._intentClient = Beans.get(IntentClient);

this.form = this._formBuilder.group({
[FLAVOR]: new FormControl(MessagingFlavor.Topic, Validators.required),
Expand Down Expand Up @@ -88,7 +90,7 @@ export class PublishMessageComponent implements OnDestroy {
}

public onPublish(): void {
this.isTopicFlavor() ? this.publishMessageToTopic() : this.issueIntent();
this.isTopicFlavor() ? this.publishMessage() : this.publishIntent();
}

public isTopicFlavor(): boolean {
Expand Down Expand Up @@ -126,7 +128,7 @@ export class PublishMessageComponent implements OnDestroy {
});
}

private publishMessageToTopic(): void {
private publishMessage(): void {
const topic = this.form.get(DESTINATION).get(TOPIC).value;
const message = this.form.get(MESSAGE).value === '' ? undefined : this.form.get(MESSAGE).value;
const requestReply = this.form.get(REQUEST_REPLY).value;
Expand Down Expand Up @@ -159,7 +161,7 @@ export class PublishMessageComponent implements OnDestroy {
}
}

private issueIntent(): void {
private publishIntent(): void {
const type: string = this.form.get(DESTINATION).get(TYPE).value;
const qualifier: Qualifier = SciParamsEnterComponent.toParamsDictionary(this.form.get(DESTINATION).get(QUALIFIER) as FormArray);

Expand All @@ -171,15 +173,15 @@ export class PublishMessageComponent implements OnDestroy {
this.publishError = null;
try {
if (requestReply) {
this._requestResponseSubscription = this._messageClient.requestByIntent$({type, qualifier}, message, {headers: headers})
this._requestResponseSubscription = this._intentClient.request$({type, qualifier}, message, {headers: headers})
.pipe(finalize(() => this.markPublishing(true)))
.subscribe(
reply => this.replies.push(reply),
error => this.publishError = error,
);
}
else {
this._messageClient.issueIntent({type, qualifier}, message, {headers: headers})
this._intentClient.publish({type, qualifier}, message, {headers: headers})
.catch(error => {
this.publishError = error;
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* SPDX-License-Identifier: EPL-2.0
*/
import { Component, OnDestroy } from '@angular/core';
import { Beans, IntentMessage, MessageClient, MessageHeaders, Qualifier, TopicMessage } from '@scion/microfrontend-platform';
import { Beans, IntentClient, IntentMessage, MessageClient, MessageHeaders, Qualifier, TopicMessage } from '@scion/microfrontend-platform';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, finalize, startWith, takeUntil } from 'rxjs/operators';
Expand Down Expand Up @@ -39,6 +39,7 @@ export class ReceiveMessageComponent implements OnDestroy {

private _destroy$ = new Subject<void>();
private _messageClient: MessageClient;
private _intentClient: IntentClient;
private _subscription: Subscription;

public form: FormGroup;
Expand All @@ -49,6 +50,7 @@ export class ReceiveMessageComponent implements OnDestroy {

constructor(private _formBuilder: FormBuilder) {
this._messageClient = Beans.get(MessageClient);
this._intentClient = Beans.get(IntentClient);

this.form = this._formBuilder.group({
[FLAVOR]: new FormControl(MessagingFlavor.Topic, Validators.required),
Expand Down Expand Up @@ -83,7 +85,7 @@ export class ReceiveMessageComponent implements OnDestroy {
this.form.disable();
this.subscribeError = null;
try {
this._subscription = this._messageClient.onMessage$(this.form.get(DESTINATION).get(TOPIC).value)
this._subscription = this._messageClient.observe$(this.form.get(DESTINATION).get(TOPIC).value)
.pipe(finalize(() => this.form.enable()))
.subscribe(
message => this.messages.push(message),
Expand All @@ -103,7 +105,7 @@ export class ReceiveMessageComponent implements OnDestroy {
this.form.disable();
this.subscribeError = null;
try {
this._subscription = this._messageClient.onIntent$({type, qualifier})
this._subscription = this._intentClient.observe$({type, qualifier})
.pipe(finalize(() => this.form.enable()))
.subscribe(
message => this.messages.push(message),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
*/
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { AngularZoneMessageClientDecorator } from './angular-zone-message-client.decorator';
import { ApplicationConfig, Beans, Handler, IntentInterceptor, IntentMessage, MessageClient, MessageHeaders, MessageInterceptor, MicrofrontendPlatform, PlatformMessageClient, PlatformState, PlatformStates, TopicMessage } from '@scion/microfrontend-platform';
import { AngularZoneIntentClientDecorator, AngularZoneMessageClientDecorator } from './angular-zone-messaging-decorators';
import { ApplicationConfig, Beans, Handler, IntentClient, IntentInterceptor, IntentMessage, MessageClient, MessageHeaders, MessageInterceptor, MicrofrontendPlatform, PlatformState, PlatformStates, TopicMessage } from '@scion/microfrontend-platform';
import { environment } from '../environments/environment';
import { HashLocationStrategy, LocationStrategy } from '@angular/common';
import { ConsoleService } from './console/console.service';
Expand Down Expand Up @@ -47,7 +47,7 @@ export class PlatformInitializer implements OnDestroy {
Beans.get(PlatformState).whenState(PlatformStates.Starting).then(() => {
Beans.register(NgZone, {useValue: this._zone});
Beans.registerDecorator(MessageClient, {useClass: AngularZoneMessageClientDecorator});
Beans.registerDecorator(PlatformMessageClient, {useClass: AngularZoneMessageClientDecorator});
Beans.registerDecorator(IntentClient, {useClass: AngularZoneIntentClientDecorator});
});

// Read the config from the query params
Expand Down Expand Up @@ -90,6 +90,7 @@ export class PlatformInitializer implements OnDestroy {
Beans.get(PlatformState).whenState(PlatformStates.Starting).then(() => {
Beans.register(NgZone, {useValue: this._zone});
Beans.registerDecorator(MessageClient, {useClass: AngularZoneMessageClientDecorator});
Beans.registerDecorator(IntentClient, {useClass: AngularZoneIntentClientDecorator});
});

// Run the microfrontend platform as client app
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ACTIVATION_CONTEXT, ActivationContext, Beans, Capability, ContextService, Intent, IntentMessage, IntentSelector, ManifestService, MessageClient, OutletRouter } from '@scion/microfrontend-platform';
import { ACTIVATION_CONTEXT, ActivationContext, Beans, Capability, ContextService, Intent, IntentClient, IntentMessage, IntentSelector, ManifestService, MessageClient, OutletRouter } from '@scion/microfrontend-platform';

`
// tag::register-activator[]
Expand Down Expand Up @@ -127,7 +127,7 @@ import { ACTIVATION_CONTEXT, ActivationContext, Beans, Capability, ContextServic
qualifier: {entity: 'product', id: '*'},
};

Beans.get(MessageClient).onIntent$(selector).subscribe((message: IntentMessage) => {
Beans.get(IntentClient).observe$(selector).subscribe((message: IntentMessage) => {
const outlet = message.headers.get('outlet'); // <1>
const microfrontendPath = message.capability.properties.path; // <2>

Expand All @@ -148,6 +148,6 @@ import { ACTIVATION_CONTEXT, ActivationContext, Beans, Capability, ContextServic
};
const headers = new Map().set('outlet', 'aside'); // <2>

Beans.get(MessageClient).issueIntent(intent, null, {headers}); // <3>
Beans.get(IntentClient).publish(intent, null, {headers}); // <3>
// end::issue-microfrontend-intent[]
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This part explains how micro applications and microfrontends can communicate wit

Cross-application communication is an integral part when implementing a microfrontend architecture. By using the browser's native `postMessage()` mechanism, you can send messages to applications loaded from different domains. For posting a message, however, you need a reference to the `Window` of the receiving application, which can quickly become complicated, also due to restrictions imposed by the <<terminology:same-origin-policy,Same-origin Policy>>.

For that reason and because of the lack of message routing, the SCION Microfrontend Platform provides a Messaging API, allowing microfrontends to communicate with each other in an easy way on the client-side. Internally, it uses the `postMessage()` mechanism. The Messaging API offers publish/subscribe messaging to microfrontends in two flavors: <<chapter:topic-based-messaging>> and <<chapter:intent-based-messaging>>.
The SCION Microfrontend Platform provides a client-side Messaging API on top of the native `postMessage` mechanism to allow microfrontends to communicate with each other easily across origins. The Messaging API offers publish/subscribe messaging to microfrontends in two flavors: <<chapter:topic-based-messaging>> and <<chapter:intent-based-messaging>>.

NOTE: Data sent from one JavaScript realm to another is serialized with the _Structured Clone Algorithm_. The algorithm supports structured objects such as nested objects, arrays, and maps.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Beans, Intent, IntentMessage, IntentSelector, MessageClient, MessageHeaders, OutletRouter, PRIMARY_OUTLET, takeUntilUnsubscribe } from '@scion/microfrontend-platform';
import { Beans, Intent, IntentClient, IntentMessage, IntentSelector, MessageClient, MessageHeaders, OutletRouter, PRIMARY_OUTLET, takeUntilUnsubscribe } from '@scion/microfrontend-platform';
import { Subject } from 'rxjs';

`
Expand Down Expand Up @@ -45,7 +45,7 @@ import { Subject } from 'rxjs';
qualifier: {entity: 'product', id: '*'},
};

Beans.get(MessageClient).onIntent$(selector).subscribe((message: IntentMessage) => { // <2>
Beans.get(IntentClient).observe$(selector).subscribe((message: IntentMessage) => { // <2>
const microfrontendPath = message.capability.properties.path; // <3>

// Instruct the router to display the microfrontend in an outlet.
Expand All @@ -68,7 +68,7 @@ import { Subject } from 'rxjs';
};
const transferData = PRIMARY_OUTLET; // <2>

Beans.get(MessageClient).issueIntent(intent, transferData); // <3>
Beans.get(IntentClient).publish(intent, transferData); // <3>
// end::issue-intent[]
}

Expand All @@ -80,7 +80,7 @@ import { Subject } from 'rxjs';
qualifier: {entity: 'product', id: '3bca695e-411f-4e0e-908d-9568f1c61556'},
};

Beans.get(MessageClient).issueIntent(intent, null, {headers: headers});
Beans.get(IntentClient).publish(intent, null, {headers: headers});
// end::issue-intent-with-headers[]
}

Expand All @@ -91,7 +91,7 @@ import { Subject } from 'rxjs';
qualifier: {entity: 'product', id: '*'},
};

Beans.get(MessageClient).onIntent$(selector).subscribe((message: IntentMessage) => {
Beans.get(IntentClient).observe$(selector).subscribe((message: IntentMessage) => {
const outlet = message.headers.get('outlet'); // <1>
const microfrontendPath = message.capability.properties.path;

Expand All @@ -111,7 +111,7 @@ import { Subject } from 'rxjs';
qualifier: {entity: 'user-access-token'},
};

Beans.get(MessageClient).requestByIntent$(authTokenIntent).subscribe(reply => { // <1>
Beans.get(IntentClient).request$(authTokenIntent).subscribe(reply => { // <1>
console.log(`token: ${reply.body}`); // <2>
});
// end::request[]
Expand All @@ -128,7 +128,7 @@ import { Subject } from 'rxjs';
qualifier: {entity: 'user-access-token'},
};

Beans.get(MessageClient).onIntent$(selector).subscribe((request: IntentMessage) => {
Beans.get(IntentClient).observe$(selector).subscribe((request: IntentMessage) => {
const replyTo = request.headers.get(MessageHeaders.ReplyTo); // <1>
authService.userAccessToken$
.pipe(takeUntilUnsubscribe(replyTo)) // <3>
Expand Down
Loading

0 comments on commit 7610eb0

Please sign in to comment.