@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.
- package/README.md +37 -5
- package/dist/chunks/{index-xhsw4xcz.js → index-vg971acr.js} +70 -6
- package/dist/chunks/index-vg971acr.js.map +12 -0
- package/dist/contract/checks.d.ts +6 -0
- package/dist/contract/checks.d.ts.map +1 -1
- package/dist/index.js +14 -33
- package/dist/index.js.map +5 -6
- package/dist/memory/index.js +3 -5
- package/dist/memory/index.js.map +2 -2
- package/dist/postgres/connect.d.ts +17 -0
- package/dist/postgres/connect.d.ts.map +1 -0
- package/dist/postgres/index.d.ts +2 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +383 -0
- package/dist/postgres/index.js.map +14 -0
- package/dist/postgres/options.d.ts +37 -0
- package/dist/postgres/options.d.ts.map +1 -0
- package/dist/postgres/rows.d.ts +21 -0
- package/dist/postgres/rows.d.ts.map +1 -0
- package/dist/postgres/schema.d.ts +20 -0
- package/dist/postgres/schema.d.ts.map +1 -0
- package/dist/postgres/statements.d.ts +54 -0
- package/dist/postgres/statements.d.ts.map +1 -0
- package/dist/postgres/store.d.ts +38 -0
- package/dist/postgres/store.d.ts.map +1 -0
- package/dist/queue/outcome.d.ts.map +1 -1
- package/dist/queue/settings/index.d.ts.map +1 -1
- package/dist/sqlite/index.js +3 -5
- package/dist/sqlite/index.js.map +2 -2
- package/dist/text.d.ts +9 -3
- package/dist/text.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/guide.md +130 -2
- package/docs/roadmap.md +50 -19
- package/docs/troubleshooting.md +99 -5
- package/package.json +9 -3
- package/dist/chunks/index-p4k4rjb7.js +0 -15
- package/dist/chunks/index-p4k4rjb7.js.map +0 -10
- package/dist/chunks/index-xhsw4xcz.js.map +0 -10
|
@@ -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,
|
|
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":"
|
|
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"}
|
package/dist/sqlite/index.js
CHANGED
|
@@ -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-
|
|
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=
|
|
285
|
+
//# debugId=1AE80C41E9F1FD2664756E2164756E21
|
|
288
286
|
//# sourceMappingURL=index.js.map
|
package/dist/sqlite/index.js.map
CHANGED
|
@@ -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": "
|
|
11
|
-
"debugId": "
|
|
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,
|
|
8
|
-
* as one, cut at `max` characters with
|
|
9
|
-
*
|
|
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
|
package/dist/text.d.ts.map
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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,
|
|
58
|
-
`Bun.
|
|
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
|
-
|
|
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.
|