@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.
- package/README.md +71 -5
- package/dist/chunks/{index-xhsw4xcz.js → index-vg971acr.js} +70 -6
- package/dist/chunks/index-vg971acr.js.map +12 -0
- package/dist/chunks/index-y3n1w2az.js +18 -0
- package/dist/chunks/index-y3n1w2az.js.map +10 -0
- package/dist/contract/checks.d.ts +6 -0
- package/dist/contract/checks.d.ts.map +1 -1
- package/dist/errors.d.ts +2 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +86 -55
- package/dist/index.js.map +8 -8
- package/dist/masked.d.ts +7 -0
- package/dist/masked.d.ts.map +1 -0
- package/dist/memory/index.js +3 -5
- package/dist/memory/index.js.map +2 -2
- package/dist/postgres/connect.d.ts +17 -0
- package/dist/postgres/connect.d.ts.map +1 -0
- package/dist/postgres/index.d.ts +2 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +374 -0
- package/dist/postgres/index.js.map +14 -0
- package/dist/postgres/options.d.ts +37 -0
- package/dist/postgres/options.d.ts.map +1 -0
- package/dist/postgres/rows.d.ts +21 -0
- package/dist/postgres/rows.d.ts.map +1 -0
- package/dist/postgres/schema.d.ts +20 -0
- package/dist/postgres/schema.d.ts.map +1 -0
- package/dist/postgres/statements.d.ts +54 -0
- package/dist/postgres/statements.d.ts.map +1 -0
- package/dist/postgres/store.d.ts +38 -0
- package/dist/postgres/store.d.ts.map +1 -0
- package/dist/queue/deliver.d.ts +7 -0
- 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/outcome.d.ts.map +1 -1
- package/dist/queue/settings/index.d.ts.map +1 -1
- package/dist/queue/worker.d.ts.map +1 -1
- package/dist/redis/connect.d.ts +25 -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 +430 -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/dist/sqlite/index.js +3 -5
- package/dist/sqlite/index.js.map +2 -2
- package/dist/text.d.ts +9 -3
- package/dist/text.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/guide.md +314 -7
- package/docs/roadmap.md +73 -19
- package/docs/troubleshooting.md +317 -7
- package/package.json +15 -3
- package/dist/chunks/index-p4k4rjb7.js +0 -15
- package/dist/chunks/index-p4k4rjb7.js.map +0 -10
- 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`,
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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=
|
|
119
|
-
//# sourceMappingURL=index-
|
|
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": ";AAiBO,MAAM,oBAAmB,MAAM;AAAA,EACnB,OAAO;AAAA,EAChB;AAAA,EAET,WAAW,CAAC,MAAsB,SAAiB;AAAA,IAClD,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA;AAEd;AAGO,IAAM,UAAU,CAAC,YAAoB,IAAI,YAAW,WAAW,OAAO;;;ACzB7E,IAAM,YAAY,CAAC,SAClB,OAAO,MAAS,QAAQ,OAAQ,QAAQ;AAGlC,SAAS,UAAU,CAAC,MAAuB;AAAA,EACjD,SAAS,IAAI,EAAG,IAAI,KAAK,QAAQ,KAAK;AAAA,IACrC,IAAI,UAAU,KAAK,WAAW,CAAC,CAAC;AAAA,MAAG,OAAO;AAAA,EAC3C;AAAA,EACA,OAAO;AAAA;AAID,SAAS,eAAe,CAAC,MAAc,IAAoB;AAAA,EACjE,IAAI,MAAM;AAAA,EACV,SAAS,IAAI,EAAG,IAAI,KAAK,QAAQ,KAAK;AAAA,IACrC,OAAO,UAAU,KAAK,WAAW,CAAC,CAAC,IAAI,KAAM,KAAK;AAAA,EACnD;AAAA,EACA,OAAO;AAAA;AASD,SAAS,SAAS,CAAC,MAAc,KAAqB;AAAA,EAC5D,MAAM,QAAQ,gBAAgB,KAAK,aAAa,GAAG,GAAG,EACpD,QAAQ,UAAU,GAAG,EACrB,KAAK;AAAA,EACP,OAAO,MAAM,SAAS,MAAM,GAAG,IAAI,OAAO,MAAM,CAAC,SAAS;AAAA;AAQ3D,SAAS,GAAG,CAAC,MAAc,QAAwB;AAAA,EAClD,MAAM,OAAO,KAAK,MAAM,GAAG,MAAM;AAAA,EACjC,MAAM,OAAO,KAAK,WAAW,KAAK,SAAS,CAAC;AAAA,EAC5C,OAAO,QAAQ,SAAU,QAAQ,QAAS,KAAK,MAAM,GAAG,EAAE,IAAI;AAAA;AAOxD,IAAM,aAAa,CAAC,SAC1B,KAAK,aAAa,KAAK,CAAC,KAAK,SAAS,MAAI;AAGpC,IAAM,YAAY,CAAC,MAAc,QACvC,UAAU,MAAM,GAAG,EAAE,QAAQ,WAAW,GAAG;;;ACzC5C,IAAM,SAAS,CAAC,UACf,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AAG5C,SAAS,SAAS,CAAC,MAAc,OAAwB;AAAA,EAC/D,IAAI,CAAC,OAAO,KAAK;AAAA,IAAG,MAAM,QAAQ,GAAG,8BAA8B;AAAA,EACnE,OAAO;AAAA;AAQD,SAAS,aAAa,CAAC,MAAc,MAAoB;AAAA,EAC/D,IAAI,CAAC,WAAW,IAAI,GAAG;AAAA,IACtB,MAAM,QACL,GAAG,iEACJ;AAAA,EACD;AAAA;AAIM,SAAS,UAAU,CAAC,OAAwB;AAAA,EAClD,IAAI,OAAO,UAAU,YAAY,UAAU,IAAI;AAAA,IAC9C,MAAM,QAAQ,kCAAkC;AAAA,EACjD;AAAA,EACA,cAAc,SAAS,KAAK;AAAA,EAC5B,OAAO;AAAA;AAIR,SAAS,WAAW,CAAC,QAA+B;AAAA,EACnD,cAAc,eAAe,OAAO,OAAO;AAAA,EAC3C,MAAM,QAAQ,OAAO;AAAA,EACrB,IAAI,UAAU;AAAA,IAAW;AAAA,EACzB,IACC,OAAO,UAAU,YACjB,UAAU,QACV,OAAO,MAAM,SAAS,UACrB;AAAA,IACD,MAAM,QAAQ,kCAAkC;AAAA,EACjD;AAAA,EACA,cAAc,cAAc,MAAM,IAAI;AAAA,EACtC,WAAW,QAAQ,CAAC,UAAU,MAAM,GAAY;AAAA,IAC/C,MAAM,QAAQ,MAAM;AAAA,IACpB,IAAI,UAAU;AAAA,MAAW;AAAA,IACzB,IAAI,OAAO,UAAU;AAAA,MACpB,MAAM,QAAQ,SAAS,uBAAuB;AAAA,IAC/C,cAAc,SAAS,QAAQ,KAAK;AAAA,EACrC;AAAA;AAGM,SAAS,YAAY,CAAC,MAA0B;AAAA,EACtD,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAAA,IAC9C,MAAM,QAAQ,yBAAyB;AAAA,EACxC;AAAA,EACA,IAAI,OAAO,KAAK,SAAS;AAAA,IAAU,MAAM,QAAQ,uBAAuB;AAAA,EACxE,cAAc,QAAQ,KAAK,IAAI;AAAA,EAC/B,MAAM,KAAK,KAAK;AAAA,EAChB,IACC,CAAC,MAAM,QAAQ,EAAE,KACjB,GAAG,WAAW,KACd,CAAC,GAAG,MAAM,CAAC,MAAM,OAAO,MAAM,YAAY,MAAM,EAAE,GACjD;AAAA,IACD,MAAM,QAAQ,2CAA2C;AAAA,EAC1D;AAAA,EACA,WAAW,WAAW;AAAA,IAAgB,cAAc,eAAe,OAAO;AAAA,EAC1E,IAAI,EAAE,KAAK,mBAAmB,aAAa;AAAA,IAC1C,MAAM,QAAQ,8BAA8B;AAAA,EAC7C;AAAA,EACA,UAAU,aAAa,KAAK,SAAS;AAAA;AAI/B,SAAS,aAAa,CAAC,KAAkC;AAAA,EAC/D,IAAI,QAAQ;AAAA,IAAW;AAAA,EACvB,IAAI,OAAO,QAAQ,YAAY,CAAC,OAAO,UAAU,GAAG,KAAK,MAAM,GAAG;AAAA,IACjE,MAAM,QAAQ,kDAAkD,KAAK;AAAA,EACtE;AAAA,EACA,OAAO;AAAA;AAGD,SAAS,UAAU,CAAC,SAA6B;AAAA,EACvD,IAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AAAA,IACpD,MAAM,QAAQ,sBAAsB;AAAA,EACrC;AAAA,EACA,WAAW,QAAQ,KAAK;AAAA,EACxB,UAAU,OAAO,QAAQ,GAAG;AAAA,EAC5B,IAAI,CAAC,OAAO,QAAQ,OAAO,KAAK,QAAQ,WAAW,GAAG;AAAA,IACrD,MAAM,QAAQ,mCAAmC;AAAA,EAClD;AAAA;AAGM,SAAS,WAAW,CAAC,QAA6B;AAAA,EACxD,IAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AAAA,IAClD,MAAM,QAAQ,gCAAgC;AAAA,EAC/C;AAAA,EACA,UAAU,OAAO,OAAO,GAAG;AAAA,EAC3B,UAAU,iBAAiB,OAAO,aAAa;AAAA,EAC/C,IAAI,CAAC,OAAO,UAAU,OAAO,QAAQ,KAAK,OAAO,WAAW,GAAG;AAAA,IAC9D,MAAM,QAAQ,2CAA2C;AAAA,EAC1D;AAAA,EACA,IAAI,OAAO,OAAO,kBAAkB,WAAW;AAAA,IAC9C,MAAM,QAAQ,qCAAqC;AAAA,EACpD;AAAA,EACA,MAAM,WAAW,CAAC,aAAa,YAAY,QAAQ;AAAA,EACnD,IACC,CAAC,MAAM,QAAQ,OAAO,UAAU,KAChC,CAAC,OAAO,WAAW,MAClB,CAAC,MAAM,OAAO,GAAG,YAAY,YAAY,SAAS,SAAS,EAAE,MAAM,CACpE,GACC;AAAA,IACD,MAAM,QACL,mFACD;AAAA,EACD;AAAA,EACA,WAAW,UAAU,OAAO;AAAA,IAAY,YAAY,MAAM;AAAA;AAIpD,SAAS,SAAS,CAAC,UAA4B,CAAC,GAGrD;AAAA,EACD,MAAM,SAAS,QAAQ,UAAU;AAAA,EACjC,MAAM,QAAQ,QAAQ,SAAS;AAAA,EAC/B,IAAI,CAAC,OAAO,UAAU,MAAM,KAAK,SAAS,GAAG;AAAA,IAC5C,MAAM,QAAQ,gDAAgD,QAAQ;AAAA,EACvE;AAAA,EACA,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,MAAM;AAAA,IAC1D,MAAM,QAAQ,gDAAgD,OAAO;AAAA,EACtE;AAAA,EACA,OAAO,EAAE,QAAQ,MAAM;AAAA;AAIjB,IAAM,SAAS,CAAC,SACtB,KAAK,WAAW,MACf,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE,WAAW,QACjD;AAMM,SAAS,YAAY,CAC3B,MACA,QACY;AAAA,EACZ,MAAM,UAAU,IAAI,IAAI,OAAO,WAAW,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;AAAA,EACpE,MAAM,aAAa,KAAK,WAAW,IAAI,CAAC,MAAsB;AAAA,IAC7D,MAAM,SAAS,QAAQ,IAAI,EAAE,OAAO;AAAA,IACpC,IAAI,CAAC,UAAU,EAAE,WAAW,eAAe,EAAE,WAAW;AAAA,MAAU,OAAO;AAAA,IACzE,OAAO;AAAA,MACN,SAAS,EAAE;AAAA,MACX,QAAQ,OAAO;AAAA,SACX,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9C,WAAW,OAAO;AAAA,IACnB;AAAA,GACA;AAAA,EACD,QAAQ,OAAO,MAAM,SAAS;AAAA,EAC9B,OAAO;AAAA,OACH;AAAA,IACH;AAAA,IACA,eAAe,OAAO;AAAA,IACtB,UAAU,OAAO;AAAA,IACjB,eAAe,OAAO;AAAA,EACvB;AAAA;AAIM,SAAS,OAAO,CAAC,IAAY,MAA+B;AAAA,EAClE,OAAO;AAAA,IACN;AAAA,IACA,MAAM,KAAK;AAAA,IACX,YAAY,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,QAAQ,UAAU,EAAE;AAAA,IACrE,MAAM,KAAK,QAAQ;AAAA,IACnB,WAAW,KAAK;AAAA,IAChB,eAAe,KAAK;AAAA,IACpB,UAAU;AAAA,IACV,eAAe;AAAA,EAChB;AAAA;AAIM,IAAM,YAAY,CAAC,MAAiB,QAC1C,KAAK,iBAAiB,QACrB,KAAK,UAAU,aAAa,KAAK,MAAM,aAAa;",
|
|
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":"
|
|
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. */
|
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;;;;;;;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
|
-
|
|
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: {
|
|
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(
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
|
956
|
+
return () => {
|
|
957
|
+
lease.done = true;
|
|
958
|
+
clearInterval(timer);
|
|
959
|
+
};
|
|
932
960
|
}
|
|
933
961
|
async#deliver(item) {
|
|
934
|
-
const
|
|
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:
|
|
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
|
-
|
|
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=
|
|
1119
|
+
//# debugId=FF4E5F691337408064756E2164756E21
|
|
1089
1120
|
//# sourceMappingURL=index.js.map
|