@bumail/queue 0.11.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.
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/conformance/describe-queue-store.ts", "../src/conformance/claims.ts", "../src/conformance/setup.ts", "../src/conformance/concurrency.ts", "../src/conformance/items.ts", "../src/conformance/dsn.ts", "../src/conformance/outcomes.ts", "../src/conformance/complete-dsns.ts", "../src/conformance/doubt.ts"],
4
+ "sourcesContent": [
5
+ "import { afterEach, describe } from 'bun:test';\nimport type { QueueStore } from '../contract/queue-store';\nimport { describeClaims } from './claims';\nimport { describeConcurrency } from './concurrency';\nimport { describeItems } from './items';\nimport { describeOutcomes } from './outcomes';\nimport type { QueueStoreHarness } from './setup';\n\n/**\n * The specs every `QueueStore` must pass, to call from a spec file run by\n * `bun test`. The harness builds an empty store with `create`; `share` is\n * another handle on the same queue (a second connection, as a second\n * process opens), so the claim's atomicity is checked across handles, not\n * only within one: without it those tests are registered as skipped.\n * `dispose` runs after every test with the stores that test created.\n */\nexport function describeQueueStore(\n\tname: string,\n\tharness: QueueStoreHarness,\n): void {\n\tdescribe(`${name}: the QueueStore contract`, () => {\n\t\tconst created: QueueStore[] = [];\n\t\tconst factories = {\n\t\t\tshare: harness.share,\n\t\t\tcreate: async () => {\n\t\t\t\tconst store = await harness.create();\n\t\t\t\tcreated.push(store);\n\t\t\t\treturn store;\n\t\t\t},\n\t\t};\n\t\tafterEach(async () => {\n\t\t\tconst stores = created.splice(0);\n\t\t\tconst settled = await Promise.allSettled(\n\t\t\t\tstores.map(async (store) => harness.dispose?.(store)),\n\t\t\t);\n\t\t\tconst failed = settled.filter((r) => r.status === 'rejected');\n\t\t\tif (failed.length === 1) throw failed[0]?.reason;\n\t\t\tif (failed.length > 1)\n\t\t\t\tthrow new AggregateError(\n\t\t\t\t\tfailed.map((r) => r.reason),\n\t\t\t\t\t'several stores failed to dispose',\n\t\t\t\t);\n\t\t});\n\t\tdescribeItems(factories);\n\t\tdescribeClaims(factories);\n\t\tdescribeOutcomes(factories);\n\t\tdescribeConcurrency(factories);\n\t});\n}\n",
6
+ "import { describe, expect, test } from 'bun:test';\nimport type { AttemptResult } from '../contract/types';\nimport { entry, MINUTE, rejects, type StoreFactories, T0 } from './setup';\n\nconst lease = { leaseMs: 10 * MINUTE };\n\nconst result = (overrides: Partial<AttemptResult> = {}): AttemptResult => ({\n\tnow: T0 + MINUTE,\n\trecipients: [],\n\tnextAttemptAt: T0 + 30 * MINUTE,\n\tattempts: 1,\n\tdelayNotified: false,\n\t...overrides,\n});\n\n/** Claims, leases and schedules. */\nexport function describeClaims(factories: StoreFactories): void {\n\tdescribe('claims and leases', () => {\n\t\tclaiming(factories);\n\t\tleasing(factories);\n\t\trescheduling(factories);\n\t});\n}\n\nfunction claiming({ create }: StoreFactories): void {\n\ttest('claim takes a due item with a lease; nothing else is due', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tconst claimed = await store.claim({ owner: 'w1', now: T0, ...lease });\n\t\texpect(claimed?.id).toBe(item.id);\n\t\texpect(claimed?.lease).toEqual({\n\t\t\towner: 'w1',\n\t\t\texpiresAt: T0 + 10 * MINUTE,\n\t\t});\n\t\texpect((await store.get(item.id))?.lease?.owner).toBe('w1');\n\t\texpect(\n\t\t\tawait store.claim({ owner: 'w2', now: T0, ...lease }),\n\t\t).toBeUndefined();\n\t});\n\n\ttest('an item not yet due is not claimed', async () => {\n\t\tconst store = await create();\n\t\tawait store.add(entry({ createdAt: T0 + MINUTE }));\n\t\texpect(\n\t\t\tawait store.claim({ owner: 'w1', now: T0, ...lease }),\n\t\t).toBeUndefined();\n\t\texpect(\n\t\t\tawait store.claim({ owner: 'w1', now: T0 + MINUTE, ...lease }),\n\t\t).toBeDefined();\n\t});\n\n\ttest('the earliest due is claimed first, then the oldest', async () => {\n\t\tconst store = await create();\n\t\tconst late = await store.add(entry({ createdAt: T0 + MINUTE }));\n\t\tconst first = await store.add(entry());\n\t\tconst second = await store.add(entry());\n\t\tconst now = T0 + MINUTE;\n\t\tconst order = [];\n\t\tfor (let i = 0; i < 3; i++) {\n\t\t\torder.push((await store.claim({ owner: 'w', now, ...lease }))?.id);\n\t\t}\n\t\texpect(order).toEqual([first.id, second.id, late.id]);\n\t});\n}\n\nfunction leasing({ create }: StoreFactories): void {\n\ttest('an expired lease is claimed again, and its old owner has lost it', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'crashed', now: T0, leaseMs: MINUTE });\n\t\texpect(\n\t\t\tawait store.claim({ owner: 'w2', now: T0 + MINUTE - 1, ...lease }),\n\t\t).toBeUndefined();\n\t\tconst again = await store.claim({\n\t\t\towner: 'w2',\n\t\t\tnow: T0 + MINUTE,\n\t\t\t...lease,\n\t\t});\n\t\texpect(again?.id).toBe(item.id);\n\t\texpect(again?.lease?.owner).toBe('w2');\n\t\texpect(await store.renew(item.id, 'crashed', T0 + 5 * MINUTE)).toBe(false);\n\t\texpect(\n\t\t\tawait store.complete(item.id, 'crashed', result(), []),\n\t\t).toBeUndefined();\n\t\texpect(await store.reschedule(item.id, T0, 'crashed')).toBe(false);\n\t\texpect((await store.get(item.id))?.lease?.owner).toBe('w2');\n\t});\n\n\ttest('renew moves the expiry while the owner holds the lease', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, leaseMs: MINUTE });\n\t\texpect(await store.renew(item.id, 'w1', T0 + 3 * MINUTE)).toBe(true);\n\t\texpect(\n\t\t\tawait store.claim({ owner: 'w2', now: T0 + 2 * MINUTE, ...lease }),\n\t\t).toBeUndefined();\n\t\texpect(await store.renew('nope', 'w1', T0)).toBe(false);\n\t});\n}\n\nfunction rescheduling({ create }: StoreFactories): void {\n\ttest('reschedule with the owner gives the item back, due at the new time', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\texpect(await store.reschedule(item.id, T0 + MINUTE, 'w2')).toBe(false);\n\t\texpect(await store.reschedule(item.id, T0 + MINUTE, 'w1')).toBe(true);\n\t\tconst stored = await store.get(item.id);\n\t\texpect(stored?.lease).toBeUndefined();\n\t\texpect(stored?.nextAttemptAt).toBe(T0 + MINUTE);\n\t\texpect(stored?.attempts).toBe(0);\n\t});\n\n\ttest('reschedule without an owner moves the time and leaves the lease alone', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tawait store.complete(item.id, 'w1', result(), []);\n\t\texpect(await store.reschedule(item.id, T0 + MINUTE)).toBe(true);\n\t\texpect(\n\t\t\t(await store.claim({ owner: 'w2', now: T0 + MINUTE, ...lease }))?.id,\n\t\t).toBe(item.id);\n\t\texpect(await store.reschedule(item.id, T0)).toBe(true);\n\t\texpect((await store.get(item.id))?.lease?.owner).toBe('w2');\n\t});\n\n\ttest('claim refuses a bad request', async () => {\n\t\tconst store = await create();\n\t\tawait rejects(store.claim({ owner: '', now: T0, ...lease }), 'INVALID');\n\t\tawait rejects(store.claim({ owner: 'w', now: T0, leaseMs: 0 }), 'INVALID');\n\t\tawait rejects(\n\t\t\tstore.claim({ owner: 'w', now: Number.NaN, ...lease }),\n\t\t\t'INVALID',\n\t\t);\n\t\tawait rejects(store.reschedule('nope', T0, ''), 'INVALID');\n\t});\n}\n",
7
+ "import { expect } from 'bun:test';\nimport type { QueueStore } from '../contract/queue-store';\nimport type { AttemptResult, NewQueueItem } from '../contract/types';\n\n/** A new store, empty. */\nexport type CreateQueueStore = () => Promise<QueueStore> | QueueStore;\n\n/**\n * Another handle on the same queue as `store`: a second connection to the\n * same database, as a second process would open. A harness with no second\n * handle omits `share`, and the two several-workers tests are then skipped.\n */\nexport type ShareStore = (store: QueueStore) => QueueStore;\n\n/** What a store's spec hands the suite. */\nexport interface QueueStoreHarness {\n\t/** A new store, empty. */\n\treadonly create: CreateQueueStore;\n\t/** Another handle on the same queue; without it the share tests are skipped. */\n\treadonly share?: ShareStore | undefined;\n\t/**\n\t * Closes a store `create` made, after the test that used it. Handles\n\t * returned by `share` are the harness's to close.\n\t */\n\treadonly dispose?: ((store: QueueStore) => Promise<void> | void) | undefined;\n}\n\n/** What the suite is handed: how to build a store, and what the harness can do beyond that. */\nexport interface StoreFactories {\n\treadonly create: CreateQueueStore;\n\t/** Absent when the harness has no second handle: the share tests are skipped. */\n\treadonly share?: ShareStore | undefined;\n}\n\nexport const bytes = (text: string) => new TextEncoder().encode(text);\n\nexport const T0 = Date.UTC(2026, 9, 1, 12);\nexport const MINUTE = 60_000;\n\nexport const lease = { leaseMs: 10 * MINUTE };\n\nexport const result = (\n\toverrides: Partial<AttemptResult> = {},\n): AttemptResult => ({\n\tnow: T0 + MINUTE,\n\trecipients: [],\n\tnextAttemptAt: T0 + 30 * MINUTE,\n\tattempts: 1,\n\tdelayNotified: false,\n\t...overrides,\n});\n\n/** A new item from mary to two recipients, created at `T0` unless told otherwise. */\nexport function entry(overrides: Partial<NewQueueItem> = {}): NewQueueItem {\n\treturn {\n\t\tfrom: 'mary@example.net',\n\t\tto: ['joe@example.com', 'ann@example.org'],\n\t\tmessage: bytes('Subject: Hi\\r\\n\\r\\nHello\\r\\n'),\n\t\tcreatedAt: T0,\n\t\t...overrides,\n\t};\n}\n\n/** Expects a promise to reject with a QueueError of this code. */\nexport async function rejects(promise: Promise<unknown>, code: string) {\n\tawait expect(promise).rejects.toMatchObject({ name: 'QueueError', code });\n}\n",
8
+ "import { describe, expect, test } from 'bun:test';\nimport type { QueueStore } from '../contract/queue-store';\nimport { entry, MINUTE, type StoreFactories, T0 } from './setup';\n\nconst delivered = (now: number) => ({\n\tnow,\n\trecipients: [\n\t\t{ address: 'joe@example.com', status: 'delivered' as const },\n\t\t{ address: 'ann@example.org', status: 'delivered' as const },\n\t],\n\tnextAttemptAt: now,\n\tattempts: 1,\n\tdelayNotified: false,\n});\n\n/** A worker: claims and delivers until nothing is due, yielding between steps. */\nasync function work(store: QueueStore, owner: string, now: number) {\n\tconst taken: string[] = [];\n\tfor (;;) {\n\t\tawait new Promise((resolve) => setTimeout(resolve, Math.random() * 2));\n\t\tconst item = await store.claim({ owner, now, leaseMs: 10 * MINUTE });\n\t\tif (!item) return taken;\n\t\ttaken.push(item.id);\n\t\tawait new Promise((resolve) => setTimeout(resolve, Math.random() * 2));\n\t\tconst done = await store.complete(item.id, owner, delivered(now), []);\n\t\texpect(done).toBeDefined();\n\t}\n}\n\n/** Several claimers on one queue, and a claimer that crashed. */\nexport function describeConcurrency({ create, share }: StoreFactories): void {\n\t// A harness with no second handle cannot prove atomicity across handles:\n\t// the test is registered as skipped, with the reason in its name.\n\tconst shared = share ? test : test.skip;\n\tconst note = share ? '' : ' (skipped: harness has no share)';\n\tdescribe('several workers', () => {\n\t\tshared(\n\t\t\t`many claimers race: every item is delivered once${note}`,\n\t\t\tasync () => {\n\t\t\t\tconst store = await create();\n\t\t\t\tconst ids = new Set<string>();\n\t\t\t\tfor (let i = 0; i < 60; i++) ids.add((await store.add(entry())).id);\n\t\t\t\tconst workers = Array.from({ length: 8 }, (_, i) =>\n\t\t\t\t\twork(share?.(store) ?? store, `w${i}`, T0),\n\t\t\t\t);\n\t\t\t\tconst taken = (await Promise.all(workers)).flat();\n\t\t\t\texpect(taken.length).toBe(60);\n\t\t\t\texpect(new Set(taken)).toEqual(ids);\n\t\t\t\texpect(await store.count()).toBe(0);\n\t\t\t},\n\t\t);\n\n\t\tshared(\n\t\t\t`a crashed worker’s items are claimed again once its leases expire${note}`,\n\t\t\tasync () => {\n\t\t\t\tconst store = await create();\n\t\t\t\tfor (let i = 0; i < 5; i++) await store.add(entry());\n\t\t\t\tconst crashed: string[] = [];\n\t\t\t\tfor (let i = 0; i < 5; i++) {\n\t\t\t\t\tconst item = await store.claim({\n\t\t\t\t\t\towner: 'crashed',\n\t\t\t\t\t\tnow: T0,\n\t\t\t\t\t\tleaseMs: MINUTE,\n\t\t\t\t\t});\n\t\t\t\t\tif (item) crashed.push(item.id);\n\t\t\t\t}\n\t\t\t\texpect(crashed.length).toBe(5);\n\t\t\t\tconst other = share?.(store) ?? store;\n\t\t\t\texpect(await work(other, 'w2', T0 + MINUTE - 1)).toEqual([]);\n\t\t\t\tconst recovered = await work(other, 'w2', T0 + MINUTE);\n\t\t\t\texpect(new Set(recovered)).toEqual(new Set(crashed));\n\t\t\t\tfor (const id of crashed) {\n\t\t\t\t\texpect(\n\t\t\t\t\t\tawait store.complete(id, 'crashed', delivered(T0), []),\n\t\t\t\t\t).toBeUndefined();\n\t\t\t\t}\n\t\t\t\texpect(await store.count()).toBe(0);\n\t\t\t},\n\t\t);\n\t});\n}\n",
9
+ "import { describe, expect, test } from 'bun:test';\nimport { dsnParameters } from './dsn';\nimport {\n\tbytes,\n\tentry,\n\tMINUTE,\n\trejects,\n\ttype StoreFactories,\n\tT0,\n} from './setup';\n\n/** Adding, reading, listing and cancelling items. */\nexport function describeItems(factories: StoreFactories): void {\n\tdescribe('items', () => {\n\t\tadding(factories);\n\t\tlisting(factories);\n\t\tdsnParameters(factories);\n\t});\n}\n\nfunction adding({ create }: StoreFactories): void {\n\ttest('add keeps the envelope, every recipient pending, due at once', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\texpect(item).toMatchObject({\n\t\t\tfrom: 'mary@example.net',\n\t\t\trecipients: [\n\t\t\t\t{ address: 'joe@example.com', status: 'pending' },\n\t\t\t\t{ address: 'ann@example.org', status: 'pending' },\n\t\t\t],\n\t\t\tsize: 22,\n\t\t\tcreatedAt: T0,\n\t\t\tnextAttemptAt: T0,\n\t\t\tattempts: 0,\n\t\t\tdelayNotified: false,\n\t\t});\n\t\texpect(item.lease).toBeUndefined();\n\t\texpect(await store.get(item.id)).toEqual(item);\n\t\texpect(await store.count()).toBe(1);\n\t});\n\n\ttest('the null sender is kept as the empty string', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry({ from: '' }));\n\t\texpect((await store.get(item.id))?.from).toBe('');\n\t});\n\n\ttest('readMessage gives the bytes back, a copy each time', async () => {\n\t\tconst store = await create();\n\t\tconst message = bytes('Subject: x\\r\\n\\r\\nbody\\r\\n');\n\t\tconst item = await store.add(entry({ message }));\n\t\tmessage[0] = 0;\n\t\tconst read = await store.readMessage(item.id);\n\t\texpect(new TextDecoder().decode(read)).toBe('Subject: x\\r\\n\\r\\nbody\\r\\n');\n\t\tif (read) read[0] = 0;\n\t\texpect((await store.readMessage(item.id))?.[0]).toBe(83);\n\t});\n\n\ttest('an item returned is a copy', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tconst first = (item.recipients as unknown as { status: string }[])[0];\n\t\tif (first) first.status = 'failed';\n\t\texpect((await store.get(item.id))?.recipients[0]?.status).toBe('pending');\n\t});\n\n\ttest('an unknown id is undefined, not an error', async () => {\n\t\tconst store = await create();\n\t\texpect(await store.get('nope')).toBeUndefined();\n\t\texpect(await store.readMessage('nope')).toBeUndefined();\n\t\texpect(await store.cancel('nope')).toBeUndefined();\n\t\texpect(await store.reschedule('nope', T0)).toBe(false);\n\t});\n\n\ttest('an id no store could hold, a NUL or a lone surrogate in it, is unknown too', async () => {\n\t\tconst store = await create();\n\t\tawait store.add(entry());\n\t\tfor (const id of ['a\\u0000b', 'a\\ud83d']) {\n\t\t\texpect(await store.get(id)).toBeUndefined();\n\t\t\texpect(await store.readMessage(id)).toBeUndefined();\n\t\t\texpect(await store.renew(id, 'w1', T0)).toBe(false);\n\t\t\texpect(await store.reschedule(id, T0)).toBe(false);\n\t\t\texpect(await store.cancel(id)).toBeUndefined();\n\t\t}\n\t\texpect(await store.count()).toBe(1);\n\t});\n}\n\nfunction listing({ create }: StoreFactories): void {\n\ttest('list gives the next due first, then the oldest, a page at a time', async () => {\n\t\tconst store = await create();\n\t\tconst a = await store.add(entry({ createdAt: T0 + 2 * MINUTE }));\n\t\tconst b = await store.add(entry({ createdAt: T0 }));\n\t\tconst c = await store.add(entry({ createdAt: T0 }));\n\t\tconst ids = (await store.list()).map((i) => i.id);\n\t\texpect(ids).toEqual([b.id, c.id, a.id]);\n\t\tconst page = await store.list({ offset: 1, limit: 1 });\n\t\texpect(page.map((i) => i.id)).toEqual([c.id]);\n\t});\n\n\ttest('list refuses a bad page', async () => {\n\t\tconst store = await create();\n\t\tawait rejects(store.list({ limit: 0 }), 'INVALID');\n\t\tawait rejects(store.list({ offset: -1 }), 'INVALID');\n\t});\n\n\ttest('cancel drops the item and its message, leased or not', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, leaseMs: MINUTE });\n\t\tconst cancelled = await store.cancel(item.id);\n\t\texpect(cancelled?.id).toBe(item.id);\n\t\texpect(await store.get(item.id)).toBeUndefined();\n\t\texpect(await store.readMessage(item.id)).toBeUndefined();\n\t\texpect(await store.count()).toBe(0);\n\t});\n\n\ttest('maxItems refuses an add past the limit with QUEUE_FULL', async () => {\n\t\tconst store = await create();\n\t\tawait store.add(entry(), { maxItems: 2 });\n\t\tawait store.add(entry(), { maxItems: 2 });\n\t\tawait rejects(store.add(entry(), { maxItems: 2 }), 'QUEUE_FULL');\n\t\texpect(await store.count()).toBe(2);\n\t\tawait rejects(store.add(entry(), { maxItems: 0 }), 'INVALID');\n\t});\n\n\ttest('add refuses what is not an item', async () => {\n\t\tconst store = await create();\n\t\tawait rejects(store.add(entry({ to: [] })), 'INVALID');\n\t\tawait rejects(\n\t\t\tstore.add(entry({ message: 'text' as unknown as Uint8Array })),\n\t\t\t'INVALID',\n\t\t);\n\t\tawait rejects(store.add(entry({ createdAt: Number.NaN })), 'INVALID');\n\t\tawait rejects(store.add(entry({ from: 'a\\u0000@example.net' })), 'INVALID');\n\t\tawait rejects(\n\t\t\tstore.add(entry({ to: ['jo\\ud83d@example.com'] })),\n\t\t\t'INVALID',\n\t\t);\n\t\texpect(await store.count()).toBe(0);\n\t});\n}\n",
10
+ "import { expect, test } from 'bun:test';\nimport {\n\tentry,\n\tlease,\n\trejects,\n\tresult,\n\ttype StoreFactories,\n\tT0,\n} from './setup';\n\n/** RFC 3461's parameters: kept as given, for the item's whole life. */\nexport function dsnParameters({ create }: StoreFactories): void {\n\tconst asked = {\n\t\tret: 'HDRS' as const,\n\t\tenvid: 'QQ314159 + x=',\n\t\trecipients: [\n\t\t\t{\n\t\t\t\taddress: 'joe@example.com',\n\t\t\t\tnotify: ['SUCCESS', 'DELAY'] as ('SUCCESS' | 'DELAY')[],\n\t\t\t\torcpt: { type: 'rfc822', address: 'Joe.Original@example.com' },\n\t\t\t},\n\t\t\t{ address: 'ann@example.org', notify: 'NEVER' as const },\n\t\t],\n\t};\n\n\ttest('RFC 3461 §4: ret, envid, notify and orcpt are kept by add, get, list and claim', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry(asked));\n\t\tconst kept = {\n\t\t\tret: 'HDRS',\n\t\t\tenvid: 'QQ314159 + x=',\n\t\t\trecipients: [\n\t\t\t\t{\n\t\t\t\t\taddress: 'joe@example.com',\n\t\t\t\t\tnotify: ['SUCCESS', 'DELAY'],\n\t\t\t\t\torcpt: { type: 'rfc822', address: 'Joe.Original@example.com' },\n\t\t\t\t},\n\t\t\t\t{ address: 'ann@example.org', notify: 'NEVER' },\n\t\t\t],\n\t\t};\n\t\texpect(item).toMatchObject(kept);\n\t\texpect(await store.get(item.id)).toEqual(item);\n\t\texpect((await store.list())[0]).toEqual(item);\n\t\texpect(await store.claim({ owner: 'w1', now: T0, ...lease })).toMatchObject(\n\t\t\tkept,\n\t\t);\n\t});\n\n\ttest('complete keeps them: the notify and orcpt of a deferred recipient, and the item level', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry(asked));\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tconst done = await store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({\n\t\t\t\trecipients: [\n\t\t\t\t\t{\n\t\t\t\t\t\taddress: 'joe@example.com',\n\t\t\t\t\t\tstatus: 'deferred',\n\t\t\t\t\t\treply: { text: 'try later' },\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}),\n\t\t\t[],\n\t\t);\n\t\texpect(done?.item.recipients[0]).toMatchObject({\n\t\t\tnotify: ['SUCCESS', 'DELAY'],\n\t\t\torcpt: { type: 'rfc822', address: 'Joe.Original@example.com' },\n\t\t});\n\t\tconst again = await store.get(item.id);\n\t\texpect(again?.ret).toBe('HDRS');\n\t\texpect(again?.envid).toBe('QQ314159 + x=');\n\t\texpect(again?.recipients[1]?.notify).toBe('NEVER');\n\t});\n\n\ttest('an item added with none carries none: the fields are absent, as before', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tconst read = await store.get(item.id);\n\t\tfor (const key of ['ret', 'envid'] as const) {\n\t\t\texpect(read).not.toHaveProperty(key);\n\t\t}\n\t\tfor (const r of read?.recipients ?? []) {\n\t\t\texpect(r).not.toHaveProperty('notify');\n\t\t\texpect(r).not.toHaveProperty('orcpt');\n\t\t}\n\t});\n\n\ttest('RFC 6533: 50 emoji in a utf-8 ORCPT are within 500 characters, as the server counts them', async () => {\n\t\tconst store = await create();\n\t\tconst orcpt = { type: 'utf-8', address: `${'😀'.repeat(50)}@x` };\n\t\tconst item = await store.add(\n\t\t\tentry({ recipients: [{ address: 'joe@example.com', orcpt }] }),\n\t\t);\n\t\texpect(item.recipients[0]?.orcpt).toEqual(orcpt);\n\t\tawait expect(\n\t\t\tstore.add(\n\t\t\t\tentry({\n\t\t\t\t\trecipients: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\taddress: 'joe@example.com',\n\t\t\t\t\t\t\torcpt: { type: 'rfc822', address: orcpt.address },\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t}),\n\t\t\t),\n\t\t).rejects.toMatchObject({ code: 'INVALID' });\n\t});\n\n\ttest('RFC 6533: a utf-8 ORCPT of 500 characters (82 escapes) is kept, 506 is INVALID', async () => {\n\t\tconst store = await create();\n\t\tconst orcpt = (n: number) => ({\n\t\t\ttype: 'utf-8',\n\t\t\taddress: `${'+'.repeat(n)}@x`,\n\t\t});\n\t\tconst item = await store.add(\n\t\t\tentry({ recipients: [{ address: 'joe@example.com', orcpt: orcpt(82) }] }),\n\t\t);\n\t\texpect(item.recipients[0]?.orcpt).toEqual(orcpt(82));\n\t\tawait rejects(\n\t\t\tstore.add(\n\t\t\t\tentry({\n\t\t\t\t\trecipients: [{ address: 'joe@example.com', orcpt: orcpt(83) }],\n\t\t\t\t}),\n\t\t\t),\n\t\t\t'INVALID',\n\t\t);\n\t});\n\n\ttest('what is not a parameter is INVALID, the same for every store', async () => {\n\t\tconst store = await create();\n\t\tconst bad: Record<string, unknown>[] = [\n\t\t\t{ ret: 'ALL' },\n\t\t\t{ envid: '' },\n\t\t\t{ envid: 'a\\r\\nb' },\n\t\t\t{ envid: 'x'.repeat(101) },\n\t\t\t{ recipients: [{ address: 'zed@example.com', notify: 'NEVER' }] },\n\t\t\t{ recipients: [{ address: 'joe@example.com', notify: 'SOMETIMES' }] },\n\t\t\t{ recipients: [{ address: 'joe@example.com', notify: [] }] },\n\t\t\t{\n\t\t\t\trecipients: [\n\t\t\t\t\t{ address: 'joe@example.com', notify: ['DELAY', 'DELAY'] },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\trecipients: [{ address: 'joe@example.com', orcpt: { type: 'rfc822' } }],\n\t\t\t},\n\t\t\t{\n\t\t\t\trecipients: [\n\t\t\t\t\t{ address: 'joe@example.com' },\n\t\t\t\t\t{ address: 'joe@example.com' },\n\t\t\t\t],\n\t\t\t},\n\t\t];\n\t\tfor (const params of bad) {\n\t\t\tawait rejects(store.add(entry(params as never)), 'INVALID');\n\t\t}\n\t\texpect(await store.count()).toBe(0);\n\t});\n}\n",
11
+ "import { describe, expect, test } from 'bun:test';\nimport { completeDsns } from './complete-dsns';\nimport { doubt, fallbackDoubt } from './doubt';\nimport {\n\tentry,\n\tlease,\n\tMINUTE,\n\trejects,\n\tresult,\n\ttype StoreFactories,\n\tT0,\n} from './setup';\n\n/** What `complete` records. */\nexport function describeOutcomes(factories: StoreFactories): void {\n\tdescribe('outcomes', () => {\n\t\trecording(factories);\n\t\trefusing(factories);\n\t\tfinality(factories);\n\t\tcompleteDsns(factories);\n\t\tdoubt(factories);\n\t\tfallbackDoubt(factories);\n\t});\n}\n\nfunction recording({ create }: StoreFactories): void {\n\ttest('complete records each outcome, the schedule, and lets go of the lease', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tconst reply = {\n\t\t\tcode: 451,\n\t\t\tstatus: '4.3.0',\n\t\t\ttext: 'try later',\n\t\t\thost: 'mx.example.com',\n\t\t};\n\t\tconst done = await store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({\n\t\t\t\trecipients: [{ address: 'joe@example.com', status: 'deferred', reply }],\n\t\t\t}),\n\t\t\t[],\n\t\t);\n\t\texpect(done?.item.recipients).toEqual([\n\t\t\t{\n\t\t\t\taddress: 'joe@example.com',\n\t\t\t\tstatus: 'deferred',\n\t\t\t\treply,\n\t\t\t\tupdatedAt: T0 + MINUTE,\n\t\t\t},\n\t\t\t{ address: 'ann@example.org', status: 'pending' },\n\t\t]);\n\t\tconst stored = await store.get(item.id);\n\t\texpect(stored).toEqual(done?.item);\n\t\texpect(stored).toMatchObject({\n\t\t\tnextAttemptAt: T0 + 30 * MINUTE,\n\t\t\tattempts: 1,\n\t\t});\n\t\texpect(stored?.lease).toBeUndefined();\n\t\texpect(\n\t\t\tawait store.claim({ owner: 'w2', now: T0 + 29 * MINUTE, ...lease }),\n\t\t).toBeUndefined();\n\t\texpect(\n\t\t\tawait store.claim({ owner: 'w2', now: T0 + 30 * MINUTE, ...lease }),\n\t\t).toBeDefined();\n\t});\n\n\ttest('an item whose every recipient is final is dropped with its message', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tconst done = await store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({\n\t\t\t\trecipients: [\n\t\t\t\t\t{\n\t\t\t\t\t\taddress: 'joe@example.com',\n\t\t\t\t\t\tstatus: 'delivered',\n\t\t\t\t\t\treply: { code: 250, text: 'ok' },\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\taddress: 'ann@example.org',\n\t\t\t\t\t\tstatus: 'failed',\n\t\t\t\t\t\treply: { code: 550, text: 'no' },\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdelayNotified: true,\n\t\t\t}),\n\t\t\t[],\n\t\t);\n\t\texpect(done?.item.recipients.map((r) => r.status)).toEqual([\n\t\t\t'delivered',\n\t\t\t'failed',\n\t\t]);\n\t\texpect(done?.item.delayNotified).toBe(true);\n\t\texpect(await store.get(item.id)).toBeUndefined();\n\t\texpect(await store.readMessage(item.id)).toBeUndefined();\n\t\texpect(await store.count()).toBe(0);\n\t});\n}\n\nfunction refusing({ create }: StoreFactories): void {\n\ttest('an outcome holding a NUL or a lone surrogate is refused, and the item stays as it was', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tconst bad = [\n\t\t\t{ code: 451, text: `${'a'.repeat(8)}\\ud83d` },\n\t\t\t{ code: 451, text: 'nul \\u0000 here' },\n\t\t\t{ code: 451, text: 'ok', host: 'mx\\udc00.example.com' },\n\t\t\t{ code: 451, status: '4.3.0\\u0000', text: 'ok' },\n\t\t];\n\t\tfor (const reply of bad) {\n\t\t\tawait rejects(\n\t\t\t\tstore.complete(\n\t\t\t\t\titem.id,\n\t\t\t\t\t'w1',\n\t\t\t\t\tresult({\n\t\t\t\t\t\trecipients: [\n\t\t\t\t\t\t\t{ address: 'joe@example.com', status: 'deferred', reply },\n\t\t\t\t\t\t],\n\t\t\t\t\t}),\n\t\t\t\t\t[],\n\t\t\t\t),\n\t\t\t\t'INVALID',\n\t\t\t);\n\t\t}\n\t\tawait rejects(\n\t\t\tstore.claim({ owner: 'w\\ud800', now: T0, ...lease }),\n\t\t\t'INVALID',\n\t\t);\n\t\tconst stored = await store.get(item.id);\n\t\texpect(stored?.attempts).toBe(0);\n\t\texpect(stored?.lease?.owner).toBe('w1');\n\t\tconst done = await store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({\n\t\t\t\trecipients: [\n\t\t\t\t\t{\n\t\t\t\t\t\taddress: 'joe@example.com',\n\t\t\t\t\t\tstatus: 'deferred',\n\t\t\t\t\t\treply: { code: 451, text: 'Try later 😀' },\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}),\n\t\t\t[],\n\t\t);\n\t\texpect(done?.item.recipients[0]?.reply?.text).toBe('Try later 😀');\n\t\texpect((await store.get(item.id))?.recipients[0]?.reply?.text).toBe(\n\t\t\t'Try later 😀',\n\t\t);\n\t});\n}\n\nfunction finality({ create }: StoreFactories): void {\n\ttest('a final state is never changed by a later outcome', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tawait store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({\n\t\t\t\trecipients: [{ address: 'joe@example.com', status: 'delivered' }],\n\t\t\t}),\n\t\t\t[],\n\t\t);\n\t\tawait store.claim({ owner: 'w1', now: T0 + 30 * MINUTE, ...lease });\n\t\tconst after = await store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({\n\t\t\t\trecipients: [{ address: 'joe@example.com', status: 'failed' }],\n\t\t\t\tattempts: 2,\n\t\t\t}),\n\t\t\t[],\n\t\t);\n\t\texpect(after?.item.recipients[0]?.status).toBe('delivered');\n\t});\n}\n",
12
+ "import { expect, test } from 'bun:test';\nimport type { NewQueueItem } from '../contract/types';\nimport {\n\tbytes,\n\tentry,\n\tlease,\n\tMINUTE,\n\trejects,\n\tresult,\n\ttype StoreFactories,\n\tT0,\n} from './setup';\n\nconst dsn = (text: string): NewQueueItem =>\n\tentry({\n\t\tfrom: '',\n\t\tto: ['mary@example.net'],\n\t\tmessage: bytes(`Subject: ${text}\\r\\n\\r\\n${text}\\r\\n`),\n\t});\n\nconst delivered = result({\n\trecipients: [\n\t\t{ address: 'joe@example.com', status: 'delivered' },\n\t\t{ address: 'ann@example.org', status: 'failed' },\n\t],\n});\n\n/** The DSNs `complete` takes: stored with the outcome, or not at all. */\nexport function completeDsns(factories: StoreFactories): void {\n\tstored(factories);\n\tleaseHeld(factories);\n\trefused(factories);\n\tnotFinal(factories);\n}\n\nfunction stored({ create }: StoreFactories): void {\n\ttest('complete stores its DSNs with the outcome: new ids, in the order given, due at once', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tconst done = await store.complete(item.id, 'w1', delivered, [\n\t\t\tdsn('failed'),\n\t\t\tdsn('relayed'),\n\t\t]);\n\t\texpect(done?.item.id).toBe(item.id);\n\t\texpect(done?.dsns).toHaveLength(2);\n\t\tconst [first, second] = done?.dsns ?? [];\n\t\texpect(first?.id).not.toBe(second?.id);\n\t\texpect([first?.id, second?.id]).not.toContain(item.id);\n\t\texpect(first).toMatchObject({ from: '', attempts: 0 });\n\t\texpect(await store.get(item.id)).toBeUndefined();\n\t\tfor (const [kept, subject] of [\n\t\t\t[first, 'failed'],\n\t\t\t[second, 'relayed'],\n\t\t] as const) {\n\t\t\texpect(await store.get(kept?.id ?? '')).toEqual(kept);\n\t\t\texpect(\n\t\t\t\tnew TextDecoder().decode(await store.readMessage(kept?.id ?? '')),\n\t\t\t).toContain(`Subject: ${subject}`);\n\t\t}\n\t\t// Due at once, the first stored first.\n\t\texpect(\n\t\t\t(await store.claim({ owner: 'w2', now: T0 + MINUTE, ...lease }))?.id,\n\t\t).toBe(first?.id);\n\t\texpect(await store.count()).toBe(2);\n\t});\n}\n\nfunction leaseHeld({ create }: StoreFactories): void {\n\ttest('complete by an owner that no longer holds the lease stores no DSN, and a second one by the first stores none twice', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\texpect(\n\t\t\tawait store.complete(item.id, 'w2', delivered, [dsn('failed')]),\n\t\t).toBeUndefined();\n\t\texpect(await store.count()).toBe(1);\n\t\texpect(\n\t\t\tawait store.complete(item.id, 'w1', delivered, [dsn('failed')]),\n\t\t).toBeDefined();\n\t\texpect(await store.count()).toBe(1);\n\t\texpect(\n\t\t\tawait store.complete(item.id, 'w1', delivered, [dsn('failed')]),\n\t\t).toBeUndefined();\n\t\texpect(await store.count()).toBe(1);\n\t});\n}\n\nfunction refused({ create }: StoreFactories): void {\n\ttest('a DSN that fails the checks of add refuses the whole call with INVALID: no outcome, no DSN', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tawait rejects(\n\t\t\tstore.complete(item.id, 'w1', delivered, [\n\t\t\t\tdsn('failed'),\n\t\t\t\t{ ...dsn('bad'), to: [] },\n\t\t\t]),\n\t\t\t'INVALID',\n\t\t);\n\t\texpect(await store.count()).toBe(1);\n\t\texpect(await store.get(item.id)).toMatchObject({\n\t\t\tattempts: 0,\n\t\t\tlease: { owner: 'w1' },\n\t\t});\n\t});\n\n\ttest('dsns that are not an array refuse the call with INVALID, nothing recorded', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tawait rejects(\n\t\t\tstore.complete(item.id, 'w1', delivered, undefined as never),\n\t\t\t'INVALID',\n\t\t);\n\t\texpect(await store.get(item.id)).toMatchObject({ attempts: 0 });\n\t});\n}\n\nfunction notFinal({ create }: StoreFactories): void {\n\ttest('complete with no DSN returns an empty list; a not-final item stays with its DSNs beside it', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tconst none = await store.complete(item.id, 'w1', result(), []);\n\t\texpect(none?.dsns).toEqual([]);\n\t\tawait store.claim({ owner: 'w1', now: T0 + 30 * MINUTE, ...lease });\n\t\tconst deferred = result({\n\t\t\trecipients: [{ address: 'joe@example.com', status: 'deferred' }],\n\t\t\tattempts: 2,\n\t\t\tdelayNotified: true,\n\t\t});\n\t\tconst done = await store.complete(item.id, 'w1', deferred, [\n\t\t\tdsn('delayed'),\n\t\t]);\n\t\texpect(done?.item.delayNotified).toBe(true);\n\t\texpect(done?.dsns).toHaveLength(1);\n\t\texpect(await store.count()).toBe(2);\n\t\texpect(await store.get(item.id)).toEqual(done?.item);\n\t});\n}\n",
13
+ "import { expect, test } from 'bun:test';\nimport type { AttemptResult } from '../contract/types';\nimport {\n\tentry,\n\tlease,\n\tMINUTE,\n\trejects,\n\tresult,\n\ttype StoreFactories,\n\tT0,\n} from './setup';\n\n/** The marks a recipient keeps when a server may hold a copy. */\nexport function doubt({ create }: StoreFactories): void {\n\tconst deferred = (mayHaveReachedMx?: unknown) =>\n\t\t({\n\t\t\taddress: 'joe@example.com',\n\t\t\tstatus: 'deferred',\n\t\t\treply: { status: '4.4.2', text: 'no reply to the final dot' },\n\t\t\t...(mayHaveReachedMx === undefined ? {} : { mayHaveReachedMx }),\n\t\t}) as AttemptResult['recipients'][number];\n\n\ttest('mayHaveReachedMx: recorded, read by get and list, and kept by a later outcome without it', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tconst done = await store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({ recipients: [deferred(true)] }),\n\t\t\t[],\n\t\t);\n\t\texpect(done?.item.recipients[0]?.mayHaveReachedMx).toBe(true);\n\t\texpect(done?.item.recipients[1]).not.toHaveProperty('mayHaveReachedMx');\n\t\texpect((await store.get(item.id))?.recipients[0]?.mayHaveReachedMx).toBe(\n\t\t\ttrue,\n\t\t);\n\t\texpect((await store.list())[0]?.recipients[0]?.mayHaveReachedMx).toBe(true);\n\t\tawait store.claim({ owner: 'w1', now: T0 + 30 * MINUTE, ...lease });\n\t\tconst again = await store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({ recipients: [deferred()], attempts: 2 }),\n\t\t\t[],\n\t\t);\n\t\texpect(again?.item.recipients[0]?.mayHaveReachedMx).toBe(true);\n\t\texpect((await store.get(item.id))?.recipients[0]?.mayHaveReachedMx).toBe(\n\t\t\ttrue,\n\t\t);\n\t});\n\n\ttest('mayHaveReachedMx: an unmarked recipient has no such key', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tawait store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({ recipients: [deferred()] }),\n\t\t\t[],\n\t\t);\n\t\tfor (const r of (await store.get(item.id))?.recipients ?? []) {\n\t\t\texpect(r).not.toHaveProperty('mayHaveReachedMx');\n\t\t}\n\t});\n\n\ttest('mayHaveReachedMx: a value other than true is INVALID, and the item stays as it was', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tfor (const bad of [false, 'true', 1, null]) {\n\t\t\tawait rejects(\n\t\t\t\tstore.complete(\n\t\t\t\t\titem.id,\n\t\t\t\t\t'w1',\n\t\t\t\t\tresult({ recipients: [deferred(bad)] }),\n\t\t\t\t\t[],\n\t\t\t\t),\n\t\t\t\t'INVALID',\n\t\t\t);\n\t\t}\n\t\tconst kept = await store.get(item.id);\n\t\texpect(kept?.recipients[0]?.status).toBe('pending');\n\t\texpect(kept?.lease?.owner).toBe('w1');\n\t});\n}\n\nexport function fallbackDoubt({ create }: StoreFactories): void {\n\tconst deferred = (mayHaveReachedFallback?: unknown) =>\n\t\t({\n\t\t\taddress: 'joe@example.com',\n\t\t\tstatus: 'deferred',\n\t\t\treply: { status: '4.4.2', text: 'no reply to the final dot' },\n\t\t\t...(mayHaveReachedFallback === undefined\n\t\t\t\t? {}\n\t\t\t\t: { mayHaveReachedFallback }),\n\t\t}) as AttemptResult['recipients'][number];\n\n\ttest('mayHaveReachedFallback: recorded, read by get and list, and kept by a later outcome without it', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tconst done = await store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({ recipients: [deferred(true)] }),\n\t\t\t[],\n\t\t);\n\t\texpect(done?.item.recipients[0]?.mayHaveReachedFallback).toBe(true);\n\t\texpect(done?.item.recipients[1]).not.toHaveProperty(\n\t\t\t'mayHaveReachedFallback',\n\t\t);\n\t\texpect(\n\t\t\t(await store.get(item.id))?.recipients[0]?.mayHaveReachedFallback,\n\t\t).toBe(true);\n\t\texpect((await store.list())[0]?.recipients[0]?.mayHaveReachedFallback).toBe(\n\t\t\ttrue,\n\t\t);\n\t\tawait store.claim({ owner: 'w1', now: T0 + 30 * MINUTE, ...lease });\n\t\tconst again = await store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({ recipients: [deferred()], attempts: 2 }),\n\t\t\t[],\n\t\t);\n\t\texpect(again?.item.recipients[0]?.mayHaveReachedFallback).toBe(true);\n\t\texpect(\n\t\t\t(await store.get(item.id))?.recipients[0]?.mayHaveReachedFallback,\n\t\t).toBe(true);\n\t});\n\n\ttest('mayHaveReachedFallback: an unmarked recipient has no such key', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tawait store.complete(\n\t\t\titem.id,\n\t\t\t'w1',\n\t\t\tresult({ recipients: [deferred()] }),\n\t\t\t[],\n\t\t);\n\t\tfor (const r of (await store.get(item.id))?.recipients ?? []) {\n\t\t\texpect(r).not.toHaveProperty('mayHaveReachedFallback');\n\t\t}\n\t});\n\n\ttest('mayHaveReachedFallback: a value other than true is INVALID, and the item stays as it was', async () => {\n\t\tconst store = await create();\n\t\tconst item = await store.add(entry());\n\t\tawait store.claim({ owner: 'w1', now: T0, ...lease });\n\t\tfor (const bad of [false, 'true', 1, null]) {\n\t\t\tawait rejects(\n\t\t\t\tstore.complete(\n\t\t\t\t\titem.id,\n\t\t\t\t\t'w1',\n\t\t\t\t\tresult({ recipients: [deferred(bad)] }),\n\t\t\t\t\t[],\n\t\t\t\t),\n\t\t\t\t'INVALID',\n\t\t\t);\n\t\t}\n\t\tconst kept = await store.get(item.id);\n\t\texpect(kept?.recipients[0]?.status).toBe('pending');\n\t\texpect(kept?.lease?.owner).toBe('w1');\n\t});\n}\n"
14
+ ],
15
+ "mappings": "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",
16
+ "debugId": "434F5188249F7D1F64756E2164756E21",
17
+ "names": []
18
+ }
@@ -0,0 +1,4 @@
1
+ import { type StoreFactories } from './setup';
2
+ /** Adding, reading, listing and cancelling items. */
3
+ export declare function describeItems(factories: StoreFactories): void;
4
+ //# sourceMappingURL=items.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../src/conformance/items.ts"],"names":[],"mappings":"AAEA,OAAO,EAKN,KAAK,cAAc,EAEnB,MAAM,SAAS,CAAC;AAEjB,qDAAqD;AACrD,wBAAgB,aAAa,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAM7D"}
@@ -0,0 +1,4 @@
1
+ import { type StoreFactories } from './setup';
2
+ /** What `complete` records. */
3
+ export declare function describeOutcomes(factories: StoreFactories): void;
4
+ //# sourceMappingURL=outcomes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outcomes.d.ts","sourceRoot":"","sources":["../../src/conformance/outcomes.ts"],"names":[],"mappings":"AAGA,OAAO,EAMN,KAAK,cAAc,EAEnB,MAAM,SAAS,CAAC;AAEjB,+BAA+B;AAC/B,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAShE"}
@@ -0,0 +1,40 @@
1
+ import type { QueueStore } from '../contract/queue-store';
2
+ import type { AttemptResult, NewQueueItem } from '../contract/types';
3
+ /** A new store, empty. */
4
+ export type CreateQueueStore = () => Promise<QueueStore> | QueueStore;
5
+ /**
6
+ * Another handle on the same queue as `store`: a second connection to the
7
+ * same database, as a second process would open. A harness with no second
8
+ * handle omits `share`, and the two several-workers tests are then skipped.
9
+ */
10
+ export type ShareStore = (store: QueueStore) => QueueStore;
11
+ /** What a store's spec hands the suite. */
12
+ export interface QueueStoreHarness {
13
+ /** A new store, empty. */
14
+ readonly create: CreateQueueStore;
15
+ /** Another handle on the same queue; without it the share tests are skipped. */
16
+ readonly share?: ShareStore | undefined;
17
+ /**
18
+ * Closes a store `create` made, after the test that used it. Handles
19
+ * returned by `share` are the harness's to close.
20
+ */
21
+ readonly dispose?: ((store: QueueStore) => Promise<void> | void) | undefined;
22
+ }
23
+ /** What the suite is handed: how to build a store, and what the harness can do beyond that. */
24
+ export interface StoreFactories {
25
+ readonly create: CreateQueueStore;
26
+ /** Absent when the harness has no second handle: the share tests are skipped. */
27
+ readonly share?: ShareStore | undefined;
28
+ }
29
+ export declare const bytes: (text: string) => Uint8Array<ArrayBuffer>;
30
+ export declare const T0: number;
31
+ export declare const MINUTE = 60000;
32
+ export declare const lease: {
33
+ leaseMs: number;
34
+ };
35
+ export declare const result: (overrides?: Partial<AttemptResult>) => AttemptResult;
36
+ /** A new item from mary to two recipients, created at `T0` unless told otherwise. */
37
+ export declare function entry(overrides?: Partial<NewQueueItem>): NewQueueItem;
38
+ /** Expects a promise to reject with a QueueError of this code. */
39
+ export declare function rejects(promise: Promise<unknown>, code: string): Promise<void>;
40
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/conformance/setup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAErE,0BAA0B;AAC1B,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC;AAEtE;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,UAAU,CAAC;AAE3D,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IACjC,0BAA0B;IAC1B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;CAC7E;AAED,+FAA+F;AAC/F,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,iFAAiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACxC;AAED,eAAO,MAAM,KAAK,GAAI,MAAM,MAAM,4BAAmC,CAAC;AAEtE,eAAO,MAAM,EAAE,QAA2B,CAAC;AAC3C,eAAO,MAAM,MAAM,QAAS,CAAC;AAE7B,eAAO,MAAM,KAAK;;CAA2B,CAAC;AAE9C,eAAO,MAAM,MAAM,GAClB,YAAW,OAAO,CAAC,aAAa,CAAM,KACpC,aAOD,CAAC;AAEH,qFAAqF;AACrF,wBAAgB,KAAK,CAAC,SAAS,GAAE,OAAO,CAAC,YAAY,CAAM,GAAG,YAAY,CAQzE;AAED,kEAAkE;AAClE,wBAAsB,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,iBAEpE"}
package/docs/guide.md CHANGED
@@ -1046,8 +1046,90 @@ codes, and keeps its promises:
1046
1046
  The queue never gives a store such text: it cleans every reply.
1047
1047
 
1048
1048
  The contract's specs, `describeQueueStore`, which every store here runs,
1049
- are internal for now: a store written outside this package cannot run
1050
- them yet.
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.
1051
1133
 
1052
1134
  ## Options
1053
1135
 
package/docs/roadmap.md CHANGED
@@ -18,11 +18,11 @@ Nothing under way.
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
21
 
24
22
  ## Later
25
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.
26
26
  - **REQUIRETLS (RFC 8689)** — a message that asks for TLS all the way is
27
27
  never sent in clear, and fails rather than downgrade.
28
28
  - **Retries per recipient** — a schedule of its own for each recipient,
@@ -46,6 +46,14 @@ Nothing under way.
46
46
 
47
47
  ## Shipped
48
48
 
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
+
49
57
  ### 0.11.0
50
58
 
51
59
  - **A DSN recorded in the same step as the outcome** — the failure, relayed
@@ -89,6 +89,11 @@ parts shown as … vary.
89
89
  - [`QueueError: reply.host must be a string`](#queueerror-replyhost-must-be-a-string)
90
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)
91
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
+
92
97
  **Delivery**
93
98
 
94
99
  - [A recipient stays `deferred` with `4.4.1` or `4.4.2`](#a-recipient-stays-deferred-with-441-or-442)
@@ -545,6 +550,43 @@ it.
545
550
  default. The error from a transaction of your own on the same client is
546
551
  yours to retry.
547
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
+
548
590
  ## The `bun:sqlite` store
549
591
 
550
592
  ### `QueueError: A SQLite queue store needs a directory`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bumail/queue",
3
- "version": "0.11.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,7 +66,8 @@
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": {