@nimbus-cqrs/eventsourcingdb 2.5.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.
- package/README.md +82 -46
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/{esm → dist}/lib/client.d.ts +28 -17
- package/{esm → dist}/lib/client.d.ts.map +1 -1
- package/{esm → dist}/lib/client.js +25 -19
- package/dist/lib/client.js.map +1 -0
- package/dist/lib/consumerRunner.d.ts +62 -0
- package/dist/lib/consumerRunner.d.ts.map +1 -0
- package/dist/lib/consumerRunner.js +386 -0
- package/dist/lib/consumerRunner.js.map +1 -0
- package/dist/lib/eventConsumer.d.ts +94 -0
- package/dist/lib/eventConsumer.d.ts.map +1 -0
- package/dist/lib/eventConsumer.js +2 -0
- package/dist/lib/eventConsumer.js.map +1 -0
- package/{esm → dist}/lib/eventMapping.d.ts +1 -1
- package/dist/lib/eventMapping.d.ts.map +1 -0
- package/{esm → dist}/lib/eventMapping.js +2 -1
- package/dist/lib/eventMapping.js.map +1 -0
- package/dist/lib/eventObserver.d.ts +158 -0
- package/dist/lib/eventObserver.d.ts.map +1 -0
- package/dist/lib/eventObserver.js +339 -0
- package/dist/lib/eventObserver.js.map +1 -0
- package/dist/lib/observerRegistry.d.ts +39 -0
- package/dist/lib/observerRegistry.d.ts.map +1 -0
- package/dist/lib/observerRegistry.js +56 -0
- package/dist/lib/observerRegistry.js.map +1 -0
- package/{esm → dist}/lib/readEvents.d.ts.map +1 -1
- package/{esm → dist}/lib/readEvents.js +3 -2
- package/dist/lib/readEvents.js.map +1 -0
- package/{esm → dist}/lib/tracing.d.ts.map +1 -1
- package/{esm → dist}/lib/tracing.js +2 -1
- package/dist/lib/tracing.js.map +1 -0
- package/dist/lib/writeEvents.d.ts +29 -0
- package/dist/lib/writeEvents.d.ts.map +1 -0
- package/dist/lib/writeEvents.js +47 -0
- package/dist/lib/writeEvents.js.map +1 -0
- package/package.json +32 -13
- package/src/index.ts +7 -0
- package/src/lib/client.ts +166 -0
- package/src/lib/consumerRunner.ts +592 -0
- package/src/lib/eventConsumer.ts +95 -0
- package/src/lib/eventMapping.ts +115 -0
- package/src/lib/eventObserver.ts +620 -0
- package/src/lib/observerRegistry.ts +69 -0
- package/src/lib/readEvents.ts +28 -0
- package/src/lib/tracing.ts +295 -0
- package/src/lib/writeEvents.ts +72 -0
- package/esm/_dnt.polyfills.d.ts +0 -7
- package/esm/_dnt.polyfills.d.ts.map +0 -1
- package/esm/_dnt.polyfills.js +0 -1
- package/esm/_dnt.shims.d.ts +0 -2
- package/esm/_dnt.shims.d.ts.map +0 -1
- package/esm/_dnt.shims.js +0 -57
- package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.d.ts +0 -700
- package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.js +0 -903
- package/esm/deps/jsr.io/@std/ulid/1.0.0/_util.d.ts +0 -14
- package/esm/deps/jsr.io/@std/ulid/1.0.0/_util.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/ulid/1.0.0/_util.js +0 -65
- package/esm/deps/jsr.io/@std/ulid/1.0.0/decode_time.d.ts +0 -20
- package/esm/deps/jsr.io/@std/ulid/1.0.0/decode_time.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/ulid/1.0.0/decode_time.js +0 -43
- package/esm/deps/jsr.io/@std/ulid/1.0.0/mod.d.ts +0 -44
- package/esm/deps/jsr.io/@std/ulid/1.0.0/mod.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/ulid/1.0.0/mod.js +0 -47
- package/esm/deps/jsr.io/@std/ulid/1.0.0/monotonic_ulid.d.ts +0 -44
- package/esm/deps/jsr.io/@std/ulid/1.0.0/monotonic_ulid.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/ulid/1.0.0/monotonic_ulid.js +0 -51
- package/esm/deps/jsr.io/@std/ulid/1.0.0/ulid.d.ts +0 -31
- package/esm/deps/jsr.io/@std/ulid/1.0.0/ulid.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/ulid/1.0.0/ulid.js +0 -37
- package/esm/index.d.ts +0 -7
- package/esm/index.d.ts.map +0 -1
- package/esm/index.js +0 -6
- package/esm/lib/eventMapping.d.ts.map +0 -1
- package/esm/lib/eventObserver.d.ts +0 -133
- package/esm/lib/eventObserver.d.ts.map +0 -1
- package/esm/lib/eventObserver.js +0 -299
- package/esm/lib/writeEvents.d.ts +0 -11
- package/esm/lib/writeEvents.d.ts.map +0 -1
- package/esm/lib/writeEvents.js +0 -28
- package/esm/package.json +0 -3
- /package/{esm → dist}/lib/readEvents.d.ts +0 -0
- /package/{esm → dist}/lib/tracing.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
|
-
#
|
|
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
|
|
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
|
|
55
|
+
import { createEvent } from '@nimbus-cqrs/core';
|
|
45
56
|
import {
|
|
46
57
|
eventSourcingDBEventToNimbusEvent,
|
|
47
58
|
setupEventSourcingDBClient,
|
|
48
59
|
writeEvents,
|
|
49
|
-
} from
|
|
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:
|
|
67
|
+
subject: '/todos',
|
|
57
68
|
recursive: true,
|
|
58
69
|
eventHandler: async (event) => {
|
|
59
70
|
const nimbusEvent = eventSourcingDBEventToNimbusEvent(event);
|
|
60
|
-
console.log(
|
|
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:
|
|
69
|
-
source:
|
|
70
|
-
subject:
|
|
71
|
-
data: { id:
|
|
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
|
|
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
|
|
103
|
-
import { writeEvents } from
|
|
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:
|
|
108
|
-
source:
|
|
109
|
-
subject:
|
|
110
|
-
data: { id:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
174
|
+
} from '@nimbus-cqrs/eventsourcingdb';
|
|
138
175
|
|
|
139
|
-
for await (const event of readEvents(
|
|
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
|
|
160
|
-
import { initEventObserver } from
|
|
195
|
+
import { getLogger } from '@nimbus-cqrs/core';
|
|
196
|
+
import { initEventObserver } from '@nimbus-cqrs/eventsourcingdb';
|
|
161
197
|
|
|
162
198
|
initEventObserver({
|
|
163
|
-
subject:
|
|
199
|
+
subject: '/todos',
|
|
164
200
|
recursive: true,
|
|
165
201
|
fromLatestEvent: {
|
|
166
|
-
subject:
|
|
167
|
-
type:
|
|
168
|
-
ifEventIsMissing:
|
|
202
|
+
subject: '/todos',
|
|
203
|
+
type: 'com.example.todo.added',
|
|
204
|
+
ifEventIsMissing: 'read-everything',
|
|
169
205
|
},
|
|
170
206
|
eventHandler: async (event) => {
|
|
171
|
-
if (event.type ===
|
|
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:
|
|
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
|
|
254
|
+
} from '@nimbus-cqrs/eventsourcingdb';
|
|
219
255
|
```
|
|
220
256
|
|
|
221
257
|
- `nimbusEventToEventSourcingDBEventCandidate(event)` — manual conversion, e.g. when batching with the raw client.
|
package/dist/index.d.ts
ADDED
|
@@ -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.
|
|
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
|
|
28
|
-
*
|
|
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
|
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
|
|
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
|
|
13
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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"}
|