v0.10.0
The typed real-time + SQL-persistence release. Two new first-class actor families — a typed WebSocket stack (server routing + client) and a subclass-first MqttActor — land alongside PostgreSQL and MariaDB persistence backends. Configuration gets a framework-wide overhaul: every configurable thing now takes a fluent builder or a plain object under one XOptions name, with no more "Settings vs Options" split. Plus HTTP middleware + auth, structured logging, a real-network multi-node integration harness, and five security hardening fixes. Pre-1.0 — this minor carries breaking changes; see below.
🚀 New features
Messaging & IO
- Typed WebSocket routing (#1) —
websocket(path, actorRef)binds aWebSocketServerActor<TOut, TIn>: codec-decoded messages (JSON default,rawCodec()for binary),this.reply(...)/this.broadcast(...), andonClientConnected/onClientDisconnected/onInvalidMessagehooks. A session actor per connection solves the first-frame race by construction. Works on Fastify, Express, and Hono (Bun/Node/Deno);withMiddleware(...)gates the handshake. WebSocketClientActor<TOut, TIn>(#1) — typed client onBrokerActor: reconnect-with-backoff, outbound buffering across reconnects, circuit breaker, HOCON settings.- Subclass-first typed
MqttActor<T, TSelf>(#345) — extend it, declare subscriptions in the constructor (this.subscribe(topic, { qos })), handle inbound inonMessage, publish withthis.publish(...). Lifecycle events run on the actor thread; still externally controllable viaref.tell({ kind: 'publish' | 'subscribe' | 'unsubscribe', … }). - Typed MQTT payloads + codec (#345) — inbound
MqttMessage<T>carries a lazily-decodingMqttPayload<T>(.bytes/.text()/.entity<U>()); pluggableMqttCodec<T>(defaultmqttJsonCodec()).
Persistence
- PostgreSQL backend (#323) —
PostgresJournal,PostgresSnapshotStore,PostgresDurableStateStore(first SQL durable-state store) onpg, viaregisterPostgresPlugins. Optimistic concurrency, indexed tags join, auto-created schema.pgis an optional peer dep; in-process fake-pool suite + livepostgres:latestCI. - MariaDB backend (#324) —
MariaDbJournal/MariaDbSnapshotStore/MariaDbDurableStateStore+registerMariaDbPluginsvia themariadbconnector (MariaDB dialect). Optional peer dep; fake-pool + livemariadb:latestCI. - Configurable compression level (#322) —
CompressionConfig.level(gzip 0–9, zstd 1–22), encoder-only, no migration (old + new bodies mix freely).
HTTP
- Route middleware framework (#312) —
withMiddleware(mw, route)+ aMiddlewaretype, composing outside-in. BearerTokenAuth({ tokens })(#312) — constant-time bearer-token gate withWWW-Authenticate.IpAllowlist({ allow })(#312) — CIDR (IPv4 + IPv6, incl. IPv4-mapped) network isolation, fail-secure.HttpRequest.remoteAddress+ backend wiring (Fastify/Express/Hono) soIpAllowlistworks on real socket peers (#312).managementRoutesauth (#312) —auth,ipAllowlist,authProtectHealth; privileged subtree gated,/health+/readyanonymous by default.
Observability
JsonLogger(#311) — one JSON object per record to stdout, MDC-aware, never-throws sanitisation.otelLogger({ api })(#311) — OTLP-Logs bridge via@opentelemetry/api-logs, auto-links the active span.
Testing
- Real-network multi-node integration tests (#313) — a Docker-compose harness (5 node containers + controller) running 15 scenarios (partition/heal, sharding rebalance, singleton failover, CRDT convergence, external
ClusterClient, management auth,CoordinatedShutdown, bounded-mailbox drops, DNS discovery, …) withiptables+tc netemfault injection.bun run test:integration.
⚠️ Breaking changes (pre-1.0)
- Options: a fluent builder OR a plain object, under one
XOptionsname (#346, #348) — every configurable type exposes three names from oneXOptions.ts:XOptionsType(plain object),XOptionsBuilder(fluent builder), andXOptions(the union every consumer accepts, plus a value alias soXOptions.create()works).new MqttActor(MqttOptions.create().withClientId('x'))andnew MqttActor({ clientId: 'x' })are interchangeable. The oldXSettingsinterface is renamedXOptionsTypeand the builder classXOptionsis renamedXOptionsBuilder; everyday call sites (XOptions.create()…, plain objects) are unaffected — only code referencing the oldXSettingstype name or the builder class by name needs updating. HOCON precedence is unchanged. - Renamed settings fields + HOCON keys (#348) — MQTT
defaultQos→qos,keepAliveSec→keepAlive; JetStreamackTimeoutMs→ackTimeout; ClusterClientlog→logger; DistributedDatagossipIntervalMs→gossipInterval; ProducerControllerresendTimeoutMs→resendTimeout. MqttActoris now abstract (#345) — subclass and overrideonMessage.MqttMessage.payloadis aMqttPayload<T>wrapper (#345), not a rawUint8Array:msg.payload.bytes/.text()/.entity().- Removed
MqttOptionsType.subscriptions+MqttSubscription(#345) — move them into the subclass constructor asthis.subscribe(topic, { target }). subscribe/unsubscribetargetsemantics (#345) — a no-targetsubscribe delivers to the actor's ownonMessage; a no-targetunsubscribe removes only foreign targets.- Bounded mailbox is the default (#310) — capacity 10 000,
drop-head; drops counted viaactor_mailbox_dropped_total. Opt back into unbounded per-actor withProps.withMailbox(() => new Mailbox()). - Removed the legacy frame-level WebSocket API —
WebSocketActor,ServerWebSocketActor, and theserverWebSocketActorOf/bunWebSocketHandlersadapters are gone; useWebSocketClientActorandwebsocket(path, ref)+WebSocketServerActor. The client HOCON keyactor-ts.io.broker.websocketis unchanged.
🔒 Security
- WebSocket DoS hardening (#1) — inbound frames size-capped (
maxFrameBytes, 1 MiB default) before decode; oversize → close 1009, undecodable → close 1003; slow-consumer backpressure pastmaxBufferedBytes. - DurableState revision tampering (#116) — opt-in HMAC-SHA256 over
{ revision, etag }for unencrypted object-storage bodies (encrypted bodies already bindrevisionas AES-GCM AAD);requireIntegrityrefuses legacy un-tagged bodies. - ClusterClient ask-ID predictability (#120) —
nextAskId()nowcrypto.randomUUID()(wasDate.now() + counter). - Master-key rotation sweep race (#109) — durable resume tokens (
progress) + pre-sweepverifyKeyringCompleteness. - LeaseMajority split-brain (#142) — monotonic
acquireEpoch, release-on-abandon with fail-safe, and optional fencing tokens (Lease.acquireWithToken?()).
🐛 Fixed
- MQTT subscriptions re-applied after reconnect (#345) — runtime subscriptions no longer silently stop receiving after a drop.
- MQTT
subscribewhile disconnected reaches the broker on connect (#345). - MQTT terminated fan-out targets cleaned up (#345) — deathwatched refs pruned; UNSUBSCRIBE fires once a pattern has no consumers.
- zstd compress on runtimes without native zstd (#321) — compress is native-only with a clear "needs Bun / Node ≥ 22.15" error; decompress keeps the
fzstdfallback; the misconfig now surfaces at plugin-init. - Object-storage compression docs corrected (EN + DE) to the real
none/gzip/zstdset, ATS1-manifest decode, andleveloption.