@sproutboat/wire 0.9.2 → 0.9.3
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 +8 -0
- package/package.json +1 -1
- package/src/broker.test.ts +30 -1
- package/src/broker.ts +21 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @sproutboat/wire
|
|
2
2
|
|
|
3
|
+
## 0.9.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5dba469: Binary data survives KV and outbound `fetch()` bodies (baronunread/sproutboat#233, and the request half of #232).
|
|
8
|
+
- **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.
|
|
9
|
+
- **`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.
|
|
10
|
+
|
|
3
11
|
## 0.9.2
|
|
4
12
|
|
|
5
13
|
### 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) =>
|
|
@@ -705,6 +733,7 @@ test("resource-backed KV persists across brokers and is shared by id", async ()
|
|
|
705
733
|
expect(await second.dispatch({ op: "kv.get", ns: "SHORTENER", key: "a" })).toEqual({
|
|
706
734
|
ok: true,
|
|
707
735
|
found: true,
|
|
736
|
+
binary: false,
|
|
708
737
|
value: "1",
|
|
709
738
|
});
|
|
710
739
|
second.close();
|
package/src/broker.ts
CHANGED
|
@@ -269,6 +269,9 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
269
269
|
);
|
|
270
270
|
const kvColumns = conn.query<{ name: string }, []>("PRAGMA table_info(kv)").all();
|
|
271
271
|
if (!kvColumns.some((column) => column.name === "expires_at")) conn.exec("ALTER TABLE kv ADD COLUMN expires_at INTEGER");
|
|
272
|
+
// #233 — 1 when the value was put as bytes (stored one char per byte), so get()
|
|
273
|
+
// can hand back exact bytes or decoded text depending on the requested type.
|
|
274
|
+
if (!kvColumns.some((column) => column.name === "binary")) conn.exec("ALTER TABLE kv ADD COLUMN binary INTEGER NOT NULL DEFAULT 0");
|
|
272
275
|
// #56 — no `body` column: an object's bytes live in their own file under
|
|
273
276
|
// `r2Dir` (or an in-memory map for `:memory:`), never in the SQLite row.
|
|
274
277
|
// Doubling every object through SQLite's own page cache on top of the
|
|
@@ -481,8 +484,8 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
481
484
|
};
|
|
482
485
|
|
|
483
486
|
type KvStmts = {
|
|
484
|
-
get: Statement<{ value: string; expires_at: number | null }, [string, string]>;
|
|
485
|
-
put: Statement<unknown, [string, string, string, number | null]>;
|
|
487
|
+
get: Statement<{ value: string; expires_at: number | null; binary: number }, [string, string]>;
|
|
488
|
+
put: Statement<unknown, [string, string, string, number | null, number]>;
|
|
486
489
|
del: Statement<unknown, [string, string]>;
|
|
487
490
|
list: Statement<{ key: string }, [string, string, number]>;
|
|
488
491
|
};
|
|
@@ -491,12 +494,12 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
491
494
|
let stmts = kvStmtCache.get(store);
|
|
492
495
|
if (!stmts) {
|
|
493
496
|
stmts = {
|
|
494
|
-
get: store.query<{ value: string; expires_at: number | null }, [string, string]>(
|
|
495
|
-
"SELECT value, expires_at FROM kv WHERE ns = ? AND key = ?",
|
|
497
|
+
get: store.query<{ value: string; expires_at: number | null; binary: number }, [string, string]>(
|
|
498
|
+
"SELECT value, expires_at, binary FROM kv WHERE ns = ? AND key = ?",
|
|
496
499
|
),
|
|
497
500
|
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",
|
|
501
|
+
"INSERT INTO kv (ns, key, value, expires_at, binary) VALUES (?1, ?2, ?3, ?4, ?5) " +
|
|
502
|
+
"ON CONFLICT (ns, key) DO UPDATE SET value = ?3, expires_at = ?4, binary = ?5",
|
|
500
503
|
),
|
|
501
504
|
del: store.query("DELETE FROM kv WHERE ns = ? AND key = ?"),
|
|
502
505
|
// #58 — an expired row is filtered out here; a periodic sweep to hard-delete
|
|
@@ -1149,6 +1152,14 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1149
1152
|
type Upstream = { reply: Frame; bytes: Buffer };
|
|
1150
1153
|
const asTextFrame = (u: Upstream) => ({ ...u.reply, body: new TextDecoder().decode(u.bytes) });
|
|
1151
1154
|
|
|
1155
|
+
/** #232 — a binary request body arrives one char per byte; send those bytes, not their UTF-8. */
|
|
1156
|
+
const upstreamBody = (msg: Frame): string | Uint8Array<ArrayBuffer> | undefined =>
|
|
1157
|
+
msg.body == null
|
|
1158
|
+
? undefined
|
|
1159
|
+
: msg.bodyBinary === true
|
|
1160
|
+
? new Uint8Array(Buffer.from(str(msg.body), "latin1"))
|
|
1161
|
+
: str(msg.body);
|
|
1162
|
+
|
|
1152
1163
|
async function proxyFetch(msg: Frame): Promise<Upstream> {
|
|
1153
1164
|
let url: URL;
|
|
1154
1165
|
try {
|
|
@@ -1169,7 +1180,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1169
1180
|
const res = await doFetch(url, {
|
|
1170
1181
|
method,
|
|
1171
1182
|
headers,
|
|
1172
|
-
body:
|
|
1183
|
+
body: method === "GET" || method === "HEAD" ? undefined : upstreamBody(msg),
|
|
1173
1184
|
redirect: "manual",
|
|
1174
1185
|
});
|
|
1175
1186
|
const outHeaders: Array<[string, string]> = [];
|
|
@@ -1215,7 +1226,7 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1215
1226
|
const res = await doFetch(target, {
|
|
1216
1227
|
method,
|
|
1217
1228
|
headers,
|
|
1218
|
-
body:
|
|
1229
|
+
body: method === "GET" || method === "HEAD" ? undefined : upstreamBody(msg),
|
|
1219
1230
|
redirect: "manual",
|
|
1220
1231
|
});
|
|
1221
1232
|
const outHeaders: Array<[string, string]> = [];
|
|
@@ -1236,11 +1247,11 @@ export function createBroker(opts: BrokerOptions = {}): Broker {
|
|
|
1236
1247
|
stmts.del.run(part, str(msg.key));
|
|
1237
1248
|
return { ok: true, found: false, value: null };
|
|
1238
1249
|
}
|
|
1239
|
-
return { ok: true, found: true, value: row.value };
|
|
1250
|
+
return { ok: true, found: true, value: row.value, binary: row.binary === 1 };
|
|
1240
1251
|
}
|
|
1241
1252
|
case "kv.put": {
|
|
1242
1253
|
const { store, part } = storeFor("kv", requireKv(msg.ns));
|
|
1243
|
-
kvStmts(store).put.run(part, str(msg.key), str(msg.value), kvExpiresAt(msg));
|
|
1254
|
+
kvStmts(store).put.run(part, str(msg.key), str(msg.value), kvExpiresAt(msg), msg.binary === true ? 1 : 0);
|
|
1244
1255
|
return { ok: true };
|
|
1245
1256
|
}
|
|
1246
1257
|
case "kv.delete": {
|