sproutboat 0.35.0 → 0.37.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 CHANGED
@@ -10,6 +10,53 @@ maintained going forward by the `release` skill.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## [0.37.0] - 2026-10-27
14
+
15
+ Built on `@sproutboat/runtime` 0.34.1, `@sproutboat/toolchain` 0.19.1 and
16
+ `@sproutboat/wire` 0.20.2.
17
+
18
+ ### Fixed
19
+
20
+ - **A failed binding call you do not await no longer ends the process.** An un-awaited
21
+ `db.prepare("INSERT ...").run()` that broke a foreign key used to take the whole binary down, even
22
+ with a `try` / `catch` around the call. As on Workers, an unhandled rejection is now logged
23
+ (`Uncaught (in promise) ...`), fires `unhandledrejection` when the app has a listener
24
+ (`addEventListener("unhandledrejection", ...)`), and the process and other requests keep going. An
25
+ un-awaited `async function` that throws is reported the same way. At every compatibility date.
26
+
27
+ ### Changed
28
+
29
+ - **Types for the Promise bindings.** `sproutboat types` gives D1 (from compatibility date 2026-10-24),
30
+ KV (2026-10-25) and R2 (2026-10-26) bindings the Promise types (`D1DatabaseAsync`,
31
+ `KVNamespaceAsync`, `R2BucketAsync`); earlier dates keep the synchronous ones.
32
+ - Docs, examples and type comments no longer say binding calls are synchronous at every date.
33
+
34
+ ## [0.36.0] - 2026-10-26
35
+
36
+ Built on `@sproutboat/runtime` 0.34.0, `@sproutboat/toolchain` 0.19.0 and
37
+ `@sproutboat/wire` 0.20.2.
38
+
39
+ ### Added
40
+
41
+ - **WebSockets everywhere, from compatibility date 2026-11-01.** Standalone executables and the
42
+ platform carry the client end of a 101 response (before the date they answer 501), and the
43
+ platform edge proxies the upgrade.
44
+ - **Outbound WebSockets, from 2026-11-01.** `new WebSocket(url, protocols)` and
45
+ `fetch(url, { headers: { Upgrade: "websocket" } })` (the Response's `webSocket`, then
46
+ `accept()`); returning that Response from a handler relays the client's socket to the upstream.
47
+ `ws`, `wss`, `http` and `https` URLs. `sproutboat check` and `build` accept `WebSocket(` from
48
+ that date and refuse it before. They run on standalone executables and sproutd; `sproutboat dev`
49
+ and the platform (broker transport) reject `fetch` upgrades for now.
50
+ - **Hibernation on sproutd, from 2026-11-01.** A Durable Object's hibernatable socket no longer
51
+ keeps its worker running: the worker is reaped while the socket idles, the auto-response is
52
+ answered without waking it, and a message or close starts a worker that constructs the object
53
+ again with `getWebSockets()` as it was.
54
+
55
+ ### Changed
56
+
57
+ - `types/sproutboat.d.ts` and the README describe the above. The toolchain moves to 0.19.0, so
58
+ compiled apps get a new fingerprint and rebuild once.
59
+
13
60
  ## [0.35.0] - 2026-10-26
14
61
 
15
62
  Built on `@sproutboat/runtime` 0.33.0, `@sproutboat/toolchain` 0.18.0 and
package/README.md CHANGED
@@ -146,7 +146,9 @@ store instead.
146
146
  The handler is `export default { fetch(request) }`. It may import from other
147
147
  files in the project and from its own `node_modules`, and may also export
148
148
  `scheduled(event)` / `queue(batch)` handlers and Durable Object classes. `env`
149
- is a global, not a parameter, and every binding call is synchronous.
149
+ is a global, not a parameter. Binding calls are synchronous until the compatibility
150
+ date that makes each one return a Promise, as on Workers (D1 from 2026-10-24, KV from
151
+ 2026-10-25, R2 from 2026-10-26); `await` works at every date, so always await them.
150
152
  [`examples/kitchen-sink/`](examples/kitchen-sink) uses every binding.
151
153
 
152
154
  For objects larger than one request, use resumable R2 multipart uploads. The
@@ -168,8 +170,9 @@ Windows, build from WSL.
168
170
  ## Limits
169
171
 
170
172
  Capability profile `http-sync-v0`: one synchronous `fetch` handler, optional
171
- `scheduled` / `queue` handlers. WebSockets (from compatibility date 2026-10-30) need
172
- a host that carries them (sproutd). Binding values are
173
+ `scheduled` / `queue` handlers. WebSockets (from compatibility date 2026-10-30; standalone
174
+ and platform sprouts from 2026-11-01) and outbound `new WebSocket(url)` (2026-11-01) are
175
+ supported. Binding values are
173
176
  text/JSON, one at a time over a 32 MiB loopback frame. Sprout uploads cap at
174
177
  16 MiB, assets at 64 MiB / 4096 files.
175
178
 
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.35.0
6
+ **Package:** `sproutboat` 0.37.0
7
7
 
8
8
  ## Commands
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
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.3.0",
65
65
  "@sproutboat/assets": "^0.3.0",
66
66
  "@sproutboat/config": "^0.6.0",
67
- "@sproutboat/runtime": "^0.33.0",
68
- "@sproutboat/toolchain": "^0.18.0",
69
- "@sproutboat/wire": "^0.20.1"
67
+ "@sproutboat/runtime": "^0.34.1",
68
+ "@sproutboat/toolchain": "^0.19.1",
69
+ "@sproutboat/wire": "^0.20.2"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@oxlint/plugins": "1.85.0",
@@ -78,10 +78,10 @@
78
78
  "typescript": "7.0.2"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@sproutboat/cli-darwin-arm64": "0.35.0",
82
- "@sproutboat/cli-darwin-x64": "0.35.0",
83
- "@sproutboat/cli-linux-arm64": "0.35.0",
84
- "@sproutboat/cli-linux-x64": "0.35.0"
81
+ "@sproutboat/cli-darwin-arm64": "0.37.0",
82
+ "@sproutboat/cli-darwin-x64": "0.37.0",
83
+ "@sproutboat/cli-linux-arm64": "0.37.0",
84
+ "@sproutboat/cli-linux-x64": "0.37.0"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=18"
package/src/main.ts CHANGED
@@ -235,7 +235,7 @@ async function readProject(directory = process.cwd()) {
235
235
  } catch (cause) {
236
236
  fail(cause instanceof BundleError ? cause.message : String(cause));
237
237
  }
238
- const supported = validateHttpSyncSource(bundle.code);
238
+ const supported = validateHttpSyncSource(bundle.code, project.config.compatibility_date);
239
239
  if (!supported.ok) fail(supported.errors.join("\n"));
240
240
  return { ...project, sourcePath, source, bundle };
241
241
  }
package/src/types-gen.ts CHANGED
@@ -26,6 +26,11 @@ const HEADER = `// Generated by \`sproutboat types\`. Do not edit.
26
26
 
27
27
  type Entry = { name: string; type: string; note?: string };
28
28
 
29
+ /** Whether the binding's calls return Promises at the project's compatibility date. */
30
+ function asyncAt(config: SproutboatConfig, date: string): boolean {
31
+ return config.compatibility_date >= date;
32
+ }
33
+
29
34
  function entries(config: SproutboatConfig): Entry[] {
30
35
  const out: Entry[] = [];
31
36
 
@@ -35,9 +40,12 @@ function entries(config: SproutboatConfig): Entry[] {
35
40
  for (const name of config.secrets ?? []) {
36
41
  out.push({ name, type: "string", note: "secret, set with `sproutboat secrets put`" });
37
42
  }
38
- for (const { binding } of resourceRefs(config.kv_namespaces)) out.push({ name: binding, type: "KVNamespace" });
39
- for (const { binding } of resourceRefs(config.d1_databases)) out.push({ name: binding, type: "D1Database" });
40
- for (const { binding } of resourceRefs(config.r2_buckets)) out.push({ name: binding, type: "R2Bucket" });
43
+ for (const { binding } of resourceRefs(config.kv_namespaces))
44
+ out.push({ name: binding, type: asyncAt(config, "2026-10-25") ? "KVNamespaceAsync" : "KVNamespace" });
45
+ for (const { binding } of resourceRefs(config.d1_databases))
46
+ out.push({ name: binding, type: asyncAt(config, "2026-10-24") ? "D1DatabaseAsync" : "D1Database" });
47
+ for (const { binding } of resourceRefs(config.r2_buckets))
48
+ out.push({ name: binding, type: asyncAt(config, "2026-10-26") ? "R2BucketAsync" : "R2Bucket" });
41
49
  for (const { binding } of resourceRefs(config.queues)) out.push({ name: binding, type: "Queue" });
42
50
  for (const name of config.analytics_engine_datasets ?? []) {
43
51
  out.push({ name, type: "AnalyticsEngineDataset" });
@@ -87,8 +95,9 @@ declare global {
87
95
  * Bindings from sproutboat.jsonc, for project "${config.name}".
88
96
  *
89
97
  * ${handlerNote}
90
- * Every call on these is synchronous, so \`await\` is allowed and does
91
- * nothing.
98
+ * D1 (from 2026-10-24), KV (2026-10-25) and R2 (2026-10-26) return Promises, as on
99
+ * Workers, so a project at or after those dates gets the *Async types; before them
100
+ * the calls are synchronous. \`await\` works at every date.
92
101
  */
93
102
  const env: {
94
103
  ${body}
@@ -6,7 +6,11 @@
6
6
  * generates a project's `sproutboat-env.d.ts`, which references this file and
7
7
  * declares `env` with that project's bindings.
8
8
  *
9
- * Bindings are synchronous. Before compatibility date 2026-09-28, `env` is a
9
+ * KV, D1 and R2 are synchronous before the compatibility date that makes each one return a
10
+ * Promise (D1 2026-10-24, KV 2026-10-25, R2 2026-10-26): `KVNamespace`, `D1Database` and
11
+ * `R2Bucket` are the synchronous types, `KVNamespaceAsync`, `D1DatabaseAsync` and
12
+ * `R2BucketAsync` the Promise types, and `sproutboat types` gives each binding the one for the
13
+ * project's date. `await` works at every date. Before compatibility date 2026-09-28, `env` is a
10
14
  * global only and `ctx` is the second handler argument. From that date, `env`
11
15
  * is also passed as the second argument and `ctx` as the third.
12
16
  */
@@ -22,7 +26,7 @@ declare global {
22
26
  }
23
27
 
24
28
  interface KVNamespace {
25
- /** The stored string, or `null`. Synchronous: no `await`. An expired key reads as absent. */
29
+ /** The stored string, or `null`. Synchronous before 2026-10-25 (a Promise from then). An expired key reads as absent. */
26
30
  get(key: string): string | null;
27
31
  put(key: string, value: string, options?: KVPutOptions): void;
28
32
  delete(key: string): void;
@@ -30,6 +34,33 @@ declare global {
30
34
  list(prefix?: string): string[];
31
35
  }
32
36
 
37
+ /**
38
+ * KV from compatibility date 2026-10-25: every method returns a Promise, as on Workers.
39
+ * `sproutboat types` picks this type for a project at or after that date.
40
+ */
41
+ interface KVNamespaceAsync {
42
+ get(key: string, type?: "text"): Promise<string | null>;
43
+ get<T = unknown>(key: string, type: "json"): Promise<T | null>;
44
+ get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>;
45
+ get(key: string, type: "stream"): Promise<ReadableStream | null>;
46
+ getWithMetadata<M = unknown>(
47
+ key: string,
48
+ type?: "text" | "json" | "arrayBuffer" | "stream",
49
+ ): Promise<{ value: unknown; metadata: M | null; cacheStatus: string | null }>;
50
+ put(
51
+ key: string,
52
+ value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
53
+ options?: KVPutOptions & { metadata?: unknown },
54
+ ): Promise<void>;
55
+ delete(key: string): Promise<void>;
56
+ list(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<{
57
+ keys: Array<{ name: string; expiration?: number; metadata?: unknown }>;
58
+ list_complete: boolean;
59
+ cursor?: string;
60
+ cacheStatus: string | null;
61
+ }>;
62
+ }
63
+
33
64
  // ---------------------------------------------------------------- D1
34
65
 
35
66
  interface D1Meta {
@@ -63,6 +94,48 @@ declare global {
63
94
  exec(sql: string): void;
64
95
  }
65
96
 
97
+ /** D1 from compatibility date 2026-10-24: workerd's meta, with D1's keys. */
98
+ interface D1MetaAsync {
99
+ served_by: string;
100
+ duration: number;
101
+ changes: number;
102
+ last_row_id: number;
103
+ changed_db: boolean;
104
+ size_after: number;
105
+ rows_read: number;
106
+ rows_written: number;
107
+ }
108
+
109
+ interface D1ResultAsync<T = Record<string, unknown>> {
110
+ results: T[];
111
+ success: boolean;
112
+ meta: D1MetaAsync;
113
+ }
114
+
115
+ /**
116
+ * From compatibility date 2026-10-24 `all()`, `run()`, `first()` and `raw()` return Promises, as
117
+ * on Workers. `sproutboat types` picks this type for a project at or after that date.
118
+ */
119
+ interface D1PreparedStatementAsync {
120
+ /** Positional `?` parameters, in order. Bytes bind as a BLOB. */
121
+ bind(...values: Array<string | number | boolean | null | ArrayBuffer | ArrayBufferView>): D1PreparedStatementAsync;
122
+ all<T = Record<string, unknown>>(): Promise<D1ResultAsync<T>>;
123
+ run<T = Record<string, unknown>>(): Promise<D1ResultAsync<T>>;
124
+ first<T = Record<string, unknown>>(): Promise<T | null>;
125
+ first<T = unknown>(column: string): Promise<T | null>;
126
+ raw<T = unknown[]>(options?: { columnNames?: false }): Promise<T[]>;
127
+ raw<T = unknown[]>(options: { columnNames: true }): Promise<[string[], ...T[]]>;
128
+ }
129
+
130
+ interface D1DatabaseAsync {
131
+ prepare(sql: string): D1PreparedStatementAsync;
132
+ batch<T = Record<string, unknown>>(statements: D1PreparedStatementAsync[]): Promise<D1ResultAsync<T>[]>;
133
+ /** Runs a script line by line and counts the lines. */
134
+ exec(sql: string): Promise<{ count: number; duration: number }>;
135
+ /** A Sproutboat addition: an online, integrity-checked copy of the database. */
136
+ backup(name?: string): Promise<{ path: string; bytes: number }>;
137
+ }
138
+
66
139
  // ---------------------------------------------------------------- R2
67
140
 
68
141
  interface R2HttpMetadata {
@@ -172,6 +245,65 @@ declare global {
172
245
  list(options?: R2ListOptions): R2Objects;
173
246
  }
174
247
 
248
+ /**
249
+ * R2 from compatibility date 2026-10-26: every method returns a Promise, as on Workers, and
250
+ * `uploaded` is a Date. `sproutboat types` picks these types for a project at or after that date.
251
+ */
252
+ interface R2ObjectAsync {
253
+ key: string;
254
+ size: number;
255
+ etag: string;
256
+ httpEtag: string;
257
+ checksums: { md5?: ArrayBuffer };
258
+ uploaded: Date;
259
+ httpMetadata?: R2HttpMetadata;
260
+ customMetadata?: Record<string, string>;
261
+ range?: { offset: number; length: number };
262
+ }
263
+
264
+ interface R2ObjectBodyAsync extends R2ObjectAsync {
265
+ body: ReadableStream;
266
+ bodyUsed: boolean;
267
+ arrayBuffer(): Promise<ArrayBuffer>;
268
+ text(): Promise<string>;
269
+ json<T = unknown>(): Promise<T>;
270
+ blob(): Promise<Blob>;
271
+ }
272
+
273
+ interface R2MultipartUploadAsync {
274
+ key: string;
275
+ uploadId: string;
276
+ uploadPart(
277
+ partNumber: number,
278
+ value: string | ArrayBuffer | ArrayBufferView | ReadableStream | Blob,
279
+ ): Promise<R2UploadedPart>;
280
+ complete(parts: R2UploadedPart[]): Promise<R2ObjectAsync>;
281
+ abort(): Promise<void>;
282
+ }
283
+
284
+ interface R2BucketAsync extends Pick<R2Bucket, "createUploadUrl" | "createDownloadUrl"> {
285
+ head(key: string): Promise<R2ObjectAsync | null>;
286
+ get(
287
+ key: string,
288
+ options?: { range?: { offset?: number; length?: number; suffix?: number }; onlyIf?: unknown },
289
+ ): Promise<R2ObjectBodyAsync | null>;
290
+ put(
291
+ key: string,
292
+ value: string | ArrayBuffer | ArrayBufferView | ReadableStream | Blob | null,
293
+ options?: { httpMetadata?: R2HttpMetadata | Headers; customMetadata?: Record<string, string>; onlyIf?: unknown },
294
+ ): Promise<R2ObjectAsync | null>;
295
+ delete(keys: string | string[]): Promise<void>;
296
+ list(options?: {
297
+ prefix?: string;
298
+ limit?: number;
299
+ cursor?: string;
300
+ delimiter?: string;
301
+ include?: Array<"httpMetadata" | "customMetadata">;
302
+ }): Promise<{ objects: R2ObjectAsync[]; truncated: boolean; cursor?: string; delimitedPrefixes: string[] }>;
303
+ createMultipartUpload(key: string, options?: R2PutOptions): Promise<R2MultipartUploadAsync>;
304
+ resumeMultipartUpload(key: string, uploadId: string): R2MultipartUploadAsync;
305
+ }
306
+
175
307
  // ------------------------------------------------------------ Queues
176
308
 
177
309
  type QueuesContentType = "json" | "text" | "bytes" | "v8";
@@ -275,7 +407,10 @@ declare global {
275
407
  /**
276
408
  * The WebSocket hibernation API, from compatibility date 2026-10-30. Messages and
277
409
  * closes on an accepted socket go to the class's `webSocketMessage` and
278
- * `webSocketClose`. On sproutd an open socket keeps the app's worker running.
410
+ * `webSocketClose`. On sproutd an open socket keeps the app's worker running before
411
+ * compatibility date 2026-11-01; from it the worker is reaped while the socket idles and
412
+ * the object is constructed again (with `getWebSockets()` as it was) when a message or
413
+ * close arrives.
279
414
  */
280
415
  acceptWebSocket(ws: WebSocket, tags?: string[]): void;
281
416
  getWebSockets(tag?: string): WebSocket[];
@@ -289,9 +424,12 @@ declare global {
289
424
  /**
290
425
  * From compatibility date 2026-10-30, as in workerd. Return the client end in a
291
426
  * `new Response(null, { status: 101, webSocket: client })` and talk on the server end.
292
- * Client sockets are carried by sproutd; a standalone or platform sprout answers a 101
293
- * with 501 for now. Inbound messages are capped at 32 MiB (1009 above it). Outbound
294
- * WebSockets (`new WebSocket(url)`) are not supported.
427
+ * Client sockets are carried by sproutd, and from compatibility date 2026-11-01 by
428
+ * standalone and platform sprouts too (before it they answer a 101 with 501). Inbound
429
+ * messages are capped at 32 MiB (1009 above it). From 2026-11-01 `new WebSocket(url,
430
+ * protocols)` and `fetch(url, { headers: { Upgrade: "websocket" } })` connect out; the
431
+ * Response's `webSocket` needs `accept()`, and returning it from a handler relays the client.
432
+ * Outbound sockets need the embedded transport (standalone, sproutd), not `sproutboat dev`.
295
433
  */
296
434
  class WebSocketPair {
297
435
  0: WebSocket;