Zum Inhalt springen
Deutsch

Future-Patterns

Promises und Actors sind zwei verschiedene Concurrency-Modelle, die in derselben TypeScript-Runtime leben. Das onReceive eines Actors kann ein Promise awaiten, aber das blockiert die Mailbox des Actors, bis das Promise settled. Die Future-Patterns des Frameworks sind ein kleines Set von Helfern, um die Brücke zu überqueren, ohne zu blockieren.

HelferRolle
pipeToSende das eventuelle Ergebnis eines Promises als Nachricht an einen Actor.
afterWarte delayMs, dann laufe eine Factory; cancelbar.
Success<T> / FailureGetaggte Envelope-Typen — pipeTo wickelt Ergebnisse standardmäßig in diese.

Zusammen lassen sie dich Async-Arbeit initiieren, onReceive non-blocking halten und das Ergebnis als andere Nachricht empfangen.

class Slow extends Actor<...> {
override async onReceive(msg): Promise<void> {
const data = await this.downstream.fetch(msg.id);
// ↑ alles in der Mailbox dieses Actors wartet, bis fetch resolved.
this.handle(data);
}
}

Die Per-Message-Garantie des Actors sagt: nur ein onReceive-Aufruf läuft gleichzeitig. Ein await hält den Aufruf am Leben, bis das Promise settled — was bedeutet, dass die nächste Nachricht bis dahin nicht verarbeitet werden kann.

Wenn downstream.fetch schnell und zuverlässig ist, ist das okay. Wenn es langsam oder flakey ist, stapelt sich die Mailbox des Actors.

import { Actor, pipeTo, Success, Failure } from 'actor-ts';
type Msg =
| { kind: 'fetch'; id: string }
| Success<Item>
| Failure;
class Fast extends Actor<Msg> {
override onReceive(msg: Msg): void {
if (msg.kind === 'fetch') {
pipeTo(this.downstream.fetch(msg.id), this.context.self);
return; // blockiert nicht — onReceive kehrt sofort zurück
}
if (msg instanceof Success) {
this.handle(msg.value);
} else if (msg instanceof Failure) {
this.log.warn(`fetch failed: ${msg.error.message}`);
}
}
}

pipeTo(promise, recipient) plant einen Callback auf dem Promise, der das Ergebnis dem Empfänger tellt:

  • Fulfillmentrecipient.tell(new Success(value))
  • Rejectionrecipient.tell(new Failure(error))

Die Mailbox des Actors ist nicht blockiert. Das Ergebnis landet als normale Nachricht, in Reihenfolge mit allem anderen verarbeitet.

Die volle Signatur:

function pipeTo<T>(
promise: Promise<T>,
recipient: ActorRef,
options?: PipeToOptions,
): Promise<T>;
interface PipeToOptions {
sender?: ActorRef | null;
wrap?: boolean; // Default true — in Success/Failure wickeln
}
pipeTo(promise, target, { wrap: false });

Mit wrap: false wird der rohe Fulfillment-Wert dem Target getellt (kein Success-Envelope), und Rejections werden still verworfen. Nützlich, wenn:

  • Das Target die rohe Form des Werts erwartet (z.B. ein HTTP-Handler-Actor, der den JSON-Body direkt erwartet).
  • Du Fehler anderswo behandelst (eine separate Error-Pipe, oder du hast bereits einen .catch angehängt).
pipeTo(promise, target, { sender: this.context.self });

Standardmäßig sendet pipeTo ohne Sender-Ref. Übergib sender, um die Nachricht einem spezifischen Actor zuzuschreiben — nützlich, um das Ergebnis zurück an denjenigen zu routen, der gefragt hat.

import { Success, Failure } from 'actor-ts';
new Success(42); // wickelt einen Wert
new Failure(new Error('oops')); // wickelt einen Fehler

Einfache getaggte Klassen — Werte sind nach der Konstruktion unveränderlich. Pattern-Match mit instanceof:

override onReceive(msg: Success<Item> | Failure | Cmd): void {
if (msg instanceof Success) {
this.handle(msg.value);
} else if (msg instanceof Failure) {
this.fail(msg.error);
} else {
// es ist ein Cmd
}
}

Oder in ts-pattern:

import { P, match } from 'ts-pattern';
match(msg)
.with(P.instanceOf(Success), (s) => this.handle(s.value))
.with(P.instanceOf(Failure), (f) => this.fail(f.error))
.otherwise(() => { /* ... */ });

Verwende sie, wenn der Nachrichtentyp des empfangenden Actors bereits beliebige Ergebnisse einschließt — die Wrapper verhindern versehentliche Behandlung roher Promise-Ergebnisse als Commands.

after — eine Factory nach einem Delay laufen lassen

Abschnitt betitelt „after — eine Factory nach einem Delay laufen lassen“
import { after } from 'actor-ts';
const result = await after(5_000, () => fetchSomething());

Warte 5 Sekunden, dann rufe die Factory und resolve mit ihrem Ergebnis. Das Warten ist kein blockierender sleep — es ist setTimeout + ein Promise — der Rest des Programms läuft also normal.

Die Factory wird einmal nach dem Delay aufgerufen; das zurückgegebene Promise resolved/rejected mit dem, was die Factory produziert.

const p = after(5_000, () => fetchSomething());
// Später, vielleicht:
p.cancel();
// → p rejected mit Error('after: cancelled')

Das zurückgegebene CancellablePromise<T> hat eine .cancel()-Methode, die den Timer cleared und das Promise rejected. Nützlich zum Bauen Timeout-artiger Abort-Logik:

const work = expensiveOperation();
const watchdog = after(5_000, async () => { throw new Error('took too long'); });
try {
const winner = await Promise.race([work, watchdog]);
watchdog.cancel(); // wir haben gewonnen, Timer aufräumen
return winner;
} catch (err) {
// entweder die Arbeit warf oder der Watchdog feuerte
throw err;
}
pipeTo(
after(1_000, () => this.downstream.fetch(id)),
this.context.self,
);

Plane einen Downstream-Fetch in 1 Sekunde; pipe das Ergebnis in die eigene Mailbox des Actors. Nützlich für “kick off später, aber bleibe non-blocking”-Patterns.

context.timers.startSingleTimer und after sehen ähnlich aus — beide feuern etwas nach einem Delay. Die Unterschiede:

Aspektcontext.timersafter
Liefert viatell an this.selfResolved ein Promise
Cancellationtimers.cancel(key)cancellable.cancel()
Auto-Cancel bei Actor-StopJaNein
Am besten fürActor-interne SchedulingBrücke Actor + Promise

Wenn du innerhalb eines Actors bist und eine verzögerte Nachricht planen willst, bevorzuge context.timers. Wenn du außerhalb eines Actors bist oder das Delay in eine Promise-Pipeline ketten willst, verwende after.

  • Ask-Pattern — für den blockierenden await-Geschmack, wenn das tatsächlich ist, was du willst.
  • Timer und Schedulingcontext.timers.startSingleTimer für actor-gebundene Delays.
  • Retry — für retry-then-pipeTo-Kombinationen.

Die pipeTo-, after- und Success- / Failure-API-Referenzen decken die volle Schnittstelle ab.