@cap-js-community/event-queue 1.10.0-beta.3 → 1.10.0-beta.5
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": "1.10.0-beta.
|
|
3
|
+
"version": "1.10.0-beta.5",
|
|
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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"node": ">=18"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@sap/xssec": "^4.
|
|
47
|
+
"@sap/xssec": "^4.5.0",
|
|
48
48
|
"cron-parser": "^5.0.0",
|
|
49
49
|
"redis": "^4.7.0",
|
|
50
50
|
"verror": "^1.10.1",
|
|
@@ -806,10 +806,10 @@ class EventQueueProcessorBase {
|
|
|
806
806
|
currentAttempt: exceededEvent.attempts,
|
|
807
807
|
}
|
|
808
808
|
);
|
|
809
|
+
await tx.rollback();
|
|
809
810
|
await executeInNewTransaction(this.__baseContext, "error-hookForExceededEvents", async (tx) =>
|
|
810
811
|
this.#persistEventQueueStatusForExceeded(tx, [exceededEvent], EventProcessingStatus.Error)
|
|
811
812
|
);
|
|
812
|
-
await tx.rollback();
|
|
813
813
|
}
|
|
814
814
|
}
|
|
815
815
|
);
|
package/src/config.js
CHANGED
|
@@ -24,6 +24,7 @@ const DEFAULT_INCREASE_PRIORITY = true;
|
|
|
24
24
|
const DEFAULT_KEEP_ALIVE_INTERVAL = 60;
|
|
25
25
|
const DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL = 3.5;
|
|
26
26
|
const DEFAULT_INHERIT_TRACE_CONTEXT = true;
|
|
27
|
+
const DEFAULT_CHECK_FOR_NEXT_CHUNK = true;
|
|
27
28
|
const SUFFIX_PERIODIC = "_PERIODIC";
|
|
28
29
|
const CAP_EVENT_TYPE = "CAP_OUTBOX";
|
|
29
30
|
const CAP_PARALLEL_DEFAULT = 5;
|
|
@@ -335,6 +336,7 @@ class Config {
|
|
|
335
336
|
this.#config.events.splice(index, 1);
|
|
336
337
|
}
|
|
337
338
|
|
|
339
|
+
// NOTE: CAP outbox defaults are injected by cds.requires.outbox
|
|
338
340
|
const eventConfig = this.#sanitizeParamsAdHocEvent({
|
|
339
341
|
type: CAP_EVENT_TYPE,
|
|
340
342
|
subType: serviceName,
|
|
@@ -507,6 +509,7 @@ class Config {
|
|
|
507
509
|
event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
|
|
508
510
|
event.keepAliveInterval = (event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL) * 1000;
|
|
509
511
|
event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
|
|
512
|
+
event.checkForNextChunk = event.checkForNextChunk ?? DEFAULT_CHECK_FOR_NEXT_CHUNK;
|
|
510
513
|
}
|
|
511
514
|
|
|
512
515
|
#sanitizeParamsBase(config, allowList) {
|
|
@@ -38,26 +38,18 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
38
38
|
this.__genericClusterHandler = clusterRelevant;
|
|
39
39
|
this.__specificClusterHandler = specificClusterRelevant;
|
|
40
40
|
await this.#setContextUser(this.context, config.userId);
|
|
41
|
-
return super.getQueueEntriesAndSetToInProgress();
|
|
41
|
+
return await super.getQueueEntriesAndSetToInProgress();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
// - we have service events
|
|
46
|
-
// - we have action specific events
|
|
47
|
-
// 1. I need to collect all action names
|
|
48
|
-
// 2. I need to check for which actions a handler exits
|
|
49
|
-
// 3. For actions with specific existing handler --> call specific action
|
|
50
|
-
// 4. For all others call generic handler
|
|
51
|
-
// NOTE: OVERALL idea is that the handler returns the cluster map and MUST not call any baseImpl functions!
|
|
52
|
-
// --> structure is a map of { key: { queueEntries: [], payload: {} }
|
|
44
|
+
// document structure is a map of { key: { queueEntries: [], payload: {} }
|
|
53
45
|
// TODO: document that clusterQueueEntries is now async!!!
|
|
54
|
-
// TODO: validate that return structure is as expected
|
|
55
46
|
async clusterQueueEntries(queueEntriesWithPayloadMap) {
|
|
56
47
|
if (!this.__genericClusterRelevantAndAvailable && !this.__specificClusterRelevantAndAvailable) {
|
|
57
48
|
return super.clusterQueueEntries(queueEntriesWithPayloadMap);
|
|
58
49
|
}
|
|
59
50
|
const { genericClusterEvents, specificClusterEvents } = this.#clusterByAction(queueEntriesWithPayloadMap);
|
|
60
51
|
|
|
52
|
+
const clusterMap = {};
|
|
61
53
|
if (Object.keys(genericClusterEvents).length) {
|
|
62
54
|
if (!this.__genericClusterRelevantAndAvailable) {
|
|
63
55
|
for (const actionName in genericClusterEvents) {
|
|
@@ -67,18 +59,30 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
67
59
|
for (const actionName in genericClusterEvents) {
|
|
68
60
|
const msg = new cds.Request({
|
|
69
61
|
event: `clusterQueueEntries`,
|
|
62
|
+
user: this.context.user,
|
|
70
63
|
eventQueue: {
|
|
71
64
|
processor: this,
|
|
72
65
|
clusterByPayloadProperty: (propertyName, cb) =>
|
|
73
|
-
this
|
|
66
|
+
this.#clusterByPayloadProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
|
|
74
67
|
clusterByEventProperty: (propertyName, cb) =>
|
|
75
|
-
this
|
|
68
|
+
this.#clusterByEventProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
|
|
76
69
|
clusterByDataProperty: (propertyName, cb) =>
|
|
77
|
-
this
|
|
70
|
+
this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
78
71
|
},
|
|
79
72
|
});
|
|
80
|
-
const
|
|
81
|
-
this.#
|
|
73
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(msg);
|
|
74
|
+
if (this.#validateCluster(clusterResult)) {
|
|
75
|
+
Object.assign(clusterMap, clusterResult);
|
|
76
|
+
} else {
|
|
77
|
+
this.logger.error(
|
|
78
|
+
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
79
|
+
{
|
|
80
|
+
handler: msg.event,
|
|
81
|
+
clusterResult: JSON.stringify(clusterResult),
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
return super.clusterQueueEntries(queueEntriesWithPayloadMap);
|
|
85
|
+
}
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
}
|
|
@@ -86,19 +90,59 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
86
90
|
for (const actionName in specificClusterEvents) {
|
|
87
91
|
const msg = new cds.Request({
|
|
88
92
|
event: `clusterQueueEntries.${actionName}`,
|
|
93
|
+
user: this.context.user,
|
|
89
94
|
eventQueue: {
|
|
90
95
|
processor: this,
|
|
91
96
|
clusterByPayloadProperty: (propertyName, cb) =>
|
|
92
|
-
this
|
|
97
|
+
this.#clusterByPayloadProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
93
98
|
clusterByEventProperty: (propertyName, cb) =>
|
|
94
|
-
this
|
|
99
|
+
this.#clusterByEventProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
95
100
|
clusterByDataProperty: (propertyName, cb) =>
|
|
96
|
-
this
|
|
101
|
+
this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
97
102
|
},
|
|
98
103
|
});
|
|
99
|
-
const
|
|
100
|
-
this.#
|
|
104
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(msg);
|
|
105
|
+
if (this.#validateCluster(clusterResult)) {
|
|
106
|
+
Object.assign(clusterMap, clusterResult);
|
|
107
|
+
} else {
|
|
108
|
+
this.logger.error(
|
|
109
|
+
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
110
|
+
{
|
|
111
|
+
handler: msg.event,
|
|
112
|
+
clusterResult: JSON.stringify(clusterResult),
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
return super.clusterQueueEntries(queueEntriesWithPayloadMap);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.#addToProcessingMap(clusterMap);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#validateCluster(obj) {
|
|
122
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const key of Object.keys(obj)) {
|
|
127
|
+
const clusterEntry = obj[key];
|
|
128
|
+
if (typeof clusterEntry !== "object" || clusterEntry === null || Array.isArray(obj)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!Array.isArray(clusterEntry.queueEntries)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
typeof clusterEntry.payload !== "object" ||
|
|
138
|
+
clusterEntry.payload === null ||
|
|
139
|
+
Array.isArray(clusterEntry.payload)
|
|
140
|
+
) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
101
143
|
}
|
|
144
|
+
|
|
145
|
+
return true;
|
|
102
146
|
}
|
|
103
147
|
|
|
104
148
|
clusterBase(queueEntriesWithPayloadMap, propertyName, refCb, cb) {
|
|
@@ -112,7 +156,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
112
156
|
for (const clustersKey in clusters) {
|
|
113
157
|
const clusterData = clusters[clustersKey];
|
|
114
158
|
const clusterResult = cb(
|
|
115
|
-
clustersKey.split("##").
|
|
159
|
+
clustersKey.split("##").pop(),
|
|
116
160
|
clusterData.queueEntries.map((entry) => entry.payload.data)
|
|
117
161
|
);
|
|
118
162
|
if (!clusterResult) {
|
|
@@ -139,7 +183,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
139
183
|
return result[key];
|
|
140
184
|
}
|
|
141
185
|
|
|
142
|
-
clusterByPayloadProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
186
|
+
#clusterByPayloadProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
143
187
|
return this.clusterBase(
|
|
144
188
|
queueEntriesWithPayloadMap,
|
|
145
189
|
propertyName,
|
|
@@ -148,7 +192,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
148
192
|
);
|
|
149
193
|
}
|
|
150
194
|
|
|
151
|
-
clusterByEventProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
195
|
+
#clusterByEventProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
152
196
|
return this.clusterBase(
|
|
153
197
|
queueEntriesWithPayloadMap,
|
|
154
198
|
propertyName,
|
|
@@ -157,7 +201,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
157
201
|
);
|
|
158
202
|
}
|
|
159
203
|
|
|
160
|
-
clusterByDataProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
204
|
+
#clusterByDataProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
161
205
|
return this.clusterBase(
|
|
162
206
|
queueEntriesWithPayloadMap,
|
|
163
207
|
propertyName,
|
|
@@ -230,18 +274,26 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
230
274
|
}
|
|
231
275
|
}
|
|
232
276
|
|
|
233
|
-
// simple here as per entry
|
|
234
277
|
async hookForExceededEvents(exceededEvent) {
|
|
235
|
-
|
|
278
|
+
const { event } = exceededEvent.payload;
|
|
279
|
+
const handlerName = this.#checkHandlerExists("hookForExceededEvents", event);
|
|
280
|
+
if (!handlerName) {
|
|
281
|
+
return await super.hookForExceededEvents(exceededEvent);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const { msg, userId } = this.#buildDispatchData(this.context, exceededEvent.payload, {
|
|
285
|
+
queueEntries: [exceededEvent],
|
|
286
|
+
});
|
|
287
|
+
await this.#setContextUser(this.context, userId, msg);
|
|
288
|
+
msg.event = handlerName;
|
|
289
|
+
await this.__srvUnboxed.tx(this.context).send(msg);
|
|
236
290
|
}
|
|
237
291
|
|
|
292
|
+
// NOTE: Currently not exposed to CAP service; we wait for a valid use case
|
|
238
293
|
async beforeProcessingEvents() {
|
|
239
294
|
return await super.beforeProcessingEvents();
|
|
240
295
|
}
|
|
241
296
|
|
|
242
|
-
// maybe async getter on req.data // only for periodic events
|
|
243
|
-
// getLastSuccessfulRunTimestamp
|
|
244
|
-
|
|
245
297
|
#checkHandlerExists(eventQueueFn, event) {
|
|
246
298
|
const specificHandler = this.__onHandlers[[eventQueueFn, event].join(".")];
|
|
247
299
|
if (specificHandler) {
|
package/src/publishEvent.js
CHANGED
|
@@ -63,8 +63,8 @@ const publishEvent = async (
|
|
|
63
63
|
event.context = JSON.stringify({ traceContext: openTelemetry.getCurrentTraceContext() });
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
if (eventConfig.timeBucket) {
|
|
67
|
-
event.startAfter
|
|
66
|
+
if (eventConfig.timeBucket && event.startAfter !== undefined) {
|
|
67
|
+
event.startAfter = CronExpressionParser.parse(eventConfig.timeBucket).next().toISOString();
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
if (config.insertEventsBeforeCommit && !skipInsertEventsBeforeCommit) {
|