@scion/microfrontend-platform
Version:
SCION Microfrontend Platform enables you to successfully implement a framework-agnostic microfrontend architecture using iframes. It provides you fundamental APIs for microfrontends to communicate with each other across origins and facilitates embedding m
1,357 lines (1,338 loc) • 333 kB
JavaScript
import { Subject, race, fromEvent, BehaviorSubject, pipe, of, throwError, Observable, merge, NEVER, ReplaySubject, AsyncSubject, lastValueFrom, timeout, EMPTY, noop, firstValueFrom, timer, defer, filter as filter$1, concatWith, from, identity, retry, interval, switchMap, concat, combineLatest, withLatestFrom, Subscription } from 'rxjs';
import { take, takeUntil, first, mergeMap, takeWhile, filter, map, finalize, tap, expand, distinctUntilChanged, startWith, share, catchError, switchMap as switchMap$1, pairwise, skipWhile, auditTime, combineLatestWith } from 'rxjs/operators';
import { Beans } from '@scion/toolkit/bean-manager';
import { Arrays, Maps, Dictionaries, Defined } from '@scion/toolkit/util';
import { UUID } from '@scion/toolkit/uuid';
import { filterArray, bufferUntil, mapArray } from '@scion/toolkit/operators';
import { fromResize$ } from '@scion/toolkit/observable';
/*
* Copyright (c) 2018-2020 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
*/
/**
* Lifecycle states of the microfrontend platform.
*
* @category Platform
*/
var PlatformState;
(function (PlatformState) {
/**
* Indicates that the platform is about to start.
*/
PlatformState[PlatformState["Starting"] = 1] = "Starting";
/**
* Indicates that the platform started.
*/
PlatformState[PlatformState["Started"] = 2] = "Started";
/**
* Indicates that the platform is about to stop.
*/
PlatformState[PlatformState["Stopping"] = 3] = "Stopping";
/**
* Indicates that the platform is not yet started.
*/
PlatformState[PlatformState["Stopped"] = 4] = "Stopped";
})(PlatformState || (PlatformState = {}));
/**
* Runlevels are used to control in which startup phase to execute initializers when starting the platform.
*
* The platform reports that it has started after all initializers have completed successfully.
*
* @internal
*/
var Runlevel;
(function (Runlevel) {
/**
* In runlevel 0, the platform host fetches manifests of registered micro applications.
*/
Runlevel[Runlevel["Zero"] = 0] = "Zero";
/**
* In runlevel 1, the platform constructs eager beans.
*/
Runlevel[Runlevel["One"] = 1] = "One";
/**
* From runlevel 2 and above, messaging is enabled. This is the default runlevel at which initializers execute if not specifying any runlevel.
*/
Runlevel[Runlevel["Two"] = 2] = "Two";
/**
* In runlevel 3, the platform host installs activator microfrontends.
*/
Runlevel[Runlevel["Three"] = 3] = "Three";
})(Runlevel || (Runlevel = {}));
/*
* Copyright (c) 2018-2020 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
*/
/**
* Built in capability types.
*
* @category Intention API
*/
var PlatformCapabilityTypes;
(function (PlatformCapabilityTypes) {
/**
* Type for registering an activator capability.
*
* @see ActivatorCapability
*/
PlatformCapabilityTypes["Activator"] = "activator";
/**
* Type for registering a microfrontend capability.
*
* @see MicrofrontendCapability
*/
PlatformCapabilityTypes["Microfrontend"] = "microfrontend";
})(PlatformCapabilityTypes || (PlatformCapabilityTypes = {}));
/**
* Symbol to determine if this app instance is running as the platform host.
*
* ```ts
* const isPlatformHost: boolean = Beans.get(IS_PLATFORM_HOST);
* ```
*
* @category Platform
*/
const IS_PLATFORM_HOST = Symbol('IS_PLATFORM_HOST');
/**
* Symbol to get the application's symbolic name from the bean manager.
*
* @category Platform
*/
const APP_IDENTITY = Symbol('APP_IDENTITY');
/**
* Key for obtaining the current activation context using {@link ContextService}.
*
* The activation context is only available to microfrontends loaded by an activator.
*
* @see {@link ActivationContext}
* @see {@link ContextService}
* @category Platform
*/
const ACTIVATION_CONTEXT = 'ɵACTIVATION_CONTEXT';
/*
* 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
*/
/**
* Symbol to get the version of the SCION Microfrontend Platform.
*
* @internal
*/
const ɵVERSION = Symbol('ɵVERSION');
/**
* Symbol to get the topmost window in the window hierarchy from the bean manager.
*
* Alias for `window.top` that can be overridden in tests, e.g., to simulate
* the client to connect to a remote host.
*
* @internal
*/
const ɵWINDOW_TOP = Symbol('ɵWINDOW_TOP');
/**
* Stops the platform and disconnects this client from the host when the browser unloads the document.
*
* By default, the platform initiates shutdown when the browser unloads the document, i.e., when `beforeunload` is triggered.
* The main reason for `beforeunload` instead of `unload` is to avoid posting messages to disposed windows.
* However, if `beforeunload` is not triggered, e.g., when an iframe is removed, we fall back to `unload`.
*
* @category Platform
*/
class MicrofrontendPlatformStopper {
}
/**
* @internal
*/
class ɵMicrofrontendPlatformStopper {
_destroy$ = new Subject();
constructor() {
race(fromEvent(window, 'beforeunload'), fromEvent(window, 'unload'))
.pipe(take(1), takeUntil(this._destroy$))
.subscribe(() => {
MicrofrontendPlatform.destroy();
});
}
preDestroy() {
this._destroy$.next();
}
}
/*
* Copyright (c) 2018-2020 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
*/
/**
* Logger used by the platform to log to the console.
*
* Replace this bean to capture the log output.
*
* @category Platform
*/
class Logger {
}
/**
* Logger used by the platform to log to the console.
*
* Replace this bean to capture the log output.
*
* @internal
*/
class ConsoleLogger {
debug(message, ...args) {
this.log('debug', message, args);
}
info(message, ...args) {
this.log('info', message, args);
}
warn(message, ...args) {
this.log('warn', message, args);
}
error(message, ...args) {
this.log('error', message, args);
}
log(severity, message, args) {
const loggingContext = args[0] instanceof LoggingContext ? args.shift() : { appSymbolicName: Beans.get(APP_IDENTITY), version: Beans.get(ɵVERSION) };
const prefix = new Array()
.concat(loggingContext.version ? `[@scion/microfrontend-platform@${loggingContext.version}]` : '[@scion/microfrontend-platform]')
.concat(`[${loggingContext.appSymbolicName}]`)
.join('');
if (console && typeof console[severity] === 'function') {
const consoleFn = console[severity];
args?.length ? consoleFn(`${prefix} ${message}`, ...args) : consoleFn(`${prefix} ${message}`);
}
}
}
/**
* Logger that does nothing.
*
* @internal
*/
const NULL_LOGGER = new class extends Logger {
debug(message, ...args) {
// NOOP
}
info(message, ...args) {
// NOOP
}
warn(message, ...args) {
// NOOP
}
error(message, ...args) {
// NOOP
}
};
/**
* Contextual information to add to the log message.
*
* Pass an instance of this class as the first argument to the logger when logging a message.
*
* @internal
*/
class LoggingContext {
appSymbolicName;
version;
constructor(appSymbolicName, version) {
this.appSymbolicName = appSymbolicName;
this.version = version;
}
}
/*
* 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
*/
/**
* Current version of the SCION Microfrontend Platform.
*/
const version = '1.4.0';
/**
* The central class of the SCION Microfrontend Platform. This class cannot be instantiated. All functionality is provided by static methods.
*
* To enable tree-shaking of the SCION Microfrontend Platform, the platform provides three separate entry points:
* - {@link MicrofrontendPlatformHost} to configure and start the platform in the host
* - {@link MicrofrontendPlatformClient} to connect to the platform from a microfrontend
* - {@link MicrofrontendPlatform} to react to platform lifecycle events and stop the platform
*
* ## SCION Microfrontend Platform
*
* SCION Microfrontend Platform is a TypeScript-based open source library that enables the implementation of a framework-agnostic
* microfrontend architecture using iframes. It provides fundamental APIs for microfrontends to communicate with each other across origins
* and facilitates embedding microfrontends using a web component and a router. SCION Microfrontend Platform is a lightweight, web stack
* agnostic library that has no user-facing components and does not dictate any form of application structure.
*
* You can continue using the frameworks you love since the platform integrates microfrontends via iframes. Iframes by nature provide
* maximum isolation and allow the integration of any web application without complex adaptation. The platform aims to shield developers
* from iframe specifics and the low-level messaging mechanism to focus instead on integrating microfrontends.
*
* #### Cross-microfrontend communication
* The platform adds a pub/sub layer on top of the native `postMessage` mechanism to enable microfrontends to communicate with each other
* easily across origins. Communication comes in two flavors: topic-based and intent-based. Both models feature request-response message
* exchange, support retained messages for late subscribers to receive the latest messages, and provide API to intercept messages to
* implement cross-cutting messaging concerns.
*
* Topic-based messaging enables you to publish messages to multiple subscribers via a common topic. Intent-based communication focuses on
* controlled collaboration between applications. To collaborate, an application must express an intention. Manifesting intentions enables
* us to see dependencies between applications down to the functional level.
*
* #### Microfrontend Integration and Routing
* The platform makes it easy to integrate microfrontends through its router-outlet. The router-outlet is a web component that wraps an iframe.
* It solves many of the cumbersome quirks of iframes and helps to overcome iframe restrictions. For example, it can adapt its size to the
* preferred size of embedded content, supports keyboard event propagation and lets you pass contextual data to embedded content.
* Using the router, you control which web content to display in an outlet. Multiple outlets can display different content, determined by
* different outlet names, all at the same time. Routing works across application boundaries and enables features such as persistent navigation.
*
* ***
*
* A microfrontend architecture can be achieved in many ways, each with its pros and cons. The SCION Microfrontend Platform uses
* the iframe approach primarily since iframes by nature provide the highest possible level of isolation through a separate browsing context.
* The microfrontend design approach is very tempting and has obvious advantages, especially for large-scale and long-lasting projects, most
* notably because we are observing an enormous dynamic in web frameworks. The SCION Microfrontend Platform provides you with the necessary
* tools to best support you in implementing such an architecture.
*
* @see {@link MicrofrontendPlatformHost}
* @see {@link MicrofrontendPlatformClient}
*
* @see {@link MessageClient}
* @see {@link IntentClient}
* @see {@link SciRouterOutletElement}
* @see {@link OutletRouter}
* @see {@link ContextService}
* @see {@link PreferredSizeService}
* @see {@link ManifestService}
* @see {@link FocusMonitor}
* @see {@link ActivatorCapability}
*
* @category Platform
* @category Lifecycle
*/
class MicrofrontendPlatform {
static _state$ = new BehaviorSubject(PlatformState.Stopped);
constructor() {
}
/**
* @internal
*/
static async startPlatform(startupFn) {
if (this.state === PlatformState.Started) {
return Promise.reject(Error('[MicrofrontendPlatformStartupError] Platform already started'));
}
try {
startupFn?.();
await this.enterState(PlatformState.Starting);
await Beans.start({ eagerBeanConstructRunlevel: Runlevel.One, initializerDefaultRunlevel: Runlevel.Two });
await this.enterState(PlatformState.Started);
return Promise.resolve();
}
catch (error) {
await this.destroy();
return Promise.reject(Error(`[MicrofrontendPlatformStartupError] Microfrontend platform failed to start: ${error}`));
}
}
/**
* Destroys this platform and releases resources allocated.
*
* @return a Promise that resolves once the platformed stopped.
*/
static async destroy() {
await this.enterState(PlatformState.Stopping);
Beans.destroy();
await this.enterState(PlatformState.Stopped);
}
/**
* @return the current platform state.
*/
static get state() {
return this._state$.getValue();
}
/**
* Observable that, when subscribed, emits the current platform lifecycle state.
* It never completes and emits continuously when the platform enters
* another state.
*/
static get state$() {
return this._state$;
}
/**
* Waits for the platform to enter the specified {@link PlatformState}.
* If already in that state, the Promise resolves instantly.
*
* @param state - the state to wait for.
* @return A Promise that resolves when the platform enters the given state.
* If already in that state, the Promise resolves instantly.
*/
static async whenState(state) {
return new Promise((resolve, reject) => {
this._state$
.pipe(first(it => it === state))
.subscribe({
error: reject,
complete: resolve,
});
});
}
static async enterState(newState) {
const currentState = (this.state === PlatformState.Stopped) ? -1 : this.state;
if (currentState >= newState) {
throw Error(`[PlatformStateError] Failed to enter platform state [prevState=${PlatformState[this.state]}, newState=${PlatformState[newState]}].`);
}
this._state$.next(newState);
// Let microtasks waiting for entering that state to resolve first.
await this.whenState(newState);
}
}
/**
* @internal
*/
function providePlatformEnvironment(config) {
Beans.register(IS_PLATFORM_HOST, { useValue: config.isPlatformHost });
Beans.register(APP_IDENTITY, { useValue: config.symbolicName });
Beans.registerIfAbsent(ɵWINDOW_TOP, { useValue: window.top });
Beans.registerIfAbsent(ɵVERSION, { useValue: version, destroyOrder: BeanDestroyOrders.CORE });
Beans.registerIfAbsent(MicrofrontendPlatformStopper, { useClass: ɵMicrofrontendPlatformStopper, eager: true });
Beans.registerIfAbsent(Logger, { useClass: ConsoleLogger, destroyOrder: BeanDestroyOrders.CORE });
}
/**
* Specifies destroy orders of platform-specific beans, enabling controlled termination of the platform.
*
* @internal
*/
var BeanDestroyOrders;
(function (BeanDestroyOrders) {
/**
* Use for core platform beans which should be destroyed as the very last beans.
*/
BeanDestroyOrders[BeanDestroyOrders["CORE"] = Number.MAX_SAFE_INTEGER] = "CORE";
/**
* Use for the {@link MessageBroker}.
*/
BeanDestroyOrders[BeanDestroyOrders["BROKER"] = BeanDestroyOrders.CORE - 1] = "BROKER";
/**
* Use for messaging-related beans.
*/
BeanDestroyOrders[BeanDestroyOrders["MESSAGING"] = BeanDestroyOrders.BROKER - 1] = "MESSAGING";
})(BeanDestroyOrders || (BeanDestroyOrders = {}));
/*
* Copyright (c) 2018-2020 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
*/
/**
* Configures the platform and defines the micro applications running in the platform.
*
* @category Platform
*/
class MicrofrontendPlatformConfig {
}
/*
* Copyright (c) 2018-2020 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
*/
/**
* Hook to intercept the host manifest before it is registered in the platform.
*
* If integrating the platform in a library, you may need to intercept the manifest of the host in order to introduce library-specific behavior.
*
* You can register the interceptor in the bean manager, as follows:
*
* ```ts
* Beans.register(HostManifestInterceptor, {useClass: YourInterceptor, multi: true});
* ```
*
* The interceptor may look as following:
* ```ts
* class YourInterceptor implements HostManifestInterceptor {
*
* public intercept(hostManifest: Manifest): void {
* hostManifest.intentions = [
* ...hostManifest.intentions || [],
* provideMicrofrontendIntention(),
* ];
* hostManifest.capabilities = [
* ...hostManifest.capabilities || [],
* provideMessageBoxCapability(),
* ];
* }
* }
*
* function provideMicrofrontendIntention(): Intention {
* return {
* type: 'microfrontend',
* qualifier: {'*': '*'},
* };
* }
*
* function provideMessageBoxCapability(): Capability {
* return {
* type: 'messagebox',
* qualifier: {},
* private: false,
* description: 'Allows displaying a simple message to the user.',
* };
* }
*
* ```
*
* @category Platform
* @category Intention API
*/
class HostManifestInterceptor {
}
/**
* Intercepts the host manifest, registering platform-specific intentions and capabilities.
*
* @internal
*/
class ɵHostManifestInterceptor {
intercept(hostManifest) {
hostManifest.intentions = [
...hostManifest.intentions || [],
...provideActivatorIntentionIfEnabled(),
];
}
}
/**
* Provides a wildcard activator intention for the platform to read activator capabilities for installing activator microfrontends.
*/
function provideActivatorIntentionIfEnabled() {
const activatorApiDisabled = Beans.get(MicrofrontendPlatformConfig).activatorApiDisabled ?? false;
if (activatorApiDisabled) {
return [];
}
return [{
type: PlatformCapabilityTypes.Activator,
qualifier: { '*': '*' },
}];
}
/**
* Central point for managing client registrations.
*
* @internal
*/
class ClientRegistry {
}
/**
* Message client for sending and receiving messages between microfrontends across origins.
*
* This client implements the topic-based pub/sub (publish/subscribe) messaging model, allowing for one message to be delivered to
* multiple subscribers using topic addressing.
*
* The communication is built on top of the native `postMessage` mechanism. The host app acts as message broker.
*
* ### Topic Addressing
* A publisher publishes a message to a topic, which then is transported to consumers subscribed to the topic. Topics are case-sensitive
* and consist of one or more segments, each separated by a forward slash. When publishing a message to a topic, the topic must be exact,
* thus not contain wildcards. Messages published to a topic are transported to all consumers subscribed to the topic. Consumers, on the
* other hand, can subscribe to multiple topics simultaneously by using wildcard segments in the topic.
*
* ### Retained Message
* You can mark a message as "retained" for helping newly subscribed clients to get the last message published to a topic immediately upon
* subscription. The broker stores one retained message per topic, i.e., a later sent retained message will replace a previously sent retained
* message. To delete a retained message, send a retained message without payload to the topic.
*
* ### Retained Request
* Unlike retained messages, retained requests are not replaced by later retained requests/messages and remain in the broker until the requestor unsubscribes.
*
* ### Request-Response Messaging
* Sometimes it is useful to initiate a request-response communication to wait for a response. Unlike with fire-and-forget messaging, a temporary
* inbox is created for the sender to receive replies. If there is no consumer subscribed on the topic, the platform throws an error.
*
* @see {@link TopicMessage}
* @see {@link takeUntilUnsubscribe}
*
* @category Messaging
*/
class MessageClient {
}
/**
* Returns an Observable that mirrors the source Observable as long as there is at least one subscriber subscribed to the
* given topic. When the subscription count on the given topic drops to zero, the returned Observable completes. If there
* is no topic subscription present at the time when subscribing to the Observable, then it completes immediately.
*
* This operator is similar to the RxJS {@link rxjs!takeUntil takeUntil} operator, but accepts a topic instead of a notifier Observable.
*
* @category Messaging
*/
function takeUntilUnsubscribe(topic) {
return takeUntil(Beans.get(MessageClient).subscriberCount$(topic).pipe(first(count => count === 0)));
}
/**
* Declares the message transports.
*
* @internal
*/
var MessagingTransport;
(function (MessagingTransport) {
/**
* Transport used by clients to communicate with the broker.
*/
MessagingTransport["ClientToBroker"] = "sci://microfrontend-platform/client-to-broker";
/**
* Transport used by the broker to communicate with its clients.
*/
MessagingTransport["BrokerToClient"] = "sci://microfrontend-platform/broker-to-client";
/**
* Transport used by a microfrontend to communicate with its embedding outlet.
*/
MessagingTransport["MicrofrontendToOutlet"] = "sci://microfrontend-platform/microfrontend-to-outlet";
})(MessagingTransport || (MessagingTransport = {}));
/**
* Defines the channels to which messages can be sent.
*
* @internal
*/
var MessagingChannel;
(function (MessagingChannel) {
/**
* Channel for clients to subscribe to a topic destination.
*/
MessagingChannel["TopicSubscribe"] = "topic-subscribe";
/**
* Channel for clients to unsubscribe from a topic destination.
*/
MessagingChannel["TopicUnsubscribe"] = "topic-unsubscribe";
/**
* Channel for clients to subscribe to intents.
*/
MessagingChannel["IntentSubscribe"] = "intent-subscribe";
/**
* Channel for clients to unsubscribe from intents.
*/
MessagingChannel["IntentUnsubscribe"] = "intent-unsubscribe";
/**
* Channel for the host to transport topic message to subscribed clients.
*/
MessagingChannel["Topic"] = "topic";
/**
* Channel for the host to transport intent messages to subscribed clients.
*/
MessagingChannel["Intent"] = "intent";
/**
* Channel for clients to send a connect request.
*/
MessagingChannel["ClientConnect"] = "client-connect";
/**
* Channel for clients to send a disconnect request.
*/
MessagingChannel["ClientDisconnect"] = "client-disconnect";
})(MessagingChannel || (MessagingChannel = {}));
/**
* Declares internal platform topics.
*
* @internal
*/
var PlatformTopics;
(function (PlatformTopics) {
/**
* Topic to request the subscription count on a topic.
*
* Messaging Pattern: Request-Response
* Request Type: {@link string}
* Response Type: {@link number}
*/
PlatformTopics.RequestSubscriberCount = 'ɵREQUEST_SUBSCRIBER_COUNT';
/**
* Topic to signal when gained the focus.
*
* Messaging Pattern: Publish-Subscribe
* Payload: {@link void}
*/
PlatformTopics.FocusIn = 'ɵFOCUS_IN';
/**
* Topic to request whether the requesting client (or a microfrontend embedded in the client) has gained focus.
*
* Messaging Pattern: Request-Response
* Request Type: {@link void}
* Response Type: {@link boolean}
*/
PlatformTopics.IsFocusWithin = 'ɵIS_FOCUS_WITHIN';
/**
* Topic to request whether the requesting client has gained focus.
*
* Messaging Pattern: Request-Response
* Request Type: {@link void}
* Response Type: {@link boolean}
*/
PlatformTopics.HasFocus = 'ɵHAS_FOCUS';
/**
* Topic to read platform properties.
*
* Messaging Pattern: Publish-Subscribe
* Payload: {@link Record}
*/
PlatformTopics.PlatformProperties = 'ɵPLATFORM_PROPERTIES';
/**
* Topic to read platform registered applications.
*
* Messaging Pattern: Publish-Subscribe
* Payload: {@link ɵApplication}
*/
PlatformTopics.Applications = 'ɵAPPLICATIONS';
/**
* Topic to request capabilities.
*
* Messaging Pattern: Request-Response
* Request Type: {@link ManifestObjectFilter}
* Response Type: {@link Array<Capability>}
*/
PlatformTopics.LookupCapabilities = 'ɵLOOKUP_CAPABILITIES';
/**
* Topic to request intentions.
*
* Messaging Pattern: Request-Response
* Request Type: {@link ManifestObjectFilter}
* Response Type: {@link Array<Intention>}
*/
PlatformTopics.LookupIntentions = 'ɵLOOKUP_INTENTIONS';
/**
* Topic to register a capability.
*
* Messaging Pattern: Request-Response
* Request Type: {@link Capability}
* Response Type: {@link string}
*/
PlatformTopics.RegisterCapability = 'ɵREGISTER_CAPABILITY';
/**
* Topic to unregister a capability.
*
* Messaging Pattern: Request-Response
* Request Type: {@link ManifestObjectFilter}
* Response Type: {@link void}
*/
PlatformTopics.UnregisterCapabilities = 'ɵUNREGISTER_CAPABILITIES';
/**
* Topic to register an intentions.
*
* Messaging Pattern: Request-Response
* Request Type: {@link Intention}
* Response Type: {@link string}
*/
PlatformTopics.RegisterIntention = 'ɵREGISTER_INTENTION';
/**
* Topic to unregister an intention.
*
* Messaging Pattern: Request-Response
* Request Type: {@link ManifestObjectFilter}
* Response Type: {@link void}
*/
PlatformTopics.UnregisterIntentions = 'ɵUNREGISTER_INTENTIONS';
/**
* Topic to check if application is qualified for the capability.
*
* Messaging Pattern: Request-Response
* Request Type: {@link ApplicationQualifiedForCapabilityRequest}
* Response Type: {@link boolean}
*/
PlatformTopics.IsApplicationQualifiedForCapability = 'ɵIS_APPLICATION_QUALIFIED_FOR_CAPABILITY';
/**
* Topic to request the platform version of a specific application.
*/
function platformVersion(appSymbolicName) {
return `ɵapplication/${appSymbolicName}/platform/version`;
}
PlatformTopics.platformVersion = platformVersion;
/**
* Topic to ping a client for liveness.
*/
function ping(clientId) {
return `ɵclient/${clientId}/ping`;
}
PlatformTopics.ping = ping;
})(PlatformTopics || (PlatformTopics = {}));
/**
* Declares headers set by the platform when sending a message.
*
* Clients are allowed to read platform-defined headers from a message.
*
* @category Messaging
*/
var MessageHeaders;
(function (MessageHeaders) {
/**
* Identifies the sending client instance of a message.
* This header is set by the platform when publishing a message or intent.
*/
MessageHeaders["ClientId"] = "\u0275CLIENT_ID";
/**
* Identifies the sending application of a message.
* This header is set by the platform when publishing a message or intent.
*/
MessageHeaders["AppSymbolicName"] = "\u0275APP_SYMBOLIC_NAME";
/**
* Unique identity of the message.
* This header is set by the platform when publishing a message or intent.
*/
MessageHeaders["MessageId"] = "\u0275MESSAGE_ID";
/**
* Destination to which to send a response to this message.
* This header is set by the platform when sending a request.
*/
MessageHeaders["ReplyTo"] = "\u0275REPLY_TO";
/**
* The time the message was sent.
* This header is set by the platform when publishing a message or intent.
*/
MessageHeaders["Timestamp"] = "\u0275TIMESTAMP";
/**
* The version of the client.
*/
MessageHeaders["Version"] = "\u0275VERSION";
/**
* Use this header to set the request method to indicate the desired action to be performed for a given resource.
* @see RequestMethods
*/
MessageHeaders["Method"] = "\u0275METHOD";
/**
* Use this header to set the response status code to indicate whether a request has been successfully completed.
* See {@link ResponseStatusCodes} for available status codes. Other codes are also allowed.
*
* Status codes are primarily used in request-reply communication. In request-response communication, by default,
* the requestor’s Observable never completes. However, the replier can include the response status code in the reply’s
* headers, allowing to control the lifecycle of the requestor’s Observable.
*
* For example, the status code {@link ResponseStatusCodes.TERMINAL 250} allows completing the requestor’s Observable
* after emitted the reply, or the status code {@link ResponseStatusCodes.ERROR 500} to error the Observable.
*
* Note that the platform evaluates status codes only in request-response communication. They are ignored when observing
* topics or intents in pub/sub communication but can still be used; however, they must be handled by the application,
* e.g., by using the {@link throwOnErrorStatus} SCION RxJS operator.
*
* @see ResponseStatusCodes
*/
MessageHeaders["Status"] = "\u0275STATUS";
/**
* Unique identity of a message or intent subscriber.
*
* @internal
*/
MessageHeaders["\u0275SubscriberId"] = "\u0275SUBSCRIBER_ID";
})(MessageHeaders || (MessageHeaders = {}));
/**
* Defines a set of request methods to indicate the desired action to be performed for a given resource.
*
* @category Messaging
*/
var RequestMethods;
(function (RequestMethods) {
/**
* The GET method requests a representation of the specified resource. Requests using GET should only retrieve data.
*/
RequestMethods["GET"] = "GET";
/**
* The DELETE method deletes the specified resource.
*/
RequestMethods["DELETE"] = "DELETE";
/**
* The PUT method replaces all current representations of the target resource with the request payload.
*/
RequestMethods["PUT"] = "PUT";
/**
* The POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server.
*/
RequestMethods["POST"] = "POST";
/**
* The OBSERVE method is used to observe the specified resource.
*/
RequestMethods["OBSERVE"] = "OBSERVE";
})(RequestMethods || (RequestMethods = {}));
/**
* Defines a set of response status codes to indicate whether a request has been successfully completed.
*
* @see throwOnErrorStatus
* @see MessageClient.request$
* @see IntentClient.request$
*
* @category Messaging
*/
var ResponseStatusCodes;
(function (ResponseStatusCodes) {
/**
* The request has succeeded.
*/
ResponseStatusCodes[ResponseStatusCodes["OK"] = 200] = "OK";
/**
* The request has succeeded. No further response to be expected.
*
* In request-reply communication, setting this status code will complete the requestor's Observable
* after emitted the reply. The reply is only emitted if not `undefined`.
*/
ResponseStatusCodes[ResponseStatusCodes["TERMINAL"] = 250] = "TERMINAL";
/**
* The receiver could not understand the request due to invalid syntax.
*
* In request-reply communication, setting this status code will error the requestor's Observable.
*/
ResponseStatusCodes[ResponseStatusCodes["BAD_REQUEST"] = 400] = "BAD_REQUEST";
/**
* The receiver could not find the requested resource.
*
* In request-reply communication, setting this status code will error the requestor's Observable.
*/
ResponseStatusCodes[ResponseStatusCodes["NOT_FOUND"] = 404] = "NOT_FOUND";
/**
* The receiver encountered an internal error. Optionally, set the error as message payload.
*
* In request-reply communication, setting this status code will error the requestor's Observable.
*/
ResponseStatusCodes[ResponseStatusCodes["ERROR"] = 500] = "ERROR";
})(ResponseStatusCodes || (ResponseStatusCodes = {}));
/**
* Returns an Observable that mirrors the source Observable unless receiving a message with
* a response status code greater than or equal to 400. Then, the stream will end with an
* {@link RequestError error} and the source Observable unsubscribed.
*
* When receiving a message with the response status code {@link ResponseStatusCodes.TERMINAL},
* the Observable emits this message and completes.
*
* If a message does not include a response status code, the message is emitted as is.
*
* Note that this operator is installed in {@link MessageClient.request$} and {@link IntentClient.request$}.
*
* @category Messaging
*/
function throwOnErrorStatus() {
return pipe(mergeMap((message) => {
const status = message.headers.get(MessageHeaders.Status) ?? ResponseStatusCodes.OK;
if (status < 400) {
return of(message); // 1xx: informational responses, 2xx: successful responses, 4xx: client errors, 5xx: server errors
}
if (typeof message.body === 'string') {
const messageBody = message.body;
return throwError(() => new RequestError(messageBody, status, message));
}
switch (status) {
case ResponseStatusCodes.BAD_REQUEST: {
return throwError(() => new RequestError('The receiver could not understand the request due to invalid syntax.', status, message));
}
case ResponseStatusCodes.NOT_FOUND: {
return throwError(() => new RequestError('The receiver could not find the requested resource.', status, message));
}
case ResponseStatusCodes.ERROR: {
return throwError(() => new RequestError('The receiver encountered an internal error.', status, message));
}
default: {
return throwError(() => new RequestError('Request error.', status, message));
}
}
}), takeWhile((message) => {
return message.headers.get(MessageHeaders.Status) !== ResponseStatusCodes.TERMINAL;
}, true), filter((message) => {
const isTerminalMessage = message.headers.get(MessageHeaders.Status) === ResponseStatusCodes.TERMINAL;
return (!isTerminalMessage || message.body !== undefined);
}));
}
/**
* Maps each message to its body.
*
* @category Messaging
*/
function mapToBody() {
return map(message => message.body);
}
/**
* Indicates that the request handler responded with an error response.
*
* @category Messaging
*/
class RequestError extends Error {
status;
msg;
constructor(error, status, msg) {
super(error);
this.status = status;
this.msg = msg;
this.name = 'RequestError';
}
}
/*
* 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
*/
/**
* Provides utilities for working with topics.
*
* @internal
*/
var Topics;
(function (Topics) {
/**
* Tests whether given topic contains wildcard segments.
*/
function containsWildcardSegments(topic) {
return split(topic).some(isWildcardSegment);
}
Topics.containsWildcardSegments = containsWildcardSegments;
/**
* Tests whether given topic contains empty segments.
*/
function containsEmptySegments(topic) {
return topic.split('/').some(segment => !segment.length);
}
Topics.containsEmptySegments = containsEmptySegments;
/**
* Tests whether given segment is a wildcard segment.
*/
function isWildcardSegment(segment) {
return segment.startsWith(':') && segment.length > 1;
}
Topics.isWildcardSegment = isWildcardSegment;
/**
* Splits given topic into its segments.
*/
function split(topic) {
return topic?.split('/').filter(Boolean) ?? [];
}
Topics.split = split;
/**
* Returns unnamed wildcard permutations for given exact topic.
* These are 2^n variations, where n is the number of segments.
*
* Example:
* Topic: myhome/kitchen/temperature
*
* +-----------+-----------+-----------*-+
* | Segment 1 | Segment 2 | Segment 3 |
* +-----------+-----------+-*-----------+
* | myhome | kitchen | temperature |
* | myhome | kitchen | * |
* | myhome | * | temperature |
* | myhome | * | * |
* | * | kitchen | temperature |
* | * | kitchen | * |
* | * | * | temperature |
* | * | * | * |
* +-----------+-----------+-------------+
*/
function computeWildcardSegmentPermutations(topic, wildcardCharacter) {
const segments = typeof topic === 'string' ? Topics.split(topic) : topic;
if (segments.length === 1) {
return [segments[0], wildcardCharacter];
}
return computeWildcardSegmentPermutations(segments.slice(1), wildcardCharacter).reduce((permutations, permutation) => {
permutations.push(`${segments[0]}/${permutation}`);
permutations.push(`${wildcardCharacter}/${permutation}`);
return permutations;
}, new Array());
}
Topics.computeWildcardSegmentPermutations = computeWildcardSegmentPermutations;
/**
* Replaces named wildcard segments with given replacement.
*
* Example: "myhome/:room/temperature" => "myhome/REPLACEMENT/temperature"
*
*/
function replaceWildcardSegments(topic, replacement) {
return topic.replace(/:[^/]+/g, replacement);
}
Topics.replaceWildcardSegments = replaceWildcardSegments;
/**
* Validates given topic.
*
* @return `null` if valid, or the `Error` otherwise.
*/
function validateTopic(topic, options) {
if (!topic) {
return Error('[IllegalTopicError] Topic must not be `null`, `undefined` or empty');
}
if (Topics.containsEmptySegments(topic)) {
return Error(`[IllegalTopicError] Topic must not contain empty segments [topic='${topic}']`);
}
if (options.exactTopic && Topics.containsWildcardSegments(topic)) {
return Error(`[IllegalTopicError] Topic must be exact, i.e., not contain wildcard segments [topic='${topic}']`);
}
return null;
}
Topics.validateTopic = validateTopic;
})(Topics || (Topics = {}));
/*
* Copyright (c) 2018-2020 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
*/
/**
* Allows testing whether an exact topic matches a pattern topic. The pattern topic may contain wildcard segments.
*
* Topics are case-sensitive and consist of one or more segments, each separated by a forward slash.
*
* @category Messaging
*/
class TopicMatcher {
_patternSegments;
/**
* Constructs a matcher that will match given topics against this pattern.
*
* @param pattern - Pattern to match topics. The pattern is a topic, not a regular expression; thus, it must consist of one or more segments,
* each separated by a forward slash. The pattern supports wildcard segments beginning with a colon (`:`). Wildcard segments
* act as a placeholder for any segment value.
*/
constructor(pattern) {
this._patternSegments = Topics.split(pattern);
if (!this._patternSegments.length) {
throw Error('[TopicMatcherError] Invalid pattern syntax. The pattern must consist of one or more topic segments, each separated by a forward slash.');
}
}
/**
* Attempts to match the given topic against the pattern which was passed to the constructor.
*
* If the match succeeds, then {@link MatcherResult.matches} evaluates to `true`. If the pattern contains wildcard segments,
* the matched segments can be read using the property {@link TopicMessage.params} property.
*
* @param topic - The topic to match against the configured pattern; must be an exact topic, thus not contain wildcard segments.
* @return The result of the topic matcher test.
*/
match(topic) {
const inputTopicSegments = Topics.split(topic);
const patternSegments = this._patternSegments;
if (!inputTopicSegments.length) {
throw Error('[TopicMatcherError] Invalid topic. The topic must consist of one or more segments, each separated by a forward slash.');
}
if (inputTopicSegments.some(Topics.isWildcardSegment)) {
throw Error('[TopicMatcherError] Invalid topic. Wildcard segments not allowed in an exact topic.');
}
if (patternSegments.length !== inputTopicSegments.length) {
return { matches: false };
}
if (Arrays.isEqual(inputTopicSegments, patternSegments, { exactOrder: true })) {
return { matches: true, params: new Map() };
}
if (!patternSegments.some(Topics.isWildcardSegment)) {
return { matches: false };
}
if (!patternSegments.every((patternSegment, i) => patternSegment === inputTopicSegments[i] || Topics.isWildcardSegment(patternSegment))) {
return { matches: false };
}
return {
matches: true,
params: patternSegments.reduce((params, segment, i) => {
if (Topics.isWildcardSegment(segment)) {
return params.set(segment.substring(1), inputTopicSegments[i]);
}
return params;
}, new Map()),
};
}
}
/*
* Copyright (c) 2018-2020 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
*/
/** @internal */
function filterByTransport(transport) {
return filter((event) => {
const envelope = event.data;
return envelope?.transport === transport && !!envelope.channel && !!envelope.message?.headers;
});
}
/** @internal */
function filterByChannel(...channel) {
const channels = new Set(Arrays.coerce(channel));
return filter((event) => {
return channels.has(event.data.channel);
});
}
/** @internal */
function filterByTopicChannel(topic) {
return pipe(filterByChannel(MessagingChannel.Topic), filter((event) => {
const messageTopic = event.data.message.topic;
return !!messageTopic && new TopicMatcher(topic).match(messageTopic).matches;
}));
}
/** @internal */
function filterByOrigin(origin) {
return filter((event) => {
return event.origin === origin;
});
}
/** @internal */
function filterByWindow(window) {
return filter((event) => {
return event.source === window;
});
}
/** @internal */
function pluckMessage() {
return map((messageEvent) => {
return messageEvent.data.message;
});
}
/*
* Copyright (c) 2018-2020 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
*/
/**
* Runs the given function. Errors are caught and logged.
*
* If producing a Promise, returns that Promise, but with a catch handler installed.
*
* @internal
*/
function runSafe(runnable) {
let result;
try {
result = runnable();
}
catch (error) {
Beans.opt(Logger)?.error('[UnexpectedError] An unexpected error occurred.', error);
return undefined;
}
if (result instanceof Promise) {
return result.catch(error => {
Beans.opt(Logger)?.error('[UnexpectedError] An unexpected error occurred.', error);
return undefined;
});
}
return result;
}
/*
* Copyright (c) 2018-2020 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
*/
/**
* Returns the error message if given an error object, or the `toString` representation otherwise.
*
* @internal
*/
function stringifyError(error) {
if (error instanceof Error) {
return error.message;
}
return `${error}`;
}
/*
* 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
*/
/**
* Enables the decoration of RxJS Observables provided by the SCION Microfrontend Platform to control their emission context.
*
* The emission context of an Observables may be different than the subscription context, which can lead to unexpected behavior
* on the subscriber side. For example, Angular uses zones (Zone.js) to trigger change detection. Angular applications expect
* an RxJS Observable to emit in the same Angular zone in which subscribed to the Observable. That is, if subscribing inside
* the Angular zone, emissions are expected to be received inside the Angular zone. Otherwise, the UI may not be updated as
* expected but delayed until the next change detection cycle. Similarly, if subscribing outside the Angular zone, emissions
* are expected to be received outside the Angular zone. Otherwise, this would cause unnecessary change detection cycles
* resulting in potential performance degradation.
*
* ### Example for Angular Applications
*
* For Angular applications, we reommend installing the following decorator:
*
* ```ts
* import {NgZone} from '@angular/core';
* import {ObservableDecorator} from '@scion/microfrontend-platform';
* import {Observable} from 'rxjs';
* import {observeIn, subscribeIn} from '@scion/toolkit/operators';
*
* 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(
* subscribeIn(fn => this.zone.runOutsideAngular(fn)),
* observeIn(fn => insideAngular ? this.zone.run(fn) : this.zone.runOutsideAngular(fn)),
* )
* .subscribe(observer);
* return () => subscription.unsubscribe();
* });
* }
* }
* ```
*
* A decorator can be registered with the bean manager under the symbol `ObservableDecorator`, as following:
*
* ```ts
* Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(zone)});
* ```
*
* @category Messaging
*/
class ObservableDecorator {
}
/**
* Decorates the source with registered {@link ObservableDecorator}, if any.
*
* @internal
*/
function decorateObservable() {
return (source$) => Beans.opt(ObservableDecorator)?.decorate$(source$) ?? source$;
}
/*
* Copyright (c) 2018-2023 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
*/
/**
* Selects items emitted by an Observable according to a static criterion.
*
* This selector was introduced to quickly filter many messages from many subscribers.
* Instead of a predicate, a key is used to dispatch messages with O(1) complexity to the subscribers.
*
* Prior to this sele