Skip to content

Event sources and streams

An event source definition gives an event source a stable name, a description, default concurrency dimensions and an optional set of named streams. Routing belongs to the append, not to event types: the same event type can be appended through different definitions.

Declare a definition with @eventSource and, optionally, one @eventStream per stream. Definitions are discovered and registered with the Kernel at startup, right after event types. Registration is an upsert, and the Kernel keeps definitions you stop registering. Two definitions with the same name, or one definition declaring the same stream twice, fail discovery with DuplicateEventSourceName or DuplicateEventStreamName. When you omit name, the class name without a trailing EventSource is used.

import { ConcurrencyDimensions, eventSource, eventStream, eventType, IEventLog } from '@cratis/chronicle';
import { field } from '@cratis/fundamentals';
@eventSource({ name: 'Account', description: 'A bank account', concurrency: ConcurrencyDimensions.eventSourceId })
@eventStream('Transactions', { description: 'Money movements' })
class AccountEventSource {}
@eventSource({ name: 'Customer' })
class CustomerEventSource {}
@eventType()
class AccountFundsDeposited {
@field(Number) readonly amount: number;
constructor(amount: number) {
this.amount = amount;
}
}
async function deposit(log: IEventLog, accountId: string, amount: number) {
return log.append(accountId, new AccountFundsDeposited(amount), {
eventSource: AccountEventSource,
eventStream: 'Transactions',
streamId: 'transfer-1'
});
}
async function depositAcrossSources(log: IEventLog, accountId: string, customerId: string) {
return log.appendMany([
{ eventSourceId: accountId, event: new AccountFundsDeposited(10) },
{ eventSourceId: customerId, event: new AccountFundsDeposited(1), eventSource: CustomerEventSource }
], { eventSource: AccountEventSource, eventStream: 'Transactions' });
}

Pass eventSource (the class or its name) in the append options, and optionally eventStream. The client:

  • writes the definition name as the event source type and as the EventSource key on the event;
  • writes the stream name as the event stream type, while streamId stays yours to choose;
  • throws UnknownEventSource, EventStreamDoesNotBelongToEventSource, EventStreamRequiresEventSource or EventRoutingContradictsEventSource before anything is sent. A batch is validated in full first, so it stays atomic.

An appendMany with EventForEventSourceId entries can carry eventSource and eventStream per event; they win over the shared options, and a shared stream is never inherited by an event that names a different source.

When no concurrencyScope (and no matching entry in concurrencyScopes) is supplied, and the definition declares concurrency dimensions, the client reads the tail sequence number for exactly those dimensions and uses it as the expected scope. A stream’s dimensions replace the source’s when it declares any. An empty scope expects no matching event. An explicit scope always wins, and a definition with no dimensions adds no check.

Chronicle accepts one concurrency scope per event source id in a batch. In appendMany (and when a unit of work commits), every registered-definition event contributes its required guard: events that need the same predicate share one scope, and an event without a guard never suppresses a later guarded one. When one event source id needs different predicates (for example two monthly stream ids, or two definitions with different dimensions), the client throws before anything is written. Pass an explicit shared concurrencyScopes entry for that id, or append the events in separate batches. Explicit scopes and batches that use no definition behave as before.

context.eventSource on appended events, reactor and reducer deliveries holds the definition name. It is undefined for events appended without a definition, including events stored before event sources existed.

Event source routing needs a Kernel that supports registered event sources (Chronicle 19.30.0 or later) and @cratis/chronicle.contracts 19.30.0. Existing appends, options and event contexts are unchanged. Transactional appends accept the same routing: eventLog.transactional.append(id, event, { eventSource, eventStream }) is carried through to the append on commit and validated there. The in-process test scenarios do not support definitions; they throw UnsupportedEventSequenceOperation for eventSource/eventStream instead of dropping them.

IEventStore.eventSources is optional on the interface so custom IEventStore implementations keep compiling; the built-in EventStore always provides it.