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 ContractTests/Client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cratis/arc.core-client-contract",
"version": "0.49.0",
"version": "0.50.0",
"private": true,
"type": "module",
"dependencies": {
Expand Down
6 changes: 3 additions & 3 deletions Documentation/chronicle/reactors/command-side-effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Return Arc commands from a Chronicle reactor so they run through va

When a book is added to the catalog, the search index should follow. The indexing command already exists, with its validation and its role check. Instead of calling a service from the reactor and repeating those checks, return the command, and Arc runs it through the same pipeline an HTTP caller would use.

This relies on the SDK's reactor result hook, available in the Chronicle SDK 6.7.0 and later.
This relies on the SDK's reactor result hook.

## Return a command

Expand Down Expand Up @@ -83,9 +83,9 @@ Do not mix commands with events or other values in one array. Arc rejects the mi

A returned command that fails, whether rejected by authorization or validation or by throwing, fails the handler with a message naming the command, the event store, and the namespace. Chronicle marks the observer partition as failed rather than acknowledging a partial side effect. When Chronicle delivers the event again, the handler returns the commands again, including any that succeeded the first time.

On SDK 6.9.0 and later, reactors accept explicit and kernel-initiated replays by default. Mark the class with `@onceOnly()` when all its handlers cause non-replayable effects, or mark individual handlers if only some do. Chronicle skips them during replay; `@replay()` can supply an alternate replay handler. See [Chronicle once-only reactors](/chronicle/reactors/once-only/). Marking a reactor once-only does **not** prevent re-delivery when a failed partition is recovered.
Reactors accept explicit and kernel-initiated replays by default. Mark the class with `@onceOnly()` when all its handlers cause non-replayable effects: Chronicle then never replays the reactor at all, neither a full replay nor a partition replay, so a `@replay()` handler on that class never runs. If only some handlers cause such effects, mark those methods with `@onceOnly()` instead; Chronicle skips only them during a replay, and `@replay()` can supply an alternate replay handler. See [Chronicle once-only reactors](/chronicle/reactors/once-only/). Marking a reactor once-only does **not** prevent re-delivery when a failed partition is recovered.

Make the commands safe to repeat even with `@onceOnly()`. Key them by the triggering event source, check current state in [`provide()` or a read model](../read-models/injecting-into-commands.md), or rely on a Chronicle constraint to reject the duplicate. SDK 6.7.x and 6.8.x do not support replay exclusion.
Make the commands safe to repeat even with `@onceOnly()`. Key them by the triggering event source, check current state in [`provide()` or a read model](../read-models/injecting-into-commands.md), or rely on a Chronicle constraint to reject the duplicate.

No transaction spans the triggering event and the commands. The triggering event is already committed when the reactor runs.

Expand Down
2 changes: 1 addition & 1 deletion Documentation/chronicle/reactors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Returning commands and events together in one array fails the handler. Anything

A handler that throws, or a returned side effect that fails, marks the observer partition for that event source as failed, with the error message. Chronicle's failed-partition handling decides when that event is delivered again. Nothing that already happened is undone, so write handlers that are safe to run twice for the same event.

Since SDK 6.9.0, reactors run on replay by default. For effects such as returned commands, use `@onceOnly()` on the class to skip all handlers during replay, or on individual methods to skip only those handlers. `@replay()` selects a separate handler for a replayed event. See [Chronicle once-only reactors](/chronicle/reactors/once-only/). These markers do not prevent ordinary re-delivery after a failed partition recovers. Keep the effects safe to repeat; SDK 6.7.x and 6.8.x cannot exclude replay.
Reactors run on replay by default. For effects such as returned commands, mark the class with `@onceOnly()` so Chronicle never replays the reactor at all, neither a full replay nor a partition replay; a `@replay()` handler on such a class therefore never runs. Mark individual methods with `@onceOnly()` instead to skip only those handlers during a replay, and use `@replay()` to select a separate handler for a replayed event. See [Chronicle once-only reactors](/chronicle/reactors/once-only/). These markers do not prevent ordinary re-delivery after a failed partition recovers, so keep the effects safe to repeat.

## Topics

Expand Down
28 changes: 15 additions & 13 deletions Documentation/chronicle/reactors/scoped-activation.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ builder.withChronicle({
});
```

Scoped activation requires `@cratis/chronicle` 6.17.0 or later. Older versions the peer range still allows either ignore the activator or cannot report per-event correlation and scope cleanup failures, so with the option set `withChronicle` fails with `activateArtifactsInScopes requires @cratis/chronicle 6.17.0 or later`. With the option off, older versions keep working.

Registration rejects the option together with `client`, because caller-owned clients are not supported yet.
Scoped activation requires `@cratis/chronicle` 6.17.0 or later, the lowest version `@cratis/arc.chronicle` accepts as a peer. To use a Chronicle client you create yourself, see [Use a caller-owned client](#use-a-caller-owned-client).

With the option set, Chronicle-only artifacts that `discover(...)` found before `withChronicle` was called are registered with Chronicle too. Without it, registration is unchanged.

Expand Down Expand Up @@ -57,27 +55,31 @@ A failed delivery is retried. It is not rolled back: appended events, commands a

## Shutdown

When the application shuts down, Arc stops accepting new deliveries, aborts `currentContext().signal` for running handlers, and waits for them to finish and dispose their scopes before it disposes services. Cancellation is cooperative: a handler that ignores the signal delays shutdown until it settles.
When the application shuts down, Arc stops accepting new deliveries, aborts `currentContext().signal` for running handlers, and waits for them to finish and dispose their scopes. Only then does it dispose services, and an Arc-owned Chronicle client is closed last, so the connection stays open while admitted deliveries finish. Cancellation is cooperative: a handler that ignores the signal delays shutdown until it settles. A delivery that arrives after shutdown has started is rejected without touching services.

## Use the activator directly
## Use a caller-owned client

`chronicleArtifactActivator(server, eventStore)` from `@cratis/arc.chronicle` is the activator this option installs. It returns an SDK `ClientArtifactsActivator`, so you can pass it as `artifactActivator` when you create a Chronicle client yourself. This also requires `@cratis/chronicle` 6.17.0 or later. On 6.16 an event delivery fails instead of running with the wrong correlation; SDKs before 6.16 ignore `artifactActivator` and construct artifacts themselves, and Arc cannot detect that for a client you create.
`chronicleArtifactActivator(server, eventStore)` from `@cratis/arc.chronicle` is the activator the option installs on an Arc-owned connection. For a Chronicle client you create yourself, pass it as `artifactActivator`, together with `reactorCommandResultHandler` as `reactorResultHandler`, and set `activateArtifactsInScopes` with the client:

```typescript title="main.ts (excerpt)"
import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle';
import { chronicleArtifactActivator } from '@cratis/arc.chronicle';
import { chronicleArtifactActivator, reactorCommandResultHandler } from '@cratis/arc.chronicle';

let application: Awaited<ReturnType<typeof builder.build>> | undefined;
const artifactActivator = chronicleArtifactActivator(() => application!.server, 'MyArcApp');
const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', { artifactActivator }));
const server = () => application!.server;
const client = new ChronicleClient(ChronicleOptions.fromConnectionString('chronicle://localhost:35000', {
artifactActivator: chronicleArtifactActivator(server, 'MyArcApp'),
reactorResultHandler: reactorCommandResultHandler(server, 'MyArcApp')
}));

application = await builder.withChronicle({ client, eventStore: 'MyArcApp' }).build();
application = await builder.withChronicle({ client, eventStore: 'MyArcApp', activateArtifactsInScopes: true }).build();
// Start observing with the client only now: the activator needs the built application.
```

Registration verifies only that the client was created with an activator from `chronicleArtifactActivator` for the same event store; passing `reactorCommandResultHandler(...)` as `reactorResultHandler` is your responsibility, and without it returned commands are not executed through Arc. Arc never changes the client's options and never disposes the client. With the client registered this way, Arc checks the reactors' and reducers' registrations when building, as it does for an Arc-owned connection.

The caller owns the ordering:

- Build Arc before the client starts observing. A delivery that arrives earlier finds no application; the delivery fails and Chronicle retries it.
- You register the reactors, reducers and their dependencies yourself, and Arc does not check them when building.
- Returned commands receive the delivery's signal and event store only when you also pass `reactorCommandResultHandler(() => application!.server, 'MyArcApp')` as `reactorResultHandler`.
- The activator joins Arc's shutdown only after its first activation. Before disposing Arc, stop the client, or call `artifactActivator.stop()` and then `await artifactActivator.drain()`, so no delivery is still using services that `application.dispose()` releases.
- Returned commands receive the delivery's signal and event store only when the client also has `reactorCommandResultHandler` as its `reactorResultHandler`.
- Dispose Arc first, then the client: `await application.dispose()` stops the activator and waits for admitted deliveries while the connection is still open, then `client.dispose()` stops observing. Deliveries that arrive in between are rejected.
4 changes: 2 additions & 2 deletions Documentation/chronicle/registration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Each recorded reactor and reducer also gets a scoped service registration in Arc
| `connectionString` | `string` | One of `connectionString` and `client` | Arc creates, connects, and disposes the SDK client |
| `client` | `IChronicleClient` from `@cratis/chronicle` | One of `connectionString` and `client` | You own the client; see [Choose who owns the client](#choose-who-owns-the-client) |
| `completionTimeoutMs` | positive integer, milliseconds | No; no wait by default | After each successful append, wait until Chronicle's observers have processed it before the command answers. See [Choose Chronicle read consistency](../queries/read-consistency.md) |
| `activateArtifactsInScopes` | `boolean` | No; off by default | Preview. Resolve reactors and reducers from Arc's container, one scope per delivery. Arc-owned connections and `@cratis/chronicle` 6.17.0 or later only. See [Scoped activation](reactors/scoped-activation.md) |
| `activateArtifactsInScopes` | `boolean` | No; off by default | Preview. Resolve reactors and reducers from Arc's container, one scope per delivery. With `client`, registration verifies only that the client was created with `chronicleArtifactActivator`; passing `reactorCommandResultHandler` as `reactorResultHandler` is up to you. See [Scoped activation](reactors/scoped-activation.md) |

Registration throws `Chronicle requires eventStore and exactly one of connectionString or client` when the event store is missing, or when neither or both of a connection string and a client are set.

Expand Down Expand Up @@ -67,7 +67,7 @@ Values follow this precedence:
| `{ connectionString, eventStore }` | Arc creates the SDK client with an artifact catalog for this application, and closes it when the application is disposed |
| `{ client, eventStore }` | You pass a caller-owned `IChronicleClient`. Arc never disposes it; your host calls `client.dispose()`. The client must already have an artifact provider that registers the event types, projections, reducers, and reactors you use |

An Arc-owned client is also wired so that [reactors can return Arc commands](reactors/command-side-effects.md). A caller-owned client needs that handler passed to the SDK before it connects; the reactor page shows how.
An Arc-owned client is also wired so that [reactors can return Arc commands](reactors/command-side-effects.md). A caller-owned client needs that handler passed to the SDK before it connects; the reactor page shows how. To use [scoped activation](reactors/scoped-activation.md#use-a-caller-owned-client) with a caller-owned client, also pass `chronicleArtifactActivator` as its `artifactActivator`. Dispose the Arc application before the client, so deliveries still running finish while the connection is open.

## Related

Expand Down
2 changes: 1 addition & 1 deletion Documentation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Arc for TypeScript is a Node.js server implementation of [Arc](/arc/), the Crati
Without it, a Node.js backend for an Arc frontend means writing every route, request parser, validation response, and status code by hand, then keeping all of it in step with the frontend. With it, commands and queries run through one pipeline that owns those concerns, the wire behavior follows Arc on .NET, and the proxy generator writes the typed frontend client from your source.

:::caution[Source preview, no full parity]
No package is published to npm; the manifests are at version 0.49.0 for a source preview. Arc for TypeScript does **not** have full parity with Arc on .NET, and package names and APIs can still change. The [capability reference](reference/capabilities.md) is the single place for status and evidence.
No package is published to npm; the manifests are at version 0.50.0 for a source preview. Arc for TypeScript does **not** have full parity with Arc on .NET, and package names and APIs can still change. The [capability reference](reference/capabilities.md) is the single place for status and evidence.
:::

## What it looks like
Expand Down
4 changes: 2 additions & 2 deletions Documentation/reference/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Packages
description: The packages this repository builds, what each exports, their peer dependencies and Node.js requirements, and how they relate to the published @cratis/arc client.
---

Every package in this repository is at version 0.49.0, the version of the source preview. **None is published to npm.** They ship ES modules only. Clone this repository, run `yarn install` and `yarn build`, and then use the packages in one of two ways:
Every package in this repository is at version 0.50.0, the version of the source preview. **None is published to npm.** They ship ES modules only. Clone this repository, run `yarn install` and `yarn build`, and then use the packages in one of two ways:

- **Inside the clone.** Put your application in a folder under `Samples/`, which the root `workspaces` list includes, and reference the packages with the `workspace:^` protocol, as [`Samples/Tasks/package.json`](https://github.com/Cratis/Arc.TypeScript/blob/main/Samples/Tasks/package.json) does. `workspace:^` resolves only inside this repository's Yarn workspace.
- **In your own project.** Pack each package you need with `yarn workspace <package> pack --out <file>` and install the tarballs with npm. Use `yarn pack`: it rewrites `workspace:^` dependencies to version ranges, and `npm pack` does not. `yarn check:consumers` installs packed packages this way to check NodeNext and Bundler consumers.
Expand All @@ -21,7 +21,7 @@ Every package in this repository is at version 0.49.0, the version of the source
| `@cratis/arc.testing` | `Source/Testing` | `CommandScenario`, `QueryScenario`, `ObservableQueryScenario`, `ArcScenario`, `given`, `shouldHaveRuleFailure` | |
| `@cratis/arc.mongodb` | `Source/MongoDB` | `withMongoDB`, `mongoCollection`, `MongoCollection`, naming policies, `MongoReadModels` | `@cratis/arc.core`, `@cratis/fundamentals`, `mongodb` `^6.21.0` |
| `@cratis/arc.drizzle` | `Source/Drizzle` | `withDrizzle`, `drizzleReadModel`, `drizzleDatabase`, `DrizzleReadModels`, column codecs | `@cratis/arc.core`, `@cratis/fundamentals`, `drizzle-orm` `^0.45.0` |
| `@cratis/arc.chronicle` | `Source/Chronicle` | Experimental: `withChronicle`, `commandAggregate`, `reactorCommandResultHandler`, `executeCommandsAsSystem`, `eventForEventSourceId`, `eventSourceIdResponse`, `eventsWithConcurrencyScopes`, routing decorators, `notAudited`, `ChronicleReadModels`; `@cratis/arc.chronicle/testing` for `ChronicleCommandScenario` and `ChronicleKernelScenario` | `@cratis/arc.core`, `@cratis/arc.testing`, `@cratis/chronicle` `^6.7.0` (tested with 6.10.0), `@cratis/fundamentals`, `zod` |
| `@cratis/arc.chronicle` | `Source/Chronicle` | Experimental: `withChronicle`, `commandAggregate`, `reactorCommandResultHandler`, `executeCommandsAsSystem`, `eventForEventSourceId`, `eventSourceIdResponse`, `eventsWithConcurrencyScopes`, routing decorators, `notAudited`, `ChronicleReadModels`; `@cratis/arc.chronicle/testing` for `ChronicleCommandScenario` and `ChronicleKernelScenario` | `@cratis/arc.core`, `@cratis/arc.testing`, `@cratis/chronicle` `^6.17.0` (tested with 6.19.0), `@cratis/fundamentals`, `zod` |
| `@cratis/cratis` | `Source/Cratis` | Experimental composition, the counterpart of the C# `Cratis` package: `CratisApplication.createBuilder`, `builder.addCratis`; re-exports Arc, Chronicle and testing (`./testing`); no implicit authentication handler | Arc core, Arc Chronicle, Arc testing, Chronicle SDK, Fundamentals, `zod` |

`@cratis/cratis` is experimental, like the Chronicle integration it composes, and is not published to npm yet. Unlike C# `AddCratis`, the TS composition does not install Microsoft identity automatically: for protected routes, explicitly choose an authentication handler (such as `microsoftIdentityPlatform()`) or your own trusted host principal; public routes need neither. It composes the client, not the event-store engine. See [The Cratis package](../chronicle/cratis-package.md).
Expand Down
2 changes: 1 addition & 1 deletion Documentation/testing/chronicle.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ await scenario.dispose();

Here `AccountBalanceReducer` handles `AccountOpened` and produces the `AccountBalance` injected into `CheckAccount`.
The SDK's `ReadModelScenario` folds reducer history (since 6.14.0) and, starting in 6.19.0, evaluates supported flat projections from seeded events on demand for each source.
The main `@cratis/arc.chronicle` entry supports `@cratis/chronicle` 6.7.0 and later. Seeding reducer history through `given.forEventSource(...).events` requires SDK 6.14.0 or later; the scenario loads its testing subpath only when it needs to fold seeded history. A different source has no balance;
The main `@cratis/arc.chronicle` entry supports `@cratis/chronicle` 6.17.0 and later. The scenario loads the SDK's testing subpath only when it needs to fold seeded history. A different source has no balance;
required `commandReadModel(AccountBalance)` rejects it and an optional read model receives `null`. Seeding does not
appear in `result.appendedEvents` or `scenario.appendedEvents`. Later command appends are **not** folded into this
scenario's read models, matching the .NET command scenario's seeded-history lookup. Use a kernel scenario to test
Expand Down
Loading
Loading