@cap-js-community/event-queue 1.6.6 → 1.6.7
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 +8 -8
- package/src/EventQueueError.js +15 -0
- package/src/config.js +77 -29
- package/src/outbox/eventQueueAsOutbox.js +9 -8
- package/src/runner/openEvents.js +15 -4
- package/src/runner/runner.js +1 -1
- package/src/shared/env.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.7",
|
|
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",
|
|
@@ -43,22 +43,22 @@
|
|
|
43
43
|
"node": ">=18"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@sap/xssec": "^4.2.
|
|
46
|
+
"@sap/xssec": "^4.2.4",
|
|
47
47
|
"redis": "^4.7.0",
|
|
48
48
|
"verror": "^1.10.1",
|
|
49
|
-
"yaml": "^2.5.
|
|
49
|
+
"yaml": "^2.5.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@
|
|
52
|
+
"@sap/cds": "^8.3.0",
|
|
53
|
+
"@sap/cds-dk": "^8.3.0",
|
|
54
|
+
"@cap-js/hana": "^1.3.0",
|
|
53
55
|
"@cap-js/sqlite": "^1.7.3",
|
|
54
|
-
"@sap/cds": "^8.1.0",
|
|
55
|
-
"@sap/cds-dk": "^8.1.0",
|
|
56
56
|
"eslint": "^8.57.0",
|
|
57
57
|
"eslint-config-prettier": "^9.1.0",
|
|
58
58
|
"eslint-plugin-jest": "^28.6.0",
|
|
59
59
|
"eslint-plugin-node": "^11.1.0",
|
|
60
|
-
"express": "^4.
|
|
61
|
-
"hdb": "^0.19.
|
|
60
|
+
"express": "^4.21.0",
|
|
61
|
+
"hdb": "^0.19.10",
|
|
62
62
|
"jest": "^29.7.0",
|
|
63
63
|
"prettier": "^2.8.8",
|
|
64
64
|
"sqlite3": "^5.1.7"
|
package/src/EventQueueError.js
CHANGED
|
@@ -19,6 +19,7 @@ const ERROR_CODES = {
|
|
|
19
19
|
LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
|
|
20
20
|
NOT_ALLOWED_PRIORITY: "NOT_ALLOWED_PRIORITY",
|
|
21
21
|
APP_NAMES_FORMAT: "APP_NAMES_FORMAT",
|
|
22
|
+
APP_INSTANCES_FORMAT: "APP_INSTANCES_FORMAT",
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const ERROR_CODES_META = {
|
|
@@ -71,6 +72,9 @@ const ERROR_CODES_META = {
|
|
|
71
72
|
[ERROR_CODES.APP_NAMES_FORMAT]: {
|
|
72
73
|
message: "The app names property must be an array and only contain strings.",
|
|
73
74
|
},
|
|
75
|
+
[ERROR_CODES.APP_INSTANCES_FORMAT]: {
|
|
76
|
+
message: "The app instances property must be an array and only contain numbers.",
|
|
77
|
+
},
|
|
74
78
|
};
|
|
75
79
|
|
|
76
80
|
class EventQueueError extends VError {
|
|
@@ -250,6 +254,17 @@ class EventQueueError extends VError {
|
|
|
250
254
|
message
|
|
251
255
|
);
|
|
252
256
|
}
|
|
257
|
+
|
|
258
|
+
static appInstancesFormat(type, subType, appInstances) {
|
|
259
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.APP_INSTANCES_FORMAT];
|
|
260
|
+
return new EventQueueError(
|
|
261
|
+
{
|
|
262
|
+
name: ERROR_CODES.APP_INSTANCES_FORMAT,
|
|
263
|
+
info: { type, subType, appInstances },
|
|
264
|
+
},
|
|
265
|
+
message
|
|
266
|
+
);
|
|
267
|
+
}
|
|
253
268
|
}
|
|
254
269
|
|
|
255
270
|
module.exports = EventQueueError;
|
package/src/config.js
CHANGED
|
@@ -22,6 +22,7 @@ const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
|
|
|
22
22
|
const CAP_EVENT_TYPE = "CAP_OUTBOX";
|
|
23
23
|
const CAP_PARALLEL_DEFAULT = 5;
|
|
24
24
|
const DELETE_TENANT_BLOCK_AFTER_MS = 5 * 60 * 1000;
|
|
25
|
+
const PRIORITIES = Object.values(Priorities);
|
|
25
26
|
|
|
26
27
|
const BASE_PERIODIC_EVENTS = [
|
|
27
28
|
{
|
|
@@ -110,7 +111,27 @@ class Config {
|
|
|
110
111
|
|
|
111
112
|
shouldBeProcessedInThisApplication(type, subType) {
|
|
112
113
|
const config = this.#eventMap[this.generateKey(type, subType)];
|
|
113
|
-
|
|
114
|
+
const appNameConfig = config._appNameMap;
|
|
115
|
+
const appInstanceConfig = config._appInstancesMap;
|
|
116
|
+
if (!appNameConfig && !appInstanceConfig) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (appNameConfig) {
|
|
121
|
+
const shouldBeProcessedBasedOnAppName = appNameConfig[this.#env.applicationName];
|
|
122
|
+
if (!shouldBeProcessedBasedOnAppName) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (appInstanceConfig) {
|
|
128
|
+
const shouldBeProcessedBasedOnAppInstance = appInstanceConfig[this.#env.applicationInstance];
|
|
129
|
+
if (!shouldBeProcessedBasedOnAppInstance) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true;
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
checkRedisEnabled() {
|
|
@@ -282,12 +303,11 @@ class Config {
|
|
|
282
303
|
checkForNextChunk: config.checkForNextChunk,
|
|
283
304
|
deleteFinishedEventsAfterDays: config.deleteFinishedEventsAfterDays,
|
|
284
305
|
appNames: config.appNames,
|
|
306
|
+
appInstances: config.appInstances,
|
|
285
307
|
useEventQueueUser: config.useEventQueueUser,
|
|
286
308
|
internalEvent: true,
|
|
287
309
|
};
|
|
288
|
-
eventConfig
|
|
289
|
-
? Object.fromEntries(new Map(eventConfig.appNames.map((a) => [a, true])))
|
|
290
|
-
: null;
|
|
310
|
+
this.#basicEventTransformationAfterValidate(eventConfig);
|
|
291
311
|
this.#config.events.push(eventConfig);
|
|
292
312
|
this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)] = eventConfig;
|
|
293
313
|
}
|
|
@@ -324,55 +344,83 @@ class Config {
|
|
|
324
344
|
config.events = config.events ?? [];
|
|
325
345
|
config.periodicEvents = (config.periodicEvents ?? []).concat(BASE_PERIODIC_EVENTS.map((event) => ({ ...event })));
|
|
326
346
|
this.#eventMap = config.events.reduce((result, event) => {
|
|
327
|
-
event
|
|
328
|
-
|
|
329
|
-
this
|
|
330
|
-
event._appNameMap = event.appNames ? Object.fromEntries(new Map(event.appNames.map((a) => [a, true]))) : null;
|
|
347
|
+
this.#basicEventTransformation(event);
|
|
348
|
+
this.#validateAdHocEvents(result, event);
|
|
349
|
+
this.#basicEventTransformationAfterValidate(event);
|
|
331
350
|
result[this.generateKey(event.type, event.subType)] = event;
|
|
332
351
|
return result;
|
|
333
352
|
}, {});
|
|
334
353
|
this.#eventMap = config.periodicEvents.reduce((result, event) => {
|
|
335
|
-
event.load = event.load ?? DEFAULT_LOAD;
|
|
336
354
|
event.priority = event.priority ?? DEFAULT_PRIORITY;
|
|
337
355
|
event.type = `${event.type}${SUFFIX_PERIODIC}`;
|
|
338
356
|
event.isPeriodic = true;
|
|
339
|
-
this
|
|
340
|
-
|
|
357
|
+
this.#basicEventTransformation(event);
|
|
358
|
+
this.#validatePeriodicConfig(result, event);
|
|
359
|
+
this.#basicEventTransformationAfterValidate(event);
|
|
341
360
|
result[this.generateKey(event.type, event.subType)] = event;
|
|
342
361
|
return result;
|
|
343
362
|
}, this.#eventMap);
|
|
344
363
|
}
|
|
345
364
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
365
|
+
#basicEventTransformation(event) {
|
|
366
|
+
event.load = event.load ?? DEFAULT_LOAD;
|
|
367
|
+
event.priority = event.priority ?? DEFAULT_PRIORITY;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#basicEventTransformationAfterValidate(event) {
|
|
371
|
+
event._appNameMap = event.appNames ? Object.fromEntries(new Map(event.appNames.map((a) => [a, true]))) : null;
|
|
372
|
+
event._appInstancesMap = event.appInstances
|
|
373
|
+
? Object.fromEntries(new Map(event.appInstances.map((a) => [a, true])))
|
|
374
|
+
: null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#basicEventValidation(event) {
|
|
378
|
+
if (!event.impl) {
|
|
379
|
+
throw EventQueueError.missingImpl(event.type, event.subType);
|
|
350
380
|
}
|
|
351
381
|
|
|
352
|
-
if (
|
|
353
|
-
|
|
382
|
+
if (event.appNames) {
|
|
383
|
+
if (!Array.isArray(event.appNames) || event.appNames.some((appName) => typeof appName !== "string")) {
|
|
384
|
+
throw EventQueueError.appNamesFormat(event.type, event.subType, event.appNames);
|
|
385
|
+
}
|
|
354
386
|
}
|
|
355
387
|
|
|
356
|
-
if (
|
|
357
|
-
|
|
388
|
+
if (event.appInstances) {
|
|
389
|
+
if (
|
|
390
|
+
!Array.isArray(event.appInstances) ||
|
|
391
|
+
event.appInstances.some((appInstance) => typeof appInstance !== "number")
|
|
392
|
+
) {
|
|
393
|
+
throw EventQueueError.appInstancesFormat(event.type, event.subType, event.appInstances);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!PRIORITIES.includes(event.priority)) {
|
|
398
|
+
throw EventQueueError.priorityNotAllowed(event.priority, "initEvent");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (event.load > this.#instanceLoadLimit) {
|
|
402
|
+
throw EventQueueError.loadHigherThanLimit(event.load, "initEvent");
|
|
358
403
|
}
|
|
359
404
|
}
|
|
360
405
|
|
|
361
|
-
|
|
362
|
-
const key = this.generateKey(
|
|
363
|
-
if (eventMap[key] &&
|
|
364
|
-
throw EventQueueError.duplicateEventRegistration(
|
|
406
|
+
#validatePeriodicConfig(eventMap, event) {
|
|
407
|
+
const key = this.generateKey(event.type, event.subType);
|
|
408
|
+
if (eventMap[key] && eventMap[key].isPeriodic) {
|
|
409
|
+
throw EventQueueError.duplicateEventRegistration(event.type, event.subType);
|
|
365
410
|
}
|
|
366
411
|
|
|
367
|
-
if (!
|
|
368
|
-
throw EventQueueError.
|
|
412
|
+
if (!event.interval || event.interval <= MIN_INTERVAL_SEC) {
|
|
413
|
+
throw EventQueueError.invalidInterval(event.type, event.subType, event.interval);
|
|
369
414
|
}
|
|
415
|
+
this.#basicEventValidation(event);
|
|
416
|
+
}
|
|
370
417
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
418
|
+
#validateAdHocEvents(eventMap, event) {
|
|
419
|
+
const key = this.generateKey(event.type, event.subType);
|
|
420
|
+
if (eventMap[key] && !eventMap[key].isPeriodic) {
|
|
421
|
+
throw EventQueueError.duplicateEventRegistration(event.type, event.subType);
|
|
375
422
|
}
|
|
423
|
+
this.#basicEventValidation(event);
|
|
376
424
|
}
|
|
377
425
|
|
|
378
426
|
generateKey(type, subType) {
|
|
@@ -7,10 +7,9 @@ const config = require("../config");
|
|
|
7
7
|
|
|
8
8
|
const OUTBOXED = Symbol("outboxed");
|
|
9
9
|
const UNBOXED = Symbol("unboxed");
|
|
10
|
-
|
|
11
10
|
const CDS_EVENT_TYPE = "CAP_OUTBOX";
|
|
12
|
-
|
|
13
11
|
const COMPONENT_NAME = "/eventQueue/eventQueueAsOutbox";
|
|
12
|
+
const EVENT_QUEUE_SPECIFIC_FIELDS = ["startAfter", "referenceEntity", "referenceEntityKey"];
|
|
14
13
|
|
|
15
14
|
function outboxed(srv, customOpts) {
|
|
16
15
|
// outbox max. once
|
|
@@ -74,12 +73,14 @@ function unboxed(srv) {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
const _mapToEventAndPublish = async (context, name, req) => {
|
|
77
|
-
|
|
76
|
+
const eventQueueSpecificValues = {};
|
|
78
77
|
for (const header in req.headers ?? {}) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
for (const field of EVENT_QUEUE_SPECIFIC_FIELDS) {
|
|
79
|
+
if (header.toLocaleLowerCase() === `x-eventqueue-${field.toLocaleLowerCase()}`) {
|
|
80
|
+
eventQueueSpecificValues[field] = req.headers[header];
|
|
81
|
+
delete req.headers[header];
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
const event = {
|
|
@@ -96,7 +97,7 @@ const _mapToEventAndPublish = async (context, name, req) => {
|
|
|
96
97
|
type: CDS_EVENT_TYPE,
|
|
97
98
|
subType: name,
|
|
98
99
|
payload: JSON.stringify(event),
|
|
99
|
-
...
|
|
100
|
+
...eventQueueSpecificValues,
|
|
100
101
|
});
|
|
101
102
|
};
|
|
102
103
|
|
package/src/runner/openEvents.js
CHANGED
|
@@ -5,7 +5,7 @@ const cds = require("@sap/cds");
|
|
|
5
5
|
const eventConfig = require("../config");
|
|
6
6
|
const { EventProcessingStatus } = require("../constants");
|
|
7
7
|
|
|
8
|
-
const getOpenQueueEntries = async (tx) => {
|
|
8
|
+
const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
|
|
9
9
|
const startTime = new Date();
|
|
10
10
|
const refDateStartAfter = new Date(startTime.getTime() + eventConfig.runInterval * 1.2);
|
|
11
11
|
const entries = await tx.run(
|
|
@@ -37,14 +37,25 @@ const getOpenQueueEntries = async (tx) => {
|
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
cds.outboxed(service);
|
|
40
|
-
if (
|
|
40
|
+
if (filterAppSpecificEvents) {
|
|
41
|
+
if (eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
|
|
42
|
+
result.push({ type, subType });
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
41
45
|
result.push({ type, subType });
|
|
42
46
|
}
|
|
43
47
|
})
|
|
44
48
|
.catch(() => {});
|
|
45
49
|
} else {
|
|
46
|
-
if (
|
|
47
|
-
|
|
50
|
+
if (filterAppSpecificEvents) {
|
|
51
|
+
if (
|
|
52
|
+
eventConfig.getEventConfig(type, subType) &&
|
|
53
|
+
eventConfig.shouldBeProcessedInThisApplication(type, subType)
|
|
54
|
+
) {
|
|
55
|
+
result.push({ type, subType });
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
eventConfig.getEventConfig(type, subType) && result.push({ type, subType });
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
}
|
package/src/runner/runner.js
CHANGED
|
@@ -134,7 +134,7 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
|
|
|
134
134
|
id: config.userId,
|
|
135
135
|
authInfo: await common.getAuthInfo(tenantId),
|
|
136
136
|
});
|
|
137
|
-
const entries = await openEvents.getOpenQueueEntries(tx);
|
|
137
|
+
const entries = await openEvents.getOpenQueueEntries(tx, false);
|
|
138
138
|
logger.info("broadcasting events for run", {
|
|
139
139
|
tenantId,
|
|
140
140
|
entries: entries.length,
|
package/src/shared/env.js
CHANGED
|
@@ -5,6 +5,7 @@ let instance;
|
|
|
5
5
|
class Env {
|
|
6
6
|
#vcapServices;
|
|
7
7
|
#vcapApplication;
|
|
8
|
+
#vcapApplicationInstance;
|
|
8
9
|
|
|
9
10
|
constructor() {
|
|
10
11
|
try {
|
|
@@ -14,6 +15,7 @@ class Env {
|
|
|
14
15
|
this.#vcapServices = {};
|
|
15
16
|
this.#vcapApplication = {};
|
|
16
17
|
}
|
|
18
|
+
this.#vcapApplicationInstance = Number(process.env.CF_INSTANCE_INDEX);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
get redisCredentialsFromEnv() {
|
|
@@ -24,6 +26,10 @@ class Env {
|
|
|
24
26
|
return this.#vcapApplication.application_name;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
get applicationInstance() {
|
|
30
|
+
return this.#vcapApplicationInstance;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
set vcapServices(value) {
|
|
28
34
|
this.#vcapServices = value;
|
|
29
35
|
}
|
|
@@ -32,6 +38,10 @@ class Env {
|
|
|
32
38
|
return this.#vcapServices;
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
set applicationInstance(value) {
|
|
42
|
+
this.#vcapApplicationInstance = value;
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
set vcapApplication(value) {
|
|
36
46
|
this.#vcapApplication = value;
|
|
37
47
|
}
|