@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.6.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.1",
46
+ "@sap/xssec": "^4.2.4",
47
47
  "redis": "^4.7.0",
48
48
  "verror": "^1.10.1",
49
- "yaml": "^2.5.0"
49
+ "yaml": "^2.5.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@cap-js/hana": "^1.1.1",
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.18.2",
61
- "hdb": "^0.19.7",
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"
@@ -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
- return !config._appNameMap || config._appNameMap[this.#env.applicationName];
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._appNameMap = eventConfig.appNames
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.load = event.load ?? DEFAULT_LOAD;
328
- event.priority = event.priority ?? DEFAULT_PRIORITY;
329
- this.validateAdHocEvents(result, event);
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.validatePeriodicConfig(result, event);
340
- event._appNameMap = event.appNames ? Object.fromEntries(new Map(event.appNames.map((a) => [a, true]))) : null;
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
- validatePeriodicConfig(eventMap, config) {
347
- const key = this.generateKey(config.type, config.subType);
348
- if (eventMap[key] && eventMap[key].isPeriodic) {
349
- throw EventQueueError.duplicateEventRegistration(config.type, config.subType);
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 (!config.interval || config.interval <= MIN_INTERVAL_SEC) {
353
- throw EventQueueError.invalidInterval(config.type, config.subType, config.interval);
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 (!config.impl) {
357
- throw EventQueueError.missingImpl(config.type, config.subType);
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
- validateAdHocEvents(eventMap, config) {
362
- const key = this.generateKey(config.type, config.subType);
363
- if (eventMap[key] && !eventMap[key].isPeriodic) {
364
- throw EventQueueError.duplicateEventRegistration(config.type, config.subType);
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 (!config.impl) {
368
- throw EventQueueError.missingImpl(config.type, config.subType);
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
- if (config.appNames) {
372
- if (!Array.isArray(config.appNames) || config.appNames.some((appName) => typeof appName !== "string")) {
373
- throw EventQueueError.appNamesFormat(config.type, config.subType, config.appNames);
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
- let startAfter;
76
+ const eventQueueSpecificValues = {};
78
77
  for (const header in req.headers ?? {}) {
79
- if (header.toLocaleLowerCase() === "x-eventqueue-startafter") {
80
- startAfter = req.headers[header];
81
- delete req.headers[header];
82
- break;
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
- ...(startAfter && { startAfter }),
100
+ ...eventQueueSpecificValues,
100
101
  });
101
102
  };
102
103
 
@@ -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 (eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
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 (eventConfig.getEventConfig(type, subType) && eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
47
- result.push({ type, subType });
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
  }
@@ -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
  }