theater-client
Version:
TypeScript client library for Theater actor system TCP protocol
212 lines • 6.94 kB
JavaScript
/**
* Actor event stream implementation for subscribing to actor events
*/
import { EventEmitter } from 'node:events';
import { TheaterError,
// TheaterConnectionError,
// TheaterProtocolError
} from '../types/client.js';
import { createLogger } from '../utils/logger.js';
const log = createLogger('ActorEventStream');
/**
* Implementation of ActorEventStream for subscribing to actor events
* Manages a long-lived connection for receiving actor events
*/
export class ActorEventStreamImpl extends EventEmitter {
actorId;
subscriptionId;
connection;
client;
eventHandlers = new Set();
closeHandlers = new Set();
errorHandlers = new Set();
_isActive = true;
eventLoopRunning = false;
constructor(actorId, subscriptionId, connection, client) {
super();
this.actorId = actorId;
this.subscriptionId = subscriptionId;
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 event listening loop
this.startEventLoop();
log.info(`Actor event stream created for actor: ${actorId}, subscription: ${subscriptionId}`);
}
get isActive() {
return this._isActive && this.connection.connected;
}
/**
* Start the continuous event listening loop
*/
async startEventLoop() {
if (this.eventLoopRunning) {
return;
}
this.eventLoopRunning = true;
log.debug(`Starting event loop for actor: ${this.actorId}`);
try {
while (this._isActive && this.connection.connected) {
try {
const response = await this.connection.receive();
this.handleResponse(response);
}
catch (error) {
if (this._isActive) {
log.error(`Error in event loop: ${error}`);
this.handleError(error instanceof Error ? error : new Error(String(error)));
}
break;
}
}
}
finally {
this.eventLoopRunning = false;
log.debug(`Event loop ended for actor: ${this.actorId}`);
}
}
/**
* Handle incoming responses from the connection
*/
handleResponse(response) {
log.debug(`Received response: ${JSON.stringify(response)}`);
if ('ActorEvent' in response && response.ActorEvent) {
log.debug(`Received actor event for actor: ${this.actorId}`);
this.notifyEventHandlers(response.ActorEvent.event);
}
else if ('Unsubscribed' in response && response.Unsubscribed) {
if (response.Unsubscribed.id === this.actorId) {
log.info(`Unsubscribed from actor: ${this.actorId}`);
this.handleClose();
}
}
else if ('Error' in response && response.Error) {
log.error(`Subscription error: ${JSON.stringify(response.Error)}`);
this.handleError(new TheaterError('Subscription error', 'SUBSCRIPTION_ERROR', response.Error.error));
}
else {
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
}
/**
* Notify all event handlers of a new event
*/
notifyEventHandlers(event) {
this.eventHandlers.forEach(handler => {
try {
handler(event);
}
catch (error) {
log.error(`Error in event handler: ${error}`);
}
});
}
/**
* Handle subscription close event
*/
handleClose() {
if (!this._isActive) {
return;
}
this._isActive = false;
log.info(`Actor event stream closed for actor: ${this.actorId}`);
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(`Actor event 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 an event handler
*/
onEvent(handler) {
log.debug(`Adding event handler, total handlers: ${this.eventHandlers.size + 1}`);
this.eventHandlers.add(handler);
return () => {
log.debug(`Removing event handler, remaining: ${this.eventHandlers.size - 1}`);
this.eventHandlers.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);
};
}
/**
* Close the subscription
*/
close() {
if (!this._isActive) {
return;
}
log.info(`Closing subscription for actor: ${this.actorId}`);
// Send unsubscribe command on a separate connection (hygienic pattern)
const unsubscribe = async () => {
const unsubConnection = new (await import('../connection/TheaterConnection.js')).TheaterConnection(this.client.host, this.client.port, this.client.config);
try {
await unsubConnection.connect();
const command = {
UnsubscribeFromActor: {
id: this.actorId,
subscription_id: this.subscriptionId
}
};
await unsubConnection.send(command);
// Don't wait for response, just close
}
catch (error) {
log.error(`Error unsubscribing: ${error}`);
}
finally {
unsubConnection.close();
}
};
// Fire and forget the unsubscribe command
unsubscribe().catch(error => {
log.error(`Failed to send unsubscribe command: ${error}`);
});
// Close our receiving connection
this.connection.close();
this.handleClose();
}
}
//# sourceMappingURL=ActorEventStream.js.map