sproutboat 0.36.0 → 0.38.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,44 @@ maintained going forward by the `release` skill.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## [0.38.0] - 2026-10-27
14
+
15
+ Built on `@sproutboat/runtime` 0.34.2, `@sproutboat/toolchain` 0.19.2 and
16
+ `@sproutboat/wire` 0.20.2.
17
+
18
+ ### Performance
19
+
20
+ - **A request costs about a quarter of the CPU it did.** Most of it was one thing: Porffor's `globalThis`
21
+ rebuilt the whole global object (one write per top-level binding) on every read, and a plain request
22
+ made about eleven reads. A read of `globalThis` now rewrites only the bindings that changed, the
23
+ runtime's own request path reads no `globalThis` at all, the client IP and trusted-proxy headers are
24
+ read once per process, and `x-sb-cpu-ms` is added to your Response instead of building a second one.
25
+ Measured on one pinned x86 core, a 404 from a standalone binary went from 319 to 89 microseconds of CPU
26
+ per request when saturated (3,100 to 11,000 requests per second on that core); a small JSON 200 from
27
+ 322 to 78. `globalThis.x` in your own code is cheap too: 20,000 reads with 600 top-level functions took
28
+ 9.3 s before and a few milliseconds now. Nothing in the responses changes.
29
+
30
+ ## [0.37.0] - 2026-10-27
31
+
32
+ Built on `@sproutboat/runtime` 0.34.1, `@sproutboat/toolchain` 0.19.1 and
33
+ `@sproutboat/wire` 0.20.2.
34
+
35
+ ### Fixed
36
+
37
+ - **A failed binding call you do not await no longer ends the process.** An un-awaited
38
+ `db.prepare("INSERT ...").run()` that broke a foreign key used to take the whole binary down, even
39
+ with a `try` / `catch` around the call. As on Workers, an unhandled rejection is now logged
40
+ (`Uncaught (in promise) ...`), fires `unhandledrejection` when the app has a listener
41
+ (`addEventListener("unhandledrejection", ...)`), and the process and other requests keep going. An
42
+ un-awaited `async function` that throws is reported the same way. At every compatibility date.
43
+
44
+ ### Changed
45
+
46
+ - **Types for the Promise bindings.** `sproutboat types` gives D1 (from compatibility date 2026-10-24),
47
+ KV (2026-10-25) and R2 (2026-10-26) bindings the Promise types (`D1DatabaseAsync`,
48
+ `KVNamespaceAsync`, `R2BucketAsync`); earlier dates keep the synchronous ones.
49
+ - Docs, examples and type comments no longer say binding calls are synchronous at every date.
50
+
13
51
  ## [0.36.0] - 2026-10-26
14
52
 
15
53
  Built on `@sproutboat/runtime` 0.34.0, `@sproutboat/toolchain` 0.19.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
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.36.0
6
+ **Package:** `sproutboat` 0.38.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.36.0",
3
+ "version": "0.38.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,8 +64,8 @@
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.34.0",
68
- "@sproutboat/toolchain": "^0.19.0",
67
+ "@sproutboat/runtime": "^0.34.2",
68
+ "@sproutboat/toolchain": "^0.19.2",
69
69
  "@sproutboat/wire": "^0.20.2"
70
70
  },
71
71
  "devDependencies": {
@@ -78,10 +78,10 @@
78
78
  "typescript": "7.0.2"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@sproutboat/cli-darwin-arm64": "0.36.0",
82
- "@sproutboat/cli-darwin-x64": "0.36.0",
83
- "@sproutboat/cli-linux-arm64": "0.36.0",
84
- "@sproutboat/cli-linux-x64": "0.36.0"
81
+ "@sproutboat/cli-darwin-arm64": "0.38.0",
82
+ "@sproutboat/cli-darwin-x64": "0.38.0",
83
+ "@sproutboat/cli-linux-arm64": "0.38.0",
84
+ "@sproutboat/cli-linux-x64": "0.38.0"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=18"
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";