Become and stash
Este conteúdo não está disponível em sua língua ainda.
An actor’s onReceive is normally a single function that handles
every message kind. Two context APIs let it reshape that
handling on the fly:
context.become(handler)— temporarily replace the current receive handler with a different one. Restore withunbecome().context.stash()+unstashAll()— park messages an actor isn’t ready for, then re-deliver them when it is.
Together they’re the actor-model answer to “this actor has phases where it doesn’t accept normal traffic” — initialization, recovery from a checkpoint, draining before shutdown, awaiting an async config load.
become — swap the behavior
Section titled “become — swap the behavior”import { match } from 'ts-pattern';import { Actor, ActorSystem } from 'actor-ts';
type OnMessage = { readonly kind: 'on' };type OffMessage = { readonly kind: 'off' };type PressMessage = { readonly kind: 'press' };type Message = OnMessage | OffMessage | PressMessage;
class Switch extends Actor<Message> { override onReceive(message: Message): void { this.offState(message); }
private offState = (message: Message): void => { match(message) .with({ kind: 'press' }, () => this.onPressWhileOff()) .otherwise(() => {}); };
private onPressWhileOff(): void { this.log.info('turning on'); this.context.become(this.onState); }
private onState = (message: Message): void => { match(message) .with({ kind: 'press' }, () => this.onPressWhileOn()) .otherwise(() => {}); };
private onPressWhileOn(): void { this.log.info('turning off'); this.context.become(this.offState); }}
const system = ActorSystem.create('demo');const sw = system.spawnAnonymous(Switch);
sw.tell({ kind: 'press' }); // → "turning on"sw.tell({ kind: 'press' }); // → "turning off"sw.tell({ kind: 'press' }); // → "turning on"become(handler) swaps the function the framework calls on the
next message. No flag fields, no inline if (this.state === 'on')
ladder — the behavior is the state.
The signature:
become(behavior: Receive<TMessage>, discardOld?: boolean): void;unbecome(): void;discardOld defaults to true (replace). Pass false to push
the new behavior onto a stack instead — unbecome() then pops it
and restores the previous one.
Stacked behaviors
Section titled “Stacked behaviors”class Loader extends Actor<Command> { override onReceive(message: Command): void { this.idle(message); }
private idle = (message: Command): void => { match(message) .with({ kind: 'load' }, () => this.onLoad()) .otherwise(() => {}); };
private onLoad(): void { this.context.become(this.loading, /* discardOld */ false); this.startLoading(); }
private loading = (message: Command): void => { match(message) .with({ kind: 'done' }, () => this.onDone()) .otherwise(() => {}); };
private onDone(): void { this.context.unbecome(); // pops loading, idle returns }}Stacking is the right tool when you have a transient sub-behavior
(“currently loading”, “currently authenticating”) that returns to a
stable base behavior once it’s done. For toggles like the Switch
example above, discardOld = true (the default) is cleaner.
Compared to a plain state field
Section titled “Compared to a plain state field”Without become, the same Switch would look like this:
class Switch extends Actor<Message> { private isOn = false; override onReceive(message: Message): void { match(message) .with({ kind: 'press' }, () => this.onPress()) .otherwise(() => {}); }
private onPress(): void { this.isOn = !this.isOn; this.log.info(this.isOn ? 'turning on' : 'turning off'); }}Two states — the plain-field version is shorter. Where become
wins is at N states: with three or four phases, the inline
if/else chain on isOn grows into a state-machine ladder that
the type system can’t help you with. become makes each phase its
own function, with its own valid-message set, naturally narrowed.
For a more formalized version of the same idea, see the FSM pattern — explicit state + transition declarations on top of the same engine.
stash — park, then replay
Section titled “stash — park, then replay”import { Actor, ActorSystem } from 'actor-ts';import { match } from 'ts-pattern';
type ConfigureMessage = { readonly kind: 'configure'; readonly url: string };type RequestMessage = { readonly kind: 'request'; readonly payload: string };type Message = ConfigureMessage | RequestMessage;
class Worker extends Actor<Message> { private url?: string;
override onReceive(message: Message): void { match(message) .with({ kind: 'configure' }, (m) => this.onConfigure(m)) .with({ kind: 'request' }, (m) => this.onRequest(m)) .exhaustive(); }
private onConfigure(m: ConfigureMessage): void { this.url = m.url; // Now drain whatever piled up while we were unconfigured. this.context.unstashAll(); }
private onRequest(m: RequestMessage): void { if (!this.url) { // Not ready yet — park this message. this.context.stash(); return; } this.log.info(`POST ${this.url}: ${m.payload}`); }}The flow: messages arriving before configure are stashed. Once
configure runs, unstashAll() re-prepends them onto the mailbox
in the order they came in, and the actor processes them with the
URL now set.
The signatures:
stash(): void;unstashAll(): void;readonly stashSize: number;Three details that matter:
stash()must be called from inside a user-message handler. It parks the currently-handled message. Calling it frompreStart, a timer callback that isn’t an actor message, or outsideonReceivethrowsStashOutsideHandlerError.unstashAll()is FIFO. Messages come back in the order they were stashed. For aPriorityMailbox, they’re re-inserted through the priority function — so an unstashed message rejoins its priority tier, not the absolute front of the queue.- The stash has a capacity. The buffer is bounded to prevent
memory leaks from runaway stashing; overflow throws
StashOverflowError. The bound is a fixed 1024 messages, compiled in — there’s no configuration knob for it.
stash + become — the canonical combo
Section titled “stash + become — the canonical combo”The two APIs compose for the “actor with an init phase” pattern:
class Worker extends Actor<Message> { private url?: string;
override preStart(): void { // Switch immediately into the "loading config" behavior. this.context.become(this.loading); void this.loadConfig(); }
private loading = (message: Message): void => { match(message) .with({ kind: 'configure' }, (m) => this.onConfigure(m)) .otherwise(() => this.onDuringLoad()); };
private onConfigure(message: ConfigureMessage): void { this.url = message.url; this.context.become(this.ready); this.context.unstashAll(); }
// Anything else arriving during load — stash it. private onDuringLoad(): void { this.context.stash(); }
private ready = (message: Message): void => { match(message) .with({ kind: 'request' }, (m) => this.onRequest(m)) .otherwise(() => {}); };
private onRequest(message: RequestMessage): void { this.log.info(`POST ${this.url}: ${message.payload}`); }
override onReceive(message: Message): void { // Initial behavior — the preStart switches into 'loading' before // any user message arrives. This is here to satisfy the signature. this.loading(message); }
private async loadConfig(): Promise<void> { const config = await fetchConfig(); this.context.self.tell({ kind: 'configure', url: config.url }); }}Two behaviors: loading (stashes everything except configure),
ready (handles requests). The become makes the transition
explicit; the stash + unstashAll makes sure no traffic is
dropped during the initialization window.
When to reach for which
Section titled “When to reach for which”| Situation | Tool |
|---|---|
| Two or more discrete phases with different valid-message sets | become |
| One phase has a transient sub-phase that should restore on done | become(..., discardOld=false) + unbecome() |
| Boolean flag — “ready”/“not ready” — with the same handler shape | A plain field (don’t reach for become) |
| Some messages must be deferred until later, in order | stash + unstashAll |
| Init phase that buffers all non-init traffic | become(loading) + stash in loading + unstashAll in transition |
Where to next
Section titled “Where to next”- Actor — the base class
whose
onReceiveyou swap withbecome. - Mailboxes —
unstashAllre-inserts into the mailbox; the mailbox decides the order. - FSM overview — a more formal
state-machine style on top of the same
becomeengine. - Typed behaviors — the typed
API expresses the same idea as functional
Behavior<T>values rather than mutating context.
The ActorContext API
reference covers become, stash, and the full surface.
