@remit/account-worker 0.0.3 → 0.0.4

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/account-worker",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { AwsQueryProtocol } from "@aws-sdk/core/protocols";
4
+ import { getSqsClient } from "./config.js";
5
+
6
+ // The queue URL a self-hosted compose stack hands the account worker
7
+ // (deploy/vps/remit.env.template). It is a plain http:// URL that is not
8
+ // localhost, which is exactly the case the endpoint heuristic used to miss.
9
+ const composeQueueUrl =
10
+ "http://queue:9324/000000000000/remit-account-purge-delete.fifo";
11
+ const awsQueueUrl =
12
+ "https://sqs.eu-west-1.amazonaws.com/123456789012/remit-account-purge-delete.fifo";
13
+
14
+ describe("getSqsClient", () => {
15
+ // Regression: the account-deletion cascade sends through this client. It
16
+ // used to be built once at import with no endpoint and no protocol, so on a
17
+ // self-hosted stack every cascade send went to real SQS instead of the local
18
+ // queue server and account deletion never completed.
19
+ it("selects the query protocol and the queue origin for a compose-stack queue URL", async () => {
20
+ const client = getSqsClient(composeQueueUrl);
21
+ assert.equal(client.config.protocol instanceof AwsQueryProtocol, true);
22
+ const endpoint = await client.config.endpoint?.();
23
+ assert.equal(endpoint?.hostname, "queue");
24
+ assert.equal(endpoint?.port, 9324);
25
+ });
26
+
27
+ it("leaves the default protocol and endpoint for a real SQS queue URL", async () => {
28
+ const client = getSqsClient(awsQueueUrl);
29
+ assert.equal(client.config.protocol instanceof AwsQueryProtocol, false);
30
+ assert.equal(client.config.endpoint, undefined);
31
+ });
32
+
33
+ it("reuses one client per queue URL and keeps distinct queues apart", () => {
34
+ assert.equal(getSqsClient(composeQueueUrl), getSqsClient(composeQueueUrl));
35
+ assert.notEqual(getSqsClient(composeQueueUrl), getSqsClient(awsQueueUrl));
36
+ });
37
+ });
package/src/config.ts CHANGED
@@ -1,14 +1,25 @@
1
- import { SQSClient } from "@aws-sdk/client-sqs";
1
+ import type { SQSClient } from "@aws-sdk/client-sqs";
2
2
  import { getClient } from "@remit/backend/client";
3
- import { resolveSqsCredentials } from "@remit/sqs-client";
3
+ import { createQueueProducer } from "@remit/sqs-client/producer";
4
4
  import type { StorageService } from "@remit/storage-service";
5
5
  import { createStorageService } from "@remit/storage-service/s3";
6
6
  import { env } from "expect-env";
7
7
  import type { CascadeServices } from "./cascade.js";
8
8
 
9
- export const sqsClient = new SQSClient({
10
- credentials: resolveSqsCredentials(),
11
- });
9
+ // A client per queue URL, resolved at send time rather than at import. The
10
+ // endpoint and wire protocol are derived from the URL, so a self-hosted stack
11
+ // (http://queue:9324/...) gets the query protocol its queue server speaks while
12
+ // real SQS keeps the default. Building at import is not an option: the queue
13
+ // URLs are read lazily for the reason given below.
14
+ const sqsClients = new Map<string, SQSClient>();
15
+
16
+ export const getSqsClient = (queueUrl: string): SQSClient => {
17
+ const existing = sqsClients.get(queueUrl);
18
+ if (existing) return existing;
19
+ const client = createQueueProducer({ queueUrl });
20
+ sqsClients.set(queueUrl, client);
21
+ return client;
22
+ };
12
23
 
13
24
  // SQS env vars are lazy-evaluated. The fanout worker needs them at handler time;
14
25
  // the finalize worker imports `getCascadeServices` from this module but talks to
@@ -1,9 +1,5 @@
1
1
  import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
2
- import {
3
- createLogger,
4
- type Logger,
5
- withTelemetry,
6
- } from "@remit/logger-lambda";
2
+ import { createLogger, type Logger, withTelemetry } from "@remit/logger-lambda";
7
3
  import type { SQSBatchResponse, SQSEvent, SQSHandler } from "aws-lambda";
8
4
  import type { CascadeServices } from "../cascade.js";
9
5
  import { enumerateCascadeEntities } from "../cascade.js";
@@ -12,7 +8,7 @@ import {
12
8
  getAccountPurgeDeleteQueueUrl,
13
9
  getCascadeServices,
14
10
  getImapWorkerQueueUrl,
15
- sqsClient,
11
+ getSqsClient,
16
12
  } from "../config.js";
17
13
  import {
18
14
  type DeletionCapabilities,
@@ -49,12 +45,13 @@ export const processAccountFanout = async (
49
45
  deps: ProcessAccountFanoutDeps = {},
50
46
  ): Promise<void> => {
51
47
  const services = deps.services ?? (await getCascadeServices());
52
- const sqs = deps.sqs ?? sqsClient;
48
+ const sqsFor = (queueUrl: string): SQSClient =>
49
+ deps.sqs ?? getSqsClient(queueUrl);
53
50
 
54
51
  if (event.type === "AccountDataPurge") {
55
52
  await processAccountDataPurge(event, log, {
56
53
  services,
57
- sqs,
54
+ sqs: deps.sqs,
58
55
  accountPurgeDeleteQueueUrl:
59
56
  deps.accountPurgeDeleteQueueUrl ?? getAccountPurgeDeleteQueueUrl(),
60
57
  });
@@ -71,14 +68,14 @@ export const processAccountFanout = async (
71
68
  return;
72
69
  }
73
70
 
74
- await processAccountDelete(event, log, services, sqs, deps);
71
+ await processAccountDelete(event, log, services, sqsFor, deps);
75
72
  };
76
73
 
77
74
  const processAccountDelete = async (
78
75
  event: AccountDeleteEvent,
79
76
  log: Logger,
80
77
  services: CascadeServices,
81
- sqs: SQSClient,
78
+ sqsFor: (queueUrl: string) => SQSClient,
82
79
  deps: ProcessAccountFanoutDeps,
83
80
  ): Promise<void> => {
84
81
  const { accountConfigId } = event;
@@ -102,7 +99,7 @@ const processAccountDelete = async (
102
99
  .map((e) => e.key.accountId);
103
100
 
104
101
  for (const accountId of accountIds) {
105
- await sqs.send(
102
+ await sqsFor(imapWorkerQueueUrl).send(
106
103
  new SendMessageCommand({
107
104
  QueueUrl: imapWorkerQueueUrl,
108
105
  MessageBody: JSON.stringify({
@@ -124,7 +121,7 @@ const processAccountDelete = async (
124
121
  type: "FinalizeAccountDelete",
125
122
  accountConfigId,
126
123
  };
127
- await sqs.send(
124
+ await sqsFor(accountFinalizeQueueUrl).send(
128
125
  new SendMessageCommand({
129
126
  QueueUrl: accountFinalizeQueueUrl,
130
127
  MessageBody: JSON.stringify(finalizeEvent),
@@ -11,10 +11,10 @@ import { Entity } from "electrodb";
11
11
  import type { CascadeServices } from "../cascade.js";
12
12
  import {
13
13
  dataBackend as defaultDataBackend,
14
- sqsClient as defaultSqsClient,
15
14
  getAccountPurgeDeleteQueueUrl,
16
15
  getCascadeServices,
17
16
  getSearchIndexQueueUrl,
17
+ getSqsClient,
18
18
  } from "../config.js";
19
19
  import type {
20
20
  AccountDataPurgeEvent,
@@ -162,7 +162,8 @@ export const processAccountDataPurge = async (
162
162
  ): Promise<void> => {
163
163
  const { accountId, accountConfigId } = event;
164
164
  const services = deps.services ?? (await getCascadeServices());
165
- const sqs = deps.sqs ?? defaultSqsClient;
165
+ const sqsFor = (queueUrl: string): SQSClient =>
166
+ deps.sqs ?? getSqsClient(queueUrl);
166
167
  const accountPurgeDeleteQueueUrl =
167
168
  deps.accountPurgeDeleteQueueUrl ?? getAccountPurgeDeleteQueueUrl();
168
169
  const searchIndexQueueUrl =
@@ -200,7 +201,12 @@ export const processAccountDataPurge = async (
200
201
  // here too would double the removal. On DynamoDB this fanout is the sole
201
202
  // producer of the vector deletes (#457).
202
203
  if (dataBackend !== "postgres") {
203
- await enqueueVectorDeletes(sqs, searchIndexQueueUrl, accountId, messageIds);
204
+ await enqueueVectorDeletes(
205
+ sqsFor(searchIndexQueueUrl),
206
+ searchIndexQueueUrl,
207
+ accountId,
208
+ messageIds,
209
+ );
204
210
  log.info(
205
211
  { accountConfigId, accountId, count: messageIds.length },
206
212
  "Enqueued search-index deletes for purged account",
@@ -208,7 +214,7 @@ export const processAccountDataPurge = async (
208
214
  }
209
215
 
210
216
  await enqueuePurgeFinalize(
211
- sqs,
217
+ sqsFor(accountPurgeDeleteQueueUrl),
212
218
  accountPurgeDeleteQueueUrl,
213
219
  accountId,
214
220
  accountConfigId,