Skip to content

State Machines

Complex lifecycle state (WebSocket connection, RPC request tracking) is modeled with mantaq Actors — not ad hoc boolean flags. The framework keeps these machines internal-only: they never leak into the public @ekrooh/bare surface.

web/connection-machine.ts owns the WebSocket lifecycle:

idle ──LOGIN_OK──▶ opening ──SOCKET_OPEN──▶ connected
│ │ ▲ │
LOGIN_FAIL │ └──RETRY_TIMER/WAKE──┐ │ SOCKET_CLOSE /
▼ └──SOCKET_CLOSE / │ │ SOCKET_REPLACED
gaveUp ◀──retry cap──── OPEN_TIMEOUT ──▶ backoff ◀┘
└──WAKE──▶ idle
any state ──CLOSE──▶ closed (final)
  • LOGIN_OK / LOGIN_FAIL drive the nonce→cookie bootstrap.
  • SOCKET_OPEN / SOCKET_CLOSE track socket state; OPEN_TIMEOUT drops a socket stuck in opening (10 s).
  • STABLE_TIMER resets the retry count once a socket has stayed open for 5 s, so a socket that opens and drops at once keeps backing off.
  • RETRY_TIMER schedules the jittered exponential backoff. The retry cap (maxRetries) applies only to a client that has never connected, and only after the 10 s boot window; a client that has connected retries forever.
  • SOCKET_REPLACED (the server closed the socket with code 4001 because another document took the session) waits 30 s in backoff instead of the normal delay.
  • WAKE (the page returned to the foreground) skips a backoff wait, and takes gaveUp back to idle so the shell can log in again. gaveUp is not final.
  • CLOSE (transport.close()) moves every state to closed, the only final state.

The transport shell drives socket I/O against the machine and reacts to state changes; gaveUp and closed fail the queued messages with a deterministic transport error. The machine uses an injected Clock, so tests run on a VirtualClock without real timers.

  • Every state’s effects run on ENTRY, auto-cancelled by the abort signal on transition — timeouts and timers can’t leak.
  • Terminal states emit outputs (RESOLVED/REJECTED) that a parent routes — no dangling promises.
  • Deterministic, testable, and readable in one glance.
import { Actor, RealClock, state, event } from '@mantaq/core';
const idle = state('idle')();
const open = state('open')();
const openEvent = event('OPEN')();
// ...wire transitions with .on(openEvent, { target: open })