8000 feat(cloudflare): support RPC type in WorkerStub (#425) · sam-goodwin/alchemy@0b682da · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Commit 0b682da

Browse files
authored
feat(cloudflare): support RPC type in WorkerStub (#425)
1 parent f35aa1c commit 0b682da

File tree

5 files changed

+214
-83
lines changed

5 files changed

+214
-83
lines changed

alchemy-web/docs/guides/cloudflare-worker.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,39 @@ const frontend = await Worker("frontend", {
175175
});
176176
```
177177

178+
## Circular Worker Bindings
179+
180+
When workers need to bind to each other (circular dependency), use `WorkerStub` to break the cycle:
181+
182+
```ts
183+
import { Worker, WorkerStub } from "alchemy/cloudflare";
184+
import type { MyWorkerA } from "./src/worker-a.ts";
185+
import type { MyWorkerB } from "./src/worker-B.ts";
186+
187+
// Create workerA that binds to workerB stub
188+
const workerA = await Worker("workerA", {
189+
name: "worker-a",
190+
entrypoint: "./src/workerA.ts",
191+
rpc: type<MyWorkerA>,
192+
bindings: {
193+
// bind to a stub (empty worker)
194+
WORKER_B: await WorkerStub<MyWorkerB>("workerB", {
195+
name: "worker-b",
196+
rpc: type<MyWorkerB>,
197+
});,
198+
},
199+
});
200+
201+
// Create workerB that binds to workerA
202+
const workerB = await Worker("workerB", {
203+
name: "worker-b",
204+
entrypoint: "./src/workerB.ts",
205+
bindings: {
206+
WORKER_A: workerA,
207+
},
208+
});
209+
```
210+
178211
## RPC
179212

180213
Create a Worker with RPC capabilities using WorkerEntrypoint and typed RPC interfaces:

alchemy-web/docs/providers/cloudflare/worker.md

Lines changed: 33 additions & 0 deletions
184
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,39 @@ const frontend = await Worker("frontend", {
174174
});
175175
```
176176

177+
## Circular Worker Bindings
178+
179+
When workers need to bind to each other (circular dependency), use `WorkerStub` to break the cycle:
180+
181+
```ts
182+
import { Worker, WorkerStub } from "alchemy/cloudflare";
183+
import type { MyWorkerA } from "./src/worker-a.ts";
+
import type { MyWorkerB } from "./src/worker-B.ts";
185+
186+
// Create workerA that binds to workerB stub
187+
const workerA = await Worker("workerA", {
188+
name: "worker-a",
189+
entrypoint: "./src/workerA.ts",
190+
rpc: type<MyWorkerA>,
191+
bindings: {
192+
// bind to a stub (empty worker)
193+
WORKER_B: await WorkerStub<MyWorkerB>("workerB", {
194+
name: "worker-b",
195+
rpc: type<MyWorkerB>,
196+
});,
197+
},
198+
});
199+
200+
// Create workerB that binds to workerA
201+
const workerB = await Worker("workerB", {
202+
name: "worker-b",
203+
entrypoint: "./src/workerB.ts",
204+
bindings: {
205+
WORKER_A: workerA,
206+
},
207+
});
208+
```
209+
177210
## RPC
178211

179212
Create a Worker with RPC capabilities using WorkerEntrypoint and typed RPC interfaces:

alchemy/src/cloudflare/bound.ts

Lines changed: 61 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,66 +18,73 @@ import type { SecretKey } from "./secret-key.ts";
1818
import type { Secret as CloudflareSecret } from "./secret.ts";
1919
import type { VectorizeIndexResource as _VectorizeIndex } from "./vectorize-index.ts";
2020
import type { VersionMetadata as _VersionMetadata } from "./version-metadata.ts";
21+
import type { WorkerStub } from "./worker-stub.ts";
2122
import type { Worker as _Worker, WorkerRef } from "./worker.ts";
2223
import type { Workflow as _Workflow } from "./workflow.ts";
2324

25+
type BoundWorker<
26+
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
27+
> = Service<RPC> & {
28+
// cloudflare's Rpc.Provider type loses mapping between properties (jump to definition)
29+
// we fix that using Pick to re-connect mappings
30+
[property in keyof Pick<
31+
RPC,
32+
Extract<keyof Rpc.Provider<RPC, "fetch" | "connect">, keyof RPC>
33+
>]: Rpc.Provider<RPC, "fetch" | "connect">[property];
34+
};
35+
2436
export type Bound<T extends Binding> = T extends _DurableObjectNamespace<
2537
infer O
2638
>
2739
? DurableObjectNamespace<O>
2840
: T extends { type: "kv_namespace" }
2941
? KVNamespace
30-
: T extends _Worker<any, infer RPC> | WorkerRef<infer RPC>
31-
? Service<RPC> & {
32-
// cloudflare's Rpc.Provider type loses mapping between properties (jump to definition)
33-
// we fix that using Pick to re-connect mappings
34-
[property in keyof Pick<
35-
RPC,
36-
Extract<keyof Rpc.Provider<RPC, "fetch" | "connect">, keyof RPC>
37-
>]: Rpc.Provider<RPC, "fetch" | "connect">[property];
38-
}
39-
: T extends { type: "service" }
40-
? Service
41-
: T extends _R2Bucket
42-
? R2Bucket
43-
: T extends _AiGateway
44-
? AiGateway
45-
: T extends _Hyperdrive
46-
? Hyperdrive
47-
: T extends Secret
48-
? string
49-
: T extends CloudflareSecret
50-
? SecretsStoreSecret
51-
: T extends SecretKey
52-
? CryptoKey
53-
: T extends Assets
54-
? Service
55-
: T extends _Workflow<infer P>
56-
? Workflow<P>
57-
: T extends D1DatabaseResource
58-
? D1Database
59-
: T extends DispatchNamespaceResource
60-
? { get(name: string): Fetcher }
61-
: T extends _VectorizeIndex
62-
? VectorizeIndex
63-
: T extends _Queue<infer Body>
64-
? Queue<Body>
65-
: T extends _AnalyticsEngineDataset
66-
? AnalyticsEngineDataset
67-
: T extends _Pipeline<infer R>
68-
? Pipeline<R>
69-
: T extends string
70-
? string
71-
: T extends BrowserRendering
72-
? Fetcher
73-
: T extends _Ai<infer M>
74-
? Ai<M>
75-
: T extends _Images
76-
? ImagesBinding
77-
: T extends _VersionMetadata
78-
? WorkerVersionMetadata
79-
: T extends Self
80-
? Service
81-
: T extends Json<infer T>
82-
? T
83-
: Service;
42+
: T extends WorkerStub<infer RPC>
43+
? BoundWorker<RPC>
44+
: T extends _Worker<any, infer RPC> | WorkerRef<infer RPC>
45+
? BoundWorker<RPC>
46+
: T extends { type: "service" }
47+
? Service
48+
: T extends _R2Bucket
49+
? R2Bucket
50+
: T extends _AiGateway
51+
? AiGateway
52+
: T extends _Hyperdrive
53+
? Hyperdrive
54+
: T extends Secret
55+
? string
56+
: T extends CloudflareSecret
57+
? SecretsStoreSecret
58+
: T extends SecretKey
59+
? CryptoKey
60+
: T extends Assets
61+
? Service
62+
: T extends _Workflow<infer P>
63+
? Workflow<P>
64+
: T extends D1DatabaseResource
65+
? D1Database
66+
: T extends DispatchNamespaceResource
67+
? { get(name: string): Fetcher }
68+
: T extends _VectorizeIndex
69+
? VectorizeIndex
70+
: T extends _Queue<infer Body>
71+
? Queue<Body>
72+
: T extends _AnalyticsEngineDataset
73+
? AnalyticsEngineDataset
74+
: T extends _Pipeline<infer R>
75+
? Pipeline<R>
76+
: T extends string
77+
? string
78+
: T extends BrowserRendering
79+
? Fetcher
80+
: T extends _Ai<infer M>
81+
? Ai<M>
82+
: T extends _Images
83+
? ImagesBinding
84+
: T extends _VersionMetadata
85+
? WorkerVersionMetadata
86+
: T extends Self
87+
? Service
88+
: T extends Json<infer T>
89+
? T
90+
: Service;

alchemy/src/cloudflare/worker-stub.ts

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Context } from "../context.ts";
22
import { Resource, ResourceKind } from "../resource.ts";
3+
import type { type } from "../type.ts";
34
import { handleApiError } from "./api-error.ts";
45
import {
56
createCloudflareApi,
@@ -10,22 +11,40 @@ import {
1011
/**
1112
* Properties for creating a Worker stub
1213
*/
13-
export interface WorkerStubProps extends CloudflareApiOptions {
14+
export interface WorkerStubProps<
15+
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
16+
> extends CloudflareApiOptions {
1417
/**
1518
* Name for the worker
1619
*/
1720
name: string;
21+
22+
/**
23+
* The RPC class to use for the worker.
24+
*
25+
* This is only used when using the rpc property.
26+
*/
27+
rpc?: (new (...args: any[]) => RPC) | type<RPC>;
1828
}
1929

2030
/**
2131
* Output returned after WorkerStub creation
2232
*/
23-
export interface WorkerStub extends Resource<"cloudflare::WorkerStub"> {
33+
export interface WorkerStub<
34+
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
35+
> extends Resource<"cloudflare::WorkerStub"> {
2436
type: "service";
2537
/**
2638
* The name of the worker
2739
*/
2840
name: string;
41+
42+
/**
43+
* Optional type branding for the worker's RPC entrypoint.
44+
*
45+
* @internal
46+
*/
47+
__rpc__?: RPC;
2948
}
3049

3150
export function isWorkerStub(resource: Resource): resource is WorkerStub {
@@ -47,33 +66,31 @@ export function isWorkerStub(resource: Resource): resource is WorkerStub {
4766
*
4867
* console.log(`Worker ${workerStub.name} exists: ${!workerStub.created}`);
4968
*/
50-
export const WorkerStub = Resource(
51-
"cloudflare::WorkerStub",
52-
async function (
53-
this: Context<WorkerStub>,
54-
_id: string,
55-
props: WorkerStubProps,
56-
): Promise<WorkerStub> {
57-
// Create Cloudflare API client with automatic account discovery
58-
const api = await createCloudflareApi(props);
59-
60-
if (this.phase === "delete") {
61-
// We don't actually delete the worker, just mark the resource as destroyed
62-
return this.destroy();
63-
}
64-
65-
// If worker doesn't exist and we're in create phase, create an empty one
66-
if (!(await exists(api, props.name)) && this.phase === "create") {
67-
await createEmptyWorker(api, props.name);
68-
}
69-
70-
// Return the worker stub info
71-
return this({
72-
type: "service",
73-
...props,
74-
});
75-
},
76-
);
69+
export const WorkerStub = Resource("cloudflare::WorkerStub", async function <
70+
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
71+
>(this: Context<WorkerStub>, _id: string, props: WorkerStubProps<RPC>): Promise<
72+
WorkerStub<RPC>
73+
> {
74+
// Create Cloudflare API client with automatic account discovery
75+
const api = await createCloudflareApi(props);
76+
77+
if (this.phase === "delete") {
78+
// We don't actually delete the worker, just mark the resource as destroyed
79+
return this.destroy();
80+
}
81+
82+
// If worker doesn't exist and we're in create phase, create an empty one
83+
if (!(await exists(api, props.name)) && this.phase === "create") {
84+
await createEmptyWorker(api, props.name);
85+
}
86+
87+
// Return the worker stub info
88+
return this({
89+
type: "service",
90+
__rpc__: props.rpc as unknown as RPC,
91+
...props,
92+
}) as WorkerStub<RPC>;
93+
});
7794

7895
async function exists(
7996
api: CloudflareApi,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { test } from "vitest";
2+
import { Worker, WorkerStub } from "../../src/cloudflare/index.ts";
3+
4+
test("worker-stub-types", () => {
5+
// type-only test
6+
async function _() {
7+
const { WorkerEntrypoint } = await import("cloudflare:workers");
8+
9+
class MyWorker extends WorkerEntrypoint {
10+
async foo() {
11+
return "foo";
12+
}
13+
}
14+
15+
const workerA = await Worker("workerA", {
16+
entrypoint: "index.ts",
17+
name: "workerA",
18+
bindings: {
19+
workerB: await WorkerStub<MyWorker>("workerB", {
20+
name: "workerB",
21+
}),
22+
},
23+
});
24+
25+
const _workerB = await Worker("workerB", {
26+
entrypoint: "index.ts",
27+
name: "workerB",
28+
bindings: {
29+
workerA,
30+
},
31+
});
32+
33+
const env = workerA.Env;
34+
35+
const _string: string = await env.workerB.foo();
36+
// @ts-expect-error
37+
const _number: number = await env.workerB.foo();
38+
// @ts-expect-error
39+
env.workerB.bar();
40+
}
41+
});

0 commit comments

Comments
 (0)
0