@cap-js-community/event-queue 1.9.0-beta.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.9.0-beta.1",
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": {
@@ -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(
@@ -852,7 +852,7 @@ class EventQueueProcessorBase {
852
852
  // eslint-disable-next-line no-unused-vars
853
853
  async beforeProcessingEvents() {}
854
854
 
855
- async isOutdatedAndKeepalive() {
855
+ async isOutdatedAndKeepAlive() {
856
856
  if (this.__keepAliveViolated) {
857
857
  return true;
858
858
  }
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,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
- 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();
@@ -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
 
@@ -159,6 +159,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
159
159
  `eventQueue-periodic-process-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
160
160
  async (tx) => {
161
161
  await trace(eventTypeInstance.context, "process-periodic-event", async () => {
162
+ eventTypeInstance.continuesKeepAlive();
162
163
  eventTypeInstance.processEventContext = tx.context;
163
164
  eventTypeInstance.setTxForEventProcessing(queueEntry.ID, cds.tx(tx.context));
164
165
  try {
@@ -170,6 +171,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
170
171
  await tx.rollback();
171
172
  return;
172
173
  } finally {
174
+ eventTypeInstance.stopKeepAlive();
173
175
  eventTypeInstance.endPerformanceTracerPeriodicEvents();
174
176
  }
175
177
  if (
@@ -198,6 +200,8 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
198
200
  eventType: eventTypeInstance?.eventType,
199
201
  eventSubType: eventTypeInstance?.eventSubType,
200
202
  });
203
+ } finally {
204
+ await eventTypeInstance.keepAlivePromise;
201
205
  }
202
206
  };
203
207
 
@@ -314,7 +318,7 @@ const _checkEventIsBlocked = async (baseInstance) => {
314
318
 
315
319
  const _processEvent = async (eventTypeInstance, processContext, key, queueEntries, payload) => {
316
320
  try {
317
- const eventOutdated = await eventTypeInstance.isOutdatedAndKeepalive(queueEntries);
321
+ const eventOutdated = await eventTypeInstance.isOutdatedAndKeepAlive(queueEntries);
318
322
  if (eventOutdated) {
319
323
  // NOTE: return empty status map to comply with the interface
320
324
  return {};
@@ -1,9 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  const cds = require("@sap/cds");
4
- let otel, telemetry;
4
+ let otel;
5
5
  try {
6
- telemetry = require("@cap-js/telemetry");
7
6
  otel = require("@opentelemetry/api");
8
7
  } catch {
9
8
  // ignore
@@ -14,12 +13,13 @@ const config = require("../config");
14
13
  const COMPONENT_NAME = "/shared/openTelemetry";
15
14
 
16
15
  const trace = async (context, label, fn, { attributes = {}, newRootSpan = false } = {}) => {
17
- if (!config.enableCAPTelemetry || !otel || !telemetry) {
16
+ const tracerProvider = otel?.trace.getTracerProvider();
17
+ // Check if a real provider is registered
18
+ if (!config.enableCAPTelemetry || !tracerProvider || tracerProvider === otel.trace.NOOP_TRACER_PROVIDER) {
18
19
  return fn();
19
20
  }
20
21
 
21
22
  const tracer = otel.trace.getTracer("eventqueue");
22
-
23
23
  const span = tracer.startSpan(`eventqueue-${label}`, {
24
24
  kind: otel.SpanKind.INTERNAL,
25
25
  root: newRootSpan,