@sproutboat/runtime 0.22.0 → 0.23.1
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 +44 -0
- package/package.json +1 -1
- package/src/d1-shim.test.js +114 -0
- package/src/native-fetch-prelude.js +154 -21
- package/src/transport-embedded.js +182 -32
- package/test/native-d1.ts +283 -0
- package/test/native-fetch-request.ts +305 -0
- package/test/native-ltx.ts +59 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.23.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7a7fd34: D1 correctness fixes, in the standalone (embedded) transport and the broker:
|
|
8
|
+
|
|
9
|
+
- **Bytes are no longer lost.** Binding a `Uint8Array`, another typed array, a `DataView` or an `ArrayBuffer` stored NULL in a standalone sprout, along with every parameter after it. It now stores a BLOB, at every compatibility date. On earlier dates a BLOB still reads back as before: as one character per byte.
|
|
10
|
+
- **`batch()` is one transaction** in a standalone sprout, as it already was through the broker and is in D1: if a statement fails, the ones before it are rolled back. This applies at every compatibility date.
|
|
11
|
+
- **From compatibility date `2026-10-24`**, D1 follows workerd's rules:
|
|
12
|
+
- a parameter must be `null`, a number, a string, a boolean or bytes, or the call throws `D1_TYPE_ERROR` (before, `undefined` and objects became NULL);
|
|
13
|
+
- the number of parameters must match the statement (`D1_ERROR: Wrong number of parameter bindings for SQL query.`);
|
|
14
|
+
- foreign keys are enforced (`PRAGMA foreign_keys = ON`);
|
|
15
|
+
- errors read `D1_ERROR: <message>: SQLITE_<CODE>`;
|
|
16
|
+
- a BLOB reads back as an array of byte values.
|
|
17
|
+
|
|
18
|
+
Apps with an earlier `compatibility_date` keep the old behaviour. To opt in, move the date to `2026-10-24` or later and rebuild.
|
|
19
|
+
- `meta.changes` is 0 for a statement that only reads. It used to repeat the previous write's count.
|
|
20
|
+
|
|
21
|
+
## 0.23.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- df612fc: `fetch(Request)`, `fetch(Request, init)` and `new Request(request, init)` behave as in workerd from compatibility date `2026-10-23`. This covers outgoing `fetch()`, service-binding `fetch()` and Durable Object stubs, on the broker and embedded transports alike. Every behaviour was checked against workerd 2026-09-27 by the platform's service-binding suite.
|
|
26
|
+
|
|
27
|
+
From `2026-10-23`:
|
|
28
|
+
|
|
29
|
+
- `fetch(input, init)` is `fetch(new Request(input, init))`, as the Fetch spec says. The Request's method, headers, body, redirect mode and signal apply unless `init` replaces them, and `init.headers` replaces all of the Request's headers.
|
|
30
|
+
- A Request given as `init` brings its method, headers and body.
|
|
31
|
+
- A body can be used once. `fetch(request)`, `new Request(request)` and reading it through `request.body` all use it, after which `bodyUsed` is true. Sending or copying a used Request throws `TypeError: Cannot reconstruct a Request with a used body.`, and reading it again throws workerd's "Body has already been used..." message. With a body in `init`, the source Request is left alone.
|
|
32
|
+
- A GET or HEAD Request with any body throws, even `""` or one inherited from another Request. An incoming GET or HEAD has `request.body === null`.
|
|
33
|
+
- Methods are workerd's: known names in any case are uppercased, and anything else throws `Invalid HTTP method string`. That includes `CONNECT` and custom names.
|
|
34
|
+
- `redirect` must be `"follow"` or `"manual"`. As in workerd, `"error"` throws when the Request is made.
|
|
35
|
+
- The headers of an incoming request, a fetched or service-binding response, and `Response.redirect()` are immutable: `set`, `append` and `delete` throw `TypeError: Can't modify immutable headers.` Copies are mutable, so `new Request(request)`, `new Response(body, response)` and `new Headers(h)` can still be changed.
|
|
36
|
+
- `AbortSignal.abort()`'s default reason reads "The operation was aborted", as in workerd.
|
|
37
|
+
|
|
38
|
+
With an earlier `compatibility_date` these keep their old behaviour: a copy shares the body, any method name and redirect mode passes, and headers stay mutable. To get the new behaviour, move `compatibility_date` to `2026-10-23` and rebuild.
|
|
39
|
+
|
|
40
|
+
These are fixed at every date, since no app can depend on the old behaviour:
|
|
41
|
+
|
|
42
|
+
- `Request.signal` exists. A Request made with `signal` passes it on to copies and to `fetch()`, which aborts with it.
|
|
43
|
+
- A `ReadableStream` body in an outgoing `fetch()` is sent as its bytes. Before, it went out as the text `[object ReadableStream]`.
|
|
44
|
+
- `request.clone()` and `response.clone()` tee a stream body, so both copies can read it. Before, the second reader failed.
|
|
45
|
+
- A copy of an incoming request keeps its raw bytes. Its `text()` decodes UTF-8, and `fetch()` sends the bytes unchanged.
|
|
46
|
+
|
|
3
47
|
## 0.22.0
|
|
4
48
|
|
|
5
49
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The D1 shim (`env.DB`) over the broker's D1 store (the production platform's path),
|
|
2
|
+
// at a compatibility date before and after 2026-10-24. The embedded transport's half is
|
|
3
|
+
// test/native-d1.ts.
|
|
4
|
+
import { expect, test } from "bun:test";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { createD1Store } from "../../wire/src/d1";
|
|
7
|
+
|
|
8
|
+
const source = readFileSync(new URL("./native-fetch-prelude.js", import.meta.url), "utf8");
|
|
9
|
+
let store;
|
|
10
|
+
// oxlint-disable-next-line no-function-constructor -- the prelude is prepended as text in production too
|
|
11
|
+
const { makeD1 } = new Function(
|
|
12
|
+
"Porffor",
|
|
13
|
+
"__testCall",
|
|
14
|
+
`${source}\n__sbCall = __testCall; return { makeD1: __sbMakeD1 };`,
|
|
15
|
+
)({ c: () => "" }, (json) => {
|
|
16
|
+
const msg = JSON.parse(json);
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(store.execute({ owner: "app", key: "app/DB", path: ":memory:", name: "DB", msg }));
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return JSON.stringify({ ok: false, error: error.message });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const fresh = (date) => {
|
|
24
|
+
globalThis.__sbCompat = date;
|
|
25
|
+
store?.close();
|
|
26
|
+
store = createD1Store();
|
|
27
|
+
return makeD1("DB");
|
|
28
|
+
};
|
|
29
|
+
const caught = (fn) => {
|
|
30
|
+
try {
|
|
31
|
+
fn();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return error.message;
|
|
34
|
+
}
|
|
35
|
+
return "did not throw";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
for (const date of ["2026-09-28", "2026-10-24"]) {
|
|
39
|
+
test(`${date}: bytes bind as a BLOB, and the parameters after them are kept`, () => {
|
|
40
|
+
const DB = fresh(date);
|
|
41
|
+
DB.exec("CREATE TABLE t (a TEXT, b BLOB, c TEXT)");
|
|
42
|
+
DB.prepare("INSERT INTO t VALUES (?, ?, ?)")
|
|
43
|
+
.bind("u8", new Uint8Array([0, 255, 7]), "after")
|
|
44
|
+
.run();
|
|
45
|
+
DB.prepare("INSERT INTO t VALUES (?, ?, ?)")
|
|
46
|
+
.bind("ab", new Uint8Array([1, 2]).buffer, "after")
|
|
47
|
+
.run();
|
|
48
|
+
DB.prepare("INSERT INTO t VALUES (?, ?, ?)")
|
|
49
|
+
.bind("view", new DataView(new Uint8Array([9, 8, 7]).buffer, 1, 2), "after")
|
|
50
|
+
.run();
|
|
51
|
+
expect(DB.prepare("SELECT a, hex(b) AS h, typeof(b) AS t, c FROM t ORDER BY rowid").all().results).toEqual([
|
|
52
|
+
{ a: "u8", h: "00FF07", t: "blob", c: "after" },
|
|
53
|
+
{ a: "ab", h: "0102", t: "blob", c: "after" },
|
|
54
|
+
{ a: "view", h: "0807", t: "blob", c: "after" },
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test(`${date}: batch() is one transaction`, () => {
|
|
59
|
+
const DB = fresh(date);
|
|
60
|
+
DB.exec("CREATE TABLE t (id INTEGER PRIMARY KEY)");
|
|
61
|
+
expect(() => DB.batch([DB.prepare("INSERT INTO t VALUES (1)"), DB.prepare("INSERT INTO t VALUES (1)")])).toThrow(
|
|
62
|
+
/UNIQUE/,
|
|
63
|
+
);
|
|
64
|
+
expect(DB.prepare("SELECT count(*) AS n FROM t").first("n")).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("before 2026-10-24: lenient parameters, no foreign keys, BLOBs as one char per byte", () => {
|
|
69
|
+
const DB = fresh("2026-09-28");
|
|
70
|
+
// (bun:sqlite has always refused a wrong parameter count here; the embedded transport has not.)
|
|
71
|
+
expect(DB.prepare("SELECT ?1 AS a").bind(undefined).first()).toEqual({ a: null });
|
|
72
|
+
DB.exec("CREATE TABLE p (id INTEGER PRIMARY KEY); CREATE TABLE c (p INTEGER REFERENCES p(id))");
|
|
73
|
+
DB.prepare("INSERT INTO c VALUES (99)").run();
|
|
74
|
+
expect(DB.prepare("SELECT x'00ff' AS b").first("b")).toBe("\u0000ÿ");
|
|
75
|
+
expect(caught(() => DB.prepare("SELEC 1").all())).toMatch(/^sproutboat d1\.query: /);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("from 2026-10-24: D1's parameter rules", () => {
|
|
79
|
+
const DB = fresh("2026-10-24");
|
|
80
|
+
expect(caught(() => DB.prepare("SELECT ?1").bind(undefined))).toBe(
|
|
81
|
+
"D1_TYPE_ERROR: Type 'undefined' not supported for value 'undefined'",
|
|
82
|
+
);
|
|
83
|
+
expect(caught(() => DB.prepare("SELECT ?1").bind({ a: 1 }))).toBe(
|
|
84
|
+
"D1_TYPE_ERROR: Type 'object' not supported for value '[object Object]'",
|
|
85
|
+
);
|
|
86
|
+
expect(caught(() => DB.prepare("SELECT ?1").bind(10n))).toBe(
|
|
87
|
+
"D1_TYPE_ERROR: Type 'bigint' not supported for value '10'",
|
|
88
|
+
);
|
|
89
|
+
expect(caught(() => DB.prepare("SELECT ?1, ?2").bind(1).all())).toBe(
|
|
90
|
+
"D1_ERROR: Wrong number of parameter bindings for SQL query.",
|
|
91
|
+
);
|
|
92
|
+
expect(caught(() => DB.prepare("SELECT ?1").bind(1, 2).all())).toBe(
|
|
93
|
+
"D1_ERROR: Wrong number of parameter bindings for SQL query.",
|
|
94
|
+
);
|
|
95
|
+
expect(caught(() => DB.prepare("SELECT ?1").all())).toBe(
|
|
96
|
+
"D1_ERROR: Wrong number of parameter bindings for SQL query.",
|
|
97
|
+
);
|
|
98
|
+
expect(DB.prepare("SELECT ?1 AS a, ?1 AS b, ?2 AS c").bind("x", true).first()).toEqual({ a: "x", b: "x", c: 1 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("from 2026-10-24: foreign keys, BLOBs as byte arrays, D1's error text", () => {
|
|
102
|
+
const DB = fresh("2026-10-24");
|
|
103
|
+
DB.exec("CREATE TABLE p (id INTEGER PRIMARY KEY); CREATE TABLE c (p INTEGER REFERENCES p(id))");
|
|
104
|
+
expect(caught(() => DB.prepare("INSERT INTO c VALUES (99)").run())).toBe(
|
|
105
|
+
"D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_FOREIGNKEY)",
|
|
106
|
+
);
|
|
107
|
+
DB.prepare("INSERT INTO p VALUES (1)").run();
|
|
108
|
+
expect(caught(() => DB.prepare("INSERT INTO p VALUES (1)").run())).toBe(
|
|
109
|
+
"D1_ERROR: UNIQUE constraint failed: p.id: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_PRIMARYKEY)",
|
|
110
|
+
);
|
|
111
|
+
expect(caught(() => DB.prepare("SELECT * FROM nope").all())).toBe("D1_ERROR: no such table: nope: SQLITE_ERROR");
|
|
112
|
+
expect(DB.prepare("SELECT x'00ff' AS b").first("b")).toEqual([0, 255]);
|
|
113
|
+
expect(DB.prepare("SELECT count(*) AS n FROM p").run().meta.changes).toBe(0);
|
|
114
|
+
});
|
|
@@ -3315,6 +3315,15 @@ for (const E of [Error, TypeError, RangeError, ReferenceError, SyntaxError, Eval
|
|
|
3315
3315
|
// Object.fromEntries(headers) work. Porffor's Headers has entries() but no iterator.
|
|
3316
3316
|
if (Headers.prototype[Symbol.iterator] == null) Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
|
|
3317
3317
|
|
|
3318
|
+
// workerd's immutable headers, from compatibility date 2026-10-23: an incoming request's,
|
|
3319
|
+
// a fetched or service-binding response's, and Response.redirect()'s refuse append, set
|
|
3320
|
+
// and delete with a TypeError. A copy (new Headers(h), new Response(body, res)) is
|
|
3321
|
+
// mutable. The runtime's own writes after the handler lift the guard first.
|
|
3322
|
+
function __sbFreezeHeaders(message) {
|
|
3323
|
+
if (globalThis.__sbCompat >= "2026-10-23") message.headers._immutable = true;
|
|
3324
|
+
return message;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3318
3327
|
// Porffor alpha-10 has no static Response.redirect().
|
|
3319
3328
|
if (Response.redirect == null) {
|
|
3320
3329
|
Response.redirect = function (url, status = 302) {
|
|
@@ -3322,7 +3331,7 @@ if (Response.redirect == null) {
|
|
|
3322
3331
|
throw new RangeError("invalid redirect status");
|
|
3323
3332
|
}
|
|
3324
3333
|
// Serialized like the URL parser does (a bare origin gains its "/"); invalid URLs throw.
|
|
3325
|
-
return new Response(null, { status, headers: { location: new URL(String(url)).href } });
|
|
3334
|
+
return __sbFreezeHeaders(new Response(null, { status, headers: { location: new URL(String(url)).href } }));
|
|
3326
3335
|
};
|
|
3327
3336
|
}
|
|
3328
3337
|
|
|
@@ -3739,7 +3748,9 @@ if (typeof globalThis.EventTarget !== "function") {
|
|
|
3739
3748
|
__sbAbort(reason) {
|
|
3740
3749
|
if (this.aborted) return;
|
|
3741
3750
|
this.aborted = true;
|
|
3742
|
-
|
|
3751
|
+
// workerd's default reason reads "The operation was aborted" (from 2026-10-23 here).
|
|
3752
|
+
const message = globalThis.__sbCompat >= "2026-10-23" ? "The operation was aborted" : "This operation was aborted";
|
|
3753
|
+
this.reason = reason === undefined ? namedError("AbortError", message) : reason;
|
|
3743
3754
|
const event = new Event("abort");
|
|
3744
3755
|
if (typeof this.onabort === "function") this.onabort.call(this, event);
|
|
3745
3756
|
this.dispatchEvent(event);
|
|
@@ -4798,17 +4809,80 @@ function __sbRpc(op, extra) {
|
|
|
4798
4809
|
|
|
4799
4810
|
// D1: a Cloudflare-shaped `env.<DB>` (prepare / bind / all / run / raw / first,
|
|
4800
4811
|
// plus batch and exec). Every call is one broker roundtrip.
|
|
4812
|
+
//
|
|
4813
|
+
// Bytes (an ArrayBuffer or a typed array) bind as a BLOB at every compatibility date:
|
|
4814
|
+
// before, they were stored as NULL. From compatibility date 2026-10-24 D1 calls are
|
|
4815
|
+
// strict, as in workerd: a parameter must be null, a number, a string, a boolean or
|
|
4816
|
+
// bytes (D1_TYPE_ERROR otherwise), the count must match the statement, foreign keys
|
|
4817
|
+
// are enforced, errors read "D1_ERROR: ...", and a BLOB comes back as an array of
|
|
4818
|
+
// byte values. Earlier dates keep the old behaviour.
|
|
4819
|
+
function __sbD1Strict() {
|
|
4820
|
+
return globalThis.__sbCompat >= "2026-10-24";
|
|
4821
|
+
}
|
|
4822
|
+
function __sbD1Param(value, strict) {
|
|
4823
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
4824
|
+
const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
4825
|
+
let hex = "";
|
|
4826
|
+
for (let i = 0; i < bytes.length; i++) hex += (bytes[i] < 16 ? "0" : "") + bytes[i].toString(16);
|
|
4827
|
+
return { $blob: hex };
|
|
4828
|
+
}
|
|
4829
|
+
if (!strict || value === null || value === true || value === false || Object(value) instanceof Number || __sbIsStr(value)) return value;
|
|
4830
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- workerd's D1_TYPE_ERROR names the value's typeof
|
|
4831
|
+
throw new Error("D1_TYPE_ERROR: Type '" + typeof value + "' not supported for value '" + String(value) + "'");
|
|
4832
|
+
}
|
|
4833
|
+
function __sbD1Rpc(op, extra, strict) {
|
|
4834
|
+
if (!strict) return __sbRpc(op, extra);
|
|
4835
|
+
extra.strict = true;
|
|
4836
|
+
try {
|
|
4837
|
+
return __sbRpc(op, extra);
|
|
4838
|
+
} catch (err) {
|
|
4839
|
+
// "sproutboat d1.query: D1_ERROR: ..." -> "D1_ERROR: ...", as workerd words it.
|
|
4840
|
+
const message = String((err && err.message) || err);
|
|
4841
|
+
const at = message.indexOf("D1_");
|
|
4842
|
+
throw new Error(at > 0 ? message.slice(at) : message);
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
// Strict replies carry columns and rows; build workerd's result objects from them.
|
|
4846
|
+
function __sbD1Rows(r) {
|
|
4847
|
+
const rows = r.rows || [];
|
|
4848
|
+
for (let i = 0; i < rows.length; i++)
|
|
4849
|
+
for (let c = 0; c < rows[i].length; c++) {
|
|
4850
|
+
const cell = rows[i][c];
|
|
4851
|
+
if (__sbIsObj(cell) && __sbIsStr(cell.$blob)) {
|
|
4852
|
+
const out = [];
|
|
4853
|
+
for (let k = 0; k + 1 < cell.$blob.length; k += 2) out.push(parseInt(cell.$blob.slice(k, k + 2), 16));
|
|
4854
|
+
rows[i][c] = out;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
return rows;
|
|
4858
|
+
}
|
|
4859
|
+
function __sbD1Objects(columns, rows) {
|
|
4860
|
+
const out = [];
|
|
4861
|
+
for (let i = 0; i < rows.length; i++) {
|
|
4862
|
+
const row = {};
|
|
4863
|
+
for (let c = 0; c < columns.length; c++) row[columns[c]] = rows[i][c];
|
|
4864
|
+
out.push(row);
|
|
4865
|
+
}
|
|
4866
|
+
return out;
|
|
4867
|
+
}
|
|
4868
|
+
function __sbD1Result(r, strict) {
|
|
4869
|
+
if (!strict) return { results: r.results || [], success: true, meta: r.meta || {} };
|
|
4870
|
+
return { results: __sbD1Objects(r.columns || [], __sbD1Rows(r)), success: true, meta: r.meta || {} };
|
|
4871
|
+
}
|
|
4801
4872
|
function __sbMakeD1(dbName) {
|
|
4802
4873
|
function stmt(sql, params) {
|
|
4803
4874
|
const s = {
|
|
4804
4875
|
__sql: sql,
|
|
4805
4876
|
__params: params,
|
|
4806
4877
|
bind() {
|
|
4807
|
-
|
|
4878
|
+
const strict = __sbD1Strict();
|
|
4879
|
+
const list = [];
|
|
4880
|
+
for (let i = 0; i < arguments.length; i++) list.push(__sbD1Param(arguments[i], strict));
|
|
4881
|
+
return stmt(sql, list);
|
|
4808
4882
|
},
|
|
4809
4883
|
all() {
|
|
4810
|
-
const
|
|
4811
|
-
return {
|
|
4884
|
+
const strict = __sbD1Strict();
|
|
4885
|
+
return __sbD1Result(__sbD1Rpc("d1.query", { db: dbName, sql, params }, strict), strict);
|
|
4812
4886
|
},
|
|
4813
4887
|
run() {
|
|
4814
4888
|
return s.all();
|
|
@@ -4836,17 +4910,17 @@ function __sbMakeD1(dbName) {
|
|
|
4836
4910
|
return stmt(String(sql), []);
|
|
4837
4911
|
},
|
|
4838
4912
|
batch(statements) {
|
|
4913
|
+
const strict = __sbD1Strict();
|
|
4839
4914
|
const list = [];
|
|
4840
4915
|
for (let i = 0; i < (statements || []).length; i++)
|
|
4841
4916
|
list.push({ sql: statements[i].__sql, params: statements[i].__params });
|
|
4842
|
-
const r =
|
|
4917
|
+
const r = __sbD1Rpc("d1.batch", { db: dbName, statements: list }, strict);
|
|
4843
4918
|
const out = [];
|
|
4844
|
-
for (let i = 0; i < (r.results || []).length; i++)
|
|
4845
|
-
out.push({ results: r.results[i].results || [], success: true, meta: r.results[i].meta || {} });
|
|
4919
|
+
for (let i = 0; i < (r.results || []).length; i++) out.push(__sbD1Result(r.results[i], strict));
|
|
4846
4920
|
return out;
|
|
4847
4921
|
},
|
|
4848
4922
|
exec(sql) {
|
|
4849
|
-
|
|
4923
|
+
__sbD1Rpc("d1.exec", { db: dbName, sql: String(sql) }, __sbD1Strict());
|
|
4850
4924
|
return { count: (String(sql).match(/;/g) || []).length, duration: 0 };
|
|
4851
4925
|
},
|
|
4852
4926
|
// Sproutboat extension (not in CF Workers D1): an online, integrity-checked
|
|
@@ -5229,7 +5303,7 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
5229
5303
|
fetch(input, init) {
|
|
5230
5304
|
if (globalThis.__sbCompat >= "2026-10-22") return __sbServiceFetch(binding, input, init);
|
|
5231
5305
|
const parts = __sbFetchParts(input, init);
|
|
5232
|
-
const msg = { binding, url: parts.url, method: parts.method, headers: parts.headers, ...
|
|
5306
|
+
const msg = { binding, url: parts.url, method: parts.method, headers: parts.headers, ...__sbPartsBody(parts) };
|
|
5233
5307
|
return __sbUpstreamResponse(globalThis.__sbFetchUpstream("service.fetch", msg));
|
|
5234
5308
|
},
|
|
5235
5309
|
};
|
|
@@ -5251,9 +5325,9 @@ async function __sbServiceFetch(binding, input, init) {
|
|
|
5251
5325
|
const parts = __sbFetchParts(input, init);
|
|
5252
5326
|
if (parts.signal) parts.signal.throwIfAborted();
|
|
5253
5327
|
// A stream body is read to the end first: the wire carries a whole body.
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
const msg = { binding, url: parts.url, method: parts.method, headers: parts.headers, ...
|
|
5328
|
+
const body = __sbUnwrapBody(parts.body);
|
|
5329
|
+
const wire = body instanceof ReadableStream ? __sbRequestBody(await __sbCollectBody(body)) : __sbPartsBody({ ...parts, body });
|
|
5330
|
+
const msg = { binding, url: parts.url, method: parts.method, headers: parts.headers, ...wire };
|
|
5257
5331
|
const response = await __sbFetchFollowing("service.fetch", msg, "sproutboat service " + binding, parts.redirect, parts.signal);
|
|
5258
5332
|
// The callee threw: as in workerd, its error is thrown here.
|
|
5259
5333
|
const uncaught = response.headers.get("x-sb-uncaught");
|
|
@@ -5436,7 +5510,29 @@ async function __sbRpcDispatch(handlers, receiver, method, request) {
|
|
|
5436
5510
|
|
|
5437
5511
|
// fetch(input, init) as Fetch reads it: a Request's method, headers, body, redirect mode
|
|
5438
5512
|
// and signal apply unless init overrides them; a URL object stands for its href.
|
|
5513
|
+
// From compatibility date 2026-10-23 it is Fetch's own first step, `new Request(input,
|
|
5514
|
+
// init)`, as in workerd: the input Request's body is used up (a second fetch of it
|
|
5515
|
+
// throws), a Request given as init brings its body, GET and HEAD refuse a body, and the
|
|
5516
|
+
// method and redirect mode are checked. Before it, the parts are read off the input.
|
|
5439
5517
|
function __sbFetchParts(input, init) {
|
|
5518
|
+
if (globalThis.__sbCompat >= "2026-10-23") {
|
|
5519
|
+
const request = new Request(input, init);
|
|
5520
|
+
const headers = [];
|
|
5521
|
+
request.headers.forEach((v, k) => headers.push([k, v]));
|
|
5522
|
+
const body = request._bodyStream != null ? request._bodyStream : request._bodyInit;
|
|
5523
|
+
// A stream the runtime made over a value is sent as that value, unread: it is used
|
|
5524
|
+
// all the same, so a Request that gave it (as input or as init) reads as used.
|
|
5525
|
+
if (body instanceof ReadableStream && body._sbBody !== undefined) body._disturbed = true;
|
|
5526
|
+
return {
|
|
5527
|
+
url: request.url,
|
|
5528
|
+
method: request.method,
|
|
5529
|
+
headers,
|
|
5530
|
+
body: body === undefined ? null : body,
|
|
5531
|
+
latin1: request._bodyLatin1 === true,
|
|
5532
|
+
redirect: request.redirect,
|
|
5533
|
+
signal: request._signal || null,
|
|
5534
|
+
};
|
|
5535
|
+
}
|
|
5440
5536
|
const req = input != null && !__sbIsStr(input) && input.url != null ? input : null;
|
|
5441
5537
|
const url = req ? String(req.url) : input != null && input.href != null ? String(input.href) : String(input);
|
|
5442
5538
|
const opts = init || {};
|
|
@@ -5449,8 +5545,17 @@ function __sbFetchParts(input, init) {
|
|
|
5449
5545
|
const body = opts.body !== undefined && !(opts instanceof Request) ? opts.body : req && __sbRawBody(req) !== "" ? __sbRawBody(req) : null;
|
|
5450
5546
|
const method = opts.method || (req && req.method) || "GET";
|
|
5451
5547
|
const redirect = opts.redirect || (req && req.redirect) || "follow";
|
|
5452
|
-
const signal = opts.signal || (req && req.
|
|
5453
|
-
|
|
5548
|
+
const signal = opts.signal || (req && req._signal) || null;
|
|
5549
|
+
// An incoming request's body is its raw bytes, one character each: sent as bytes.
|
|
5550
|
+
const latin1 = body != null && opts.body === undefined && req !== null && req._bodyLatin1 === true;
|
|
5551
|
+
return { url, method, headers, body, latin1, redirect, signal };
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
// The wire fields of a fetch body (see __sbRequestBody). Raw ingress bytes go as bytes,
|
|
5555
|
+
// not as text encoded again.
|
|
5556
|
+
function __sbPartsBody(parts) {
|
|
5557
|
+
if (parts.latin1 === true && __sbIsStr(parts.body)) return { body: parts.body, bodyBinary: true };
|
|
5558
|
+
return __sbRequestBody(parts.body);
|
|
5454
5559
|
}
|
|
5455
5560
|
|
|
5456
5561
|
// An upstream reply (fetch or service.fetch) -> a Response. Headers are appended,
|
|
@@ -5469,7 +5574,7 @@ function __sbUpstreamResponse(r) {
|
|
|
5469
5574
|
if (r.body != null) respHeaders.set("x-sb-raw-body", "1");
|
|
5470
5575
|
const response = __sbRawBodyResponse(r.body, { status: r.status || 502, headers: respHeaders });
|
|
5471
5576
|
if (!typed) response.headers.delete("content-type");
|
|
5472
|
-
return response;
|
|
5577
|
+
return __sbFreezeHeaders(response);
|
|
5473
5578
|
}
|
|
5474
5579
|
|
|
5475
5580
|
// A streamed upstream response (from compatibility date 2026-10-22, with the other
|
|
@@ -5520,7 +5625,7 @@ function __sbUpstreamStreamed(op, msg, label, signal) {
|
|
|
5520
5625
|
__sbCancelAsync(id);
|
|
5521
5626
|
},
|
|
5522
5627
|
});
|
|
5523
|
-
resolve(new Response(body, { status: reply.status || 502, headers }));
|
|
5628
|
+
resolve(__sbFreezeHeaders(new Response(body, { status: reply.status || 502, headers })));
|
|
5524
5629
|
return;
|
|
5525
5630
|
}
|
|
5526
5631
|
const frame = JSON.parse(raw);
|
|
@@ -5645,9 +5750,23 @@ function __sbUpstream(op, msg, label, signal, always) {
|
|
|
5645
5750
|
// prelude must not replace fetch just by being evaluated.
|
|
5646
5751
|
globalThis.__sbInstallFetch = function () {
|
|
5647
5752
|
globalThis.fetch = function (input, init) {
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5753
|
+
let parts;
|
|
5754
|
+
try {
|
|
5755
|
+
parts = __sbFetchParts(input, init);
|
|
5756
|
+
} catch (error) {
|
|
5757
|
+
// From 2026-10-23 an invalid request rejects, as in workerd; before it, it threw.
|
|
5758
|
+
if (globalThis.__sbCompat >= "2026-10-23") return Promise.reject(error);
|
|
5759
|
+
throw error;
|
|
5760
|
+
}
|
|
5761
|
+
const send = (body) => {
|
|
5762
|
+
const msg = { url: parts.url, method: parts.method, headers: parts.headers, ...body };
|
|
5763
|
+
return __sbFetchFollowing("fetch", msg, "sproutboat fetch", parts.redirect, parts.signal);
|
|
5764
|
+
};
|
|
5765
|
+
// A stream body is read to the end first, as for a service binding: the wire carries a
|
|
5766
|
+
// whole body. It was sent as the text "[object ReadableStream]".
|
|
5767
|
+
const stream = __sbUnwrapBody(parts.body);
|
|
5768
|
+
if (stream instanceof ReadableStream) return __sbCollectBody(stream).then((bytes) => send(__sbRequestBody(bytes)));
|
|
5769
|
+
return send(__sbPartsBody(parts));
|
|
5651
5770
|
};
|
|
5652
5771
|
};
|
|
5653
5772
|
|
|
@@ -5691,7 +5810,11 @@ function __sbMakeDONamespace(binding, className) {
|
|
|
5691
5810
|
return {
|
|
5692
5811
|
fetch(input, init) {
|
|
5693
5812
|
let req;
|
|
5694
|
-
|
|
5813
|
+
// From 2026-10-23 the object gets the Request Fetch makes of input and init, as in
|
|
5814
|
+
// workerd: method, headers and body survive, and the input's body is used up.
|
|
5815
|
+
if (globalThis.__sbCompat >= "2026-10-23") {
|
|
5816
|
+
req = new Request(input, init);
|
|
5817
|
+
} else if (__sbIsObj(input) && __sbIsStr(input.url) && !init) {
|
|
5695
5818
|
req = input;
|
|
5696
5819
|
} else {
|
|
5697
5820
|
const url = __sbIsStr(input) ? input : String((input && input.url) || "https://do/");
|
|
@@ -6101,6 +6224,8 @@ function __sbStreamingSupported() {
|
|
|
6101
6224
|
function __sbStreamResponse(response) {
|
|
6102
6225
|
const stream = __sbRawBody(response);
|
|
6103
6226
|
const id = __sbStreamOp("o", "0", "");
|
|
6227
|
+
// A fetched response returned as it is has immutable headers; this mark is ours.
|
|
6228
|
+
response.headers._immutable = false;
|
|
6104
6229
|
response.headers.set("x-sb-stream", id);
|
|
6105
6230
|
response.body = "";
|
|
6106
6231
|
(async () => {
|
|
@@ -6227,6 +6352,12 @@ async function __sbQueueWithDrain(result, tasks) {
|
|
|
6227
6352
|
function __sbDecodeIngress(request) {
|
|
6228
6353
|
const raw = __sbRawBody(request);
|
|
6229
6354
|
if (raw == null) return;
|
|
6355
|
+
// A GET or HEAD has no body, as in workerd (from 2026-10-23): request.body is null, and
|
|
6356
|
+
// new Request(request) copies none. Before it, the body is the empty string.
|
|
6357
|
+
if (raw === "" && (request.method === "GET" || request.method === "HEAD") && globalThis.__sbCompat >= "2026-10-23") {
|
|
6358
|
+
request.body = null;
|
|
6359
|
+
return;
|
|
6360
|
+
}
|
|
6230
6361
|
// Raw bytes, one character each: arrayBuffer(), blob() and body read them as bytes.
|
|
6231
6362
|
request._bodyLatin1 = true;
|
|
6232
6363
|
let decoded = null;
|
|
@@ -6258,6 +6389,8 @@ globalThis.__sbEntry = function (handlers, request) {
|
|
|
6258
6389
|
const __tlsCipher = __sbTrustedProxyHeader(request, "x-sb-tls-cipher");
|
|
6259
6390
|
if (__tlsCipher) __cf.tlsCipher = __tlsCipher;
|
|
6260
6391
|
request.cf = __cf;
|
|
6392
|
+
// The edge's headers are gone: from 2026-10-23 the handler cannot change the rest.
|
|
6393
|
+
__sbFreezeHeaders(request);
|
|
6261
6394
|
// #28 — per-invocation CPU time. One fetch turn per process (serial), so the
|
|
6262
6395
|
// process CPU delta across the handler is this invocation's CPU.
|
|
6263
6396
|
// ponytail: serial-turn assumption; revisit if the profile ever allows
|