@sproutboat/runtime 0.13.4 → 0.13.5
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/native-fetch-prelude.js +32 -5
- package/src/transport-embedded.js +41 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.13.5
|
|
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.13.4
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -646,6 +646,18 @@ function __sbFromUtf8(bytes) {
|
|
|
646
646
|
// on every asset request regardless of whether anything used it -- this was
|
|
647
647
|
// a real, shipped performance regression on sproutboat.com, caught after
|
|
648
648
|
// the fact by unexpectedly slow page loads.
|
|
649
|
+
// #232 — a fetch() request body for the wire: text as-is, and an ArrayBuffer or
|
|
650
|
+
// any view as its bytes (one char per byte) flagged bodyBinary, instead of
|
|
651
|
+
// String(view), which sent "0,1,2,...".
|
|
652
|
+
function __sbRequestBody(body) {
|
|
653
|
+
if (body == null) return { body: null };
|
|
654
|
+
if (!__sbIsStr(body) && body.byteLength !== undefined) {
|
|
655
|
+
const bytes = body.buffer !== undefined ? new Uint8Array(body.buffer, body.byteOffset, body.byteLength) : body;
|
|
656
|
+
return { body: __sbToBytes(bytes), bodyBinary: true };
|
|
657
|
+
}
|
|
658
|
+
return { body: String(body) };
|
|
659
|
+
}
|
|
660
|
+
|
|
649
661
|
function __sbRawBodyResponse(body, init) {
|
|
650
662
|
const resp = new Response(body == null ? "" : body, init);
|
|
651
663
|
if (body != null) {
|
|
@@ -1364,12 +1376,27 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
1364
1376
|
for (let i = 0; i < (bindings.kv || []).length; i++) {
|
|
1365
1377
|
const ns = bindings.kv[i];
|
|
1366
1378
|
target[ns] = {
|
|
1367
|
-
get(key)
|
|
1379
|
+
// #233 — Workers' get(key, type | { type }): "text" (default), "json",
|
|
1380
|
+
// "arrayBuffer". A value put as bytes is stored one char per byte and
|
|
1381
|
+
// flagged, so text decodes it as UTF-8 and arrayBuffer returns it
|
|
1382
|
+
// exactly; a text value read as arrayBuffer is its UTF-8 encoding.
|
|
1383
|
+
get(key, typeOrOptions) {
|
|
1368
1384
|
const r = __sbRpc("kv.get", { ns, key: String(key) });
|
|
1369
|
-
|
|
1385
|
+
if (!r.found) return null;
|
|
1386
|
+
const type = typeOrOptions == null ? "text" : __sbIsStr(typeOrOptions) ? typeOrOptions : typeOrOptions.type || "text";
|
|
1387
|
+
if (type === "arrayBuffer") return __sbBufFrom(r.binary ? r.value : __sbToBytes(r.value));
|
|
1388
|
+
if (type === "stream") throw new TypeError('KV get type "stream" is not supported (no ReadableStream); use "arrayBuffer"');
|
|
1389
|
+
const text = r.binary ? __sbFromUtf8(r.value) : r.value;
|
|
1390
|
+
return type === "json" ? JSON.parse(text) : text;
|
|
1370
1391
|
},
|
|
1371
1392
|
put(key, value, options) {
|
|
1372
|
-
const msg = { ns, key: String(key), value:
|
|
1393
|
+
const msg = { ns, key: String(key), value: "" };
|
|
1394
|
+
if (value != null && !__sbIsStr(value) && value.byteLength !== undefined) {
|
|
1395
|
+
// ArrayBuffer or any ArrayBufferView: its bytes, not String(view) ("0,1,2,...").
|
|
1396
|
+
const bytes = value.buffer !== undefined ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : value;
|
|
1397
|
+
msg.value = __sbToBytes(bytes);
|
|
1398
|
+
msg.binary = true;
|
|
1399
|
+
} else msg.value = String(value);
|
|
1373
1400
|
if (options && options.expirationTtl != null) msg.expirationTtl = Number(options.expirationTtl);
|
|
1374
1401
|
else if (options && options.expiration != null) msg.expiration = Number(options.expiration);
|
|
1375
1402
|
__sbRpc("kv.put", msg);
|
|
@@ -1595,7 +1622,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
1595
1622
|
url,
|
|
1596
1623
|
method: opts.method || "GET",
|
|
1597
1624
|
headers,
|
|
1598
|
-
|
|
1625
|
+
...__sbRequestBody(opts.body),
|
|
1599
1626
|
});
|
|
1600
1627
|
const respHeaders = new Headers();
|
|
1601
1628
|
for (let j = 0; j < (r.headers || []).length; j++) respHeaders.set(r.headers[j][0], r.headers[j][1]);
|
|
@@ -1621,7 +1648,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
1621
1648
|
url,
|
|
1622
1649
|
method: opts.method || "GET",
|
|
1623
1650
|
headers,
|
|
1624
|
-
|
|
1651
|
+
...__sbRequestBody(opts.body),
|
|
1625
1652
|
});
|
|
1626
1653
|
const respHeaders = new Headers();
|
|
1627
1654
|
for (let j = 0; j < (r.headers || []).length; j++) respHeaders.set(r.headers[j][0], r.headers[j][1]);
|
|
@@ -1042,14 +1042,15 @@ static int sb_tcp_connect(const char* host, int port) {
|
|
|
1042
1042
|
return fd;
|
|
1043
1043
|
}
|
|
1044
1044
|
|
|
1045
|
+
// body_len, not strlen(body): a binary body can contain NUL bytes (#232).
|
|
1045
1046
|
static void sb_build_request(sb_buf* req, const char* host, const char* path, const char* method,
|
|
1046
|
-
const char* headers, const char* body) {
|
|
1047
|
+
const char* headers, const char* body, size_t body_len) {
|
|
1047
1048
|
sb_puts(req, method); sb_puts(req, " "); sb_puts(req, path); sb_puts(req, " HTTP/1.1\r\n");
|
|
1048
1049
|
sb_puts(req, "Host: "); sb_puts(req, host); sb_puts(req, "\r\n");
|
|
1049
1050
|
sb_puts(req, "Connection: close\r\n");
|
|
1050
1051
|
sb_puts(req, "Accept-Encoding: identity\r\n");
|
|
1051
1052
|
if (headers && *headers) sb_puts(req, headers);
|
|
1052
|
-
size_t blen = body ?
|
|
1053
|
+
size_t blen = body ? body_len : 0;
|
|
1053
1054
|
if (blen) {
|
|
1054
1055
|
char cl[64];
|
|
1055
1056
|
int k = snprintf(cl, sizeof(cl), "Content-Length: %zu\r\n", blen);
|
|
@@ -1153,12 +1154,12 @@ static char* sb_error_json(const char* prefix, const char* detail) {
|
|
|
1153
1154
|
}
|
|
1154
1155
|
|
|
1155
1156
|
static char* sb_http_plain(const char* host, int port, const char* path, const char* method,
|
|
1156
|
-
const char* headers, const char* body) {
|
|
1157
|
+
const char* headers, const char* body, size_t body_len) {
|
|
1157
1158
|
int fd = sb_tcp_connect(host, port);
|
|
1158
1159
|
if (fd < 0) return sb_error_json("could not connect to ", host);
|
|
1159
1160
|
|
|
1160
1161
|
sb_buf req = { 0, 0, 0 };
|
|
1161
|
-
sb_build_request(&req, host, path, method, headers, body);
|
|
1162
|
+
sb_build_request(&req, host, path, method, headers, body, body_len);
|
|
1162
1163
|
size_t sent = 0;
|
|
1163
1164
|
while (sent < req.len) {
|
|
1164
1165
|
long n = write(fd, req.p + sent, req.len - sent);
|
|
@@ -1343,7 +1344,7 @@ static int sb_sock_write(void* ctx, const unsigned char* buf, size_t len) {
|
|
|
1343
1344
|
}
|
|
1344
1345
|
|
|
1345
1346
|
static char* sb_https(const char* host, int port, const char* path, const char* method,
|
|
1346
|
-
const char* headers, const char* body) {
|
|
1347
|
+
const char* headers, const char* body, size_t body_len) {
|
|
1347
1348
|
int fd = sb_tcp_connect(host, port);
|
|
1348
1349
|
if (fd < 0) return sb_error_json("could not connect to ", host);
|
|
1349
1350
|
|
|
@@ -1362,7 +1363,7 @@ static char* sb_https(const char* host, int port, const char* path, const char*
|
|
|
1362
1363
|
br_sslio_init(&ioc, &sc->eng, sb_sock_read, &fd, sb_sock_write, &fd);
|
|
1363
1364
|
|
|
1364
1365
|
sb_buf req = { 0, 0, 0 };
|
|
1365
|
-
sb_build_request(&req, host, path, method, headers, body);
|
|
1366
|
+
sb_build_request(&req, host, path, method, headers, body, body_len);
|
|
1366
1367
|
int wrote = br_sslio_write_all(&ioc, req.p, req.len);
|
|
1367
1368
|
free(req.p);
|
|
1368
1369
|
if (wrote != 0) {
|
|
@@ -1412,9 +1413,9 @@ static char* sb_https(const char* host, int port, const char* path, const char*
|
|
|
1412
1413
|
}
|
|
1413
1414
|
|
|
1414
1415
|
static char* sb_http_request(const char* host, int port, const char* path, const char* method,
|
|
1415
|
-
const char* headers, const char* body, int tls) {
|
|
1416
|
-
return tls ? sb_https(host, port, path, method, headers, body)
|
|
1417
|
-
: sb_http_plain(host, port, path, method, headers, body);
|
|
1416
|
+
const char* headers, const char* body, size_t body_len, int tls) {
|
|
1417
|
+
return tls ? sb_https(host, port, path, method, headers, body, body_len)
|
|
1418
|
+
: sb_http_plain(host, port, path, method, headers, body, body_len);
|
|
1418
1419
|
}
|
|
1419
1420
|
`;
|
|
1420
1421
|
|
|
@@ -1730,17 +1731,33 @@ function __sbHttpRaw(host, portStr, path, method, headersText, body, tlsFlag) {
|
|
|
1730
1731
|
char* __hdrs = (char*)malloc(__hdl + 1); memcpy(__hdrs, __hd, __hdl); __hdrs[__hdl] = 0;
|
|
1731
1732
|
if (__hdo) free(__hdo);
|
|
1732
1733
|
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
char* __body = (char*)malloc(__bl + 1); memcpy(__body, __b, __bl); __body[__bl] = 0;
|
|
1736
|
-
if (__bo) free(__bo);
|
|
1737
|
-
|
|
1734
|
+
// tlsFlag is "0"/"1", plus "b" when the body is raw bytes (#233-style flag
|
|
1735
|
+
// for #232's request half): one char per byte, sent as-is, not UTF-8 encoded.
|
|
1738
1736
|
const char* __t; size_t __tl; char* __to = 0;
|
|
1739
1737
|
porf_native_fetch_read_value(tlsFlag, &__t, &__tl, &__to);
|
|
1740
1738
|
int __tls = (__tl > 0 && __t[0] == '1') ? 1 : 0;
|
|
1739
|
+
int __rawBody = (__tl > 1 && __t[1] == 'b') ? 1 : 0;
|
|
1741
1740
|
if (__to) free(__to);
|
|
1742
1741
|
|
|
1743
|
-
char*
|
|
1742
|
+
const char* __b; size_t __bl; char* __bo = 0;
|
|
1743
|
+
if (__rawBody && porf_native_fetch_read_raw_bytes(body, &__b, &__bl) == 0) {
|
|
1744
|
+
// a bytestring is already one byte per char
|
|
1745
|
+
} else if (__rawBody && body.type == 67) {
|
|
1746
|
+
// A UTF-16 string (type 67: u32 length, then u16 units) still holds one
|
|
1747
|
+
// byte per unit here; narrow each unit instead of UTF-8 encoding it.
|
|
1748
|
+
const u32 __bp = (u32)body.val;
|
|
1749
|
+
__bl = (size_t)*(u32*)(MEM + __bp);
|
|
1750
|
+
__bo = (char*)malloc(__bl + 1);
|
|
1751
|
+
const u16* __bs = (const u16*)(MEM + __bp + 4);
|
|
1752
|
+
for (size_t __i = 0; __i < __bl; __i++) __bo[__i] = (char)(__bs[__i] & 0xff);
|
|
1753
|
+
__b = __bo;
|
|
1754
|
+
} else {
|
|
1755
|
+
porf_native_fetch_read_value(body, &__b, &__bl, &__bo);
|
|
1756
|
+
}
|
|
1757
|
+
char* __body = (char*)malloc(__bl + 1); memcpy(__body, __b, __bl); __body[__bl] = 0;
|
|
1758
|
+
if (__bo) free(__bo);
|
|
1759
|
+
|
|
1760
|
+
char* __out = sb_http_request(__host, atoi(__portbuf), __path, __method, __hdrs, __body, __bl, __tls);
|
|
1744
1761
|
free(__host); free(__path); free(__method); free(__hdrs); free(__body);
|
|
1745
1762
|
if (__out) {
|
|
1746
1763
|
res = porf_box((f64)porf_native_fetch_alloc_bytestring(__out, strlen(__out)), 195);
|
|
@@ -1835,6 +1852,10 @@ function __sbEnsureSchema() {
|
|
|
1835
1852
|
let hasKvExpiresAt = false;
|
|
1836
1853
|
for (let i = 0; i < kvColumns.length; i++) if (kvColumns[i][1] === "expires_at") hasKvExpiresAt = true;
|
|
1837
1854
|
if (!hasKvExpiresAt) __sbSql(s, "ALTER TABLE kv ADD COLUMN expires_at INTEGER", []);
|
|
1855
|
+
// #233 — 1 when the value was put as bytes, same column as the broker's.
|
|
1856
|
+
let hasKvBinary = false;
|
|
1857
|
+
for (let i = 0; i < kvColumns.length; i++) if (kvColumns[i][1] === "binary") hasKvBinary = true;
|
|
1858
|
+
if (!hasKvBinary) __sbSql(s, "ALTER TABLE kv ADD COLUMN binary INTEGER NOT NULL DEFAULT 0", []);
|
|
1838
1859
|
// #56 — no `body` column: an object's bytes live in their own file under
|
|
1839
1860
|
// "<data-dir>/r2-blobs/" (sb_r2_put_c/sb_r2_get_c), never in the sqlite row.
|
|
1840
1861
|
__sbSql(
|
|
@@ -1928,14 +1949,14 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1928
1949
|
if (op === "ping") return { ok: true, op: "pong", echo: msg.msg };
|
|
1929
1950
|
|
|
1930
1951
|
if (op === "kv.get") {
|
|
1931
|
-
const r = __sbSql(store, "SELECT value, expires_at FROM kv WHERE ns = ? AND key = ?", [msg.ns, msg.key]);
|
|
1952
|
+
const r = __sbSql(store, "SELECT value, expires_at, binary FROM kv WHERE ns = ? AND key = ?", [msg.ns, msg.key]);
|
|
1932
1953
|
if (!r.rows.length) return { ok: true, found: false, value: null };
|
|
1933
1954
|
const expiresAt = r.rows[0][1];
|
|
1934
1955
|
if (expiresAt != null && expiresAt <= Date.now()) {
|
|
1935
1956
|
__sbSql(store, "DELETE FROM kv WHERE ns = ? AND key = ?", [msg.ns, msg.key]);
|
|
1936
1957
|
return { ok: true, found: false, value: null };
|
|
1937
1958
|
}
|
|
1938
|
-
return { ok: true, found: true, value: r.rows[0][0] };
|
|
1959
|
+
return { ok: true, found: true, value: r.rows[0][0], binary: r.rows[0][2] === 1 };
|
|
1939
1960
|
}
|
|
1940
1961
|
if (op === "kv.put") {
|
|
1941
1962
|
// #58 — CF rejects expirationTtl under 60 seconds; matched here.
|
|
@@ -1949,8 +1970,8 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1949
1970
|
}
|
|
1950
1971
|
__sbSql(
|
|
1951
1972
|
store,
|
|
1952
|
-
"INSERT INTO kv (ns, key, value, expires_at) VALUES (?1,?2,?3,?4) ON CONFLICT (ns, key) DO UPDATE SET value = ?3, expires_at = ?4",
|
|
1953
|
-
[msg.ns, msg.key, msg.value, expiresAt],
|
|
1973
|
+
"INSERT INTO kv (ns, key, value, expires_at, binary) VALUES (?1,?2,?3,?4,?5) ON CONFLICT (ns, key) DO UPDATE SET value = ?3, expires_at = ?4, binary = ?5",
|
|
1974
|
+
[msg.ns, msg.key, msg.value, expiresAt, msg.binary === true ? 1 : 0],
|
|
1954
1975
|
);
|
|
1955
1976
|
return { ok: true };
|
|
1956
1977
|
}
|
|
@@ -2047,7 +2068,7 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
2047
2068
|
String(msg.method || "GET").toUpperCase(),
|
|
2048
2069
|
headerText,
|
|
2049
2070
|
msg.body == null ? "" : String(msg.body),
|
|
2050
|
-
tls ? "1" : "0",
|
|
2071
|
+
(tls ? "1" : "0") + (msg.bodyBinary === true ? "b" : ""),
|
|
2051
2072
|
),
|
|
2052
2073
|
);
|
|
2053
2074
|
if (reply.ok === false) throw new Error(reply.error);
|