@bumail/queue 0.2.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 +40 -6
- package/dist/chunks/index-vg971acr.js.map +2 -2
- package/dist/chunks/index-y3n1w2az.js +18 -0
- package/dist/chunks/index-y3n1w2az.js.map +10 -0
- package/dist/errors.d.ts +2 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +73 -23
- package/dist/index.js.map +6 -5
- package/dist/masked.d.ts +7 -0
- package/dist/masked.d.ts.map +1 -0
- package/dist/postgres/connect.d.ts.map +1 -1
- package/dist/postgres/index.js +4 -13
- package/dist/postgres/index.js.map +3 -3
- 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/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/docs/README.md +1 -1
- package/docs/guide.md +185 -6
- package/docs/roadmap.md +30 -7
- package/docs/troubleshooting.md +229 -13
- package/package.json +9 -3
package/docs/troubleshooting.md
CHANGED
|
@@ -34,9 +34,12 @@ parts shown as … vary.
|
|
|
34
34
|
|
|
35
35
|
- [`QueueError: The lease on … was lost before its outcome was recorded; another worker will try it again`](#queueerror-the-lease-on--was-lost-before-its-outcome-was-recorded-another-worker-will-try-it-again)
|
|
36
36
|
- [`QueueError: The lease on … was lost while it was delivered`](#queueerror-the-lease-on--was-lost-while-it-was-delivered)
|
|
37
|
+
- [`QueueError: The lease on … 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`](#queueerror-the-lease-on--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)
|
|
38
|
+
- [`QueueError: The lease on … 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`](#queueerror-the-lease-on--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)
|
|
37
39
|
- [`SmtpError: sendMail(): …` (`INVALID_OPTION`), with recipients deferred as `4.3.5`](#smtperror-sendmail--invalid_option-with-recipients-deferred-as-435)
|
|
38
40
|
- [`SQLiteError: database is locked`, or another store error, during a delivery](#sqliteerror-database-is-locked-or-another-store-error-during-a-delivery)
|
|
39
41
|
- [`PostgresError: …`, or a connection error, during a delivery](#postgreserror--or-a-connection-error-during-a-delivery)
|
|
42
|
+
- [`RedisError: …` during a delivery: `Connection closed`, `OOM command not allowed …`, `READONLY …`](#rediserror--during-a-delivery-connection-closed-oom-command-not-allowed--readonly-)
|
|
40
43
|
|
|
41
44
|
**The `bun:sqlite` store** (`@bumail/queue/sqlite`)
|
|
42
45
|
|
|
@@ -55,6 +58,16 @@ parts shown as … vary.
|
|
|
55
58
|
- [`QueueError: The PostgreSQL queue cannot be set up: …`](#queueerror-the-postgresql-queue-cannot-be-set-up-)
|
|
56
59
|
- [`QueueError: The database is at schema version …, newer than this store's …`](#queueerror-the-database-is-at-schema-version--newer-than-this-stores-), and [`QueueError: The queue store is closed`](#queueerror-the-queue-store-is-closed), as for `bun:sqlite`
|
|
57
60
|
|
|
61
|
+
**The Redis store** (`@bumail/queue/redis`)
|
|
62
|
+
|
|
63
|
+
- [`QueueError: A Redis queue store needs client, a Bun.RedisClient, or url, a redis:// URL`](#queueerror-a-redis-queue-store-needs-client-a-bunredisclient-or-url-a-redis-url)
|
|
64
|
+
- [`QueueError: A Redis queue store takes client or url, not both`](#queueerror-a-redis-queue-store-takes-client-or-url-not-both)
|
|
65
|
+
- [`QueueError: keyPrefix must be lowercase letters, digits, '_', ':', '.' and '-', starting with a letter, at most 40 characters, not …`](#queueerror-keyprefix-must-be-lowercase-letters-digits-_---and---starting-with-a-letter-at-most-40-characters-not-)
|
|
66
|
+
- [`QueueError: The URL in url cannot be opened: …`](#queueerror-the-url-in-url-cannot-be-opened-)
|
|
67
|
+
- [`QueueError: The Redis queue cannot be set up: …`](#queueerror-the-redis-queue-cannot-be-set-up-)
|
|
68
|
+
- [`QueueError: The key …schema does not hold a layout version: is the prefix another application's?`](#queueerror-the-key-schema-does-not-hold-a-layout-version-is-the-prefix-another-applications)
|
|
69
|
+
- [`QueueError: The database is at schema version …, newer than this store's …`](#queueerror-the-database-is-at-schema-version--newer-than-this-stores-), and [`QueueError: The queue store is closed`](#queueerror-the-queue-store-is-closed), as for `bun:sqlite`
|
|
70
|
+
|
|
58
71
|
**Listing** (`list`)
|
|
59
72
|
|
|
60
73
|
- [`QueueError: limit must be an integer from 1 to 1000, not …`](#queueerror-limit-must-be-an-integer-from-1-to-1000-not-)
|
|
@@ -63,6 +76,10 @@ parts shown as … vary.
|
|
|
63
76
|
**A store's own checks** (a store of your own, called directly)
|
|
64
77
|
|
|
65
78
|
- [`QueueError: … must be a finite number`, `owner must be a non-empty string`, and the others](#a-stores-own-checks)
|
|
79
|
+
- [`QueueError: A reply is an object with a text`](#queueerror-a-reply-is-an-object-with-a-text)
|
|
80
|
+
- [`QueueError: reply.status must be a string`](#queueerror-replystatus-must-be-a-string)
|
|
81
|
+
- [`QueueError: reply.host must be a string`](#queueerror-replyhost-must-be-a-string)
|
|
82
|
+
- [`QueueError: … holds a NUL or a lone surrogate, which a store cannot keep`](#queueerror--holds-a-nul-or-a-lone-surrogate-which-a-store-cannot-keep)
|
|
66
83
|
|
|
67
84
|
**Delivery**
|
|
68
85
|
|
|
@@ -238,8 +255,9 @@ or `… must be a number from … to …, not …`. A number option out of its r
|
|
|
238
255
|
|
|
239
256
|
### `QueueError: owner must be a non-empty string`
|
|
240
257
|
|
|
241
|
-
**Code:** `INVALID`, also as
|
|
242
|
-
|
|
258
|
+
**Code:** `INVALID`, also as
|
|
259
|
+
[`owner holds a NUL or a lone surrogate, which a store cannot keep`](#queueerror--holds-a-nul-or-a-lone-surrogate-which-a-store-cannot-keep).
|
|
260
|
+
Leave `owner` out for a random one, or give
|
|
243
261
|
each worker its own name.
|
|
244
262
|
|
|
245
263
|
## The `error` event
|
|
@@ -256,8 +274,54 @@ the process stalled longer than that, or the store could not be reached.
|
|
|
256
274
|
|
|
257
275
|
### `QueueError: The lease on … was lost while it was delivered`
|
|
258
276
|
|
|
259
|
-
**Code:** `LEASE_LOST`. A renewal
|
|
260
|
-
|
|
277
|
+
**Code:** `LEASE_LOST`. A renewal was refused while the item was still in
|
|
278
|
+
the store, held by another worker or by none (another worker took it and
|
|
279
|
+
let go of it): the same cause as above, seen sooner. A renewal that finds the item gone
|
|
280
|
+
says nothing: the outcome, once the sessions end, tells a cancel from a
|
|
281
|
+
lease lost (below).
|
|
282
|
+
|
|
283
|
+
### `QueueError: The lease on … 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`
|
|
284
|
+
|
|
285
|
+
**Code:** `LEASE_LOST`.
|
|
286
|
+
**When:** a delivery ran past its lease's expiry — no renewal reached the
|
|
287
|
+
store in time — and, when it tried to record its outcome, the item was no
|
|
288
|
+
longer in the store. No `delivered`, `deferred` or `failed` event and no
|
|
289
|
+
DSN come from this attempt.
|
|
290
|
+
**Why:** past its expiry, another worker may claim the item, deliver it
|
|
291
|
+
and drop it; but a `cancel` drops it too, and the store keeps no record of
|
|
292
|
+
which happened. The worker cannot tell them apart, so it says both rather
|
|
293
|
+
than report outcomes nobody recorded. When another worker took it, the
|
|
294
|
+
recipients both reached got the message twice; that worker's events tell
|
|
295
|
+
what it delivered.
|
|
296
|
+
**Fix:** as for the lease lost above: a longer `leaseMs`, or find what
|
|
297
|
+
stalled the process — an event loop blocked, a store out of reach. A
|
|
298
|
+
cancel under a lease that still held is told as before: its outcomes on
|
|
299
|
+
the events, with no error and no DSN.
|
|
300
|
+
|
|
301
|
+
When the instances' clocks are out of step, one whose clock runs ahead
|
|
302
|
+
can claim the item while this worker's own clock says the lease still
|
|
303
|
+
holds. If it finishes and drops the item between two of this worker's
|
|
304
|
+
renewals, no renewal saw it taken and the expiry has not passed by this
|
|
305
|
+
clock: the attempt reads as a cancel, its outcomes told on the events
|
|
306
|
+
with no error. Keep the machines on NTP; the 10-minute `leaseMs` leaves
|
|
307
|
+
room for seconds of skew.
|
|
308
|
+
|
|
309
|
+
### `QueueError: The lease on … 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`
|
|
310
|
+
|
|
311
|
+
**Code:** `LEASE_LOST`, after
|
|
312
|
+
[`The lease on … was lost while it was delivered`](#queueerror-the-lease-on--was-lost-while-it-was-delivered).
|
|
313
|
+
**When:** a renewal was refused while the item was still in the store,
|
|
314
|
+
held by another worker or by none, and when this worker tried to record
|
|
315
|
+
its outcome the item was gone. No outcome event and no DSN come from this
|
|
316
|
+
attempt.
|
|
317
|
+
**Why:** another worker claimed the item once this one's lease had
|
|
318
|
+
expired by its own clock — this worker stalled, or the instances' clocks
|
|
319
|
+
disagree (an instance whose clock runs ahead sees leases expire early) —
|
|
320
|
+
and has most likely finished and dropped it, the recipients both reached
|
|
321
|
+
getting the message twice. A `cancel` after the renewal drops it too, and
|
|
322
|
+
the store keeps no record of which happened, so the message names both.
|
|
323
|
+
**Fix:** a longer `leaseMs`, find what stalled the process, and keep the
|
|
324
|
+
machines on NTP.
|
|
261
325
|
|
|
262
326
|
### `SmtpError: sendMail(): …` (`INVALID_OPTION`), with recipients deferred as `4.3.5`
|
|
263
327
|
|
|
@@ -298,6 +362,31 @@ its lease expires — its recipients may then get the message twice.
|
|
|
298
362
|
too many connections, give each instance's client a smaller `max` (see
|
|
299
363
|
the guide's Pool size) or raise `max_connections`.
|
|
300
364
|
|
|
365
|
+
### `RedisError: …` during a delivery: `Connection closed`, `OOM command not allowed …`, `READONLY …`
|
|
366
|
+
|
|
367
|
+
**When:** the `error` event, with the item's `id`, while it is delivered,
|
|
368
|
+
on `@bumail/queue/redis`. A call of yours — `enqueue`, `list`, `get`,
|
|
369
|
+
`retryNow`, `cancel` — rejects with the same `RedisError` for the same
|
|
370
|
+
reasons: an `enqueue` refused with `OOM …` kept nothing.
|
|
371
|
+
**Why:** a lease renewal failed. `Connection closed`: Redis restarted,
|
|
372
|
+
or the connection dropped, while the command was on its way; a command
|
|
373
|
+
sent while the client is reconnecting waits instead. `Max reconnection
|
|
374
|
+
attempts reached` or `Connection has failed`: Redis is out of reach, and
|
|
375
|
+
`Bun.RedisClient` gave up reconnecting (20 tries by default). `OOM
|
|
376
|
+
command not allowed when used memory > 'maxmemory'`: Redis is full and
|
|
377
|
+
refuses writes. `READONLY You can't write against a read only replica`:
|
|
378
|
+
the client reaches a replica, or a primary that was demoted by a
|
|
379
|
+
failover. As on PostgreSQL, the next renewal tries again, only a lease
|
|
380
|
+
taken by another instance stops them, a claim that fails rejects
|
|
381
|
+
`deliverDue()` or is told on `error` under `start()`, and an outcome
|
|
382
|
+
that cannot be recorded leaves the item to be claimed again once its
|
|
383
|
+
lease expires — its recipients may then get the message twice.
|
|
384
|
+
**Fix:** check Redis is up and that the URL names the primary (through
|
|
385
|
+
your provider's endpoint or Sentinel's, which follows a failover). For
|
|
386
|
+
`OOM`, give Redis more `maxmemory`, or drain the queue; keep
|
|
387
|
+
`maxmemory-policy noeviction` (the guide's [Redis](guide.md#redis)), or
|
|
388
|
+
Redis drops queue items to make room instead of refusing.
|
|
389
|
+
|
|
301
390
|
## The `bun:sqlite` store
|
|
302
391
|
|
|
303
392
|
### `QueueError: A SQLite queue store needs a directory`
|
|
@@ -315,7 +404,9 @@ should read the queue.
|
|
|
315
404
|
### `QueueError: The database is at schema version …, newer than this store's …`
|
|
316
405
|
|
|
317
406
|
**Code:** `INVALID`. A newer version of the package wrote the database
|
|
318
|
-
(`queue.sqlite`,
|
|
407
|
+
(`queue.sqlite`, on PostgreSQL the prefix's tables, on Redis the
|
|
408
|
+
prefix's keys, whose layout version is `<prefix>schema`): upgrade this
|
|
409
|
+
one.
|
|
319
410
|
|
|
320
411
|
### `QueueError: busyTimeout must be an integer of at least 0, not …`
|
|
321
412
|
|
|
@@ -324,8 +415,8 @@ should read the queue.
|
|
|
324
415
|
### `QueueError: The queue store is closed`
|
|
325
416
|
|
|
326
417
|
**Code:** `CLOSED`. The store was used after `close()`: call
|
|
327
|
-
`queue.stop()` first, then `store.close()`. The PostgreSQL
|
|
328
|
-
same.
|
|
418
|
+
`queue.stop()` first, then `store.close()`. The PostgreSQL and Redis
|
|
419
|
+
stores say the same.
|
|
329
420
|
|
|
330
421
|
## The PostgreSQL store
|
|
331
422
|
|
|
@@ -387,6 +478,75 @@ their narrower role (the guide's [Migrations](guide.md#migrations)); or
|
|
|
387
478
|
give the role `CREATE` on the schema. The next call tries again: nothing is left half made,
|
|
388
479
|
since a migration is one transaction.
|
|
389
480
|
|
|
481
|
+
## The Redis store
|
|
482
|
+
|
|
483
|
+
### `QueueError: A Redis queue store needs client, a Bun.RedisClient, or url, a redis:// URL`
|
|
484
|
+
|
|
485
|
+
**Code:** `INVALID`.
|
|
486
|
+
**When:** `RedisQueueStore.open` with neither `client` nor `url`, a
|
|
487
|
+
`url` that is not a URL of a scheme `Bun.RedisClient` takes (`redis://`,
|
|
488
|
+
`rediss://`, `valkey://`, `valkeys://`, `redis+tls://`, `redis+unix://`,
|
|
489
|
+
`redis+tls+unix://`), or a `client` without `send`, `getBuffer` and
|
|
490
|
+
`close`. The message never repeats the URL, which may hold a password.
|
|
491
|
+
**Fix:**
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
RedisQueueStore.open({ url: 'rediss://bumail:secret@cache.internal:6380/0' });
|
|
495
|
+
// or a client of yours, configured as you need:
|
|
496
|
+
RedisQueueStore.open({ client: new Bun.RedisClient(url, { connectionTimeout: 5000 }) });
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### `QueueError: A Redis queue store takes client or url, not both`
|
|
500
|
+
|
|
501
|
+
**Code:** `INVALID`. Give one: `client` for a client you configure and
|
|
502
|
+
close, `url` for one the store opens and closes.
|
|
503
|
+
|
|
504
|
+
### `QueueError: keyPrefix must be lowercase letters, digits, '_', ':', '.' and '-', starting with a letter, at most 40 characters, not …`
|
|
505
|
+
|
|
506
|
+
**Code:** `INVALID`.
|
|
507
|
+
**Why:** the prefix starts every key the store writes. Anything Redis or
|
|
508
|
+
a `SCAN MATCH` reads specially is refused: `{` (a Cluster hash tag), `*`,
|
|
509
|
+
`?`, `[` and `\` (glob characters), spaces and control characters.
|
|
510
|
+
**Fix:** `keyPrefix: 'mail:queue:'`. Give each queue in one Redis
|
|
511
|
+
database its own prefix.
|
|
512
|
+
|
|
513
|
+
### `QueueError: The URL in url cannot be opened: …`
|
|
514
|
+
|
|
515
|
+
**Code:** `INVALID`.
|
|
516
|
+
**When:** `RedisQueueStore.open` with a URL `Bun.RedisClient` refuses
|
|
517
|
+
before connecting, such as a database that is not a number (`Invalid
|
|
518
|
+
database number in Redis URL: "…"`). The URL is never repeated, and the
|
|
519
|
+
password is masked should the reason name it.
|
|
520
|
+
**Fix:** correct the URL: the database is the path, `/0` to `/15` on a
|
|
521
|
+
default Redis.
|
|
522
|
+
|
|
523
|
+
### `QueueError: The Redis queue cannot be set up: …`
|
|
524
|
+
|
|
525
|
+
**Code:** `INVALID`.
|
|
526
|
+
**When:** the first call on a store, which reads the layout version of
|
|
527
|
+
its keys (`<prefix>schema`), writing it on a new queue. The rest of the
|
|
528
|
+
message is Redis's or Bun's: `Max reconnection attempts reached` (out of
|
|
529
|
+
reach), `WRONGPASS invalid username-password pair or user is disabled.`,
|
|
530
|
+
`ERR DB index is out of range`, `NOPERM …` (an ACL user without
|
|
531
|
+
`EVALSHA`, `EVAL` or the keys).
|
|
532
|
+
**Why:** the server is out of reach, the credentials or the database are
|
|
533
|
+
wrong, or the user may not run scripts. With `Bun.RedisClient`'s
|
|
534
|
+
defaults an unreachable server takes about half a minute of reconnecting
|
|
535
|
+
before the call fails.
|
|
536
|
+
**Fix:** check the URL and that Redis answers. An ACL user needs the
|
|
537
|
+
commands the store sends and those its scripts call, on the keys under
|
|
538
|
+
its prefix, and nothing else (the guide's [Redis](guide.md#redis) has
|
|
539
|
+
the `ACL SETUSER` line). The next call tries again.
|
|
540
|
+
|
|
541
|
+
### `QueueError: The key …schema does not hold a layout version: is the prefix another application's?`
|
|
542
|
+
|
|
543
|
+
**Code:** `INVALID`.
|
|
544
|
+
**When:** the first call on a store whose `<prefix>schema` key holds
|
|
545
|
+
something other than a whole number of at least 1.
|
|
546
|
+
**Why:** the store keeps its layout version there; anything else means
|
|
547
|
+
another application, or a hand, wrote under the same prefix.
|
|
548
|
+
**Fix:** give the queue a `keyPrefix` of its own. Nothing was written.
|
|
549
|
+
|
|
390
550
|
## Listing
|
|
391
551
|
|
|
392
552
|
### `QueueError: limit must be an integer from 1 to 1000, not …`
|
|
@@ -425,15 +585,71 @@ never gives it. Each message names what is wrong:
|
|
|
425
585
|
- `QueueError: delayNotified must be true or false`
|
|
426
586
|
- `QueueError: recipients must be an array of { address, status:
|
|
427
587
|
delivered, deferred or failed }`
|
|
428
|
-
- `QueueError: A reply is an object with a text
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
`
|
|
433
|
-
them; the queue's own replies never hold one
|
|
588
|
+
- [`QueueError: A reply is an object with a text`](#queueerror-a-reply-is-an-object-with-a-text),
|
|
589
|
+
[`reply.status must be a string`](#queueerror-replystatus-must-be-a-string),
|
|
590
|
+
[`reply.host must be a string`](#queueerror-replyhost-must-be-a-string)
|
|
591
|
+
- [`QueueError: … holds a NUL or a lone surrogate, which a store cannot
|
|
592
|
+
keep`](#queueerror--holds-a-nul-or-a-lone-surrogate-which-a-store-cannot-keep)
|
|
434
593
|
|
|
435
594
|
A list's `offset` and `limit` are checked as under [Listing](#listing).
|
|
436
595
|
|
|
596
|
+
### `QueueError: A reply is an object with a text`
|
|
597
|
+
|
|
598
|
+
**Code:** `INVALID`.
|
|
599
|
+
**When:** `complete` called directly with a recipient whose `reply` is
|
|
600
|
+
not an object, or has no string `text`.
|
|
601
|
+
**Why:** a reply is a `Diagnostic`, `{ code?, status?, text, host? }`:
|
|
602
|
+
`text` is what a DSN and the events show, so every store needs it. The
|
|
603
|
+
queue's own outcomes always have one.
|
|
604
|
+
**Fix:** give the text, even for a reply with no code:
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
await store.complete(id, owner, {
|
|
608
|
+
...result,
|
|
609
|
+
recipients: [{ address, status: 'deferred', reply: { status: '4.4.1', text: 'No answer' } }],
|
|
610
|
+
});
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### `QueueError: reply.status must be a string`
|
|
614
|
+
|
|
615
|
+
**Code:** `INVALID`.
|
|
616
|
+
**When:** `complete` called directly with a `reply.status` that is
|
|
617
|
+
neither left out nor a string — often the reply code given as a number.
|
|
618
|
+
**Why:** `status` is the enhanced status code, `x.y.z` (RFC 3463), kept
|
|
619
|
+
as text; the reply code goes in `code`.
|
|
620
|
+
**Fix:** `reply: { code: 451, status: '4.3.0', text: 'Try later' }`, or
|
|
621
|
+
leave `status` out.
|
|
622
|
+
|
|
623
|
+
### `QueueError: reply.host must be a string`
|
|
624
|
+
|
|
625
|
+
**Code:** `INVALID`.
|
|
626
|
+
**When:** `complete` called directly with a `reply.host` that is neither
|
|
627
|
+
left out nor a string, such as an address object from a socket.
|
|
628
|
+
**Why:** `host` is the name or address the attempt was for, kept as text.
|
|
629
|
+
**Fix:** give it as text — `host: 'mx1.example.com'` or
|
|
630
|
+
`host: '192.0.2.1'` — or leave it out.
|
|
631
|
+
|
|
632
|
+
### `QueueError: … holds a NUL or a lone surrogate, which a store cannot keep`
|
|
633
|
+
|
|
634
|
+
**Code:** `INVALID`. The … is `from`, `A recipient`, `owner`,
|
|
635
|
+
`reply.text`, `reply.status` or `reply.host`.
|
|
636
|
+
**When:** `add`, `complete`, or any call that takes an `owner`, called
|
|
637
|
+
directly with a text holding U+0000 or half of a surrogate pair — a string cut in the
|
|
638
|
+
middle of an emoji, say. `createQueue` refuses such an `owner` the same
|
|
639
|
+
way.
|
|
640
|
+
**Why:** PostgreSQL cannot keep a NUL in text, and no UTF-8 encoding can
|
|
641
|
+
carry a lone surrogate (`Bun.RedisClient` would write one as U+FFFD
|
|
642
|
+
without a word), so every store refuses them alike — whichever
|
|
643
|
+
store you give it, the same text is kept or refused. The queue's own
|
|
644
|
+
replies never hold one: it cuts a reply's text only between whole
|
|
645
|
+
characters.
|
|
646
|
+
**Fix:** drop the NULs and cut text on whole characters before you give
|
|
647
|
+
it to the store:
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
const clean = text.replaceAll('\0', '').toWellFormed();
|
|
651
|
+
```
|
|
652
|
+
|
|
437
653
|
## Delivery
|
|
438
654
|
|
|
439
655
|
### A recipient stays `deferred` with `4.4.1` or `4.4.2`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bumail/queue",
|
|
3
|
-
"version": "0.
|
|
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
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"import": "./dist/postgres/index.js",
|
|
35
35
|
"default": "./dist/postgres/index.js"
|
|
36
36
|
},
|
|
37
|
+
"./redis": {
|
|
38
|
+
"types": "./dist/redis/index.d.ts",
|
|
39
|
+
"import": "./dist/redis/index.js",
|
|
40
|
+
"default": "./dist/redis/index.js"
|
|
41
|
+
},
|
|
37
42
|
"./package.json": "./package.json"
|
|
38
43
|
},
|
|
39
44
|
"repository": {
|
|
@@ -55,7 +60,8 @@
|
|
|
55
60
|
"src/index.ts",
|
|
56
61
|
"src/memory/index.ts",
|
|
57
62
|
"src/sqlite/index.ts",
|
|
58
|
-
"src/postgres/index.ts"
|
|
63
|
+
"src/postgres/index.ts",
|
|
64
|
+
"src/redis/index.ts"
|
|
59
65
|
]
|
|
60
66
|
},
|
|
61
67
|
"devDependencies": {
|