@bumail/queue 0.10.0 → 0.11.0

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 (46) hide show
  1. package/README.md +27 -1
  2. package/dist/chunks/{index-sz13a17b.js → index-t6q7zxgj.js} +14 -3
  3. package/dist/chunks/{index-sz13a17b.js.map → index-t6q7zxgj.js.map} +4 -4
  4. package/dist/contract/checks.d.ts +4 -0
  5. package/dist/contract/checks.d.ts.map +1 -1
  6. package/dist/contract/queue-store.d.ts +15 -7
  7. package/dist/contract/queue-store.d.ts.map +1 -1
  8. package/dist/contract/types.d.ts +5 -0
  9. package/dist/contract/types.d.ts.map +1 -1
  10. package/dist/errors.d.ts +3 -1
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +64 -26
  15. package/dist/index.js.map +4 -4
  16. package/dist/memory/index.js +10 -6
  17. package/dist/memory/index.js.map +3 -3
  18. package/dist/memory/store.d.ts +2 -2
  19. package/dist/memory/store.d.ts.map +1 -1
  20. package/dist/postgres/complete.d.ts +13 -0
  21. package/dist/postgres/complete.d.ts.map +1 -0
  22. package/dist/postgres/index.js +85 -74
  23. package/dist/postgres/index.js.map +6 -5
  24. package/dist/postgres/store.d.ts +2 -2
  25. package/dist/postgres/store.d.ts.map +1 -1
  26. package/dist/queue/deliver.d.ts.map +1 -1
  27. package/dist/queue/notify.d.ts +35 -6
  28. package/dist/queue/notify.d.ts.map +1 -1
  29. package/dist/redis/complete.d.ts +11 -0
  30. package/dist/redis/complete.d.ts.map +1 -0
  31. package/dist/redis/index.js +133 -110
  32. package/dist/redis/index.js.map +7 -6
  33. package/dist/redis/scripts.d.ts +8 -5
  34. package/dist/redis/scripts.d.ts.map +1 -1
  35. package/dist/redis/store.d.ts +2 -2
  36. package/dist/redis/store.d.ts.map +1 -1
  37. package/dist/sqlite/index.js +29 -20
  38. package/dist/sqlite/index.js.map +5 -4
  39. package/dist/sqlite/insert.d.ts +5 -0
  40. package/dist/sqlite/insert.d.ts.map +1 -0
  41. package/dist/sqlite/store.d.ts +2 -2
  42. package/dist/sqlite/store.d.ts.map +1 -1
  43. package/docs/guide.md +29 -0
  44. package/docs/roadmap.md +36 -10
  45. package/docs/troubleshooting.md +64 -1
  46. package/package.json +4 -4
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/sqlite/open.ts", "../src/sqlite/schema.ts", "../src/sqlite/rows.ts", "../src/sqlite/store.ts"],
3
+ "sources": ["../src/sqlite/insert.ts", "../src/sqlite/open.ts", "../src/sqlite/schema.ts", "../src/sqlite/rows.ts", "../src/sqlite/store.ts"],
4
4
  "sourcesContent": [
5
+ "import type { Database } from 'bun:sqlite';\nimport { dsnColumn } from '../contract/dsn-column';\nimport type { QueueItem } from '../contract/types';\n\n/** Stores a new item and its message; the caller holds the transaction. */\nexport function insertItem(\n\tdb: Database,\n\tadded: QueueItem,\n\tmessage: Uint8Array,\n): void {\n\tdb.query(\n\t\t`INSERT INTO items (id, sender, recipients, size, created_at,\n\t\tnext_attempt_at, attempts, delay_notified, dsn)\n\t\tVALUES ($id, $sender, $recipients, $size, $created_at,\n\t\t$next_attempt_at, 0, 0, $dsn)`,\n\t).run({\n\t\tid: added.id,\n\t\tsender: added.from,\n\t\trecipients: JSON.stringify(added.recipients),\n\t\tsize: added.size,\n\t\tcreated_at: added.createdAt,\n\t\tnext_attempt_at: added.nextAttemptAt,\n\t\tdsn: dsnColumn(added),\n\t});\n\tdb.query('INSERT INTO messages (item_id, content) VALUES (?, ?)').run(\n\t\tadded.id,\n\t\tmessage,\n\t);\n}\n",
5
6
  "import { Database } from 'bun:sqlite';\nimport { chmodSync, existsSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { QueueError } from '../errors';\nimport type { SqliteQueueStoreOptions } from './options';\nimport { migrate } from './schema';\n\n/**\n * The queue's files, and the directory when the store makes it: its\n * owner's alone, as mail is. A directory that already exists keeps its\n * mode — it is the operator's, who may share it with a group on purpose.\n */\nconst PRIVATE_DIRECTORY = 0o700;\nconst PRIVATE_FILE = 0o600;\n\n/**\n * Several processes may share the database: WAL lets readers go on while\n * one writes, and a writer waits up to `busy_timeout` for another. Every\n * write is flushed before it is acknowledged.\n */\nfunction configure(db: Database, busyTimeout: number): void {\n\tdb.exec(`PRAGMA busy_timeout = ${busyTimeout}`);\n\tdb.exec('PRAGMA journal_mode = WAL');\n\tdb.exec('PRAGMA synchronous = FULL');\n\t// On macOS a plain fsync leaves the data in the drive's cache.\n\tdb.exec('PRAGMA fullfsync = ON');\n\tdb.exec('PRAGMA foreign_keys = ON');\n}\n\nfunction keepPrivate(file: string): void {\n\tfor (const path of [file, `${file}-wal`, `${file}-shm`]) {\n\t\tif (existsSync(path)) chmodSync(path, PRIVATE_FILE);\n\t}\n}\n\nfunction busyTimeoutOf(value: unknown): number {\n\tif (value === undefined) return 5000;\n\tif (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n\t\tthrow new QueueError(\n\t\t\t'INVALID',\n\t\t\t`busyTimeout must be an integer of at least 0, not ${value}`,\n\t\t);\n\t}\n\treturn value;\n}\n\nconst messageOf = (error: unknown) =>\n\terror instanceof Error ? error.message : String(error);\n\n/**\n * Opens the queue's database in `directory`, at the last migration.\n * Whatever fails is `INVALID`, naming the directory.\n */\nexport function openDatabase(options: SqliteQueueStoreOptions): Database {\n\tconst directory = (options as SqliteQueueStoreOptions | undefined)?.directory;\n\tif (typeof directory !== 'string' || directory === '') {\n\t\tthrow new QueueError('INVALID', 'A SQLite queue store needs a directory');\n\t}\n\tconst busyTimeout = busyTimeoutOf(options.busyTimeout);\n\tlet db: Database | undefined;\n\ttry {\n\t\tif (!existsSync(directory)) {\n\t\t\tmkdirSync(directory, { recursive: true, mode: PRIVATE_DIRECTORY });\n\t\t\t// The mode given to mkdir is narrowed by the umask, never widened: set it.\n\t\t\tchmodSync(directory, PRIVATE_DIRECTORY);\n\t\t}\n\t\tconst file = join(directory, 'queue.sqlite');\n\t\tdb = new Database(file, { create: true, readwrite: true, strict: true });\n\t\tconfigure(db, busyTimeout);\n\t\tkeepPrivate(file);\n\t\tmigrate(db);\n\t\treturn db;\n\t} catch (error) {\n\t\tdb?.close();\n\t\tif (error instanceof QueueError) throw error;\n\t\tthrow new QueueError(\n\t\t\t'INVALID',\n\t\t\t`The queue at \"${directory}\" cannot be opened: ${messageOf(error)}`,\n\t\t);\n\t}\n}\n",
6
7
  "import type { Database } from 'bun:sqlite';\nimport { QueueError } from '../errors';\n\n/**\n * The migrations, in order: the database's `PRAGMA user_version` is how many\n * of them it has run. A migration is never edited once released; a change\n * is a new one at the end.\n */\nexport const MIGRATIONS: readonly string[] = [\n\t// 1: the items, and their messages in a table of their own.\n\t`\n\tCREATE TABLE items (\n\t\tseq INTEGER PRIMARY KEY,\n\t\tid TEXT NOT NULL UNIQUE,\n\t\tsender TEXT NOT NULL,\n\t\trecipients TEXT NOT NULL CHECK (json_valid(recipients)),\n\t\tsize INTEGER NOT NULL,\n\t\tcreated_at REAL NOT NULL,\n\t\tnext_attempt_at REAL NOT NULL,\n\t\tattempts INTEGER NOT NULL,\n\t\tdelay_notified INTEGER NOT NULL,\n\t\tlease_owner TEXT,\n\t\tlease_expires_at REAL,\n\t\tCHECK ((lease_owner IS NULL) = (lease_expires_at IS NULL))\n\t) STRICT;\n\tCREATE INDEX items_due ON items (next_attempt_at, seq);\n\n\tCREATE TABLE messages (\n\t\titem_id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE,\n\t\tcontent BLOB NOT NULL\n\t) STRICT;\n\t`,\n\t// 2: the item's DSN parameters (RET, ENVID) as JSON; NULL for the rows of before.\n\t`ALTER TABLE items ADD COLUMN dsn TEXT CHECK (dsn IS NULL OR json_valid(dsn))`,\n];\n\n/**\n * Brings the database to the last migration, in one transaction: a\n * migration that fails leaves the database as it was. A database written\n * by a newer store, with more migrations than this one knows, is refused.\n */\nexport function migrate(db: Database): void {\n\tdb.transaction(() => {\n\t\tconst { user_version: version } = db\n\t\t\t.query<{ user_version: number }, []>('PRAGMA user_version')\n\t\t\t.get() as { user_version: number };\n\t\tif (version > MIGRATIONS.length) {\n\t\t\tthrow new QueueError(\n\t\t\t\t'INVALID',\n\t\t\t\t`The database is at schema version ${version}, newer than this store's ${MIGRATIONS.length}`,\n\t\t\t);\n\t\t}\n\t\tfor (const sql of MIGRATIONS.slice(version)) db.exec(sql);\n\t\tif (version < MIGRATIONS.length) {\n\t\t\tdb.exec(`PRAGMA user_version = ${MIGRATIONS.length}`);\n\t\t}\n\t}).immediate();\n}\n",
7
8
  "import { dsnParams } from '../contract/dsn-column';\nimport type { QueueItem, RecipientState } from '../contract/types';\n\n/** A row of `items`, as `bun:sqlite` reads it. */\nexport interface ItemRow {\n\treadonly id: string;\n\treadonly sender: string;\n\treadonly recipients: string;\n\treadonly size: number;\n\treadonly created_at: number;\n\treadonly next_attempt_at: number;\n\treadonly attempts: number;\n\treadonly delay_notified: number;\n\treadonly lease_owner: string | null;\n\treadonly lease_expires_at: number | null;\n\treadonly dsn: string | null;\n}\n\n/** The columns a query reads to build an item. */\nexport const COLUMNS =\n\t'id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at, dsn';\n\nexport function itemOf(row: ItemRow): QueueItem {\n\tconst item: QueueItem = {\n\t\tid: row.id,\n\t\tfrom: row.sender,\n\t\trecipients: JSON.parse(row.recipients) as RecipientState[],\n\t\tsize: row.size,\n\t\tcreatedAt: row.created_at,\n\t\tnextAttemptAt: row.next_attempt_at,\n\t\tattempts: row.attempts,\n\t\tdelayNotified: row.delay_notified === 1,\n\t\t...dsnParams(row.dsn),\n\t};\n\tif (row.lease_owner === null || row.lease_expires_at === null) return item;\n\treturn {\n\t\t...item,\n\t\tlease: { owner: row.lease_owner, expiresAt: row.lease_expires_at },\n\t};\n}\n\n/** The named parameters that write an item's state back. */\nexport function stateOf(item: QueueItem) {\n\treturn {\n\t\tid: item.id,\n\t\trecipients: JSON.stringify(item.recipients),\n\t\tnext_attempt_at: item.nextAttemptAt,\n\t\tattempts: item.attempts,\n\t\tdelay_notified: item.delayNotified ? 1 : 0,\n\t\tlease_owner: item.lease?.owner ?? null,\n\t\tlease_expires_at: item.lease?.expiresAt ?? null,\n\t};\n}\n",
8
- "import type { Database } from 'bun:sqlite';\nimport {\n\tapplyAttempt,\n\tcheckClaim,\n\tcheckList,\n\tcheckMaxItems,\n\tcheckNewItem,\n\tcheckOwner,\n\tcheckResult,\n\tcheckTime,\n\tisDone,\n\tnewItem,\n} from '../contract/checks';\nimport { dsnColumn } from '../contract/dsn-column';\nimport type { QueueStore } from '../contract/queue-store';\nimport type {\n\tAddOptions,\n\tAttemptResult,\n\tClaimRequest,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n} from '../contract/types';\nimport { QueueError } from '../errors';\nimport { openDatabase } from './open';\nimport type { SqliteQueueStoreOptions } from './options';\nimport { COLUMNS, type ItemRow, itemOf, stateOf } from './rows';\n\nexport type { SqliteQueueStoreOptions } from './options';\n\nconst DUE = `next_attempt_at <= $now\n\tAND (lease_owner IS NULL OR lease_expires_at <= $now)`;\n\n/**\n * A `QueueStore` on `bun:sqlite`: one directory holding `queue.sqlite`,\n * which several processes on one machine may open together. Every\n * operation is one transaction — a claim is a single `UPDATE … RETURNING`\n * — so two workers never take the same item. Messages live in a table of\n * their own, dropped with their item.\n */\nexport class SqliteQueueStore implements QueueStore {\n\treadonly #db: Database;\n\t#closed = false;\n\n\tprivate constructor(db: Database) {\n\t\tthis.#db = db;\n\t}\n\n\t/** Opens the store in `directory`, creating it if need be; what fails is `INVALID`. */\n\tstatic open(options: SqliteQueueStoreOptions): SqliteQueueStore {\n\t\treturn new SqliteQueueStore(openDatabase(options));\n\t}\n\n\t/** Lets go of the database; closing twice is fine. */\n\tclose(): void {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tthis.#db.close();\n\t}\n\n\tget #open(): Database {\n\t\tif (this.#closed) {\n\t\t\tthrow new QueueError('CLOSED', 'The queue store is closed');\n\t\t}\n\t\treturn this.#db;\n\t}\n\n\t#row(id: string, owner?: string): ItemRow | null {\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn db\n\t\t\t\t.query<ItemRow, [string]>(`SELECT ${COLUMNS} FROM items WHERE id = ?`)\n\t\t\t\t.get(id);\n\t\t}\n\t\treturn db\n\t\t\t.query<ItemRow, [string, string]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.get(id, owner);\n\t}\n\n\t#write(item: QueueItem): void {\n\t\tthis.#open\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET recipients = $recipients,\n\t\t\t\tnext_attempt_at = $next_attempt_at, attempts = $attempts,\n\t\t\t\tdelay_notified = $delay_notified, lease_owner = $lease_owner,\n\t\t\t\tlease_expires_at = $lease_expires_at WHERE id = $id`,\n\t\t\t)\n\t\t\t.run(stateOf(item));\n\t}\n\n\tasync add(item: NewQueueItem, options: AddOptions = {}): Promise<QueueItem> {\n\t\tcheckNewItem(item);\n\t\tconst max = checkMaxItems(options.maxItems);\n\t\tconst db = this.#open;\n\t\tconst added = newItem(crypto.randomUUID(), item);\n\t\tdb.transaction(() => {\n\t\t\tif (max !== undefined) {\n\t\t\t\tconst { n } = db\n\t\t\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t\t\t.get() as { n: number };\n\t\t\t\tif (n >= max) {\n\t\t\t\t\tthrow new QueueError(\n\t\t\t\t\t\t'QUEUE_FULL',\n\t\t\t\t\t\t`The queue holds ${n} items, its limit`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tdb.query(\n\t\t\t\t`INSERT INTO items (id, sender, recipients, size, created_at,\n\t\t\t\tnext_attempt_at, attempts, delay_notified, dsn)\n\t\t\t\tVALUES ($id, $sender, $recipients, $size, $created_at,\n\t\t\t\t$next_attempt_at, 0, 0, $dsn)`,\n\t\t\t).run({\n\t\t\t\tid: added.id,\n\t\t\t\tsender: added.from,\n\t\t\t\trecipients: JSON.stringify(added.recipients),\n\t\t\t\tsize: added.size,\n\t\t\t\tcreated_at: added.createdAt,\n\t\t\t\tnext_attempt_at: added.nextAttemptAt,\n\t\t\t\tdsn: dsnColumn(added),\n\t\t\t});\n\t\t\tdb.query('INSERT INTO messages (item_id, content) VALUES (?, ?)').run(\n\t\t\t\tadded.id,\n\t\t\t\titem.message,\n\t\t\t);\n\t\t}).immediate();\n\t\treturn added;\n\t}\n\n\tasync get(id: string): Promise<QueueItem | undefined> {\n\t\tconst row = this.#row(id);\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync list(options?: QueueListOptions): Promise<QueueItem[]> {\n\t\tconst { offset, limit } = checkList(options);\n\t\treturn this.#open\n\t\t\t.query<ItemRow, [number, number]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items ORDER BY next_attempt_at, seq\n\t\t\t\tLIMIT ? OFFSET ?`,\n\t\t\t)\n\t\t\t.all(limit, offset)\n\t\t\t.map(itemOf);\n\t}\n\n\tasync count(): Promise<number> {\n\t\tconst row = this.#open\n\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t.get();\n\t\treturn row?.n ?? 0;\n\t}\n\n\tasync readMessage(id: string): Promise<Uint8Array | undefined> {\n\t\tconst row = this.#open\n\t\t\t.query<{ content: Uint8Array }, [string]>(\n\t\t\t\t'SELECT content FROM messages WHERE item_id = ?',\n\t\t\t)\n\t\t\t.get(id);\n\t\treturn row ? new Uint8Array(row.content) : undefined;\n\t}\n\n\tasync claim(request: ClaimRequest): Promise<QueueItem | undefined> {\n\t\tcheckClaim(request);\n\t\tconst row = this.#open\n\t\t\t.query<ItemRow, Record<string, string | number>>(\n\t\t\t\t`UPDATE items SET lease_owner = $owner, lease_expires_at = $expires\n\t\t\t\tWHERE seq = (SELECT seq FROM items WHERE ${DUE}\n\t\t\t\t\tORDER BY next_attempt_at, seq LIMIT 1)\n\t\t\t\tRETURNING ${COLUMNS}`,\n\t\t\t)\n\t\t\t.get({\n\t\t\t\towner: request.owner,\n\t\t\t\tnow: request.now,\n\t\t\t\texpires: request.now + request.leaseMs,\n\t\t\t});\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync renew(id: string, owner: string, expiresAt: number): Promise<boolean> {\n\t\tcheckOwner(owner);\n\t\tcheckTime('expiresAt', expiresAt);\n\t\tconst { changes } = this.#open\n\t\t\t.query(\n\t\t\t\t'UPDATE items SET lease_expires_at = ? WHERE id = ? AND lease_owner = ?',\n\t\t\t)\n\t\t\t.run(expiresAt, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync complete(\n\t\tid: string,\n\t\towner: string,\n\t\tresult: AttemptResult,\n\t): Promise<QueueItem | undefined> {\n\t\tcheckOwner(owner);\n\t\tcheckResult(result);\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction(() => {\n\t\t\t\tconst row = this.#row(id, owner);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tconst item = applyAttempt(itemOf(row), result);\n\t\t\t\tif (isDone(item)) {\n\t\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\t} else {\n\t\t\t\t\tthis.#write(item);\n\t\t\t\t}\n\t\t\t\treturn item;\n\t\t\t})\n\t\t\t.immediate();\n\t}\n\n\tasync reschedule(id: string, at: number, owner?: string): Promise<boolean> {\n\t\tcheckTime('at', at);\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn (\n\t\t\t\tdb\n\t\t\t\t\t.query('UPDATE items SET next_attempt_at = ? WHERE id = ?')\n\t\t\t\t\t.run(at, id).changes > 0\n\t\t\t);\n\t\t}\n\t\tcheckOwner(owner);\n\t\tconst { changes } = db\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET next_attempt_at = ?, lease_owner = NULL,\n\t\t\t\tlease_expires_at = NULL WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.run(at, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync cancel(id: string): Promise<QueueItem | undefined> {\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction(() => {\n\t\t\t\tconst row = this.#row(id);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\treturn itemOf(row);\n\t\t\t})\n\t\t\t.immediate();\n\t}\n}\n"
9
+ "import type { Database } from 'bun:sqlite';\nimport {\n\tapplyAttempt,\n\tcheckClaim,\n\tcheckComplete,\n\tcheckList,\n\tcheckMaxItems,\n\tcheckNewItem,\n\tcheckOwner,\n\tcheckTime,\n\tisDone,\n\tnewItem,\n} from '../contract/checks';\nimport type { QueueStore } from '../contract/queue-store';\nimport type {\n\tAddOptions,\n\tAttemptResult,\n\tClaimRequest,\n\tCompleted,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n} from '../contract/types';\nimport { QueueError } from '../errors';\nimport { insertItem } from './insert';\nimport { openDatabase } from './open';\nimport type { SqliteQueueStoreOptions } from './options';\nimport { COLUMNS, type ItemRow, itemOf, stateOf } from './rows';\n\nexport type { SqliteQueueStoreOptions } from './options';\n\nconst DUE = `next_attempt_at <= $now\n\tAND (lease_owner IS NULL OR lease_expires_at <= $now)`;\n\n/**\n * A `QueueStore` on `bun:sqlite`: one directory holding `queue.sqlite`,\n * which several processes on one machine may open together. Every\n * operation is one transaction — a claim is a single `UPDATE … RETURNING`\n * — so two workers never take the same item. Messages live in a table of\n * their own, dropped with their item.\n */\nexport class SqliteQueueStore implements QueueStore {\n\treadonly #db: Database;\n\t#closed = false;\n\n\tprivate constructor(db: Database) {\n\t\tthis.#db = db;\n\t}\n\n\t/** Opens the store in `directory`, creating it if need be; what fails is `INVALID`. */\n\tstatic open(options: SqliteQueueStoreOptions): SqliteQueueStore {\n\t\treturn new SqliteQueueStore(openDatabase(options));\n\t}\n\n\t/** Lets go of the database; closing twice is fine. */\n\tclose(): void {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tthis.#db.close();\n\t}\n\n\tget #open(): Database {\n\t\tif (this.#closed) {\n\t\t\tthrow new QueueError('CLOSED', 'The queue store is closed');\n\t\t}\n\t\treturn this.#db;\n\t}\n\n\t#row(id: string, owner?: string): ItemRow | null {\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn db\n\t\t\t\t.query<ItemRow, [string]>(`SELECT ${COLUMNS} FROM items WHERE id = ?`)\n\t\t\t\t.get(id);\n\t\t}\n\t\treturn db\n\t\t\t.query<ItemRow, [string, string]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.get(id, owner);\n\t}\n\n\t#write(item: QueueItem): void {\n\t\tthis.#open\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET recipients = $recipients,\n\t\t\t\tnext_attempt_at = $next_attempt_at, attempts = $attempts,\n\t\t\t\tdelay_notified = $delay_notified, lease_owner = $lease_owner,\n\t\t\t\tlease_expires_at = $lease_expires_at WHERE id = $id`,\n\t\t\t)\n\t\t\t.run(stateOf(item));\n\t}\n\n\tasync add(item: NewQueueItem, options: AddOptions = {}): Promise<QueueItem> {\n\t\tcheckNewItem(item);\n\t\tconst max = checkMaxItems(options.maxItems);\n\t\tconst db = this.#open;\n\t\tconst added = newItem(crypto.randomUUID(), item);\n\t\tdb.transaction(() => {\n\t\t\tif (max !== undefined) {\n\t\t\t\tconst { n } = db\n\t\t\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t\t\t.get() as { n: number };\n\t\t\t\tif (n >= max) {\n\t\t\t\t\tthrow new QueueError(\n\t\t\t\t\t\t'QUEUE_FULL',\n\t\t\t\t\t\t`The queue holds ${n} items, its limit`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tinsertItem(db, added, item.message);\n\t\t}).immediate();\n\t\treturn added;\n\t}\n\n\tasync get(id: string): Promise<QueueItem | undefined> {\n\t\tconst row = this.#row(id);\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync list(options?: QueueListOptions): Promise<QueueItem[]> {\n\t\tconst { offset, limit } = checkList(options);\n\t\treturn this.#open\n\t\t\t.query<ItemRow, [number, number]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items ORDER BY next_attempt_at, seq\n\t\t\t\tLIMIT ? OFFSET ?`,\n\t\t\t)\n\t\t\t.all(limit, offset)\n\t\t\t.map(itemOf);\n\t}\n\n\tasync count(): Promise<number> {\n\t\tconst row = this.#open\n\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t.get();\n\t\treturn row?.n ?? 0;\n\t}\n\n\tasync readMessage(id: string): Promise<Uint8Array | undefined> {\n\t\tconst row = this.#open\n\t\t\t.query<{ content: Uint8Array }, [string]>(\n\t\t\t\t'SELECT content FROM messages WHERE item_id = ?',\n\t\t\t)\n\t\t\t.get(id);\n\t\treturn row ? new Uint8Array(row.content) : undefined;\n\t}\n\n\tasync claim(request: ClaimRequest): Promise<QueueItem | undefined> {\n\t\tcheckClaim(request);\n\t\tconst row = this.#open\n\t\t\t.query<ItemRow, Record<string, string | number>>(\n\t\t\t\t`UPDATE items SET lease_owner = $owner, lease_expires_at = $expires\n\t\t\t\tWHERE seq = (SELECT seq FROM items WHERE ${DUE}\n\t\t\t\t\tORDER BY next_attempt_at, seq LIMIT 1)\n\t\t\t\tRETURNING ${COLUMNS}`,\n\t\t\t)\n\t\t\t.get({\n\t\t\t\towner: request.owner,\n\t\t\t\tnow: request.now,\n\t\t\t\texpires: request.now + request.leaseMs,\n\t\t\t});\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync renew(id: string, owner: string, expiresAt: number): Promise<boolean> {\n\t\tcheckOwner(owner);\n\t\tcheckTime('expiresAt', expiresAt);\n\t\tconst { changes } = this.#open\n\t\t\t.query(\n\t\t\t\t'UPDATE items SET lease_expires_at = ? WHERE id = ? AND lease_owner = ?',\n\t\t\t)\n\t\t\t.run(expiresAt, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync complete(\n\t\tid: string,\n\t\towner: string,\n\t\tresult: AttemptResult,\n\t\tdsns: readonly NewQueueItem[],\n\t): Promise<Completed | undefined> {\n\t\tcheckComplete(owner, result, dsns);\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction((): Completed | undefined => {\n\t\t\t\tconst row = this.#row(id, owner);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tconst item = applyAttempt(itemOf(row), result);\n\t\t\t\tif (isDone(item)) {\n\t\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\t} else {\n\t\t\t\t\tthis.#write(item);\n\t\t\t\t}\n\t\t\t\tconst added = dsns.map((dsn) => {\n\t\t\t\t\tconst stored = newItem(crypto.randomUUID(), dsn);\n\t\t\t\t\tinsertItem(db, stored, dsn.message);\n\t\t\t\t\treturn stored;\n\t\t\t\t});\n\t\t\t\treturn { item, dsns: added };\n\t\t\t})\n\t\t\t.immediate();\n\t}\n\n\tasync reschedule(id: string, at: number, owner?: string): Promise<boolean> {\n\t\tcheckTime('at', at);\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn (\n\t\t\t\tdb\n\t\t\t\t\t.query('UPDATE items SET next_attempt_at = ? WHERE id = ?')\n\t\t\t\t\t.run(at, id).changes > 0\n\t\t\t);\n\t\t}\n\t\tcheckOwner(owner);\n\t\tconst { changes } = db\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET next_attempt_at = ?, lease_owner = NULL,\n\t\t\t\tlease_expires_at = NULL WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.run(at, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync cancel(id: string): Promise<QueueItem | undefined> {\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction(() => {\n\t\t\t\tconst row = this.#row(id);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\treturn itemOf(row);\n\t\t\t})\n\t\t\t.immediate();\n\t}\n}\n"
9
10
  ],
10
- "mappings": "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",
11
- "debugId": "92D7A7C42951038864756E2164756E21",
11
+ "mappings": ";;;;;;;;;;;;;;;;;;;AAKO,SAAS,UAAU,CACzB,IACA,OACA,SACO;AAAA,EACP,GAAG,MACF;AAAA;AAAA;AAAA,gCAID,EAAE,IAAI;AAAA,IACL,IAAI,MAAM;AAAA,IACV,QAAQ,MAAM;AAAA,IACd,YAAY,KAAK,UAAU,MAAM,UAAU;AAAA,IAC3C,MAAM,MAAM;AAAA,IACZ,YAAY,MAAM;AAAA,IAClB,iBAAiB,MAAM;AAAA,IACvB,KAAK,UAAU,KAAK;AAAA,EACrB,CAAC;AAAA,EACD,GAAG,MAAM,uDAAuD,EAAE,IACjE,MAAM,IACN,OACD;AAAA;;;AC3BD;AACA;AACA;;;ACMO,IAAM,aAAgC;AAAA,EAE5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA;AACD;AAOO,SAAS,OAAO,CAAC,IAAoB;AAAA,EAC3C,GAAG,YAAY,MAAM;AAAA,IACpB,QAAQ,cAAc,YAAY,GAChC,MAAoC,qBAAqB,EACzD,IAAI;AAAA,IACN,IAAI,UAAU,WAAW,QAAQ;AAAA,MAChC,MAAM,IAAI,YACT,WACA,qCAAqC,oCAAoC,WAAW,QACrF;AAAA,IACD;AAAA,IACA,WAAW,OAAO,WAAW,MAAM,OAAO;AAAA,MAAG,GAAG,KAAK,GAAG;AAAA,IACxD,IAAI,UAAU,WAAW,QAAQ;AAAA,MAChC,GAAG,KAAK,yBAAyB,WAAW,QAAQ;AAAA,IACrD;AAAA,GACA,EAAE,UAAU;AAAA;;;AD5Cd,IAAM,oBAAoB;AAC1B,IAAM,eAAe;AAOrB,SAAS,SAAS,CAAC,IAAc,aAA2B;AAAA,EAC3D,GAAG,KAAK,yBAAyB,aAAa;AAAA,EAC9C,GAAG,KAAK,2BAA2B;AAAA,EACnC,GAAG,KAAK,2BAA2B;AAAA,EAEnC,GAAG,KAAK,uBAAuB;AAAA,EAC/B,GAAG,KAAK,0BAA0B;AAAA;AAGnC,SAAS,WAAW,CAAC,MAAoB;AAAA,EACxC,WAAW,QAAQ,CAAC,MAAM,GAAG,YAAY,GAAG,UAAU,GAAG;AAAA,IACxD,IAAI,WAAW,IAAI;AAAA,MAAG,UAAU,MAAM,YAAY;AAAA,EACnD;AAAA;AAGD,SAAS,aAAa,CAAC,OAAwB;AAAA,EAC9C,IAAI,UAAU;AAAA,IAAW,OAAO;AAAA,EAChC,IAAI,OAAO,UAAU,YAAY,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GAAG;AAAA,IACvE,MAAM,IAAI,YACT,WACA,qDAAqD,OACtD;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAGR,IAAM,YAAY,CAAC,UAClB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAM/C,SAAS,YAAY,CAAC,SAA4C;AAAA,EACxE,MAAM,YAAa,SAAiD;AAAA,EACpE,IAAI,OAAO,cAAc,YAAY,cAAc,IAAI;AAAA,IACtD,MAAM,IAAI,YAAW,WAAW,wCAAwC;AAAA,EACzE;AAAA,EACA,MAAM,cAAc,cAAc,QAAQ,WAAW;AAAA,EACrD,IAAI;AAAA,EACJ,IAAI;AAAA,IACH,IAAI,CAAC,WAAW,SAAS,GAAG;AAAA,MAC3B,UAAU,WAAW,EAAE,WAAW,MAAM,MAAM,kBAAkB,CAAC;AAAA,MAEjE,UAAU,WAAW,iBAAiB;AAAA,IACvC;AAAA,IACA,MAAM,OAAO,KAAK,WAAW,cAAc;AAAA,IAC3C,KAAK,IAAI,SAAS,MAAM,EAAE,QAAQ,MAAM,WAAW,MAAM,QAAQ,KAAK,CAAC;AAAA,IACvE,UAAU,IAAI,WAAW;AAAA,IACzB,YAAY,IAAI;AAAA,IAChB,QAAQ,EAAE;AAAA,IACV,OAAO;AAAA,IACN,OAAO,OAAO;AAAA,IACf,IAAI,MAAM;AAAA,IACV,IAAI,iBAAiB;AAAA,MAAY,MAAM;AAAA,IACvC,MAAM,IAAI,YACT,WACA,iBAAiB,gCAAgC,UAAU,KAAK,GACjE;AAAA;AAAA;;;AE3DK,IAAM,UACZ;AAEM,SAAS,MAAM,CAAC,KAAyB;AAAA,EAC/C,MAAM,OAAkB;AAAA,IACvB,IAAI,IAAI;AAAA,IACR,MAAM,IAAI;AAAA,IACV,YAAY,KAAK,MAAM,IAAI,UAAU;AAAA,IACrC,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,eAAe,IAAI;AAAA,IACnB,UAAU,IAAI;AAAA,IACd,eAAe,IAAI,mBAAmB;AAAA,OACnC,UAAU,IAAI,GAAG;AAAA,EACrB;AAAA,EACA,IAAI,IAAI,gBAAgB,QAAQ,IAAI,qBAAqB;AAAA,IAAM,OAAO;AAAA,EACtE,OAAO;AAAA,OACH;AAAA,IACH,OAAO,EAAE,OAAO,IAAI,aAAa,WAAW,IAAI,iBAAiB;AAAA,EAClE;AAAA;AAIM,SAAS,OAAO,CAAC,MAAiB;AAAA,EACxC,OAAO;AAAA,IACN,IAAI,KAAK;AAAA,IACT,YAAY,KAAK,UAAU,KAAK,UAAU;AAAA,IAC1C,iBAAiB,KAAK;AAAA,IACtB,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK,gBAAgB,IAAI;AAAA,IACzC,aAAa,KAAK,OAAO,SAAS;AAAA,IAClC,kBAAkB,KAAK,OAAO,aAAa;AAAA,EAC5C;AAAA;;;ACpBD,IAAM,MAAM;AAAA;AAAA;AAUL,MAAM,iBAAuC;AAAA,EAC1C;AAAA,EACT,UAAU;AAAA,EAEF,WAAW,CAAC,IAAc;AAAA,IACjC,KAAK,MAAM;AAAA;AAAA,SAIL,IAAI,CAAC,SAAoD;AAAA,IAC/D,OAAO,IAAI,iBAAiB,aAAa,OAAO,CAAC;AAAA;AAAA,EAIlD,KAAK,GAAS;AAAA,IACb,IAAI,KAAK;AAAA,MAAS;AAAA,IAClB,KAAK,UAAU;AAAA,IACf,KAAK,IAAI,MAAM;AAAA;AAAA,MAGZ,KAAK,GAAa;AAAA,IACrB,IAAI,KAAK,SAAS;AAAA,MACjB,MAAM,IAAI,YAAW,UAAU,2BAA2B;AAAA,IAC3D;AAAA,IACA,OAAO,KAAK;AAAA;AAAA,EAGb,IAAI,CAAC,IAAY,OAAgC;AAAA,IAChD,MAAM,KAAK,KAAK;AAAA,IAChB,IAAI,UAAU,WAAW;AAAA,MACxB,OAAO,GACL,MAAyB,UAAU,iCAAiC,EACpE,IAAI,EAAE;AAAA,IACT;AAAA,IACA,OAAO,GACL,MACA,UAAU,qDACX,EACC,IAAI,IAAI,KAAK;AAAA;AAAA,EAGhB,MAAM,CAAC,MAAuB;AAAA,IAC7B,KAAK,MACH,MACA;AAAA;AAAA;AAAA,wDAID,EACC,IAAI,QAAQ,IAAI,CAAC;AAAA;AAAA,QAGd,GAAG,CAAC,MAAoB,UAAsB,CAAC,GAAuB;AAAA,IAC3E,aAAa,IAAI;AAAA,IACjB,MAAM,MAAM,cAAc,QAAQ,QAAQ;AAAA,IAC1C,MAAM,KAAK,KAAK;AAAA,IAChB,MAAM,QAAQ,QAAQ,OAAO,WAAW,GAAG,IAAI;AAAA,IAC/C,GAAG,YAAY,MAAM;AAAA,MACpB,IAAI,QAAQ,WAAW;AAAA,QACtB,QAAQ,MAAM,GACZ,MAAyB,iCAAiC,EAC1D,IAAI;AAAA,QACN,IAAI,KAAK,KAAK;AAAA,UACb,MAAM,IAAI,YACT,cACA,mBAAmB,oBACpB;AAAA,QACD;AAAA,MACD;AAAA,MACA,WAAW,IAAI,OAAO,KAAK,OAAO;AAAA,KAClC,EAAE,UAAU;AAAA,IACb,OAAO;AAAA;AAAA,QAGF,GAAG,CAAC,IAA4C;AAAA,IACrD,MAAM,MAAM,KAAK,KAAK,EAAE;AAAA,IACxB,OAAO,MAAM,OAAO,GAAG,IAAI;AAAA;AAAA,QAGtB,IAAI,CAAC,SAAkD;AAAA,IAC5D,QAAQ,QAAQ,UAAU,UAAU,OAAO;AAAA,IAC3C,OAAO,KAAK,MACV,MACA,UAAU;AAAA,qBAEX,EACC,IAAI,OAAO,MAAM,EACjB,IAAI,MAAM;AAAA;AAAA,QAGP,KAAK,GAAoB;AAAA,IAC9B,MAAM,MAAM,KAAK,MACf,MAAyB,iCAAiC,EAC1D,IAAI;AAAA,IACN,OAAO,KAAK,KAAK;AAAA;AAAA,QAGZ,WAAW,CAAC,IAA6C;AAAA,IAC9D,MAAM,MAAM,KAAK,MACf,MACA,gDACD,EACC,IAAI,EAAE;AAAA,IACR,OAAO,MAAM,IAAI,WAAW,IAAI,OAAO,IAAI;AAAA;AAAA,QAGtC,KAAK,CAAC,SAAuD;AAAA,IAClE,WAAW,OAAO;AAAA,IAClB,MAAM,MAAM,KAAK,MACf,MACA;AAAA,+CAC2C;AAAA;AAAA,gBAE/B,SACb,EACC,IAAI;AAAA,MACJ,OAAO,QAAQ;AAAA,MACf,KAAK,QAAQ;AAAA,MACb,SAAS,QAAQ,MAAM,QAAQ;AAAA,IAChC,CAAC;AAAA,IACF,OAAO,MAAM,OAAO,GAAG,IAAI;AAAA;AAAA,QAGtB,KAAK,CAAC,IAAY,OAAe,WAAqC;AAAA,IAC3E,WAAW,KAAK;AAAA,IAChB,UAAU,aAAa,SAAS;AAAA,IAChC,QAAQ,YAAY,KAAK,MACvB,MACA,wEACD,EACC,IAAI,WAAW,IAAI,KAAK;AAAA,IAC1B,OAAO,UAAU;AAAA;AAAA,QAGZ,QAAQ,CACb,IACA,OACA,QACA,MACiC;AAAA,IACjC,cAAc,OAAO,QAAQ,IAAI;AAAA,IACjC,MAAM,KAAK,KAAK;AAAA,IAChB,OAAO,GACL,YAAY,MAA6B;AAAA,MACzC,MAAM,MAAM,KAAK,KAAK,IAAI,KAAK;AAAA,MAC/B,IAAI,CAAC;AAAA,QAAK;AAAA,MACV,MAAM,OAAO,aAAa,OAAO,GAAG,GAAG,MAAM;AAAA,MAC7C,IAAI,OAAO,IAAI,GAAG;AAAA,QACjB,GAAG,MAAM,gCAAgC,EAAE,IAAI,EAAE;AAAA,MAClD,EAAE,KAAK;AAAA,QACN,KAAK,OAAO,IAAI;AAAA;AAAA,MAEjB,MAAM,QAAQ,KAAK,IAAI,CAAC,QAAQ;AAAA,QAC/B,MAAM,SAAS,QAAQ,OAAO,WAAW,GAAG,GAAG;AAAA,QAC/C,WAAW,IAAI,QAAQ,IAAI,OAAO;AAAA,QAClC,OAAO;AAAA,OACP;AAAA,MACD,OAAO,EAAE,MAAM,MAAM,MAAM;AAAA,KAC3B,EACA,UAAU;AAAA;AAAA,QAGP,UAAU,CAAC,IAAY,IAAY,OAAkC;AAAA,IAC1E,UAAU,MAAM,EAAE;AAAA,IAClB,MAAM,KAAK,KAAK;AAAA,IAChB,IAAI,UAAU,WAAW;AAAA,MACxB,OACC,GACE,MAAM,mDAAmD,EACzD,IAAI,IAAI,EAAE,EAAE,UAAU;AAAA,IAE1B;AAAA,IACA,WAAW,KAAK;AAAA,IAChB,QAAQ,YAAY,GAClB,MACA;AAAA,6DAED,EACC,IAAI,IAAI,IAAI,KAAK;AAAA,IACnB,OAAO,UAAU;AAAA;AAAA,QAGZ,MAAM,CAAC,IAA4C;AAAA,IACxD,MAAM,KAAK,KAAK;AAAA,IAChB,OAAO,GACL,YAAY,MAAM;AAAA,MAClB,MAAM,MAAM,KAAK,KAAK,EAAE;AAAA,MACxB,IAAI,CAAC;AAAA,QAAK;AAAA,MACV,GAAG,MAAM,gCAAgC,EAAE,IAAI,EAAE;AAAA,MACjD,OAAO,OAAO,GAAG;AAAA,KACjB,EACA,UAAU;AAAA;AAEd;",
12
+ "debugId": "69534685BB7970EA64756E2164756E21",
12
13
  "names": []
13
14
  }
@@ -0,0 +1,5 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { QueueItem } from '../contract/types';
3
+ /** Stores a new item and its message; the caller holds the transaction. */
4
+ export declare function insertItem(db: Database, added: QueueItem, message: Uint8Array): void;
5
+ //# sourceMappingURL=insert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../src/sqlite/insert.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,2EAA2E;AAC3E,wBAAgB,UAAU,CACzB,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,UAAU,GACjB,IAAI,CAmBN"}
@@ -1,5 +1,5 @@
1
1
  import type { QueueStore } from '../contract/queue-store';
2
- import type { AddOptions, AttemptResult, ClaimRequest, NewQueueItem, QueueItem, QueueListOptions } from '../contract/types';
2
+ import type { AddOptions, AttemptResult, ClaimRequest, Completed, NewQueueItem, QueueItem, QueueListOptions } from '../contract/types';
3
3
  import type { SqliteQueueStoreOptions } from './options';
4
4
  export type { SqliteQueueStoreOptions } from './options';
5
5
  /**
@@ -23,7 +23,7 @@ export declare class SqliteQueueStore implements QueueStore {
23
23
  readMessage(id: string): Promise<Uint8Array | undefined>;
24
24
  claim(request: ClaimRequest): Promise<QueueItem | undefined>;
25
25
  renew(id: string, owner: string, expiresAt: number): Promise<boolean>;
26
- complete(id: string, owner: string, result: AttemptResult): Promise<QueueItem | undefined>;
26
+ complete(id: string, owner: string, result: AttemptResult, dsns: readonly NewQueueItem[]): Promise<Completed | undefined>;
27
27
  reschedule(id: string, at: number, owner?: string): Promise<boolean>;
28
28
  cancel(id: string): Promise<QueueItem | undefined>;
29
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sqlite/store.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAGzD,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAKzD;;;;;;GAMG;AACH,qBAAa,gBAAiB,YAAW,UAAU;;IAIlD,OAAO;IAIP,uFAAuF;IACvF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB;IAI/D,sDAAsD;IACtD,KAAK,IAAI,IAAI;IAsCP,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC;IAuCrE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAK/C,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAWtD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAOxB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IASxD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAiB5D,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrE,QAAQ,CACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAmB3B,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBpE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;CAWxD"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sqlite/store.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAGzD,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAKzD;;;;;;GAMG;AACH,qBAAa,gBAAiB,YAAW,UAAU;;IAIlD,OAAO;IAIP,uFAAuF;IACvF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB;IAI/D,sDAAsD;IACtD,KAAK,IAAI,IAAI;IAsCP,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC;IAsBrE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAK/C,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAWtD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAOxB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IASxD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAiB5D,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrE,QAAQ,CACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,SAAS,YAAY,EAAE,GAC3B,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAuB3B,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBpE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;CAWxD"}
package/docs/guide.md CHANGED
@@ -296,6 +296,21 @@ ran, and nothing was recorded. An item that is gone along with its
296
296
  message (cancelled, or finished by another worker) is left alone, as
297
297
  before.
298
298
 
299
+ The DSNs of an outcome are built before `complete` and stored by it, in
300
+ the same step as the outcome, so a crash or a store error cannot leave
301
+ the outcome recorded without its failure, relayed or delay DSN. They are
302
+ delivered at least once: a DSN stored by `complete` is an item like any
303
+ other, and a worker that crashes after the step but before the `dsn` event
304
+ only misses the event. When a DSN cannot be built (a `MimeError`, say),
305
+ the outcome is recorded without it and the `error` event says so, once the
306
+ outcome is recorded; a delayed DSN that cannot be built leaves
307
+ `delayNotified` set, since building it again would fail the same way. When
308
+ the store's write of a DSN fails, `complete` throws, nothing is recorded
309
+ and the item is redelivered after its lease expires. The `delivered` and
310
+ `failed` DSNs an application writes for a local delivery are its own
311
+ `enqueue`: a crash between the delivery and that call gives a duplicate
312
+ message, not a lost DSN.
313
+
299
314
  A store keeps an item from being claimed before its message can be read
300
315
  — the message written first, or both in one step — so a message missing
301
316
  at a claim is always damage, never an add still under way. Every store
@@ -992,6 +1007,20 @@ codes, and keeps its promises:
992
1007
  of the lease, in one step and only for the lease's owner; a final
993
1008
  status is never changed; an item whose every recipient is final is
994
1009
  dropped with its message.
1010
+ - `complete(id, owner, result, dsns)` stores the DSN items the outcome
1011
+ calls for (`dsns`, each a `NewQueueItem` as `add` takes it) in the same
1012
+ step as the outcome, and returns `{ item, dsns }` (`Completed`): the item
1013
+ as it now stands, and the items inserted, in the order given. All or
1014
+ nothing: a lost lease (not the owner, or the item gone) returns
1015
+ `undefined` and writes no DSN; a DSN the store cannot keep throws and
1016
+ leaves the item as it was, lease held, so it is redelivered once the
1017
+ lease expires. A bad DSN is refused as `INVALID` before the store opens
1018
+ a connection or a transaction. DSNs ignore `limits.maxItems`: a full
1019
+ queue still bounces, and the count may exceed the limit by the DSNs it
1020
+ holds. A store that returns the bare item, as stores written
1021
+ before 0.11 do, still runs, but its DSNs are added one by one, outside
1022
+ the step (see [Queue store
1023
+ outdated](./troubleshooting.md#queueerror-queue-store-outdated-complete-returned-the-item-alone-not--item-dsns--its-dsns-are-added-one-by-one-outside-the-step-that-records-the-outcome)).
995
1024
  - `complete` records a recipient's `mayHaveReachedMx: true` and
996
1025
  `mayHaveReachedFallback: true` (on its `RecipientUpdate`) and keeps
997
1026
  them: `get`, `list` and `claim` return it on
package/docs/roadmap.md CHANGED
@@ -6,7 +6,7 @@ only number on it.
6
6
 
7
7
  ## Now
8
8
 
9
- Nothing is under way besides what is marked shipped below.
9
+ Nothing under way.
10
10
 
11
11
  ## Next
12
12
 
@@ -20,9 +20,6 @@ Nothing is under way besides what is marked shipped below.
20
20
  connections.
21
21
  - **The contract's specs, exported**, so a store written outside this
22
22
  package can hold itself to them.
23
- - **A DSN that cannot be lost on a crash** — today a crash between
24
- recording a failure and enqueuing its DSN loses that DSN; the store
25
- will take both in one step.
26
23
 
27
24
  ## Later
28
25
 
@@ -49,7 +46,27 @@ Nothing is under way besides what is marked shipped below.
49
46
 
50
47
  ## Shipped
51
48
 
52
- ### Unreleased
49
+ ### 0.11.0
50
+
51
+ - **A DSN recorded in the same step as the outcome** — the failure, relayed
52
+ and delay DSNs are written in the same store step as the outcome they
53
+ report, in the memory, SQLite, PostgreSQL and Redis stores, so a crash or
54
+ a failed add no longer loses them. Breaking for a custom `QueueStore`:
55
+ `complete(id, owner, result, dsns)` returns `{ item, dsns }`; a store that
56
+ answers the old shape is reported on the `error` event and its DSNs fall
57
+ back to `add`. The `delivered` and `failed` DSNs a server writes for a
58
+ local delivery are not this gap: a crash there gives a duplicate, not a
59
+ loss.
60
+
61
+ ### 0.10.0
62
+
63
+ - **`failedDsn()`** — builds the `failed` DSN of a recipient the
64
+ application could not deliver locally, the counterpart of `deliveredDsn()`:
65
+ it follows `NOTIFY` (none for `NEVER` or a list without `FAILURE`, none for
66
+ a null reverse-path), `ORCPT`, `ENVID` and `RET`; `DEFAULT_MAX_RETURN` (64
67
+ KiB) is the default of what a DSN returns of the original.
68
+
69
+ ### 0.9.0
53
70
 
54
71
  - **DSN requests (RFC 3461)**: the queue honours a sender's `NOTIFY`, `RET`,
55
72
  `ENVID` and `ORCPT` given at enqueue. `NOTIFY=NEVER` silences failure
@@ -58,22 +75,31 @@ Nothing is under way besides what is marked shipped below.
58
75
  `Original-Recipient`; and `deliveredDsn()` builds the `delivered` DSN for
59
76
  a local delivery. The SQLite and PostgreSQL stores run migration 2: stop
60
77
  every old worker before starting the new ones.
61
- - **The MX's doubt is remembered** — a recipient whose session by MX ended
62
- after the final dot with no reply is marked `mayHaveReachedMx`, kept by
63
- every store: no later attempt falls back to the smarthost on its own, it
64
- stays on the MX until `retry.giveUpAfter` (a `4.4.7` DSN that says so),
65
- and the `deferred` and `failed` events carry the mark.
78
+
79
+ ### 0.8.0
80
+
66
81
  - **The fallback smarthost's doubt is remembered** — a recipient whose
67
82
  session by the fallback smarthost ended after the final dot with no reply
68
83
  is marked `mayHaveReachedFallback`, kept by every store: later attempts
69
84
  skip the MX for it and go to the smarthost again, until
70
85
  `retry.giveUpAfter` (a `4.4.7` DSN that says so), and the `deferred` and
71
86
  `failed` events carry the mark.
87
+
88
+ ### 0.7.1
89
+
72
90
  - **Only the recipients the MX did not refuse are in doubt** — in a session that
73
91
  failed after the final dot, a recipient the MX refused at `RCPT` is
74
92
  settled from its own reply (5xx failed, 4xx deferred) and not marked, so
75
93
  it may fall back to the smarthost.
76
94
 
95
+ ### 0.7.0
96
+
97
+ - **The MX's doubt is remembered** — a recipient whose session by MX ended
98
+ after the final dot with no reply is marked `mayHaveReachedMx`, kept by
99
+ every store: no later attempt falls back to the smarthost on its own, it
100
+ stays on the MX until `retry.giveUpAfter` (a `4.4.7` DSN that says so),
101
+ and the `deferred` and `failed` events carry the mark.
102
+
77
103
  ### 0.5.0
78
104
 
79
105
  - **A fallback smarthost** — `fallback`: a recipient
@@ -31,6 +31,7 @@ parts shown as … vary.
31
31
  - [`QueueError: dsn.returnContent must be 'headers' or 'full', not …`](#queueerror-dsnreturncontent-must-be-headers-or-full-not-)
32
32
  - [`QueueError: createQueue() takes an options object`](#queueerror-createqueue-takes-an-options-object)
33
33
  - [`QueueError: owner must be a non-empty string`](#queueerror-owner-must-be-a-non-empty-string)
34
+ - [`QueueError: dsns must be an array`](#queueerror-dsns-must-be-an-array)
34
35
 
35
36
  **The `error` event**
36
37
 
@@ -39,6 +40,8 @@ parts shown as … vary.
39
40
  - [`QueueError: The lease on … expired before its outcome was recorded, and the item is gone: lost to another worker that finished it, the message then sent twice, or cancelled`](#queueerror-the-lease-on--expired-before-its-outcome-was-recorded-and-the-item-is-gone-lost-to-another-worker-that-finished-it-the-message-then-sent-twice-or-cancelled)
40
41
  - [`QueueError: The lease on … was lost while it was delivered, and the item is gone: finished by the worker that took it, the message then sent twice, or cancelled`](#queueerror-the-lease-on--was-lost-while-it-was-delivered-and-the-item-is-gone-finished-by-the-worker-that-took-it-the-message-then-sent-twice-or-cancelled)
41
42
  - [`QueueError: The message of … is unreadable: the store holds the item but not its message, so every pending recipient failed`](#queueerror-the-message-of--is-unreadable-the-store-holds-the-item-but-not-its-message-so-every-pending-recipient-failed)
43
+ - [`QueueError: Queue store outdated: complete returned the item alone, not { item, dsns }; its DSNs are added one by one, outside the step that records the outcome`](#queueerror-queue-store-outdated-complete-returned-the-item-alone-not--item-dsns--its-dsns-are-added-one-by-one-outside-the-step-that-records-the-outcome)
44
+ - [`QueueError: The queue store stored … of the … DSNs of …: a … DSN to … is lost`](#queueerror-the-queue-store-stored--of-the--dsns-of--a--dsn-to--is-lost)
42
45
  - [`SmtpError: sendMail(): …` (`INVALID_OPTION`), with recipients deferred as `4.3.5`](#smtperror-sendmail--invalid_option-with-recipients-deferred-as-435)
43
46
  - [`SQLiteError: database is locked`, or another store error, during a delivery](#sqliteerror-database-is-locked-or-another-store-error-during-a-delivery)
44
47
  - [`PostgresError: …`, or a connection error, during a delivery](#postgreserror--or-a-connection-error-during-a-delivery)
@@ -302,6 +305,21 @@ or `… must be a number from … to …, not …`. A number option out of its r
302
305
 
303
306
  **Code:** `INVALID`.
304
307
 
308
+ ### `QueueError: dsns must be an array`
309
+
310
+ **Code:** `INVALID`.
311
+ **When:** `complete(id, owner, result, dsns)` of a store of this package
312
+ was called with something other than an array as `dsns`, typically by a
313
+ caller of the store that still uses the three-argument form.
314
+ **Why:** since 0.11 `complete` takes the DSN items the outcome calls for
315
+ (an empty array for none); the store checks them before it opens a
316
+ connection or a transaction, so nothing was written.
317
+ **Fix:** pass an array, `[]` when the outcome calls for no DSN:
318
+
319
+ ```ts
320
+ await store.complete(id, owner, result, []);
321
+ ```
322
+
305
323
  ### `QueueError: owner must be a non-empty string`
306
324
 
307
325
  **Code:** `INVALID`, also as
@@ -395,6 +413,51 @@ guide's [Redis](guide.md#redis)); never delete a queue's keys or rows by
395
413
  hand, `cancel(id)` drops an item with its message. The sender has the
396
414
  DSN and may send again.
397
415
 
416
+ ### `QueueError: Queue store outdated: complete returned the item alone, not { item, dsns }; its DSNs are added one by one, outside the step that records the outcome`
417
+
418
+ **Code:** `INVALID`, on the `error` event, with the item's `id`.
419
+ **When:** a custom `QueueStore` still answers `complete` as before 0.11,
420
+ with the bare `QueueItem`, and an
421
+ outcome calls for a DSN (a failure, a `relayed` or a delay).
422
+ **Why:** since 0.11 the queue hands the DSNs to `complete`, which stores
423
+ them in the same step as the outcome and returns `{ item, dsns }`. A store
424
+ that answers with the item alone has stored none of them: the queue adds
425
+ each one with `add` afterwards, as it did before, so a crash or an `add`
426
+ error between the two steps still loses a DSN, and an `add` that fails is
427
+ one more `error` event. The stores of this package are not affected.
428
+ **Fix:** port the store's `complete`: take `dsns`, insert them in the
429
+ transaction (or script) that records the outcome, and return them (the
430
+ `this.*` calls are your store's own helpers, a sketch):
431
+
432
+ ```ts
433
+ async complete(id, owner, result, dsns) {
434
+ return this.transaction(async () => {
435
+ const item = await this.apply(id, owner, result);
436
+ if (!item) return undefined; // lease lost: no DSN written
437
+ const stored = [];
438
+ for (const dsn of dsns) stored.push(await this.insert(dsn)); // in order
439
+ return { item, dsns: stored };
440
+ });
441
+ }
442
+ ```
443
+
444
+ See [Writing a store](./guide.md#writing-a-store).
445
+
446
+ ### `QueueError: The queue store stored … of the … DSNs of …: a … DSN to … is lost`
447
+
448
+ **Code:** `INVALID`, on the `error` event, with the item's `id`.
449
+ **When:** a custom `QueueStore`'s `complete` returned `{ item, dsns }` with
450
+ fewer DSNs than it was given, and an outcome called for them (a failure, a
451
+ `relayed` or a delay).
452
+ **Why:** the queue matches the returned DSNs to the ones it asked for, in
453
+ order. A DSN with no stored counterpart was not kept, so its notice to the
454
+ sender is lost: no `dsn` event is emitted for it. The stores of this
455
+ package return every DSN or throw.
456
+ **Fix:** make `complete` return one stored item per DSN, in the order
457
+ given, or throw to leave the item leased (see the sketch under
458
+ [Queue store outdated](#queueerror-queue-store-outdated-complete-returned-the-item-alone-not--item-dsns--its-dsns-are-added-one-by-one-outside-the-step-that-records-the-outcome)
459
+ and [Writing a store](./guide.md#writing-a-store)).
460
+
398
461
  ### `SmtpError: sendMail(): …` (`INVALID_OPTION`), with recipients deferred as `4.3.5`
399
462
 
400
463
  **When:** `sendMail` refused the options of a session — the route, the
@@ -720,7 +783,7 @@ queue's own outcomes always have one.
720
783
  await store.complete(id, owner, {
721
784
  ...result,
722
785
  recipients: [{ address, status: 'deferred', reply: { status: '4.4.1', text: 'No answer' } }],
723
- });
786
+ }, []); // the DSN items the outcome calls for, none here
724
787
  ```
725
788
 
726
789
  ### `QueueError: reply.status must be a string`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bumail/queue",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "The outbound mail queue of a mail server: every recipient's state, retries with back-off, delivery status notifications (RFC 3464), leases for several workers, and a memory, a bun:sqlite, a PostgreSQL (Bun.sql) and a Redis (Bun.redis) store, delivering through @bumail/smtp/client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -65,14 +65,14 @@
65
65
  ]
66
66
  },
67
67
  "devDependencies": {
68
- "@bumail/dns": "^0.2.1",
68
+ "@bumail/dns": "^0.2.2",
69
69
  "@bumail/mime": "^0.2.0",
70
- "@bumail/smtp": "^0.11.0",
70
+ "@bumail/smtp": "^0.11.1",
71
71
  "@types/bun": "^1.4.2"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "@bumail/mime": "^0.2.0",
75
- "@bumail/smtp": "^0.11.0",
75
+ "@bumail/smtp": "^0.11.1",
76
76
  "typescript": "^6.0.3 || ^7.0.0"
77
77
  },
78
78
  "peerDependenciesMeta": {