@cap-js-community/event-queue 1.10.0-beta.4 → 1.10.0-beta.6
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.6",
|
|
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,11 +44,11 @@
|
|
|
44
44
|
"node": ">=18"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@sap/xssec": "^4.
|
|
48
|
-
"cron-parser": "^5.
|
|
47
|
+
"@sap/xssec": "^4.5.0",
|
|
48
|
+
"cron-parser": "^5.1.0",
|
|
49
49
|
"redis": "^4.7.0",
|
|
50
50
|
"verror": "^1.10.1",
|
|
51
|
-
"yaml": "^2.
|
|
51
|
+
"yaml": "^2.7.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@cap-js/cds-test": "^0.2.0",
|
|
@@ -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;
|
|
@@ -152,18 +153,50 @@ class Config {
|
|
|
152
153
|
return !!this.#env.redisRequires?.credentials;
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
#parseRegexOrString(str) {
|
|
157
|
+
const regexLiteralPattern = /^\/((?:\\.|[^\\/])*)\/([gimsuy]*)$/;
|
|
158
|
+
const match = str.match(regexLiteralPattern);
|
|
159
|
+
|
|
160
|
+
if (match) {
|
|
161
|
+
try {
|
|
162
|
+
return { type: "regex", value: new RegExp(match[1], match[2]) };
|
|
163
|
+
} catch {
|
|
164
|
+
return { type: "string", value: str };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { type: "string", value: str };
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
shouldBeProcessedInThisApplication(type, subType) {
|
|
156
171
|
const config = this.#eventMap[this.generateKey(type, subType)];
|
|
157
172
|
const appNameConfig = config._appNameMap;
|
|
158
173
|
const appInstanceConfig = config._appInstancesMap;
|
|
174
|
+
let result = true;
|
|
159
175
|
if (!appNameConfig && !appInstanceConfig) {
|
|
160
|
-
return
|
|
176
|
+
return result;
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
if (appNameConfig) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
if (config._appNameContainsRegex) {
|
|
181
|
+
for (const configKey in appNameConfig) {
|
|
182
|
+
const config = appNameConfig[configKey];
|
|
183
|
+
if (config.type === "regex") {
|
|
184
|
+
result = config.value.test(this.#env.applicationName);
|
|
185
|
+
} else {
|
|
186
|
+
const shouldBeProcessedBasedOnAppName = appNameConfig[this.#env.applicationName];
|
|
187
|
+
if (!shouldBeProcessedBasedOnAppName) {
|
|
188
|
+
result = config.value === this.#env.applicationName;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (result) {
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
const shouldBeProcessedBasedOnAppName = appNameConfig[this.#env.applicationName];
|
|
197
|
+
if (!shouldBeProcessedBasedOnAppName) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
167
200
|
}
|
|
168
201
|
}
|
|
169
202
|
|
|
@@ -174,7 +207,7 @@ class Config {
|
|
|
174
207
|
}
|
|
175
208
|
}
|
|
176
209
|
|
|
177
|
-
return
|
|
210
|
+
return result;
|
|
178
211
|
}
|
|
179
212
|
|
|
180
213
|
checkRedisEnabled() {
|
|
@@ -335,6 +368,7 @@ class Config {
|
|
|
335
368
|
this.#config.events.splice(index, 1);
|
|
336
369
|
}
|
|
337
370
|
|
|
371
|
+
// NOTE: CAP outbox defaults are injected by cds.requires.outbox
|
|
338
372
|
const eventConfig = this.#sanitizeParamsAdHocEvent({
|
|
339
373
|
type: CAP_EVENT_TYPE,
|
|
340
374
|
subType: serviceName,
|
|
@@ -507,6 +541,7 @@ class Config {
|
|
|
507
541
|
event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
|
|
508
542
|
event.keepAliveInterval = (event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL) * 1000;
|
|
509
543
|
event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
|
|
544
|
+
event.checkForNextChunk = event.checkForNextChunk ?? DEFAULT_CHECK_FOR_NEXT_CHUNK;
|
|
510
545
|
}
|
|
511
546
|
|
|
512
547
|
#sanitizeParamsBase(config, allowList) {
|
|
@@ -527,7 +562,12 @@ class Config {
|
|
|
527
562
|
}
|
|
528
563
|
|
|
529
564
|
#basicEventTransformationAfterValidate(event) {
|
|
530
|
-
event._appNameMap = event.appNames
|
|
565
|
+
event._appNameMap = event.appNames
|
|
566
|
+
? Object.fromEntries(new Map(event.appNames.map((a) => [a, this.#parseRegexOrString(a)])))
|
|
567
|
+
: null;
|
|
568
|
+
event._appNameContainsRegex = event.appNames
|
|
569
|
+
? event.appNames.some((appName) => this.#parseRegexOrString(appName).type === "regex")
|
|
570
|
+
: null;
|
|
531
571
|
event._appInstancesMap = event.appInstances
|
|
532
572
|
? Object.fromEntries(new Map(event.appInstances.map((a) => [a, true])))
|
|
533
573
|
: null;
|
|
@@ -9,6 +9,12 @@ const config = require("../config");
|
|
|
9
9
|
|
|
10
10
|
const COMPONENT_NAME = "/eventQueue/outbox/generic";
|
|
11
11
|
|
|
12
|
+
const EVENT_QUEUE_ACTIONS = {
|
|
13
|
+
EXCEEDED: "eventQueueRetriesExceeded",
|
|
14
|
+
CLUSTER: "eventQueueCluster",
|
|
15
|
+
CHECK_AND_ADJUST: "eventQueueCheckAndAdjustPayload",
|
|
16
|
+
};
|
|
17
|
+
|
|
12
18
|
class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
13
19
|
constructor(context, eventType, eventSubType, config) {
|
|
14
20
|
super(context, eventType, eventSubType, config);
|
|
@@ -21,7 +27,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
21
27
|
this.__srvUnboxed = cds.unboxed(this.__srv);
|
|
22
28
|
const { handlers, clusterRelevant, specificClusterRelevant } = this.__srvUnboxed.handlers.on.reduce(
|
|
23
29
|
(result, handler) => {
|
|
24
|
-
if (handler.on.startsWith(
|
|
30
|
+
if (handler.on.startsWith(EVENT_QUEUE_ACTIONS.CLUSTER)) {
|
|
25
31
|
if (handler.on.split(".").length === 2) {
|
|
26
32
|
result.specificClusterRelevant = true;
|
|
27
33
|
} else {
|
|
@@ -38,26 +44,16 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
38
44
|
this.__genericClusterHandler = clusterRelevant;
|
|
39
45
|
this.__specificClusterHandler = specificClusterRelevant;
|
|
40
46
|
await this.#setContextUser(this.context, config.userId);
|
|
41
|
-
return super.getQueueEntriesAndSetToInProgress();
|
|
47
|
+
return await super.getQueueEntriesAndSetToInProgress();
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
// NOTE: issue here: if events are not sorted before it might not be unique here:
|
|
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: {} }
|
|
53
|
-
// TODO: document that clusterQueueEntries is now async!!!
|
|
54
|
-
// TODO: validate that return structure is as expected
|
|
55
50
|
async clusterQueueEntries(queueEntriesWithPayloadMap) {
|
|
56
51
|
if (!this.__genericClusterRelevantAndAvailable && !this.__specificClusterRelevantAndAvailable) {
|
|
57
52
|
return super.clusterQueueEntries(queueEntriesWithPayloadMap);
|
|
58
53
|
}
|
|
59
54
|
const { genericClusterEvents, specificClusterEvents } = this.#clusterByAction(queueEntriesWithPayloadMap);
|
|
60
55
|
|
|
56
|
+
const clusterMap = {};
|
|
61
57
|
if (Object.keys(genericClusterEvents).length) {
|
|
62
58
|
if (!this.__genericClusterRelevantAndAvailable) {
|
|
63
59
|
for (const actionName in genericClusterEvents) {
|
|
@@ -66,39 +62,91 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
66
62
|
} else {
|
|
67
63
|
for (const actionName in genericClusterEvents) {
|
|
68
64
|
const msg = new cds.Request({
|
|
69
|
-
event:
|
|
65
|
+
event: EVENT_QUEUE_ACTIONS.CLUSTER,
|
|
66
|
+
user: this.context.user,
|
|
70
67
|
eventQueue: {
|
|
71
68
|
processor: this,
|
|
72
69
|
clusterByPayloadProperty: (propertyName, cb) =>
|
|
73
|
-
this
|
|
70
|
+
this.#clusterByPayloadProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
|
|
74
71
|
clusterByEventProperty: (propertyName, cb) =>
|
|
75
|
-
this
|
|
72
|
+
this.#clusterByEventProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
|
|
76
73
|
clusterByDataProperty: (propertyName, cb) =>
|
|
77
|
-
this
|
|
74
|
+
this.#clusterByDataProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
|
|
78
75
|
},
|
|
79
76
|
});
|
|
80
|
-
const
|
|
81
|
-
this.#
|
|
77
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(msg);
|
|
78
|
+
if (this.#validateCluster(clusterResult)) {
|
|
79
|
+
Object.assign(clusterMap, clusterResult);
|
|
80
|
+
} else {
|
|
81
|
+
this.logger.error(
|
|
82
|
+
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
83
|
+
{
|
|
84
|
+
handler: msg.event,
|
|
85
|
+
clusterResult: JSON.stringify(clusterResult),
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
return super.clusterQueueEntries(queueEntriesWithPayloadMap);
|
|
89
|
+
}
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
for (const actionName in specificClusterEvents) {
|
|
87
95
|
const msg = new cds.Request({
|
|
88
|
-
event:
|
|
96
|
+
event: `${EVENT_QUEUE_ACTIONS.CLUSTER}.${actionName}`,
|
|
97
|
+
user: this.context.user,
|
|
89
98
|
eventQueue: {
|
|
90
99
|
processor: this,
|
|
91
100
|
clusterByPayloadProperty: (propertyName, cb) =>
|
|
92
|
-
this
|
|
101
|
+
this.#clusterByPayloadProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
93
102
|
clusterByEventProperty: (propertyName, cb) =>
|
|
94
|
-
this
|
|
103
|
+
this.#clusterByEventProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
95
104
|
clusterByDataProperty: (propertyName, cb) =>
|
|
96
|
-
this
|
|
105
|
+
this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
|
|
97
106
|
},
|
|
98
107
|
});
|
|
99
|
-
const
|
|
100
|
-
this.#
|
|
108
|
+
const clusterResult = await this.__srvUnboxed.tx(this.context).send(msg);
|
|
109
|
+
if (this.#validateCluster(clusterResult)) {
|
|
110
|
+
Object.assign(clusterMap, clusterResult);
|
|
111
|
+
} else {
|
|
112
|
+
this.logger.error(
|
|
113
|
+
"cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
|
|
114
|
+
{
|
|
115
|
+
handler: msg.event,
|
|
116
|
+
clusterResult: JSON.stringify(clusterResult),
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
return super.clusterQueueEntries(queueEntriesWithPayloadMap);
|
|
120
|
+
}
|
|
101
121
|
}
|
|
122
|
+
this.#addToProcessingMap(clusterMap);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#validateCluster(obj) {
|
|
126
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const key of Object.keys(obj)) {
|
|
131
|
+
const clusterEntry = obj[key];
|
|
132
|
+
if (typeof clusterEntry !== "object" || clusterEntry === null || Array.isArray(obj)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!Array.isArray(clusterEntry.queueEntries)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
typeof clusterEntry.payload !== "object" ||
|
|
142
|
+
clusterEntry.payload === null ||
|
|
143
|
+
Array.isArray(clusterEntry.payload)
|
|
144
|
+
) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return true;
|
|
102
150
|
}
|
|
103
151
|
|
|
104
152
|
clusterBase(queueEntriesWithPayloadMap, propertyName, refCb, cb) {
|
|
@@ -139,7 +187,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
139
187
|
return result[key];
|
|
140
188
|
}
|
|
141
189
|
|
|
142
|
-
clusterByPayloadProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
190
|
+
#clusterByPayloadProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
143
191
|
return this.clusterBase(
|
|
144
192
|
queueEntriesWithPayloadMap,
|
|
145
193
|
propertyName,
|
|
@@ -148,7 +196,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
148
196
|
);
|
|
149
197
|
}
|
|
150
198
|
|
|
151
|
-
clusterByEventProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
199
|
+
#clusterByEventProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
152
200
|
return this.clusterBase(
|
|
153
201
|
queueEntriesWithPayloadMap,
|
|
154
202
|
propertyName,
|
|
@@ -157,7 +205,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
157
205
|
);
|
|
158
206
|
}
|
|
159
207
|
|
|
160
|
-
clusterByDataProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
208
|
+
#clusterByDataProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
|
|
161
209
|
return this.clusterBase(
|
|
162
210
|
queueEntriesWithPayloadMap,
|
|
163
211
|
propertyName,
|
|
@@ -205,13 +253,13 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
205
253
|
}
|
|
206
254
|
|
|
207
255
|
#hasEventSpecificClusterHandler(queueEntry) {
|
|
208
|
-
return !!this.__onHandlers[[
|
|
256
|
+
return !!this.__onHandlers[[EVENT_QUEUE_ACTIONS.CLUSTER, queueEntry.payload.event].join(".")];
|
|
209
257
|
}
|
|
210
258
|
|
|
211
259
|
async checkEventAndGeneratePayload(queueEntry) {
|
|
212
260
|
const payload = await super.checkEventAndGeneratePayload(queueEntry);
|
|
213
261
|
const { event } = payload;
|
|
214
|
-
const handlerName = this.#checkHandlerExists(
|
|
262
|
+
const handlerName = this.#checkHandlerExists(EVENT_QUEUE_ACTIONS.CHECK_AND_ADJUST, event);
|
|
215
263
|
if (!handlerName) {
|
|
216
264
|
return payload;
|
|
217
265
|
}
|
|
@@ -230,18 +278,26 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
230
278
|
}
|
|
231
279
|
}
|
|
232
280
|
|
|
233
|
-
// simple here as per entry
|
|
234
281
|
async hookForExceededEvents(exceededEvent) {
|
|
235
|
-
|
|
282
|
+
const { event } = exceededEvent.payload;
|
|
283
|
+
const handlerName = this.#checkHandlerExists(EVENT_QUEUE_ACTIONS.EXCEEDED, event);
|
|
284
|
+
if (!handlerName) {
|
|
285
|
+
return await super.hookForExceededEvents(exceededEvent);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { msg, userId } = this.#buildDispatchData(this.context, exceededEvent.payload, {
|
|
289
|
+
queueEntries: [exceededEvent],
|
|
290
|
+
});
|
|
291
|
+
await this.#setContextUser(this.context, userId, msg);
|
|
292
|
+
msg.event = handlerName;
|
|
293
|
+
await this.__srvUnboxed.tx(this.context).send(msg);
|
|
236
294
|
}
|
|
237
295
|
|
|
296
|
+
// NOTE: Currently not exposed to CAP service; we wait for a valid use case
|
|
238
297
|
async beforeProcessingEvents() {
|
|
239
298
|
return await super.beforeProcessingEvents();
|
|
240
299
|
}
|
|
241
300
|
|
|
242
|
-
// maybe async getter on req.data // only for periodic events
|
|
243
|
-
// getLastSuccessfulRunTimestamp
|
|
244
|
-
|
|
245
301
|
#checkHandlerExists(eventQueueFn, event) {
|
|
246
302
|
const specificHandler = this.__onHandlers[[eventQueueFn, event].join(".")];
|
|
247
303
|
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) {
|