theater-client
Version:
TypeScript client library for Theater actor system TCP protocol
493 lines • 18.8 kB
JavaScript
/**
* High-level Theater client with hygienic connection management
* Each operation gets its own TCP connection to avoid response multiplexing
*/
import { TheaterConnection } from '../connection/TheaterConnection.js';
import { TheaterError,
// TheaterConnectionError,
// TheaterTimeoutError,
TheaterProtocolError } from '../types/client.js';
import { createLogger } from '../utils/logger.js';
import { uint8ArrayToNumbers, numbersToUint8Array } from '../utils/serialization.js';
import { Actor } from './Actor.js';
// Note: These imports are done dynamically to avoid circular dependencies
// import { ChannelStreamImpl } from './ChannelStream.js';
// import { ActorEventStreamImpl } from './ActorEventStream.js';
const log = createLogger('TheaterClient');
/**
* High-level Theater client that manages operations using hygienic connections
* Each operation gets its own TCP connection to avoid response multiplexing issues
*/
export class TheaterClient {
host;
port;
config;
constructor(host = '127.0.0.1', port = 9000, config = {}) {
this.host = host;
this.port = port;
this.config = {
timeout: 30000,
retryAttempts: 3,
retryDelay: 1000,
...config
};
log.info(`Theater client initialized for ${host}:${port}`);
}
/**
* Create and connect a new Theater connection for a specific operation
*/
async createConnection() {
const connection = new TheaterConnection(this.host, this.port, this.config);
await connection.connect();
return connection;
}
/**
* Execute an operation with a dedicated connection and automatic cleanup
*/
async withConnection(operation) {
const connection = await this.createConnection();
try {
return await operation(connection);
}
finally {
connection.close();
}
}
/**
* Handle error responses from the server
*/
handleErrorResponse(response) {
if ('Error' in response && response.Error) {
const error = response.Error.error;
let message;
if (typeof error === 'string') {
message = error;
}
else if (typeof error === 'object' && error !== null) {
const errorObj = error;
const errorType = Object.keys(errorObj)[0];
if (errorType) {
const errorValue = errorObj[errorType];
message = typeof errorValue === 'string' ? `${errorType}: ${errorValue}` : errorType;
}
else {
message = 'Unknown error object';
}
}
else {
message = 'Unknown error';
}
throw new TheaterError(message, 'SERVER_ERROR', error);
}
throw new TheaterProtocolError(`Unexpected response: ${JSON.stringify(response)}`);
}
// ===== ACTOR MANAGEMENT OPERATIONS =====
/**
* Get an Actor wrapper for an existing actor ID
*/
actor(id) {
return new Actor(id, this);
}
/**
* Start a new actor and return an Actor wrapper
*/
async startActor(params) {
const needsSupervision = params.onEvent || params.onError || params.onActorResult;
if (needsSupervision) {
return this.startSupervisedActor(params);
}
else {
return this.startRegularActor(params);
}
}
/**
* Start a supervised actor with event streaming
*/
async startSupervisedActor(params) {
log.info(`Starting supervised actor with manifest: ${params.manifest}`);
// Create dedicated connection for event stream
const eventConnection = await this.createConnection();
try {
const command = {
StartActor: {
manifest: params.manifest,
initial_state: params.initialState ? uint8ArrayToNumbers(params.initialState) : null,
parent: !!(params.onActorResult),
subscribe: !!(params.onEvent || params.onError)
}
};
await eventConnection.send(command);
// Wait for ActorStarted, but keep connection open
while (true) {
const response = await eventConnection.receive();
if ('ActorStarted' in response && response.ActorStarted) {
const actorId = response.ActorStarted.id;
log.info(`Supervised actor started: ${actorId}`);
// Return actor with event connection
const callbacks = {};
if (params.onEvent)
callbacks.onEvent = params.onEvent;
if (params.onError)
callbacks.onError = params.onError;
if (params.onActorResult)
callbacks.onActorResult = params.onActorResult;
return new Actor(actorId, this, eventConnection, callbacks);
}
if ('Error' in response) {
eventConnection.close();
this.handleErrorResponse(response);
}
// Ignore other message types and continue waiting
log.debug(`Ignoring response type while starting supervised actor: ${Object.keys(response)[0]}`);
}
}
catch (error) {
eventConnection.close();
throw error;
}
}
/**
* Start a regular actor (current behavior)
*/
async startRegularActor(params) {
const actorId = await this.startActorRaw(params);
return new Actor(actorId, this);
}
/**
* Start a new actor with the given parameters (returns raw ID)
*/
async startActorRaw(params) {
log.info(`Starting actor with manifest: ${params.manifest}`);
return this.withConnection(async (conn) => {
const command = {
StartActor: {
manifest: params.manifest,
initial_state: params.initialState ? uint8ArrayToNumbers(params.initialState) : null,
parent: false,
subscribe: false
}
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('ActorStarted' in response && response.ActorStarted) {
log.info(`Actor started: ${response.ActorStarted.id}`);
return response.ActorStarted.id;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
// Ignore other message types and continue waiting
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
/**
* Stop a running actor
*/
async stopActor(id) {
log.info(`Stopping actor: ${id}`);
return this.withConnection(async (conn) => {
const command = {
StopActor: { id }
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('ActorStopped' in response && response.ActorStopped) {
log.info(`Actor stopped: ${id}`);
return;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
/**
* List all running actors and return Actor wrappers
*/
async listActors() {
const actorInfos = await this.listActorsRaw();
return actorInfos.map(info => new Actor(info.id, this));
}
/**
* List all running actors (returns raw actor info)
*/
async listActorsRaw() {
log.info('Listing actors');
return this.withConnection(async (conn) => {
const command = {
ListActors: {}
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('ActorList' in response && response.ActorList) {
const actors = response.ActorList.actors.map(([id, manifest]) => ({
id,
manifest
}));
log.info(`Found ${actors.length} actors`);
return actors;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
/**
* Get the status of a specific actor
*/
async getActorStatus(id) {
log.info(`Getting status for actor: ${id}`);
return this.withConnection(async (conn) => {
const command = {
GetActorStatus: { id }
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('ActorStatus' in response && response.ActorStatus) {
log.info(`Got status for actor: ${id}`);
return response.ActorStatus.status;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
/**
* Restart an actor
*/
async restartActor(id) {
log.info(`Restarting actor: ${id}`);
return this.withConnection(async (conn) => {
const command = {
RestartActor: { id }
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('Restarted' in response && response.Restarted) {
log.info(`Actor restarted: ${id}`);
return;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
/**
* Get the manifest configuration of an actor
*/
async getActorManifest(id) {
log.info(`Getting manifest for actor: ${id}`);
return this.withConnection(async (conn) => {
const command = {
GetActorManifest: { id }
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('ActorManifest' in response && response.ActorManifest) {
log.info(`Got manifest for actor: ${id}`);
return response.ActorManifest.manifest;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
/**
* Get the current state of an actor
*/
async getActorState(id) {
log.info(`Getting state for actor: ${id}`);
return this.withConnection(async (conn) => {
const command = {
GetActorState: { id }
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('ActorState' in response && response.ActorState) {
log.info(`Got state for actor: ${id}`);
return response.ActorState.state ? numbersToUint8Array(response.ActorState.state) : null;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
/**
* Get the event history for an actor
*/
async getActorEvents(id) {
log.info(`Getting events for actor: ${id}`);
return this.withConnection(async (conn) => {
const command = {
GetActorEvents: { id }
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('ActorEvents' in response && response.ActorEvents) {
log.info(`Got ${response.ActorEvents.events.length} events for actor: ${id}`);
return response.ActorEvents.events;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
/**
* Get metrics for an actor
*/
async getActorMetrics(id) {
log.info(`Getting metrics for actor: ${id}`);
return this.withConnection(async (conn) => {
const command = {
GetActorMetrics: { id }
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('ActorMetrics' in response && response.ActorMetrics) {
log.info(`Got metrics for actor: ${id}`);
return response.ActorMetrics.metrics;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
// ===== MESSAGING OPERATIONS =====
/**
* Send a fire-and-forget message to an actor
*/
async sendActorMessage(id, data) {
log.info(`Sending message to actor: ${id}`);
return this.withConnection(async (conn) => {
const command = {
SendActorMessage: {
id,
data: uint8ArrayToNumbers(data)
}
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('SentMessage' in response && response.SentMessage) {
log.info(`Message sent to actor: ${id}`);
return;
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
/**
* Send a request message to an actor and wait for response
*/
async requestActorMessage(id, data) {
log.info(`Requesting message from actor: ${id}`);
return this.withConnection(async (conn) => {
const command = {
RequestActorMessage: {
id,
data: uint8ArrayToNumbers(data)
}
};
await conn.send(command);
while (true) {
const response = await conn.receive();
if ('RequestedMessage' in response && response.RequestedMessage) {
log.info(`Got response from actor: ${id}`);
return numbersToUint8Array(response.RequestedMessage.message);
}
if ('Error' in response) {
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type: ${Object.keys(response)[0]}`);
}
});
}
// ===== CHANNEL OPERATIONS =====
/**
* Open a communication channel with an actor
*/
async openChannel(participant, initialMessage) {
log.info(`Opening channel with participant: ${JSON.stringify(participant)}`);
// Channels need long-lived connections, so we don't use withConnection here
const connection = await this.createConnection();
const command = {
OpenChannel: {
actor_id: participant,
initial_message: initialMessage ? uint8ArrayToNumbers(initialMessage) : []
}
};
await connection.send(command);
// Wait for channel to open
while (true) {
const response = await connection.receive();
if ('ChannelOpened' in response && response.ChannelOpened) {
log.info(`Channel opened: ${response.ChannelOpened.channel_id}`);
// Dynamically import to avoid circular dependencies
const { ChannelStreamImpl } = await import('./ChannelStream.js');
return new ChannelStreamImpl(response.ChannelOpened.channel_id, connection, this);
}
if ('Error' in response) {
connection.close();
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type while waiting for channel: ${Object.keys(response)[0]}`);
}
}
// ===== SUBSCRIPTION OPERATIONS =====
/**
* Subscribe to events from an actor
*/
async subscribeToActor(id) {
log.info(`Subscribing to actor events: ${id}`);
// Subscriptions need long-lived connections, so we don't use withConnection here
const connection = await this.createConnection();
const command = {
SubscribeToActor: { id }
};
await connection.send(command);
// Wait for subscription confirmation
while (true) {
const response = await connection.receive();
if ('Subscribed' in response && response.Subscribed) {
log.info(`Subscribed to actor: ${id} with subscription ID: ${response.Subscribed.subscription_id}`);
// Dynamically import to avoid circular dependencies
const { ActorEventStreamImpl } = await import('./ActorEventStream.js');
return new ActorEventStreamImpl(id, response.Subscribed.subscription_id, connection, this);
}
if ('Error' in response) {
connection.close();
this.handleErrorResponse(response);
}
log.debug(`Ignoring response type while waiting for subscription: ${Object.keys(response)[0]}`);
}
}
}
//# sourceMappingURL=TheaterClient.js.map