@cap-js-community/event-queue 1.9.0-beta.2 → 1.9.1

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.9.0-beta.2",
3
+ "version": "1.9.1",
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": {
@@ -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(
@@ -929,6 +929,7 @@ class EventQueueProcessorBase {
929
929
  }
930
930
  });
931
931
  }
932
+ this.logger.info("keep alive finished!", { numberOfEvents: ids.length });
932
933
  });
933
934
  }).catch((err) => {
934
935
  this.logger.error("keep alive handling failed!", err);
@@ -971,7 +972,7 @@ class EventQueueProcessorBase {
971
972
  { expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
972
973
  );
973
974
  if (!lockAcquired) {
974
- this.logger.error("renewing redis lock failed!", {
975
+ this.logger.error("renewing distributed lock failed!", {
975
976
  type: this.#eventType,
976
977
  subType: this.#eventSubType,
977
978
  });
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
- const shouldIncludeBaseEvents = cds.env.profiles.includes("production") || cds.env.profiles.includes("test");
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,18 @@ 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
+ get hasConfigEvents() {
533
+ return !!(Object.keys(this.#configEvents ?? {}).length || Object.keys(this.#configPeriodicEvents ?? {}).length);
534
+ }
535
+
536
+ set configPeriodicEvents(value) {
537
+ this.#configPeriodicEvents = JSON.parse(JSON.stringify(value));
538
+ }
539
+
509
540
  get periodicEvents() {
510
541
  return this.#config.periodicEvents;
511
542
  }
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
- config.fileContent = await readConfigFromFile(config.configFilePath);
104
+ const fileContent = await readConfigFromFile(config.configFilePath);
105
+ config.mixFileContentWithEnv(fileContent);
101
106
 
102
107
  monkeyPatchCAPOutbox();
103
108
  registerCdsShutdown();
@@ -129,7 +134,7 @@ const readConfigFromFile = async (configFilepath) => {
129
134
  "configFilepath with unsupported extension, allowed extensions are .yaml and .json"
130
135
  );
131
136
  } catch (err) {
132
- if (config.useAsCAPOutbox) {
137
+ if (!configFilepath && (config.useAsCAPOutbox || config.hasConfigEvents)) {
133
138
  return {};
134
139
  }
135
140
  throw err;
@@ -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
 
@@ -150,8 +150,14 @@ const _insertPeriodEvents = async (tx, events, now) => {
150
150
  processChunkedSync(eventsToBeInserted, CHUNK_SIZE_INSERT_PERIODIC_EVENTS, (chunk) => {
151
151
  logger.info(`${counter}/${chunks} | inserting chunk of changed or new periodic events`, {
152
152
  events: chunk.map(({ type, subType, startAfter }) => {
153
- const { interval } = eventConfig.getEventConfig(type, subType);
154
- return { type, subType, interval, ...(startAfter && { startAfter }) };
153
+ const { interval, cron } = eventConfig.getEventConfig(type, subType);
154
+ return {
155
+ type,
156
+ subType,
157
+ ...(startAfter && { startAfter }),
158
+ ...(interval && { interval }),
159
+ ...(cron && { cron }),
160
+ };
155
161
  }),
156
162
  });
157
163
  counter++;
@@ -20,7 +20,7 @@ const processEventQueue = async (context, eventType, eventSubType, startTime = n
20
20
  try {
21
21
  let eventTypeInstance;
22
22
  const eventConfig = config.getEventConfig(eventType, eventSubType);
23
- const [err, EventTypeClass] = resilientRequire(eventConfig);
23
+ const [err, EventTypeClass] = await resilientRequire(eventConfig);
24
24
  if (!eventConfig || err || !(typeof EventTypeClass.constructor === "function")) {
25
25
  cds.log(COMPONENT_NAME).error("No Implementation found in the provided configuration file.", {
26
26
  eventType,
@@ -331,12 +331,24 @@ const _processEvent = async (eventTypeInstance, processContext, key, queueEntrie
331
331
  }
332
332
  };
333
333
 
334
- const resilientRequire = (eventConfig) => {
334
+ const resilientRequire = async (eventConfig) => {
335
335
  try {
336
336
  const path = eventConfig?.impl;
337
337
  const internal = eventConfig?.internalEvent;
338
- const module = require(pathLib.join(internal ? __dirname : process.cwd(), path));
339
- return [null, module];
338
+ const filePath = pathLib.join(internal ? __dirname : process.cwd(), path);
339
+ const fileExtension = pathLib.extname(filePath);
340
+ switch (fileExtension) {
341
+ case ".js":
342
+ return [null, require(filePath)];
343
+ case ".mjs":
344
+ return [null, (await import(`file://${filePath}`)).default];
345
+ case "":
346
+ try {
347
+ return [null, require(filePath)];
348
+ } catch {
349
+ return [null, (await import(`file://${filePath}`)).default];
350
+ }
351
+ }
340
352
  } catch (err) {
341
353
  return [err, null];
342
354
  }
@@ -51,7 +51,10 @@ const _scheduleFunction = async (singleRunFn, periodicFn) => {
51
51
  }
52
52
  if (!singleRunDone) {
53
53
  singleRunDone = true;
54
- singleRunFn().catch(() => (singleRunDone = false));
54
+ singleRunFn()
55
+ .then(periodicFn)
56
+ .catch(() => (singleRunDone = false));
57
+ return;
55
58
  }
56
59
  return periodicFn();
57
60
  };
@@ -123,7 +123,6 @@ const getTokenInfo = async (tenantId) => {
123
123
  }
124
124
 
125
125
  if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i)) {
126
- cds.log(COMPONENT_NAME).warn("Only 'jwt' or 'xsuaa' are supported as values for auth.kind.");
127
126
  return null;
128
127
  }
129
128
 
@@ -90,10 +90,20 @@ const _acquireLockRedis = async (
90
90
 
91
91
  const _renewLockRedis = async (context, fullKey, expiryTime, { value = "true" } = {}) => {
92
92
  const client = await redis.createMainClientAndConnect(config.redisOptions);
93
- const result = await client.set(fullKey, value, {
93
+ let result = await client.set(fullKey, value, {
94
94
  PX: Math.round(expiryTime),
95
95
  XX: true,
96
96
  });
97
+
98
+ if (result !== REDIS_COMMAND_OK) {
99
+ const readResult = await client.get(fullKey);
100
+ if (!readResult) {
101
+ result = await client.set(fullKey, value, {
102
+ PX: Math.round(expiryTime),
103
+ });
104
+ }
105
+ }
106
+
97
107
  return result === REDIS_COMMAND_OK;
98
108
  };
99
109