@cap-js-community/event-queue 1.9.2 → 1.10.0-beta.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.2",
3
+ "version": "1.10.0-beta.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",
@@ -23,6 +23,7 @@
23
23
  "scripts": {
24
24
  "test:unit": "jest --testPathIgnorePatterns=\"/test-integration/\"",
25
25
  "test:integration": "jest --testPathIgnorePatterns=\"/test/\" --runInBand --forceExit",
26
+ "voter:test:integration": "jest --testPathIgnorePatterns=\"/test/\" --forceExit",
26
27
  "test": "npm run test:unit && npm run test:integration",
27
28
  "test:all:coverage": "jest --runInBand --forceExit --collect-coverage",
28
29
  "test:prepare": "npm run build:ci --prefix=./test-integration/_env",
@@ -64,7 +65,8 @@
64
65
  "jest": "^29.7.0",
65
66
  "prettier": "^2.8.8",
66
67
  "sqlite3": "^5.1.7",
67
- "@opentelemetry/api": "^1.9.0"
68
+ "@opentelemetry/api": "^1.9.0",
69
+ "@actions/core": "^1.11.1"
68
70
  },
69
71
  "homepage": "https://cap-js-community.github.io/event-queue/",
70
72
  "repository": {
package/src/config.js CHANGED
@@ -36,6 +36,44 @@ const BASE_TABLES = {
36
36
  LOCK: "sap.eventqueue.Lock",
37
37
  };
38
38
 
39
+ const ALLOWED_EVENT_OPTIONS_BASE = [
40
+ "type",
41
+ "subType",
42
+ "load",
43
+ "impl",
44
+ "transactionMode",
45
+ "deleteFinishedEventsAfterDays",
46
+ "useEventQueueUser",
47
+ "priority",
48
+ "keepAliveInterval",
49
+ "increasePriorityOverTime",
50
+ "keepAliveMaxInProgressTime",
51
+ "appNames",
52
+ "appInstances",
53
+ "internalEvent",
54
+ ];
55
+
56
+ const ALLOWED_EVENT_OPTIONS_AD_HOC = [
57
+ ...ALLOWED_EVENT_OPTIONS_BASE,
58
+ "inheritTraceContext",
59
+ "selectMaxChunkSize",
60
+ "parallelEventProcessing",
61
+ "retryAttempts",
62
+ "processAfterCommit",
63
+ "checkForNextChunk",
64
+ "retryFailedAfter",
65
+ "multiInstanceProcessing",
66
+ "kind",
67
+ ];
68
+
69
+ const ALLOWED_EVENT_OPTIONS_PERIODIC_EVENT = [
70
+ ...ALLOWED_EVENT_OPTIONS_BASE,
71
+ "interval",
72
+ "cron",
73
+ "utc",
74
+ "useCronTimezone",
75
+ ];
76
+
39
77
  class Config {
40
78
  #logger;
41
79
  #config;
@@ -286,7 +324,7 @@ class Config {
286
324
  });
287
325
  }
288
326
 
289
- addCAPOutboxEvent(serviceName, config) {
327
+ addCAPOutboxEventBase(serviceName, config) {
290
328
  if (this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)]) {
291
329
  const index = this.#config.events.findIndex(
292
330
  (event) => event.type === CAP_EVENT_TYPE && event.subType === serviceName
@@ -294,36 +332,49 @@ class Config {
294
332
  this.#config.events.splice(index, 1);
295
333
  }
296
334
 
297
- const eventConfig = {
335
+ const eventConfig = this.#sanitizeParamsAdHocEvent({
298
336
  type: CAP_EVENT_TYPE,
299
337
  subType: serviceName,
300
- load: config.load,
301
338
  impl: "./outbox/EventQueueGenericOutboxHandler",
302
- selectMaxChunkSize: config.chunkSize,
339
+ kind: config.kind ?? "persistent-outbox",
340
+ selectMaxChunkSize: config.selectMaxChunkSize ?? config.chunkSize,
303
341
  parallelEventProcessing: config.parallelEventProcessing ?? (config.parallel && CAP_PARALLEL_DEFAULT),
304
- retryAttempts: config.maxAttempts,
305
- transactionMode: config.transactionMode,
306
- processAfterCommit: config.processAfterCommit,
307
- checkForNextChunk: config.checkForNextChunk,
308
- deleteFinishedEventsAfterDays: config.deleteFinishedEventsAfterDays,
309
- appNames: config.appNames,
310
- appInstances: config.appInstances,
311
- useEventQueueUser: config.useEventQueueUser,
312
- retryFailedAfter: config.retryFailedAfter,
313
- priority: config.priority,
314
- multiInstanceProcessing: config.multiInstanceProcessing,
315
- increasePriorityOverTime: config.increasePriorityOverTime,
316
- keepAliveInterval: config.keepAliveInterval,
317
- inheritTraceContext: true,
318
- internalEvent: true,
319
- };
342
+ retryAttempts: config.retryAttempts ?? config.maxAttempts,
343
+ ...config,
344
+ });
345
+ eventConfig.internalEvent = true;
320
346
 
321
347
  this.#basicEventTransformation(eventConfig);
348
+ this.#validateAdHocEvents(this.#eventMap, eventConfig, false);
322
349
  this.#basicEventTransformationAfterValidate(eventConfig);
323
350
  this.#config.events.push(eventConfig);
324
351
  this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)] = eventConfig;
325
352
  }
326
353
 
354
+ addCAPOutboxEventSpecificAction(serviceName, actionName) {
355
+ const subType = [serviceName, actionName].join(".");
356
+ if (this.#eventMap[this.generateKey(CAP_EVENT_TYPE, subType)]) {
357
+ const index = this.#config.events.findIndex(
358
+ (event) => event.type === CAP_EVENT_TYPE && event.subType === serviceName
359
+ );
360
+ this.#config.events.splice(index, 1);
361
+ }
362
+
363
+ const eventConfig = this.#sanitizeParamsAdHocEvent({
364
+ ...this.getEventConfig(CAP_EVENT_TYPE, serviceName),
365
+ ...this.getCdsOutboxEventSpecificConfig(serviceName, actionName),
366
+ subType,
367
+ });
368
+ eventConfig.internalEvent = true;
369
+
370
+ this.#basicEventTransformation(eventConfig);
371
+ this.#validateAdHocEvents(this.#eventMap, eventConfig, false);
372
+ this.#basicEventTransformationAfterValidate(eventConfig);
373
+ this.#config.events.push(eventConfig);
374
+ this.#eventMap[this.generateKey(CAP_EVENT_TYPE, subType)] = eventConfig;
375
+ return eventConfig;
376
+ }
377
+
327
378
  #unblockEventLocalState(key, tenant) {
328
379
  const map = this.#blockedEvents[key];
329
380
  if (!map) {
@@ -356,11 +407,59 @@ class Config {
356
407
  fileContent.periodicEvents ??= [];
357
408
  const events = this.#configEvents ?? {};
358
409
  const periodicEvents = this.#configPeriodicEvents ?? {};
410
+ const periodicCapServiceEvents = this.#cdsPeriodicOutboxServicesFromEnv();
359
411
  fileContent.events = fileContent.events.concat(this.#mapEnvEvents(events));
360
- fileContent.periodicEvents = fileContent.periodicEvents.concat(this.#mapEnvEvents(periodicEvents));
412
+ fileContent.periodicEvents = fileContent.periodicEvents
413
+ .concat(this.#mapEnvEvents(periodicEvents))
414
+ .concat(this.#mapCapOutboxPeriodicEvent(periodicCapServiceEvents));
361
415
  this.fileContent = fileContent;
362
416
  }
363
417
 
418
+ #mapCapOutboxPeriodicEvent(periodicEventMap) {
419
+ return Object.values(periodicEventMap);
420
+ }
421
+
422
+ #cdsPeriodicOutboxServicesFromEnv() {
423
+ return Object.entries(cds.env.requires).reduce((result, [name, value]) => {
424
+ if (value.outbox?.events) {
425
+ for (const fnName in value.outbox.events) {
426
+ const base = { ...value.outbox };
427
+ const fnConfig = value.outbox.events[fnName];
428
+ if (fnConfig.interval || fnConfig.cron) {
429
+ if ("interval" in base || "cron" in base) {
430
+ this.#logger.error(
431
+ "The properties interval|cron must be defined in the event section and will be ignored in the outbox section.",
432
+ { serviceName: name }
433
+ );
434
+ delete base.cron;
435
+ delete base.interval;
436
+ }
437
+
438
+ result[fnName] = Object.assign(
439
+ {
440
+ type: CAP_EVENT_TYPE,
441
+ subType: `${name}.${fnName}`,
442
+ impl: "./outbox/EventQueueGenericOutboxHandler",
443
+ internalEvent: true,
444
+ },
445
+ base,
446
+ fnConfig
447
+ );
448
+ }
449
+ }
450
+ }
451
+ return result;
452
+ }, {});
453
+ }
454
+
455
+ getCdsOutboxEventSpecificConfig(serviceName, action) {
456
+ if (cds.env.requires[serviceName]?.outbox?.events?.[action]) {
457
+ return cds.env.requires[serviceName].outbox.events[action];
458
+ } else {
459
+ return null;
460
+ }
461
+ }
462
+
364
463
  #mapEnvEvents(events) {
365
464
  return Object.entries(events)
366
465
  .map(([key, event]) => {
@@ -379,19 +478,21 @@ class Config {
379
478
  config.events = config.events ?? [];
380
479
  config.periodicEvents = config.periodicEvents ?? [];
381
480
  this.#eventMap = config.events.reduce((result, event) => {
382
- this.#basicEventTransformation(event);
383
- this.#validateAdHocEvents(result, event);
384
- this.#basicEventTransformationAfterValidate(event);
385
- result[this.generateKey(event.type, event.subType)] = event;
481
+ const eventSanitized = this.#sanitizeParamsAdHocEvent(event);
482
+ this.#basicEventTransformation(eventSanitized);
483
+ this.#validateAdHocEvents(result, eventSanitized);
484
+ this.#basicEventTransformationAfterValidate(eventSanitized);
485
+ result[this.generateKey(eventSanitized.type, eventSanitized.subType)] = eventSanitized;
386
486
  return result;
387
487
  }, {});
388
488
  this.#eventMap = config.periodicEvents.reduce((result, event) => {
389
- event.type = `${event.type}${SUFFIX_PERIODIC}`;
390
- event.isPeriodic = true;
391
- this.#basicEventTransformation(event);
392
- this.#validatePeriodicConfig(result, event);
393
- this.#basicEventTransformationAfterValidate(event);
394
- result[this.generateKey(event.type, event.subType)] = event;
489
+ const eventSanitized = this.#sanitizeParamsPeriodicEventEvent(event);
490
+ eventSanitized.type = `${eventSanitized.type}${SUFFIX_PERIODIC}`;
491
+ eventSanitized.isPeriodic = true;
492
+ this.#basicEventTransformation(eventSanitized);
493
+ this.#validatePeriodicConfig(result, eventSanitized);
494
+ this.#basicEventTransformationAfterValidate(eventSanitized);
495
+ result[this.generateKey(eventSanitized.type, eventSanitized.subType)] = eventSanitized;
395
496
  return result;
396
497
  }, this.#eventMap);
397
498
  this.#config = config;
@@ -405,6 +506,23 @@ class Config {
405
506
  event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
406
507
  }
407
508
 
509
+ #sanitizeParamsBase(config, allowList) {
510
+ return Object.entries(config).reduce((result, [name, value]) => {
511
+ if (allowList.includes(name)) {
512
+ result[name] = value;
513
+ }
514
+ return result;
515
+ }, {});
516
+ }
517
+
518
+ #sanitizeParamsAdHocEvent(config) {
519
+ return this.#sanitizeParamsBase(config, ALLOWED_EVENT_OPTIONS_AD_HOC);
520
+ }
521
+
522
+ #sanitizeParamsPeriodicEventEvent(config) {
523
+ return this.#sanitizeParamsBase(config, ALLOWED_EVENT_OPTIONS_PERIODIC_EVENT);
524
+ }
525
+
408
526
  #basicEventTransformationAfterValidate(event) {
409
527
  event._appNameMap = event.appNames ? Object.fromEntries(new Map(event.appNames.map((a) => [a, true]))) : null;
410
528
  event._appInstancesMap = event.appInstances
@@ -498,16 +616,12 @@ class Config {
498
616
  throw EventQueueError.invalidInterval(event.type, event.subType, event.interval);
499
617
  }
500
618
 
501
- if (event.multiInstanceProcessing) {
502
- throw EventQueueError.multiInstanceProcessingNotAllowed(event.type, event.subType);
503
- }
504
-
505
619
  this.#basicEventValidation(event);
506
620
  }
507
621
 
508
- #validateAdHocEvents(eventMap, event) {
622
+ #validateAdHocEvents(eventMap, event, checkForDuplication = true) {
509
623
  const key = this.generateKey(event.type, event.subType);
510
- if (eventMap[key] && !eventMap[key].isPeriodic) {
624
+ if (eventMap[key] && !eventMap[key].isPeriodic && checkForDuplication) {
511
625
  throw EventQueueError.duplicateEventRegistration(event.type, event.subType);
512
626
  }
513
627
 
@@ -540,7 +654,7 @@ class Config {
540
654
  }
541
655
 
542
656
  get events() {
543
- return this.#config.events;
657
+ return Object.values(this.#eventMap).filter((e) => !e.isPeriodic);
544
658
  }
545
659
 
546
660
  set configEvents(value) {
@@ -556,7 +670,7 @@ class Config {
556
670
  }
557
671
 
558
672
  get periodicEvents() {
559
- return this.#config.periodicEvents;
673
+ return Object.values(this.#eventMap).filter((e) => e.isPeriodic);
560
674
  }
561
675
 
562
676
  isPeriodicEvent(type, subType) {
@@ -564,7 +678,7 @@ class Config {
564
678
  }
565
679
 
566
680
  get allEvents() {
567
- return this.#config.events.concat(this.#config.periodicEvents);
681
+ return Object.values(this.#eventMap);
568
682
  }
569
683
 
570
684
  get forUpdateTimeout() {
package/src/index.d.ts CHANGED
@@ -194,7 +194,7 @@ declare class Config {
194
194
  blockEvent(type: any, subType: any, isPeriodic: any, tenant?: string): void;
195
195
  clearPeriodicEventBlockList(): void;
196
196
  unblockEvent(type: any, subType: any, isPeriodic: any, tenant?: string): void;
197
- addCAPOutboxEvent(serviceName: any, config: any): void;
197
+ addCAPOutboxEventBase(serviceName: any, config: any): void;
198
198
  isEventBlocked(type: any, subType: any, isPeriodicEvent: any, tenant: any): any;
199
199
  set isEventQueueActive(value: boolean);
200
200
  get isEventQueueActive(): boolean;
@@ -15,9 +15,22 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
15
15
  this.logger = cds.log(`${COMPONENT_NAME}/${eventSubType}`);
16
16
  }
17
17
 
18
+ async processPeriodicEvent(processContext, key, queueEntry) {
19
+ const [serviceName, action] = this.eventSubType.split(".");
20
+ const service = await cds.connect.to(serviceName);
21
+ const msg = new cds.Event({ event: action });
22
+ processContext.user = new cds.User.Privileged({
23
+ id: config.userId,
24
+ authInfo: await common.getTokenInfo(processContext.tenant),
25
+ });
26
+ processContext._eventQueue = { processor: this, key, queueEntries: [queueEntry] };
27
+ await cds.unboxed(service).tx(processContext)["emit"](msg);
28
+ }
29
+
18
30
  async processEvent(processContext, key, queueEntries, payload) {
19
31
  try {
20
- const service = await cds.connect.to(this.eventSubType);
32
+ const [srvName] = this.eventSubType.split(".");
33
+ const service = await cds.connect.to(srvName);
21
34
  const { useEventQueueUser } = this.eventConfig;
22
35
  const userId = useEventQueueUser ? config.userId : payload.contextUser;
23
36
  const msg = payload._fromSend ? new cds.Request(payload) : new cds.Event(payload);
@@ -20,16 +20,14 @@ function outboxed(srv, customOpts) {
20
20
  }
21
21
 
22
22
  const logger = cds.log(COMPONENT_NAME);
23
- const outboxOpts = Object.assign(
23
+ let outboxOpts = Object.assign(
24
24
  {},
25
25
  (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
26
26
  (typeof srv.options?.outbox === "object" && srv.options.outbox) || {},
27
27
  customOpts || {}
28
28
  );
29
29
 
30
- if (outboxOpts.kind === "persistent-outbox") {
31
- config.addCAPOutboxEvent(srv.name, outboxOpts);
32
- }
30
+ config.addCAPOutboxEventBase(srv.name, outboxOpts);
33
31
 
34
32
  const originalSrv = srv[UNBOXED] || srv;
35
33
  const outboxedSrv = Object.create(originalSrv);
@@ -42,9 +40,20 @@ function outboxed(srv, customOpts) {
42
40
  }
43
41
  outboxedSrv.handle = async function (req) {
44
42
  const context = req.context || cds.context;
43
+ outboxOpts = Object.assign(
44
+ {},
45
+ (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
46
+ (typeof srv.options?.outbox === "object" && srv.options.outbox) || {},
47
+ customOpts || {}
48
+ );
49
+ config.addCAPOutboxEventBase(srv.name, outboxOpts);
50
+ const specificSettings = config.getCdsOutboxEventSpecificConfig(srv.name, req.method);
51
+ if (specificSettings) {
52
+ outboxOpts = config.addCAPOutboxEventSpecificAction(srv.name, req.event);
53
+ }
54
+
45
55
  if (outboxOpts.kind === "persistent-outbox") {
46
- config.addCAPOutboxEvent(srv.name, outboxOpts);
47
- await _mapToEventAndPublish(context, srv.name, req);
56
+ await _mapToEventAndPublish(context, srv.name, req, !!specificSettings);
48
57
  return;
49
58
  }
50
59
  context.on("succeeded", async () => {
@@ -70,7 +79,7 @@ function unboxed(srv) {
70
79
  return srv[UNBOXED] || srv;
71
80
  }
72
81
 
73
- const _mapToEventAndPublish = async (context, name, req) => {
82
+ const _mapToEventAndPublish = async (context, name, req, actionSpecific) => {
74
83
  const eventQueueSpecificValues = {};
75
84
  for (const header in req.headers ?? {}) {
76
85
  for (const field of EVENT_QUEUE_SPECIFIC_FIELDS) {
@@ -93,7 +102,7 @@ const _mapToEventAndPublish = async (context, name, req) => {
93
102
 
94
103
  await publishEvent(cds.tx(context), {
95
104
  type: CDS_EVENT_TYPE,
96
- subType: name,
105
+ subType: actionSpecific ? [name, req.event].join(".") : name,
97
106
  payload: JSON.stringify(event),
98
107
  ...eventQueueSpecificValues,
99
108
  });
@@ -4,6 +4,7 @@ const cds = require("@sap/cds");
4
4
 
5
5
  const eventConfig = require("../config");
6
6
  const { EventProcessingStatus } = require("../constants");
7
+ const config = require("../config");
7
8
 
8
9
  const MS_IN_DAYS = 24 * 60 * 60 * 1000;
9
10
 
@@ -36,8 +37,9 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
36
37
  const result = [];
37
38
  for (const { type, subType } of entries) {
38
39
  if (eventConfig.isCapOutboxEvent(type)) {
40
+ const [srvName, actionName] = subType.split(".");
39
41
  cds.connect
40
- .to(subType)
42
+ .to(srvName)
41
43
  .then((service) => {
42
44
  if (!filterAppSpecificEvents) {
43
45
  return; // will be done in finally
@@ -47,6 +49,9 @@ const getOpenQueueEntries = async (tx, filterAppSpecificEvents = true) => {
47
49
  return;
48
50
  }
49
51
  cds.outboxed(service);
52
+ if (actionName) {
53
+ config.addCAPOutboxEventSpecificAction(srvName, actionName);
54
+ }
50
55
  if (filterAppSpecificEvents) {
51
56
  if (eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
52
57
  result.push({ type, subType });
@@ -45,12 +45,6 @@ const trace = async (context, label, fn, { attributes = {}, newRootSpan = false,
45
45
 
46
46
  const _startOtelTrace = async (ctxWithSpan, traceContext, span, fn) => {
47
47
  return otel.context.with(ctxWithSpan, async () => {
48
- if (traceContext) {
49
- cds.log("/eventQueue/telemetry").info("Linked span:", span.spanContext());
50
- const carrier = {};
51
- otel.propagation.inject(ctxWithSpan, carrier);
52
- cds.log("/eventQueue/telemetry").info("Extracted trace context by inject", carrier);
53
- }
54
48
  const onSuccess = (res) => {
55
49
  span.setStatus({ code: otel.SpanStatusCode.OK });
56
50
  return res;