@cap-js-community/event-queue 2.0.3 → 2.0.5

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/db/Event.cds CHANGED
@@ -24,6 +24,6 @@ entity Event: cuid {
24
24
  createdAt: Timestamp @cds.on.insert : $now;
25
25
  startAfter: Timestamp;
26
26
  context: LargeString;
27
- error: String;
27
+ error: LargeString;
28
28
  namespace: String default 'default';
29
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
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",
@@ -22,8 +22,6 @@
22
22
  "multi-tenancy"
23
23
  ],
24
24
  "scripts": {
25
- "start": "PORT=4005 cds-serve",
26
- "watch": "PORT=4005 cds watch",
27
25
  "test:unit": "jest --selectProjects unit",
28
26
  "test:integration": "jest --selectProjects integration --runInBand",
29
27
  "voter:test:integration": "jest --selectProjects integration",
@@ -38,39 +36,35 @@
38
36
  "eslint:ci": "eslint .",
39
37
  "prettier": "prettier --write --loglevel error .",
40
38
  "prettier:ci": "prettier --check .",
41
- "prepareRelease": "npm prune --production",
42
39
  "docs": "cd docs && bundle exec jekyll serve",
43
40
  "docs:install": "cd docs && npx shx rm -rf vendor Gemfile.lock && bundle install",
44
41
  "upgrade-lock": "npx shx rm -rf package-lock.json node_modules && npm i --package-lock"
45
42
  },
46
43
  "engines": {
47
- "node": ">=18"
44
+ "node": ">=20"
48
45
  },
49
46
  "dependencies": {
50
- "@cap-js-community/common": "0.3.2",
51
- "@sap/xssec": "^4.11.0",
52
- "cron-parser": "^5.4.0",
47
+ "@cap-js-community/common": "^0.4.0",
48
+ "@sap/xssec": "^4.12.2",
49
+ "cron-parser": "^5.5.0",
53
50
  "verror": "^1.10.1",
54
- "yaml": "^2.7.1"
51
+ "yaml": "^2.8.2"
55
52
  },
56
53
  "devDependencies": {
57
- "@actions/core": "^1.11.1",
58
- "@cap-js/cds-test": "^0.4.0",
59
- "@cap-js/db-service": "^2.6.0",
60
- "@cap-js/hana": "^2.3.4",
61
- "@cap-js/sqlite": "^2.1.0",
54
+ "@actions/core": "^2.0.2",
55
+ "@cap-js/cds-test": "^0.4.1",
56
+ "@cap-js/db-service": "^2.8.2",
57
+ "@cap-js/hana": "^2.6.0",
58
+ "@cap-js/sqlite": "^2.1.3",
62
59
  "@opentelemetry/api": "^1.9.0",
63
- "@sap/cds": "^9.4.5",
64
- "@sap/cds-dk": "^9.4.2",
65
- "eslint": "^8.57.0",
66
- "eslint-config-prettier": "^9.1.0",
67
- "eslint-plugin-jest": "^28.6.0",
60
+ "@sap/cds": "^9.7.0",
61
+ "@sap/cds-dk": "^9.7.0",
62
+ "eslint": "^8.57.1",
63
+ "eslint-config-prettier": "^10.1.8",
64
+ "eslint-plugin-jest": "^29.12.1",
68
65
  "eslint-plugin-node": "^11.1.0",
69
- "express": "^4.21.2",
70
- "hdb": "^2.26.1",
71
66
  "jest": "^29.7.0",
72
- "prettier": "^2.8.8",
73
- "sqlite3": "^5.1.7"
67
+ "prettier": "^2.8.8"
74
68
  },
75
69
  "homepage": "https://cap-js-community.github.io/event-queue/",
76
70
  "repository": {
@@ -106,8 +100,7 @@
106
100
  "requires": {
107
101
  "xsuaa-eventQueue": {
108
102
  "vcap": {
109
- "label": "xsuaa",
110
- "plan": "application"
103
+ "label": "xsuaa"
111
104
  }
112
105
  },
113
106
  "redis-eventQueue": {
@@ -25,7 +25,7 @@ const TRIES_FOR_EXCEEDED_EVENTS = 3;
25
25
  const EVENT_START_AFTER_HEADROOM = 3 * 1000;
26
26
  const SUFFIX_PERIODIC = "_PERIODIC";
27
27
 
28
- const ALLOWED_FIELDS_FOR_UPDATE = ["status", "startAfter", "error"];
28
+ const ALLOWED_FIELDS_FOR_UPDATE = ["status", "startAfter", "error", "nextData"];
29
29
 
30
30
  class EventQueueProcessorBase {
31
31
  #eventsWithExceededTries = [];
@@ -43,6 +43,7 @@ class EventQueueProcessorBase {
43
43
  #currentKeepAlivePromise = Promise.resolve();
44
44
  #etagMap;
45
45
  #namespace;
46
+ #nextSagaEvents;
46
47
 
47
48
  constructor(context, eventType, eventSubType, config) {
48
49
  this.__context = context;
@@ -407,7 +408,7 @@ class EventQueueProcessorBase {
407
408
  UPDATE.entity(this.#config.tableNameEventQueue)
408
409
  .set({
409
410
  status: status,
410
- ...(error && { error: this.#error2String(error) }),
411
+ ...(error && { error: this._error2String(error) }),
411
412
  })
412
413
  .where({
413
414
  ID: queueEntryIds,
@@ -468,6 +469,10 @@ class EventQueueProcessorBase {
468
469
  return result;
469
470
  }, {});
470
471
 
472
+ if (!Object.values(updateData).length) {
473
+ return;
474
+ }
475
+
471
476
  for (const { ids, data } of Object.values(updateData)) {
472
477
  if (!("status" in data)) {
473
478
  // TODO: Can this still happen?
@@ -493,16 +498,22 @@ class EventQueueProcessorBase {
493
498
  }
494
499
 
495
500
  if (data.error) {
496
- data.error = this.#error2String(data.error);
501
+ data.error = this._error2String(data.error);
497
502
  }
498
503
 
499
504
  if (!data.startAfter && [EventProcessingStatus.Error, EventProcessingStatus.Open].includes(data.status)) {
500
505
  data.startAfter = new Date(
501
506
  Date.now() +
502
- (EventProcessingStatus.Error ? this.#eventConfig.retryFailedAfter : this.#eventConfig.retryOpenAfter)
507
+ (data.status === EventProcessingStatus.Error
508
+ ? this.#eventConfig.retryFailedAfter
509
+ : this.#eventConfig.retryOpenAfter)
503
510
  );
504
511
  }
505
512
 
513
+ if (data.status === EventProcessingStatus.Open && !("attempts" in data)) {
514
+ data.attempts = { "-=": 1 };
515
+ }
516
+
506
517
  if (data.startAfter) {
507
518
  this.#eventSchedulerInstance.scheduleEvent(
508
519
  this.__context.tenant,
@@ -522,10 +533,15 @@ class EventQueueProcessorBase {
522
533
  .where("ID IN", ids)
523
534
  );
524
535
  }
536
+
537
+ if (this.#nextSagaEvents?.length) {
538
+ await tx.run(INSERT.into(this.#config.tableNameEventQueue).entries(this.#nextSagaEvents));
539
+ this.#nextSagaEvents = [];
540
+ }
525
541
  });
526
542
  }
527
543
 
528
- #error2String(error) {
544
+ _error2String(error) {
529
545
  return JSON.stringify(error, (_, value) => this.#errorReplacer(value));
530
546
  }
531
547
 
@@ -1390,6 +1406,10 @@ class EventQueueProcessorBase {
1390
1406
  get allowedFieldsEventHandler() {
1391
1407
  return ALLOWED_FIELDS_FOR_UPDATE;
1392
1408
  }
1409
+
1410
+ set nextSagaEvents(value) {
1411
+ this.#nextSagaEvents = value;
1412
+ }
1393
1413
  }
1394
1414
 
1395
1415
  module.exports = EventQueueProcessorBase;
package/src/config.js CHANGED
@@ -28,6 +28,8 @@ const DEFAULT_RETRY_AFTER = 5 * 60 * 1000;
28
28
  const PRIORITIES = Object.values(Priorities);
29
29
  const UTC_DEFAULT = false;
30
30
  const USE_CRON_TZ_DEFAULT = true;
31
+ const SAGA_SUCCESS = "#succeeded";
32
+ const SAGA_FAILED = "#failed";
31
33
 
32
34
  const BASE_TABLES = {
33
35
  EVENT: "sap.eventqueue.Event",
@@ -62,6 +64,7 @@ const ALLOWED_EVENT_OPTIONS_AD_HOC = [
62
64
  "checkForNextChunk",
63
65
  "retryFailedAfter",
64
66
  "propagateHeaders",
67
+ "propagateContextProperties",
65
68
  "retryOpenAfter",
66
69
  "multiInstanceProcessing",
67
70
  "kind",
@@ -236,7 +239,7 @@ class Config {
236
239
 
237
240
  executeUnsubscribeHandlers(tenantId) {
238
241
  this.#unsubscribedTenants[tenantId] = true;
239
- setTimeout(() => delete this.#unsubscribedTenants[tenantId], DELETE_TENANT_BLOCK_AFTER_MS);
242
+ setTimeout(() => delete this.#unsubscribedTenants[tenantId], DELETE_TENANT_BLOCK_AFTER_MS).unref();
240
243
  for (const unsubscribeHandler of this.#unsubscribeHandlers) {
241
244
  try {
242
245
  unsubscribeHandler(tenantId);
@@ -383,6 +386,20 @@ class Config {
383
386
  result.adHoc
384
387
  );
385
388
  result.adHoc[key] = specificEventConfig;
389
+ const sagaSuccessKey = [fnName, SAGA_SUCCESS].join("/");
390
+ if (config.events[sagaSuccessKey]) {
391
+ const [sagaKey, sagaSpecificEventConfig] = this.addCAPOutboxEventSpecificAction(
392
+ srvConfig,
393
+ name,
394
+ fnName,
395
+ result.adHoc
396
+ );
397
+ result.adHoc[sagaKey] = sagaSpecificEventConfig;
398
+ } else {
399
+ const sagaConfig = { ...specificEventConfig };
400
+ sagaConfig.subType = [sagaConfig.subType, SAGA_SUCCESS].join("/");
401
+ result.adHoc[[key, SAGA_SUCCESS].join("/")] = sagaConfig;
402
+ }
386
403
  }
387
404
  }
388
405
  return result;
@@ -405,11 +422,24 @@ class Config {
405
422
  getCdsOutboxEventSpecificConfig(serviceName, action) {
406
423
  const srv = cds.env.requires[serviceName];
407
424
  const config = srv?.outbox ?? srv?.queued;
408
- if (config?.events?.[action]) {
425
+ const specificConfig = config?.events?.[action];
426
+
427
+ if (specificConfig) {
409
428
  return this.#mixCAPPropertyNamesWithEventQueueNames(config.events[action]);
410
- } else {
429
+ }
430
+
431
+ if (!action) {
411
432
  return null;
412
433
  }
434
+
435
+ const [withoutSaga, sagaSuffix] = action.split("/");
436
+ if ([SAGA_FAILED, SAGA_SUCCESS].includes(sagaSuffix)) {
437
+ if (config?.events?.[withoutSaga]) {
438
+ return this.#mixCAPPropertyNamesWithEventQueueNames(config.events[withoutSaga]);
439
+ }
440
+ }
441
+
442
+ return null;
413
443
  }
414
444
 
415
445
  #mapEnvEvents(events) {
@@ -488,6 +518,7 @@ class Config {
488
518
  ? Object.fromEntries(new Map(event.appInstances.map((a) => [a, true])))
489
519
  : null;
490
520
  event.propagateHeaders = event.propagateHeaders ?? [];
521
+ event.propagateContextProperties = event.propagateContextProperties ?? [];
491
522
  event.retryFailedAfter = event.retryFailedAfter ?? DEFAULT_RETRY_AFTER;
492
523
  event.retryOpenAfter = event.retryOpenAfter ?? DEFAULT_RETRY_AFTER;
493
524
  }
package/src/initialize.js CHANGED
@@ -123,6 +123,7 @@ const initialize = async (options = {}) => {
123
123
  multiTenancyEnabled: config.isMultiTenancy,
124
124
  redisEnabled: config.redisEnabled,
125
125
  runInterval: config.runInterval,
126
+ useAsCAPQueue: config.useAsCAPQueue,
126
127
  });
127
128
  resolveFn();
128
129
  };
@@ -14,6 +14,8 @@ const EVENT_QUEUE_ACTIONS = {
14
14
  EXCEEDED: "eventQueueRetriesExceeded",
15
15
  CLUSTER: "eventQueueCluster",
16
16
  CHECK_AND_ADJUST: "eventQueueCheckAndAdjustPayload",
17
+ SAGA_SUCCESS: "#succeeded",
18
+ SAGA_FAILED: "#failed",
17
19
  };
18
20
 
19
21
  class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
@@ -25,7 +27,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
25
27
  async getQueueEntriesAndSetToInProgress() {
26
28
  const { srvName } = config.normalizeSubType(this.eventType, this.eventSubType);
27
29
  this.__srv = await cds.connect.to(srvName);
28
- this.__srvUnboxed = cds.unboxed(this.__srv);
30
+ this.__srvUnboxed = cds.unqueued(this.__srv);
29
31
  const { handlers, clusterRelevant, specificClusterRelevant } = this.__srvUnboxed.handlers.on.reduce(
30
32
  (result, handler) => {
31
33
  if (handler.on.startsWith(EVENT_QUEUE_ACTIONS.CLUSTER)) {
@@ -62,7 +64,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
62
64
  }
63
65
  } else {
64
66
  for (const actionName in genericClusterEvents) {
65
- const reg = new cds.Request({
67
+ const req = new cds.Request({
66
68
  event: EVENT_QUEUE_ACTIONS.CLUSTER,
67
69
  user: this.context.user,
68
70
  eventQueue: {
@@ -75,14 +77,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
75
77
  this.#clusterByDataProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
76
78
  },
77
79
  });
78
- const clusterResult = await this.__srvUnboxed.tx(this.context).send(reg);
80
+ const clusterResult = await this.__srvUnboxed.tx(this.context).send(req);
79
81
  if (this.#validateCluster(clusterResult)) {
80
82
  Object.assign(clusterMap, clusterResult);
81
83
  } else {
82
84
  this.logger.error(
83
85
  "cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
84
86
  {
85
- handler: reg.event,
87
+ handler: req.event,
86
88
  clusterResult: JSON.stringify(clusterResult),
87
89
  }
88
90
  );
@@ -93,7 +95,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
93
95
  }
94
96
 
95
97
  for (const actionName in specificClusterEvents) {
96
- const reg = new cds.Request({
98
+ const req = new cds.Request({
97
99
  event: `${EVENT_QUEUE_ACTIONS.CLUSTER}.${actionName}`,
98
100
  user: this.context.user,
99
101
  eventQueue: {
@@ -106,14 +108,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
106
108
  this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
107
109
  },
108
110
  });
109
- const clusterResult = await this.__srvUnboxed.tx(this.context).send(reg);
111
+ const clusterResult = await this.__srvUnboxed.tx(this.context).send(req);
110
112
  if (this.#validateCluster(clusterResult)) {
111
113
  Object.assign(clusterMap, clusterResult);
112
114
  } else {
113
115
  this.logger.error(
114
116
  "cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
115
117
  {
116
- handler: reg.event,
118
+ handler: req.event,
117
119
  clusterResult: JSON.stringify(clusterResult),
118
120
  }
119
121
  );
@@ -260,17 +262,17 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
260
262
  async checkEventAndGeneratePayload(queueEntry) {
261
263
  const payload = await super.checkEventAndGeneratePayload(queueEntry);
262
264
  const { event } = payload;
263
- const handlerName = this.#checkHandlerExists(EVENT_QUEUE_ACTIONS.CHECK_AND_ADJUST, event);
265
+ const handlerName = this.#checkHandlerExists({ eventQueueFn: EVENT_QUEUE_ACTIONS.CHECK_AND_ADJUST, event });
264
266
  if (!handlerName) {
265
267
  return payload;
266
268
  }
267
269
 
268
- const { reg, userId } = this.#buildDispatchData(this.context, payload, {
270
+ const { req, userId } = this.#buildDispatchData(payload, {
269
271
  queueEntries: [queueEntry],
270
272
  });
271
- reg.event = handlerName;
272
- await this.#setContextUser(this.context, userId, reg);
273
- const data = await this.__srvUnboxed.tx(this.context).send(reg);
273
+ req.event = handlerName;
274
+ await this.#setContextUser(this.context, userId, req);
275
+ const data = await this.__srvUnboxed.tx(this.context).send(req);
274
276
  if (data) {
275
277
  payload.data = data;
276
278
  return payload;
@@ -281,17 +283,17 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
281
283
 
282
284
  async hookForExceededEvents(exceededEvent) {
283
285
  const { event } = exceededEvent.payload;
284
- const handlerName = this.#checkHandlerExists(EVENT_QUEUE_ACTIONS.EXCEEDED, event);
286
+ const handlerName = this.#checkHandlerExists({ eventQueueFn: EVENT_QUEUE_ACTIONS.EXCEEDED, event });
285
287
  if (!handlerName) {
286
288
  return await super.hookForExceededEvents(exceededEvent);
287
289
  }
288
290
 
289
- const { reg, userId } = this.#buildDispatchData(this.context, exceededEvent.payload, {
291
+ const { req, userId } = this.#buildDispatchData(exceededEvent.payload, {
290
292
  queueEntries: [exceededEvent],
291
293
  });
292
- await this.#setContextUser(this.context, userId, reg);
293
- reg.event = handlerName;
294
- await this.__srvUnboxed.tx(this.context).send(reg);
294
+ await this.#setContextUser(this.context, userId, req);
295
+ req.event = handlerName;
296
+ await this.__srvUnboxed.tx(this.context).send(req);
295
297
  }
296
298
 
297
299
  // NOTE: Currently not exposed to CAP service; we wait for a valid use case
@@ -299,57 +301,78 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
299
301
  return await super.beforeProcessingEvents();
300
302
  }
301
303
 
302
- #checkHandlerExists(eventQueueFn, event) {
303
- const specificHandler = this.__onHandlers[[eventQueueFn, event].join(".")];
304
+ #checkHandlerExists({ eventQueueFn, event, saga } = {}) {
305
+ if (eventQueueFn) {
306
+ const specificHandler = this.__onHandlers[[eventQueueFn, event].join(".")];
307
+ if (specificHandler) {
308
+ return specificHandler;
309
+ }
310
+
311
+ const genericHandler = this.__onHandlers[eventQueueFn];
312
+ return genericHandler ?? null;
313
+ }
314
+
315
+ if (event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_SUCCESS)) {
316
+ [event] = event.split("/");
317
+ }
318
+
319
+ const specificHandler = this.__onHandlers[[event, saga].join("/")];
304
320
  if (specificHandler) {
305
321
  return specificHandler;
306
322
  }
307
323
 
308
- const genericHandler = this.__onHandlers[eventQueueFn];
309
- return genericHandler ?? null;
324
+ return this.__onHandlers[saga];
310
325
  }
311
326
 
312
327
  async processPeriodicEvent(processContext, key, queueEntry) {
313
328
  const { actionName } = config.normalizeSubType(this.eventType, this.eventSubType);
314
- const reg = new cds.Event({ event: actionName, eventQueue: { processor: this, key, queueEntries: [queueEntry] } });
315
- await this.#setContextUser(processContext, config.userId, reg);
316
- await this.__srvUnboxed.tx(processContext).emit(reg);
329
+ const req = new cds.Event({ event: actionName, eventQueue: { processor: this, key, queueEntries: [queueEntry] } });
330
+ await this.#setContextUser(processContext, config.userId, req);
331
+ await this.__srvUnboxed.tx(processContext).emit(req);
317
332
  }
318
333
 
319
- #buildDispatchData(context, payload, { key, queueEntries } = {}) {
334
+ #buildDispatchData(payload, { key, queueEntries } = {}) {
320
335
  const { useEventQueueUser } = this.eventConfig;
321
336
  const userId = useEventQueueUser ? config.userId : payload.contextUser;
322
- const reg = payload._fromSend ? new cds.Request(payload) : new cds.Event(payload);
337
+ const req = payload._fromSend ? new cds.Request(payload) : new cds.Event(payload);
323
338
  const invocationFn = payload._fromSend ? "send" : "emit";
324
- delete reg._fromSend;
325
- delete reg.contextUser;
326
- reg.eventQueue = { processor: this, key, queueEntries, payload };
327
- return { reg, userId, invocationFn };
339
+ delete req._fromSend;
340
+ delete req.contextUser;
341
+ req.eventQueue = { processor: this, key, queueEntries, payload };
342
+
343
+ if (this.eventConfig.propagateContextProperties?.length && this.transactionMode === "isolated" && cds.context) {
344
+ for (const prop of this.eventConfig.propagateContextProperties) {
345
+ req[prop] && (cds.context[prop] = req[prop]);
346
+ }
347
+ }
348
+
349
+ return { req, userId, invocationFn };
328
350
  }
329
351
 
330
- async #setContextUser(context, userId, reg) {
352
+ async #setContextUser(context, userId, req) {
331
353
  const authInfo = await common.getAuthContext(context.tenant);
332
354
  context.user = new cds.User.Privileged({
333
355
  id: userId,
334
356
  authInfo,
335
357
  tokenInfo: authInfo?.token,
336
358
  });
337
- if (reg) {
338
- reg.user = context.user;
359
+ if (req) {
360
+ req.user = context.user;
339
361
  }
340
362
  }
341
363
 
342
364
  async processEvent(processContext, key, queueEntries, payload) {
365
+ let statusTuple;
366
+ const { userId, invocationFn, req } = this.#buildDispatchData(payload, { key, queueEntries });
343
367
  try {
344
- const { userId, invocationFn, reg } = this.#buildDispatchData(processContext, payload, { key, queueEntries });
345
- await this.#setContextUser(processContext, userId, reg);
346
- const result = await this.__srvUnboxed.tx(processContext)[invocationFn](reg);
347
- return this.#determineResultStatus(result, queueEntries);
368
+ await this.#setContextUser(processContext, userId, req);
369
+ const result = await this.__srvUnboxed.tx(processContext)[invocationFn](req);
370
+ statusTuple = this.#determineResultStatus(result, queueEntries);
348
371
  } catch (err) {
349
372
  this.logger.error("error processing outboxed service call", err, {
350
373
  serviceName: this.eventSubType,
351
374
  });
352
- return queueEntries.map((queueEntry) => [
375
+ statusTuple = queueEntries.map((queueEntry) => [
353
376
  queueEntry.ID,
354
377
  {
355
378
  status: EventProcessingStatus.Error,
@@ -357,13 +380,65 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
357
380
  },
358
381
  ]);
359
382
  }
383
+
384
+ await this.#publishFollowupEvents(processContext, req, statusTuple);
385
+ return statusTuple;
386
+ }
387
+
388
+ async #publishFollowupEvents(processContext, req, statusTuple) {
389
+ const succeeded = this.#checkHandlerExists({ event: req.event, saga: EVENT_QUEUE_ACTIONS.SAGA_SUCCESS });
390
+ const failed = this.#checkHandlerExists({ event: req.event, saga: EVENT_QUEUE_ACTIONS.SAGA_FAILED });
391
+
392
+ if (!succeeded && !failed) {
393
+ return;
394
+ }
395
+
396
+ if (req.event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_FAILED)) {
397
+ return;
398
+ }
399
+
400
+ // NOTE: required for #failed because tx is rolledback and new events would not be commmited!
401
+ const tx = cds.tx(processContext);
402
+ const nextEvents = tx._eventQueue?.events;
403
+
404
+ if (nextEvents?.length) {
405
+ tx._eventQueue.events = [];
406
+ }
407
+
408
+ for (const [, result] of statusTuple) {
409
+ const data = result.nextData ?? req.data;
410
+ if (
411
+ succeeded &&
412
+ result.status === EventProcessingStatus.Done &&
413
+ !req.event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_SUCCESS)
414
+ ) {
415
+ await this.__srv.tx(processContext).send(succeeded, data);
416
+ }
417
+
418
+ if (failed && result.status === EventProcessingStatus.Error) {
419
+ result.error && (data.error = this._error2String(result.error));
420
+ await this.__srv.tx(processContext).send(failed, data);
421
+ }
422
+
423
+ delete result.nextData;
424
+ }
425
+
426
+ if (config.insertEventsBeforeCommit) {
427
+ this.nextSagaEvents = tx._eventQueue?.events;
428
+ } else {
429
+ this.nextSagaEvents = tx._eventQueue?.events.filter((event) => JSON.parse(event.payload).event === failed);
430
+ }
431
+
432
+ if (tx._eventQueue) {
433
+ tx._eventQueue.events = nextEvents ?? [];
434
+ }
360
435
  }
361
436
 
362
437
  #determineResultStatus(result, queueEntries) {
363
438
  const validStatusValues = Object.values(EventProcessingStatus);
364
439
  const validStatus = validStatusValues.includes(result);
365
440
  if (validStatus) {
366
- return queueEntries.map((queueEntry) => [queueEntry.ID, result]);
441
+ return queueEntries.map((queueEntry) => [queueEntry.ID, { status: result }]);
367
442
  }
368
443
 
369
444
  if (result instanceof Object && !Array.isArray(result)) {
@@ -375,7 +450,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
375
450
  }
376
451
 
377
452
  if (!Array.isArray(result)) {
378
- return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
453
+ return queueEntries.map((queueEntry) => [queueEntry.ID, { status: EventProcessingStatus.Done }]);
379
454
  }
380
455
 
381
456
  const [firstEntry] = result;
@@ -430,7 +505,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
430
505
  if (valid) {
431
506
  return result;
432
507
  } else {
433
- return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
508
+ return queueEntries.map((queueEntry) => [queueEntry.ID, { status: EventProcessingStatus.Done }]);
434
509
  }
435
510
  }
436
511
  }
@@ -11,6 +11,8 @@ const CDS_EVENT_TYPE = "CAP_OUTBOX";
11
11
  const COMPONENT_NAME = "/eventQueue/eventQueueAsOutbox";
12
12
  const EVENT_QUEUE_SPECIFIC_FIELDS = ["startAfter", "referenceEntity", "referenceEntityKey", "namespace"];
13
13
 
14
+ const TO_COPY = ["inbound", "event", "data", "queue", "results", "method", "path", "params", "entity", "service"];
15
+
14
16
  function outboxed(srv, customOpts) {
15
17
  if (!(new.target || customOpts)) {
16
18
  const former = srv[OUTBOXED];
@@ -43,8 +45,9 @@ function outboxed(srv, customOpts) {
43
45
 
44
46
  const outboxOpts = config.getEventConfig(CDS_EVENT_TYPE, subType, srvConfig.namespace);
45
47
  const eventHeaders = getPropagatedHeaders(outboxOpts, req);
48
+ const contextProperties = getPropagateContextProperties(outboxOpts, req);
46
49
  if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
47
- await _mapToEventAndPublish(context, subType, req, eventHeaders, srvConfig.namespace);
50
+ await _mapToEventAndPublish(req, srvConfig.namespace, subType, eventHeaders, contextProperties);
48
51
  return;
49
52
  }
50
53
  context.on("succeeded", async () => {
@@ -80,7 +83,17 @@ const getPropagatedHeaders = (config, req) => {
80
83
  return Object.assign(propagateHeaders, req.headers);
81
84
  };
82
85
 
83
- const _mapToEventAndPublish = async (context, subType, req, eventHeaders, namespace) => {
86
+ const getPropagateContextProperties = (config, req) => {
87
+ return config.propagateContextProperties.reduce((properties, name) => {
88
+ if (name in req.tx.context) {
89
+ properties[name] = req.tx.context[name];
90
+ }
91
+ return properties;
92
+ }, {});
93
+ };
94
+
95
+ const _mapToEventAndPublish = async (req, namespace, subType, eventHeaders, contextProperties) => {
96
+ const context = req.context || cds.context;
84
97
  const eventQueueSpecificValues = {};
85
98
  for (const header in req.headers ?? {}) {
86
99
  for (const field of EVENT_QUEUE_SPECIFIC_FIELDS) {
@@ -91,17 +104,27 @@ const _mapToEventAndPublish = async (context, subType, req, eventHeaders, namesp
91
104
  }
92
105
  }
93
106
  }
94
-
95
107
  const event = {
96
108
  contextUser: context.user.id,
97
109
  ...(req._fromSend || (req.reply && { _fromSend: true })), // send or emit
98
- ...(req.inbound && { inbound: req.inbound }),
99
- ...(req.event && { event: req.event }),
100
- ...(req.data && { data: req.data }),
101
110
  ...(eventHeaders && { headers: eventHeaders }),
102
- ...(req.query && { query: req.query }),
111
+ ...(Object.keys(contextProperties).length && { ...contextProperties }),
103
112
  };
104
113
 
114
+ for (const prop of TO_COPY) {
115
+ if (req[prop]) {
116
+ event[prop] = req[prop];
117
+ }
118
+ }
119
+
120
+ if (req.query) {
121
+ event.query = typeof req.query.flat === "function" ? req.query.flat() : req.query;
122
+ delete event.query._target;
123
+ delete event.query.__target;
124
+ delete event.query.target;
125
+ delete event.data; // `req.data` should be a getter to whatever is in `req.query`
126
+ }
127
+
105
128
  await publishEvent(
106
129
  cds.tx(context),
107
130
  {
@@ -76,8 +76,9 @@ const publishEvent = async (
76
76
  event.namespace = config.namespace;
77
77
  }
78
78
  }
79
+ _addEventsToContext(tx, events);
79
80
  if (config.insertEventsBeforeCommit && !skipInsertEventsBeforeCommit) {
80
- _registerHandlerAndAddEvents(tx, events, skipBroadcast);
81
+ _registerHandler(tx, skipBroadcast);
81
82
  } else {
82
83
  let result;
83
84
  tx._skipEventQueueBroadcast = skipBroadcast;
@@ -87,10 +88,7 @@ const publishEvent = async (
87
88
  }
88
89
  };
89
90
 
90
- const _registerHandlerAndAddEvents = (tx, events, skipBroadcast) => {
91
- tx._eventQueue ??= { events: [], handlerRegistered: false };
92
- tx._eventQueue.events = tx._eventQueue.events.concat(events);
93
-
91
+ const _registerHandler = (tx, skipBroadcast) => {
94
92
  if (tx._eventQueue.handlerRegistered) {
95
93
  return;
96
94
  }
@@ -106,6 +104,11 @@ const _registerHandlerAndAddEvents = (tx, events, skipBroadcast) => {
106
104
  });
107
105
  };
108
106
 
107
+ const _addEventsToContext = (tx, events) => {
108
+ tx._eventQueue ??= { events: [], handlerRegistered: false };
109
+ tx._eventQueue.events = tx._eventQueue.events.concat(events);
110
+ };
111
+
109
112
  module.exports = {
110
113
  publishEvent,
111
114
  };
@@ -1,7 +1,5 @@
1
1
  "use strict";
2
2
 
3
- const { promisify } = require("util");
4
-
5
3
  const cds = require("@sap/cds");
6
4
 
7
5
  const redis = require("../shared/redis");
@@ -17,7 +15,11 @@ const COMPONENT_NAME = "/eventQueue/redisPub";
17
15
  const TRIES_FOR_PUBLISH_PERIODIC_EVENT = 10;
18
16
  const SLEEP_TIME_FOR_PUBLISH_PERIODIC_EVENT = 30 * 1000;
19
17
 
20
- const wait = promisify(setTimeout);
18
+ const wait = async (ms) => {
19
+ return new Promise((resolve) => {
20
+ setTimeout(resolve, ms).unref();
21
+ });
22
+ };
21
23
 
22
24
  /**
23
25
  * Broadcasts events to the event queue, either locally or through Redis.
@@ -130,7 +130,7 @@ const _getAllTenantBase = async () => {
130
130
  if (cds.services["cds.xt.SaasProvisioningService"] || cds.services["saas-registry"]) {
131
131
  break;
132
132
  }
133
- await new Promise((resolve) => setTimeout(resolve, 1000));
133
+ await new Promise((resolve) => setTimeout(resolve, 1000).unref());
134
134
  }
135
135
 
136
136
  const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
@@ -13,7 +13,7 @@ const COMPONENT_NAME = "/eventQueue/admin";
13
13
  module.exports = class AdminService extends cds.ApplicationService {
14
14
  async init() {
15
15
  const { Event: EventService, Lock: LockService } = this.entities;
16
- const { Event: EventDb } = cds.db.entities("sap.eventqueue");
16
+ const { Event: EventDb } = cds.entities("sap.eventqueue");
17
17
  const { publishEvent } = this.actions;
18
18
 
19
19
  this.before("*", (req) => {