@cap-js-community/event-queue 2.1.0-beta.0 → 2.1.0-beta.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.2",
|
|
4
4
|
"description": "An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -22,8 +22,6 @@
|
|
|
22
22
|
"multi-tenancy"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"start": "PORT=4005 cds-serve",
|
|
26
|
-
"watch": "PORT=4005 cds watch",
|
|
27
25
|
"test:unit": "jest --selectProjects unit",
|
|
28
26
|
"test:integration": "jest --selectProjects integration --runInBand",
|
|
29
27
|
"voter:test:integration": "jest --selectProjects integration",
|
|
@@ -38,13 +36,12 @@
|
|
|
38
36
|
"eslint:ci": "eslint .",
|
|
39
37
|
"prettier": "prettier --write --loglevel error .",
|
|
40
38
|
"prettier:ci": "prettier --check .",
|
|
41
|
-
"prepareRelease": "npm prune --production",
|
|
42
39
|
"docs": "cd docs && bundle exec jekyll serve",
|
|
43
40
|
"docs:install": "cd docs && npx shx rm -rf vendor Gemfile.lock && bundle install",
|
|
44
41
|
"upgrade-lock": "npx shx rm -rf package-lock.json node_modules && npm i --package-lock"
|
|
45
42
|
},
|
|
46
43
|
"engines": {
|
|
47
|
-
"node": ">=
|
|
44
|
+
"node": ">=20"
|
|
48
45
|
},
|
|
49
46
|
"dependencies": {
|
|
50
47
|
"@cap-js-community/common": "^0.3.4",
|
|
@@ -408,7 +408,7 @@ class EventQueueProcessorBase {
|
|
|
408
408
|
UPDATE.entity(this.#config.tableNameEventQueue)
|
|
409
409
|
.set({
|
|
410
410
|
status: status,
|
|
411
|
-
...(error && { error: this
|
|
411
|
+
...(error && { error: this._error2String(error) }),
|
|
412
412
|
})
|
|
413
413
|
.where({
|
|
414
414
|
ID: queueEntryIds,
|
|
@@ -498,16 +498,22 @@ class EventQueueProcessorBase {
|
|
|
498
498
|
}
|
|
499
499
|
|
|
500
500
|
if (data.error) {
|
|
501
|
-
data.error = this
|
|
501
|
+
data.error = this._error2String(data.error);
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
if (!data.startAfter && [EventProcessingStatus.Error, EventProcessingStatus.Open].includes(data.status)) {
|
|
505
505
|
data.startAfter = new Date(
|
|
506
506
|
Date.now() +
|
|
507
|
-
(
|
|
507
|
+
(data.status === EventProcessingStatus.Error
|
|
508
|
+
? this.#eventConfig.retryFailedAfter
|
|
509
|
+
: this.#eventConfig.retryOpenAfter)
|
|
508
510
|
);
|
|
509
511
|
}
|
|
510
512
|
|
|
513
|
+
if (data.status === EventProcessingStatus.Open && !("attempts" in data)) {
|
|
514
|
+
data.attempts = { "-=": 1 };
|
|
515
|
+
}
|
|
516
|
+
|
|
511
517
|
if (data.startAfter) {
|
|
512
518
|
this.#eventSchedulerInstance.scheduleEvent(
|
|
513
519
|
this.__context.tenant,
|
|
@@ -535,7 +541,7 @@ class EventQueueProcessorBase {
|
|
|
535
541
|
});
|
|
536
542
|
}
|
|
537
543
|
|
|
538
|
-
|
|
544
|
+
_error2String(error) {
|
|
539
545
|
return JSON.stringify(error, (_, value) => this.#errorReplacer(value));
|
|
540
546
|
}
|
|
541
547
|
|
package/src/config.js
CHANGED
|
@@ -64,7 +64,7 @@ const ALLOWED_EVENT_OPTIONS_AD_HOC = [
|
|
|
64
64
|
"checkForNextChunk",
|
|
65
65
|
"retryFailedAfter",
|
|
66
66
|
"propagateHeaders",
|
|
67
|
-
"
|
|
67
|
+
"propagateContextProperties",
|
|
68
68
|
"retryOpenAfter",
|
|
69
69
|
"multiInstanceProcessing",
|
|
70
70
|
"kind",
|
|
@@ -518,7 +518,7 @@ class Config {
|
|
|
518
518
|
? Object.fromEntries(new Map(event.appInstances.map((a) => [a, true])))
|
|
519
519
|
: null;
|
|
520
520
|
event.propagateHeaders = event.propagateHeaders ?? [];
|
|
521
|
-
event.
|
|
521
|
+
event.propagateContextProperties = event.propagateContextProperties ?? [];
|
|
522
522
|
event.retryFailedAfter = event.retryFailedAfter ?? DEFAULT_RETRY_AFTER;
|
|
523
523
|
event.retryOpenAfter = event.retryOpenAfter ?? DEFAULT_RETRY_AFTER;
|
|
524
524
|
}
|
|
@@ -64,7 +64,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
64
64
|
}
|
|
65
65
|
} else {
|
|
66
66
|
for (const actionName in genericClusterEvents) {
|
|
67
|
-
const
|
|
67
|
+
const req = new cds.Request({
|
|
68
68
|
event: EVENT_QUEUE_ACTIONS.CLUSTER,
|
|
69
69
|
user: this.context.user,
|
|
70
70
|
eventQueue: {
|
|
@@ -77,14 +77,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
77
77
|
this.#clusterByDataProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
|
-
const clusterResult = await this.__srvUnboxed.tx(this.context).send(
|
|
80
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(req);
|
|
81
81
|
if (this.#validateCluster(clusterResult)) {
|
|
82
82
|
Object.assign(clusterMap, clusterResult);
|
|
83
83
|
} else {
|
|
84
84
|
this.logger.error(
|
|
85
85
|
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
86
86
|
{
|
|
87
|
-
handler:
|
|
87
|
+
handler: req.event,
|
|
88
88
|
clusterResult: JSON.stringify(clusterResult),
|
|
89
89
|
}
|
|
90
90
|
);
|
|
@@ -95,7 +95,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
for (const actionName in specificClusterEvents) {
|
|
98
|
-
const
|
|
98
|
+
const req = new cds.Request({
|
|
99
99
|
event: `${EVENT_QUEUE_ACTIONS.CLUSTER}.${actionName}`,
|
|
100
100
|
user: this.context.user,
|
|
101
101
|
eventQueue: {
|
|
@@ -108,14 +108,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
108
108
|
this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
109
109
|
},
|
|
110
110
|
});
|
|
111
|
-
const clusterResult = await this.__srvUnboxed.tx(this.context).send(
|
|
111
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(req);
|
|
112
112
|
if (this.#validateCluster(clusterResult)) {
|
|
113
113
|
Object.assign(clusterMap, clusterResult);
|
|
114
114
|
} else {
|
|
115
115
|
this.logger.error(
|
|
116
116
|
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
117
117
|
{
|
|
118
|
-
handler:
|
|
118
|
+
handler: req.event,
|
|
119
119
|
clusterResult: JSON.stringify(clusterResult),
|
|
120
120
|
}
|
|
121
121
|
);
|
|
@@ -267,12 +267,12 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
267
267
|
return payload;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
const {
|
|
270
|
+
const { req, userId } = this.#buildDispatchData(payload, {
|
|
271
271
|
queueEntries: [queueEntry],
|
|
272
272
|
});
|
|
273
|
-
|
|
274
|
-
await this.#setContextUser(this.context, userId,
|
|
275
|
-
const data = await this.__srvUnboxed.tx(this.context).send(
|
|
273
|
+
req.event = handlerName;
|
|
274
|
+
await this.#setContextUser(this.context, userId, req);
|
|
275
|
+
const data = await this.__srvUnboxed.tx(this.context).send(req);
|
|
276
276
|
if (data) {
|
|
277
277
|
payload.data = data;
|
|
278
278
|
return payload;
|
|
@@ -288,12 +288,12 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
288
288
|
return await super.hookForExceededEvents(exceededEvent);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
const {
|
|
291
|
+
const { req, userId } = this.#buildDispatchData(exceededEvent.payload, {
|
|
292
292
|
queueEntries: [exceededEvent],
|
|
293
293
|
});
|
|
294
|
-
await this.#setContextUser(this.context, userId,
|
|
295
|
-
|
|
296
|
-
await this.__srvUnboxed.tx(this.context).send(
|
|
294
|
+
await this.#setContextUser(this.context, userId, req);
|
|
295
|
+
req.event = handlerName;
|
|
296
|
+
await this.__srvUnboxed.tx(this.context).send(req);
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
// NOTE: Currently not exposed to CAP service; we wait for a valid use case
|
|
@@ -312,6 +312,10 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
312
312
|
return genericHandler ?? null;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
if (event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_SUCCESS)) {
|
|
316
|
+
[event] = event.split("/");
|
|
317
|
+
}
|
|
318
|
+
|
|
315
319
|
const specificHandler = this.__onHandlers[[event, saga].join("/")];
|
|
316
320
|
if (specificHandler) {
|
|
317
321
|
return specificHandler;
|
|
@@ -322,47 +326,53 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
322
326
|
|
|
323
327
|
async processPeriodicEvent(processContext, key, queueEntry) {
|
|
324
328
|
const { actionName } = config.normalizeSubType(this.eventType, this.eventSubType);
|
|
325
|
-
const
|
|
326
|
-
await this.#setContextUser(processContext, config.userId,
|
|
327
|
-
await this.__srvUnboxed.tx(processContext).emit(
|
|
329
|
+
const req = new cds.Event({ event: actionName, eventQueue: { processor: this, key, queueEntries: [queueEntry] } });
|
|
330
|
+
await this.#setContextUser(processContext, config.userId, req);
|
|
331
|
+
await this.__srvUnboxed.tx(processContext).emit(req);
|
|
328
332
|
}
|
|
329
333
|
|
|
330
|
-
#buildDispatchData(
|
|
334
|
+
#buildDispatchData(payload, { key, queueEntries } = {}) {
|
|
331
335
|
const { useEventQueueUser } = this.eventConfig;
|
|
332
336
|
const userId = useEventQueueUser ? config.userId : payload.contextUser;
|
|
333
|
-
const
|
|
337
|
+
const req = payload._fromSend ? new cds.Request(payload) : new cds.Event(payload);
|
|
334
338
|
const invocationFn = payload._fromSend ? "send" : "emit";
|
|
335
|
-
delete
|
|
336
|
-
delete
|
|
337
|
-
|
|
338
|
-
|
|
339
|
+
delete req._fromSend;
|
|
340
|
+
delete req.contextUser;
|
|
341
|
+
req.eventQueue = { processor: this, key, queueEntries, payload };
|
|
342
|
+
|
|
343
|
+
if (this.eventConfig.propagateContextProperties?.length && this.transactionMode === "isolated" && cds.context) {
|
|
344
|
+
for (const prop of this.eventConfig.propagateContextProperties) {
|
|
345
|
+
req[prop] && (cds.context[prop] = req[prop]);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { req, userId, invocationFn };
|
|
339
350
|
}
|
|
340
351
|
|
|
341
|
-
async #setContextUser(context, userId,
|
|
352
|
+
async #setContextUser(context, userId, req) {
|
|
342
353
|
const authInfo = await common.getAuthContext(context.tenant);
|
|
343
354
|
context.user = new cds.User.Privileged({
|
|
344
355
|
id: userId,
|
|
345
356
|
authInfo,
|
|
346
357
|
tokenInfo: authInfo?.token,
|
|
347
358
|
});
|
|
348
|
-
if (
|
|
349
|
-
|
|
359
|
+
if (req) {
|
|
360
|
+
req.user = context.user;
|
|
350
361
|
}
|
|
351
362
|
}
|
|
352
363
|
|
|
353
364
|
async processEvent(processContext, key, queueEntries, payload) {
|
|
365
|
+
let statusTuple;
|
|
366
|
+
const { userId, invocationFn, req } = this.#buildDispatchData(payload, { key, queueEntries });
|
|
354
367
|
try {
|
|
355
|
-
|
|
356
|
-
await this
|
|
357
|
-
|
|
358
|
-
const statusTuple = this.#determineResultStatus(result, queueEntries);
|
|
359
|
-
await this.#publishFollowupEvents(processContext, reg, statusTuple);
|
|
360
|
-
return statusTuple;
|
|
368
|
+
await this.#setContextUser(processContext, userId, req);
|
|
369
|
+
const result = await this.__srvUnboxed.tx(processContext)[invocationFn](req);
|
|
370
|
+
statusTuple = this.#determineResultStatus(result, queueEntries);
|
|
361
371
|
} catch (err) {
|
|
362
372
|
this.logger.error("error processing outboxed service call", err, {
|
|
363
373
|
serviceName: this.eventSubType,
|
|
364
374
|
});
|
|
365
|
-
|
|
375
|
+
statusTuple = queueEntries.map((queueEntry) => [
|
|
366
376
|
queueEntry.ID,
|
|
367
377
|
{
|
|
368
378
|
status: EventProcessingStatus.Error,
|
|
@@ -370,6 +380,9 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
370
380
|
},
|
|
371
381
|
]);
|
|
372
382
|
}
|
|
383
|
+
|
|
384
|
+
await this.#publishFollowupEvents(processContext, req, statusTuple);
|
|
385
|
+
return statusTuple;
|
|
373
386
|
}
|
|
374
387
|
|
|
375
388
|
async #publishFollowupEvents(processContext, req, statusTuple) {
|
|
@@ -380,7 +393,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
380
393
|
return;
|
|
381
394
|
}
|
|
382
395
|
|
|
383
|
-
if (req.event.endsWith(EVENT_QUEUE_ACTIONS.
|
|
396
|
+
if (req.event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_FAILED)) {
|
|
384
397
|
return;
|
|
385
398
|
}
|
|
386
399
|
|
|
@@ -393,23 +406,32 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
393
406
|
}
|
|
394
407
|
|
|
395
408
|
for (const [, result] of statusTuple) {
|
|
396
|
-
|
|
397
|
-
|
|
409
|
+
const data = result.nextData ?? req.data;
|
|
410
|
+
if (
|
|
411
|
+
succeeded &&
|
|
412
|
+
result.status === EventProcessingStatus.Done &&
|
|
413
|
+
!req.event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_SUCCESS)
|
|
414
|
+
) {
|
|
415
|
+
await this.__srv.tx(processContext).send(succeeded, data);
|
|
398
416
|
}
|
|
399
417
|
|
|
400
418
|
if (failed && result.status === EventProcessingStatus.Error) {
|
|
401
|
-
|
|
419
|
+
result.error && (data.error = this._error2String(result.error));
|
|
420
|
+
await this.__srv.tx(processContext).send(failed, data);
|
|
402
421
|
}
|
|
403
422
|
|
|
404
423
|
delete result.nextData;
|
|
405
424
|
}
|
|
406
425
|
|
|
407
426
|
if (config.insertEventsBeforeCommit) {
|
|
408
|
-
this.nextSagaEvents = tx._eventQueue
|
|
427
|
+
this.nextSagaEvents = tx._eventQueue?.events;
|
|
409
428
|
} else {
|
|
410
|
-
this.nextSagaEvents = tx._eventQueue
|
|
429
|
+
this.nextSagaEvents = tx._eventQueue?.events.filter((event) => JSON.parse(event.payload).event === failed);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (tx._eventQueue) {
|
|
433
|
+
tx._eventQueue.events = nextEvents ?? [];
|
|
411
434
|
}
|
|
412
|
-
tx._eventQueue.events = nextEvents ?? [];
|
|
413
435
|
}
|
|
414
436
|
|
|
415
437
|
#determineResultStatus(result, queueEntries) {
|
|
@@ -43,7 +43,7 @@ function outboxed(srv, customOpts) {
|
|
|
43
43
|
|
|
44
44
|
const outboxOpts = config.getEventConfig(CDS_EVENT_TYPE, subType, srvConfig.namespace);
|
|
45
45
|
const eventHeaders = getPropagatedHeaders(outboxOpts, req);
|
|
46
|
-
const contextProperties =
|
|
46
|
+
const contextProperties = getPropagateContextProperties(outboxOpts, req);
|
|
47
47
|
if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
|
|
48
48
|
await _mapToEventAndPublish(req, srvConfig.namespace, subType, eventHeaders, contextProperties);
|
|
49
49
|
return;
|
|
@@ -81,8 +81,8 @@ const getPropagatedHeaders = (config, req) => {
|
|
|
81
81
|
return Object.assign(propagateHeaders, req.headers);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
const
|
|
85
|
-
return config.
|
|
84
|
+
const getPropagateContextProperties = (config, req) => {
|
|
85
|
+
return config.propagateContextProperties.reduce((properties, name) => {
|
|
86
86
|
if (name in req.tx.context) {
|
|
87
87
|
properties[name] = req.tx.context[name];
|
|
88
88
|
}
|