@friggframework/core 2.0.0--canary.652.60de836.0 → 2.0.0--canary.653.af34bd7.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
@@ -414,6 +414,43 @@ const decrypted = await cryptor.decrypt(encrypted);
414
414
  - `integration-event-dispatcher.js` - Routes events to integration handlers
415
415
  - Supports lifecycle events and user actions
416
416
 
417
+ **Queue handler delivery**: a handler dispatched from the integration queue
418
+ (any `this.events` entry reached through SQS, including `ON_WEBHOOK` and
419
+ scheduled jobs) receives `{ data, context, delivery }`. HTTP-dispatched
420
+ handlers (`{ req, res, next }`) and `this.on` events do not.
421
+
422
+ | Field | Type | Value |
423
+ |---|---|---|
424
+ | `delivery.receiveCount` | `number \| undefined` | SQS `ApproximateReceiveCount` of this delivery |
425
+ | `delivery.maxReceiveCount` | `number \| undefined` | Receives allowed before SQS moves the message to the DLQ (`FRIGG_QUEUE_MAX_RECEIVE_COUNT`) |
426
+ | `delivery.isLastAttempt` | `boolean` | `true` only when both counts are known and `receiveCount >= maxReceiveCount` |
427
+
428
+ `isLastAttempt` is `false` when either count is unknown: a local or non-SQS
429
+ invocation, or a queue whose redrive policy the stack does not own
430
+ (`ownership.queue: 'external'`). The value is information only: core still
431
+ rethrows retryable errors and discards halt errors (4xx except 408/429).
432
+
433
+ Use it to end a run or count lost work on the final try: when a retryable
434
+ error (429, 5xx, network) is about to be rethrown and `isLastAttempt` is
435
+ `true`, the message goes to the DLQ next, so mark the run failed or count the
436
+ message's records as failed before rethrowing.
437
+
438
+ ```javascript
439
+ async processBatch({ data, delivery }) {
440
+ try {
441
+ await this.syncPage(data);
442
+ } catch (error) {
443
+ if (delivery?.isLastAttempt) await this.failRun(data.processId, error);
444
+ throw error;
445
+ }
446
+ }
447
+ ```
448
+
449
+ The single source of the max receive count is
450
+ `INTEGRATION_QUEUE_MAX_RECEIVE_COUNT` in `queues/queue-delivery.js`. The
451
+ devtools integration builder uses it for the queue's `RedrivePolicy` and sets
452
+ it as `FRIGG_QUEUE_MAX_RECEIVE_COUNT` on the queue worker function.
453
+
417
454
  ### 8. Error Handling (`/errors`)
418
455
 
419
456
  **Purpose**: Standardized error types with proper HTTP semantics.
@@ -662,6 +699,7 @@ Use test doubles from `@friggframework/test` package for consistent mocking.
662
699
  - `SECRET_ARN` - AWS Secrets Manager ARN for auto-injection
663
700
  - `DEBUG` - Debug logging pattern
664
701
  - `LOG_LEVEL` - Logging level (debug, info, warn, error)
702
+ - `FRIGG_QUEUE_MAX_RECEIVE_COUNT` - Set by devtools on integration queue workers whose queue the stack owns; feeds `delivery.maxReceiveCount`
665
703
 
666
704
  ## Version Information
667
705
 
package/core/CLAUDE.md CHANGED
@@ -52,9 +52,10 @@ const handler = createHandler({
52
52
  **Usage Pattern**:
53
53
  ```javascript
54
54
  class MyWorker extends Worker {
55
- async _run(params, context = {}) {
55
+ async _run(params, context = {}, delivery) {
56
56
  // Your job processing logic here
57
57
  // params are already JSON.parsed from SQS message body
58
+ // delivery is { receiveCount, maxReceiveCount, isLastAttempt } (see ../CLAUDE.md "Queue handler delivery")
58
59
  }
59
60
 
60
61
  _validateParams(params) {
package/core/Worker.js CHANGED
@@ -2,6 +2,7 @@ const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/
2
2
  const _ = require('lodash');
3
3
  const { RequiredPropertyError } = require('../errors');
4
4
  const { get } = require('../assertions');
5
+ const { readQueueDelivery } = require('../queues/queue-delivery');
5
6
 
6
7
  const sqs = new SQSClient({ region: process.env.AWS_REGION });
7
8
 
@@ -42,7 +43,7 @@ class Worker {
42
43
  try {
43
44
  const runParams = JSON.parse(record.body);
44
45
  this._validateParams(runParams);
45
- await this._run(runParams, context);
46
+ await this._run(runParams, context, readQueueDelivery(record));
46
47
  console.log(`[Worker] record success`, {
47
48
  messageId: record.messageId,
48
49
  event: runParams?.event,
@@ -164,7 +164,7 @@ const createQueueWorker = (integrationClass) => {
164
164
  const integrationName = integrationClass.Definition.name;
165
165
 
166
166
  class QueueWorker extends Worker {
167
- async _run(params, context) {
167
+ async _run(params, context, delivery) {
168
168
  const logCtx = {
169
169
  integration: integrationName,
170
170
  event: params.event,
@@ -254,6 +254,7 @@ const createQueueWorker = (integrationClass) => {
254
254
  event: params.event,
255
255
  data: params.data,
256
256
  context: context,
257
+ delivery,
257
258
  });
258
259
  console.log(
259
260
  `[QueueWorker] ${params.event} dispatched ok`,
@@ -18,9 +18,9 @@ class IntegrationEventDispatcher {
18
18
  );
19
19
  }
20
20
 
21
- async dispatchJob({ event, data, context }) {
21
+ async dispatchJob({ event, data, context, delivery }) {
22
22
  return this._dispatch(event, (instance, handler) =>
23
- handler.call(instance, { data, context })
23
+ handler.call(instance, { data, context, delivery })
24
24
  );
25
25
  }
26
26
 
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.652.60de836.0",
4
+ "version": "2.0.0--canary.653.af34bd7.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.652.60de836.0",
52
- "@friggframework/prettier-config": "2.0.0--canary.652.60de836.0",
53
- "@friggframework/test": "2.0.0--canary.652.60de836.0",
51
+ "@friggframework/eslint-config": "2.0.0--canary.653.af34bd7.0",
52
+ "@friggframework/prettier-config": "2.0.0--canary.653.af34bd7.0",
53
+ "@friggframework/test": "2.0.0--canary.653.af34bd7.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": "60de83654af65d71fc331c7dbfee727c4f2534ff"
93
+ "gitHead": "af34bd7c677948dcd9f4a7a8dc0a59b038c35c48"
94
94
  }
@@ -0,0 +1,41 @@
1
+ const INTEGRATION_QUEUE_MAX_RECEIVE_COUNT = 3;
2
+ const QUEUE_MAX_RECEIVE_COUNT_ENV = 'FRIGG_QUEUE_MAX_RECEIVE_COUNT';
3
+
4
+ const toPositiveInteger = (value) => {
5
+ const number = Number(value);
6
+ return Number.isInteger(number) && number > 0 ? number : undefined;
7
+ };
8
+
9
+ /**
10
+ * @typedef {Object} QueueDelivery
11
+ * @property {number|undefined} receiveCount SQS ApproximateReceiveCount of this delivery.
12
+ * @property {number|undefined} maxReceiveCount Receives allowed before the queue dead-letters the message.
13
+ * @property {boolean} isLastAttempt True only when both counts are known and receiveCount >= maxReceiveCount.
14
+ */
15
+
16
+ /**
17
+ * @param {Object} record SQS event record
18
+ * @param {Object} [env=process.env]
19
+ * @returns {QueueDelivery}
20
+ */
21
+ const readQueueDelivery = (record, env = process.env) => {
22
+ const receiveCount = toPositiveInteger(
23
+ record?.attributes?.ApproximateReceiveCount
24
+ );
25
+ const maxReceiveCount = toPositiveInteger(env[QUEUE_MAX_RECEIVE_COUNT_ENV]);
26
+
27
+ return {
28
+ receiveCount,
29
+ maxReceiveCount,
30
+ isLastAttempt:
31
+ receiveCount !== undefined &&
32
+ maxReceiveCount !== undefined &&
33
+ receiveCount >= maxReceiveCount,
34
+ };
35
+ };
36
+
37
+ module.exports = {
38
+ INTEGRATION_QUEUE_MAX_RECEIVE_COUNT,
39
+ QUEUE_MAX_RECEIVE_COUNT_ENV,
40
+ readQueueDelivery,
41
+ };