UNPKG

theater-client

Version:

TypeScript client library for Theater actor system TCP protocol

231 lines (195 loc) 6.02 kB
/** * Actor wrapper class - provides ergonomic interface around an actor ID */ import type { TheaterClient } from './TheaterClient.js'; import { TheaterConnection } from '../connection/TheaterConnection.js'; import type { TheaterId, ActorStatus, ManifestConfig, ChainEvent, ManagementResponse } from '../types/protocol.js'; import type { ChannelStream, ActorEventStream, ActorCallbacks } from '../types/client.js'; import { TheaterConnectionError } from '../types/client.js'; import { encodeJson, decodeJson } from '../utils/serialization.js'; import { createLogger } from '../utils/logger.js'; const log = createLogger('Actor'); /** * Actor wrapper that provides a clean, object-oriented interface * around an actor ID, automatically filling in the ID for all operations */ export class Actor { // Separate connection just for event streaming private eventConnection: TheaterConnection | undefined; private callbacks: ActorCallbacks | undefined; constructor( public readonly id: TheaterId, private readonly client: TheaterClient, eventConnection?: TheaterConnection, callbacks?: ActorCallbacks ) { this.eventConnection = eventConnection || undefined; this.callbacks = callbacks || undefined; if (this.eventConnection && this.callbacks) { this.setupEventHandling(); } } // ===== ACTOR MANAGEMENT ===== /** * Get the current status of this actor */ async getStatus(): Promise<ActorStatus> { return this.client.getActorStatus(this.id); } /** * Restart this actor */ async restart(): Promise<void> { return this.client.restartActor(this.id); } /** * Stop this actor */ async stop(): Promise<void> { // Stop the actor via hygienic connection await this.client.stopActor(this.id); // Clean up event connection this.eventConnection?.close(); } /** * Get the manifest configuration of this actor */ async getManifest(): Promise<ManifestConfig> { return this.client.getActorManifest(this.id); } /** * Get the current state of this actor */ async getState(): Promise<Uint8Array | null> { return this.client.getActorState(this.id); } /** * Get the event history for this actor */ async getEvents(): Promise<ChainEvent[]> { return this.client.getActorEvents(this.id); } /** * Get metrics for this actor */ async getMetrics(): Promise<any> { return this.client.getActorMetrics(this.id); } // ===== MESSAGING ===== /** * Send raw bytes to this actor (fire-and-forget) */ async sendBytes(data: Uint8Array): Promise<void> { return this.client.sendActorMessage(this.id, data); } /** * Send raw bytes to this actor and wait for response */ async requestBytes(data: Uint8Array): Promise<Uint8Array> { return this.client.requestActorMessage(this.id, data); } /** * Send a JSON object to this actor (fire-and-forget) */ async sendJson(obj: any): Promise<void> { return this.sendBytes(encodeJson(obj)); } /** * Send a JSON object to this actor and wait for JSON response */ async requestJson<T = any>(obj: any): Promise<T> { const response = await this.requestBytes(encodeJson(obj)); return decodeJson<T>(response); } /** * Send a string to this actor (fire-and-forget) */ async sendString(text: string): Promise<void> { return this.sendBytes(new TextEncoder().encode(text)); } /** * Send a string to this actor and wait for string response */ async requestString(text: string): Promise<string> { const response = await this.requestBytes(new TextEncoder().encode(text)); return new TextDecoder().decode(response); } // ===== REAL-TIME COMMUNICATION ===== /** * Open a communication channel with this actor */ async openChannel(initialMessage?: Uint8Array): Promise<ChannelStream> { return this.client.openChannel({ Actor: this.id }, initialMessage); } /** * Subscribe to events from this actor */ async subscribe(): Promise<ActorEventStream> { return this.client.subscribeToActor(this.id); } // ===== SUPERVISION ===== /** * Check if this actor has supervision (event stream connection) */ get isSupervised(): boolean { return !!this.eventConnection; } /** * Set up event handling for supervised actors */ private setupEventHandling(): void { if (!this.eventConnection || !this.callbacks) return; log.info(`Setting up event handling for supervised actor: ${this.id}`); // Listen for events on the dedicated connection this.eventConnection.onMessage((response: ManagementResponse) => { if ('ActorEvent' in response && response.ActorEvent) { this.callbacks?.onEvent?.(response.ActorEvent.event); } else if ('ActorError' in response && response.ActorError) { this.callbacks?.onError?.(response.ActorError.error); } else if ('ActorResult' in response && response.ActorResult) { this.callbacks?.onActorResult?.(response.ActorResult); } }); this.eventConnection.onError((error: Error) => { // Fail fast - let the application deal with it const connectionError = new TheaterConnectionError(`Event stream connection failed for actor ${this.id}: ${error.message}`, error); log.error(`Event stream connection failed for actor ${this.id}`, error); throw connectionError; }); } // ===== UTILITY METHODS ===== /** * String representation of this actor (returns the ID) */ toString(): string { return this.id; } /** * Check if this actor has the same ID as another actor */ equals(other: Actor): boolean { return this.id === other.id; } /** * Get a JSON representation of this actor */ toJSON(): { id: TheaterId } { return { id: this.id }; } /** * Check if this actor represents the same ID as a string */ hasId(id: TheaterId): boolean { return this.id === id; } }