Zum Inhalt springen
Deutsch

Von Akka (JVM)

actor-ts ist der engste spirituelle Cousin von Akka im TypeScript-Umfeld. Die meisten Konzepte mappen 1:1; viele APIs sind identisch benannt. Dieser Guide führt durch die Übersetzung.

Akka (JVM)actor-ts
akka.actor.ActorActor<TMessage>
akka.actor.ActorRefActorRef<T>
akka.actor.Propsdie Actor-Klasse, eine Factory oder ActorOptions<TMessage>
tell(message) / !tell(message)
ask(message).mapTo[T]ref.ask<TRes>(message, timeoutMs)
context.spawnAnonymous(props)context.spawnAnonymous(actor)
OneForOneStrategyOneForOneStrategy
AllForOneStrategyAllForOneStrategy
become(receive)context.become(handler)
stash() / unstashAll()context.stash() / context.unstashAll()
context.watch(ref)context.watch(ref)
Terminated(ref)Terminated-Systemnachricht
PoisonPillPoisonPill.instance
KillKill.instance
setReceiveTimeout(d)context.setReceiveTimeout(ms)
EventStreamsystem.eventStream
Cluster.get(system).join(...)Cluster.join(system, settings)
ClusterShardingcluster.sharding (+ ShardKey)
ClusterSingletoncluster.singleton (+ SingletonKey)
DistributedPubSubDistributedPubSub
DistributedDataDistributedData (per Extension)
PersistentActorPersistentActor
persist(event)(cb)this.persist(event, afterPersist)
Akka HTTPHttpExtension + Route-DSL
Akka StreamsNICHT verfügbar
// Akka Scala:
class Counter extends Actor {
var count = 0
def receive = {
case "inc" => count += 1
case "get" => sender() ! count
}
}
// actor-ts:
import { match } from 'ts-pattern';
import { Actor, type ActorRef } from 'actor-ts';
type Command = { kind: 'increment' } | { kind: 'get'; replyTo: ActorRef<number> };
class Counter extends Actor<Command> {
private count = 0;
override onReceive(command: Command): void {
match(command)
.with({ kind: 'increment' }, () => this.onIncrement())
.with({ kind: 'get' }, (c) => this.onGet(c))
.exhaustive();
}
private onIncrement(): void { this.count++; }
private onGet(command: GetCommand): void { command.replyTo.tell(this.count); }
}

Unterschiede:

  • TypeScript braucht explizite Nachrichten-Typen (Command) - Akkas Any lässt sich nicht übersetzen.
  • sender() wird zu einer expliziten replyTo-Ref in der Nachricht oder zu this.sender (Option).
  • Keine Pattern-Matching-Syntax; nutze if/else oder ts-pattern.
// Akka Scala:
override val supervisorStrategy = OneForOneStrategy() {
case _: ArithmeticException => Resume
case _: NullPointerException => Restart
case _: Exception => Escalate
}
// actor-ts:
override supervisorStrategy = new OneForOneStrategy(
decideBy([
{ match: ArithmeticError, then: Directive.Resume },
{ match: NullPointerError, then: Directive.Restart },
{ match: Error, then: Directive.Escalate },
]),
);

Benennung + Struktur identisch; die Direktiven sind dieselben.

// Akka Scala:
val cluster = Cluster(system)
cluster.join(Address("akka", "MySystem", "host", port))
val region = ClusterSharding(system).start(
typeName = "Counter",
entityProps = Props[Counter],
settings = ClusterShardingSettings(system),
extractEntityId = ...,
extractShardId = ...,
)
// actor-ts:
const clusterOptions = ClusterOptions.create()
.withHost(host)
.withPort(port)
.withSeeds(seeds);
const cluster = await Cluster.join(system, clusterOptions);
const region = cluster.sharding.start({
typeName: 'Counter',
entityActor: Counter,
extractEntityId: (message) => message.id,
numShards: 100,
});

Größtenteils Drop-in. Unterschiede:

  • extractShardId wird in actor-ts aus extractEntityId + numShards abgeleitet (shardId = hash(entityId) % numShards). Keine separate Funktion.
  • ClusterShardingSettings steht inline als Optionen von start().
// Akka Scala:
class Account(val id: String) extends PersistentActor {
override def persistenceId = s"account-$id"
var balance = 0
override def receiveCommand = {
case Deposit(amt) =>
persist(Deposited(amt))(e => balance += e.amount)
}
override def receiveRecover = {
case Deposited(amt) => balance += amt
}
}
// actor-ts:
class Account extends PersistentActor<Command, Event, State> {
constructor(public readonly id: string) { super(); }
readonly persistenceId = `account-${this.id}`;
initialState(): State { return { balance: 0 }; }
onEvent(state: State, event: Event): State {
return match(event)
.with({ kind: 'deposited' }, (e) => ({ balance: state.balance + e.amount }))
.exhaustive();
}
onCommand(state: State, command: Command): void {
match(command)
.with({ kind: 'deposit' }, (c) => this.onDeposit(c))
.exhaustive();
}
private onDeposit(command: DepositCommand): void {
this.persist({ kind: 'deposited', amount: command.amount }, () => {});
}
}

Schlüssel-Unterschiede:

  • Ein einziges onEvent statt geteilter receiveCommand / receiveRecover. Läuft sowohl beim Persistieren als auch bei Recovery.
  • persist-Callback-Signatur - (newState) => void statt (event) => unit.
  • State ist expliziter Typ-Parameter - Akkas zustandsbehaftete Variable wird zu einer State-Form.
// Akka Scala:
val singleton = system.spawn(
ClusterSingletonManager.props(
singletonProps = Props[MyActor],
terminationMessage = PoisonPill,
settings = ClusterSingletonManagerSettings(system),
),
name = "singletonManager",
)
// actor-ts:
class MyActor extends Actor<MyCommand> {
static readonly singleton = SingletonKey.of<MyCommand>('my-singleton');
}
const singleton = cluster.singleton.start(MyActor);
singleton.tell({ kind: 'do-something' });

Dasselbe Modell, weniger Zeremonie — der Actor deklariert seine Identität als Static, und start gibt direkt den Proxy-ActorRef zurück statt eines Handles zum Auspacken. Ein terminationMessage- Äquivalent gibt es nicht; ein Node, der die Rotation verlässt, stoppt sein Kind über den normalen postStop-Pfad.

Nodes, die nur mit dem Singleton sprechen müssen, rufen cluster.singleton.ref(MyActor) — das Gegenstück zu Akkas ClusterSingletonProxy.props und zu ClusterSharding.startProxy auf der Sharding-Seite.

  • Akka Streams - keine Portierung. Nutze Promise-basierte Patterns oder eine separate Streams-Library.
  • Akka HTTPs typisierte Route-DSL - actor-ts hat eine eigene DSL, aber sie ist einfacher / weniger feature-reich.
  • Akka Persistence Querys reaktive Stream-API - actor-ts hat PersistenceQuery, aber als AsyncIterable, nicht als Stream.
  • Einige fortgeschrittene Supervision-Features - Backoff-Supervision existiert; “watch a Future”-Patterns brauchen manuelles Wiring.
  • TypeScript-Typen - Nachrichten-Typen werden an der Compile-Grenze geprüft. Keine Any- / case class-Runtime-Checks.
  • Buns schneller Start - sub-100 ms statt 1+ Sekunde der JVM.
  • Kleinere Binaries - keine JVM zum Ausliefern.
  • Einfacheres operationelles Modell - Single-Process JS statt JVM-Tuning.

Für eine bestehende Akka-App, die actor-ts erwägt:

  1. Nicht alles auf einmal umschreiben. Lass actor-ts in einem neuen Service laufen, der das Akka-System via HTTP/gRPC vorlagert.
  2. Einen Bounded Context nach dem anderen migrieren - wähle eine self-contained Domäne (Orders, Sessions) und portiere sie.
  3. Persistenz vorsichtig re-exportieren - von Akka geschriebene Events sind JSON, wenn du Jackson genutzt hast; actor-ts kann sie mit einem EventAdapter lesen.