Перейти к содержимому
Русский

Quickstart

Это содержимое пока не доступно на вашем языке.

Five minutes from “what’s an actor” to “I have one running, sending and receiving messages.” This guide assumes no prior actor-model experience.

A small program that creates an ActorSystem, spawns a single Greeter actor inside it, sends the actor a message, and shuts down. The same file runs unchanged under Bun, Node, and Deno — picking the right runtime backend automatically.

After this guide you’ll know:

  • How to define an actor by extending Actor<T>.
  • How to spawn it via system.spawnAnonymous(Props.create(...)).
  • The difference between tell (fire-and-forget) and the actor’s onReceive handler.
  • How to shut the system down without leaking timers or sockets.
Terminal window
bun add actor-ts

For a full installation walkthrough — including optional peer dependencies (better-sqlite3, @hono/node-server, broker clients) — see the Installation page.

Create hello.ts and paste:

import { Actor, ActorSystem, Props } from 'actor-ts';
// 1. Define an actor. The type parameter `<string>` constrains what
// messages this actor accepts — the type system catches bad sends
// at compile time.
class Greeter extends Actor<string> {
override onReceive(name: string): void {
console.log(`hello, ${name}!`);
}
}
// 2. Create the system. One `ActorSystem` per process is the norm;
// everything else lives inside it.
const system = ActorSystem.create('hello');
// 3. Spawn the actor. `Props.create(() => new Greeter())` is the
// factory; `'greeter'` is the actor's name in the hierarchy.
const ref = system.spawn(Props.create(() => new Greeter()), 'greeter');
// 4. Send a message. `tell` is fire-and-forget — it returns
// immediately, the actor processes the message asynchronously on
// its own mailbox.
ref.tell('world');
// 5. Give the mailbox a tick to drain, then shut down. In a real
// app you'd terminate on SIGTERM via `CoordinatedShutdown`; for
// this script a short sleep is enough.
await new Promise((resolve) => setTimeout(resolve, 20));
await system.terminate();

Run it:

Terminal window
bun run hello.ts

You’ll see:

hello, world!

That’s the whole loop: define → spawn → tell → terminate.

The five lines map to the actor model’s five core ideas:

  1. class Greeter extends Actor<string> — an actor is a class with a private mailbox and a single onReceive handler. The type parameter says “this actor accepts string messages” — sending it a number would be a TypeScript error at compile time.

  2. ActorSystem.create('hello') — the ActorSystem is the runtime container. It owns the dispatcher (which schedules message processing), the supervisor hierarchy (which catches actor failures), the scheduler, the event stream. There’s typically one per process.

  3. system.spawnAnonymous(Props.create(() => new Greeter()))Props is actor-ts’s way to defer construction. The system needs to control when the actor instance is created (on its mailbox thread, not on yours), so you hand it a factory rather than a pre-built instance. The returned ActorRef is a handle, not the actor itself — you can pass it across the cluster, store it, hand it to other actors.

  4. ref.tell('world')tell is the primary actor verb. It enqueues the message into the actor’s mailbox and returns immediately. The actor processes its mailbox one message at a time on a single logical thread, so you never have to think about locks or races inside onReceive.

  5. system.terminate() — graceful shutdown. Stops the dispatcher, stops all actors (via the supervisor tree), closes the transports. Returns a promise that resolves when teardown is complete. For real apps, hook this into a SIGTERM handler — see Coordinated shutdown.

A string mailbox is the absolute minimum — the framework gets interesting once messages start carrying structure. Two short extensions of the hello-actor sample to make that concrete:

Real actors receive commands, not bare values. The convention is a discriminated union — every message carries a kind literal that narrows the union inside onReceive. match(cmd).exhaustive() from ts-pattern gives you a compile-time check that every variant is handled: add a new kind without a matching with(...) arm and TypeScript fails the build.

import { Actor, ActorSystem, Props, type ActorRef } from 'actor-ts';
import { match } from 'ts-pattern';
type Cmd =
| { kind: 'inc' }
| { kind: 'dec' }
| { kind: 'get'; replyTo: ActorRef<number> };
class Counter extends Actor<Cmd> {
private count = 0;
override onReceive(cmd: Cmd): void {
match(cmd)
.with({ kind: 'inc' }, () => { this.count++; })
.with({ kind: 'dec' }, () => { this.count--; })
.with({ kind: 'get' }, (m) => m.replyTo.tell(this.count))
.exhaustive();
}
}
const system = ActorSystem.create('counters');
const counter = system.spawnAnonymous(Props.create(() => new Counter()));
counter.tell({ kind: 'inc' });
counter.tell({ kind: 'inc' });
counter.tell({ kind: 'dec' });

The { kind: 'get'; replyTo: ActorRef<number> } variant is the standard shape for “give me a reply”: the requester passes its own ref (or, more commonly, one provided by ask, below) and the counter tells the answer back. See Messages and Pattern matching for the deep dive.

tell is fire-and-forget. For request/response there are two equivalent shapes:

import { ActorSystem, Props } from 'actor-ts';
const system = ActorSystem.create('counters');
const counter = system.spawnAnonymous(Props.create(() => new Counter()));
counter.tell({ kind: 'inc' });
counter.tell({ kind: 'inc' });
// Method-on-ref — terse, infers the reply type:
const value = await counter.ask<number>({ kind: 'get' }, 5_000);
console.log(value); // 2
await system.terminate();

Or use the free function for the same effect:

import { ask } from 'actor-ts';
const value = await ask<Cmd, number>(counter, { kind: 'get' }, 5_000);

Either way, the framework spawns a one-shot reply actor under the hood, fills it in as replyTo AND as context.sender on the target, and resolves the promise with the first reply (or rejects with AskTimeoutError). Callers never write replyTo themselves.

See Ask pattern for timeout behaviour, error handling, and the dead-letter case (target actor stopped before replying).

You have a running actor. From here the docs branch out depending on what you want to do:

  • Send more interesting messages: see Messages for the conventions (discriminated unions, immutability, the kind field) and Pattern matching for the match().exhaustive() dispatch idiom this codebase uses everywhere.

  • Get a reply back from the actor: see Ask pattern — the request/response equivalent of tell.

  • Handle failures: see Supervision for how parent actors recover from child crashes.

  • Distribute across machines: see the Cluster overview and Sharding overview — the same code you wrote above can run across N nodes with a single extra line. Cluster.bootstrap builds the ActorSystem, joins the cluster, starts the Receptionist and wires SIGTERM / SIGINT shutdown in one call:

    import { Cluster, Props } from 'actor-ts';
    const { system } = await Cluster.bootstrap({ name: 'hello' });
    const ref = system.spawn(Props.create(() => new Greeter()), 'greeter');
    ref.tell('world');

    Discovery defaults to an env-driven chain — CLUSTER_SEEDS → Kubernetes API → DNS — so the same code runs single-node in local dev and joins an existing cluster in production without a config change.

  • Persist state across restarts: see PersistentActor for event sourcing and Snapshots for bounded recovery time.

If you want to see the framework’s whole surface area at once, the chat sample is the most-comprehensive end-to-end demonstration — it uses sharding, persistence, distributed pubsub, distributed data, cluster singleton, HTTP routing, six interchangeable frontends, the lot.