@remit/sqs-client 0.0.1
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 +40 -0
- package/src/index.test.ts +39 -0
- package/src/index.ts +23 -0
- package/src/poller.test.ts +90 -0
- package/src/poller.ts +190 -0
- package/src/producer.test.ts +28 -0
- package/src/producer.ts +32 -0
- package/tsconfig.json +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@remit/sqs-client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./poller": {
|
|
13
|
+
"types": "./src/poller.ts",
|
|
14
|
+
"default": "./src/poller.ts"
|
|
15
|
+
},
|
|
16
|
+
"./producer": {
|
|
17
|
+
"types": "./src/producer.ts",
|
|
18
|
+
"default": "./src/producer.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test:typecheck": "tsgo --noEmit",
|
|
23
|
+
"test:run": "node --env-file=../../localhost-test-unit.env --test 'src/**/*.test.ts'",
|
|
24
|
+
"test": "npm run test:typecheck && npm run test:run"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@aws-sdk/client-sqs": "*",
|
|
28
|
+
"@aws-sdk/core": "*",
|
|
29
|
+
"@types/aws-lambda": "*"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/remit-mail/remit.git",
|
|
38
|
+
"directory": "packages/sqs-client"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { resolveSqsCredentials } from "./index.js";
|
|
4
|
+
|
|
5
|
+
test("returns the SQS pair when both are set", () => {
|
|
6
|
+
const creds = resolveSqsCredentials({
|
|
7
|
+
SQS_ACCESS_KEY_ID: "SCWXXX",
|
|
8
|
+
SQS_SECRET_ACCESS_KEY: "secret",
|
|
9
|
+
});
|
|
10
|
+
assert.deepEqual(creds, { accessKeyId: "SCWXXX", secretAccessKey: "secret" });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("falls back to the default chain when the access key is missing", () => {
|
|
14
|
+
assert.equal(
|
|
15
|
+
resolveSqsCredentials({ SQS_SECRET_ACCESS_KEY: "secret" }),
|
|
16
|
+
undefined,
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("falls back to the default chain when the secret is missing", () => {
|
|
21
|
+
assert.equal(
|
|
22
|
+
resolveSqsCredentials({ SQS_ACCESS_KEY_ID: "SCWXXX" }),
|
|
23
|
+
undefined,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("falls back to the default chain when neither is set", () => {
|
|
28
|
+
assert.equal(resolveSqsCredentials({}), undefined);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("ignores the AWS S3 credentials — reads only the SQS pair", () => {
|
|
32
|
+
assert.equal(
|
|
33
|
+
resolveSqsCredentials({
|
|
34
|
+
AWS_ACCESS_KEY_ID: "AKIA",
|
|
35
|
+
AWS_SECRET_ACCESS_KEY: "aws-secret",
|
|
36
|
+
}),
|
|
37
|
+
undefined,
|
|
38
|
+
);
|
|
39
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SqsStaticCredentials {
|
|
2
|
+
accessKeyId: string;
|
|
3
|
+
secretAccessKey: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// On AWS, SQS shares the process-wide credential chain (AWS_ACCESS_KEY_ID / IAM
|
|
7
|
+
// role). On Scaleway, Messaging & Queuing has its own credential pair, distinct
|
|
8
|
+
// from the Object Storage (S3) keys, so it can't ride the default chain — the app
|
|
9
|
+
// is handed SQS_ACCESS_KEY_ID / SQS_SECRET_ACCESS_KEY and must feed them to the
|
|
10
|
+
// SQS client explicitly. When they are absent (AWS, or local ElasticMQ), returning
|
|
11
|
+
// undefined lets the SDK fall back to its default credential provider chain.
|
|
12
|
+
export function resolveSqsCredentials(
|
|
13
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
14
|
+
): SqsStaticCredentials | undefined {
|
|
15
|
+
const accessKeyId = env.SQS_ACCESS_KEY_ID;
|
|
16
|
+
const secretAccessKey = env.SQS_SECRET_ACCESS_KEY;
|
|
17
|
+
|
|
18
|
+
if (accessKeyId && secretAccessKey) {
|
|
19
|
+
return { accessKeyId, secretAccessKey };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it, mock } from "node:test";
|
|
3
|
+
import { SQSClient } from "@aws-sdk/client-sqs";
|
|
4
|
+
import type { SQSBatchResponse, SQSEvent } from "aws-lambda";
|
|
5
|
+
import { runQueuePoller } from "./poller.js";
|
|
6
|
+
|
|
7
|
+
const QUEUE_URL = "http://localhost:9324/000000000000/remit-test";
|
|
8
|
+
|
|
9
|
+
const buildLog = () => ({
|
|
10
|
+
info: mock.fn(() => {}),
|
|
11
|
+
error: mock.fn(() => {}),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const buildMessage = (id: string) => ({
|
|
15
|
+
MessageId: id,
|
|
16
|
+
ReceiptHandle: `receipt-${id}`,
|
|
17
|
+
Body: "{}",
|
|
18
|
+
Attributes: { ApproximateReceiveCount: "1" },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("runQueuePoller", () => {
|
|
22
|
+
it("deletes succeeded messages and leaves batchItemFailures for redelivery, then stops on signal", async () => {
|
|
23
|
+
const deletedReceiptHandles: string[] = [];
|
|
24
|
+
let receiveCalls = 0;
|
|
25
|
+
|
|
26
|
+
// SQSClient.prototype.send is mocked globally (poller.ts constructs its
|
|
27
|
+
// own client internally — there is no injection seam), matching the
|
|
28
|
+
// same shape every AWS SDK v3 command carries: `{ input, constructor }`.
|
|
29
|
+
const sendMock = mock.method(
|
|
30
|
+
SQSClient.prototype,
|
|
31
|
+
"send",
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: minimal SDK command shape, not worth typing per-command here
|
|
33
|
+
async function (this: SQSClient, command: any) {
|
|
34
|
+
if (command.constructor.name === "ReceiveMessageCommand") {
|
|
35
|
+
receiveCalls++;
|
|
36
|
+
if (receiveCalls === 1) {
|
|
37
|
+
// Simulate the shutdown signal landing while this batch is
|
|
38
|
+
// in flight — the loop is expected to finish processing it
|
|
39
|
+
// (delete succeeded / leave failures) and then exit before
|
|
40
|
+
// issuing a second ReceiveMessage.
|
|
41
|
+
process.emit("SIGWINCH", "SIGWINCH");
|
|
42
|
+
return { Messages: [buildMessage("m1"), buildMessage("m2")] };
|
|
43
|
+
}
|
|
44
|
+
throw new Error(
|
|
45
|
+
"unexpected second ReceiveMessage — poller did not stop on signal",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (command.constructor.name === "DeleteMessageCommand") {
|
|
49
|
+
deletedReceiptHandles.push(command.input.ReceiptHandle);
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`unexpected command: ${command.constructor.name}`);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const handler = mock.fn(
|
|
57
|
+
async (_event: SQSEvent): Promise<SQSBatchResponse> => ({
|
|
58
|
+
batchItemFailures: [{ itemIdentifier: "m2" }],
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await runQueuePoller({
|
|
64
|
+
targets: [{ queueUrl: QUEUE_URL, handler, functionName: "test-fn" }],
|
|
65
|
+
log: buildLog(),
|
|
66
|
+
signals: ["SIGWINCH"],
|
|
67
|
+
});
|
|
68
|
+
} finally {
|
|
69
|
+
sendMock.mock.restore();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
assert.equal(handler.mock.calls.length, 1);
|
|
73
|
+
const event = handler.mock.calls[0]?.arguments[0] as SQSEvent;
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
event.Records.map((r) => r.messageId),
|
|
76
|
+
["m1", "m2"],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// m1 succeeded (not in batchItemFailures) -> deleted. m2 failed -> left
|
|
80
|
+
// for redelivery, matching the SQS batchItemFailures contract.
|
|
81
|
+
assert.deepEqual(deletedReceiptHandles, ["receipt-m1"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("throws when constructed with no targets", async () => {
|
|
85
|
+
await assert.rejects(
|
|
86
|
+
() => runQueuePoller({ targets: [], log: buildLog() }),
|
|
87
|
+
/no targets configured/,
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/poller.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeleteMessageCommand,
|
|
3
|
+
ReceiveMessageCommand,
|
|
4
|
+
} from "@aws-sdk/client-sqs";
|
|
5
|
+
import type {
|
|
6
|
+
Context,
|
|
7
|
+
SQSBatchResponse,
|
|
8
|
+
SQSEvent,
|
|
9
|
+
SQSHandler,
|
|
10
|
+
} from "aws-lambda";
|
|
11
|
+
import { createQueueProducer } from "./producer.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Production queue poller — the non-e2e counterpart of the per-worker
|
|
15
|
+
* `e2e-processor-shim.ts` scripts. It receives from an SQS-compatible
|
|
16
|
+
* endpoint (real SQS or ElasticMQ), wraps each batch in an `SQSEvent`, and
|
|
17
|
+
* invokes the exact production Lambda `handler`, honouring
|
|
18
|
+
* `batchItemFailures` the way the SQS service does: delete what succeeded,
|
|
19
|
+
* leave failures for redelivery.
|
|
20
|
+
*
|
|
21
|
+
* Unlike the e2e shim (one child process per queue, via `node:cluster`),
|
|
22
|
+
* this runs every target queue as a concurrent poll loop inside a single
|
|
23
|
+
* process — the right shape for a container whose only job is "poll these
|
|
24
|
+
* queues, invoke this handler, exit on SIGTERM."
|
|
25
|
+
*/
|
|
26
|
+
export interface QueuePollerLog {
|
|
27
|
+
info: (fields: Record<string, unknown>, message: string) => void;
|
|
28
|
+
error: (fields: Record<string, unknown>, message: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface QueuePollerTarget {
|
|
32
|
+
readonly queueUrl: string;
|
|
33
|
+
readonly handler: SQSHandler;
|
|
34
|
+
/** Reported as the Lambda `Context.functionName` the handler observes. */
|
|
35
|
+
readonly functionName: string;
|
|
36
|
+
readonly maxMessages?: number;
|
|
37
|
+
readonly visibilityTimeoutSeconds?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RunQueuePollerOptions {
|
|
41
|
+
readonly targets: readonly QueuePollerTarget[];
|
|
42
|
+
readonly log: QueuePollerLog;
|
|
43
|
+
readonly signals?: readonly NodeJS.Signals[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_MAX_MESSAGES = 10; // SQS API limit
|
|
47
|
+
const DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
|
|
48
|
+
|
|
49
|
+
const pollTarget = async (
|
|
50
|
+
target: QueuePollerTarget,
|
|
51
|
+
log: QueuePollerLog,
|
|
52
|
+
isShuttingDown: () => boolean,
|
|
53
|
+
): Promise<void> => {
|
|
54
|
+
const queueName = new URL(target.queueUrl).pathname.split("/").pop();
|
|
55
|
+
const sqs = createQueueProducer({ queueUrl: target.queueUrl });
|
|
56
|
+
const maxMessages = target.maxMessages ?? DEFAULT_MAX_MESSAGES;
|
|
57
|
+
const visibilityTimeout =
|
|
58
|
+
target.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS;
|
|
59
|
+
const lambdaContext = { functionName: target.functionName } as Context;
|
|
60
|
+
|
|
61
|
+
log.info({ queue: queueName, maxMessages }, "poller: started");
|
|
62
|
+
|
|
63
|
+
// Always long-poll (SQS max WaitTimeSeconds). An empty long poll is free
|
|
64
|
+
// and returns as soon as a message arrives, so this is never slower than
|
|
65
|
+
// short-polling under load — it just stops burning a ReceiveMessage call
|
|
66
|
+
// (and, against real SQS, its cost) every loop iteration while traffic is
|
|
67
|
+
// continuous.
|
|
68
|
+
const LONG_POLL_WAIT_SECONDS = 20;
|
|
69
|
+
|
|
70
|
+
while (!isShuttingDown()) {
|
|
71
|
+
const response = await sqs.send(
|
|
72
|
+
new ReceiveMessageCommand({
|
|
73
|
+
QueueUrl: target.queueUrl,
|
|
74
|
+
MaxNumberOfMessages: maxMessages,
|
|
75
|
+
WaitTimeSeconds: LONG_POLL_WAIT_SECONDS,
|
|
76
|
+
VisibilityTimeout: visibilityTimeout,
|
|
77
|
+
MessageSystemAttributeNames: ["ApproximateReceiveCount"],
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!response.Messages || response.Messages.length === 0) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const messages = response.Messages.flatMap((m) =>
|
|
86
|
+
m.Body && m.ReceiptHandle && m.MessageId
|
|
87
|
+
? [
|
|
88
|
+
{
|
|
89
|
+
messageId: m.MessageId,
|
|
90
|
+
receiptHandle: m.ReceiptHandle,
|
|
91
|
+
body: m.Body,
|
|
92
|
+
receiveCount: m.Attributes?.ApproximateReceiveCount ?? "1",
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
: [],
|
|
96
|
+
);
|
|
97
|
+
if (messages.length === 0) continue;
|
|
98
|
+
|
|
99
|
+
log.info(
|
|
100
|
+
{ queue: queueName, count: messages.length },
|
|
101
|
+
"poller: invoking handler",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const event: SQSEvent = {
|
|
105
|
+
Records: messages.map((m) => ({
|
|
106
|
+
messageId: m.messageId,
|
|
107
|
+
receiptHandle: m.receiptHandle,
|
|
108
|
+
body: m.body,
|
|
109
|
+
attributes: {
|
|
110
|
+
ApproximateReceiveCount: m.receiveCount,
|
|
111
|
+
SentTimestamp: "0",
|
|
112
|
+
SenderId: "poller",
|
|
113
|
+
ApproximateFirstReceiveTimestamp: "0",
|
|
114
|
+
},
|
|
115
|
+
messageAttributes: {},
|
|
116
|
+
md5OfBody: "",
|
|
117
|
+
eventSource: "aws:sqs",
|
|
118
|
+
eventSourceARN: target.queueUrl,
|
|
119
|
+
awsRegion: "local",
|
|
120
|
+
})),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = (await target.handler(event, lambdaContext, () => {})) as
|
|
124
|
+
| SQSBatchResponse
|
|
125
|
+
| undefined;
|
|
126
|
+
|
|
127
|
+
const failedIds = new Set(
|
|
128
|
+
(result?.batchItemFailures ?? []).map((f) => f.itemIdentifier),
|
|
129
|
+
);
|
|
130
|
+
const succeeded = messages.filter((m) => !failedIds.has(m.messageId));
|
|
131
|
+
|
|
132
|
+
// Batch is capped at maxMessages (SQS API limit: 10), so a handful of
|
|
133
|
+
// concurrent DeleteMessage calls is safe and avoids paying N sequential
|
|
134
|
+
// round trips between every receive/handler cycle.
|
|
135
|
+
await Promise.all(
|
|
136
|
+
succeeded.map((message) =>
|
|
137
|
+
sqs.send(
|
|
138
|
+
new DeleteMessageCommand({
|
|
139
|
+
QueueUrl: target.queueUrl,
|
|
140
|
+
ReceiptHandle: message.receiptHandle,
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
log.info(
|
|
147
|
+
{
|
|
148
|
+
queue: queueName,
|
|
149
|
+
deleted: succeeded.length,
|
|
150
|
+
leftForRedelivery: failedIds.size,
|
|
151
|
+
},
|
|
152
|
+
"poller: batch processed",
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
log.info({ queue: queueName }, "poller: stopped");
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Runs every target's poll loop concurrently until a shutdown signal is
|
|
161
|
+
* received (default: SIGINT, SIGTERM), then lets each loop finish its
|
|
162
|
+
* current iteration and returns. Rejects (crashes the process) if any loop
|
|
163
|
+
* throws — a stuck poller should exit loudly, not degrade silently.
|
|
164
|
+
*/
|
|
165
|
+
export const runQueuePoller = async (
|
|
166
|
+
options: RunQueuePollerOptions,
|
|
167
|
+
): Promise<void> => {
|
|
168
|
+
const { targets, log } = options;
|
|
169
|
+
const signals = options.signals ?? ["SIGINT", "SIGTERM"];
|
|
170
|
+
|
|
171
|
+
if (targets.length === 0) {
|
|
172
|
+
throw new Error("runQueuePoller: no targets configured");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let shuttingDown = false;
|
|
176
|
+
const isShuttingDown = () => shuttingDown;
|
|
177
|
+
const onSignal = (signal: NodeJS.Signals) => {
|
|
178
|
+
log.info({ signal }, "poller: shutdown signal received");
|
|
179
|
+
shuttingDown = true;
|
|
180
|
+
};
|
|
181
|
+
for (const signal of signals) {
|
|
182
|
+
process.on(signal, onSignal);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
log.info({ queues: targets.map((t) => t.queueUrl) }, "poller: starting");
|
|
186
|
+
|
|
187
|
+
await Promise.all(
|
|
188
|
+
targets.map((target) => pollTarget(target, log, isShuttingDown)),
|
|
189
|
+
);
|
|
190
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { createQueueProducer } from "./producer.js";
|
|
4
|
+
|
|
5
|
+
describe("createQueueProducer", () => {
|
|
6
|
+
it("returns a real SQSClient for a local queue URL", () => {
|
|
7
|
+
const client = createQueueProducer({
|
|
8
|
+
queueUrl: "http://localhost:4566/000000000000/test-queue",
|
|
9
|
+
});
|
|
10
|
+
assert.equal(typeof client.send, "function");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns a real SQSClient for a real AWS queue URL", () => {
|
|
14
|
+
const client = createQueueProducer({
|
|
15
|
+
queueUrl:
|
|
16
|
+
"https://sqs.eu-west-1.amazonaws.com/123456789012/remit-dev-mailboxes.fifo",
|
|
17
|
+
});
|
|
18
|
+
assert.equal(typeof client.send, "function");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("accepts an explicit endpoint override", () => {
|
|
22
|
+
const client = createQueueProducer({
|
|
23
|
+
queueUrl: "http://localhost:4566/000000000000/test-queue",
|
|
24
|
+
endpoint: "http://custom-endpoint:1234",
|
|
25
|
+
});
|
|
26
|
+
assert.equal(typeof client.send, "function");
|
|
27
|
+
});
|
|
28
|
+
});
|
package/src/producer.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SQSClient } from "@aws-sdk/client-sqs";
|
|
2
|
+
import { AwsQueryProtocol } from "@aws-sdk/core/protocols";
|
|
3
|
+
import { resolveSqsCredentials } from "./index.js";
|
|
4
|
+
|
|
5
|
+
export interface CreateQueueProducerOptions {
|
|
6
|
+
readonly queueUrl: string;
|
|
7
|
+
readonly endpoint?: string;
|
|
8
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const isLocalEndpoint = (queueUrl: string): boolean =>
|
|
12
|
+
queueUrl.startsWith("http://") || queueUrl.startsWith("https://localhost");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Producer-side counterpart of `runQueuePoller`: the one place an
|
|
16
|
+
* SQS-compatible client is constructed for sending. A queue URL that points at
|
|
17
|
+
* a local endpoint (ElasticMQ) gets its origin as the SDK endpoint and the
|
|
18
|
+
* query protocol ElasticMQ speaks; real SQS and Scaleway M&Q resolve their own
|
|
19
|
+
* endpoint and ride the default credential chain unless
|
|
20
|
+
* `SQS_ACCESS_KEY_ID`/`SQS_SECRET_ACCESS_KEY` are present.
|
|
21
|
+
*/
|
|
22
|
+
export const createQueueProducer = (
|
|
23
|
+
options: CreateQueueProducerOptions,
|
|
24
|
+
): SQSClient => {
|
|
25
|
+
const { queueUrl, endpoint, env } = options;
|
|
26
|
+
const local = isLocalEndpoint(queueUrl);
|
|
27
|
+
return new SQSClient({
|
|
28
|
+
endpoint: endpoint ?? (local ? new URL(queueUrl).origin : undefined),
|
|
29
|
+
...(local && { protocol: AwsQueryProtocol }),
|
|
30
|
+
credentials: resolveSqsCredentials(env),
|
|
31
|
+
});
|
|
32
|
+
};
|