@amadeus-it-group/microfrontends
Version:
Amadeus Micro Frontend Toolkit
944 lines (934 loc) • 34.6 kB
JavaScript
/**
* 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