-
Notifications
You must be signed in to change notification settings - Fork 792
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: add support for JSRPC (#5916)
* feature: add support for JSRPC * Add messaging + example test to clarify `using` --------- Co-authored-by: bcoll <bcoll@cloudflare.com> Co-authored-by: Samuel Macleod <smacleod@cloudflare.com>
- Loading branch information
1 parent
1f3acc6
commit e42f320
Showing
30 changed files
with
1,104 additions
and
234 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@cloudflare/vitest-pool-workers": minor | ||
--- | ||
|
||
feature: add support for JSRPC |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# 🤯 rpc | ||
|
||
This Worker defines `WorkerEntrypoint` default and named exports. It also defines Durable Objects subclassing `DurableObject`. All of these classes define properties and methods that can be called over RPC. Integration tests dispatch events, access properties and call methods using the `SELF` helper from the `cloudflare:test` module. | ||
|
||
| Test | Overview | | ||
| --------------------------------------------------------- | -------------------------------------------- | | ||
| [integration-self.test.ts](test/integration-self.test.ts) | Integration test using `SELF` | | ||
| [unit.test.ts](test/unit.test.ts) | Unit tests calling exported classes directly | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
interface Env { | ||
KV_NAMESPACE: KVNamespace; | ||
TEST_OBJECT: DurableObjectNamespace<import("./index").TestObject>; | ||
TEST_NAMED_HANDLER: Service; | ||
TEST_NAMED_ENTRYPOINT: Service<import("./index").TestNamedEntrypoint>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { DurableObject, RpcTarget, WorkerEntrypoint } from "cloudflare:workers"; | ||
|
||
export class Counter extends RpcTarget { | ||
#value: number; | ||
|
||
constructor(value: number) { | ||
super(); | ||
this.#value = value; | ||
} | ||
|
||
get value() { | ||
return this.#value; | ||
} | ||
|
||
increment(by = 1) { | ||
return (this.#value += by); | ||
} | ||
|
||
clone() { | ||
return new Counter(this.#value); | ||
} | ||
|
||
asObject() { | ||
return { val: this.#value }; | ||
} | ||
} | ||
|
||
export class TestObject extends DurableObject<Env> { | ||
#value: number = 0; | ||
|
||
constructor(ctx: DurableObjectState, env: Env) { | ||
super(ctx, env); | ||
void ctx.blockConcurrencyWhile(async () => { | ||
this.#value = (await ctx.storage.get("count")) ?? 0; | ||
}); | ||
} | ||
|
||
get value() { | ||
return this.#value; | ||
} | ||
|
||
increment(by = 1) { | ||
this.#value += by; | ||
void this.ctx.storage.put("count", this.#value); | ||
return this.#value; | ||
} | ||
|
||
scheduleReset(afterMillis: number) { | ||
void this.ctx.storage.setAlarm(Date.now() + afterMillis); | ||
} | ||
|
||
async fetch(request: Request) { | ||
return Response.json({ | ||
source: "TestObject", | ||
method: request.method, | ||
url: request.url, | ||
ctxWaitUntil: typeof this.ctx.waitUntil, | ||
envKeys: Object.keys(this.env).sort(), | ||
}); | ||
} | ||
|
||
getCounter() { | ||
return new Counter(0); | ||
} | ||
|
||
getObject() { | ||
return { hello: "world" }; | ||
} | ||
|
||
alarm() { | ||
this.#value = 0; | ||
void this.ctx.storage.put("count", this.#value); | ||
} | ||
|
||
instanceProperty = "👻"; | ||
} | ||
|
||
export const testNamedHandler = <ExportedHandler<Env>>{ | ||
fetch(request, env, ctx) { | ||
return Response.json({ | ||
source: "testNamedHandler", | ||
method: request.method, | ||
url: request.url, | ||
ctxWaitUntil: typeof ctx.waitUntil, | ||
envKeys: Object.keys(env).sort(), | ||
}); | ||
}, | ||
}; | ||
|
||
export class TestNamedEntrypoint extends WorkerEntrypoint<Env> { | ||
fetch(request: Request) { | ||
return Response.json({ | ||
source: "TestNamedEntrypoint", | ||
method: request.method, | ||
url: request.url, | ||
ctxWaitUntil: typeof this.ctx.waitUntil, | ||
envKeys: Object.keys(this.env).sort(), | ||
}); | ||
} | ||
|
||
ping() { | ||
return "pong"; | ||
} | ||
|
||
getCounter() { | ||
return new Counter(0); | ||
} | ||
} | ||
|
||
export class TestSuperEntrypoint extends WorkerEntrypoint<Env> { | ||
superMethod() { | ||
return "🦸"; | ||
} | ||
} | ||
|
||
let lastController: ScheduledController | undefined; | ||
export default class TestDefaultEntrypoint extends TestSuperEntrypoint { | ||
async fetch(request: Request) { | ||
return Response.json({ | ||
source: "TestDefaultEntrypoint", | ||
method: request.method, | ||
url: request.url, | ||
ctxWaitUntil: typeof this.ctx.waitUntil, | ||
envKeys: Object.keys(this.env).sort(), | ||
}); | ||
} | ||
|
||
async scheduled(controller: ScheduledController) { | ||
lastController = controller; | ||
} | ||
|
||
get lastControllerCron() { | ||
return lastController?.cron; | ||
} | ||
|
||
sum(...args: number[]) { | ||
return args.reduce((acc, value) => acc + value, 0); | ||
} | ||
|
||
backgroundWrite(key: string, value: string) { | ||
this.ctx.waitUntil(this.env.KV_NAMESPACE.put(key, value)); | ||
} | ||
|
||
async read(key: string) { | ||
return this.env.KV_NAMESPACE.get(key); | ||
} | ||
|
||
createCounter(value = 0) { | ||
return new Counter(value); | ||
} | ||
|
||
instanceProperty = "👻"; | ||
instanceMethod = () => "👻"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"extends": "../../tsconfig.workerd.json", | ||
"include": ["./**/*.ts"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
declare module "cloudflare:test" { | ||
// Controls the type of `import("cloudflare:test").env` | ||
interface ProvidedEnv extends Env {} | ||
|
||
// Ensure RPC properties and methods can be accessed with `SELF` | ||
export const SELF: Service<import("../src/index").default>; | ||
} |
84 changes: 84 additions & 0 deletions
84
fixtures/vitest-pool-workers-examples/rpc/test/integration-self.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { env, SELF } from "cloudflare:test"; | ||
import { expect, it, vi } from "vitest"; | ||
|
||
it("dispatches fetch event", async () => { | ||
const response = await SELF.fetch("https://example.com/"); | ||
expect(await response.json()).toMatchInlineSnapshot(` | ||
{ | ||
"ctxWaitUntil": "function", | ||
"envKeys": [ | ||
"KV_NAMESPACE", | ||
"TEST_NAMED_ENTRYPOINT", | ||
"TEST_NAMED_HANDLER", | ||
"TEST_OBJECT", | ||
], | ||
"method": "GET", | ||
"source": "TestDefaultEntrypoint", | ||
"url": "https://example.com/", | ||
} | ||
`); | ||
}); | ||
|
||
it("dispatches scheduled event and accesses property with rpc", async () => { | ||
await SELF.scheduled({ cron: "* * * * 30" }); | ||
const lastControllerCron = await SELF.lastControllerCron; | ||
expect(lastControllerCron).toBe("* * * * 30"); | ||
}); | ||
|
||
it("calls multi-argument methods with rpc", async () => { | ||
const result = await SELF.sum(1, 2, 3); | ||
expect(result).toBe(6); | ||
}); | ||
|
||
it("calls methods using ctx and env with rpc", async () => { | ||
expect(await env.KV_NAMESPACE.get("key")).toBe(null); | ||
await SELF.backgroundWrite("key", "value"); | ||
await vi.waitUntil( | ||
async () => (await env.KV_NAMESPACE.get("key")) === "value" | ||
); | ||
}); | ||
|
||
it("calls async methods with rpc", async () => { | ||
await env.KV_NAMESPACE.put("key", "value"); | ||
expect(await SELF.read("key")).toBe("value"); | ||
}); | ||
|
||
it("calls methods with rpc and pipelining", async () => { | ||
const result = await SELF.createCounter(5).clone().increment(3); | ||
expect(result).toBe(8); | ||
}); | ||
|
||
it("can access methods from superclass", async () => { | ||
const result = await SELF.superMethod(); | ||
expect(result).toBe("🦸"); | ||
}); | ||
it("cannot access instance properties or methods", async () => { | ||
await expect(async () => await SELF.instanceProperty).rejects | ||
.toThrowErrorMatchingInlineSnapshot(` | ||
[TypeError: The RPC receiver's prototype does not implement "instanceProperty", but the receiver instance does. | ||
Only properties and methods defined on the prototype can be accessed over RPC. | ||
Ensure properties are declared like \`get instanceProperty() { ... }\` instead of \`instanceProperty = ...\`, | ||
and methods are declared like \`instanceProperty() { ... }\` instead of \`instanceProperty = () => { ... }\`.] | ||
`); | ||
await expect(async () => await SELF.instanceMethod()).rejects | ||
.toThrowErrorMatchingInlineSnapshot(` | ||
[TypeError: The RPC receiver's prototype does not implement "instanceMethod", but the receiver instance does. | ||
Only properties and methods defined on the prototype can be accessed over RPC. | ||
Ensure properties are declared like \`get instanceMethod() { ... }\` instead of \`instanceMethod = ...\`, | ||
and methods are declared like \`instanceMethod() { ... }\` instead of \`instanceMethod = () => { ... }\`.] | ||
`); | ||
}); | ||
it("cannot access non-existent properties or methods", async () => { | ||
await expect( | ||
// @ts-expect-error intentionally testing incorrect types | ||
async () => await SELF.nonExistentProperty | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`[TypeError: The RPC receiver does not implement "nonExistentProperty".]` | ||
); | ||
await expect( | ||
// @ts-expect-error intentionally testing incorrect types | ||
async () => await SELF.nonExistentMethod() | ||
).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`[TypeError: The RPC receiver does not implement "nonExistentMethod".]` | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"extends": "../../tsconfig.workerd-test.json", | ||
"include": ["./**/*.ts", "../src/env.d.ts"] | ||
} |
Oops, something went wrong.