Skip to content

v0.16.0

Choose a tag to compare

@pathosDev pathosDev released this 15 Aug 13:17
· 829 commits to main since this release
d0f15d0

The entry points, logging and old defects release, and the largest window the project has had: 181 commits since v0.15.0. Three threads run through it. Entry points — the root export stops being a barrel over the entire framework and becomes core-only, with eighteen subsystem subpaths beside it. Logging — the logger stops writing to exactly one place and grows a sink architecture, with rotating files and ten platform integrations, none of which pull in an SDK. Old defects — the 50 oldest open bug and security issues were worked as one unit, 42 of them resolved, including most of the 2026-08-01 security catalogue.

Twenty-six entries carry a BREAKING marker, each with a migration note. Two of them decide how you should deploy this, and they are first below.

⚠️ Breaking changes (pre-1.0)

  • A rolling upgrade across this release does not converge — upgrade the cluster in one step (#112). A gossip frame is a snapshot of the member map, and a member's version only moves when its status does, so a frame captured off the wire stayed valid indefinitely. Against a converged receiver that was harmless. Against an entry the receiver had deleted it was not: the failure detector's down path deletes outright so a healed partition can re-discover the peer, an expired tombstone is pruned for the same reason, and the branch that files a first sighting had no lower version bound at all. Replaying a downed member's own pre-down record therefore brought it back at its old version, up, carrying its roles — and roles are what shard placement, singleton hosting and downing quorums are computed from.

    Every frame now carries a sequence its author stamps, seeded from that node's wall clock at startup so a restart out-numbers its own previous incarnation, and a receiver drops any frame that does not out-number the highest it has accepted from that connection peer. There is no new knob: the comparison is between a peer and itself, so it needs no clock-skew budget.

    Migration. GossipMessage gains a required sequence, and a frame without it is refused at the decode boundary. Nothing outside the framework composes gossip frames, so application code is unaffected — but an upgraded peer refuses an old node's frames and an old node ignores the new one. Upgrade every node in one step, or accept that membership does not converge while both versions are running.

  • The root 'actor-ts' export is core-only; subsystems moved to subpath exports (#414). The root barrel re-exported every subsystem, which dragged the whole framework through one entry point — the testkit shipped in the production entry (#685), and import { ActorSystem } paid for whatever any subsystem pulled in eagerly (#1005). Core — actors, supervision, scheduler/dispatcher, EventStream, system messages, config, mailboxes, patterns/Router, typed behaviors, the util value types and the base loggers — stays at 'actor-ts'; everything else lives at its own entry.

    Migration. Import moved symbols from their subsystem entry:

    import { PersistentActor } from 'actor-ts/persistence';
    import { Cluster } from 'actor-ts/cluster';
    import { FileSink } from 'actor-ts/logging';

    Aliased root names keep working as spelled aliases: import { Subscribe as ReceptionistSubscribe } from 'actor-ts/discovery', import { Transition as FsmTransition } from 'actor-ts/fsm'.

  • The rest, in one line each — every one carries its own migration note in CHANGELOG.md: HttpRequest.path is the bare pathname on every backend (#605); every HTTP backend caps a request body at the same 1 MiB (#613); Lease.release() reports a failure instead of swallowing it (#598); the ClusterClient envelope no longer carries a sender field; serializeCookie is now safe by omission and validates Path and Domain (#626); the CSRF cookie defaults to the __Host- prefix (#626); CSRF origin checks compare whole origins (#604); DEFAULT_MIME_TYPES has a null prototype (#608); Express WebSocket handshakes now run the app's middleware (#623); an HMAC integrity tag can no longer be stripped to skip verification; a unary gRPC call is bounded by the configured deadline; every HttpClient call carries a deadline and a response-size cap (#602, #625); HttpClient has a redirect policy of its own, with the hop budget down from 20 to 5; getFromDirectory enforces symlinks: 'within-root'; DevTools.mount() demands an acknowledgement; @hono/node-ws must be 1.2.0 or newer for websocket() routes (#586); and MemcachedClientLike is typed in Uint8Array rather than Buffer (#1006).

🚀 New features

  • Logging grew a sink architecture (#1150). The logger wrote to exactly one place; it now fans one record out to as many destinations as you configure, each with its own minimum level, bounded delivery and a flush on shutdown.

    const consoleSink = new ConsoleSink({ minLevel: LogLevel.Info });
    const auditSink = new ConsoleSink({ minLevel: LogLevel.Error, format: 'json' });
    const systemOptions = ActorSystemOptions.create().withLogSinks([consoleSink, auditSink]);
    const system = ActorSystem.create('my-app', systemOptions);

    Nothing about the existing surface changed: Logger, ConsoleLogger, JsonLogger and NoopLogger are untouched, this.log behaves as before, and a system whose config nobody edited logs exactly what it logged yesterday. The pieces: MultiSinkLogger and the LogSink contract (#1151), BatchingSink with a bounded queue, batching, retry with jittered backoff and drop accounting (#1152), FileSink with rotation and retention (#1153), and ten platform sinks (#1154#1161) — starting with OtlpHttpSink, which reaches Grafana Loki 3+, Parseable, SigNoz, Datadog, Axiom, Honeycomb, New Relic and every OpenTelemetry Collector through one endpoint format. Every integration is dependency-free; the two that could have pulled an SDK, OpenTelemetry and Sentry, take the opposite route and say why on their own pages.

  • Per-subsystem subpath exports (#414, #1001). The exports map grew one entry per subsystem barrel, so the subpaths the documentation already used — actor-ts/http, /coordination, /serialization, /discovery — resolve now, and the smoke suite loads every declared entry on Bun, Node and Deno (#1003).

  • The default Fastify backend loads lazily (#1005). import { ActorSystem } from 'actor-ts' no longer parses Fastify and its ~20 transitive packages; the default backend resolves on the first bind, exactly like the express and hono arms always did.

  • HttpClient calls are bounded by time and by bytes (#602, #625), and carry a redirect policy of their own rather than the platform's.

  • The cluster warns at startup when remote.tls.enabled is not honoured (#591), rather than running plaintext in silence.

🔒 Security

Most of the 2026-08-01 security catalogue (#575#626) closed in this window. The larger ones: cluster frame decoding is now linear in the bytes received rather than quadratic in the chunk count (#588) — a 16 MiB frame delivered in ~1400-byte writes was roughly 100 GB of memory copying on a path that runs before the hello gate; an idle inbound cluster socket is bounded in both directions (#588); the pod's mounted ServiceAccount token is never paired with an untrusted API server; the Kubernetes API seed provider percent-encodes the path; and the Idempotency-Key header is validated before it reaches a cache key (#607 narrowed, the eviction policy in #1080 is the remainder).

📦 Packaging

The packaging surface was brought in line with what actually ships.

  • No more dangling source maps (#1007). declarationMap and sourceMap were on while files publishes only dist/, so all 1262 maps pointed at a ../src/*.ts the tarball never carried, and none held sourcesContent. A dangling map is worse than an absent one, since a missing map degrades cleanly to the .d.ts while a dangling one sends the editor after a file that never arrives. Published, the package goes from 8.07 MB over 2360 files at v0.15.0 to 6.07 MB over 1268 files — and that is with the whole logging-sink subsystem added in the same window.

  • NodeNext module resolution (#1008). The build said moduleResolution: "Bundler", but no bundler runs — the consumer is Node ≥ 24 or Deno on the real ESM resolver, and Bundler relaxes exactly the rules that resolver enforces. The mandatory .js suffix already satisfied NodeNext, so the switch changes no emitted byte; what it buys is that the next forgotten suffix is a compile error here instead of an ERR_MODULE_NOT_FOUND in your process, and that .d.ts resolution becomes exports-map-aware now that the package has eighteen subpaths.

  • @types/node is no longer a silent requirement (#1006). The declarations used Node-only types in public signatures while @types/node was a devDependency, so type-checking with skipLibCheck: false produced errors out of node_modules/actor-ts/ that were not yours to fix. NodeJS.Signals is replaced by the new ProcessSignal (a member-for-member mirror, so every call site is unchanged), MemcachedClientLike speaks Uint8Array, and @types/node is declared as an optional peer dependency for the one remaining surface — ExpressBackend, which constructs a ServerResponse at run time.

🐛 Fixed

42 of the 50 oldest open defects. A sample: inbound TCP bytes are buffered in a doubling slab rather than a fresh array per chunk (#610); an accepted socket is put on the handshake clock (#588); one rejected discovery rung no longer takes the whole ladder down; LeaseMajority tracks the acquire an abandoned reset leaves behind (#600); content-types resolve against own keys only (#608); Origin merges into a handler's Vary whatever its case (#603); and a rejected WebSocket upgrade reaches the client on Bun (#623).


Full detail, with the reasoning and every migration note, is in CHANGELOG.md.