diff --git a/.changeset/polite-dodos-happen.md b/.changeset/polite-dodos-happen.md new file mode 100644 index 000000000000..1f9bfef4bc31 --- /dev/null +++ b/.changeset/polite-dodos-happen.md @@ -0,0 +1,9 @@ +--- +"miniflare": patch +--- + +fix: Allow the magic proxy to proxy objects containing functions indexed by symbols + +In https://github.com/cloudflare/workers-sdk/pull/5670 we introduced the possibility +of the magic proxy to handle object containing functions, the implementation didn't +account for functions being indexed by symbols, address such issue diff --git a/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts b/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts index 3d58b938f955..4bbfabdd5158 100644 --- a/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts +++ b/fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts @@ -188,9 +188,15 @@ describe("getPlatformProxy - env", () => { rpc = env.MY_RPC as unknown as EntrypointService; return dispose; }); - it("can call RPC methods directly", async () => { + it("can call RPC methods returning a string", async () => { expect(await rpc.sum([1, 2, 3])).toMatchInlineSnapshot(`6`); }); + it("can call RPC methods returning an object", async () => { + expect(await rpc.sumObj([1, 2, 3, 5])).toEqual({ + isObject: true, + value: 11, + }); + }); it("can call RPC methods returning a Response", async () => { const resp = await rpc.asJsonResponse([1, 2, 3]); expect(resp.status).toMatchInlineSnapshot(`200`); diff --git a/fixtures/get-platform-proxy/workers/rpc-worker/index.ts b/fixtures/get-platform-proxy/workers/rpc-worker/index.ts index 540f908c63ac..0d284d9fed94 100644 --- a/fixtures/get-platform-proxy/workers/rpc-worker/index.ts +++ b/fixtures/get-platform-proxy/workers/rpc-worker/index.ts @@ -12,6 +12,14 @@ export class NamedEntrypoint extends WorkerEntrypoint { sum(args: number[]): number { return args.reduce((a, b) => a + b); } + + sumObj(args: number[]): { isObject: true; value: number } { + return { + isObject: true, + value: args.reduce((a, b) => a + b), + }; + } + asJsonResponse(args: unknown): { status: number; text: () => Promise; diff --git a/packages/miniflare/src/workers/core/proxy.worker.ts b/packages/miniflare/src/workers/core/proxy.worker.ts index ae400da5ebee..99c3e2db1cb1 100644 --- a/packages/miniflare/src/workers/core/proxy.worker.ts +++ b/packages/miniflare/src/workers/core/proxy.worker.ts @@ -68,8 +68,15 @@ function isPlainObject(value: unknown) { Object.getOwnPropertyNames(proto).sort().join("\0") === objectProtoNames ); } -function objectContainsFunctions(obj: Record): boolean { - for (const [, entry] of Object.entries(obj)) { +function objectContainsFunctions( + obj: Record +): boolean { + const propertyNames = Object.getOwnPropertyNames(obj); + const propertySymbols = Object.getOwnPropertySymbols(obj); + const properties = [...propertyNames, ...propertySymbols]; + + for (const property of properties) { + const entry = obj[property]; if (typeof entry === "function") { return true; } diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index 302c55cff131..83543452e610 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -821,6 +821,104 @@ test("Miniflare: service binding to named entrypoint", async (t) => { }); }); +test("Miniflare: service binding to named entrypoint that implements a method returning a plain object", async (t) => { + const mf = new Miniflare({ + workers: [ + { + name: "a", + serviceBindings: { + RPC_SERVICE: { name: "b", entrypoint: "RpcEntrypoint" }, + }, + compatibilityFlags: ["rpc"], + modules: true, + script: ` + export default { + async fetch(request, env) { + const obj = await env.RPC_SERVICE.getObject(); + return Response.json({ obj }); + } + } + `, + }, + { + name: "b", + modules: true, + script: ` + import { WorkerEntrypoint } from "cloudflare:workers"; + export class RpcEntrypoint extends WorkerEntrypoint { + getObject() { + return { + isPlainObject: true, + value: 123, + } + } + } + `, + }, + ], + }); + t.teardown(() => mf.dispose()); + + const bindings = await mf.getBindings<{ RPC_SERVICE: any }>(); + const o = await bindings.RPC_SERVICE.getObject(); + t.deepEqual(o.isPlainObject, true); + t.deepEqual(o.value, 123); +}); + +test("Miniflare: service binding to named entrypoint that implements a method returning an RpcTarget instance", async (t) => { + const mf = new Miniflare({ + workers: [ + { + name: "a", + serviceBindings: { + RPC_SERVICE: { name: "b", entrypoint: "RpcEntrypoint" }, + }, + compatibilityFlags: ["rpc"], + modules: true, + script: ` + export default { + async fetch(request, env) { + const rpcTarget = await env.RPC_SERVICE.getRpcTarget(); + return Response.json(rpcTarget.id); + } + } + `, + }, + { + name: "b", + modules: true, + script: ` + import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers"; + + export class RpcEntrypoint extends WorkerEntrypoint { + getRpcTarget() { + return new SubService("test-id"); + } + } + + class SubService extends RpcTarget { + #id + + constructor(id) { + super() + this.#id = id + } + + get id() { + return this.#id + } + } + `, + }, + ], + }); + t.teardown(() => mf.dispose()); + + const bindings = await mf.getBindings<{ RPC_SERVICE: any }>(); + const rpcTarget = await bindings.RPC_SERVICE.getRpcTarget(); + t.deepEqual(rpcTarget.id, "test-id"); +}); + test("Miniflare: custom outbound service", async (t) => { const mf = new Miniflare({ workers: [