@boringnode/transmit
Version:
A framework agnostic Server-Sent-Event library
150 lines (149 loc) • 4.86 kB
JavaScript
/*
* @boringnode/transmit
*
* @license MIT
* @copyright BoringNode
*/
import { clearInterval } from 'node:timers';
import Emittery from 'emittery';
import { Bus } from '@boringnode/bus';
import string from '@poppinss/utils/string';
import { StreamManager } from './stream_manager.js';
import { TransportMessageType } from './transport_message_type.js';
export class Transmit {
/**
* The configuration for the transmit instance
*/
#config;
/**
* The stream manager instance
*/
#manager;
/**
* The transport channel to synchronize messages and subscriptions
* across multiple instance.
*/
#transportChannel;
/**
* The transport provider to synchronize messages and subscriptions
* across multiple instance.
*/
#bus;
/**
* The emittery instance to emit events.
*/
#emittery;
/**
* The interval to send ping messages to all the subscribers.
*/
#interval;
constructor(config, transport) {
this.#config = config;
this.#manager = new StreamManager();
this.#emittery = new Emittery();
this.#bus = transport ? new Bus(transport, { retryQueue: { enabled: true } }) : null;
this.#transportChannel = this.#config.transport?.channel ?? 'transmit::broadcast';
// Subscribe to the transport channel and handle incoming messages
void this.#bus?.subscribe(this.#transportChannel, (message) => {
void this.#broadcastLocally(message.channel, message.payload);
});
// Start the ping interval if configured
if (this.#config.pingInterval) {
const intervalValue = typeof this.#config.pingInterval === 'number'
? this.#config.pingInterval
: string.milliseconds.parse(this.#config.pingInterval);
this.#interval = setInterval(() => this.#ping(), intervalValue);
}
}
getManager() {
return this.#manager;
}
createStream(params) {
return this.#manager.createStream({
...params,
onConnect: () => {
void this.#emittery.emit('connect', {
uid: params.uid,
context: params.context,
});
},
onDisconnect: () => {
void this.#emittery.emit('disconnect', {
uid: params.uid,
context: params.context,
});
},
});
}
authorize(channel, callback) {
this.#manager.authorize(channel, callback);
}
subscribe(params) {
return this.#manager.subscribe({
...params,
onSubscribe: ({ uid, channel, context }) => {
void this.#emittery.emit('subscribe', {
uid,
channel,
context,
});
},
});
}
unsubscribe(params) {
return this.#manager.unsubscribe({
...params,
onUnsubscribe: ({ uid, channel, context }) => {
void this.#emittery.emit('unsubscribe', {
uid,
channel,
context,
});
},
});
}
#broadcastLocally(channel, payload, senderUid) {
const subscribers = this.#manager.findByChannel(channel);
for (const subscriber of subscribers) {
if (Array.isArray(senderUid)
? senderUid.includes(subscriber.getUid())
: senderUid === subscriber.getUid()) {
continue;
}
subscriber.writeMessage({ data: { channel, payload } });
}
}
broadcastExcept(channel, payload, senderUid) {
return this.#broadcastLocally(channel, payload, senderUid);
}
broadcast(channel, payload) {
if (payload === undefined) {
payload = null;
}
void this.#bus?.publish(this.#transportChannel, {
type: TransportMessageType.Broadcast,
channel,
payload,
});
this.#broadcastLocally(channel, payload);
void this.#emittery.emit('broadcast', { channel, payload });
}
on(event, callback) {
return this.#emittery.on(event, callback);
}
async shutdown() {
if (this.#interval) {
clearInterval(this.#interval);
}
await this.#bus?.disconnect();
}
getSubscribersFor(channel) {
const subscribers = this.#manager.findByChannel(channel);
return Array.from(subscribers).map((subscriber) => subscriber.getUid());
}
#ping() {
for (const [stream] of this.#manager.getAllSubscribers()) {
stream.writeMessage({ data: { channel: '$$transmit/ping', payload: {} } });
}
}
}