UNPKG

@amadeus-it-group/microfrontends

Version:
944 lines (934 loc) 34.6 kB
/** * List of all service message types */ const SERVICE_MESSAGE_TYPES = { handshake: true, error: true, disconnect: true, declare_messages: true, connect: true, }; /** * Checks if a particular message is a {@link ServiceMessage}, like `connect`, `disconnect`, `handshake`, etc. * * ```ts * // Example of usage * if (isServiceMessage(message)) { * switch (message.type) { * case 'connect': * // handle connect message with narrowed type * break; * } * } * ``` * @param message - Message to check */ function isServiceMessage(message) { return SERVICE_MESSAGE_TYPES[message?.type] !== undefined; } /** * Error class for errors related to message processing * @param message - The error message * @param messageObject - The message that caused the error */ class MessageError extends Error { messageObject; constructor(messageObject, message) { super(message); this.messageObject = messageObject; this.name = 'MessageError'; } } let LOGGING_ENABLED = false; /** * If true, tracing information to help debugging will be logged in the console * @param enabled */ function enableLogging(enabled = true) { LOGGING_ENABLED = enabled; } /** * Logs things * @param args - whatever */ function logger(...args) { if (LOGGING_ENABLED) { console.log(...args); } } /** * Checks that message has correct 'from', 'to', 'payload', 'payload.type' and 'payload.version' properties * @param message Message to check * @param strategy Message check strategy * @throws MessageError */ function checkMessageHasCorrectStructure(message, strategy = 'default') { // check 'from' and 'to' if (!(message && message.from && message.to && typeof message.from === 'string' && Array.isArray(message.to))) { throw new MessageError(message, `Message should have 'from'(string) and 'to'(string|string[]) properties`); } // check 'payload', 'payload.type' const { payload } = message; if (!(payload && payload.type && typeof payload.type === 'string')) { throw new MessageError(message, `Message should have 'payload' property that has 'type'(string) defined`); } // check 'payload.version' only if necessary if (strategy === 'version' && !(payload.version && typeof payload.version === 'string')) { throw new MessageError(message, `Message should have 'payload' property that has 'version'(string) defined`); } } function checkOriginIsValid(origin) { const parsedURL = URL.parse(origin); if (!parsedURL) { throw new Error(`'${origin}' is not a valid URL`); } if (parsedURL.origin !== origin) { throw new Error(`'${origin}' is not a valid origin, did you mean '${parsedURL.origin}'?`); } } function normalizeFilter(filter) { switch (typeof filter) { case 'string': return { id: filter }; case 'function': return { predicate: filter }; default: return filter; } } function createHandshakeMessage(from, to, knownPeers) { return structuredClone({ from, to: [to], payload: { type: 'handshake', version: '1.0', endpointId: to, remoteId: from, knownPeers, }, }); } function eventMatchesFilters(event, connectionFilters) { const { origin, source, data: message } = event; const { remoteId } = message.payload; return (connectionFilters.length === 0 || connectionFilters.some((f) => (f.id !== undefined || f.source !== undefined || f.origin !== undefined || f.predicate) && (f.id === undefined || f.id === remoteId) && (f.source === undefined || f.source === source) && (f.origin === undefined || f.origin === origin || (origin === 'null' && f.source === source)) && (f.predicate === undefined || f.predicate(message, source, origin)))); } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Simple MessagePort implementation */ class LocalMessagePort extends EventTarget { otherPort = null; onmessage = null; onmessageerror = null; postMessage(message) { const event = new MessageEvent('message', { data: structuredClone(message) }); this.otherPort?.dispatchEvent(event); this.otherPort?.onmessage?.call(this.otherPort, event); } start() { // no need to implement } close() { // no need to implement } } /** * Simple MessageChannel implementation */ class LocalMessageChannel { port1; port2; constructor() { this.port1 = new LocalMessagePort(); this.port2 = new LocalMessagePort(); this.port1.otherPort = this.port2; this.port2.otherPort = this.port1; } } class Endpoint { id; remoteId; port; connection; connected = false; #messageQueue = []; #resolve = () => { // noop, will be set later }; reject = () => { // noop, will be set later }; constructor(id, remoteId, port) { this.id = id; this.remoteId = remoteId; this.port = port; this.connection = new Promise((resolve, reject) => { this.#resolve = resolve; this.reject = reject; }); } disconnect() { this.connected = false; this.port.close(); } resolve(disconnectFn) { this.connected = true; this.#sendQueuedMessages(); this.#resolve(disconnectFn); } send(message) { if (this.connected && this.port) { logger(`EP(${this.id}): sending message '${message.payload.type}' to '${this.remoteId}':`, message); this.port.postMessage(message); } else { logger(`EP(${this.id}): queueing message:`, message); // making sure we have a cloned message in case message contains references // and pushing connect message before in the queue if (message.payload.type === 'connect') { this.#messageQueue.unshift(structuredClone(message)); } else { this.#messageQueue.push(structuredClone(message)); } } } #sendQueuedMessages() { for (const message of this.#messageQueue) { this.send(message); } this.#messageQueue.length = 0; } } class ConnectEndpoint extends Endpoint { targetWindow; targetOrigin; #remotePort; constructor(id, remoteId, targetWindow, targetOrigin) { // create a new message channel // same window -> simple LocalMessageChannel based on EventTarget // different windows -> real MessageChannel const { port1, port2 } = window === targetWindow ? new LocalMessageChannel() : new MessageChannel(); super(id, remoteId, port1); this.targetWindow = targetWindow; this.targetOrigin = targetOrigin; this.#remotePort = port2; } sendHandshake(message) { // Same window -> CustomEvent if (window === this.targetWindow) { logger(`EP(${this.id}): sending 'CustomEvent' handshake to '${this.remoteId}':`, message); window.dispatchEvent(new CustomEvent('handshake', { detail: { data: message, origin: this.targetOrigin, ports: [this.#remotePort], }, })); } // Different window -> postMessage else { logger(`EP(${this.id}): sending 'postMessage' handshake to '${this.remoteId}':`, message); this.targetWindow.postMessage(message, { targetOrigin: this.targetOrigin, transfer: [this.#remotePort], }); } } } /** * Checks that message type is known for the peer * @param message Message to check * @param peer Endpoint that processes the message */ function checkMessageIsKnown(message, peer) { const knownMessages = peer.knownPeers.get(peer.id); const { payload } = message; if (knownMessages && !knownMessages.find(({ type }) => type === payload.type)) { const knownTypes = [...new Set(knownMessages.map(({ type }) => type))]; throw new MessageError(message, `Unknown message type "${payload.type}". Known types: ${JSON.stringify(knownTypes)}`); } } /** * Checks that message version is known for the peer * @param message Message to check * @param peer Endpoint that processes the message */ function checkMessageVersionIsKnown(message, peer) { const knownMessages = peer.knownPeers.get(peer.id); const { payload } = message; if (knownMessages && !knownMessages?.find(({ type, version }) => type === payload.type && version === payload.version)) { const knownVersions = knownMessages .filter(({ type }) => type === payload.type) .map(({ version }) => version); throw new MessageError(message, `Unknown message version "${payload.version}". Known versions: ${JSON.stringify(knownVersions)}`); } } /** * Get default message checks for the given strategy * * @param strategy */ function getDefaultMessageChecks(strategy) { const checks = []; if (strategy === 'type' || strategy === 'version') { checks.push({ description: 'Check that message type is known', check: checkMessageIsKnown, }); } if (strategy === 'version') { checks.push({ description: 'Check that message version is known', check: checkMessageVersionIsKnown, }); } return checks; } const EMPTY_SUBSCRIPTION = { unsubscribe: () => { // do nothing }, }; class Emitter { #subscribers = new Set(); get subscribers() { return this.#subscribers; } subscribe(subscriber) { if (subscriber) { this.#subscribers.add(subscriber); return { unsubscribe: () => { this.#subscribers.delete(subscriber); }, }; } else { return EMPTY_SUBSCRIPTION; } } emit(value) { for (const subscriber of this.#subscribers) { if (typeof subscriber === 'function') { subscriber(value); } else { subscriber.next?.(value); } } } [Symbol.observable]() { return this; } ['@@observable']() { return this; } } /** * A global map of handshake handlers that are listening for incoming connections. * Maps peer id to a function that handles the handshake event for this peer. */ const HANDSHAKE_HANDLERS = new Map(); /** * Registers a global handshake handler for a specific peer id. * @param id * @param h */ function registerGlobalHandshakeHandler(id, h) { HANDSHAKE_HANDLERS.set(id, h); } /** * Unregisters a global handshake handler for a specific peer id. * * @param id */ function unregisterGlobalHandshakeHandler(id) { HANDSHAKE_HANDLERS.delete(id); } /** * Global handler for handshake messages. * * It checks if the message has the correct structure, is a handshake message, * and if there is a peer that is listening for this handshake * * @param handshakeEvent - postMessage or custom event that contains the handshake message */ const GLOBAL_HANDSHAKE_HANDLER = (handshakeEvent) => { let event; if (handshakeEvent instanceof CustomEvent) { event = handshakeEvent.detail; logger(`Received 'CustomEvent'`, handshakeEvent); } else { event = handshakeEvent; logger(`Received 'postMessage'`, handshakeEvent); } const message = event.data; // only accept messages of the expected structure (from, to, payload) try { checkMessageHasCorrectStructure(message); const { payload } = message; // -> (structure ok) // 1. Is this a 'handshake' message destined for us? if (!(payload.type === `handshake`)) { return; } // -> (structure ok; message of type 'handshake') // 2. Is there a handshakeHandler that is listening for connections? const handshakeHandler = HANDSHAKE_HANDLERS.get(payload.endpointId); if (handshakeHandler) { handshakeHandler(event); } else { logger(`HS declined: peer '${payload.endpointId}' is not among listening peers:`, [ ...HANDSHAKE_HANDLERS.keys(), ]); } } catch { // ignore malformed messages } }; window.addEventListener('message', GLOBAL_HANDSHAKE_HANDLER); window.addEventListener('handshake', GLOBAL_HANDSHAKE_HANDLER); /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Default message peer that can send and receive messages to/from other peers in the same document * or across different windows or iframes. * * Messages will be sent in a synchronous way if both peers are in the same window. * Otherwise, a `MessageChannel` will be established to send messages between different windows. * * ```ts * // Simple example of creating two peers and sending messages between them * const one = new MessagePeer({ id: 'one' }); * const two = new MessagePeer({ id: 'two' }); * * // connecting two peers * one.listen(); * two.connect('one'); * * // sending messages * one.send({ type: 'ping', version: '1.0' }); // broadcast * two.send({ type: 'pong', version: '1.0' }, { to: 'one' }); // send to a specific peer * * // receiving messages * one.messages.subscribe((message) => {}); // receives all messages sent to 'one' * one.serviceMessages.subscribe((message) => {}); // receives service messages like 'connect', 'disconnect', etc. * * // learning about the network * one.knownPeers; // lists all known peers and messages they can receive * * // disconnecting * one.disconnect(); // disconnects from all peers * one.disconnect('two'); // disconnects from a specific peer * ``` */ class MessagePeer { #id; #endpoints = new Map(); #endpointPeers = new Map(); #connectionFilters = []; #stopListening; #messageEmitter = new Emitter(); #serviceMessageEmitter = new Emitter(); #errorEmitter = new Emitter(); #messageCheckStrategy; #defaultMessageChecks; #knownPeers = new Map(); #messageQueue = []; constructor(options) { this.#id = options.id; this.#knownPeers.set(this.id, []); if (options.knownMessages) { for (const message of options.knownMessages) { this.registerMessage(message); } } this.#messageCheckStrategy = options.messageCheckStrategy || 'default'; this.#defaultMessageChecks = getDefaultMessageChecks(this.#messageCheckStrategy); this.#stopListening = () => { logger(`PEER(${this.id}): stopped listening for connections`); unregisterGlobalHandshakeHandler(this.id); this.#connectionFilters.length = 0; }; logger(`PEER(${this.id}): created`, this.#knownPeers); } /** * @inheritDoc */ get id() { return this.#id; } /** * @inheritDoc */ get knownPeers() { return this.#knownPeers; } /** * @inheritDoc */ get peerConnections() { return this.#endpointPeers; } /** * @inheritDoc */ get messages() { return this.#messageEmitter; } /** * @inheritDoc */ get serviceMessages() { return this.#serviceMessageEmitter; } /** * @inheritDoc */ get errors() { return this.#errorEmitter; } /** * @inheritDoc */ connect(remoteId, options) { logger(`PEER(${this.id}): connecting to '${remoteId}'`); // 1. processing options const hostWindow = options?.window || window; const hostOrigin = options?.origin || window.origin; checkOriginIsValid(hostOrigin); // 2. creating or getting an existing endpoint let existingEndpoint = this.#endpoints.get(remoteId); if (!existingEndpoint) { // creating a new endpoint const endpoint = new ConnectEndpoint(this.id, remoteId, hostWindow, hostOrigin); this.#endpoints.set(remoteId, endpoint); // 3. setting up port for message handling endpoint.port.onmessage = (event) => { const message = event.data; const payload = message.payload; logger(`PEER(${this.id}): '${payload.type}' message received from '${remoteId}':`, message); // 4. if handshake was done - we can handle the message if (endpoint.connected) { this.#handleMessage(endpoint, message); } // 5. handling handshake messages else if (payload.type === 'handshake') { if (payload.endpointId === this.id && payload.remoteId === remoteId) { logger(`PEER(${this.id}): handshake received from ${remoteId}:`, hostOrigin); this.#handleMessage(endpoint, message); endpoint.resolve(() => this.#disconnectEndpoint(endpoint)); } } else { logger(`PEER(${this.id}): handshake was expected, got:`, message); endpoint.reject(`Handshake was expected, got: ${JSON.stringify(message)}`); } }; // 6. sending handshake message to the peer const handshake = createHandshakeMessage(this.id, remoteId, this.#knownPeers); endpoint.sendHandshake(handshake); existingEndpoint = endpoint; } return existingEndpoint.connection; } /** * @inheritDoc */ send(message, options) { this.#send(message, options); } /** * @inheritDoc */ listen(filters = []) { // 1. normalizing filters // making sure that we have only `ConnectionFilter` objects in an array const normalizedFilters = Array.isArray(filters) ? filters.map(normalizeFilter) : [normalizeFilter(filters)]; // checking that filters are correct for (const { origin } of normalizedFilters) { if (origin !== undefined) { checkOriginIsValid(origin); } } this.#connectionFilters.length = 0; this.#connectionFilters.push(...normalizedFilters); // 2. accepting handshake messages registerGlobalHandshakeHandler(this.id, this.#handleHandshakeEvent.bind(this)); logger(`PEER(${this.id}): waiting for connections`, this.#connectionFilters); return this.#stopListening; } /** * @inheritDoc */ registerMessage(message) { const knownMessages = this.#knownPeers.get(this.id); if (!knownMessages.find((m) => m.type === message.type && m.version === message.version)) { knownMessages.push(message); } } /** * Logs the current state of the peer to the console */ log() { const endpoints = [...this.#endpoints.values()].map((e) => `${e.id}:${e.connected ? e.remoteId : e.remoteId + '*'}`); const endpointPeers = [...this.#endpointPeers].map(([id, peers]) => `${id}: ${[...peers].join(', ')}`); console.log(`PEER(${this.id}):`, endpoints, endpointPeers, this.#knownPeers); } /** * @inheritDoc */ disconnect(peerId) { if (peerId) { const endpoint = this.#endpoints.get(peerId); if (endpoint) { this.#disconnectEndpoint(endpoint); } } else { for (const endpoint of this.#endpoints.values()) { this.#disconnectEndpoint(endpoint); } } } /** * Handle handshake provided by the global handler * * @param event - handshake event pre-approved by the global handler */ #handleHandshakeEvent(event) { const { data: handshakeMessage, ports } = event; const { payload } = handshakeMessage; const { remoteId } = payload; // -> (structure ok; 'handshake' for us) // 1. Does the peer match our filters? if (!eventMatchesFilters(event, this.#connectionFilters)) { logger(`PEER(${this.id}): HS declined: connection from '${remoteId}' does not match any of the filters:`, this.#connectionFilters); return; } // -> (structure ok; 'handshake' for us; matches filters) // 2. Do we already have a connection to this peer? const exisingEndpoint = this.#endpoints.get(remoteId); if (exisingEndpoint) { logger(`PEER(${this.id}): already connected to '${remoteId}' -> disconnecting`); this.#disconnectEndpoint(exisingEndpoint); } // -> (structure ok; 'handshake' for us; matches filters; handled exising) // 3. Create a new endpoint for the peer, configure, and register it const [port] = ports; // configuring the connection port we've just received port.onmessage = ({ data }) => this.#handleMessage(endpoint, data); // new endpoint for the peer const endpoint = new Endpoint(this.id, remoteId, port); this.#endpoints.set(remoteId, endpoint); logger(`PEER(${this.id}): created endpoint '${remoteId}'`, endpoint); // 4. Do the handshake // creating a handshake before processing the message to avoid changing knownPeers const handshake = createHandshakeMessage(this.id, remoteId, this.#knownPeers); // processing 'handshake' internally this.#handleMessage(endpoint, handshakeMessage); // sending back the handshake message directly through the port endpoint.port.postMessage(handshake); // open connection endpoint.resolve(() => this.disconnect(remoteId)); } /** * Disconnect from a particular endpoint * @param endpoint - endpoint to disconnect from */ #disconnectEndpoint(endpoint) { const { remoteId } = endpoint; // 0. collecting all peers that will be disconnected const disconnectedPeers = [...(this.#endpointPeers.get(remoteId) || [])]; const unreachable = [this.id]; for (const [peerId, peers] of this.#endpointPeers) { if (peerId !== remoteId) { unreachable.push(...peers); } } // 1. notify the other side that WE will disconnect if (endpoint.connected) { endpoint.send({ from: this.id, to: [], payload: { type: 'disconnect', version: '1.0', disconnected: this.id, unreachable, }, }); } // 2. physically disconnecting this.#endpointPeers.delete(remoteId); for (const id of disconnectedPeers) { this.#knownPeers.delete(id); } this.#endpoints.delete(remoteId); endpoint.disconnect(); // 3. notify all other endpoints about the disconnection this.#send({ type: 'disconnect', version: '1.0', disconnected: remoteId, unreachable: disconnectedPeers, }); logger(`PEER(${this.id}): disconnected from '${remoteId}'`, this.#endpoints, this.#knownPeers); } #handleEndpointError(endpoint, error) { this.#errorEmitter.emit(error); if (this.#errorEmitter.subscribers.size === 0) { console.error(error); } // sending back to the endpoint we got it from endpoint.send({ from: this.id, to: [error.messageObject.from], payload: { type: 'error', version: '1.0', error: error.message, message: error.messageObject, }, }); } /** * Processes the message received form a particular endpoint. * In the end it can either notify the user about the message or forward it to other endpoints. * @param endpoint - endpoint that sent the message * @param message - message to process */ #handleMessage(endpoint, message) { logger(`PEER(${this.id}): received message`, message, this.#knownPeers); // validating incoming message structure try { checkMessageHasCorrectStructure(message, this.#messageCheckStrategy); } catch (error) { logger(`PEER(${this.id}): message is malformed`, message); this.#handleEndpointError(endpoint, error); return; } const { payload } = message; // handle service messages if (isServiceMessage(payload)) { switch (payload.type) { case 'handshake': { logger(`PEER(${this.id}): handshake message from '${payload.remoteId}'`, payload); const connected = [...this.knownPeers.keys()]; // 1. registering the new endpoint and its messages for (const [id, messages] of payload.knownPeers) { this.#registerRemoteMessages(id, messages); } this.#endpointPeers.set(payload.remoteId, new Set(payload.knownPeers.keys())); // 2. passing the message to the user this.#serviceMessageEmitter.emit(message); // 3. notifying all other endpoints that new endpoint is connected for (const e of this.#endpoints.values()) { if (e !== endpoint && e.connected) { e.send({ from: this.id, to: [], payload: { type: 'connect', version: '1.0', knownPeers: new Map([...this.#knownPeers].filter((key) => [...payload.knownPeers.keys()].includes(key[0]))), connected: [...payload.knownPeers.keys()], }, }); } } // 4. notifying the new endpoint about all other previously connected endpoints endpoint.send({ from: this.id, to: [payload.remoteId], payload: { type: 'connect', version: '1.0', knownPeers: new Map([...this.#knownPeers].filter((key) => connected.includes(key[0]))), connected, }, }); break; } case 'connect': { // only process the message if it's addressed to us, forward otherwise if (message.to.includes(this.id) || message.to.length === 0) { logger(`PEER(${this.id}): connect message from '${endpoint.remoteId}'`, payload); // 1. registering new messages for (const [id, messages] of payload.knownPeers) { this.#registerRemoteMessages(id, messages); } // 2. updating the list of known peers const knownPeers = this.#endpointPeers.get(endpoint.remoteId); if (knownPeers) { for (const id of payload.knownPeers.keys()) { if (id === this.id || [...this.#endpoints.keys()].includes(id)) { continue; } knownPeers.add(id); } } // as soon as we're connected properly, we dump the message queue this.#sendQueuedMessages(); // 3. passing the message to the user this.#serviceMessageEmitter.emit(message); } this.#forwardMessage(endpoint, message); break; } case 'disconnect': { logger(`PEER(${this.id}): disconnect message from '${endpoint.remoteId}'`, payload); // 1. disconnecting the endpoint const endpointToDisconnect = this.#endpoints.get(payload.disconnected); if (endpointToDisconnect) { endpoint.disconnect(); } // 2. removing all unreachable peers and endpoints for (const id of payload.unreachable) { this.#knownPeers.delete(id); this.#endpoints.delete(id); for (const [peerId, peers] of this.#endpointPeers) { peers.delete(id); if (peers.size === 0) { this.#endpointPeers.delete(peerId); } } } // 3. passing the message to the user and further on the network this.#serviceMessageEmitter.emit(message); this.#forwardMessage(endpoint, message); break; } case 'declare_messages': { logger(`PEER(${this.id}): declare_messages from '${message.from}'`, payload); this.#registerRemoteMessages(message.from, payload.messages); this.#serviceMessageEmitter.emit(message); this.#forwardMessage(endpoint, message); break; } case 'error': { logger(`PEER(${this.id}): 'error' message from '${message.from}'`, payload); if (message.to.includes(this.id)) { this.#serviceMessageEmitter.emit(message); } else { this.#forwardMessage(endpoint, message); } break; } default: { logger(`PEER(${this.id}):`, `unknown message type: ${payload['type']}`); this.#handleEndpointError(endpoint, new MessageError(message, `unknown message type: ${payload['type']}`)); } } } // handling user messages, processing errors and forwarding the message further if necessary else { try { if (message.to.includes(this.id) || message.to.length === 0) { for (const { check } of this.#defaultMessageChecks) { check(message, this); } this.#messageEmitter.emit(message); } } catch (error) { logger(`PEER(${this.id}):`, error); this.#handleEndpointError(endpoint, error); } finally { this.#forwardMessage(endpoint, message); } } } /** * Sends a message `M` or {@link ServiceMessage} to the network. * @param payload - message to send * @param options - additional {@link PeerSendOptions} for the message delivery */ #send(payload, options) { const message = { from: this.id, to: options?.to ? (Array.isArray(options.to) ? options.to : [options.to]) : [], payload, }; // we have established connection to at least one peer if (this.#knownPeers.size > 1) { for (const endpoint of this.#endpoints.values()) { endpoint.send(message); } } // queueing only user messages, ex. no need to queue 'connect'/'disconnect'/'error' messages // all the necessary data will be passed in the initial 'connect' message for new peers else if (!isServiceMessage(payload)) { this.#messageQueue.push(message); } } /** * Forwards the message to all other endpoints except the receivedFrom. * @param receivedFrom - endpoint that should not receive the message, we just got the message from it * @param message - message to forward */ #forwardMessage(receivedFrom, message) { // we're the only recipient for the message -> no need to forward if (message.to.length === 1 && message.to[0] === this.id) { return; } // forwarding the message to all other endpoints for (const e of this.#endpoints.values()) { if (e !== receivedFrom && e.connected) { e.send(message); } } } /** * Registers messages that the remote peer can receive. * @param peerId - id of the peer that can receive the messages * @param messages - list of messages the peer can receive */ #registerRemoteMessages(peerId, messages) { if (this.id !== peerId) { const knownMessages = this.#knownPeers.get(peerId); if (!knownMessages) { this.#knownPeers.set(peerId, messages); } else { for (const message of messages) { if (!knownMessages.find((m) => m.type === message.type && m.version === message.version)) { knownMessages.push(message); } } } } } #sendQueuedMessages() { for (const message of this.#messageQueue) { for (const e of this.#endpoints.values()) { e.send(message); } } this.#messageQueue.length = 0; } } export { MessageError, MessagePeer, enableLogging, isServiceMessage }; //# sourceMappingURL=index.js.map