@remit/queue-sidecar 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.
@@ -0,0 +1,386 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
+ import type { Server } from "node:http";
4
+ import type { AddressInfo } from "node:net";
5
+ import { join } from "node:path";
6
+ import { after, before, describe, it } from "node:test";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
9
+ CreateQueueCommand,
10
+ DeleteMessageCommand,
11
+ GetQueueAttributesCommand,
12
+ PurgeQueueCommand,
13
+ ReceiveMessageCommand,
14
+ SendMessageBatchCommand,
15
+ SendMessageCommand,
16
+ SQSClient,
17
+ } from "@aws-sdk/client-sqs";
18
+ import { AwsQueryProtocol } from "@aws-sdk/core/protocols";
19
+ import { bootstrapQueues, parseQueuesConfig } from "./queues-config.js";
20
+ import { createSidecarServer } from "./server.js";
21
+ import { QueueStore } from "./store.js";
22
+
23
+ const tmpRoot = join(
24
+ fileURLToPath(new URL(".", import.meta.url)),
25
+ "..",
26
+ ".tmp",
27
+ "sqs",
28
+ );
29
+
30
+ const config = parseQueuesConfig({
31
+ queues: [
32
+ {
33
+ name: "sync.fifo",
34
+ fifo: true,
35
+ visibilityTimeoutSeconds: 30,
36
+ deadLetter: { name: "sync-dlq.fifo", maxReceiveCount: 3 },
37
+ },
38
+ { name: "sync-dlq.fifo", fifo: true, visibilityTimeoutSeconds: 30 },
39
+ {
40
+ name: "work",
41
+ visibilityTimeoutSeconds: 1,
42
+ deadLetter: { name: "work-dlq", maxReceiveCount: 2 },
43
+ },
44
+ { name: "work-dlq", visibilityTimeoutSeconds: 30 },
45
+ ],
46
+ });
47
+
48
+ interface Harness {
49
+ client: SQSClient;
50
+ baseUrl: string;
51
+ queueUrl: (name: string) => string;
52
+ restart: () => Promise<void>;
53
+ stop: () => Promise<void>;
54
+ dbPath: string;
55
+ }
56
+
57
+ const listen = (server: Server, port = 0): Promise<number> =>
58
+ new Promise((resolve) => {
59
+ server.listen(port, "127.0.0.1", () => {
60
+ resolve((server.address() as AddressInfo).port);
61
+ });
62
+ });
63
+
64
+ const startHarness = async (): Promise<Harness> => {
65
+ mkdirSync(tmpRoot, { recursive: true });
66
+ const dir = mkdtempSync(join(tmpRoot, "sqs-"));
67
+ const dbPath = join(dir, "queue.db");
68
+
69
+ let store = new QueueStore(dbPath);
70
+ bootstrapQueues(store, config);
71
+ let server = createSidecarServer({ store, longPollIntervalMs: 25 });
72
+ let port = await listen(server);
73
+
74
+ const baseUrl = () => `http://127.0.0.1:${port}`;
75
+ const client = new SQSClient({
76
+ endpoint: baseUrl(),
77
+ protocol: AwsQueryProtocol,
78
+ region: "local",
79
+ credentials: { accessKeyId: "local", secretAccessKey: "local" },
80
+ maxAttempts: 3,
81
+ });
82
+
83
+ const closeServer = (): Promise<void> =>
84
+ new Promise((resolve) => server.close(() => resolve()));
85
+
86
+ return {
87
+ client,
88
+ get baseUrl() {
89
+ return baseUrl();
90
+ },
91
+ queueUrl: (name) => `${baseUrl()}/000000000000/${name}`,
92
+ restart: async () => {
93
+ await closeServer();
94
+ store.close();
95
+ store = new QueueStore(dbPath);
96
+ server = createSidecarServer({ store, longPollIntervalMs: 25 });
97
+ port = await listen(server, port);
98
+ },
99
+ stop: async () => {
100
+ await closeServer();
101
+ store.close();
102
+ },
103
+ dbPath,
104
+ };
105
+ };
106
+
107
+ describe("SQS wire protocol via the AWS SDK", () => {
108
+ let h: Harness;
109
+
110
+ before(async () => {
111
+ h = await startHarness();
112
+ });
113
+
114
+ after(async () => {
115
+ await h.stop();
116
+ rmSync(tmpRoot, { recursive: true, force: true });
117
+ });
118
+
119
+ it("creates a queue and returns its URL", async () => {
120
+ const result = await h.client.send(
121
+ new CreateQueueCommand({
122
+ QueueName: "sync.fifo",
123
+ Attributes: { FifoQueue: "true" },
124
+ }),
125
+ );
126
+ assert.equal(result.QueueUrl, h.queueUrl("sync.fifo"));
127
+ });
128
+
129
+ it("sends and receives a message through the SDK", async () => {
130
+ const url = h.queueUrl("sync.fifo");
131
+ const sent = await h.client.send(
132
+ new SendMessageCommand({
133
+ QueueUrl: url,
134
+ MessageBody: "hello world",
135
+ MessageGroupId: "g-send",
136
+ MessageDeduplicationId: "d-send",
137
+ }),
138
+ );
139
+ assert.ok(sent.MessageId);
140
+ assert.equal(sent.MD5OfMessageBody, "5eb63bbbe01eeed093cb22bb8f5acdc3");
141
+
142
+ const received = await h.client.send(
143
+ new ReceiveMessageCommand({
144
+ QueueUrl: url,
145
+ MaxNumberOfMessages: 10,
146
+ WaitTimeSeconds: 2,
147
+ }),
148
+ );
149
+ assert.equal(received.Messages?.length, 1);
150
+ assert.equal(received.Messages?.[0].Body, "hello world");
151
+
152
+ await h.client.send(
153
+ new DeleteMessageCommand({
154
+ QueueUrl: url,
155
+ ReceiptHandle: received.Messages?.[0].ReceiptHandle,
156
+ }),
157
+ );
158
+ });
159
+
160
+ it("preserves FIFO order within a message group", async () => {
161
+ const url = h.queueUrl("sync.fifo");
162
+ await h.client.send(new PurgeQueueCommand({ QueueUrl: url }));
163
+ for (const body of ["one", "two", "three"]) {
164
+ await h.client.send(
165
+ new SendMessageCommand({
166
+ QueueUrl: url,
167
+ MessageBody: body,
168
+ MessageGroupId: "ordered",
169
+ MessageDeduplicationId: `order-${body}`,
170
+ }),
171
+ );
172
+ }
173
+
174
+ const bodies: string[] = [];
175
+ for (let i = 0; i < 3; i += 1) {
176
+ const received = await h.client.send(
177
+ new ReceiveMessageCommand({
178
+ QueueUrl: url,
179
+ MaxNumberOfMessages: 10,
180
+ WaitTimeSeconds: 2,
181
+ }),
182
+ );
183
+ assert.equal(received.Messages?.length, 1);
184
+ const message = received.Messages?.[0];
185
+ if (!message?.Body || !message.ReceiptHandle)
186
+ throw new Error("no message");
187
+ bodies.push(message.Body);
188
+ await h.client.send(
189
+ new DeleteMessageCommand({
190
+ QueueUrl: url,
191
+ ReceiptHandle: message.ReceiptHandle,
192
+ }),
193
+ );
194
+ }
195
+ assert.deepEqual(bodies, ["one", "two", "three"]);
196
+ });
197
+
198
+ it("sends a batch and reports per-entry ids", async () => {
199
+ const url = h.queueUrl("sync.fifo");
200
+ await h.client.send(new PurgeQueueCommand({ QueueUrl: url }));
201
+ const result = await h.client.send(
202
+ new SendMessageBatchCommand({
203
+ QueueUrl: url,
204
+ Entries: [
205
+ {
206
+ Id: "a",
207
+ MessageBody: "batch-a",
208
+ MessageGroupId: "batch",
209
+ MessageDeduplicationId: "batch-a",
210
+ },
211
+ {
212
+ Id: "b",
213
+ MessageBody: "batch-b",
214
+ MessageGroupId: "batch",
215
+ MessageDeduplicationId: "batch-b",
216
+ },
217
+ ],
218
+ }),
219
+ );
220
+ assert.equal(result.Successful?.length, 2);
221
+ assert.deepEqual(result.Successful?.map((e) => e.Id).sort(), ["a", "b"]);
222
+ });
223
+
224
+ it("returns per-entry failures for a partially invalid batch", async () => {
225
+ const url = h.queueUrl("sync.fifo");
226
+ await h.client.send(new PurgeQueueCommand({ QueueUrl: url }));
227
+ const result = await h.client.send(
228
+ new SendMessageBatchCommand({
229
+ QueueUrl: url,
230
+ Entries: [
231
+ {
232
+ Id: "good",
233
+ MessageBody: "ok",
234
+ MessageGroupId: "g",
235
+ MessageDeduplicationId: "good-1",
236
+ },
237
+ // No MessageGroupId on a FIFO queue — SQS rejects this entry,
238
+ // the valid one still lands.
239
+ { Id: "bad", MessageBody: "no-group" },
240
+ ],
241
+ }),
242
+ );
243
+ assert.equal(result.Successful?.length, 1);
244
+ assert.equal(result.Successful?.[0].Id, "good");
245
+ assert.equal(result.Failed?.length, 1);
246
+ assert.equal(result.Failed?.[0].Id, "bad");
247
+ assert.equal(result.Failed?.[0].Code, "MissingParameter");
248
+ assert.equal(result.Failed?.[0].SenderFault, true);
249
+ });
250
+
251
+ it("rejects a FIFO SendMessage without a MessageGroupId", async () => {
252
+ const url = h.queueUrl("sync.fifo");
253
+ await assert.rejects(
254
+ h.client.send(
255
+ new SendMessageCommand({ QueueUrl: url, MessageBody: "no-group" }),
256
+ ),
257
+ (error: Error) => {
258
+ assert.match(`${error.name} ${error.message}`, /MissingParameter/);
259
+ return true;
260
+ },
261
+ );
262
+ });
263
+
264
+ it("redelivers after the visibility timeout and reports the receive count", async () => {
265
+ const url = h.queueUrl("work");
266
+ await h.client.send(new PurgeQueueCommand({ QueueUrl: url }));
267
+ await h.client.send(
268
+ new SendMessageCommand({ QueueUrl: url, MessageBody: "retry-me" }),
269
+ );
270
+
271
+ const first = await h.client.send(
272
+ new ReceiveMessageCommand({
273
+ QueueUrl: url,
274
+ MaxNumberOfMessages: 1,
275
+ WaitTimeSeconds: 2,
276
+ MessageSystemAttributeNames: ["ApproximateReceiveCount"],
277
+ }),
278
+ );
279
+ assert.equal(first.Messages?.length, 1);
280
+ assert.equal(first.Messages?.[0].Attributes?.ApproximateReceiveCount, "1");
281
+
282
+ await delay(1500);
283
+
284
+ const second = await h.client.send(
285
+ new ReceiveMessageCommand({
286
+ QueueUrl: url,
287
+ MaxNumberOfMessages: 1,
288
+ WaitTimeSeconds: 2,
289
+ MessageSystemAttributeNames: ["ApproximateReceiveCount"],
290
+ }),
291
+ );
292
+ assert.equal(second.Messages?.length, 1);
293
+ assert.equal(second.Messages?.[0].Attributes?.ApproximateReceiveCount, "2");
294
+ assert.equal(second.Messages?.[0].Body, "retry-me");
295
+ });
296
+
297
+ it("redrives a poison message to the DLQ after maxReceiveCount", async () => {
298
+ const url = h.queueUrl("work");
299
+ const dlqUrl = h.queueUrl("work-dlq");
300
+ await h.client.send(new PurgeQueueCommand({ QueueUrl: url }));
301
+ await h.client.send(new PurgeQueueCommand({ QueueUrl: dlqUrl }));
302
+ await h.client.send(
303
+ new SendMessageCommand({ QueueUrl: url, MessageBody: "poison" }),
304
+ );
305
+
306
+ for (let attempt = 1; attempt <= 2; attempt += 1) {
307
+ const received = await h.client.send(
308
+ new ReceiveMessageCommand({
309
+ QueueUrl: url,
310
+ MaxNumberOfMessages: 1,
311
+ WaitTimeSeconds: 2,
312
+ }),
313
+ );
314
+ assert.equal(received.Messages?.length, 1, `delivery ${attempt}`);
315
+ await delay(1200);
316
+ }
317
+
318
+ const drained = await h.client.send(
319
+ new ReceiveMessageCommand({
320
+ QueueUrl: url,
321
+ MaxNumberOfMessages: 1,
322
+ WaitTimeSeconds: 1,
323
+ }),
324
+ );
325
+ assert.equal(drained.Messages?.length ?? 0, 0, "source queue is empty");
326
+
327
+ const dlq = await h.client.send(
328
+ new ReceiveMessageCommand({
329
+ QueueUrl: dlqUrl,
330
+ MaxNumberOfMessages: 1,
331
+ WaitTimeSeconds: 2,
332
+ }),
333
+ );
334
+ assert.equal(dlq.Messages?.length, 1);
335
+ assert.equal(dlq.Messages?.[0].Body, "poison");
336
+ });
337
+
338
+ it("reports queue attributes for DLQ depth monitoring", async () => {
339
+ const url = h.queueUrl("sync.fifo");
340
+ await h.client.send(new PurgeQueueCommand({ QueueUrl: url }));
341
+ await h.client.send(
342
+ new SendMessageCommand({
343
+ QueueUrl: url,
344
+ MessageBody: "depth",
345
+ MessageGroupId: "g",
346
+ MessageDeduplicationId: "depth-1",
347
+ }),
348
+ );
349
+ const attributes = await h.client.send(
350
+ new GetQueueAttributesCommand({
351
+ QueueUrl: url,
352
+ AttributeNames: ["All"],
353
+ }),
354
+ );
355
+ assert.equal(attributes.Attributes?.ApproximateNumberOfMessages, "1");
356
+ assert.equal(attributes.Attributes?.FifoQueue, "true");
357
+ });
358
+
359
+ it("keeps enqueued messages across a sidecar restart", async () => {
360
+ const url = h.queueUrl("sync.fifo");
361
+ await h.client.send(new PurgeQueueCommand({ QueueUrl: url }));
362
+ await h.client.send(
363
+ new SendMessageCommand({
364
+ QueueUrl: url,
365
+ MessageBody: "survive-restart",
366
+ MessageGroupId: "durable",
367
+ MessageDeduplicationId: "durable-1",
368
+ }),
369
+ );
370
+
371
+ await h.restart();
372
+
373
+ const received = await h.client.send(
374
+ new ReceiveMessageCommand({
375
+ QueueUrl: url,
376
+ MaxNumberOfMessages: 10,
377
+ WaitTimeSeconds: 2,
378
+ }),
379
+ );
380
+ assert.equal(received.Messages?.length, 1);
381
+ assert.equal(received.Messages?.[0].Body, "survive-restart");
382
+ });
383
+ });
384
+
385
+ const delay = (ms: number): Promise<void> =>
386
+ new Promise((resolve) => setTimeout(resolve, ms));
@@ -0,0 +1,320 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { after, beforeEach, describe, it } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import { bootstrapQueues, parseQueuesConfig } from "./queues-config.js";
7
+ import {
8
+ InvalidParameterValueError,
9
+ MissingParameterError,
10
+ QueueStore,
11
+ } from "./store.js";
12
+
13
+ const tmpRoot = join(
14
+ fileURLToPath(new URL(".", import.meta.url)),
15
+ "..",
16
+ ".tmp",
17
+ "store",
18
+ );
19
+
20
+ const makeTempDir = (): string => {
21
+ mkdirSync(tmpRoot, { recursive: true });
22
+ return mkdtempSync(join(tmpRoot, "store-"));
23
+ };
24
+
25
+ const config = parseQueuesConfig({
26
+ queues: [
27
+ {
28
+ name: "orders.fifo",
29
+ fifo: true,
30
+ visibilityTimeoutSeconds: 30,
31
+ deadLetter: { name: "orders-dlq.fifo", maxReceiveCount: 3 },
32
+ },
33
+ { name: "orders-dlq.fifo", fifo: true, visibilityTimeoutSeconds: 30 },
34
+ {
35
+ name: "jobs",
36
+ visibilityTimeoutSeconds: 30,
37
+ deadLetter: { name: "jobs-dlq", maxReceiveCount: 2 },
38
+ },
39
+ { name: "jobs-dlq", visibilityTimeoutSeconds: 30 },
40
+ ],
41
+ });
42
+
43
+ describe("QueueStore", () => {
44
+ let dir: string;
45
+ let store: QueueStore;
46
+
47
+ beforeEach(() => {
48
+ dir = makeTempDir();
49
+ store = new QueueStore(join(dir, "queue.db"));
50
+ bootstrapQueues(store, config);
51
+ });
52
+
53
+ after(() => {
54
+ rmSync(tmpRoot, { recursive: true, force: true });
55
+ });
56
+
57
+ it("round-trips a standard message and computes its md5", () => {
58
+ const sent = store.sendMessage({ queueName: "jobs", body: "hello" });
59
+ assert.equal(sent.md5OfBody, "5d41402abc4b2a76b9719d911017c592");
60
+
61
+ const received = store.receiveMessages({
62
+ queueName: "jobs",
63
+ maxMessages: 10,
64
+ });
65
+ assert.equal(received.length, 1);
66
+ assert.equal(received[0].body, "hello");
67
+ assert.equal(received[0].receiveCount, 1);
68
+
69
+ store.deleteMessage("jobs", received[0].receiptHandle);
70
+ assert.equal(
71
+ store.receiveMessages({ queueName: "jobs", maxMessages: 10 }).length,
72
+ 0,
73
+ );
74
+ });
75
+
76
+ it("rejects a FIFO send with no MessageGroupId", () => {
77
+ assert.throws(
78
+ () =>
79
+ store.sendMessage({
80
+ queueName: "orders.fifo",
81
+ body: "x",
82
+ deduplicationId: "d",
83
+ }),
84
+ MissingParameterError,
85
+ );
86
+ });
87
+
88
+ it("rejects a FIFO send with neither a dedup id nor content-based dedup", () => {
89
+ assert.throws(
90
+ () =>
91
+ store.sendMessage({
92
+ queueName: "orders.fifo",
93
+ body: "x",
94
+ groupId: "g1",
95
+ }),
96
+ InvalidParameterValueError,
97
+ );
98
+ });
99
+
100
+ it("delivers FIFO messages in send order within a group", () => {
101
+ const now = 1_000;
102
+ for (const body of ["a", "b", "c"]) {
103
+ store.sendMessage({
104
+ queueName: "orders.fifo",
105
+ body,
106
+ groupId: "g1",
107
+ deduplicationId: body,
108
+ now,
109
+ });
110
+ }
111
+
112
+ const bodies: string[] = [];
113
+ for (let i = 0; i < 3; i += 1) {
114
+ const [message] = store.receiveMessages({
115
+ queueName: "orders.fifo",
116
+ maxMessages: 10,
117
+ visibilityTimeoutSeconds: 30,
118
+ now,
119
+ });
120
+ assert.ok(message, "a message is available");
121
+ bodies.push(message.body);
122
+ store.deleteMessage("orders.fifo", message.receiptHandle);
123
+ }
124
+ assert.deepEqual(bodies, ["a", "b", "c"]);
125
+ });
126
+
127
+ it("locks a FIFO group while a message is in flight", () => {
128
+ const now = 1_000;
129
+ store.sendMessage({
130
+ queueName: "orders.fifo",
131
+ body: "a",
132
+ groupId: "g1",
133
+ deduplicationId: "a",
134
+ now,
135
+ });
136
+ store.sendMessage({
137
+ queueName: "orders.fifo",
138
+ body: "b",
139
+ groupId: "g1",
140
+ deduplicationId: "b",
141
+ now,
142
+ });
143
+
144
+ const first = store.receiveMessages({
145
+ queueName: "orders.fifo",
146
+ maxMessages: 10,
147
+ visibilityTimeoutSeconds: 30,
148
+ now,
149
+ });
150
+ assert.equal(first.length, 1);
151
+ assert.equal(first[0].body, "a");
152
+
153
+ const blocked = store.receiveMessages({
154
+ queueName: "orders.fifo",
155
+ maxMessages: 10,
156
+ visibilityTimeoutSeconds: 30,
157
+ now,
158
+ });
159
+ assert.equal(
160
+ blocked.length,
161
+ 0,
162
+ "group is locked until the first is deleted",
163
+ );
164
+
165
+ store.deleteMessage("orders.fifo", first[0].receiptHandle);
166
+ const next = store.receiveMessages({
167
+ queueName: "orders.fifo",
168
+ maxMessages: 10,
169
+ visibilityTimeoutSeconds: 30,
170
+ now,
171
+ });
172
+ assert.equal(next[0].body, "b");
173
+ });
174
+
175
+ it("parallelises distinct FIFO groups", () => {
176
+ const now = 1_000;
177
+ store.sendMessage({
178
+ queueName: "orders.fifo",
179
+ body: "g1-a",
180
+ groupId: "g1",
181
+ deduplicationId: "g1-a",
182
+ now,
183
+ });
184
+ store.sendMessage({
185
+ queueName: "orders.fifo",
186
+ body: "g2-a",
187
+ groupId: "g2",
188
+ deduplicationId: "g2-a",
189
+ now,
190
+ });
191
+ const received = store.receiveMessages({
192
+ queueName: "orders.fifo",
193
+ maxMessages: 10,
194
+ visibilityTimeoutSeconds: 30,
195
+ now,
196
+ });
197
+ assert.deepEqual(received.map((m) => m.body).sort(), ["g1-a", "g2-a"]);
198
+ });
199
+
200
+ it("deduplicates FIFO sends inside the dedup window", () => {
201
+ const now = 1_000;
202
+ const first = store.sendMessage({
203
+ queueName: "orders.fifo",
204
+ body: "a",
205
+ groupId: "g1",
206
+ deduplicationId: "dup",
207
+ now,
208
+ });
209
+ const second = store.sendMessage({
210
+ queueName: "orders.fifo",
211
+ body: "a-again",
212
+ groupId: "g1",
213
+ deduplicationId: "dup",
214
+ now: now + 1000,
215
+ });
216
+ assert.equal(second.deduplicated, true);
217
+ assert.equal(second.messageId, first.messageId);
218
+
219
+ const received = store.receiveMessages({
220
+ queueName: "orders.fifo",
221
+ maxMessages: 10,
222
+ now,
223
+ });
224
+ assert.equal(received.length, 1);
225
+ assert.equal(received[0].body, "a");
226
+ });
227
+
228
+ it("redelivers a message after its visibility timeout expires", () => {
229
+ const now = 1_000;
230
+ store.sendMessage({ queueName: "jobs", body: "x", now });
231
+
232
+ const first = store.receiveMessages({
233
+ queueName: "jobs",
234
+ maxMessages: 10,
235
+ visibilityTimeoutSeconds: 5,
236
+ now,
237
+ });
238
+ assert.equal(first.length, 1);
239
+
240
+ const stillHidden = store.receiveMessages({
241
+ queueName: "jobs",
242
+ maxMessages: 10,
243
+ now: now + 4_000,
244
+ });
245
+ assert.equal(stillHidden.length, 0);
246
+
247
+ const redelivered = store.receiveMessages({
248
+ queueName: "jobs",
249
+ maxMessages: 10,
250
+ now: now + 6_000,
251
+ });
252
+ assert.equal(redelivered.length, 1);
253
+ assert.equal(redelivered[0].receiveCount, 2);
254
+ });
255
+
256
+ it("moves a message to the DLQ after maxReceiveCount deliveries", () => {
257
+ let now = 1_000;
258
+ store.sendMessage({ queueName: "jobs", body: "poison", now });
259
+
260
+ for (let attempt = 1; attempt <= 2; attempt += 1) {
261
+ const received = store.receiveMessages({
262
+ queueName: "jobs",
263
+ maxMessages: 10,
264
+ visibilityTimeoutSeconds: 1,
265
+ now,
266
+ });
267
+ assert.equal(received.length, 1, `delivery ${attempt}`);
268
+ assert.equal(received[0].receiveCount, attempt);
269
+ now += 2_000;
270
+ }
271
+
272
+ const afterMax = store.receiveMessages({
273
+ queueName: "jobs",
274
+ maxMessages: 10,
275
+ now,
276
+ });
277
+ assert.equal(afterMax.length, 0, "source queue is drained");
278
+
279
+ const dlq = store.receiveMessages({
280
+ queueName: "jobs-dlq",
281
+ maxMessages: 10,
282
+ now,
283
+ });
284
+ assert.equal(dlq.length, 1);
285
+ assert.equal(dlq[0].body, "poison");
286
+ assert.equal(dlq[0].receiveCount, 1, "receive count resets in the DLQ");
287
+ });
288
+
289
+ it("persists enqueued messages across a store restart", () => {
290
+ const path = join(dir, "durable.db");
291
+ const first = new QueueStore(path);
292
+ bootstrapQueues(first, config);
293
+ first.sendMessage({
294
+ queueName: "orders.fifo",
295
+ body: "durable",
296
+ groupId: "g1",
297
+ deduplicationId: "d1",
298
+ });
299
+ first.close();
300
+
301
+ const reopened = new QueueStore(path);
302
+ const received = reopened.receiveMessages({
303
+ queueName: "orders.fifo",
304
+ maxMessages: 10,
305
+ });
306
+ assert.equal(received.length, 1);
307
+ assert.equal(received[0].body, "durable");
308
+ reopened.close();
309
+ });
310
+
311
+ it("purges all messages from a queue", () => {
312
+ store.sendMessage({ queueName: "jobs", body: "1" });
313
+ store.sendMessage({ queueName: "jobs", body: "2" });
314
+ store.purgeQueue("jobs");
315
+ assert.equal(
316
+ store.receiveMessages({ queueName: "jobs", maxMessages: 10 }).length,
317
+ 0,
318
+ );
319
+ });
320
+ });