Design decisions
Este conteúdo não está disponível em sua língua ainda.
A handful of choices in the framework’s design that aren’t obvious. Reading this is optional — useful when you wonder “why isn’t it like X?”
Why Bun first
Section titled “Why Bun first”Three reasons:
- Fast startup — sub-100ms cold start vs Node’s 300-500ms. Matters for test loops + serverless cold starts (when those apply).
- Built-in SQLite, test runner, HTTP — fewer peer deps.
- Modern runtime ergonomics — top-level await, built-in bundler, simpler stdlib surface.
Node is also fully supported. Deno is best-effort. The framework is runtime-agnostic by design; Bun is just the primary target for testing + benchmarks.
Why ts-pattern (not switch / if-else)
Section titled “Why ts-pattern (not switch / if-else)”match(msg) .with({ kind: 'inc' }, () => state.count++) .with({ kind: 'dec' }, () => state.count--) .exhaustive();- Compile-time exhaustiveness. Adding a new variant to the
union without an
with(...)arm fails to compile. No silent fallthrough. - Type narrowing inside arms. No casts, no manual guards.
- Readable at scale. Switch + assertNever works at 2-3 variants; loses out at 5+.
It’s an opt-in convention — you can write actors with plain
switch. The docs use match because it’s the safer pattern.
Why a single-threaded model
Section titled “Why a single-threaded model”JavaScript is single-threaded per process. Multi-threading options:
worker_threads— separate JS contexts. Hard to share state.- Cluster module — multi-process via fork.
The framework picks single-threaded per ActorSystem for simplicity. For parallelism:
- Cluster across processes — N processes, each one ActorSystem, joined into one cluster.
- Worker mesh — N worker threads, each one ActorSystem, in the same process via MessageChannel.
Both give parallelism without the shared-mutable-state problems that multi-threaded JVM-based actor systems historically had.
Why these CRDTs
Section titled “Why these CRDTs”The framework ships:
- GCounter / PNCounter (counters).
- GSet / ORSet (sets).
- LWWRegister / MVRegister (single values).
- LWWMap / ORMap / GCounterMap (maps).
Why not more? Coverage of the common 95% of distributed-state use cases:
- Counters → GCounter / PNCounter.
- Sets (frequent in chat / presence / configs) → GSet / ORSet.
- Single values (configs / flags) → LWW / MV register.
- Per-key state → maps.
What’s missing?
- Sequence CRDTs (RGA, LSEQ for ordered lists). Niche; hard to get right; rarely needed.
- Tree CRDTs for collaborative docs. Out of scope — domain-specific libraries handle this better.
- Counter-with-cap CRDT (decrement gated by a max). Not a standard CRDT; could be expressed via custom merge.
For the missing pieces, build app-specific patterns on top of the primitives or open an issue if widely needed.
Why no Streams DSL
Section titled “Why no Streams DSL”The JVM actor world has a separate streams library on top of its actor toolkit — Akka Streams and Pekko Streams cover backpressure, materializers, a graph DSL. Porting an equivalent to TypeScript would be a major project on its own. The framework’s scope is the actor model + clustering + persistence; streams DSLs are out of scope.
For TypeScript-side streaming:
- AsyncIterable for pull-based.
pipeTofor actor-bridge.- Third-party libraries (RxJS, Effect’s Stream) for richer patterns.
Why HOCON instead of YAML / TOML
Section titled “Why HOCON instead of YAML / TOML”actor-ts { cluster.gossip-interval = 1s cluster.failure-detector.unreachable-after = 2s}HOCON is the de-facto JVM-config format — readers coming from Akka or Pekko or any Lightbend-Config-based stack land in a familiar shape with minimal changes.
HOCON has features that YAML / TOML don’t:
- Environment-variable substitution —
${?ENV_VAR}. - Duration types —
1s,5m,2hnatively understood. - Size types —
64K,1M,2G. - File includes —
include "shared.conf". - Object merging — additive overlays.
YAML is more widely known but lacks some of these. TOML is similarly featured but less ecosystem-supported.
Why explicit replyTo refs
Section titled “Why explicit replyTo refs”Classic JVM actor frameworks expose an implicit sender().
actor-ts has this.sender (Option) and explicit replyTo
refs in messages.
Why both?
this.senderis implicit and tied to the message’s arrival. Works for ask-style + tell-with-sender.- Explicit
replyTois type-safe — the message type declares what reply type to expect; the compiler can verify.
Convention: explicit replyTo for ask-style request/response,
this.sender for opt-in reply patterns where the caller may
or may not want a reply.
Why no built-in transactions across actors
Section titled “Why no built-in transactions across actors”Multi-actor transactions need either:
- A distributed transaction protocol (2PC, 3PC, paxos). Complex; brittle in real networks.
- Sagas with explicit compensation steps. Easier to reason about; better matches actor semantics.
The framework supports the saga pattern (via PersistentFSM or custom workflows). It doesn’t ship built-in transactions — the cost-benefit isn’t there for most workloads.
Where to next
Section titled “Where to next”- Architecture Decision Records — if you want even more “why” content.
- Version policy — what’s stable vs experimental.
- FAQ — common questions about the framework.