@sproutboat/runtime 0.19.2 → 0.21.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 +95 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +488 -83
- package/src/source.ts +8 -4
- package/src/transport-broker.js +566 -56
- package/src/transport-embedded.js +4 -0
- package/src/wrap.ts +30 -3
- package/test/native-base64.ts +11 -2
- package/test/native-service-bindings.ts +396 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.21.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6f253b9: Service bindings support Workers RPC.
|
|
8
|
+
|
|
9
|
+
**Callee.** A deployment whose default export extends `WorkerEntrypoint`, imported from `cloudflare:workers`, exposes its methods:
|
|
10
|
+
|
|
11
|
+
- `env.SERVICE.add(2, 3)` runs `add(2, 3)` on a fresh instance, with `this.env` and `this.ctx` set, and resolves to its result.
|
|
12
|
+
- Arguments and results are copied as structured clone does: `undefined`, `NaN`, `-0`, `Date`, `RegExp`, binary data, `Map`, `Set` and errors survive the trip.
|
|
13
|
+
- An error the method throws rejects the call with the same class and message.
|
|
14
|
+
- An unknown method rejects with workerd's `TypeError`.
|
|
15
|
+
- Passing functions or RPC stubs is not supported yet.
|
|
16
|
+
|
|
17
|
+
**Where it works.** RPC travels over the service-binding calls a platform edge vouches for, so no client can invoke a method.
|
|
18
|
+
|
|
19
|
+
**Imports.** `cloudflare:workers` provides `WorkerEntrypoint`, `DurableObject`, `RpcTarget` and `env`. The runtime turns the import into globals, so a handler that imports it runs both bundled and as plain ESM.
|
|
20
|
+
|
|
21
|
+
**Default exports.**
|
|
22
|
+
- A class default export is instantiated per request, for `fetch`, `scheduled` and `queue`.
|
|
23
|
+
- A `WorkerEntrypoint` may have no `fetch` at all.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- 581c2f4: `atob()` and `btoa()` give the right result for any string. They used Porffor's own implementations, which read how a string is stored, so they went wrong on strings held as UTF-16, such as ones from `JSON.parse` or `TextDecoder`:
|
|
28
|
+
|
|
29
|
+
- `atob(JSON.parse('"AQL/"'))` returned two wrong bytes instead of `01 02 ff`;
|
|
30
|
+
- `btoa` of a parsed `"héllo"` encoded the UTF-16 storage.
|
|
31
|
+
|
|
32
|
+
Both are now plain JavaScript, with the same validation and errors as before.
|
|
33
|
+
|
|
34
|
+
## 0.20.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- 85aa74d: Service-binding calls and outbound `fetch()` no longer block a deployed sprout. In a deployed sprout, each call runs off the event loop on one I/O thread, so the sprout keeps answering other requests while a call is in flight. Concurrent calls overlap: eight 500 ms calls finish in about 510 ms, where they used to queue for about 4 s. A deployment can also call itself through a binding, to any depth, where this used to deadlock. The toolchain installs the loop hook the runtime uses for this; with an older runtime it does nothing.
|
|
39
|
+
|
|
40
|
+
`env.SERVICE.fetch()` now always returns a promise, as in Workers, and reads its arguments as `fetch()` does:
|
|
41
|
+
|
|
42
|
+
- a `URL` object works as input;
|
|
43
|
+
- `fetch(request, init)` keeps the request's body when `init` sets no body of its own;
|
|
44
|
+
- a `ReadableStream` body is sent whole, where it used to arrive as the text `[object ReadableStream]`.
|
|
45
|
+
|
|
46
|
+
Responses from bindings and from `fetch()` keep every `Set-Cookie` header, where they used to keep only the last one.
|
|
47
|
+
|
|
48
|
+
A broker exchange is now resent only when the broker cannot have acted on it: the request never fully left, or the connection had gone stale. A `fetch` or service call that the broker may already have run is no longer repeated.
|
|
49
|
+
- f2d2b72: Service-binding calls behave as in workerd in two more ways, on a platform edge that vouches for them:
|
|
50
|
+
|
|
51
|
+
- **The callee sees the caller's URL.** `request.url` is the URL the caller passed (`https://peer/path`), where it used to be the callee deployment's own hostname.
|
|
52
|
+
- **A callee that throws makes the caller throw.** The caller's `fetch()` rejects with the callee's error, keeping its class for standard errors and its message, where it used to get a plain 500 response.
|
|
53
|
+
|
|
54
|
+
How the platform vouches for a call:
|
|
55
|
+
|
|
56
|
+
1. The broker sends each service call with the edge's secret (`SB_EDGE_SECRET`, which the edge gives only to brokers) and the caller's URL.
|
|
57
|
+
2. The edge forwards the call to the callee with that sprout's own token (`SB_EDGE_TOKEN`).
|
|
58
|
+
3. The callee trusts a call only when the token matches.
|
|
59
|
+
|
|
60
|
+
A client can forge none of this. The edge removes its headers from every other request, and the error marker never reaches a client.
|
|
61
|
+
|
|
62
|
+
A vouched call also no longer shows the broker's loopback address as `x-sb-remote-addr`. The edge's internal headers are removed before any handler sees a request.
|
|
63
|
+
|
|
64
|
+
In `@sproutboat/wire`, the broker reads `SB_EDGE_SECRET` and takes an `edgeSecret` option.
|
|
65
|
+
- b287117: `fetch()` and service bindings honour an abort signal, set as `init.signal` or a `Request`'s `signal`:
|
|
66
|
+
|
|
67
|
+
- A signal that is already aborted rejects the call at once.
|
|
68
|
+
- Aborting it later, by hand or through `AbortSignal.timeout()`, rejects the pending call with the signal's reason.
|
|
69
|
+
|
|
70
|
+
In a deployed sprout the abort also stops the work. The sprout closes the call's broker connection, the broker aborts its upstream request when a sprout's connection closes, and the platform's edge stops the callee's request in turn. Before, an abandoned call ran to the end anyway.
|
|
71
|
+
|
|
72
|
+
In sproutd the request still runs to the end, and its late reply is dropped.
|
|
73
|
+
|
|
74
|
+
In `@sproutboat/wire`, `Broker.handleFrame` takes an optional `AbortSignal`, which the broker server aborts when the connection closes.
|
|
75
|
+
- 9773e45: From compatibility date `2026-10-22`, `fetch()` and service bindings follow redirects as Fetch does:
|
|
76
|
+
|
|
77
|
+
- Under `redirect: "follow"`, the default, the request goes again to the new location, up to 20 times. Each outbound hop is checked against the egress rules like the first.
|
|
78
|
+
- A 303 turns the request into a GET with no body, and so does a 301 or 302 that answers a POST.
|
|
79
|
+
- A hop to another origin drops `Authorization`.
|
|
80
|
+
- A service binding follows through the same binding, as in workerd.
|
|
81
|
+
- `redirect: "error"` rejects with a `TypeError`.
|
|
82
|
+
- `redirect: "manual"` returns the 3xx as it is.
|
|
83
|
+
|
|
84
|
+
Before this date, a 3xx came back to the handler whatever the mode, and still does for apps with an earlier `compatibility_date`.
|
|
85
|
+
|
|
86
|
+
A response from `fetch()` or a service binding that has no `Content-Type` no longer gains `text/plain;charset=UTF-8`.
|
|
87
|
+
- 3798994: Responses to `fetch()` and service-binding calls stream. In a deployed sprout with compatibility date `2026-10-21` or later, the call resolves as soon as the status and headers arrive. `response.body` is then a `ReadableStream` that receives each chunk as the upstream sends it.
|
|
88
|
+
|
|
89
|
+
Before, the whole body had to arrive first. A response streamed over several seconds, such as server-sent events or a long download, reached the caller only at the end.
|
|
90
|
+
|
|
91
|
+
The 32 MiB `SB_FETCH_MAX_BYTES` cap still applies to the total. Cancelling the body, or aborting the call's signal, stops the upstream request.
|
|
92
|
+
|
|
93
|
+
In `@sproutboat/wire`:
|
|
94
|
+
- the broker streams a `fetch` or `service.fetch` that asks for it (`stream: true`), sending the head and each chunk as `FRAME_V1_MORE` frames through the new `emit` argument of `handleFrame`;
|
|
95
|
+
- the final frame ends the response;
|
|
96
|
+
- a request that doesn't ask is answered whole, as before.
|
|
97
|
+
|
|
3
98
|
## 0.19.2
|
|
4
99
|
|
|
5
100
|
### Patch Changes
|