Preserve existing append routes
The move to kernel-owned append routing is a major change for the TypeScript client, introduced in @cratis/chronicle 6.0.0. Before 6.0.0, an append with no route options sent source type Default, stream type Default, and the event source identifier as the stream identifier. From 6.0.0 the client omits unspecified route dimensions. A kernel supporting the new behavior resolves missing or empty dimensions to source type Default, stream type All, and stream identifier Default.
This guide applies when you upgrade from a 5.x client to 6.0.0 or later and you have events that earlier clients appended. A new application with no stored events needs no action.
| Area | Before 6.0.0 | 6.0.0 and later | What you do |
|---|---|---|---|
| Append without route options | Written to source type Default, stream type Default, stream id = event source id | Written to source type Default, stream type All, stream id Default | Pass the legacy route explicitly to continue an existing stream; see Make existing routes explicit |
| Route-scoped reads without route arguments | Narrowed to the legacy route | Not narrowed on the omitted dimensions | Pass the route explicitly to read one stream; see Route-scoped reads |
| Kernel version | No check | The client refuses a kernel whose contract is incompatible | Upgrade every kernel node first; the 6.0.0 release required kernel 18.4.1 or later |
The table covers appends and route-scoped reads. getFromSequenceNumber() and hasEventsFor() carry no route and are unchanged.
Before upgrading
Section titled “Before upgrading”Use this guide when you must continue an existing stream rather than select the kernel defaults. Upgrade every kernel node before upgrading the client: the client checks its installed contract descriptor against the server and refuses an incompatible one before an append is sent.
The client checks its installed contracts descriptor before connection succeeds and before event-sequence operations, including direct append calls. An incompatible server or an unavailable compatibility endpoint prevents writes. A server without the compatibility RPC is rejected as incompatible rather than retried indefinitely. Transient failures can be retried; an incompatible verdict is retained until the channel is replaced. It checks again for each replacement channel.
If a replacement server is incompatible during background recovery, the client stays disconnected and logs the terminal failure. Current and later client operations reject with IncompatibleChronicleServer; they do not retry writes. Correct the server deployment, then create a new client instance.
Make existing routes explicit
Section titled “Make existing routes explicit”Pass all three legacy dimensions when appending to an existing stream. The event source argument still selects the source; the reserved AppendOptions.eventSourceId property does not override it.
import { field } from '@cratis/fundamentals';import { eventType, IEventLog } from '@cratis/chronicle';
@eventType()class LegacyOrderNoteRecorded { @field(String) note!: string;
constructor(note: string) { this.note = note; }}
async function appendToExistingOrder(log: IEventLog, orderId: string, note: string) { return log.append(orderId, new LegacyOrderNoteRecorded(note), { sourceType: 'Default', streamType: 'Default', streamId: orderId });}
async function appendToExistingOrders(log: IEventLog, orderIds: string[], note: string) { return log.appendMany(orderIds.map(orderId => ({ eventSourceId: orderId, event: new LegacyOrderNoteRecorded(note), eventSourceType: 'Default', eventStreamType: 'Default', eventStreamId: orderId })));}For a batch containing different sources, set each entry’s eventStreamId to that entry’s source identifier. A shared streamId would target the same stream identifier for every entry.
Review metadata overrides
Section titled “Review metadata overrides”Shared AppendOptions accept sourceType, streamType, streamId, subject, and occurred (Date). Rich EventForEventSourceId entries use eventSourceType, eventStreamType, eventStreamId, subject, and occurred.
For each dimension, an entry’s value wins over the shared option. Otherwise the client omits the route or occurrence time and the kernel resolves it. Explicit empty route strings are sent unchanged; they ask the kernel to resolve that dimension, not to use the shared option. Explicit Default strings and source-based stream identifiers remain exact values.
Subject policy is unchanged: entry subject, then shared subject, then the event source identifier. Tags remain additive across the event type, entry, and shared options. Reactor bare-event returns retain the triggering stream type and identifier; rich returns retain their explicit metadata.
Keep concurrency scopes separate
Section titled “Keep concurrency scopes separate”Leave concurrencyScope and concurrencyScopes configured for the consistency boundary your application requires. Route options do not create or alter a concurrency scope. Per-source scopes still override the shared scope independently of the append route.
Route-scoped reads
Section titled “Route-scoped reads”Reads that take route arguments no longer default to the legacy Default source and stream type. This is part of the same breaking change, and skipping it makes the new appends unreadable.
The kernel resolves an append that carries no route to source type Default, stream type All, and stream identifier Default. A read that narrowed to stream type Default therefore matched none of those events, and returned an empty collection or an unset tail rather than an error. getTailSequenceNumber(), getNextSequenceNumber(), getTailSequenceNumberForObserver(), and getForEventSourceIdAndEventTypes() now leave an unsupplied dimension unnarrowed, so one read sees both the events the kernel routed and the events written to an explicit legacy stream. The kernel treats an empty dimension as “do not narrow” on every one of these queries.
Pass the dimensions explicitly when you want a single stream:
async function readExistingOrder(log: IEventLog, orderId: string) { const legacyTail = await log.getTailSequenceNumber(undefined, 'Default', 'Default', orderId); const legacyEvents = await log.getForEventSourceIdAndEventTypes(orderId, [LegacyOrderNoteRecorded], 'Default', orderId);
return { legacyTail, legacyEvents };}Against kernel 18.4.1 an unnarrowed tail read reports the tail of the whole sequence; a legacy-scoped read reports only that stream, and reports EventSequenceNumber.unset when that stream holds no events. getNextSequenceNumber() already maps unset to EventSequenceNumber.first; compare against EventSequenceNumber.unset yourself when you read a scoped tail directly. The event source type argument getForEventSourceIdAndEventTypes() accepts narrows the read from kernel 18.5.0 onward. Before that the query carried no such field on the wire, so the argument was accepted and silently discarded — a caller that narrowed by it got every source type back with no way to tell — and against an older kernel it still does nothing. getFromSequenceNumber() and hasEventsFor() carry no route dimensions at all and are unchanged.
If your application stores a checkpoint taken with an earlier client, re-read it against the dimensions you intend: an unnarrowed tail is at least as high as the legacy-scoped tail it replaces.
Verify the selected stream
Section titled “Verify the selected stream”Read the appended events and verify context.eventSourceType, context.eventStreamType, context.eventStreamId, and context.subject. Reads, reactors, and reducers preserve the kernel’s metadata, including occurrence time, correlation identifier, causation, tags, identity, hash, and observation state. The added context properties are optional so existing consumer-created contexts remain valid.