8000 feat(cloudflare): Worker RPC binding types (#223) · sam-goodwin/alchemy@055354c · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Commit 055354c

Browse files
authored
feat(cloudflare): Worker RPC binding types (#223)
1 parent d7c0d2c commit 055354c

File tree

8 files changed

+172
-106
lines changed

8 files changed

+172
-106
lines changed

alchemy/src/cloudflare/bound.ts

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { HyperdriveResource as _Hyperdrive } from "./hyperdrive.js";
1313
import type { PipelineResource as _Pipeline } from "./pipeline.js";
1414
import type { QueueResource as _Queue } from "./queue.js";
1515
import type { VectorizeIndexResource as _VectorizeIndex } from "./vectorize-index.js";
16+
import type { Worker as _Worker } from "./worker.js";
1617
import type { Workflow as _Workflow } from "./workflow.js";
1718

1819
export type Bound<T extends Binding> = T extends _DurableObjectNamespace<
@@ -21,38 +22,47 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace<
2122
? DurableObjectNamespace<O>
2223
: T extends { type: "kv_namespace" }
2324
? KVNamespace
24-
: T extends { type: "service" }
25-
? Service
26-
: T extends _R2Bucket
27-
? R2Bucket
28-
: T extends _AiGateway
29-
? AiGateway
30-
: T extends _Hyperdrive
31-
? Hyperdrive
32-
: T extends Secret
33-
? string
34-
: T extends Assets
35-
? Service
36-
: T extends _Workflow<infer P>
37-
? Workflow<P>
38-
: T extends D1DatabaseResource
39-
? D1Database
40-
: T extends _VectorizeIndex
41-
? VectorizeIndex
42-
: T extends _Queue<infer Body>
43-
? Queue<Body>
44-
: T extends _AnalyticsEngineDataset
45-
? AnalyticsEngineDataset
46-
: T extends _Pipeline<infer R>
47-
? Pipeline<R>
48-
: T extends string
49-
? string
50-
: T extends BrowserRendering
51-
? Fetcher
52-
: T extends _Ai<infer M>
53-
? Ai<M>
54-
: T extends Self
55-
? Service
56-
: T extends Json<infer T>
57-
? T
58-
: Service;
25+
: T extends _Worker<any, infer RPC>
26+
? Service<RPC> & {
27+
// cloudflare's Rpc.Provider type loses mapping between properties (jump to definition)
28+
// we fix that using Pick to re-connect mappings
29+
[property in keyof Pick<
30+
RPC,
31+
Extract<keyof Rpc.Provider<RPC, "fetch" | "connect">, keyof RPC>
32+
>]: Rpc.Provider<RPC, "fetch" | "connect">[property];
33+
}
34+
: T extends { type: "service" }
35+
? Service
36+
: T extends _R2Bucket
37+
? R2Bucket
38+
: T extends _AiGateway
39+
? AiGateway
40+
: T extends _Hyperdrive
41+
? Hyperdrive
42+
: T extends Secret
43+
? string
44+
: T extends Assets
45+
? Service
46+
: T extends _Workflow<infer P>
47+
? Workflow<P>
48+
: T extends D1DatabaseResource
49+
? D1Database
50+
: T extends _VectorizeIndex
51+
? VectorizeIndex
52+
: T extends _Queue<infer Body>
53+
? Queue<Body>
54+
: T extends _AnalyticsEngineDataset
55+
? AnalyticsEngineDataset
56+
: T extends _Pipeline<infer R>
57+
? Pipeline<R>
58+
: T extends string
59+
? string
60+
: T extends BrowserRendering
61+
? Fetcher
62+
: T extends _Ai<infer M>
63+
? Ai<M>
64+
: T extends Self
65+
? Service
66+
: T extends Json<infer T>
67+
? T
68+
: Service;

alchemy/src/cloudflare/worker.ts

Lines changed: 89 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { bootstrapPlugin } from "../runtime/plugin.js";
88
import { Scope } from "../scope.js";
99
import { Secret, secret } from "../secret.js";
1010
import { serializeScope } from "../serde.js";
11+
import type { type } from "../type.js";
1112
import { withExponentialBackoff } from "../util/retry.js";
1213
import { slugify } from "../util/slugify.js";
1314
import { CloudflareApiError, handleApiError } from "./api-error.js";
@@ -99,8 +100,10 @@ export interface AssetsConfig {
99100
serve_directly?: boolean;
100101
}
101102

102-
export interface BaseWorkerProps<B extends Bindings | undefined = undefined>
103-
extends CloudflareApiOptions {
103+
export interface BaseWorkerProps<
104+
B extends Bindings | undefined = undefined,
105+
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
106+
> extends CloudflareApiOptions {
104107
/**
105108
* Bundle options when using entryPoint
106109
*
@@ -207,27 +210,38 @@ export interface BaseWorkerProps<B extends Bindings | undefined = undefined>
207210
* Can include queues, streams, or other event sources.
208211
*/
209212
eventSources?: EventSource[];
213+
214+
/**
215+
* The RPC class to use for the worker.
216+
*
217+
* This is only used when using the rpc property.
218+
*/
219+
rpc?: (new (...args: any[]) => RPC) | type<RPC>;
210220
}
211221

212-
export interface InlineWorkerProps<B extends Bindings | undefined = Bindings>
213-
extends BaseWorkerProps<B> {
222+
export interface InlineWorkerProps<
223+
B extends Bindings | undefined = Bindings,
224+
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
225+
> extends BaseWorkerProps<B, RPC> {
214226
script: string;
215227
entrypoint?: undefined;
216228
}
217229

218230
export interface EntrypointWorkerProps<
219231
B extends Bindings | undefined = Bindings,
220-
> extends BaseWorkerProps<B> {
232+
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
233+
> extends BaseWorkerProps<B, RPC> {
221234
entrypoint: string;
222235
script?: undefined;
223236
}
224237

225238
/**
226239
* Properties for creating or updating a Worker
227240
*/
228-
export type WorkerProps<B extends Bindings | undefined = Bindings> =
229-
| InlineWorkerProps<B>
230-
| EntrypointWorkerProps<B>;
241+
export type WorkerProps<
242+
B extends Bindings | undefined = Bindings,
243+
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
244+
> = InlineWorkerProps<B, RPC> | EntrypointWorkerProps<B, RPC>;
231245

232246
export type FetchWorkerProps<
233247
B extends Bindings = Bindings,
@@ -275,65 +289,70 @@ export function isWorker(resource: Resource): resource is Worker<any> {
275289
/**
276290
* Output returned after Worker creation/update
277291
*/
278-
export type Worker<B extends Bindings | undefined = Bindings | undefined> =
279-
Resource<"cloudflare::Worker"> &
280-
Omit<WorkerProps<B>, "url" | "script"> &
281-
globalThis.Service & {
282-
type: "service";
283-
284-
/**
285-
* The ID of the worker
286-
*/
287-
id: string;
288-
289-
/**
290-
* The name of the worker
291-
*/
292-
name: string;
293-
294-
/**
295-
* Time at which the worker was created
296-
*/
297-
createdAt: number;
298-
299-
/**
300-
* Time at which the worker was last updated
301-
*/
302-
updatedAt: number;
303-
304-
/**
305-
* The worker's URL if enabled
306-
* Format: {name}.{subdomain}.workers.dev
307-
*/
308-
url?: string;
309-
310-
/**
311-
* The bindings that were created
312-
*/
313-
bindings: B;
314-
315-
/**
316-
* Configuration for static assets
317-
*/
318-
assets?: AssetsConfig;
319-
320-
// phantom property (for typeof myWorker.Env)
321-
Env: B extends Bindings
322-
? {
323-
[bindingName in keyof B]: Bound<B[bindingName]>;
324-
}
325-
: undefined;
292+
export type Worker<
293+
B extends Bindings | undefined = Bindings | undefined,
294+
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
295+
> = Resource<"cloudflare::Worker"> &
296+
Omit<WorkerProps<B>, "url" | "script"> &
297+
globalThis.Service & {
298+
/** @internal phantom property */
299+
__rpc__?: RPC;
326300

327-
/**
328-
* The compatibility date for the worker
329-
*/
330-
compatibilityDate: string;
301+
type: "service";
331302

332-
/**
333-
* The compatibility flags for the worker
334-
*/
335-
compatibilityFlags: string[];
336-
};
303+
/**
304+
* The ID of the worker
305+
*/
306+
id: string;
307+
308+
/**
309+
* The name of the worker
310+
*/
311+
name: string;
312+
313+
/**
314+
* Time at which the worker was created
315+
*/
316+
createdAt: number;
317+
318+
/**
319+
* Time at which the worker was last updated
320+
*/
321+
updatedAt: number;
322+
323+
/**
324+
* The worker's URL if enabled
325+
* Format: {name}.{subdomain}.workers.dev
326+
*/
327+
url?: string;
328+
329+
/**
330+
* The bindings that were created
331+
*/
332+
bindings: B;
333+
334+
/**
335+
* Configuration for static assets
336+
*/
337+
assets?: AssetsConfig;
338+
339+
// phantom property (for typeof myWorker.Env)
340+
Env: B extends Bindings
341+
? {
342+
[bindingName in keyof B]: Bound<B[bindingName]>;
343+
}
344+
: undefined;
345+
346+
/**
347+
* The compatibility date for the worker
348+
*/
349+
compatibilityDate: string;
350+
351+
/**
352+
* The compatibility flags for the worker
353+
*/
354+
compatibilityFlags: string[];
355+
};
337356

338357
/**
339358
* A Cloudflare Worker is a serverless function that can be deployed to the Cloudflare network.
@@ -450,10 +469,10 @@ export type Worker<B extends Bindings | undefined = Bindings | undefined> =
450469
* @see
451470
* https://developers.cloudflare.com/workers/
452471
*/
453-
export function Worker<const B extends Bindings>(
454-
id: string,
455-
props: WorkerProps<B>,
456-
): Promise<Worker<B>>;
472+
export function Worker<
473+
const B extends Bindings,
474+
RPC extends Rpc.WorkerEntrypointBranded,
475+
>(id: string, props: WorkerProps<B, RPC>): Promise<Worker<B, RPC>>;
457476
export function Worker<const B extends Bindings, const E extends EventSource[]>(
458477
id: string,
459478
meta: ImportMeta,
@@ -466,6 +485,7 @@ export function Worker<const B extends Bindings>(
466485
): Promise<Worker<B>> {
467486
const [id, meta, props] =
468487
args.length === 2 ? [args[0], undefined, args[1]] : args;
488+
469489
if (("fetch" in props && props.fetch) || ("queue" in props && props.queue)) {
470490
const scope = Scope.current;
471491

alchemy/src/cloudflare/wrangler.json.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface WranglerJsonProps {
1717
/**
1818
* The worker to generate the wrangler.json file for
1919
*/
20-
worker: Worker;
20+
worker: Worker<any>;
2121
/**
2222
* Path to write the wrangler.json file to
2323
*
@@ -393,6 +393,10 @@ function processBindings(
393393
}
394394
// Process each binding
395395
for (const [bindingName, binding] of Object.entries(bindings)) {
396+
if (typeof binding === "function") {
397+
// this is only reachable in the
398+
throw new Error(`Invalid binding ${bindingName} is a function`);
399+
}
396400
if (typeof binding === "string") {
397401
// Plain text binding - add to vars
398402
if (!spec.vars) {

alchemy/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type * from "./scope.js";
55
export * from "./secret.js";
66
export * from "./serde.js";
77
export * from "./state.js";
8+
export * from "./type.js";
89
export * from "./util/ignore.js";
910

1011
import { alchemy } from "./alchemy.js";

alchemy/src/type.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export type type<T> = typeof type<T>;
2+
/**
3+
* Used to construct type-level alias information.
4+
*/
5+
export const type = ((): any => {
6+
throw new Error("Should never be called, purely for type-level aliasing");
7+
}) as (<T>() => T) &
8+
// we also want to make this a "class type" so that syntax highlighting is always as a type
9+
(new <T>() => T);

examples/cloudflare-worker/alchemy.run.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import {
77
Workflow,
88
WranglerJson,
99
} from "../../alchemy/src/cloudflare/index.js";
10-
import alchemy from "../../alchemy/src/index.js";
10+
import alchemy, { type } from "../../alchemy/src/index.js";
1111
import type { HelloWorldDO } from "./src/do.js";
12+
import type MyRPC from "./src/rpc.js";
1213

1314
const BRANCH_PREFIX = process.env.BRANCH_PREFIX ?? "";
1415
const app = await alchemy("cloudflare-worker", {
@@ -28,6 +29,11 @@ export const queue = await Queue<{
2829
adopt: true,
2930
});
3031

32+
export const rpc = await Worker(`cloudflare-worker-rpc${BRANCH_PREFIX}`, {
33+
entrypoint: "./src/rpc.ts",
34+
rpc: type<MyRPC>,
35+
});
36+
3137
export const worker = await Worker(`cloudflare-worker-worker${BRANCH_PREFIX}`, {
3238
entrypoint: "./src/worker.ts",
3339
bindings: {
@@ -44,6 +50,7 @@ export const worker = await Worker(`cloudflare-worker-worker${BRANCH_PREFIX}`, {
4450
className: "HelloWorldDO",
4551
sqlite: true,
4652
}),
53+
RPC: rpc,
4754
},
4855
url: true,
4956
eventSources: [queue],

examples/cloudflare-worker/src/rpc.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { WorkerEntrypoint } from "cloudflare:workers";
2+
3+
export default class MyRPC extends WorkerEntrypoint {
4+
/**
5+
* Hello world
6+
*/
7+
async hello(name: string) {
8+
return `Hello, ${name}!`;
9+
}
10+
async fetch() {
11+
return new Response("Hello from Worker B");
12+
}
13+
}

examples/cloudflare-worker/src/worker.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export default {
1818
}
1919
await obj.fetch(new Request("https://example.com"));
2020

21+
await env.RPC.hello("John Doe");
22+
2123
return new Response("Ok");
2224
},
2325
async queue(batch: typeof queue.Batch, _env: typeof worker.Env) {

0 commit comments

Comments
 (0)
0