@bumail/queue 0.1.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.
Files changed (66) hide show
  1. package/README.md +71 -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/chunks/index-y3n1w2az.js +18 -0
  5. package/dist/chunks/index-y3n1w2az.js.map +10 -0
  6. package/dist/contract/checks.d.ts +6 -0
  7. package/dist/contract/checks.d.ts.map +1 -1
  8. package/dist/errors.d.ts +2 -1
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/index.js +86 -55
  11. package/dist/index.js.map +8 -8
  12. package/dist/masked.d.ts +7 -0
  13. package/dist/masked.d.ts.map +1 -0
  14. package/dist/memory/index.js +3 -5
  15. package/dist/memory/index.js.map +2 -2
  16. package/dist/postgres/connect.d.ts +17 -0
  17. package/dist/postgres/connect.d.ts.map +1 -0
  18. package/dist/postgres/index.d.ts +2 -0
  19. package/dist/postgres/index.d.ts.map +1 -0
  20. package/dist/postgres/index.js +374 -0
  21. package/dist/postgres/index.js.map +14 -0
  22. package/dist/postgres/options.d.ts +37 -0
  23. package/dist/postgres/options.d.ts.map +1 -0
  24. package/dist/postgres/rows.d.ts +21 -0
  25. package/dist/postgres/rows.d.ts.map +1 -0
  26. package/dist/postgres/schema.d.ts +20 -0
  27. package/dist/postgres/schema.d.ts.map +1 -0
  28. package/dist/postgres/statements.d.ts +54 -0
  29. package/dist/postgres/statements.d.ts.map +1 -0
  30. package/dist/postgres/store.d.ts +38 -0
  31. package/dist/postgres/store.d.ts.map +1 -0
  32. package/dist/queue/deliver.d.ts +7 -0
  33. package/dist/queue/deliver.d.ts.map +1 -1
  34. package/dist/queue/lease.d.ts +41 -0
  35. package/dist/queue/lease.d.ts.map +1 -0
  36. package/dist/queue/outcome.d.ts.map +1 -1
  37. package/dist/queue/settings/index.d.ts.map +1 -1
  38. package/dist/queue/worker.d.ts.map +1 -1
  39. package/dist/redis/connect.d.ts +25 -0
  40. package/dist/redis/connect.d.ts.map +1 -0
  41. package/dist/redis/index.d.ts +3 -0
  42. package/dist/redis/index.d.ts.map +1 -0
  43. package/dist/redis/index.js +430 -0
  44. package/dist/redis/index.js.map +14 -0
  45. package/dist/redis/items.d.ts +13 -0
  46. package/dist/redis/items.d.ts.map +1 -0
  47. package/dist/redis/options.d.ts +47 -0
  48. package/dist/redis/options.d.ts.map +1 -0
  49. package/dist/redis/run.d.ts +12 -0
  50. package/dist/redis/run.d.ts.map +1 -0
  51. package/dist/redis/scripts.d.ts +60 -0
  52. package/dist/redis/scripts.d.ts.map +1 -0
  53. package/dist/redis/store.d.ts +30 -0
  54. package/dist/redis/store.d.ts.map +1 -0
  55. package/dist/sqlite/index.js +3 -5
  56. package/dist/sqlite/index.js.map +2 -2
  57. package/dist/text.d.ts +9 -3
  58. package/dist/text.d.ts.map +1 -1
  59. package/docs/README.md +1 -1
  60. package/docs/guide.md +314 -7
  61. package/docs/roadmap.md +73 -19
  62. package/docs/troubleshooting.md +317 -7
  63. package/package.json +15 -3
  64. package/dist/chunks/index-p4k4rjb7.js +0 -15
  65. package/dist/chunks/index-p4k4rjb7.js.map +0 -10
  66. package/dist/chunks/index-xhsw4xcz.js.map +0 -10
@@ -0,0 +1,374 @@
1
+ import {
2
+ masked
3
+ } from "../chunks/index-y3n1w2az.js";
4
+ import {
5
+ QueueError2,
6
+ invalid,
7
+ isStorable,
8
+ checkTime,
9
+ checkOwner,
10
+ checkNewItem,
11
+ checkMaxItems,
12
+ checkClaim,
13
+ checkResult,
14
+ checkList,
15
+ isDone,
16
+ applyAttempt,
17
+ newItem
18
+ } from "../chunks/index-vg971acr.js";
19
+
20
+ // src/postgres/connect.ts
21
+ var DEFAULT_PREFIX = "bumail_queue_";
22
+ var PREFIX = /^[a-z_][a-z0-9_]{0,39}$/;
23
+ function tablesOf(value) {
24
+ const prefix = value ?? DEFAULT_PREFIX;
25
+ if (typeof prefix !== "string" || !PREFIX.test(prefix)) {
26
+ throw invalid(`tablePrefix must be lowercase letters, digits and underscores, starting with a letter or an underscore, at most 40 characters, not ${JSON.stringify(prefix)}`);
27
+ }
28
+ return {
29
+ prefix,
30
+ items: `${prefix}items`,
31
+ messages: `${prefix}messages`,
32
+ schema: `${prefix}schema`
33
+ };
34
+ }
35
+ var NEEDS_SQL = "A PostgreSQL queue store needs sql: a Bun.SQL client or a postgres:// URL";
36
+ function urlOf(value) {
37
+ const url = URL.parse(String(value));
38
+ if (url === null || !["postgres:", "postgresql:"].includes(url.protocol)) {
39
+ throw invalid(NEEDS_SQL);
40
+ }
41
+ return url;
42
+ }
43
+ function clientFor(url) {
44
+ try {
45
+ return new Bun.SQL(url.href);
46
+ } catch (error) {
47
+ const reason = error instanceof Error ? error.message : String(error);
48
+ throw invalid(`The URL in sql cannot be opened: ${masked(reason, url.password)}`);
49
+ }
50
+ }
51
+ var isClient = (value) => (typeof value === "object" || typeof value === "function") && value !== null && typeof value.unsafe === "function" && typeof value.begin === "function" && typeof value.close === "function";
52
+ function checkAdapter(client) {
53
+ const adapter = client.options?.adapter;
54
+ if (adapter !== undefined && adapter !== "postgres") {
55
+ throw invalid(`sql is a ${String(adapter)} client; the queue needs a PostgreSQL one`);
56
+ }
57
+ }
58
+ function connect(options) {
59
+ const given = options?.sql;
60
+ const tables = tablesOf(options?.tablePrefix);
61
+ if (isClient(given)) {
62
+ checkAdapter(given);
63
+ return { client: given, owned: false, tables };
64
+ }
65
+ if (typeof given !== "string" && !(given instanceof URL)) {
66
+ throw invalid(NEEDS_SQL);
67
+ }
68
+ return { client: clientFor(urlOf(given)), owned: true, tables };
69
+ }
70
+
71
+ // src/postgres/rows.ts
72
+ var COLUMNS = "id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at";
73
+ function itemOf(row) {
74
+ const recipients = typeof row.recipients === "string" ? JSON.parse(row.recipients) : structuredClone(row.recipients);
75
+ const item = {
76
+ id: row.id,
77
+ from: row.sender,
78
+ recipients,
79
+ size: row.size,
80
+ createdAt: row.created_at,
81
+ nextAttemptAt: row.next_attempt_at,
82
+ attempts: row.attempts,
83
+ delayNotified: row.delay_notified
84
+ };
85
+ if (row.lease_owner === null || row.lease_expires_at === null)
86
+ return item;
87
+ return {
88
+ ...item,
89
+ lease: { owner: row.lease_owner, expiresAt: row.lease_expires_at }
90
+ };
91
+ }
92
+ var rowsOf = async (pending) => await pending;
93
+
94
+ // src/postgres/schema.ts
95
+ function migrations({
96
+ prefix,
97
+ items,
98
+ messages
99
+ }) {
100
+ return [
101
+ [
102
+ `CREATE TABLE ${items} (
103
+ seq bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
104
+ id text NOT NULL UNIQUE,
105
+ sender text NOT NULL,
106
+ recipients jsonb NOT NULL CHECK (jsonb_typeof(recipients) = 'array'),
107
+ size integer NOT NULL,
108
+ created_at double precision NOT NULL,
109
+ next_attempt_at double precision NOT NULL,
110
+ attempts integer NOT NULL,
111
+ delay_notified boolean NOT NULL,
112
+ lease_owner text,
113
+ lease_expires_at double precision,
114
+ CHECK ((lease_owner IS NULL) = (lease_expires_at IS NULL))
115
+ )`,
116
+ `CREATE INDEX ${prefix}items_due ON ${items} (next_attempt_at, seq)`,
117
+ `CREATE TABLE ${messages} (
118
+ item_id text PRIMARY KEY REFERENCES ${items} (id) ON DELETE CASCADE,
119
+ content bytea NOT NULL
120
+ )`
121
+ ]
122
+ ];
123
+ }
124
+ function checkNotNewer(version, known) {
125
+ if (version > known) {
126
+ throw new QueueError2("INVALID", `The database is at schema version ${version}, newer than this store's ${known}`);
127
+ }
128
+ }
129
+ async function versionOf(sql, tables) {
130
+ const [found] = await rowsOf(sql.unsafe("SELECT to_regclass($1) IS NOT NULL AS present", [
131
+ tables.schema
132
+ ]));
133
+ if (!found?.present)
134
+ return;
135
+ const [row] = await rowsOf(sql.unsafe(`SELECT version FROM ${tables.schema}`));
136
+ return row?.version ?? 0;
137
+ }
138
+ async function migrate(client, tables) {
139
+ const all = migrations(tables);
140
+ const current = await versionOf(client, tables);
141
+ if (current !== undefined) {
142
+ checkNotNewer(current, all.length);
143
+ if (current === all.length)
144
+ return;
145
+ }
146
+ await client.begin(async (sql) => {
147
+ await sql.unsafe("SELECT pg_advisory_xact_lock(hashtext($1)) IS NULL", [
148
+ tables.schema
149
+ ]);
150
+ await sql.unsafe(`CREATE TABLE IF NOT EXISTS ${tables.schema} (version integer NOT NULL)`);
151
+ const [row] = await rowsOf(sql.unsafe(`SELECT version FROM ${tables.schema}`));
152
+ const version = row?.version ?? 0;
153
+ checkNotNewer(version, all.length);
154
+ for (const statements of all.slice(version)) {
155
+ for (const statement of statements)
156
+ await sql.unsafe(statement);
157
+ }
158
+ if (row === undefined) {
159
+ await sql.unsafe(`INSERT INTO ${tables.schema} (version) VALUES ($1::int)`, [all.length]);
160
+ } else if (version < all.length) {
161
+ await sql.unsafe(`UPDATE ${tables.schema} SET version = $1::int`, [
162
+ all.length
163
+ ]);
164
+ }
165
+ });
166
+ }
167
+
168
+ // src/postgres/statements.ts
169
+ function statementsOf({ items, messages }) {
170
+ return {
171
+ insert: `WITH item AS (
172
+ INSERT INTO ${items} (id, sender, recipients, size, created_at,
173
+ next_attempt_at, attempts, delay_notified)
174
+ VALUES ($1, $2, $3::text::jsonb, $4::int, $5::float8, $5::float8, 0, false)
175
+ RETURNING id)
176
+ INSERT INTO ${messages} (item_id, content) SELECT id, $6::bytea FROM item`,
177
+ lockAdds: `SELECT pg_advisory_xact_lock(hashtext('${items}')) IS NULL`,
178
+ count: `SELECT count(*)::int AS n FROM ${items}`,
179
+ get: `SELECT ${COLUMNS} FROM ${items} WHERE id = $1`,
180
+ list: `SELECT ${COLUMNS} FROM ${items}
181
+ ORDER BY next_attempt_at, seq LIMIT $1::int OFFSET $2::int`,
182
+ message: `SELECT content FROM ${messages} WHERE item_id = $1`,
183
+ claim: `UPDATE ${items} SET lease_owner = $1, lease_expires_at = $3::float8
184
+ WHERE seq = (SELECT seq FROM ${items}
185
+ WHERE next_attempt_at <= $2::float8
186
+ AND (lease_owner IS NULL OR lease_expires_at <= $2::float8)
187
+ ORDER BY next_attempt_at, seq LIMIT 1
188
+ FOR UPDATE SKIP LOCKED)
189
+ RETURNING ${COLUMNS}`,
190
+ renew: `UPDATE ${items} SET lease_expires_at = $1::float8
191
+ WHERE id = $2 AND lease_owner = $3 RETURNING seq`,
192
+ held: `SELECT ${COLUMNS} FROM ${items}
193
+ WHERE id = $1 AND lease_owner = $2 FOR UPDATE`,
194
+ record: `UPDATE ${items} SET recipients = $2::text::jsonb,
195
+ next_attempt_at = $3::float8, attempts = $4::int,
196
+ delay_notified = $5::boolean, lease_owner = NULL,
197
+ lease_expires_at = NULL WHERE id = $1`,
198
+ drop: `DELETE FROM ${items} WHERE id = $1 RETURNING ${COLUMNS}`,
199
+ moveDue: `UPDATE ${items} SET next_attempt_at = $1::float8
200
+ WHERE id = $2 RETURNING seq`,
201
+ giveBack: `UPDATE ${items} SET next_attempt_at = $1::float8,
202
+ lease_owner = NULL, lease_expires_at = NULL
203
+ WHERE id = $2 AND lease_owner = $3 RETURNING seq`
204
+ };
205
+ }
206
+
207
+ // src/postgres/store.ts
208
+ var messageOf = (error) => error instanceof Error ? error.message : String(error);
209
+
210
+ class PostgresQueueStore {
211
+ #client;
212
+ #owned;
213
+ #tables;
214
+ #q;
215
+ #migrated;
216
+ #closed = false;
217
+ constructor({ client, owned, tables }) {
218
+ this.#client = client;
219
+ this.#owned = owned;
220
+ this.#tables = tables;
221
+ this.#q = statementsOf(tables);
222
+ }
223
+ static open(options) {
224
+ return new PostgresQueueStore(connect(options));
225
+ }
226
+ migrate() {
227
+ if (this.#closed)
228
+ return Promise.reject(closed());
229
+ this.#migrated ??= migrate(this.#client, this.#tables).catch((error) => {
230
+ this.#migrated = undefined;
231
+ if (error instanceof QueueError2)
232
+ throw error;
233
+ throw new QueueError2("INVALID", `The PostgreSQL queue cannot be set up: ${messageOf(error)}`);
234
+ });
235
+ return this.#migrated;
236
+ }
237
+ async close() {
238
+ if (this.#closed)
239
+ return;
240
+ this.#closed = true;
241
+ if (this.#owned)
242
+ await this.#client.close();
243
+ }
244
+ async#sql() {
245
+ if (this.#closed)
246
+ throw closed();
247
+ await this.migrate();
248
+ return this.#client;
249
+ }
250
+ async#rows(query, values = []) {
251
+ return rowsOf((await this.#sql()).unsafe(query, values));
252
+ }
253
+ async add(item, options = {}) {
254
+ checkNewItem(item);
255
+ const max = checkMaxItems(options.maxItems);
256
+ const added = newItem(crypto.randomUUID(), item);
257
+ const values = [
258
+ added.id,
259
+ added.from,
260
+ JSON.stringify(added.recipients),
261
+ added.size,
262
+ added.createdAt,
263
+ item.message
264
+ ];
265
+ const sql = await this.#sql();
266
+ const q = this.#q;
267
+ if (max === undefined) {
268
+ await sql.unsafe(q.insert, values);
269
+ return added;
270
+ }
271
+ await sql.begin(async (tx) => {
272
+ await tx.unsafe(q.lockAdds);
273
+ const [row] = await rowsOf(tx.unsafe(q.count));
274
+ const n = row?.n ?? 0;
275
+ if (n >= max) {
276
+ throw new QueueError2("QUEUE_FULL", `The queue holds ${n} items, its limit`);
277
+ }
278
+ await tx.unsafe(q.insert, values);
279
+ });
280
+ return added;
281
+ }
282
+ async get(id) {
283
+ if (!isStorable(id))
284
+ return;
285
+ const [row] = await this.#rows(this.#q.get, [id]);
286
+ return row ? itemOf(row) : undefined;
287
+ }
288
+ async list(options) {
289
+ const { offset, limit } = checkList(options);
290
+ const rows = await this.#rows(this.#q.list, [limit, offset]);
291
+ return rows.map(itemOf);
292
+ }
293
+ async count() {
294
+ const [row] = await this.#rows(this.#q.count);
295
+ return row?.n ?? 0;
296
+ }
297
+ async readMessage(id) {
298
+ if (!isStorable(id))
299
+ return;
300
+ const [row] = await this.#rows(this.#q.message, [
301
+ id
302
+ ]);
303
+ return row ? new Uint8Array(row.content) : undefined;
304
+ }
305
+ async claim(request) {
306
+ checkClaim(request);
307
+ const { owner, now, leaseMs } = request;
308
+ const [row] = await this.#rows(this.#q.claim, [
309
+ owner,
310
+ now,
311
+ now + leaseMs
312
+ ]);
313
+ return row ? itemOf(row) : undefined;
314
+ }
315
+ async renew(id, owner, expiresAt) {
316
+ checkOwner(owner);
317
+ checkTime("expiresAt", expiresAt);
318
+ if (!isStorable(id))
319
+ return false;
320
+ const rows = await this.#rows(this.#q.renew, [expiresAt, id, owner]);
321
+ return rows.length > 0;
322
+ }
323
+ async complete(id, owner, result) {
324
+ checkOwner(owner);
325
+ checkResult(result);
326
+ if (!isStorable(id))
327
+ return;
328
+ const sql = await this.#sql();
329
+ const q = this.#q;
330
+ let done;
331
+ await sql.begin(async (tx) => {
332
+ const [row] = await rowsOf(tx.unsafe(q.held, [id, owner]));
333
+ if (!row)
334
+ return;
335
+ const item = applyAttempt(itemOf(row), result);
336
+ if (isDone(item)) {
337
+ await tx.unsafe(q.drop, [id]);
338
+ } else {
339
+ await tx.unsafe(q.record, [
340
+ id,
341
+ JSON.stringify(item.recipients),
342
+ item.nextAttemptAt,
343
+ item.attempts,
344
+ item.delayNotified
345
+ ]);
346
+ }
347
+ done = item;
348
+ });
349
+ return done;
350
+ }
351
+ async reschedule(id, at, owner) {
352
+ checkTime("at", at);
353
+ if (!isStorable(id))
354
+ return false;
355
+ if (owner === undefined) {
356
+ return (await this.#rows(this.#q.moveDue, [at, id])).length > 0;
357
+ }
358
+ checkOwner(owner);
359
+ return (await this.#rows(this.#q.giveBack, [at, id, owner])).length > 0;
360
+ }
361
+ async cancel(id) {
362
+ if (!isStorable(id))
363
+ return;
364
+ const [row] = await this.#rows(this.#q.drop, [id]);
365
+ return row ? itemOf(row) : undefined;
366
+ }
367
+ }
368
+ var closed = () => new QueueError2("CLOSED", "The queue store is closed");
369
+ export {
370
+ PostgresQueueStore
371
+ };
372
+
373
+ //# debugId=C2B9C7B259B45F6964756E2164756E21
374
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/postgres/connect.ts", "../src/postgres/rows.ts", "../src/postgres/schema.ts", "../src/postgres/statements.ts", "../src/postgres/store.ts"],
4
+ "sourcesContent": [
5
+ "import { invalid } from '../errors';\nimport { masked } from '../masked';\nimport type { PostgresClient, PostgresQueueStoreOptions } from './options';\n\n/** The tables of one queue, each name its prefix and a fixed suffix. */\nexport interface Tables {\n\treadonly prefix: string;\n\treadonly items: string;\n\treadonly messages: string;\n\treadonly schema: string;\n}\n\n/** The client a store uses, and whether it opened it (and so closes it). */\nexport interface Connection {\n\treadonly client: PostgresClient;\n\treadonly owned: boolean;\n\treadonly tables: Tables;\n}\n\nconst DEFAULT_PREFIX = 'bumail_queue_';\n\n/**\n * Within PostgreSQL's 63 bytes for a name once the longest name derived\n * from it (`messages_item_id_fkey`, 21) follows. Nothing else is allowed:\n * the prefix is written into the statements, never bound.\n */\nconst PREFIX = /^[a-z_][a-z0-9_]{0,39}$/;\n\nfunction tablesOf(value: unknown): Tables {\n\tconst prefix = value ?? DEFAULT_PREFIX;\n\tif (typeof prefix !== 'string' || !PREFIX.test(prefix)) {\n\t\tthrow invalid(\n\t\t\t`tablePrefix must be lowercase letters, digits and underscores, starting with a letter or an underscore, at most 40 characters, not ${JSON.stringify(prefix)}`,\n\t\t);\n\t}\n\treturn {\n\t\tprefix,\n\t\titems: `${prefix}items`,\n\t\tmessages: `${prefix}messages`,\n\t\tschema: `${prefix}schema`,\n\t};\n}\n\nconst NEEDS_SQL =\n\t'A PostgreSQL queue store needs sql: a Bun.SQL client or a postgres:// URL';\n\n/** A `postgres://` or `postgresql://` URL; never echoed, since it may hold a password. */\nfunction urlOf(value: string | URL): URL {\n\tconst url = URL.parse(String(value));\n\tif (url === null || !['postgres:', 'postgresql:'].includes(url.protocol)) {\n\t\tthrow invalid(NEEDS_SQL);\n\t}\n\treturn url;\n}\n\n/**\n * A client of the store's own for the URL. `Bun.SQL` refuses some\n * parameters at once (a `sslmode` it does not know): that is `INVALID`,\n * its reason kept and the password, should it be repeated, masked.\n */\nfunction clientFor(url: URL): PostgresClient {\n\ttry {\n\t\treturn new Bun.SQL(url.href);\n\t} catch (error) {\n\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\tthrow invalid(\n\t\t\t`The URL in sql cannot be opened: ${masked(reason, url.password)}`,\n\t\t);\n\t}\n}\n\nconst isClient = (value: unknown): value is PostgresClient =>\n\t(typeof value === 'object' || typeof value === 'function') &&\n\tvalue !== null &&\n\ttypeof (value as PostgresClient).unsafe === 'function' &&\n\ttypeof (value as PostgresClient).begin === 'function' &&\n\ttypeof (value as PostgresClient).close === 'function';\n\n/** A `Bun.SQL` client says which database it speaks: the queue needs PostgreSQL. */\nfunction checkAdapter(client: PostgresClient): void {\n\tconst adapter = (client as { options?: { adapter?: unknown } }).options\n\t\t?.adapter;\n\tif (adapter !== undefined && adapter !== 'postgres') {\n\t\tthrow invalid(\n\t\t\t`sql is a ${String(adapter)} client; the queue needs a PostgreSQL one`,\n\t\t);\n\t}\n}\n\n/** Checks the options and opens a client for a URL; connects to nothing yet. */\nexport function connect(options: PostgresQueueStoreOptions): Connection {\n\tconst given = (options as PostgresQueueStoreOptions | undefined)?.sql;\n\tconst tables = tablesOf(options?.tablePrefix);\n\tif (isClient(given)) {\n\t\tcheckAdapter(given);\n\t\treturn { client: given, owned: false, tables };\n\t}\n\tif (typeof given !== 'string' && !(given instanceof URL)) {\n\t\tthrow invalid(NEEDS_SQL);\n\t}\n\treturn { client: clientFor(urlOf(given)), owned: true, tables };\n}\n",
6
+ "import type { QueueItem, RecipientState } from '../contract/types';\n\n/** A row of the items table, as `Bun.SQL` reads it. */\nexport interface ItemRow {\n\treadonly id: string;\n\treadonly sender: string;\n\t/** `jsonb`: parsed or not, depending on the client. */\n\treadonly recipients: string | RecipientState[];\n\treadonly size: number;\n\treadonly created_at: number;\n\treadonly next_attempt_at: number;\n\treadonly attempts: number;\n\treadonly delay_notified: boolean;\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 recipients =\n\t\ttypeof row.recipients === 'string'\n\t\t\t? (JSON.parse(row.recipients) as RecipientState[])\n\t\t\t: structuredClone(row.recipients);\n\tconst item: QueueItem = {\n\t\tid: row.id,\n\t\tfrom: row.sender,\n\t\trecipients,\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,\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 rows a statement returned. */\nexport const rowsOf = async <T>(pending: PromiseLike<unknown>) =>\n\t(await pending) as T[];\n",
7
+ "import { QueueError } from '../errors';\nimport type { Tables } from './connect';\nimport type { PostgresClient, PostgresQueryable } from './options';\nimport { rowsOf } from './rows';\n\n/**\n * The migrations, in order, each a list of statements: the schema table's\n * `version` is how many of them the database has run. A migration is\n * never edited once released; a change is a new one at the end. They\n * follow the `bun:sqlite` store's, column for column: times are\n * milliseconds as `double precision` (SQLite's `REAL`), the recipients\n * JSON, the message its bytes, in a table of its own.\n */\nexport function migrations({\n\tprefix,\n\titems,\n\tmessages,\n}: Tables): readonly (readonly string[])[] {\n\treturn [\n\t\t// 1: the items, and their messages in a table of their own.\n\t\t[\n\t\t\t`CREATE TABLE ${items} (\n\t\t\t\tseq bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n\t\t\t\tid text NOT NULL UNIQUE,\n\t\t\t\tsender text NOT NULL,\n\t\t\t\trecipients jsonb NOT NULL CHECK (jsonb_typeof(recipients) = 'array'),\n\t\t\t\tsize integer NOT NULL,\n\t\t\t\tcreated_at double precision NOT NULL,\n\t\t\t\tnext_attempt_at double precision NOT NULL,\n\t\t\t\tattempts integer NOT NULL,\n\t\t\t\tdelay_notified boolean NOT NULL,\n\t\t\t\tlease_owner text,\n\t\t\t\tlease_expires_at double precision,\n\t\t\t\tCHECK ((lease_owner IS NULL) = (lease_expires_at IS NULL))\n\t\t\t)`,\n\t\t\t// The claim's order, and its range: the earliest due first.\n\t\t\t`CREATE INDEX ${prefix}items_due ON ${items} (next_attempt_at, seq)`,\n\t\t\t`CREATE TABLE ${messages} (\n\t\t\t\titem_id text PRIMARY KEY REFERENCES ${items} (id) ON DELETE CASCADE,\n\t\t\t\tcontent bytea NOT NULL\n\t\t\t)`,\n\t\t],\n\t];\n}\n\n/** Tables a newer store wrote, with more migrations than this one knows, are refused. */\nfunction checkNotNewer(version: number, known: number): void {\n\tif (version > known) {\n\t\tthrow new QueueError(\n\t\t\t'INVALID',\n\t\t\t`The database is at schema version ${version}, newer than this store's ${known}`,\n\t\t);\n\t}\n}\n\n/**\n * The version the tables are at, read with no lock and no DDL — so a role\n * with no `CREATE` reads it — or `undefined` with no schema table yet.\n */\nasync function versionOf(\n\tsql: PostgresQueryable,\n\ttables: Tables,\n): Promise<number | undefined> {\n\tconst [found] = await rowsOf<{ present: boolean }>(\n\t\tsql.unsafe('SELECT to_regclass($1) IS NOT NULL AS present', [\n\t\t\ttables.schema,\n\t\t]),\n\t);\n\tif (!found?.present) return undefined;\n\tconst [row] = await rowsOf<{ version: number }>(\n\t\tsql.unsafe(`SELECT version FROM ${tables.schema}`),\n\t);\n\treturn row?.version ?? 0;\n}\n\n/**\n * Brings the tables to the last migration. Tables already current are\n * only read: no lock, no DDL, so a worker's role needs no `CREATE`.\n * Otherwise in one transaction — a migration that fails leaves them as\n * they were — under one advisory lock, so instances starting together\n * run each migration once.\n */\nexport async function migrate(\n\tclient: PostgresClient,\n\ttables: Tables,\n): Promise<void> {\n\tconst all = migrations(tables);\n\tconst current = await versionOf(client, tables);\n\tif (current !== undefined) {\n\t\tcheckNotNewer(current, all.length);\n\t\tif (current === all.length) return;\n\t}\n\tawait client.begin(async (sql: PostgresQueryable) => {\n\t\tawait sql.unsafe('SELECT pg_advisory_xact_lock(hashtext($1)) IS NULL', [\n\t\t\ttables.schema,\n\t\t]);\n\t\tawait sql.unsafe(\n\t\t\t`CREATE TABLE IF NOT EXISTS ${tables.schema} (version integer NOT NULL)`,\n\t\t);\n\t\tconst [row] = await rowsOf<{ version: number }>(\n\t\t\tsql.unsafe(`SELECT version FROM ${tables.schema}`),\n\t\t);\n\t\tconst version = row?.version ?? 0;\n\t\tcheckNotNewer(version, all.length);\n\t\tfor (const statements of all.slice(version)) {\n\t\t\tfor (const statement of statements) await sql.unsafe(statement);\n\t\t}\n\t\tif (row === undefined) {\n\t\t\tawait sql.unsafe(\n\t\t\t\t`INSERT INTO ${tables.schema} (version) VALUES ($1::int)`,\n\t\t\t\t[all.length],\n\t\t\t);\n\t\t} else if (version < all.length) {\n\t\t\tawait sql.unsafe(`UPDATE ${tables.schema} SET version = $1::int`, [\n\t\t\t\tall.length,\n\t\t\t]);\n\t\t}\n\t});\n}\n",
8
+ "import type { Tables } from './connect';\nimport { COLUMNS } from './rows';\n\n/**\n * The statements a store runs on its tables, written once per store: the\n * table names are its prefix's (checked to be a plain name), and every\n * value is a parameter. Each parameter is cast, so nothing depends on the\n * type a client infers for it; JSON goes as text, cast to `jsonb`.\n */\nexport function statementsOf({ items, messages }: Tables) {\n\treturn {\n\t\t/** $1 id, $2 sender, $3 recipients, $4 size, $5 created_at, $6 message: the item and its message in one statement, or neither. */\n\t\tinsert: `WITH item AS (\n\t\t\tINSERT INTO ${items} (id, sender, recipients, size, created_at,\n\t\t\t\tnext_attempt_at, attempts, delay_notified)\n\t\t\tVALUES ($1, $2, $3::text::jsonb, $4::int, $5::float8, $5::float8, 0, false)\n\t\t\tRETURNING id)\n\t\tINSERT INTO ${messages} (item_id, content) SELECT id, $6::bytea FROM item`,\n\t\t/** Taken before counting, by every add given `maxItems`. */\n\t\tlockAdds: `SELECT pg_advisory_xact_lock(hashtext('${items}')) IS NULL`,\n\t\tcount: `SELECT count(*)::int AS n FROM ${items}`,\n\t\tget: `SELECT ${COLUMNS} FROM ${items} WHERE id = $1`,\n\t\tlist: `SELECT ${COLUMNS} FROM ${items}\n\t\t\tORDER BY next_attempt_at, seq LIMIT $1::int OFFSET $2::int`,\n\t\tmessage: `SELECT content FROM ${messages} WHERE item_id = $1`,\n\t\t/**\n\t\t * $1 owner, $2 now, $3 expiry. The earliest due item no other\n\t\t * transaction holds, locked as it is picked — one being claimed\n\t\t * elsewhere is skipped, not waited for — then leased.\n\t\t */\n\t\tclaim: `UPDATE ${items} SET lease_owner = $1, lease_expires_at = $3::float8\n\t\t\tWHERE seq = (SELECT seq FROM ${items}\n\t\t\t\tWHERE next_attempt_at <= $2::float8\n\t\t\t\t\tAND (lease_owner IS NULL OR lease_expires_at <= $2::float8)\n\t\t\t\tORDER BY next_attempt_at, seq LIMIT 1\n\t\t\t\tFOR UPDATE SKIP LOCKED)\n\t\t\tRETURNING ${COLUMNS}`,\n\t\trenew: `UPDATE ${items} SET lease_expires_at = $1::float8\n\t\t\tWHERE id = $2 AND lease_owner = $3 RETURNING seq`,\n\t\t/** The owner's item, locked until the transaction ends. */\n\t\theld: `SELECT ${COLUMNS} FROM ${items}\n\t\t\tWHERE id = $1 AND lease_owner = $2 FOR UPDATE`,\n\t\t/** $1 id, $2 recipients, $3 next attempt, $4 attempts, $5 delay notified: the lease let go of. */\n\t\trecord: `UPDATE ${items} SET recipients = $2::text::jsonb,\n\t\t\tnext_attempt_at = $3::float8, attempts = $4::int,\n\t\t\tdelay_notified = $5::boolean, lease_owner = NULL,\n\t\t\tlease_expires_at = NULL WHERE id = $1`,\n\t\t/** The message goes with it: ON DELETE CASCADE. */\n\t\tdrop: `DELETE FROM ${items} WHERE id = $1 RETURNING ${COLUMNS}`,\n\t\tmoveDue: `UPDATE ${items} SET next_attempt_at = $1::float8\n\t\t\tWHERE id = $2 RETURNING seq`,\n\t\tgiveBack: `UPDATE ${items} SET next_attempt_at = $1::float8,\n\t\t\tlease_owner = NULL, lease_expires_at = NULL\n\t\t\tWHERE id = $2 AND lease_owner = $3 RETURNING seq`,\n\t} as const;\n}\n\nexport type Statements = ReturnType<typeof statementsOf>;\n",
9
+ "import {\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 { isStorable } from '../text';\nimport { type Connection, connect, type Tables } from './connect';\nimport type {\n\tPostgresClient,\n\tPostgresQueryable,\n\tPostgresQueueStoreOptions,\n} from './options';\nimport { type ItemRow, itemOf, rowsOf } from './rows';\nimport { migrate } from './schema';\nimport { type Statements, statementsOf } from './statements';\n\nexport type {\n\tPostgresClient,\n\tPostgresQueryable,\n\tPostgresQueueStoreOptions,\n} from './options';\n\nconst messageOf = (error: unknown) =>\n\terror instanceof Error ? error.message : String(error);\n\n/**\n * A `QueueStore` on PostgreSQL through Bun's own `Bun.SQL`: several\n * instances, on one machine or many, share one queue. A claim is one\n * `UPDATE … RETURNING` whose item a `SELECT … FOR UPDATE SKIP LOCKED`\n * picks, so two instances never take the same item, and none waits on an\n * item another is taking. Messages live in a table of their own, dropped\n * with their item. The tables are made, or brought up to date, by\n * `migrate()`, or by the first call that needs them.\n */\nexport class PostgresQueueStore implements QueueStore {\n\treadonly #client: PostgresClient;\n\treadonly #owned: boolean;\n\treadonly #tables: Tables;\n\treadonly #q: Statements;\n\t#migrated: Promise<void> | undefined;\n\t#closed = false;\n\n\tprivate constructor({ client, owned, tables }: Connection) {\n\t\tthis.#client = client;\n\t\tthis.#owned = owned;\n\t\tthis.#tables = tables;\n\t\tthis.#q = statementsOf(tables);\n\t}\n\n\t/** Checks the options; connects to nothing until the first call. What is wrong is `INVALID`. */\n\tstatic open(options: PostgresQueueStoreOptions): PostgresQueueStore {\n\t\treturn new PostgresQueueStore(connect(options));\n\t}\n\n\t/**\n\t * Makes the tables, or brings them to the last migration; once per\n\t * store, and safe while other instances do the same. A failure is\n\t * `INVALID`, and the next call tries again.\n\t */\n\tmigrate(): Promise<void> {\n\t\tif (this.#closed) return Promise.reject(closed());\n\t\tthis.#migrated ??= migrate(this.#client, this.#tables).catch((error) => {\n\t\t\tthis.#migrated = undefined;\n\t\t\tif (error instanceof QueueError) throw error;\n\t\t\tthrow new QueueError(\n\t\t\t\t'INVALID',\n\t\t\t\t`The PostgreSQL queue cannot be set up: ${messageOf(error)}`,\n\t\t\t);\n\t\t});\n\t\treturn this.#migrated;\n\t}\n\n\t/** Closes the client the store opened for a URL, never one it was given; closing twice is fine. */\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tif (this.#owned) await this.#client.close();\n\t}\n\n\t/** The client, once the tables are there; `CLOSED` after `close()`. */\n\tasync #sql(): Promise<PostgresClient> {\n\t\tif (this.#closed) throw closed();\n\t\tawait this.migrate();\n\t\treturn this.#client;\n\t}\n\n\tasync #rows<T>(query: string, values: unknown[] = []): Promise<T[]> {\n\t\treturn rowsOf<T>((await this.#sql()).unsafe(query, values));\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 added = newItem(crypto.randomUUID(), item);\n\t\tconst values = [\n\t\t\tadded.id,\n\t\t\tadded.from,\n\t\t\tJSON.stringify(added.recipients),\n\t\t\tadded.size,\n\t\t\tadded.createdAt,\n\t\t\titem.message,\n\t\t];\n\t\tconst sql = await this.#sql();\n\t\tconst q = this.#q;\n\t\tif (max === undefined) {\n\t\t\tawait sql.unsafe(q.insert, values);\n\t\t\treturn added;\n\t\t}\n\t\t// Adds that count wait on one lock, so two never both take the last place.\n\t\tawait sql.begin(async (tx: PostgresQueryable) => {\n\t\t\tawait tx.unsafe(q.lockAdds);\n\t\t\tconst [row] = await rowsOf<{ n: number }>(tx.unsafe(q.count));\n\t\t\tconst n = row?.n ?? 0;\n\t\t\tif (n >= max) {\n\t\t\t\tthrow new QueueError(\n\t\t\t\t\t'QUEUE_FULL',\n\t\t\t\t\t`The queue holds ${n} items, its limit`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait tx.unsafe(q.insert, values);\n\t\t});\n\t\treturn added;\n\t}\n\n\tasync get(id: string): Promise<QueueItem | undefined> {\n\t\tif (!isStorable(id)) return undefined;\n\t\tconst [row] = await this.#rows<ItemRow>(this.#q.get, [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\tconst rows = await this.#rows<ItemRow>(this.#q.list, [limit, offset]);\n\t\treturn rows.map(itemOf);\n\t}\n\n\tasync count(): Promise<number> {\n\t\tconst [row] = await this.#rows<{ n: number }>(this.#q.count);\n\t\treturn row?.n ?? 0;\n\t}\n\n\tasync readMessage(id: string): Promise<Uint8Array | undefined> {\n\t\tif (!isStorable(id)) return undefined;\n\t\tconst [row] = await this.#rows<{ content: Uint8Array }>(this.#q.message, [\n\t\t\tid,\n\t\t]);\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 { owner, now, leaseMs } = request;\n\t\tconst [row] = await this.#rows<ItemRow>(this.#q.claim, [\n\t\t\towner,\n\t\t\tnow,\n\t\t\tnow + leaseMs,\n\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\tif (!isStorable(id)) return false;\n\t\tconst rows = await this.#rows(this.#q.renew, [expiresAt, id, owner]);\n\t\treturn rows.length > 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\tif (!isStorable(id)) return undefined;\n\t\tconst sql = await this.#sql();\n\t\tconst q = this.#q;\n\t\tlet done: QueueItem | undefined;\n\t\tawait sql.begin(async (tx: PostgresQueryable) => {\n\t\t\tconst [row] = await rowsOf<ItemRow>(tx.unsafe(q.held, [id, owner]));\n\t\t\tif (!row) return;\n\t\t\tconst item = applyAttempt(itemOf(row), result);\n\t\t\tif (isDone(item)) {\n\t\t\t\tawait tx.unsafe(q.drop, [id]);\n\t\t\t} else {\n\t\t\t\tawait tx.unsafe(q.record, [\n\t\t\t\t\tid,\n\t\t\t\t\tJSON.stringify(item.recipients),\n\t\t\t\t\titem.nextAttemptAt,\n\t\t\t\t\titem.attempts,\n\t\t\t\t\titem.delayNotified,\n\t\t\t\t]);\n\t\t\t}\n\t\t\tdone = item;\n\t\t});\n\t\treturn done;\n\t}\n\n\tasync reschedule(id: string, at: number, owner?: string): Promise<boolean> {\n\t\tcheckTime('at', at);\n\t\tif (!isStorable(id)) return false;\n\t\tif (owner === undefined) {\n\t\t\treturn (await this.#rows(this.#q.moveDue, [at, id])).length > 0;\n\t\t}\n\t\tcheckOwner(owner);\n\t\treturn (await this.#rows(this.#q.giveBack, [at, id, owner])).length > 0;\n\t}\n\n\tasync cancel(id: string): Promise<QueueItem | undefined> {\n\t\t// An id PostgreSQL cannot hold is one no item has.\n\t\tif (!isStorable(id)) return undefined;\n\t\tconst [row] = await this.#rows<ItemRow>(this.#q.drop, [id]);\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n}\n\nconst closed = () => new QueueError('CLOSED', 'The queue store is closed');\n"
10
+ ],
11
+ "mappings": "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",
12
+ "debugId": "C2B9C7B259B45F6964756E2164756E21",
13
+ "names": []
14
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Where a PostgreSQL queue keeps its items, and the client it reaches
3
+ * them through.
4
+ *
5
+ * Typed by shape rather than as `Bun.SQL`, so the declarations a
6
+ * consumer's `tsc` reads from `@bumail/queue/postgres` name no global of
7
+ * Bun's: a `Bun.SQL` client fits `PostgresClient` (`open.spec.ts` checks
8
+ * it), and so does any client with these methods.
9
+ */
10
+ /** What runs one statement: a client, or the transaction `begin` hands over. */
11
+ export interface PostgresQueryable {
12
+ /** One statement, its parameters as `$1`, `$2`, …; resolves with the rows. */
13
+ unsafe(query: string, values?: unknown[]): PromiseLike<unknown>;
14
+ }
15
+ /** A PostgreSQL client with a pool, such as `new Bun.SQL(url)`. */
16
+ export interface PostgresClient extends PostgresQueryable {
17
+ /** Runs `fn` in a transaction on one connection: committed once it resolves, rolled back if it throws. */
18
+ begin(fn: (sql: PostgresQueryable) => Promise<unknown>): PromiseLike<unknown>;
19
+ close(options?: {
20
+ timeout?: number;
21
+ }): PromiseLike<void>;
22
+ }
23
+ export interface PostgresQueueStoreOptions {
24
+ /**
25
+ * The database: a client of the application's (`new Bun.SQL(url)`,
26
+ * which it sizes and closes itself), or a `postgres://` URL, for which
27
+ * the store opens a client of its own, closed by `close()`.
28
+ */
29
+ readonly sql: PostgresClient | string | URL;
30
+ /**
31
+ * Put before each table's name, so several queues, or a queue and the
32
+ * application, share one database: lowercase letters, digits and
33
+ * underscores, at most 40. Default `bumail_queue_`.
34
+ */
35
+ readonly tablePrefix?: string;
36
+ }
37
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/postgres/options.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,gFAAgF;AAChF,MAAM,WAAW,iBAAiB;IACjC,8EAA8E;IAC9E,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;CAChE;AAED,mEAAmE;AACnE,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACxD,0GAA0G;IAC1G,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAC9E,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,yBAAyB;IACzC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,GAAG,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B"}
@@ -0,0 +1,21 @@
1
+ import type { QueueItem, RecipientState } from '../contract/types';
2
+ /** A row of the items table, as `Bun.SQL` reads it. */
3
+ export interface ItemRow {
4
+ readonly id: string;
5
+ readonly sender: string;
6
+ /** `jsonb`: parsed or not, depending on the client. */
7
+ readonly recipients: string | RecipientState[];
8
+ readonly size: number;
9
+ readonly created_at: number;
10
+ readonly next_attempt_at: number;
11
+ readonly attempts: number;
12
+ readonly delay_notified: boolean;
13
+ readonly lease_owner: string | null;
14
+ readonly lease_expires_at: number | null;
15
+ }
16
+ /** The columns a query reads to build an item. */
17
+ export declare const COLUMNS = "id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at";
18
+ export declare function itemOf(row: ItemRow): QueueItem;
19
+ /** The rows a statement returned. */
20
+ export declare const rowsOf: <T>(pending: PromiseLike<unknown>) => Promise<T[]>;
21
+ //# sourceMappingURL=rows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../src/postgres/rows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnE,uDAAuD;AACvD,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,EAAE,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED,kDAAkD;AAClD,eAAO,MAAM,OAAO,uHACiG,CAAC;AAEtH,wBAAgB,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAoB9C;AAED,qCAAqC;AACrC,eAAO,MAAM,MAAM,GAAU,CAAC,EAAE,SAAS,WAAW,CAAC,OAAO,CAAC,iBACtC,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { Tables } from './connect';
2
+ import type { PostgresClient } from './options';
3
+ /**
4
+ * The migrations, in order, each a list of statements: the schema table's
5
+ * `version` is how many of them the database has run. A migration is
6
+ * never edited once released; a change is a new one at the end. They
7
+ * follow the `bun:sqlite` store's, column for column: times are
8
+ * milliseconds as `double precision` (SQLite's `REAL`), the recipients
9
+ * JSON, the message its bytes, in a table of its own.
10
+ */
11
+ export declare function migrations({ prefix, items, messages, }: Tables): readonly (readonly string[])[];
12
+ /**
13
+ * Brings the tables to the last migration. Tables already current are
14
+ * only read: no lock, no DDL, so a worker's role needs no `CREATE`.
15
+ * Otherwise in one transaction — a migration that fails leaves them as
16
+ * they were — under one advisory lock, so instances starting together
17
+ * run each migration once.
18
+ */
19
+ export declare function migrate(client: PostgresClient, tables: Tables): Promise<void>;
20
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/postgres/schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,WAAW,CAAC;AAGnE;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,EAC1B,MAAM,EACN,KAAK,EACL,QAAQ,GACR,EAAE,MAAM,GAAG,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CA0BzC;AAgCD;;;;;;GAMG;AACH,wBAAsB,OAAO,CAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAiCf"}
@@ -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"}