theater-client
Version:
TypeScript client library for Theater actor system TCP protocol
370 lines (267 loc) โข 9.49 kB
Markdown
# Theater Client
A TypeScript client library for the Theater actor system with hygienic connection management.
## Features
- **๐งน Hygienic Connections** - Each operation gets its own TCP connection to avoid response multiplexing
- **๐ก Protocol Fidelity** - Types exactly match the Rust Theater server implementation
- **โก Real-time Communication** - Channel streams and actor event subscriptions
- **๐ก๏ธ Type Safety** - Full TypeScript support with comprehensive error handling
- **๐ฏ Single Responsibility** - Pure Theater protocol client with no domain logic
## Installation
```bash
npm install theater-client
# or
bun add theater-client
```
## Quick Start
### Using the Actor Wrapper (Recommended)
```typescript
import { TheaterClient } from 'theater-client';
const client = new TheaterClient('127.0.0.1', 9000);
// Start an actor - returns Actor wrapper
const actor = await client.startActor({
manifest: '/path/to/manifest.toml',
initialState: new TextEncoder().encode(JSON.stringify({ config: 'value' }))
});
// Much cleaner API - no more passing actor ID everywhere!
await actor.sendJson({ type: 'command', data: 'value' });
const response = await actor.requestJson({ type: 'query' });
await actor.stop();
```
### Using Raw Client (Also Available)
```typescript
// Traditional approach - still supported
const actorId = await client.startActorRaw({ manifest: '/path/to/manifest.toml' });
await client.sendActorMessage(actorId, data);
const response = await client.requestActorMessage(actorId, request);
await client.stopActor(actorId);
```
## API Reference
### Actor Wrapper (Recommended)
The `Actor` class provides a clean, object-oriented interface around an actor ID.
```typescript
export class Actor {
readonly id: TheaterId;
// Actor management
async getStatus(): Promise<ActorStatus>;
async restart(): Promise<void>;
async stop(): Promise<void>;
async getManifest(): Promise<ManifestConfig>;
async getState(): Promise<Uint8Array | null>;
async getEvents(): Promise<ChainEvent[]>;
async getMetrics(): Promise<any>;
// Raw messaging (core methods)
async sendBytes(data: Uint8Array): Promise<void>;
async requestBytes(data: Uint8Array): Promise<Uint8Array>;
// Convenience messaging
async sendJson(obj: any): Promise<void>;
async requestJson<T>(obj: any): Promise<T>;
async sendString(text: string): Promise<void>;
async requestString(text: string): Promise<string>;
// Real-time communication
async openChannel(initialMessage?: Uint8Array): Promise<ChannelStream>;
async subscribe(): Promise<ActorEventStream>;
// Utility methods
toString(): string;
equals(other: Actor): boolean;
hasId(id: TheaterId): boolean;
}
```
#### Actor Examples
```typescript
const client = new TheaterClient();
// Start actor - returns Actor wrapper
const actor = await client.startActor({ manifest: '/path/to/manifest.toml' });
// Clean operations - no more actor ID repetition!
const status = await actor.getStatus();
await actor.sendJson({ type: 'hello', message: 'world' });
const response = await actor.requestJson({ type: 'ping' });
// Raw bytes when needed
await actor.sendBytes(new Uint8Array([1, 2, 3, 4]));
const rawResponse = await actor.requestBytes(someData);
// Real-time communication
const channel = await actor.openChannel();
const events = await actor.subscribe();
// Stop when done
await actor.stop();
```
#### Working with Existing Actors
```typescript
// Get Actor wrapper for existing ID
const existingActor = client.actor('actor-id-123');
await existingActor.sendJson({ type: 'command' });
// List all actors as Actor wrappers
const actors = await client.listActors();
for (const actor of actors) {
const status = await actor.getStatus();
console.log(`Actor ${actor.id}: ${status}`);
}
```
### TheaterClient
The main client class that provides all Theater operations with automatic connection management.
#### Constructor
```typescript
new TheaterClient(host?: string, port?: number, config?: Partial<TheaterClientConfig>)
```
#### Actor Management
```typescript
// Start an actor and return Actor wrapper
async startActor(params: StartActorParams): Promise<Actor>
// Start an actor and return raw ID
async startActorRaw(params: StartActorParams): Promise<TheaterId>
// Get Actor wrapper for existing ID
actor(id: TheaterId): Actor
// List all actors as Actor wrappers
async listActors(): Promise<Actor[]>
// List all actors as raw info
async listActorsRaw(): Promise<ActorInfo[]>
// Stop an actor
async stopActor(id: TheaterId): Promise<void>
// List all actors
async listActors(): Promise<ActorInfo[]>
// Get actor status
async getActorStatus(id: TheaterId): Promise<ActorStatus>
// Restart an actor
async restartActor(id: TheaterId): Promise<void>
// Get actor manifest
async getActorManifest(id: TheaterId): Promise<ManifestConfig>
// Get actor state
async getActorState(id: TheaterId): Promise<Uint8Array | null>
// Get actor events
async getActorEvents(id: TheaterId): Promise<ChainEvent[]>
// Get actor metrics
async getActorMetrics(id: TheaterId): Promise<any>
```
#### Messaging
```typescript
// Send fire-and-forget message
async sendActorMessage(id: TheaterId, data: Uint8Array): Promise<void>
// Send request and wait for response
async requestActorMessage(id: TheaterId, data: Uint8Array): Promise<Uint8Array>
```
#### Real-time Communication
```typescript
// Open a channel for bidirectional communication
async openChannel(participant: ChannelParticipant, initialMessage?: Uint8Array): Promise<ChannelStream>
// Subscribe to actor events
async subscribeToActor(id: TheaterId): Promise<ActorEventStream>
```
### ChannelStream
Real-time bidirectional communication with actors.
```typescript
interface ChannelStream {
readonly channelId: string;
readonly isOpen: boolean;
// Event handling
onMessage(handler: (message: ChannelMessage) => void): () => void;
onClose(handler: () => void): () => void;
onError(handler: (error: Error) => void): () => void;
// Operations
sendMessage(data: Uint8Array): Promise<void>;
close(): void;
}
```
#### Example
```typescript
const channel = await client.openChannel({ Actor: actorId });
// Listen for messages
const unsubscribe = channel.onMessage((message) => {
const text = new TextDecoder().decode(message.data);
console.log(`Received: ${text}`);
});
// Send messages
await channel.sendMessage(new TextEncoder().encode('Hello!'));
// Clean up
unsubscribe();
channel.close();
```
### ActorEventStream
Subscribe to events from a specific actor.
```typescript
interface ActorEventStream {
readonly actorId: string;
readonly subscriptionId: string;
readonly isActive: boolean;
// Event handling
onEvent(handler: (event: ChainEvent) => void): () => void;
onError(handler: (error: Error) => void): () => void;
onClose(handler: () => void): () => void;
// Operations
close(): void;
}
```
#### Example
```typescript
const eventStream = await client.subscribeToActor(actorId);
eventStream.onEvent((event) => {
console.log('Actor event:', event);
});
// Clean up when done
eventStream.close();
```
## Error Handling
The library provides specific error types for different scenarios:
```typescript
import {
TheaterError,
TheaterConnectionError,
TheaterTimeoutError,
TheaterProtocolError
} from 'theater-client';
try {
await client.startActor({ manifest: '/invalid/path' });
} catch (error) {
if (error instanceof TheaterConnectionError) {
console.error('Connection failed:', error.message);
} else if (error instanceof TheaterTimeoutError) {
console.error('Operation timed out:', error.message);
} else if (error instanceof TheaterError) {
console.error('Theater error:', error.message, error.details);
}
}
```
## Configuration
```typescript
const client = new TheaterClient('127.0.0.1', 9000, {
timeout: 30000, // 30 second timeout
retryAttempts: 3, // Retry failed operations 3 times
retryDelay: 1000 // 1 second delay between retries
});
```
## Logging
Control logging output:
```typescript
import { setLogLevel } from 'theater-client';
// Set global log level
setLogLevel('debug'); // 'debug' | 'info' | 'warn' | 'error'
```
## Architecture
### Hygienic Connection Pattern
This library implements a "hygienic connection pattern" where each operation gets its own TCP connection. This provides several benefits:
- **No response multiplexing** - Each operation has a dedicated connection
- **Simplified error handling** - Errors are isolated to specific operations
- **Automatic cleanup** - Connections are automatically closed after operations
- **Concurrency safety** - Multiple operations can run safely in parallel
### Protocol Compatibility
Types are designed to exactly match the Rust Theater server implementation:
- `ManagementCommand` and `ManagementResponse` enums
- Binary data handling with `Uint8Array` โ `number[]` conversion
- FragmentingCodec support for large messages
- Complete error type mapping
## Examples
See the `/examples` directory for complete usage examples:
- `basic-operations.ts` - Actor management and messaging
- `channel-communication.ts` - Real-time bidirectional communication
- `event-subscription.ts` - Actor event monitoring
## Development
```bash
# Install dependencies
bun install
# Build the library
bun run build
# Run tests
bun test
# Watch mode during development
bun run build:watch
```
## License
MIT