@nimbus-cqrs/core 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 +56 -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 +18 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +17 -0
- package/esm/lib/eventBus/eventBus.d.ts +329 -0
- package/esm/lib/eventBus/eventBus.d.ts.map +1 -0
- package/esm/lib/eventBus/eventBus.js +473 -0
- package/esm/lib/exception/exception.d.ts +10 -0
- package/esm/lib/exception/exception.d.ts.map +1 -0
- package/esm/lib/exception/exception.js +26 -0
- package/esm/lib/exception/forbiddenException.d.ts +8 -0
- package/esm/lib/exception/forbiddenException.d.ts.map +1 -0
- package/esm/lib/exception/forbiddenException.js +9 -0
- package/esm/lib/exception/genericException.d.ts +8 -0
- package/esm/lib/exception/genericException.d.ts.map +1 -0
- package/esm/lib/exception/genericException.js +9 -0
- package/esm/lib/exception/invalidInputException.d.ts +18 -0
- package/esm/lib/exception/invalidInputException.d.ts.map +1 -0
- package/esm/lib/exception/invalidInputException.js +28 -0
- package/esm/lib/exception/notFoundException.d.ts +8 -0
- package/esm/lib/exception/notFoundException.d.ts.map +1 -0
- package/esm/lib/exception/notFoundException.js +9 -0
- package/esm/lib/exception/unauthorizedException.d.ts +8 -0
- package/esm/lib/exception/unauthorizedException.d.ts.map +1 -0
- package/esm/lib/exception/unauthorizedException.js +9 -0
- package/esm/lib/log/logFormatter.d.ts +26 -0
- package/esm/lib/log/logFormatter.d.ts.map +1 -0
- package/esm/lib/log/logFormatter.js +59 -0
- package/esm/lib/log/logLevel.d.ts +24 -0
- package/esm/lib/log/logLevel.d.ts.map +1 -0
- package/esm/lib/log/logLevel.js +27 -0
- package/esm/lib/log/logger.d.ts +377 -0
- package/esm/lib/log/logger.d.ts.map +1 -0
- package/esm/lib/log/logger.js +394 -0
- package/esm/lib/log/options.d.ts +34 -0
- package/esm/lib/log/options.d.ts.map +1 -0
- package/esm/lib/log/options.js +9 -0
- package/esm/lib/message/command.d.ts +100 -0
- package/esm/lib/message/command.d.ts.map +1 -0
- package/esm/lib/message/command.js +47 -0
- package/esm/lib/message/event.d.ts +103 -0
- package/esm/lib/message/event.d.ts.map +1 -0
- package/esm/lib/message/event.js +47 -0
- package/esm/lib/message/message.d.ts +18 -0
- package/esm/lib/message/message.d.ts.map +1 -0
- package/esm/lib/message/message.js +1 -0
- package/esm/lib/message/query.d.ts +95 -0
- package/esm/lib/message/query.d.ts.map +1 -0
- package/esm/lib/message/query.js +45 -0
- package/esm/lib/message/router.d.ts +233 -0
- package/esm/lib/message/router.d.ts.map +1 -0
- package/esm/lib/message/router.js +304 -0
- package/esm/lib/tracing/withSpan.d.ts +67 -0
- package/esm/lib/tracing/withSpan.d.ts.map +1 -0
- package/esm/lib/tracing/withSpan.js +87 -0
- package/esm/package.json +3 -0
- package/package.json +33 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { metrics, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
|
|
2
|
+
import EventEmitter from 'node:events';
|
|
3
|
+
import { GenericException } from '../exception/genericException.js';
|
|
4
|
+
import { getLogger } from '../log/logger.js';
|
|
5
|
+
const tracer = trace.getTracer('nimbus');
|
|
6
|
+
const meter = metrics.getMeter('nimbus');
|
|
7
|
+
const eventsPublishedCounter = meter.createCounter('eventbus_events_published_total', {
|
|
8
|
+
description: 'Total number of events published to the event bus',
|
|
9
|
+
});
|
|
10
|
+
const eventsDeliveredCounter = meter.createCounter('eventbus_events_delivered_total', {
|
|
11
|
+
description: 'Total number of events delivered to handlers',
|
|
12
|
+
});
|
|
13
|
+
const handlingDuration = meter.createHistogram('eventbus_event_handling_duration_seconds', {
|
|
14
|
+
description: 'Duration of event handler execution in seconds',
|
|
15
|
+
unit: 's',
|
|
16
|
+
});
|
|
17
|
+
const retryAttemptsCounter = meter.createCounter('eventbus_retry_attempts_total', {
|
|
18
|
+
description: 'Total number of retry attempts for event handling',
|
|
19
|
+
});
|
|
20
|
+
const eventSizeBytes = meter.createHistogram('eventbus_event_size_bytes', {
|
|
21
|
+
description: 'Size of events published to the event bus in bytes',
|
|
22
|
+
unit: 'By',
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* The NimbusEventBus is used to publish and subscribe to events within the application.
|
|
26
|
+
*
|
|
27
|
+
* Events are delivered asynchronously to all registered handlers. If a handler fails,
|
|
28
|
+
* it will be retried using exponential backoff until it succeeds or the maximum retry
|
|
29
|
+
* count is reached.
|
|
30
|
+
*
|
|
31
|
+
* All operations are instrumented with OpenTelemetry tracing and metrics for observability.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { createEvent, NimbusEventBus } from '@nimbus/core';
|
|
36
|
+
*
|
|
37
|
+
* const eventBus = new NimbusEventBus({
|
|
38
|
+
* name: 'orders',
|
|
39
|
+
* maxRetries: 3,
|
|
40
|
+
* baseDelay: 1000,
|
|
41
|
+
* maxDelay: 30000,
|
|
42
|
+
* useJitter: true,
|
|
43
|
+
* logPublish: (event) => {
|
|
44
|
+
* console.log('Event published:', event.type, event.correlationid);
|
|
45
|
+
* },
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* // Subscribe to events
|
|
49
|
+
* eventBus.subscribeEvent({
|
|
50
|
+
* type: 'at.overlap.nimbus.order-created',
|
|
51
|
+
* handler: async (event) => {
|
|
52
|
+
* console.log('Order created:', event.data);
|
|
53
|
+
* },
|
|
54
|
+
* onError: (error, event) => {
|
|
55
|
+
* console.error('Failed to handle event:', event.id, error.message);
|
|
56
|
+
* },
|
|
57
|
+
* options: {
|
|
58
|
+
* maxRetries: 5,
|
|
59
|
+
* baseDelay: 500,
|
|
60
|
+
* maxDelay: 15000,
|
|
61
|
+
* useJitter: true,
|
|
62
|
+
* },
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Publish an event
|
|
66
|
+
* const event = createEvent({
|
|
67
|
+
* type: 'at.overlap.nimbus.order-created',
|
|
68
|
+
* source: 'https://api.example.com',
|
|
69
|
+
* correlationid: '550e8400-e29b-41d4-a716-446655440000',
|
|
70
|
+
* subject: '/orders/12345',
|
|
71
|
+
* data: { orderId: '12345', customerId: '67890' },
|
|
72
|
+
* datacontenttype: 'application/json',
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* eventBus.putEvent(event);
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export class NimbusEventBus {
|
|
79
|
+
_eventEmitter;
|
|
80
|
+
_name;
|
|
81
|
+
_maxRetries;
|
|
82
|
+
_baseDelay;
|
|
83
|
+
_maxDelay;
|
|
84
|
+
_useJitter;
|
|
85
|
+
_logPublish;
|
|
86
|
+
/**
|
|
87
|
+
* Create a new NimbusEventBus instance.
|
|
88
|
+
*
|
|
89
|
+
* @param options - The options for the event bus.
|
|
90
|
+
* @param options.name - The name of the event bus instance for metrics and traces. Defaults to 'default'.
|
|
91
|
+
* @param options.maxRetries - The maximum number of retries for handling the event in case of an error. Defaults to 2.
|
|
92
|
+
* @param options.baseDelay - The base delay for exponential backoff in milliseconds. Defaults to 1000ms.
|
|
93
|
+
* @param options.maxDelay - The maximum delay cap for exponential backoff in milliseconds. Defaults to 30000ms.
|
|
94
|
+
* @param options.useJitter - Whether to add jitter to the retry delay. Defaults to true.
|
|
95
|
+
* @param options.logPublish - Optional callback invoked when an event is published.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* import { getLogger, NimbusEventBus } from '@nimbus/core';
|
|
100
|
+
*
|
|
101
|
+
* const eventBus = new NimbusEventBus({
|
|
102
|
+
* name: 'orders',
|
|
103
|
+
* maxRetries: 3,
|
|
104
|
+
* baseDelay: 1000,
|
|
105
|
+
* maxDelay: 30000,
|
|
106
|
+
* useJitter: true,
|
|
107
|
+
* logPublish: (event) => {
|
|
108
|
+
* getLogger().debug({
|
|
109
|
+
* category: 'EventBus',
|
|
110
|
+
* message: 'Published event',
|
|
111
|
+
* data: { type: event.type, id: event.id },
|
|
112
|
+
* correlationId: event.correlationid,
|
|
113
|
+
* });
|
|
114
|
+
* },
|
|
115
|
+
* });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
constructor(options) {
|
|
119
|
+
this._eventEmitter = new EventEmitter();
|
|
120
|
+
this._name = options?.name ?? 'default';
|
|
121
|
+
this._maxRetries = options?.maxRetries ?? 2;
|
|
122
|
+
this._baseDelay = options?.baseDelay ?? 1000;
|
|
123
|
+
this._maxDelay = options?.maxDelay ?? 30000;
|
|
124
|
+
this._useJitter = options?.useJitter ?? true;
|
|
125
|
+
this._logPublish = options?.logPublish;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Publish an event to the event bus.
|
|
129
|
+
*
|
|
130
|
+
* The event is validated against the CloudEvents 64KB size limit before publishing.
|
|
131
|
+
* All subscribers registered for this event type will receive the event asynchronously.
|
|
132
|
+
*
|
|
133
|
+
* @param event - The CloudEvents-compliant event to publish.
|
|
134
|
+
* @throws {GenericException} If the event size exceeds 64KB.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* import { createEvent, getEventBus } from '@nimbus/core';
|
|
139
|
+
*
|
|
140
|
+
* const eventBus = getEventBus('default');
|
|
141
|
+
*
|
|
142
|
+
* // Create and publish an event with all CloudEvents properties
|
|
143
|
+
* const event = createEvent({
|
|
144
|
+
* type: 'at.overlap.nimbus.order-created',
|
|
145
|
+
* source: 'https://api.example.com',
|
|
146
|
+
* correlationid: '550e8400-e29b-41d4-a716-446655440000',
|
|
147
|
+
* subject: '/orders/12345',
|
|
148
|
+
* data: {
|
|
149
|
+
* orderId: '12345',
|
|
150
|
+
* customerId: '67890',
|
|
151
|
+
* items: ['item-1', 'item-2'],
|
|
152
|
+
* total: 99.99,
|
|
153
|
+
* },
|
|
154
|
+
* datacontenttype: 'application/json',
|
|
155
|
+
* dataschema: 'https://schemas.example.com/order-created.json',
|
|
156
|
+
* });
|
|
157
|
+
*
|
|
158
|
+
* eventBus.putEvent(event);
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
putEvent(event) {
|
|
162
|
+
const eventSize = this._validateEventSize(event);
|
|
163
|
+
const metricLabels = {
|
|
164
|
+
eventbus_name: this._name,
|
|
165
|
+
event_type: event.type,
|
|
166
|
+
};
|
|
167
|
+
tracer.startActiveSpan('eventbus.publish', {
|
|
168
|
+
kind: SpanKind.PRODUCER,
|
|
169
|
+
attributes: {
|
|
170
|
+
'messaging.system': 'nimbusEventBus',
|
|
171
|
+
'messaging.eventbus_name': this._name,
|
|
172
|
+
'messaging.operation': 'publish',
|
|
173
|
+
'messaging.destination': event.type,
|
|
174
|
+
'cloudevents.event_id': event.id,
|
|
175
|
+
'cloudevents.event_source': event.source,
|
|
176
|
+
...(event.correlationid && {
|
|
177
|
+
correlation_id: event.correlationid,
|
|
178
|
+
}),
|
|
179
|
+
},
|
|
180
|
+
}, (span) => {
|
|
181
|
+
try {
|
|
182
|
+
eventsPublishedCounter.add(1, metricLabels);
|
|
183
|
+
eventSizeBytes.record(eventSize, metricLabels);
|
|
184
|
+
if (this._logPublish) {
|
|
185
|
+
this._logPublish(event);
|
|
186
|
+
}
|
|
187
|
+
this._eventEmitter.emit(event.type, event);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
span.setStatus({
|
|
191
|
+
code: SpanStatusCode.ERROR,
|
|
192
|
+
message: error instanceof Error
|
|
193
|
+
? error.message
|
|
194
|
+
: 'Unknown error',
|
|
195
|
+
});
|
|
196
|
+
span.recordException(error instanceof Error
|
|
197
|
+
? error
|
|
198
|
+
: new Error('Unknown error'));
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
span.end();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Subscribe to an event type with a handler function.
|
|
208
|
+
*
|
|
209
|
+
* When an event matching the specified type is published, the handler is invoked.
|
|
210
|
+
* If the handler throws an error, it will be retried using exponential backoff
|
|
211
|
+
* (delay doubles with each attempt) until either it succeeds or the maximum retry
|
|
212
|
+
* count is reached.
|
|
213
|
+
*
|
|
214
|
+
* @param input - The subscription configuration.
|
|
215
|
+
* @param input.type - The CloudEvents event type to subscribe to.
|
|
216
|
+
* @param input.handler - The async handler function to process events.
|
|
217
|
+
* @param input.onError - Optional callback invoked when all retries are exhausted.
|
|
218
|
+
* @param input.options - Optional retry options to override EventBus defaults.
|
|
219
|
+
* @param input.options.maxRetries - Override maximum retry attempts for this subscription.
|
|
220
|
+
* @param input.options.baseDelay - Override base delay in milliseconds for this subscription.
|
|
221
|
+
* @param input.options.maxDelay - Override maximum delay cap in milliseconds for this subscription.
|
|
222
|
+
* @param input.options.useJitter - Override jitter setting for this subscription.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* import { getEventBus, getLogger } from '@nimbus/core';
|
|
227
|
+
*
|
|
228
|
+
* const eventBus = getEventBus('default');
|
|
229
|
+
*
|
|
230
|
+
* // Subscribe with all available options
|
|
231
|
+
* eventBus.subscribeEvent({
|
|
232
|
+
* type: 'at.overlap.nimbus.order-created',
|
|
233
|
+
* handler: async (event) => {
|
|
234
|
+
* // Process the event
|
|
235
|
+
* console.log('Order created:', event.data.orderId);
|
|
236
|
+
* console.log('Correlation ID:', event.correlationid);
|
|
237
|
+
* },
|
|
238
|
+
* onError: (error, event) => {
|
|
239
|
+
* getLogger().error({
|
|
240
|
+
* category: 'OrderHandler',
|
|
241
|
+
* message: 'Failed to process order after all retries',
|
|
242
|
+
* data: { eventId: event.id, orderId: event.data.orderId },
|
|
243
|
+
* error,
|
|
244
|
+
* correlationId: event.correlationid,
|
|
245
|
+
* });
|
|
246
|
+
* },
|
|
247
|
+
* options: {
|
|
248
|
+
* maxRetries: 5,
|
|
249
|
+
* baseDelay: 500,
|
|
250
|
+
* maxDelay: 15000,
|
|
251
|
+
* useJitter: true,
|
|
252
|
+
* },
|
|
253
|
+
* });
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
subscribeEvent({ type, handler, onError, options, }) {
|
|
257
|
+
getLogger().debug({
|
|
258
|
+
category: 'Nimbus',
|
|
259
|
+
message: `Subscribed to ${type} event`,
|
|
260
|
+
});
|
|
261
|
+
const maxRetries = options?.maxRetries ?? this._maxRetries;
|
|
262
|
+
const baseDelay = options?.baseDelay ?? this._baseDelay;
|
|
263
|
+
const maxDelay = options?.maxDelay ?? this._maxDelay;
|
|
264
|
+
const useJitter = options?.useJitter ?? this._useJitter;
|
|
265
|
+
const handleEvent = async (event) => {
|
|
266
|
+
try {
|
|
267
|
+
await this._processEvent(handler, event, maxRetries, baseDelay, maxDelay, useJitter);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (onError) {
|
|
271
|
+
onError(error, event);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
getLogger().error({
|
|
275
|
+
category: 'Nimbus',
|
|
276
|
+
message: error.message,
|
|
277
|
+
error,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
this._eventEmitter.on(type, handleEvent);
|
|
283
|
+
}
|
|
284
|
+
_processEvent(handler, event, maxRetries, baseDelay, maxDelay, useJitter) {
|
|
285
|
+
const startTime = performance.now();
|
|
286
|
+
const metricLabels = {
|
|
287
|
+
eventbus_name: this._name,
|
|
288
|
+
event_type: event.type,
|
|
289
|
+
};
|
|
290
|
+
return tracer.startActiveSpan('eventbus.handle', {
|
|
291
|
+
kind: SpanKind.CONSUMER,
|
|
292
|
+
attributes: {
|
|
293
|
+
'messaging.system': 'nimbusEventBus',
|
|
294
|
+
'messaging.eventbus_name': this._name,
|
|
295
|
+
'messaging.operation': 'process',
|
|
296
|
+
'messaging.destination': event.type,
|
|
297
|
+
'cloudevents.event_id': event.id,
|
|
298
|
+
'cloudevents.event_source': event.source,
|
|
299
|
+
...(event.correlationid && {
|
|
300
|
+
correlation_id: event.correlationid,
|
|
301
|
+
}),
|
|
302
|
+
},
|
|
303
|
+
}, async (span) => {
|
|
304
|
+
let attempt = 0;
|
|
305
|
+
while (attempt <= maxRetries) {
|
|
306
|
+
try {
|
|
307
|
+
await handler(event);
|
|
308
|
+
this._recordDeliveryMetrics(metricLabels, 'success', startTime);
|
|
309
|
+
span.end();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
attempt++;
|
|
314
|
+
if (attempt > maxRetries) {
|
|
315
|
+
this._handleFinalFailure({
|
|
316
|
+
error,
|
|
317
|
+
event,
|
|
318
|
+
span,
|
|
319
|
+
metricLabels,
|
|
320
|
+
startTime,
|
|
321
|
+
retryConfig: {
|
|
322
|
+
maxRetries,
|
|
323
|
+
baseDelay,
|
|
324
|
+
maxDelay,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
retryAttemptsCounter.add(1, metricLabels);
|
|
329
|
+
const delayMs = this._calculateRetryDelay(attempt, baseDelay, maxDelay, useJitter);
|
|
330
|
+
span.addEvent('retry', { attempt, delay_ms: delayMs });
|
|
331
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
_recordDeliveryMetrics(metricLabels, status, startTime) {
|
|
337
|
+
eventsDeliveredCounter.add(1, { ...metricLabels, status });
|
|
338
|
+
handlingDuration.record((performance.now() - startTime) / 1000, metricLabels);
|
|
339
|
+
}
|
|
340
|
+
_handleFinalFailure(options) {
|
|
341
|
+
const { error, event, span, metricLabels, startTime, retryConfig } = options;
|
|
342
|
+
this._recordDeliveryMetrics(metricLabels, 'error', startTime);
|
|
343
|
+
const errorMessage = error instanceof Error
|
|
344
|
+
? error.message
|
|
345
|
+
: 'Unknown error';
|
|
346
|
+
const errorInstance = error instanceof Error
|
|
347
|
+
? error
|
|
348
|
+
: new Error('Unknown error');
|
|
349
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });
|
|
350
|
+
span.recordException(errorInstance);
|
|
351
|
+
span.end();
|
|
352
|
+
const exception = new GenericException(`Failed to handle event: ${event.type} from ${event.source}`, retryConfig);
|
|
353
|
+
if (error instanceof Error && error.stack) {
|
|
354
|
+
exception.stack = error.stack;
|
|
355
|
+
}
|
|
356
|
+
throw exception;
|
|
357
|
+
}
|
|
358
|
+
_calculateRetryDelay(attempt, baseDelay, maxDelay, useJitter) {
|
|
359
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
360
|
+
const jitter = useJitter ? Math.random() * delay * 0.1 : 0;
|
|
361
|
+
return delay + jitter;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Validate the size of the event and return the size in bytes.
|
|
365
|
+
*
|
|
366
|
+
* To comply with the CloudEvent spec a transmitted event
|
|
367
|
+
* can only have a maximum size of 64KB.
|
|
368
|
+
*
|
|
369
|
+
* @param event - The event to validate.
|
|
370
|
+
* @returns The size of the event in bytes.
|
|
371
|
+
*/
|
|
372
|
+
_validateEventSize(event) {
|
|
373
|
+
const eventJson = JSON.stringify(event);
|
|
374
|
+
const size = new TextEncoder().encode(eventJson).length;
|
|
375
|
+
const maxSizeBytes = 64 * 1024; // 64KB
|
|
376
|
+
if (size > maxSizeBytes) {
|
|
377
|
+
throw new GenericException(`Event size exceeds the limit of 64KB`, {
|
|
378
|
+
eventType: event.type,
|
|
379
|
+
eventSource: event.source,
|
|
380
|
+
eventSizeBytes: size,
|
|
381
|
+
maxSizeBytes,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return size;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Registry to store named EventBus instances.
|
|
389
|
+
*/
|
|
390
|
+
const eventBusRegistry = new Map();
|
|
391
|
+
/**
|
|
392
|
+
* Setup a named EventBus instance and register it for later retrieval.
|
|
393
|
+
*
|
|
394
|
+
* Use this function to configure an EventBus with specific options at application
|
|
395
|
+
* startup, then retrieve it later using {@link getEventBus}.
|
|
396
|
+
*
|
|
397
|
+
* @param name - The unique name for this EventBus instance.
|
|
398
|
+
* @param options - Optional configuration options for the EventBus.
|
|
399
|
+
* @param options.maxRetries - The maximum number of retries for handling events. Defaults to 2.
|
|
400
|
+
* @param options.baseDelay - The base delay for exponential backoff in milliseconds. Defaults to 1000ms.
|
|
401
|
+
* @param options.maxDelay - The maximum delay cap for exponential backoff in milliseconds. Defaults to 30000ms.
|
|
402
|
+
* @param options.useJitter - Whether to add jitter to the retry delay. Defaults to true.
|
|
403
|
+
* @param options.logPublish - Optional callback invoked when an event is published.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* import { getLogger, setupEventBus } from '@nimbus/core';
|
|
408
|
+
*
|
|
409
|
+
* // At application startup, configure the event bus with all options
|
|
410
|
+
* setupEventBus('default', {
|
|
411
|
+
* maxRetries: 3,
|
|
412
|
+
* baseDelay: 1000,
|
|
413
|
+
* maxDelay: 30000,
|
|
414
|
+
* useJitter: true,
|
|
415
|
+
* logPublish: (event) => {
|
|
416
|
+
* getLogger().debug({
|
|
417
|
+
* category: 'EventBus',
|
|
418
|
+
* message: 'Published event',
|
|
419
|
+
* data: { type: event.type, id: event.id },
|
|
420
|
+
* correlationId: event.correlationid,
|
|
421
|
+
* });
|
|
422
|
+
* },
|
|
423
|
+
* });
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
export const setupEventBus = (name, options) => {
|
|
427
|
+
eventBusRegistry.set(name, new NimbusEventBus({ ...options, name }));
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Get a named EventBus instance.
|
|
431
|
+
*
|
|
432
|
+
* If an EventBus with the given name has been configured via {@link setupEventBus},
|
|
433
|
+
* that instance is returned. Otherwise, a new EventBus with default options is created
|
|
434
|
+
* and registered.
|
|
435
|
+
*
|
|
436
|
+
* @param name - The name of the EventBus instance to retrieve. Defaults to 'default'.
|
|
437
|
+
* @returns The NimbusEventBus instance.
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```ts
|
|
441
|
+
* import { createEvent, getEventBus } from '@nimbus/core';
|
|
442
|
+
*
|
|
443
|
+
* // Get the event bus configured earlier with setupEventBus
|
|
444
|
+
* const eventBus = getEventBus('default');
|
|
445
|
+
*
|
|
446
|
+
* // Subscribe to events
|
|
447
|
+
* eventBus.subscribeEvent({
|
|
448
|
+
* type: 'at.overlap.nimbus.order-created',
|
|
449
|
+
* handler: async (event) => {
|
|
450
|
+
* console.log('Order created:', event.data.orderId);
|
|
451
|
+
* },
|
|
452
|
+
* });
|
|
453
|
+
*
|
|
454
|
+
* // Publish an event
|
|
455
|
+
* const event = createEvent({
|
|
456
|
+
* type: 'at.overlap.nimbus.order-created',
|
|
457
|
+
* source: 'https://api.example.com',
|
|
458
|
+
* correlationid: '550e8400-e29b-41d4-a716-446655440000',
|
|
459
|
+
* data: { orderId: '12345', customerId: '67890' },
|
|
460
|
+
* datacontenttype: 'application/json',
|
|
461
|
+
* });
|
|
462
|
+
*
|
|
463
|
+
* eventBus.putEvent(event);
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
export const getEventBus = (name = 'default') => {
|
|
467
|
+
let eventBus = eventBusRegistry.get(name);
|
|
468
|
+
if (!eventBus) {
|
|
469
|
+
eventBus = new NimbusEventBus({ name });
|
|
470
|
+
eventBusRegistry.set(name, eventBus);
|
|
471
|
+
}
|
|
472
|
+
return eventBus;
|
|
473
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base exception
|
|
3
|
+
*/
|
|
4
|
+
export declare class Exception extends Error {
|
|
5
|
+
details?: Record<string, unknown>;
|
|
6
|
+
statusCode?: number;
|
|
7
|
+
constructor(name: string, message: string, details?: Record<string, unknown>, statusCode?: number);
|
|
8
|
+
fromError(error: Error): Exception;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=exception.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exception.d.ts","sourceRoot":"","sources":["../../../src/lib/exception/exception.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;gBAGvB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,UAAU,CAAC,EAAE,MAAM;IAmBhB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS;CAM5C"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base exception
|
|
3
|
+
*/
|
|
4
|
+
export class Exception extends Error {
|
|
5
|
+
details;
|
|
6
|
+
statusCode;
|
|
7
|
+
constructor(name, message, details, statusCode) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = name;
|
|
10
|
+
if (details) {
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
if (statusCode) {
|
|
14
|
+
this.statusCode = statusCode;
|
|
15
|
+
}
|
|
16
|
+
// Maintains proper stack trace in V8 environments
|
|
17
|
+
if (Error.captureStackTrace) {
|
|
18
|
+
Error.captureStackTrace(this, this.constructor);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
fromError(error) {
|
|
22
|
+
this.message = error.message;
|
|
23
|
+
this.stack = error.stack;
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forbiddenException.d.ts","sourceRoot":"","sources":["../../../src/lib/exception/forbiddenException.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;gBACjC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAGlE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"genericException.d.ts","sourceRoot":"","sources":["../../../src/lib/exception/genericException.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,SAAS;gBAC/B,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAQlE"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ZodError } from 'zod';
|
|
2
|
+
import { Exception } from './exception.js';
|
|
3
|
+
/**
|
|
4
|
+
* Invalid input exception
|
|
5
|
+
*/
|
|
6
|
+
export declare class InvalidInputException extends Exception {
|
|
7
|
+
constructor(message?: string, details?: Record<string, unknown>);
|
|
8
|
+
/**
|
|
9
|
+
* Takes a Zod error and creates an InvalidInputException from it.
|
|
10
|
+
* Takes care to preserve the stack and issues from the Zod error.
|
|
11
|
+
*
|
|
12
|
+
* @param {ZodError} error - The Zod error.
|
|
13
|
+
*
|
|
14
|
+
* @returns {InvalidInputException} The InvalidInputException.
|
|
15
|
+
*/
|
|
16
|
+
fromZodError(error: ZodError): InvalidInputException;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=invalidInputException.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalidInputException.d.ts","sourceRoot":"","sources":["../../../src/lib/exception/invalidInputException.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,SAAS;gBACpC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAS/D;;;;;;;OAOG;IACI,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,qBAAqB;CAa9D"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Exception } from './exception.js';
|
|
2
|
+
/**
|
|
3
|
+
* Invalid input exception
|
|
4
|
+
*/
|
|
5
|
+
export class InvalidInputException extends Exception {
|
|
6
|
+
constructor(message, details) {
|
|
7
|
+
super('INVALID_INPUT', message ?? 'Invalid input', details, 400);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Takes a Zod error and creates an InvalidInputException from it.
|
|
11
|
+
* Takes care to preserve the stack and issues from the Zod error.
|
|
12
|
+
*
|
|
13
|
+
* @param {ZodError} error - The Zod error.
|
|
14
|
+
*
|
|
15
|
+
* @returns {InvalidInputException} The InvalidInputException.
|
|
16
|
+
*/
|
|
17
|
+
fromZodError(error) {
|
|
18
|
+
if (error.stack) {
|
|
19
|
+
this.stack = error.stack;
|
|
20
|
+
}
|
|
21
|
+
if (error.issues) {
|
|
22
|
+
this.details = {
|
|
23
|
+
issues: error.issues,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notFoundException.d.ts","sourceRoot":"","sources":["../../../src/lib/exception/notFoundException.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,SAAS;gBAChC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAGlE"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Exception } from './exception.js';
|
|
2
|
+
/**
|
|
3
|
+
* Unauthorized exception
|
|
4
|
+
*/
|
|
5
|
+
export declare class UnauthorizedException extends Exception {
|
|
6
|
+
constructor(message?: string, details?: Record<string, unknown>);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=unauthorizedException.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unauthorizedException.d.ts","sourceRoot":"","sources":["../../../src/lib/exception/unauthorizedException.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,SAAS;gBACpC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAQlE"}
|