Skip to content

v0.10.0

Choose a tag to compare

@pathosDev pathosDev released this 08 Jul 08:42
· 2099 commits to main since this release
bbd3ab7

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 a WebSocketServerActor<TOut, TIn>: codec-decoded messages (JSON default, rawCodec() for binary), this.reply(...) / this.broadcast(...), and onClientConnected / onClientDisconnected / onInvalidMessage hooks. 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 on BrokerActor: 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 in onMessage, publish with this.publish(...). Lifecycle events run on the actor thread; still externally controllable via ref.tell({ kind: 'publish' | 'subscribe' | 'unsubscribe', … }).
  • Typed MQTT payloads + codec (#345) — inbound MqttMessage<T> carries a lazily-decoding MqttPayload<T> (.bytes / .text() / .entity<U>()); pluggable MqttCodec<T> (default mqttJsonCodec()).

Persistence

  • PostgreSQL backend (#323) — PostgresJournal, PostgresSnapshotStore, PostgresDurableStateStore (first SQL durable-state store) on pg, via registerPostgresPlugins. Optimistic concurrency, indexed tags join, auto-created schema. pg is an optional peer dep; in-process fake-pool suite + live postgres:latest CI.
  • MariaDB backend (#324) — MariaDbJournal / MariaDbSnapshotStore / MariaDbDurableStateStore + registerMariaDbPlugins via the mariadb connector (MariaDB dialect). Optional peer dep; fake-pool + live mariadb:latest CI.
  • 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) + a Middleware type, composing outside-in.
  • BearerTokenAuth({ tokens }) (#312) — constant-time bearer-token gate with WWW-Authenticate.
  • IpAllowlist({ allow }) (#312) — CIDR (IPv4 + IPv6, incl. IPv4-mapped) network isolation, fail-secure.
  • HttpRequest.remoteAddress + backend wiring (Fastify/Express/Hono) so IpAllowlist works on real socket peers (#312).
  • managementRoutes auth (#312) — auth, ipAllowlist, authProtectHealth; privileged subtree gated, /health + /ready anonymous 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, …) with iptables + tc netem fault injection. bun run test:integration.

⚠️ Breaking changes (pre-1.0)

  • Options: a fluent builder OR a plain object, under one XOptions name (#346, #348) — every configurable type exposes three names from one XOptions.ts: XOptionsType (plain object), XOptionsBuilder (fluent builder), and XOptions (the union every consumer accepts, plus a value alias so XOptions.create() works). new MqttActor(MqttOptions.create().withClientId('x')) and new MqttActor({ clientId: 'x' }) are interchangeable. The old XSettings interface is renamed XOptionsType and the builder class XOptions is renamed XOptionsBuilder; everyday call sites (XOptions.create()…, plain objects) are unaffected — only code referencing the old XSettings type name or the builder class by name needs updating. HOCON precedence is unchanged.
  • Renamed settings fields + HOCON keys (#348) — MQTT defaultQosqos, keepAliveSeckeepAlive; JetStream ackTimeoutMsackTimeout; ClusterClient loglogger; DistributedData gossipIntervalMsgossipInterval; ProducerController resendTimeoutMsresendTimeout.
  • MqttActor is now abstract (#345) — subclass and override onMessage.
  • MqttMessage.payload is a MqttPayload<T> wrapper (#345), not a raw Uint8Array: msg.payload.bytes / .text() / .entity().
  • Removed MqttOptionsType.subscriptions + MqttSubscription (#345) — move them into the subclass constructor as this.subscribe(topic, { target }).
  • subscribe / unsubscribe target semantics (#345) — a no-target subscribe delivers to the actor's own onMessage; a no-target unsubscribe removes only foreign targets.
  • Bounded mailbox is the default (#310) — capacity 10 000, drop-head; drops counted via actor_mailbox_dropped_total. Opt back into unbounded per-actor with Props.withMailbox(() => new Mailbox()).
  • Removed the legacy frame-level WebSocket APIWebSocketActor, ServerWebSocketActor, and the serverWebSocketActorOf / bunWebSocketHandlers adapters are gone; use WebSocketClientActor and websocket(path, ref) + WebSocketServerActor. The client HOCON key actor-ts.io.broker.websocket is 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 past maxBufferedBytes.
  • DurableState revision tampering (#116) — opt-in HMAC-SHA256 over { revision, etag } for unencrypted object-storage bodies (encrypted bodies already bind revision as AES-GCM AAD); requireIntegrity refuses legacy un-tagged bodies.
  • ClusterClient ask-ID predictability (#120) — nextAskId() now crypto.randomUUID() (was Date.now() + counter).
  • Master-key rotation sweep race (#109) — durable resume tokens (progress) + pre-sweep verifyKeyringCompleteness.
  • 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 subscribe while 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 fzstd fallback; the misconfig now surfaces at plugin-init.
  • Object-storage compression docs corrected (EN + DE) to the real none / gzip / zstd set, ATS1-manifest decode, and level option.