theater-client
Version:
TypeScript client library for Theater actor system TCP protocol
256 lines • 8.92 kB
JavaScript
/**
* Channel stream implementation for real-time communication with Theater actors
*/
import { EventEmitter } from 'node:events';
import { TheaterError,
// TheaterConnectionError,
// TheaterProtocolError
} from '../types/client.js';
import { createLogger } from '../utils/logger.js';
import { uint8ArrayToNumbers, numbersToUint8Array } from '../utils/serialization.js';
const log = createLogger('ChannelStream');
/**
* Implementation of ChannelStream for real-time communication
* Manages a long-lived connection for bidirectional messaging
*/
export class ChannelStreamImpl extends EventEmitter {
channelId;
connection;
client;
messageHandlers = new Set();
closeHandlers = new Set();
errorHandlers = new Set();
_isOpen = true;
messageLoopRunning = false;
constructor(channelId, connection, client) {
super();
this.channelId = channelId;
this.connection = connection;
this.client = client;
// Set up connection event handlers
this.connection.on('disconnect', () => {
this.handleClose();
});
this.connection.on('error', (error) => {
this.handleError(error);
});
// Start the message listening loop
this.startMessageLoop();
log.info(`Channel stream created: ${channelId}`);
}
get isOpen() {
return this._isOpen && this.connection.connected;
}
/**
* Start the continuous message listening loop
*/
async startMessageLoop() {
if (this.messageLoopRunning) {
return;
}
this.messageLoopRunning = true;
log.debug(`Starting message loop for channel: ${this.channelId}`);
try {
while (this._isOpen && this.connection.connected) {
try {
const response = await this.connection.receive();
this.handleResponse(response);
}
catch (error) {
if (this._isOpen) {
log.error(`Error in message loop: ${error}`);
this.handleError(error instanceof Error ? error : new Error(String(error)));
}
break;
}
}
}
finally {
this.messageLoopRunning = false;
log.debug(`Message loop ended for channel: ${this.channelId}`);
}
}
/**
* Handle incoming responses from the connection
*/
handleResponse(response) {
log.debug(`Received response: ${JSON.stringify(response)}`);
if ('ChannelMessage' in response && response.ChannelMessage) {
if (response.ChannelMessage.channel_id === this.channelId) {
const message = {
senderId: response.ChannelMessage.sender_id,
data: numbersToUint8Array(response.ChannelMessage.message),
timestamp: new Date()
};
log.debug(`Received channel message from ${JSON.stringify(message.senderId)}, length: ${message.data.length}`);
this.notifyMessageHandlers(message);
}
}
else if ('ChannelClosed' in response && response.ChannelClosed) {
if (response.ChannelClosed.channel_id === this.channelId) {
log.info(`Channel closed: ${this.channelId}`);
this.handleClose();
}
}
else if ('Error' in response && response.Error) {
log.error(`Channel error: ${JSON.stringify(response.Error)}`);
this.handleError(new TheaterError('Channel error', 'CHANNEL_ERROR', response.Error.error));
}
else {
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
}
/**
* Notify all message handlers of a new message
*/
notifyMessageHandlers(message) {
this.messageHandlers.forEach(handler => {
try {
handler(message);
}
catch (error) {
log.error(`Error in message handler: ${error}`);
}
});
}
/**
* Handle channel close event
*/
handleClose() {
if (!this._isOpen) {
return;
}
this._isOpen = false;
log.info(`Channel stream closed: ${this.channelId}`);
this.closeHandlers.forEach(handler => {
try {
handler();
}
catch (error) {
log.error(`Error in close handler: ${error}`);
}
});
this.emit('close');
}
/**
* Handle error events
*/
handleError(error) {
log.error(`Channel stream error: ${error.message}`);
this.errorHandlers.forEach(handler => {
try {
handler(error);
}
catch (handlerError) {
log.error(`Error in error handler: ${handlerError}`);
}
});
this.emit('error', error);
}
// ===== PUBLIC INTERFACE =====
/**
* Register a message handler
*/
onMessage(handler) {
log.debug(`Adding message handler, total handlers: ${this.messageHandlers.size + 1}`);
this.messageHandlers.add(handler);
return () => {
log.debug(`Removing message handler, remaining: ${this.messageHandlers.size - 1}`);
this.messageHandlers.delete(handler);
};
}
/**
* Register a close handler
*/
onClose(handler) {
this.closeHandlers.add(handler);
return () => {
this.closeHandlers.delete(handler);
};
}
/**
* Register an error handler
*/
onError(handler) {
this.errorHandlers.add(handler);
return () => {
this.errorHandlers.delete(handler);
};
}
/**
* Send a message on this channel
*/
async sendMessage(data) {
if (!this.isOpen) {
throw new TheaterError('Cannot send message on closed channel', 'CHANNEL_CLOSED');
}
log.info(`Sending message on channel ${this.channelId}, length: ${data.length}`);
// Create a new connection for sending (hygienic pattern)
// The receiving connection stays dedicated to listening
const sendConnection = new (await import('../connection/TheaterConnection.js')).TheaterConnection(this.client.host, this.client.port, this.client.config);
try {
await sendConnection.connect();
const command = {
SendOnChannel: {
channel_id: this.channelId,
message: uint8ArrayToNumbers(data)
}
};
await sendConnection.send(command);
// Wait for confirmation
while (true) {
const response = await sendConnection.receive();
if ('MessageSent' in response && response.MessageSent) {
if (response.MessageSent.channel_id === this.channelId) {
log.info(`Message sent on channel: ${this.channelId}`);
return;
}
}
if ('Error' in response && response.Error) {
throw new TheaterError('Failed to send message', 'SEND_ERROR', response.Error.error);
}
log.debug(`Ignoring response type while sending: ${Object.keys(response)[0]}`);
}
}
finally {
sendConnection.close();
}
}
/**
* Close the channel
*/
close() {
if (!this._isOpen) {
return;
}
log.info(`Closing channel: ${this.channelId}`);
// Send close command on a separate connection (hygienic pattern)
const closeChannel = async () => {
const closeConnection = new (await import('../connection/TheaterConnection.js')).TheaterConnection(this.client.host, this.client.port, this.client.config);
try {
await closeConnection.connect();
const command = {
CloseChannel: {
channel_id: this.channelId
}
};
await closeConnection.send(command);
// Don't wait for response, just close
}
catch (error) {
log.error(`Error closing channel: ${error}`);
}
finally {
closeConnection.close();
}
};
// Fire and forget the close command
closeChannel().catch(error => {
log.error(`Failed to send close command: ${error}`);
});
// Close our receiving connection
this.connection.close();
this.handleClose();
}
}
//# sourceMappingURL=ChannelStream.js.map