@bumail/queue 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +44 -8
  2. package/dist/chunks/index-b4xvg3xn.js +23 -0
  3. package/dist/chunks/index-b4xvg3xn.js.map +10 -0
  4. package/dist/chunks/index-vg971acr.js.map +2 -2
  5. package/dist/contract/queue-store.d.ts +8 -2
  6. package/dist/contract/queue-store.d.ts.map +1 -1
  7. package/dist/dsn/build.d.ts +7 -3
  8. package/dist/dsn/build.d.ts.map +1 -1
  9. package/dist/errors.d.ts +5 -2
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/index.js +117 -33
  12. package/dist/index.js.map +8 -7
  13. package/dist/masked.d.ts +8 -0
  14. package/dist/masked.d.ts.map +1 -0
  15. package/dist/postgres/connect.d.ts +6 -0
  16. package/dist/postgres/connect.d.ts.map +1 -1
  17. package/dist/postgres/index.js +49 -28
  18. package/dist/postgres/index.js.map +7 -6
  19. package/dist/postgres/isolation.d.ts +6 -0
  20. package/dist/postgres/isolation.d.ts.map +1 -0
  21. package/dist/postgres/schema.d.ts.map +1 -1
  22. package/dist/postgres/store.d.ts.map +1 -1
  23. package/dist/queue/deliver.d.ts +10 -1
  24. package/dist/queue/deliver.d.ts.map +1 -1
  25. package/dist/queue/lease.d.ts +41 -0
  26. package/dist/queue/lease.d.ts.map +1 -0
  27. package/dist/queue/notify.d.ts +3 -2
  28. package/dist/queue/notify.d.ts.map +1 -1
  29. package/dist/queue/worker.d.ts.map +1 -1
  30. package/dist/redis/connect.d.ts +27 -0
  31. package/dist/redis/connect.d.ts.map +1 -0
  32. package/dist/redis/index.d.ts +3 -0
  33. package/dist/redis/index.d.ts.map +1 -0
  34. package/dist/redis/index.js +438 -0
  35. package/dist/redis/index.js.map +14 -0
  36. package/dist/redis/items.d.ts +13 -0
  37. package/dist/redis/items.d.ts.map +1 -0
  38. package/dist/redis/options.d.ts +47 -0
  39. package/dist/redis/options.d.ts.map +1 -0
  40. package/dist/redis/run.d.ts +12 -0
  41. package/dist/redis/run.d.ts.map +1 -0
  42. package/dist/redis/scripts.d.ts +60 -0
  43. package/dist/redis/scripts.d.ts.map +1 -0
  44. package/dist/redis/store.d.ts +30 -0
  45. package/dist/redis/store.d.ts.map +1 -0
  46. package/docs/README.md +1 -1
  47. package/docs/guide.md +271 -16
  48. package/docs/roadmap.md +49 -7
  49. package/docs/troubleshooting.md +303 -16
  50. package/package.json +9 -3
package/README.md CHANGED
@@ -4,9 +4,9 @@ The outbound queue of a mail server: it keeps every message your server
4
4
  sends to another one, delivers it through `@bumail/smtp/client`, retries
5
5
  what failed for now, and sends a delivery status notification (RFC 3464)
6
6
  back when it gives up. Each recipient has its own state, the queue
7
- survives a restart on `bun:sqlite` or PostgreSQL, and several workers —
8
- on one machine, or on many with PostgreSQL — can share one queue. No
9
- runtime dependency: only peers.
7
+ survives a restart on `bun:sqlite`, PostgreSQL or Redis, and several
8
+ workers — on one machine, or on many with PostgreSQL or Redis — can
9
+ share one queue. No runtime dependency: only peers.
10
10
 
11
11
  ## Install
12
12
 
@@ -27,6 +27,7 @@ queue that only uses a smarthost needs none.
27
27
  | `@bumail/queue/memory` | `MemoryQueueStore`: in memory, lost on a restart — for specs and trials |
28
28
  | `@bumail/queue/sqlite` | `SqliteQueueStore`: on disk with `bun:sqlite`, shared by the processes of one machine |
29
29
  | `@bumail/queue/postgres` | `PostgresQueueStore`: on PostgreSQL through Bun's own `Bun.sql`, shared by instances on several machines |
30
+ | `@bumail/queue/redis` | `RedisQueueStore`: on Redis through Bun's own `Bun.redis`, shared by instances on several machines |
30
31
 
31
32
  ## Usage
32
33
 
@@ -172,9 +173,43 @@ process.on('SIGTERM', async () => {
172
173
  A claim is one `UPDATE … RETURNING` whose item a `SELECT … FOR UPDATE
173
174
  SKIP LOCKED` picks: two instances never take the same item, and none
174
175
  waits on one another is taking. An instance that crashes loses its items
175
- when their leases expire, as with `bun:sqlite`. No driver to install:
176
+ when their leases expire, as with `bun:sqlite`. Every write runs at
177
+ `READ COMMITTED`, so a client whose sessions default to `repeatable
178
+ read` or `serializable` serves the queue too. No driver to install:
176
179
  `Bun.sql` is Bun's.
177
180
 
181
+ ## Redis, for several machines
182
+
183
+ ```ts
184
+ import { nodeResolver } from '@bumail/dns';
185
+ import { createQueue } from '@bumail/queue';
186
+ import { RedisQueueStore } from '@bumail/queue/redis';
187
+
188
+ // In each instance, on the same Redis. A Bun.RedisClient of your own works too:
189
+ // RedisQueueStore.open({ client: new Bun.RedisClient(url) }).
190
+ const store = RedisQueueStore.open({
191
+ url: Bun.env['REDIS_URL'] ?? 'redis://localhost:6379',
192
+ keyPrefix: 'bumail:queue:', // the default: bumail:queue:items, …:item:<id>, …
193
+ });
194
+ const queue = createQueue({ store, hostname: 'mail.example.net', resolver: nodeResolver() });
195
+ queue.start();
196
+
197
+ process.on('SIGTERM', async () => {
198
+ await queue.stop();
199
+ await store.close(); // closes the client it opened for the URL, never yours
200
+ });
201
+ ```
202
+
203
+ Every operation that writes is one Lua script, which Redis runs whole:
204
+ two instances never take the same item, and a crashed instance's items
205
+ are claimed again once their leases expire. The message is kept byte for
206
+ byte. One Redis, or a primary with replicas — not Redis Cluster. Redis
207
+ acknowledges a write once it is in memory: with `appendfsync everysec`
208
+ a crash loses up to a second of writes, and a failover to a replica can
209
+ lose acknowledged ones, so keep `appendonly yes`, `appendfsync always`
210
+ and `maxmemory-policy noeviction` where a lost message matters (the
211
+ guide's Redis section). No driver to install: `Bun.redis` is Bun's.
212
+
178
213
  ## Events and admin
179
214
 
180
215
  ```ts
@@ -243,8 +278,8 @@ control character in it.
243
278
 
244
279
  ## Traps
245
280
 
246
- - **Bun only.** The stores use `bun:sqlite` and `Bun.sql`, so the package
247
- runs on Bun 1.4.2 or later, not on Node.
281
+ - **Bun only.** The stores use `bun:sqlite`, `Bun.sql` and `Bun.redis`,
282
+ so the package runs on Bun 1.4.2 or later, not on Node.
248
283
  - **It sends what you enqueue, to anyone.** The queue is not a relay
249
284
  policy: enqueue only what an authenticated user submitted, or what your
250
285
  own server writes, never what an unauthenticated client handed you.
@@ -260,16 +295,17 @@ control character in it.
260
295
  | `QueueStore` | the contract a store answers: `add`, `get`, `list`, `count`, `readMessage`, `claim`, `renew`, `complete`, `reschedule`, `cancel` |
261
296
  | `QueueItem`, `RecipientState`, `RecipientStatus`, `FinalStatus`, `Diagnostic`, `Lease` | an item, and where each recipient stands |
262
297
  | `NewQueueItem`, `AddOptions`, `ClaimRequest`, `AttemptResult`, `RecipientUpdate`, `QueueListOptions` | what a store takes |
263
- | `QueueError`, `QueueErrorCode` | `INVALID`, `MESSAGE_TOO_BIG`, `TOO_MANY_RECIPIENTS`, `QUEUE_FULL`, `CLOSED`, `LEASE_LOST` |
298
+ | `QueueError`, `QueueErrorCode` | `INVALID`, `MESSAGE_TOO_BIG`, `TOO_MANY_RECIPIENTS`, `QUEUE_FULL`, `CLOSED`, `LEASE_LOST`, `MESSAGE_UNREADABLE` |
264
299
  | `MemoryQueueStore` | from `@bumail/queue/memory` |
265
300
  | `SqliteQueueStore`, `SqliteQueueStoreOptions` | from `@bumail/queue/sqlite`: `SqliteQueueStore.open({ directory, busyTimeout? })`, `close()` |
266
301
  | `PostgresQueueStore`, `PostgresQueueStoreOptions`, `PostgresClient`, `PostgresQueryable` | from `@bumail/queue/postgres`: `PostgresQueueStore.open({ sql, tablePrefix? })`, `migrate()`, `close()`; `sql` a `Bun.SQL` client or a `postgres://` URL |
302
+ | `RedisQueueStore`, `RedisQueueStoreOptions`, `RedisQueueClientOptions`, `RedisQueueUrlOptions`, `RedisQueueClient` | from `@bumail/queue/redis`: `RedisQueueStore.open({ client \| url, keyPrefix? })`, `close()`; `client` a `Bun.RedisClient`, `url` a `redis://` URL |
267
303
 
268
304
  ## Documentation
269
305
 
270
306
  These pages ship in the package, under `docs/`.
271
307
 
272
308
  - [Index](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/README.md): the pages, and when to read each.
273
- - [Guide](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/guide.md): enqueuing, delivery and routing, the retry schedule, DSNs, several workers and leases, events and admin, the stores (PostgreSQL included), testing, and writing a store of your own.
309
+ - [Guide](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/guide.md): enqueuing, delivery and routing, the retry schedule, DSNs, several workers and leases, events and admin, the stores (PostgreSQL and Redis included), testing, and writing a store of your own.
274
310
  - [Troubleshooting](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/troubleshooting.md): every `QueueError`, and what to do about it.
275
311
  - [Roadmap](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/roadmap.md): what is coming, and what is not planned.
@@ -0,0 +1,23 @@
1
+ // src/masked.ts
2
+ var MIN_BARE = 4;
3
+ function masked(text, raw) {
4
+ let decoded = raw;
5
+ try {
6
+ decoded = decodeURIComponent(raw);
7
+ } catch {}
8
+ const forms = [raw, decoded].filter((password) => password !== "");
9
+ let out = text;
10
+ for (const password of forms) {
11
+ out = out.replaceAll(`:${password}@`, ":…@");
12
+ }
13
+ for (const password of forms) {
14
+ if ([...password].length >= MIN_BARE)
15
+ out = out.replaceAll(password, "…");
16
+ }
17
+ return out;
18
+ }
19
+
20
+ export { masked };
21
+
22
+ //# debugId=DC4C707DD68894B564756E2164756E21
23
+ //# sourceMappingURL=index-b4xvg3xn.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/masked.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * The shortest password masked wherever it occurs. A shorter one, in\n * characters, is masked only where a URL holds it (`:…@`): replacing\n * `x` everywhere would turn \"execute\" into \"e…ecute\", and the reason\n * would no longer say what went wrong — while a password that short is\n * guessed anyway.\n */\nconst MIN_BARE = 4;\n\n/**\n * `text` with a URL's password masked, as written in the URL and decoded,\n * if it decodes: what a store opening a URL repeats of the client's\n * reason, which may name it. Its userinfo form, `:password@`, is always\n * masked; the password alone, elsewhere, only from `MIN_BARE` characters.\n */\nexport function masked(text: string, raw: string): string {\n\tlet decoded = raw;\n\ttry {\n\t\tdecoded = decodeURIComponent(raw);\n\t} catch {\n\t\t// Not percent-encoding a client reads either: the raw form is what it would repeat.\n\t}\n\tconst forms = [raw, decoded].filter((password) => password !== '');\n\tlet out = text;\n\tfor (const password of forms) {\n\t\tout = out.replaceAll(`:${password}@`, ':…@');\n\t}\n\tfor (const password of forms) {\n\t\tif ([...password].length >= MIN_BARE) out = out.replaceAll(password, '…');\n\t}\n\treturn out;\n}\n"
6
+ ],
7
+ "mappings": ";AAOA,IAAM,WAAW;AAQV,SAAS,MAAM,CAAC,MAAc,KAAqB;AAAA,EACzD,IAAI,UAAU;AAAA,EACd,IAAI;AAAA,IACH,UAAU,mBAAmB,GAAG;AAAA,IAC/B,MAAM;AAAA,EAGR,MAAM,QAAQ,CAAC,KAAK,OAAO,EAAE,OAAO,CAAC,aAAa,aAAa,EAAE;AAAA,EACjE,IAAI,MAAM;AAAA,EACV,WAAW,YAAY,OAAO;AAAA,IAC7B,MAAM,IAAI,WAAW,IAAI,aAAa,KAAK;AAAA,EAC5C;AAAA,EACA,WAAW,YAAY,OAAO;AAAA,IAC7B,IAAI,CAAC,GAAG,QAAQ,EAAE,UAAU;AAAA,MAAU,MAAM,IAAI,WAAW,UAAU,GAAG;AAAA,EACzE;AAAA,EACA,OAAO;AAAA;",
8
+ "debugId": "DC4C707DD68894B564756E2164756E21",
9
+ "names": []
10
+ }
@@ -2,11 +2,11 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/errors.ts", "../src/text.ts", "../src/contract/checks.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Why the queue or a store refused a call. `INVALID`: an option or an\n * argument it cannot take. `MESSAGE_TOO_BIG`, `TOO_MANY_RECIPIENTS` and\n * `QUEUE_FULL`: a limit `enqueue` enforces. `CLOSED`: a store used after\n * `close()`. `LEASE_LOST`: given to the `error` event when another worker\n * took an item before its outcome was recorded.\n */\nexport type QueueErrorCode =\n\t| 'INVALID'\n\t| 'MESSAGE_TOO_BIG'\n\t| 'TOO_MANY_RECIPIENTS'\n\t| 'QUEUE_FULL'\n\t| 'CLOSED'\n\t| 'LEASE_LOST';\n\n/** Thrown by the queue and by every store, whichever answers the contract, with the same codes. */\nexport class QueueError extends Error {\n\toverride readonly name = 'QueueError';\n\treadonly code: QueueErrorCode;\n\n\tconstructor(code: QueueErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n}\n\n/** `INVALID`, for an option or an argument. */\nexport const invalid = (message: string) => new QueueError('INVALID', message);\n",
5
+ "/**\n * Why the queue or a store refused a call. `INVALID`: an option or an\n * argument it cannot take. `MESSAGE_TOO_BIG`, `TOO_MANY_RECIPIENTS` and\n * `QUEUE_FULL`: a limit `enqueue` enforces. `CLOSED`: a store used after\n * `close()`. `LEASE_LOST`: given to the `error` event when another worker\n * took an item before its outcome was recorded — or, once the lease's\n * expiry passed, when the item is gone: lost or cancelled.\n * `MESSAGE_UNREADABLE`: given to the `error` event when the store holds an\n * item but not its message, whose pending recipients then fail.\n */\nexport type QueueErrorCode =\n\t| 'INVALID'\n\t| 'MESSAGE_TOO_BIG'\n\t| 'TOO_MANY_RECIPIENTS'\n\t| 'QUEUE_FULL'\n\t| 'CLOSED'\n\t| 'LEASE_LOST'\n\t| 'MESSAGE_UNREADABLE';\n\n/** Thrown by the queue and by every store, whichever answers the contract, with the same codes. */\nexport class QueueError extends Error {\n\toverride readonly name = 'QueueError';\n\treadonly code: QueueErrorCode;\n\n\tconstructor(code: QueueErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n}\n\n/** `INVALID`, for an option or an argument. */\nexport const invalid = (message: string) => new QueueError('INVALID', message);\n",
6
6
  "/** Text from the other side, made safe to keep and to write in a header field. */\n\n/** C0, DEL and C1: every control character, CR, LF and TAB included. */\nconst isControl = (code: number) =>\n\tcode < 0x20 || (code >= 0x7f && code <= 0x9f);\n\n/** Whether the text holds a control character. */\nexport function hasControl(text: string): boolean {\n\tfor (let i = 0; i < text.length; i++) {\n\t\tif (isControl(text.charCodeAt(i))) return true;\n\t}\n\treturn false;\n}\n\n/** Each control character replaced by `by`. */\nexport function replaceControls(text: string, by: string): string {\n\tlet out = '';\n\tfor (let i = 0; i < text.length; i++) {\n\t\tout += isControl(text.charCodeAt(i)) ? by : (text[i] as string);\n\t}\n\treturn out;\n}\n\n/**\n * Every control character (CR and LF included) as a space, a lone\n * surrogate as U+FFFD, runs of space as one, cut at `max` characters with\n * `...`, never inside a surrogate pair: what a reply leaves in an item,\n * an event and a DSN.\n */\nexport function cleanText(text: string, max: number): string {\n\tconst clean = replaceControls(text.toWellFormed(), ' ')\n\t\t.replace(/ {2,}/g, ' ')\n\t\t.trim();\n\treturn clean.length > max ? `${cut(clean, max - 3)}...` : clean;\n}\n\n/**\n * The first `length` UTF-16 units, never ending inside a surrogate pair:\n * a lone surrogate is text no store can keep (PostgreSQL's `jsonb` refuses\n * it), nor any UTF-8 encode.\n */\nfunction cut(text: string, length: number): string {\n\tconst head = text.slice(0, length);\n\tconst last = head.charCodeAt(head.length - 1);\n\treturn last >= 0xd800 && last <= 0xdbff ? head.slice(0, -1) : head;\n}\n\n/**\n * Text every store can keep: no NUL (PostgreSQL's `text` and `jsonb` hold\n * none) and no lone surrogate. `cleanText`'s output always is.\n */\nexport const isStorable = (text: string): boolean =>\n\ttext.isWellFormed() && !text.includes('\\0');\n\n/** `cleanText`, then anything not printable ASCII as `?`: for a `message/delivery-status` field. */\nexport const asciiText = (text: string, max: number): string =>\n\tcleanText(text, max).replace(/[^ -~]/g, '?');\n",
7
7
  "import { invalid } from '../errors';\nimport { isStorable } from '../text';\nimport type {\n\tAttemptResult,\n\tClaimRequest,\n\tDiagnostic,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n\tRecipientState,\n\tRecipientUpdate,\n} from './types';\n\n/** What every store checks the same way, so each refuses the same input with the same error. */\n\nconst isTime = (value: unknown): value is number =>\n\ttypeof value === 'number' && Number.isFinite(value);\n\n/** A time in milliseconds since the epoch, or `INVALID` naming it. */\nexport function checkTime(name: string, value: unknown): number {\n\tif (!isTime(value)) throw invalid(`${name} must be a finite number`);\n\treturn value;\n}\n\n/**\n * Text a store keeps: refused, the same way by every store, when it holds\n * a NUL or a lone surrogate, which PostgreSQL cannot keep and no UTF-8\n * encode can carry.\n */\nexport function checkStorable(name: string, text: string): void {\n\tif (!isStorable(text)) {\n\t\tthrow invalid(\n\t\t\t`${name} holds a NUL or a lone surrogate, which a store cannot keep`,\n\t\t);\n\t}\n}\n\n/** A lease owner: a non-empty string. */\nexport function checkOwner(owner: unknown): string {\n\tif (typeof owner !== 'string' || owner === '') {\n\t\tthrow invalid('owner must be a non-empty string');\n\t}\n\tcheckStorable('owner', owner);\n\treturn owner;\n}\n\n/** Every text of a recipient's outcome. */\nfunction checkUpdate(update: RecipientUpdate): void {\n\tcheckStorable('A recipient', update.address);\n\tconst reply = update.reply as Partial<Diagnostic> | undefined;\n\tif (reply === undefined) return;\n\tif (\n\t\ttypeof reply !== 'object' ||\n\t\treply === null ||\n\t\ttypeof reply.text !== 'string'\n\t) {\n\t\tthrow invalid('A reply is an object with a text');\n\t}\n\tcheckStorable('reply.text', reply.text);\n\tfor (const name of ['status', 'host'] as const) {\n\t\tconst value = reply[name];\n\t\tif (value === undefined) continue;\n\t\tif (typeof value !== 'string')\n\t\t\tthrow invalid(`reply.${name} must be a string`);\n\t\tcheckStorable(`reply.${name}`, value);\n\t}\n}\n\nexport function checkNewItem(item: NewQueueItem): void {\n\tif (typeof item !== 'object' || item === null) {\n\t\tthrow invalid('A new item is an object');\n\t}\n\tif (typeof item.from !== 'string') throw invalid('from must be a string');\n\tcheckStorable('from', item.from);\n\tconst to = item.to as unknown;\n\tif (\n\t\t!Array.isArray(to) ||\n\t\tto.length === 0 ||\n\t\t!to.every((a) => typeof a === 'string' && a !== '')\n\t) {\n\t\tthrow invalid('to must be a non-empty array of addresses');\n\t}\n\tfor (const address of to as string[]) checkStorable('A recipient', address);\n\tif (!(item.message instanceof Uint8Array)) {\n\t\tthrow invalid('message must be a Uint8Array');\n\t}\n\tcheckTime('createdAt', item.createdAt);\n}\n\n/** `maxItems`: a positive integer, or nothing. */\nexport function checkMaxItems(max: unknown): number | undefined {\n\tif (max === undefined) return undefined;\n\tif (typeof max !== 'number' || !Number.isInteger(max) || max < 1) {\n\t\tthrow invalid(`maxItems must be an integer of at least 1, not ${max}`);\n\t}\n\treturn max;\n}\n\nexport function checkClaim(request: ClaimRequest): void {\n\tif (typeof request !== 'object' || request === null) {\n\t\tthrow invalid('A claim is an object');\n\t}\n\tcheckOwner(request.owner);\n\tcheckTime('now', request.now);\n\tif (!isTime(request.leaseMs) || request.leaseMs <= 0) {\n\t\tthrow invalid('leaseMs must be a positive number');\n\t}\n}\n\nexport function checkResult(result: AttemptResult): void {\n\tif (typeof result !== 'object' || result === null) {\n\t\tthrow invalid('An attempt result is an object');\n\t}\n\tcheckTime('now', result.now);\n\tcheckTime('nextAttemptAt', result.nextAttemptAt);\n\tif (!Number.isInteger(result.attempts) || result.attempts < 0) {\n\t\tthrow invalid('attempts must be an integer of at least 0');\n\t}\n\tif (typeof result.delayNotified !== 'boolean') {\n\t\tthrow invalid('delayNotified must be true or false');\n\t}\n\tconst statuses = ['delivered', 'deferred', 'failed'];\n\tif (\n\t\t!Array.isArray(result.recipients) ||\n\t\t!result.recipients.every(\n\t\t\t(u) => typeof u?.address === 'string' && statuses.includes(u.status),\n\t\t)\n\t) {\n\t\tthrow invalid(\n\t\t\t'recipients must be an array of { address, status: delivered, deferred or failed }',\n\t\t);\n\t}\n\tfor (const update of result.recipients) checkUpdate(update);\n}\n\n/** The offset and limit of a list: integers, the limit 1 to 1000. */\nexport function checkList(options: QueueListOptions = {}): {\n\toffset: number;\n\tlimit: number;\n} {\n\tconst offset = options.offset ?? 0;\n\tconst limit = options.limit ?? 100;\n\tif (!Number.isInteger(offset) || offset < 0) {\n\t\tthrow invalid(`offset must be an integer of at least 0, not ${offset}`);\n\t}\n\tif (!Number.isInteger(limit) || limit < 1 || limit > 1000) {\n\t\tthrow invalid(`limit must be an integer from 1 to 1000, not ${limit}`);\n\t}\n\treturn { offset, limit };\n}\n\n/** Every recipient is `delivered` or `failed`. */\nexport const isDone = (item: Pick<QueueItem, 'recipients'>): boolean =>\n\titem.recipients.every(\n\t\t(r) => r.status === 'delivered' || r.status === 'failed',\n\t);\n\n/**\n * The item once an attempt is recorded: its recipients' outcomes, a final\n * state never changed again, the schedule, and no lease.\n */\nexport function applyAttempt(\n\titem: QueueItem,\n\tresult: AttemptResult,\n): QueueItem {\n\tconst updates = new Map(result.recipients.map((u) => [u.address, u]));\n\tconst recipients = item.recipients.map((r): RecipientState => {\n\t\tconst update = updates.get(r.address);\n\t\tif (!update || r.status === 'delivered' || r.status === 'failed') return r;\n\t\treturn {\n\t\t\taddress: r.address,\n\t\t\tstatus: update.status,\n\t\t\t...(update.reply ? { reply: update.reply } : {}),\n\t\t\tupdatedAt: result.now,\n\t\t};\n\t});\n\tconst { lease: _, ...rest } = item;\n\treturn {\n\t\t...rest,\n\t\trecipients,\n\t\tnextAttemptAt: result.nextAttemptAt,\n\t\tattempts: result.attempts,\n\t\tdelayNotified: result.delayNotified,\n\t};\n}\n\n/** A new item as a store first keeps it. */\nexport function newItem(id: string, item: NewQueueItem): QueueItem {\n\treturn {\n\t\tid,\n\t\tfrom: item.from,\n\t\trecipients: item.to.map((address) => ({ address, status: 'pending' })),\n\t\tsize: item.message.length,\n\t\tcreatedAt: item.createdAt,\n\t\tnextAttemptAt: item.createdAt,\n\t\tattempts: 0,\n\t\tdelayNotified: false,\n\t};\n}\n\n/** Whether a lease, if any, leaves the item free to claim at `now`. */\nexport const claimable = (item: QueueItem, now: number): boolean =>\n\titem.nextAttemptAt <= now &&\n\t(item.lease === undefined || item.lease.expiresAt <= now);\n"
8
8
  ],
9
- "mappings": "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",
9
+ "mappings": "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",
10
10
  "debugId": "81614FFC0DB6330C64756E2164756E21",
11
11
  "names": []
12
12
  }
@@ -17,7 +17,9 @@ export interface QueueStore {
17
17
  /**
18
18
  * Adds an item, due at once, with every recipient `pending`. With
19
19
  * `maxItems`, refuses with `QUEUE_FULL` when the store holds that many
20
- * already, in the same step as the add.
20
+ * already, in the same step as the add. The item is never claimable
21
+ * before its message is readable: write the message first, or both in
22
+ * one step — a claim that finds no message fails the item's recipients.
21
23
  */
22
24
  add(item: NewQueueItem, options?: AddOptions): Promise<QueueItem>;
23
25
  /** The item, or `undefined` when there is none by that id (done, cancelled, or never added). */
@@ -26,7 +28,11 @@ export interface QueueStore {
26
28
  list(options?: QueueListOptions): Promise<QueueItem[]>;
27
29
  /** How many items the store holds. */
28
30
  count(): Promise<number>;
29
- /** The message of an item, or `undefined` once the item is gone. */
31
+ /**
32
+ * The message of an item, or `undefined` once the item is gone. A
33
+ * store that holds the item but has lost its message gives `undefined`
34
+ * too: the queue then fails the item's pending recipients.
35
+ */
30
36
  readMessage(id: string): Promise<Uint8Array | undefined>;
31
37
  /**
32
38
  * Takes the next item due at `now` — no lease, or a lease expired —
@@ -1 +1 @@
1
- {"version":3,"file":"queue-store.d.ts","sourceRoot":"","sources":["../../src/contract/queue-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,UAAU;IAC1B;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAElE,gGAAgG;IAChG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAEhD,0DAA0D;IAC1D,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAEvD,sCAAsC;IACtC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzB,oEAAoE;IACpE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAEzD;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAE7D;;;OAGG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtE;;;;;OAKG;IACH,QAAQ,CACP,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAElC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAErE,0FAA0F;IAC1F,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;CACnD"}
1
+ {"version":3,"file":"queue-store.d.ts","sourceRoot":"","sources":["../../src/contract/queue-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,UAAU;IAC1B;;;;;;OAMG;IACH,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAElE,gGAAgG;IAChG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAEhD,0DAA0D;IAC1D,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAEvD,sCAAsC;IACtC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAEzD;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAE7D;;;OAGG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtE;;;;;OAKG;IACH,QAAQ,CACP,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAElC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAErE,0FAA0F;IAC1F,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;CACnD"}
@@ -16,8 +16,11 @@ export interface DsnInput {
16
16
  readonly recipients: readonly DsnRecipient[];
17
17
  /** With `delayed`: when the queue gives up. */
18
18
  readonly willRetryUntil?: Date;
19
- /** The original message, as enqueued. */
20
- readonly original: Uint8Array;
19
+ /**
20
+ * The original message, as enqueued; absent when it could not be read,
21
+ * and the DSN then has no third part (RFC 6522 §3 makes it optional).
22
+ */
23
+ readonly original?: Uint8Array;
21
24
  readonly returnContent: 'headers' | 'full';
22
25
  /** The most bytes of the original returned. */
23
26
  readonly maxReturn: number;
@@ -27,7 +30,8 @@ export interface DsnInput {
27
30
  * (RFC 6522) of `report-type=delivery-status` holding the text a person
28
31
  * reads, the `message/delivery-status` fields a program reads, and the
29
32
  * original's header fields (or the whole original, when asked and small
30
- * enough), bounded by `maxReturn`. Nothing from the remote server or the
33
+ * enough), bounded by `maxReturn` — or, with no `original`, no third
34
+ * part at all. Nothing from the remote server or the
31
35
  * envelope reaches a header field raw: control characters, CR and LF are
32
36
  * taken out first.
33
37
  */
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/dsn/build.ts"],"names":[],"mappings":"AAQA,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,YAAY,EAGjB,MAAM,UAAU,CAAC;AAElB,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAEtD,qDAAqD;AACrD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,UAAU,EAAE,SAAS,YAAY,EAAE,CAAC;IAC7C,+CAA+C;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC;IAC/B,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,SAAS,GAAG,MAAM,CAAC;IAC3C,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAkFD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,UAAU,CAwCpD"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/dsn/build.ts"],"names":[],"mappings":"AAQA,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,YAAY,EAGjB,MAAM,UAAU,CAAC;AAElB,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAEtD,qDAAqD;AACrD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,UAAU,EAAE,SAAS,YAAY,EAAE,CAAC;IAC7C,+CAA+C;IAC/C,QAAQ,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,aAAa,EAAE,SAAS,GAAG,MAAM,CAAC;IAC3C,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAwFD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,UAAU,CA2CpD"}
package/dist/errors.d.ts CHANGED
@@ -3,9 +3,12 @@
3
3
  * argument it cannot take. `MESSAGE_TOO_BIG`, `TOO_MANY_RECIPIENTS` and
4
4
  * `QUEUE_FULL`: a limit `enqueue` enforces. `CLOSED`: a store used after
5
5
  * `close()`. `LEASE_LOST`: given to the `error` event when another worker
6
- * took an item before its outcome was recorded.
6
+ * took an item before its outcome was recorded — or, once the lease's
7
+ * expiry passed, when the item is gone: lost or cancelled.
8
+ * `MESSAGE_UNREADABLE`: given to the `error` event when the store holds an
9
+ * item but not its message, whose pending recipients then fail.
7
10
  */
8
- export type QueueErrorCode = 'INVALID' | 'MESSAGE_TOO_BIG' | 'TOO_MANY_RECIPIENTS' | 'QUEUE_FULL' | 'CLOSED' | 'LEASE_LOST';
11
+ export type QueueErrorCode = 'INVALID' | 'MESSAGE_TOO_BIG' | 'TOO_MANY_RECIPIENTS' | 'QUEUE_FULL' | 'CLOSED' | 'LEASE_LOST' | 'MESSAGE_UNREADABLE';
9
12
  /** Thrown by the queue and by every store, whichever answers the contract, with the same codes. */
10
13
  export declare class QueueError extends Error {
11
14
  readonly name = "QueueError";
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GACvB,SAAS,GACT,iBAAiB,GACjB,qBAAqB,GACrB,YAAY,GACZ,QAAQ,GACR,YAAY,CAAC;AAEhB,mGAAmG;AACnG,qBAAa,UAAW,SAAQ,KAAK;IACpC,SAAkB,IAAI,gBAAgB;IACtC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAElB,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM;CAIjD;AAED,+CAA+C;AAC/C,eAAO,MAAM,OAAO,GAAI,SAAS,MAAM,eAAuC,CAAC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,GACvB,SAAS,GACT,iBAAiB,GACjB,qBAAqB,GACrB,YAAY,GACZ,QAAQ,GACR,YAAY,GACZ,oBAAoB,CAAC;AAExB,mGAAmG;AACnG,qBAAa,UAAW,SAAQ,KAAK;IACpC,SAAkB,IAAI,gBAAgB;IACtC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAElB,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM;CAIjD;AAED,+CAA+C;AAC/C,eAAO,MAAM,OAAO,GAAI,SAAS,MAAM,eAAuC,CAAC"}
package/dist/index.js CHANGED
@@ -429,6 +429,9 @@ function humanText(input) {
429
429
  ...input.willRetryUntil ? [`until ${formatDate2(input.willRetryUntil)}.`] : [],
430
430
  "You do not need to send it again."
431
431
  ];
432
+ if (!input.original) {
433
+ lines.push("", "Your message could not be read from the queue, so it is not returned.");
434
+ }
432
435
  lines.push("");
433
436
  for (const r of input.recipients) {
434
437
  const said = r.reply ? `${r.reply.code ?? ""} ${cleanText(r.reply.text, 900)}`.trim() : statusOf(input.kind);
@@ -471,8 +474,8 @@ function boundaryFor(content) {
471
474
  }
472
475
  }
473
476
  function buildDsn(input) {
474
- const original = returned(input.original, input.returnContent, input.maxReturn);
475
- const boundary = boundaryFor(original.body);
477
+ const original = input.original ? returned(input.original, input.returnContent, input.maxReturn) : undefined;
478
+ const boundary = boundaryFor(original?.body ?? new Uint8Array);
476
479
  const text = encodeQuotedPrintable(humanText(input));
477
480
  const status = deliveryStatus({
478
481
  kind: input.kind,
@@ -481,7 +484,7 @@ function buildDsn(input) {
481
484
  recipients: input.recipients,
482
485
  ...input.willRetryUntil ? { willRetryUntil: input.willRetryUntil } : {}
483
486
  });
484
- const head = [
487
+ const parts = [
485
488
  ...headerFields(input, boundary),
486
489
  "",
487
490
  "This is a MIME-formatted delivery status notification.",
@@ -494,7 +497,16 @@ function buildDsn(input) {
494
497
  `--${boundary}`,
495
498
  "Content-Type: message/delivery-status",
496
499
  "",
497
- status,
500
+ status
501
+ ];
502
+ const end = `--${boundary}--\r
503
+ `;
504
+ if (!original)
505
+ return encoder.encode(`${parts.join(`\r
506
+ `)}\r
507
+ ${end}`);
508
+ const head = [
509
+ ...parts,
498
510
  `--${boundary}`,
499
511
  `Content-Type: ${original.type}`,
500
512
  ...original.eightBit ? ["Content-Transfer-Encoding: 8bit"] : [],
@@ -506,8 +518,7 @@ function buildDsn(input) {
506
518
  encoder.encode(head),
507
519
  original.body,
508
520
  encoder.encode(`\r
509
- --${boundary}--\r
510
- `)
521
+ ${end}`)
511
522
  ]);
512
523
  }
513
524
 
@@ -545,7 +556,7 @@ async function notify(ctx, item, message, kind, recipients, now) {
545
556
  lastAttempt
546
557
  })),
547
558
  ...kind === "delayed" ? { willRetryUntil: new Date(giveUpAt(item.createdAt, settings.retry)) } : {},
548
- original: message,
559
+ ...message ? { original: message } : {},
549
560
  returnContent: settings.dsn.returnContent,
550
561
  maxReturn: settings.limits.maxDsnReturn
551
562
  });
@@ -714,6 +725,7 @@ function settle(item, updates, skipped, now, settings) {
714
725
  }
715
726
 
716
727
  // src/queue/deliver.ts
728
+ var goneAfter = (lapse, id) => new QueueError2("LEASE_LOST", lapse === "taken" ? `The lease on ${id} 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` : `The lease on ${id} 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`);
717
729
  function groupsOf(item) {
718
730
  const groups = new Map;
719
731
  for (const r of item.recipients) {
@@ -744,6 +756,15 @@ var badSender = (address) => ({
744
756
  text: "The sender's address is not one SMTP can carry"
745
757
  }
746
758
  });
759
+ var unreadable = (address) => ({
760
+ address,
761
+ status: "failed",
762
+ reply: {
763
+ status: "5.3.0",
764
+ text: "Message unreadable: the queue store holds the item but not its message"
765
+ }
766
+ });
767
+ var unreadableError = (id) => new QueueError2("MESSAGE_UNREADABLE", `The message of ${id} is unreadable: the store holds the item but not its message, so every pending recipient failed`);
747
768
  async function attemptGroup(ctx, item, message, domain, recipients) {
748
769
  const { settings } = ctx;
749
770
  const max = settings.limits.maxReplyText;
@@ -783,10 +804,15 @@ function emitOutcomes(events, item, settled) {
783
804
  events.emit("failed", base(u));
784
805
  }
785
806
  async function deliverItem(ctx, item) {
786
- const { settings, store, events } = ctx;
807
+ const { store } = ctx;
787
808
  const message = await store.readMessage(item.id);
788
- if (!message)
809
+ if (!message) {
810
+ if (await store.get(item.id) === undefined)
811
+ return;
812
+ const failed = [...groupsOf(item).values()].flat().map(unreadable);
813
+ await record(ctx, item, failed, false, undefined);
789
814
  return;
815
+ }
790
816
  const groups = groupsOf(item);
791
817
  let skipped = false;
792
818
  const outcomes = item.from !== "" && !isMailbox2(item.from) ? [[...groups.values()].flat().map(badSender)] : await Promise.all([...groups].map(async ([domain, group]) => {
@@ -801,11 +827,22 @@ async function deliverItem(ctx, item) {
801
827
  release();
802
828
  }
803
829
  }));
830
+ await record(ctx, item, outcomes.flat(), skipped, message);
831
+ }
832
+ async function record(ctx, item, updates, skipped, message) {
833
+ const { settings, store, events } = ctx;
804
834
  const now = settings.now();
805
- const settled = settle(item, outcomes.flat(), skipped, now, settings);
835
+ const settled = settle(item, updates, skipped, now, settings);
806
836
  const after = await store.complete(item.id, settings.owner, settled.result);
807
837
  ctx.recorded();
808
838
  if (!after && await store.get(item.id) === undefined) {
839
+ const lapse = await ctx.leaseLapsed(now);
840
+ if (lapse) {
841
+ events.emit("error", { error: goneAfter(lapse, item.id), id: item.id });
842
+ return;
843
+ }
844
+ if (!message)
845
+ return;
809
846
  emitOutcomes(events, item, settled);
810
847
  return;
811
848
  }
@@ -814,6 +851,9 @@ async function deliverItem(ctx, item) {
814
851
  events.emit("error", { error, id: item.id });
815
852
  return;
816
853
  }
854
+ if (!message) {
855
+ events.emit("error", { error: unreadableError(item.id), id: item.id });
856
+ }
817
857
  emitOutcomes(events, item, settled);
818
858
  await notify(ctx, item, message, "failed", settled.failed, now);
819
859
  if (settled.delayDsn) {
@@ -821,6 +861,57 @@ async function deliverItem(ctx, item) {
821
861
  }
822
862
  }
823
863
 
864
+ // src/queue/lease.ts
865
+ function leaseOf(ctx, item) {
866
+ const { settings } = ctx;
867
+ return {
868
+ item,
869
+ done: false,
870
+ expiresAt: item.lease?.expiresAt ?? settings.now() + settings.leaseMs,
871
+ taken: false,
872
+ looking: Promise.resolve()
873
+ };
874
+ }
875
+ async function lookAfterRefusal(ctx, lease) {
876
+ const { id } = lease.item;
877
+ if (await ctx.store.get(id) === undefined)
878
+ return;
879
+ lease.taken = true;
880
+ const error = new QueueError2("LEASE_LOST", `The lease on ${id} was lost while it was delivered`);
881
+ ctx.events.emit("error", { error, id });
882
+ }
883
+ async function renew(ctx, lease) {
884
+ const { settings, store, events } = ctx;
885
+ const { id } = lease.item;
886
+ const next = settings.now() + settings.leaseMs;
887
+ let held;
888
+ try {
889
+ held = await store.renew(id, settings.owner, next);
890
+ } catch (error) {
891
+ if (!lease.done)
892
+ events.emit("error", { error, id });
893
+ return !lease.done;
894
+ }
895
+ if (held)
896
+ lease.expiresAt = Math.max(lease.expiresAt, next);
897
+ if (lease.done)
898
+ return false;
899
+ if (held)
900
+ return true;
901
+ lease.done = true;
902
+ lease.looking = lookAfterRefusal(ctx, lease).catch((error) => {
903
+ events.emit("error", { error, id });
904
+ });
905
+ return false;
906
+ }
907
+ async function lapsed(lease, now) {
908
+ await lease.looking;
909
+ if (lease.taken)
910
+ return "taken";
911
+ return lease.expiresAt <= now ? "expired" : undefined;
912
+ }
913
+ var settled = (lease) => lease.looking;
914
+
824
915
  // src/queue/limiter.ts
825
916
  class Limiter {
826
917
  #size;
@@ -888,42 +979,35 @@ class Worker {
888
979
  this.#slots = new Limiter(ctx.settings.concurrency);
889
980
  this.#domains = new KeyedLimiter(ctx.settings.perDomain);
890
981
  }
891
- #renewing(item) {
892
- const { settings, store, events } = this.#ctx;
893
- let done = false;
894
- const stop = () => {
895
- done = true;
896
- clearInterval(timer);
897
- };
982
+ #renewing(lease) {
898
983
  const timer = setInterval(() => {
899
- const expiresAt = settings.now() + settings.leaseMs;
900
- store.renew(item.id, settings.owner, expiresAt).then((held) => {
901
- if (held || done)
902
- return;
903
- stop();
904
- const error = new QueueError2("LEASE_LOST", `The lease on ${item.id} was lost while it was delivered`);
905
- events.emit("error", { error, id: item.id });
906
- }, (error) => {
907
- if (!done)
908
- events.emit("error", { error, id: item.id });
984
+ renew(this.#ctx, lease).then((more) => {
985
+ if (!more)
986
+ clearInterval(timer);
909
987
  });
910
- }, settings.leaseMs / 3);
988
+ }, this.#ctx.settings.leaseMs / 3);
911
989
  timer.unref?.();
912
- return stop;
990
+ return () => {
991
+ lease.done = true;
992
+ clearInterval(timer);
993
+ };
913
994
  }
914
995
  async#deliver(item) {
915
- const stopRenewing = this.#renewing(item);
996
+ const lease = leaseOf(this.#ctx, item);
997
+ const stop = this.#renewing(lease);
916
998
  try {
917
999
  await deliverItem({
918
1000
  ...this.#ctx,
919
1001
  domains: this.#domains,
920
1002
  stopping: () => this.#stopping,
921
- recorded: stopRenewing
1003
+ recorded: stop,
1004
+ leaseLapsed: (now) => lapsed(lease, now)
922
1005
  }, item);
923
1006
  } catch (error) {
924
1007
  this.#ctx.events.emit("error", { error, id: item.id });
925
1008
  } finally {
926
- stopRenewing();
1009
+ stop();
1010
+ await settled(lease);
927
1011
  }
928
1012
  }
929
1013
  #claim() {
@@ -1066,5 +1150,5 @@ export {
1066
1150
  createQueue
1067
1151
  };
1068
1152
 
1069
- //# debugId=D3F637CD4BEA969664756E2164756E21
1153
+ //# debugId=F644E902CC0F818C64756E2164756E21
1070
1154
  //# sourceMappingURL=index.js.map