Event stream
이 콘텐츠는 아직 번역되지 않았습니다.
The event stream is a system-wide bus for one-to-many messaging
that doesn’t fit the parent-child or sender-receiver shape. Any
actor can subscribe to a class of event, or to a kind-discriminated
type; any code can publish; the bus matches and tells the event to
every subscriber.
import { Actor, ActorSystem } from 'actor-ts';
class UserLoggedIn { constructor(public readonly userId: string) {}}
class AuditLogger extends Actor<UserLoggedIn> { override preStart(): void { this.context.system.eventStream.subscribe(this.context.self, UserLoggedIn); } override onReceive(event: UserLoggedIn): void { this.log.info(`user ${event.userId} logged in`); }}
class MetricsCollector extends Actor<UserLoggedIn> { override preStart(): void { this.context.system.eventStream.subscribe(this.context.self, UserLoggedIn); } override onReceive(event: UserLoggedIn): void { this.incrementCounter('logins'); }}
const system = ActorSystem.create('demo');system.spawnAnonymous(AuditLogger);system.spawnAnonymous(MetricsCollector);
// Anywhere — including outside an actor:system.eventStream.publish(new UserLoggedIn('user-42'));// → both subscribers receive the eventThe publisher doesn’t know how many subscribers exist, and the subscribers don’t know who’s publishing. That’s the whole point — loose coupling for cross-cutting concerns (logging, metrics, audit, fan-out alerts).
The API
Section titled “The API”// A channel is named by a class, by an EventKey, or by the bare kind string.type EventChannel<TEvent> = | (abstract new (...args: any[]) => TEvent) | EventKey<TEvent> | KindOf<TEvent>;
class EventStream { subscribe<TEvent>( subscriber: ActorRef, channel: EventChannel<TEvent>, predicate?: (event: TEvent) => boolean, ): boolean;
unsubscribe<TEvent>(subscriber: ActorRef, channel?: EventChannel<TEvent>): boolean;
publish(event: object): void;}Three operations:
subscribe(ref, channel)— register interest. The channel is a class, anEventKey, or a kind string. Returnstrueif a new subscription was added,falseif a duplicate was ignored, and throws if the channel is unusable (see the caution below).unsubscribe(ref, channel?)— remove subscriptions. With a channel, just that one; without, every subscription for that ref.publish(event)— fire-and-forget. The bus walks its subscription list, matches each subscription’s channel, andtells every match.
Class channels match with instanceof, so subclass instances reach
base-class subscribers:
class SystemEvent {}class UserLoggedIn extends SystemEvent {}class UserLoggedOut extends SystemEvent {}
eventStream.subscribe(auditor, SystemEvent); // catches botheventStream.subscribe(loginCounter, UserLoggedIn); // catches only the in-eventsThis makes hierarchical event taxonomies easy: subscribers pick their level.
Kind channels
Section titled “Kind channels”Messages tells you to prefer plain objects
({ kind: 'x', n: 1 }) over classes — and a plain type has no
constructor to hand subscribe. Name it by its kind instead. A
type and a const of the same name give it the call shape a class
gets for free:
import { EventKey } from 'actor-ts';
export type UserLoggedInEvent = { readonly kind: 'user-logged-in'; readonly userId: string;};export const UserLoggedInEvent = EventKey.of<UserLoggedInEvent>('user-logged-in');
class AuditLogger extends Actor<UserLoggedInEvent> { override preStart(): void { this.context.system.eventStream.subscribe(this.context.self, UserLoggedInEvent); } override onReceive(event: UserLoggedInEvent): void { this.log.info(`user ${event.userId} logged in`); }}
system.eventStream.publish({ kind: 'user-logged-in', userId: 'user-42' });The key carries the event type, so a predicate written against it sees the real shape without you annotating the parameter:
eventStream.subscribe(auditor, UserLoggedInEvent, (event) => event.userId !== 'system');The bare kind string is the shorthand, and it costs the type —
there is nothing for TEvent to be inferred from, so the predicate
sees unknown. Supplying the argument brings the typing back and
makes the string itself checkable against the type’s kind:
eventStream.subscribe(auditor, 'user-logged-in'); // predicate sees unknowneventStream.subscribe<UserLoggedInEvent>(auditor, 'user-logged-in'); // typed, and the string is checkedTwo identity rules follow from this:
- A key and its string are the same channel. Subscribing both
ways dedups, and either form unsubscribes the other — including a
freshly built
EventKey.of('user-logged-in'). - A class and a kind are two channels, even when the class’s
instances carry that
kind. They select overlapping events, just like a base class and its subclass, so an actor holding both subscriptions receives two deliveries per publish.
Predicate filtering
Section titled “Predicate filtering”eventStream.subscribe( metricsActor, HttpResponse, (event) => event.status >= 500, // only deliver 5xx responses);For high-frequency channels (cluster events, every HTTP response,
every metric tick), filtering on the bus side is cheaper than
filtering inside each subscriber’s onReceive.
Three details:
- No dedup for predicate-bearing subscriptions. Without a
predicate, the bus dedups per
(subscriber, channel)pair — re-callingsubscribeis a no-op. With a predicate, every call adds a new subscription, because predicate functions have no identity contract. “Replace this filter” meansunsubscribethensubscribeagain. - A throwing predicate is treated as “no match” for that delivery. Other subscribers are unaffected; the subscription stays active. A warning is logged via the system logger.
- Predicates run at publish time, on the publisher’s stack. Keep them fast and pure — a heavy predicate slows every publish.
When to use the event stream
Section titled “When to use the event stream”Three good fits:
- Cross-cutting observation — logging, metrics, audit. Many subscribers, none of which the publisher should know about.
- System-wide notifications — “the cluster gained a member,” “the cache flushed.” The cluster extension publishes its own events here; you can subscribe and react.
- De-coupling actor wiring in tests — instead of plumbing a spy ref through five layers, publish on the stream and let the spy subscribe directly.
When NOT to use it
Section titled “When NOT to use it”Built-in events worth subscribing to
Section titled “Built-in events worth subscribing to”The framework publishes several event types you can subscribe to:
| Event | Where it comes from |
|---|---|
DeadLetter | Every message routed to /deadLetters is also published. Useful for “alarms on lost messages.” |
MemberUp, MemberRemoved, MemberUnreachable, … | Concrete cluster membership events published by the cluster extension. Subscribe to each class directly — ClusterEvent is a union type, not a subscribable base class. See Cluster. |
ReachabilityChanged | The publishing node’s own failure detector gained or lost sight of a peer — a local observation, unlike MemberUnreachable, which may have arrived in gossip. See Failure detector. |
| Broker events | BrokerActor subclasses publish connect/disconnect/lag events. |
Everything the framework itself publishes is a class, so class channels remain the way to reach all of the above. Kind channels are for your own application events.
One cluster event is deliberately not here: CurrentClusterState
is delivered only to the listener that asked for it, by
cluster.subscribe(listener, { replayMode: 'snapshot' }). It states where
that subscriber is starting from, which is not news to anybody else,
so it never reaches this bus.
See each extension’s docs for the full event vocabulary it publishes.
Where to next
Section titled “Where to next”- DistributedPubSub — cluster-wide publish/subscribe with topic routing; complementary to the local event stream.
- Actor system —
system.eventStreamis where you reach the bus. - Messages — the shape
rules (immutable, no method refs) apply equally to events, and the
kindconvention documented there is directly subscribable.
The EventStream API
reference covers the full surface.
