@sproutboat/wire 0.9.3 → 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 +6 -0
- package/package.json +1 -1
- package/src/broker.test.ts +34 -0
- package/src/broker.ts +30 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.9.3
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
package/src/broker.test.ts
CHANGED
|
@@ -706,6 +706,40 @@ test("cronMatches: fields, steps, ranges, lists (UTC)", () => {
|
|
|
706
706
|
// #74 — a binding wired to an account-level resource stores in its own file
|
|
707
707
|
// under resourceDir, keyed by id, so the data outlives a "redeploy" (a new
|
|
708
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
|
+
|
|
709
743
|
test("resource-backed KV persists across brokers and is shared by id", async () => {
|
|
710
744
|
const root = mkdtempSync(join(tmpdir(), "sb-broker-res-"));
|
|
711
745
|
try {
|
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. */
|
|
@@ -312,9 +318,14 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
312
318
|
};
|
|
313
319
|
|
|
314
320
|
const db = openStore(dbPath);
|
|
315
|
-
// Durable Object
|
|
316
|
-
//
|
|
317
|
-
|
|
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(
|
|
318
329
|
"CREATE TABLE IF NOT EXISTS do_storage (cls TEXT NOT NULL, id TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, " +
|
|
319
330
|
"PRIMARY KEY (cls, id, key))",
|
|
320
331
|
);
|
|
@@ -324,12 +335,12 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
324
335
|
);
|
|
325
336
|
// #125 — at most one pending alarm per object, which is the Workers rule: a
|
|
326
337
|
// later setAlarm replaces the earlier one rather than queueing beside it.
|
|
327
|
-
|
|
338
|
+
doDb.exec(
|
|
328
339
|
"CREATE TABLE IF NOT EXISTS do_alarm (cls TEXT NOT NULL, id TEXT NOT NULL, at INTEGER NOT NULL, " +
|
|
329
340
|
"attempts INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (cls, id))",
|
|
330
341
|
);
|
|
331
342
|
// Alarm polling reads the earliest due objects across classes.
|
|
332
|
-
|
|
343
|
+
doDb.exec("CREATE INDEX IF NOT EXISTS do_alarm_due ON do_alarm (at)");
|
|
333
344
|
// #69 — fixed-window rate-limiter counters, per binding name + key.
|
|
334
345
|
db.exec(
|
|
335
346
|
"CREATE TABLE IF NOT EXISTS ratelimit (name TEXT NOT NULL, key TEXT NOT NULL, window_start INTEGER NOT NULL, " +
|
|
@@ -1564,7 +1575,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1564
1575
|
|
|
1565
1576
|
case "do.storage.get": {
|
|
1566
1577
|
const cls = requireDoClass(msg.cls);
|
|
1567
|
-
const row =
|
|
1578
|
+
const row = doDb
|
|
1568
1579
|
.query<{ value: string }, [string, string, string]>(
|
|
1569
1580
|
"SELECT value FROM do_storage WHERE cls = ? AND id = ? AND key = ?",
|
|
1570
1581
|
)
|
|
@@ -1572,24 +1583,24 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1572
1583
|
return row ? { ok: true, found: true, value: row.value } : { ok: true, found: false };
|
|
1573
1584
|
}
|
|
1574
1585
|
case "do.storage.put":
|
|
1575
|
-
|
|
1586
|
+
doDb.query(
|
|
1576
1587
|
"INSERT INTO do_storage (cls, id, key, value) VALUES (?1,?2,?3,?4) " +
|
|
1577
1588
|
"ON CONFLICT (cls, id, key) DO UPDATE SET value = ?4",
|
|
1578
1589
|
).run(requireDoClass(msg.cls), str(msg.id), str(msg.key), str(msg.value));
|
|
1579
1590
|
return { ok: true };
|
|
1580
1591
|
case "do.storage.delete": {
|
|
1581
|
-
const r =
|
|
1592
|
+
const r = doDb
|
|
1582
1593
|
.query("DELETE FROM do_storage WHERE cls = ? AND id = ? AND key = ?")
|
|
1583
1594
|
.run(requireDoClass(msg.cls), str(msg.id), str(msg.key));
|
|
1584
1595
|
return { ok: true, deleted: r.changes > 0 };
|
|
1585
1596
|
}
|
|
1586
1597
|
case "do.storage.delete_all":
|
|
1587
|
-
|
|
1598
|
+
doDb.query("DELETE FROM do_storage WHERE cls = ? AND id = ?").run(requireDoClass(msg.cls), str(msg.id));
|
|
1588
1599
|
return { ok: true };
|
|
1589
1600
|
case "do.storage.list": {
|
|
1590
1601
|
const cls = requireDoClass(msg.cls);
|
|
1591
1602
|
const limit = Math.min(Math.max(Number(msg.limit) || 1000, 1), 10000);
|
|
1592
|
-
const rows =
|
|
1603
|
+
const rows = doDb
|
|
1593
1604
|
.query<{ key: string; value: string }, [string, string, string, number]>(
|
|
1594
1605
|
"SELECT key, value FROM do_storage WHERE cls = ? AND id = ? AND key LIKE ? || '%' ORDER BY key LIMIT ?",
|
|
1595
1606
|
)
|
|
@@ -1600,19 +1611,19 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1600
1611
|
// #125 — alarms. `at` is epoch ms; setting one replaces any pending alarm
|
|
1601
1612
|
// for that object, and attempts resets because this is a fresh schedule.
|
|
1602
1613
|
case "do.alarm.set":
|
|
1603
|
-
|
|
1614
|
+
doDb.query(
|
|
1604
1615
|
"INSERT INTO do_alarm (cls, id, at, attempts) VALUES (?1,?2,?3,0) " +
|
|
1605
1616
|
"ON CONFLICT (cls, id) DO UPDATE SET at = ?3, attempts = 0",
|
|
1606
1617
|
).run(requireDoClass(msg.cls), str(msg.id), Math.trunc(Number(msg.at) || 0));
|
|
1607
1618
|
return { ok: true };
|
|
1608
1619
|
case "do.alarm.get": {
|
|
1609
|
-
const row =
|
|
1620
|
+
const row = doDb
|
|
1610
1621
|
.query<{ at: number }, [string, string]>("SELECT at FROM do_alarm WHERE cls = ? AND id = ?")
|
|
1611
1622
|
.get(requireDoClass(msg.cls), str(msg.id));
|
|
1612
1623
|
return { ok: true, at: row ? row.at : null };
|
|
1613
1624
|
}
|
|
1614
1625
|
case "do.alarm.delete": {
|
|
1615
|
-
const r =
|
|
1626
|
+
const r = doDb.query("DELETE FROM do_alarm WHERE cls = ? AND id = ?").run(requireDoClass(msg.cls), str(msg.id));
|
|
1616
1627
|
return { ok: true, deleted: r.changes > 0 };
|
|
1617
1628
|
}
|
|
1618
1629
|
|
|
@@ -1930,14 +1941,14 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1930
1941
|
function fireAlarmsOnce(): void {
|
|
1931
1942
|
if (!opts.sproutUrl || !dispatchEnabled() || bindings.do.length === 0) return;
|
|
1932
1943
|
const now = Date.now();
|
|
1933
|
-
const due =
|
|
1944
|
+
const due = doDb
|
|
1934
1945
|
.query<{ cls: string; id: string; at: number; attempts: number }, [number]>(
|
|
1935
1946
|
"SELECT cls, id, at, attempts FROM do_alarm WHERE at <= ? ORDER BY at LIMIT 10",
|
|
1936
1947
|
)
|
|
1937
1948
|
.all(now);
|
|
1938
1949
|
if (due.length === 0) return;
|
|
1939
|
-
const claim =
|
|
1940
|
-
const rearm =
|
|
1950
|
+
const claim = doDb.query("DELETE FROM do_alarm WHERE cls = ? AND id = ?");
|
|
1951
|
+
const rearm = doDb.query(
|
|
1941
1952
|
"INSERT INTO do_alarm (cls, id, at, attempts) VALUES (?1,?2,?3,?4) ON CONFLICT (cls, id) DO NOTHING",
|
|
1942
1953
|
);
|
|
1943
1954
|
for (const row of due) {
|
|
@@ -1980,6 +1991,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1980
1991
|
for (const t of timers) clearInterval(t);
|
|
1981
1992
|
for (const conn of d1Conns.values()) conn.close();
|
|
1982
1993
|
for (const conn of resourceDbs.values()) conn.close();
|
|
1994
|
+
if (doDb !== db) doDb.close();
|
|
1983
1995
|
db.close();
|
|
1984
1996
|
},
|
|
1985
1997
|
};
|
|
@@ -2086,6 +2098,7 @@ if (import.meta.main) {
|
|
|
2086
2098
|
db: { type: "string" },
|
|
2087
2099
|
"data-dir": { type: "string" },
|
|
2088
2100
|
"resource-dir": { type: "string" },
|
|
2101
|
+
"do-db": { type: "string" },
|
|
2089
2102
|
"owner-id": { type: "string" },
|
|
2090
2103
|
"r2-quota-bytes": { type: "string" },
|
|
2091
2104
|
"r2-resource-ids": { type: "string" },
|
|
@@ -2138,6 +2151,7 @@ if (import.meta.main) {
|
|
|
2138
2151
|
db: values.db,
|
|
2139
2152
|
dataDir: values["data-dir"],
|
|
2140
2153
|
resourceDir: values["resource-dir"],
|
|
2154
|
+
doDb: values["do-db"],
|
|
2141
2155
|
ownerId: values["owner-id"],
|
|
2142
2156
|
r2QuotaBytes: Number(values["r2-quota-bytes"] ?? process.env.SPROUTBOAT_R2_QUOTA_BYTES) || undefined,
|
|
2143
2157
|
r2ResourceIds: values["r2-resource-ids"] ? String(values["r2-resource-ids"]).split(",").filter(Boolean) : undefined,
|