@friggframework/core 2.0.0-next.75 → 2.0.0-next.77

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/core/Worker.js CHANGED
@@ -18,12 +18,25 @@ class Worker {
18
18
 
19
19
  async run(params, context = {}) {
20
20
  const records = get(params, 'Records');
21
+ const batchItemFailures = [];
21
22
 
22
23
  for (const record of records) {
23
- const runParams = JSON.parse(record.body);
24
- this._validateParams(runParams);
25
- await this._run(runParams, context);
24
+ try {
25
+ const runParams = JSON.parse(record.body);
26
+ this._validateParams(runParams);
27
+ await this._run(runParams, context);
28
+ } catch (error) {
29
+ if (error.isHaltError) {
30
+ // HaltError means "discard this message, don't retry".
31
+ // Treat as success so SQS deletes it from the queue.
32
+ continue;
33
+ }
34
+ console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
35
+ batchItemFailures.push({ itemIdentifier: record.messageId });
36
+ }
26
37
  }
38
+
39
+ return { batchItemFailures };
27
40
  }
28
41
 
29
42
  async _run(params, context = {}) {
@@ -154,6 +154,12 @@ const createQueueWorker = (integrationClass) => {
154
154
  params.data.processId,
155
155
  integrationClass
156
156
  );
157
+ if (integrationInstance?.status === 'DISABLED') {
158
+ console.warn(
159
+ `[${integrationClass.Definition.name}] Integration for process ${params.data.processId} is DISABLED. Discarding ${params.event} message.`
160
+ );
161
+ return;
162
+ }
157
163
  } else if (params.data?.integrationId) {
158
164
  integrationInstance = await loadIntegrationForWebhook(
159
165
  params.data.integrationId
@@ -164,6 +170,12 @@ const createQueueWorker = (integrationClass) => {
164
170
  );
165
171
  return;
166
172
  }
173
+ if (integrationInstance.status === 'DISABLED') {
174
+ console.warn(
175
+ `[${integrationClass.Definition.name}] Integration ${params.data.integrationId} is DISABLED. Discarding ${params.event} message.`
176
+ );
177
+ return;
178
+ }
167
179
  } else {
168
180
  // Instantiates a DRY integration class without database records.
169
181
  // There will be cases where we need to use helpers that the api modules can export.
@@ -186,6 +198,19 @@ const createQueueWorker = (integrationClass) => {
186
198
  `Error in ${params.event} for ${integrationClass.Definition.name}:`,
187
199
  error
188
200
  );
201
+
202
+ // 4xx HTTP errors are permanent — the requester already
203
+ // attempted token refresh (401) and backoff (429/5xx).
204
+ // By the time a 4xx reaches here, retrying won't help.
205
+ // 408 (timeout) and 429 (rate limit) are excluded — both are transient.
206
+ const status = error.statusCode;
207
+ if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) {
208
+ error.isHaltError = true;
209
+ console.warn(
210
+ `[${integrationClass.Definition.name}] Permanent ${status} error for ${params.event} — message will be discarded (no retry)`
211
+ );
212
+ }
213
+
189
214
  throw error;
190
215
  }
191
216
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * DLQ Processor — logs failed messages from the InternalErrorQueue
3
+ * with structured context for monitoring and debugging.
4
+ *
5
+ * This handler MUST NOT throw. If it throws, the message goes back to
6
+ * the DLQ and creates an infinite loop. All errors are caught and logged.
7
+ */
8
+
9
+ function extractQueueName(eventSourceARN) {
10
+ if (!eventSourceARN) return 'UNKNOWN';
11
+ const parts = eventSourceARN.split(':');
12
+ return parts[parts.length - 1] || 'UNKNOWN';
13
+ }
14
+
15
+ function parseMessageBody(body) {
16
+ try {
17
+ const parsed = JSON.parse(body);
18
+ return {
19
+ event: parsed.event || 'UNKNOWN',
20
+ integrationId: parsed.data?.integrationId || null,
21
+ processId: parsed.data?.processId || null,
22
+ data: parsed.data,
23
+ };
24
+ } catch (error) {
25
+ console.warn('[DLQ] Failed to parse message body', { error: error.message, body });
26
+ return {
27
+ event: 'UNKNOWN',
28
+ integrationId: null,
29
+ processId: null,
30
+ rawBody: body,
31
+ };
32
+ }
33
+ }
34
+
35
+ async function dlqProcessor(event) {
36
+ if (!event?.Records?.length) return { batchItemFailures: [] };
37
+
38
+ for (const record of event.Records) {
39
+ try {
40
+ const parsed = parseMessageBody(record.body);
41
+
42
+ console.error('[DLQ] Failed message', {
43
+ messageId: record.messageId,
44
+ event: parsed.event,
45
+ integrationId: parsed.integrationId,
46
+ processId: parsed.processId,
47
+ receiveCount: record.attributes?.ApproximateReceiveCount,
48
+ sentTimestamp: record.attributes?.SentTimestamp,
49
+ sourceQueue: extractQueueName(record.eventSourceARN),
50
+ ...(parsed.rawBody !== undefined && { rawBody: parsed.rawBody }),
51
+ });
52
+ } catch (error) {
53
+ console.error('[DLQ] Error processing DLQ record', {
54
+ messageId: record.messageId,
55
+ error: error.message,
56
+ });
57
+ }
58
+ }
59
+
60
+ return { batchItemFailures: [] };
61
+ }
62
+
63
+ module.exports = { dlqProcessor };
@@ -14,11 +14,7 @@ integrationClasses.forEach((IntegrationClass) => {
14
14
  isUserFacingResponse: false,
15
15
  method: async (event, context) => {
16
16
  const worker = new defaultQueueWorker();
17
- await worker.run(event, context);
18
- return {
19
- message: 'Successfully processed the Generic Queue Worker',
20
- input: event,
21
- };
17
+ return await worker.run(event, context);
22
18
  },
23
19
  }),
24
20
  };
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-next.75",
4
+ "version": "2.0.0-next.77",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0-next.75",
42
- "@friggframework/prettier-config": "2.0.0-next.75",
43
- "@friggframework/test": "2.0.0-next.75",
41
+ "@friggframework/eslint-config": "2.0.0-next.77",
42
+ "@friggframework/prettier-config": "2.0.0-next.77",
43
+ "@friggframework/test": "2.0.0-next.77",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "d8b091ab80329d8d8ed22c019d120f3122419bb9"
83
+ "gitHead": "0f451bd5c8493b4ec6f82964a976cf2533aa94d8"
84
84
  }
@@ -26,10 +26,18 @@ declare module "@friggframework/core" {
26
26
  ): Promise<any>;
27
27
  }
28
28
 
29
+ export interface BatchItemFailure {
30
+ itemIdentifier: string;
31
+ }
32
+
33
+ export interface BatchItemFailuresResponse {
34
+ batchItemFailures: BatchItemFailure[];
35
+ }
36
+
29
37
  export class Worker implements IWorker {
30
38
  getQueueURL(params: GetQueueURLParams): Promise<string | undefined>;
31
39
 
32
- run(params: { Records: any }): Promise<void>;
40
+ run(params: { Records: any }, context?: object): Promise<BatchItemFailuresResponse>;
33
41
 
34
42
  send(params: object & { QueueUrl: any }, delay?: number): Promise<string>;
35
43
 
@@ -38,7 +46,7 @@ declare module "@friggframework/core" {
38
46
 
39
47
  interface IWorker {
40
48
  getQueueURL(params: GetQueueURLParams): Promise<string | undefined>;
41
- run(params: { Records: any }): Promise<void>;
49
+ run(params: { Records: any }, context?: object): Promise<BatchItemFailuresResponse>;
42
50
  send(params: object & { QueueUrl: any }, delay?: number): Promise<string>;
43
51
  sendAsyncSQSMessage(params: SendSQSMessageParams): Promise<string>;
44
52
  }