@remit/imap-worker 0.0.89 → 0.0.90

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.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/src/cli.ts +3 -9
  3. package/src/emit.test.ts +28 -10
  4. package/src/emit.ts +70 -72
  5. package/src/handlers/delete-account-objects.test.ts +11 -13
  6. package/src/handlers/delete-account-objects.ts +5 -3
  7. package/src/handlers/message-copy.sqlite.test.ts +3 -3
  8. package/src/handlers/message-copy.ts +5 -2
  9. package/src/handlers/message-delete.sqlite.test.ts +1 -1
  10. package/src/handlers/message-delete.ts +7 -4
  11. package/src/handlers/message-move.sqlite.test.ts +1 -1
  12. package/src/handlers/message-move.ts +9 -6
  13. package/src/handlers/placement-move-push.ts +9 -6
  14. package/src/handlers/placement-push.sqlite.test.ts +2 -1
  15. package/src/handlers/probe-message-id.sqlite.test.ts +1 -1
  16. package/src/handlers/sync-mailboxes.ts +8 -4
  17. package/src/handlers/sync-message-body.sqlite.test.ts +2 -2
  18. package/src/handlers/sync-message-body.ts +2 -1
  19. package/src/handlers/sync-messages.sqlite.test.ts +14 -2
  20. package/src/handlers/sync-messages.ts +20 -8
  21. package/src/index.ts +3 -7
  22. package/src/mail-sync-selection.sqlite.test.ts +4 -11
  23. package/src/pending-sync.sqlite.test.ts +9 -10
  24. package/src/processor.ts +10 -8
  25. package/src/queues.ts +14 -23
  26. package/src/scheduler/attachment-sweep.sqlite.test.ts +2 -3
  27. package/src/scheduler/handler.ts +31 -26
  28. package/src/scheduler/run-tick.sqlite.test.ts +1 -1
  29. package/src/scheduler/runner.ts +5 -10
  30. package/src/stranded-placement.sqlite.test.ts +5 -0
  31. package/src/test-helpers/test-world.ts +30 -32
  32. package/src/test-helpers/worker-queues.ts +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/imap-worker",
3
- "version": "0.0.89",
3
+ "version": "0.0.90",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
package/src/cli.ts CHANGED
@@ -6,12 +6,8 @@
6
6
  // in npm-scripts/docker-bundle.mjs). Do not route it through the logger — usage
7
7
  // text and argument errors are for the person who typed the command.
8
8
  import { parseArgs } from "node:util";
9
- import { emitEvent } from "./emit.js";
10
- import {
11
- setWorkerQueues,
12
- sqsWorkerQueues,
13
- workerQueueUrlsFromEnv,
14
- } from "./queues.js";
9
+ import { createEmitEvent } from "./emit.js";
10
+ import { sqsWorkerQueues, workerQueueUrlsFromEnv } from "./queues.js";
15
11
 
16
12
  const HELP = `
17
13
  remit-imap-worker - Process IMAP sync events
@@ -209,11 +205,9 @@ const buildEvent = () => {
209
205
 
210
206
  const event = buildEvent();
211
207
 
212
- setWorkerQueues(sqsWorkerQueues(workerQueueUrlsFromEnv()));
213
-
214
208
  console.log(`Enqueueing ${event.type} event for account ${event.accountId}...`);
215
209
 
216
- emitEvent(event)
210
+ createEmitEvent(sqsWorkerQueues(workerQueueUrlsFromEnv()))(event)
217
211
  .then(() => {
218
212
  console.log("Event enqueued successfully");
219
213
  process.exit(0);
package/src/emit.test.ts CHANGED
@@ -11,13 +11,18 @@ import {
11
11
  } from "@remit/data-ports/recording-queue";
12
12
  import { MailboxLockRepo } from "@remit/drizzle-service";
13
13
  import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
14
- import { emitEvent, SYNC_MESSAGES_PENDING } from "./emit.js";
14
+ import {
15
+ createEmitEvent,
16
+ type EmitEvent,
17
+ SYNC_MESSAGES_PENDING,
18
+ } from "./emit.js";
15
19
  import type {
16
20
  FlagPushEvent,
17
21
  SyncMailboxesEvent,
22
+ SyncMessageBodyEvent,
18
23
  SyncMessagesEvent,
19
24
  } from "./events.js";
20
- import { setWorkerQueues } from "./queues.js";
25
+ import { workerQueuesOn } from "./test-helpers/worker-queues.js";
21
26
 
22
27
  type Emitted<T> = Omit<T, "eventId" | "timestamp">;
23
28
 
@@ -27,16 +32,11 @@ let queue: RecordingQueue;
27
32
 
28
33
  const sentCommands = () => queue.sent;
29
34
 
35
+ const emitEvent: EmitEvent = (event) =>
36
+ createEmitEvent(workerQueuesOn(queue))(event);
37
+
30
38
  beforeEach(() => {
31
39
  queue = recordingQueue();
32
- setWorkerQueues({
33
- mailboxes: queue,
34
- messages: queue,
35
- flags: queue,
36
- body: queue,
37
- mailboxManagement: queue,
38
- messageManagement: queue,
39
- });
40
40
  const store = createShippedSqliteDb();
41
41
  closeStore = store.close;
42
42
  locks = new MailboxLockRepo(store.db);
@@ -125,3 +125,21 @@ describe("emitEvent on a FIFO queue", () => {
125
125
  assert.equal(sent.deduplicationId, sent.body.eventId);
126
126
  });
127
127
  });
128
+
129
+ describe("emitEvent on a standard queue", () => {
130
+ it("sends a body sync with no group or deduplication id", async () => {
131
+ const event: Emitted<SyncMessageBodyEvent> = {
132
+ type: "SYNC_MESSAGE_BODY",
133
+ accountId: "acc-1",
134
+ mailboxId: "mbx-1",
135
+ messageIds: ["msg-1"],
136
+ };
137
+
138
+ await emitEvent(event);
139
+
140
+ const [sent] = sentCommands();
141
+ if (!sent) throw new Error("expected a send");
142
+ assert.equal(sent.groupId, undefined);
143
+ assert.equal(sent.deduplicationId, undefined);
144
+ });
145
+ });
package/src/emit.ts CHANGED
@@ -1,32 +1,53 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getClient } from "@remit/backend/client";
3
- import type { FifoQueueProducer } from "@remit/data-ports";
3
+ import type { FifoQueueProducer, QueueProducer } from "@remit/data-ports";
4
4
  import { recordImapFailure } from "@remit/logger-lambda/metrics";
5
5
  import type { ImapEvent } from "./events.js";
6
- import { type WorkerQueues, workerQueues } from "./queues.js";
6
+ import type { WorkerQueues } from "./queues.js";
7
7
 
8
8
  type EventInput = Omit<ImapEvent, "eventId" | "timestamp">;
9
9
 
10
+ export type EmitEvent = (event: EventInput) => Promise<void>;
11
+
12
+ type Send = (event: ImapEvent) => Promise<void>;
13
+
14
+ // The deduplication id is the event's own id, so the queue suppresses a
15
+ // re-send of one event (the retry SQS's own idempotency guard is for) and
16
+ // nothing else. A shared id per account or per mailbox instead made the
17
+ // 5-minute window a rate limiter: the second sync of a mailbox within five
18
+ // minutes was discarded before any worker saw it, so mail that arrived after
19
+ // a sync could not be fetched until the window elapsed (issue #37).
20
+ const fifo =
21
+ (queue: FifoQueueProducer<ImapEvent>): Send =>
22
+ (event) =>
23
+ queue.send({
24
+ body: event,
25
+ groupId: event.accountId,
26
+ deduplicationId: event.eventId,
27
+ });
28
+
29
+ const standard =
30
+ (queue: QueueProducer<ImapEvent>): Send =>
31
+ (event) =>
32
+ queue.send({ body: event });
33
+
10
34
  export const SYNC_MESSAGES_PENDING = "SYNC_MESSAGES_PENDING";
11
35
 
12
36
  const SYNC_MESSAGES_PENDING_TTL_SECONDS = 300;
13
37
 
14
- const queueFor = (
15
- type: ImapEvent["type"],
16
- queues: WorkerQueues,
17
- ): FifoQueueProducer<ImapEvent> => {
38
+ const senderFor = (type: ImapEvent["type"], queues: WorkerQueues): Send => {
18
39
  switch (type) {
19
40
  case "SYNC_MAILBOXES":
20
- return queues.mailboxes;
41
+ return fifo(queues.mailboxes);
21
42
  case "SYNC_MESSAGES":
22
- return queues.messages;
43
+ return fifo(queues.messages);
23
44
  // SYNC_MESSAGE_BODY routes to the single standard body queue (#612).
24
45
  case "SYNC_MESSAGE_BODY":
25
- return queues.body;
46
+ return standard(queues.body);
26
47
  case "MAILBOX_CREATE":
27
48
  case "MAILBOX_RENAME":
28
49
  case "MAILBOX_DELETE":
29
- return queues.mailboxManagement;
50
+ return standard(queues.mailboxManagement);
30
51
  case "MESSAGE_DELETE":
31
52
  case "MESSAGE_MOVE":
32
53
  case "MESSAGE_COPY":
@@ -36,7 +57,7 @@ const queueFor = (
36
57
  // payload carries only our message id (never a UID, unlike the legacy
37
58
  // MESSAGE_MOVE event); per-event-type payload shape, same queue.
38
59
  case "PLACEMENT_MOVE_PUSH":
39
- return queues.messageManagement;
60
+ return standard(queues.messageManagement);
40
61
  // Rides the existing flags queue (issue #1273) — this is imap-worker's OWN
41
62
  // re-arm hint (the periodic per-mailbox sync tick catching up a marker
42
63
  // stuck `pending`), distinct from `FlagPushService`'s user-facing hint
@@ -44,74 +65,51 @@ const queueFor = (
44
65
  // already consumes; dispatch is by `type`, not by which queue delivered
45
66
  // the message.
46
67
  case "FLAG_PUSH":
47
- return queues.flags;
68
+ return fifo(queues.flags);
48
69
  }
49
70
  };
50
71
 
51
- export interface EmitEventOptions {
52
- /**
53
- * Delay delivery of the message by this many seconds (0-900).
54
- * Useful for retry backoff to avoid overwhelming IMAP servers.
55
- */
56
- delaySeconds?: number;
57
- }
58
-
59
- export const emitEvent = async (
60
- event: EventInput,
61
- options?: EmitEventOptions,
62
- ) => {
63
- const fullEvent: ImapEvent = {
64
- ...event,
65
- eventId: randomUUID(),
66
- timestamp: Date.now(),
67
- } as ImapEvent;
68
-
69
- const queue = queueFor(event.type, workerQueues());
70
-
71
- // The deduplication id is the event's own id, so the queue suppresses a
72
- // re-send of one event (the retry SQS's own idempotency guard is for) and
73
- // nothing else. A shared id per account or per mailbox instead made the
74
- // 5-minute window a rate limiter: the second sync of a mailbox within five
75
- // minutes was discarded before any worker saw it, so mail that arrived after
76
- // a sync could not be fetched until the window elapsed (issue #37).
77
- const send = () =>
78
- queue.send({
79
- body: fullEvent,
80
- groupId: event.accountId,
81
- deduplicationId: fullEvent.eventId,
82
- ...(options?.delaySeconds && { delaySeconds: options.delaySeconds }),
83
- });
84
- if (fullEvent.type !== "SYNC_MESSAGES") {
85
- await send();
86
- return;
87
- }
72
+ export const createEmitEvent =
73
+ (queues: WorkerQueues): EmitEvent =>
74
+ async (event) => {
75
+ const fullEvent: ImapEvent = {
76
+ ...event,
77
+ eventId: randomUUID(),
78
+ timestamp: Date.now(),
79
+ } as ImapEvent;
88
80
 
89
- const { mailboxLock } = await getClient();
90
- const { accountId, mailboxId, eventId } = fullEvent;
91
- const previous = await mailboxLock.get(
92
- accountId,
93
- mailboxId,
94
- SYNC_MESSAGES_PENDING,
95
- );
96
- const acquired = await mailboxLock.tryAcquireLock(
97
- mailboxId,
98
- SYNC_MESSAGES_PENDING,
99
- accountId,
100
- eventId,
101
- SYNC_MESSAGES_PENDING_TTL_SECONDS,
102
- );
103
- if (!acquired) return;
104
- if (previous && previous.ttl < Math.floor(Date.now() / 1000)) {
105
- recordImapFailure("SYNC_PENDING_EXPIRED", "other");
106
- }
81
+ const send = () => senderFor(fullEvent.type, queues)(fullEvent);
82
+ if (fullEvent.type !== "SYNC_MESSAGES") {
83
+ await send();
84
+ return;
85
+ }
107
86
 
108
- await send().catch(async (error: unknown) => {
109
- await mailboxLock.releaseLock(
87
+ const { mailboxLock } = await getClient();
88
+ const { accountId, mailboxId, eventId } = fullEvent;
89
+ const previous = await mailboxLock.get(
110
90
  accountId,
111
91
  mailboxId,
112
92
  SYNC_MESSAGES_PENDING,
93
+ );
94
+ const acquired = await mailboxLock.tryAcquireLock(
95
+ mailboxId,
96
+ SYNC_MESSAGES_PENDING,
97
+ accountId,
113
98
  eventId,
99
+ SYNC_MESSAGES_PENDING_TTL_SECONDS,
114
100
  );
115
- throw error;
116
- });
117
- };
101
+ if (!acquired) return;
102
+ if (previous && previous.ttl < Math.floor(Date.now() / 1000)) {
103
+ recordImapFailure("SYNC_PENDING_EXPIRED", "other");
104
+ }
105
+
106
+ await send().catch(async (error: unknown) => {
107
+ await mailboxLock.releaseLock(
108
+ accountId,
109
+ mailboxId,
110
+ SYNC_MESSAGES_PENDING,
111
+ eventId,
112
+ );
113
+ throw error;
114
+ });
115
+ };
@@ -12,7 +12,7 @@ import {
12
12
  import { noopLogger } from "@remit/logger";
13
13
  import { mockClient } from "aws-sdk-client-mock";
14
14
  import type { DeleteAccountObjectsEvent } from "../events.js";
15
- import { setWorkerQueues } from "../queues.js";
15
+ import { workerQueuesOn } from "../test-helpers/worker-queues.js";
16
16
  import { handleDeleteAccountObjects } from "./delete-account-objects.js";
17
17
 
18
18
  const s3Mock = mockClient(S3Client);
@@ -26,14 +26,6 @@ const event: DeleteAccountObjectsEvent = {
26
26
  describe("handleDeleteAccountObjects", () => {
27
27
  beforeEach(() => {
28
28
  queue = recordingQueue();
29
- setWorkerQueues({
30
- mailboxes: queue,
31
- messages: queue,
32
- flags: queue,
33
- body: queue,
34
- mailboxManagement: queue,
35
- messageManagement: queue,
36
- });
37
29
  });
38
30
 
39
31
  afterEach(() => {
@@ -50,7 +42,7 @@ describe("handleDeleteAccountObjects", () => {
50
42
  });
51
43
  s3Mock.on(DeleteObjectsCommand).resolves({});
52
44
 
53
- await handleDeleteAccountObjects(event, noopLogger);
45
+ await handleDeleteAccountObjects(event, noopLogger, workerQueuesOn(queue));
54
46
 
55
47
  const listCall = s3Mock.commandCalls(ListObjectsV2Command)[0];
56
48
  assert.equal(listCall.args[0].input.Prefix, "accounts/cfg-1/");
@@ -77,7 +69,7 @@ describe("handleDeleteAccountObjects", () => {
77
69
  });
78
70
  s3Mock.on(DeleteObjectsCommand).resolves({});
79
71
 
80
- await handleDeleteAccountObjects(event, noopLogger);
72
+ await handleDeleteAccountObjects(event, noopLogger, workerQueuesOn(queue));
81
73
 
82
74
  const listCalls = s3Mock.commandCalls(ListObjectsV2Command);
83
75
  assert.equal(listCalls.length, 2);
@@ -88,7 +80,7 @@ describe("handleDeleteAccountObjects", () => {
88
80
  it("skips the delete call when a page holds no keys", async () => {
89
81
  s3Mock.on(ListObjectsV2Command).resolves({ IsTruncated: false });
90
82
 
91
- await handleDeleteAccountObjects(event, noopLogger);
83
+ await handleDeleteAccountObjects(event, noopLogger, workerQueuesOn(queue));
92
84
 
93
85
  assert.equal(s3Mock.commandCalls(DeleteObjectsCommand).length, 0);
94
86
  });
@@ -99,6 +91,7 @@ describe("handleDeleteAccountObjects", () => {
99
91
  await handleDeleteAccountObjects(
100
92
  { ...event, continuationToken: "tok-mid" },
101
93
  noopLogger,
94
+ workerQueuesOn(queue),
102
95
  () => 5_000,
103
96
  );
104
97
 
@@ -120,7 +113,12 @@ describe("handleDeleteAccountObjects", () => {
120
113
  });
121
114
  s3Mock.on(DeleteObjectsCommand).resolves({});
122
115
 
123
- await handleDeleteAccountObjects(event, noopLogger, () => 120_000);
116
+ await handleDeleteAccountObjects(
117
+ event,
118
+ noopLogger,
119
+ workerQueuesOn(queue),
120
+ () => 120_000,
121
+ );
124
122
 
125
123
  assert.equal(s3Mock.commandCalls(ListObjectsV2Command).length, 1);
126
124
  assert.equal(queue.sent.length, 0);
@@ -6,7 +6,7 @@ import {
6
6
  import type { Logger } from "@remit/logger";
7
7
  import { env } from "expect-env";
8
8
  import type { DeleteAccountObjectsEvent } from "../events.js";
9
- import { workerQueues } from "../queues.js";
9
+ import type { WorkerQueues } from "../queues.js";
10
10
 
11
11
  const s3 = new S3Client({});
12
12
 
@@ -16,6 +16,7 @@ const MIN_REMAINING_MS = 30_000;
16
16
  export const handleDeleteAccountObjects = async (
17
17
  event: DeleteAccountObjectsEvent,
18
18
  log: Logger,
19
+ queues: WorkerQueues,
19
20
  getRemainingTimeMs?: () => number,
20
21
  ): Promise<void> => {
21
22
  const BUCKET_NAME = env.S3_BUCKET_NAME;
@@ -37,7 +38,7 @@ export const handleDeleteAccountObjects = async (
37
38
  { accountConfigId, totalDeleted, continuationToken: currentToken },
38
39
  "Near timeout, re-enqueuing with continuation token",
39
40
  );
40
- await reenqueue(accountConfigId, currentToken);
41
+ await reenqueue(queues, accountConfigId, currentToken);
41
42
  return;
42
43
  }
43
44
 
@@ -81,6 +82,7 @@ export const handleDeleteAccountObjects = async (
81
82
  };
82
83
 
83
84
  const reenqueue = async (
85
+ queues: WorkerQueues,
84
86
  accountConfigId: string,
85
87
  continuationToken: string | undefined,
86
88
  ): Promise<void> => {
@@ -90,5 +92,5 @@ const reenqueue = async (
90
92
  continuationToken,
91
93
  };
92
94
 
93
- await workerQueues().messageManagement.send({ body: event });
95
+ await queues.messageManagement.send({ body: event });
94
96
  };
@@ -73,7 +73,7 @@ const arrange = async (
73
73
  };
74
74
 
75
75
  const run = (a: Arranged, receiveCount = 1) =>
76
- handleMessageCopy(a.event, a.w.log, receiveCount, a.w.scope);
76
+ handleMessageCopy(a.event, a.w.log, a.w.queues, receiveCount, a.w.scope);
77
77
 
78
78
  describe("a message copy", () => {
79
79
  it("lands at the destination, leaves the source alone and settles the copy row", async () => {
@@ -264,7 +264,7 @@ describe("a fresh copy acted on before the server confirms it", () => {
264
264
  assert.ok(event, "the move enqueued a MESSAGE_MOVE");
265
265
  assert.equal(event.uid, copyUid);
266
266
 
267
- await handleMessageMove(event, a.w.log, 1, a.w.scope);
267
+ await handleMessageMove(event, a.w.log, a.w.queues, 1, a.w.scope);
268
268
 
269
269
  assert.deepEqual(a.w.imap.uids("Projects", a.messageIdHeader), []);
270
270
  assert.equal(a.w.imap.uids("INBOX", a.messageIdHeader).length, 2);
@@ -288,7 +288,7 @@ describe("a fresh copy acted on before the server confirms it", () => {
288
288
  const event = a.w.queued().find(isDelete);
289
289
  assert.ok(event, "the delete enqueued a MESSAGE_DELETE");
290
290
 
291
- await handleMessageDelete(event, a.w.log, 1, a.w.scope);
291
+ await handleMessageDelete(event, a.w.log, a.w.queues, 1, a.w.scope);
292
292
 
293
293
  assert.deepEqual(a.w.imap.uids("Projects", a.messageIdHeader), []);
294
294
  assert.deepEqual(a.w.imap.uids("INBOX"), [a.uid]);
@@ -14,9 +14,10 @@ import {
14
14
  import { attemptBudget } from "@remit/sqs-client/attempt-budget";
15
15
  import { isAccountDeleted } from "../account-check.js";
16
16
  import { createConnectionScopeWithCredentials } from "../connection-scope.js";
17
- import { emitEvent } from "../emit.js";
17
+ import { createEmitEvent } from "../emit.js";
18
18
  import type { MessageCopyEvent } from "../events.js";
19
19
  import { findMailboxRow } from "../mailbox-row.js";
20
+ import type { WorkerQueues } from "../queues.js";
20
21
  import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
21
22
  import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
22
23
  import {
@@ -68,9 +69,11 @@ export type CopyProbe =
68
69
  export const handleMessageCopy = async (
69
70
  event: MessageCopyEvent,
70
71
  log: Logger,
72
+ queues: WorkerQueues,
71
73
  receiveCount = 1,
72
74
  createConnectionScope = createConnectionScopeWithCredentials,
73
75
  ): Promise<void> => {
76
+ const emit = createEmitEvent(queues);
74
77
  const {
75
78
  account: accountService,
76
79
  message: messageService,
@@ -383,7 +386,7 @@ export const handleMessageCopy = async (
383
386
  }
384
387
  }
385
388
 
386
- await emitMoveResync(emitEvent, {
389
+ await emitMoveResync(emit, {
387
390
  accountId,
388
391
  sourceMailboxId,
389
392
  destinationMailboxId,
@@ -72,7 +72,7 @@ const arrange = async (
72
72
  };
73
73
 
74
74
  const run = (a: Arranged, receiveCount = 1, event = a.event) =>
75
- handleMessageDelete(event, a.w.log, receiveCount, a.w.scope);
75
+ handleMessageDelete(event, a.w.log, a.w.queues, receiveCount, a.w.scope);
76
76
 
77
77
  const resyncs = (w: World): unknown[] =>
78
78
  w
@@ -24,9 +24,10 @@ import {
24
24
  import { attemptBudget } from "@remit/sqs-client/attempt-budget";
25
25
  import { isAccountDeleted } from "../account-check.js";
26
26
  import { createConnectionScopeWithCredentials } from "../connection-scope.js";
27
- import { emitEvent } from "../emit.js";
27
+ import { createEmitEvent } from "../emit.js";
28
28
  import type { MessageDeleteEvent } from "../events.js";
29
29
  import { findMailboxRow } from "../mailbox-row.js";
30
+ import type { WorkerQueues } from "../queues.js";
30
31
  import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
31
32
  import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
32
33
  import { resolveExhaustedMessageDeleteFailure } from "./message-delete-terminal.js";
@@ -153,9 +154,11 @@ const confirmTrashMoveUid = async (
153
154
  export const handleMessageDelete = async (
154
155
  event: MessageDeleteEvent,
155
156
  log: Logger,
157
+ queues: WorkerQueues,
156
158
  receiveCount = 1,
157
159
  createConnectionScope = createConnectionScopeWithCredentials,
158
160
  ): Promise<void> => {
161
+ const emit = createEmitEvent(queues);
159
162
  const {
160
163
  account: accountService,
161
164
  message: messageService,
@@ -246,14 +249,14 @@ export const handleMessageDelete = async (
246
249
  */
247
250
  const emitSettleResync = async (): Promise<void> => {
248
251
  if (destinationMailboxId) {
249
- await emitMoveResync(emitEvent, {
252
+ await emitMoveResync(emit, {
250
253
  accountId,
251
254
  sourceMailboxId: mailboxId,
252
255
  destinationMailboxId,
253
256
  });
254
257
  return;
255
258
  }
256
- await emitMailboxResync(emitEvent, { accountId, mailboxId });
259
+ await emitMailboxResync(emit, { accountId, mailboxId });
257
260
  };
258
261
 
259
262
  /**
@@ -623,7 +626,7 @@ export const handleMessageDelete = async (
623
626
  "Refused to delete: the destination mailbox no longer exists (deleted)",
624
627
  "message_delete_destination_gone",
625
628
  );
626
- await emitMailboxResync(emitEvent, { accountId, mailboxId });
629
+ await emitMailboxResync(emit, { accountId, mailboxId });
627
630
  return;
628
631
  }
629
632
 
@@ -68,7 +68,7 @@ const arrange = async (
68
68
  };
69
69
 
70
70
  const run = (a: Arranged, receiveCount = 1) =>
71
- handleMessageMove(a.event, a.w.log, receiveCount, a.w.scope);
71
+ handleMessageMove(a.event, a.w.log, a.w.queues, receiveCount, a.w.scope);
72
72
 
73
73
  const resyncs = (w: World): unknown[] =>
74
74
  w
@@ -17,9 +17,10 @@ import {
17
17
  import { attemptBudget } from "@remit/sqs-client/attempt-budget";
18
18
  import { isAccountDeleted } from "../account-check.js";
19
19
  import { createConnectionScopeWithCredentials } from "../connection-scope.js";
20
- import { emitEvent } from "../emit.js";
20
+ import { createEmitEvent } from "../emit.js";
21
21
  import type { MessageMoveEvent, SyncMessagesEvent } from "../events.js";
22
22
  import { findMailboxRow } from "../mailbox-row.js";
23
+ import type { WorkerQueues } from "../queues.js";
23
24
  import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
24
25
  import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
25
26
  import { resolveExhaustedMessageMoveFailure } from "./message-move-terminal.js";
@@ -244,9 +245,11 @@ export const buildThreadMessageMoveUpdate = (
244
245
  export const handleMessageMove = async (
245
246
  event: MessageMoveEvent,
246
247
  log: Logger,
248
+ queues: WorkerQueues,
247
249
  receiveCount = 1,
248
250
  createConnectionScope = createConnectionScopeWithCredentials,
249
251
  ): Promise<void> => {
252
+ const emit = createEmitEvent(queues);
250
253
  const {
251
254
  account: accountService,
252
255
  message: messageService,
@@ -343,7 +346,7 @@ export const handleMessageMove = async (
343
346
  },
344
347
  "MESSAGE_MOVE not pushed: source mailbox no longer exists (deleted); stale row reconciled",
345
348
  );
346
- await emitMailboxResync(emitEvent, {
349
+ await emitMailboxResync(emit, {
347
350
  accountId,
348
351
  mailboxId: destinationMailboxId,
349
352
  });
@@ -449,7 +452,7 @@ export const handleMessageMove = async (
449
452
  { metric: "message_move_destination_gone" },
450
453
  "destination mailbox no longer exists (deleted)",
451
454
  );
452
- await emitMailboxResync(emitEvent, {
455
+ await emitMailboxResync(emit, {
453
456
  accountId,
454
457
  mailboxId: sourceMailboxId,
455
458
  });
@@ -543,7 +546,7 @@ export const handleMessageMove = async (
543
546
  );
544
547
  }
545
548
 
546
- await emitMoveResync(emitEvent, {
549
+ await emitMoveResync(emit, {
547
550
  accountId,
548
551
  sourceMailboxId,
549
552
  destinationMailboxId,
@@ -624,7 +627,7 @@ export const handleMessageMove = async (
624
627
  await settleMoved(newUid);
625
628
  },
626
629
  () =>
627
- emitMoveResync(emitEvent, {
630
+ emitMoveResync(emit, {
628
631
  accountId,
629
632
  sourceMailboxId,
630
633
  destinationMailboxId,
@@ -739,7 +742,7 @@ export const handleMessageMove = async (
739
742
  // re-projects it with the server's own UID. BROKEN, the row has just
740
743
  // been put back at the source the server confirmed, and the resync
741
744
  // is what carries any drift either folder has picked up since.
742
- await emitMoveResync(emitEvent, {
745
+ await emitMoveResync(emit, {
743
746
  accountId,
744
747
  sourceMailboxId,
745
748
  destinationMailboxId,
@@ -14,9 +14,10 @@ import {
14
14
  import { attemptBudget } from "@remit/sqs-client/attempt-budget";
15
15
  import { isAccountDeleted } from "../account-check.js";
16
16
  import { createConnectionScopeWithCredentials } from "../connection-scope.js";
17
- import { emitEvent } from "../emit.js";
17
+ import { createEmitEvent } from "../emit.js";
18
18
  import type { PlacementMovePushEvent } from "../events.js";
19
19
  import { findMailboxRow } from "../mailbox-row.js";
20
+ import type { WorkerQueues } from "../queues.js";
20
21
  import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
21
22
  import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
22
23
  import {
@@ -152,9 +153,11 @@ export const attemptMove = async (
152
153
  export const handlePlacementMovePush = async (
153
154
  event: PlacementMovePushEvent,
154
155
  log: Logger,
156
+ queues: WorkerQueues,
155
157
  receiveCount = 1,
156
158
  createConnectionScope = createConnectionScopeWithCredentials,
157
159
  ): Promise<void> => {
160
+ const emit = createEmitEvent(queues);
158
161
  const {
159
162
  account: accountService,
160
163
  mailbox: mailboxService,
@@ -238,7 +241,7 @@ export const handlePlacementMovePush = async (
238
241
  "Placement move not pushed: its source folder is gone; the stale row was reconciled",
239
242
  );
240
243
  if (destination.kind === "present") {
241
- await emitMailboxResync(emitEvent, {
244
+ await emitMailboxResync(emit, {
242
245
  accountId,
243
246
  mailboxId: marker.destinationMailboxId,
244
247
  });
@@ -261,7 +264,7 @@ export const handlePlacementMovePush = async (
261
264
  context,
262
265
  "Placement move not pushed: its destination folder is gone; the row was handed back to its source",
263
266
  );
264
- await emitMailboxResync(emitEvent, {
267
+ await emitMailboxResync(emit, {
265
268
  accountId,
266
269
  mailboxId: marker.sourceMailboxId,
267
270
  });
@@ -354,7 +357,7 @@ export const handlePlacementMovePush = async (
354
357
  },
355
358
  "Message no longer at its pending-move source (external delete or moved away); marker dropped, stale row reconciled",
356
359
  );
357
- await emitMoveResync(emitEvent, {
360
+ await emitMoveResync(emit, {
358
361
  accountId,
359
362
  sourceMailboxId: marker.sourceMailboxId,
360
363
  destinationMailboxId: marker.destinationMailboxId,
@@ -404,7 +407,7 @@ export const handlePlacementMovePush = async (
404
407
  "Placement move confirmed on IMAP; marker cleared",
405
408
  );
406
409
 
407
- await emitMoveResync(emitEvent, {
410
+ await emitMoveResync(emit, {
408
411
  accountId,
409
412
  sourceMailboxId: marker.sourceMailboxId,
410
413
  destinationMailboxId: marker.destinationMailboxId,
@@ -452,7 +455,7 @@ export const handlePlacementMovePush = async (
452
455
  );
453
456
 
454
457
  if (outcome === "reconciled") {
455
- await emitMoveResync(emitEvent, {
458
+ await emitMoveResync(emit, {
456
459
  accountId,
457
460
  sourceMailboxId: marker.sourceMailboxId,
458
461
  destinationMailboxId: marker.destinationMailboxId,
@@ -92,7 +92,8 @@ const pushMove = (
92
92
  a: Arranged,
93
93
  event: PlacementMovePushEvent,
94
94
  receiveCount = 1,
95
- ) => handlePlacementMovePush(event, a.w.log, receiveCount, a.w.scope);
95
+ ) =>
96
+ handlePlacementMovePush(event, a.w.log, a.w.queues, receiveCount, a.w.scope);
96
97
 
97
98
  const seenOnServer = (a: Arranged, path: string): boolean[] =>
98
99
  a.w.imap