---
title: Reactors
editUrl: https://github.com/Cratis/Chronicle.TypeScript/edit/main/Documentation/reactors.md
description: Where reactors are documented, and how TypeScript reactors return side effects and handle replays.
---


Reactors are documented in the shared Chronicle docs with synchronized examples for C#, Kotlin, Java, Elixir, and TypeScript.

- [Getting started with reactors](/chronicle/reactors/getting-started/)
- [Reactor event processing](/chronicle/reactors/event-processing/)
- [Returning side effects](/chronicle/reactors/side-effects/)
- [OnceOnly](/chronicle/reactors/once-only/)
- [Replay](/chronicle/reactors/replay/)
- [Reactors overview](/chronicle/reactors/)

Use the [TypeScript get started page](/chronicle/clients/typescript/getting-started/) for package installation and connection setup.

## TypeScript client notes

### Choosing a handler name

By default, Chronicle calls the method whose name is the camelCase name of a registered event class: `authorRegistered(event)` handles `AuthorRegistered`. No method decorator is needed for that convention.

To name the method after what it does, use `@handles(AuthorRegistered)` instead:

```typescript
import { field } from '@cratis/fundamentals';
import { eventType, handles, reactor } from '@cratis/chronicle';

@eventType()
class AuthorRegistered {
    @field(String) name = '';
}

@reactor()
class AuthorReactor {
    @handles(AuthorRegistered)
    notify(event: AuthorRegistered): void {
        console.log(`Author registered: ${event.name}`);
    }
}
```

With these artifacts registered in your event store, an `AuthorRegistered` event calls `notify`. The decorator identifies the event constructor; TypeScript erases the parameter type at runtime. `@handles` supports legacy (`experimentalDecorators`) and standard TC39 decorators, and is also exported from `@cratis/chronicle/reactors` and `@cratis/chronicle/reducers`.

- An explicit event type replaces the method-name convention **for that method**, even if its name matches a different event class.
- Registration fails if the constructor passed to `@handles` is not a registered `@eventType`, or if two methods handle the same event type. That includes one explicit handler alongside a conventional handler. Keep one live handler per event; rename a conventional method only if it should be a helper.
- Undecorated methods that match no registered event class remain ordinary helpers. Chronicle cannot tell a misspelled handler from a helper; use `@handles` when you want an explicit declaration checked at registration.
- Use a public, string-named instance method, not a static method, private `#method`, accessor, symbol method, or function-valued field. Each method accepts one `@handles` declaration. Inherited handlers work. An override of a method carrying `@handles` must declare its own `@handles`, even when keeping the same event type. Otherwise registration fails with `Override '<method>' on '<Derived>' hides @handles(<EventType>) declared on '<Base>'; redecorate the override.` Chronicle validates methods carrying `@handles` when each instance is created, including instances returned by an activator and `ReactorScenario` instances. Binding the declared method in the constructor is supported; other own properties, including arrow-field overrides, are rejected. Use a method decorated with `@handles` for an override. Conventional and `@replay` handlers retain their existing instance-property behavior.
- `@onceOnly()` works with `@handles` in either decorator order. Without a replay-specific handler, the explicit handler runs during replay too unless marked once-only. For separate replay behavior, use `@replay(AuthorRegistered)` on another method; do not combine `@handles` and `@replay` on one method.

`ReactorScenario` from `@cratis/chronicle/testing` uses the same handler selection and rejects invalid prototype mappings before constructing the reactor. It also validates instance properties hiding `@handles` methods after construction, allowing methods bound with `Function.prototype.bind` in a constructor (including bound again in a subclass constructor). [Reducers](/chronicle/clients/typescript/reducers/) also support `@handles`, without changing their handler arguments or returned state.

### Filtering by event source and stream type

To receive only events from a particular source or stream type, put `@eventSourceType('customer')` or `@eventStreamType('payments')` on the reactor class. Import both decorators from `@cratis/chronicle` or `@cratis/chronicle/events`; they support legacy and standard decorators. Derived reactors inherit these filters unless they declare their own value.

| Decorator | Default | Effect |
| --- | --- | --- |
| `@eventSourceType(value: string)` | `''` (unspecified) | Restricts delivery to the given source type; `''` removes that restriction. |
| `@eventStreamType(value: string)` | `'All'` | Restricts delivery to the given stream type; `'All'` removes that restriction. |

The kernel combines source type, stream type, and `@filterEventsByTag` restrictions with **AND**. Multiple filter tags match **any** of those tags. `@tag` labels the observer and does not restrict delivery. These decorators belong on observers and do not filter projections.

On a reactor, they also supply append metadata for **bare returned events**, matching .NET `[EventSourceType]` and `[EventStreamType]`: a non-empty source type sets the returned event's source type, and a non-empty declared stream type overrides the triggering stream type. Without a stream decorator, bare returns keep the triggering stream type; explicitly declaring `'All'` also supplies that value to returned events. Each returned `EventForEventSourceId` keeps its own metadata, including omitted or empty type values, rather than inheriting the reactor's types. Explicit calls to `services.eventStore.eventLog.append` do not inherit these decorators; set `sourceType` and `streamType` in append options to route those events.

`ReactorScenario` rejects non-default source or stream filters with `UnsupportedReactorOperation` rather than silently delivering unfiltered events. Use a kernel-backed specification for filtered delivery.

### Handler arguments and side effects

The optional third argument, `ReactorServices`, gives the owning observation's `eventStore`, its `readModels` (`services.readModels === services.eventStore.readModels`), and a disconnect/shutdown `signal`. It is the consuming store and namespace, including for imported events, not an application-wide default or the upstream event's provenance. Existing one- and two-argument methods work unchanged; rest-parameter handlers now see this additional argument.

To append explicitly, await `services.eventStore.eventLog.append(context.eventSourceId, event)` and check `result.isSuccess`; a rejected append does not throw automatically. Throw if it failed so the observation is not acknowledged. To read state, use `services.readModels.findInstanceById(Model, context.eventSourceId)` and handle `null`. Read models are eventually consistent: a read may lag the triggering event or be absent, including during replay. There is no automatic historical snapshot at the event's sequence number. If a missing model prevents your side effect, throw rather than acknowledging lost work; make retries safe.

A reactor handler method can return a side effect instead of only observing: a single event, an array of events, a single `EventForEventSourceId` (to target an event source other than the one that triggered the reactor), an array of those, or a mix. Whatever is returned is appended in one atomic `appendMany` call once the handler completes — a bare event uses the triggering event's own event source id, stream id, and subject, with source and stream types resolved as described above; an `EventForEventSourceId` entry keeps its own target. If the side-effect append fails, the reactor's partition is marked Failed, the same as if the handler itself had thrown.

For application-owned return types, pass `reactorResultHandler` to `ChronicleOptions.fromConnectionString(connectionString, { reactorResultHandler })` (or `development({ reactorResultHandler })`). The callback receives the returned value, triggering `EventContext`, reactor class, event store name, and namespace. Return `true` only after handling the entire result; return `false` to let Chronicle append its recognized event returns. Throw on failure so the partition fails instead of acknowledging a lost side effect. The hook is installed before reactor observations begin. It does not provide a transaction across commands and events; handle mixed returns deliberately.

### Activating artifacts

The SDK normally calls the zero-argument constructor **once per observation stream** and reuses that instance across deliveries and replay notifications. Set `artifactActivator` on `ChronicleOptions.development({ artifactActivator })` (or `fromConnectionString`) to resolve constructor dependencies instead. With an activator, each delivered event batch gets one lease and one shared instance; replay lifecycle notifications get **separate** leases. No lease is made for a message without events or a replay transition. When every event in a batch is skipped, no event-batch lease is made (though a replay transition in that message still gets its separate notification lease). A reconnect creates a new observation, and its context identifies the exact store/namespace and signal for that generation.

```typescript
import { ArtifactDelivery, ArtifactKind, ChronicleOptions, type ClientArtifactsActivator } from '@cratis/chronicle';

const artifactActivator: ClientArtifactsActivator = async (type, context) => {
    // Resolve `type` through your container in a delivery-owned scope here.
    // This simple example works only for classes without constructor dependencies.
    const instance = new type();
    if (context.kind === ArtifactKind.Reactor && context.delivery === ArtifactDelivery.Events) {
        console.log(context.artifactId, context.eventStore.namespace.value, context.eventContext.sequenceNumber);
    }
    return { instance };
};
const options = ChronicleOptions.development({ artifactActivator });
```

The context contains `kind`, `artifactId`, `eventStore`, `readModels`, `eventSequenceId`, `partition`, and `signal`. Its `delivery` discriminant provides `eventContext` (the **first handled** event, not a promise that all events have the same context) or `replayState`. The activator may return `{ instance, run, complete, dispose }`: `run(callback, invocation)` optionally enters your scope for **each** handler invocation **and its returned side-effect dispatch**. The SDK always supplies `invocation`: for events, it has `delivery: ArtifactDelivery.Events`, the **current** `eventContext`, and `methodName`; for replay notifications it has `delivery: ArtifactDelivery.ReplayNotification` and `replayState`. Existing `run(callback)` implementations can ignore the second argument.

`complete()` is awaited once per created lease after processing (even when a handler fails), **before** the observation result is acknowledged. If it rejects, the partition fails with `ArtifactCompletionFailed`; any processing failure is also reported. A completion failure does not advance the batch's checkpoint or publish its tentative reducer state, but already performed external effects are not rolled back and may repeat on retry. Make them idempotent. `dispose()` still runs unconditionally after completion and is awaited once; its errors are logged, not treated as delivery failures. Only the lease is disposed by the SDK, not a container-owned instance separately. Without an activator, no new ambient metadata/execution boundary is installed. Keep batch-local mutable instance state out of cross-batch assumptions, and stop work cooperatively when `signal` aborts; arbitrary application promises cannot be forcibly canceled.

A `@reactor`/`@reducer`-decorated class instance can also optionally implement `ICanBeNotifiedWhenReplay` (`beginReplay`/`endReplay`) and/or `ICanBeNotifiedWhenPartitionReplayed` (`beginReplayPartition`/`endReplayPartition`, both given the partition) to be notified when a replay begins and ends, at the full-observer or per-partition granularity.

## Replay policies

Reactors are replayable by default. Put `@onceOnly()` on a handler to skip that handler for replayed events, or on the reactor class to register the entire reactor as non-replayable so the kernel does not replay it.

```typescript
import { eventType, reactor, onceOnly, replay } from '@cratis/chronicle';

@eventType()
class OrderPlaced {
    constructor(readonly orderId: string = '') {}
}

@reactor()
class OrderReactor {
    @onceOnly()
    async orderPlaced(event: OrderPlaced): Promise<void> {
        // Notify the customer on live delivery; never notify again during replay.
    }

    @replay()
    async replayOrderPlaced(event: OrderPlaced): Promise<void> {
        // Rebuild replay-specific state instead of running orderPlaced.
    }
}
```

**Upgrade caution:** Existing reactors now accept kernel replays, including explicit replays and automatic replays such as revision/redaction rewinds or definition-change replays when enabled. Mark side-effecting reactors with class-level `@onceOnly()` if none of their handlers should replay; use method-level `@onceOnly()` when only particular handlers have side effects. Review existing reactors before upgrading to avoid repeating notifications, external calls, or returned events.

`@replay()` uses the `replay<EventClassName>` method naming convention; `@replay(OrderPlaced)` also accepts an explicit event type when a different name is useful. A replay-only handler subscribes to its event type, but does not run during live delivery. Without a replay-specific handler, the ordinary event handler (camelCase or `@handles`) runs for replayed events unless that method has `@onceOnly()`. When both are present, **only** the replay handler runs during replay; method-level `@onceOnly()` on the ordinary handler does not prevent it. Both decorators support legacy and standard TypeScript decorator syntax.

These policies apply to the **per-event observation state** sent by the kernel, not the separate begin/end replay lifecycle notifications. A message with both a notification and events notifies first, then handles the events; a failed notification fails the partition instead. `@onceOnly()` excludes replay, **not duplicates or retries**. Explicit writes and returned appends are not automatically one transaction with the triggering delivery; make effects idempotent, including under concurrent retries. Replay is not an append sandbox: unguarded explicit or returned writes can happen again during replay.
