@remit/account-worker 0.0.7 → 0.0.8
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 +1 -1
- package/src/config.test.ts +24 -2
- package/src/config.ts +8 -2
- package/src/handlers/account-fanout.test.ts +119 -0
- package/src/handlers/account-fanout.ts +24 -13
- package/src/poller.ts +8 -5
package/package.json
CHANGED
package/src/config.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
2
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
3
|
import { AwsQueryProtocol } from "@aws-sdk/core/protocols";
|
|
4
|
-
import { getSqsClient } from "./config.js";
|
|
4
|
+
import { getImapWorkerQueueUrl, getSqsClient } from "./config.js";
|
|
5
5
|
|
|
6
6
|
// The queue URL a self-hosted compose stack hands the account worker
|
|
7
7
|
// (deploy/vps/remit.env.template). It is a plain http:// URL that is not
|
|
@@ -35,3 +35,25 @@ describe("getSqsClient", () => {
|
|
|
35
35
|
assert.notEqual(getSqsClient(composeQueueUrl), getSqsClient(awsQueueUrl));
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
describe("getImapWorkerQueueUrl", () => {
|
|
40
|
+
// The imap-worker stop queue is optional: the self-host compose stacks do not
|
|
41
|
+
// provision it, and reading it through expect-env would throw and abort the
|
|
42
|
+
// deletion fanout before it ever reached the finalize step. It must yield
|
|
43
|
+
// undefined when unset so the fanout can skip the (no-op) stop signal.
|
|
44
|
+
const prev = process.env.SQS_QUEUE_URL_IMAP_WORKER;
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
if (prev === undefined) delete process.env.SQS_QUEUE_URL_IMAP_WORKER;
|
|
47
|
+
else process.env.SQS_QUEUE_URL_IMAP_WORKER = prev;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns undefined when the var is unset instead of throwing", () => {
|
|
51
|
+
delete process.env.SQS_QUEUE_URL_IMAP_WORKER;
|
|
52
|
+
assert.equal(getImapWorkerQueueUrl(), undefined);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns the configured value when set", () => {
|
|
56
|
+
process.env.SQS_QUEUE_URL_IMAP_WORKER = "http://queue/imap-worker";
|
|
57
|
+
assert.equal(getImapWorkerQueueUrl(), "http://queue/imap-worker");
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -28,8 +28,14 @@ export const getSqsClient = (queueUrl: string): SQSClient => {
|
|
|
28
28
|
// sites.
|
|
29
29
|
export const getSearchIndexQueueUrl = (): string =>
|
|
30
30
|
env.SQS_QUEUE_URL_SEARCH_INDEX;
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// Optional, and read through `process.env` rather than expect-env so an unset
|
|
32
|
+
// var yields undefined instead of throwing. IMAP_WORKER_STOP is a no-op
|
|
33
|
+
// acknowledgement in the cascade contract — the account tombstone fence is what
|
|
34
|
+
// actually halts the worker — so a deployment that never provisions a dedicated
|
|
35
|
+
// imap-worker stop queue (the self-host compose stacks) skips the signal rather
|
|
36
|
+
// than failing the whole fanout. AWS sets the var and keeps sending it.
|
|
37
|
+
export const getImapWorkerQueueUrl = (): string | undefined =>
|
|
38
|
+
process.env.SQS_QUEUE_URL_IMAP_WORKER;
|
|
33
39
|
export const getAccountFinalizeQueueUrl = (): string =>
|
|
34
40
|
env.SQS_QUEUE_URL_ACCOUNT_FINALIZE;
|
|
35
41
|
export const getAccountPurgeDeleteQueueUrl = (): string =>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
|
+
import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
4
|
+
import type { Logger } from "@remit/logger-lambda";
|
|
5
|
+
import type { CascadeServices } from "../cascade.js";
|
|
6
|
+
import type { AccountDeleteEvent } from "../events.js";
|
|
7
|
+
import { processAccountFanout } from "./account-fanout.js";
|
|
8
|
+
|
|
9
|
+
const noopLog = {
|
|
10
|
+
info: () => {},
|
|
11
|
+
warn: () => {},
|
|
12
|
+
error: () => {},
|
|
13
|
+
debug: () => {},
|
|
14
|
+
fatal: () => {},
|
|
15
|
+
trace: () => {},
|
|
16
|
+
child: () => noopLog,
|
|
17
|
+
} as unknown as Logger;
|
|
18
|
+
|
|
19
|
+
// A CascadeServices fake with one account and no data: enough for
|
|
20
|
+
// enumerateCascadeEntities to yield a single Account row (so the stop-signal
|
|
21
|
+
// loop has one accountId to iterate) without standing up a database.
|
|
22
|
+
const buildServices = (): CascadeServices =>
|
|
23
|
+
({
|
|
24
|
+
accountConfigService: {
|
|
25
|
+
get: async () => ({ userId: "user-1" }),
|
|
26
|
+
describe: async () => ({
|
|
27
|
+
account: [{ accountId: "acc-1" }],
|
|
28
|
+
address: [],
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
accountService: {
|
|
32
|
+
describe: async () => ({ mailbox: [] }),
|
|
33
|
+
},
|
|
34
|
+
messageService: {
|
|
35
|
+
listAllByMailbox: async () => [],
|
|
36
|
+
describe: async () => ({}),
|
|
37
|
+
},
|
|
38
|
+
outboxMessageService: { listByAccount: async () => ({ items: [] }) },
|
|
39
|
+
mailboxLockService: { listByAccount: async () => [] },
|
|
40
|
+
messagePlacementMoveService: { listByAccountId: async () => [] },
|
|
41
|
+
messageFlagPushService: { listByAccountId: async () => [] },
|
|
42
|
+
threadMessageService: { listAllByAccount: async () => [] },
|
|
43
|
+
accountSettingService: { listByAccountConfig: async () => [] },
|
|
44
|
+
filterService: { listByAccountConfig: async () => [] },
|
|
45
|
+
filterAnchorService: { get: async () => null },
|
|
46
|
+
labelService: { listByAccountConfig: async () => [] },
|
|
47
|
+
messageLabelService: { listByLabelId: async () => [] },
|
|
48
|
+
}) as unknown as CascadeServices;
|
|
49
|
+
|
|
50
|
+
interface SentMessage {
|
|
51
|
+
queueUrl: string | undefined;
|
|
52
|
+
body: { type?: string; accountId?: string };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const recordingSqs = (sent: SentMessage[]): SQSClient =>
|
|
56
|
+
({
|
|
57
|
+
send: async (command: {
|
|
58
|
+
input: { QueueUrl?: string; MessageBody?: string };
|
|
59
|
+
}) => {
|
|
60
|
+
sent.push({
|
|
61
|
+
queueUrl: command.input.QueueUrl,
|
|
62
|
+
body: JSON.parse(command.input.MessageBody ?? "{}"),
|
|
63
|
+
});
|
|
64
|
+
return {};
|
|
65
|
+
},
|
|
66
|
+
}) as unknown as SQSClient;
|
|
67
|
+
|
|
68
|
+
const deleteEvent: AccountDeleteEvent = {
|
|
69
|
+
type: "AccountDelete",
|
|
70
|
+
accountConfigId: "cfg-1",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
describe("processAccountFanout — imap-worker stop signal on account delete", () => {
|
|
74
|
+
const prev = process.env.SQS_QUEUE_URL_IMAP_WORKER;
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
if (prev === undefined) delete process.env.SQS_QUEUE_URL_IMAP_WORKER;
|
|
77
|
+
else process.env.SQS_QUEUE_URL_IMAP_WORKER = prev;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("skips the stop signal when no imap-worker queue is configured, still enqueuing finalize", async () => {
|
|
81
|
+
delete process.env.SQS_QUEUE_URL_IMAP_WORKER;
|
|
82
|
+
const sent: SentMessage[] = [];
|
|
83
|
+
|
|
84
|
+
await processAccountFanout(deleteEvent, noopLog, {
|
|
85
|
+
services: buildServices(),
|
|
86
|
+
sqs: recordingSqs(sent),
|
|
87
|
+
signOut: async () => {},
|
|
88
|
+
accountFinalizeQueueUrl: "http://queue/finalize",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const stops = sent.filter((m) => m.body.type === "IMAP_WORKER_STOP");
|
|
92
|
+
const finalize = sent.filter(
|
|
93
|
+
(m) => m.body.type === "FinalizeAccountDelete",
|
|
94
|
+
);
|
|
95
|
+
assert.equal(stops.length, 0, "no stop signal without a configured queue");
|
|
96
|
+
assert.equal(finalize.length, 1, "finalize is still enqueued");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("enqueues one stop signal per account when a queue is configured", async () => {
|
|
100
|
+
const sent: SentMessage[] = [];
|
|
101
|
+
|
|
102
|
+
await processAccountFanout(deleteEvent, noopLog, {
|
|
103
|
+
services: buildServices(),
|
|
104
|
+
sqs: recordingSqs(sent),
|
|
105
|
+
signOut: async () => {},
|
|
106
|
+
imapWorkerQueueUrl: "http://queue/imap",
|
|
107
|
+
accountFinalizeQueueUrl: "http://queue/finalize",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const stops = sent.filter((m) => m.body.type === "IMAP_WORKER_STOP");
|
|
111
|
+
assert.equal(stops.length, 1, "one stop signal for the single account");
|
|
112
|
+
assert.equal(stops[0]?.queueUrl, "http://queue/imap");
|
|
113
|
+
assert.equal(stops[0]?.body.accountId, "acc-1");
|
|
114
|
+
assert.equal(
|
|
115
|
+
sent.filter((m) => m.body.type === "FinalizeAccountDelete").length,
|
|
116
|
+
1,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -98,22 +98,33 @@ const processAccountDelete = async (
|
|
|
98
98
|
.filter((e) => e.entityType === "Account")
|
|
99
99
|
.map((e) => e.key.accountId);
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
// IMAP_WORKER_STOP is a no-op acknowledgement — the account tombstone fence
|
|
102
|
+
// (isActive=false + deletedAt) is what actually halts the worker. Deployments
|
|
103
|
+
// that provision a dedicated imap-worker stop queue get the signal; the
|
|
104
|
+
// self-host stacks, which do not, skip it and let the fence do the work.
|
|
105
|
+
if (imapWorkerQueueUrl) {
|
|
106
|
+
for (const accountId of accountIds) {
|
|
107
|
+
await sqsFor(imapWorkerQueueUrl).send(
|
|
108
|
+
new SendMessageCommand({
|
|
109
|
+
QueueUrl: imapWorkerQueueUrl,
|
|
110
|
+
MessageBody: JSON.stringify({
|
|
111
|
+
type: "IMAP_WORKER_STOP",
|
|
112
|
+
accountConfigId,
|
|
113
|
+
accountId,
|
|
114
|
+
}),
|
|
109
115
|
}),
|
|
110
|
-
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
log.info(
|
|
119
|
+
{ accountConfigId, count: accountIds.length },
|
|
120
|
+
"Enqueued imap-worker stop signals",
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
log.info(
|
|
124
|
+
{ accountConfigId, count: accountIds.length },
|
|
125
|
+
"No imap-worker stop queue configured; skipping stop signals (tombstone fence halts the worker)",
|
|
111
126
|
);
|
|
112
127
|
}
|
|
113
|
-
log.info(
|
|
114
|
-
{ accountConfigId, count: accountIds.length },
|
|
115
|
-
"Enqueued imap-worker stop signals",
|
|
116
|
-
);
|
|
117
128
|
|
|
118
129
|
await signOut(userId, log);
|
|
119
130
|
|
package/src/poller.ts
CHANGED
|
@@ -9,11 +9,14 @@ import { fanoutHandler, finalizeHandler } from "./index.js";
|
|
|
9
9
|
* CI (see AGENTS.md worker roster notes). This is the standalone
|
|
10
10
|
* production entrypoint for the dedicated image.
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
13
|
-
* invalidation
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* The deployment-specific steps of the cascade — sign-out, content
|
|
13
|
+
* invalidation, storage cleanup, and the row cascade — resolve through the
|
|
14
|
+
* deletion capabilities seam (deletion-capabilities.ts): AWS runs Cognito
|
|
15
|
+
* global sign-out and CloudFront invalidation; the relational self-host
|
|
16
|
+
* backends run the no-op counterparts and a filesystem/Drizzle cascade. The
|
|
17
|
+
* imap-worker stop signal is a no-op acknowledgement whose queue is optional,
|
|
18
|
+
* skipped where it is not provisioned. Account deletion completes on every
|
|
19
|
+
* deployment flavor.
|
|
17
20
|
*/
|
|
18
21
|
const log = createLogger();
|
|
19
22
|
|