@remit/queue-sidecar 0.0.13 → 0.0.15

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/queue-sidecar",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -21,9 +21,9 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@remit/logger-lambda": "*",
24
- "better-sqlite3": "^12.11.1",
25
- "@types/better-sqlite3": "^7.6.13",
26
- "prom-client": "^15.1.3"
24
+ "prom-client": "^15.1.3",
25
+ "@remit/drizzle-service": "*",
26
+ "drizzle-orm": "1.0.0-rc.5"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@aws-sdk/client-sqs": "*",
package/src/store.test.ts CHANGED
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { after, beforeEach, describe, it } from "node:test";
5
5
  import { fileURLToPath } from "node:url";
6
- import Database from "better-sqlite3";
6
+ import { openSqlite } from "@remit/drizzle-service/sqlite";
7
7
  import { bootstrapQueues, parseQueuesConfig } from "./queues-config.js";
8
8
  import {
9
9
  InvalidParameterValueError,
@@ -322,8 +322,13 @@ describe("QueueStore", () => {
322
322
  const seedBacklog = (rows: number, groups: number): void => {
323
323
  const path = join(dir, "queue.db");
324
324
  store.close();
325
- const seed = new Database(path);
326
- seed
325
+ const seed = openSqlite({
326
+ filename: path,
327
+ mode: "open",
328
+ synchronous: "FULL",
329
+ schema: {},
330
+ });
331
+ seed.sqlite
327
332
  .prepare(
328
333
  `WITH RECURSIVE n(i) AS (
329
334
  SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < ?
package/src/store.ts CHANGED
@@ -1,5 +1,59 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import Database from "better-sqlite3";
2
+ import { openSqlite, type SqliteHandle } from "@remit/drizzle-service/sqlite";
3
+ import { and, asc, count, eq, gt, lt, lte, sql } from "drizzle-orm";
4
+ import {
5
+ integer,
6
+ primaryKey,
7
+ sqliteTable,
8
+ text,
9
+ } from "drizzle-orm/sqlite-core";
10
+
11
+ const queues = sqliteTable("queues", {
12
+ name: text("name").primaryKey(),
13
+ fifo: integer("fifo").notNull().default(0),
14
+ visibilityTimeoutSeconds: integer("visibility_timeout_seconds")
15
+ .notNull()
16
+ .default(30),
17
+ deadLetterTargetName: text("dead_letter_target_name"),
18
+ maxReceiveCount: integer("max_receive_count"),
19
+ contentBasedDeduplication: integer("content_based_deduplication")
20
+ .notNull()
21
+ .default(0),
22
+ });
23
+
24
+ const messages = sqliteTable("messages", {
25
+ id: integer("id").primaryKey({ autoIncrement: true }),
26
+ messageId: text("message_id").notNull(),
27
+ queueName: text("queue_name").notNull(),
28
+ body: text("body").notNull(),
29
+ md5Body: text("md5_body").notNull(),
30
+ groupId: text("group_id"),
31
+ dedupId: text("dedup_id"),
32
+ sequenceNumber: text("sequence_number"),
33
+ receiveCount: integer("receive_count").notNull().default(0),
34
+ visibleAt: integer("visible_at").notNull().default(0),
35
+ receiptHandle: text("receipt_handle"),
36
+ sentAt: integer("sent_at").notNull(),
37
+ firstReceivedAt: integer("first_received_at"),
38
+ });
39
+
40
+ const dedup = sqliteTable(
41
+ "dedup",
42
+ {
43
+ queueName: text("queue_name").notNull(),
44
+ dedupId: text("dedup_id").notNull(),
45
+ messageId: text("message_id").notNull(),
46
+ createdAt: integer("created_at").notNull(),
47
+ },
48
+ (table) => [primaryKey({ columns: [table.queueName, table.dedupId] })],
49
+ );
50
+
51
+ const releaseRedrive = sqliteTable("release_redrive", {
52
+ id: integer("id").primaryKey(),
53
+ release: text("release").notNull(),
54
+ });
55
+
56
+ const schema = { queues, messages, dedup, releaseRedrive };
3
57
 
4
58
  export interface QueueDefinition {
5
59
  readonly name: string;
@@ -46,43 +100,24 @@ export interface QueueAttributes {
46
100
  readonly redrivePolicy: string | null;
47
101
  }
48
102
 
49
- interface QueueRow {
50
- name: string;
51
- fifo: number;
52
- visibility_timeout_seconds: number;
53
- dead_letter_target_name: string | null;
54
- max_receive_count: number | null;
55
- content_based_deduplication: number;
56
- }
103
+ type QueueRow = typeof queues.$inferSelect;
57
104
 
58
- interface MessageRow {
59
- id: number;
60
- message_id: string;
61
- queue_name: string;
62
- body: string;
63
- md5_body: string;
64
- group_id: string | null;
65
- dedup_id: string | null;
66
- sequence_number: string | null;
67
- receive_count: number;
68
- visible_at: number;
69
- receipt_handle: string | null;
70
- sent_at: number;
71
- first_received_at: number | null;
72
- }
105
+ type MessageRow = typeof messages.$inferSelect;
73
106
 
74
107
  const DEDUP_WINDOW_MS = 5 * 60 * 1000;
75
108
 
109
+ const IMMEDIATE = { behavior: "immediate" } as const;
110
+
76
111
  const md5Hex = (value: string): string =>
77
112
  createHash("md5").update(value, "utf8").digest("hex");
78
113
 
79
114
  const toQueueRecord = (row: QueueRow): QueueRecord => ({
80
115
  name: row.name,
81
116
  fifo: row.fifo === 1,
82
- visibilityTimeoutSeconds: row.visibility_timeout_seconds,
83
- deadLetterTargetName: row.dead_letter_target_name,
84
- maxReceiveCount: row.max_receive_count,
85
- contentBasedDeduplication: row.content_based_deduplication === 1,
117
+ visibilityTimeoutSeconds: row.visibilityTimeoutSeconds,
118
+ deadLetterTargetName: row.deadLetterTargetName,
119
+ maxReceiveCount: row.maxReceiveCount,
120
+ contentBasedDeduplication: row.contentBasedDeduplication === 1,
86
121
  });
87
122
 
88
123
  /**
@@ -95,27 +130,31 @@ const toQueueRecord = (row: QueueRow): QueueRecord => ({
95
130
  * here so the behaviour is testable without a running HTTP server.
96
131
  */
97
132
  export class QueueStore {
98
- private readonly db: Database.Database;
133
+ private readonly handle: SqliteHandle<typeof schema>;
134
+ private readonly db: SqliteHandle<typeof schema>["db"];
99
135
  private sequenceCounter: bigint;
100
136
  private readonly startedAt = Date.now();
101
137
  private readonly lastReceiveAt = new Map<string, number>();
102
138
 
103
139
  constructor(filename: string) {
104
- this.db = new Database(filename);
105
- this.db.pragma("journal_mode = WAL");
106
- this.db.pragma("busy_timeout = 5000");
107
140
  // FULL, not NORMAL: the outbox path makes durability non-negotiable
108
141
  // (ADR constraint 5) — a queued send lost is a mail the user believes
109
142
  // left but never did. FULL flushes the WAL on every commit, so a
110
143
  // committed send survives host power-loss, not only a process restart.
111
144
  // At this queue's write volume the extra fsync cost is irrelevant.
112
- this.db.pragma("synchronous = FULL");
145
+ this.handle = openSqlite({
146
+ filename,
147
+ mode: "create",
148
+ synchronous: "FULL",
149
+ schema,
150
+ });
151
+ this.db = this.handle.db;
113
152
  this.migrate();
114
153
  this.sequenceCounter = this.readMaxSequence();
115
154
  }
116
155
 
117
156
  private migrate(): void {
118
- this.db.exec(`
157
+ this.handle.exec(`
119
158
  CREATE TABLE IF NOT EXISTS queues (
120
159
  name TEXT PRIMARY KEY,
121
160
  fifo INTEGER NOT NULL DEFAULT 0,
@@ -159,11 +198,14 @@ export class QueueStore {
159
198
 
160
199
  private readMaxSequence(): bigint {
161
200
  const row = this.db
162
- .prepare(
163
- "SELECT MAX(CAST(sequence_number AS INTEGER)) AS max FROM messages",
164
- )
165
- .get() as { max: number | null };
166
- return BigInt(row.max ?? 0);
201
+ .select({
202
+ max: sql<
203
+ number | null
204
+ >`MAX(CAST(${messages.sequenceNumber} AS INTEGER))`,
205
+ })
206
+ .from(messages)
207
+ .get();
208
+ return BigInt(row?.max ?? 0);
167
209
  }
168
210
 
169
211
  private nextSequence(): string {
@@ -172,32 +214,22 @@ export class QueueStore {
172
214
  }
173
215
 
174
216
  close(): void {
175
- this.db.close();
217
+ this.handle.close();
176
218
  }
177
219
 
178
220
  upsertQueue(def: QueueDefinition): QueueRecord {
221
+ const settings = {
222
+ fifo: def.fifo ? 1 : 0,
223
+ visibilityTimeoutSeconds: def.visibilityTimeoutSeconds,
224
+ deadLetterTargetName: def.deadLetterTargetName ?? null,
225
+ maxReceiveCount: def.maxReceiveCount ?? null,
226
+ contentBasedDeduplication: def.contentBasedDeduplication ? 1 : 0,
227
+ };
179
228
  this.db
180
- .prepare(
181
- `INSERT INTO queues (
182
- name, fifo, visibility_timeout_seconds,
183
- dead_letter_target_name, max_receive_count,
184
- content_based_deduplication
185
- ) VALUES (?, ?, ?, ?, ?, ?)
186
- ON CONFLICT(name) DO UPDATE SET
187
- fifo = excluded.fifo,
188
- visibility_timeout_seconds = excluded.visibility_timeout_seconds,
189
- dead_letter_target_name = excluded.dead_letter_target_name,
190
- max_receive_count = excluded.max_receive_count,
191
- content_based_deduplication = excluded.content_based_deduplication`,
192
- )
193
- .run(
194
- def.name,
195
- def.fifo ? 1 : 0,
196
- def.visibilityTimeoutSeconds,
197
- def.deadLetterTargetName ?? null,
198
- def.maxReceiveCount ?? null,
199
- def.contentBasedDeduplication ? 1 : 0,
200
- );
229
+ .insert(queues)
230
+ .values({ name: def.name, ...settings })
231
+ .onConflictDoUpdate({ target: queues.name, set: settings })
232
+ .run();
201
233
  const created = this.getQueue(def.name);
202
234
  if (!created) throw new Error(`queue ${def.name} vanished after upsert`);
203
235
  return created;
@@ -205,15 +237,15 @@ export class QueueStore {
205
237
 
206
238
  getQueue(name: string): QueueRecord | undefined {
207
239
  const row = this.db
208
- .prepare("SELECT * FROM queues WHERE name = ?")
209
- .get(name) as QueueRow | undefined;
240
+ .select()
241
+ .from(queues)
242
+ .where(eq(queues.name, name))
243
+ .get();
210
244
  return row ? toQueueRecord(row) : undefined;
211
245
  }
212
246
 
213
247
  listQueues(): QueueRecord[] {
214
- const rows = this.db
215
- .prepare("SELECT * FROM queues ORDER BY name")
216
- .all() as QueueRow[];
248
+ const rows = this.db.select().from(queues).orderBy(asc(queues.name)).all();
217
249
  return rows.map(toQueueRecord);
218
250
  }
219
251
 
@@ -236,26 +268,29 @@ export class QueueStore {
236
268
  (queue.contentBasedDeduplication ? md5 : undefined))
237
269
  : undefined;
238
270
 
239
- const send = this.db.transaction((): SendResult => {
271
+ return this.db.transaction((): SendResult => {
240
272
  if (dedupId) {
241
273
  this.pruneDedup(input.queueName, now);
242
274
  const existing = this.db
243
- .prepare(
244
- "SELECT message_id FROM dedup WHERE queue_name = ? AND dedup_id = ?",
275
+ .select({ messageId: dedup.messageId })
276
+ .from(dedup)
277
+ .where(
278
+ and(
279
+ eq(dedup.queueName, input.queueName),
280
+ eq(dedup.dedupId, dedupId),
281
+ ),
245
282
  )
246
- .get(input.queueName, dedupId) as { message_id: string } | undefined;
283
+ .get();
247
284
  if (existing) {
248
285
  const seq = this.db
249
- .prepare(
250
- "SELECT sequence_number FROM messages WHERE message_id = ?",
251
- )
252
- .get(existing.message_id) as
253
- | { sequence_number: string | null }
254
- | undefined;
286
+ .select({ sequenceNumber: messages.sequenceNumber })
287
+ .from(messages)
288
+ .where(eq(messages.messageId, existing.messageId))
289
+ .get();
255
290
  return {
256
- messageId: existing.message_id,
291
+ messageId: existing.messageId,
257
292
  md5OfBody: md5,
258
- sequenceNumber: seq?.sequence_number ?? null,
293
+ sequenceNumber: seq?.sequenceNumber ?? null,
259
294
  deduplicated: true,
260
295
  };
261
296
  }
@@ -263,31 +298,27 @@ export class QueueStore {
263
298
 
264
299
  const messageId = randomUUID();
265
300
  const sequenceNumber = queue.fifo ? this.nextSequence() : null;
266
- this.db
267
- .prepare(
268
- `INSERT INTO messages (
269
- message_id, queue_name, body, md5_body, group_id,
270
- dedup_id, sequence_number, receive_count, visible_at,
271
- receipt_handle, sent_at, first_received_at
272
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?, NULL)`,
273
- )
274
- .run(
275
- messageId,
276
- input.queueName,
277
- input.body,
278
- md5,
279
- input.groupId ?? null,
280
- dedupId ?? null,
281
- sequenceNumber,
282
- now,
283
- );
301
+ this.insertMessage({
302
+ messageId,
303
+ queueName: input.queueName,
304
+ body: input.body,
305
+ md5Body: md5,
306
+ groupId: input.groupId ?? null,
307
+ dedupId: dedupId ?? null,
308
+ sequenceNumber,
309
+ sentAt: now,
310
+ });
284
311
 
285
312
  if (dedupId) {
286
313
  this.db
287
- .prepare(
288
- "INSERT INTO dedup (queue_name, dedup_id, message_id, created_at) VALUES (?, ?, ?, ?)",
289
- )
290
- .run(input.queueName, dedupId, messageId, now);
314
+ .insert(dedup)
315
+ .values({
316
+ queueName: input.queueName,
317
+ dedupId,
318
+ messageId,
319
+ createdAt: now,
320
+ })
321
+ .run();
291
322
  }
292
323
 
293
324
  return {
@@ -296,9 +327,11 @@ export class QueueStore {
296
327
  sequenceNumber,
297
328
  deduplicated: false,
298
329
  };
299
- });
330
+ }, IMMEDIATE);
331
+ }
300
332
 
301
- return send();
333
+ private insertMessage(message: typeof messages.$inferInsert): void {
334
+ this.db.insert(messages).values(message).run();
302
335
  }
303
336
 
304
337
  receiveMessages(input: {
@@ -312,7 +345,7 @@ export class QueueStore {
312
345
  const visibilityTimeout =
313
346
  input.visibilityTimeoutSeconds ?? queue.visibilityTimeoutSeconds;
314
347
 
315
- const receive = this.db.transaction((): ReceivedMessage[] => {
348
+ const received = this.db.transaction((): ReceivedMessage[] => {
316
349
  const picked: ReceivedMessage[] = [];
317
350
 
318
351
  for (const candidate of this.nextCandidates(
@@ -323,49 +356,46 @@ export class QueueStore {
323
356
  if (
324
357
  queue.maxReceiveCount !== null &&
325
358
  queue.deadLetterTargetName !== null &&
326
- candidate.receive_count >= queue.maxReceiveCount
359
+ candidate.receiveCount >= queue.maxReceiveCount
327
360
  ) {
328
361
  this.db
329
- .prepare("UPDATE messages SET queue_name = ? WHERE id = ?")
330
- .run(queue.deadLetterTargetName, candidate.id);
362
+ .update(messages)
363
+ .set({ queueName: queue.deadLetterTargetName })
364
+ .where(eq(messages.id, candidate.id))
365
+ .run();
331
366
  continue;
332
367
  }
333
368
 
334
369
  const receiptHandle = randomUUID();
335
- const receiveCount = candidate.receive_count + 1;
336
- const firstReceived = candidate.first_received_at ?? now;
370
+ const receiveCount = candidate.receiveCount + 1;
371
+ const firstReceived = candidate.firstReceivedAt ?? now;
337
372
  this.db
338
- .prepare(
339
- `UPDATE messages SET
340
- receive_count = ?, visible_at = ?, receipt_handle = ?,
341
- first_received_at = ?
342
- WHERE id = ?`,
343
- )
344
- .run(
373
+ .update(messages)
374
+ .set({
345
375
  receiveCount,
346
- now + visibilityTimeout * 1000,
376
+ visibleAt: now + visibilityTimeout * 1000,
347
377
  receiptHandle,
348
- firstReceived,
349
- candidate.id,
350
- );
378
+ firstReceivedAt: firstReceived,
379
+ })
380
+ .where(eq(messages.id, candidate.id))
381
+ .run();
351
382
 
352
383
  picked.push({
353
- messageId: candidate.message_id,
384
+ messageId: candidate.messageId,
354
385
  body: candidate.body,
355
- md5OfBody: candidate.md5_body,
386
+ md5OfBody: candidate.md5Body,
356
387
  receiptHandle,
357
388
  receiveCount,
358
- sentTimestamp: candidate.sent_at,
389
+ sentTimestamp: candidate.sentAt,
359
390
  firstReceivedTimestamp: firstReceived,
360
- groupId: candidate.group_id,
361
- sequenceNumber: candidate.sequence_number,
391
+ groupId: candidate.groupId,
392
+ sequenceNumber: candidate.sequenceNumber,
362
393
  });
363
394
  }
364
395
 
365
396
  return picked;
366
- });
397
+ }, IMMEDIATE);
367
398
 
368
- const received = receive();
369
399
  if (received.length > 0) this.lastReceiveAt.set(input.queueName, now);
370
400
  return received;
371
401
  }
@@ -379,33 +409,41 @@ export class QueueStore {
379
409
  deleteMessage(queueName: string, receiptHandle: string): void {
380
410
  this.requireQueue(queueName);
381
411
  this.db
382
- .prepare(
383
- "DELETE FROM messages WHERE queue_name = ? AND receipt_handle = ?",
412
+ .delete(messages)
413
+ .where(
414
+ and(
415
+ eq(messages.queueName, queueName),
416
+ eq(messages.receiptHandle, receiptHandle),
417
+ ),
384
418
  )
385
- .run(queueName, receiptHandle);
419
+ .run();
386
420
  }
387
421
 
388
422
  purgeQueue(queueName: string): void {
389
423
  this.requireQueue(queueName);
390
- this.db.prepare("DELETE FROM messages WHERE queue_name = ?").run(queueName);
424
+ this.db.delete(messages).where(eq(messages.queueName, queueName)).run();
391
425
  }
392
426
 
393
427
  getQueueAttributes(queueName: string, now = Date.now()): QueueAttributes {
394
428
  const queue = this.requireQueue(queueName);
395
429
  const visible = this.db
396
- .prepare(
397
- "SELECT COUNT(*) AS n FROM messages WHERE queue_name = ? AND visible_at <= ?",
430
+ .select({ n: count() })
431
+ .from(messages)
432
+ .where(
433
+ and(eq(messages.queueName, queueName), lte(messages.visibleAt, now)),
398
434
  )
399
- .get(queueName, now) as { n: number };
435
+ .get();
400
436
  const notVisible = this.db
401
- .prepare(
402
- "SELECT COUNT(*) AS n FROM messages WHERE queue_name = ? AND visible_at > ?",
437
+ .select({ n: count() })
438
+ .from(messages)
439
+ .where(
440
+ and(eq(messages.queueName, queueName), gt(messages.visibleAt, now)),
403
441
  )
404
- .get(queueName, now) as { n: number };
442
+ .get();
405
443
 
406
444
  return {
407
- approximateNumberOfMessages: visible.n,
408
- approximateNumberOfMessagesNotVisible: notVisible.n,
445
+ approximateNumberOfMessages: visible?.n ?? 0,
446
+ approximateNumberOfMessagesNotVisible: notVisible?.n ?? 0,
409
447
  visibilityTimeout: queue.visibilityTimeoutSeconds,
410
448
  fifoQueue: queue.fifo,
411
449
  redrivePolicy:
@@ -425,34 +463,42 @@ export class QueueStore {
425
463
  ): MessageRow[] {
426
464
  if (!queue.fifo) {
427
465
  return this.db
428
- .prepare(
429
- `SELECT * FROM messages
430
- WHERE queue_name = ? AND visible_at <= ?
431
- ORDER BY id ASC LIMIT ?`,
466
+ .select()
467
+ .from(messages)
468
+ .where(
469
+ and(eq(messages.queueName, queue.name), lte(messages.visibleAt, now)),
432
470
  )
433
- .all(queue.name, now, limit) as MessageRow[];
471
+ .orderBy(asc(messages.id))
472
+ .limit(limit)
473
+ .all();
434
474
  }
435
475
 
476
+ const groupHeads = sql`WITH RECURSIVE g(gid) AS (
477
+ SELECT MIN(group_id) FROM messages WHERE queue_name = ${queue.name}
478
+ UNION ALL
479
+ SELECT (
480
+ SELECT MIN(group_id) FROM messages
481
+ WHERE queue_name = ${queue.name} AND group_id > g.gid
482
+ )
483
+ FROM g WHERE g.gid IS NOT NULL
484
+ )
485
+ SELECT (
486
+ SELECT MIN(id) FROM messages
487
+ WHERE queue_name = ${queue.name} AND group_id = g.gid
488
+ ) FROM g`;
489
+
436
490
  return this.db
437
- .prepare(
438
- `WITH RECURSIVE g(gid) AS (
439
- SELECT MIN(group_id) FROM messages WHERE queue_name = @queue
440
- UNION ALL
441
- SELECT (
442
- SELECT MIN(group_id) FROM messages
443
- WHERE queue_name = @queue AND group_id > g.gid
444
- )
445
- FROM g WHERE g.gid IS NOT NULL
446
- )
447
- SELECT m.* FROM g
448
- JOIN messages m ON m.id = (
449
- SELECT MIN(id) FROM messages
450
- WHERE queue_name = @queue AND group_id = g.gid
451
- )
452
- WHERE m.visible_at <= @now
453
- ORDER BY m.id ASC LIMIT @limit`,
491
+ .select()
492
+ .from(messages)
493
+ .where(
494
+ and(
495
+ sql`${messages.id} IN (${groupHeads})`,
496
+ lte(messages.visibleAt, now),
497
+ ),
454
498
  )
455
- .all({ queue: queue.name, now, limit }) as MessageRow[];
499
+ .orderBy(asc(messages.id))
500
+ .limit(limit)
501
+ .all();
456
502
  }
457
503
 
458
504
  /**
@@ -473,29 +519,33 @@ export class QueueStore {
473
519
  }
474
520
  const destination = source.name;
475
521
 
476
- const redrive = this.db.transaction((): number => {
522
+ return this.db.transaction((): number => {
477
523
  const rows = this.db
478
- .prepare("SELECT * FROM messages WHERE queue_name = ? ORDER BY id ASC")
479
- .all(deadLetterName) as MessageRow[];
524
+ .select()
525
+ .from(messages)
526
+ .where(eq(messages.queueName, deadLetterName))
527
+ .orderBy(asc(messages.id))
528
+ .all();
480
529
  for (const row of rows) this.moveToQueue(row, destination, now);
481
530
  return rows.length;
482
- });
483
- return redrive();
531
+ }, IMMEDIATE);
484
532
  }
485
533
 
486
534
  redriveForRelease(release: string, deadLetterNames: string[]): void {
487
535
  this.db.transaction((): void => {
488
536
  const marker = this.db
489
- .prepare("SELECT release FROM release_redrive WHERE id = 1")
490
- .get() as { release: string } | undefined;
537
+ .select({ release: releaseRedrive.release })
538
+ .from(releaseRedrive)
539
+ .where(eq(releaseRedrive.id, 1))
540
+ .get();
491
541
  if (marker?.release === release) return;
492
542
  for (const name of deadLetterNames) this.redriveDeadLetters(name);
493
543
  this.db
494
- .prepare(
495
- "INSERT INTO release_redrive (id, release) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET release = excluded.release",
496
- )
497
- .run(release);
498
- })();
544
+ .insert(releaseRedrive)
545
+ .values({ id: 1, release })
546
+ .onConflictDoUpdate({ target: releaseRedrive.id, set: { release } })
547
+ .run();
548
+ }, IMMEDIATE);
499
549
  }
500
550
 
501
551
  private moveToQueue(
@@ -503,31 +553,29 @@ export class QueueStore {
503
553
  targetName: string,
504
554
  now: number,
505
555
  ): void {
506
- this.db.prepare("DELETE FROM messages WHERE id = ?").run(message.id);
507
- this.db
508
- .prepare(
509
- `INSERT INTO messages (
510
- message_id, queue_name, body, md5_body, group_id, dedup_id,
511
- sequence_number, receive_count, visible_at, receipt_handle,
512
- sent_at, first_received_at
513
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, ?, NULL)`,
514
- )
515
- .run(
516
- message.message_id,
517
- targetName,
518
- message.body,
519
- message.md5_body,
520
- message.group_id,
521
- message.dedup_id,
522
- message.sequence_number,
523
- now,
524
- );
556
+ this.db.delete(messages).where(eq(messages.id, message.id)).run();
557
+ this.insertMessage({
558
+ messageId: message.messageId,
559
+ queueName: targetName,
560
+ body: message.body,
561
+ md5Body: message.md5Body,
562
+ groupId: message.groupId,
563
+ dedupId: message.dedupId,
564
+ sequenceNumber: message.sequenceNumber,
565
+ sentAt: now,
566
+ });
525
567
  }
526
568
 
527
569
  private pruneDedup(queueName: string, now: number): void {
528
570
  this.db
529
- .prepare("DELETE FROM dedup WHERE queue_name = ? AND created_at < ?")
530
- .run(queueName, now - DEDUP_WINDOW_MS);
571
+ .delete(dedup)
572
+ .where(
573
+ and(
574
+ eq(dedup.queueName, queueName),
575
+ lt(dedup.createdAt, now - DEDUP_WINDOW_MS),
576
+ ),
577
+ )
578
+ .run();
531
579
  }
532
580
 
533
581
  private requireQueue(name: string): QueueRecord {