@friggframework/core 2.0.0--canary.657.07aea31.0 → 2.0.0--canary.657.e372ef6.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
@@ -521,8 +521,9 @@ minute redelivery. It puts the message back so it runs at `retryAt`:
521
521
  | Over 900 s, no scheduler | Extends the message's visibility timeout to `retryAt` (12 h less 20 minutes at most). This uses one receive | reported failed |
522
522
 
523
523
  - The new message starts at receive count 1. A deferred body carries
524
- `_frigg: { deferrals, firstDeferredAt }`; `_frigg` is reserved and the handler
525
- never sees it.
524
+ `_frigg: { deferrals, firstDeferredAt, deferralId }`; `_frigg` is reserved and
525
+ the handler never sees it. `deferralId` is the SQS message id of the first
526
+ message that was put back, and every later deferral of that work keeps it.
526
527
  - The visibility tier sets a timeout of at most 12 h less 20 minutes
527
528
  (42,000 s). SQS counts its 12 h maximum from the receive, and an extension
528
529
  does not reset it. The margin covers the longest Lambda run (900 s) plus the
@@ -537,12 +538,16 @@ minute redelivery. It puts the message back so it runs at `retryAt`:
537
538
  would be lost with the acknowledged message.
538
539
  - With a `processId` in the message, the worker writes
539
540
  `Process.context.rateLimit = { status: 'WAITING' | 'EXHAUSTED', mechanism,
540
- retryAt, reason, module, deferrals, updatedAt }`. `EXHAUSTED` means a cap ended
541
- the deferrals, or a visibility change came on the last delivery. The value is
542
- one per process, so it holds the latest known wait: a new wait replaces it only
543
- when its `retryAt` is later, a `WAITING` value never hides an `EXHAUSTED` one,
544
- and after a deferred or redelivered message ran the worker sets it to `null`
545
- only when it is `WAITING` and its `retryAt` is past or within 5 s.
541
+ retryAt, reason, module, deferrals, deferralId, updatedAt }`. `EXHAUSTED` means
542
+ a cap ended the deferrals, or a visibility change came on the last delivery.
543
+ The value is one per process, so it holds the latest known wait: a new wait
544
+ replaces it only when its `retryAt` is later, and a `WAITING` value never hides
545
+ an `EXHAUSTED` one. After a deferred or redelivered message ran, the worker
546
+ sets a `WAITING` value to `null` at once when that message set it (same
547
+ `deferralId`, or the message's own id for a visibility change), and for any
548
+ other message only when its `retryAt` is past or within 5 s. So a job that
549
+ resumes before its `retryAt` clears its own wait, and cannot clear the wait of
550
+ another message.
546
551
  - Delivery stays at least once. The new message is sent before the old one is
547
552
  acknowledged, so a crash between the two can run the work twice.
548
553
  - Handlers check `delivery.isLastAttempt` first, then rethrow. The delay and
package/core/CLAUDE.md CHANGED
@@ -98,8 +98,10 @@ the last attempt they write one ERROR, `record_lost_rate_limited`, with the
98
98
  details, and no WARN.
99
99
 
100
100
  Subclasses override two no-op hooks to keep run state: `recordRateLimitWait(body,
101
- error, state)` runs after the message was put back, and `clearRateLimitWait(body)`
102
- runs after a deferred or redelivered message succeeded. A failing hook logs
101
+ error, state)` runs after the message was put back, and `clearRateLimitWait(body,
102
+ { deferralId })` runs after a deferred or redelivered message succeeded.
103
+ `state.deferralId` and `deferralId` name the message that was first put back
104
+ (`_frigg.deferralId`, or the message's own id). A failing hook logs
103
105
  `frigg.worker.rate_limit_state_failed` and never changes the outcome. The queue
104
106
  worker of an integration (`createQueueWorker`) writes `Process.context.rateLimit`
105
107
  in them. See `packages/core/CLAUDE.md` section 7.
package/core/Worker.js CHANGED
@@ -63,7 +63,7 @@ class Worker {
63
63
  runParams = JSON.parse(record.body);
64
64
  this._validateParams(runParams);
65
65
  await this._run(runParams, context, delivery);
66
- await this._clearDeferredState(runParams, delivery);
66
+ await this._clearDeferredState(record, runParams, delivery);
67
67
  log.debug('Record succeeded', { eventName: 'frigg.worker.record_succeeded' });
68
68
  } catch (error) {
69
69
  let deferralResult;
@@ -146,7 +146,7 @@ class Worker {
146
146
  }
147
147
 
148
148
  const now = Date.now();
149
- const counters = nextDeferral(body, now);
149
+ const counters = nextDeferral(body, now, record.messageId);
150
150
  const deferral = {
151
151
  record,
152
152
  queueName,
@@ -312,15 +312,21 @@ class Worker {
312
312
  status,
313
313
  mechanism,
314
314
  deferrals: counters.deferrals,
315
+ deferralId: counters.deferralId,
315
316
  retryAt,
316
317
  })
317
318
  );
318
319
  }
319
320
 
320
- async _clearDeferredState(body, delivery) {
321
+ async _clearDeferredState(record, body, delivery) {
321
322
  const redelivered = delivery.receiveCount > 1;
322
- if (readDeferral(body).deferrals === 0 && !redelivered) return;
323
- await this._runStateHook('clear', () => this.clearRateLimitWait(body));
323
+ const { deferrals, deferralId } = readDeferral(body);
324
+ if (deferrals === 0 && !redelivered) return;
325
+ await this._runStateHook('clear', () =>
326
+ this.clearRateLimitWait(body, {
327
+ deferralId: deferralId ?? record.messageId,
328
+ })
329
+ );
324
330
  }
325
331
 
326
332
  async _runStateHook(operation, hook) {
@@ -184,13 +184,13 @@ const createQueueWorker = (integrationClass) => {
184
184
  });
185
185
  }
186
186
 
187
- async clearRateLimitWait(body) {
187
+ async clearRateLimitWait(body, { deferralId } = {}) {
188
188
  const processId = body?.data?.processId;
189
189
  if (!processId) return;
190
190
  const clearRateLimitWait = new ClearRateLimitWait({
191
191
  processRepository: createProcessRepository(),
192
192
  });
193
- await clearRateLimitWait.execute(processId);
193
+ await clearRateLimitWait.execute(processId, { deferralId });
194
194
  }
195
195
 
196
196
  async _run(params, context, delivery) {
@@ -1,5 +1,11 @@
1
1
  const CLEAR_RATE_LIMIT_TOLERANCE_MS = 5_000;
2
2
 
3
+ const isOwnWait = (current, deferralId) =>
4
+ Boolean(deferralId) && current.deferralId === deferralId;
5
+
6
+ const isOver = (current) =>
7
+ Date.parse(current.retryAt) - Date.now() <= CLEAR_RATE_LIMIT_TOLERANCE_MS;
8
+
3
9
  class ClearRateLimitWait {
4
10
  constructor({ processRepository }) {
5
11
  if (!processRepository) {
@@ -8,12 +14,11 @@ class ClearRateLimitWait {
8
14
  this.processRepository = processRepository;
9
15
  }
10
16
 
11
- async execute(processId) {
17
+ async execute(processId, { deferralId } = {}) {
12
18
  const process = await this.processRepository.findById(processId);
13
19
  const current = process?.context?.rateLimit;
14
20
  if (current?.status !== 'WAITING') return;
15
- const remainingMs = Date.parse(current.retryAt) - Date.now();
16
- if (remainingMs > CLEAR_RATE_LIMIT_TOLERANCE_MS) return;
21
+ if (!isOwnWait(current, deferralId) && !isOver(current)) return;
17
22
  await this.processRepository.applyProcessUpdate(processId, {
18
23
  set: { 'context.rateLimit': null },
19
24
  });
@@ -25,6 +25,7 @@ class RecordRateLimitWait {
25
25
  reason: wait.reason,
26
26
  module: wait.module,
27
27
  deferrals: wait.deferrals,
28
+ deferralId: wait.deferralId,
28
29
  updatedAt: new Date().toISOString(),
29
30
  },
30
31
  },
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.07aea31.0",
4
+ "version": "2.0.0--canary.657.e372ef6.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.07aea31.0",
52
- "@friggframework/prettier-config": "2.0.0--canary.657.07aea31.0",
53
- "@friggframework/test": "2.0.0--canary.657.07aea31.0",
51
+ "@friggframework/eslint-config": "2.0.0--canary.657.e372ef6.0",
52
+ "@friggframework/prettier-config": "2.0.0--canary.657.e372ef6.0",
53
+ "@friggframework/test": "2.0.0--canary.657.e372ef6.0",
54
54
  "@prisma/client": "^6.19.3",
55
55
  "@types/lodash": "4.17.15",
56
56
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -91,5 +91,5 @@
91
91
  "publishConfig": {
92
92
  "access": "public"
93
93
  },
94
- "gitHead": "07aea3197703a6bc5116bdb6c52308c0ac9b50db"
94
+ "gitHead": "e372ef6b74e5bb76673a99181fed6d54d097dc99"
95
95
  }
@@ -30,14 +30,19 @@ const readDeferral = (body) => {
30
30
  !Number.isNaN(Date.parse(frigg.firstDeferredAt))
31
31
  ? frigg.firstDeferredAt
32
32
  : undefined;
33
- return { deferrals, firstDeferredAt };
33
+ const deferralId =
34
+ typeof frigg?.deferralId === 'string' && frigg.deferralId !== ''
35
+ ? frigg.deferralId
36
+ : undefined;
37
+ return { deferrals, firstDeferredAt, deferralId };
34
38
  };
35
39
 
36
- const nextDeferral = (body, now = Date.now()) => {
40
+ const nextDeferral = (body, now = Date.now(), messageId) => {
37
41
  const current = readDeferral(body);
38
42
  return {
39
43
  deferrals: current.deferrals + 1,
40
44
  firstDeferredAt: current.firstDeferredAt ?? new Date(now).toISOString(),
45
+ deferralId: current.deferralId ?? messageId,
41
46
  };
42
47
  };
43
48
 
@@ -54,13 +54,17 @@ declare module "@friggframework/core" {
54
54
  state: RateLimitWaitState
55
55
  ): Promise<void>;
56
56
 
57
- clearRateLimitWait(body: object): Promise<void>;
57
+ clearRateLimitWait(
58
+ body: object,
59
+ options?: { deferralId?: string }
60
+ ): Promise<void>;
58
61
  }
59
62
 
60
63
  export type RateLimitWaitState = {
61
64
  status: "WAITING" | "EXHAUSTED";
62
65
  mechanism: "delay" | "schedule" | "visibility" | "none";
63
66
  deferrals: number;
67
+ deferralId?: string;
64
68
  retryAt: Date;
65
69
  };
66
70