@sproutboat/wire 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +28 -13
- package/package.json +1 -1
- package/src/broker.test.ts +24 -0
- package/src/broker.ts +34 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @sproutboat/wire
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Standalone runtime: client IP, D1 online backup, and a prepared-statement cache.
|
|
8
|
+
|
|
9
|
+
- `request.cf.clientIp` is populated from the connection's remote address
|
|
10
|
+
(`x-sb-remote-addr`, appended by the server and stripped from client input),
|
|
11
|
+
with `SB_TRUSTED_PROXIES` resolving `X-Forwarded-For` behind a reverse proxy.
|
|
12
|
+
IPv4-mapped IPv6 peers are folded to dotted form. (baronunread/sproutboat#163)
|
|
13
|
+
- `env.<D1>.backup(name?)` — a Sproutboat extension: an online, integrity-checked
|
|
14
|
+
single-file snapshot via `VACUUM INTO`, on both the embedded and broker
|
|
15
|
+
transports. New `d1.backup` op. (baronunread/sproutboat#164)
|
|
16
|
+
- The embedded transport caches prepared statements per database (FIFO, 32/db)
|
|
17
|
+
instead of recompiling the SQL on every binding op. (baronunread/sproutboat#155)
|
|
18
|
+
|
|
3
19
|
## 0.2.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
# `@sproutboat/wire`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
Broker frame protocol and the JSON validation utilities it is built on.
|
|
4
|
+
Single source of truth shared by the CLI dev broker and the platform's
|
|
5
|
+
edge and supervisor.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @sproutboat/wire
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createBroker } from "@sproutboat/wire";
|
|
13
|
+
import { parseJsonValue } from "@sproutboat/wire";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## API
|
|
17
|
+
|
|
18
|
+
- `createBroker(options)` opens a binding broker over a resource file:
|
|
19
|
+
KV, D1, R2, queues, Durable Object storage and alarms, Analytics Engine
|
|
20
|
+
points, secrets, outbound fetch. Frame envelope, both directions:
|
|
21
|
+
`[u32 LE length][payload]`.
|
|
22
|
+
- Frame helpers: `frameOf`, `encodeFrame`, `encodeV1`, `FRAME_V1`,
|
|
23
|
+
`cronMatches`, plus the `Broker`, `BrokerOptions`, `BrokerServer`,
|
|
24
|
+
`FetchLike` and `Frame` types.
|
|
25
|
+
- JSON boundary parsing: `parseJsonValue`, `jsonObject`, guards
|
|
26
|
+
(`isString`, `isBoolean`, `isSafeInteger`), `JsonValue` and `JsonObject`
|
|
27
|
+
types. Parse at the I/O boundary, then narrow; nothing downstream takes
|
|
28
|
+
an unparsed value.
|
|
29
|
+
|
|
30
|
+
See `src/broker.test.ts` for the protocol contract in executable form.
|
package/package.json
CHANGED
package/src/broker.test.ts
CHANGED
|
@@ -105,6 +105,30 @@ test("D1: query / run / batch on a bound database, isolated per name", async ()
|
|
|
105
105
|
await expect(b.dispatch({ op: "d1.query", db: "NOPE", sql: "SELECT 1", params: [] })).rejects.toThrow("not bound");
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
test("D1: backup writes an integrity-checked snapshot, replayed from cache", async () => {
|
|
109
|
+
const dir = mkdtempSync(join(tmpdir(), "sb-broker-backup-"));
|
|
110
|
+
try {
|
|
111
|
+
const b = make({ db: join(dir, "store.sqlite"), bindings: { d1: ["DB"] } });
|
|
112
|
+
await b.dispatch({ op: "d1.exec", db: "DB", sql: "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)" });
|
|
113
|
+
await b.dispatch({ op: "d1.query", db: "DB", sql: "INSERT INTO t (name) VALUES (?)", params: ["ada"] });
|
|
114
|
+
|
|
115
|
+
const bk = await b.dispatch({ op: "d1.backup", db: "DB", name: "snap.sqlite" });
|
|
116
|
+
expect(bk.ok).toBe(true);
|
|
117
|
+
expect(String(bk.path).endsWith(join("backups", "snap.sqlite"))).toBe(true);
|
|
118
|
+
expect(Number(bk.bytes)).toBeGreaterThan(0);
|
|
119
|
+
expect(existsSync(String(bk.path))).toBe(true);
|
|
120
|
+
|
|
121
|
+
// the snapshot is a real, readable database with the row in it
|
|
122
|
+
const snap = new Database(String(bk.path), { readonly: true });
|
|
123
|
+
expect(snap.query("SELECT name FROM t").all()).toEqual([{ name: "ada" }]);
|
|
124
|
+
snap.close();
|
|
125
|
+
|
|
126
|
+
await expect(b.dispatch({ op: "d1.backup", db: "NOPE" })).rejects.toThrow("not bound");
|
|
127
|
+
} finally {
|
|
128
|
+
rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
108
132
|
test("R2: put / get / head / list / delete on a bound bucket", async () => {
|
|
109
133
|
const b = make({ bindings: { r2: ["ASSETS"] } });
|
|
110
134
|
const put = await b.dispatch({
|
package/src/broker.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { Database, type Statement } from "bun:sqlite";
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
23
23
|
import { dirname, join, normalize, resolve } from "node:path";
|
|
24
24
|
import { parseArgs } from "node:util";
|
|
25
25
|
import { resolveAssetKey, type AssetManifest } from "@sproutboat/assets";
|
|
@@ -375,6 +375,34 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
375
375
|
};
|
|
376
376
|
};
|
|
377
377
|
|
|
378
|
+
// #164 — an online, integrity-checked snapshot of a D1 database via
|
|
379
|
+
// VACUUM INTO. Standalone (transport-embedded) is the primary target; the
|
|
380
|
+
// broker carries it too so one conformance suite covers both backends.
|
|
381
|
+
const d1Backup = (conn: Database, dbName: string, name: string) => {
|
|
382
|
+
const source = conn.filename;
|
|
383
|
+
if (!source || source === ":memory:") throw new Error("d1 backup needs a file-backed database");
|
|
384
|
+
// A caller-supplied name must be a plain basename; else use the default.
|
|
385
|
+
const file = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)
|
|
386
|
+
? name
|
|
387
|
+
: `${dbName}-${new Date().toISOString().replace(/[:.]/g, "-")}.sqlite`;
|
|
388
|
+
const dir = join(dirname(dirname(resolve(source))), "backups");
|
|
389
|
+
mkdirSync(dir, { recursive: true });
|
|
390
|
+
const dest = join(dir, file);
|
|
391
|
+
if (existsSync(dest)) rmSync(dest); // VACUUM INTO refuses an existing target
|
|
392
|
+
conn.run("VACUUM INTO ?", [dest]);
|
|
393
|
+
const copy = new Database(dest, { readonly: true });
|
|
394
|
+
try {
|
|
395
|
+
const row = copy.query<{ integrity_check: string }, []>("PRAGMA integrity_check").get();
|
|
396
|
+
if (row?.integrity_check !== "ok") {
|
|
397
|
+
rmSync(dest, { force: true });
|
|
398
|
+
throw new Error("integrity check failed on the backup");
|
|
399
|
+
}
|
|
400
|
+
} finally {
|
|
401
|
+
copy.close();
|
|
402
|
+
}
|
|
403
|
+
return { path: dest, bytes: statSync(dest).size };
|
|
404
|
+
};
|
|
405
|
+
|
|
378
406
|
/** An object body as bytes, whichever storage class it came back in. */
|
|
379
407
|
const r2Bytes = (body: string | Uint8Array): Uint8Array =>
|
|
380
408
|
body instanceof Uint8Array ? body : new TextEncoder().encode(body);
|
|
@@ -553,6 +581,10 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
553
581
|
conn.exec(str(msg.sql));
|
|
554
582
|
return { ok: true };
|
|
555
583
|
}
|
|
584
|
+
case "d1.backup": {
|
|
585
|
+
const name = requireD1(msg.db);
|
|
586
|
+
return { ok: true, ...d1Backup(d1(name), name, msg.name == null ? "" : str(msg.name)) };
|
|
587
|
+
}
|
|
556
588
|
|
|
557
589
|
case "r2.put": {
|
|
558
590
|
const { store, part: bucket } = storeFor("r2", requireR2(msg.bucket));
|
|
@@ -864,6 +896,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
864
896
|
"d1.query",
|
|
865
897
|
"d1.exec",
|
|
866
898
|
"d1.batch",
|
|
899
|
+
"d1.backup",
|
|
867
900
|
"r2.put",
|
|
868
901
|
"r2.delete",
|
|
869
902
|
"do.storage.put",
|