Setup
ihsm is isomorphic: the same package runs on the server (Node) and in the browser. There is
one runtime import for application code and one for tests.
npm install ihsm
import * as ihsm from 'ihsm'; // application code
import * as ihsm from 'ihsm/testing'; // tests only (TestPort, makeTestActor, @mock)
ihsm has zero production dependencies and ships dual CommonJS + ESM builds with type
declarations. It requires a runtime with globalThis.crypto (Node 22+, and every modern browser).
Server (Node)
Create a state machine, instantiate it with makeActor, and drive it through its faceted handles.
import { InitialState, makeActor, Port, registerStateNames, TopState } from 'ihsm';
interface CounterCtx {
value: number;
}
interface CounterConfig {
context: CounterCtx;
notifications: { increment(): void; reset(): void };
services: { read(): Promise<number> };
}
class CounterTop extends TopState<CounterConfig> {
increment(): void {
this.ctx.value += 1;
}
reset(): void {
this.ctx.value = 0;
}
async read(): Promise<number> {
return this.ctx.value;
}
}
@InitialState
class Running extends CounterTop {}
// Register class names so traces and persistence survive minification.
registerStateNames({ CounterTop, Running });
async function main(): Promise<void> {
// The production Port supplies real timers and randomness from the host.
const counter = makeActor(CounterTop, { value: 0 });
await counter.hsm.sync(); // wait for initialization to settle
counter.notify.increment();
counter.notify.increment();
await counter.hsm.sync(); // wait for the mailbox to drain
console.log(await counter.call.read()); // 2
}
void main();
Run it with any TypeScript toolchain. Enable decorators in tsconfig.json so @InitialState
compiles:
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"strict": true
}
}
Browser
The browser build is intended for development, debugging, and demonstration — interactive playgrounds, visualizers, and in-page simulations. The API and behaviour are identical to the server; you simply import the same package and bundle it.
import { InitialState, makeActor, Port, registerStateNames, TopState } from 'ihsm';
interface ToggleCtx {
on: boolean;
}
interface ToggleConfig {
context: ToggleCtx;
notifications: { flip(): void };
}
class ToggleTop extends TopState<ToggleConfig> {
flip(): void {
this.ctx.on = !this.ctx.on;
document.body.dataset.state = this.ctx.on ? 'on' : 'off';
}
}
@InitialState
class Ready extends ToggleTop {}
registerStateNames({ ToggleTop, Ready });
const toggle = makeActor(ToggleTop, { on: false });
await toggle.hsm.sync();
document.querySelector('#flip')?.addEventListener('click', () => toggle.notify.flip());
Bundler note
ihsm uses Node's async_hooks for one non-production feature — self-call deadlock detection
under elevated trace levels. It is loaded through a guarded dynamic import, so in the browser the
feature is simply disabled and everything else runs unchanged. If your bundler tries to resolve
node:async_hooks, mark it empty:
// vite.config.ts
export default {
resolve: { alias: { 'node:async_hooks': new URL('./empty.js', import.meta.url).pathname } },
};
// webpack.config.js
module.exports = {
resolve: { fallback: { 'node:async_hooks': false } },
};
Run actors in the browser for development and debugging. For production telemetry and workloads, the server build is the supported deployment target.