@remit/imap-worker 0.0.84 → 0.0.86

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/imap-worker",
3
- "version": "0.0.84",
3
+ "version": "0.0.86",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
package/src/emit.test.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import assert from "node:assert/strict";
2
- import { before, beforeEach, describe, it } from "node:test";
2
+ import { afterEach, before, beforeEach, describe, it } from "node:test";
3
3
  import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
4
+ import {
5
+ _resetForTest,
6
+ type RemitClient,
7
+ setClient,
8
+ } from "@remit/backend/client";
9
+ import { MailboxLockRepo } from "@remit/drizzle-service";
10
+ import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
4
11
  import { mockClient } from "aws-sdk-client-mock";
5
- import type { emitEvent as EmitEvent } from "./emit.js";
12
+ import type * as Emit from "./emit.js";
6
13
  import type {
7
14
  FlagPushEvent,
8
15
  SyncMailboxesEvent,
@@ -20,7 +27,10 @@ const fifo = (name: string) =>
20
27
 
21
28
  const sqsMock = mockClient(SQSClient);
22
29
 
23
- let emitEvent: typeof EmitEvent;
30
+ let emitEvent: typeof Emit.emitEvent;
31
+ let SYNC_MESSAGES_PENDING: typeof Emit.SYNC_MESSAGES_PENDING;
32
+ let locks: MailboxLockRepo;
33
+ let closeStore: () => void;
24
34
 
25
35
  const sentCommands = (): SendMessageCommand["input"][] =>
26
36
  sqsMock.commandCalls(SendMessageCommand).map((call) => call.args[0].input);
@@ -32,11 +42,21 @@ before(async () => {
32
42
  process.env.SQS_QUEUE_URL_MAILBOXES = fifo("mailboxes");
33
43
  process.env.SQS_QUEUE_URL_MESSAGES = fifo("messages");
34
44
  process.env.SQS_QUEUE_URL_FLAGS = fifo("flags");
35
- ({ emitEvent } = await import("./emit.js"));
45
+ ({ emitEvent, SYNC_MESSAGES_PENDING } = await import("./emit.js"));
36
46
  });
37
47
 
38
48
  beforeEach(() => {
39
49
  sqsMock.reset();
50
+ sqsMock.on(SendMessageCommand).resolves({});
51
+ const store = createShippedSqliteDb();
52
+ closeStore = store.close;
53
+ locks = new MailboxLockRepo(store.db);
54
+ setClient({ mailboxLock: locks } as unknown as RemitClient);
55
+ });
56
+
57
+ afterEach(() => {
58
+ _resetForTest();
59
+ closeStore();
40
60
  });
41
61
 
42
62
  describe("emitEvent on a FIFO queue", () => {
@@ -68,6 +88,14 @@ describe("emitEvent on a FIFO queue", () => {
68
88
  };
69
89
 
70
90
  await emitEvent(event);
91
+ const [first] = sentCommands();
92
+ if (!first) throw new Error("expected a send");
93
+ await locks.releaseLock(
94
+ "acc-1",
95
+ "mbx-1",
96
+ SYNC_MESSAGES_PENDING,
97
+ String(bodyOf(first).eventId),
98
+ );
71
99
  await emitEvent(event);
72
100
 
73
101
  const sent = sentCommands();
package/src/emit.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { SendMessageCommand } from "@aws-sdk/client-sqs";
3
+ import { getClient } from "@remit/backend/client";
4
+ import { recordImapFailure } from "@remit/logger-lambda";
3
5
  import {
4
6
  createQueueProducer,
5
7
  isLocalEndpoint,
@@ -9,6 +11,10 @@ import type { ImapEvent } from "./events.js";
9
11
 
10
12
  type EventInput = Omit<ImapEvent, "eventId" | "timestamp">;
11
13
 
14
+ export const SYNC_MESSAGES_PENDING = "SYNC_MESSAGES_PENDING";
15
+
16
+ const SYNC_MESSAGES_PENDING_TTL_SECONDS = 300;
17
+
12
18
  const mailboxesQueueUrl = env.SQS_QUEUE_URL_MAILBOXES;
13
19
  const messagesQueueUrl = env.SQS_QUEUE_URL_MESSAGES;
14
20
  // SYNC_MESSAGE_BODY events route to the single standard body queue (#612). It is
@@ -77,29 +83,58 @@ export const emitEvent = async (
77
83
  // ElasticMQ FIFO queues don't support per-message DelaySeconds
78
84
  const useDelay = options?.delaySeconds && !isLocal;
79
85
 
80
- await sqs.send(
81
- new SendMessageCommand({
82
- QueueUrl: queueUrl,
83
- MessageBody: JSON.stringify(fullEvent),
84
- // Delay delivery for retry backoff (skip for local ElasticMQ)
85
- ...(useDelay && { DelaySeconds: options.delaySeconds }),
86
- // FIFO queue parameters — only set if the queue is FIFO. The
87
- // deduplication id is the event's own id, so the queue suppresses a
88
- // re-send of one event (the retry SQS's own idempotency guard is for)
89
- // and nothing else. A shared id per account or per mailbox instead made
90
- // the 5-minute window a rate limiter: the second sync of a mailbox
91
- // within five minutes was discarded before any worker saw it, so mail
92
- // that arrived after a sync could not be fetched until the window
93
- // elapsed (issue #37).
94
- //
95
- // Dropping events is not how repeated work is bounded — that is the
96
- // freshness gate in the sync-mailboxes fan-out, which decides whether
97
- // a mailbox is worth enumerating at all. MessageGroupId only orders an
98
- // account's events; it makes duplicates serial, not cheap.
99
- ...(useFifo && {
100
- MessageGroupId: event.accountId,
101
- MessageDeduplicationId: fullEvent.eventId,
86
+ const send = () =>
87
+ sqs.send(
88
+ new SendMessageCommand({
89
+ QueueUrl: queueUrl,
90
+ MessageBody: JSON.stringify(fullEvent),
91
+ // Delay delivery for retry backoff (skip for local ElasticMQ)
92
+ ...(useDelay && { DelaySeconds: options.delaySeconds }),
93
+ // FIFO queue parameters — only set if the queue is FIFO. The
94
+ // deduplication id is the event's own id, so the queue suppresses a
95
+ // re-send of one event (the retry SQS's own idempotency guard is for)
96
+ // and nothing else. A shared id per account or per mailbox instead made
97
+ // the 5-minute window a rate limiter: the second sync of a mailbox
98
+ // within five minutes was discarded before any worker saw it, so mail
99
+ // that arrived after a sync could not be fetched until the window
100
+ // elapsed (issue #37).
101
+ ...(useFifo && {
102
+ MessageGroupId: event.accountId,
103
+ MessageDeduplicationId: fullEvent.eventId,
104
+ }),
102
105
  }),
103
- }),
106
+ );
107
+ if (fullEvent.type !== "SYNC_MESSAGES") {
108
+ await send();
109
+ return;
110
+ }
111
+
112
+ const { mailboxLock } = await getClient();
113
+ const { accountId, mailboxId, eventId } = fullEvent;
114
+ const previous = await mailboxLock.get(
115
+ accountId,
116
+ mailboxId,
117
+ SYNC_MESSAGES_PENDING,
104
118
  );
119
+ const acquired = await mailboxLock.tryAcquireLock(
120
+ mailboxId,
121
+ SYNC_MESSAGES_PENDING,
122
+ accountId,
123
+ eventId,
124
+ SYNC_MESSAGES_PENDING_TTL_SECONDS,
125
+ );
126
+ if (!acquired) return;
127
+ if (previous && previous.ttl < Math.floor(Date.now() / 1000)) {
128
+ recordImapFailure("SYNC_PENDING_EXPIRED", "other");
129
+ }
130
+
131
+ await send().catch(async (error: unknown) => {
132
+ await mailboxLock.releaseLock(
133
+ accountId,
134
+ mailboxId,
135
+ SYNC_MESSAGES_PENDING,
136
+ eventId,
137
+ );
138
+ throw error;
139
+ });
105
140
  };
@@ -0,0 +1,63 @@
1
+ import assert from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import type { MailboxItem } from "@remit/data-ports";
4
+ import type { CreateMailboxQueueInput } from "@remit/mailbox-service";
5
+ import { createImportedFolder } from "./imported-folder.js";
6
+
7
+ const inbox = {
8
+ mailboxId: "mbx-inbox",
9
+ accountId: "acc-1",
10
+ fullPath: "INBOX",
11
+ namespaceType: "personal",
12
+ namespacePrefix: "INBOX.",
13
+ hierarchyDelimiter: ".",
14
+ } as MailboxItem;
15
+
16
+ describe("createImportedFolder (#1103)", () => {
17
+ it("creates the folder through the pending folder-create path, in the INBOX namespace", async () => {
18
+ const calls: [CreateMailboxQueueInput, string, boolean | undefined][] = [];
19
+ const create = createImportedFolder(
20
+ { findByPath: async () => inbox },
21
+ {
22
+ createMailbox: async (input, accountId, subscribe) => {
23
+ calls.push([input, accountId, subscribe]);
24
+ return {
25
+ outcome: "Created" as const,
26
+ mailbox: {
27
+ ...inbox,
28
+ mailboxId: "mbx-new",
29
+ fullPath: input.fullPath,
30
+ },
31
+ };
32
+ },
33
+ },
34
+ );
35
+
36
+ const created = await create("acc-1", "INBOX.Facturen");
37
+
38
+ assert.equal(
39
+ created.outcome === "Created" && created.mailbox.mailboxId,
40
+ "mbx-new",
41
+ );
42
+ assert.equal(calls.length, 1);
43
+ const [input, accountId, subscribe] = calls[0];
44
+ assert.equal(accountId, "acc-1");
45
+ assert.equal(subscribe, true);
46
+ assert.equal(input.fullPath, "INBOX.Facturen");
47
+ assert.equal(input.namespacePrefix, "INBOX.");
48
+ assert.equal(input.hierarchyDelimiter, ".");
49
+ });
50
+
51
+ it("refuses to guess a namespace for an account with no INBOX", async () => {
52
+ const create = createImportedFolder(
53
+ { findByPath: async () => null },
54
+ {
55
+ createMailbox: async () => {
56
+ throw new Error("must not be called");
57
+ },
58
+ },
59
+ );
60
+
61
+ await assert.rejects(create("acc-1", "Facturen"), /no INBOX/);
62
+ });
63
+ });
@@ -0,0 +1,38 @@
1
+ import type { CreateImportedFolder } from "@remit/config-transfer";
2
+ import type { IMailboxRepository } from "@remit/data-ports";
3
+ import type { MailboxQueueService } from "@remit/mailbox-service";
4
+
5
+ export const createImportedFolder =
6
+ (
7
+ mailbox: Pick<IMailboxRepository, "findByPath">,
8
+ queue: Pick<MailboxQueueService, "createMailbox">,
9
+ ): CreateImportedFolder =>
10
+ async (accountId, folderPath) => {
11
+ const inbox = await mailbox.findByPath(accountId, "INBOX");
12
+ if (!inbox) {
13
+ throw new Error(
14
+ `Account ${accountId} has no INBOX to take a namespace from, so "${folderPath}" cannot be created`,
15
+ );
16
+ }
17
+ return queue.createMailbox(
18
+ {
19
+ accountId,
20
+ namespaceType: inbox.namespaceType,
21
+ namespacePrefix: inbox.namespacePrefix,
22
+ hierarchyDelimiter: inbox.hierarchyDelimiter,
23
+ fullPath: folderPath,
24
+ uidValidity: 0,
25
+ uidNext: 1,
26
+ highestModseq: "0",
27
+ messageCount: 0,
28
+ unseenCount: 0,
29
+ deletedCount: 0,
30
+ totalSize: 0,
31
+ lastSyncUid: 0,
32
+ highWaterMarkUid: 0,
33
+ lastMessageSyncAt: 0,
34
+ },
35
+ accountId,
36
+ true,
37
+ );
38
+ };
@@ -3,6 +3,7 @@ import { writeFolderRoleAppointment } from "@remit/backend/folder-role-appointme
3
3
  import {
4
4
  bindImportedFolders,
5
5
  type ConfigBinderDeps,
6
+ disableFiltersMissingFolders,
6
7
  } from "@remit/config-transfer";
7
8
  import type {
8
9
  AccountItem,
@@ -16,10 +17,12 @@ import type { Logger } from "@remit/logger-lambda";
16
17
  import { RefreshTokenError } from "@remit/mail-oauth-service";
17
18
  import {
18
19
  createConnectionWithCredentials,
20
+ MailboxQueueService,
19
21
  MailboxSyncService,
20
22
  MailConnectionError,
21
23
  type MailCredentials,
22
24
  } from "@remit/mailbox-service";
25
+ import { env } from "expect-env";
23
26
  import pMap from "p-map";
24
27
  import {
25
28
  isAccountDeleted,
@@ -30,6 +33,7 @@ import { emitEvent } from "../emit.js";
30
33
  import type { SyncMailboxesEvent, SyncMessagesEvent } from "../events.js";
31
34
  import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
32
35
  import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
36
+ import { createImportedFolder } from "./imported-folder.js";
33
37
  import { orderMailboxesForSync } from "./mailbox-sync-order.js";
34
38
 
35
39
  const EVENT_EMIT_CONCURRENCY = 20;
@@ -110,6 +114,14 @@ export const syncMailboxes = async (
110
114
  mailboxId,
111
115
  lastKnownPath,
112
116
  ),
117
+ createFolder: createImportedFolder(
118
+ mailboxService,
119
+ new MailboxQueueService({
120
+ mailboxService,
121
+ sqsQueueUrl: env.SQS_QUEUE_URL_MAILBOX_MGMT,
122
+ logger: log,
123
+ }),
124
+ ),
113
125
  };
114
126
 
115
127
  const { accountId } = event;
@@ -233,10 +245,21 @@ const syncMailboxesForAccount = async (
233
245
  account.accountConfigId,
234
246
  accountId,
235
247
  );
236
- if (bound.bound > 0 || bound.dropped > 0 || bound.stillPending > 0) {
248
+ if (Object.values(bound).some((count) => count > 0)) {
237
249
  log.info({ accountId, ...bound }, "Bound imported folder references");
238
250
  }
239
251
 
252
+ const disabled = await disableFiltersMissingFolders(
253
+ binder.repositories,
254
+ account.accountConfigId,
255
+ );
256
+ if (disabled > 0) {
257
+ log.warn(
258
+ { accountId, disabled },
259
+ "Disabled filters whose move folder no longer exists",
260
+ );
261
+ }
262
+
240
263
  const allMailboxes = await collectAllMailboxes(accountId, mailboxService);
241
264
 
242
265
  // This fan-out is where an account's IMAP work is decided: one SEARCH per
@@ -287,6 +287,7 @@ const buildSyncDeps = (opts: {
287
287
  mailbox: {
288
288
  get: opts.mailboxGet,
289
289
  },
290
+ mailboxLock: { releaseLock: async () => {} },
290
291
  secrets: {},
291
292
  }),
292
293
  buildLifecycleDeps: () => ({}),
@@ -425,6 +426,7 @@ const buildRunningSyncDeps = (
425
426
  },
426
427
  },
427
428
  mailboxLock: {
429
+ releaseLock: async () => {},
428
430
  withMailboxLock: async (
429
431
  _mailboxId: string,
430
432
  _operation: string,
@@ -32,7 +32,7 @@ import {
32
32
  isMailSyncDisabled,
33
33
  isUnsyncableHost,
34
34
  } from "../account-check.js";
35
- import { emitEvent } from "../emit.js";
35
+ import { emitEvent, SYNC_MESSAGES_PENDING } from "../emit.js";
36
36
  import type {
37
37
  FlagPushEvent,
38
38
  SyncMessageBodyEvent,
@@ -101,6 +101,13 @@ export const syncMessages = async (
101
101
  secrets,
102
102
  } = await deps.getClient();
103
103
 
104
+ await mailboxLockService.releaseLock(
105
+ event.accountId,
106
+ event.mailboxId,
107
+ SYNC_MESSAGES_PENDING,
108
+ event.eventId,
109
+ );
110
+
104
111
  // A deleted account never has its DDB row purged in lockstep with the queued
105
112
  // SYNC_MESSAGES triggers, so a trigger can outlive its account. The lookup
106
113
  // then returns null (or throws a named NotFoundError), which can never succeed
@@ -17,6 +17,7 @@ import {
17
17
  import {
18
18
  AccountRepo,
19
19
  DrizzleMessageRepository,
20
+ MailboxLockRepo,
20
21
  MailboxRepo,
21
22
  } from "@remit/drizzle-service";
22
23
  import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
@@ -47,6 +48,7 @@ const silentLogger: Logger = (() => {
47
48
  let accounts: AccountRepo;
48
49
  let mailboxes: MailboxRepo;
49
50
  let messages: DrizzleMessageRepository;
51
+ let locks: MailboxLockRepo;
50
52
  let client: RemitClient;
51
53
  let close: () => void;
52
54
 
@@ -56,10 +58,12 @@ beforeEach(() => {
56
58
  accounts = new AccountRepo(store.db);
57
59
  mailboxes = new MailboxRepo(store.db);
58
60
  messages = new DrizzleMessageRepository(store.db as never);
61
+ locks = new MailboxLockRepo(store.db);
59
62
  client = {
60
63
  account: accounts,
61
64
  mailbox: mailboxes,
62
65
  message: messages,
66
+ mailboxLock: locks,
63
67
  } as unknown as RemitClient;
64
68
  setClient(client);
65
69
  });
@@ -0,0 +1,167 @@
1
+ import assert from "node:assert/strict";
2
+ import { afterEach, before, beforeEach, describe, it } from "node:test";
3
+ import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
4
+ import {
5
+ _resetForTest,
6
+ type RemitClient,
7
+ setClient,
8
+ } from "@remit/backend/client";
9
+ import { MailboxLockRepo } from "@remit/drizzle-service";
10
+ import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
11
+ import { mockClient } from "aws-sdk-client-mock";
12
+ import type * as Emit from "./emit.js";
13
+ import type { SyncMessagesEvent } from "./events.js";
14
+ import { emitMailboxResync, emitMoveResync } from "./handlers/message-move.js";
15
+ import { emitSyncMessagesEvents } from "./handlers/sync-mailboxes.js";
16
+
17
+ const ACCOUNT = "account-1366";
18
+ const FOLDERS = [
19
+ { mailboxId: "mbx-inbox", fullPath: "INBOX" },
20
+ { mailboxId: "mbx-sent", fullPath: "Sent" },
21
+ { mailboxId: "mbx-archive", fullPath: "Archive" },
22
+ ];
23
+
24
+ const sqsMock = mockClient(SQSClient);
25
+
26
+ let emitEvent: typeof Emit.emitEvent;
27
+ let SYNC_MESSAGES_PENDING: typeof Emit.SYNC_MESSAGES_PENDING;
28
+ let locks: MailboxLockRepo;
29
+ let closeStore: () => void;
30
+
31
+ type Queued = { mailboxId: string; eventId: string };
32
+
33
+ const queued = (): Queued[] =>
34
+ sqsMock.commandCalls(SendMessageCommand).map((call) => {
35
+ const body = JSON.parse(call.args[0].input.MessageBody ?? "{}") as Queued;
36
+ return { mailboxId: body.mailboxId, eventId: body.eventId };
37
+ });
38
+
39
+ const inboxSync = (): Omit<SyncMessagesEvent, "eventId" | "timestamp"> => ({
40
+ type: "SYNC_MESSAGES",
41
+ accountId: ACCOUNT,
42
+ mailboxId: "mbx-inbox",
43
+ });
44
+
45
+ const fanOut = (): Promise<void> =>
46
+ emitSyncMessagesEvents(ACCOUNT, FOLDERS, emitEvent);
47
+
48
+ before(async () => {
49
+ const fifo = (name: string) =>
50
+ `https://sqs.eu-west-1.amazonaws.com/0/test-${name}.fifo`;
51
+ process.env.SQS_QUEUE_URL_MAILBOXES = fifo("mailboxes");
52
+ process.env.SQS_QUEUE_URL_MESSAGES = fifo("messages");
53
+ process.env.SQS_QUEUE_URL_FLAGS = fifo("flags");
54
+ ({ emitEvent, SYNC_MESSAGES_PENDING } = await import("./emit.js"));
55
+ });
56
+
57
+ beforeEach(() => {
58
+ sqsMock.reset();
59
+ sqsMock.on(SendMessageCommand).resolves({});
60
+ const store = createShippedSqliteDb();
61
+ closeStore = store.close;
62
+ locks = new MailboxLockRepo(store.db);
63
+ setClient({ mailboxLock: locks } as unknown as RemitClient);
64
+ });
65
+
66
+ afterEach(() => {
67
+ _resetForTest();
68
+ closeStore();
69
+ });
70
+
71
+ describe("SYNC_MESSAGES under explicit refreshes (#1366)", () => {
72
+ it("queues one event per folder however many rounds arrive while none has started", async () => {
73
+ for (let round = 0; round < 25; round += 1) await fanOut();
74
+
75
+ assert.deepEqual(
76
+ queued()
77
+ .map((event) => event.mailboxId)
78
+ .sort(),
79
+ ["mbx-archive", "mbx-inbox", "mbx-sent"],
80
+ );
81
+ });
82
+
83
+ it("queues a folder again once its pending sync has started", async () => {
84
+ await fanOut();
85
+ const inbox = queued().find((event) => event.mailboxId === "mbx-inbox");
86
+ if (!inbox) throw new Error("INBOX was not queued");
87
+
88
+ await locks.releaseLock(
89
+ ACCOUNT,
90
+ inbox.mailboxId,
91
+ SYNC_MESSAGES_PENDING,
92
+ inbox.eventId,
93
+ );
94
+ await fanOut();
95
+
96
+ assert.equal(queued().length, FOLDERS.length + 1);
97
+ assert.equal(queued().at(-1)?.mailboxId, "mbx-inbox");
98
+ });
99
+
100
+ it("keeps a folder's pending marker when a stale event starts", async () => {
101
+ await fanOut();
102
+ await locks.releaseLock(
103
+ ACCOUNT,
104
+ "mbx-inbox",
105
+ SYNC_MESSAGES_PENDING,
106
+ "an-event-from-before-the-marker",
107
+ );
108
+ await fanOut();
109
+
110
+ assert.equal(queued().length, FOLDERS.length);
111
+ });
112
+
113
+ it("takes the same marker for a move's resync and a delete's resync", async () => {
114
+ await emitMoveResync(emitEvent, {
115
+ accountId: ACCOUNT,
116
+ sourceMailboxId: "mbx-inbox",
117
+ destinationMailboxId: "mbx-sent",
118
+ });
119
+ await emitMailboxResync(emitEvent, {
120
+ accountId: ACCOUNT,
121
+ mailboxId: "mbx-inbox",
122
+ });
123
+ await fanOut();
124
+
125
+ assert.equal(queued().length, FOLDERS.length);
126
+ });
127
+
128
+ it("takes over a marker whose event was lost once it has expired", async () => {
129
+ await locks.tryAcquireLock(
130
+ "mbx-inbox",
131
+ "SYNC_MESSAGES_PENDING",
132
+ ACCOUNT,
133
+ "lost-event",
134
+ -1,
135
+ );
136
+
137
+ await emitEvent(inboxSync());
138
+
139
+ assert.equal(queued().length, 1);
140
+ });
141
+
142
+ it("releases the marker when the send fails, so the next round is not swallowed", async () => {
143
+ sqsMock
144
+ .on(SendMessageCommand)
145
+ .rejectsOnce(new Error("queue unreachable"))
146
+ .resolves({});
147
+
148
+ await assert.rejects(emitEvent(inboxSync()), /queue unreachable/);
149
+ await emitEvent(inboxSync());
150
+
151
+ assert.equal(queued().length, 2);
152
+ });
153
+
154
+ it("fails the operation when the marker store fails", async () => {
155
+ setClient({
156
+ mailboxLock: {
157
+ get: async () => null,
158
+ tryAcquireLock: async () => {
159
+ throw new Error("marker store down");
160
+ },
161
+ },
162
+ } as unknown as RemitClient);
163
+
164
+ await assert.rejects(emitEvent(inboxSync()), /marker store down/);
165
+ assert.equal(sqsMock.commandCalls(SendMessageCommand).length, 0);
166
+ });
167
+ });