@synet/realtime
Version:
Realtime Communication server/client implementations
220 lines (218 loc) • 8.33 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebSocketRealtimeChannel = void 0;
const realtime_1 = require("@synet/patterns/realtime");
const node_crypto_1 = __importDefault(require("node:crypto")); // Add this import
/**
* WebSocket implementation of RealtimeChannel
*/
class WebSocketRealtimeChannel {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.id = node_crypto_1.default.randomUUID();
this.socket = null;
this.state = realtime_1.ChannelState.DISCONNECTED;
this.eventHandlers = new Map();
this.reconnectTimer = null;
this.reconnectAttempts = 0;
this.connectionPromise = null;
if (this.options.authToken) {
const urlObj = new URL(this.url);
urlObj.searchParams.set("token", this.options.authToken);
this.url = urlObj.toString();
}
}
/**
* Connect to the WebSocket channel
* Returns immediately but establishes connection in background
*/
connect() {
// If already connecting or connected, return the existing promise
if (this.connectionPromise) {
return this.connectionPromise;
}
if (this.state === realtime_1.ChannelState.CONNECTED) {
return Promise.resolve();
}
this.state = realtime_1.ChannelState.CONNECTING;
this.connectionPromise = new Promise((resolve, reject) => {
try {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
this.state = realtime_1.ChannelState.CONNECTED;
this.reconnectAttempts = 0;
resolve();
};
this.socket.onclose = (event) => {
const previousState = this.state;
this.state = realtime_1.ChannelState.DISCONNECTED;
this.connectionPromise = null;
if (previousState === realtime_1.ChannelState.CONNECTED &&
this.options.reconnect?.enabled) {
this.scheduleReconnect();
}
if (previousState === realtime_1.ChannelState.CONNECTING) {
reject(new Error(`Failed to connect: ${event.code}`));
}
};
this.socket.onerror = (error) => {
const previousState = this.state;
this.state = realtime_1.ChannelState.ERROR;
if (previousState === realtime_1.ChannelState.CONNECTING) {
this.connectionPromise = null;
reject(error);
}
};
this.socket.onmessage = (message) => {
try {
const event = JSON.parse(message.data);
this.processIncomingEvent(event);
}
catch (error) {
console.error("Error processing message:", error);
}
};
}
catch (error) {
this.state = realtime_1.ChannelState.ERROR;
this.connectionPromise = null;
reject(error);
}
});
// Connection happens in background, we don't await it here
return this.connectionPromise;
}
on(selector, handler) {
let eventType;
if (typeof selector === "string") {
eventType = selector;
}
else if (selector.type && selector.source) {
eventType = `${selector.source}.${selector.type}`;
}
else if (selector.type) {
eventType = selector.type;
}
else if (selector.source) {
eventType = `${selector.source}.*`;
}
else {
eventType = "*";
}
if (!this.eventHandlers.has(eventType)) {
this.eventHandlers.set(eventType, new Set());
}
const handlers = this.eventHandlers.get(eventType);
if (handlers) {
handlers.add(handler);
}
return () => {
const handlers = this.eventHandlers.get(eventType);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.eventHandlers.delete(eventType);
}
}
};
}
/*
async emit(RealtimeEvent: TOut): Promise<void> {
if (this.state !== ChannelState.CONNECTED || !this.socket) {
throw new Error(`Cannot emit RealtimeEvent: channel not connected (state: ${this.state})`);
}
this.socket.send(JSON.stringify(RealtimeEvent));
} */
async emit(event) {
// If connecting, wait for the connection to complete
if (this.state === realtime_1.ChannelState.CONNECTING && this.connectionPromise) {
await this.connectionPromise;
}
// If disconnected or error, try to establish connection
else if (this.state !== realtime_1.ChannelState.CONNECTED) {
await this.connect();
}
// At this point we should be connected, but double-check
if (!this.socket || this.state !== realtime_1.ChannelState.CONNECTED) {
throw new Error(`Cannot emit event: channel not connected (state: ${this.state})`);
}
this.socket.send(JSON.stringify(event));
}
async close() {
if (this.state === realtime_1.ChannelState.DISCONNECTED || !this.socket) {
return;
}
this.state = realtime_1.ChannelState.DISCONNECTING;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
return new Promise((resolve) => {
if (!this.socket) {
this.state = realtime_1.ChannelState.DISCONNECTED;
resolve();
return;
}
this.socket.onclose = () => {
this.state = realtime_1.ChannelState.DISCONNECTED;
this.socket = null;
resolve();
};
this.socket.close();
});
}
getState() {
return this.state;
}
processIncomingEvent(RealtimeEvent) {
// Process exact type handlers
const exactHandlers = this.eventHandlers.get(RealtimeEvent.type);
if (exactHandlers) {
for (const handler of exactHandlers) {
try {
handler(RealtimeEvent);
}
catch (error) {
console.error(`Error in RealtimeEvent handler for ${RealtimeEvent.type}:`, error);
}
}
}
// Process wildcard handlers
const wildcardHandlers = this.eventHandlers.get("*");
if (wildcardHandlers) {
for (const handler of wildcardHandlers) {
try {
handler(RealtimeEvent);
}
catch (error) {
console.error(`Error in wildcard handler for ${RealtimeEvent.type}:`, error);
}
}
}
}
scheduleReconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
const maxAttempts = this.options.reconnect?.maxAttempts ?? 5;
if (this.reconnectAttempts >= maxAttempts) {
return;
}
const initialDelay = this.options.reconnect?.initialDelayMs ?? 1000;
const maxDelay = this.options.reconnect?.maxDelayMs ?? 30000;
// Exponential backoff
const delay = Math.min(initialDelay * 2 ** this.reconnectAttempts, maxDelay);
this.reconnectAttempts++;
this.reconnectTimer = setTimeout(() => {
this.connect().catch((error) => {
console.error("Reconnection failed:", error);
this.scheduleReconnect();
});
}, delay);
}
}
exports.WebSocketRealtimeChannel = WebSocketRealtimeChannel;