@bumail/queue 0.1.0 → 0.2.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 (39) hide show
  1. package/README.md +37 -5
  2. package/dist/chunks/{index-xhsw4xcz.js → index-vg971acr.js} +70 -6
  3. package/dist/chunks/index-vg971acr.js.map +12 -0
  4. package/dist/contract/checks.d.ts +6 -0
  5. package/dist/contract/checks.d.ts.map +1 -1
  6. package/dist/index.js +14 -33
  7. package/dist/index.js.map +5 -6
  8. package/dist/memory/index.js +3 -5
  9. package/dist/memory/index.js.map +2 -2
  10. package/dist/postgres/connect.d.ts +17 -0
  11. package/dist/postgres/connect.d.ts.map +1 -0
  12. package/dist/postgres/index.d.ts +2 -0
  13. package/dist/postgres/index.d.ts.map +1 -0
  14. package/dist/postgres/index.js +383 -0
  15. package/dist/postgres/index.js.map +14 -0
  16. package/dist/postgres/options.d.ts +37 -0
  17. package/dist/postgres/options.d.ts.map +1 -0
  18. package/dist/postgres/rows.d.ts +21 -0
  19. package/dist/postgres/rows.d.ts.map +1 -0
  20. package/dist/postgres/schema.d.ts +20 -0
  21. package/dist/postgres/schema.d.ts.map +1 -0
  22. package/dist/postgres/statements.d.ts +54 -0
  23. package/dist/postgres/statements.d.ts.map +1 -0
  24. package/dist/postgres/store.d.ts +38 -0
  25. package/dist/postgres/store.d.ts.map +1 -0
  26. package/dist/queue/outcome.d.ts.map +1 -1
  27. package/dist/queue/settings/index.d.ts.map +1 -1
  28. package/dist/sqlite/index.js +3 -5
  29. package/dist/sqlite/index.js.map +2 -2
  30. package/dist/text.d.ts +9 -3
  31. package/dist/text.d.ts.map +1 -1
  32. package/docs/README.md +1 -1
  33. package/docs/guide.md +130 -2
  34. package/docs/roadmap.md +50 -19
  35. package/docs/troubleshooting.md +99 -5
  36. package/package.json +9 -3
  37. package/dist/chunks/index-p4k4rjb7.js +0 -15
  38. package/dist/chunks/index-p4k4rjb7.js.map +0 -10
  39. package/dist/chunks/index-xhsw4xcz.js.map +0 -10
@@ -0,0 +1,54 @@
1
+ import type { Tables } from './connect';
2
+ /**
3
+ * The statements a store runs on its tables, written once per store: the
4
+ * table names are its prefix's (checked to be a plain name), and every
5
+ * value is a parameter. Each parameter is cast, so nothing depends on the
6
+ * type a client infers for it; JSON goes as text, cast to `jsonb`.
7
+ */
8
+ export declare function statementsOf({ items, messages }: Tables): {
9
+ /** $1 id, $2 sender, $3 recipients, $4 size, $5 created_at, $6 message: the item and its message in one statement, or neither. */
10
+ readonly insert: `WITH item AS (
11
+ \t\t\tINSERT INTO ${string} (id, sender, recipients, size, created_at,
12
+ \t\t\t\tnext_attempt_at, attempts, delay_notified)
13
+ \t\t\tVALUES ($1, $2, $3::text::jsonb, $4::int, $5::float8, $5::float8, 0, false)
14
+ \t\t\tRETURNING id)
15
+ \t\tINSERT INTO ${string} (item_id, content) SELECT id, $6::bytea FROM item`;
16
+ /** Taken before counting, by every add given `maxItems`. */
17
+ readonly lockAdds: `SELECT pg_advisory_xact_lock(hashtext('${string}')) IS NULL`;
18
+ readonly count: `SELECT count(*)::int AS n FROM ${string}`;
19
+ readonly get: `SELECT id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at FROM ${string} WHERE id = $1`;
20
+ readonly list: `SELECT id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at FROM ${string}
21
+ \t\t\tORDER BY next_attempt_at, seq LIMIT $1::int OFFSET $2::int`;
22
+ readonly message: `SELECT content FROM ${string} WHERE item_id = $1`;
23
+ /**
24
+ * $1 owner, $2 now, $3 expiry. The earliest due item no other
25
+ * transaction holds, locked as it is picked — one being claimed
26
+ * elsewhere is skipped, not waited for — then leased.
27
+ */
28
+ readonly claim: `UPDATE ${string} SET lease_owner = $1, lease_expires_at = $3::float8
29
+ \t\t\tWHERE seq = (SELECT seq FROM ${string}
30
+ \t\t\t\tWHERE next_attempt_at <= $2::float8
31
+ \t\t\t\t\tAND (lease_owner IS NULL OR lease_expires_at <= $2::float8)
32
+ \t\t\t\tORDER BY next_attempt_at, seq LIMIT 1
33
+ \t\t\t\tFOR UPDATE SKIP LOCKED)
34
+ \t\t\tRETURNING id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at`;
35
+ readonly renew: `UPDATE ${string} SET lease_expires_at = $1::float8
36
+ \t\t\tWHERE id = $2 AND lease_owner = $3 RETURNING seq`;
37
+ /** The owner's item, locked until the transaction ends. */
38
+ readonly held: `SELECT id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at FROM ${string}
39
+ \t\t\tWHERE id = $1 AND lease_owner = $2 FOR UPDATE`;
40
+ /** $1 id, $2 recipients, $3 next attempt, $4 attempts, $5 delay notified: the lease let go of. */
41
+ readonly record: `UPDATE ${string} SET recipients = $2::text::jsonb,
42
+ \t\t\tnext_attempt_at = $3::float8, attempts = $4::int,
43
+ \t\t\tdelay_notified = $5::boolean, lease_owner = NULL,
44
+ \t\t\tlease_expires_at = NULL WHERE id = $1`;
45
+ /** The message goes with it: ON DELETE CASCADE. */
46
+ readonly drop: `DELETE FROM ${string} WHERE id = $1 RETURNING id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at`;
47
+ readonly moveDue: `UPDATE ${string} SET next_attempt_at = $1::float8
48
+ \t\t\tWHERE id = $2 RETURNING seq`;
49
+ readonly giveBack: `UPDATE ${string} SET next_attempt_at = $1::float8,
50
+ \t\t\tlease_owner = NULL, lease_expires_at = NULL
51
+ \t\t\tWHERE id = $2 AND lease_owner = $3 RETURNING seq`;
52
+ };
53
+ export type Statements = ReturnType<typeof statementsOf>;
54
+ //# sourceMappingURL=statements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statements.d.ts","sourceRoot":"","sources":["../../src/postgres/statements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGxC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM;IAEtD,kIAAkI;;;;;;;IAOlI,4DAA4D;;;;;;;IAO5D;;;;OAIG;;;;;;;;;;IAUH,2DAA2D;;;IAG3D,kGAAkG;;;;;IAKlG,mDAAmD;;;;;;;EAQpD;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC"}
@@ -0,0 +1,38 @@
1
+ import type { QueueStore } from '../contract/queue-store';
2
+ import type { AddOptions, AttemptResult, ClaimRequest, NewQueueItem, QueueItem, QueueListOptions } from '../contract/types';
3
+ import type { PostgresQueueStoreOptions } from './options';
4
+ export type { PostgresClient, PostgresQueryable, PostgresQueueStoreOptions, } from './options';
5
+ /**
6
+ * A `QueueStore` on PostgreSQL through Bun's own `Bun.SQL`: several
7
+ * instances, on one machine or many, share one queue. A claim is one
8
+ * `UPDATE … RETURNING` whose item a `SELECT … FOR UPDATE SKIP LOCKED`
9
+ * picks, so two instances never take the same item, and none waits on an
10
+ * item another is taking. Messages live in a table of their own, dropped
11
+ * with their item. The tables are made, or brought up to date, by
12
+ * `migrate()`, or by the first call that needs them.
13
+ */
14
+ export declare class PostgresQueueStore implements QueueStore {
15
+ #private;
16
+ private constructor();
17
+ /** Checks the options; connects to nothing until the first call. What is wrong is `INVALID`. */
18
+ static open(options: PostgresQueueStoreOptions): PostgresQueueStore;
19
+ /**
20
+ * Makes the tables, or brings them to the last migration; once per
21
+ * store, and safe while other instances do the same. A failure is
22
+ * `INVALID`, and the next call tries again.
23
+ */
24
+ migrate(): Promise<void>;
25
+ /** Closes the client the store opened for a URL, never one it was given; closing twice is fine. */
26
+ close(): Promise<void>;
27
+ add(item: NewQueueItem, options?: AddOptions): Promise<QueueItem>;
28
+ get(id: string): Promise<QueueItem | undefined>;
29
+ list(options?: QueueListOptions): Promise<QueueItem[]>;
30
+ count(): Promise<number>;
31
+ readMessage(id: string): Promise<Uint8Array | undefined>;
32
+ claim(request: ClaimRequest): Promise<QueueItem | undefined>;
33
+ renew(id: string, owner: string, expiresAt: number): Promise<boolean>;
34
+ complete(id: string, owner: string, result: AttemptResult): Promise<QueueItem | undefined>;
35
+ reschedule(id: string, at: number, owner?: string): Promise<boolean>;
36
+ cancel(id: string): Promise<QueueItem | undefined>;
37
+ }
38
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/postgres/store.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,KAAK,EAGX,yBAAyB,EACzB,MAAM,WAAW,CAAC;AAKnB,YAAY,EACX,cAAc,EACd,iBAAiB,EACjB,yBAAyB,GACzB,MAAM,WAAW,CAAC;AAKnB;;;;;;;;GAQG;AACH,qBAAa,kBAAmB,YAAW,UAAU;;IAQpD,OAAO;IAOP,gGAAgG;IAChG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB;IAInE;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAaxB,mGAAmG;IAC7F,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBtB,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC;IAkCrE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAM/C,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAMtD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAKxB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAQxD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAW5D,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQrE,QAAQ,CACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IA2B3B,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUpE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;CAMxD"}
@@ -1 +1 @@
1
- {"version":3,"file":"outcome.d.ts","sourceRoot":"","sources":["../../src/queue/outcome.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,KAAK,EACL,cAAc,EACd,SAAS,EAET,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGrE;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,SAGC,CAAC;AAExD;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,OACQ,CAAC;AAKvD,8CAA8C;AAC9C,wBAAgB,YAAY,CAC3B,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,GACX,UAAU,CAWZ;AAsCD,sFAAsF;AACtF,wBAAgB,UAAU,CACzB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,GAAG,EAAE,MAAM,GACT,eAAe,EAAE,CA0BnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,GACX,eAAe,EAAE,CAuBnB"}
1
+ {"version":3,"file":"outcome.d.ts","sourceRoot":"","sources":["../../src/queue/outcome.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,KAAK,EACL,cAAc,EACd,SAAS,EAET,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGrE;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,SAGC,CAAC;AAExD;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,OACQ,CAAC;AAKvD,8CAA8C;AAC9C,wBAAgB,YAAY,CAC3B,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,GACX,UAAU,CAWZ;AAsCD,sFAAsF;AACtF,wBAAgB,UAAU,CACzB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,GAAG,EAAE,MAAM,GACT,eAAe,EAAE,CA6BnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,GACX,eAAe,EAAE,CAuBnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/queue/settings/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,KAAK,WAAW,EAAS,MAAM,OAAO,CAAC;AAChD,OAAO,EACN,KAAK,aAAa,EAElB,KAAK,aAAa,EAElB,MAAM,UAAU,CAAC;AAKlB,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC;AAE1D,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;CAC9B;AAKD,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,QAAQ,CAqD1D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/queue/settings/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,KAAK,WAAW,EAAS,MAAM,OAAO,CAAC;AAChD,OAAO,EACN,KAAK,aAAa,EAElB,KAAK,aAAa,EAElB,MAAM,UAAU,CAAC;AAKlB,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC;AAE1D,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;CAC9B;AAKD,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,QAAQ,CAkD1D"}
@@ -1,4 +1,5 @@
1
1
  import {
2
+ QueueError2,
2
3
  checkTime,
3
4
  checkOwner,
4
5
  checkNewItem,
@@ -9,10 +10,7 @@ import {
9
10
  isDone,
10
11
  applyAttempt,
11
12
  newItem
12
- } from "../chunks/index-xhsw4xcz.js";
13
- import {
14
- QueueError2
15
- } from "../chunks/index-p4k4rjb7.js";
13
+ } from "../chunks/index-vg971acr.js";
16
14
 
17
15
  // src/sqlite/open.ts
18
16
  import { Database } from "bun:sqlite";
@@ -284,5 +282,5 @@ export {
284
282
  SqliteQueueStore
285
283
  };
286
284
 
287
- //# debugId=90554FFDDCF51D9864756E2164756E21
285
+ //# debugId=1AE80C41E9F1FD2664756E2164756E21
288
286
  //# sourceMappingURL=index.js.map
@@ -7,7 +7,7 @@
7
7
  "import type { QueueItem, RecipientState } from '../contract/types';\n\n/** A row of `items`, as `bun:sqlite` reads it. */\nexport interface ItemRow {\n\treadonly id: string;\n\treadonly sender: string;\n\treadonly recipients: string;\n\treadonly size: number;\n\treadonly created_at: number;\n\treadonly next_attempt_at: number;\n\treadonly attempts: number;\n\treadonly delay_notified: number;\n\treadonly lease_owner: string | null;\n\treadonly lease_expires_at: number | null;\n}\n\n/** The columns a query reads to build an item. */\nexport const COLUMNS =\n\t'id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at';\n\nexport function itemOf(row: ItemRow): QueueItem {\n\tconst item: QueueItem = {\n\t\tid: row.id,\n\t\tfrom: row.sender,\n\t\trecipients: JSON.parse(row.recipients) as RecipientState[],\n\t\tsize: row.size,\n\t\tcreatedAt: row.created_at,\n\t\tnextAttemptAt: row.next_attempt_at,\n\t\tattempts: row.attempts,\n\t\tdelayNotified: row.delay_notified === 1,\n\t};\n\tif (row.lease_owner === null || row.lease_expires_at === null) return item;\n\treturn {\n\t\t...item,\n\t\tlease: { owner: row.lease_owner, expiresAt: row.lease_expires_at },\n\t};\n}\n\n/** The named parameters that write an item's state back. */\nexport function stateOf(item: QueueItem) {\n\treturn {\n\t\tid: item.id,\n\t\trecipients: JSON.stringify(item.recipients),\n\t\tnext_attempt_at: item.nextAttemptAt,\n\t\tattempts: item.attempts,\n\t\tdelay_notified: item.delayNotified ? 1 : 0,\n\t\tlease_owner: item.lease?.owner ?? null,\n\t\tlease_expires_at: item.lease?.expiresAt ?? null,\n\t};\n}\n",
8
8
  "import type { Database } from 'bun:sqlite';\nimport {\n\tapplyAttempt,\n\tcheckClaim,\n\tcheckList,\n\tcheckMaxItems,\n\tcheckNewItem,\n\tcheckOwner,\n\tcheckResult,\n\tcheckTime,\n\tisDone,\n\tnewItem,\n} from '../contract/checks';\nimport type { QueueStore } from '../contract/queue-store';\nimport type {\n\tAddOptions,\n\tAttemptResult,\n\tClaimRequest,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n} from '../contract/types';\nimport { QueueError } from '../errors';\nimport { openDatabase } from './open';\nimport type { SqliteQueueStoreOptions } from './options';\nimport { COLUMNS, type ItemRow, itemOf, stateOf } from './rows';\n\nexport type { SqliteQueueStoreOptions } from './options';\n\nconst DUE = `next_attempt_at <= $now\n\tAND (lease_owner IS NULL OR lease_expires_at <= $now)`;\n\n/**\n * A `QueueStore` on `bun:sqlite`: one directory holding `queue.sqlite`,\n * which several processes on one machine may open together. Every\n * operation is one transaction — a claim is a single `UPDATE … RETURNING`\n * — so two workers never take the same item. Messages live in a table of\n * their own, dropped with their item.\n */\nexport class SqliteQueueStore implements QueueStore {\n\treadonly #db: Database;\n\t#closed = false;\n\n\tprivate constructor(db: Database) {\n\t\tthis.#db = db;\n\t}\n\n\t/** Opens the store in `directory`, creating it if need be; what fails is `INVALID`. */\n\tstatic open(options: SqliteQueueStoreOptions): SqliteQueueStore {\n\t\treturn new SqliteQueueStore(openDatabase(options));\n\t}\n\n\t/** Lets go of the database; closing twice is fine. */\n\tclose(): void {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tthis.#db.close();\n\t}\n\n\tget #open(): Database {\n\t\tif (this.#closed) {\n\t\t\tthrow new QueueError('CLOSED', 'The queue store is closed');\n\t\t}\n\t\treturn this.#db;\n\t}\n\n\t#row(id: string, owner?: string): ItemRow | null {\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn db\n\t\t\t\t.query<ItemRow, [string]>(`SELECT ${COLUMNS} FROM items WHERE id = ?`)\n\t\t\t\t.get(id);\n\t\t}\n\t\treturn db\n\t\t\t.query<ItemRow, [string, string]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.get(id, owner);\n\t}\n\n\t#write(item: QueueItem): void {\n\t\tthis.#open\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET recipients = $recipients,\n\t\t\t\tnext_attempt_at = $next_attempt_at, attempts = $attempts,\n\t\t\t\tdelay_notified = $delay_notified, lease_owner = $lease_owner,\n\t\t\t\tlease_expires_at = $lease_expires_at WHERE id = $id`,\n\t\t\t)\n\t\t\t.run(stateOf(item));\n\t}\n\n\tasync add(item: NewQueueItem, options: AddOptions = {}): Promise<QueueItem> {\n\t\tcheckNewItem(item);\n\t\tconst max = checkMaxItems(options.maxItems);\n\t\tconst db = this.#open;\n\t\tconst added = newItem(crypto.randomUUID(), item);\n\t\tdb.transaction(() => {\n\t\t\tif (max !== undefined) {\n\t\t\t\tconst { n } = db\n\t\t\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t\t\t.get() as { n: number };\n\t\t\t\tif (n >= max) {\n\t\t\t\t\tthrow new QueueError(\n\t\t\t\t\t\t'QUEUE_FULL',\n\t\t\t\t\t\t`The queue holds ${n} items, its limit`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tdb.query(\n\t\t\t\t`INSERT INTO items (id, sender, recipients, size, created_at,\n\t\t\t\tnext_attempt_at, attempts, delay_notified)\n\t\t\t\tVALUES ($id, $sender, $recipients, $size, $created_at,\n\t\t\t\t$next_attempt_at, 0, 0)`,\n\t\t\t).run({\n\t\t\t\tid: added.id,\n\t\t\t\tsender: added.from,\n\t\t\t\trecipients: JSON.stringify(added.recipients),\n\t\t\t\tsize: added.size,\n\t\t\t\tcreated_at: added.createdAt,\n\t\t\t\tnext_attempt_at: added.nextAttemptAt,\n\t\t\t});\n\t\t\tdb.query('INSERT INTO messages (item_id, content) VALUES (?, ?)').run(\n\t\t\t\tadded.id,\n\t\t\t\titem.message,\n\t\t\t);\n\t\t}).immediate();\n\t\treturn added;\n\t}\n\n\tasync get(id: string): Promise<QueueItem | undefined> {\n\t\tconst row = this.#row(id);\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync list(options?: QueueListOptions): Promise<QueueItem[]> {\n\t\tconst { offset, limit } = checkList(options);\n\t\treturn this.#open\n\t\t\t.query<ItemRow, [number, number]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items ORDER BY next_attempt_at, seq\n\t\t\t\tLIMIT ? OFFSET ?`,\n\t\t\t)\n\t\t\t.all(limit, offset)\n\t\t\t.map(itemOf);\n\t}\n\n\tasync count(): Promise<number> {\n\t\tconst row = this.#open\n\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t.get();\n\t\treturn row?.n ?? 0;\n\t}\n\n\tasync readMessage(id: string): Promise<Uint8Array | undefined> {\n\t\tconst row = this.#open\n\t\t\t.query<{ content: Uint8Array }, [string]>(\n\t\t\t\t'SELECT content FROM messages WHERE item_id = ?',\n\t\t\t)\n\t\t\t.get(id);\n\t\treturn row ? new Uint8Array(row.content) : undefined;\n\t}\n\n\tasync claim(request: ClaimRequest): Promise<QueueItem | undefined> {\n\t\tcheckClaim(request);\n\t\tconst row = this.#open\n\t\t\t.query<ItemRow, Record<string, string | number>>(\n\t\t\t\t`UPDATE items SET lease_owner = $owner, lease_expires_at = $expires\n\t\t\t\tWHERE seq = (SELECT seq FROM items WHERE ${DUE}\n\t\t\t\t\tORDER BY next_attempt_at, seq LIMIT 1)\n\t\t\t\tRETURNING ${COLUMNS}`,\n\t\t\t)\n\t\t\t.get({\n\t\t\t\towner: request.owner,\n\t\t\t\tnow: request.now,\n\t\t\t\texpires: request.now + request.leaseMs,\n\t\t\t});\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync renew(id: string, owner: string, expiresAt: number): Promise<boolean> {\n\t\tcheckOwner(owner);\n\t\tcheckTime('expiresAt', expiresAt);\n\t\tconst { changes } = this.#open\n\t\t\t.query(\n\t\t\t\t'UPDATE items SET lease_expires_at = ? WHERE id = ? AND lease_owner = ?',\n\t\t\t)\n\t\t\t.run(expiresAt, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync complete(\n\t\tid: string,\n\t\towner: string,\n\t\tresult: AttemptResult,\n\t): Promise<QueueItem | undefined> {\n\t\tcheckOwner(owner);\n\t\tcheckResult(result);\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction(() => {\n\t\t\t\tconst row = this.#row(id, owner);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tconst item = applyAttempt(itemOf(row), result);\n\t\t\t\tif (isDone(item)) {\n\t\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\t} else {\n\t\t\t\t\tthis.#write(item);\n\t\t\t\t}\n\t\t\t\treturn item;\n\t\t\t})\n\t\t\t.immediate();\n\t}\n\n\tasync reschedule(id: string, at: number, owner?: string): Promise<boolean> {\n\t\tcheckTime('at', at);\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn (\n\t\t\t\tdb\n\t\t\t\t\t.query('UPDATE items SET next_attempt_at = ? WHERE id = ?')\n\t\t\t\t\t.run(at, id).changes > 0\n\t\t\t);\n\t\t}\n\t\tcheckOwner(owner);\n\t\tconst { changes } = db\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET next_attempt_at = ?, lease_owner = NULL,\n\t\t\t\tlease_expires_at = NULL WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.run(at, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync cancel(id: string): Promise<QueueItem | undefined> {\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction(() => {\n\t\t\t\tconst row = this.#row(id);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\treturn itemOf(row);\n\t\t\t})\n\t\t\t.immediate();\n\t}\n}\n"
9
9
  ],
10
- "mappings": "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",
11
- "debugId": "90554FFDDCF51D9864756E2164756E21",
10
+ "mappings": "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",
11
+ "debugId": "1AE80C41E9F1FD2664756E2164756E21",
12
12
  "names": []
13
13
  }
package/dist/text.d.ts CHANGED
@@ -4,11 +4,17 @@ export declare function hasControl(text: string): boolean;
4
4
  /** Each control character replaced by `by`. */
5
5
  export declare function replaceControls(text: string, by: string): string;
6
6
  /**
7
- * Every control character (CR and LF included) as a space, runs of space
8
- * as one, cut at `max` characters with `...`: what a reply leaves in an
9
- * item, an event and a DSN.
7
+ * Every control character (CR and LF included) as a space, a lone
8
+ * surrogate as U+FFFD, runs of space as one, cut at `max` characters with
9
+ * `...`, never inside a surrogate pair: what a reply leaves in an item,
10
+ * an event and a DSN.
10
11
  */
11
12
  export declare function cleanText(text: string, max: number): string;
13
+ /**
14
+ * Text every store can keep: no NUL (PostgreSQL's `text` and `jsonb` hold
15
+ * none) and no lone surrogate. `cleanText`'s output always is.
16
+ */
17
+ export declare const isStorable: (text: string) => boolean;
12
18
  /** `cleanText`, then anything not printable ASCII as `?`: for a `message/delivery-status` field. */
13
19
  export declare const asciiText: (text: string, max: number) => string;
14
20
  //# sourceMappingURL=text.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAMlF,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKhD;AAED,+CAA+C;AAC/C,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED,oGAAoG;AACpG,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,KAAK,MAAM,KAAG,MACT,CAAC"}
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAMlF,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKhD;AAED,+CAA+C;AAC/C,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAK3D;AAaD;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OACE,CAAC;AAE7C,oGAAoG;AACpG,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,EAAE,KAAK,MAAM,KAAG,MACT,CAAC"}
package/docs/README.md CHANGED
@@ -5,6 +5,6 @@ long one.
5
5
 
6
6
  | Page | Read it when |
7
7
  | --- | --- |
8
- | [Guide](guide.md) | enqueuing, choosing a route (MX, a smarthost, per domain), tuning the retry schedule and the DSNs, running several workers, listening to events, testing with a fake clock and sender, or writing a store of your own |
8
+ | [Guide](guide.md) | enqueuing, choosing a route (MX, a smarthost, per domain), tuning the retry schedule and the DSNs, running several workers or instances on PostgreSQL, listening to events, testing with a fake clock and sender, or writing a store of your own |
9
9
  | [Troubleshooting](troubleshooting.md) | the queue or a store threw a `QueueError`, the `error` event fired, or mail stays deferred |
10
10
  | [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
package/docs/guide.md CHANGED
@@ -13,6 +13,7 @@ workers share it.
13
13
  - [Events](#events)
14
14
  - [Admin](#admin)
15
15
  - [The stores](#the-stores)
16
+ - [PostgreSQL](#postgresql)
16
17
  - [Testing](#testing)
17
18
  - [Writing a store](#writing-a-store)
18
19
  - [Options](#options)
@@ -234,7 +235,8 @@ is written as `utf-8; j\x{F6}rg@…`, RFC 6533), and every field is folded.
234
235
 
235
236
  ## Several workers, leases and stopping
236
237
 
237
- Any number of queues may share one store, in one process or several:
238
+ Any number of queues may share one store, in one process or several —
239
+ or, with [PostgreSQL](#postgresql), on several machines:
238
240
 
239
241
  ```ts
240
242
  const store = SqliteQueueStore.open({ directory: '/var/lib/bumail/queue' });
@@ -352,7 +354,7 @@ store.close(); // closing twice is fine
352
354
  each item is claimed once; a writer waits up to `busyTimeout`
353
355
  milliseconds for another.
354
356
  Not on a network file system: SQLite's locks do not hold there. For
355
- several machines, a server database is on the roadmap.
357
+ several machines, use [PostgreSQL](#postgresql).
356
358
  - **Messages in a table of their own**, `messages`, dropped with their
357
359
  item in the same transaction. A queue holds a message for days at most
358
360
  and never shares one between items, so content addressing (as
@@ -361,6 +363,128 @@ store.close(); // closing twice is fine
361
363
  - **Migrations** run on open, in one transaction; a database written by a
362
364
  newer version is refused.
363
365
 
366
+ ### `PostgresQueueStore`
367
+
368
+ On PostgreSQL, for instances on several machines: see
369
+ [PostgreSQL](#postgresql).
370
+
371
+ ## PostgreSQL
372
+
373
+ ```ts
374
+ import { createQueue } from '@bumail/queue';
375
+ import { PostgresQueueStore } from '@bumail/queue/postgres';
376
+
377
+ const sql = new Bun.SQL({ url: Bun.env['DATABASE_URL'], max: 10 });
378
+ const store = PostgresQueueStore.open({ sql });
379
+ await store.migrate();
380
+ const queue = createQueue({ store, hostname, resolver, owner: `${host}-${process.pid}` });
381
+ queue.start();
382
+
383
+ process.on('SIGTERM', async () => {
384
+ await queue.stop();
385
+ await sql.close(); // yours: the store never closes a client it was given
386
+ });
387
+ ```
388
+
389
+ `@bumail/queue/postgres` runs on Bun's own `Bun.sql`: there is no
390
+ driver to install and no peer to add. Every instance of your server opens
391
+ a store on the same database, and they share one queue.
392
+
393
+ ### The client
394
+
395
+ - **`sql`** is a `Bun.SQL` client of yours — you size its pool and close
396
+ it — or a `postgres://` (or `postgresql://`) URL, for which the store
397
+ opens a client with Bun's defaults and closes it in `close()`. A
398
+ `Bun.SQL` client for SQLite or MySQL is refused.
399
+ - It is typed by its shape, `PostgresClient` (`unsafe`, `begin` and
400
+ `close`), so the declarations need nothing of `@types/bun`; a `Bun.SQL`
401
+ client fits it.
402
+ - **Nothing connects at `open`**: a wrong option is refused there, and a
403
+ database out of reach on the first call.
404
+
405
+ ### The schema
406
+
407
+ Three tables, each named by `tablePrefix` (`bumail_queue_` by default),
408
+ in the connection's default schema (its `search_path`):
409
+
410
+ | table | what it holds |
411
+ | --- | --- |
412
+ | `<prefix>items` | an item a row: `seq` (an identity, the order among items equally due), `id` (`text`, unique), `sender`, `recipients` (`jsonb`, each recipient's state), `size`, `created_at`, `next_attempt_at`, `attempts`, `delay_notified`, `lease_owner` and `lease_expires_at`. Times are milliseconds since the epoch, as `double precision`, as the queue gives them. `<prefix>items_due`, on `(next_attempt_at, seq)`, serves the claim |
413
+ | `<prefix>messages` | the message as enqueued, whole, as `bytea`, keyed by its item's `id` and dropped with it (`ON DELETE CASCADE`) |
414
+ | `<prefix>schema` | the version: how many migrations ran |
415
+
416
+ They are the `bun:sqlite` store's tables, column for column. The prefix
417
+ is lowercase letters, digits and underscores, starting with a letter or
418
+ an underscore, 40 characters at most: it is written into the statements,
419
+ never bound. Give each queue its own prefix to keep several in one
420
+ database. A `bytea` holds 1 GB at most, far above `limits.maxMessageSize`
421
+ (25 MiB by default).
422
+
423
+ ### Migrations
424
+
425
+ `migrate()` makes the tables, or brings them to the last migration, in
426
+ one transaction; without it, the first call that needs them does. It
427
+ runs once per store. Instances starting together wait on one advisory
428
+ lock (`pg_advisory_xact_lock`), so each migration runs once. A
429
+ migration that fails is `INVALID`, `The PostgreSQL queue cannot be set
430
+ up: …`, and the next call tries again; tables written by a newer version
431
+ of the package are refused.
432
+
433
+ The role needs `CREATE` on the schema the first time, then only `SELECT`,
434
+ `INSERT`, `UPDATE` and `DELETE` on the three tables: a store whose tables
435
+ are current only reads their version, with no lock and no DDL. So run
436
+ `migrate()` once from a deploy step with an owner's role, and give the
437
+ workers a narrower one:
438
+
439
+ ```sql
440
+ GRANT SELECT, INSERT, UPDATE, DELETE
441
+ ON bumail_queue_items, bumail_queue_messages, bumail_queue_schema
442
+ TO bumail_worker;
443
+ ```
444
+
445
+ After an upgrade that adds a migration, run that step again before the
446
+ workers start: theirs cannot make the change.
447
+
448
+ ### Several instances and leases
449
+
450
+ ```sql
451
+ UPDATE <prefix>items SET lease_owner = $1, lease_expires_at = $3
452
+ WHERE seq = (SELECT seq FROM <prefix>items
453
+ WHERE next_attempt_at <= $2 AND (lease_owner IS NULL OR lease_expires_at <= $2)
454
+ ORDER BY next_attempt_at, seq LIMIT 1
455
+ FOR UPDATE SKIP LOCKED)
456
+ RETURNING …
457
+ ```
458
+
459
+ - **A claim** is that one statement: the earliest due item that no other
460
+ transaction is taking, locked as it is picked, then leased. Two
461
+ instances never take the same item, and none waits on an item another
462
+ is claiming: it takes the next.
463
+ - **The lease** works as on `bun:sqlite`: renewed every third of
464
+ `leaseMs`, let go of with the outcome. `complete` locks the item's row
465
+ and records the outcome only while its owner holds the lease. An
466
+ instance that crashes loses its items when their leases expire, and
467
+ another takes them then; one that stalled past its lease records
468
+ nothing once another instance has claimed the item (`LEASE_LOST`):
469
+ `complete` checks who owns the lease, not when it expires.
470
+ - **The clocks** of the instances must agree: each gives the time of its
471
+ own claims and leases, as the store keeps no clock. An instance whose
472
+ clock runs ahead sees leases expire early by as much. Keep the machines
473
+ on NTP; the 10-minute `leaseMs` leaves room for seconds of skew.
474
+ - **`limits.maxItems`**: the adds that count wait on one advisory lock,
475
+ so two instances never both take the last place.
476
+
477
+ ### Pool size
478
+
479
+ Each call holds a connection for one statement, `complete` and an add
480
+ with `limits.maxItems` for one short transaction. A worker has at most
481
+ `concurrency` items (20) in flight, each mostly waiting on a remote SMTP
482
+ server, not the database: `Bun.SQL`'s default pool of 10 connections is
483
+ enough for one, and a call waits for a free connection rather than fail.
484
+ Raise `max` only when a busy worker waits on the pool, and keep every
485
+ instance's pool, and your application's, within the server's
486
+ `max_connections` (100 by default): four instances of `max: 10` take 40.
487
+
364
488
  ## Testing
365
489
 
366
490
  Inject the clock and the sender, and call `deliverDue()` by hand: no
@@ -425,6 +549,10 @@ codes, and keeps its promises:
425
549
  dropped with its message.
426
550
  - Times are given by the caller; the store keeps no clock.
427
551
  - Nothing it returns is shared with what it keeps.
552
+ - Text it keeps holds no NUL and no lone surrogate: the contract's
553
+ checks refuse them (`INVALID`) in what is added, claimed and recorded,
554
+ since PostgreSQL cannot keep them, and an id holding one is unknown.
555
+ The queue never gives a store such text: it cleans every reply.
428
556
 
429
557
  The contract's specs, `describeQueueStore`, which both stores here run,
430
558
  are internal for now: a store written outside this package cannot run
package/docs/roadmap.md CHANGED
@@ -6,26 +6,26 @@ only number on it.
6
6
 
7
7
  ## Now
8
8
 
9
- - **The first slice, in review** — `createQueue`: every recipient's own
10
- state, delivery by domain through `@bumail/smtp/client` (direct MX, a
11
- smarthost, or a route per domain), a global and a per-domain
12
- concurrency limit, retries with exponential back-off and jitter on RFC
13
- 5321 §4.5.4.1's schedule, a 5xx failed at once, DSNs (RFC 3464) for a
14
- failure and a delay, never about a message from `<>`; the `QueueStore`
15
- contract with an atomic claim and leases for several workers, its
16
- memory store and its `bun:sqlite` store, held to one spec suite;
17
- events, `list`, `retryNow` and `cancel`; an injectable clock and sender.
9
+ Nothing in progress: the Redis store, first under Next, is the next one
10
+ taken. The PostgreSQL store is merged (see Shipped).
18
11
 
19
12
  ## Next
20
13
 
21
- - **A PostgreSQL store**, on `Bun.sql` — the same contract for workers on
22
- several machines, the claim a `SELECT … FOR UPDATE SKIP LOCKED`. It
23
- runs the same contract specs.
24
- - **A Redis store**, on `Bun.redis` — the due items in a sorted set, the
25
- claim and the lease in one script.
26
- - **A MongoDB store** — the contract answered structurally, by a
14
+ More answers to the `QueueStore` contract, built for several instances of
15
+ a server sharing one queue — each claim atomic across them, each lease
16
+ taken back from an instance that crashed — and each held to the same
17
+ contract specs as the memory, `bun:sqlite` and PostgreSQL stores. None
18
+ adds a dependency.
19
+
20
+ - **A Redis store**, on Bun's own `Bun.redis` — the due items in a sorted
21
+ set, the claim and the lease in one script.
22
+ - **A MongoDB store** — the contract answered structurally, against a
27
23
  collection of the shape a MongoDB driver gives, so the package peers on
28
- no driver; the claim a `findOneAndUpdate`.
24
+ no driver: the application passes in its own collection; the claim a
25
+ `findOneAndUpdate`.
26
+
27
+ Then:
28
+
29
29
  - **MTA-STS (RFC 8461) and TLS-RPT (RFC 8460)** — a recipient domain's
30
30
  policy fetched, cached and enforced, `mxTls: 'required'` with the
31
31
  certificate checked against the policy's MX names, and the reports.
@@ -54,8 +54,9 @@ only number on it.
54
54
  ## Not planned
55
55
 
56
56
  - **A runtime dependency.** The memory store needs none, the `bun:sqlite`
57
- store uses Bun's own SQLite, and the stores to come use Bun's own
58
- `Bun.sql` and `Bun.redis`.
57
+ store uses Bun's own SQLite, the PostgreSQL store Bun's own `Bun.sql`,
58
+ the Redis store to come `Bun.redis`, and the MongoDB store will take
59
+ the application's own collection, typed by its shape.
59
60
  - **Relaying on its own.** The queue sends what the app enqueues; who may
60
61
  send is decided before, by the SMTP server's AUTH or the app. No option
61
62
  will make the queue accept mail from the network.
@@ -64,4 +65,34 @@ only number on it.
64
65
 
65
66
  ## Shipped
66
67
 
67
- Nothing yet: the first slice is in review (see Now).
68
+ ### Unreleased — merged, not yet published
69
+
70
+ - **A PostgreSQL store**, `@bumail/queue/postgres`, on Bun's own
71
+ `Bun.sql`, for instances of a server on several machines sharing one
72
+ queue: `PostgresQueueStore.open({ sql })` takes a `Bun.SQL` client or a
73
+ `postgres://` URL, and needs no driver. A claim is one `UPDATE …
74
+ RETURNING` whose item a `SELECT … FOR UPDATE SKIP LOCKED` picks, so two
75
+ instances never take the same item; a crashed instance's leases expire
76
+ and its items are claimed again. The tables — the `bun:sqlite` store's,
77
+ the message as `bytea` — are made by `migrate()` or the first call,
78
+ once however many instances start together, under a `tablePrefix`.
79
+ Held to the same contract specs as the other stores, and to specs of
80
+ two instances delivering every item exactly once. Once the tables are
81
+ current, a worker's role needs no `CREATE`.
82
+ - **A reply cut at its limit never splits a character** — the text kept
83
+ for a recipient ends before a surrogate pair rather than inside it, and
84
+ every store refuses a NUL or a lone surrogate in what it keeps, so an
85
+ outcome is always recorded.
86
+
87
+ ### 0.1.0
88
+
89
+ - **The first slice** — `createQueue`: every recipient's own state,
90
+ delivery by domain through `@bumail/smtp/client` (direct MX, a
91
+ smarthost, or a route per domain), a global and a per-domain
92
+ concurrency limit, retries with exponential back-off and jitter on RFC
93
+ 5321 §4.5.4.1's schedule, a 5xx failed at once, DSNs (RFC 3464) for a
94
+ failure and a delay, never about a message from `<>`; the `QueueStore`
95
+ contract with an atomic claim and leases for several workers, its
96
+ memory store and its `bun:sqlite` store, held to one spec suite;
97
+ events, `list`, `retryNow` and `cancel`; an injectable clock and sender.
98
+ The package's first release.