@bumail/queue 0.1.0 → 0.3.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 (66) hide show
  1. package/README.md +71 -5
  2. package/dist/chunks/{index-xhsw4xcz.js → index-vg971acr.js} +70 -6
  3. package/dist/chunks/index-vg971acr.js.map +12 -0
  4. package/dist/chunks/index-y3n1w2az.js +18 -0
  5. package/dist/chunks/index-y3n1w2az.js.map +10 -0
  6. package/dist/contract/checks.d.ts +6 -0
  7. package/dist/contract/checks.d.ts.map +1 -1
  8. package/dist/errors.d.ts +2 -1
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/index.js +86 -55
  11. package/dist/index.js.map +8 -8
  12. package/dist/masked.d.ts +7 -0
  13. package/dist/masked.d.ts.map +1 -0
  14. package/dist/memory/index.js +3 -5
  15. package/dist/memory/index.js.map +2 -2
  16. package/dist/postgres/connect.d.ts +17 -0
  17. package/dist/postgres/connect.d.ts.map +1 -0
  18. package/dist/postgres/index.d.ts +2 -0
  19. package/dist/postgres/index.d.ts.map +1 -0
  20. package/dist/postgres/index.js +374 -0
  21. package/dist/postgres/index.js.map +14 -0
  22. package/dist/postgres/options.d.ts +37 -0
  23. package/dist/postgres/options.d.ts.map +1 -0
  24. package/dist/postgres/rows.d.ts +21 -0
  25. package/dist/postgres/rows.d.ts.map +1 -0
  26. package/dist/postgres/schema.d.ts +20 -0
  27. package/dist/postgres/schema.d.ts.map +1 -0
  28. package/dist/postgres/statements.d.ts +54 -0
  29. package/dist/postgres/statements.d.ts.map +1 -0
  30. package/dist/postgres/store.d.ts +38 -0
  31. package/dist/postgres/store.d.ts.map +1 -0
  32. package/dist/queue/deliver.d.ts +7 -0
  33. package/dist/queue/deliver.d.ts.map +1 -1
  34. package/dist/queue/lease.d.ts +41 -0
  35. package/dist/queue/lease.d.ts.map +1 -0
  36. package/dist/queue/outcome.d.ts.map +1 -1
  37. package/dist/queue/settings/index.d.ts.map +1 -1
  38. package/dist/queue/worker.d.ts.map +1 -1
  39. package/dist/redis/connect.d.ts +25 -0
  40. package/dist/redis/connect.d.ts.map +1 -0
  41. package/dist/redis/index.d.ts +3 -0
  42. package/dist/redis/index.d.ts.map +1 -0
  43. package/dist/redis/index.js +430 -0
  44. package/dist/redis/index.js.map +14 -0
  45. package/dist/redis/items.d.ts +13 -0
  46. package/dist/redis/items.d.ts.map +1 -0
  47. package/dist/redis/options.d.ts +47 -0
  48. package/dist/redis/options.d.ts.map +1 -0
  49. package/dist/redis/run.d.ts +12 -0
  50. package/dist/redis/run.d.ts.map +1 -0
  51. package/dist/redis/scripts.d.ts +60 -0
  52. package/dist/redis/scripts.d.ts.map +1 -0
  53. package/dist/redis/store.d.ts +30 -0
  54. package/dist/redis/store.d.ts.map +1 -0
  55. package/dist/sqlite/index.js +3 -5
  56. package/dist/sqlite/index.js.map +2 -2
  57. package/dist/text.d.ts +9 -3
  58. package/dist/text.d.ts.map +1 -1
  59. package/docs/README.md +1 -1
  60. package/docs/guide.md +314 -7
  61. package/docs/roadmap.md +73 -19
  62. package/docs/troubleshooting.md +317 -7
  63. package/package.json +15 -3
  64. package/dist/chunks/index-p4k4rjb7.js +0 -15
  65. package/dist/chunks/index-p4k4rjb7.js.map +0 -10
  66. package/dist/chunks/index-xhsw4xcz.js.map +0 -10
package/README.md CHANGED
@@ -4,8 +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`, and several workers can share one
8
- queue. No 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.
9
10
 
10
11
  ## Install
11
12
 
@@ -25,6 +26,8 @@ queue that only uses a smarthost needs none.
25
26
  | `@bumail/queue` | `createQueue`, the `QueueStore` contract, `QueueError` and the types |
26
27
  | `@bumail/queue/memory` | `MemoryQueueStore`: in memory, lost on a restart — for specs and trials |
27
28
  | `@bumail/queue/sqlite` | `SqliteQueueStore`: on disk with `bun:sqlite`, shared by the processes of one machine |
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 |
28
31
 
29
32
  ## Usage
30
33
 
@@ -144,6 +147,67 @@ makes is 0700; one that exists keeps its mode.
144
147
  item opens one session per recipient domain — and `perDomain` (2) its
145
148
  sessions to one recipient domain.
146
149
 
150
+ ## PostgreSQL, for several machines
151
+
152
+ ```ts
153
+ import { nodeResolver } from '@bumail/dns';
154
+ import { createQueue } from '@bumail/queue';
155
+ import { PostgresQueueStore } from '@bumail/queue/postgres';
156
+
157
+ // In each instance, on the same database. A Bun.SQL client of your own works too:
158
+ // PostgresQueueStore.open({ sql: new Bun.SQL({ url, max: 10 }) }).
159
+ const store = PostgresQueueStore.open({
160
+ sql: Bun.env['DATABASE_URL'] ?? 'postgres://bumail@localhost:5432/mail',
161
+ tablePrefix: 'bumail_queue_', // the default: bumail_queue_items, …_messages, …_schema
162
+ });
163
+ await store.migrate(); // makes the tables; the first call would anyway
164
+ const queue = createQueue({ store, hostname: 'mail.example.net', resolver: nodeResolver() });
165
+ queue.start();
166
+
167
+ process.on('SIGTERM', async () => {
168
+ await queue.stop();
169
+ await store.close(); // closes the client it opened for the URL, never yours
170
+ });
171
+ ```
172
+
173
+ A claim is one `UPDATE … RETURNING` whose item a `SELECT … FOR UPDATE
174
+ SKIP LOCKED` picks: two instances never take the same item, and none
175
+ waits on one another is taking. An instance that crashes loses its items
176
+ when their leases expire, as with `bun:sqlite`. No driver to install:
177
+ `Bun.sql` is Bun's.
178
+
179
+ ## Redis, for several machines
180
+
181
+ ```ts
182
+ import { nodeResolver } from '@bumail/dns';
183
+ import { createQueue } from '@bumail/queue';
184
+ import { RedisQueueStore } from '@bumail/queue/redis';
185
+
186
+ // In each instance, on the same Redis. A Bun.RedisClient of your own works too:
187
+ // RedisQueueStore.open({ client: new Bun.RedisClient(url) }).
188
+ const store = RedisQueueStore.open({
189
+ url: Bun.env['REDIS_URL'] ?? 'redis://localhost:6379',
190
+ keyPrefix: 'bumail:queue:', // the default: bumail:queue:items, …:item:<id>, …
191
+ });
192
+ const queue = createQueue({ store, hostname: 'mail.example.net', resolver: nodeResolver() });
193
+ queue.start();
194
+
195
+ process.on('SIGTERM', async () => {
196
+ await queue.stop();
197
+ await store.close(); // closes the client it opened for the URL, never yours
198
+ });
199
+ ```
200
+
201
+ Every operation that writes is one Lua script, which Redis runs whole:
202
+ two instances never take the same item, and a crashed instance's items
203
+ are claimed again once their leases expire. The message is kept byte for
204
+ byte. One Redis, or a primary with replicas — not Redis Cluster. Redis
205
+ acknowledges a write once it is in memory: with `appendfsync everysec`
206
+ a crash loses up to a second of writes, and a failover to a replica can
207
+ lose acknowledged ones, so keep `appendonly yes`, `appendfsync always`
208
+ and `maxmemory-policy noeviction` where a lost message matters (the
209
+ guide's Redis section). No driver to install: `Bun.redis` is Bun's.
210
+
147
211
  ## Events and admin
148
212
 
149
213
  ```ts
@@ -212,8 +276,8 @@ control character in it.
212
276
 
213
277
  ## Traps
214
278
 
215
- - **Bun only.** The store on disk uses `bun:sqlite`, so the package runs
216
- on Bun 1.4.2 or later, not on Node.
279
+ - **Bun only.** The stores use `bun:sqlite`, `Bun.sql` and `Bun.redis`,
280
+ so the package runs on Bun 1.4.2 or later, not on Node.
217
281
  - **It sends what you enqueue, to anyone.** The queue is not a relay
218
282
  policy: enqueue only what an authenticated user submitted, or what your
219
283
  own server writes, never what an unauthenticated client handed you.
@@ -232,12 +296,14 @@ control character in it.
232
296
  | `QueueError`, `QueueErrorCode` | `INVALID`, `MESSAGE_TOO_BIG`, `TOO_MANY_RECIPIENTS`, `QUEUE_FULL`, `CLOSED`, `LEASE_LOST` |
233
297
  | `MemoryQueueStore` | from `@bumail/queue/memory` |
234
298
  | `SqliteQueueStore`, `SqliteQueueStoreOptions` | from `@bumail/queue/sqlite`: `SqliteQueueStore.open({ directory, busyTimeout? })`, `close()` |
299
+ | `PostgresQueueStore`, `PostgresQueueStoreOptions`, `PostgresClient`, `PostgresQueryable` | from `@bumail/queue/postgres`: `PostgresQueueStore.open({ sql, tablePrefix? })`, `migrate()`, `close()`; `sql` a `Bun.SQL` client or a `postgres://` URL |
300
+ | `RedisQueueStore`, `RedisQueueStoreOptions`, `RedisQueueClientOptions`, `RedisQueueUrlOptions`, `RedisQueueClient` | from `@bumail/queue/redis`: `RedisQueueStore.open({ client \| url, keyPrefix? })`, `close()`; `client` a `Bun.RedisClient`, `url` a `redis://` URL |
235
301
 
236
302
  ## Documentation
237
303
 
238
304
  These pages ship in the package, under `docs/`.
239
305
 
240
306
  - [Index](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/README.md): the pages, and when to read each.
241
- - [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, testing, and writing a store of your own.
307
+ - [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.
242
308
  - [Troubleshooting](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/troubleshooting.md): every `QueueError`, and what to do about it.
243
309
  - [Roadmap](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/roadmap.md): what is coming, and what is not planned.
@@ -1,6 +1,41 @@
1
- import {
2
- invalid
3
- } from "./index-p4k4rjb7.js";
1
+ // src/errors.ts
2
+ class QueueError2 extends Error {
3
+ name = "QueueError";
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.code = code;
8
+ }
9
+ }
10
+ var invalid = (message) => new QueueError2("INVALID", message);
11
+
12
+ // src/text.ts
13
+ var isControl = (code) => code < 32 || code >= 127 && code <= 159;
14
+ function hasControl(text) {
15
+ for (let i = 0;i < text.length; i++) {
16
+ if (isControl(text.charCodeAt(i)))
17
+ return true;
18
+ }
19
+ return false;
20
+ }
21
+ function replaceControls(text, by) {
22
+ let out = "";
23
+ for (let i = 0;i < text.length; i++) {
24
+ out += isControl(text.charCodeAt(i)) ? by : text[i];
25
+ }
26
+ return out;
27
+ }
28
+ function cleanText(text, max) {
29
+ const clean = replaceControls(text.toWellFormed(), " ").replace(/ {2,}/g, " ").trim();
30
+ return clean.length > max ? `${cut(clean, max - 3)}...` : clean;
31
+ }
32
+ function cut(text, length) {
33
+ const head = text.slice(0, length);
34
+ const last = head.charCodeAt(head.length - 1);
35
+ return last >= 55296 && last <= 56319 ? head.slice(0, -1) : head;
36
+ }
37
+ var isStorable = (text) => text.isWellFormed() && !text.includes("\x00");
38
+ var asciiText = (text, max) => cleanText(text, max).replace(/[^ -~]/g, "?");
4
39
 
5
40
  // src/contract/checks.ts
6
41
  var isTime = (value) => typeof value === "number" && Number.isFinite(value);
@@ -9,22 +44,49 @@ function checkTime(name, value) {
9
44
  throw invalid(`${name} must be a finite number`);
10
45
  return value;
11
46
  }
47
+ function checkStorable(name, text) {
48
+ if (!isStorable(text)) {
49
+ throw invalid(`${name} holds a NUL or a lone surrogate, which a store cannot keep`);
50
+ }
51
+ }
12
52
  function checkOwner(owner) {
13
53
  if (typeof owner !== "string" || owner === "") {
14
54
  throw invalid("owner must be a non-empty string");
15
55
  }
56
+ checkStorable("owner", owner);
16
57
  return owner;
17
58
  }
59
+ function checkUpdate(update) {
60
+ checkStorable("A recipient", update.address);
61
+ const reply = update.reply;
62
+ if (reply === undefined)
63
+ return;
64
+ if (typeof reply !== "object" || reply === null || typeof reply.text !== "string") {
65
+ throw invalid("A reply is an object with a text");
66
+ }
67
+ checkStorable("reply.text", reply.text);
68
+ for (const name of ["status", "host"]) {
69
+ const value = reply[name];
70
+ if (value === undefined)
71
+ continue;
72
+ if (typeof value !== "string")
73
+ throw invalid(`reply.${name} must be a string`);
74
+ checkStorable(`reply.${name}`, value);
75
+ }
76
+ }
18
77
  function checkNewItem(item) {
19
78
  if (typeof item !== "object" || item === null) {
20
79
  throw invalid("A new item is an object");
21
80
  }
22
81
  if (typeof item.from !== "string")
23
82
  throw invalid("from must be a string");
83
+ checkStorable("from", item.from);
24
84
  const to = item.to;
25
85
  if (!Array.isArray(to) || to.length === 0 || !to.every((a) => typeof a === "string" && a !== "")) {
26
86
  throw invalid("to must be a non-empty array of addresses");
27
87
  }
88
+ for (const address of to)
89
+ checkStorable("A recipient", address);
28
90
  if (!(item.message instanceof Uint8Array)) {
29
91
  throw invalid("message must be a Uint8Array");
30
92
  }
@@ -64,6 +126,8 @@ function checkResult(result) {
64
126
  if (!Array.isArray(result.recipients) || !result.recipients.every((u) => typeof u?.address === "string" && statuses.includes(u.status))) {
65
127
  throw invalid("recipients must be an array of { address, status: delivered, deferred or failed }");
66
128
  }
129
+ for (const update of result.recipients)
130
+ checkUpdate(update);
67
131
  }
68
132
  function checkList(options = {}) {
69
133
  const offset = options.offset ?? 0;
@@ -113,7 +177,7 @@ function newItem(id, item) {
113
177
  }
114
178
  var claimable = (item, now) => item.nextAttemptAt <= now && (item.lease === undefined || item.lease.expiresAt <= now);
115
179
 
116
- export { checkTime, checkOwner, checkNewItem, checkMaxItems, checkClaim, checkResult, checkList, isDone, applyAttempt, newItem, claimable };
180
+ export { QueueError2, invalid, hasControl, replaceControls, cleanText, isStorable, asciiText, checkTime, checkOwner, checkNewItem, checkMaxItems, checkClaim, checkResult, checkList, isDone, applyAttempt, newItem, claimable };
117
181
 
118
- //# debugId=70E823C3942B47A164756E2164756E21
119
- //# sourceMappingURL=index-xhsw4xcz.js.map
182
+ //# debugId=81614FFC0DB6330C64756E2164756E21
183
+ //# sourceMappingURL=index-vg971acr.js.map
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors.ts", "../src/text.ts", "../src/contract/checks.ts"],
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 — or, once the lease's\n * expiry passed, when the item is gone: lost or cancelled.\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",
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
+ "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
+ ],
9
+ "mappings": "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",
10
+ "debugId": "81614FFC0DB6330C64756E2164756E21",
11
+ "names": []
12
+ }
@@ -0,0 +1,18 @@
1
+ // src/masked.ts
2
+ function masked(text, raw) {
3
+ let decoded = raw;
4
+ try {
5
+ decoded = decodeURIComponent(raw);
6
+ } catch {}
7
+ let out = text;
8
+ for (const password of [raw, decoded]) {
9
+ if (password !== "")
10
+ out = out.replaceAll(password, "…");
11
+ }
12
+ return out;
13
+ }
14
+
15
+ export { masked };
16
+
17
+ //# debugId=64BFD310901EABF064756E2164756E21
18
+ //# sourceMappingURL=index-y3n1w2az.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/masked.ts"],
4
+ "sourcesContent": [
5
+ "/**\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.\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\tlet out = text;\n\tfor (const password of [raw, decoded]) {\n\t\tif (password !== '') out = out.replaceAll(password, '…');\n\t}\n\treturn out;\n}\n"
6
+ ],
7
+ "mappings": ";AAKO,SAAS,MAAM,CAAC,MAAc,KAAqB;AAAA,EACzD,IAAI,UAAU;AAAA,EACd,IAAI;AAAA,IACH,UAAU,mBAAmB,GAAG;AAAA,IAC/B,MAAM;AAAA,EAGR,IAAI,MAAM;AAAA,EACV,WAAW,YAAY,CAAC,KAAK,OAAO,GAAG;AAAA,IACtC,IAAI,aAAa;AAAA,MAAI,MAAM,IAAI,WAAW,UAAU,GAAG;AAAA,EACxD;AAAA,EACA,OAAO;AAAA;",
8
+ "debugId": "64BFD310901EABF064756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,6 +1,12 @@
1
1
  import type { AttemptResult, ClaimRequest, NewQueueItem, QueueItem, QueueListOptions } from './types';
2
2
  /** A time in milliseconds since the epoch, or `INVALID` naming it. */
3
3
  export declare function checkTime(name: string, value: unknown): number;
4
+ /**
5
+ * Text a store keeps: refused, the same way by every store, when it holds
6
+ * a NUL or a lone surrogate, which PostgreSQL cannot keep and no UTF-8
7
+ * encode can carry.
8
+ */
9
+ export declare function checkStorable(name: string, text: string): void;
4
10
  /** A lease owner: a non-empty string. */
5
11
  export declare function checkOwner(owner: unknown): string;
6
12
  export declare function checkNewItem(item: NewQueueItem): void;
@@ -1 +1 @@
1
- {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/contract/checks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAEhB,MAAM,SAAS,CAAC;AAOjB,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAG9D;AAED,yCAAyC;AACzC,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKjD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAiBrD;AAED,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAM9D;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAStD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAuBvD;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd,CAUA;AAED,kDAAkD;AAClD,eAAO,MAAM,MAAM,GAAI,MAAM,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,KAAG,OAG3D,CAAC;AAEH;;;GAGG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,aAAa,GACnB,SAAS,CAoBX;AAED,4CAA4C;AAC5C,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,SAAS,CAWjE;AAED,uEAAuE;AACvE,eAAO,MAAM,SAAS,GAAI,MAAM,SAAS,EAAE,KAAK,MAAM,KAAG,OAEC,CAAC"}
1
+ {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/contract/checks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,aAAa,EACb,YAAY,EAEZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAGhB,MAAM,SAAS,CAAC;AAOjB,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAG9D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAM9D;AAED,yCAAyC;AACzC,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMjD;AAwBD,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAmBrD;AAED,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAM9D;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAStD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAwBvD;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd,CAUA;AAED,kDAAkD;AAClD,eAAO,MAAM,MAAM,GAAI,MAAM,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,KAAG,OAG3D,CAAC;AAEH;;;GAGG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,aAAa,GACnB,SAAS,CAoBX;AAED,4CAA4C;AAC5C,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,SAAS,CAWjE;AAED,uEAAuE;AACvE,eAAO,MAAM,SAAS,GAAI,MAAM,SAAS,EAAE,KAAK,MAAM,KAAG,OAEC,CAAC"}
package/dist/errors.d.ts CHANGED
@@ -3,7 +3,8 @@
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.
7
8
  */
8
9
  export type QueueErrorCode = 'INVALID' | 'MESSAGE_TOO_BIG' | 'TOO_MANY_RECIPIENTS' | 'QUEUE_FULL' | 'CLOSED' | 'LEASE_LOST';
9
10
  /** Thrown by the queue and by every store, whichever answers the contract, with the same codes. */
@@ -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;;;;;;;GAOG;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"}
package/dist/index.js CHANGED
@@ -1,33 +1,14 @@
1
1
  import {
2
2
  QueueError2,
3
- invalid
4
- } from "./chunks/index-p4k4rjb7.js";
3
+ invalid,
4
+ hasControl,
5
+ replaceControls,
6
+ cleanText,
7
+ asciiText,
8
+ checkOwner
9
+ } from "./chunks/index-vg971acr.js";
5
10
  // src/queue/envelope.ts
6
11
  import { isMailbox } from "@bumail/smtp/client";
7
-
8
- // src/text.ts
9
- var isControl = (code) => code < 32 || code >= 127 && code <= 159;
10
- function hasControl(text) {
11
- for (let i = 0;i < text.length; i++) {
12
- if (isControl(text.charCodeAt(i)))
13
- return true;
14
- }
15
- return false;
16
- }
17
- function replaceControls(text, by) {
18
- let out = "";
19
- for (let i = 0;i < text.length; i++) {
20
- out += isControl(text.charCodeAt(i)) ? by : text[i];
21
- }
22
- return out;
23
- }
24
- function cleanText(text, max) {
25
- const clean = replaceControls(text, " ").replace(/ {2,}/g, " ").trim();
26
- return clean.length > max ? `${clean.slice(0, max - 3)}...` : clean;
27
- }
28
- var asciiText = (text, max) => cleanText(text, max).replace(/[^ -~]/g, "?");
29
-
30
- // src/queue/envelope.ts
31
12
  var ADDRESS = /^[^\s<>]{1,64}@[^\s<>@]{1,253}$/u;
32
13
  function checkAddress(name, address) {
33
14
  if (typeof address !== "string" || address.length > 254 || hasControl(address) || !ADDRESS.test(address) || !isMailbox(address)) {
@@ -280,10 +261,7 @@ function settingsOf(options) {
280
261
  }
281
262
  checkRoutes(options);
282
263
  checkSessions(options);
283
- const owner = options.owner ?? crypto.randomUUID();
284
- if (typeof owner !== "string" || owner === "") {
285
- throw invalid("owner must be a non-empty string");
286
- }
264
+ const owner = checkOwner(options.owner ?? crypto.randomUUID());
287
265
  const clock = options.clock;
288
266
  return {
289
267
  options,
@@ -619,7 +597,7 @@ function diagnosticOfError(error, temporary, max, host) {
619
597
  return {
620
598
  status: `${temporary ? 4 : 5}.${detail}`,
621
599
  text: cleanText(text, max),
622
- ...host ? { host } : {}
600
+ ...host ? { host: cleanText(host, 255) } : {}
623
601
  };
624
602
  }
625
603
  function outcomesOf(result, group, max) {
@@ -642,7 +620,10 @@ function outcomesOf(result, group, max) {
642
620
  return group.map((address) => updates.get(address) ?? {
643
621
  address,
644
622
  status: "deferred",
645
- reply: { text: "The delivery said nothing of this recipient", host }
623
+ reply: {
624
+ text: "The delivery said nothing of this recipient",
625
+ ...host ? { host: cleanText(host, 255) } : {}
626
+ }
646
627
  });
647
628
  }
648
629
  function outcomesOfError(error, group, max, host) {
@@ -733,6 +714,7 @@ function settle(item, updates, skipped, now, settings) {
733
714
  }
734
715
 
735
716
  // src/queue/deliver.ts
717
+ 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`);
736
718
  function groupsOf(item) {
737
719
  const groups = new Map;
738
720
  for (const r of item.recipients) {
@@ -825,6 +807,11 @@ async function deliverItem(ctx, item) {
825
807
  const after = await store.complete(item.id, settings.owner, settled.result);
826
808
  ctx.recorded();
827
809
  if (!after && await store.get(item.id) === undefined) {
810
+ const lapse = await ctx.leaseLapsed(now);
811
+ if (lapse) {
812
+ events.emit("error", { error: goneAfter(lapse, item.id), id: item.id });
813
+ return;
814
+ }
828
815
  emitOutcomes(events, item, settled);
829
816
  return;
830
817
  }
@@ -840,6 +827,57 @@ async function deliverItem(ctx, item) {
840
827
  }
841
828
  }
842
829
 
830
+ // src/queue/lease.ts
831
+ function leaseOf(ctx, item) {
832
+ const { settings } = ctx;
833
+ return {
834
+ item,
835
+ done: false,
836
+ expiresAt: item.lease?.expiresAt ?? settings.now() + settings.leaseMs,
837
+ taken: false,
838
+ looking: Promise.resolve()
839
+ };
840
+ }
841
+ async function lookAfterRefusal(ctx, lease) {
842
+ const { id } = lease.item;
843
+ if (await ctx.store.get(id) === undefined)
844
+ return;
845
+ lease.taken = true;
846
+ const error = new QueueError2("LEASE_LOST", `The lease on ${id} was lost while it was delivered`);
847
+ ctx.events.emit("error", { error, id });
848
+ }
849
+ async function renew(ctx, lease) {
850
+ const { settings, store, events } = ctx;
851
+ const { id } = lease.item;
852
+ const next = settings.now() + settings.leaseMs;
853
+ let held;
854
+ try {
855
+ held = await store.renew(id, settings.owner, next);
856
+ } catch (error) {
857
+ if (!lease.done)
858
+ events.emit("error", { error, id });
859
+ return !lease.done;
860
+ }
861
+ if (held)
862
+ lease.expiresAt = Math.max(lease.expiresAt, next);
863
+ if (lease.done)
864
+ return false;
865
+ if (held)
866
+ return true;
867
+ lease.done = true;
868
+ lease.looking = lookAfterRefusal(ctx, lease).catch((error) => {
869
+ events.emit("error", { error, id });
870
+ });
871
+ return false;
872
+ }
873
+ async function lapsed(lease, now) {
874
+ await lease.looking;
875
+ if (lease.taken)
876
+ return "taken";
877
+ return lease.expiresAt <= now ? "expired" : undefined;
878
+ }
879
+ var settled = (lease) => lease.looking;
880
+
843
881
  // src/queue/limiter.ts
844
882
  class Limiter {
845
883
  #size;
@@ -907,42 +945,35 @@ class Worker {
907
945
  this.#slots = new Limiter(ctx.settings.concurrency);
908
946
  this.#domains = new KeyedLimiter(ctx.settings.perDomain);
909
947
  }
910
- #renewing(item) {
911
- const { settings, store, events } = this.#ctx;
912
- let done = false;
913
- const stop = () => {
914
- done = true;
915
- clearInterval(timer);
916
- };
948
+ #renewing(lease) {
917
949
  const timer = setInterval(() => {
918
- const expiresAt = settings.now() + settings.leaseMs;
919
- store.renew(item.id, settings.owner, expiresAt).then((held) => {
920
- if (held || done)
921
- return;
922
- stop();
923
- const error = new QueueError2("LEASE_LOST", `The lease on ${item.id} was lost while it was delivered`);
924
- events.emit("error", { error, id: item.id });
925
- }, (error) => {
926
- if (!done)
927
- events.emit("error", { error, id: item.id });
950
+ renew(this.#ctx, lease).then((more) => {
951
+ if (!more)
952
+ clearInterval(timer);
928
953
  });
929
- }, settings.leaseMs / 3);
954
+ }, this.#ctx.settings.leaseMs / 3);
930
955
  timer.unref?.();
931
- return stop;
956
+ return () => {
957
+ lease.done = true;
958
+ clearInterval(timer);
959
+ };
932
960
  }
933
961
  async#deliver(item) {
934
- const stopRenewing = this.#renewing(item);
962
+ const lease = leaseOf(this.#ctx, item);
963
+ const stop = this.#renewing(lease);
935
964
  try {
936
965
  await deliverItem({
937
966
  ...this.#ctx,
938
967
  domains: this.#domains,
939
968
  stopping: () => this.#stopping,
940
- recorded: stopRenewing
969
+ recorded: stop,
970
+ leaseLapsed: (now) => lapsed(lease, now)
941
971
  }, item);
942
972
  } catch (error) {
943
973
  this.#ctx.events.emit("error", { error, id: item.id });
944
974
  } finally {
945
- stopRenewing();
975
+ stop();
976
+ await settled(lease);
946
977
  }
947
978
  }
948
979
  #claim() {
@@ -1085,5 +1116,5 @@ export {
1085
1116
  createQueue
1086
1117
  };
1087
1118
 
1088
- //# debugId=06C900A54468AF5364756E2164756E21
1119
+ //# debugId=FF4E5F691337408064756E2164756E21
1089
1120
  //# sourceMappingURL=index.js.map