Messaging
Clients reach an actor through three faceted handles:
| Facet | Shape | Delivery |
|---|---|---|
actor.notify.x() | notifications (void) | default queue (FIFO) |
actor.notifyNow.x() | notifications (void) | priority queue (drains first) |
await actor.call.x() | services (Promise<T>) | awaited service |
Inside a handler, the same facets are available as this.notify, this.notifyNow, and the result
of a call is awaited directly. Each event runs to completion before the next one starts.
Post and sync (run-to-completion)
Events posted from a handler are queued, not run inline. They execute after the current handler
returns, in order. One sync() drains the whole cascade.
import { InitialState, makeActor, Port, registerStateNames, TopState } from 'ihsm';
interface QueueCtx {
events: string[];
}
interface QueueConfig {
context: QueueCtx;
notifications: { start(): void; tick(): void; done(): void };
}
class QueueTop extends TopState<QueueConfig> {
start(): void {
this.ctx.events.push('start');
this.notify.tick(); // queued — runs after start() returns
this.notify.tick();
this.notify.done();
}
tick(): void {
this.ctx.events.push('tick');
}
done(): void {
this.ctx.events.push('done');
}
}
@InitialState
class Idle extends QueueTop {}
registerStateNames({ QueueTop, Idle });
const m = makeActor(QueueTop, { events: [] });
await m.hsm.sync();
m.notify.start();
await m.hsm.sync();
console.log(m.ctx.events); // ['start', 'tick', 'tick', 'done']
Priority events with notifyNow
notifyNow posts to a priority queue that drains before the default queue — even when a normal
event was posted first in the same handler.
import { InitialState, makeActor, Port, registerStateNames, TopState } from 'ihsm';
interface CheckoutCtx {
steps: string[];
}
interface CheckoutConfig {
context: CheckoutCtx;
notifications: { confirm(): void; cancel(): void; lock(): void; capture(): void };
}
class CheckoutTop extends TopState<CheckoutConfig> {
confirm(): void {
this.notify.cancel(); // default queue
this.notifyNow.lock(); // priority — runs before cancel
this.notifyNow.capture();
}
lock(): void {
this.ctx.steps.push('lock');
}
capture(): void {
this.ctx.steps.push('capture');
}
cancel(): void {
this.ctx.steps.push('cancel');
}
}
@InitialState
class Draft extends CheckoutTop {}
registerStateNames({ CheckoutTop, Draft });
const c = makeActor(CheckoutTop, { steps: [] });
await c.hsm.sync();
c.notify.confirm();
await c.hsm.sync();
console.log(c.ctx.steps); // ['lock', 'capture', 'cancel']
Services: call for request/response
A services handler returns a value or a Promise; clients await actor.call.x(). Sync and async
handlers share one signature — no resolve/reject plumbing.
import { InitialState, makeActor, Port, registerStateNames, TopState } from 'ihsm';
interface WalletCtx {
balance: number;
}
interface WalletConfig {
context: WalletCtx;
notifications: { deposit(amount: number): void };
services: {
getBalance(): Promise<number>;
withdraw(amount: number): Promise<number>;
};
}
class WalletTop extends TopState<WalletConfig> {
deposit(amount: number): void {
this.ctx.balance += amount;
}
getBalance(): number {
return this.ctx.balance; // sync return is fine
}
withdraw(amount: number): number {
if (amount > this.ctx.balance) {
throw new Error('insufficient funds'); // surfaces as a rejected promise
}
this.ctx.balance -= amount;
return this.ctx.balance;
}
}
@InitialState
class OpenAccount extends WalletTop {}
registerStateNames({ WalletTop, OpenAccount });
const wallet = makeActor(WalletTop, { balance: 100 });
await wallet.hsm.sync();
wallet.notify.deposit(50);
console.log(await wallet.call.getBalance()); // 150
console.log(await wallet.call.withdraw(30)); // 120
await wallet.call.withdraw(1000).catch((e) => console.log(e.message)); // 'insufficient funds'
Call timeouts
Pass a { timeoutMs } option as the final call argument. If the service does not settle in time,
the promise rejects with CallTimeoutError. The deadline is armed on the actor's port timer, so
under a TestPort it is driven by the virtual clock and stays fully deterministic.
import { CallTimeoutError, InitialState, makeActor, Port, registerStateNames, TopState } from 'ihsm';
interface RpcConfig {
context: Record<string, never>;
services: { slow(): Promise<string> };
}
class RpcTop extends TopState<RpcConfig> {
async slow(): Promise<string> {
await new Promise((r) => this.hsm.port.setTimeout(r, 10_000)); // long-running
return 'done';
}
}
@InitialState
class Ready extends RpcTop {}
registerStateNames({ RpcTop, Ready });
const rpc = makeActor(RpcTop, {});
await rpc.hsm.sync();
try {
await rpc.call.slow({ timeoutMs: 50 });
} catch (err) {
if (err instanceof CallTimeoutError) {
console.log(`timed out: ${err.method}`); // 'timed out: slow'
}
}
A timeoutMs of 0 rejects immediately.