Skip to content

Reactors

After an author registers, the catalog should build a shelf for them. The registration command should not do that work itself: the shelf belongs to another slice, and the registration is complete the moment the fact is recorded. A projection answers “what does this look like now?”. A reactor answers “what should happen because of this?”.

Reactors are part of the Chronicle SDK, @cratis/chronicle. The Arc integration adds one thing to them: a reactor can return Arc commands, and Arc runs them through its command pipeline. See Returning commands from a reactor.

ShelfBuilder.ts
import { onceOnly, reactor } from '@cratis/chronicle/reactors';
import type { EventContext } from '@cratis/chronicle/events';
import { AuthorRegistered } from '../Registration/Registration.js';
import { CreateShelf } from './CreateShelf.js';
@onceOnly()
@reactor()
export class ShelfBuilder {
authorRegistered(event: AuthorRegistered, context: EventContext): CreateShelf {
return new CreateShelf(context.eventSourceId);
}
}

CreateShelf is an ordinary Arc @command() in your application. Register the reactor the way you register every other artifact, with builder.add(...) or builder.discover(...) after withChronicle. A reactor has no Arc decorator, so one discovered before withChronicle is dropped unless activateArtifactsInScopes is on. The Arc-owned Chronicle client starts observing it when the application builds.

The SDK looks for a method named after the event class in camelCase: AuthorRegistered is handled by authorRegistered. The parameter type plays no part in the match.

The first argument is the stored event content parsed from JSON, not an instance of your event class. Read its properties, but do not call its methods or test it with instanceof. A concept property arrives as its underlying primitive value, so event.name on AuthorRegistered is a string at runtime even though the class declares an AuthorName. The second argument is the event’s EventContext, which carries the event source ID, sequence number, occurred time, correlation ID, causation, and the identity that caused it.

By default the SDK constructs the reactor itself. It has no dependency injection, so a reactor cannot take constructor services. To have Arc construct reactors with their services instead, see Activate reactors and reducers in Arc scopes (preview).

ReturnWhat happens
NothingThe event is acknowledged
A registered event, or an array of themThe SDK appends them to the triggering event’s source and stream as one batch
An EventForEventSourceId entry, or an array mixing entries and eventsThe SDK appends each entry to its own target
An Arc @command() instance, or a nonempty array of only commandsArc executes each command in order; see Returning commands

Returning commands and events together in one array fails the handler. Anything else the SDK does not recognize is ignored.

A handler that throws, or a returned side effect that fails, marks the observer partition for that event source as failed, with the error message. Chronicle’s failed-partition handling decides when that event is delivered again. Nothing that already happened is undone, so write handlers that are safe to run twice for the same event.

Reactors run on replay by default. For effects such as returned commands, mark the class with @onceOnly() so Chronicle never replays the reactor at all, neither a full replay nor a partition replay; a @replay() handler on such a class therefore never runs. Mark individual methods with @onceOnly() instead to skip only those handlers during a replay, and use @replay() to select a separate handler for a replayed event. See Chronicle once-only reactors. These markers do not prevent ordinary re-delivery after a failed partition recovers, so keep the effects safe to repeat.

TopicDescription
Returning commandsReturn Arc commands from a reactor and have Arc execute them with validation and authorization
Chronicle reactorsReactor concepts in the Chronicle documentation