@bumail/queue 0.7.2 → 0.9.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 (77) hide show
  1. package/README.md +27 -3
  2. package/dist/chunks/{index-3pt7rc59.js → index-9yp15arf.js} +98 -25
  3. package/dist/chunks/index-9yp15arf.js.map +14 -0
  4. package/dist/chunks/index-r8wb840a.js +24 -0
  5. package/dist/chunks/index-r8wb840a.js.map +10 -0
  6. package/dist/contract/checks.d.ts +2 -6
  7. package/dist/contract/checks.d.ts.map +1 -1
  8. package/dist/contract/dsn-checks.d.ts +4 -0
  9. package/dist/contract/dsn-checks.d.ts.map +1 -0
  10. package/dist/contract/dsn-column.d.ts +12 -0
  11. package/dist/contract/dsn-column.d.ts.map +1 -0
  12. package/dist/contract/queue-store.d.ts +2 -1
  13. package/dist/contract/queue-store.d.ts.map +1 -1
  14. package/dist/contract/storable.d.ts +7 -0
  15. package/dist/contract/storable.d.ts.map +1 -0
  16. package/dist/contract/types.d.ts +52 -0
  17. package/dist/contract/types.d.ts.map +1 -1
  18. package/dist/dsn/build.d.ts +2 -0
  19. package/dist/dsn/build.d.ts.map +1 -1
  20. package/dist/dsn/delivered.d.ts +39 -0
  21. package/dist/dsn/delivered.d.ts.map +1 -0
  22. package/dist/dsn/fields.d.ts +11 -2
  23. package/dist/dsn/fields.d.ts.map +1 -1
  24. package/dist/dsn/human.d.ts +7 -0
  25. package/dist/dsn/human.d.ts.map +1 -0
  26. package/dist/dsn/policy.d.ts +17 -0
  27. package/dist/dsn/policy.d.ts.map +1 -0
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +474 -300
  31. package/dist/index.js.map +20 -15
  32. package/dist/memory/index.js +1 -1
  33. package/dist/memory/index.js.map +1 -1
  34. package/dist/postgres/index.js +17 -9
  35. package/dist/postgres/index.js.map +6 -6
  36. package/dist/postgres/rows.d.ts +6 -1
  37. package/dist/postgres/rows.d.ts.map +1 -1
  38. package/dist/postgres/schema.d.ts.map +1 -1
  39. package/dist/postgres/statements.d.ts +9 -8
  40. package/dist/postgres/statements.d.ts.map +1 -1
  41. package/dist/postgres/store.d.ts.map +1 -1
  42. package/dist/queue/attempt.d.ts +7 -7
  43. package/dist/queue/attempt.d.ts.map +1 -1
  44. package/dist/queue/deliver.d.ts.map +1 -1
  45. package/dist/queue/envelope.d.ts +24 -4
  46. package/dist/queue/envelope.d.ts.map +1 -1
  47. package/dist/queue/events.d.ts +8 -1
  48. package/dist/queue/events.d.ts.map +1 -1
  49. package/dist/queue/fallback-session.d.ts +22 -0
  50. package/dist/queue/fallback-session.d.ts.map +1 -0
  51. package/dist/queue/notify.d.ts +6 -3
  52. package/dist/queue/notify.d.ts.map +1 -1
  53. package/dist/queue/outcome.d.ts.map +1 -1
  54. package/dist/queue/queue.d.ts.map +1 -1
  55. package/dist/queue/route.d.ts +7 -0
  56. package/dist/queue/route.d.ts.map +1 -1
  57. package/dist/queue/session.d.ts +14 -0
  58. package/dist/queue/session.d.ts.map +1 -0
  59. package/dist/queue/settle.d.ts.map +1 -1
  60. package/dist/redis/index.js +26 -13
  61. package/dist/redis/index.js.map +5 -5
  62. package/dist/redis/items.d.ts +2 -0
  63. package/dist/redis/items.d.ts.map +1 -1
  64. package/dist/redis/scripts.d.ts +1 -1
  65. package/dist/redis/scripts.d.ts.map +1 -1
  66. package/dist/redis/store.d.ts.map +1 -1
  67. package/dist/sqlite/index.js +15 -8
  68. package/dist/sqlite/index.js.map +5 -5
  69. package/dist/sqlite/rows.d.ts +2 -1
  70. package/dist/sqlite/rows.d.ts.map +1 -1
  71. package/dist/sqlite/schema.d.ts.map +1 -1
  72. package/dist/sqlite/store.d.ts.map +1 -1
  73. package/docs/guide.md +97 -7
  74. package/docs/roadmap.md +14 -7
  75. package/docs/troubleshooting.md +52 -1
  76. package/package.json +3 -3
  77. package/dist/chunks/index-3pt7rc59.js.map +0 -12
package/docs/guide.md CHANGED
@@ -59,6 +59,12 @@ const item = await queue.enqueue(message, { from: 'mary@example.net', to: ['joe@
59
59
  for the null sender of a bounce you write yourself. `to` is one address
60
60
  or an array. Each recipient is kept once: two addresses that differ
61
61
  only in the case of their domain are one.
62
+ - **The DSN parameters** (RFC 3461) are optional: `ret` (`'FULL'` or
63
+ `'HDRS'`), `envid` (1 to 100 printable ASCII characters, 100 or fewer once xtext-encoded) and
64
+ `recipients`, a list of `{ address, notify?, orcpt? }` matched to `to`
65
+ by address, the shape of `@bumail/smtp`'s `Envelope.recipients`. They
66
+ are kept with the item by every store and described in
67
+ [Delivery status notifications](#delivery-status-notifications).
62
68
  - **What comes back** is the item: an `id`, every recipient `pending`,
63
69
  `nextAttemptAt` now. `start()` wakes at once to deliver it.
64
70
 
@@ -190,6 +196,26 @@ An explicit route is still followed: if the operator sets a smarthost for
190
196
  the domain (`route` or `routes`), a marked recipient is sent there at the
191
197
  next attempt. Only the automatic fallback is blocked.
192
198
 
199
+ The reverse holds too. When the *fallback smarthost's* session ends after
200
+ the final dot with no reply (`afterData`), the smarthost may hold the
201
+ message, and an attempt by the MX would send a second copy. The queue
202
+ records `mayHaveReachedFallback: true` on the recipient (same rules as
203
+ `mayHaveReachedMx`: every recipient the smarthost did not refuse at
204
+ `RCPT`, kept once recorded, never `false`). While a `fallback` is
205
+ configured, every later attempt leaves the MX out for a marked recipient
206
+ and sends it to the smarthost in a session of its own, with no MX session
207
+ at all when every recipient of the domain is marked. If the smarthost
208
+ refuses it, the recipient stays `deferred` (no DSN, since the earlier copy
209
+ may still arrive) with the normal back-off, until `retry.giveUpAfter`
210
+ fails it as `4.4.7` with the usual failed DSN, whose diagnostic adds that
211
+ an earlier attempt may have reached the fallback smarthost. The `deferred`
212
+ and `failed` events carry `mayHaveReachedFallback: true`. If `fallback` is
213
+ no longer configured, a marked recipient goes by MX as before. A recipient
214
+ with both marks (possible only if `fallback` was removed and added again)
215
+ follows `mayHaveReachedMx`: it stays on the MX and never falls back. As for the
216
+ MX mark, items deferred before this version are not backfilled and all
217
+ workers on a store are upgraded together.
218
+
193
219
  Two things to know when upgrading from 0.6. A recipient deferred under
194
220
  0.6 after the final dot carries no mark and is not backfilled: it may
195
221
  still fall back once, as it did. And all the workers on one store are
@@ -343,9 +369,56 @@ Last-Attempt-Date: Thu, 01 Oct 2026 12:00:03 +0000
343
369
  cut after the last whole line within that bound, and every line past
344
370
  998 bytes (RFC 5322 §2.1.1) is cut there, never inside a UTF-8
345
371
  character, so the DSN itself is never refused for a long line.
372
+ - **`NOTIFY`** (RFC 3461 §4.1) filters them, per recipient:
373
+ `'NEVER'` sends none; absent, a failure and a delay, as before; a
374
+ list, only those events (`SUCCESS`, `FAILURE`, `DELAY`). The
375
+ `failed` and `dsn` events still fire; only the DSN is left out.
376
+ - **`SUCCESS`** (§5.2.2): when a recipient asked for it and the next hop
377
+ did not list `DSN` in its EHLO (`SendMailResult.dsnOffered` is `false`),
378
+ the queue sends a `relayed` DSN (`Action: relayed`, status `2.0.0`):
379
+ the message was handed on and no further notice can follow. When the
380
+ next hop offered DSN, it took the parameters and the responsibility, and
381
+ the queue sends none. A `dsn` event of kind `relayed` tells it.
382
+ - **`RET`** (§4.3): `HDRS` returns the header fields only, `FULL` the
383
+ whole message (still bounded by `limits.maxDsnReturn`, headers when it
384
+ is larger); absent, `dsn.returnContent`. It speaks of failure and delay DSNs
385
+ (§4.3): a `relayed` or `delivered` DSN returns the header fields
386
+ whatever `RET` says.
387
+ - **`ENVID` and `ORCPT`** (RFC 3464 §2.2.1, §2.3.1): `Original-Envelope-Id`
388
+ in the per-message fields and `Original-Recipient` (`rfc822; addr`) in
389
+ the recipient's, when the envelope gave them. An `ORCPT` too long for a
390
+ header line (over 900 characters once escaped) is left out of the DSN,
391
+ which is still sent.
392
+ - **Passed on**: `ret`, `envid` and the recipients' `notify` and `orcpt`
393
+ go to `sendMail`, which sends them when the next hop lists `DSN`.
346
394
  - **Its From** is `Mail Delivery System <postmaster@<hostname>>`, or
347
395
  `dsn.from`. It carries `Auto-Submitted: auto-replied` (RFC 3834).
348
396
 
397
+ ### The `delivered` DSN of a local delivery
398
+
399
+ When the application puts a message in a local mailbox itself, it writes
400
+ the `delivered` DSN (`Action: delivered`, RFC 3461 §5.2.3) with
401
+ `deliveredDsn()` and enqueues it from the null sender:
402
+
403
+ ```ts
404
+ import { deliveredDsn } from '@bumail/queue';
405
+
406
+ const dsn = deliveredDsn({
407
+ reportingMta: 'mail.example.net',
408
+ from: 'postmaster@example.net',
409
+ to: envelope.from, // the reverse-path of the delivered message
410
+ arrival: received,
411
+ recipients: [{ address: 'joe@example.net', notify: ['SUCCESS'], orcpt }],
412
+ envid,
413
+ original: message, // optional; without it no third part
414
+ });
415
+ if (dsn) await queue.enqueue(dsn, { from: '', to: envelope.from });
416
+ ```
417
+
418
+ It returns `undefined` when there is nothing to send: the reverse-path is
419
+ null, or no recipient asked for `SUCCESS`. Only the recipients that asked
420
+ are listed.
421
+
349
422
  Nothing from outside reaches a header field raw: a reply's text, a host
350
423
  name and an address have their control characters taken out (CR and LF
351
424
  included), the `message/delivery-status` part is 7-bit (a UTF-8 address
@@ -423,9 +496,9 @@ off(); // stops listening
423
496
  | event | payload |
424
497
  | --- | --- |
425
498
  | `delivered` | `{ id, from, recipient, reply?, attempts, route?, fallback?, mxReply? }`: `route` is `'mx'` or `'smarthost'`, `fallback` is `true` after the [fallback](#falling-back-to-a-smarthost), `mxReply` the MX's reply that led to it |
426
- | `deferred` | the same, and `nextAttemptAt`, and `mayHaveReachedMx: true` when the MX may hold a copy ([why](#falling-back-to-a-smarthost)) |
427
- | `failed` | the same as `delivered`, and `mayHaveReachedMx: true` as for `deferred` |
428
- | `dsn` | `{ kind: 'delayed' \| 'failed', id, of, to, recipients }`: `id` is the DSN's own item, `of` the item it reports on |
499
+ | `deferred` | the same, and `nextAttemptAt`, and `mayHaveReachedMx: true` when the MX may hold a copy, `mayHaveReachedFallback: true` when the fallback smarthost may ([why](#falling-back-to-a-smarthost)) |
500
+ | `failed` | the same as `delivered`, and `mayHaveReachedMx` and `mayHaveReachedFallback` as for `deferred` |
501
+ | `dsn` | `{ kind: 'delayed' \| 'failed' \| 'relayed', id, of, to, recipients }`: `id` is the DSN's own item, `of` the item it reports on |
429
502
  | `error` | `{ error, id? }`: a store that failed (a lease renewal included), a DSN that could not be enqueued, a lease lost (`LEASE_LOST`), a message the store lost (`MESSAGE_UNREADABLE`), a route `sendMail` refused (`INVALID_OPTION`) |
430
503
 
431
504
  Events about an outcome come once it is recorded, in this order: a
@@ -600,7 +673,17 @@ GRANT SELECT, INSERT, UPDATE, DELETE
600
673
  ```
601
674
 
602
675
  After an upgrade that adds a migration, run that step again before the
603
- workers start: theirs cannot make the change.
676
+ workers start: theirs cannot make the change. The second migration adds
677
+ one nullable `dsn jsonb` column to the items table (the item's `RET` and
678
+ `ENVID`); existing rows keep `NULL` and are delivered as before. A
679
+ worker of the previous version *started* after the migration is refused
680
+ (`schema version 2, newer than this store's 1`); one *still running*
681
+ keeps claiming items, ignores `NOTIFY`, `RET` and `ENVID`, and drops the
682
+ recipients' `notify` and `orcpt` when it completes an attempt. So stop
683
+ every old worker first, then start the new ones. The same holds for the
684
+ SQLite store, which migrates itself on open, and for Redis, which needs
685
+ no migration (an item's hash gains `ret` and `envid` fields) but where an
686
+ old worker drops `notify` and `orcpt` just alike.
604
687
 
605
688
  ### Several instances and leases
606
689
 
@@ -833,6 +916,7 @@ const send: Sender = async (_message, options) => {
833
916
  port: 25,
834
917
  tls: false,
835
918
  authenticated: false,
919
+ dsnOffered: true,
836
920
  };
837
921
  };
838
922
  const queue = createQueue({
@@ -878,13 +962,19 @@ codes, and keeps its promises:
878
962
  of the lease, in one step and only for the lease's owner; a final
879
963
  status is never changed; an item whose every recipient is final is
880
964
  dropped with its message.
881
- - `complete` records a recipient's `mayHaveReachedMx: true` (on its
882
- `RecipientUpdate`) and keeps it: `get`, `list` and `claim` return it on
965
+ - `complete` records a recipient's `mayHaveReachedMx: true` and
966
+ `mayHaveReachedFallback: true` (on its `RecipientUpdate`) and keeps
967
+ them: `get`, `list` and `claim` return it on
883
968
  the `RecipientState`, a later outcome without it never clears it, and
884
969
  the store never writes `false`. Without it the queue falls back to a
885
970
  smarthost for a recipient the MX may already hold. A 0.6 worker drops
886
971
  the mark when it completes an attempt, so upgrade every worker on a
887
972
  store together.
973
+ - `add` keeps the item's `ret` and `envid` and each recipient's `notify`
974
+ and `orcpt` (`NewQueueItem`), and `get`, `list`, `claim` and `complete`
975
+ return them unchanged, even after an outcome. The contract's
976
+ `checkNewItem` refuses a bad one as `INVALID`. An item stored before
977
+ they existed has none of them and is delivered as before.
888
978
  - Times are given by the caller; the store keeps no clock.
889
979
  - Nothing it returns is shared with what it keeps.
890
980
  - `reschedule` with an owner refuses an empty owner as `INVALID`, even
@@ -928,5 +1018,5 @@ them yet.
928
1018
  | `pollInterval` | `number`, to 2147483647 | 5000 | ms between passes of `start()` |
929
1019
  | `owner` | `string` | a random UUID | this worker's name on its leases |
930
1020
  | `clock` | `{ now(): number }` | `Date.now` | the time |
931
- | `send` | `Sender` | `sendMail` | delivers one session |
1021
+ | `send` | `Sender` | `sendMail` | delivers one session; a custom one should return `dsnOffered` (leave it out and no `relayed` DSN is ever sent) |
932
1022
  | `random` | `() => number` | `Math.random` | the jitter |
package/docs/roadmap.md CHANGED
@@ -6,7 +6,7 @@ only number on it.
6
6
 
7
7
  ## Now
8
8
 
9
- Nothing in progress.
9
+ Nothing is under way besides what is marked shipped below.
10
10
 
11
11
  ## Next
12
12
 
@@ -20,10 +20,6 @@ Nothing in progress.
20
20
  connections.
21
21
  - **The contract's specs, exported**, so a store written outside this
22
22
  package can hold itself to them.
23
- - **The reverse doubt** — a recipient whose session by the smarthost
24
- ended after the final dot with no reply is not yet marked either: the
25
- queue could keep that doubt too, and not send it again by MX (issue
26
- #144).
27
23
  - **A DSN that cannot be lost on a crash** — today a crash between
28
24
  recording a failure and enqueuing its DSN loses that DSN; the store
29
25
  will take both in one step.
@@ -32,8 +28,6 @@ Nothing in progress.
32
28
 
33
29
  - **REQUIRETLS (RFC 8689)** — a message that asks for TLS all the way is
34
30
  never sent in clear, and fails rather than downgrade.
35
- - **DSN requests (RFC 3461)** — `NOTIFY`, `RET` and `ENVID` given at
36
- enqueue and passed on, with a "delivered" DSN when asked for.
37
31
  - **Retries per recipient** — a schedule of its own for each recipient,
38
32
  rather than one per message.
39
33
  - **A history** — delivered and failed items kept for a while, for an
@@ -57,11 +51,24 @@ Nothing in progress.
57
51
 
58
52
  ### Unreleased
59
53
 
54
+ - **DSN requests (RFC 3461)**: the queue honours a sender's `NOTIFY`, `RET`,
55
+ `ENVID` and `ORCPT` given at enqueue. `NOTIFY=NEVER` silences failure
56
+ DSNs; a message handed to a next hop without DSN is reported with a
57
+ `relayed` DSN; DSNs carry `Original-Envelope-Id` and
58
+ `Original-Recipient`; and `deliveredDsn()` builds the `delivered` DSN for
59
+ a local delivery. The SQLite and PostgreSQL stores run migration 2: stop
60
+ every old worker before starting the new ones.
60
61
  - **The MX's doubt is remembered** — a recipient whose session by MX ended
61
62
  after the final dot with no reply is marked `mayHaveReachedMx`, kept by
62
63
  every store: no later attempt falls back to the smarthost on its own, it
63
64
  stays on the MX until `retry.giveUpAfter` (a `4.4.7` DSN that says so),
64
65
  and the `deferred` and `failed` events carry the mark.
66
+ - **The fallback smarthost's doubt is remembered** — a recipient whose
67
+ session by the fallback smarthost ended after the final dot with no reply
68
+ is marked `mayHaveReachedFallback`, kept by every store: later attempts
69
+ skip the MX for it and go to the smarthost again, until
70
+ `retry.giveUpAfter` (a `4.4.7` DSN that says so), and the `deferred` and
71
+ `failed` events carry the mark.
65
72
  - **Only the recipients the MX did not refuse are in doubt** — in a session that
66
73
  failed after the final dot, a recipient the MX refused at `RCPT` is
67
74
  settled from its own reply (5xx failed, 4xx deferred) and not marked, so
@@ -11,6 +11,7 @@ parts shown as … vary.
11
11
  - [`QueueError: The queue holds … items, its limit`](#queueerror-the-queue-holds--items-its-limit)
12
12
  - [`QueueError: … must be an address, local@domain, not "…"`](#queueerror--must-be-an-address-localdomain-not-)
13
13
  - [`QueueError: to must hold one recipient or more`](#queueerror-to-must-hold-one-recipient-or-more)
14
+ - [`QueueError: ret must be 'FULL' or 'HDRS'`, and the other DSN parameters](#queueerror-ret-must-be-full-or-hdrs-and-the-other-dsn-parameters)
14
15
  - [`QueueError: The envelope is an object: { from, to }`](#queueerror-the-envelope-is-an-object--from-to-)
15
16
  - [`QueueError: The message is a Uint8Array, a string or a ReadableStream<Uint8Array>`](#queueerror-the-message-is-a-uint8array-a-string-or-a-readablestreamuint8array)
16
17
  - [`QueueError: The message stream must give Uint8Array chunks`](#queueerror-the-message-stream-must-give-uint8array-chunks)
@@ -49,6 +50,7 @@ parts shown as … vary.
49
50
  - [`QueueError: A SQLite queue store needs a directory`](#queueerror-a-sqlite-queue-store-needs-a-directory)
50
51
  - [`QueueError: The queue at "…" cannot be opened: …`](#queueerror-the-queue-at--cannot-be-opened-)
51
52
  - [`QueueError: The database is at schema version …, newer than this store's …`](#queueerror-the-database-is-at-schema-version--newer-than-this-stores-)
53
+ - [`QueueError: The database is at schema version 2, newer than this store's 1`](#queueerror-the-database-is-at-schema-version-2-newer-than-this-stores-1)
52
54
  - [`QueueError: busyTimeout must be an integer of at least 0, not …`](#queueerror-busytimeout-must-be-an-integer-of-at-least-0-not-)
53
55
  - [`QueueError: The queue store is closed`](#queueerror-the-queue-store-is-closed)
54
56
 
@@ -148,6 +150,36 @@ For a bounce of your own, the null sender is `from: ''`.
148
150
  **Code:** `INVALID`. `to` is an empty array, or not an array nor a
149
151
  string. Give one address or more.
150
152
 
153
+ ### `QueueError: ret must be 'FULL' or 'HDRS'`, and the other DSN parameters
154
+
155
+ **Code:** `INVALID`. The envelope's DSN parameters (RFC 3461) are
156
+ checked at `enqueue`, and by every store at `add`, never at delivery:
157
+
158
+ - `ret must be 'FULL' or 'HDRS'`: `RET` is one of the two.
159
+ - `envid must be 1 to 100 printable ASCII characters, at most 100 once encoded as xtext`: the decoded
160
+ `ENVID`, with no control character, whose xtext is 100 characters or fewer
161
+ (RFC 3461 §4.4).
162
+ - `recipients names "…", which is not in to` and `recipients names … twice`:
163
+ each entry of `recipients` matches one address of `to`, once.
164
+ - `notify of … must be 'NEVER' or a list of 'SUCCESS', 'FAILURE', 'DELAY', each once`:
165
+ `NEVER` alone, or a non-empty list without repeats.
166
+ - `orcpt of … must be { type, address }`: both strings, the address not
167
+ empty, or the type is not a token (letters, digits, hyphens).
168
+ - `orcpt of … is over 500 characters once encoded`: `type;` and the xtext of
169
+ the address take at most 500 characters (RFC 3461 §4.2).
170
+
171
+ ```ts
172
+ await queue.enqueue(message, {
173
+ from: 'mary@example.net',
174
+ to: ['joe@example.com'],
175
+ ret: 'HDRS',
176
+ envid: 'QQ314159',
177
+ recipients: [{ address: 'joe@example.com', notify: ['SUCCESS', 'FAILURE'] }],
178
+ });
179
+ ```
180
+
181
+ The SMTP server's `Envelope.recipients` is already in this shape.
182
+
151
183
  ### `QueueError: The envelope is an object: { from, to }`
152
184
 
153
185
  **Code:** `INVALID`. `enqueue`'s second argument is missing.
@@ -372,7 +404,8 @@ recipient's: the recipients are deferred as `4.3.5` (system incorrectly
372
404
  configured), not failed, so no DSN goes out for it until
373
405
  `retry.giveUpAfter` (5 days by default), when they fail as `4.4.7` with a
374
406
  DSN like any deferred recipient (for a recipient marked `mayHaveReachedMx`,
375
- the failed DSN's diagnostic adds that the message may have been delivered;
407
+ the failed DSN's diagnostic adds that the message may have been delivered,
408
+ and likewise for one marked `mayHaveReachedFallback`;
376
409
  see [a recipient stays `deferred` with `4.4.1` or `4.4.2`](#a-recipient-stays-deferred-with-441-or-442)).
377
410
  **Fix:** read the message, fix the option, restart: the deferred items
378
411
  go out on their next attempt, or at once with `retryNow`.
@@ -469,6 +502,15 @@ should read the queue.
469
502
  prefix's keys, whose layout version is `<prefix>schema`): upgrade this
470
503
  one.
471
504
 
505
+ ### `QueueError: The database is at schema version 2, newer than this store's 1`
506
+
507
+ **Code:** `INVALID`. A worker of a version before the DSN parameters was
508
+ started on a database that a newer one migrated (SQLite and PostgreSQL;
509
+ the second migration adds the `dsn` column). Upgrade it. When rolling
510
+ out, stop every old worker first: one still running is not refused, but
511
+ it ignores `NOTIFY`, `RET` and `ENVID` and drops the recipients' `notify`
512
+ and `orcpt` from the items it completes.
513
+
472
514
  ### `QueueError: busyTimeout must be an integer of at least 0, not …`
473
515
 
474
516
  **Code:** `INVALID`. Milliseconds a write waits for another process.
@@ -742,6 +784,15 @@ A `4.4.x` that never falls back to the smarthost you configured is that
742
784
  mark. Wait for the MX, or cancel the item; to send it by the smarthost
743
785
  anyway, accepting a possible second copy, route the domain to it
744
786
  explicitly (`routes`).
787
+
788
+ A `4.4.x` recipient that goes to the smarthost again and never to the MX is
789
+ the reverse case: the `fallback` smarthost's own session ended after the final
790
+ dot, so the recipient is marked `mayHaveReachedFallback: true` and the next
791
+ attempts skip the MX (a refusal by the smarthost keeps it `deferred`, with no
792
+ DSN, until `retry.giveUpAfter` fails it as `4.4.7`; the DSN says the fallback
793
+ smarthost may have delivered it). If you removed `fallback` meanwhile, it goes
794
+ by MX again.
795
+
745
796
  **Fix:** route through a smarthost on port 587 or 465:
746
797
 
747
798
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bumail/queue",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "The outbound mail queue of a mail server: every recipient's state, retries with back-off, delivery status notifications (RFC 3464), leases for several workers, and a memory, a bun:sqlite, a PostgreSQL (Bun.sql) and a Redis (Bun.redis) store, delivering through @bumail/smtp/client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -67,12 +67,12 @@
67
67
  "devDependencies": {
68
68
  "@bumail/dns": "^0.2.1",
69
69
  "@bumail/mime": "^0.2.0",
70
- "@bumail/smtp": "^0.9.0",
70
+ "@bumail/smtp": "^0.10.0",
71
71
  "@types/bun": "^1.4.2"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "@bumail/mime": "^0.2.0",
75
- "@bumail/smtp": "^0.9.0",
75
+ "@bumail/smtp": "^0.10.0",
76
76
  "typescript": "^6.0.3 || ^7.0.0"
77
77
  },
78
78
  "peerDependenciesMeta": {
@@ -1,12 +0,0 @@
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 * `MESSAGE_UNREADABLE`: given to the `error` event when the store holds an\n * item but not its message, whose pending recipients then fail.\n */\nexport type QueueErrorCode =\n\t| 'INVALID'\n\t| 'MESSAGE_TOO_BIG'\n\t| 'TOO_MANY_RECIPIENTS'\n\t| 'QUEUE_FULL'\n\t| 'CLOSED'\n\t| 'LEASE_LOST'\n\t| 'MESSAGE_UNREADABLE';\n\n/** Thrown by the queue and by every store, whichever answers the contract, with the same codes. */\nexport class QueueError extends Error {\n\toverride readonly name = 'QueueError';\n\treadonly code: QueueErrorCode;\n\n\tconstructor(code: QueueErrorCode, message: string) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n}\n\n/** `INVALID`, for an option or an argument. */\nexport const invalid = (message: string) => new QueueError('INVALID', message);\n",
6
- "/** 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 mark = update.mayHaveReachedMx as unknown;\n\tif (mark !== undefined && mark !== true) {\n\t\tthrow invalid('mayHaveReachedMx must be true or absent');\n\t}\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\t...(r.mayHaveReachedMx || update.mayHaveReachedMx\n\t\t\t\t? { mayHaveReachedMx: true as const }\n\t\t\t\t: {}),\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": ";AAoBO,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;;;AC5B7E,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,OAAO,OAAO;AAAA,EACpB,IAAI,SAAS,aAAa,SAAS,MAAM;AAAA,IACxC,MAAM,QAAQ,yCAAyC;AAAA,EACxD;AAAA,EACA,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,SACd,EAAE,oBAAoB,OAAO,mBAC9B,EAAE,kBAAkB,KAAc,IAClC,CAAC;AAAA,IACL;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": "8430FF293E3942D164756E2164756E21",
11
- "names": []
12
- }