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.
Connection machine
Section titled “Connection machine”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_REPLACEDgaveUp ◀──retry cap──── OPEN_TIMEOUT ──▶ backoff ◀┘ └──WAKE──▶ idle
any state ──CLOSE──▶ closed (final)LOGIN_OK/LOGIN_FAILdrive the nonce→cookie bootstrap.SOCKET_OPEN/SOCKET_CLOSEtrack socket state;OPEN_TIMEOUTdrops a socket stuck inopening(10 s).STABLE_TIMERresets 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_TIMERschedules 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 inbackoffinstead of the normal delay.WAKE(the page returned to the foreground) skips a backoff wait, and takesgaveUpback toidleso the shell can log in again.gaveUpis not final.CLOSE(transport.close()) moves every state toclosed, 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.
Why actors
Section titled “Why actors”- 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 })