@friggframework/core 2.0.0--canary.657.8f335eb.0 → 2.0.0--canary.657.b14221f.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/CLAUDE.md CHANGED
@@ -570,9 +570,12 @@ back so it runs at `retryAt`:
570
570
  would be lost with the acknowledged message.
571
571
  - With a `processId` in the message, the worker writes
572
572
  `Process.context.rateLimit = { status: 'WAITING' | 'EXHAUSTED', mechanism,
573
- retryAt, reason, module, deferrals, updatedAt }`, and `null` after the message
574
- ran. `EXHAUSTED` means a cap ended the deferrals, or a visibility change came
575
- on the last delivery.
573
+ retryAt, reason, module, deferrals, updatedAt }`. `EXHAUSTED` means a cap ended
574
+ the deferrals, or a visibility change came on the last delivery. The value is
575
+ one per process, so it holds the latest known wait: a new wait replaces it only
576
+ when its `retryAt` is later, a `WAITING` value never hides an `EXHAUSTED` one,
577
+ and after a deferred or redelivered message ran the worker sets it to `null`
578
+ only when it is `WAITING` and its `retryAt` is past or within 5 s.
576
579
  - Delivery stays at least once. The new message is sent before the old one is
577
580
  acknowledged, so a crash between the two can run the work twice.
578
581
  - Handlers check `delivery.isLastAttempt` first, then rethrow. The delay and
package/core/Worker.js CHANGED
@@ -182,60 +182,101 @@ class Worker {
182
182
  return { outcome: 'failed' };
183
183
  }
184
184
 
185
- const next = withDeferral(body, deferral);
186
-
187
- if (waitMs <= MAX_DELAY_SECONDS * 1000) {
188
- const delaySeconds = Math.ceil(waitMs / 1000);
189
- try {
190
- const queueUrl = await this._queueUrl(queueName);
191
- await this.send({ ...next, QueueUrl: queueUrl }, delaySeconds);
192
- } catch (cause) {
193
- return { outcome: 'skipped', skipped: 'send_failed', cause };
194
- }
195
- await this._recordWait(body, error, state('WAITING', 'delay'));
196
- log.warn('Record deferred', {
197
- eventName: 'frigg.worker.record_deferred',
198
- mechanism: 'delay',
199
- delaySeconds,
200
- ...details,
201
- error,
202
- });
203
- return { outcome: 'acked' };
185
+ const tier = {
186
+ record,
187
+ queueName,
188
+ body,
189
+ next: withDeferral(body, deferral),
190
+ error,
191
+ delivery,
192
+ retryAt,
193
+ waitMs,
194
+ details,
195
+ state,
196
+ };
197
+ if (waitMs <= MAX_DELAY_SECONDS * 1000)
198
+ return this._deferWithDelay(tier);
199
+ const scheduled = await this._deferWithSchedule(tier);
200
+ if (scheduled?.outcome) return scheduled;
201
+ return this._deferWithVisibility({
202
+ ...tier,
203
+ scheduleError: scheduled?.scheduleError,
204
+ });
205
+ }
206
+
207
+ async _deferWithDelay({
208
+ queueName,
209
+ body,
210
+ next,
211
+ error,
212
+ waitMs,
213
+ details,
214
+ state,
215
+ }) {
216
+ const delaySeconds = Math.ceil(waitMs / 1000);
217
+ try {
218
+ const queueUrl = await this._queueUrl(queueName);
219
+ await this.send({ ...next, QueueUrl: queueUrl }, delaySeconds);
220
+ } catch (cause) {
221
+ return { outcome: 'skipped', skipped: 'send_failed', cause };
204
222
  }
223
+ await this._recordWait(body, error, state('WAITING', 'delay'));
224
+ log.warn('Record deferred', {
225
+ eventName: 'frigg.worker.record_deferred',
226
+ mechanism: 'delay',
227
+ delaySeconds,
228
+ ...details,
229
+ error,
230
+ });
231
+ return { outcome: 'acked' };
232
+ }
205
233
 
206
- let scheduleError;
234
+ /** `acked`, `{ scheduleError }` when the schedule failed, null with no scheduler. */
235
+ async _deferWithSchedule({
236
+ record,
237
+ body,
238
+ next,
239
+ error,
240
+ retryAt,
241
+ details,
242
+ state,
243
+ }) {
207
244
  const scheduler = this.getSchedulerService();
208
- if (scheduler) {
209
- const scheduleName = `frigg-defer-${
210
- record.messageId || randomUUID()
211
- }`;
212
- try {
213
- await scheduler.scheduleOneTime({
214
- scheduleName,
215
- scheduleAt: retryAt,
216
- queueResourceId: record.eventSourceARN,
217
- payload: next,
218
- });
219
- } catch (cause) {
220
- if (cause?.name !== 'ConflictException') scheduleError = cause;
221
- }
222
- if (!scheduleError) {
223
- await this._recordWait(
224
- body,
225
- error,
226
- state('WAITING', 'schedule')
227
- );
228
- log.warn('Record deferred', {
229
- eventName: 'frigg.worker.record_deferred',
230
- mechanism: 'schedule',
231
- scheduleName,
232
- ...details,
233
- error,
234
- });
235
- return { outcome: 'acked' };
236
- }
245
+ if (!scheduler) return null;
246
+ const scheduleName = `frigg-defer-${record.messageId || randomUUID()}`;
247
+ try {
248
+ await scheduler.scheduleOneTime({
249
+ scheduleName,
250
+ scheduleAt: retryAt,
251
+ queueResourceId: record.eventSourceARN,
252
+ payload: next,
253
+ });
254
+ } catch (cause) {
255
+ if (cause?.name !== 'ConflictException')
256
+ return { scheduleError: cause };
237
257
  }
258
+ await this._recordWait(body, error, state('WAITING', 'schedule'));
259
+ log.warn('Record deferred', {
260
+ eventName: 'frigg.worker.record_deferred',
261
+ mechanism: 'schedule',
262
+ scheduleName,
263
+ ...details,
264
+ error,
265
+ });
266
+ return { outcome: 'acked' };
267
+ }
238
268
 
269
+ async _deferWithVisibility({
270
+ record,
271
+ queueName,
272
+ body,
273
+ error,
274
+ delivery,
275
+ waitMs,
276
+ details,
277
+ state,
278
+ scheduleError,
279
+ }) {
239
280
  const visibilityTimeout = Math.min(
240
281
  Math.ceil(waitMs / 1000),
241
282
  MAX_VISIBILITY_TIMEOUT_SECONDS
@@ -301,13 +342,17 @@ class Worker {
301
342
  * @param {Error} error The RateLimitError.
302
343
  * @param {{status: 'WAITING'|'EXHAUSTED', mechanism: string, deferrals: number, retryAt: Date}} state
303
344
  */
304
- async recordRateLimitWait() {}
345
+ async recordRateLimitWait() {
346
+ // A subclass keeps the run state; nothing to do by default.
347
+ }
305
348
 
306
349
  /**
307
350
  * Hook: a message that was put back for a rate limit ran without error.
308
351
  * @param {Object} body The parsed message body.
309
352
  */
310
- async clearRateLimitWait() {}
353
+ async clearRateLimitWait() {
354
+ // A subclass keeps the run state; nothing to do by default.
355
+ }
311
356
 
312
357
  _queueUrl(queueName) {
313
358
  this._queueUrls ??= new Map();
@@ -330,8 +375,8 @@ class Worker {
330
375
  }
331
376
 
332
377
  async _clearDeferredState(body, delivery) {
333
- if (readDeferral(body).deferrals === 0 && !(delivery.receiveCount > 1))
334
- return;
378
+ const redelivered = delivery.receiveCount > 1;
379
+ if (readDeferral(body).deferrals === 0 && !redelivered) return;
335
380
  await this._runStateHook('clear', () => this.clearRateLimitWait(body));
336
381
  }
337
382
 
@@ -22,6 +22,15 @@ const {
22
22
  } = require('../integrations/utils/map-integration-dto');
23
23
  const { processNotFound } = require('../integrations/use-cases/process-errors');
24
24
 
25
+ const CLEAR_RATE_LIMIT_TOLERANCE_MS = 5_000;
26
+
27
+ const keepsCurrentRateLimit = (current, next) => {
28
+ if (!current) return false;
29
+ if (next.status === 'EXHAUSTED') return false;
30
+ if (current.status === 'EXHAUSTED') return true;
31
+ return Date.parse(current.retryAt) > next.retryAt.getTime();
32
+ };
33
+
25
34
  const loadRouterFromObject = (IntegrationClass, routerObject) => {
26
35
  const router = Router();
27
36
  const { path, method, event } = routerObject;
@@ -168,7 +177,11 @@ const createQueueWorker = (integrationClass) => {
168
177
  async recordRateLimitWait(body, error, state) {
169
178
  const processId = body.data?.processId;
170
179
  if (!processId) return;
171
- await createProcessRepository().applyProcessUpdate(processId, {
180
+ const processRepository = createProcessRepository();
181
+ const process = await processRepository.findById(processId);
182
+ if (keepsCurrentRateLimit(process?.context?.rateLimit, state))
183
+ return;
184
+ await processRepository.applyProcessUpdate(processId, {
172
185
  set: {
173
186
  'context.rateLimit': {
174
187
  status: state.status,
@@ -186,7 +199,13 @@ const createQueueWorker = (integrationClass) => {
186
199
  async clearRateLimitWait(body) {
187
200
  const processId = body?.data?.processId;
188
201
  if (!processId) return;
189
- await createProcessRepository().applyProcessUpdate(processId, {
202
+ const processRepository = createProcessRepository();
203
+ const process = await processRepository.findById(processId);
204
+ const current = process?.context?.rateLimit;
205
+ if (current?.status !== 'WAITING') return;
206
+ const remainingMs = Date.parse(current.retryAt) - Date.now();
207
+ if (remainingMs > CLEAR_RATE_LIMIT_TOLERANCE_MS) return;
208
+ await processRepository.applyProcessUpdate(processId, {
190
209
  set: { 'context.rateLimit': null },
191
210
  });
192
211
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.657.8f335eb.0",
4
+ "version": "2.0.0--canary.657.b14221f.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -48,9 +48,9 @@
48
48
  }
49
49
  },
50
50
  "devDependencies": {
51
- "@friggframework/eslint-config": "2.0.0--canary.657.8f335eb.0",
52
- "@friggframework/prettier-config": "2.0.0--canary.657.8f335eb.0",
53
- "@friggframework/test": "2.0.0--canary.657.8f335eb.0",
51
+ "@friggframework/eslint-config": "2.0.0--canary.657.b14221f.0",
52
+ "@friggframework/prettier-config": "2.0.0--canary.657.b14221f.0",
53
+ "@friggframework/test": "2.0.0--canary.657.b14221f.0",
54
54
  "@prisma/client": "^6.19.3",
55
55
  "@types/lodash": "4.17.15",
56
56
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -90,5 +90,5 @@
90
90
  "publishConfig": {
91
91
  "access": "public"
92
92
  },
93
- "gitHead": "8f335eb16f678038ad9448d9603c734ad663b3dc"
93
+ "gitHead": "b14221fca51737284a61407b42e003637930465e"
94
94
  }