Skip to main content

TypeScript/Node.js Client

The official TypeScript/Node.js client library for rstmdb.

Repository: github.com/rstmdb/rstmdb-js

Installation

npm install @rstmdb/client

Or with other package managers:

pnpm add @rstmdb/client
yarn add @rstmdb/client

Requirements: Node.js 18.0.0+

Features

  • Full TypeScript support with complete type definitions
  • Promise-based async API
  • Streaming support via AsyncIterator and EventEmitter
  • Automatic reconnection
  • TLS/mTLS support
  • Connection pooling

Quick Start

import { Client } from '@rstmdb/client';

async function main() {
// Connect to server
const client = await Client.connect('localhost', 7401, {
auth: 'my-secret-token'
});

// Define a state machine
await client.putMachine('order', 1, {
states: ['pending', 'paid', 'shipped', 'delivered'],
initial: 'pending',
transitions: [
{ from: 'pending', event: 'PAY', to: 'paid' },
{ from: 'paid', event: 'SHIP', to: 'shipped' },
{ from: 'shipped', event: 'DELIVER', to: 'delivered' }
]
});

// Create an instance
const instance = await client.createInstance({
machine: 'order',
version: 1,
id: 'order-001',
context: { customer: 'alice', total: 99.99 }
});
console.log(`Created: ${instance.id} in state ${instance.state}`);

// Apply events
const result = await client.applyEvent({
instanceId: 'order-001',
event: 'PAY',
payload: { paymentId: 'pay-123' }
});
console.log(`Transitioned: ${result.previousState} -> ${result.currentState}`);

await client.close();
}

main();

Connection

import { Client } from '@rstmdb/client';

const client = await Client.connect('localhost', 7401, {
auth: 'my-secret-token'
});

Builder Pattern

import { Client, ClientOptions } from '@rstmdb/client';

const config = ClientOptions
.create('localhost')
.port(7401)
.auth('my-secret-token')
.connectionTimeout(10000)
.requestTimeout(30000)
.build();

const client = new Client(config);
await client.connect();

TLS Connection

const client = await Client.connect('secure.example.com', 7401, {
auth: 'my-secret-token',
tls: {
ca: fs.readFileSync('/path/to/ca.pem')
}
});

Mutual TLS (mTLS)

const client = await Client.connect('secure.example.com', 7401, {
auth: 'my-secret-token',
tls: {
ca: fs.readFileSync('/path/to/ca.pem'),
cert: fs.readFileSync('/path/to/client.pem'),
key: fs.readFileSync('/path/to/client-key.pem')
}
});

Auto-Reconnection

const client = await Client.connect('localhost', 7401, {
reconnect: {
enabled: true,
interval: 1000, // 1 second between attempts
maxAttempts: 10 // Give up after 10 attempts
}
});

Configuration Options

OptionTypeDefaultDescription
authstring-Authentication token
connectionTimeoutnumber10000Connection timeout (ms)
requestTimeoutnumber30000Request timeout (ms)
tlsTlsOptions-TLS configuration
reconnect.enabledbooleantrueEnable auto-reconnect
reconnect.intervalnumber1000Reconnect interval (ms)
reconnect.maxAttemptsnumber10Max reconnect attempts
clientNamestring-Client identifier

API Reference

Machine Operations

putMachine

Register a state machine definition.

await client.putMachine('order', 1, {
states: ['pending', 'paid', 'shipped'],
initial: 'pending',
transitions: [
{ from: 'pending', event: 'PAY', to: 'paid' },
{ from: 'paid', event: 'SHIP', to: 'shipped' }
]
});

getMachine

Retrieve a machine definition.

const machine = await client.getMachine('order', 1);
console.log(machine.definition.states);
console.log(machine.definition.initial);

listMachines

List all machines.

const machines = await client.listMachines();
for (const m of machines) {
console.log(`${m.name}: ${m.versions.join(', ')}`);
}

Instance Operations

createInstance

Create a new instance.

const instance = await client.createInstance({
machine: 'order',
version: 1,
id: 'order-001',
context: { customer: 'alice' }
});

getInstance

Get instance state and context.

const instance = await client.getInstance('order-001');
console.log(`State: ${instance.state}`);
console.log(`Context:`, instance.context);

deleteInstance

Delete an instance.

await client.deleteInstance('order-001');

Event Operations

applyEvent

Apply an event to trigger a state transition.

const result = await client.applyEvent({
instanceId: 'order-001',
event: 'PAY',
payload: { amount: 99.99 }
});

console.log(`Previous: ${result.previousState}`);
console.log(`Current: ${result.currentState}`);

applyEvent with expectedState

Optimistic concurrency control.

const result = await client.applyEvent({
instanceId: 'order-001',
event: 'SHIP',
expectedState: 'paid' // Fails if not in 'paid' state
});

batch

Execute multiple operations atomically.

const results = await client.batch({
mode: 'atomic',
operations: [
{ op: 'applyEvent', params: { instanceId: 'order-001', event: 'PAY' } },
{ op: 'applyEvent', params: { instanceId: 'order-002', event: 'PAY' } }
]
});

Streaming

watchInstance (AsyncIterator)

const stream = await client.watchInstance('order-001');

for await (const event of stream) {
console.log(`Event: ${event.event}, New state: ${event.toState}`);
}

watchInstance (EventEmitter)

const stream = await client.watchInstance('order-001');

stream.on('event', (event) => {
console.log(`Event: ${event.event}, New state: ${event.toState}`);
});

stream.on('error', (error) => {
console.error('Stream error:', error);
});

// Later: stop watching
await stream.close();

watchAll

Subscribe to events with filtering.

const stream = await client.watchAll({
machines: ['order'],
toStates: ['shipped', 'delivered']
});

for await (const event of stream) {
console.log(`${event.instanceId}: ${event.event} -> ${event.toState}`);
}

System Operations

ping

Health check.

await client.ping();

info

Get server information.

const info = await client.info();
console.log(`Version: ${info.version}`);
console.log(`Instances: ${info.stats.instances}`);

Error Handling

import {
Client,
NotFoundError,
ConflictError,
InvalidTransitionError,
AuthenticationError,
ConnectionError,
TimeoutError
} from '@rstmdb/client';

try {
await client.applyEvent({ instanceId: 'order-001', event: 'PAY' });
} catch (error) {
if (error instanceof NotFoundError) {
console.log('Instance not found');
} else if (error instanceof InvalidTransitionError) {
console.log('Cannot apply event from current state');
} else if (error instanceof ConflictError) {
console.log('Concurrent modification - retry');
} else if (error instanceof AuthenticationError) {
console.log('Authentication failed');
} else if (error instanceof ConnectionError) {
console.log('Connection lost');
} else if (error instanceof TimeoutError) {
console.log('Request timed out');
}

// Check if error is retryable
if (error.retryable) {
// Safe to retry
}
}

TypeScript Types

import type {
Client,
Machine,
MachineDefinition,
Instance,
ApplyEventResult,
WatchStream,
StreamEvent,
ServerInfo
} from '@rstmdb/client';

Examples

Order Processing

import { Client } from '@rstmdb/client';

async function processOrder(client: Client, orderId: string) {
// Create order
await client.createInstance({
machine: 'order',
version: 1,
id: orderId,
context: { items: ['item-1', 'item-2'], total: 149.99 }
});

// Process payment
await client.applyEvent({
instanceId: orderId,
event: 'PAY',
payload: { paymentId: 'pay-123' }
});

// Ship order
await client.applyEvent({
instanceId: orderId,
event: 'SHIP',
payload: { tracking: '1Z999' }
});

// Get final state
const order = await client.getInstance(orderId);
console.log(`Order ${orderId} is now: ${order.state}`);
}

async function main() {
const client = await Client.connect('localhost', 7401);
await processOrder(client, 'order-001');
await client.close();
}

main();

Event Consumer

import { Client } from '@rstmdb/client';

async function consumeEvents() {
const client = await Client.connect('localhost', 7401);

console.log('Listening for shipped orders...');

const stream = await client.watchAll({
machines: ['order'],
toStates: ['shipped']
});

for await (const event of stream) {
console.log(`Order ${event.instanceId} shipped!`);
// Send notification, update external system, etc.
}
}

consumeEvents();

Retry with Backoff

import { Client, TimeoutError, ConnectionError } from '@rstmdb/client';

async function applyWithRetry(
client: Client,
instanceId: string,
event: string,
maxRetries = 3
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await client.applyEvent({ instanceId, event });
} catch (error) {
if (!error.retryable || attempt === maxRetries - 1) {
throw error;
}
const delay = 100 * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

Resources