Preserve existing append routes
The move to kernel-owned append routing is a major change for the TypeScript client. Previously, an append with no route options sent source type Default, stream type Default, and the event source identifier as the stream identifier. The client now 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.
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 { eventType, IEventLog } from '@cratis/chronicle';
@eventType()class LegacyOrderNoteRecorded { constructor(readonly note: string) {}}
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.