@remit/sqs-client 0.0.10 → 0.0.12

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": "@remit/sqs-client",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -9,6 +9,10 @@
9
9
  "types": "./src/index.ts",
10
10
  "default": "./src/index.ts"
11
11
  },
12
+ "./heartbeat": {
13
+ "types": "./src/heartbeat.ts",
14
+ "default": "./src/heartbeat.ts"
15
+ },
12
16
  "./poller": {
13
17
  "types": "./src/poller.ts",
14
18
  "default": "./src/poller.ts"
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it, mock } from "node:test";
3
3
  import { SQSClient } from "@aws-sdk/client-sqs";
4
- import type { SQSBatchResponse, SQSEvent } from "aws-lambda";
4
+ import type { Context, SQSBatchResponse, SQSEvent } from "aws-lambda";
5
5
  import { runQueuePoller } from "./poller.js";
6
6
 
7
7
  const QUEUE_URL = "http://localhost:9324/000000000000/remit-test";
@@ -231,6 +231,67 @@ describe("runQueuePoller", () => {
231
231
  assert.match(String(failures[0]?.arguments[0]?.error), /ENOSPC/);
232
232
  });
233
233
 
234
+ // The `requestId` every line of one invocation is grouped by
235
+ // (deploy/vps/README.md, "Logs") is minted here, and it has to reach both the
236
+ // handler and the poller's own bracketing lines — otherwise grouping on it
237
+ // stops right before the line saying what happened to the message.
238
+ it("gives each batch its own requestId, on the handler context and on its own lines", async () => {
239
+ let receiveCalls = 0;
240
+ const sendMock = mock.method(
241
+ SQSClient.prototype,
242
+ "send",
243
+ // biome-ignore lint/suspicious/noExplicitAny: minimal SDK command shape, not worth typing per-command here
244
+ async function (this: SQSClient, command: any) {
245
+ if (command.constructor.name === "ReceiveMessageCommand") {
246
+ receiveCalls++;
247
+ if (receiveCalls >= 2) process.emit("SIGWINCH", "SIGWINCH");
248
+ return { Messages: [buildMessage(`m${receiveCalls}`)] };
249
+ }
250
+ if (command.constructor.name === "DeleteMessageCommand") return {};
251
+ throw new Error(`unexpected command: ${command.constructor.name}`);
252
+ },
253
+ );
254
+
255
+ const handler = mock.fn(
256
+ async (
257
+ _event: SQSEvent,
258
+ _context: Context,
259
+ ): Promise<SQSBatchResponse> => ({ batchItemFailures: [] }),
260
+ );
261
+ const log = buildLog();
262
+
263
+ try {
264
+ await runQueuePoller({
265
+ targets: [{ queueUrl: QUEUE_URL, handler, functionName: "test-fn" }],
266
+ log,
267
+ signals: ["SIGWINCH"],
268
+ createHeartbeat: () => async () => {},
269
+ });
270
+ } finally {
271
+ sendMock.mock.restore();
272
+ }
273
+
274
+ const requestIds = handler.mock.calls.map(
275
+ (call) => call.arguments[1].awsRequestId,
276
+ );
277
+ assert.equal(requestIds.length, 2);
278
+ for (const requestId of requestIds)
279
+ assert.match(requestId, /^[0-9a-f-]{36}$/);
280
+ assert.notEqual(requestIds[0], requestIds[1]);
281
+
282
+ const bracketing = ["poller: invoking handler", "poller: batch processed"];
283
+ for (const message of bracketing) {
284
+ const lines = log.info.mock.calls.filter(
285
+ (call) => call.arguments[1] === message,
286
+ );
287
+ assert.equal(lines.length, 2, `expected two ${message} lines`);
288
+ assert.deepEqual(
289
+ lines.map((call) => call.arguments[0].requestId),
290
+ requestIds,
291
+ );
292
+ }
293
+ });
294
+
234
295
  it("throws when constructed with no targets", async () => {
235
296
  await assert.rejects(
236
297
  () => runQueuePoller({ targets: [], log: buildLog() }),
package/src/poller.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { inspect } from "node:util";
2
3
  import {
3
4
  DeleteMessageCommand,
@@ -75,8 +76,6 @@ const pollTarget = async (
75
76
  const maxMessages = target.maxMessages ?? DEFAULT_MAX_MESSAGES;
76
77
  const visibilityTimeout =
77
78
  target.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS;
78
- const lambdaContext = { functionName: target.functionName } as Context;
79
-
80
79
  log.info({ queue: queueName, maxMessages }, "poller: started");
81
80
 
82
81
  // Always long-poll (SQS max WaitTimeSeconds). An empty long poll is free
@@ -146,8 +145,26 @@ const pollTarget = async (
146
145
  );
147
146
  if (messages.length === 0) continue;
148
147
 
148
+ // One batch is one handler invocation, so the id is minted per batch: it is
149
+ // the `requestId` every line of that invocation is grouped by (see
150
+ // deploy/vps/README.md, "Logs"). A context built once per target would put
151
+ // the whole queue's lifetime under one id, which correlates nothing.
152
+ //
153
+ // Minted before the poller's own bracketing lines rather than next to the
154
+ // handler call: grouping on the id has to reach the line saying whether the
155
+ // message was deleted or left for redelivery, which is what someone looks
156
+ // for after a failure.
157
+ const lambdaContext = {
158
+ functionName: target.functionName,
159
+ awsRequestId: randomUUID(),
160
+ } as Context;
161
+
149
162
  log.info(
150
- { queue: queueName, count: messages.length },
163
+ {
164
+ queue: queueName,
165
+ count: messages.length,
166
+ requestId: lambdaContext.awsRequestId,
167
+ },
151
168
  "poller: invoking handler",
152
169
  );
153
170
 
@@ -198,6 +215,7 @@ const pollTarget = async (
198
215
  queue: queueName,
199
216
  deleted: succeeded.length,
200
217
  leftForRedelivery: failedIds.size,
218
+ requestId: lambdaContext.awsRequestId,
201
219
  },
202
220
  "poller: batch processed",
203
221
  );