UNPKG

@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
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