@cap-js-community/event-queue 1.10.0-beta.4 → 1.10.0-beta.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.10.0-beta.4",
3
+ "version": "1.10.0-beta.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",
@@ -44,7 +44,7 @@
44
44
  "node": ">=18"
45
45
  },
46
46
  "dependencies": {
47
- "@sap/xssec": "^4.2.4",
47
+ "@sap/xssec": "^4.5.0",
48
48
  "cron-parser": "^5.0.0",
49
49
  "redis": "^4.7.0",
50
50
  "verror": "^1.10.1",
@@ -806,10 +806,10 @@ class EventQueueProcessorBase {
806
806
  currentAttempt: exceededEvent.attempts,
807
807
  }
808
808
  );
809
+ await tx.rollback();
809
810
  await executeInNewTransaction(this.__baseContext, "error-hookForExceededEvents", async (tx) =>
810
811
  this.#persistEventQueueStatusForExceeded(tx, [exceededEvent], EventProcessingStatus.Error)
811
812
  );
812
- await tx.rollback();
813
813
  }
814
814
  }
815
815
  );
package/src/config.js CHANGED
@@ -24,6 +24,7 @@ const DEFAULT_INCREASE_PRIORITY = true;
24
24
  const DEFAULT_KEEP_ALIVE_INTERVAL = 60;
25
25
  const DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL = 3.5;
26
26
  const DEFAULT_INHERIT_TRACE_CONTEXT = true;
27
+ const DEFAULT_CHECK_FOR_NEXT_CHUNK = true;
27
28
  const SUFFIX_PERIODIC = "_PERIODIC";
28
29
  const CAP_EVENT_TYPE = "CAP_OUTBOX";
29
30
  const CAP_PARALLEL_DEFAULT = 5;
@@ -335,6 +336,7 @@ class Config {
335
336
  this.#config.events.splice(index, 1);
336
337
  }
337
338
 
339
+ // NOTE: CAP outbox defaults are injected by cds.requires.outbox
338
340
  const eventConfig = this.#sanitizeParamsAdHocEvent({
339
341
  type: CAP_EVENT_TYPE,
340
342
  subType: serviceName,
@@ -507,6 +509,7 @@ class Config {
507
509
  event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
508
510
  event.keepAliveInterval = (event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL) * 1000;
509
511
  event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
512
+ event.checkForNextChunk = event.checkForNextChunk ?? DEFAULT_CHECK_FOR_NEXT_CHUNK;
510
513
  }
511
514
 
512
515
  #sanitizeParamsBase(config, allowList) {
@@ -38,26 +38,18 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
38
38
  this.__genericClusterHandler = clusterRelevant;
39
39
  this.__specificClusterHandler = specificClusterRelevant;
40
40
  await this.#setContextUser(this.context, config.userId);
41
- return super.getQueueEntriesAndSetToInProgress();
41
+ return await super.getQueueEntriesAndSetToInProgress();
42
42
  }
43
43
 
44
- // NOTE: issue here: if events are not sorted before it might not be unique here:
45
- // - we have service events
46
- // - we have action specific events
47
- // 1. I need to collect all action names
48
- // 2. I need to check for which actions a handler exits
49
- // 3. For actions with specific existing handler --> call specific action
50
- // 4. For all others call generic handler
51
- // NOTE: OVERALL idea is that the handler returns the cluster map and MUST not call any baseImpl functions!
52
- // --> structure is a map of { key: { queueEntries: [], payload: {} }
44
+ // document structure is a map of { key: { queueEntries: [], payload: {} }
53
45
  // TODO: document that clusterQueueEntries is now async!!!
54
- // TODO: validate that return structure is as expected
55
46
  async clusterQueueEntries(queueEntriesWithPayloadMap) {
56
47
  if (!this.__genericClusterRelevantAndAvailable && !this.__specificClusterRelevantAndAvailable) {
57
48
  return super.clusterQueueEntries(queueEntriesWithPayloadMap);
58
49
  }
59
50
  const { genericClusterEvents, specificClusterEvents } = this.#clusterByAction(queueEntriesWithPayloadMap);
60
51
 
52
+ const clusterMap = {};
61
53
  if (Object.keys(genericClusterEvents).length) {
62
54
  if (!this.__genericClusterRelevantAndAvailable) {
63
55
  for (const actionName in genericClusterEvents) {
@@ -67,18 +59,30 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
67
59
  for (const actionName in genericClusterEvents) {
68
60
  const msg = new cds.Request({
69
61
  event: `clusterQueueEntries`,
62
+ user: this.context.user,
70
63
  eventQueue: {
71
64
  processor: this,
72
65
  clusterByPayloadProperty: (propertyName, cb) =>
73
- this.clusterByPayloadProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
66
+ this.#clusterByPayloadProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
74
67
  clusterByEventProperty: (propertyName, cb) =>
75
- this.clusterByEventProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
68
+ this.#clusterByEventProperty(actionName, genericClusterEvents[actionName], propertyName, cb),
76
69
  clusterByDataProperty: (propertyName, cb) =>
77
- this.clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
70
+ this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
78
71
  },
79
72
  });
80
- const handlerCluster = await this.__srvUnboxed.tx(this.context).send(msg);
81
- this.#addToProcessingMap(handlerCluster);
73
+ const clusterResult = await this.__srvUnboxed.tx(this.context).send(msg);
74
+ if (this.#validateCluster(clusterResult)) {
75
+ Object.assign(clusterMap, clusterResult);
76
+ } else {
77
+ this.logger.error(
78
+ "cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
79
+ {
80
+ handler: msg.event,
81
+ clusterResult: JSON.stringify(clusterResult),
82
+ }
83
+ );
84
+ return super.clusterQueueEntries(queueEntriesWithPayloadMap);
85
+ }
82
86
  }
83
87
  }
84
88
  }
@@ -86,19 +90,59 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
86
90
  for (const actionName in specificClusterEvents) {
87
91
  const msg = new cds.Request({
88
92
  event: `clusterQueueEntries.${actionName}`,
93
+ user: this.context.user,
89
94
  eventQueue: {
90
95
  processor: this,
91
96
  clusterByPayloadProperty: (propertyName, cb) =>
92
- this.clusterByPayloadProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
97
+ this.#clusterByPayloadProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
93
98
  clusterByEventProperty: (propertyName, cb) =>
94
- this.clusterByEventProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
99
+ this.#clusterByEventProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
95
100
  clusterByDataProperty: (propertyName, cb) =>
96
- this.clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
101
+ this.#clusterByDataProperty(actionName, specificClusterEvents[actionName], propertyName, cb),
97
102
  },
98
103
  });
99
- const handlerCluster = await this.__srvUnboxed.tx(this.context).send(msg);
100
- this.#addToProcessingMap(handlerCluster);
104
+ const clusterResult = await this.__srvUnboxed.tx(this.context).send(msg);
105
+ if (this.#validateCluster(clusterResult)) {
106
+ Object.assign(clusterMap, clusterResult);
107
+ } else {
108
+ this.logger.error(
109
+ "cluster result of handler is not valid. Check the documentation for the expected structure. Continuing without clustering!",
110
+ {
111
+ handler: msg.event,
112
+ clusterResult: JSON.stringify(clusterResult),
113
+ }
114
+ );
115
+ return super.clusterQueueEntries(queueEntriesWithPayloadMap);
116
+ }
117
+ }
118
+ this.#addToProcessingMap(clusterMap);
119
+ }
120
+
121
+ #validateCluster(obj) {
122
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
123
+ return false;
124
+ }
125
+
126
+ for (const key of Object.keys(obj)) {
127
+ const clusterEntry = obj[key];
128
+ if (typeof clusterEntry !== "object" || clusterEntry === null || Array.isArray(obj)) {
129
+ return false;
130
+ }
131
+
132
+ if (!Array.isArray(clusterEntry.queueEntries)) {
133
+ return false;
134
+ }
135
+
136
+ if (
137
+ typeof clusterEntry.payload !== "object" ||
138
+ clusterEntry.payload === null ||
139
+ Array.isArray(clusterEntry.payload)
140
+ ) {
141
+ return false;
142
+ }
101
143
  }
144
+
145
+ return true;
102
146
  }
103
147
 
104
148
  clusterBase(queueEntriesWithPayloadMap, propertyName, refCb, cb) {
@@ -139,7 +183,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
139
183
  return result[key];
140
184
  }
141
185
 
142
- clusterByPayloadProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
186
+ #clusterByPayloadProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
143
187
  return this.clusterBase(
144
188
  queueEntriesWithPayloadMap,
145
189
  propertyName,
@@ -148,7 +192,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
148
192
  );
149
193
  }
150
194
 
151
- clusterByEventProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
195
+ #clusterByEventProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
152
196
  return this.clusterBase(
153
197
  queueEntriesWithPayloadMap,
154
198
  propertyName,
@@ -157,7 +201,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
157
201
  );
158
202
  }
159
203
 
160
- clusterByDataProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
204
+ #clusterByDataProperty(actionName, queueEntriesWithPayloadMap, propertyName, cb) {
161
205
  return this.clusterBase(
162
206
  queueEntriesWithPayloadMap,
163
207
  propertyName,
@@ -230,18 +274,26 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
230
274
  }
231
275
  }
232
276
 
233
- // simple here as per entry
234
277
  async hookForExceededEvents(exceededEvent) {
235
- return await super.hookForExceededEvents(exceededEvent);
278
+ const { event } = exceededEvent.payload;
279
+ const handlerName = this.#checkHandlerExists("hookForExceededEvents", event);
280
+ if (!handlerName) {
281
+ return await super.hookForExceededEvents(exceededEvent);
282
+ }
283
+
284
+ const { msg, userId } = this.#buildDispatchData(this.context, exceededEvent.payload, {
285
+ queueEntries: [exceededEvent],
286
+ });
287
+ await this.#setContextUser(this.context, userId, msg);
288
+ msg.event = handlerName;
289
+ await this.__srvUnboxed.tx(this.context).send(msg);
236
290
  }
237
291
 
292
+ // NOTE: Currently not exposed to CAP service; we wait for a valid use case
238
293
  async beforeProcessingEvents() {
239
294
  return await super.beforeProcessingEvents();
240
295
  }
241
296
 
242
- // maybe async getter on req.data // only for periodic events
243
- // getLastSuccessfulRunTimestamp
244
-
245
297
  #checkHandlerExists(eventQueueFn, event) {
246
298
  const specificHandler = this.__onHandlers[[eventQueueFn, event].join(".")];
247
299
  if (specificHandler) {
@@ -63,8 +63,8 @@ const publishEvent = async (
63
63
  event.context = JSON.stringify({ traceContext: openTelemetry.getCurrentTraceContext() });
64
64
  }
65
65
 
66
- if (eventConfig.timeBucket) {
67
- event.startAfter ??= CronExpressionParser.parse(eventConfig.timeBucket).next().toISOString();
66
+ if (eventConfig.timeBucket && event.startAfter !== undefined) {
67
+ event.startAfter = CronExpressionParser.parse(eventConfig.timeBucket).next().toISOString();
68
68
  }
69
69
  }
70
70
  if (config.insertEventsBeforeCommit && !skipInsertEventsBeforeCommit) {