UNPKG

theater-client

Version:

TypeScript client library for Theater actor system TCP protocol

493 lines 18.8 kB
/** * 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