@sproutboat/wire 0.19.0 → 0.20.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/CHANGELOG.md +51 -0
- package/package.json +2 -2
- package/src/broker.test.ts +61 -0
- package/src/broker.ts +125 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @sproutboat/wire
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 482a55c: Queues like Cloudflare's, from compatibility date 2026-10-31, with queue names and consumer
|
|
8
|
+
settings in `sproutboat.jsonc` at every date.
|
|
9
|
+
|
|
10
|
+
- **Config (`@sproutboat/config`):** a `queues` entry may be `{ "binding": "QUEUE", "queue": "q",
|
|
11
|
+
"max_batch_size": 10, "max_batch_timeout": 1, "max_retries": 3, "retry_delay": 0,
|
|
12
|
+
"dead_letter_queue": "dlq" }`, the settings named as in wrangler's `queues.consumers`; `id`
|
|
13
|
+
is optional there. `queueSettings()` normalizes the entries for the build, and `pinBindingId`
|
|
14
|
+
adds an id to such an object.
|
|
15
|
+
- **Names and settings, at every date (opt-in):** `batch.queue` is the queue's name (the binding
|
|
16
|
+
name when unset). `max_batch_size` (1 to 100, default 10) and `max_batch_timeout` (0 to 60 s,
|
|
17
|
+
default 0: delivered at once, where Cloudflare waits up to 5 s) shape batches; `max_retries`,
|
|
18
|
+
`retry_delay` and `dead_letter_queue` decide what a retry does. A dead letter queue the worker
|
|
19
|
+
doesn't bind keeps its messages until one does.
|
|
20
|
+
- **From 2026-10-31:**
|
|
21
|
+
- `send()` and `sendBatch()` return Promises of `{ metadata: { metrics } }`, the queue's backlog
|
|
22
|
+
after the send as Cloudflare documents it (miniflare answers zeros); `metrics()` is added.
|
|
23
|
+
- Bodies keep their content type: `json` (the default), `text`, `bytes` (arrives as an
|
|
24
|
+
ArrayBuffer) and `v8` (structured clone). A string that looks like JSON or a number arrives as
|
|
25
|
+
the same string.
|
|
26
|
+
- workerd's argument errors: an undefined or BigInt `json` body, a `text` body that isn't a
|
|
27
|
+
string, an unknown content type, a body over 128 KiB, an empty batch, more than 100 messages
|
|
28
|
+
or 256 KiB in a batch, a batch that isn't iterable.
|
|
29
|
+
- A message is `{ id, timestamp, body, attempts }`, `timestamp` a Date of when it was sent, with
|
|
30
|
+
`ack()` and `retry({ delaySeconds })` on its prototype; a batch has `queue`, `messages` and
|
|
31
|
+
`metadata`, with `ackAll()` and `retryAll({ delaySeconds })`. The first of `ack()` and `retry()`
|
|
32
|
+
wins, and a message's own call wins over `ackAll()`/`retryAll()`, as Cloudflare documents
|
|
33
|
+
(miniflare lets a later `retryAll()` win).
|
|
34
|
+
- By default a message is retried at once, 3 times, then dropped or sent to the dead letter
|
|
35
|
+
queue. Earlier dates keep 5 s between attempts and 5 attempts.
|
|
36
|
+
- **Stores (embedded transport and `@sproutboat/wire`'s broker):** messages keep their send time
|
|
37
|
+
(an additive `sent_at` column), `queue.send` answers the backlog when asked, `queue.metrics` is
|
|
38
|
+
new, and a batch's outcome may carry retry delays and messages for the dead letter queue. An
|
|
39
|
+
older broker ignores those and retries as before; bodies sent from 2026-10-31 are tagged text, so
|
|
40
|
+
they travel through it unchanged.
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- Updated dependencies [482a55c]
|
|
45
|
+
- @sproutboat/runtime@0.32.0
|
|
46
|
+
|
|
47
|
+
## 0.19.1
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [a04d0aa]
|
|
52
|
+
- @sproutboat/runtime@0.31.0
|
|
53
|
+
|
|
3
54
|
## 0.19.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutboat/wire",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,6 +23,6 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@sproutboat/assets": "^0.3.0",
|
|
26
|
-
"@sproutboat/runtime": "^0.
|
|
26
|
+
"@sproutboat/runtime": "^0.32.0"
|
|
27
27
|
}
|
|
28
28
|
}
|
package/src/broker.test.ts
CHANGED
|
@@ -1025,6 +1025,67 @@ test("Queues: send enqueues; the consumer delivers a batch and acked messages ar
|
|
|
1025
1025
|
await expect(b.dispatch({ op: "queue.send", queue: "NOPE", body: "x" })).rejects.toThrow("not bound");
|
|
1026
1026
|
});
|
|
1027
1027
|
|
|
1028
|
+
test("Queues: send reports the backlog; batches carry send times, metrics and the batch settings", async () => {
|
|
1029
|
+
const delivered: JsonObject[] = [];
|
|
1030
|
+
const fetchImpl: FetchLike = async (_url, init) => {
|
|
1031
|
+
const body = obj(parseJsonValue(String(init?.body)));
|
|
1032
|
+
delivered.push(body);
|
|
1033
|
+
return new Response(JSON.stringify({ ack: arr(body.messages).map((m) => obj(m).id), retry: [] }), { status: 200 });
|
|
1034
|
+
};
|
|
1035
|
+
const b = make({
|
|
1036
|
+
bindings: {
|
|
1037
|
+
queues: ["JOBS"],
|
|
1038
|
+
queueConfig: [{ binding: "JOBS", queue: "jobs", maxBatchSize: 2, maxBatchTimeout: 1 }],
|
|
1039
|
+
},
|
|
1040
|
+
sproutUrl: "http://127.0.0.1:1/",
|
|
1041
|
+
fetchImpl,
|
|
1042
|
+
});
|
|
1043
|
+
const before = Date.now();
|
|
1044
|
+
const sent = await b.dispatch({ op: "queue.send", queue: "JOBS", body: "one", metrics: true });
|
|
1045
|
+
expect(obj(sent.metrics)).toMatchObject({ backlogCount: 1, backlogBytes: 3 });
|
|
1046
|
+
expect(Number(obj(sent.metrics).oldestMessageTimestamp)).toBeGreaterThanOrEqual(before);
|
|
1047
|
+
expect(await b.dispatch({ op: "queue.metrics", queue: "JOBS" })).toMatchObject({ metrics: { backlogCount: 1 } });
|
|
1048
|
+
// One message waits for max_batch_timeout (1 s); three fill a batch of two at once.
|
|
1049
|
+
await Bun.sleep(700);
|
|
1050
|
+
expect(delivered.length).toBe(0);
|
|
1051
|
+
await Bun.sleep(900);
|
|
1052
|
+
expect(delivered.length).toBe(1);
|
|
1053
|
+
const batch = obj(delivered[0]);
|
|
1054
|
+
expect(Number(obj(arr(batch.messages)[0]).timestamp)).toBeGreaterThanOrEqual(before);
|
|
1055
|
+
expect(obj(obj(batch.metadata).metrics).backlogCount).toBe(1);
|
|
1056
|
+
await b.dispatch({ op: "queue.send_batch", queue: "JOBS", messages: [{ body: "a" }, { body: "b" }, { body: "c" }] });
|
|
1057
|
+
await Bun.sleep(600);
|
|
1058
|
+
expect(arr(obj(delivered[1]).messages).length).toBe(2);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test("Queues: retry delays from the sprout, and messages past max_retries move to the dead letter queue", async () => {
|
|
1062
|
+
const delivered: JsonObject[] = [];
|
|
1063
|
+
const fetchImpl: FetchLike = async (_url, init) => {
|
|
1064
|
+
const body = obj(parseJsonValue(String(init?.body)));
|
|
1065
|
+
delivered.push(body);
|
|
1066
|
+
const ids = arr(body.messages).map((m) => String(obj(m).id));
|
|
1067
|
+
if (body.queue === "DLQ") return new Response(JSON.stringify({ ack: ids, retry: [] }), { status: 200 });
|
|
1068
|
+
// The first delivery retries at once; the second sends the message to the dead letter queue.
|
|
1069
|
+
const second = Number(obj(arr(body.messages)[0]).attempts) > 1;
|
|
1070
|
+
return new Response(
|
|
1071
|
+
JSON.stringify(
|
|
1072
|
+
second
|
|
1073
|
+
? { ack: [], retry: ids, delays: {}, dead: ids, deadLetter: "DLQ" }
|
|
1074
|
+
: { ack: [], retry: ids, delays: Object.fromEntries(ids.map((id) => [id, 0])), dead: [] },
|
|
1075
|
+
),
|
|
1076
|
+
{ status: 200 },
|
|
1077
|
+
);
|
|
1078
|
+
};
|
|
1079
|
+
const b = make({ bindings: { queues: ["JOBS", "DLQ"] }, sproutUrl: "http://127.0.0.1:1/", fetchImpl });
|
|
1080
|
+
await b.dispatch({ op: "queue.send", queue: "JOBS", body: "work" });
|
|
1081
|
+
await Bun.sleep(1800);
|
|
1082
|
+
expect(delivered.map((d) => [d.queue, obj(arr(d.messages)[0]).attempts, obj(arr(d.messages)[0]).body])).toEqual([
|
|
1083
|
+
["JOBS", 1, "work"],
|
|
1084
|
+
["JOBS", 2, "work"],
|
|
1085
|
+
["DLQ", 1, "work"],
|
|
1086
|
+
]);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1028
1089
|
test("assets.get: serves a bound file, falls back per not_found_handling", async () => {
|
|
1029
1090
|
const dir = mkdtempSync(join(tmpdir(), "sb-assets-"));
|
|
1030
1091
|
const assetsDir = join(dir, "assets");
|
package/src/broker.ts
CHANGED
|
@@ -48,6 +48,15 @@ export type Bindings = {
|
|
|
48
48
|
d1: string[];
|
|
49
49
|
r2: string[];
|
|
50
50
|
queues: string[];
|
|
51
|
+
/** Each queue binding's name and consumer settings (`queueSettings` in @sproutboat/config).
|
|
52
|
+
* The broker reads the batch settings; the sprout applies the retry settings. */
|
|
53
|
+
queueConfig?: Array<{
|
|
54
|
+
binding: string;
|
|
55
|
+
queue: string;
|
|
56
|
+
maxBatchSize?: number;
|
|
57
|
+
maxBatchTimeout?: number;
|
|
58
|
+
maxRetries?: number;
|
|
59
|
+
}>;
|
|
51
60
|
analytics: string[];
|
|
52
61
|
do: Array<{ binding: string; className: string }>;
|
|
53
62
|
/** #48 — worker-to-worker: binding name -> the project it calls. */
|
|
@@ -369,6 +378,14 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
369
378
|
.some((c) => c.name === "claim_token")
|
|
370
379
|
)
|
|
371
380
|
conn.exec("ALTER TABLE mq ADD COLUMN claim_token TEXT NOT NULL DEFAULT ''");
|
|
381
|
+
// When the message was sent (message.timestamp); 0 for messages queued before the column.
|
|
382
|
+
if (
|
|
383
|
+
!conn
|
|
384
|
+
.query<{ name: string }, []>("PRAGMA table_info(mq)")
|
|
385
|
+
.all()
|
|
386
|
+
.some((c) => c.name === "sent_at")
|
|
387
|
+
)
|
|
388
|
+
conn.exec("ALTER TABLE mq ADD COLUMN sent_at INTEGER NOT NULL DEFAULT 0");
|
|
372
389
|
return conn;
|
|
373
390
|
};
|
|
374
391
|
|
|
@@ -647,6 +664,20 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
647
664
|
const requireD1 = bound(bindings.d1, "D1 database");
|
|
648
665
|
const requireR2 = bound(bindings.r2, "R2 bucket");
|
|
649
666
|
const requireQueue = bound(bindings.queues, "queue");
|
|
667
|
+
/** A queue's backlog as Queues reports it: live messages, their bytes, the oldest send time. */
|
|
668
|
+
const queueBacklog = (store: Database, part: string): JsonObject => {
|
|
669
|
+
const r = store
|
|
670
|
+
.query<{ n: number; bytes: number; oldest: number | null }, [string]>(
|
|
671
|
+
"SELECT COUNT(*) AS n, COALESCE(SUM(LENGTH(CAST(body AS BLOB))), 0) AS bytes, MIN(NULLIF(sent_at, 0)) AS oldest FROM mq WHERE queue = ? AND dead = 0",
|
|
672
|
+
)
|
|
673
|
+
.get(part);
|
|
674
|
+
return { backlogCount: r?.n ?? 0, backlogBytes: r?.bytes ?? 0, oldestMessageTimestamp: r?.oldest ?? 0 };
|
|
675
|
+
};
|
|
676
|
+
/** Where a queue partition named by the sprout lives: a bound queue, or "#name" (a dead
|
|
677
|
+
* letter queue the sprout does not bind) in the per-broker db. */
|
|
678
|
+
const queueStore = (part: string): { store: Database; part: string } =>
|
|
679
|
+
part.startsWith("#") ? { store: db, part } : storeFor("queue", requireQueue(part));
|
|
680
|
+
const queueSettingsOf = (binding: string) => bindings.queueConfig?.find((q) => q.binding === binding);
|
|
650
681
|
const requireAe = bound(bindings.analytics, "analytics dataset");
|
|
651
682
|
const rateLimiterNames = bindings.ratelimiters.map((r) => r.binding);
|
|
652
683
|
const requireRateLimiter = bound(rateLimiterNames, "rate limiter");
|
|
@@ -1750,21 +1781,29 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1750
1781
|
|
|
1751
1782
|
case "queue.send": {
|
|
1752
1783
|
const { store, part: q } = storeFor("queue", requireQueue(msg.queue));
|
|
1753
|
-
const
|
|
1784
|
+
const now = Date.now();
|
|
1785
|
+
const at = now + Math.max(0, Number(msg.delaySeconds) || 0) * 1000;
|
|
1754
1786
|
store
|
|
1755
|
-
.query("INSERT INTO mq (queue, id, body, visible_at) VALUES (?, ?, ?, ?)")
|
|
1756
|
-
.run(q, newId(), str(msg.body), at);
|
|
1757
|
-
return { ok: true };
|
|
1787
|
+
.query("INSERT INTO mq (queue, id, body, visible_at, sent_at) VALUES (?, ?, ?, ?, ?)")
|
|
1788
|
+
.run(q, newId(), str(msg.body), at, now);
|
|
1789
|
+
return msg.metrics === true ? { ok: true, metrics: queueBacklog(store, q) } : { ok: true };
|
|
1758
1790
|
}
|
|
1759
1791
|
case "queue.send_batch": {
|
|
1760
1792
|
const { store, part: q } = storeFor("queue", requireQueue(msg.queue));
|
|
1761
1793
|
const msgs = Array.isArray(msg.messages) ? msg.messages.map((m) => jsonObject(m) ?? {}) : [];
|
|
1762
|
-
const ins = store.query("INSERT INTO mq (queue, id, body, visible_at) VALUES (?, ?, ?, ?)");
|
|
1794
|
+
const ins = store.query("INSERT INTO mq (queue, id, body, visible_at, sent_at) VALUES (?, ?, ?, ?, ?)");
|
|
1795
|
+
const now = Date.now();
|
|
1763
1796
|
store.transaction(() => {
|
|
1764
1797
|
for (const m of msgs)
|
|
1765
|
-
ins.run(q, newId(), str(m.body),
|
|
1798
|
+
ins.run(q, newId(), str(m.body), now + Math.max(0, Number(m.delaySeconds) || 0) * 1000, now);
|
|
1766
1799
|
})();
|
|
1767
|
-
return
|
|
1800
|
+
return msg.metrics === true
|
|
1801
|
+
? { ok: true, count: msgs.length, metrics: queueBacklog(store, q) }
|
|
1802
|
+
: { ok: true, count: msgs.length };
|
|
1803
|
+
}
|
|
1804
|
+
case "queue.metrics": {
|
|
1805
|
+
const { store, part: q } = storeFor("queue", requireQueue(msg.queue));
|
|
1806
|
+
return { ok: true, metrics: queueBacklog(store, q) };
|
|
1768
1807
|
}
|
|
1769
1808
|
|
|
1770
1809
|
case "ae.write": {
|
|
@@ -2212,18 +2251,30 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
2212
2251
|
if (!opts.sproutUrl || !dispatchEnabled() || closed) return;
|
|
2213
2252
|
for (const binding of bindings.queues) {
|
|
2214
2253
|
const { store, part } = storeFor("queue", binding);
|
|
2254
|
+
const settings = queueSettingsOf(binding);
|
|
2255
|
+
const size = Math.min(100, Math.max(1, Number(settings?.maxBatchSize) || QUEUE_BATCH));
|
|
2256
|
+
const wait = Math.max(0, Number(settings?.maxBatchTimeout) || 0) * 1000;
|
|
2257
|
+
// Deliveries that never came back (the sprout died or timed out each time) stop after
|
|
2258
|
+
// 5 attempts, or max_retries + 1 when that is more.
|
|
2259
|
+
const cap = Math.max(QUEUE_MAX_ATTEMPTS, (settings?.maxRetries ?? 3) + 1);
|
|
2215
2260
|
const now = Date.now();
|
|
2216
2261
|
const claim = randomBytes(24).toString("hex");
|
|
2217
2262
|
const rows = store
|
|
2218
2263
|
.transaction(() => {
|
|
2219
2264
|
store
|
|
2220
2265
|
.query("UPDATE mq SET dead = 1 WHERE queue = ? AND visible_at <= ? AND attempts >= ?")
|
|
2221
|
-
.run(part, now,
|
|
2266
|
+
.run(part, now, cap);
|
|
2222
2267
|
const due = store
|
|
2223
|
-
.query<
|
|
2224
|
-
|
|
2268
|
+
.query<
|
|
2269
|
+
{ id: string; body: string; attempts: number; sent_at: number; visible_at: number },
|
|
2270
|
+
[string, number, number]
|
|
2271
|
+
>(
|
|
2272
|
+
"SELECT id, body, attempts, sent_at, visible_at FROM mq WHERE queue = ? AND dead = 0 AND visible_at <= ? ORDER BY visible_at LIMIT ?",
|
|
2225
2273
|
)
|
|
2226
|
-
.all(part, now,
|
|
2274
|
+
.all(part, now, size);
|
|
2275
|
+
// max_batch_timeout: a batch short of max_batch_size waits until its oldest
|
|
2276
|
+
// message has been due that long.
|
|
2277
|
+
if (wait > 0 && due.length > 0 && due.length < size && due[0].visible_at + wait > now) return [];
|
|
2227
2278
|
for (const row of due)
|
|
2228
2279
|
store
|
|
2229
2280
|
.query(
|
|
@@ -2239,14 +2290,18 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
2239
2290
|
messages: rows.map((r) => ({
|
|
2240
2291
|
id: r.id,
|
|
2241
2292
|
body: r.body,
|
|
2242
|
-
timestamp: now,
|
|
2293
|
+
timestamp: r.sent_at || now,
|
|
2243
2294
|
attempts: r.attempts + 1,
|
|
2244
2295
|
})),
|
|
2296
|
+
metadata: { metrics: queueBacklog(store, part) },
|
|
2245
2297
|
}).then(async (res) => {
|
|
2246
2298
|
if (closed || !dispatchEnabled()) return;
|
|
2247
2299
|
const claimed = new Set(rows.map((r) => r.id));
|
|
2248
2300
|
let ack = rows.map((r) => r.id);
|
|
2249
2301
|
let retry: string[] = [];
|
|
2302
|
+
let dead: string[] = [];
|
|
2303
|
+
let deadLetter = "";
|
|
2304
|
+
let delays: JsonObject | null = null;
|
|
2250
2305
|
if (res?.ok) {
|
|
2251
2306
|
try {
|
|
2252
2307
|
const parsed = jsonObject(parseJsonValue(await res.text()));
|
|
@@ -2254,6 +2309,11 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
2254
2309
|
Array.isArray(v) ? v.filter(isString).filter((id) => claimed.has(id)) : null;
|
|
2255
2310
|
ack = (parsed && ids(parsed.ack)) ?? ack;
|
|
2256
2311
|
retry = (parsed && ids(parsed.retry)) ?? [];
|
|
2312
|
+
// From runtime 0.32: retry delays, and messages past max_retries (to the dead
|
|
2313
|
+
// letter queue when one is named). An older sprout sends neither.
|
|
2314
|
+
dead = (parsed && ids(parsed.dead)) ?? [];
|
|
2315
|
+
deadLetter = parsed && isString(parsed.deadLetter) ? parsed.deadLetter : "";
|
|
2316
|
+
delays = (parsed && jsonObject(parsed.delays ?? null)) ?? null;
|
|
2257
2317
|
} catch {
|
|
2258
2318
|
ack = [];
|
|
2259
2319
|
retry = rows.map((r) => r.id);
|
|
@@ -2264,19 +2324,68 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
2264
2324
|
}
|
|
2265
2325
|
if (closed || !dispatchEnabled()) return;
|
|
2266
2326
|
const retrySet = new Set(retry);
|
|
2327
|
+
const deadSet = new Set(dead);
|
|
2328
|
+
let target: { store: Database; part: string } | null = null;
|
|
2329
|
+
if (dead.length && deadLetter) {
|
|
2330
|
+
try {
|
|
2331
|
+
target = queueStore(deadLetter);
|
|
2332
|
+
} catch {
|
|
2333
|
+
target = null; // not a queue this broker serves: mark them dead instead
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
// Into the dead letter queue first, then out of this one: a crash between the two
|
|
2337
|
+
// leaves a copy in each rather than none.
|
|
2338
|
+
if (target && target.store !== store) {
|
|
2339
|
+
const moved = store.query<{ id: string; body: string; sent_at: number }, [string, string, string, number]>(
|
|
2340
|
+
"SELECT id, body, sent_at FROM mq WHERE queue = ? AND id = ? AND claim_token = ? AND visible_at > ?",
|
|
2341
|
+
);
|
|
2342
|
+
const found = dead.flatMap((id) => moved.all(part, id, claim, Date.now()));
|
|
2343
|
+
const ins = target.store.query(
|
|
2344
|
+
"INSERT OR IGNORE INTO mq (queue, id, body, visible_at, sent_at) VALUES (?, ?, ?, ?, ?)",
|
|
2345
|
+
);
|
|
2346
|
+
const at = Date.now();
|
|
2347
|
+
const into = target;
|
|
2348
|
+
into.store.transaction(() => {
|
|
2349
|
+
for (const r of found) ins.run(into.part, r.id, r.body, at, r.sent_at);
|
|
2350
|
+
})();
|
|
2351
|
+
}
|
|
2267
2352
|
store
|
|
2268
2353
|
.transaction(() => {
|
|
2354
|
+
const at = Date.now();
|
|
2355
|
+
for (const id of dead) {
|
|
2356
|
+
if (target && target.store === store)
|
|
2357
|
+
store
|
|
2358
|
+
.query(
|
|
2359
|
+
"UPDATE mq SET queue = ?, visible_at = ?, attempts = 0, claim_token = '', dead = 0 WHERE queue = ? AND id = ? AND claim_token = ? AND visible_at > ?",
|
|
2360
|
+
)
|
|
2361
|
+
.run(target.part, at, part, id, claim, at);
|
|
2362
|
+
else if (target)
|
|
2363
|
+
store
|
|
2364
|
+
.query("DELETE FROM mq WHERE queue = ? AND id = ? AND claim_token = ? AND visible_at > ?")
|
|
2365
|
+
.run(part, id, claim, at);
|
|
2366
|
+
else
|
|
2367
|
+
store
|
|
2368
|
+
.query(
|
|
2369
|
+
"UPDATE mq SET dead = 1, claim_token = '' WHERE queue = ? AND id = ? AND claim_token = ? AND visible_at > ?",
|
|
2370
|
+
)
|
|
2371
|
+
.run(part, id, claim, at);
|
|
2372
|
+
}
|
|
2269
2373
|
for (const id of ack)
|
|
2270
|
-
if (!retrySet.has(id))
|
|
2374
|
+
if (!retrySet.has(id) && !deadSet.has(id))
|
|
2271
2375
|
store
|
|
2272
2376
|
.query("DELETE FROM mq WHERE queue = ? AND id = ? AND claim_token = ? AND visible_at > ?")
|
|
2273
|
-
.run(part, id, claim,
|
|
2274
|
-
for (const id of retry)
|
|
2377
|
+
.run(part, id, claim, at);
|
|
2378
|
+
for (const id of retry) {
|
|
2379
|
+
if (deadSet.has(id)) continue;
|
|
2380
|
+
const seconds = delays ? Number(delays[id]) : NaN;
|
|
2381
|
+
const delay = Number.isFinite(seconds) ? Math.max(0, seconds) * 1000 : 5_000;
|
|
2382
|
+
// With runtime delays, max_retries is the sprout's: only the delivery cap applies.
|
|
2275
2383
|
store
|
|
2276
2384
|
.query(
|
|
2277
2385
|
"UPDATE mq SET visible_at = ?, claim_token = '', dead = CASE WHEN attempts >= ? THEN 1 ELSE 0 END WHERE queue = ? AND id = ? AND claim_token = ? AND visible_at > ?",
|
|
2278
2386
|
)
|
|
2279
|
-
.run(
|
|
2387
|
+
.run(at + delay, delays ? cap : QUEUE_MAX_ATTEMPTS, part, id, claim, at);
|
|
2388
|
+
}
|
|
2280
2389
|
})
|
|
2281
2390
|
.immediate();
|
|
2282
2391
|
});
|