@bumail/queue 0.1.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/LICENSE +21 -0
- package/README.md +243 -0
- package/dist/chunks/index-p4k4rjb7.js +15 -0
- package/dist/chunks/index-p4k4rjb7.js.map +10 -0
- package/dist/chunks/index-xhsw4xcz.js +119 -0
- package/dist/chunks/index-xhsw4xcz.js.map +10 -0
- package/dist/contract/checks.d.ts +27 -0
- package/dist/contract/checks.d.ts.map +1 -0
- package/dist/contract/queue-store.d.ts +60 -0
- package/dist/contract/queue-store.d.ts.map +1 -0
- package/dist/contract/types.d.ts +102 -0
- package/dist/contract/types.d.ts.map +1 -0
- package/dist/dsn/build.d.ts +35 -0
- package/dist/dsn/build.d.ts.map +1 -0
- package/dist/dsn/content.d.ts +18 -0
- package/dist/dsn/content.d.ts.map +1 -0
- package/dist/dsn/fields.d.ts +38 -0
- package/dist/dsn/fields.d.ts.map +1 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1089 -0
- package/dist/index.js.map +30 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +121 -0
- package/dist/memory/index.js.map +10 -0
- package/dist/memory/store.d.ts +21 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/queue/deliver.d.ts +20 -0
- package/dist/queue/deliver.d.ts.map +1 -0
- package/dist/queue/envelope.d.ts +26 -0
- package/dist/queue/envelope.d.ts.map +1 -0
- package/dist/queue/events.d.ts +48 -0
- package/dist/queue/events.d.ts.map +1 -0
- package/dist/queue/limiter.d.ts +14 -0
- package/dist/queue/limiter.d.ts.map +1 -0
- package/dist/queue/notify.d.ts +19 -0
- package/dist/queue/notify.d.ts.map +1 -0
- package/dist/queue/options.d.ts +111 -0
- package/dist/queue/options.d.ts.map +1 -0
- package/dist/queue/outcome.d.ts +26 -0
- package/dist/queue/outcome.d.ts.map +1 -0
- package/dist/queue/queue.d.ts +43 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/retry.d.ts +16 -0
- package/dist/queue/retry.d.ts.map +1 -0
- package/dist/queue/route.d.ts +10 -0
- package/dist/queue/route.d.ts.map +1 -0
- package/dist/queue/settings/dsn.d.ts +8 -0
- package/dist/queue/settings/dsn.d.ts.map +1 -0
- package/dist/queue/settings/index.d.ts +25 -0
- package/dist/queue/settings/index.d.ts.map +1 -0
- package/dist/queue/settings/limits.d.ts +18 -0
- package/dist/queue/settings/limits.d.ts.map +1 -0
- package/dist/queue/settings/numbers.d.ts +7 -0
- package/dist/queue/settings/numbers.d.ts.map +1 -0
- package/dist/queue/settings/routes.d.ts +3 -0
- package/dist/queue/settings/routes.d.ts.map +1 -0
- package/dist/queue/settings/sessions.d.ts +5 -0
- package/dist/queue/settings/sessions.d.ts.map +1 -0
- package/dist/queue/settle.d.ts +21 -0
- package/dist/queue/settle.d.ts.map +1 -0
- package/dist/queue/worker.d.ts +27 -0
- package/dist/queue/worker.d.ts.map +1 -0
- package/dist/sqlite/index.d.ts +2 -0
- package/dist/sqlite/index.d.ts.map +1 -0
- package/dist/sqlite/index.js +288 -0
- package/dist/sqlite/index.js.map +13 -0
- package/dist/sqlite/open.d.ts +8 -0
- package/dist/sqlite/open.d.ts.map +1 -0
- package/dist/sqlite/options.d.ts +16 -0
- package/dist/sqlite/options.d.ts.map +1 -0
- package/dist/sqlite/rows.d.ts +28 -0
- package/dist/sqlite/rows.d.ts.map +1 -0
- package/dist/sqlite/schema.d.ts +14 -0
- package/dist/sqlite/schema.d.ts.map +1 -0
- package/dist/sqlite/store.d.ts +30 -0
- package/dist/sqlite/store.d.ts.map +1 -0
- package/dist/text.d.ts +14 -0
- package/dist/text.d.ts.map +1 -0
- package/docs/README.md +10 -0
- package/docs/guide.md +461 -0
- package/docs/roadmap.md +67 -0
- package/docs/troubleshooting.md +394 -0
- package/package.json +71 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkTime,
|
|
3
|
+
checkOwner,
|
|
4
|
+
checkNewItem,
|
|
5
|
+
checkMaxItems,
|
|
6
|
+
checkClaim,
|
|
7
|
+
checkResult,
|
|
8
|
+
checkList,
|
|
9
|
+
isDone,
|
|
10
|
+
applyAttempt,
|
|
11
|
+
newItem
|
|
12
|
+
} from "../chunks/index-xhsw4xcz.js";
|
|
13
|
+
import {
|
|
14
|
+
QueueError2
|
|
15
|
+
} from "../chunks/index-p4k4rjb7.js";
|
|
16
|
+
|
|
17
|
+
// src/sqlite/open.ts
|
|
18
|
+
import { Database } from "bun:sqlite";
|
|
19
|
+
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
// src/sqlite/schema.ts
|
|
23
|
+
var MIGRATIONS = [
|
|
24
|
+
`
|
|
25
|
+
CREATE TABLE items (
|
|
26
|
+
seq INTEGER PRIMARY KEY,
|
|
27
|
+
id TEXT NOT NULL UNIQUE,
|
|
28
|
+
sender TEXT NOT NULL,
|
|
29
|
+
recipients TEXT NOT NULL CHECK (json_valid(recipients)),
|
|
30
|
+
size INTEGER NOT NULL,
|
|
31
|
+
created_at REAL NOT NULL,
|
|
32
|
+
next_attempt_at REAL NOT NULL,
|
|
33
|
+
attempts INTEGER NOT NULL,
|
|
34
|
+
delay_notified INTEGER NOT NULL,
|
|
35
|
+
lease_owner TEXT,
|
|
36
|
+
lease_expires_at REAL,
|
|
37
|
+
CHECK ((lease_owner IS NULL) = (lease_expires_at IS NULL))
|
|
38
|
+
) STRICT;
|
|
39
|
+
CREATE INDEX items_due ON items (next_attempt_at, seq);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE messages (
|
|
42
|
+
item_id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE,
|
|
43
|
+
content BLOB NOT NULL
|
|
44
|
+
) STRICT;
|
|
45
|
+
`
|
|
46
|
+
];
|
|
47
|
+
function migrate(db) {
|
|
48
|
+
db.transaction(() => {
|
|
49
|
+
const { user_version: version } = db.query("PRAGMA user_version").get();
|
|
50
|
+
if (version > MIGRATIONS.length) {
|
|
51
|
+
throw new QueueError2("INVALID", `The database is at schema version ${version}, newer than this store's ${MIGRATIONS.length}`);
|
|
52
|
+
}
|
|
53
|
+
for (const sql of MIGRATIONS.slice(version))
|
|
54
|
+
db.exec(sql);
|
|
55
|
+
if (version < MIGRATIONS.length) {
|
|
56
|
+
db.exec(`PRAGMA user_version = ${MIGRATIONS.length}`);
|
|
57
|
+
}
|
|
58
|
+
}).immediate();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/sqlite/open.ts
|
|
62
|
+
var PRIVATE_DIRECTORY = 448;
|
|
63
|
+
var PRIVATE_FILE = 384;
|
|
64
|
+
function configure(db, busyTimeout) {
|
|
65
|
+
db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
66
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
67
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
68
|
+
db.exec("PRAGMA fullfsync = ON");
|
|
69
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
70
|
+
}
|
|
71
|
+
function keepPrivate(file) {
|
|
72
|
+
for (const path of [file, `${file}-wal`, `${file}-shm`]) {
|
|
73
|
+
if (existsSync(path))
|
|
74
|
+
chmodSync(path, PRIVATE_FILE);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function busyTimeoutOf(value) {
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
return 5000;
|
|
80
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
81
|
+
throw new QueueError2("INVALID", `busyTimeout must be an integer of at least 0, not ${value}`);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
86
|
+
function openDatabase(options) {
|
|
87
|
+
const directory = options?.directory;
|
|
88
|
+
if (typeof directory !== "string" || directory === "") {
|
|
89
|
+
throw new QueueError2("INVALID", "A SQLite queue store needs a directory");
|
|
90
|
+
}
|
|
91
|
+
const busyTimeout = busyTimeoutOf(options.busyTimeout);
|
|
92
|
+
let db;
|
|
93
|
+
try {
|
|
94
|
+
if (!existsSync(directory)) {
|
|
95
|
+
mkdirSync(directory, { recursive: true, mode: PRIVATE_DIRECTORY });
|
|
96
|
+
chmodSync(directory, PRIVATE_DIRECTORY);
|
|
97
|
+
}
|
|
98
|
+
const file = join(directory, "queue.sqlite");
|
|
99
|
+
db = new Database(file, { create: true, readwrite: true, strict: true });
|
|
100
|
+
configure(db, busyTimeout);
|
|
101
|
+
keepPrivate(file);
|
|
102
|
+
migrate(db);
|
|
103
|
+
return db;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
db?.close();
|
|
106
|
+
if (error instanceof QueueError2)
|
|
107
|
+
throw error;
|
|
108
|
+
throw new QueueError2("INVALID", `The queue at "${directory}" cannot be opened: ${messageOf(error)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/sqlite/rows.ts
|
|
113
|
+
var COLUMNS = "id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at";
|
|
114
|
+
function itemOf(row) {
|
|
115
|
+
const item = {
|
|
116
|
+
id: row.id,
|
|
117
|
+
from: row.sender,
|
|
118
|
+
recipients: JSON.parse(row.recipients),
|
|
119
|
+
size: row.size,
|
|
120
|
+
createdAt: row.created_at,
|
|
121
|
+
nextAttemptAt: row.next_attempt_at,
|
|
122
|
+
attempts: row.attempts,
|
|
123
|
+
delayNotified: row.delay_notified === 1
|
|
124
|
+
};
|
|
125
|
+
if (row.lease_owner === null || row.lease_expires_at === null)
|
|
126
|
+
return item;
|
|
127
|
+
return {
|
|
128
|
+
...item,
|
|
129
|
+
lease: { owner: row.lease_owner, expiresAt: row.lease_expires_at }
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function stateOf(item) {
|
|
133
|
+
return {
|
|
134
|
+
id: item.id,
|
|
135
|
+
recipients: JSON.stringify(item.recipients),
|
|
136
|
+
next_attempt_at: item.nextAttemptAt,
|
|
137
|
+
attempts: item.attempts,
|
|
138
|
+
delay_notified: item.delayNotified ? 1 : 0,
|
|
139
|
+
lease_owner: item.lease?.owner ?? null,
|
|
140
|
+
lease_expires_at: item.lease?.expiresAt ?? null
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/sqlite/store.ts
|
|
145
|
+
var DUE = `next_attempt_at <= $now
|
|
146
|
+
AND (lease_owner IS NULL OR lease_expires_at <= $now)`;
|
|
147
|
+
|
|
148
|
+
class SqliteQueueStore {
|
|
149
|
+
#db;
|
|
150
|
+
#closed = false;
|
|
151
|
+
constructor(db) {
|
|
152
|
+
this.#db = db;
|
|
153
|
+
}
|
|
154
|
+
static open(options) {
|
|
155
|
+
return new SqliteQueueStore(openDatabase(options));
|
|
156
|
+
}
|
|
157
|
+
close() {
|
|
158
|
+
if (this.#closed)
|
|
159
|
+
return;
|
|
160
|
+
this.#closed = true;
|
|
161
|
+
this.#db.close();
|
|
162
|
+
}
|
|
163
|
+
get #open() {
|
|
164
|
+
if (this.#closed) {
|
|
165
|
+
throw new QueueError2("CLOSED", "The queue store is closed");
|
|
166
|
+
}
|
|
167
|
+
return this.#db;
|
|
168
|
+
}
|
|
169
|
+
#row(id, owner) {
|
|
170
|
+
const db = this.#open;
|
|
171
|
+
if (owner === undefined) {
|
|
172
|
+
return db.query(`SELECT ${COLUMNS} FROM items WHERE id = ?`).get(id);
|
|
173
|
+
}
|
|
174
|
+
return db.query(`SELECT ${COLUMNS} FROM items WHERE id = ? AND lease_owner = ?`).get(id, owner);
|
|
175
|
+
}
|
|
176
|
+
#write(item) {
|
|
177
|
+
this.#open.query(`UPDATE items SET recipients = $recipients,
|
|
178
|
+
next_attempt_at = $next_attempt_at, attempts = $attempts,
|
|
179
|
+
delay_notified = $delay_notified, lease_owner = $lease_owner,
|
|
180
|
+
lease_expires_at = $lease_expires_at WHERE id = $id`).run(stateOf(item));
|
|
181
|
+
}
|
|
182
|
+
async add(item, options = {}) {
|
|
183
|
+
checkNewItem(item);
|
|
184
|
+
const max = checkMaxItems(options.maxItems);
|
|
185
|
+
const db = this.#open;
|
|
186
|
+
const added = newItem(crypto.randomUUID(), item);
|
|
187
|
+
db.transaction(() => {
|
|
188
|
+
if (max !== undefined) {
|
|
189
|
+
const { n } = db.query("SELECT count(*) AS n FROM items").get();
|
|
190
|
+
if (n >= max) {
|
|
191
|
+
throw new QueueError2("QUEUE_FULL", `The queue holds ${n} items, its limit`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
db.query(`INSERT INTO items (id, sender, recipients, size, created_at,
|
|
195
|
+
next_attempt_at, attempts, delay_notified)
|
|
196
|
+
VALUES ($id, $sender, $recipients, $size, $created_at,
|
|
197
|
+
$next_attempt_at, 0, 0)`).run({
|
|
198
|
+
id: added.id,
|
|
199
|
+
sender: added.from,
|
|
200
|
+
recipients: JSON.stringify(added.recipients),
|
|
201
|
+
size: added.size,
|
|
202
|
+
created_at: added.createdAt,
|
|
203
|
+
next_attempt_at: added.nextAttemptAt
|
|
204
|
+
});
|
|
205
|
+
db.query("INSERT INTO messages (item_id, content) VALUES (?, ?)").run(added.id, item.message);
|
|
206
|
+
}).immediate();
|
|
207
|
+
return added;
|
|
208
|
+
}
|
|
209
|
+
async get(id) {
|
|
210
|
+
const row = this.#row(id);
|
|
211
|
+
return row ? itemOf(row) : undefined;
|
|
212
|
+
}
|
|
213
|
+
async list(options) {
|
|
214
|
+
const { offset, limit } = checkList(options);
|
|
215
|
+
return this.#open.query(`SELECT ${COLUMNS} FROM items ORDER BY next_attempt_at, seq
|
|
216
|
+
LIMIT ? OFFSET ?`).all(limit, offset).map(itemOf);
|
|
217
|
+
}
|
|
218
|
+
async count() {
|
|
219
|
+
const row = this.#open.query("SELECT count(*) AS n FROM items").get();
|
|
220
|
+
return row?.n ?? 0;
|
|
221
|
+
}
|
|
222
|
+
async readMessage(id) {
|
|
223
|
+
const row = this.#open.query("SELECT content FROM messages WHERE item_id = ?").get(id);
|
|
224
|
+
return row ? new Uint8Array(row.content) : undefined;
|
|
225
|
+
}
|
|
226
|
+
async claim(request) {
|
|
227
|
+
checkClaim(request);
|
|
228
|
+
const row = this.#open.query(`UPDATE items SET lease_owner = $owner, lease_expires_at = $expires
|
|
229
|
+
WHERE seq = (SELECT seq FROM items WHERE ${DUE}
|
|
230
|
+
ORDER BY next_attempt_at, seq LIMIT 1)
|
|
231
|
+
RETURNING ${COLUMNS}`).get({
|
|
232
|
+
owner: request.owner,
|
|
233
|
+
now: request.now,
|
|
234
|
+
expires: request.now + request.leaseMs
|
|
235
|
+
});
|
|
236
|
+
return row ? itemOf(row) : undefined;
|
|
237
|
+
}
|
|
238
|
+
async renew(id, owner, expiresAt) {
|
|
239
|
+
checkOwner(owner);
|
|
240
|
+
checkTime("expiresAt", expiresAt);
|
|
241
|
+
const { changes } = this.#open.query("UPDATE items SET lease_expires_at = ? WHERE id = ? AND lease_owner = ?").run(expiresAt, id, owner);
|
|
242
|
+
return changes > 0;
|
|
243
|
+
}
|
|
244
|
+
async complete(id, owner, result) {
|
|
245
|
+
checkOwner(owner);
|
|
246
|
+
checkResult(result);
|
|
247
|
+
const db = this.#open;
|
|
248
|
+
return db.transaction(() => {
|
|
249
|
+
const row = this.#row(id, owner);
|
|
250
|
+
if (!row)
|
|
251
|
+
return;
|
|
252
|
+
const item = applyAttempt(itemOf(row), result);
|
|
253
|
+
if (isDone(item)) {
|
|
254
|
+
db.query("DELETE FROM items WHERE id = ?").run(id);
|
|
255
|
+
} else {
|
|
256
|
+
this.#write(item);
|
|
257
|
+
}
|
|
258
|
+
return item;
|
|
259
|
+
}).immediate();
|
|
260
|
+
}
|
|
261
|
+
async reschedule(id, at, owner) {
|
|
262
|
+
checkTime("at", at);
|
|
263
|
+
const db = this.#open;
|
|
264
|
+
if (owner === undefined) {
|
|
265
|
+
return db.query("UPDATE items SET next_attempt_at = ? WHERE id = ?").run(at, id).changes > 0;
|
|
266
|
+
}
|
|
267
|
+
checkOwner(owner);
|
|
268
|
+
const { changes } = db.query(`UPDATE items SET next_attempt_at = ?, lease_owner = NULL,
|
|
269
|
+
lease_expires_at = NULL WHERE id = ? AND lease_owner = ?`).run(at, id, owner);
|
|
270
|
+
return changes > 0;
|
|
271
|
+
}
|
|
272
|
+
async cancel(id) {
|
|
273
|
+
const db = this.#open;
|
|
274
|
+
return db.transaction(() => {
|
|
275
|
+
const row = this.#row(id);
|
|
276
|
+
if (!row)
|
|
277
|
+
return;
|
|
278
|
+
db.query("DELETE FROM items WHERE id = ?").run(id);
|
|
279
|
+
return itemOf(row);
|
|
280
|
+
}).immediate();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
export {
|
|
284
|
+
SqliteQueueStore
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
//# debugId=90554FFDDCF51D9864756E2164756E21
|
|
288
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/sqlite/open.ts", "../src/sqlite/schema.ts", "../src/sqlite/rows.ts", "../src/sqlite/store.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { Database } from 'bun:sqlite';\nimport { chmodSync, existsSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { QueueError } from '../errors';\nimport type { SqliteQueueStoreOptions } from './options';\nimport { migrate } from './schema';\n\n/**\n * The queue's files, and the directory when the store makes it: its\n * owner's alone, as mail is. A directory that already exists keeps its\n * mode — it is the operator's, who may share it with a group on purpose.\n */\nconst PRIVATE_DIRECTORY = 0o700;\nconst PRIVATE_FILE = 0o600;\n\n/**\n * Several processes may share the database: WAL lets readers go on while\n * one writes, and a writer waits up to `busy_timeout` for another. Every\n * write is flushed before it is acknowledged.\n */\nfunction configure(db: Database, busyTimeout: number): void {\n\tdb.exec(`PRAGMA busy_timeout = ${busyTimeout}`);\n\tdb.exec('PRAGMA journal_mode = WAL');\n\tdb.exec('PRAGMA synchronous = FULL');\n\t// On macOS a plain fsync leaves the data in the drive's cache.\n\tdb.exec('PRAGMA fullfsync = ON');\n\tdb.exec('PRAGMA foreign_keys = ON');\n}\n\nfunction keepPrivate(file: string): void {\n\tfor (const path of [file, `${file}-wal`, `${file}-shm`]) {\n\t\tif (existsSync(path)) chmodSync(path, PRIVATE_FILE);\n\t}\n}\n\nfunction busyTimeoutOf(value: unknown): number {\n\tif (value === undefined) return 5000;\n\tif (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n\t\tthrow new QueueError(\n\t\t\t'INVALID',\n\t\t\t`busyTimeout must be an integer of at least 0, not ${value}`,\n\t\t);\n\t}\n\treturn value;\n}\n\nconst messageOf = (error: unknown) =>\n\terror instanceof Error ? error.message : String(error);\n\n/**\n * Opens the queue's database in `directory`, at the last migration.\n * Whatever fails is `INVALID`, naming the directory.\n */\nexport function openDatabase(options: SqliteQueueStoreOptions): Database {\n\tconst directory = (options as SqliteQueueStoreOptions | undefined)?.directory;\n\tif (typeof directory !== 'string' || directory === '') {\n\t\tthrow new QueueError('INVALID', 'A SQLite queue store needs a directory');\n\t}\n\tconst busyTimeout = busyTimeoutOf(options.busyTimeout);\n\tlet db: Database | undefined;\n\ttry {\n\t\tif (!existsSync(directory)) {\n\t\t\tmkdirSync(directory, { recursive: true, mode: PRIVATE_DIRECTORY });\n\t\t\t// The mode given to mkdir is narrowed by the umask, never widened: set it.\n\t\t\tchmodSync(directory, PRIVATE_DIRECTORY);\n\t\t}\n\t\tconst file = join(directory, 'queue.sqlite');\n\t\tdb = new Database(file, { create: true, readwrite: true, strict: true });\n\t\tconfigure(db, busyTimeout);\n\t\tkeepPrivate(file);\n\t\tmigrate(db);\n\t\treturn db;\n\t} catch (error) {\n\t\tdb?.close();\n\t\tif (error instanceof QueueError) throw error;\n\t\tthrow new QueueError(\n\t\t\t'INVALID',\n\t\t\t`The queue at \"${directory}\" cannot be opened: ${messageOf(error)}`,\n\t\t);\n\t}\n}\n",
|
|
6
|
+
"import type { Database } from 'bun:sqlite';\nimport { QueueError } from '../errors';\n\n/**\n * The migrations, in order: the database's `PRAGMA user_version` is how many\n * of them it has run. A migration is never edited once released; a change\n * is a new one at the end.\n */\nexport const MIGRATIONS: readonly string[] = [\n\t// 1: the items, and their messages in a table of their own.\n\t`\n\tCREATE TABLE items (\n\t\tseq INTEGER PRIMARY KEY,\n\t\tid TEXT NOT NULL UNIQUE,\n\t\tsender TEXT NOT NULL,\n\t\trecipients TEXT NOT NULL CHECK (json_valid(recipients)),\n\t\tsize INTEGER NOT NULL,\n\t\tcreated_at REAL NOT NULL,\n\t\tnext_attempt_at REAL NOT NULL,\n\t\tattempts INTEGER NOT NULL,\n\t\tdelay_notified INTEGER NOT NULL,\n\t\tlease_owner TEXT,\n\t\tlease_expires_at REAL,\n\t\tCHECK ((lease_owner IS NULL) = (lease_expires_at IS NULL))\n\t) STRICT;\n\tCREATE INDEX items_due ON items (next_attempt_at, seq);\n\n\tCREATE TABLE messages (\n\t\titem_id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE,\n\t\tcontent BLOB NOT NULL\n\t) STRICT;\n\t`,\n];\n\n/**\n * Brings the database to the last migration, in one transaction: a\n * migration that fails leaves the database as it was. A database written\n * by a newer store, with more migrations than this one knows, is refused.\n */\nexport function migrate(db: Database): void {\n\tdb.transaction(() => {\n\t\tconst { user_version: version } = db\n\t\t\t.query<{ user_version: number }, []>('PRAGMA user_version')\n\t\t\t.get() as { user_version: number };\n\t\tif (version > MIGRATIONS.length) {\n\t\t\tthrow new QueueError(\n\t\t\t\t'INVALID',\n\t\t\t\t`The database is at schema version ${version}, newer than this store's ${MIGRATIONS.length}`,\n\t\t\t);\n\t\t}\n\t\tfor (const sql of MIGRATIONS.slice(version)) db.exec(sql);\n\t\tif (version < MIGRATIONS.length) {\n\t\t\tdb.exec(`PRAGMA user_version = ${MIGRATIONS.length}`);\n\t\t}\n\t}).immediate();\n}\n",
|
|
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
|
+
"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
|
+
],
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "90554FFDDCF51D9864756E2164756E21",
|
|
12
|
+
"names": []
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import type { SqliteQueueStoreOptions } from './options';
|
|
3
|
+
/**
|
|
4
|
+
* Opens the queue's database in `directory`, at the last migration.
|
|
5
|
+
* Whatever fails is `INVALID`, naming the directory.
|
|
6
|
+
*/
|
|
7
|
+
export declare function openDatabase(options: SqliteQueueStoreOptions): Database;
|
|
8
|
+
//# sourceMappingURL=open.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/sqlite/open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AA6CzD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,uBAAuB,GAAG,QAAQ,CA2BvE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a SQLite queue keeps its items.
|
|
3
|
+
*
|
|
4
|
+
* Apart from `open.ts`, which imports `bun:sqlite`, so the declarations a
|
|
5
|
+
* consumer's `tsc` reads from `@bumail/queue/sqlite` never name it.
|
|
6
|
+
*/
|
|
7
|
+
export interface SqliteQueueStoreOptions {
|
|
8
|
+
/** Holds `queue.sqlite`; created if need be. */
|
|
9
|
+
readonly directory: string;
|
|
10
|
+
/**
|
|
11
|
+
* How long a write waits for another process holding the database, in
|
|
12
|
+
* milliseconds. Default 5000.
|
|
13
|
+
*/
|
|
14
|
+
readonly busyTimeout?: number;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/sqlite/options.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { QueueItem } from '../contract/types';
|
|
2
|
+
/** A row of `items`, as `bun:sqlite` reads it. */
|
|
3
|
+
export interface ItemRow {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly sender: string;
|
|
6
|
+
readonly recipients: string;
|
|
7
|
+
readonly size: number;
|
|
8
|
+
readonly created_at: number;
|
|
9
|
+
readonly next_attempt_at: number;
|
|
10
|
+
readonly attempts: number;
|
|
11
|
+
readonly delay_notified: number;
|
|
12
|
+
readonly lease_owner: string | null;
|
|
13
|
+
readonly lease_expires_at: number | null;
|
|
14
|
+
}
|
|
15
|
+
/** The columns a query reads to build an item. */
|
|
16
|
+
export declare const COLUMNS = "id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at";
|
|
17
|
+
export declare function itemOf(row: ItemRow): QueueItem;
|
|
18
|
+
/** The named parameters that write an item's state back. */
|
|
19
|
+
export declare function stateOf(item: QueueItem): {
|
|
20
|
+
id: string;
|
|
21
|
+
recipients: string;
|
|
22
|
+
next_attempt_at: number;
|
|
23
|
+
attempts: number;
|
|
24
|
+
delay_notified: number;
|
|
25
|
+
lease_owner: string | null;
|
|
26
|
+
lease_expires_at: number | null;
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=rows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../src/sqlite/rows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,mBAAmB,CAAC;AAEnE,kDAAkD;AAClD,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,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,MAAM,CAAC;IAChC,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,CAgB9C;AAED,4DAA4D;AAC5D,wBAAgB,OAAO,CAAC,IAAI,EAAE,SAAS;;;;;;;;EAUtC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
/**
|
|
3
|
+
* The migrations, in order: the database's `PRAGMA user_version` is how many
|
|
4
|
+
* of them it has run. A migration is never edited once released; a change
|
|
5
|
+
* is a new one at the end.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MIGRATIONS: readonly string[];
|
|
8
|
+
/**
|
|
9
|
+
* Brings the database to the last migration, in one transaction: a
|
|
10
|
+
* migration that fails leaves the database as it was. A database written
|
|
11
|
+
* by a newer store, with more migrations than this one knows, is refused.
|
|
12
|
+
*/
|
|
13
|
+
export declare function migrate(db: Database): void;
|
|
14
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/sqlite/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,MAAM,EAwBvC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAgB1C"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { QueueStore } from '../contract/queue-store';
|
|
2
|
+
import type { AddOptions, AttemptResult, ClaimRequest, NewQueueItem, QueueItem, QueueListOptions } from '../contract/types';
|
|
3
|
+
import type { SqliteQueueStoreOptions } from './options';
|
|
4
|
+
export type { SqliteQueueStoreOptions } from './options';
|
|
5
|
+
/**
|
|
6
|
+
* A `QueueStore` on `bun:sqlite`: one directory holding `queue.sqlite`,
|
|
7
|
+
* which several processes on one machine may open together. Every
|
|
8
|
+
* operation is one transaction — a claim is a single `UPDATE … RETURNING`
|
|
9
|
+
* — so two workers never take the same item. Messages live in a table of
|
|
10
|
+
* their own, dropped with their item.
|
|
11
|
+
*/
|
|
12
|
+
export declare class SqliteQueueStore implements QueueStore {
|
|
13
|
+
#private;
|
|
14
|
+
private constructor();
|
|
15
|
+
/** Opens the store in `directory`, creating it if need be; what fails is `INVALID`. */
|
|
16
|
+
static open(options: SqliteQueueStoreOptions): SqliteQueueStore;
|
|
17
|
+
/** Lets go of the database; closing twice is fine. */
|
|
18
|
+
close(): void;
|
|
19
|
+
add(item: NewQueueItem, options?: AddOptions): Promise<QueueItem>;
|
|
20
|
+
get(id: string): Promise<QueueItem | undefined>;
|
|
21
|
+
list(options?: QueueListOptions): Promise<QueueItem[]>;
|
|
22
|
+
count(): Promise<number>;
|
|
23
|
+
readMessage(id: string): Promise<Uint8Array | undefined>;
|
|
24
|
+
claim(request: ClaimRequest): Promise<QueueItem | undefined>;
|
|
25
|
+
renew(id: string, owner: string, expiresAt: number): Promise<boolean>;
|
|
26
|
+
complete(id: string, owner: string, result: AttemptResult): Promise<QueueItem | undefined>;
|
|
27
|
+
reschedule(id: string, at: number, owner?: string): Promise<boolean>;
|
|
28
|
+
cancel(id: string): Promise<QueueItem | undefined>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sqlite/store.ts"],"names":[],"mappings":"AAaA,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;AAG3B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAGzD,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAKzD;;;;;;GAMG;AACH,qBAAa,gBAAiB,YAAW,UAAU;;IAIlD,OAAO;IAIP,uFAAuF;IACvF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB;IAI/D,sDAAsD;IACtD,KAAK,IAAI,IAAI;IAsCP,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC;IAsCrE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAK/C,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAWtD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAOxB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IASxD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAiB5D,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrE,QAAQ,CACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAmB3B,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBpE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;CAWxD"}
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Text from the other side, made safe to keep and to write in a header field. */
|
|
2
|
+
/** Whether the text holds a control character. */
|
|
3
|
+
export declare function hasControl(text: string): boolean;
|
|
4
|
+
/** Each control character replaced by `by`. */
|
|
5
|
+
export declare function replaceControls(text: string, by: string): string;
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
export declare function cleanText(text: string, max: number): string;
|
|
12
|
+
/** `cleanText`, then anything not printable ASCII as `?`: for a `message/delivery-status` field. */
|
|
13
|
+
export declare const asciiText: (text: string, max: number) => string;
|
|
14
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +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"}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @bumail/queue documentation
|
|
2
|
+
|
|
3
|
+
The [package README](../README.md) is the short version. This folder is the
|
|
4
|
+
long one.
|
|
5
|
+
|
|
6
|
+
| Page | Read it when |
|
|
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 |
|
|
9
|
+
| [Troubleshooting](troubleshooting.md) | the queue or a store threw a `QueueError`, the `error` event fired, or mail stays deferred |
|
|
10
|
+
| [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
|