git:20261012.8a4854a to git:20261013.ff56fde
20 added, 20 removed. Audit A to A.
---
name: cratis-chronicle-client-typescript
description: Talk to a Chronicle server from a Node.js or TypeScript application with @cratis/chronicle - reflect-metadata and decorator compiler settings, ChronicleClient and connection strings, @eventType classes, eventLog.append, reactors and reducers dispatched by camelCase method name, model-bound and declarative projections, variants (@variantOf/@entersOn/@globalFor or .variantOf()/.entersOn()) for an entity with mutually exclusive lifecycle shapes, glob-based artifact discovery, and the lazy connect and keepalive lifecycle. Use when a Node application appends to or observes a Chronicle event store. Do not use for the .NET, Kotlin, or Elixir clients, and do not use for Arc React frontends or generated Arc proxies.
license: MIT
---
# The Chronicle client for TypeScript
`@cratis/chronicle` is a **standalone Node.js client SDK** that speaks gRPC to a
Chronicle server. It is not a browser library and it is not the Arc TypeScript
proxy layer — an Arc frontend calls generated command and query proxies over
HTTP and never sees this package.
## Common pitfalls
| Pitfall | Why it bites |
| --- | --- |
| Naming a reactor method after the handler's purpose | Use the camelCase event class name or explicit `@handles(EventType)` |
| `instanceof` on the event inside a handler | It is a `JSON.parse`d plain object, not your class |
| Event fields with no `@field` types under `tsx` | No type metadata is emitted; unresolved members make registration fail |
| Two `@eventType()` classes sharing a class name | The id defaults to the class name and they collide |
| Deploying compiled `dist/*.js` with default discovery | Compiled-JS entries do not scan; import artifact modules explicitly |
| Omitting a side-effect import of an artifact module | Its decorator never runs, so it is never registered |
| Forgetting `import 'reflect-metadata'` at the entry point | Decorators do not work at runtime |
| Forgetting to install `@cratis/fundamentals` | It is a peer dependency and is not installed for you |
| Importing `ChronicleConnectionString` | There is no `./connection` export; pass a string |
| Carrying a pre-6.0 stream forward | Pass all three route dimensions explicitly; omitted routes now resolve to `Default`/`All`/`Default` |
| A connection string with no credentials | Silently falls back to the development client credentials |
| Shipping the default TLS behavior | `skipTlsValidation` defaults to `true` |
| Constructor-injecting without an `artifactActivator` | By default the client constructs the artifact itself |
- | Side-effect reactor without `@onceOnly()` | Reactors run again on kernel replay by default since 6.9.0 |
+ | Side-effect reactor without `@onceOnly()` | Reactors run again on kernel replay by default |
| Reading `result.sequenceNumber` as a number | It is a `bigint` behind `.value` |
| A `@globalFor` mapping targeting a property one variant lacks | `GlobalHandlerPropertyNotOnVariant` when the group is built, not a silently skipped mapping |
| Expecting a `@globalFor`-only class to appear as a projection | It is never registered on its own; it is merged into its variants |
## Verified product sources
- Verified against `Cratis/Chronicle.TypeScript` tag **`v6.51.0`**, reading
+ Source: `Cratis/Chronicle.TypeScript`, reading
`Source/**` and `Documentation/getting-started.md`, `testing.md`,
- `migrate-append-routing.md` and `reactors.md`. Source paths below refer to that tag.
+ `migrate-append-routing.md` and `reactors.md`.
- | Package | Version | Where |
- | --- | --- | --- |
- | `@cratis/chronicle` | `6.51.0` | npm; `Source/package.json` carries a `1.0.0` placeholder |
- | `@cratis/chronicle.contracts` | `19.31.10` | Client's pinned dependency, not the latest contracts package |
- | `@cratis/fundamentals` | `^7.22.4` | **peer dependency** — you install it |
+ | Package | Where |
+ | --- | --- |
+ | `@cratis/chronicle` | npm; `Source/package.json` carries a placeholder version |
+ | `@cratis/chronicle.contracts` | Client's pinned dependency, not the latest contracts package |
+ | `@cratis/fundamentals` | **peer dependency** — you install it |
- > **Do not read the version out of the repository.** `Source/package.json` says
- > `1.0.0`; `publish-version.js` overwrites it at publish time. Take the version
+ > **Do not read the version out of the repository.** `Source/package.json` carries
+ > a placeholder; `publish-version.js` overwrites it at publish time. Take the version
> from npm.
- **Upgrade the kernel first.** The 5.x→6.0.0 migration changes omitted append
- routes and requires compatibility checks (kernel 18.4.1+); registered event
- sources introduced in 6.49.0 require kernel 19.30+. Reactors replay by default
- since 6.9.0, so review side-effect handlers when upgrading.
+ **Upgrade the kernel first.** The client requires a kernel that supports its
+ contract compatibility check and, for registered event sources, named event source
+ definitions. Reactors replay by default, so review side-effect handlers when
+ upgrading.
## Project setup
```shell
yarn add @cratis/chronicle @cratis/fundamentals reflect-metadata
```
`@cratis/fundamentals` is a peer dependency, so it does not arrive on its own.
`reflect-metadata` is now a regular dependency. Import it **once, at the entry
point**, before any decorated class is loaded:
```typescript
import 'reflect-metadata';
```
The package is **ESM-only** and needs Node.js **22.19+** for its `undici`
dependency (the client itself has no `engines` field). Standard TC39 decorators
are supported (TypeScript 5.2+) and are the documentation default:
```jsonc
// tsconfig.json — standard decorator mode
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "ESNext.Decorators"]
}
}
```
Legacy mode with `experimentalDecorators: true` and
`emitDecoratorMetadata: true` still works. Standard decorators and esbuild/tsx
emit no type metadata: declare every serialized property with `@field(Type)`
from `@cratis/fundamentals`; default values are only an inference fallback.
## Connecting
```typescript
import 'reflect-metadata';
import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle';
const client = new ChronicleClient(ChronicleOptions.development());
try {
const store = await client.getEventStore('<EventStoreName>');
// ... use store ...
} finally {
client.dispose();
}
```
`IChronicleClient` (`Source/IChronicleClient.ts`) exposes `readonly options`,
`getEventStore(name, namespace?): Promise<IEventStore>`,
`getEventStores(): Promise<EventStoreName[]>`, and `dispose(): void`.
`ChronicleClient`'s constructor takes exactly one argument —
`constructor(readonly options: ChronicleOptions)` (`Source/ChronicleClient.ts`).
**Connecting is implicit and lazy.** There is no `connect()`. The constructor
starts a health-check watchdog and kicks off artifact discovery; the actual
connect happens on the first `getEventStore`/`getEventStores` through an internal
`ensureConnected()`. `getEventStore` also *creates* the store if it does not
exist — it calls `ensureEventStore`.
`dispose()` is **synchronous**, not
`Symbol.dispose` or `Symbol.asyncDispose`. After disposal every call throws.
### Options
`ChronicleOptions` has a **private constructor** (`Source/ChronicleOptions.ts`);
use `ChronicleOptions.fromConnectionString(connectionString, options?)` or
`ChronicleOptions.development(options?)`.
The optional bag accepts `telemetry`, `logger`, `clientArtifactsProvider`,
`discoveryPatterns`, `defaultSinkTypeId`, `reactorResultHandler`,
`artifactActivator`, and `readModelNamingPolicy`. `defaultSinkTypeId` defaults
to `WellKnownSinks.MongoDB`.
> `programIdentifier`, `softwareVersion`, and `softwareCommit` exist on the type
> but **neither public factory forwards them**, so they cannot be set.
> Do not write guidance that configures them.
### The connection string
Schemes `chronicle://` and `chronicle+srv://`, default port `35000`
(`Source/connection/ChronicleConnectionString.ts`). Query parameters: `apiKey`,
`disableTls`, `skipTlsValidation`, `certificatePath`, `certificatePassword`,
`loadBalancer`, `srvNameServer`.
Two facts to carry into production guidance:
- **`skipTlsValidation` defaults to `true`.** TLS is on but the certificate chain
is not validated, because a development kernel serves a self-signed
certificate. Set `?skipTlsValidation=false` against a real server.
- **A connection string with no credentials silently falls back to the
development client credentials** (`Source/connection/ChronicleConnection.ts`).
An anonymous-looking connection string is not anonymous; it is
`chronicle-dev-client`.
> **There is no `./connection` subpath export.** `ChronicleConnectionString`,
> `ChronicleConnectionStringBuilder`, `AuthenticationMode`, and `LoadBalancerMode`
> are unreachable from the public entry points — `Source/index.ts` never
> re-exports them and `Source/package.json`'s `exports` map has no
> `"./connection"` key. **Application code passes a connection *string*.** Do not
> write an example that imports a connection-string type.
The subpath exports that do exist: `.`, `./events`, `./eventSequences`,
`./eventStoreSubscriptions`, `./compliance`, `./reactors`, `./reducers`,
`./seeding`, `./readModels`, `./projections`, `./jobs`, `./webhooks`,
`./externalServices`, `./identities`, `./observation`, `./sinks`, `./schemas`,
`./types`, `./artifacts`, `./identity`, `./auditing`, `./correlation`,
`./transactions`, `./confidentiality`, `./patterns`, `./testing`, and
`./package.json`. Event-source APIs are root exports; there is no
`./eventSources` subpath. `IncompatibleChronicleServer` and
`RejectedChronicleCredentials` are also root exports.
## Event types
```typescript
// Excerpt: replace the domain placeholders with your event and value types.
import { field } from '@cratis/fundamentals';
import { eventType } from '@cratis/chronicle';
@eventType()
export class <EventName> {
@field(<RuntimeType>) <property>: <Type>;
constructor(<property>: <Type>) { this.<property> = <property>; }
}
```
`eventType` supports no args, `id`, `id/generation`, `id/generation/tombstone`,
`generation`, `generation/tombstone`, or `tombstone`
(`Source/events/eventTypeDecorator.ts`); both decorator modes work.
**The event type id defaults to the class name** — `new EventTypeId(id || constructor.name)`
in the decorator. Two classes with the same name in different modules collide
on the wire.
**Use explicit `@field` declarations.** `Source/types/TypeIntrospector.ts` checks
field metadata before inference from tracked properties, default-constructed
instances and legacy metadata. Use `@field(Array, { genericArguments: [Item] })`
for typed arrays; concepts need `static readonly valueType`. A property the
client cannot type makes registration fail — do not rely on constructor-only
properties under standard decorators or tsx.
Tag an event type at declaration with `@tag(...)` / `@tags(...)`.
## Appending
`IEventLog` is an empty extension of `IEventSequence`
(`Source/eventSequences/IEventLog.ts`); the surface is on `IEventSequence`:
```typescript
append(eventSourceId: string, event: object, options?: AppendOptions): Promise<AppendResult>;
appendMany(eventSourceId: string, events: object[], options?: AppendOptions): Promise<AppendResult[]>;
appendMany(events: EventForEventSourceId[], options?: AppendOptions): Promise<AppendResult[]>;
```
```typescript
const result = await store.eventLog.append('<event-source-id>', new <EventName>('<value>'));
if (!result.isSuccess) {
// result.constraintViolations, result.concurrencyViolation, result.errors
}
```
- **The event source id is a plain `string`.** There is no `EventSourceId` value
type at this boundary.
- `AppendResult` (`Source/eventSequences/AppendResult.ts`): `sequenceNumber`,
`constraintViolations` (including the constraint name), `concurrencyViolation?`,
`errors`, `isSuccess`, and `waitForCompletion({ timeoutMs, signal }?)`
(timeout defaults to 5000 ms).
- **`sequenceNumber` wraps a `bigint`.** Read it as `result.sequenceNumber.value`.
`EventContext.sequenceNumber` is a `bigint` too.
- `AppendOptions` (`Source/eventSequences/AppendOptions.ts`): `correlationId`,
`sourceType`, `streamType`, `streamId`, `subject`, `occurred`, `tags`, `namedTags`,
`concurrencyScope`, `concurrencyScopes`, `eventSource`, and `eventStream`.
The reserved legacy `eventSourceId` option does not override the method's id.
- **Single `append()` supports routing and subject options.** Since 6.0.0 an
+ **Single `append()` supports routing and subject options.** An
unspecified route is omitted on the wire; the kernel resolves source `Default`,
stream type `All`, stream id `Default`. Subject precedence is the explicit option,
then the event's `@subject()` property, then its event source id.
To continue a pre-6.0 stream, pass **all three** dimensions explicitly:
`{ sourceType: 'Default', streamType: 'Default', streamId: id }`.
See `Documentation/migrate-append-routing.md` at the verified tag.
Correlation id, identity, and the causation chain are picked up from the ambient
module-level managers (`identityProvider`, `causationManager`,
`correlationIdManager`) — you do not pass them.
Several appends as one unit:
```typescript
const unitOfWork = store.unitOfWorkManager.begin();
await store.eventLog.transactional.append('<id>', new <EventName>('<value>'));
await unitOfWork.commit();
```
`ITransactionalEventSequence` returns `Promise<void>`, not an `AppendResult`;
- results are on the unit of work. Since 6.51.0 transactional appends retain
+ results are on the unit of work. Transactional appends retain
routing, subject, occurrence, tags and concurrency scopes in enrollment order;
conflicting scopes for one source throw `ConflictingConcurrencyScopesInUnitOfWork`
before enrollment.
## Observing
### Handlers use camelCase names or `@handles(EventType)`
The default is the camelCase event class name, **not** parameter-type dispatch.
An unmatched name is simply never called. Reactors and reducers also support
`@handles(EventType)` on a public instance method: one handler per event;
a reactor handler cannot combine it with `@replay`.
- **Reactors replay by default since 6.9.0.** Use class `@onceOnly()` (not inherited)
+ **Reactors replay by default.** Use class `@onceOnly()` (not inherited)
or method `@onceOnly()` for effects that must not run on replay; `@replay(EventType)`
provides a separate replay handler. This never prevents failed-partition
redelivery or proves exactly-once effects.
```typescript
import { onceOnly, reactor } from '@cratis/chronicle/reactors';
import { EventContext, EventForEventSourceId } from '@cratis/chronicle';
@reactor()
export class <ReactorName> {
@onceOnly()
async <eventName>(event: <EventName>, context: EventContext): Promise<EventForEventSourceId> {
return {
eventSourceId: '<other-event-source-id>',
event: new <SideEffectEvent>(context.eventSourceId)
};
}
}
```
A returned event, array, or `EventForEventSourceId` is appended as a side effect;
a failed side-effect append fails the partition.
Reactor arguments are `(event, context, services, delivery)`
(`Source/reactors/ReactorDispatcher.ts`). `ReactorServices` exposes
`{ eventStore, readModels, signal }` for the observation's owning store/namespace.
`ReactorDelivery` supplies an idempotency identity, not an exactly-once guarantee.
> **Event content is parsed JSON, not an instance of your event class.**
> `instanceof` checks and event methods do not work. Read properties only.
`@reducer(id?, eventSequenceId?, readModel?, isActive?)` also registers the supplied
read model type. Reducers receive `(content, currentState, context)` and may be
sync or async.
Filter with `@filterEventsByTag(value)`, `@eventSourceType(...)`, or
`@eventStreamType(...)`. `reactorResultHandler` handles application-owned returns.
### Read models and projections
Read through `store.readModels`; projections are model-bound (decorators) or
fluent (`@projection`). Constraints use `@unique`/`@removeConstraint` or a builder.
Read [references/read-models-and-projections.md](references/read-models-and-projections.md) when reading a read model instance, choosing a projection style, or declaring a constraint.
### Variants — mutually exclusive read models for one entity's lifecycle
One entity with mutually exclusive lifecycle shapes is modeled as variants, with `@variantOf`/`@entersOn`/`@globalFor` or the fluent `.variantOf()`/`.entersOn()`; every variant needs an entering event.
Read [references/variants.md](references/variants.md) when a read model for one entity changes shape over its lifecycle, or when you meet `VariantMustDeclareEntersOnEvent` or `GlobalHandlerPropertyNotOnVariant`.
## Artifact discovery
`ChronicleOptions.discoveryPatterns` defaults to `**/*.ts` and `**/*.tsx` only
for TypeScript entries (`.ts/.tsx/.mts/.cts`), Vitest, or tsx/ts-node/type-stripping
loader arguments. It excludes declaration files, `node_modules`, `dist`, `build`,
`.git`, `.vscode`, and `*.spec/test.ts(x)` (`Source/ChronicleOptions.ts`).
**A compiled-JS entry defaults to `[]`.** Decorators register when their module is
imported, so side-effect imports of artifacts remain load-bearing in production.
Explicit patterns always win; `[]` disables scanning. The client imports matching
files at construction; failed imports name the file. Registration uses a
process-wide static map.
`EventStore.registerArtifacts()` registers event types first, then everything else
in parallel, and re-runs for every cached event store on reconnect.
## Lifecycle
- A health-check watchdog runs `getVersionInfo` every 5 seconds and reconnects on
failure; the interval handle is `unref()`'d so it does not hold the process open.
- Reconnect uses exponential backoff with jitter, capped at 30 seconds, rebuilding
the gRPC channel on every attempt.
- **Keepalive registration is what keeps observers alive.** Losing it triggers a
full reconnect, because the kernel tears down the client's observers otherwise.
- Failed operations are retried once after reconnecting, keyed on gRPC status
codes 4/13/14 plus a substring match over the error text.
The client reports its **installed package version**, read from
`@cratis/chronicle/package.json`, and performs `CheckCompatibility` with
`ProtocolVersion` and the contracts descriptor set before a connection is usable
and before event-sequence calls. `IncompatibleChronicleServer` is a permanent
verdict, not retried (`Source/connection/CompatibilityPreflight.ts`).
Three consecutive credential rejections cause `RejectedChronicleCredentials`.
A failed token request is logged and the call proceeds without a token (allowing
an auth-less kernel); tokens come from the selected server, not the first host.
- Since 6.43.0 built-in spans use `cratis.chronicle.client.*` and scope
+ Built-in spans use `cratis.chronicle.client.*` and scope
`Cratis.Chronicle.Client`; metrics use seconds. Legacy millisecond metrics and
`chronicle.*` attributes are retired except the retained exact-string
- `chronicle.sequence_number`. Historical constants, `ChronicleMetrics`, and
+ `chronicle.sequence_number`. The old constants, `ChronicleMetrics`, and
`telemetry.spanNames` are deprecated; migrate dashboards.
## Artifact activation
There is no built-in container. By default the client constructs artifacts.
For reactor/reducer constructor dependencies supply `artifactActivator` in
`ChronicleOptions`: activation is per delivered batch and can implement
`ActivatedArtifact.run/complete/dispose` around handlers and returned effects.
## Testing without a kernel
Read [references/testing.md](references/testing.md) when specifying appends,
reactors or read models with `EventScenario`, `ReactorScenario`, or
`ReadModelScenario`. Unsupported shapes fail explicitly; they need kernel tests.
## More surface
| Need | Public surface and boundary |
| --- | --- |
| Registered sources/streams | Root `@eventSource`, repeatable `@eventStream`, `ConcurrencyDimensions`, `store.eventSources`; append `eventSource/eventStream`. Kernel 19.30+; see `Source/eventSources/` |
| Named tags | `namedTags` on appends; batches cannot combine them with registered event sources |
| Personal data and retained secrets | `@pii`, `@subject`, `store.pii`; `@encrypted` with `EncryptionScope` from `./confidentiality`; release through `readModels.release*`. Never combine PII/encrypted or put either on `eventSourceId` |
- | Subject migration | Subject uses `@subject()` since 6.41.1; earlier appended events used the source id for protection keys |
+ | Subject migration | Subject uses `@subject()`; events appended before it used the source id for protection keys |
| Operational catalogs | `store.observers` (list/remove), `store.patterns`; mutations need authority |
| Diagnostics/privacy | `ChronicleOptions.telemetry` and per-client `logger`; source ids are omitted by default, raw/HMAC recording is opt-in |
## Verify
- The installed version is the one you intended, taken from npm, and
`@cratis/fundamentals` is installed alongside it.
- `reflect-metadata` is imported exactly once, at the entry point.
- Decorator mode is consistent: standard mode uses explicit `@field` types;
legacy mode uses `experimentalDecorators` with `emitDecoratorMetadata`.
- Every serialized property has a runtime type and a non-empty schema.
- Compiled-JS deployments import artifacts, or configure explicit discovery.
- Every handler uses exact camelCase naming or `@handles(EventType)`, and an
appended event demonstrably reaches it; side-effect replay policy is explicit.
- A production connection string carries credentials and `skipTlsValidation=false`.
- `client.dispose()` runs on shutdown.
- Every variant group has at least one `@entersOn`/`entersOn(...)` per variant,
and every `@globalFor` member exists on every variant it targets.
- Lint, `tsc`, and the test suite are clean against the verified package version.