sproutboat 0.54.0 → 0.55.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/src/types-gen.ts CHANGED
@@ -1,45 +1,66 @@
1
1
  /**
2
- * `sproutboat types` — the project's `env`, as TypeScript.
2
+ * `sproutboat types` — the project's `Env`, as TypeScript, the way `wrangler types` writes it.
3
3
  *
4
- * `env` remains available as a global in both compatibility modes. This file
5
- * gives it the project's binding names and types. From 2026-09-28 it is also
6
- * the second handler argument.
4
+ * The output is `worker-configuration.d.ts` (Wrangler's file name), a script file that declares an `Env` interface
5
+ * and `Cloudflare.Env` (what `import { env } from "cloudflare:workers"` reads) from `sproutboat.jsonc`, then
6
+ * references the ambient types shipped in `types/sproutboat.d.ts`. Binding *names* version with the project and
7
+ * binding *shapes* with the CLI. A handler is typed with `satisfies ExportedHandler<Env>`.
7
8
  *
8
- * The output declares `env` from `sproutboat.jsonc` and references the ambient
9
- * types shipped in `types/sproutboat.d.ts`, so binding *shapes* version with
10
- * the CLI and binding *names* version with the project.
11
- *
12
- * Names need no quoting: `parseConfig` already holds every binding and var to
13
- * /^[A-Z][A-Z0-9_]*$/, so all of them are plain identifiers.
9
+ * Names need no quoting: `parseConfig` already holds every binding and var to /^[A-Z][A-Z0-9_]*$/, so all of them
10
+ * are plain identifiers.
14
11
  */
12
+ import { createHash } from "node:crypto";
15
13
  import { resourceRefs, type SproutboatConfig } from "./config";
16
14
 
17
- /** The file the generator writes, relative to the project directory. */
18
- export const TYPES_FILE = "sproutboat-env.d.ts";
15
+ /** The file `sproutboat types` writes by default, relative to the project directory. */
16
+ export const TYPES_FILE = "worker-configuration.d.ts";
19
17
 
20
- const HEADER = `// Generated by \`sproutboat types\`. Do not edit.
21
- //
22
- // Regenerated whenever sproutboat.jsonc is newer than this file, so a binding
23
- // added to the config shows up in the editor without asking. Commit it: it is
24
- // part of the project's type surface, like a schema.
25
- `;
18
+ /** The file earlier releases wrote. It is only ever removed when it still carries its generated header. */
19
+ export const LEGACY_TYPES_FILE = "sproutboat-env.d.ts";
20
+ export const LEGACY_TYPES_HEADER = "// Generated by `sproutboat types`. Do not edit.";
21
+
22
+ export type TypesOptions = {
23
+ /** Name of the interface (`--env-interface`). Default `Env`. */
24
+ envInterface?: string;
25
+ /** Literal types for `vars` (`--strict-vars`, default on); off, every var is a `string`. */
26
+ strictVars?: boolean;
27
+ /** Reference the runtime types shipped with the CLI (`--include-runtime`, default on). */
28
+ includeRuntime?: boolean;
29
+ /** Write the `Env` types (`--include-env`, default on). */
30
+ includeEnv?: boolean;
31
+ /** The command as typed, for the header. */
32
+ command?: string;
33
+ /**
34
+ * The entry module, so a Durable Object binding is typed with its class and `stub.method()` is checked.
35
+ * `importPath` is relative to the output file and has no extension; only a TypeScript entry is offered.
36
+ */
37
+ mainModule?: { importPath: string; classes: readonly string[] };
38
+ };
26
39
 
27
40
  type Entry = { name: string; type: string; note?: string };
28
41
 
29
- /** Whether the binding's calls return Promises at the project's compatibility date. */
42
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
43
+
44
+ /** Whether a binding's calls return Promises at the project's compatibility date. */
30
45
  function asyncAt(config: SproutboatConfig, date: string): boolean {
31
46
  return config.compatibility_date >= date;
32
47
  }
33
48
 
34
- function entries(config: SproutboatConfig): Entry[] {
49
+ /** `export class Name` declarations of an entry module, for typing Durable Object bindings. */
50
+ export function exportedClasses(source: string): string[] {
51
+ return [...source.matchAll(/^\s*export\s+(?:abstract\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)/gm)].map(
52
+ (match) => match[1]!,
53
+ );
54
+ }
55
+
56
+ function entries(config: SproutboatConfig, options: TypesOptions): Entry[] {
35
57
  const out: Entry[] = [];
58
+ const strictVars = options.strictVars ?? true;
36
59
 
37
60
  for (const [name, value] of Object.entries(config.vars ?? {})) {
38
- out.push({ name, type: "string", note: `vars: ${JSON.stringify(value)}` });
39
- }
40
- for (const name of config.secrets ?? []) {
41
- out.push({ name, type: "string", note: "secret, set with `sproutboat secrets put`" });
61
+ out.push({ name, type: strictVars ? JSON.stringify(value) : "string" });
42
62
  }
63
+ for (const name of config.secrets ?? []) out.push({ name, type: "string", note: "secret" });
43
64
  for (const { binding } of resourceRefs(config.kv_namespaces))
44
65
  out.push({ name: binding, type: asyncAt(config, "2026-10-25") ? "KVNamespaceAsync" : "KVNamespace" });
45
66
  for (const { binding } of resourceRefs(config.d1_databases))
@@ -47,63 +68,78 @@ function entries(config: SproutboatConfig): Entry[] {
47
68
  for (const { binding } of resourceRefs(config.r2_buckets))
48
69
  out.push({ name: binding, type: asyncAt(config, "2026-10-26") ? "R2BucketAsync" : "R2Bucket" });
49
70
  for (const { binding } of resourceRefs(config.queues)) out.push({ name: binding, type: "Queue" });
50
- for (const name of config.analytics_engine_datasets ?? []) {
51
- out.push({ name, type: "AnalyticsEngineDataset" });
52
- }
71
+ for (const name of config.analytics_engine_datasets ?? []) out.push({ name, type: "AnalyticsEngineDataset" });
53
72
  for (const [binding, className] of Object.entries(config.durable_objects ?? {})) {
54
- out.push({ name: binding, type: "DurableObjectNamespace", note: `class ${className}` });
73
+ if (!asyncAt(config, "2026-10-28")) {
74
+ out.push({ name: binding, type: "DurableObjectNamespace", note: className });
75
+ continue;
76
+ }
77
+ const typed = options.mainModule?.classes.includes(className) && IDENTIFIER.test(className);
78
+ out.push({
79
+ name: binding,
80
+ type: typed
81
+ ? `DurableObjectNamespaceAsync<import(${JSON.stringify(options.mainModule!.importPath)}).${className}>`
82
+ : "DurableObjectNamespaceAsync",
83
+ note: typed ? undefined : className,
84
+ });
55
85
  }
56
86
  for (const service of config.services ?? []) {
57
- out.push({ name: service.binding, type: "Fetcher", note: `service binding: ${service.service}` });
58
- }
59
- for (const rl of config.ratelimiters ?? []) {
60
- out.push({ name: rl.binding, type: "RateLimit", note: `rate limit: ${rl.limit} per ${rl.period}s` });
87
+ out.push({
88
+ name: service.binding,
89
+ type: asyncAt(config, "2026-10-22") ? "FetcherAsync" : "Fetcher",
90
+ note: `service: ${service.service}`,
91
+ });
61
92
  }
93
+ for (const limiter of config.ratelimiters ?? []) out.push({ name: limiter.binding, type: "RateLimit" });
62
94
  if (config.assets?.binding) {
63
- out.push({ name: config.assets.binding, type: "Fetcher", note: `static assets: ${config.assets.directory}` });
64
- }
65
- if (config.version_metadata) {
66
- out.push({ name: config.version_metadata, type: "VersionMetadata" });
95
+ out.push({ name: config.assets.binding, type: asyncAt(config, "2026-10-22") ? "FetcherAsync" : "Fetcher" });
67
96
  }
97
+ if (config.version_metadata) out.push({ name: config.version_metadata, type: "VersionMetadata" });
68
98
  return out;
69
99
  }
70
100
 
101
+ const sha = (text: string) => createHash("sha256").update(text).digest("hex").slice(0, 32);
102
+
103
+ /** The hash a generated file carries in its first line, or `undefined` for anything else. */
104
+ export function hashOf(file: string): string | undefined {
105
+ return /^\/\* eslint-disable \*\/\n\/\/ Generated by Sproutboat by running `[^\n]*` \(hash: ([0-9a-f]{32})\)\n/.exec(
106
+ file,
107
+ )?.[1];
108
+ }
109
+
71
110
  /**
72
- * The contents of `sproutboat-env.d.ts` for `config`.
111
+ * The contents of `worker-configuration.d.ts` for `config`.
73
112
  *
74
- * Pure, so the test can assert on the text and `dev` can compare it against
75
- * what is already on disk without writing.
113
+ * Pure, so the tests can assert on the text and `dev` can compare it with what is on disk without writing.
76
114
  */
77
- export function generateTypes(config: SproutboatConfig): string {
78
- const rows = entries(config);
79
- const body = rows.length
80
- ? rows
81
- .map((entry) => ` ${entry.note ? `/** ${entry.note} */\n ` : ""}${entry.name}: ${entry.type};`)
82
- .join("\n")
83
- : " // No bindings declared in sproutboat.jsonc yet.";
84
-
85
- const handlerNote =
86
- config.compatibility_date >= "2026-09-28"
87
- ? "The same object is passed as the second handler argument: fetch(request, env, ctx)."
88
- : "The handler signature is fetch(request, ctx); env is only a global.";
115
+ export function generateTypes(config: SproutboatConfig, options: TypesOptions = {}): string {
116
+ const name = options.envInterface ?? "Env";
117
+ if (!IDENTIFIER.test(name)) throw new Error(`"${name}" is not a valid interface name`);
118
+ const includeRuntime = options.includeRuntime ?? true;
119
+ const includeEnv = options.includeEnv ?? true;
120
+ const legacy = config.compatibility_date < "2026-09-28";
89
121
 
90
- return `${HEADER}
91
- /// <reference types="sproutboat/types" />
92
-
93
- declare global {
94
- /**
95
- * Bindings from sproutboat.jsonc, for project "${config.name}".
96
- *
97
- * ${handlerNote}
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.
101
- */
102
- const env: {
103
- ${body}
104
- };
122
+ const parts: string[] = [];
123
+ if (includeRuntime) parts.push('/// <reference types="sproutboat/types" />');
124
+ if (includeEnv) {
125
+ const rows = entries(config, options);
126
+ const base = `__Base${name}`;
127
+ const members = rows.map((row) => `\t${row.name}: ${row.type};${row.note ? ` /* ${row.note} */` : ""}`);
128
+ parts.push(
129
+ `interface ${base} {${members.length ? `\n${members.join("\n")}\n` : ""}}`,
130
+ `declare namespace Cloudflare {\n\tinterface Env extends ${base} {}\n}`,
131
+ `interface ${name} extends ${base} {}`,
132
+ );
133
+ // Before compatibility date 2026-09-28 the handler is `fetch(request, ctx)` and `env` is only a global.
134
+ if (legacy) parts.push(`declare const env: ${name};`);
135
+ }
136
+ const body = parts.join("\n") + "\n";
137
+ const command = options.command ?? "sproutboat types";
138
+ return `/* eslint-disable */\n// Generated by Sproutboat by running \`${command}\` (hash: ${sha(body)})\n${body}`;
105
139
  }
106
140
 
107
- export {};
108
- `;
141
+ /** Whether `file` is what `generateTypes` would write for the same input, by the hash in its header. */
142
+ export function isCurrent(file: string, wanted: string): boolean {
143
+ const have = hashOf(file);
144
+ return have !== undefined && have === hashOf(wanted);
109
145
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The `cloudflare:workers` module, as the runtime provides it: the base classes Workers code extends and the
3
+ * `env` import. A script file on purpose: an ambient `declare module` is only a declaration here, not an
4
+ * augmentation. `sproutboat.d.ts` references it, so the one reference a project's generated file carries is enough.
5
+ *
6
+ * `Cloudflare.Env` is declared by the project's `worker-configuration.d.ts`.
7
+ */
8
+ /// <reference path="./sproutboat.d.ts" />
9
+
10
+ declare module "cloudflare:workers" {
11
+ /** The bindings of this project, same object as the second handler argument. */
12
+ export const env: Cloudflare.Env;
13
+
14
+ /**
15
+ * A Durable Object class, from compatibility date 2026-10-29 (2026-10-28 for the Promise storage): `this.ctx` and
16
+ * `this.env` are set, and any method is callable as `await stub.method()`.
17
+ */
18
+ export abstract class DurableObject<Bindings = Cloudflare.Env> {
19
+ constructor(ctx: DurableObjectStateAsync, env: Bindings);
20
+ protected ctx: DurableObjectStateAsync;
21
+ protected env: Bindings;
22
+ fetch?(request: Request): Response | Promise<Response>;
23
+ /** Runs after `storage.setAlarm`, with no request in flight. */
24
+ alarm?(info?: { retryCount: number; isRetry: boolean }): void | Promise<void>;
25
+ webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise<void>;
26
+ webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise<void>;
27
+ }
28
+
29
+ /**
30
+ * A Worker as a class: `export default class extends WorkerEntrypoint<Env>`. Its methods are what a service
31
+ * binding calls over RPC (from compatibility date 2026-11-18).
32
+ */
33
+ export abstract class WorkerEntrypoint<Bindings = Cloudflare.Env> {
34
+ constructor(ctx: ExecutionContext, env: Bindings);
35
+ protected ctx: ExecutionContext;
36
+ protected env: Bindings;
37
+ fetch?(request: Request): Response | Promise<Response>;
38
+ scheduled?(controller: ScheduledController): void | Promise<void>;
39
+ queue?(batch: MessageBatch): void | Promise<void>;
40
+ }
41
+
42
+ export class RpcTarget {}
43
+ }
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Shipped with the CLI rather than as a separate package, so the types always
5
5
  * describe the runtime that the installed CLI compiles. `sproutboat types`
6
- * generates a project's `sproutboat-env.d.ts`, which references this file and
7
- * declares `env` with that project's bindings.
6
+ * generates a project's `worker-configuration.d.ts`, which references this file
7
+ * and declares `Env` with that project's bindings, as `wrangler types` does.
8
8
  *
9
9
  * KV, D1 and R2 are synchronous before the compatibility date that makes each one return a
10
10
  * Promise (D1 2026-10-24, KV 2026-10-25, R2 2026-10-26): `KVNamespace`, `D1Database` and
@@ -13,9 +13,24 @@
13
13
  * project's date. `await` works at every date. Before compatibility date 2026-09-28, `env` is a
14
14
  * global only and `ctx` is the second handler argument. From that date, `env`
15
15
  * is also passed as the second argument and `ctx` as the third.
16
+ *
17
+ * Durable Objects follow the same rule: from 2026-10-28 storage and `stub.fetch()` return Promises
18
+ * (`DurableObjectNamespaceAsync`, `DurableObjectStorageAsync`, `DurableObjectStateAsync`), from
19
+ * 2026-10-29 a class that extends `DurableObject` from "cloudflare:workers" is called over RPC, and from
20
+ * 2026-10-22 `fetch()` on a service or assets binding returns a Promise (`FetcherAsync`).
16
21
  */
17
22
 
23
+ /// <reference path="./cloudflare-workers.d.ts" />
24
+
18
25
  declare global {
26
+ /**
27
+ * The project's bindings. Empty here; `sproutboat types` writes `interface Env extends ...` into
28
+ * `worker-configuration.d.ts`, which merges with this one, and `import { env } from "cloudflare:workers"` reads it.
29
+ */
30
+ namespace Cloudflare {
31
+ interface Env {}
32
+ }
33
+
19
34
  // ---------------------------------------------------------------- KV
20
35
 
21
36
  interface KVPutOptions {
@@ -262,6 +277,8 @@ declare global {
262
277
  }
263
278
 
264
279
  interface R2ObjectBodyAsync extends R2ObjectAsync {
280
+ /** Copies `httpMetadata` (content type, cache control, ...) onto `headers`, as on Workers. */
281
+ writeHttpMetadata(headers: Headers): void;
265
282
  body: ReadableStream;
266
283
  bodyUsed: boolean;
267
284
  arrayBuffer(): Promise<ArrayBuffer>;
@@ -419,6 +436,118 @@ declare global {
419
436
  getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;
420
437
  }
421
438
 
439
+ // ------------------------------------- Durable Objects, from 2026-10-28
440
+
441
+ /** Workers RPC: the methods of a class, as Promise-returning stub methods. */
442
+ type RpcMethods<T> = {
443
+ [
444
+ K in keyof T as K extends
445
+ | "fetch"
446
+ | "alarm"
447
+ | "webSocketMessage"
448
+ | "webSocketClose"
449
+ | "webSocketError"
450
+ | "scheduled"
451
+ | "queue"
452
+ | "ctx"
453
+ | "env"
454
+ ? never
455
+ : T[K] extends (...args: never[]) => unknown
456
+ ? K
457
+ : never
458
+ ]: T[K] extends (...args: infer Args) => infer Result ? (...args: Args) => Promise<Awaited<Result>> : never;
459
+ };
460
+
461
+ /**
462
+ * What `namespace.get(id)` returns from 2026-10-28: `fetch()` is a Promise, and the methods of a
463
+ * class that extends `DurableObject` are callable as `await stub.method(...)` from 2026-10-29. Give
464
+ * the class as the type argument: `DurableObjectNamespaceAsync<Counter>`.
465
+ */
466
+ type DurableObjectStubAsync<T = undefined> = (T extends undefined ? unknown : RpcMethods<T>) & {
467
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
468
+ readonly id: DurableObjectId;
469
+ readonly name?: string;
470
+ };
471
+
472
+ interface DurableObjectNamespaceAsync<T = undefined> {
473
+ idFromName(name: string): DurableObjectId;
474
+ /** A 64-digit hex id, from `id.toString()`. */
475
+ idFromString(id: string): DurableObjectId;
476
+ newUniqueId(): DurableObjectId;
477
+ get(id: DurableObjectId): DurableObjectStubAsync<T>;
478
+ /** `get(idFromName(name))`, from 2026-10-29. */
479
+ getByName(name: string): DurableObjectStubAsync<T>;
480
+ }
481
+
482
+ interface DurableObjectListOptions {
483
+ prefix?: string;
484
+ start?: string;
485
+ startAfter?: string;
486
+ end?: string;
487
+ limit?: number;
488
+ reverse?: boolean;
489
+ }
490
+
491
+ interface DurableObjectTransaction {
492
+ get<T = unknown>(key: string): Promise<T | undefined>;
493
+ put<T>(key: string, value: T): Promise<void>;
494
+ delete(key: string): Promise<boolean>;
495
+ list<T = unknown>(options?: DurableObjectListOptions): Promise<Map<string, T>>;
496
+ rollback(): void;
497
+ }
498
+
499
+ type SqlStorageValue = ArrayBuffer | string | number | null;
500
+
501
+ interface SqlStorageCursor<T = Record<string, SqlStorageValue>> extends Iterable<T> {
502
+ readonly columnNames: string[];
503
+ readonly rowsRead: number;
504
+ readonly rowsWritten: number;
505
+ toArray(): T[];
506
+ one(): T;
507
+ raw<R extends SqlStorageValue[] = SqlStorageValue[]>(): IterableIterator<R>;
508
+ next(): IteratorResult<T>;
509
+ }
510
+
511
+ /** From 2026-10-29: each object has its own SQLite database. */
512
+ interface SqlStorage {
513
+ exec<T = Record<string, SqlStorageValue>>(query: string, ...bindings: unknown[]): SqlStorageCursor<T>;
514
+ readonly databaseSize: number;
515
+ }
516
+
517
+ /** Storage from 2026-10-28: every call returns a Promise, and an object keeps its own input gate. */
518
+ interface DurableObjectStorageAsync {
519
+ get<T = unknown>(key: string): Promise<T | undefined>;
520
+ get<T = unknown>(keys: string[]): Promise<Map<string, T>>;
521
+ put<T>(key: string, value: T): Promise<void>;
522
+ put(entries: Record<string, unknown>): Promise<void>;
523
+ delete(key: string): Promise<boolean>;
524
+ delete(keys: string[]): Promise<number>;
525
+ deleteAll(): Promise<void>;
526
+ list<T = unknown>(options?: DurableObjectListOptions): Promise<Map<string, T>>;
527
+ transaction<T>(closure: (txn: DurableObjectTransaction) => Promise<T>): Promise<T>;
528
+ /** Synchronous transaction over `sql`; rolls back if `closure` throws. */
529
+ transactionSync<T>(closure: () => T): T;
530
+ sync(): Promise<void>;
531
+ /** At most one alarm is pending per object; a later `setAlarm` replaces it. */
532
+ setAlarm(scheduledTime: number | Date): Promise<void>;
533
+ getAlarm(): Promise<number | null>;
534
+ deleteAlarm(): Promise<void>;
535
+ readonly sql: SqlStorage;
536
+ }
537
+
538
+ interface DurableObjectStateAsync {
539
+ readonly id: DurableObjectId;
540
+ readonly storage: DurableObjectStorageAsync;
541
+ /** Holds every other event out of the object until `callback` settles. Use it for start-up work. */
542
+ blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>;
543
+ waitUntil(promise: Promise<unknown>): void;
544
+ acceptWebSocket(ws: WebSocket, tags?: string[]): void;
545
+ getWebSockets(tag?: string): WebSocket[];
546
+ getTags(ws: WebSocket): string[];
547
+ setWebSocketAutoResponse(pair?: WebSocketRequestResponsePair): void;
548
+ getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;
549
+ }
550
+
422
551
  // ---------------------------------------------------------- WebSockets
423
552
 
424
553
  /**
@@ -502,11 +631,20 @@ declare global {
502
631
 
503
632
  // ------------------------------------ Fetchers: services and assets
504
633
 
505
- /** A service binding, and the shape of the static-asset binding. */
634
+ /** A service binding, and the shape of the static-asset binding, before 2026-10-22. */
506
635
  interface Fetcher {
507
636
  fetch(request: Request): Response;
508
637
  }
509
638
 
639
+ /**
640
+ * From 2026-10-22 `fetch()` on a service or assets binding returns a Promise. A service binding to a
641
+ * `WorkerEntrypoint` also exposes its methods over RPC from 2026-11-18: give the class as the type
642
+ * argument, `FetcherAsync<QuoteService>`, and `await env.PEER.random()` is checked.
643
+ */
644
+ type FetcherAsync<T = undefined> = (T extends undefined ? unknown : RpcMethods<T>) & {
645
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
646
+ };
647
+
510
648
  // --------------------------------------------------------- Handlers
511
649
 
512
650
  interface ScheduledEvent {
@@ -515,6 +653,15 @@ declare global {
515
653
  scheduledTime: number;
516
654
  }
517
655
 
656
+ /** What `scheduled(controller, env, ctx)` receives, from compatibility date 2026-09-28. */
657
+ interface ScheduledController {
658
+ /** The expression that fired, as written in `triggers.crons`. */
659
+ readonly cron: string;
660
+ readonly scheduledTime: number;
661
+ /** Do not retry this run if the handler throws. */
662
+ noRetry(): void;
663
+ }
664
+
518
665
  /** Work that outlives the response, drained in-process and capped at 25s. */
519
666
  interface ExecutionContext {
520
667
  waitUntil(promise: Promise<unknown>): void;
@@ -529,6 +676,16 @@ declare global {
529
676
  queue?(batch: MessageBatch<QueueBody>, ctx: ExecutionContext): void | Promise<void>;
530
677
  }
531
678
 
679
+ /**
680
+ * The default export of a Worker, as in Workers: `satisfies ExportedHandler<Env>`. Compatibility dates
681
+ * from 2026-09-28; `Env` comes from `sproutboat types`.
682
+ */
683
+ interface ExportedHandler<Bindings = unknown, QueueBody = unknown> {
684
+ fetch?(request: Request, env: Bindings, ctx: ExecutionContext): Response | Promise<Response>;
685
+ scheduled?(controller: ScheduledController, env: Bindings, ctx: ExecutionContext): void | Promise<void>;
686
+ queue?(batch: MessageBatch<QueueBody>, env: Bindings, ctx: ExecutionContext): void | Promise<void>;
687
+ }
688
+
532
689
  /** Handler signature for compatibility dates from 2026-09-28. */
533
690
  interface WorkersHandler<Bindings = Record<string, unknown>, QueueBody = unknown> {
534
691
  fetch(request: Request, env: Bindings, ctx: ExecutionContext): Response | Promise<Response>;