@bumail/queue 0.1.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +243 -0
  3. package/dist/chunks/index-p4k4rjb7.js +15 -0
  4. package/dist/chunks/index-p4k4rjb7.js.map +10 -0
  5. package/dist/chunks/index-xhsw4xcz.js +119 -0
  6. package/dist/chunks/index-xhsw4xcz.js.map +10 -0
  7. package/dist/contract/checks.d.ts +27 -0
  8. package/dist/contract/checks.d.ts.map +1 -0
  9. package/dist/contract/queue-store.d.ts +60 -0
  10. package/dist/contract/queue-store.d.ts.map +1 -0
  11. package/dist/contract/types.d.ts +102 -0
  12. package/dist/contract/types.d.ts.map +1 -0
  13. package/dist/dsn/build.d.ts +35 -0
  14. package/dist/dsn/build.d.ts.map +1 -0
  15. package/dist/dsn/content.d.ts +18 -0
  16. package/dist/dsn/content.d.ts.map +1 -0
  17. package/dist/dsn/fields.d.ts +38 -0
  18. package/dist/dsn/fields.d.ts.map +1 -0
  19. package/dist/errors.d.ts +17 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +1089 -0
  24. package/dist/index.js.map +30 -0
  25. package/dist/memory/index.d.ts +2 -0
  26. package/dist/memory/index.d.ts.map +1 -0
  27. package/dist/memory/index.js +121 -0
  28. package/dist/memory/index.js.map +10 -0
  29. package/dist/memory/store.d.ts +21 -0
  30. package/dist/memory/store.d.ts.map +1 -0
  31. package/dist/queue/deliver.d.ts +20 -0
  32. package/dist/queue/deliver.d.ts.map +1 -0
  33. package/dist/queue/envelope.d.ts +26 -0
  34. package/dist/queue/envelope.d.ts.map +1 -0
  35. package/dist/queue/events.d.ts +48 -0
  36. package/dist/queue/events.d.ts.map +1 -0
  37. package/dist/queue/limiter.d.ts +14 -0
  38. package/dist/queue/limiter.d.ts.map +1 -0
  39. package/dist/queue/notify.d.ts +19 -0
  40. package/dist/queue/notify.d.ts.map +1 -0
  41. package/dist/queue/options.d.ts +111 -0
  42. package/dist/queue/options.d.ts.map +1 -0
  43. package/dist/queue/outcome.d.ts +26 -0
  44. package/dist/queue/outcome.d.ts.map +1 -0
  45. package/dist/queue/queue.d.ts +43 -0
  46. package/dist/queue/queue.d.ts.map +1 -0
  47. package/dist/queue/retry.d.ts +16 -0
  48. package/dist/queue/retry.d.ts.map +1 -0
  49. package/dist/queue/route.d.ts +10 -0
  50. package/dist/queue/route.d.ts.map +1 -0
  51. package/dist/queue/settings/dsn.d.ts +8 -0
  52. package/dist/queue/settings/dsn.d.ts.map +1 -0
  53. package/dist/queue/settings/index.d.ts +25 -0
  54. package/dist/queue/settings/index.d.ts.map +1 -0
  55. package/dist/queue/settings/limits.d.ts +18 -0
  56. package/dist/queue/settings/limits.d.ts.map +1 -0
  57. package/dist/queue/settings/numbers.d.ts +7 -0
  58. package/dist/queue/settings/numbers.d.ts.map +1 -0
  59. package/dist/queue/settings/routes.d.ts +3 -0
  60. package/dist/queue/settings/routes.d.ts.map +1 -0
  61. package/dist/queue/settings/sessions.d.ts +5 -0
  62. package/dist/queue/settings/sessions.d.ts.map +1 -0
  63. package/dist/queue/settle.d.ts +21 -0
  64. package/dist/queue/settle.d.ts.map +1 -0
  65. package/dist/queue/worker.d.ts +27 -0
  66. package/dist/queue/worker.d.ts.map +1 -0
  67. package/dist/sqlite/index.d.ts +2 -0
  68. package/dist/sqlite/index.d.ts.map +1 -0
  69. package/dist/sqlite/index.js +288 -0
  70. package/dist/sqlite/index.js.map +13 -0
  71. package/dist/sqlite/open.d.ts +8 -0
  72. package/dist/sqlite/open.d.ts.map +1 -0
  73. package/dist/sqlite/options.d.ts +16 -0
  74. package/dist/sqlite/options.d.ts.map +1 -0
  75. package/dist/sqlite/rows.d.ts +28 -0
  76. package/dist/sqlite/rows.d.ts.map +1 -0
  77. package/dist/sqlite/schema.d.ts +14 -0
  78. package/dist/sqlite/schema.d.ts.map +1 -0
  79. package/dist/sqlite/store.d.ts +30 -0
  80. package/dist/sqlite/store.d.ts.map +1 -0
  81. package/dist/text.d.ts +14 -0
  82. package/dist/text.d.ts.map +1 -0
  83. package/docs/README.md +10 -0
  84. package/docs/guide.md +461 -0
  85. package/docs/roadmap.md +67 -0
  86. package/docs/troubleshooting.md +394 -0
  87. package/package.json +71 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Tsala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # @bumail/queue
2
+
3
+ The outbound queue of a mail server: it keeps every message your server
4
+ sends to another one, delivers it through `@bumail/smtp/client`, retries
5
+ what failed for now, and sends a delivery status notification (RFC 3464)
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.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ bun add @bumail/queue @bumail/smtp @bumail/mime
14
+ bun add @bumail/dns # for direct delivery by MX: the resolver
15
+ ```
16
+
17
+ `@bumail/smtp` (its client) and `@bumail/mime` (for the DSNs) are required
18
+ peers. `@bumail/dns` is optional: any resolver of its shape will do, and a
19
+ queue that only uses a smarthost needs none.
20
+
21
+ ## Subpaths
22
+
23
+ | import | what it gives |
24
+ | --- | --- |
25
+ | `@bumail/queue` | `createQueue`, the `QueueStore` contract, `QueueError` and the types |
26
+ | `@bumail/queue/memory` | `MemoryQueueStore`: in memory, lost on a restart — for specs and trials |
27
+ | `@bumail/queue/sqlite` | `SqliteQueueStore`: on disk with `bun:sqlite`, shared by the processes of one machine |
28
+
29
+ ## Usage
30
+
31
+ ```ts
32
+ import { nodeResolver } from '@bumail/dns';
33
+ import { createQueue } from '@bumail/queue';
34
+ import { SqliteQueueStore } from '@bumail/queue/sqlite';
35
+
36
+ const queue = createQueue({
37
+ store: SqliteQueueStore.open({ directory: '/var/lib/bumail/queue' }),
38
+ hostname: 'mail.example.net', // your public name: EHLO, and the DSN's Reporting-MTA
39
+ resolver: nodeResolver(),
40
+ });
41
+ queue.start();
42
+
43
+ // The message as it leaves: whole, headers first, CRLF, already DKIM-signed
44
+ // (with @bumail/auth's signDkim, say).
45
+ const signedMessage = await Bun.file('outgoing.eml').bytes();
46
+ const item = await queue.enqueue(signedMessage, {
47
+ from: 'mary@example.net',
48
+ to: ['joe@example.com', 'ann@example.org'],
49
+ });
50
+ item.recipients; // [{ address: 'joe@example.com', status: 'pending' }, …]
51
+
52
+ process.on('SIGTERM', () => queue.stop()); // lets deliveries under way end
53
+ ```
54
+
55
+ The message is a `Uint8Array`, a string or a `ReadableStream<Uint8Array>`,
56
+ with CRLF line ends: a bare CR or LF is refused, as `sendMail` would
57
+ refuse it. Every address is checked as one `sendMail` takes
58
+ (`@bumail/smtp/client`'s `isMailbox`: an RFC 5321 Mailbox with no source
59
+ route, no control character (C0, DEL or C1), no `>`, no U+2028 or U+2029,
60
+ no Unicode format character (`\p{Cf}`), no lone surrogate and no IPv4
61
+ literal octet above 255), so a bad one is refused at `enqueue`, never at
62
+ delivery.
63
+ Delivery groups the recipients by domain: one session per domain per
64
+ attempt, with STARTTLS when offered.
65
+
66
+ ## Retries and bounces
67
+
68
+ A 4xx, a connection error or a timeout leaves the recipient `deferred`;
69
+ the queue tries it again after 30 minutes, then 1 hour, 2, 4, and every 4
70
+ hours, each plus up to 10% of jitter, and gives up after 5 days
71
+ (RFC 5321 §4.5.4.1). A 5xx fails the recipient at once.
72
+
73
+ ```ts
74
+ import { nodeResolver } from '@bumail/dns';
75
+ import { createQueue } from '@bumail/queue';
76
+ import { MemoryQueueStore } from '@bumail/queue/memory';
77
+
78
+ const queue = createQueue({
79
+ store: new MemoryQueueStore(),
80
+ hostname: 'mail.example.net',
81
+ resolver: nodeResolver(),
82
+ retry: { first: 15 * 60_000, giveUpAfter: 3 * 24 * 3_600_000 },
83
+ dsn: { delayAfter: 4 * 3_600_000, returnContent: 'headers' },
84
+ });
85
+ ```
86
+
87
+ A failed recipient gets a DSN back to the sender, a `multipart/report`
88
+ with the original's header fields; a recipient still deferred after
89
+ `delayAfter` gets one "delayed" warning. A DSN is enqueued from the null
90
+ sender `<>`, and a message from `<>` never causes one.
91
+
92
+ ## Routing
93
+
94
+ ```ts
95
+ import { createQueue } from '@bumail/queue';
96
+ import { MemoryQueueStore } from '@bumail/queue/memory';
97
+
98
+ const smtpPassword = (await Bun.file('/run/secrets/smtp').text()).trim(); // from your secrets
99
+ createQueue({
100
+ store: new MemoryQueueStore(),
101
+ hostname: 'mail.example.net',
102
+ // Port 25 blocked? Everything through a provider's submission port:
103
+ route: {
104
+ host: 'smtp.provider.example',
105
+ port: 587,
106
+ auth: { username: 'mary@example.net', password: smtpPassword },
107
+ },
108
+ // …or only some domains, the rest by MX (which needs the resolver):
109
+ routes: { 'partner.example': { host: 'relay.partner.example' } },
110
+ });
111
+ ```
112
+
113
+ `route` defaults to `'mx'`: each recipient domain's own mail hosts.
114
+ Credentials go only over TLS whose certificate checked out: `createQueue`
115
+ refuses `auth` with a `tls` other than `'required'`, and checks the ports,
116
+ the TLS modes and the timeouts as `sendMail` would. A route `sendMail`
117
+ still refuses (`INVALID_OPTION`) defers its recipients as `4.3.5` and
118
+ says so on the `error` event; it does not bounce them until
119
+ `retry.giveUpAfter`, when they fail as `4.4.7` with a DSN like any
120
+ deferred recipient.
121
+
122
+ ## Several workers
123
+
124
+ ```ts
125
+ import { nodeResolver } from '@bumail/dns';
126
+ import { createQueue } from '@bumail/queue';
127
+ import { SqliteQueueStore } from '@bumail/queue/sqlite';
128
+
129
+ // In each process, on the same directory: a claim leases an item to one worker.
130
+ const queue = createQueue({
131
+ store: SqliteQueueStore.open({ directory: '/var/lib/bumail/queue' }),
132
+ hostname: 'mail.example.net',
133
+ resolver: nodeResolver(),
134
+ });
135
+ queue.start();
136
+ ```
137
+
138
+ A worker claims a due item with a lease, renews it while it delivers, and
139
+ lets go of it with the outcome; a renewal that fails is told on `error`
140
+ and tried again. If it crashes, another worker claims the item once the
141
+ lease expires (`leaseMs`, 10 minutes by default). A directory the store
142
+ makes is 0700; one that exists keeps its mode.
143
+ `concurrency` (20) bounds the items one worker delivers at once — each
144
+ item opens one session per recipient domain — and `perDomain` (2) its
145
+ sessions to one recipient domain.
146
+
147
+ ## Events and admin
148
+
149
+ ```ts
150
+ // queue: the one created under Usage.
151
+ queue.on('delivered', ({ id, recipient, reply }) => console.info({ id, recipient, reply }));
152
+ queue.on('deferred', ({ recipient, reply, nextAttemptAt }) => {});
153
+ queue.on('failed', ({ recipient, reply }) => {}); // reply: { code?, status?, text, host? }
154
+ queue.on('dsn', ({ kind, of, to }) => {});
155
+ queue.on('error', ({ error, id }) => console.error(id, error)); // the store, a lost lease, a bad route
156
+
157
+ const [next] = await queue.list({ limit: 50 }); // the next due first
158
+ if (next) await queue.retryNow(next.id); // false while a worker delivers it
159
+ if (next) await queue.cancel(next.id); // no DSN
160
+ ```
161
+
162
+ ## Testing
163
+
164
+ ```ts
165
+ import { createQueue, type Sender } from '@bumail/queue';
166
+ import { MemoryQueueStore } from '@bumail/queue/memory';
167
+
168
+ let now = Date.UTC(2026, 0, 1);
169
+ const send: Sender = async (_message, options) => ({
170
+ accepted: [options.to].flat().map((recipient) => ({ recipient, reply: { code: 250, text: 'OK' } })),
171
+ rejected: [],
172
+ reply: { code: 250, text: 'Queued' },
173
+ host: 'mx.test',
174
+ port: 25,
175
+ tls: false,
176
+ authenticated: false,
177
+ });
178
+ const queue = createQueue({
179
+ store: new MemoryQueueStore(),
180
+ hostname: 'mail.test',
181
+ route: { host: 'mx.test' },
182
+ clock: { now: () => now },
183
+ send,
184
+ });
185
+ await queue.enqueue('Subject: hi\r\n\r\nhello\r\n', { from: 'a@test', to: 'b@test' });
186
+ await queue.deliverDue(); // one pass, by hand: no timer, no network
187
+ now += 30 * 60_000;
188
+ ```
189
+
190
+ ## Limits
191
+
192
+ ```ts
193
+ import { nodeResolver } from '@bumail/dns';
194
+ import { createQueue } from '@bumail/queue';
195
+ import { MemoryQueueStore } from '@bumail/queue/memory';
196
+
197
+ createQueue({
198
+ store: new MemoryQueueStore(),
199
+ hostname: 'mail.example.net',
200
+ resolver: nodeResolver(),
201
+ limits: { maxMessageSize: 10 * 1024 * 1024, maxRecipients: 50, maxItems: 100_000 },
202
+ });
203
+ ```
204
+
205
+ Everything is bounded, through `limits`: the message (25 MiB), the
206
+ recipients per message (100), the items in the store (none by default),
207
+ the reply text kept per recipient (512 characters, control characters
208
+ replaced by spaces), and the original a DSN returns (64 KiB, each line cut
209
+ at 998 bytes). Nothing a
210
+ remote server says reaches a DSN's header fields with a CR, an LF or a
211
+ control character in it.
212
+
213
+ ## Traps
214
+
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.
217
+ - **It sends what you enqueue, to anyone.** The queue is not a relay
218
+ policy: enqueue only what an authenticated user submitted, or what your
219
+ own server writes, never what an unauthenticated client handed you.
220
+
221
+ ## API
222
+
223
+ | export | |
224
+ | --- | --- |
225
+ | `createQueue(options)`, `Queue` | the queue: `enqueue`, `deliverDue`, `start`, `stop`, `on`, `list`, `get`, `retryNow`, `cancel`, `owner` |
226
+ | `QueueOptions`, `Route`, `Smarthost`, `RetrySchedule`, `DsnOptions`, `QueueLimits`, `Clock`, `Sender` | what `createQueue` takes |
227
+ | `MessageSource`, `QueueEnvelope` | what `enqueue` takes |
228
+ | `QueueEvents`, `QueueListener`, `RecipientEvent`, `DeferredEvent`, `DsnEvent`, `QueueErrorEvent` | the events |
229
+ | `QueueStore` | the contract a store answers: `add`, `get`, `list`, `count`, `readMessage`, `claim`, `renew`, `complete`, `reschedule`, `cancel` |
230
+ | `QueueItem`, `RecipientState`, `RecipientStatus`, `FinalStatus`, `Diagnostic`, `Lease` | an item, and where each recipient stands |
231
+ | `NewQueueItem`, `AddOptions`, `ClaimRequest`, `AttemptResult`, `RecipientUpdate`, `QueueListOptions` | what a store takes |
232
+ | `QueueError`, `QueueErrorCode` | `INVALID`, `MESSAGE_TOO_BIG`, `TOO_MANY_RECIPIENTS`, `QUEUE_FULL`, `CLOSED`, `LEASE_LOST` |
233
+ | `MemoryQueueStore` | from `@bumail/queue/memory` |
234
+ | `SqliteQueueStore`, `SqliteQueueStoreOptions` | from `@bumail/queue/sqlite`: `SqliteQueueStore.open({ directory, busyTimeout? })`, `close()` |
235
+
236
+ ## Documentation
237
+
238
+ These pages ship in the package, under `docs/`.
239
+
240
+ - [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.
242
+ - [Troubleshooting](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/troubleshooting.md): every `QueueError`, and what to do about it.
243
+ - [Roadmap](https://github.com/softistx/bumail/blob/develop/packages/queue/docs/roadmap.md): what is coming, and what is not planned.
@@ -0,0 +1,15 @@
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
+ export { QueueError2, invalid };
13
+
14
+ //# debugId=465D2E3985AB194764756E2164756E21
15
+ //# sourceMappingURL=index-p4k4rjb7.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/errors.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.\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
+ ],
7
+ "mappings": ";AAgBO,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;",
8
+ "debugId": "465D2E3985AB194764756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ invalid
3
+ } from "./index-p4k4rjb7.js";
4
+
5
+ // src/contract/checks.ts
6
+ var isTime = (value) => typeof value === "number" && Number.isFinite(value);
7
+ function checkTime(name, value) {
8
+ if (!isTime(value))
9
+ throw invalid(`${name} must be a finite number`);
10
+ return value;
11
+ }
12
+ function checkOwner(owner) {
13
+ if (typeof owner !== "string" || owner === "") {
14
+ throw invalid("owner must be a non-empty string");
15
+ }
16
+ return owner;
17
+ }
18
+ function checkNewItem(item) {
19
+ if (typeof item !== "object" || item === null) {
20
+ throw invalid("A new item is an object");
21
+ }
22
+ if (typeof item.from !== "string")
23
+ throw invalid("from must be a string");
24
+ const to = item.to;
25
+ if (!Array.isArray(to) || to.length === 0 || !to.every((a) => typeof a === "string" && a !== "")) {
26
+ throw invalid("to must be a non-empty array of addresses");
27
+ }
28
+ if (!(item.message instanceof Uint8Array)) {
29
+ throw invalid("message must be a Uint8Array");
30
+ }
31
+ checkTime("createdAt", item.createdAt);
32
+ }
33
+ function checkMaxItems(max) {
34
+ if (max === undefined)
35
+ return;
36
+ if (typeof max !== "number" || !Number.isInteger(max) || max < 1) {
37
+ throw invalid(`maxItems must be an integer of at least 1, not ${max}`);
38
+ }
39
+ return max;
40
+ }
41
+ function checkClaim(request) {
42
+ if (typeof request !== "object" || request === null) {
43
+ throw invalid("A claim is an object");
44
+ }
45
+ checkOwner(request.owner);
46
+ checkTime("now", request.now);
47
+ if (!isTime(request.leaseMs) || request.leaseMs <= 0) {
48
+ throw invalid("leaseMs must be a positive number");
49
+ }
50
+ }
51
+ function checkResult(result) {
52
+ if (typeof result !== "object" || result === null) {
53
+ throw invalid("An attempt result is an object");
54
+ }
55
+ checkTime("now", result.now);
56
+ checkTime("nextAttemptAt", result.nextAttemptAt);
57
+ if (!Number.isInteger(result.attempts) || result.attempts < 0) {
58
+ throw invalid("attempts must be an integer of at least 0");
59
+ }
60
+ if (typeof result.delayNotified !== "boolean") {
61
+ throw invalid("delayNotified must be true or false");
62
+ }
63
+ const statuses = ["delivered", "deferred", "failed"];
64
+ if (!Array.isArray(result.recipients) || !result.recipients.every((u) => typeof u?.address === "string" && statuses.includes(u.status))) {
65
+ throw invalid("recipients must be an array of { address, status: delivered, deferred or failed }");
66
+ }
67
+ }
68
+ function checkList(options = {}) {
69
+ const offset = options.offset ?? 0;
70
+ const limit = options.limit ?? 100;
71
+ if (!Number.isInteger(offset) || offset < 0) {
72
+ throw invalid(`offset must be an integer of at least 0, not ${offset}`);
73
+ }
74
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
75
+ throw invalid(`limit must be an integer from 1 to 1000, not ${limit}`);
76
+ }
77
+ return { offset, limit };
78
+ }
79
+ var isDone = (item) => item.recipients.every((r) => r.status === "delivered" || r.status === "failed");
80
+ function applyAttempt(item, result) {
81
+ const updates = new Map(result.recipients.map((u) => [u.address, u]));
82
+ const recipients = item.recipients.map((r) => {
83
+ const update = updates.get(r.address);
84
+ if (!update || r.status === "delivered" || r.status === "failed")
85
+ return r;
86
+ return {
87
+ address: r.address,
88
+ status: update.status,
89
+ ...update.reply ? { reply: update.reply } : {},
90
+ updatedAt: result.now
91
+ };
92
+ });
93
+ const { lease: _, ...rest } = item;
94
+ return {
95
+ ...rest,
96
+ recipients,
97
+ nextAttemptAt: result.nextAttemptAt,
98
+ attempts: result.attempts,
99
+ delayNotified: result.delayNotified
100
+ };
101
+ }
102
+ function newItem(id, item) {
103
+ return {
104
+ id,
105
+ from: item.from,
106
+ recipients: item.to.map((address) => ({ address, status: "pending" })),
107
+ size: item.message.length,
108
+ createdAt: item.createdAt,
109
+ nextAttemptAt: item.createdAt,
110
+ attempts: 0,
111
+ delayNotified: false
112
+ };
113
+ }
114
+ var claimable = (item, now) => item.nextAttemptAt <= now && (item.lease === undefined || item.lease.expiresAt <= now);
115
+
116
+ export { checkTime, checkOwner, checkNewItem, checkMaxItems, checkClaim, checkResult, checkList, isDone, applyAttempt, newItem, claimable };
117
+
118
+ //# debugId=70E823C3942B47A164756E2164756E21
119
+ //# sourceMappingURL=index-xhsw4xcz.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/contract/checks.ts"],
4
+ "sourcesContent": [
5
+ "import { invalid } from '../errors';\nimport type {\n\tAttemptResult,\n\tClaimRequest,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n\tRecipientState,\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/** 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\treturn owner;\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\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\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}\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"
6
+ ],
7
+ "mappings": ";;;;;AAYA,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;AAID,SAAS,UAAU,CAAC,OAAwB;AAAA,EAClD,IAAI,OAAO,UAAU,YAAY,UAAU,IAAI;AAAA,IAC9C,MAAM,QAAQ,kCAAkC;AAAA,EACjD;AAAA,EACA,OAAO;AAAA;AAGD,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,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,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;AAIM,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;",
8
+ "debugId": "70E823C3942B47A164756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,27 @@
1
+ import type { AttemptResult, ClaimRequest, NewQueueItem, QueueItem, QueueListOptions } from './types';
2
+ /** A time in milliseconds since the epoch, or `INVALID` naming it. */
3
+ export declare function checkTime(name: string, value: unknown): number;
4
+ /** A lease owner: a non-empty string. */
5
+ export declare function checkOwner(owner: unknown): string;
6
+ export declare function checkNewItem(item: NewQueueItem): void;
7
+ /** `maxItems`: a positive integer, or nothing. */
8
+ export declare function checkMaxItems(max: unknown): number | undefined;
9
+ export declare function checkClaim(request: ClaimRequest): void;
10
+ export declare function checkResult(result: AttemptResult): void;
11
+ /** The offset and limit of a list: integers, the limit 1 to 1000. */
12
+ export declare function checkList(options?: QueueListOptions): {
13
+ offset: number;
14
+ limit: number;
15
+ };
16
+ /** Every recipient is `delivered` or `failed`. */
17
+ export declare const isDone: (item: Pick<QueueItem, "recipients">) => boolean;
18
+ /**
19
+ * The item once an attempt is recorded: its recipients' outcomes, a final
20
+ * state never changed again, the schedule, and no lease.
21
+ */
22
+ export declare function applyAttempt(item: QueueItem, result: AttemptResult): QueueItem;
23
+ /** A new item as a store first keeps it. */
24
+ export declare function newItem(id: string, item: NewQueueItem): QueueItem;
25
+ /** Whether a lease, if any, leaves the item free to claim at `now`. */
26
+ export declare const claimable: (item: QueueItem, now: number) => boolean;
27
+ //# sourceMappingURL=checks.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,60 @@
1
+ import type { AddOptions, AttemptResult, ClaimRequest, NewQueueItem, QueueItem, QueueListOptions } from './types';
2
+ /**
3
+ * Where the queue keeps its items, made for several workers, in one
4
+ * process or many, sharing one store. A worker **claims** a due item: it
5
+ * gets a lease, an owner and an expiry, and no other worker gets the item
6
+ * until the lease expires — a worker that crashed loses it then, and
7
+ * another claims it. Its owner renews the lease while it delivers, records
8
+ * the outcome with `complete`, which lets go of the lease, or gives the
9
+ * item back with `reschedule`.
10
+ *
11
+ * An item whose every recipient is `delivered` or `failed` is done: the
12
+ * store drops it, and its message, in the same step. Times are
13
+ * milliseconds since the epoch, given by the caller: a store keeps no
14
+ * clock. Nothing a store returns is shared with what it keeps.
15
+ */
16
+ export interface QueueStore {
17
+ /**
18
+ * Adds an item, due at once, with every recipient `pending`. With
19
+ * `maxItems`, refuses with `QUEUE_FULL` when the store holds that many
20
+ * already, in the same step as the add.
21
+ */
22
+ add(item: NewQueueItem, options?: AddOptions): Promise<QueueItem>;
23
+ /** The item, or `undefined` when there is none by that id (done, cancelled, or never added). */
24
+ get(id: string): Promise<QueueItem | undefined>;
25
+ /** The items, the next due first; done items are gone. */
26
+ list(options?: QueueListOptions): Promise<QueueItem[]>;
27
+ /** How many items the store holds. */
28
+ count(): Promise<number>;
29
+ /** The message of an item, or `undefined` once the item is gone. */
30
+ readMessage(id: string): Promise<Uint8Array | undefined>;
31
+ /**
32
+ * Takes the next item due at `now` — no lease, or a lease expired —
33
+ * and gives it a lease of `leaseMs` for `owner`, atomically: two
34
+ * claimers never take the same item. The earliest due goes first, then
35
+ * the oldest. `undefined` when nothing is due.
36
+ */
37
+ claim(request: ClaimRequest): Promise<QueueItem | undefined>;
38
+ /**
39
+ * Moves the lease's expiry, while `owner` still holds it. False when it
40
+ * does not: another worker claimed the item, or it is gone.
41
+ */
42
+ renew(id: string, owner: string, expiresAt: number): Promise<boolean>;
43
+ /**
44
+ * Records an attempt: each recipient's outcome, the next attempt and the
45
+ * count, and lets go of the lease — in one step, while `owner` holds it.
46
+ * Returns the item as it now stands, done (every recipient final, and
47
+ * the item dropped) or not; `undefined` when `owner` no longer holds it.
48
+ */
49
+ complete(id: string, owner: string, result: AttemptResult): Promise<QueueItem | undefined>;
50
+ /**
51
+ * Moves the next attempt to `at`. With `owner`, only while that owner
52
+ * holds the lease, which it lets go of: a worker giving an item back.
53
+ * Without, whoever holds it keeps the lease: an admin's "retry now".
54
+ * False when the item is gone, or `owner` does not hold it.
55
+ */
56
+ reschedule(id: string, at: number, owner?: string): Promise<boolean>;
57
+ /** Drops an item and its message, leased or not; the item as it stood, or `undefined`. */
58
+ cancel(id: string): Promise<QueueItem | undefined>;
59
+ }
60
+ //# sourceMappingURL=queue-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-store.d.ts","sourceRoot":"","sources":["../../src/contract/queue-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,MAAM,SAAS,CAAC;AAEjB;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,UAAU;IAC1B;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IAElE,gGAAgG;IAChG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAEhD,0DAA0D;IAC1D,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAEvD,sCAAsC;IACtC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzB,oEAAoE;IACpE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAEzD;;;;;OAKG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAE7D;;;OAGG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtE;;;;;OAKG;IACH,QAAQ,CACP,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAElC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAErE,0FAA0F;IAC1F,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;CACnD"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Where one recipient stands. `pending`: not tried yet. `deferred`: a
3
+ * temporary failure, tried again later. `delivered` and `failed` are
4
+ * final.
5
+ */
6
+ export type RecipientStatus = 'pending' | 'delivered' | 'deferred' | 'failed';
7
+ /** The final states: the queue never tries such a recipient again. */
8
+ export type FinalStatus = 'delivered' | 'failed';
9
+ /**
10
+ * What the other side said, or why nobody said anything: the SMTP reply
11
+ * (RFC 5321 §4.2) with its enhanced status code (RFC 3463), or, with no
12
+ * `code`, the connection error or the timeout. The text is bounded and
13
+ * holds no control character.
14
+ */
15
+ export interface Diagnostic {
16
+ /** The reply code; absent when no server answered. */
17
+ readonly code?: number;
18
+ /** The enhanced status code, `x.y.z`. */
19
+ readonly status?: string;
20
+ readonly text: string;
21
+ /** The host that answered, or that the attempt was for. */
22
+ readonly host?: string;
23
+ }
24
+ export interface RecipientState {
25
+ /** `local@domain`, as enqueued. */
26
+ readonly address: string;
27
+ readonly status: RecipientStatus;
28
+ /** The last answer for this recipient. */
29
+ readonly reply?: Diagnostic;
30
+ /** When the status last changed, in milliseconds since the epoch. */
31
+ readonly updatedAt?: number;
32
+ }
33
+ /** Who is delivering an item, until when. */
34
+ export interface Lease {
35
+ readonly owner: string;
36
+ /** Milliseconds since the epoch; past it, another worker may claim the item. */
37
+ readonly expiresAt: number;
38
+ }
39
+ /** A message in the queue, and where each of its recipients stands. */
40
+ export interface QueueItem {
41
+ readonly id: string;
42
+ /** The reverse-path; `''` for the null sender of a DSN. */
43
+ readonly from: string;
44
+ readonly recipients: readonly RecipientState[];
45
+ /** The message's size in bytes. */
46
+ readonly size: number;
47
+ /** Milliseconds since the epoch. */
48
+ readonly createdAt: number;
49
+ /** When the item is due next, in milliseconds since the epoch. */
50
+ readonly nextAttemptAt: number;
51
+ /** How many attempts were made. */
52
+ readonly attempts: number;
53
+ /** The "delayed" DSN was sent for it. */
54
+ readonly delayNotified: boolean;
55
+ /** Present while a worker holds the item. */
56
+ readonly lease?: Lease;
57
+ }
58
+ /** What `add` takes. */
59
+ export interface NewQueueItem {
60
+ readonly from: string;
61
+ /** One address or more, each once. */
62
+ readonly to: readonly string[];
63
+ /** The whole message, as it will be sent. */
64
+ readonly message: Uint8Array;
65
+ /** Milliseconds since the epoch; due at once. */
66
+ readonly createdAt: number;
67
+ }
68
+ export interface AddOptions {
69
+ /** Refuse with `QUEUE_FULL` when the store already holds this many items. */
70
+ readonly maxItems?: number;
71
+ }
72
+ /** What `claim` takes: who claims, at what time, for how long. */
73
+ export interface ClaimRequest {
74
+ readonly owner: string;
75
+ /** Milliseconds since the epoch. */
76
+ readonly now: number;
77
+ /** How long the lease lasts, in milliseconds. */
78
+ readonly leaseMs: number;
79
+ }
80
+ /** One recipient's outcome in an attempt. */
81
+ export interface RecipientUpdate {
82
+ readonly address: string;
83
+ readonly status: Exclude<RecipientStatus, 'pending'>;
84
+ readonly reply?: Diagnostic;
85
+ }
86
+ /** What `complete` records: the attempt's outcomes, and when to try again. */
87
+ export interface AttemptResult {
88
+ /** Milliseconds since the epoch: when the outcomes were known. */
89
+ readonly now: number;
90
+ /** The recipients this attempt reached; the others keep their state. */
91
+ readonly recipients: readonly RecipientUpdate[];
92
+ readonly nextAttemptAt: number;
93
+ readonly attempts: number;
94
+ readonly delayNotified: boolean;
95
+ }
96
+ export interface QueueListOptions {
97
+ /** Default 0. */
98
+ readonly offset?: number;
99
+ /** Default 100. */
100
+ readonly limit?: number;
101
+ }
102
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/contract/types.ts"],"names":[],"mappings":"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"}