Events and subscribe
Typed semantic coordination between actors, without reaching through to write another actor's state.
Events are how an actor says what happened without knowing who cares. subscribe is how
another actor decides to care.
type ButtonEvents = {
pressed: { source: "keyboard" | "pointer" };
};
class Button extends Actor<never, ButtonEvents> {
*press(source: ButtonEvents["pressed"]["source"]) {
yield* this.emit.pressed({ source });
}
}
subscribe(this, button, {
*pressed(event) {
yield* this.submit(event.source);
},
});The event map is the contract, and it is declared exactly once: as the second type parameter
of Actor (slots come first — pass never when the actor hosts nothing). this.emit is
typed from it, and subscribe infers the same map from the source's emit, so payloads are
typed at both ends and a handler cannot read a field the emitter does not send. There is no
runtime registration — an event exists because the type says so.
Use them for
- Semantic intent between actors —
pressed,keyPressed,windowClosed. - Coordination that must not become a reach-through write.
- Anything a composition should be able to re-route without editing either actor.
Why not just write the other actor's state
Because the owner's invariants stop holding. An actor that owns windows knows that
opening one implies focusing it and moving it to front; an outsider writing
windows.set(...) knows only the array.
// Wrong — the caller now owns invariants it cannot see.
dock.onClick(() => desktop.windows.set(withWindowOpened(desktop.windows(), id)));
// Right — the owner responds to intent.
subscribe(desktop, dock, {
*itemActivated({ appId }) {
yield* this.openWindow(`${appId}-window`);
},
});Composition-level routing
Because handlers are attached rather than baked in, the same actor behaves differently in different compositions. The audio system is built entirely on this — actors emit semantic events and stay sound-agnostic, and the composition declares which events make noise:
audio.bindSounds(keyboard, sounds, {
keyPressed: ({ keyId }) => getKeyboardSoundCue(keyId),
});