theater-client
Version:
TypeScript client library for Theater actor system TCP protocol
231 lines (195 loc) • 6.02 kB
text/typescript
/**
* 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;
}
}