@nimbus-cqrs/eventsourcingdb 2.0.0-beta.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/LICENSE +85 -0
- package/README.md +26 -0
- package/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +57 -0
- package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.d.ts +700 -0
- package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/fmt/1.0.10/colors.js +903 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/_util.d.ts +14 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/_util.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/_util.js +65 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/decode_time.d.ts +20 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/decode_time.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/decode_time.js +43 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/mod.d.ts +44 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/mod.js +47 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/monotonic_ulid.d.ts +44 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/monotonic_ulid.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/monotonic_ulid.js +51 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/ulid.d.ts +31 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/ulid.d.ts.map +1 -0
- package/esm/deps/jsr.io/@std/ulid/1.0.0/ulid.js +37 -0
- package/esm/index.d.ts +6 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +5 -0
- package/esm/lib/client.d.ts +90 -0
- package/esm/lib/client.d.ts.map +1 -0
- package/esm/lib/client.js +112 -0
- package/esm/lib/eventMapping.d.ts +51 -0
- package/esm/lib/eventMapping.d.ts.map +1 -0
- package/esm/lib/eventMapping.js +71 -0
- package/esm/lib/eventObserver.d.ts +85 -0
- package/esm/lib/eventObserver.d.ts.map +1 -0
- package/esm/lib/eventObserver.js +154 -0
- package/esm/lib/readEvents.d.ts +16 -0
- package/esm/lib/readEvents.d.ts.map +1 -0
- package/esm/lib/readEvents.js +21 -0
- package/esm/lib/tracing.d.ts +37 -0
- package/esm/lib/tracing.d.ts.map +1 -0
- package/esm/lib/tracing.js +114 -0
- package/esm/lib/writeEvents.d.ts +11 -0
- package/esm/lib/writeEvents.d.ts.map +1 -0
- package/esm/lib/writeEvents.js +20 -0
- package/esm/package.json +3 -0
- package/package.json +35 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Event } from '../deps/jsr.io/@nimbus/core/2.0.0/src/index.js';
|
|
2
|
+
import type { Event as EventSourcingDBEvent, EventCandidate } from 'eventsourcingdb';
|
|
3
|
+
/**
|
|
4
|
+
* Metadata that Nimbus attaches to events stored in EventSourcingDB
|
|
5
|
+
* to preserve correlation and schema information.
|
|
6
|
+
*
|
|
7
|
+
* @property {string} correlationid - A globally unique identifier that indicates a correlation to previous and subsequent messages.
|
|
8
|
+
* @property {string} dataschema - An absolute URL to the schema that the data adheres to (optional).
|
|
9
|
+
*/
|
|
10
|
+
export type NimbusEventMetadata = {
|
|
11
|
+
correlationid: string;
|
|
12
|
+
dataschema?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The data structure used to store Nimbus events in EventSourcingDB.
|
|
16
|
+
* It wraps the original event payload together with Nimbus-specific metadata.
|
|
17
|
+
*
|
|
18
|
+
* @property {Record<string, unknown>} payload - The actual business data of the event.
|
|
19
|
+
* @property {NimbusEventMetadata} nimbusMeta - Nimbus-specific metadata such as correlation id and data schema.
|
|
20
|
+
*/
|
|
21
|
+
export type EventData = {
|
|
22
|
+
payload: Record<string, unknown>;
|
|
23
|
+
nimbusMeta: NimbusEventMetadata;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Type guard that checks whether the given value conforms to the {@link EventData} structure
|
|
27
|
+
* by verifying the presence of both `payload` and `nimbusMeta` properties.
|
|
28
|
+
*
|
|
29
|
+
* @param data - The value to check.
|
|
30
|
+
* @returns `true` if the value is an {@link EventData}, `false` otherwise.
|
|
31
|
+
*/
|
|
32
|
+
export declare const isEventData: (data: unknown) => data is EventData;
|
|
33
|
+
/**
|
|
34
|
+
* Converts a Nimbus {@link Event} into an EventSourcingDB {@link EventCandidate}
|
|
35
|
+
* by mapping the event properties and wrapping the data with Nimbus metadata.
|
|
36
|
+
*
|
|
37
|
+
* @param event - The Nimbus event to convert.
|
|
38
|
+
* @returns An EventSourcingDB event candidate ready to be written.
|
|
39
|
+
*/
|
|
40
|
+
export declare const nimbusEventToEventSourcingDBEventCandidate: (event: Event, traceparent?: string, tracestate?: string) => EventCandidate;
|
|
41
|
+
/**
|
|
42
|
+
* Converts an EventSourcingDB event back into a Nimbus {@link Event}.
|
|
43
|
+
* If the event data contains Nimbus metadata, it extracts the original payload
|
|
44
|
+
* and correlation information. Otherwise, it treats the entire data as the payload
|
|
45
|
+
* and generates a new correlation id.
|
|
46
|
+
*
|
|
47
|
+
* @param eventSourcingDBEvent - The EventSourcingDB event to convert.
|
|
48
|
+
* @returns A Nimbus event.
|
|
49
|
+
*/
|
|
50
|
+
export declare const eventSourcingDBEventToNimbusEvent: <TEvent extends Event>(eventSourcingDBEvent: EventSourcingDBEvent) => TEvent;
|
|
51
|
+
//# sourceMappingURL=eventMapping.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eventMapping.d.ts","sourceRoot":"","sources":["../../src/lib/eventMapping.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,gDAAgD,CAAC;AAEzF,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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createEvent } from '../deps/jsr.io/@nimbus/core/2.0.0/src/index.js';
|
|
2
|
+
import { ulid } from '../deps/jsr.io/@std/ulid/1.0.0/mod.js';
|
|
3
|
+
/**
|
|
4
|
+
* Type guard that checks whether the given value conforms to the {@link EventData} structure
|
|
5
|
+
* by verifying the presence of both `payload` and `nimbusMeta` properties.
|
|
6
|
+
*
|
|
7
|
+
* @param data - The value to check.
|
|
8
|
+
* @returns `true` if the value is an {@link EventData}, `false` otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export const isEventData = (data) => {
|
|
11
|
+
return (typeof data === 'object' &&
|
|
12
|
+
data !== null &&
|
|
13
|
+
'payload' in data &&
|
|
14
|
+
'nimbusMeta' in data);
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Converts a Nimbus {@link Event} into an EventSourcingDB {@link EventCandidate}
|
|
18
|
+
* by mapping the event properties and wrapping the data with Nimbus metadata.
|
|
19
|
+
*
|
|
20
|
+
* @param event - The Nimbus event to convert.
|
|
21
|
+
* @returns An EventSourcingDB event candidate ready to be written.
|
|
22
|
+
*/
|
|
23
|
+
export const nimbusEventToEventSourcingDBEventCandidate = (event, traceparent, tracestate) => {
|
|
24
|
+
return {
|
|
25
|
+
source: event.source,
|
|
26
|
+
subject: event.subject,
|
|
27
|
+
type: event.type,
|
|
28
|
+
data: {
|
|
29
|
+
payload: event.data,
|
|
30
|
+
nimbusMeta: {
|
|
31
|
+
correlationid: event.correlationid,
|
|
32
|
+
...(event.dataschema && { dataschema: event.dataschema }),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
...(traceparent && { traceparent: traceparent }),
|
|
36
|
+
...(tracestate && { tracestate: tracestate }),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Converts an EventSourcingDB event back into a Nimbus {@link Event}.
|
|
41
|
+
* If the event data contains Nimbus metadata, it extracts the original payload
|
|
42
|
+
* and correlation information. Otherwise, it treats the entire data as the payload
|
|
43
|
+
* and generates a new correlation id.
|
|
44
|
+
*
|
|
45
|
+
* @param eventSourcingDBEvent - The EventSourcingDB event to convert.
|
|
46
|
+
* @returns A Nimbus event.
|
|
47
|
+
*/
|
|
48
|
+
export const eventSourcingDBEventToNimbusEvent = (eventSourcingDBEvent) => {
|
|
49
|
+
let data;
|
|
50
|
+
let correlationid;
|
|
51
|
+
let dataschema;
|
|
52
|
+
if (isEventData(eventSourcingDBEvent.data)) {
|
|
53
|
+
data = eventSourcingDBEvent.data.payload;
|
|
54
|
+
correlationid = eventSourcingDBEvent.data.nimbusMeta.correlationid;
|
|
55
|
+
dataschema = eventSourcingDBEvent.data.nimbusMeta.dataschema;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
data = eventSourcingDBEvent.data;
|
|
59
|
+
correlationid = ulid();
|
|
60
|
+
}
|
|
61
|
+
return createEvent({
|
|
62
|
+
id: eventSourcingDBEvent.id,
|
|
63
|
+
time: eventSourcingDBEvent.time.toISOString(),
|
|
64
|
+
source: eventSourcingDBEvent.source,
|
|
65
|
+
subject: eventSourcingDBEvent.subject,
|
|
66
|
+
type: eventSourcingDBEvent.type,
|
|
67
|
+
correlationid: correlationid,
|
|
68
|
+
data: data,
|
|
69
|
+
...(dataschema && { dataschema: dataschema }),
|
|
70
|
+
});
|
|
71
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Event as EventSourcingDBEvent } from 'eventsourcingdb';
|
|
2
|
+
type Bound = {
|
|
3
|
+
id: string;
|
|
4
|
+
type: 'inclusive' | 'exclusive';
|
|
5
|
+
};
|
|
6
|
+
type ObserveFromLatestEvent = {
|
|
7
|
+
subject: string;
|
|
8
|
+
type: string;
|
|
9
|
+
ifEventIsMissing: 'read-everything' | 'wait-for-event';
|
|
10
|
+
};
|
|
11
|
+
export type RetryOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* The maximum number of retry attempts before giving up.
|
|
14
|
+
* Defaults to 3.
|
|
15
|
+
*/
|
|
16
|
+
maxRetries: number;
|
|
17
|
+
/**
|
|
18
|
+
* The initial delay in milliseconds before the first retry.
|
|
19
|
+
* Subsequent retries will use exponential backoff with jitter.
|
|
20
|
+
* Defaults to 3000ms.
|
|
21
|
+
*/
|
|
22
|
+
initialRetryDelayMs: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* An event observer defines a handler function which will be applied to each event
|
|
26
|
+
* and the options to observe the events according to the EventSourcingDB API.
|
|
27
|
+
*
|
|
28
|
+
* See https://docs.eventsourcingdb.io/getting-started/observing-events for more information.
|
|
29
|
+
*/
|
|
30
|
+
export type EventObserver = {
|
|
31
|
+
/**
|
|
32
|
+
* The subject of the events to observe.
|
|
33
|
+
*/
|
|
34
|
+
subject: string;
|
|
35
|
+
/**
|
|
36
|
+
* Whether to observe events recursively.
|
|
37
|
+
* Defaults to false.
|
|
38
|
+
*/
|
|
39
|
+
recursive?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The lower bound of the events to observe.
|
|
42
|
+
* Defaults to undefined.
|
|
43
|
+
*/
|
|
44
|
+
lowerBound?: Bound;
|
|
45
|
+
/**
|
|
46
|
+
* The from latest event to observe.
|
|
47
|
+
* Defaults to undefined.
|
|
48
|
+
*/
|
|
49
|
+
fromLatestEvent?: ObserveFromLatestEvent;
|
|
50
|
+
/**
|
|
51
|
+
* The event handler which will be called when an event is observed.
|
|
52
|
+
*
|
|
53
|
+
* @param event - The EventSourcingDB event that was observed.
|
|
54
|
+
* @returns A promise that resolves when the event has been handled.
|
|
55
|
+
*/
|
|
56
|
+
eventHandler: (event: EventSourcingDBEvent) => Promise<void> | void;
|
|
57
|
+
/**
|
|
58
|
+
* Options for retry behavior when the connection fails.
|
|
59
|
+
* Uses exponential backoff with jitter between retries.
|
|
60
|
+
* Defaults to { maxRetries: 3, initialRetryDelayMs: 3000 }.
|
|
61
|
+
*/
|
|
62
|
+
retryOptions?: RetryOptions;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Calculates an exponential backoff delay with jitter for a given
|
|
66
|
+
* retry attempt. The jitter adds up to 30% of the base delay to
|
|
67
|
+
* avoid thundering-herd effects.
|
|
68
|
+
*
|
|
69
|
+
* @param initialDelayMs - The base delay in milliseconds before
|
|
70
|
+
* exponential scaling.
|
|
71
|
+
* @param attempt - The zero-based retry attempt number.
|
|
72
|
+
* @returns The backoff delay in milliseconds.
|
|
73
|
+
*/
|
|
74
|
+
export declare const calculateBackoffDelay: (initialDelayMs: number, attempt: number) => number;
|
|
75
|
+
/**
|
|
76
|
+
* Initializes an event observer by starting the observation loop in
|
|
77
|
+
* 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.
|
|
80
|
+
*
|
|
81
|
+
* @param eventObserver - The event observer configuration.
|
|
82
|
+
*/
|
|
83
|
+
export declare const initEventObserver: (eventObserver: EventObserver) => void;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=eventObserver.d.ts.map
|
|
@@ -0,0 +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;AA+KF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,eAAe,aAAa,KAAG,IAEhE,CAAC"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { getLogger } from '../deps/jsr.io/@nimbus/core/2.0.0/src/index.js';
|
|
2
|
+
import { getEventSourcingDBClient } from './client.js';
|
|
3
|
+
import { withSpan } from './tracing.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns a promise that resolves after the given number of milliseconds.
|
|
6
|
+
*/
|
|
7
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
/**
|
|
9
|
+
* Calculates an exponential backoff delay with jitter for a given
|
|
10
|
+
* retry attempt. The jitter adds up to 30% of the base delay to
|
|
11
|
+
* avoid thundering-herd effects.
|
|
12
|
+
*
|
|
13
|
+
* @param initialDelayMs - The base delay in milliseconds before
|
|
14
|
+
* exponential scaling.
|
|
15
|
+
* @param attempt - The zero-based retry attempt number.
|
|
16
|
+
* @returns The backoff delay in milliseconds.
|
|
17
|
+
*/
|
|
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
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Logs an informational message when an event observer connects or
|
|
26
|
+
* reconnects to EventSourcingDB. When {@link retryCount} is greater
|
|
27
|
+
* than zero the message indicates a successful reconnection.
|
|
28
|
+
*
|
|
29
|
+
* @param subject - The observed subject.
|
|
30
|
+
* @param retryCount - The number of retries that preceded this
|
|
31
|
+
* connection (0 for the initial connection).
|
|
32
|
+
* @param data - Additional context logged alongside the message.
|
|
33
|
+
*/
|
|
34
|
+
const logObserverConnection = (subject, retryCount, data) => {
|
|
35
|
+
const retryLabel = retryCount === 1 ? 'retry' : 'retries';
|
|
36
|
+
const message = retryCount > 0
|
|
37
|
+
? `Reconnected event observer for subject "${subject}" after ${retryCount} ${retryLabel}`
|
|
38
|
+
: `Observing events for subject "${subject}"`;
|
|
39
|
+
getLogger().info({ category: 'Nimbus', message, data });
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
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.
|
|
46
|
+
*
|
|
47
|
+
* @param error - The error that caused the observer to disconnect.
|
|
48
|
+
* @param subject - The observed subject.
|
|
49
|
+
* @param retryCount - The current (1-based) retry attempt number.
|
|
50
|
+
* @param maxRetries - The maximum number of allowed retries.
|
|
51
|
+
* @param initialRetryDelayMs - The base delay used for exponential
|
|
52
|
+
* backoff calculation.
|
|
53
|
+
* @returns `true` if the observer should retry, `false` if retries
|
|
54
|
+
* are exhausted.
|
|
55
|
+
*/
|
|
56
|
+
const handleObserverError = async (error, subject, retryCount, maxRetries, initialRetryDelayMs) => {
|
|
57
|
+
if (retryCount > maxRetries) {
|
|
58
|
+
getLogger().critical({
|
|
59
|
+
category: 'Nimbus',
|
|
60
|
+
message: `Failed to observe events for subject "${subject}" after ${maxRetries} ${maxRetries === 1 ? 'retry' : 'retries'}.`,
|
|
61
|
+
});
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const backoffDelay = calculateBackoffDelay(initialRetryDelayMs, retryCount - 1);
|
|
65
|
+
getLogger().error({
|
|
66
|
+
category: 'Nimbus',
|
|
67
|
+
message: `Error observing events for subject "${subject}" (retry ${retryCount}/${maxRetries}), retrying in ${backoffDelay}ms`,
|
|
68
|
+
error: error,
|
|
69
|
+
});
|
|
70
|
+
await delay(backoffDelay);
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* 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.
|
|
82
|
+
*
|
|
83
|
+
* After every successfully handled event the lower bound is advanced
|
|
84
|
+
* so that a reconnection resumes from the last processed position.
|
|
85
|
+
*
|
|
86
|
+
* When the connection drops, exponential backoff with jitter is
|
|
87
|
+
* applied up to the configured maximum number of retries.
|
|
88
|
+
*
|
|
89
|
+
* @param eventObserver - The event observer configuration.
|
|
90
|
+
*/
|
|
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;
|
|
96
|
+
let lastProcessedEventId;
|
|
97
|
+
while (true) {
|
|
98
|
+
try {
|
|
99
|
+
// Once we have a concrete position, use it as lower bound and
|
|
100
|
+
// drop fromLatestEvent; otherwise fall back to the original options.
|
|
101
|
+
const lowerBound = lastProcessedEventId
|
|
102
|
+
? { id: lastProcessedEventId, type: 'exclusive' }
|
|
103
|
+
: eventObserver.lowerBound;
|
|
104
|
+
const fromLatestEvent = lastProcessedEventId
|
|
105
|
+
? undefined
|
|
106
|
+
: eventObserver.fromLatestEvent;
|
|
107
|
+
// Verify connection
|
|
108
|
+
await eventSourcingDBClient.ping();
|
|
109
|
+
logObserverConnection(eventObserver.subject, retryCount, {
|
|
110
|
+
recursive: eventObserver.recursive ?? false,
|
|
111
|
+
lowerBound,
|
|
112
|
+
fromLatestEvent,
|
|
113
|
+
});
|
|
114
|
+
retryCount = 0;
|
|
115
|
+
for await (const event of eventSourcingDBClient.observeEvents(eventObserver.subject, {
|
|
116
|
+
recursive: eventObserver.recursive ?? false,
|
|
117
|
+
...(lowerBound ? { lowerBound } : {}),
|
|
118
|
+
...(fromLatestEvent ? { fromLatestEvent } : {}),
|
|
119
|
+
})) {
|
|
120
|
+
const traceContext = event.traceparent
|
|
121
|
+
? {
|
|
122
|
+
traceparent: event.traceparent,
|
|
123
|
+
tracestate: event.tracestate,
|
|
124
|
+
}
|
|
125
|
+
: undefined;
|
|
126
|
+
await withSpan('observeEvent', async () => {
|
|
127
|
+
await eventObserver.eventHandler(event);
|
|
128
|
+
}, traceContext);
|
|
129
|
+
// Track last processed position so retries resume from here
|
|
130
|
+
lastProcessedEventId = event.id;
|
|
131
|
+
}
|
|
132
|
+
// If the loop completes normally (stream ended), we're done
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
retryCount++;
|
|
137
|
+
const shouldRetry = await handleObserverError(error, eventObserver.subject, retryCount, maxRetries, initialRetryDelayMs);
|
|
138
|
+
if (!shouldRetry) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Initializes an event observer by starting the observation loop in
|
|
146
|
+
* the background (non-blocking). The observer will keep running and
|
|
147
|
+
* reconnecting according to its retry options until the stream ends
|
|
148
|
+
* or retries are exhausted.
|
|
149
|
+
*
|
|
150
|
+
* @param eventObserver - The event observer configuration.
|
|
151
|
+
*/
|
|
152
|
+
export const initEventObserver = (eventObserver) => {
|
|
153
|
+
observeWithRetry(eventObserver);
|
|
154
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Event, ReadEventsOptions } from 'eventsourcingdb';
|
|
2
|
+
/**
|
|
3
|
+
* Reads events from EventSourcingDB for a given subject.
|
|
4
|
+
*
|
|
5
|
+
* Returns an async generator that yields raw EventSourcingDB
|
|
6
|
+
* {@link Event} instances (not Nimbus events). Use
|
|
7
|
+
* {@link eventSourcingDBEventToNimbusEvent} to convert them
|
|
8
|
+
* into Nimbus events if needed.
|
|
9
|
+
*
|
|
10
|
+
* @param subject - The subject to read events for.
|
|
11
|
+
* @param options - Options to control which events are read.
|
|
12
|
+
* @param signal - An optional abort signal to cancel the read.
|
|
13
|
+
* @returns An async generator yielding EventSourcingDB events.
|
|
14
|
+
*/
|
|
15
|
+
export declare const readEvents: (subject: string, options: ReadEventsOptions, signal?: AbortSignal) => AsyncGenerator<Event, void, void>;
|
|
16
|
+
//# sourceMappingURL=readEvents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readEvents.d.ts","sourceRoot":"","sources":["../../src/lib/readEvents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAIhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,GACnB,SAAS,MAAM,EACf,SAAS,iBAAiB,EAC1B,SAAS,WAAW,KACrB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAUlC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getEventSourcingDBClient } from './client.js';
|
|
2
|
+
import { withAsyncGeneratorSpan } from './tracing.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads events from EventSourcingDB for a given subject.
|
|
5
|
+
*
|
|
6
|
+
* Returns an async generator that yields raw EventSourcingDB
|
|
7
|
+
* {@link Event} instances (not Nimbus events). Use
|
|
8
|
+
* {@link eventSourcingDBEventToNimbusEvent} to convert them
|
|
9
|
+
* into Nimbus events if needed.
|
|
10
|
+
*
|
|
11
|
+
* @param subject - The subject to read events for.
|
|
12
|
+
* @param options - Options to control which events are read.
|
|
13
|
+
* @param signal - An optional abort signal to cancel the read.
|
|
14
|
+
* @returns An async generator yielding EventSourcingDB events.
|
|
15
|
+
*/
|
|
16
|
+
export const readEvents = (subject, options, signal) => {
|
|
17
|
+
return withAsyncGeneratorSpan('readEvents', () => {
|
|
18
|
+
const eventSourcingDBClient = getEventSourcingDBClient();
|
|
19
|
+
return eventSourcingDBClient.readEvents(subject, options, signal);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const tracer: import("@opentelemetry/api").Tracer;
|
|
2
|
+
export declare const DB_SYSTEM = "eventsourcingdb";
|
|
3
|
+
/**
|
|
4
|
+
* Trace context extracted from an EventSourcingDB event, used to link
|
|
5
|
+
* the processing span to the span that originally wrote the event.
|
|
6
|
+
*/
|
|
7
|
+
export type TraceContext = {
|
|
8
|
+
traceparent: string;
|
|
9
|
+
tracestate?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Wraps an async function with OpenTelemetry tracing and metrics.
|
|
13
|
+
*
|
|
14
|
+
* Records:
|
|
15
|
+
* - `eventsourcingdb_operation_total` counter with operation and status labels
|
|
16
|
+
* - `eventsourcingdb_operation_duration_seconds` histogram with operation label
|
|
17
|
+
*
|
|
18
|
+
* @param operation - The EventSourcingDB operation name (e.g., 'readEvents', 'writeEvents')
|
|
19
|
+
* @param fn - The async function to execute within the span
|
|
20
|
+
* @param traceContext - Optional trace context from an EventSourcingDB event to
|
|
21
|
+
* continue a distributed trace from the event writer.
|
|
22
|
+
* @returns The result of the async function
|
|
23
|
+
*/
|
|
24
|
+
export declare const withSpan: <T>(operation: string, fn: () => Promise<T>, traceContext?: TraceContext) => Promise<T>;
|
|
25
|
+
/**
|
|
26
|
+
* Wraps an async generator with OpenTelemetry tracing and metrics.
|
|
27
|
+
*
|
|
28
|
+
* Records:
|
|
29
|
+
* - `eventsourcingdb_operation_total` counter with operation and status labels
|
|
30
|
+
* - `eventsourcingdb_operation_duration_seconds` histogram with operation label
|
|
31
|
+
*
|
|
32
|
+
* @param operation - The EventSourcingDB operation name (e.g., 'readEvents')
|
|
33
|
+
* @param fn - The function returning an async generator to execute within the span
|
|
34
|
+
* @returns An async generator that yields the same values as the inner generator
|
|
35
|
+
*/
|
|
36
|
+
export declare function withAsyncGeneratorSpan<T>(operation: string, fn: () => AsyncGenerator<T, void, void>): AsyncGenerator<T, void, void>;
|
|
37
|
+
//# sourceMappingURL=tracing.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { context as otelContext, metrics, propagation, SpanKind, SpanStatusCode, trace, } from '@opentelemetry/api';
|
|
2
|
+
export const tracer = trace.getTracer('nimbus');
|
|
3
|
+
export const DB_SYSTEM = 'eventsourcingdb';
|
|
4
|
+
const meter = metrics.getMeter('nimbus');
|
|
5
|
+
const operationCounter = meter.createCounter('eventsourcingdb_operation_total', {
|
|
6
|
+
description: 'Total number of EventSourcingDB operations',
|
|
7
|
+
});
|
|
8
|
+
const operationDuration = meter.createHistogram('eventsourcingdb_operation_duration_seconds', {
|
|
9
|
+
description: 'Duration of EventSourcingDB operations in seconds',
|
|
10
|
+
unit: 's',
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Wraps an async function with OpenTelemetry tracing and metrics.
|
|
14
|
+
*
|
|
15
|
+
* Records:
|
|
16
|
+
* - `eventsourcingdb_operation_total` counter with operation and status labels
|
|
17
|
+
* - `eventsourcingdb_operation_duration_seconds` histogram with operation label
|
|
18
|
+
*
|
|
19
|
+
* @param operation - The EventSourcingDB operation name (e.g., 'readEvents', 'writeEvents')
|
|
20
|
+
* @param fn - The async function to execute within the span
|
|
21
|
+
* @param traceContext - Optional trace context from an EventSourcingDB event to
|
|
22
|
+
* continue a distributed trace from the event writer.
|
|
23
|
+
* @returns The result of the async function
|
|
24
|
+
*/
|
|
25
|
+
export const withSpan = (operation, fn, traceContext) => {
|
|
26
|
+
const startTime = performance.now();
|
|
27
|
+
const metricLabels = {
|
|
28
|
+
operation,
|
|
29
|
+
};
|
|
30
|
+
const parentContext = traceContext
|
|
31
|
+
? propagation.extract(otelContext.active(), traceContext)
|
|
32
|
+
: otelContext.active();
|
|
33
|
+
return tracer.startActiveSpan(`eventsourcingdb.${operation}`, {
|
|
34
|
+
kind: SpanKind.CLIENT,
|
|
35
|
+
attributes: {
|
|
36
|
+
'db.system': DB_SYSTEM,
|
|
37
|
+
'db.operation': operation,
|
|
38
|
+
},
|
|
39
|
+
}, parentContext, async (span) => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await fn();
|
|
42
|
+
// Record success metrics
|
|
43
|
+
operationCounter.add(1, {
|
|
44
|
+
...metricLabels,
|
|
45
|
+
status: 'success',
|
|
46
|
+
});
|
|
47
|
+
operationDuration.record((performance.now() - startTime) / 1000, metricLabels);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// Record error metrics
|
|
52
|
+
operationCounter.add(1, {
|
|
53
|
+
...metricLabels,
|
|
54
|
+
status: 'error',
|
|
55
|
+
});
|
|
56
|
+
operationDuration.record((performance.now() - startTime) / 1000, metricLabels);
|
|
57
|
+
span.setStatus({
|
|
58
|
+
code: SpanStatusCode.ERROR,
|
|
59
|
+
message: error instanceof Error
|
|
60
|
+
? error.message
|
|
61
|
+
: 'Unknown error',
|
|
62
|
+
});
|
|
63
|
+
span.recordException(error instanceof Error ? error : new Error('Unknown error'));
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
span.end();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Wraps an async generator with OpenTelemetry tracing and metrics.
|
|
73
|
+
*
|
|
74
|
+
* Records:
|
|
75
|
+
* - `eventsourcingdb_operation_total` counter with operation and status labels
|
|
76
|
+
* - `eventsourcingdb_operation_duration_seconds` histogram with operation label
|
|
77
|
+
*
|
|
78
|
+
* @param operation - The EventSourcingDB operation name (e.g., 'readEvents')
|
|
79
|
+
* @param fn - The function returning an async generator to execute within the span
|
|
80
|
+
* @returns An async generator that yields the same values as the inner generator
|
|
81
|
+
*/
|
|
82
|
+
export async function* withAsyncGeneratorSpan(operation, fn) {
|
|
83
|
+
const startTime = performance.now();
|
|
84
|
+
const metricLabels = {
|
|
85
|
+
operation,
|
|
86
|
+
};
|
|
87
|
+
const span = tracer.startSpan(`eventsourcingdb.${operation}`, {
|
|
88
|
+
kind: SpanKind.CLIENT,
|
|
89
|
+
attributes: {
|
|
90
|
+
'db.system': DB_SYSTEM,
|
|
91
|
+
'db.operation': operation,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
yield* fn();
|
|
96
|
+
// Record success metrics
|
|
97
|
+
operationCounter.add(1, { ...metricLabels, status: 'success' });
|
|
98
|
+
operationDuration.record((performance.now() - startTime) / 1000, metricLabels);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Record error metrics
|
|
102
|
+
operationCounter.add(1, { ...metricLabels, status: 'error' });
|
|
103
|
+
operationDuration.record((performance.now() - startTime) / 1000, metricLabels);
|
|
104
|
+
span.setStatus({
|
|
105
|
+
code: SpanStatusCode.ERROR,
|
|
106
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
107
|
+
});
|
|
108
|
+
span.recordException(error instanceof Error ? error : new Error('Unknown error'));
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
span.end();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Event } from '../deps/jsr.io/@nimbus/core/2.0.0/src/index.js';
|
|
2
|
+
import type { Precondition } from 'eventsourcingdb';
|
|
3
|
+
/**
|
|
4
|
+
* Writes one or more Nimbus events to EventSourcingDB. Each event is
|
|
5
|
+
* converted into an EventSourcingDB event candidate before being persisted.
|
|
6
|
+
*
|
|
7
|
+
* @param events - The Nimbus events to write.
|
|
8
|
+
* @param preconditions - Optional preconditions that must be met for the write to succeed.
|
|
9
|
+
*/
|
|
10
|
+
export declare const writeEvents: (events: Event[], preconditions?: Precondition[]) => Promise<void>;
|
|
11
|
+
//# sourceMappingURL=writeEvents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writeEvents.d.ts","sourceRoot":"","sources":["../../src/lib/writeEvents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gDAAgD,CAAC;AAE5E,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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { context, propagation } from '@opentelemetry/api';
|
|
2
|
+
import { getEventSourcingDBClient } from './client.js';
|
|
3
|
+
import { nimbusEventToEventSourcingDBEventCandidate } from './eventMapping.js';
|
|
4
|
+
import { withSpan } from './tracing.js';
|
|
5
|
+
/**
|
|
6
|
+
* Writes one or more Nimbus events to EventSourcingDB. Each event is
|
|
7
|
+
* converted into an EventSourcingDB event candidate before being persisted.
|
|
8
|
+
*
|
|
9
|
+
* @param events - The Nimbus events to write.
|
|
10
|
+
* @param preconditions - Optional preconditions that must be met for the write to succeed.
|
|
11
|
+
*/
|
|
12
|
+
export const writeEvents = (events, preconditions) => {
|
|
13
|
+
return withSpan('writeEvents', async () => {
|
|
14
|
+
const eventSourcingDBClient = getEventSourcingDBClient();
|
|
15
|
+
const carrier = {};
|
|
16
|
+
propagation.inject(context.active(), carrier);
|
|
17
|
+
const eventCandidates = events.map((event) => nimbusEventToEventSourcingDBEventCandidate(event, carrier['traceparent'], carrier['tracestate']));
|
|
18
|
+
await eventSourcingDBClient.writeEvents(eventCandidates, preconditions);
|
|
19
|
+
});
|
|
20
|
+
};
|
package/esm/package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nimbus-cqrs/eventsourcingdb",
|
|
3
|
+
"version": "2.0.0-beta.0",
|
|
4
|
+
"description": "EventSourcingDB integration for the Nimbus CQRS framework.",
|
|
5
|
+
"author": "Daniel Gördes <d.goerdes@overlap.at> (https://overlap.at)",
|
|
6
|
+
"homepage": "https://nimbus.overlap.at",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/overlap-dev/Nimbus"
|
|
10
|
+
},
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/overlap-dev/Nimbus/issues"
|
|
14
|
+
},
|
|
15
|
+
"module": "./esm/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./esm/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@opentelemetry/api": "^1.9.1",
|
|
27
|
+
"eventsourcingdb": "^1.8.1",
|
|
28
|
+
"zod": "^4.3.6",
|
|
29
|
+
"@nimbus-cqrs/core": "^2.0.0-beta.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0"
|
|
33
|
+
},
|
|
34
|
+
"_generatedBy": "dnt@dev"
|
|
35
|
+
}
|