@cap-js-community/event-queue 2.1.0-beta.1 → 2.1.0-beta.3
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/db/Event.cds
CHANGED
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.3",
|
|
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",
|
|
@@ -504,10 +504,16 @@ class EventQueueProcessorBase {
|
|
|
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,
|
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
|
|
@@ -326,40 +326,47 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
326
326
|
|
|
327
327
|
async processPeriodicEvent(processContext, key, queueEntry) {
|
|
328
328
|
const { actionName } = config.normalizeSubType(this.eventType, this.eventSubType);
|
|
329
|
-
const
|
|
330
|
-
await this.#setContextUser(processContext, config.userId,
|
|
331
|
-
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);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
#buildDispatchData(
|
|
334
|
+
#buildDispatchData(payload, { key, queueEntries } = {}) {
|
|
335
335
|
const { useEventQueueUser } = this.eventConfig;
|
|
336
336
|
const userId = useEventQueueUser ? config.userId : payload.contextUser;
|
|
337
|
-
const
|
|
337
|
+
const req = payload._fromSend ? new cds.Request(payload) : new cds.Event(payload);
|
|
338
338
|
const invocationFn = payload._fromSend ? "send" : "emit";
|
|
339
|
-
delete
|
|
340
|
-
delete
|
|
341
|
-
|
|
342
|
-
|
|
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 };
|
|
343
350
|
}
|
|
344
351
|
|
|
345
|
-
async #setContextUser(context, userId,
|
|
352
|
+
async #setContextUser(context, userId, req) {
|
|
346
353
|
const authInfo = await common.getAuthContext(context.tenant);
|
|
347
354
|
context.user = new cds.User.Privileged({
|
|
348
355
|
id: userId,
|
|
349
356
|
authInfo,
|
|
350
357
|
tokenInfo: authInfo?.token,
|
|
351
358
|
});
|
|
352
|
-
if (
|
|
353
|
-
|
|
359
|
+
if (req) {
|
|
360
|
+
req.user = context.user;
|
|
354
361
|
}
|
|
355
362
|
}
|
|
356
363
|
|
|
357
364
|
async processEvent(processContext, key, queueEntries, payload) {
|
|
358
365
|
let statusTuple;
|
|
359
|
-
const { userId, invocationFn,
|
|
366
|
+
const { userId, invocationFn, req } = this.#buildDispatchData(payload, { key, queueEntries });
|
|
360
367
|
try {
|
|
361
|
-
await this.#setContextUser(processContext, userId,
|
|
362
|
-
const result = await this.__srvUnboxed.tx(processContext)[invocationFn](
|
|
368
|
+
await this.#setContextUser(processContext, userId, req);
|
|
369
|
+
const result = await this.__srvUnboxed.tx(processContext)[invocationFn](req);
|
|
363
370
|
statusTuple = this.#determineResultStatus(result, queueEntries);
|
|
364
371
|
} catch (err) {
|
|
365
372
|
this.logger.error("error processing outboxed service call", err, {
|
|
@@ -374,7 +381,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
374
381
|
]);
|
|
375
382
|
}
|
|
376
383
|
|
|
377
|
-
await this.#publishFollowupEvents(processContext,
|
|
384
|
+
await this.#publishFollowupEvents(processContext, req, statusTuple);
|
|
378
385
|
return statusTuple;
|
|
379
386
|
}
|
|
380
387
|
|
|
@@ -11,6 +11,8 @@ const CDS_EVENT_TYPE = "CAP_OUTBOX";
|
|
|
11
11
|
const COMPONENT_NAME = "/eventQueue/eventQueueAsOutbox";
|
|
12
12
|
const EVENT_QUEUE_SPECIFIC_FIELDS = ["startAfter", "referenceEntity", "referenceEntityKey", "namespace"];
|
|
13
13
|
|
|
14
|
+
const TO_COPY = ["inbound", "event", "data", "queue", "results", "method", "path", "params", "entity", "service"];
|
|
15
|
+
|
|
14
16
|
function outboxed(srv, customOpts) {
|
|
15
17
|
if (!(new.target || customOpts)) {
|
|
16
18
|
const former = srv[OUTBOXED];
|
|
@@ -43,7 +45,7 @@ function outboxed(srv, customOpts) {
|
|
|
43
45
|
|
|
44
46
|
const outboxOpts = config.getEventConfig(CDS_EVENT_TYPE, subType, srvConfig.namespace);
|
|
45
47
|
const eventHeaders = getPropagatedHeaders(outboxOpts, req);
|
|
46
|
-
const contextProperties =
|
|
48
|
+
const contextProperties = getPropagateContextProperties(outboxOpts, req);
|
|
47
49
|
if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
|
|
48
50
|
await _mapToEventAndPublish(req, srvConfig.namespace, subType, eventHeaders, contextProperties);
|
|
49
51
|
return;
|
|
@@ -81,8 +83,8 @@ const getPropagatedHeaders = (config, req) => {
|
|
|
81
83
|
return Object.assign(propagateHeaders, req.headers);
|
|
82
84
|
};
|
|
83
85
|
|
|
84
|
-
const
|
|
85
|
-
return config.
|
|
86
|
+
const getPropagateContextProperties = (config, req) => {
|
|
87
|
+
return config.propagateContextProperties.reduce((properties, name) => {
|
|
86
88
|
if (name in req.tx.context) {
|
|
87
89
|
properties[name] = req.tx.context[name];
|
|
88
90
|
}
|
|
@@ -102,18 +104,27 @@ const _mapToEventAndPublish = async (req, namespace, subType, eventHeaders, cont
|
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
|
-
|
|
106
107
|
const event = {
|
|
107
108
|
contextUser: context.user.id,
|
|
108
109
|
...(req._fromSend || (req.reply && { _fromSend: true })), // send or emit
|
|
109
|
-
...(req.inbound && { inbound: req.inbound }),
|
|
110
|
-
...(req.event && { event: req.event }),
|
|
111
|
-
...(req.data && { data: req.data }),
|
|
112
110
|
...(eventHeaders && { headers: eventHeaders }),
|
|
113
|
-
...(req.query && { query: req.query }),
|
|
114
111
|
...(Object.keys(contextProperties).length && { ...contextProperties }),
|
|
115
112
|
};
|
|
116
113
|
|
|
114
|
+
for (const prop of TO_COPY) {
|
|
115
|
+
if (req[prop]) {
|
|
116
|
+
event[prop] = req[prop];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (req.query) {
|
|
121
|
+
event.query = typeof req.query.flat === "function" ? req.query.flat() : req.query;
|
|
122
|
+
delete event.query._target;
|
|
123
|
+
delete event.query.__target;
|
|
124
|
+
delete event.query.target;
|
|
125
|
+
delete event.data; // `req.data` should be a getter to whatever is in `req.query`
|
|
126
|
+
}
|
|
127
|
+
|
|
117
128
|
await publishEvent(
|
|
118
129
|
cds.tx(context),
|
|
119
130
|
{
|