Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ A hosted run does not replace local verification. The hosted CI workflow also su

### Upstream declaration errors

The core, host adapters, MongoDB, testing, proxy generator, and ESLint plugin consumer files use `skipLibCheck: false`. Chronicle and Drizzle consumer files use `skipLibCheck: true` **only** for third-party declaration errors; the script first runs their NodeNext compilation with `skipLibCheck: false`, prints the upstream diagnostics, and rejects errors in Arc declarations or consumer code. The pinned `@cratis/chronicle@6.35.0` and `@cratis/chronicle.contracts@19.26.2` declarations no longer require an exception; any Chronicle declaration error fails the guard. `drizzle-orm@0.45.3` has `gel-core/columns/date-duration.d.ts(1,35)` TS2307 (missing `gel`) and `pg-core/query-builders/query.d.ts(23,22)` TS2420 (`PgRelationalQuery` lacks `getSQL`), among other internal declaration errors. Fix these in their owning packages before removing the temporary integration exception.
The core, host adapters, MongoDB, testing, proxy generator, and ESLint plugin consumer files use `skipLibCheck: false`. Chronicle and Drizzle consumer files use `skipLibCheck: true` **only** for third-party declaration errors; the script first runs their NodeNext compilation with `skipLibCheck: false`, prints the upstream diagnostics, and rejects errors in Arc declarations or consumer code. The pinned `@cratis/chronicle@6.49.0` and `@cratis/chronicle.contracts@19.26.2` declarations no longer require an exception; any Chronicle declaration error fails the guard. `drizzle-orm@0.45.3` has `gel-core/columns/date-duration.d.ts(1,35)` TS2307 (missing `gel`) and `pg-core/query-builders/query.d.ts(23,22)` TS2420 (`PgRelationalQuery` lacks `getSQL`), among other internal declaration errors. Fix these in their owning packages before removing the temporary integration exception.

## Conventions

Expand Down
2 changes: 1 addition & 1 deletion Documentation/chronicle/add-event-sourcing.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ cd ../my-arc-app
Install it together with the Chronicle SDK and RxJS:

```bash
npm install ../arc-packages/arc.chronicle.tgz @cratis/chronicle@~6.35.0 rxjs@^7.8.2
npm install ../arc-packages/arc.chronicle.tgz @cratis/chronicle@~6.49.0 rxjs@^7.8.2
```

| Package | What it gives you |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ You may `return order.commit()` to make the commit visible. Do **not** also retu

The aggregate belongs to the command's key: the `@key()` field, `getKey()`, or `getEventSourceId()`. A command without a key fails with the exception `A command key is required for Order` before `handle()` runs.

Loading uses the same route the command's returned events use: the current tenant's namespace, the command's `@eventSourceType`, `@eventStreamType`, and its stream ID from `getEventStreamId()` or `@eventStreamId`. Arc reads the events of the types the aggregate handles, in order, and replays them. See [Event metadata](../commands/event-metadata.md).
Loading uses the same route the command's returned events use: the current tenant's namespace, the command's `@eventSourceType`, `@eventStreamType`, and its stream ID from `getEventStreamId()` or `@eventStreamId`. Arc reads the events of the types the aggregate handles, in order, and replays them. See [Event metadata](../commands/event-metadata.md). An aggregate can instead declare an [event source definition](../commands/event-source-definitions.md), which guards and rehydrates only that source and stream.

Arc loads each aggregate type once per command. A second parameter of the same type receives the same instance. For a second aggregate **type** on the same key, bind another `commandAggregate(Type)`. There is no way to load an aggregate for a different ID; the key decides.

Expand Down
3 changes: 3 additions & 0 deletions Documentation/chronicle/commands/event-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ An event records more than its payload. It also records which entity it belongs
| Caused by | The signed-in principal, or Chronicle's system identity for an anonymous caller | None |
| Causation | An `Arc.Command` entry with the command name and its values; see [Causation and auditing](causation.md) | None |

To select a registered Chronicle event source definition instead of free-form strings, see [Event source definitions](event-source-definitions.md).

Routing decorators come from `@cratis/arc.chronicle` and apply to every event the command returns. A value set on an `eventForEventSourceId` entry wins over the command's default for that entry only.

## Set command-wide defaults
Expand Down Expand Up @@ -90,4 +92,5 @@ export class Onboarding {

- [Returning events](index.md)
- [Resolving the event source ID](../resolving-event-source-id.md)
- [Event source definitions](event-source-definitions.md)
- [Concurrency](concurrency.md), where the same routing decorators opt into tail checks
89 changes: 89 additions & 0 deletions Documentation/chronicle/commands/event-source-definitions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: Event source definitions
description: Route a command's events, and an aggregate's, through a Chronicle event source definition and stream, and know what Arc validates at startup and what Chronicle enforces on append.
---

A string such as `@eventSourceType('Account')` names where an event goes, but nothing checks that the name exists or that the stream belongs to it. A Chronicle event source definition is that check: a registered class that declares a source and its streams. A command selects one with `@eventSourceDefinition`, and each event it returns records the definition it was appended through.

Source and stream are routing. You declare them on the command or the aggregate, never on an event type.

This feature needs `@cratis/chronicle` 6.49.0 or later. Older SDKs keep working for string routing, and a command that selects a definition fails with a message naming the required version.

## Declare a definition and select it

Declare the definition with the SDK's decorators, then select it on the command.

```typescript
import { field } from '@cratis/fundamentals';
import { ConcurrencyDimensions, eventSource, eventStream } from '@cratis/chronicle';
import { command, key } from '@cratis/arc.core';
import { eventSourceDefinition } from '@cratis/arc.chronicle';

@eventSource()
@eventStream('Transactions', { concurrency: ConcurrencyDimensions.eventStreamType | ConcurrencyDimensions.eventStreamId })
export class Account {}

@eventSourceDefinition(Account, 'Transactions')
@command()
export class Deposit {
@field(String) @key() id = '';
handle(): FundsDeposited { return new FundsDeposited(); }
}
```

`FundsDeposited` is an `@eventType()` class. The event is appended with source `Account` and stream type `Transactions`. Chronicle records the definition on the event, and a reactor or projection reads it from `EventContext.eventSource`.

The first argument is the class, its registered name, or a function returning the class. Use the function form, `@eventSourceDefinition(() => Account, 'Transactions')`, when the definition's module imports the command and the plain class would be read before it is defined.

Referencing the class registers it with Chronicle, so you do not add it to discovery separately. A name can only be resolved against definitions that are registered, so a name Arc cannot find fails at startup.

## What fails at startup

Arc checks every command that selects a definition when the application is built, without a connection, and refuses to build when:

- the class is not decorated with `@eventSource()`;
- the stream is not one the definition declares;
- a name matches no registered definition; or
- `@eventSourceType` or `@eventStreamType` on the same command contradicts the definition. Repeating the definition's own name is allowed.

An aggregate, which Arc only meets when a command uses it, is checked the first time it loads.

## Concurrency comes from the definition

When a command selects a definition and sets no concurrency flags, Arc passes no scope and Chronicle derives one from the dimensions the definition or stream declares. Explicit flags win: `@eventStreamId('2026-05', { concurrency: true })` builds the same explicit scope as without a definition, using the definition's source name, and Chronicle then derives nothing.

Chronicle's client refuses events for one event source ID that would need different automatic scopes within a single batch. Arc does not work around that; the command fails and appends nothing. Pass an explicit scope with `eventsWithConcurrencyScopes`, or return the events in separate commands.

## Override one event

An event entry that names its own `eventSource` and `eventStream`, or its own raw `eventSourceType` or `eventStreamType`, takes over source and stream for that event as one unit. The command's definition is neither merged into it nor used to rewrite it.

```typescript
import { eventForEventSourceId } from '@cratis/arc.chronicle';

handle() {
return eventForEventSourceId({ eventSourceId: this.id, event: new FundsPosted(),
eventSource: Ledger, eventStream: 'Postings' });
}
```

A raw `eventStreamType` on an entry wins over the command's definition the same way, and the entry is appended without a definition. The stream ID stays a separate default from `getEventStreamId()`.

## Aggregates

Declare the definition on the aggregate class to guard and rehydrate only that source and stream.

```typescript
@eventSourceDefinition(Account, 'Transactions')
export class Wallet extends AggregateRoot {
constructor() { super(); this.on(FundsDeposited, () => {}); }
}
```

Loading reads the tail and the events for the declared source and stream only, the concurrency scope carries the same source and stream, and every event the aggregate applies records the definition. A command and its aggregate may each declare a definition when they agree; two that name different sources or streams fail instead of one silently winning.

## Related

- [Event metadata](event-metadata.md)
- [Concurrency](concurrency.md)
- [Defining an aggregate root](../aggregates/defining-an-aggregate-root.md)
2 changes: 2 additions & 0 deletions Documentation/chronicle/commands/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
href: index.md
- name: Event metadata
href: event-metadata.md
- name: Event source definitions
href: event-source-definitions.md
- name: Subject
href: subject.md
- name: Concurrency
Expand Down
1 change: 1 addition & 0 deletions Documentation/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ From `@cratis/arc.chronicle` (experimental):
| `@eventSourceType('Type', { concurrency? })` | command class | Default event source type for returned events | Command event metadata |
| `@eventStreamType('Type', { concurrency? })` | command class | Default event stream type | Command event metadata |
| `@eventStreamId('id', { concurrency? })` | command class | Default event stream ID | Command event metadata |
| `@eventSourceDefinition(Source, 'Stream'?)` | command or aggregate class | Route events through a Chronicle event source definition and stream | [Event source definitions](chronicle/commands/event-source-definitions.md) |
| `@eventSubject('subject')` | command class | Default compliance subject | Command event metadata |
| `@notAudited()` | command field | Keeps the value out of the causation chain | `[NotAudited]` |

Expand Down
5 changes: 3 additions & 2 deletions Documentation/reference/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,11 @@ Evidence paths are relative to the repository root. Spec folders follow `for_<Su
| Reactor replay exclusion | Bounded (SDK 6.9.0+) | The SDK replays reactors by default and supports class- or handler-level `@onceOnly()` and alternate `@replay()` handlers. Arc executes returned commands under those SDK rules; type-checked `ARCCHR0006` warns on returned commands without a replay decision. Failed-partition recovery can re-deliver effects even with `@onceOnly()`. See [Reactors](../chronicle/reactors/index.md). | SDK decorators; `Source/CodeAnalysis/for_rules/when_linting_artifacts/with_reactor_replay_decisions.ts` checks the lint rule; `Source/Chronicle/Integration/LiveArtifacts.ts` and `bash Source/Chronicle/run-integration.sh` exercise a once-only reactor returning a command on normal delivery, not replay exclusion |
| [Chronicle code analysis](../chronicle/code-analysis.md) | Bounded | `ARCCHR0003` checks reactor store fields initialized from `this.client`/`this.runtime` (ownership is not proven); type-checked `ARCCHR0006` flags returned commands from live handlers without a replay decision; `ARCCHR0007` flags direct default-log appends from command `handle`/`provide`, including injected Chronicle services; `ARCCHR0009` checks unmasked secret-looking fields; type-checked `ARCCHR0010` flags Guid values beside direct decorated events on keyless commands. Other .NET diagnostics are inapplicable, checked at runtime, or require review. | `Source/CodeAnalysis/for_rules/when_linting_artifacts/with_chronicle_rules.ts`, `Source/CodeAnalysis/for_rules/when_linting_artifacts/with_reactor_replay_decisions.ts` |
| Transactions and units of work | Experimental | Chronicle stages returned and aggregate-applied events from nested commands in one tenant, correlation, and event store, and sends one `appendMany` after the outer command succeeds. It participates in Arc command-operation failure handling; it is not a transaction across stores or immediate SDK appends. | `Source/Chronicle/for_ChronicleUnitOfWork`, `Source/Chronicle/for_ChronicleCommandScope` |
| Event source definitions | Experimental | `@eventSourceDefinition` on a command or aggregate routes events through a registered Chronicle definition and stream: startup validation, per-event source and stream replacement as a unit, definition-derived concurrency when no explicit scope or flags are set, and aggregate rehydration limited to the declared source and stream. Needs `@cratis/chronicle` 6.49.0 or later; string routing works with older SDKs. Verified against Chronicle kernel 19.30.0 and SDK 6.49.0. Screenplay grammar and generation for definitions are not part of this package. | `Source/Chronicle/for_ChronicleResponseHandler/when_routing_through_an_event_source_definition`, `Source/Chronicle/for_withChronicle/when_validating_event_source_definitions`, `Source/Chronicle/for_AggregateRoot/when_declaring_an_event_source_definition`, `ARC_CHRONICLE_TEST_SUITE=event-source-routing bash Source/Chronicle/run-integration.sh` |

- **MongoDB.** Also [joined observation](../mongodb/joined-observe.md), a [scoped watcher](../mongodb/change-stream-watcher.md), [GeoJSON geometry](../mongodb/geospatial.md), bounded transient read retries, MongoDB driver metrics for Arc-owned clients, and `Cratis:MongoDB:{Server,Database}` configuration binding. No durable watcher checkpoint; nonresumable stream failures terminate subscriptions. .NET's process-wide watcher, general-purpose resilience interceptors, and comprehensive metrics for supplied clients are not implemented.
- **SQL with Drizzle.** `bash Source/Drizzle/run-integration.sh` exercises live PostgreSQL and MySQL for existing SQL reads and command lookup, not observation. In-process observation is checked using SQLite (`sql.js`), gated race and burst specs, and SSE/GET through Express, Fastify, and Hono. Command read models load by a single column-level `.primaryKey()` with `@field` in the tenant scope; models without `@field` on their column-level key still serve queries but cannot be injected into commands. Tables without a column-level primary key are rejected at registration. Custom columns bind typed keys, plain columns primitives. Command read models are tested against SQLite, live MySQL 8.4, and live PostgreSQL 16 with node-postgres. PostgreSQL coverage includes typed GUID and concept keys, tenant isolation, and missing required and optional rows.
- **Chronicle.** It also resolves Chronicle read models by command key and in validators, batches nested returned events, and executes Arc commands returned from reactors through the SDK reactor result hook. SDK 6.35.0 loads in native Node ESM. Keyed aggregates and returned reactor commands are experimental.
- **Chronicle.** It also resolves Chronicle read models by command key and in validators, batches nested returned events, and executes Arc commands returned from reactors through the SDK reactor result hook. SDK 6.49.0 loads in native Node ESM. Keyed aggregates and returned reactor commands are experimental.
- **Chronicle compliance.** Mark projected read-model properties `@pii()` to encrypt them at rest; event-only marking does not protect the materialized field. Chronicle kernel reads already release values (including command injection), and Arc skips releasing those instances twice. For protected Chronicle models decoded into the exact read-model class by `MongoCollection`, Arc releases at the query edge, including snapshots, pages, and observable emissions. Raw `MongoReadModels` documents typed with `readModel` are released with the request's tenant and each document's subject: `subjectFor(document)` when given, otherwise the stored `__subject`, otherwise a string or numeric `_id`. It must match any stored `__subject` and the model's `@subject()` or `id`. Kernel bookkeeping fields are stripped. The path fails closed on undeclared fields, non-JSON BSON values (including `Guid` fields stored as `Binary`), per-property `__subjects`, a subject or tenant mismatch, typed documents nested in another shape, and MongoDB projections. Unreleased instances of a protected class nested in another returned shape, such as a joined `select` result, fail the query. Specs check snapshots, pages, and observable SSE through Express, Fastify, and Hono against a Chronicle substitute; the kernel integration reads a real materialized document through `MongoReadModels`. Untyped raw documents, typed documents of models not registered with Chronicle, codec-selected derived subtypes, `MongoDBWatcher.changes()` payloads, documents hidden behind `toJSON()`, getters, or private fields, and DTOs, mapped objects, or copies are served as stored and need explicit `readModels.release` on the tenant store. Nested, array-item, class-level PII and `@encrypted()` security metadata are detected. A directly read protected model needs `@subject()` or `id` matching the event subject to release; one without either is served only when it holds no protected value, and fails otherwise. The kernel integration checks raw MongoDB ciphertext, Chronicle delivery, and Arc query-edge release for a direct MongoDB read.

## Testing
Expand Down Expand Up @@ -183,7 +184,7 @@ The integration pages describe behavior. This section records the checks behind

### Chronicle checks

The specs run with the published Chronicle TypeScript SDK, `@cratis/chronicle` 6.35.0, and `@cratis/fundamentals` 7.22.0; both load in native Node ESM with NodeNext resolution. The live suite was last verified with SDK 6.10.0; a 6.14.0 attempt could not start the local kernel because its MongoDB connection was refused. The ordinary `yarn test` specs use typed substitutes or the SDK's in-memory `ReadModelScenario` and never start a kernel. An opt-in suite, `bash Source/Chronicle/run-integration.sh`, runs [`Source/Chronicle/Integration/live.test.mjs`](https://github.com/Cratis/Arc.TypeScript/blob/main/Source/Chronicle/Integration/live.test.mjs) against a real development kernel and checks:
The specs run with the published Chronicle TypeScript SDK, `@cratis/chronicle` 6.49.0, and `@cratis/fundamentals` 7.22.0; both load in native Node ESM with NodeNext resolution. The live suite was last verified with SDK 6.10.0; a 6.14.0 attempt could not start the local kernel because its MongoDB connection was refused. The ordinary `yarn test` specs use typed substitutes or the SDK's in-memory `ReadModelScenario` and never start a kernel. An opt-in suite, `bash Source/Chronicle/run-integration.sh`, runs [`Source/Chronicle/Integration/live.test.mjs`](https://github.com/Cratis/Arc.TypeScript/blob/main/Source/Chronicle/Integration/live.test.mjs) against a real development kernel and checks:

- returned-event batches, readback, and tenant isolation;
- a reactor that returns an Arc command, executed in the triggering event's tenant;
Expand Down
Loading