@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.13.4",
3
+ "version": "0.13.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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
- return r.found ? r.value : null;
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: String(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
- body: opts.body == null ? null : String(opts.body),
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
- body: opts.body == null ? null : String(opts.body),
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 ? strlen(body) : 0;
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
- const char* __b; size_t __bl; char* __bo = 0;
1734
- porf_native_fetch_read_value(body, &__b, &__bl, &__bo);
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* __out = sb_http_request(__host, atoi(__portbuf), __path, __method, __hdrs, __body, __tls);
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);