@cap-js-community/event-queue 2.1.0-beta.0 → 2.1.0-beta.2

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": "2.1.0-beta.0",
3
+ "version": "2.1.0-beta.2",
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,13 +36,12 @@
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
47
  "@cap-js-community/common": "^0.3.4",
@@ -408,7 +408,7 @@ class EventQueueProcessorBase {
408
408
  UPDATE.entity(this.#config.tableNameEventQueue)
409
409
  .set({
410
410
  status: status,
411
- ...(error && { error: this.#error2String(error) }),
411
+ ...(error && { error: this._error2String(error) }),
412
412
  })
413
413
  .where({
414
414
  ID: queueEntryIds,
@@ -498,16 +498,22 @@ class EventQueueProcessorBase {
498
498
  }
499
499
 
500
500
  if (data.error) {
501
- data.error = this.#error2String(data.error);
501
+ data.error = this._error2String(data.error);
502
502
  }
503
503
 
504
504
  if (!data.startAfter && [EventProcessingStatus.Error, EventProcessingStatus.Open].includes(data.status)) {
505
505
  data.startAfter = new Date(
506
506
  Date.now() +
507
- (EventProcessingStatus.Error ? this.#eventConfig.retryFailedAfter : this.#eventConfig.retryOpenAfter)
507
+ (data.status === EventProcessingStatus.Error
508
+ ? this.#eventConfig.retryFailedAfter
509
+ : this.#eventConfig.retryOpenAfter)
508
510
  );
509
511
  }
510
512
 
513
+ if (data.status === EventProcessingStatus.Open && !("attempts" in data)) {
514
+ data.attempts = { "-=": 1 };
515
+ }
516
+
511
517
  if (data.startAfter) {
512
518
  this.#eventSchedulerInstance.scheduleEvent(
513
519
  this.__context.tenant,
@@ -535,7 +541,7 @@ class EventQueueProcessorBase {
535
541
  });
536
542
  }
537
543
 
538
- #error2String(error) {
544
+ _error2String(error) {
539
545
  return JSON.stringify(error, (_, value) => this.#errorReplacer(value));
540
546
  }
541
547
 
package/src/config.js CHANGED
@@ -64,7 +64,7 @@ const ALLOWED_EVENT_OPTIONS_AD_HOC = [
64
64
  "checkForNextChunk",
65
65
  "retryFailedAfter",
66
66
  "propagateHeaders",
67
- "propagatedContextProperties",
67
+ "propagateContextProperties",
68
68
  "retryOpenAfter",
69
69
  "multiInstanceProcessing",
70
70
  "kind",
@@ -518,7 +518,7 @@ class Config {
518
518
  ? Object.fromEntries(new Map(event.appInstances.map((a) => [a, true])))
519
519
  : null;
520
520
  event.propagateHeaders = event.propagateHeaders ?? [];
521
- event.propagatedContextProperties = event.propagatedContextProperties ?? [];
521
+ event.propagateContextProperties = event.propagateContextProperties ?? [];
522
522
  event.retryFailedAfter = event.retryFailedAfter ?? DEFAULT_RETRY_AFTER;
523
523
  event.retryOpenAfter = event.retryOpenAfter ?? DEFAULT_RETRY_AFTER;
524
524
  }
@@ -64,7 +64,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
64
64
  }
65
65
  } else {
66
66
  for (const actionName in genericClusterEvents) {
67
- const reg = new cds.Request({
67
+ const req = new cds.Request({
68
68
  event: EVENT_QUEUE_ACTIONS.CLUSTER,
69
69
  user: this.context.user,
70
70
  eventQueue: {
@@ -77,14 +77,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
77
77
  this.#clusterByDataProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
78
78
  },
79
79
  });
80
- const clusterResult = await this.__srvUnboxed.tx(this.context).send(reg);
80
+ const clusterResult = await this.__srvUnboxed.tx(this.context).send(req);
81
81
  if (this.#validateCluster(clusterResult)) {
82
82
  Object.assign(clusterMap, clusterResult);
83
83
  } else {
84
84
  this.logger.error(
85
85
  "cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
86
86
  {
87
- handler: reg.event,
87
+ handler: req.event,
88
88
  clusterResult: JSON.stringify(clusterResult),
89
89
  }
90
90
  );
@@ -95,7 +95,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
95
95
  }
96
96
 
97
97
  for (const actionName in specificClusterEvents) {
98
- const reg = new cds.Request({
98
+ const req = new cds.Request({
99
99
  event: `${EVENT_QUEUE_ACTIONS.CLUSTER}.${actionName}`,
100
100
  user: this.context.user,
101
101
  eventQueue: {
@@ -108,14 +108,14 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
108
108
  this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
109
109
  },
110
110
  });
111
- const clusterResult = await this.__srvUnboxed.tx(this.context).send(reg);
111
+ const clusterResult = await this.__srvUnboxed.tx(this.context).send(req);
112
112
  if (this.#validateCluster(clusterResult)) {
113
113
  Object.assign(clusterMap, clusterResult);
114
114
  } else {
115
115
  this.logger.error(
116
116
  "cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
117
117
  {
118
- handler: reg.event,
118
+ handler: req.event,
119
119
  clusterResult: JSON.stringify(clusterResult),
120
120
  }
121
121
  );
@@ -267,12 +267,12 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
267
267
  return payload;
268
268
  }
269
269
 
270
- const { reg, userId } = this.#buildDispatchData(this.context, payload, {
270
+ const { req, userId } = this.#buildDispatchData(payload, {
271
271
  queueEntries: [queueEntry],
272
272
  });
273
- reg.event = handlerName;
274
- await this.#setContextUser(this.context, userId, reg);
275
- 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);
276
276
  if (data) {
277
277
  payload.data = data;
278
278
  return payload;
@@ -288,12 +288,12 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
288
288
  return await super.hookForExceededEvents(exceededEvent);
289
289
  }
290
290
 
291
- const { reg, userId } = this.#buildDispatchData(this.context, exceededEvent.payload, {
291
+ const { req, userId } = this.#buildDispatchData(exceededEvent.payload, {
292
292
  queueEntries: [exceededEvent],
293
293
  });
294
- await this.#setContextUser(this.context, userId, reg);
295
- reg.event = handlerName;
296
- 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);
297
297
  }
298
298
 
299
299
  // NOTE: Currently not exposed to CAP service; we wait for a valid use case
@@ -312,6 +312,10 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
312
312
  return genericHandler ?? null;
313
313
  }
314
314
 
315
+ if (event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_SUCCESS)) {
316
+ [event] = event.split("/");
317
+ }
318
+
315
319
  const specificHandler = this.__onHandlers[[event, saga].join("/")];
316
320
  if (specificHandler) {
317
321
  return specificHandler;
@@ -322,47 +326,53 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
322
326
 
323
327
  async processPeriodicEvent(processContext, key, queueEntry) {
324
328
  const { actionName } = config.normalizeSubType(this.eventType, this.eventSubType);
325
- const reg = new cds.Event({ event: actionName, eventQueue: { processor: this, key, queueEntries: [queueEntry] } });
326
- await this.#setContextUser(processContext, config.userId, reg);
327
- 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);
328
332
  }
329
333
 
330
- #buildDispatchData(context, payload, { key, queueEntries } = {}) {
334
+ #buildDispatchData(payload, { key, queueEntries } = {}) {
331
335
  const { useEventQueueUser } = this.eventConfig;
332
336
  const userId = useEventQueueUser ? config.userId : payload.contextUser;
333
- 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);
334
338
  const invocationFn = payload._fromSend ? "send" : "emit";
335
- delete reg._fromSend;
336
- delete reg.contextUser;
337
- reg.eventQueue = { processor: this, key, queueEntries, payload };
338
- 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 };
339
350
  }
340
351
 
341
- async #setContextUser(context, userId, reg) {
352
+ async #setContextUser(context, userId, req) {
342
353
  const authInfo = await common.getAuthContext(context.tenant);
343
354
  context.user = new cds.User.Privileged({
344
355
  id: userId,
345
356
  authInfo,
346
357
  tokenInfo: authInfo?.token,
347
358
  });
348
- if (reg) {
349
- reg.user = context.user;
359
+ if (req) {
360
+ req.user = context.user;
350
361
  }
351
362
  }
352
363
 
353
364
  async processEvent(processContext, key, queueEntries, payload) {
365
+ let statusTuple;
366
+ const { userId, invocationFn, req } = this.#buildDispatchData(payload, { key, queueEntries });
354
367
  try {
355
- const { userId, invocationFn, reg } = this.#buildDispatchData(processContext, payload, { key, queueEntries });
356
- await this.#setContextUser(processContext, userId, reg);
357
- const result = await this.__srvUnboxed.tx(processContext)[invocationFn](reg);
358
- const statusTuple = this.#determineResultStatus(result, queueEntries);
359
- await this.#publishFollowupEvents(processContext, reg, statusTuple);
360
- return statusTuple;
368
+ await this.#setContextUser(processContext, userId, req);
369
+ const result = await this.__srvUnboxed.tx(processContext)[invocationFn](req);
370
+ statusTuple = this.#determineResultStatus(result, queueEntries);
361
371
  } catch (err) {
362
372
  this.logger.error("error processing outboxed service call", err, {
363
373
  serviceName: this.eventSubType,
364
374
  });
365
- return queueEntries.map((queueEntry) => [
375
+ statusTuple = queueEntries.map((queueEntry) => [
366
376
  queueEntry.ID,
367
377
  {
368
378
  status: EventProcessingStatus.Error,
@@ -370,6 +380,9 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
370
380
  },
371
381
  ]);
372
382
  }
383
+
384
+ await this.#publishFollowupEvents(processContext, req, statusTuple);
385
+ return statusTuple;
373
386
  }
374
387
 
375
388
  async #publishFollowupEvents(processContext, req, statusTuple) {
@@ -380,7 +393,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
380
393
  return;
381
394
  }
382
395
 
383
- if (req.event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_SUCCESS) || req.event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_FAILED)) {
396
+ if (req.event.endsWith(EVENT_QUEUE_ACTIONS.SAGA_FAILED)) {
384
397
  return;
385
398
  }
386
399
 
@@ -393,23 +406,32 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
393
406
  }
394
407
 
395
408
  for (const [, result] of statusTuple) {
396
- if (succeeded && result.status === EventProcessingStatus.Done) {
397
- await this.__srv.tx(processContext).send(succeeded, result.nextData ?? req.data);
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);
398
416
  }
399
417
 
400
418
  if (failed && result.status === EventProcessingStatus.Error) {
401
- await this.__srv.tx(processContext).send(failed, result.nextData ?? req.data);
419
+ result.error && (data.error = this._error2String(result.error));
420
+ await this.__srv.tx(processContext).send(failed, data);
402
421
  }
403
422
 
404
423
  delete result.nextData;
405
424
  }
406
425
 
407
426
  if (config.insertEventsBeforeCommit) {
408
- this.nextSagaEvents = tx._eventQueue.events;
427
+ this.nextSagaEvents = tx._eventQueue?.events;
409
428
  } else {
410
- this.nextSagaEvents = tx._eventQueue.events.filter((event) => JSON.parse(event.payload).event === failed);
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 ?? [];
411
434
  }
412
- tx._eventQueue.events = nextEvents ?? [];
413
435
  }
414
436
 
415
437
  #determineResultStatus(result, queueEntries) {
@@ -43,7 +43,7 @@ function outboxed(srv, customOpts) {
43
43
 
44
44
  const outboxOpts = config.getEventConfig(CDS_EVENT_TYPE, subType, srvConfig.namespace);
45
45
  const eventHeaders = getPropagatedHeaders(outboxOpts, req);
46
- const contextProperties = getPropagatedContextProperties(outboxOpts, req);
46
+ const contextProperties = getPropagateContextProperties(outboxOpts, req);
47
47
  if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
48
48
  await _mapToEventAndPublish(req, srvConfig.namespace, subType, eventHeaders, contextProperties);
49
49
  return;
@@ -81,8 +81,8 @@ const getPropagatedHeaders = (config, req) => {
81
81
  return Object.assign(propagateHeaders, req.headers);
82
82
  };
83
83
 
84
- const getPropagatedContextProperties = (config, req) => {
85
- return config.propagatedContextProperties.reduce((properties, name) => {
84
+ const getPropagateContextProperties = (config, req) => {
85
+ return config.propagateContextProperties.reduce((properties, name) => {
86
86
  if (name in req.tx.context) {
87
87
  properties[name] = req.tx.context[name];
88
88
  }