@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.
- package/README.md +44 -8
- package/dist/chunks/index-b4xvg3xn.js +23 -0
- package/dist/chunks/index-b4xvg3xn.js.map +10 -0
- package/dist/chunks/index-vg971acr.js.map +2 -2
- package/dist/contract/queue-store.d.ts +8 -2
- package/dist/contract/queue-store.d.ts.map +1 -1
- package/dist/dsn/build.d.ts +7 -3
- package/dist/dsn/build.d.ts.map +1 -1
- package/dist/errors.d.ts +5 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +117 -33
- package/dist/index.js.map +8 -7
- package/dist/masked.d.ts +8 -0
- package/dist/masked.d.ts.map +1 -0
- package/dist/postgres/connect.d.ts +6 -0
- package/dist/postgres/connect.d.ts.map +1 -1
- package/dist/postgres/index.js +49 -28
- package/dist/postgres/index.js.map +7 -6
- package/dist/postgres/isolation.d.ts +6 -0
- package/dist/postgres/isolation.d.ts.map +1 -0
- package/dist/postgres/schema.d.ts.map +1 -1
- package/dist/postgres/store.d.ts.map +1 -1
- package/dist/queue/deliver.d.ts +10 -1
- package/dist/queue/deliver.d.ts.map +1 -1
- package/dist/queue/lease.d.ts +41 -0
- package/dist/queue/lease.d.ts.map +1 -0
- package/dist/queue/notify.d.ts +3 -2
- package/dist/queue/notify.d.ts.map +1 -1
- package/dist/queue/worker.d.ts.map +1 -1
- package/dist/redis/connect.d.ts +27 -0
- package/dist/redis/connect.d.ts.map +1 -0
- package/dist/redis/index.d.ts +3 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +438 -0
- package/dist/redis/index.js.map +14 -0
- package/dist/redis/items.d.ts +13 -0
- package/dist/redis/items.d.ts.map +1 -0
- package/dist/redis/options.d.ts +47 -0
- package/dist/redis/options.d.ts.map +1 -0
- package/dist/redis/run.d.ts +12 -0
- package/dist/redis/run.d.ts.map +1 -0
- package/dist/redis/scripts.d.ts +60 -0
- package/dist/redis/scripts.d.ts.map +1 -0
- package/dist/redis/store.d.ts +30 -0
- package/dist/redis/store.d.ts.map +1 -0
- package/docs/README.md +1 -1
- package/docs/guide.md +271 -16
- package/docs/roadmap.md +49 -7
- package/docs/troubleshooting.md +303 -16
- 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
|
|
8
|
-
on one machine, or on many with PostgreSQL — can
|
|
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`.
|
|
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.
|
|
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": ";
|
|
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
|
-
/**
|
|
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
|
|
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"}
|
package/dist/dsn/build.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
20
|
-
|
|
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
|
|
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
|
*/
|
package/dist/dsn/build.d.ts.map
CHANGED
|
@@ -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
|
|
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";
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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(
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
|
990
|
+
return () => {
|
|
991
|
+
lease.done = true;
|
|
992
|
+
clearInterval(timer);
|
|
993
|
+
};
|
|
913
994
|
}
|
|
914
995
|
async#deliver(item) {
|
|
915
|
-
const
|
|
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:
|
|
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
|
-
|
|
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=
|
|
1153
|
+
//# debugId=F644E902CC0F818C64756E2164756E21
|
|
1070
1154
|
//# sourceMappingURL=index.js.map
|