@nimbus-cqrs/eventsourcingdb 2.4.0 → 3.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +82 -46
  2. package/dist/index.d.ts +8 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +8 -0
  5. package/dist/index.js.map +1 -0
  6. package/{esm → dist}/lib/client.d.ts +28 -17
  7. package/{esm → dist}/lib/client.d.ts.map +1 -1
  8. package/{esm → dist}/lib/client.js +25 -19
  9. package/dist/lib/client.js.map +1 -0
  10. package/dist/lib/consumerRunner.d.ts +62 -0
  11. package/dist/lib/consumerRunner.d.ts.map +1 -0
  12. package/dist/lib/consumerRunner.js +386 -0
  13. package/dist/lib/consumerRunner.js.map +1 -0
  14. package/dist/lib/eventConsumer.d.ts +94 -0
  15. package/dist/lib/eventConsumer.d.ts.map +1 -0
  16. package/dist/lib/eventConsumer.js +2 -0
  17. package/dist/lib/eventConsumer.js.map +1 -0
  18. package/{esm → dist}/lib/eventMapping.d.ts +1 -1
  19. package/dist/lib/eventMapping.d.ts.map +1 -0
  20. package/{esm → dist}/lib/eventMapping.js +2 -1
  21. package/dist/lib/eventMapping.js.map +1 -0
  22. package/dist/lib/eventObserver.d.ts +158 -0
  23. package/dist/lib/eventObserver.d.ts.map +1 -0
  24. package/dist/lib/eventObserver.js +339 -0
  25. package/dist/lib/eventObserver.js.map +1 -0
  26. package/dist/lib/observerRegistry.d.ts +39 -0
  27. package/dist/lib/observerRegistry.d.ts.map +1 -0
  28. package/dist/lib/observerRegistry.js +56 -0
  29. package/dist/lib/observerRegistry.js.map +1 -0
  30. package/{esm → dist}/lib/readEvents.d.ts.map +1 -1
  31. package/{esm → dist}/lib/readEvents.js +3 -2
  32. package/dist/lib/readEvents.js.map +1 -0
  33. package/{esm → dist}/lib/tracing.d.ts +3 -0
  34. package/dist/lib/tracing.d.ts.map +1 -0
  35. package/{esm → dist}/lib/tracing.js +5 -1
  36. package/dist/lib/tracing.js.map +1 -0
  37. package/dist/lib/writeEvents.d.ts +29 -0
  38. package/dist/lib/writeEvents.d.ts.map +1 -0
  39. package/dist/lib/writeEvents.js +47 -0
  40. package/dist/lib/writeEvents.js.map +1 -0
  41. package/package.json +32 -13
  42. package/src/index.ts +7 -0
  43. package/src/lib/client.ts +166 -0
  44. package/src/lib/consumerRunner.ts +592 -0
  45. package/src/lib/eventConsumer.ts +95 -0
  46. package/src/lib/eventMapping.ts +115 -0
  47. package/src/lib/eventObserver.ts +620 -0
  48. package/src/lib/observerRegistry.ts +69 -0
  49. package/src/lib/readEvents.ts +28 -0
  50. package/src/lib/tracing.ts +295 -0
  51. package/src/lib/writeEvents.ts +72 -0
  52. package/esm/_dnt.polyfills.d.ts +0 -7
  53. package/esm/_dnt.polyfills.d.ts.map +0 -1
  54. package/esm/_dnt.polyfills.js +0 -1
  55. package/esm/_dnt.shims.d.ts +0 -2
  56. package/esm/_dnt.shims.d.ts.map +0 -1
  57. package/esm/_dnt.shims.js +0 -57
  58. package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.d.ts +0 -700
  59. package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.d.ts.map +0 -1
  60. package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.js +0 -903
  61. package/esm/deps/jsr.io/@std/ulid/1.0.0/_util.d.ts +0 -14
  62. package/esm/deps/jsr.io/@std/ulid/1.0.0/_util.d.ts.map +0 -1
  63. package/esm/deps/jsr.io/@std/ulid/1.0.0/_util.js +0 -65
  64. package/esm/deps/jsr.io/@std/ulid/1.0.0/decode_time.d.ts +0 -20
  65. package/esm/deps/jsr.io/@std/ulid/1.0.0/decode_time.d.ts.map +0 -1
  66. package/esm/deps/jsr.io/@std/ulid/1.0.0/decode_time.js +0 -43
  67. package/esm/deps/jsr.io/@std/ulid/1.0.0/mod.d.ts +0 -44
  68. package/esm/deps/jsr.io/@std/ulid/1.0.0/mod.d.ts.map +0 -1
  69. package/esm/deps/jsr.io/@std/ulid/1.0.0/mod.js +0 -47
  70. package/esm/deps/jsr.io/@std/ulid/1.0.0/monotonic_ulid.d.ts +0 -44
  71. package/esm/deps/jsr.io/@std/ulid/1.0.0/monotonic_ulid.d.ts.map +0 -1
  72. package/esm/deps/jsr.io/@std/ulid/1.0.0/monotonic_ulid.js +0 -51
  73. package/esm/deps/jsr.io/@std/ulid/1.0.0/ulid.d.ts +0 -31
  74. package/esm/deps/jsr.io/@std/ulid/1.0.0/ulid.d.ts.map +0 -1
  75. package/esm/deps/jsr.io/@std/ulid/1.0.0/ulid.js +0 -37
  76. package/esm/index.d.ts +0 -7
  77. package/esm/index.d.ts.map +0 -1
  78. package/esm/index.js +0 -6
  79. package/esm/lib/eventMapping.d.ts.map +0 -1
  80. package/esm/lib/eventObserver.d.ts +0 -133
  81. package/esm/lib/eventObserver.d.ts.map +0 -1
  82. package/esm/lib/eventObserver.js +0 -291
  83. package/esm/lib/tracing.d.ts.map +0 -1
  84. package/esm/lib/writeEvents.d.ts +0 -11
  85. package/esm/lib/writeEvents.d.ts.map +0 -1
  86. package/esm/lib/writeEvents.js +0 -31
  87. package/esm/package.json +0 -3
  88. /package/{esm → dist}/lib/readEvents.d.ts +0 -0
package/README.md CHANGED
@@ -6,7 +6,6 @@
6
6
  # Nimbus EventSourcingDB
7
7
 
8
8
  Integration between Nimbus and [EventSourcingDB](https://www.eventsourcingdb.io/). The package wraps the official `eventsourcingdb` client with:
9
-
10
9
  - a **singleton setup** that pings the server, verifies the API token and registers long-running observers in one call,
11
10
  - typed **`writeEvents` / `readEvents`** helpers that translate between Nimbus events and EventSourcingDB events while preserving correlation IDs, data schemas and W3C trace context (`traceparent` / `tracestate`),
12
11
  - resilient **event observers** with exponential-backoff retries, jitter, position tracking across reconnects and OpenTelemetry span linking back to the original writer.
@@ -18,17 +17,29 @@ Also refer to the [EventSourcingDB documentation](https://docs.eventsourcingdb.i
18
17
  ## Install
19
18
 
20
19
  ```bash
21
- # Deno
22
- deno add npm:@nimbus-cqrs/eventsourcingdb
23
-
24
- # NPM
20
+ # npm
25
21
  npm install @nimbus-cqrs/eventsourcingdb
26
22
 
23
+ # pnpm
24
+ pnpm add @nimbus-cqrs/eventsourcingdb
25
+
27
26
  # Bun
28
27
  bun add @nimbus-cqrs/eventsourcingdb
28
+
29
+ # Deno
30
+ deno add npm:@nimbus-cqrs/eventsourcingdb
29
31
  ```
30
32
 
31
- `eventsourcingdb` is a peer dependency — install it (or use one of the runtimes that resolves it via `npm:`/`jsr:` specifiers).
33
+ `eventsourcingdb` (version 2 or later) is a peer dependency — install it alongside this package. The SDK talks to both the v2 API of EventSourcingDB 1.3+ and the v1 API of older versions and picks the right one at runtime.
34
+
35
+ ## Upgrading from `eventsourcingdb` 1.x
36
+
37
+ If you upgrade an existing application from the 1.x SDK, see the [SDK migration guide](https://github.com/thenativeweb/eventsourcingdb-client-javascript#upgrading-from-version-1). The points that matter for Nimbus users:
38
+ - **Server errors** are `DbApiError` instances with a `code`. Replace checks like `error.message.includes("409")` with `error instanceof DbApiError && error.code === ErrorCode.preconditionViolated`.
39
+ - **Failed writes** whose outcome is unknown (stalled connection, abort after sending) throw an `OutcomeUnknownError` whose `cause` is the original error.
40
+ - **Observers** no longer end quietly when the server closes the stream. Nimbus treats this as a connection failure and reconnects according to `connectionRetryOptions`.
41
+ - **Proxies** in front of EventSourcingDB 1.3+ must allow the HTTP method `QUERY`, which the SDK uses for all reads.
42
+ - **`writeEvents`** now returns the written events instead of `void`.
32
43
 
33
44
  # Examples
34
45
 
@@ -41,23 +52,23 @@ The snippets below use a tiny `Todo` domain to walk through the package — even
41
52
  A typical wiring at application startup: configure the client, register the observers you want to keep running, then write events from your command handlers.
42
53
 
43
54
  ```typescript
44
- import { createEvent } from "@nimbus-cqrs/core";
55
+ import { createEvent } from '@nimbus-cqrs/core';
45
56
  import {
46
57
  eventSourcingDBEventToNimbusEvent,
47
58
  setupEventSourcingDBClient,
48
59
  writeEvents,
49
- } from "@nimbus-cqrs/eventsourcingdb";
60
+ } from '@nimbus-cqrs/eventsourcingdb';
50
61
 
51
62
  await setupEventSourcingDBClient({
52
- url: new URL(process.env.ESDB_URL ?? ""),
53
- apiToken: process.env.ESDB_API_TOKEN ?? "",
63
+ url: new URL(process.env.ESDB_URL ?? ''),
64
+ apiToken: process.env.ESDB_API_TOKEN ?? '',
54
65
  eventObservers: [
55
66
  {
56
- subject: "/todos",
67
+ subject: '/todos',
57
68
  recursive: true,
58
69
  eventHandler: async (event) => {
59
70
  const nimbusEvent = eventSourcingDBEventToNimbusEvent(event);
60
- console.log("reacting to", nimbusEvent.type, nimbusEvent.data);
71
+ console.log('reacting to', nimbusEvent.type, nimbusEvent.data);
61
72
  },
62
73
  },
63
74
  ],
@@ -65,10 +76,10 @@ await setupEventSourcingDBClient({
65
76
 
66
77
  await writeEvents([
67
78
  createEvent({
68
- type: "com.example.todo.added",
69
- source: "https://app.example.com",
70
- subject: "/todos/todo-1",
71
- data: { id: "todo-1", title: "Write the README" },
79
+ type: 'com.example.todo.added',
80
+ source: 'https://app.example.com',
81
+ subject: '/todos/todo-1',
82
+ data: { id: 'todo-1', title: 'Write the README' },
72
83
  }),
73
84
  ]);
74
85
  ```
@@ -81,11 +92,13 @@ await writeEvents([
81
92
  import {
82
93
  getEventSourcingDBClient,
83
94
  setupEventSourcingDBClient,
84
- } from "@nimbus-cqrs/eventsourcingdb";
95
+ } from '@nimbus-cqrs/eventsourcingdb';
85
96
 
86
97
  await setupEventSourcingDBClient({
87
- url: new URL(process.env.ESDB_URL ?? ""),
88
- apiToken: process.env.ESDB_API_TOKEN ?? "",
98
+ url: new URL(process.env.ESDB_URL ?? ''),
99
+ apiToken: process.env.ESDB_API_TOKEN ?? '',
100
+ // Optional: fail a request once nothing arrives for 10 s (default 30 s).
101
+ stallTimeout: 10_000,
89
102
  });
90
103
 
91
104
  // Anywhere else:
@@ -94,36 +107,60 @@ const client = getEventSourcingDBClient();
94
107
 
95
108
  The `eventObservers` array is optional; passing observers here is just a convenience that calls `initEventObserver` for each entry once the client is ready.
96
109
 
110
+ `stallTimeout` is optional as well. A request that receives nothing from the server for that long throws a `ConnectionStalledError`. Every event, row or heartbeat that arrives resets the timer, so healthy long-running streams are never cut off.
111
+
97
112
  ## writeEvents
98
113
 
99
114
  `writeEvents` takes an array of Nimbus events and persists them to EventSourcingDB. Before writing it wraps each event payload with Nimbus metadata (`payload` + `nimbusMeta`) so the correlation ID and optional `dataschema` survive a round-trip, and it injects the active OpenTelemetry context as `traceparent` / `tracestate` so distributed traces stitch together end-to-end.
100
115
 
101
116
  ```typescript
102
- import { createEvent } from "@nimbus-cqrs/core";
103
- import { writeEvents } from "@nimbus-cqrs/eventsourcingdb";
117
+ import { createEvent } from '@nimbus-cqrs/core';
118
+ import { writeEvents } from '@nimbus-cqrs/eventsourcingdb';
104
119
 
105
120
  await writeEvents([
106
121
  createEvent({
107
- type: "com.example.todo.added",
108
- source: "https://app.example.com",
109
- subject: "/todos/todo-1",
110
- data: { id: "todo-1", title: "Write the README" },
122
+ type: 'com.example.todo.added',
123
+ source: 'https://app.example.com',
124
+ subject: '/todos/todo-1',
125
+ data: { id: 'todo-1', title: 'Write the README' },
111
126
  }),
112
127
  ]);
113
128
  ```
114
129
 
115
- The second argument accepts EventSourcingDB [preconditions](https://docs.eventsourcingdb.io/getting-started/writing-events/#using-preconditions) — useful for optimistic concurrency, e.g. only appending an event when no event of a given type already exists for a subject:
130
+ `writeEvents` returns the written EventSourcingDB events including the server-assigned fields (`id`, `time`, `hash`, ...). The `id` of the last written event is the new revision of the subject. This is useful for subsequent writes to the same subject or to hand it back to the client.
131
+
132
+ The second argument accepts EventSourcingDB [preconditions](https://docs.eventsourcingdb.io/getting-started/writing-events/#using-preconditions) which are useful for optimistic concurrency, e.g. only appending an event when the subject has no events yet:
116
133
 
117
134
  ```typescript
118
- await writeEvents(
135
+ import { isSubjectPristine } from 'eventsourcingdb';
136
+
137
+ const writtenEvents = await writeEvents(
119
138
  [todoAddedEvent],
120
- [
121
- {
122
- type: "isSubjectPristine",
123
- payload: { subject: "/todos/todo-1" },
124
- },
125
- ],
139
+ [isSubjectPristine('/todos/todo-1')],
126
140
  );
141
+
142
+ const revision = writtenEvents[writtenEvents.length - 1].id;
143
+ ```
144
+
145
+ A violated precondition surfaces as a `DbApiError` with the code `ErrorCode.preconditionViolated`. A write whose outcome is unknown (e.g. the connection stalled after the request was sent) throws an `OutcomeUnknownError`. Better replay the subject's stream before writing again. An optional `AbortSignal` can be passed as the third argument.
146
+
147
+ ```typescript
148
+ import { DbApiError, ErrorCode, isSubjectOnEventId } from 'eventsourcingdb';
149
+
150
+ try {
151
+ await writeEvents(
152
+ [todoRenamedEvent],
153
+ [isSubjectOnEventId('/todos/todo-1', expectedRevision)],
154
+ );
155
+ } catch (error) {
156
+ if (
157
+ error instanceof DbApiError &&
158
+ error.code === ErrorCode.preconditionViolated
159
+ ) {
160
+ // Somebody else modified the todo in the meantime.
161
+ }
162
+ throw error;
163
+ }
127
164
  ```
128
165
 
129
166
  ## readEvents
@@ -134,9 +171,9 @@ await writeEvents(
134
171
  import {
135
172
  eventSourcingDBEventToNimbusEvent,
136
173
  readEvents,
137
- } from "@nimbus-cqrs/eventsourcingdb";
174
+ } from '@nimbus-cqrs/eventsourcingdb';
138
175
 
139
- for await (const event of readEvents("/todos/todo-1", { recursive: false })) {
176
+ for await (const event of readEvents('/todos/todo-1', { recursive: false })) {
140
177
  const nimbusEvent = eventSourcingDBEventToNimbusEvent(event);
141
178
  // ...rebuild your aggregate, replay state, etc.
142
179
  }
@@ -147,8 +184,7 @@ Pass an `AbortSignal` as the third argument if you need to cancel a long-running
147
184
  ## Event observers
148
185
 
149
186
  An `EventObserver` is a long-running consumer attached to a subject. `initEventObserver` (or the `eventObservers` array on `setupEventSourcingDBClient`) starts it in the background and keeps it alive. Connection and handler failures are retried separately with exponential backoff plus jitter (defaults: 3 retries, 3000 ms initial delay for each):
150
-
151
- - **Connection failures** reconnect the observe stream (`connectionRetryOptions`; deprecated alias: `retryOptions`).
187
+ - **Connection failures** reconnect the observe stream (`connectionRetryOptions`; deprecated alias: `retryOptions`). Observing never ends on its own. A stream closed by the server (e.g. during a restart) or a stalled connection counts as a connection failure, so size the retries for the longest outage an observer should survive.
152
188
  - **Handler failures** are retried in-place without reconnecting (`handlerRetryOptions`). After handler retries are exhausted the event is skipped (optional `onHandlerError`) and observation continues.
153
189
 
154
190
  On every handled or skipped event the observer advances its lower bound so a reconnection resumes from exactly where it left off — no replays, no gaps.
@@ -156,19 +192,19 @@ On every handled or skipped event the observer advances its lower bound so a rec
156
192
  Each event handler runs inside an OpenTelemetry span. If the source event carries a `traceparent`, that span is linked back to the writer's trace, giving you end-to-end visibility from the command that produced the event to every subscriber that reacted to it.
157
193
 
158
194
  ```typescript
159
- import { getLogger } from "@nimbus-cqrs/core";
160
- import { initEventObserver } from "@nimbus-cqrs/eventsourcingdb";
195
+ import { getLogger } from '@nimbus-cqrs/core';
196
+ import { initEventObserver } from '@nimbus-cqrs/eventsourcingdb';
161
197
 
162
198
  initEventObserver({
163
- subject: "/todos",
199
+ subject: '/todos',
164
200
  recursive: true,
165
201
  fromLatestEvent: {
166
- subject: "/todos",
167
- type: "com.example.todo.added",
168
- ifEventIsMissing: "read-everything",
202
+ subject: '/todos',
203
+ type: 'com.example.todo.added',
204
+ ifEventIsMissing: 'read-everything',
169
205
  },
170
206
  eventHandler: async (event) => {
171
- if (event.type === "com.example.todo.added") {
207
+ if (event.type === 'com.example.todo.added') {
172
208
  // ...update a read model, send a notification, ...
173
209
  }
174
210
  },
@@ -182,7 +218,7 @@ initEventObserver({
182
218
  },
183
219
  onHandlerError: (error, event) => {
184
220
  getLogger().error({
185
- category: "EventObserver",
221
+ category: 'EventObserver',
186
222
  message: `Skipping event after handler retries: ${event.id}`,
187
223
  error,
188
224
  });
@@ -215,7 +251,7 @@ import {
215
251
  eventSourcingDBEventToNimbusEvent,
216
252
  isEventData,
217
253
  nimbusEventToEventSourcingDBEventCandidate,
218
- } from "@nimbus-cqrs/eventsourcingdb";
254
+ } from '@nimbus-cqrs/eventsourcingdb';
219
255
  ```
220
256
 
221
257
  - `nimbusEventToEventSourcingDBEventCandidate(event)` — manual conversion, e.g. when batching with the raw client.
@@ -0,0 +1,8 @@
1
+ export * from './lib/client.ts';
2
+ export * from './lib/eventConsumer.ts';
3
+ export * from './lib/eventMapping.ts';
4
+ export * from './lib/eventObserver.ts';
5
+ export * from './lib/observerRegistry.ts';
6
+ export * from './lib/readEvents.ts';
7
+ export * from './lib/writeEvents.ts';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./lib/client.js";
2
+ export * from "./lib/eventConsumer.js";
3
+ export * from "./lib/eventMapping.js";
4
+ export * from "./lib/eventObserver.js";
5
+ export * from "./lib/observerRegistry.js";
6
+ export * from "./lib/readEvents.js";
7
+ export * from "./lib/writeEvents.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { Client } from 'eventsourcingdb';
2
- import { type EventObserver } from './eventObserver.js';
2
+ import { type EventObserver } from './eventObserver.ts';
3
3
  /**
4
4
  * Configuration options for setting up the EventSourcingDB client.
5
5
  */
@@ -12,6 +12,14 @@ export type SetupEventSourcingDBClientInput = {
12
12
  * The API token for authenticating with EventSourcingDB.
13
13
  */
14
14
  apiToken: string;
15
+ /**
16
+ * The stall timeout in milliseconds. A request fails with a
17
+ * `ConnectionStalledError` once nothing arrives from the server for
18
+ * this long. Every item received (events, rows, heartbeats) resets the
19
+ * timer, so long-running streams are never cut off while they are
20
+ * healthy. Defaults to 30000ms (the SDK default).
21
+ */
22
+ stallTimeout?: number;
15
23
  /**
16
24
  * An optional array of event observers to observe events.
17
25
  */
@@ -24,41 +32,44 @@ export type SetupEventSourcingDBClientInput = {
24
32
  * the server, and validates the provided API token. It should be called once at
25
33
  * application startup before using {@link getEventSourcingDBClient}.
26
34
  *
27
- * Optionally, you can provide event observers that will start observing events
28
- * in the background after the client is initialized.
35
+ * Optionally, you can provide event observers. They are started one after
36
+ * another once the client is initialized. Each one loads its consumers'
37
+ * checkpoints before the function resolves, so a checkpoint store that
38
+ * cannot be reached fails the setup.
29
39
  *
30
40
  * @param {SetupEventSourcingDBClientInput} options - The configuration options
31
41
  * @param {URL} options.url - The URL of the EventSourcingDB server
32
42
  * @param {string} options.apiToken - The API token for authentication
43
+ * @param {number} [options.stallTimeout] - Optional stall timeout in milliseconds (defaults to 30000)
33
44
  * @param {EventObserver[]} [options.eventObservers] - Optional array of event observers
34
45
  *
35
46
  * @throws {GenericException} If the connection to EventSourcingDB fails
36
47
  * @throws {GenericException} If the API token is invalid
48
+ * @throws {GenericException} If an event observer cannot be started
37
49
  *
38
50
  * @example
39
51
  * ```ts
52
+ * import { InMemoryCheckpointStore } from '@nimbus-cqrs/core';
40
53
  * import { setupEventSourcingDBClient } from '@nimbus-cqrs/eventsourcingdb';
41
- * import type { Event } from 'eventsourcingdb';
42
54
  *
43
55
  * await setupEventSourcingDBClient({
44
56
  * url: new URL(process.env.ESDB_URL ?? ''),
45
57
  * apiToken: process.env.ESDB_API_TOKEN ?? '',
46
58
  * eventObservers: [
47
59
  * {
60
+ * name: 'users',
48
61
  * subject: '/users',
49
62
  * recursive: true,
50
- * lowerBound: {
51
- * id: 'last-processed-event-id',
52
- * type: 'exclusive',
53
- * },
54
- * fromLatestEvent: {
55
- * subject: '/users',
56
- * type: 'io.nimbus.users.invited',
57
- * ifEventIsMissing: 'read-everything',
58
- * },
59
- * eventHandler: async (event: Event) => {
60
- * console.log('Received event:', event);
61
- * },
63
+ * checkpointStore: new InMemoryCheckpointStore(),
64
+ * consumers: [
65
+ * {
66
+ * name: 'users.projection',
67
+ * handles: (event) => event.type.startsWith('io.nimbus.users.'),
68
+ * handle: async (event) => {
69
+ * console.log('Received event:', event);
70
+ * },
71
+ * },
72
+ * ],
62
73
  * connectionRetryOptions: {
63
74
  * maxRetries: 3,
64
75
  * initialRetryDelayMs: 3000,
@@ -72,7 +83,7 @@ export type SetupEventSourcingDBClientInput = {
72
83
  * });
73
84
  * ```
74
85
  */
75
- export declare const setupEventSourcingDBClient: ({ url, apiToken, eventObservers }: SetupEventSourcingDBClientInput) => Promise<void>;
86
+ export declare const setupEventSourcingDBClient: ({ url, apiToken, stallTimeout, eventObservers, }: SetupEventSourcingDBClientInput) => Promise<void>;
76
87
  /**
77
88
  * Get the EventSourcingDB client instance.
78
89
  *
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,oBAAoB,CAAC;AAI3E;;GAEG;AACH,MAAM,MAAM,+BAA+B,GAAG;IAC1C;;OAEG;IACH,GAAG,EAAE,GAAG,CAAC;IACT;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,0BAA0B,GACnC,mCAAmC,+BAA+B,KACnE,OAAO,CAAC,IAAI,CA0Cd,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB,QAAO,MAQ3C,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,oBAAoB,CAAC;AAI3E;;GAEG;AACH,MAAM,MAAM,+BAA+B,GAAG;IAC1C;;OAEG;IACH,GAAG,EAAE,GAAG,CAAC;IACT;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,eAAO,MAAM,0BAA0B,GAAU,kDAK9C,+BAA+B,KAAG,OAAO,CAAC,IAAI,CA2ChD,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB,QAAO,MAQ3C,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { GenericException, getLogger } from '@nimbus-cqrs/core';
2
2
  import { Client } from 'eventsourcingdb';
3
- import { initEventObserver } from './eventObserver.js';
3
+ import { initEventObserver } from "./eventObserver.js";
4
4
  let eventSourcingDBClient = null;
5
5
  /**
6
6
  * Initialize and configure the EventSourcingDB client.
@@ -9,41 +9,44 @@ let eventSourcingDBClient = null;
9
9
  * the server, and validates the provided API token. It should be called once at
10
10
  * application startup before using {@link getEventSourcingDBClient}.
11
11
  *
12
- * Optionally, you can provide event observers that will start observing events
13
- * in the background after the client is initialized.
12
+ * Optionally, you can provide event observers. They are started one after
13
+ * another once the client is initialized. Each one loads its consumers'
14
+ * checkpoints before the function resolves, so a checkpoint store that
15
+ * cannot be reached fails the setup.
14
16
  *
15
17
  * @param {SetupEventSourcingDBClientInput} options - The configuration options
16
18
  * @param {URL} options.url - The URL of the EventSourcingDB server
17
19
  * @param {string} options.apiToken - The API token for authentication
20
+ * @param {number} [options.stallTimeout] - Optional stall timeout in milliseconds (defaults to 30000)
18
21
  * @param {EventObserver[]} [options.eventObservers] - Optional array of event observers
19
22
  *
20
23
  * @throws {GenericException} If the connection to EventSourcingDB fails
21
24
  * @throws {GenericException} If the API token is invalid
25
+ * @throws {GenericException} If an event observer cannot be started
22
26
  *
23
27
  * @example
24
28
  * ```ts
29
+ * import { InMemoryCheckpointStore } from '@nimbus-cqrs/core';
25
30
  * import { setupEventSourcingDBClient } from '@nimbus-cqrs/eventsourcingdb';
26
- * import type { Event } from 'eventsourcingdb';
27
31
  *
28
32
  * await setupEventSourcingDBClient({
29
33
  * url: new URL(process.env.ESDB_URL ?? ''),
30
34
  * apiToken: process.env.ESDB_API_TOKEN ?? '',
31
35
  * eventObservers: [
32
36
  * {
37
+ * name: 'users',
33
38
  * subject: '/users',
34
39
  * recursive: true,
35
- * lowerBound: {
36
- * id: 'last-processed-event-id',
37
- * type: 'exclusive',
38
- * },
39
- * fromLatestEvent: {
40
- * subject: '/users',
41
- * type: 'io.nimbus.users.invited',
42
- * ifEventIsMissing: 'read-everything',
43
- * },
44
- * eventHandler: async (event: Event) => {
45
- * console.log('Received event:', event);
46
- * },
40
+ * checkpointStore: new InMemoryCheckpointStore(),
41
+ * consumers: [
42
+ * {
43
+ * name: 'users.projection',
44
+ * handles: (event) => event.type.startsWith('io.nimbus.users.'),
45
+ * handle: async (event) => {
46
+ * console.log('Received event:', event);
47
+ * },
48
+ * },
49
+ * ],
47
50
  * connectionRetryOptions: {
48
51
  * maxRetries: 3,
49
52
  * initialRetryDelayMs: 3000,
@@ -57,8 +60,8 @@ let eventSourcingDBClient = null;
57
60
  * });
58
61
  * ```
59
62
  */
60
- export const setupEventSourcingDBClient = async ({ url, apiToken, eventObservers }) => {
61
- eventSourcingDBClient = new Client(url, apiToken);
63
+ export const setupEventSourcingDBClient = async ({ url, apiToken, stallTimeout, eventObservers, }) => {
64
+ eventSourcingDBClient = new Client(url, apiToken, stallTimeout !== undefined ? { stallTimeout } : undefined);
62
65
  try {
63
66
  await eventSourcingDBClient.ping();
64
67
  }
@@ -86,8 +89,10 @@ export const setupEventSourcingDBClient = async ({ url, apiToken, eventObservers
86
89
  message: 'EventSourcingDB client initialized successfully',
87
90
  });
88
91
  if (eventObservers?.length) {
92
+ // Sequential on purpose: observers register in declaration order
93
+ // and the first one that cannot start fails the setup.
89
94
  for (const eventObserver of eventObservers) {
90
- initEventObserver(eventObserver);
95
+ await initEventObserver(eventObserver); // NOSONAR
91
96
  }
92
97
  }
93
98
  };
@@ -114,3 +119,4 @@ export const getEventSourcingDBClient = () => {
114
119
  }
115
120
  return eventSourcingDBClient;
116
121
  };
122
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAsB,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE3E,IAAI,qBAAqB,GAAkB,IAAI,CAAC;AA4BhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,EAAE,EAC7C,GAAG,EACH,QAAQ,EACR,YAAY,EACZ,cAAc,GACgB,EAAiB,EAAE;IACjD,qBAAqB,GAAG,IAAI,MAAM,CAC9B,GAAG,EACH,QAAQ,EACR,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,CAC5D,CAAC;IAEF,IAAI,CAAC;QACD,MAAM,qBAAqB,CAAC,IAAI,EAAE,CAAC;IACvC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,SAAS,EAAE,CAAC,QAAQ,CAAC;YACjB,QAAQ,EAAE,QAAQ;YAClB,OAAO,EAAE,sCAAsC;YAC/C,KAAK,EAAE,KAAc;SACxB,CAAC,CAAC;QACH,MAAM,IAAI,gBAAgB,CAAC,sCAAsC,CAAC,CAAC;IACvE,CAAC;IAED,IAAI,CAAC;QACD,MAAM,qBAAqB,CAAC,cAAc,EAAE,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,SAAS,EAAE,CAAC,KAAK,CAAC;YACd,QAAQ,EAAE,QAAQ;YAClB,OAAO,EAAE,iDAAiD;YAC1D,KAAK,EAAE,KAAc;SACxB,CAAC,CAAC;QACH,MAAM,IAAI,gBAAgB,CACtB,iDAAiD,CACpD,CAAC;IACN,CAAC;IAED,SAAS,EAAE,CAAC,IAAI,CAAC;QACb,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,iDAAiD;KAC7D,CAAC,CAAC;IAEH,IAAI,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,iEAAiE;QACjE,uDAAuD;QACvD,KAAK,MAAM,aAAa,IAAI,cAAc,EAAE,CAAC;YACzC,MAAM,iBAAiB,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU;QACtD,CAAC;IACL,CAAC;AACL,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAW,EAAE;IACjD,IAAI,CAAC,qBAAqB,EAAE,CAAC;QACzB,MAAM,IAAI,gBAAgB,CACtB,6FAA6F,CAChG,CAAC;IACN,CAAC;IAED,OAAO,qBAAqB,CAAC;AACjC,CAAC,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { type CheckpointStore } from '@nimbus-cqrs/core';
2
+ import type { Event as EventSourcingDBEvent } from 'eventsourcingdb';
3
+ import type { ConsumerStatus, EventConsumer } from './eventConsumer.ts';
4
+ /**
5
+ * Where an observer starts reading the stream.
6
+ */
7
+ export type Bound = {
8
+ id: string;
9
+ type: 'inclusive' | 'exclusive';
10
+ };
11
+ /**
12
+ * Runs the consumers of one observer and keeps their checkpoints.
13
+ */
14
+ export type ConsumerRunner = {
15
+ /**
16
+ * The lowest position of all consumers, from where the observer has
17
+ * to resume so that every consumer sees what it still misses.
18
+ */
19
+ readonly lowerBound: Bound;
20
+ /**
21
+ * Offers one event to every consumer in declaration order. Rejects
22
+ * when at least one consumer failed so the observer retries in place.
23
+ */
24
+ readonly handle: (raw: EventSourcingDBEvent) => Promise<void>;
25
+ /**
26
+ * Halts the consumers that failed on the most recent attempt and
27
+ * persists their failure. Called once handler retries are exhausted.
28
+ */
29
+ readonly onExhausted: (error: Error, raw: EventSourcingDBEvent) => Promise<void>;
30
+ /**
31
+ * The current state of every consumer.
32
+ */
33
+ readonly status: () => ConsumerStatus[];
34
+ };
35
+ export type CreateConsumerRunnerOptions = {
36
+ observer: string;
37
+ consumers: readonly EventConsumer[];
38
+ checkpointStore: CheckpointStore;
39
+ head: string | null;
40
+ };
41
+ /**
42
+ * Wires the consumers of one observer onto a shared stream.
43
+ *
44
+ * Consumers run sequentially in declaration order. Every non-halted
45
+ * consumer advances its position on every event — handled or not — which
46
+ * keeps the position contiguous and lets a restart resume exactly where it
47
+ * stopped.
48
+ *
49
+ * A consumer that throws holds its position while its siblings advance, as
50
+ * does every consumer that depends on it. The handler then rejects so the
51
+ * observer retries in place. On the next retry the advanced consumers skip
52
+ * themselves, so only the failing ones run again. Once retries are
53
+ * exhausted those consumers are halted. They are skipped for the rest of
54
+ * the process lifetime and their checkpoint stays at the last event they
55
+ * finished, so a restart resumes at the event they could not handle.
56
+ *
57
+ * Consumers with `startFrom: 'head'` ignore their stored position and
58
+ * start at the head of the subject on every start. Their checkpoint is
59
+ * still written like any other, so operators see where they are.
60
+ */
61
+ export declare const createConsumerRunner: ({ observer, consumers, checkpointStore, head, }: CreateConsumerRunnerOptions) => Promise<ConsumerRunner>;
62
+ //# sourceMappingURL=consumerRunner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumerRunner.d.ts","sourceRoot":"","sources":["../../src/lib/consumerRunner.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,KAAK,eAAe,EAKvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,KAAK,IAAI,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxE;;GAEG;AACH,MAAM,MAAM,KAAK,GAAG;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC;CACnC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IACzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,CAClB,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,oBAAoB,KACxB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,cAAc,EAAE,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,SAAS,aAAa,EAAE,CAAC;IACpC,eAAe,EAAE,eAAe,CAAC;IACjC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AA8NF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,GAAU,iDAKxC,2BAA2B,KAAG,OAAO,CAAC,cAAc,CAmStD,CAAC"}