Zum Inhalt springen

Future patterns

Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.

Promises and actors are two different concurrency models living in the same TypeScript runtime. An actor’s onReceive can await a promise, but doing so blocks the actor’s mailbox until the promise settles. The framework’s future patterns are a small set of helpers for crossing the bridge without blocking.

HelperRole
pipeToSend a promise’s eventual result as a message to an actor.
afterWait delayMs, then run a factory; cancellable.
Success<T> / FailureTagged envelope types — pipeTo wraps results in these by default.

Together they let you initiate async work, keep onReceive non-blocking, and receive the result as another message.

class Slow extends Actor<...> {
override async onReceive(msg): Promise<void> {
const data = await this.downstream.fetch(msg.id);
// ↑ everything in this actor's mailbox waits until fetch resolves.
this.handle(data);
}
}

The actor’s per-message guarantee says: only one onReceive invocation runs at a time. An await keeps the invocation alive until the promise settles — meaning the next message can’t be processed until then.

If downstream.fetch is fast and reliable, this is fine. If it’s slow or flaky, the actor’s mailbox piles up.

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; // doesn't block — onReceive returns immediately
}
if (msg instanceof Success) {
this.handle(msg.value);
} else if (msg instanceof Failure) {
this.log.warn(`fetch failed: ${msg.error.message}`);
}
}
}

pipeTo(promise, recipient) schedules a callback on the promise that tells the result to the recipient:

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

The actor’s mailbox isn’t blocked. The result lands as a normal message, processed in order alongside everything else.

The full signature:

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

With wrap: false, the raw fulfillment value is told to the target (no Success envelope), and rejections are dropped silently. Useful when:

  • The target expects the value’s raw shape (e.g. an HTTP-handler actor expecting the JSON body directly).
  • You handle errors elsewhere (a separate error pipe, or you’ve already attached a .catch).
pipeTo(promise, target, { sender: this.context.self });

By default, pipeTo sends with no sender ref. Pass sender to attribute the message to a specific actor — useful for routing the result back to whoever asked.

Success and Failure — the envelope types

Section titled “Success and Failure — the envelope types”
import { Success, Failure } from 'actor-ts';
new Success(42); // wraps a value
new Failure(new Error('oops')); // wraps an error

Simple tagged classes — values are immutable after construction. Pattern-match with 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 {
// it's a Cmd
}
}

Or 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(() => { /* ... */ });

Use them when the receiving actor’s message type already includes arbitrary results — the wrappers prevent accidental treatment of raw promise results as commands.

import { after } from 'actor-ts';
const result = await after(5_000, () => fetchSomething());

Wait 5 seconds, then call the factory and resolve with its result. The wait isn’t a blocking sleep — it’s setTimeout + a Promise — so the rest of the program runs normally.

The factory is called once after the delay; the returned Promise resolves/rejects with whatever the factory produces.

const p = after(5_000, () => fetchSomething());
// Later, maybe:
p.cancel();
// → p rejects with Error('after: cancelled')

The returned CancellablePromise<T> has a .cancel() method that clears the timer and rejects the promise. Useful for building timeout-style abort logic:

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(); // we won, clean up the timer
return winner;
} catch (err) {
// either the work threw or the watchdog fired
throw err;
}
pipeTo(
after(1_000, () => this.downstream.fetch(id)),
this.context.self,
);

Schedule a downstream fetch in 1 second; pipe the result into the actor’s own mailbox. Useful for “kick off later but stay non-blocking” patterns.

context.timers.startSingleTimer and after look similar — both fire something after a delay. The differences:

Aspectcontext.timersafter
Delivers viatell to this.selfResolves a Promise
Cancellationtimers.cancel(key)cancellable.cancel()
Auto-cancel on actor stopYesNo
Best forActor-internal schedulingBridging actor + promise

If you’re inside an actor and want to schedule a delayed message, prefer context.timers. If you’re outside an actor or you want the delay to chain into a promise pipeline, use after.

  • Ask pattern — for the blocking await flavor when that’s actually what you want.
  • Timers and schedulingcontext.timers.startSingleTimer for actor-bound delays.
  • Retry — for retry-then-pipeTo combinations.

The pipeTo, after, and Success / Failure API references cover the full surface.