@cap-js-community/event-queue 1.9.0-beta.2 → 1.9.0
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 +12 -1
- package/src/EventQueueError.js +30 -0
- package/src/config.js +44 -17
- package/src/initialize.js +8 -3
- package/src/outbox/EventQueueGenericOutboxHandler.js +26 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/event-queue",
|
|
3
|
-
"version": "1.9.0
|
|
3
|
+
"version": "1.9.0",
|
|
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",
|
|
@@ -79,6 +79,17 @@
|
|
|
79
79
|
"registerAsEventProcessor": false,
|
|
80
80
|
"updatePeriodicEvents": false,
|
|
81
81
|
"insertEventsBeforeCommit": false
|
|
82
|
+
},
|
|
83
|
+
"periodicEvents": {
|
|
84
|
+
"[production]": {
|
|
85
|
+
"EVENT_QUEUE_BASE/DELETE_EVENTS": {
|
|
86
|
+
"priority": "low",
|
|
87
|
+
"impl": "./housekeeping/EventQueueDeleteEvents",
|
|
88
|
+
"load": 20,
|
|
89
|
+
"interval": 86400,
|
|
90
|
+
"internalEvent": true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
82
93
|
}
|
|
83
94
|
},
|
|
84
95
|
"requires": {
|
package/src/EventQueueError.js
CHANGED
|
@@ -18,6 +18,8 @@ const ERROR_CODES = {
|
|
|
18
18
|
NO_INTERVAL_OR_CRON: "NO_INTERVAL_OR_CRON",
|
|
19
19
|
INTERVAL_AND_CRON: "INTERVAL_AND_CRON",
|
|
20
20
|
MISSING_IMPL: "MISSING_IMPL",
|
|
21
|
+
MISSING_TYPE: "MISSING_TYPE",
|
|
22
|
+
MISSING_SUBTYPE: "MISSING_SUBTYPE",
|
|
21
23
|
DUPLICATE_EVENT_REGISTRATION: "DUPLICATE_EVENT_REGISTRATION",
|
|
22
24
|
NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
|
|
23
25
|
LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
|
|
@@ -62,6 +64,12 @@ const ERROR_CODES_META = {
|
|
|
62
64
|
[ERROR_CODES.MISSING_IMPL]: {
|
|
63
65
|
message: "Missing path to event class implementation.",
|
|
64
66
|
},
|
|
67
|
+
[ERROR_CODES.MISSING_TYPE]: {
|
|
68
|
+
message: "Missing type event implementation.",
|
|
69
|
+
},
|
|
70
|
+
[ERROR_CODES.MISSING_SUBTYPE]: {
|
|
71
|
+
message: "Missing subtype event implementation.",
|
|
72
|
+
},
|
|
65
73
|
[ERROR_CODES.DUPLICATE_EVENT_REGISTRATION]: {
|
|
66
74
|
message: "Duplicate event registration, check the uniqueness of type and subType.",
|
|
67
75
|
},
|
|
@@ -265,6 +273,28 @@ class EventQueueError extends VError {
|
|
|
265
273
|
);
|
|
266
274
|
}
|
|
267
275
|
|
|
276
|
+
static missingType(config) {
|
|
277
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.MISSING_TYPE];
|
|
278
|
+
return new EventQueueError(
|
|
279
|
+
{
|
|
280
|
+
name: ERROR_CODES.MISSING_TYPE,
|
|
281
|
+
info: { config },
|
|
282
|
+
},
|
|
283
|
+
message
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
static missingSubType(config) {
|
|
288
|
+
const { message } = ERROR_CODES_META[ERROR_CODES.MISSING_SUBTYPE];
|
|
289
|
+
return new EventQueueError(
|
|
290
|
+
{
|
|
291
|
+
name: ERROR_CODES.MISSING_SUBTYPE,
|
|
292
|
+
info: { config },
|
|
293
|
+
},
|
|
294
|
+
message
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
268
298
|
static duplicateEventRegistration(type, subType) {
|
|
269
299
|
const { message } = ERROR_CODES_META[ERROR_CODES.DUPLICATE_EVENT_REGISTRATION];
|
|
270
300
|
return new EventQueueError(
|
package/src/config.js
CHANGED
|
@@ -30,18 +30,6 @@ const PRIORITIES = Object.values(Priorities);
|
|
|
30
30
|
const UTC_DEFAULT = false;
|
|
31
31
|
const USE_CRON_TZ_DEFAULT = true;
|
|
32
32
|
|
|
33
|
-
const BASE_PERIODIC_EVENTS = [
|
|
34
|
-
{
|
|
35
|
-
type: "EVENT_QUEUE_BASE",
|
|
36
|
-
subType: "DELETE_EVENTS",
|
|
37
|
-
priority: Priorities.Low,
|
|
38
|
-
impl: "./housekeeping/EventQueueDeleteEvents",
|
|
39
|
-
load: 20,
|
|
40
|
-
interval: 86400, // 1 day,
|
|
41
|
-
internalEvent: true,
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
|
|
45
33
|
const BASE_TABLES = {
|
|
46
34
|
EVENT: "sap.eventqueue.Event",
|
|
47
35
|
LOCK: "sap.eventqueue.Lock",
|
|
@@ -82,6 +70,8 @@ class Config {
|
|
|
82
70
|
#crashOnRedisUnavailable;
|
|
83
71
|
#tenantIdFilterTokenInfoCb;
|
|
84
72
|
#tenantIdFilterEventProcessingCb;
|
|
73
|
+
#configEvents;
|
|
74
|
+
#configPeriodicEvents;
|
|
85
75
|
static #instance;
|
|
86
76
|
constructor() {
|
|
87
77
|
this.#logger = cds.log(COMPONENT_NAME);
|
|
@@ -357,13 +347,33 @@ class Config {
|
|
|
357
347
|
this.#isEventQueueActive = value;
|
|
358
348
|
}
|
|
359
349
|
|
|
350
|
+
mixFileContentWithEnv(fileContent) {
|
|
351
|
+
fileContent.events ??= [];
|
|
352
|
+
fileContent.periodicEvents ??= [];
|
|
353
|
+
const events = this.#configEvents ?? {};
|
|
354
|
+
const periodicEvents = this.#configPeriodicEvents ?? {};
|
|
355
|
+
fileContent.events = fileContent.events.concat(this.#mapEnvEvents(events));
|
|
356
|
+
fileContent.periodicEvents = fileContent.periodicEvents.concat(this.#mapEnvEvents(periodicEvents));
|
|
357
|
+
this.fileContent = fileContent;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#mapEnvEvents(events) {
|
|
361
|
+
return Object.entries(events)
|
|
362
|
+
.map(([key, event]) => {
|
|
363
|
+
if (!event) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const [type, subType] = key.split("/");
|
|
367
|
+
event.type ??= type;
|
|
368
|
+
event.subType ??= subType;
|
|
369
|
+
return { ...event };
|
|
370
|
+
})
|
|
371
|
+
.filter((a) => a);
|
|
372
|
+
}
|
|
373
|
+
|
|
360
374
|
set fileContent(config) {
|
|
361
|
-
this.#config = config;
|
|
362
375
|
config.events = config.events ?? [];
|
|
363
|
-
|
|
364
|
-
config.periodicEvents = (config.periodicEvents ?? []).concat(
|
|
365
|
-
(shouldIncludeBaseEvents ? BASE_PERIODIC_EVENTS : []).map((event) => ({ ...event }))
|
|
366
|
-
);
|
|
376
|
+
config.periodicEvents = config.periodicEvents ?? [];
|
|
367
377
|
this.#eventMap = config.events.reduce((result, event) => {
|
|
368
378
|
this.#basicEventTransformation(event);
|
|
369
379
|
this.#validateAdHocEvents(result, event);
|
|
@@ -380,6 +390,7 @@ class Config {
|
|
|
380
390
|
result[this.generateKey(event.type, event.subType)] = event;
|
|
381
391
|
return result;
|
|
382
392
|
}, this.#eventMap);
|
|
393
|
+
this.#config = config;
|
|
383
394
|
}
|
|
384
395
|
|
|
385
396
|
#basicEventTransformation(event) {
|
|
@@ -402,6 +413,14 @@ class Config {
|
|
|
402
413
|
throw EventQueueError.missingImpl(event.type, event.subType);
|
|
403
414
|
}
|
|
404
415
|
|
|
416
|
+
if (!event.type) {
|
|
417
|
+
throw EventQueueError.missingType(event);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!event.subType) {
|
|
421
|
+
throw EventQueueError.missingSubType(event);
|
|
422
|
+
}
|
|
423
|
+
|
|
405
424
|
if (event.appNames) {
|
|
406
425
|
if (!Array.isArray(event.appNames) || event.appNames.some((appName) => typeof appName !== "string")) {
|
|
407
426
|
throw EventQueueError.appNamesFormat(event.type, event.subType, event.appNames);
|
|
@@ -506,6 +525,14 @@ class Config {
|
|
|
506
525
|
return this.#config.events;
|
|
507
526
|
}
|
|
508
527
|
|
|
528
|
+
set configEvents(value) {
|
|
529
|
+
this.#configEvents = JSON.parse(JSON.stringify(value));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
set configPeriodicEvents(value) {
|
|
533
|
+
this.#configPeriodicEvents = JSON.parse(JSON.stringify(value));
|
|
534
|
+
}
|
|
535
|
+
|
|
509
536
|
get periodicEvents() {
|
|
510
537
|
return this.#config.periodicEvents;
|
|
511
538
|
}
|
package/src/initialize.js
CHANGED
|
@@ -26,6 +26,8 @@ const TIMEOUT_SHUTDOWN = 2500;
|
|
|
26
26
|
|
|
27
27
|
const CONFIG_VARS = [
|
|
28
28
|
["configFilePath", null],
|
|
29
|
+
["events", null, "configEvents"],
|
|
30
|
+
["periodicEvents", null, "configPeriodicEvents"],
|
|
29
31
|
["registerAsEventProcessor", true],
|
|
30
32
|
["processEventsAfterPublish", true],
|
|
31
33
|
["isEventQueueActive", true],
|
|
@@ -49,6 +51,8 @@ const CONFIG_VARS = [
|
|
|
49
51
|
*
|
|
50
52
|
* @param {Object} options - The configuration options.
|
|
51
53
|
* @param {string} [options.configFilePath=null] - Path to the configuration file.
|
|
54
|
+
* @param {string} [options.events={}] - Options to allow events in the configuration.
|
|
55
|
+
* @param {string} [options.periodicEvents={}] - Options to allow periodicEvents in the configuration.
|
|
52
56
|
* @param {boolean} [options.registerAsEventProcessor=true] - Register the instance as an event processor.
|
|
53
57
|
* @param {boolean} [options.processEventsAfterPublish=true] - Process events immediately after publishing.
|
|
54
58
|
* @param {boolean} [options.isEventQueueActive=true] - Flag to activate/deactivate the event queue.
|
|
@@ -97,7 +101,8 @@ const initialize = async (options = {}) => {
|
|
|
97
101
|
throw EventQueueError.redisConnectionFailure();
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
const fileContent = await readConfigFromFile(config.configFilePath);
|
|
105
|
+
config.mixFileContentWithEnv(fileContent);
|
|
101
106
|
|
|
102
107
|
monkeyPatchCAPOutbox();
|
|
103
108
|
registerCdsShutdown();
|
|
@@ -178,9 +183,9 @@ const monkeyPatchCAPOutbox = () => {
|
|
|
178
183
|
};
|
|
179
184
|
|
|
180
185
|
const mixConfigVarsWithEnv = (options) => {
|
|
181
|
-
CONFIG_VARS.forEach(([configName, defaultValue]) => {
|
|
186
|
+
CONFIG_VARS.forEach(([configName, defaultValue, mappingName]) => {
|
|
182
187
|
const configValue = options[configName];
|
|
183
|
-
config[configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
|
|
188
|
+
config[mappingName ?? configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
|
|
184
189
|
});
|
|
185
190
|
};
|
|
186
191
|
|
|
@@ -16,7 +16,6 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async processEvent(processContext, key, queueEntries, payload) {
|
|
19
|
-
let status = EventProcessingStatus.Done;
|
|
20
19
|
try {
|
|
21
20
|
const service = await cds.connect.to(this.eventSubType);
|
|
22
21
|
const { useEventQueueUser } = this.eventConfig;
|
|
@@ -30,14 +29,37 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
|
|
|
30
29
|
authInfo: await common.getTokenInfo(processContext.tenant),
|
|
31
30
|
});
|
|
32
31
|
processContext._eventQueue = { processor: this, key, queueEntries, payload };
|
|
33
|
-
await cds.unboxed(service).tx(processContext)[invocationFn](msg);
|
|
32
|
+
const result = await cds.unboxed(service).tx(processContext)[invocationFn](msg);
|
|
33
|
+
return this.#determineResultStatus(result, queueEntries);
|
|
34
34
|
} catch (err) {
|
|
35
|
-
status = EventProcessingStatus.Error;
|
|
36
35
|
this.logger.error("error processing outboxed service call", err, {
|
|
37
36
|
serviceName: this.eventSubType,
|
|
38
37
|
});
|
|
38
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Error]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#determineResultStatus(result, queueEntries) {
|
|
43
|
+
const validStatusValues = Object.values(EventProcessingStatus);
|
|
44
|
+
const validStatus = validStatusValues.includes(result);
|
|
45
|
+
if (validStatus) {
|
|
46
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, result]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!Array.isArray(result)) {
|
|
50
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const valid = !result.some((entry) => {
|
|
54
|
+
const [, status] = entry;
|
|
55
|
+
return !validStatusValues.includes(status);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (valid) {
|
|
59
|
+
return result;
|
|
60
|
+
} else {
|
|
61
|
+
return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
|
|
39
62
|
}
|
|
40
|
-
return queueEntries.map((queueEntry) => [queueEntry.ID, status]);
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
65
|
|