@sproutboat/wire 0.9.2 → 0.10.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 +14 -0
- package/package.json +1 -1
- package/src/broker.test.ts +64 -1
- package/src/broker.ts +51 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @sproutboat/wire
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 276fd90: New `doDb` option and `--do-db` flag: Durable Object storage and alarms can live in their own SQLite file instead of the per-deployment database (baronunread/sproutboat#207). The platform points it at a per-project file so an object's state survives redeploys, as on Workers. Without it, behaviour is unchanged.
|
|
8
|
+
|
|
9
|
+
## 0.9.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 5dba469: Binary data survives KV and outbound `fetch()` bodies (baronunread/sproutboat#233, and the request half of #232).
|
|
14
|
+
- **KV:** `put()` of an `ArrayBuffer` or any typed array stores its bytes, flagged binary, instead of `String(view)` (`"0,1,2,..."`). `get(key, type | { type })` supports `"text"`, `"json"` and `"arrayBuffer"` as in Workers: a binary value read as text decodes as UTF-8, and a text value read as `arrayBuffer` is its UTF-8 encoding. `"stream"` throws a clear error. Existing stores gain the `binary` column on first use.
|
|
15
|
+
- **`fetch()` and service-binding request bodies:** an `ArrayBuffer` or view is sent as its exact bytes in both transports, instead of `"0,1,2,..."`. The standalone HTTP client also stopped truncating bodies at the first NUL byte.
|
|
16
|
+
|
|
3
17
|
## 0.9.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/package.json
CHANGED
package/src/broker.test.ts
CHANGED
|
@@ -39,12 +39,25 @@ test("ping round-trips", async () => {
|
|
|
39
39
|
expect(await b.dispatch({ op: "ping", msg: "hi" })).toMatchObject({ ok: true, op: "pong", echo: "hi" });
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
test("#233: a KV value put as bytes is flagged binary and survives an existing store", async () => {
|
|
43
|
+
const b = make({ bindings: { kv: ["CACHE"] } });
|
|
44
|
+
// One char per byte, as the runtime sends a put of an ArrayBuffer/view.
|
|
45
|
+
const bytes = Array.from({ length: 256 }, (_, i) => String.fromCharCode(i)).join("");
|
|
46
|
+
await b.dispatch({ op: "kv.put", ns: "CACHE", key: "bin", value: bytes, binary: true });
|
|
47
|
+
await b.dispatch({ op: "kv.put", ns: "CACHE", key: "txt", value: "café" });
|
|
48
|
+
expect(await b.dispatch({ op: "kv.get", ns: "CACHE", key: "bin" })).toEqual({ ok: true, found: true, value: bytes, binary: true });
|
|
49
|
+
expect(await b.dispatch({ op: "kv.get", ns: "CACHE", key: "txt" })).toMatchObject({ value: "café", binary: false });
|
|
50
|
+
// Overwriting a binary value with text clears the flag.
|
|
51
|
+
await b.dispatch({ op: "kv.put", ns: "CACHE", key: "bin", value: "now text" });
|
|
52
|
+
expect(await b.dispatch({ op: "kv.get", ns: "CACHE", key: "bin" })).toMatchObject({ value: "now text", binary: false });
|
|
53
|
+
});
|
|
54
|
+
|
|
42
55
|
test("KV put / get / list / delete, scoped to a bound namespace", async () => {
|
|
43
56
|
const b = make({ bindings: { kv: ["CACHE"] } });
|
|
44
57
|
expect(await b.dispatch({ op: "kv.get", ns: "CACHE", key: "k" })).toEqual({ ok: true, found: false, value: null });
|
|
45
58
|
await b.dispatch({ op: "kv.put", ns: "CACHE", key: "k", value: "v" });
|
|
46
59
|
await b.dispatch({ op: "kv.put", ns: "CACHE", key: "k2", value: "v2" });
|
|
47
|
-
expect(await b.dispatch({ op: "kv.get", ns: "CACHE", key: "k" })).toEqual({ ok: true, found: true, value: "v" });
|
|
60
|
+
expect(await b.dispatch({ op: "kv.get", ns: "CACHE", key: "k" })).toEqual({ ok: true, found: true, value: "v", binary: false });
|
|
48
61
|
expect(await b.dispatch({ op: "kv.list", ns: "CACHE", prefix: "k" })).toEqual({ ok: true, keys: ["k", "k2"] });
|
|
49
62
|
await b.dispatch({ op: "kv.delete", ns: "CACHE", key: "k" });
|
|
50
63
|
expect(await b.dispatch({ op: "kv.get", ns: "CACHE", key: "k" })).toMatchObject({ found: false });
|
|
@@ -154,6 +167,21 @@ test("fetch is gated by the exact-host allowlist", async () => {
|
|
|
154
167
|
expect(calls).toEqual(["https://api.example.com/x"]);
|
|
155
168
|
});
|
|
156
169
|
|
|
170
|
+
test("#232: a binary fetch request body is sent as its bytes, a text body as UTF-8", async () => {
|
|
171
|
+
const seen: Uint8Array[] = [];
|
|
172
|
+
const fetchImpl: FetchLike = async (_url, init) => {
|
|
173
|
+
seen.push(new Uint8Array(await new Response(init?.body).arrayBuffer()));
|
|
174
|
+
return new Response("ok");
|
|
175
|
+
};
|
|
176
|
+
const b = make({ bindings: { outbound: ["api.example.com"] }, fetchImpl });
|
|
177
|
+
// The runtime sends a binary body one char per byte, flagged bodyBinary.
|
|
178
|
+
const raw = Array.from({ length: 256 }, (_, i) => String.fromCharCode(i)).join("");
|
|
179
|
+
await b.dispatch({ op: "fetch", url: "https://api.example.com/up", method: "POST", body: raw, bodyBinary: true });
|
|
180
|
+
await b.dispatch({ op: "fetch", url: "https://api.example.com/up", method: "POST", body: "café" });
|
|
181
|
+
expect(Array.from(seen[0])).toEqual(Array.from({ length: 256 }, (_, i) => i));
|
|
182
|
+
expect(Array.from(seen[1])).toEqual([99, 97, 102, 195, 169]);
|
|
183
|
+
});
|
|
184
|
+
|
|
157
185
|
test("#232: a v1 fetch returns the upstream body as raw bytes; v0 keeps decoded text", async () => {
|
|
158
186
|
const binary = Uint8Array.from({ length: 256 }, (_, i) => i);
|
|
159
187
|
const fetchImpl: FetchLike = async (url) =>
|
|
@@ -678,6 +706,40 @@ test("cronMatches: fields, steps, ranges, lists (UTC)", () => {
|
|
|
678
706
|
// #74 — a binding wired to an account-level resource stores in its own file
|
|
679
707
|
// under resourceDir, keyed by id, so the data outlives a "redeploy" (a new
|
|
680
708
|
// broker with a fresh per-deployment db but the same resourceDir).
|
|
709
|
+
test("#207: Durable Object storage and alarms in doDb outlive a redeploy", async () => {
|
|
710
|
+
const root = mkdtempSync(join(tmpdir(), "sb-broker-do-"));
|
|
711
|
+
try {
|
|
712
|
+
mkdirSync(join(root, "dep-a"), { recursive: true });
|
|
713
|
+
mkdirSync(join(root, "dep-b"), { recursive: true });
|
|
714
|
+
const doDb = join(root, "do_0123456789abcdef01234567.sqlite");
|
|
715
|
+
const bindings = { do: [{ binding: "COUNTER", className: "Counter" }] };
|
|
716
|
+
|
|
717
|
+
const first = createBroker({ db: join(root, "dep-a", "state.sqlite"), doDb, bindings });
|
|
718
|
+
await first.dispatch({ op: "do.storage.put", cls: "Counter", id: "global", key: "n", value: "41" });
|
|
719
|
+
await first.dispatch({ op: "do.alarm.set", cls: "Counter", id: "global", at: 1234 });
|
|
720
|
+
first.close();
|
|
721
|
+
|
|
722
|
+
// A redeploy: new per-deployment state.sqlite, same per-project doDb.
|
|
723
|
+
const second = createBroker({ db: join(root, "dep-b", "state.sqlite"), doDb, bindings });
|
|
724
|
+
expect(await second.dispatch({ op: "do.storage.get", cls: "Counter", id: "global", key: "n" })).toEqual({
|
|
725
|
+
ok: true,
|
|
726
|
+
found: true,
|
|
727
|
+
value: "41",
|
|
728
|
+
});
|
|
729
|
+
expect(await second.dispatch({ op: "do.alarm.get", cls: "Counter", id: "global" })).toMatchObject({ at: 1234 });
|
|
730
|
+
second.close();
|
|
731
|
+
|
|
732
|
+
// Without doDb the state stays per deployment, as before.
|
|
733
|
+
const third = createBroker({ db: join(root, "dep-a", "state.sqlite"), bindings });
|
|
734
|
+
expect(await third.dispatch({ op: "do.storage.get", cls: "Counter", id: "global", key: "n" })).toMatchObject({
|
|
735
|
+
found: false,
|
|
736
|
+
});
|
|
737
|
+
third.close();
|
|
738
|
+
} finally {
|
|
739
|
+
rmSync(root, { recursive: true, force: true });
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
681
743
|
test("resource-backed KV persists across brokers and is shared by id", async () => {
|
|
682
744
|
const root = mkdtempSync(join(tmpdir(), "sb-broker-res-"));
|
|
683
745
|
try {
|
|
@@ -705,6 +767,7 @@ test("resource-backed KV persists across brokers and is shared by id", async ()
|
|
|
705
767
|
expect(await second.dispatch({ op: "kv.get", ns: "SHORTENER", key: "a" })).toEqual({
|
|
706
768
|
ok: true,
|
|
707
769
|
found: true,
|
|
770
|
+
binary: false,
|
|
708
771
|
value: "1",
|
|
709
772
|
});
|
|
710
773
|
second.close();
|
package/src/broker.ts
CHANGED
|
@@ -85,6 +85,12 @@ export type BrokerOptions = {
|
|
|
85
85
|
* data outlives a redeploy.
|
|
86
86
|
*/
|
|
87
87
|
resourceDir?: string;
|
|
88
|
+
/**
|
|
89
|
+
* #207 — SQLite file for Durable Object storage and alarms. The supervisor
|
|
90
|
+
* points this at a per-project path, so an object's state outlives a
|
|
91
|
+
* redeploy as it does on Workers. Defaults to the per-broker `db`.
|
|
92
|
+
*/
|
|
93
|
+
doDb?: string;
|
|
88
94
|
/** Account whose R2 resources this broker may mutate. */
|
|
89
95
|
ownerId?: string;
|
|
90
96
|
/** Account-wide R2 byte ceiling. Zero disables the ceiling for local development. */
|
|
@@ -269,6 +275,9 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
269
275
|
);
|
|
270
276
|
const kvColumns = conn.query<{ name: string }, []>("PRAGMA table_info(kv)").all();
|
|
271
277
|
if (!kvColumns.some((column) => column.name === "expires_at")) conn.exec("ALTER TABLE kv ADD COLUMN expires_at INTEGER");
|
|
278
|
+
// #233 — 1 when the value was put as bytes (stored one char per byte), so get()
|
|
279
|
+
// can hand back exact bytes or decoded text depending on the requested type.
|
|
280
|
+
if (!kvColumns.some((column) => column.name === "binary")) conn.exec("ALTER TABLE kv ADD COLUMN binary INTEGER NOT NULL DEFAULT 0");
|
|
272
281
|
// #56 — no `body` column: an object's bytes live in their own file under
|
|
273
282
|
// `r2Dir` (or an in-memory map for `:memory:`), never in the SQLite row.
|
|
274
283
|
// Doubling every object through SQLite's own page cache on top of the
|
|
@@ -309,9 +318,14 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
309
318
|
};
|
|
310
319
|
|
|
311
320
|
const db = openStore(dbPath);
|
|
312
|
-
// Durable Object
|
|
313
|
-
//
|
|
314
|
-
|
|
321
|
+
// #207 — Durable Object state lives in its own file when the supervisor gives
|
|
322
|
+
// one (per project, stable across deploys); otherwise in the per-broker db.
|
|
323
|
+
const doDb = opts.doDb ? new Database(opts.doDb, { create: true }) : db;
|
|
324
|
+
if (doDb !== db) {
|
|
325
|
+
doDb.exec("PRAGMA journal_mode = WAL");
|
|
326
|
+
doDb.exec("PRAGMA synchronous = NORMAL");
|
|
327
|
+
}
|
|
328
|
+
doDb.exec(
|
|
315
329
|
"CREATE TABLE IF NOT EXISTS do_storage (cls TEXT NOT NULL, id TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, " +
|
|
316
330
|
"PRIMARY KEY (cls, id, key))",
|
|
317
331
|
);
|
|
@@ -321,12 +335,12 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
321
335
|
);
|
|
322
336
|
// #125 — at most one pending alarm per object, which is the Workers rule: a
|
|
323
337
|
// later setAlarm replaces the earlier one rather than queueing beside it.
|
|
324
|
-
|
|
338
|
+
doDb.exec(
|
|
325
339
|
"CREATE TABLE IF NOT EXISTS do_alarm (cls TEXT NOT NULL, id TEXT NOT NULL, at INTEGER NOT NULL, " +
|
|
326
340
|
"attempts INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (cls, id))",
|
|
327
341
|
);
|
|
328
342
|
// Alarm polling reads the earliest due objects across classes.
|
|
329
|
-
|
|
343
|
+
doDb.exec("CREATE INDEX IF NOT EXISTS do_alarm_due ON do_alarm (at)");
|
|
330
344
|
// #69 — fixed-window rate-limiter counters, per binding name + key.
|
|
331
345
|
db.exec(
|
|
332
346
|
"CREATE TABLE IF NOT EXISTS ratelimit (name TEXT NOT NULL, key TEXT NOT NULL, window_start INTEGER NOT NULL, " +
|
|
@@ -481,8 +495,8 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
481
495
|
};
|
|
482
496
|
|
|
483
497
|
type KvStmts = {
|
|
484
|
-
get: Statement<{ value: string; expires_at: number | null }, [string, string]>;
|
|
485
|
-
put: Statement<unknown, [string, string, string, number | null]>;
|
|
498
|
+
get: Statement<{ value: string; expires_at: number | null; binary: number }, [string, string]>;
|
|
499
|
+
put: Statement<unknown, [string, string, string, number | null, number]>;
|
|
486
500
|
del: Statement<unknown, [string, string]>;
|
|
487
501
|
list: Statement<{ key: string }, [string, string, number]>;
|
|
488
502
|
};
|
|
@@ -491,12 +505,12 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
491
505
|
let stmts = kvStmtCache.get(store);
|
|
492
506
|
if (!stmts) {
|
|
493
507
|
stmts = {
|
|
494
|
-
get: store.query<{ value: string; expires_at: number | null }, [string, string]>(
|
|
495
|
-
"SELECT value, expires_at FROM kv WHERE ns = ? AND key = ?",
|
|
508
|
+
get: store.query<{ value: string; expires_at: number | null; binary: number }, [string, string]>(
|
|
509
|
+
"SELECT value, expires_at, binary FROM kv WHERE ns = ? AND key = ?",
|
|
496
510
|
),
|
|
497
511
|
put: store.query(
|
|
498
|
-
"INSERT INTO kv (ns, key, value, expires_at) VALUES (?1, ?2, ?3, ?4) " +
|
|
499
|
-
"ON CONFLICT (ns, key) DO UPDATE SET value = ?3, expires_at = ?4",
|
|
512
|
+
"INSERT INTO kv (ns, key, value, expires_at, binary) VALUES (?1, ?2, ?3, ?4, ?5) " +
|
|
513
|
+
"ON CONFLICT (ns, key) DO UPDATE SET value = ?3, expires_at = ?4, binary = ?5",
|
|
500
514
|
),
|
|
501
515
|
del: store.query("DELETE FROM kv WHERE ns = ? AND key = ?"),
|
|
502
516
|
// #58 — an expired row is filtered out here; a periodic sweep to hard-delete
|
|
@@ -1149,6 +1163,14 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1149
1163
|
type Upstream = { reply: Frame; bytes: Buffer };
|
|
1150
1164
|
const asTextFrame = (u: Upstream) => ({ ...u.reply, body: new TextDecoder().decode(u.bytes) });
|
|
1151
1165
|
|
|
1166
|
+
/** #232 — a binary request body arrives one char per byte; send those bytes, not their UTF-8. */
|
|
1167
|
+
const upstreamBody = (msg: Frame): string | Uint8Array<ArrayBuffer> | undefined =>
|
|
1168
|
+
msg.body == null
|
|
1169
|
+
? undefined
|
|
1170
|
+
: msg.bodyBinary === true
|
|
1171
|
+
? new Uint8Array(Buffer.from(str(msg.body), "latin1"))
|
|
1172
|
+
: str(msg.body);
|
|
1173
|
+
|
|
1152
1174
|
async function proxyFetch(msg: Frame): Promise<Upstream> {
|
|
1153
1175
|
let url: URL;
|
|
1154
1176
|
try {
|
|
@@ -1169,7 +1191,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1169
1191
|
const res = await doFetch(url, {
|
|
1170
1192
|
method,
|
|
1171
1193
|
headers,
|
|
1172
|
-
body:
|
|
1194
|
+
body: method === "GET" || method === "HEAD" ? undefined : upstreamBody(msg),
|
|
1173
1195
|
redirect: "manual",
|
|
1174
1196
|
});
|
|
1175
1197
|
const outHeaders: Array<[string, string]> = [];
|
|
@@ -1215,7 +1237,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1215
1237
|
const res = await doFetch(target, {
|
|
1216
1238
|
method,
|
|
1217
1239
|
headers,
|
|
1218
|
-
body:
|
|
1240
|
+
body: method === "GET" || method === "HEAD" ? undefined : upstreamBody(msg),
|
|
1219
1241
|
redirect: "manual",
|
|
1220
1242
|
});
|
|
1221
1243
|
const outHeaders: Array<[string, string]> = [];
|
|
@@ -1236,11 +1258,11 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1236
1258
|
stmts.del.run(part, str(msg.key));
|
|
1237
1259
|
return { ok: true, found: false, value: null };
|
|
1238
1260
|
}
|
|
1239
|
-
return { ok: true, found: true, value: row.value };
|
|
1261
|
+
return { ok: true, found: true, value: row.value, binary: row.binary === 1 };
|
|
1240
1262
|
}
|
|
1241
1263
|
case "kv.put": {
|
|
1242
1264
|
const { store, part } = storeFor("kv", requireKv(msg.ns));
|
|
1243
|
-
kvStmts(store).put.run(part, str(msg.key), str(msg.value), kvExpiresAt(msg));
|
|
1265
|
+
kvStmts(store).put.run(part, str(msg.key), str(msg.value), kvExpiresAt(msg), msg.binary === true ? 1 : 0);
|
|
1244
1266
|
return { ok: true };
|
|
1245
1267
|
}
|
|
1246
1268
|
case "kv.delete": {
|
|
@@ -1553,7 +1575,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1553
1575
|
|
|
1554
1576
|
case "do.storage.get": {
|
|
1555
1577
|
const cls = requireDoClass(msg.cls);
|
|
1556
|
-
const row =
|
|
1578
|
+
const row = doDb
|
|
1557
1579
|
.query<{ value: string }, [string, string, string]>(
|
|
1558
1580
|
"SELECT value FROM do_storage WHERE cls = ? AND id = ? AND key = ?",
|
|
1559
1581
|
)
|
|
@@ -1561,24 +1583,24 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1561
1583
|
return row ? { ok: true, found: true, value: row.value } : { ok: true, found: false };
|
|
1562
1584
|
}
|
|
1563
1585
|
case "do.storage.put":
|
|
1564
|
-
|
|
1586
|
+
doDb.query(
|
|
1565
1587
|
"INSERT INTO do_storage (cls, id, key, value) VALUES (?1,?2,?3,?4) " +
|
|
1566
1588
|
"ON CONFLICT (cls, id, key) DO UPDATE SET value = ?4",
|
|
1567
1589
|
).run(requireDoClass(msg.cls), str(msg.id), str(msg.key), str(msg.value));
|
|
1568
1590
|
return { ok: true };
|
|
1569
1591
|
case "do.storage.delete": {
|
|
1570
|
-
const r =
|
|
1592
|
+
const r = doDb
|
|
1571
1593
|
.query("DELETE FROM do_storage WHERE cls = ? AND id = ? AND key = ?")
|
|
1572
1594
|
.run(requireDoClass(msg.cls), str(msg.id), str(msg.key));
|
|
1573
1595
|
return { ok: true, deleted: r.changes > 0 };
|
|
1574
1596
|
}
|
|
1575
1597
|
case "do.storage.delete_all":
|
|
1576
|
-
|
|
1598
|
+
doDb.query("DELETE FROM do_storage WHERE cls = ? AND id = ?").run(requireDoClass(msg.cls), str(msg.id));
|
|
1577
1599
|
return { ok: true };
|
|
1578
1600
|
case "do.storage.list": {
|
|
1579
1601
|
const cls = requireDoClass(msg.cls);
|
|
1580
1602
|
const limit = Math.min(Math.max(Number(msg.limit) || 1000, 1), 10000);
|
|
1581
|
-
const rows =
|
|
1603
|
+
const rows = doDb
|
|
1582
1604
|
.query<{ key: string; value: string }, [string, string, string, number]>(
|
|
1583
1605
|
"SELECT key, value FROM do_storage WHERE cls = ? AND id = ? AND key LIKE ? || '%' ORDER BY key LIMIT ?",
|
|
1584
1606
|
)
|
|
@@ -1589,19 +1611,19 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1589
1611
|
// #125 — alarms. `at` is epoch ms; setting one replaces any pending alarm
|
|
1590
1612
|
// for that object, and attempts resets because this is a fresh schedule.
|
|
1591
1613
|
case "do.alarm.set":
|
|
1592
|
-
|
|
1614
|
+
doDb.query(
|
|
1593
1615
|
"INSERT INTO do_alarm (cls, id, at, attempts) VALUES (?1,?2,?3,0) " +
|
|
1594
1616
|
"ON CONFLICT (cls, id) DO UPDATE SET at = ?3, attempts = 0",
|
|
1595
1617
|
).run(requireDoClass(msg.cls), str(msg.id), Math.trunc(Number(msg.at) || 0));
|
|
1596
1618
|
return { ok: true };
|
|
1597
1619
|
case "do.alarm.get": {
|
|
1598
|
-
const row =
|
|
1620
|
+
const row = doDb
|
|
1599
1621
|
.query<{ at: number }, [string, string]>("SELECT at FROM do_alarm WHERE cls = ? AND id = ?")
|
|
1600
1622
|
.get(requireDoClass(msg.cls), str(msg.id));
|
|
1601
1623
|
return { ok: true, at: row ? row.at : null };
|
|
1602
1624
|
}
|
|
1603
1625
|
case "do.alarm.delete": {
|
|
1604
|
-
const r =
|
|
1626
|
+
const r = doDb.query("DELETE FROM do_alarm WHERE cls = ? AND id = ?").run(requireDoClass(msg.cls), str(msg.id));
|
|
1605
1627
|
return { ok: true, deleted: r.changes > 0 };
|
|
1606
1628
|
}
|
|
1607
1629
|
|
|
@@ -1919,14 +1941,14 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1919
1941
|
function fireAlarmsOnce(): void {
|
|
1920
1942
|
if (!opts.sproutUrl || !dispatchEnabled() || bindings.do.length === 0) return;
|
|
1921
1943
|
const now = Date.now();
|
|
1922
|
-
const due =
|
|
1944
|
+
const due = doDb
|
|
1923
1945
|
.query<{ cls: string; id: string; at: number; attempts: number }, [number]>(
|
|
1924
1946
|
"SELECT cls, id, at, attempts FROM do_alarm WHERE at <= ? ORDER BY at LIMIT 10",
|
|
1925
1947
|
)
|
|
1926
1948
|
.all(now);
|
|
1927
1949
|
if (due.length === 0) return;
|
|
1928
|
-
const claim =
|
|
1929
|
-
const rearm =
|
|
1950
|
+
const claim = doDb.query("DELETE FROM do_alarm WHERE cls = ? AND id = ?");
|
|
1951
|
+
const rearm = doDb.query(
|
|
1930
1952
|
"INSERT INTO do_alarm (cls, id, at, attempts) VALUES (?1,?2,?3,?4) ON CONFLICT (cls, id) DO NOTHING",
|
|
1931
1953
|
);
|
|
1932
1954
|
for (const row of due) {
|
|
@@ -1969,6 +1991,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1969
1991
|
for (const t of timers) clearInterval(t);
|
|
1970
1992
|
for (const conn of d1Conns.values()) conn.close();
|
|
1971
1993
|
for (const conn of resourceDbs.values()) conn.close();
|
|
1994
|
+
if (doDb !== db) doDb.close();
|
|
1972
1995
|
db.close();
|
|
1973
1996
|
},
|
|
1974
1997
|
};
|
|
@@ -2075,6 +2098,7 @@ if (import.meta.main) {
|
|
|
2075
2098
|
db: { type: "string" },
|
|
2076
2099
|
"data-dir": { type: "string" },
|
|
2077
2100
|
"resource-dir": { type: "string" },
|
|
2101
|
+
"do-db": { type: "string" },
|
|
2078
2102
|
"owner-id": { type: "string" },
|
|
2079
2103
|
"r2-quota-bytes": { type: "string" },
|
|
2080
2104
|
"r2-resource-ids": { type: "string" },
|
|
@@ -2127,6 +2151,7 @@ if (import.meta.main) {
|
|
|
2127
2151
|
db: values.db,
|
|
2128
2152
|
dataDir: values["data-dir"],
|
|
2129
2153
|
resourceDir: values["resource-dir"],
|
|
2154
|
+
doDb: values["do-db"],
|
|
2130
2155
|
ownerId: values["owner-id"],
|
|
2131
2156
|
r2QuotaBytes: Number(values["r2-quota-bytes"] ?? process.env.SPROUTBOAT_R2_QUOTA_BYTES) || undefined,
|
|
2132
2157
|
r2ResourceIds: values["r2-resource-ids"] ? String(values["r2-resource-ids"]).split(",").filter(Boolean) : undefined,
|