sproutboat 0.11.5 → 0.11.7
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 +20 -1
- package/README.md +4 -0
- package/SURFACE.md +2 -2
- package/package.json +8 -8
- package/src/compile.ts +1 -0
- package/src/dev.ts +21 -7
- package/src/types-gen.ts +1 -1
- package/types/sproutboat.d.ts +82 -9
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,26 @@ a breaking change.
|
|
|
8
8
|
Reconstructed from git history on 2026-09-03 for everything through v0.4.11;
|
|
9
9
|
maintained going forward by the `release` skill.
|
|
10
10
|
|
|
11
|
-
## [
|
|
11
|
+
## [0.11.6] - 2026-09-15
|
|
12
|
+
### Fixed
|
|
13
|
+
- The 100% CPU livelock on a `fetch` handler that resolves a promise with a
|
|
14
|
+
plain object (baronunread/sproutboat#168). Root cause was
|
|
15
|
+
`__Porffor_promise_resolve`'s `.then` duck-type probe spinning forever on a
|
|
16
|
+
prototype-chain fixed point instead of terminating; patched in
|
|
17
|
+
`@sproutboat/toolchain` alongside the other local Porffor patches. Bumped
|
|
18
|
+
`@sproutboat/toolchain` to `^0.4.1` to pick it up.
|
|
19
|
+
- `new Response(r2Object.body)` corrupted any binary R2 object (each byte
|
|
20
|
+
0x80-0xFF doubled on the wire): the runtime couldn't tell opaque bytes
|
|
21
|
+
apart from a Latin-1-range string, so it UTF-8-encoded them a second time
|
|
22
|
+
(baronunread/sproutboat#177). Added `R2Object.toResponse(init?)`, which
|
|
23
|
+
serves the bytes unmodified. Bumped `@sproutboat/runtime` to `^0.7.1` and
|
|
24
|
+
`@sproutboat/wire` to `^0.6.0` to pick this up, along with the R2
|
|
25
|
+
file-backed storage rework (baronunread/sproutboat#56) both depend on.
|
|
26
|
+
- `sproutboat-env.d.ts`'s generated secret comment named the wrong command
|
|
27
|
+
(`secrets set`; the real one is `secrets put`), `SproutboatHandler` claimed
|
|
28
|
+
there is no `ctx`/`ctx.waitUntil` (shipped since #57/#171), and
|
|
29
|
+
`docs/standalone.md` still said a missing secret refuses the boot (0.11.1
|
|
30
|
+
changed that to throwing on first use instead).
|
|
12
31
|
|
|
13
32
|
## [0.11.5] - 2026-09-14
|
|
14
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -150,6 +150,10 @@ files in the project and from its own `node_modules`, and may also export
|
|
|
150
150
|
is a global, not a parameter, and every binding call is synchronous.
|
|
151
151
|
[`examples/kitchen-sink/`](examples/kitchen-sink) uses every binding.
|
|
152
152
|
|
|
153
|
+
For objects larger than one request, use resumable R2 multipart uploads. The
|
|
154
|
+
[large-object guide](docs/r2-large-objects.md) covers part sizing, browser
|
|
155
|
+
chunking, resume state, cleanup, and the remaining download limitation.
|
|
156
|
+
|
|
153
157
|
`sproutboat dev` runs against a real local broker, so bindings behave like
|
|
154
158
|
production without a deploy — except `secrets`, which live only in the control
|
|
155
159
|
plane. Put values in a `.dev.vars` file next to `sproutboat.jsonc`
|
package/SURFACE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Generated by `src/surface.test.ts` from `src/surface.ts` + the pinned
|
|
4
4
|
> toolchain constants. Do not edit by hand — run `UPDATE_SURFACE=1 bun test`.
|
|
5
5
|
|
|
6
|
-
**Package:** `sproutboat` 0.11.
|
|
6
|
+
**Package:** `sproutboat` 0.11.7 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -74,7 +74,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
74
74
|
| | |
|
|
75
75
|
| --- | --- |
|
|
76
76
|
| Zig | `0.16.0` (`zig cc -target x86_64-linux-musl`, static) |
|
|
77
|
-
| Provenance stamp | `zig-musl/0.16.0+porffor/
|
|
77
|
+
| Provenance stamp | `zig-musl/0.16.0+porffor/038f415+uws/360c276d` |
|
|
78
78
|
| Artifact schema | `2` |
|
|
79
79
|
| Runtime | `native-fetch` |
|
|
80
80
|
| Capability profile | `http-sync-v0` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"@sproutboat/artifact": "^0.2.0",
|
|
65
65
|
"@sproutboat/assets": "^0.2.0",
|
|
66
66
|
"@sproutboat/config": "^0.3.0",
|
|
67
|
-
"@sproutboat/runtime": "^0.
|
|
68
|
-
"@sproutboat/toolchain": "^0.4.
|
|
69
|
-
"@sproutboat/wire": "^0.
|
|
67
|
+
"@sproutboat/runtime": "^0.9.3",
|
|
68
|
+
"@sproutboat/toolchain": "^0.4.5",
|
|
69
|
+
"@sproutboat/wire": "^0.7.0",
|
|
70
70
|
"esbuild": "^0.28.2"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
@@ -78,10 +78,10 @@
|
|
|
78
78
|
"typescript": "7.0.2"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
|
-
"@sproutboat/cli-darwin-arm64": "0.11.
|
|
82
|
-
"@sproutboat/cli-darwin-x64": "0.11.
|
|
83
|
-
"@sproutboat/cli-linux-arm64": "0.11.
|
|
84
|
-
"@sproutboat/cli-linux-x64": "0.11.
|
|
81
|
+
"@sproutboat/cli-darwin-arm64": "0.11.7",
|
|
82
|
+
"@sproutboat/cli-darwin-x64": "0.11.7",
|
|
83
|
+
"@sproutboat/cli-linux-arm64": "0.11.7",
|
|
84
|
+
"@sproutboat/cli-linux-x64": "0.11.7"
|
|
85
85
|
},
|
|
86
86
|
"engines": {
|
|
87
87
|
"node": ">=18"
|
package/src/compile.ts
CHANGED
package/src/dev.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { mkdir, readFile, rm } from "node:fs/promises";
|
|
|
15
15
|
import { connect } from "node:net";
|
|
16
16
|
import { dirname, resolve } from "node:path";
|
|
17
17
|
import { buildArtifact } from "./build";
|
|
18
|
-
import { createBroker, listen, type Bindings, type Broker } from "./broker";
|
|
18
|
+
import { createBroker, listen, listenTransfers, type Bindings, type Broker } from "./broker";
|
|
19
19
|
import { jsonObject, parseJsonValue } from "./json";
|
|
20
20
|
import { amber, dim, leaf, ok } from "./style";
|
|
21
21
|
import type { SproutboatConfig } from "./config";
|
|
@@ -97,6 +97,8 @@ export type Running = {
|
|
|
97
97
|
/** Set before a kill we initiated, so its exit code is not reported as a crash. */
|
|
98
98
|
expected: boolean;
|
|
99
99
|
port: number;
|
|
100
|
+
/** Private direct-transfer listener, absent in test candidates. */
|
|
101
|
+
transferPort?: number;
|
|
100
102
|
enableDispatch: () => void;
|
|
101
103
|
disableDispatch: () => void;
|
|
102
104
|
};
|
|
@@ -142,10 +144,13 @@ async function start(input: DevInput, port: number): Promise<Running> {
|
|
|
142
144
|
assetsDir: existsSync(assetsDir) ? assetsDir : undefined,
|
|
143
145
|
dispatchEnabled: () => dispatchEnabled,
|
|
144
146
|
});
|
|
145
|
-
let server: ReturnType<typeof listen
|
|
147
|
+
let server: ReturnType<typeof listen> | undefined;
|
|
148
|
+
let transferServer: ReturnType<typeof listenTransfers> | undefined;
|
|
146
149
|
try {
|
|
147
150
|
server = listen(broker, "127.0.0.1", 0);
|
|
151
|
+
transferServer = listenTransfers(broker, "127.0.0.1", 0);
|
|
148
152
|
} catch (error) {
|
|
153
|
+
server?.stop();
|
|
149
154
|
broker.close();
|
|
150
155
|
throw error;
|
|
151
156
|
}
|
|
@@ -156,14 +161,15 @@ async function start(input: DevInput, port: number): Promise<Running> {
|
|
|
156
161
|
env: {
|
|
157
162
|
...process.env,
|
|
158
163
|
PORT: String(port),
|
|
159
|
-
SB_BROKER_PORT: String(server
|
|
164
|
+
SB_BROKER_PORT: String(server!.port),
|
|
160
165
|
SB_BROKER_TOKEN: "sproutboat-dev",
|
|
161
166
|
},
|
|
162
167
|
stdout: "inherit",
|
|
163
168
|
stderr: "inherit",
|
|
164
169
|
});
|
|
165
170
|
} catch (error) {
|
|
166
|
-
server
|
|
171
|
+
server?.stop();
|
|
172
|
+
transferServer?.stop();
|
|
167
173
|
broker.close();
|
|
168
174
|
throw error;
|
|
169
175
|
}
|
|
@@ -173,11 +179,13 @@ async function start(input: DevInput, port: number): Promise<Running> {
|
|
|
173
179
|
artifactDir,
|
|
174
180
|
broker,
|
|
175
181
|
stopBroker: () => {
|
|
176
|
-
server
|
|
182
|
+
server?.stop();
|
|
183
|
+
transferServer?.stop();
|
|
177
184
|
broker.close();
|
|
178
185
|
},
|
|
179
186
|
expected: false,
|
|
180
187
|
port,
|
|
188
|
+
transferPort: transferServer?.port,
|
|
181
189
|
enableDispatch: () => {
|
|
182
190
|
dispatchEnabled = true;
|
|
183
191
|
},
|
|
@@ -262,6 +270,7 @@ export async function runDev(input: DevInput): Promise<void> {
|
|
|
262
270
|
}
|
|
263
271
|
running.enableDispatch();
|
|
264
272
|
let activePort = running.port;
|
|
273
|
+
let activeTransferPort = running.transferPort;
|
|
265
274
|
// Keep the public port stable while candidates boot on private ports. This is
|
|
266
275
|
// what lets a failed startup leave the last known-good process reachable.
|
|
267
276
|
let proxy: ReturnType<typeof Bun.serve>;
|
|
@@ -269,9 +278,13 @@ export async function runDev(input: DevInput): Promise<void> {
|
|
|
269
278
|
proxy = Bun.serve({
|
|
270
279
|
hostname: "127.0.0.1",
|
|
271
280
|
port: input.port,
|
|
272
|
-
|
|
281
|
+
maxRequestBodySize: 5 * 1024 * 1024 * 1024,
|
|
282
|
+
fetch(request, server) {
|
|
273
283
|
const target = new URL(request.url);
|
|
274
|
-
|
|
284
|
+
const directTransfer = /^\/__sb\/r2\/transfer\/[A-Z][A-Z0-9_]*\/[0-9a-f]{24}$/.test(target.pathname);
|
|
285
|
+
if (directTransfer) server.timeout(request, 255);
|
|
286
|
+
if (directTransfer && !activeTransferPort) return new Response("direct transfers unavailable", { status: 503 });
|
|
287
|
+
target.host = `127.0.0.1:${directTransfer ? activeTransferPort : activePort}`;
|
|
275
288
|
return fetch(target, {
|
|
276
289
|
method: request.method,
|
|
277
290
|
headers: request.headers,
|
|
@@ -373,6 +386,7 @@ export async function runDev(input: DevInput): Promise<void> {
|
|
|
373
386
|
current = { ...current, ...next };
|
|
374
387
|
const switchedAt = now();
|
|
375
388
|
activePort = candidate.port;
|
|
389
|
+
activeTransferPort = candidate.transferPort;
|
|
376
390
|
resetWatchers();
|
|
377
391
|
stop(previous);
|
|
378
392
|
watchExit(running);
|
package/src/types-gen.ts
CHANGED
|
@@ -34,7 +34,7 @@ function entries(config: SproutboatConfig): Entry[] {
|
|
|
34
34
|
out.push({ name, type: "string", note: `vars: ${JSON.stringify(value)}` });
|
|
35
35
|
}
|
|
36
36
|
for (const name of config.secrets ?? []) {
|
|
37
|
-
out.push({ name, type: "string", note: "secret, set with `sproutboat secrets
|
|
37
|
+
out.push({ name, type: "string", note: "secret, set with `sproutboat secrets put`" });
|
|
38
38
|
}
|
|
39
39
|
for (const { binding } of resourceRefs(config.kv_namespaces)) out.push({ name: binding, type: "KVNamespace" });
|
|
40
40
|
for (const { binding } of resourceRefs(config.d1_databases)) out.push({ name: binding, type: "D1Database" });
|
package/types/sproutboat.d.ts
CHANGED
|
@@ -56,20 +56,46 @@ declare global {
|
|
|
56
56
|
|
|
57
57
|
// ---------------------------------------------------------------- R2
|
|
58
58
|
|
|
59
|
+
interface R2HttpMetadata {
|
|
60
|
+
contentType?: string;
|
|
61
|
+
contentDisposition?: string;
|
|
62
|
+
contentEncoding?: string;
|
|
63
|
+
contentLanguage?: string;
|
|
64
|
+
cacheControl?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
interface R2Object {
|
|
60
68
|
key: string;
|
|
61
69
|
size: number;
|
|
62
70
|
etag: string;
|
|
71
|
+
/** `etag`, quoted, ready for an `ETag` response header. */
|
|
72
|
+
httpEtag: string;
|
|
63
73
|
uploaded: string;
|
|
74
|
+
httpMetadata: R2HttpMetadata;
|
|
64
75
|
customMetadata?: Record<string, string>;
|
|
65
76
|
/**
|
|
66
|
-
* The object as a string.
|
|
67
|
-
*
|
|
77
|
+
* The object as a string. A get holds the object whole in memory, so keep
|
|
78
|
+
* downloads small until native download tickets are available.
|
|
79
|
+
*
|
|
80
|
+
* `new Response(obj.body, ...)` corrupts any non-ASCII/binary content: the
|
|
81
|
+
* runtime cannot tell these bytes apart from a string a handler built, so
|
|
82
|
+
* it UTF-8-encodes them a second time (baronunread/sproutboat#177). Use
|
|
83
|
+
* `obj.toResponse()` instead for binary content — it serves the bytes
|
|
84
|
+
* unmodified.
|
|
68
85
|
*/
|
|
69
86
|
body: string;
|
|
87
|
+
text(): string;
|
|
88
|
+
json(): unknown;
|
|
89
|
+
/**
|
|
90
|
+
* A `Response` that serves `body` byte-for-byte, unlike `new
|
|
91
|
+
* Response(obj.body, ...)` (see `body` above). `init.headers`/`.status`
|
|
92
|
+
* merge in as usual; `content-type` defaults from `httpMetadata` and
|
|
93
|
+
* `etag` from `httpEtag` when not set.
|
|
94
|
+
*/
|
|
95
|
+
toResponse(init?: { status?: number; headers?: HeadersInit }): Response;
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
type R2ObjectHead = Omit<R2Object, "body">;
|
|
98
|
+
type R2ObjectHead = Omit<R2Object, "body" | "text" | "json" | "toResponse">;
|
|
73
99
|
|
|
74
100
|
interface R2Objects {
|
|
75
101
|
objects: R2ObjectHead[];
|
|
@@ -78,6 +104,7 @@ declare global {
|
|
|
78
104
|
}
|
|
79
105
|
|
|
80
106
|
interface R2PutOptions {
|
|
107
|
+
httpMetadata?: R2HttpMetadata;
|
|
81
108
|
customMetadata?: Record<string, string>;
|
|
82
109
|
}
|
|
83
110
|
|
|
@@ -87,8 +114,48 @@ declare global {
|
|
|
87
114
|
cursor?: string;
|
|
88
115
|
}
|
|
89
116
|
|
|
117
|
+
interface R2UploadedPart {
|
|
118
|
+
partNumber: number;
|
|
119
|
+
etag: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface R2MultipartUpload {
|
|
123
|
+
key: string;
|
|
124
|
+
uploadId: string;
|
|
125
|
+
uploadPart(partNumber: number, value: string): R2UploadedPart;
|
|
126
|
+
complete(parts: R2UploadedPart[]): R2ObjectHead;
|
|
127
|
+
abort(): void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface R2TransferTicket {
|
|
131
|
+
/** Same-origin reserved path. Send the file directly to this URL. */
|
|
132
|
+
url: string;
|
|
133
|
+
expiresAt: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface R2UploadTicketOptions extends R2PutOptions {
|
|
137
|
+
/** Maximum accepted body size in bytes. Default 100 MiB. */
|
|
138
|
+
maxBytes?: number;
|
|
139
|
+
/** Ticket lifetime in seconds, clamped to 30-3600. */
|
|
140
|
+
expiresIn?: number;
|
|
141
|
+
/** Optional lowercase hexadecimal SHA-256 checksum. */
|
|
142
|
+
sha256?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface R2DownloadTicketOptions {
|
|
146
|
+
expiresIn?: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
90
149
|
interface R2Bucket {
|
|
91
150
|
put(key: string, value: string, options?: R2PutOptions): void;
|
|
151
|
+
/** Starts a resumable upload whose individual parts stay memory-bounded. */
|
|
152
|
+
createMultipartUpload(key: string, options?: R2PutOptions): R2MultipartUpload;
|
|
153
|
+
/** Reconstructs a multipart handle after storing its key and upload id. */
|
|
154
|
+
resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload;
|
|
155
|
+
/** Broker-backed direct PUT. Not available in native standalone mode yet. */
|
|
156
|
+
createUploadUrl(key: string, options?: R2UploadTicketOptions): R2TransferTicket;
|
|
157
|
+
/** Broker-backed direct GET or HEAD, including Range. */
|
|
158
|
+
createDownloadUrl(key: string, options?: R2DownloadTicketOptions): R2TransferTicket;
|
|
92
159
|
get(key: string): R2Object | null;
|
|
93
160
|
/** Metadata without the body. */
|
|
94
161
|
head(key: string): R2ObjectHead | null;
|
|
@@ -226,16 +293,22 @@ declare global {
|
|
|
226
293
|
scheduledTime: number;
|
|
227
294
|
}
|
|
228
295
|
|
|
296
|
+
/**
|
|
297
|
+
* `env` is a global, not a parameter. `ctx` is the second argument, and its
|
|
298
|
+
* only member is `waitUntil`: work that outlives the response, drained
|
|
299
|
+
* in-process and capped at 25s.
|
|
300
|
+
*/
|
|
301
|
+
interface ExecutionContext {
|
|
302
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
303
|
+
}
|
|
304
|
+
|
|
229
305
|
/**
|
|
230
306
|
* The default export.
|
|
231
|
-
*
|
|
232
|
-
* `fetch` takes only a request: `env` is a global, and there is no `ctx`, so
|
|
233
|
-
* there is no `ctx.waitUntil`. Use a queue for work that outlives a response.
|
|
234
307
|
*/
|
|
235
308
|
interface SproutboatHandler<QueueBody = unknown> {
|
|
236
|
-
fetch(request: Request): Response | Promise<Response>;
|
|
237
|
-
scheduled?(event: ScheduledEvent): void | Promise<void>;
|
|
238
|
-
queue?(batch: MessageBatch<QueueBody
|
|
309
|
+
fetch(request: Request, ctx: ExecutionContext): Response | Promise<Response>;
|
|
310
|
+
scheduled?(event: ScheduledEvent, ctx: ExecutionContext): void | Promise<void>;
|
|
311
|
+
queue?(batch: MessageBatch<QueueBody>, ctx: ExecutionContext): void | Promise<void>;
|
|
239
312
|
}
|
|
240
313
|
|
|
241
314
|
/**
|