@bumail/queue 0.10.0 → 0.12.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.
- package/README.md +52 -1
- package/dist/chunks/{index-sz13a17b.js → index-t6q7zxgj.js} +14 -3
- package/dist/chunks/{index-sz13a17b.js.map → index-t6q7zxgj.js.map} +4 -4
- package/dist/conformance/claims.d.ts +4 -0
- package/dist/conformance/claims.d.ts.map +1 -0
- package/dist/conformance/complete-dsns.d.ts +4 -0
- package/dist/conformance/complete-dsns.d.ts.map +1 -0
- package/dist/conformance/concurrency.d.ts +4 -0
- package/dist/conformance/concurrency.d.ts.map +1 -0
- package/dist/conformance/describe-queue-store.d.ts +11 -0
- package/dist/conformance/describe-queue-store.d.ts.map +1 -0
- package/dist/conformance/doubt.d.ts +5 -0
- package/dist/conformance/doubt.d.ts.map +1 -0
- package/dist/conformance/dsn.d.ts +4 -0
- package/dist/conformance/dsn.d.ts.map +1 -0
- package/dist/conformance/index.d.ts +3 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +820 -0
- package/dist/conformance/index.js.map +18 -0
- package/dist/conformance/items.d.ts +4 -0
- package/dist/conformance/items.d.ts.map +1 -0
- package/dist/conformance/outcomes.d.ts +4 -0
- package/dist/conformance/outcomes.d.ts.map +1 -0
- package/dist/conformance/setup.d.ts +40 -0
- package/dist/conformance/setup.d.ts.map +1 -0
- package/dist/contract/checks.d.ts +4 -0
- package/dist/contract/checks.d.ts.map +1 -1
- package/dist/contract/queue-store.d.ts +15 -7
- package/dist/contract/queue-store.d.ts.map +1 -1
- package/dist/contract/types.d.ts +5 -0
- package/dist/contract/types.d.ts.map +1 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +64 -26
- package/dist/index.js.map +4 -4
- package/dist/memory/index.js +10 -6
- package/dist/memory/index.js.map +3 -3
- package/dist/memory/store.d.ts +2 -2
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/postgres/complete.d.ts +13 -0
- package/dist/postgres/complete.d.ts.map +1 -0
- package/dist/postgres/index.js +85 -74
- package/dist/postgres/index.js.map +6 -5
- package/dist/postgres/store.d.ts +2 -2
- package/dist/postgres/store.d.ts.map +1 -1
- package/dist/queue/deliver.d.ts.map +1 -1
- package/dist/queue/notify.d.ts +35 -6
- package/dist/queue/notify.d.ts.map +1 -1
- package/dist/redis/complete.d.ts +11 -0
- package/dist/redis/complete.d.ts.map +1 -0
- package/dist/redis/index.js +133 -110
- package/dist/redis/index.js.map +7 -6
- package/dist/redis/scripts.d.ts +8 -5
- package/dist/redis/scripts.d.ts.map +1 -1
- package/dist/redis/store.d.ts +2 -2
- package/dist/redis/store.d.ts.map +1 -1
- package/dist/sqlite/index.js +29 -20
- package/dist/sqlite/index.js.map +5 -4
- package/dist/sqlite/insert.d.ts +5 -0
- package/dist/sqlite/insert.d.ts.map +1 -0
- package/dist/sqlite/store.d.ts +2 -2
- package/dist/sqlite/store.d.ts.map +1 -1
- package/docs/guide.md +113 -2
- package/docs/roadmap.md +46 -12
- package/docs/troubleshooting.md +106 -1
- package/package.json +11 -5
package/dist/sqlite/index.js.map
CHANGED
|
@@ -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\
|
|
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": "
|
|
11
|
+
"mappings": "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",
|
|
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"}
|
package/dist/sqlite/store.d.ts
CHANGED
|
@@ -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<
|
|
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":"
|
|
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
|
|
@@ -1017,8 +1046,90 @@ codes, and keeps its promises:
|
|
|
1017
1046
|
The queue never gives a store such text: it cleans every reply.
|
|
1018
1047
|
|
|
1019
1048
|
The contract's specs, `describeQueueStore`, which every store here runs,
|
|
1020
|
-
are
|
|
1021
|
-
|
|
1049
|
+
are exported as `@bumail/queue/conformance`: see
|
|
1050
|
+
[Holding your own store to the contract](#holding-your-own-store-to-the-contract).
|
|
1051
|
+
|
|
1052
|
+
## Holding your own store to the contract
|
|
1053
|
+
|
|
1054
|
+
A store you write (over a file, a database, a service) is only a
|
|
1055
|
+
`QueueStore` if it answers the contract above to the letter, in the cases
|
|
1056
|
+
nobody thinks of: a lease that expired, a DSN that fails the checks, a
|
|
1057
|
+
second process claiming at the same time. `describeQueueStore` is the
|
|
1058
|
+
suite the memory, SQLite, PostgreSQL and Redis stores run; call
|
|
1059
|
+
it from a spec of yours and it registers them.
|
|
1060
|
+
|
|
1061
|
+
**It needs Bun.** The suite imports `bun:test`, so the spec runs with
|
|
1062
|
+
`bun test`, not with Node, Vitest or Jest. It is a development
|
|
1063
|
+
dependency: install `@bumail/queue` and write the spec.
|
|
1064
|
+
|
|
1065
|
+
```sh
|
|
1066
|
+
bun add -d @bumail/queue
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
```ts
|
|
1070
|
+
// my-store.spec.ts
|
|
1071
|
+
import { describeQueueStore } from '@bumail/queue/conformance';
|
|
1072
|
+
import { MyQueueStore } from './my-store';
|
|
1073
|
+
|
|
1074
|
+
describeQueueStore('MyQueueStore', {
|
|
1075
|
+
// a new store, empty: called once per test
|
|
1076
|
+
create: () => MyQueueStore.open({ url: process.env.MY_STORE_URL }),
|
|
1077
|
+
// another handle on the same queue, as a second connection or process
|
|
1078
|
+
share: (store) => (store as MyQueueStore).connectAgain(),
|
|
1079
|
+
// closes a store `create` made, after the test that used it
|
|
1080
|
+
dispose: (store) => (store as MyQueueStore).close(),
|
|
1081
|
+
});
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
```sh
|
|
1085
|
+
bun test my-store.spec.ts
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
A spec project typechecked with `tsc` needs `@types/bun` (for `bun:test`), as
|
|
1089
|
+
the example's `devDependencies` have it.
|
|
1090
|
+
|
|
1091
|
+
[`examples/custom-store`](https://github.com/softistx/bumail/tree/develop/examples/custom-store)
|
|
1092
|
+
in the repository holds a complete one, a store over the memory store with its
|
|
1093
|
+
spec, that passes.
|
|
1094
|
+
|
|
1095
|
+
### What it covers
|
|
1096
|
+
|
|
1097
|
+
| group | what is held |
|
|
1098
|
+
| --- | --- |
|
|
1099
|
+
| `items` | `add` keeps the envelope, every recipient `pending`, due at once; the null sender; `readMessage` and every item are copies; an unknown id, or one no store could hold, is `undefined`; `list` order and paging; `cancel`; `maxItems` and `QUEUE_FULL`; what is not an item is `INVALID`; the DSN parameters (`ret`, `envid`, `notify`, `orcpt`) and their limits |
|
|
1100
|
+
| `claims and leases` | `claim` takes the earliest due, then the oldest, and only what is due; an expired lease is claimed again and its old owner has lost it; `renew`; `reschedule` with and without an owner; a bad request is `INVALID` |
|
|
1101
|
+
| `outcomes` | `complete` records each recipient, the schedule and lets go of the lease; a finished item is dropped with its message; text with a NUL or a lone surrogate is refused; a final state never changes; the DSNs stored in the same step (new ids, in order, none on a lost lease, all refused when one is bad); `mayHaveReachedMx` and `mayHaveReachedFallback` recorded and kept |
|
|
1102
|
+
| `several workers` | many claimers on one queue deliver every item once; a crashed worker's items are claimed again when its leases expire |
|
|
1103
|
+
|
|
1104
|
+
### `share`: a second handle
|
|
1105
|
+
|
|
1106
|
+
The two `several workers` tests need `share`, another handle on **the
|
|
1107
|
+
same queue**: a second connection to the same database, as a second
|
|
1108
|
+
process would open. Without it they are registered as skipped, named
|
|
1109
|
+
`… (skipped: harness has no share)`, because one handle cannot show that
|
|
1110
|
+
a claim is atomic across handles.
|
|
1111
|
+
|
|
1112
|
+
A store that lives in one process, such as an in-memory one, may return a
|
|
1113
|
+
second object over the same data, as the example does. A **networked**
|
|
1114
|
+
store (PostgreSQL, Redis, any server) should pass a real second
|
|
1115
|
+
connection: that is where two workers race, and where a claim that reads
|
|
1116
|
+
then writes loses. Passing none for a store like that leaves the one
|
|
1117
|
+
property that matters unproved.
|
|
1118
|
+
|
|
1119
|
+
### `dispose`
|
|
1120
|
+
|
|
1121
|
+
`dispose(store)` runs after every test, for each store `create` made in
|
|
1122
|
+
that test, once the test is over (pass or fail). Use it to close a
|
|
1123
|
+
connection or remove a file. It may be async.
|
|
1124
|
+
|
|
1125
|
+
- Every created store is disposed, even when the disposal of another
|
|
1126
|
+
one fails.
|
|
1127
|
+
- One failure is rethrown as it is. Several in one test are thrown
|
|
1128
|
+
together as an `AggregateError` (`several stores failed to dispose`),
|
|
1129
|
+
each reason in its `errors`.
|
|
1130
|
+
- Handles returned by `share` are **not** passed to `dispose`: the
|
|
1131
|
+
harness closes them, for instance in the same `dispose`, from the store
|
|
1132
|
+
they were made from.
|
|
1022
1133
|
|
|
1023
1134
|
## Options
|
|
1024
1135
|
|
package/docs/roadmap.md
CHANGED
|
@@ -6,7 +6,7 @@ only number on it.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
Nothing
|
|
9
|
+
Nothing under way.
|
|
10
10
|
|
|
11
11
|
## Next
|
|
12
12
|
|
|
@@ -18,14 +18,11 @@ Nothing is under way besides what is marked shipped below.
|
|
|
18
18
|
- **One session for several messages** — the queue hands several items
|
|
19
19
|
for one domain to one SMTP session, once `@bumail/smtp`'s client reuses
|
|
20
20
|
connections.
|
|
21
|
-
- **The contract's specs, exported**, so a store written outside this
|
|
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
21
|
|
|
27
22
|
## Later
|
|
28
23
|
|
|
24
|
+
- **A suite that is not tied to `bun:test`** — the contract's specs run under
|
|
25
|
+
any test runner, for a store written where Bun is not the runner.
|
|
29
26
|
- **REQUIRETLS (RFC 8689)** — a message that asks for TLS all the way is
|
|
30
27
|
never sent in clear, and fails rather than downgrade.
|
|
31
28
|
- **Retries per recipient** — a schedule of its own for each recipient,
|
|
@@ -49,7 +46,35 @@ Nothing is under way besides what is marked shipped below.
|
|
|
49
46
|
|
|
50
47
|
## Shipped
|
|
51
48
|
|
|
52
|
-
###
|
|
49
|
+
### 0.12.0
|
|
50
|
+
|
|
51
|
+
- **The contract's specs, exported** — `@bumail/queue/conformance` exports
|
|
52
|
+
`describeQueueStore(name, { create, share?, dispose? })`, run under
|
|
53
|
+
`bun test`, so a queue store written outside this package runs the same
|
|
54
|
+
contract suite as the built-in ones; a capability a store lacks shows as a
|
|
55
|
+
skipped test with its reason. Additive.
|
|
56
|
+
|
|
57
|
+
### 0.11.0
|
|
58
|
+
|
|
59
|
+
- **A DSN recorded in the same step as the outcome** — the failure, relayed
|
|
60
|
+
and delay DSNs are written in the same store step as the outcome they
|
|
61
|
+
report, in the memory, SQLite, PostgreSQL and Redis stores, so a crash or
|
|
62
|
+
a failed add no longer loses them. Breaking for a custom `QueueStore`:
|
|
63
|
+
`complete(id, owner, result, dsns)` returns `{ item, dsns }`; a store that
|
|
64
|
+
answers the old shape is reported on the `error` event and its DSNs fall
|
|
65
|
+
back to `add`. The `delivered` and `failed` DSNs a server writes for a
|
|
66
|
+
local delivery are not this gap: a crash there gives a duplicate, not a
|
|
67
|
+
loss.
|
|
68
|
+
|
|
69
|
+
### 0.10.0
|
|
70
|
+
|
|
71
|
+
- **`failedDsn()`** — builds the `failed` DSN of a recipient the
|
|
72
|
+
application could not deliver locally, the counterpart of `deliveredDsn()`:
|
|
73
|
+
it follows `NOTIFY` (none for `NEVER` or a list without `FAILURE`, none for
|
|
74
|
+
a null reverse-path), `ORCPT`, `ENVID` and `RET`; `DEFAULT_MAX_RETURN` (64
|
|
75
|
+
KiB) is the default of what a DSN returns of the original.
|
|
76
|
+
|
|
77
|
+
### 0.9.0
|
|
53
78
|
|
|
54
79
|
- **DSN requests (RFC 3461)**: the queue honours a sender's `NOTIFY`, `RET`,
|
|
55
80
|
`ENVID` and `ORCPT` given at enqueue. `NOTIFY=NEVER` silences failure
|
|
@@ -58,22 +83,31 @@ Nothing is under way besides what is marked shipped below.
|
|
|
58
83
|
`Original-Recipient`; and `deliveredDsn()` builds the `delivered` DSN for
|
|
59
84
|
a local delivery. The SQLite and PostgreSQL stores run migration 2: stop
|
|
60
85
|
every old worker before starting the new ones.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
86
|
+
|
|
87
|
+
### 0.8.0
|
|
88
|
+
|
|
66
89
|
- **The fallback smarthost's doubt is remembered** — a recipient whose
|
|
67
90
|
session by the fallback smarthost ended after the final dot with no reply
|
|
68
91
|
is marked `mayHaveReachedFallback`, kept by every store: later attempts
|
|
69
92
|
skip the MX for it and go to the smarthost again, until
|
|
70
93
|
`retry.giveUpAfter` (a `4.4.7` DSN that says so), and the `deferred` and
|
|
71
94
|
`failed` events carry the mark.
|
|
95
|
+
|
|
96
|
+
### 0.7.1
|
|
97
|
+
|
|
72
98
|
- **Only the recipients the MX did not refuse are in doubt** — in a session that
|
|
73
99
|
failed after the final dot, a recipient the MX refused at `RCPT` is
|
|
74
100
|
settled from its own reply (5xx failed, 4xx deferred) and not marked, so
|
|
75
101
|
it may fall back to the smarthost.
|
|
76
102
|
|
|
103
|
+
### 0.7.0
|
|
104
|
+
|
|
105
|
+
- **The MX's doubt is remembered** — a recipient whose session by MX ended
|
|
106
|
+
after the final dot with no reply is marked `mayHaveReachedMx`, kept by
|
|
107
|
+
every store: no later attempt falls back to the smarthost on its own, it
|
|
108
|
+
stays on the MX until `retry.giveUpAfter` (a `4.4.7` DSN that says so),
|
|
109
|
+
and the `deferred` and `failed` events carry the mark.
|
|
110
|
+
|
|
77
111
|
### 0.5.0
|
|
78
112
|
|
|
79
113
|
- **A fallback smarthost** — `fallback`: a recipient
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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)
|
|
@@ -86,6 +89,11 @@ parts shown as … vary.
|
|
|
86
89
|
- [`QueueError: reply.host must be a string`](#queueerror-replyhost-must-be-a-string)
|
|
87
90
|
- [`QueueError: … holds a NUL or a lone surrogate, which a store cannot keep`](#queueerror--holds-a-nul-or-a-lone-surrogate-which-a-store-cannot-keep)
|
|
88
91
|
|
|
92
|
+
**A store's spec** (`describeQueueStore`)
|
|
93
|
+
|
|
94
|
+
- [`AggregateError: several stores failed to dispose`](#aggregateerror-several-stores-failed-to-dispose)
|
|
95
|
+
- [`… (skipped: harness has no share)` in the test names](#-skipped-harness-has-no-share-in-the-test-names)
|
|
96
|
+
|
|
89
97
|
**Delivery**
|
|
90
98
|
|
|
91
99
|
- [A recipient stays `deferred` with `4.4.1` or `4.4.2`](#a-recipient-stays-deferred-with-441-or-442)
|
|
@@ -302,6 +310,21 @@ or `… must be a number from … to …, not …`. A number option out of its r
|
|
|
302
310
|
|
|
303
311
|
**Code:** `INVALID`.
|
|
304
312
|
|
|
313
|
+
### `QueueError: dsns must be an array`
|
|
314
|
+
|
|
315
|
+
**Code:** `INVALID`.
|
|
316
|
+
**When:** `complete(id, owner, result, dsns)` of a store of this package
|
|
317
|
+
was called with something other than an array as `dsns`, typically by a
|
|
318
|
+
caller of the store that still uses the three-argument form.
|
|
319
|
+
**Why:** since 0.11 `complete` takes the DSN items the outcome calls for
|
|
320
|
+
(an empty array for none); the store checks them before it opens a
|
|
321
|
+
connection or a transaction, so nothing was written.
|
|
322
|
+
**Fix:** pass an array, `[]` when the outcome calls for no DSN:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
await store.complete(id, owner, result, []);
|
|
326
|
+
```
|
|
327
|
+
|
|
305
328
|
### `QueueError: owner must be a non-empty string`
|
|
306
329
|
|
|
307
330
|
**Code:** `INVALID`, also as
|
|
@@ -395,6 +418,51 @@ guide's [Redis](guide.md#redis)); never delete a queue's keys or rows by
|
|
|
395
418
|
hand, `cancel(id)` drops an item with its message. The sender has the
|
|
396
419
|
DSN and may send again.
|
|
397
420
|
|
|
421
|
+
### `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`
|
|
422
|
+
|
|
423
|
+
**Code:** `INVALID`, on the `error` event, with the item's `id`.
|
|
424
|
+
**When:** a custom `QueueStore` still answers `complete` as before 0.11,
|
|
425
|
+
with the bare `QueueItem`, and an
|
|
426
|
+
outcome calls for a DSN (a failure, a `relayed` or a delay).
|
|
427
|
+
**Why:** since 0.11 the queue hands the DSNs to `complete`, which stores
|
|
428
|
+
them in the same step as the outcome and returns `{ item, dsns }`. A store
|
|
429
|
+
that answers with the item alone has stored none of them: the queue adds
|
|
430
|
+
each one with `add` afterwards, as it did before, so a crash or an `add`
|
|
431
|
+
error between the two steps still loses a DSN, and an `add` that fails is
|
|
432
|
+
one more `error` event. The stores of this package are not affected.
|
|
433
|
+
**Fix:** port the store's `complete`: take `dsns`, insert them in the
|
|
434
|
+
transaction (or script) that records the outcome, and return them (the
|
|
435
|
+
`this.*` calls are your store's own helpers, a sketch):
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
async complete(id, owner, result, dsns) {
|
|
439
|
+
return this.transaction(async () => {
|
|
440
|
+
const item = await this.apply(id, owner, result);
|
|
441
|
+
if (!item) return undefined; // lease lost: no DSN written
|
|
442
|
+
const stored = [];
|
|
443
|
+
for (const dsn of dsns) stored.push(await this.insert(dsn)); // in order
|
|
444
|
+
return { item, dsns: stored };
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
See [Writing a store](./guide.md#writing-a-store).
|
|
450
|
+
|
|
451
|
+
### `QueueError: The queue store stored … of the … DSNs of …: a … DSN to … is lost`
|
|
452
|
+
|
|
453
|
+
**Code:** `INVALID`, on the `error` event, with the item's `id`.
|
|
454
|
+
**When:** a custom `QueueStore`'s `complete` returned `{ item, dsns }` with
|
|
455
|
+
fewer DSNs than it was given, and an outcome called for them (a failure, a
|
|
456
|
+
`relayed` or a delay).
|
|
457
|
+
**Why:** the queue matches the returned DSNs to the ones it asked for, in
|
|
458
|
+
order. A DSN with no stored counterpart was not kept, so its notice to the
|
|
459
|
+
sender is lost: no `dsn` event is emitted for it. The stores of this
|
|
460
|
+
package return every DSN or throw.
|
|
461
|
+
**Fix:** make `complete` return one stored item per DSN, in the order
|
|
462
|
+
given, or throw to leave the item leased (see the sketch under
|
|
463
|
+
[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)
|
|
464
|
+
and [Writing a store](./guide.md#writing-a-store)).
|
|
465
|
+
|
|
398
466
|
### `SmtpError: sendMail(): …` (`INVALID_OPTION`), with recipients deferred as `4.3.5`
|
|
399
467
|
|
|
400
468
|
**When:** `sendMail` refused the options of a session — the route, the
|
|
@@ -482,6 +550,43 @@ it.
|
|
|
482
550
|
default. The error from a transaction of your own on the same client is
|
|
483
551
|
yours to retry.
|
|
484
552
|
|
|
553
|
+
## Your own store's spec
|
|
554
|
+
|
|
555
|
+
### `AggregateError: several stores failed to dispose`
|
|
556
|
+
|
|
557
|
+
`describeQueueStore`, after a test. The test created more than one store
|
|
558
|
+
(a spec that calls `create` again, as the several-workers tests do) and
|
|
559
|
+
the `dispose` of more than one of them threw. The suite disposes every
|
|
560
|
+
store it created, collects the failures and throws them together; each
|
|
561
|
+
reason is in `error.errors`. A single failure is thrown as it is, not
|
|
562
|
+
wrapped.
|
|
563
|
+
|
|
564
|
+
**Fix:** read `error.errors`; each entry is one store's failure. The suite
|
|
565
|
+
calls `dispose` once per created store, so the causes are a store's own
|
|
566
|
+
`close` failing (each entry says why), or a `create` that hands back a
|
|
567
|
+
shared object, so that each test's `dispose` closes the same thing. Have
|
|
568
|
+
`create` return a fresh store each call. Handles from `share` are not passed
|
|
569
|
+
to `dispose`; close them in the harness.
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
create: () => new MyQueueStore(freshPath()), // a new store each call, never a shared one
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### `… (skipped: harness has no share)` in the test names
|
|
576
|
+
|
|
577
|
+
`describeQueueStore`. The harness passed no `share`, so the two tests of
|
|
578
|
+
"several workers" (many claimers race, a crashed worker's items) are
|
|
579
|
+
registered as skipped. This is not a failure, but the atomicity of a claim
|
|
580
|
+
across handles is then unproved.
|
|
581
|
+
|
|
582
|
+
**Fix:** pass `share`, which returns another handle on the same queue (a
|
|
583
|
+
second connection to the same database), and give a networked store a real
|
|
584
|
+
second connection:
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
share: (store) => (store as MyQueueStore).connectAgain(),
|
|
588
|
+
```
|
|
589
|
+
|
|
485
590
|
## The `bun:sqlite` store
|
|
486
591
|
|
|
487
592
|
### `QueueError: A SQLite queue store needs a directory`
|
|
@@ -720,7 +825,7 @@ queue's own outcomes always have one.
|
|
|
720
825
|
await store.complete(id, owner, {
|
|
721
826
|
...result,
|
|
722
827
|
recipients: [{ address, status: 'deferred', reply: { status: '4.4.1', text: 'No answer' } }],
|
|
723
|
-
});
|
|
828
|
+
}, []); // the DSN items the outcome calls for, none here
|
|
724
829
|
```
|
|
725
830
|
|
|
726
831
|
### `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.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
"import": "./dist/redis/index.js",
|
|
40
40
|
"default": "./dist/redis/index.js"
|
|
41
41
|
},
|
|
42
|
+
"./conformance": {
|
|
43
|
+
"types": "./dist/conformance/index.d.ts",
|
|
44
|
+
"import": "./dist/conformance/index.js",
|
|
45
|
+
"default": "./dist/conformance/index.js"
|
|
46
|
+
},
|
|
42
47
|
"./package.json": "./package.json"
|
|
43
48
|
},
|
|
44
49
|
"repository": {
|
|
@@ -61,18 +66,19 @@
|
|
|
61
66
|
"src/memory/index.ts",
|
|
62
67
|
"src/sqlite/index.ts",
|
|
63
68
|
"src/postgres/index.ts",
|
|
64
|
-
"src/redis/index.ts"
|
|
69
|
+
"src/redis/index.ts",
|
|
70
|
+
"src/conformance/index.ts"
|
|
65
71
|
]
|
|
66
72
|
},
|
|
67
73
|
"devDependencies": {
|
|
68
|
-
"@bumail/dns": "^0.2.
|
|
74
|
+
"@bumail/dns": "^0.2.2",
|
|
69
75
|
"@bumail/mime": "^0.2.0",
|
|
70
|
-
"@bumail/smtp": "^0.11.
|
|
76
|
+
"@bumail/smtp": "^0.11.1",
|
|
71
77
|
"@types/bun": "^1.4.2"
|
|
72
78
|
},
|
|
73
79
|
"peerDependencies": {
|
|
74
80
|
"@bumail/mime": "^0.2.0",
|
|
75
|
-
"@bumail/smtp": "^0.11.
|
|
81
|
+
"@bumail/smtp": "^0.11.1",
|
|
76
82
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
77
83
|
},
|
|
78
84
|
"peerDependenciesMeta": {
|