@nimbus-cqrs/eventsourcingdb 2.1.2 → 2.3.0

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 CHANGED
@@ -7,9 +7,9 @@
7
7
 
8
8
  Integration between Nimbus and [EventSourcingDB](https://www.eventsourcingdb.io/). The package wraps the official `eventsourcingdb` client with:
9
9
 
10
- - a **singleton setup** that pings the server, verifies the API token and registers long-running observers in one call,
11
- - 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
- - resilient **event observers** with exponential-backoff retries, jitter, position tracking across reconnects and OpenTelemetry span linking back to the original writer.
10
+ - a **singleton setup** that pings the server, verifies the API token and registers long-running observers in one call,
11
+ - 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
+ - resilient **event observers** with exponential-backoff retries, jitter, position tracking across reconnects and OpenTelemetry span linking back to the original writer.
13
13
 
14
14
  Refer to the [Nimbus main repository](https://github.com/overlap-dev/Nimbus) or the [Nimbus documentation](https://nimbus.overlap.at) for more information about the Nimbus framework.
15
15
 
@@ -122,7 +122,7 @@ await writeEvents(
122
122
  type: "isSubjectPristine",
123
123
  payload: { subject: "/todos/todo-1" },
124
124
  },
125
- ]
125
+ ],
126
126
  );
127
127
  ```
128
128
 
@@ -146,11 +146,17 @@ Pass an `AbortSignal` as the third argument if you need to cancel a long-running
146
146
 
147
147
  ## Event observers
148
148
 
149
- 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: on connection failures it retries with exponential backoff plus jitter (defaults: 3 retries, 3000 ms initial delay) and on every successful event it advances its lower bound, so a reconnection resumes from exactly where it left off — no replays, no gaps.
149
+ 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`).
152
+ - **Handler failures** are retried in-place without reconnecting (`handlerRetryOptions`). After handler retries are exhausted the event is skipped (optional `onHandlerError`) and observation continues.
153
+
154
+ 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.
150
155
 
151
156
  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.
152
157
 
153
158
  ```typescript
159
+ import { getLogger } from "@nimbus-cqrs/core";
154
160
  import { initEventObserver } from "@nimbus-cqrs/eventsourcingdb";
155
161
 
156
162
  initEventObserver({
@@ -166,10 +172,21 @@ initEventObserver({
166
172
  // ...update a read model, send a notification, ...
167
173
  }
168
174
  },
169
- retryOptions: {
175
+ connectionRetryOptions: {
170
176
  maxRetries: 5,
171
177
  initialRetryDelayMs: 1000,
172
178
  },
179
+ handlerRetryOptions: {
180
+ maxRetries: 3,
181
+ initialRetryDelayMs: 1000,
182
+ },
183
+ onHandlerError: (error, event) => {
184
+ getLogger().error({
185
+ category: "EventObserver",
186
+ message: `Skipping event after handler retries: ${event.id}`,
187
+ error,
188
+ });
189
+ },
173
190
  });
174
191
  ```
175
192
 
@@ -201,9 +218,9 @@ import {
201
218
  } from "@nimbus-cqrs/eventsourcingdb";
202
219
  ```
203
220
 
204
- - `nimbusEventToEventSourcingDBEventCandidate(event)` — manual conversion, e.g. when batching with the raw client.
205
- - `eventSourcingDBEventToNimbusEvent<TEvent>(event)` — typed conversion when reading or observing.
206
- - `isEventData(value)` — type guard to detect the Nimbus envelope on arbitrary stored data.
221
+ - `nimbusEventToEventSourcingDBEventCandidate(event)` — manual conversion, e.g. when batching with the raw client.
222
+ - `eventSourcingDBEventToNimbusEvent<TEvent>(event)` — typed conversion when reading or observing.
223
+ - `isEventData(value)` — type guard to detect the Nimbus envelope on arbitrary stored data.
207
224
 
208
225
  # License
209
226
 
@@ -0,0 +1,7 @@
1
+ declare global {
2
+ interface Error {
3
+ cause?: unknown;
4
+ }
5
+ }
6
+ export {};
7
+ //# sourceMappingURL=_dnt.polyfills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_dnt.polyfills.d.ts","sourceRoot":"","sources":["../src/_dnt.polyfills.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,KAAK;QACb,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CACF;AAED,OAAO,EAAE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
package/esm/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import "./_dnt.polyfills.js";
1
2
  export * from './lib/client.js';
2
3
  export * from './lib/eventMapping.js';
3
4
  export * from './lib/eventObserver.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
package/esm/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import "./_dnt.polyfills.js";
1
2
  export * from './lib/client.js';
2
3
  export * from './lib/eventMapping.js';
3
4
  export * from './lib/eventObserver.js';
@@ -59,7 +59,11 @@ export type SetupEventSourcingDBClientInput = {
59
59
  * eventHandler: async (event: Event) => {
60
60
  * console.log('Received event:', event);
61
61
  * },
62
- * retryOptions: {
62
+ * connectionRetryOptions: {
63
+ * maxRetries: 3,
64
+ * initialRetryDelayMs: 3000,
65
+ * },
66
+ * handlerRetryOptions: {
63
67
  * maxRetries: 3,
64
68
  * initialRetryDelayMs: 3000,
65
69
  * },
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;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;;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"}
package/esm/lib/client.js CHANGED
@@ -44,7 +44,11 @@ let eventSourcingDBClient = null;
44
44
  * eventHandler: async (event: Event) => {
45
45
  * console.log('Received event:', event);
46
46
  * },
47
- * retryOptions: {
47
+ * connectionRetryOptions: {
48
+ * maxRetries: 3,
49
+ * initialRetryDelayMs: 3000,
50
+ * },
51
+ * handlerRetryOptions: {
48
52
  * maxRetries: 3,
49
53
  * initialRetryDelayMs: 3000,
50
54
  * },
@@ -1 +1 @@
1
- {"version":3,"file":"eventMapping.d.ts","sourceRoot":"","sources":["../../src/lib/eventMapping.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,qDAAqD,CAAC;AAE9F,OAAO,KAAK,EACR,KAAK,IAAI,oBAAoB,EAC7B,cAAc,EACjB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,UAAU,EAAE,mBAAmB,CAAC;CACnC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,OAAO,KAAG,IAAI,IAAI,SAOnD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,0CAA0C,GACnD,OAAO,KAAK,EACZ,cAAc,MAAM,EACpB,aAAa,MAAM,KACpB,cAeF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iCAAiC,GAAI,MAAM,SAAS,KAAK,EAClE,sBAAsB,oBAAoB,KAC3C,MAwBF,CAAC"}
1
+ {"version":3,"file":"eventMapping.d.ts","sourceRoot":"","sources":["../../src/lib/eventMapping.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,KAAK,EACb,MAAM,qDAAqD,CAAC;AAE7D,OAAO,KAAK,EACR,KAAK,IAAI,oBAAoB,EAC7B,cAAc,EACjB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,UAAU,EAAE,mBAAmB,CAAC;CACnC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,OAAO,KAAG,IAAI,IAAI,SAOnD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,0CAA0C,GACnD,OAAO,KAAK,EACZ,cAAc,MAAM,EACpB,aAAa,MAAM,KACpB,cAeF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iCAAiC,GAAI,MAAM,SAAS,KAAK,EAClE,sBAAsB,oBAAoB,KAC3C,MAwBF,CAAC"}
@@ -1,4 +1,4 @@
1
- import { createEvent } from '@nimbus-cqrs/core';
1
+ import { createEvent, } from '@nimbus-cqrs/core';
2
2
  import { ulid } from '../deps/jsr.io/@std/ulid/1.0.0/mod.js';
3
3
  /**
4
4
  * Type guard that checks whether the given value conforms to the {@link EventData} structure
@@ -10,8 +10,8 @@ type ObserveFromLatestEvent = {
10
10
  };
11
11
  export type RetryOptions = {
12
12
  /**
13
- * The maximum number of retry attempts before giving up.
14
- * Defaults to 3.
13
+ * The maximum number of retries after the initial attempt
14
+ * before giving up. Defaults to 3 (4 failed attempts total).
15
15
  */
16
16
  maxRetries: number;
17
17
  /**
@@ -55,11 +55,33 @@ export type EventObserver = {
55
55
  */
56
56
  eventHandler: (event: EventSourcingDBEvent) => Promise<void> | void;
57
57
  /**
58
- * Options for retry behavior when the connection fails.
59
- * Uses exponential backoff with jitter between retries.
58
+ * Options for retry behavior when the observe stream / connection fails.
59
+ * Uses exponential backoff with jitter between reconnects.
60
60
  * Defaults to { maxRetries: 3, initialRetryDelayMs: 3000 }.
61
61
  */
62
+ connectionRetryOptions?: RetryOptions;
63
+ /**
64
+ * Options for retry behavior when {@link eventHandler} throws.
65
+ * Handler retries happen in-place without reconnecting the stream.
66
+ * Defaults to { maxRetries: 3, initialRetryDelayMs: 3000 }.
67
+ */
68
+ handlerRetryOptions?: RetryOptions;
69
+ /**
70
+ * @deprecated Use {@link connectionRetryOptions} instead.
71
+ *
72
+ * Options for retry behavior when the observe stream / connection fails.
73
+ * Ignored when {@link connectionRetryOptions} is set.
74
+ */
62
75
  retryOptions?: RetryOptions;
76
+ /**
77
+ * Called when {@link eventHandler} keeps failing after all handler
78
+ * retries are exhausted. The event is then skipped and observation
79
+ * continues. When omitted, a critical log entry is emitted instead.
80
+ *
81
+ * @param error - The last error thrown by the handler.
82
+ * @param event - The event that could not be handled.
83
+ */
84
+ onHandlerError?: (error: Error, event: EventSourcingDBEvent) => Promise<void> | void;
63
85
  };
64
86
  /**
65
87
  * Calculates an exponential backoff delay with jitter for a given
@@ -70,13 +92,39 @@ export type EventObserver = {
70
92
  * exponential scaling.
71
93
  * @param attempt - The zero-based retry attempt number.
72
94
  * @returns The backoff delay in milliseconds.
95
+ *
96
+ * @deprecated Import {@link calculateBackoffDelay} from
97
+ * `@nimbus-cqrs/core` instead. This wrapper keeps the historical
98
+ * 30% jitter default used by EventSourcingDB observers.
73
99
  */
74
100
  export declare const calculateBackoffDelay: (initialDelayMs: number, attempt: number) => number;
101
+ /**
102
+ * Starts observing events for the given {@link EventObserver} with
103
+ * automatic reconnection on stream failure and separate in-place
104
+ * retries for handler failures.
105
+ *
106
+ * Events are consumed from the stream and each one is passed to the
107
+ * observer's event handler inside an OpenTelemetry span. If the event
108
+ * carries a `traceparent`, the span is linked to the original writer's
109
+ * trace for end-to-end distributed tracing.
110
+ *
111
+ * Handler failures are retried without reconnecting. After handler
112
+ * retries are exhausted the event is skipped (lower bound advanced)
113
+ * so observation can continue. When the connection drops, exponential
114
+ * backoff with jitter is applied up to the configured maximum number
115
+ * of connection retries.
116
+ *
117
+ * @param eventObserver - The event observer configuration.
118
+ * @returns A promise that resolves when the observe stream ends or
119
+ * connection retries are exhausted.
120
+ */
121
+ export declare const observeWithRetry: (eventObserver: EventObserver) => Promise<void>;
75
122
  /**
76
123
  * Initializes an event observer by starting the observation loop in
77
124
  * the background (non-blocking). The observer will keep running and
78
- * reconnecting according to its retry options until the stream ends
79
- * or retries are exhausted.
125
+ * reconnecting according to its connection retry options until the
126
+ * stream ends or connection retries are exhausted. Handler failures
127
+ * are retried separately and do not stop the observer.
80
128
  *
81
129
  * @param eventObserver - The event observer configuration.
82
130
  */
@@ -1 +1 @@
1
- {"version":3,"file":"eventObserver.d.ts","sourceRoot":"","sources":["../../src/lib/eventObserver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,IAAI,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAIrE,KAAK,KAAK,GAAG;IACT,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC;CACnC,CAAC;AAEF,KAAK,sBAAsB,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;CAC1D,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACvB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,mBAAmB,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IACxB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB;;;OAGG;IACH,eAAe,CAAC,EAAE,sBAAsB,CAAC;IACzC;;;;;OAKG;IACH,YAAY,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;CAC/B,CAAC;AAQF;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GAC9B,gBAAgB,MAAM,EACtB,SAAS,MAAM,KAChB,MAOF,CAAC;AA8KF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,eAAe,aAAa,KAAG,IAEhE,CAAC"}
1
+ {"version":3,"file":"eventObserver.d.ts","sourceRoot":"","sources":["../../src/lib/eventObserver.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,IAAI,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAarE,KAAK,KAAK,GAAG;IACT,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC;CACnC,CAAC;AAEF,KAAK,sBAAsB,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;CAC1D,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACvB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,mBAAmB,EAAE,MAAM,CAAC;CAC/B,CAAC;AAOF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IACxB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB;;;OAGG;IACH,eAAe,CAAC,EAAE,sBAAsB,CAAC;IACzC;;;;;OAKG;IACH,YAAY,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,YAAY,CAAC;IACtC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,YAAY,CAAC;IACnC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,CACb,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,oBAAoB,KAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC7B,CAAC;AAQF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAC9B,gBAAgB,MAAM,EACtB,SAAS,MAAM,KAChB,MAIG,CAAC;AAgPP;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gBAAgB,GACzB,eAAe,aAAa,KAC7B,OAAO,CAAC,IAAI,CAiGd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAAI,eAAe,aAAa,KAAG,IAEhE,CAAC"}
@@ -1,6 +1,12 @@
1
- import { getLogger } from '@nimbus-cqrs/core';
1
+ import { calculateBackoffDelay as coreCalculateBackoffDelay, getLogger, withRetry, } from '@nimbus-cqrs/core';
2
+ import { SpanStatusCode } from '@opentelemetry/api';
2
3
  import { getEventSourcingDBClient } from './client.js';
3
- import { withSpan } from './tracing.js';
4
+ import { isEventData } from './eventMapping.js';
5
+ import { observerConnectionReconnectsCounter, observerConnectionRetryAttemptsCounter, observerEventsHandledCounter, observerHandlerRetryAttemptsCounter, observerHandlingDuration, withObserveEventSpan, } from './tracing.js';
6
+ const DEFAULT_RETRY_OPTIONS = {
7
+ maxRetries: 3,
8
+ initialRetryDelayMs: 3000,
9
+ };
4
10
  /**
5
11
  * Returns a promise that resolves after the given number of milliseconds.
6
12
  */
@@ -14,13 +20,37 @@ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
14
20
  * exponential scaling.
15
21
  * @param attempt - The zero-based retry attempt number.
16
22
  * @returns The backoff delay in milliseconds.
23
+ *
24
+ * @deprecated Import {@link calculateBackoffDelay} from
25
+ * `@nimbus-cqrs/core` instead. This wrapper keeps the historical
26
+ * 30% jitter default used by EventSourcingDB observers.
17
27
  */
18
- export const calculateBackoffDelay = (initialDelayMs, attempt) => {
19
- const baseDelay = initialDelayMs * Math.pow(2, attempt);
20
- // Add jitter: random value between 0 and 30% of the base delay
21
- const jitter = Math.random() * baseDelay * 0.3;
22
- return Math.floor(baseDelay + jitter);
23
- };
28
+ export const calculateBackoffDelay = (initialDelayMs, attempt) => coreCalculateBackoffDelay(initialDelayMs, attempt, {
29
+ jitterFactor: 0.3,
30
+ maxDelayMs: Infinity,
31
+ });
32
+ /**
33
+ * Resolves connection retry options, preferring
34
+ * {@link EventObserver.connectionRetryOptions} over the deprecated
35
+ * {@link EventObserver.retryOptions}.
36
+ */
37
+ const resolveConnectionRetryOptions = (eventObserver) => ({
38
+ maxRetries: eventObserver.connectionRetryOptions?.maxRetries ??
39
+ eventObserver.retryOptions?.maxRetries ??
40
+ DEFAULT_RETRY_OPTIONS.maxRetries,
41
+ initialRetryDelayMs: eventObserver.connectionRetryOptions?.initialRetryDelayMs ??
42
+ eventObserver.retryOptions?.initialRetryDelayMs ??
43
+ DEFAULT_RETRY_OPTIONS.initialRetryDelayMs,
44
+ });
45
+ /**
46
+ * Resolves handler retry options from {@link EventObserver.handlerRetryOptions}.
47
+ */
48
+ const resolveHandlerRetryOptions = (eventObserver) => ({
49
+ maxRetries: eventObserver.handlerRetryOptions?.maxRetries ??
50
+ DEFAULT_RETRY_OPTIONS.maxRetries,
51
+ initialRetryDelayMs: eventObserver.handlerRetryOptions?.initialRetryDelayMs ??
52
+ DEFAULT_RETRY_OPTIONS.initialRetryDelayMs,
53
+ });
24
54
  /**
25
55
  * Logs an informational message when an event observer connects or
26
56
  * reconnects to EventSourcingDB. When {@link retryCount} is greater
@@ -39,10 +69,10 @@ const logObserverConnection = (subject, retryCount, data) => {
39
69
  getLogger().info({ category: 'Nimbus', message, data });
40
70
  };
41
71
  /**
42
- * Handles an observer error by logging it and waiting with exponential
43
- * backoff before the next retry attempt. When the maximum number of
44
- * retries is exceeded a critical log entry is emitted and no further
45
- * retries are attempted.
72
+ * Handles a connection / stream error by logging it and waiting with
73
+ * exponential backoff before the next reconnect attempt. When the
74
+ * maximum number of retries is exceeded a critical log entry is
75
+ * emitted and no further retries are attempted.
46
76
  *
47
77
  * @param error - The error that caused the observer to disconnect.
48
78
  * @param subject - The observed subject.
@@ -53,7 +83,7 @@ const logObserverConnection = (subject, retryCount, data) => {
53
83
  * @returns `true` if the observer should retry, `false` if retries
54
84
  * are exhausted.
55
85
  */
56
- const handleObserverError = async (error, subject, retryCount, maxRetries, initialRetryDelayMs) => {
86
+ const handleConnectionError = async (error, subject, retryCount, maxRetries, initialRetryDelayMs) => {
57
87
  if (retryCount > maxRetries) {
58
88
  getLogger().critical({
59
89
  category: 'Nimbus',
@@ -61,6 +91,7 @@ const handleObserverError = async (error, subject, retryCount, maxRetries, initi
61
91
  });
62
92
  return false;
63
93
  }
94
+ observerConnectionRetryAttemptsCounter.add(1, { subject });
64
95
  const backoffDelay = calculateBackoffDelay(initialRetryDelayMs, retryCount - 1);
65
96
  getLogger().error({
66
97
  category: 'Nimbus',
@@ -70,32 +101,130 @@ const handleObserverError = async (error, subject, retryCount, maxRetries, initi
70
101
  await delay(backoffDelay);
71
102
  return true;
72
103
  };
104
+ const recordHandlerMetrics = (metricLabels, status, startTime) => {
105
+ observerEventsHandledCounter.add(1, { ...metricLabels, status });
106
+ observerHandlingDuration.record((performance.now() - startTime) / 1000, metricLabels);
107
+ };
108
+ /**
109
+ * Invokes the event handler with in-place retries. Does not reconnect
110
+ * the observe stream. When all retries are exhausted, calls
111
+ * {@link EventObserver.onHandlerError} or logs critically, then
112
+ * returns without throwing so the observer can skip the event.
113
+ */
114
+ const handleEventWithRetry = async (eventObserver, event, handlerRetryOptions, span, startTime) => {
115
+ const { maxRetries, initialRetryDelayMs } = handlerRetryOptions;
116
+ const metricLabels = {
117
+ subject: eventObserver.subject,
118
+ event_type: event.type,
119
+ };
120
+ try {
121
+ await withRetry(() => eventObserver.eventHandler(event), {
122
+ maxRetries,
123
+ initialDelayMs: initialRetryDelayMs,
124
+ maxDelayMs: Infinity,
125
+ jitterFactor: 0.3,
126
+ onRetry: ({ error, attempt, delayMs }) => {
127
+ observerHandlerRetryAttemptsCounter.add(1, metricLabels);
128
+ span.addEvent('retry', {
129
+ attempt,
130
+ delay_ms: delayMs,
131
+ });
132
+ getLogger().error({
133
+ category: 'Nimbus',
134
+ message: `Error handling event "${event.id}" (${event.type}) for subject "${eventObserver.subject}" (retry ${attempt}/${maxRetries}), retrying in ${delayMs}ms`,
135
+ error,
136
+ ...(isEventData(event.data)
137
+ ? {
138
+ correlationId: event.data.nimbusMeta.correlationid,
139
+ }
140
+ : {}),
141
+ });
142
+ },
143
+ });
144
+ recordHandlerMetrics(metricLabels, 'success', startTime);
145
+ }
146
+ catch (error) {
147
+ const handlerError = error instanceof Error
148
+ ? error
149
+ : new Error(String(error));
150
+ recordHandlerMetrics(metricLabels, 'skipped', startTime);
151
+ span.setStatus({
152
+ code: SpanStatusCode.ERROR,
153
+ message: handlerError.message,
154
+ });
155
+ span.recordException(handlerError);
156
+ await handleHandlerExhausted(eventObserver, event, handlerError, maxRetries);
157
+ }
158
+ };
159
+ /**
160
+ * Handles exhausted handler retries by invoking
161
+ * {@link EventObserver.onHandlerError} or emitting a critical log.
162
+ */
163
+ const handleHandlerExhausted = async (eventObserver, event, handlerError, maxRetries) => {
164
+ if (eventObserver.onHandlerError) {
165
+ try {
166
+ await eventObserver.onHandlerError(handlerError, event);
167
+ }
168
+ catch (err) {
169
+ // Isolate user callback failures so the poison
170
+ // event is still skipped and observation continues.
171
+ const callbackError = err instanceof Error
172
+ ? err
173
+ : new Error(String(err));
174
+ getLogger().critical({
175
+ category: 'Nimbus',
176
+ message: `onHandlerError failed for event "${event.id}" (${event.type}) for subject "${eventObserver.subject}". Skipping event.`,
177
+ error: callbackError,
178
+ ...(isEventData(event.data)
179
+ ? {
180
+ correlationId: event.data.nimbusMeta.correlationid,
181
+ }
182
+ : {}),
183
+ });
184
+ }
185
+ return;
186
+ }
187
+ getLogger().critical({
188
+ category: 'Nimbus',
189
+ message: `Failed to handle event "${event.id}" (${event.type}) for subject "${eventObserver.subject}" after ${maxRetries} ${maxRetries === 1 ? 'retry' : 'retries'}. Skipping event.`,
190
+ error: handlerError,
191
+ ...(isEventData(event.data)
192
+ ? {
193
+ correlationId: event.data.nimbusMeta.correlationid,
194
+ }
195
+ : {}),
196
+ });
197
+ };
73
198
  /**
74
199
  * Starts observing events for the given {@link EventObserver} with
75
- * automatic reconnection on failure.
76
- *
77
- * On each connection attempt the EventSourcingDB server is pinged
78
- * first. Events are then consumed from the stream and each one is
79
- * passed to the observer's event handler inside an OpenTelemetry
80
- * span. If the event carries a `traceparent`, the span is linked to
81
- * the original writer's trace for end-to-end distributed tracing.
200
+ * automatic reconnection on stream failure and separate in-place
201
+ * retries for handler failures.
82
202
  *
83
- * After every successfully handled event the lower bound is advanced
84
- * so that a reconnection resumes from the last processed position.
203
+ * Events are consumed from the stream and each one is passed to the
204
+ * observer's event handler inside an OpenTelemetry span. If the event
205
+ * carries a `traceparent`, the span is linked to the original writer's
206
+ * trace for end-to-end distributed tracing.
85
207
  *
86
- * When the connection drops, exponential backoff with jitter is
87
- * applied up to the configured maximum number of retries.
208
+ * Handler failures are retried without reconnecting. After handler
209
+ * retries are exhausted the event is skipped (lower bound advanced)
210
+ * so observation can continue. When the connection drops, exponential
211
+ * backoff with jitter is applied up to the configured maximum number
212
+ * of connection retries.
88
213
  *
89
214
  * @param eventObserver - The event observer configuration.
215
+ * @returns A promise that resolves when the observe stream ends or
216
+ * connection retries are exhausted.
90
217
  */
91
- const observeWithRetry = async (eventObserver) => {
92
- const eventSourcingDBClient = getEventSourcingDBClient();
93
- const maxRetries = eventObserver.retryOptions?.maxRetries ?? 3;
94
- const initialRetryDelayMs = eventObserver.retryOptions?.initialRetryDelayMs ?? 3000;
95
- let retryCount = 0;
218
+ export const observeWithRetry = async (eventObserver) => {
219
+ const connectionRetryOptions = resolveConnectionRetryOptions(eventObserver);
220
+ const handlerRetryOptions = resolveHandlerRetryOptions(eventObserver);
221
+ let connectionRetryCount = 0;
96
222
  let lastProcessedEventId;
97
223
  while (true) {
98
224
  try {
225
+ // Resolve the client on every attempt so a re-initialized
226
+ // singleton is picked up after reconnecting to EventSourcingDB.
227
+ const eventSourcingDBClient = getEventSourcingDBClient();
99
228
  // Once we have a concrete position, use it as lower bound and
100
229
  // drop fromLatestEvent; otherwise fall back to the original options.
101
230
  const lowerBound = lastProcessedEventId
@@ -104,7 +233,7 @@ const observeWithRetry = async (eventObserver) => {
104
233
  const fromLatestEvent = lastProcessedEventId
105
234
  ? undefined
106
235
  : eventObserver.fromLatestEvent;
107
- logObserverConnection(eventObserver.subject, retryCount, {
236
+ logObserverConnection(eventObserver.subject, connectionRetryCount, {
108
237
  recursive: eventObserver.recursive ?? false,
109
238
  lowerBound,
110
239
  fromLatestEvent,
@@ -114,26 +243,34 @@ const observeWithRetry = async (eventObserver) => {
114
243
  ...(lowerBound ? { lowerBound } : {}),
115
244
  ...(fromLatestEvent ? { fromLatestEvent } : {}),
116
245
  })) {
117
- // Reset the retry count as soon as we successfully receive an event
118
- retryCount = 0;
246
+ // Stream is healthy once events flow again. Count a successful
247
+ // reconnect only here so failed reconnect attempts are excluded.
248
+ if (connectionRetryCount > 0) {
249
+ observerConnectionReconnectsCounter.add(1, {
250
+ subject: eventObserver.subject,
251
+ });
252
+ }
253
+ connectionRetryCount = 0;
119
254
  const traceContext = event.traceparent
120
255
  ? {
121
256
  traceparent: event.traceparent,
122
257
  tracestate: event.tracestate,
123
258
  }
124
259
  : undefined;
125
- await withSpan('observeEvent', async () => {
126
- await eventObserver.eventHandler(event);
127
- }, traceContext);
128
- // Track last processed position so retries resume from here
260
+ const startTime = performance.now();
261
+ await withObserveEventSpan(event, eventObserver.subject, traceContext, async (span) => {
262
+ await handleEventWithRetry(eventObserver, event, handlerRetryOptions, span, startTime);
263
+ });
264
+ // Advance after success or skip so poison events do not block
265
+ // the stream and reconnects resume past the last attempted event.
129
266
  lastProcessedEventId = event.id;
130
267
  }
131
268
  // If the loop completes normally (stream ended), we're done
132
269
  return;
133
270
  }
134
271
  catch (error) {
135
- retryCount++;
136
- const shouldRetry = await handleObserverError(error, eventObserver.subject, retryCount, maxRetries, initialRetryDelayMs);
272
+ connectionRetryCount++;
273
+ const shouldRetry = await handleConnectionError(error, eventObserver.subject, connectionRetryCount, connectionRetryOptions.maxRetries, connectionRetryOptions.initialRetryDelayMs);
137
274
  if (!shouldRetry) {
138
275
  return;
139
276
  }
@@ -143,8 +280,9 @@ const observeWithRetry = async (eventObserver) => {
143
280
  /**
144
281
  * Initializes an event observer by starting the observation loop in
145
282
  * the background (non-blocking). The observer will keep running and
146
- * reconnecting according to its retry options until the stream ends
147
- * or retries are exhausted.
283
+ * reconnecting according to its connection retry options until the
284
+ * stream ends or connection retries are exhausted. Handler failures
285
+ * are retried separately and do not stop the observer.
148
286
  *
149
287
  * @param eventObserver - The event observer configuration.
150
288
  */
@@ -1,5 +1,31 @@
1
+ import { type Span } from '@opentelemetry/api';
1
2
  export declare const tracer: import("@opentelemetry/api").Tracer;
2
3
  export declare const DB_SYSTEM = "eventsourcingdb";
4
+ /**
5
+ * Histogram of serialized Nimbus event sizes written via writeEvents.
6
+ */
7
+ export declare const eventSizeBytes: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
8
+ /**
9
+ * Counter of events handled by an event observer, labeled with
10
+ * `status: success|skipped`.
11
+ */
12
+ export declare const observerEventsHandledCounter: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
13
+ /**
14
+ * Histogram of observer handler duration in seconds (includes retries).
15
+ */
16
+ export declare const observerHandlingDuration: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
17
+ /**
18
+ * Counter of in-place handler retry attempts for event observers.
19
+ */
20
+ export declare const observerHandlerRetryAttemptsCounter: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
21
+ /**
22
+ * Counter of connection reconnect attempts scheduled after stream failure.
23
+ */
24
+ export declare const observerConnectionRetryAttemptsCounter: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
25
+ /**
26
+ * Counter of successful reconnects after one or more connection retries.
27
+ */
28
+ export declare const observerConnectionReconnectsCounter: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
3
29
  /**
4
30
  * Trace context extracted from an EventSourcingDB event, used to link
5
31
  * the processing span to the span that originally wrote the event.
@@ -22,6 +48,20 @@ export type TraceContext = {
22
48
  * @returns The result of the async function
23
49
  */
24
50
  export declare const withSpan: <T>(operation: string, fn: () => Promise<T>, traceContext?: TraceContext) => Promise<T>;
51
+ /**
52
+ * Starts a consumer span for observing a single EventSourcingDB event,
53
+ * optionally linked to the writer's trace via {@link TraceContext}.
54
+ *
55
+ * @param event - The observed EventSourcingDB event.
56
+ * @param observerSubject - The observer's configured subject.
57
+ * @param traceContext - Optional writer trace context from the event.
58
+ * @param fn - Work to run inside the active span.
59
+ */
60
+ export declare const withObserveEventSpan: <T>(event: {
61
+ id: string;
62
+ type: string;
63
+ subject: string;
64
+ }, observerSubject: string, traceContext: TraceContext | undefined, fn: (span: Span) => Promise<T>) => Promise<T>;
25
65
  /**
26
66
  * Wraps an async generator with OpenTelemetry tracing and metrics.
27
67
  *
@@ -1 +1 @@
1
- {"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/lib/tracing.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,MAAM,qCAA4B,CAAC;AAEhD,eAAO,MAAM,SAAS,oBAAoB,CAAC;AAmB3C;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EACtB,WAAW,MAAM,EACjB,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,eAAe,YAAY,KAC5B,OAAO,CAAC,CAAC,CA6DX,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAuB,sBAAsB,CAAC,CAAC,EAC3C,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GACxC,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CA0C/B"}
1
+ {"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../../src/lib/tracing.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,IAAI,EAIZ,MAAM,oBAAoB,CAAC;AAE5B,eAAO,MAAM,MAAM,qCAA4B,CAAC;AAEhD,eAAO,MAAM,SAAS,oBAAoB,CAAC;AAmB3C;;GAEG;AACH,eAAO,MAAM,cAAc,iFAM1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,+EAMxC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wBAAwB,iFAOpC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mCAAmC,+EAM/C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sCAAsC,+EAMlD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mCAAmC,+EAM/C,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EACtB,WAAW,MAAM,EACjB,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,eAAe,YAAY,KAC5B,OAAO,CAAC,CAAC,CA6DX,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAClC,OAAO;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACpD,iBAAiB,MAAM,EACvB,cAAc,YAAY,GAAG,SAAS,EACtC,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAC/B,OAAO,CAAC,CAAC,CA8BX,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAuB,sBAAsB,CAAC,CAAC,EAC3C,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GACxC,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CA0C/B"}
@@ -9,6 +9,45 @@ const operationDuration = meter.createHistogram('eventsourcingdb_operation_durat
9
9
  description: 'Duration of EventSourcingDB operations in seconds',
10
10
  unit: 's',
11
11
  });
12
+ /**
13
+ * Histogram of serialized Nimbus event sizes written via writeEvents.
14
+ */
15
+ export const eventSizeBytes = meter.createHistogram('eventsourcingdb_event_size_bytes', {
16
+ description: 'Size of events written to EventSourcingDB in bytes',
17
+ unit: 'By',
18
+ });
19
+ /**
20
+ * Counter of events handled by an event observer, labeled with
21
+ * `status: success|skipped`.
22
+ */
23
+ export const observerEventsHandledCounter = meter.createCounter('eventsourcingdb_observer_events_handled_total', {
24
+ description: 'Total number of events handled by EventSourcingDB observers',
25
+ });
26
+ /**
27
+ * Histogram of observer handler duration in seconds (includes retries).
28
+ */
29
+ export const observerHandlingDuration = meter.createHistogram('eventsourcingdb_observer_handling_duration_seconds', {
30
+ description: 'Duration of EventSourcingDB observer event handling in seconds',
31
+ unit: 's',
32
+ });
33
+ /**
34
+ * Counter of in-place handler retry attempts for event observers.
35
+ */
36
+ export const observerHandlerRetryAttemptsCounter = meter.createCounter('eventsourcingdb_observer_handler_retry_attempts_total', {
37
+ description: 'Total number of handler retry attempts for EventSourcingDB observers',
38
+ });
39
+ /**
40
+ * Counter of connection reconnect attempts scheduled after stream failure.
41
+ */
42
+ export const observerConnectionRetryAttemptsCounter = meter.createCounter('eventsourcingdb_observer_connection_retry_attempts_total', {
43
+ description: 'Total number of connection retry attempts for EventSourcingDB observers',
44
+ });
45
+ /**
46
+ * Counter of successful reconnects after one or more connection retries.
47
+ */
48
+ export const observerConnectionReconnectsCounter = meter.createCounter('eventsourcingdb_observer_connection_reconnects_total', {
49
+ description: 'Total number of successful connection reconnects for EventSourcingDB observers',
50
+ });
12
51
  /**
13
52
  * Wraps an async function with OpenTelemetry tracing and metrics.
14
53
  *
@@ -68,6 +107,41 @@ export const withSpan = (operation, fn, traceContext) => {
68
107
  }
69
108
  });
70
109
  };
110
+ /**
111
+ * Starts a consumer span for observing a single EventSourcingDB event,
112
+ * optionally linked to the writer's trace via {@link TraceContext}.
113
+ *
114
+ * @param event - The observed EventSourcingDB event.
115
+ * @param observerSubject - The observer's configured subject.
116
+ * @param traceContext - Optional writer trace context from the event.
117
+ * @param fn - Work to run inside the active span.
118
+ */
119
+ export const withObserveEventSpan = (event, observerSubject, traceContext, fn) => {
120
+ const parentContext = traceContext
121
+ ? propagation.extract(otelContext.active(), traceContext)
122
+ : otelContext.active();
123
+ return tracer.startActiveSpan('eventsourcingdb.observeEvent', {
124
+ kind: SpanKind.CONSUMER,
125
+ attributes: {
126
+ 'db.system': DB_SYSTEM,
127
+ 'db.operation': 'observeEvent',
128
+ 'messaging.system': 'eventsourcingdb',
129
+ 'messaging.operation': 'process',
130
+ 'messaging.destination': event.type,
131
+ 'cloudevents.event_id': event.id,
132
+ 'cloudevents.event_type': event.type,
133
+ 'cloudevents.event_subject': event.subject,
134
+ 'eventsourcingdb.observer.subject': observerSubject,
135
+ },
136
+ }, parentContext, async (span) => {
137
+ try {
138
+ return await fn(span);
139
+ }
140
+ finally {
141
+ span.end();
142
+ }
143
+ });
144
+ };
71
145
  /**
72
146
  * Wraps an async generator with OpenTelemetry tracing and metrics.
73
147
  *
@@ -1 +1 @@
1
- {"version":3,"file":"writeEvents.d.ts","sourceRoot":"","sources":["../../src/lib/writeEvents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qDAAqD,CAAC;AAEjF,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKpE;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACpB,QAAQ,KAAK,EAAE,EACf,gBAAgB,YAAY,EAAE,KAC/B,OAAO,CAAC,IAAI,CAoBd,CAAC"}
1
+ {"version":3,"file":"writeEvents.d.ts","sourceRoot":"","sources":["../../src/lib/writeEvents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qDAAqD,CAAC;AAEjF,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAapE;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACpB,QAAQ,KAAK,EAAE,EACf,gBAAgB,YAAY,EAAE,KAC/B,OAAO,CAAC,IAAI,CAyBd,CAAC"}
@@ -1,7 +1,12 @@
1
1
  import { context, propagation } from '@opentelemetry/api';
2
2
  import { getEventSourcingDBClient } from './client.js';
3
3
  import { nimbusEventToEventSourcingDBEventCandidate } from './eventMapping.js';
4
- import { withSpan } from './tracing.js';
4
+ import { eventSizeBytes, withSpan } from './tracing.js';
5
+ const textEncoder = new TextEncoder();
6
+ /**
7
+ * Returns the UTF-8 byte length of a serialized Nimbus event.
8
+ */
9
+ const getEventSizeBytes = (event) => textEncoder.encode(JSON.stringify(event)).length;
5
10
  /**
6
11
  * Writes one or more Nimbus events to EventSourcingDB. Each event is
7
12
  * converted into an EventSourcingDB event candidate before being persisted.
@@ -14,7 +19,13 @@ export const writeEvents = (events, preconditions) => {
14
19
  const eventSourcingDBClient = getEventSourcingDBClient();
15
20
  const carrier = {};
16
21
  propagation.inject(context.active(), carrier);
17
- const eventCandidates = events.map((event) => nimbusEventToEventSourcingDBEventCandidate(event, carrier['traceparent'], carrier['tracestate']));
22
+ const eventCandidates = events.map((event) => {
23
+ eventSizeBytes.record(getEventSizeBytes(event), {
24
+ subject: event.subject,
25
+ event_type: event.type,
26
+ });
27
+ return nimbusEventToEventSourcingDBEventCandidate(event, carrier['traceparent'], carrier['tracestate']);
28
+ });
18
29
  await eventSourcingDBClient.writeEvents(eventCandidates, preconditions);
19
30
  });
20
31
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbus-cqrs/eventsourcingdb",
3
- "version": "2.1.2",
3
+ "version": "2.3.0",
4
4
  "description": "Simplify Event-Driven Applications - EventSourcingDB integration for the Nimbus framework.",
5
5
  "keywords": [
6
6
  "nimbus",
@@ -35,9 +35,9 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@opentelemetry/api": "^1.9.1",
38
- "eventsourcingdb": "^1.8.1",
39
- "zod": "^4.3.6",
40
- "@nimbus-cqrs/core": "^2.1.2"
38
+ "eventsourcingdb": "^1.8.2",
39
+ "zod": "^4.4.3",
40
+ "@nimbus-cqrs/core": "^2.3.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^22.0.0"