From 1bd0ceee1e58947393cefd4dfbcbf89b20fab595 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 27 Oct 2025 16:11:47 +0100 Subject: [PATCH] feat: add support for symbols --- .../src/platform/services/sdk/rpc/protocol.ts | 40 ++++++ .../src/platform/services/sdk/rpc/proxies.ts | 135 +++++++++++------- .../src/platform/services/sdk/rpc/rpc.spec.ts | 30 +++- .../src/platform/services/sdk/rpc/server.ts | 14 +- 4 files changed, 156 insertions(+), 63 deletions(-) diff --git a/libs/common/src/platform/services/sdk/rpc/protocol.ts b/libs/common/src/platform/services/sdk/rpc/protocol.ts index 04fe7189161..399d9160bfa 100644 --- a/libs/common/src/platform/services/sdk/rpc/protocol.ts +++ b/libs/common/src/platform/services/sdk/rpc/protocol.ts @@ -1,8 +1,18 @@ export type ReferenceId = number; +export type PropertySymbol = keyof typeof PropertySymbolMap; +export type SerializedPropertySymbol = (typeof PropertySymbolMap)[keyof typeof PropertySymbolMap]; + export type Command = | { method: "get"; referenceId: ReferenceId; propertyName: string } + | { method: "get"; referenceId: ReferenceId; propertySymbol: SerializedPropertySymbol } | { method: "call"; referenceId: ReferenceId; propertyName: string; args: unknown[] } + | { + method: "call"; + referenceId: ReferenceId; + propertySymbol: SerializedPropertySymbol; + args: unknown[]; + } | { method: "release"; referenceId: ReferenceId }; export type Response = { status: "success"; result: Result } | { status: "error"; error: unknown }; @@ -10,3 +20,33 @@ export type Response = { status: "success"; result: Result } | { status: "error" export type Result = | { type: "value"; value: unknown } | { type: "reference"; referenceId: ReferenceId; objectType?: string }; +// | { type: "rc", referenceId: ReferenceId, objectType?: string }; + +// A list of supported property symbols and their wire names +const PropertySymbolMap = { + [Symbol.dispose]: "dispose", +} as const; + +// Build reverse lookup that includes symbol keys (Object.entries omits symbols) +const SymbolToString = new Map([ + [Symbol.dispose, PropertySymbolMap[Symbol.dispose]], +]); +const StringToSymbol = new Map([ + [PropertySymbolMap[Symbol.dispose], Symbol.dispose], +]); + +export function deserializeSymbol(name: SerializedPropertySymbol): symbol { + const sym = StringToSymbol.get(name as SerializedPropertySymbol); + if (!sym) { + throw new Error(`Unsupported property symbol: ${name}. This property cannot be used over RPC.`); + } + return sym; +} + +export function serializeSymbol(symbol: PropertySymbol): SerializedPropertySymbol { + const value = SymbolToString.get(symbol as unknown as symbol); + if (!value) { + throw new Error(`Unsupported serialized property symbol: ${String(symbol)}`); + } + return value; +} diff --git a/libs/common/src/platform/services/sdk/rpc/proxies.ts b/libs/common/src/platform/services/sdk/rpc/proxies.ts index 34dc01a3e20..a832b1eea93 100644 --- a/libs/common/src/platform/services/sdk/rpc/proxies.ts +++ b/libs/common/src/platform/services/sdk/rpc/proxies.ts @@ -1,5 +1,5 @@ import { RpcRequestChannel } from "./client"; -import { Command, ReferenceId } from "./protocol"; +import { Command, PropertySymbol, ReferenceId, Response, serializeSymbol } from "./protocol"; /** * A reference to a remote object. @@ -24,14 +24,14 @@ function ProxiedReference( reference: RpcObjectReference, ): RpcObjectReference { return new Proxy(reference, { - get(target, propertyName: string) { - if (propertyName === "then") { + get(target, property: string | PropertySymbol) { + if (property === "then") { // Allow awaiting the proxy itself return undefined; } - // console.log(`Accessing ${reference.objectType}.${propertyName}`); - return RpcPropertyReference(channel, { objectReference: target, propertyName }); + // console.log(`Accessing ${reference.objectType}.${String(propertyName)}`); + return RpcPropertyReference(channel, { objectReference: target, property }); }, }); } @@ -41,7 +41,7 @@ function ProxiedReference( */ type RpcPropertyReference = { objectReference: RpcObjectReference; - propertyName: string; + property: string | PropertySymbol; }; /** @@ -77,8 +77,8 @@ type RpcPropertyReference = { function RpcPropertyReference(channel: RpcRequestChannel, reference: RpcPropertyReference) { const target = () => {}; Object.defineProperty(target, "name", { value: `RpcPropertyReference`, configurable: true }); - target.objectReference = reference.objectReference; - target.propertyName = reference.propertyName; + (target as any).objectReference = reference.objectReference; + (target as any).property = reference.property; return new Proxy(target, { get(_target, propertyName: string) { @@ -86,67 +86,92 @@ function RpcPropertyReference(channel: RpcRequestChannel, reference: RpcProperty // `Accessing ${reference.objectReference.objectType}.${reference.propertyName}.${propertyName}`, // ); + // Allow Function.prototype.call to be used by TS helpers (e.g., disposables) if (propertyName === "call") { - return undefined; + return Function.prototype.call; } if (propertyName !== "then") { - throw new Error(`Cannot access property '${propertyName}' on remote proxy synchronously`); + // Support chained call like: (await obj.prop).method() AND obj.prop.method() + // by lazily resolving obj.prop first, then invoking method/property on the resolved reference. + return (...argArray: unknown[]) => { + return (async () => { + // First resolve the original referenced property value via GET + const getResult = await sendAndUnwrap(channel, buildGetCommand(reference)); + if (getResult.type !== "reference") { + throw new Error( + `Cannot access property '${propertyName}' on non-reference value returned by remote property`, + ); + } + + // Now perform the requested operation on the resolved reference + const callResult = await sendAndUnwrap( + channel, + buildCallCommand(getResult.referenceId, propertyName, argArray), + ); + return unwrapResult(channel, callResult); + })(); + }; } return (onFulfilled: (value: any) => void, onRejected: (error: any) => void) => { (async () => { - // Handle property access - const command: Command = { - method: "get", - referenceId: reference.objectReference.referenceId, - propertyName: reference.propertyName, - }; - // Send the command over the channel - const result = await channel.sendCommand(command); - - if (result.status === "error") { - throw new Error(`RPC Error: ${result.error}`); - } - - if (result.result.type === "value") { - return result.result.value; - } else if (result.result.type === "reference") { - return RpcObjectReference.create( - channel, - result.result.referenceId, - result.result.objectType, - ); - } + const result = await sendAndUnwrap(channel, buildGetCommand(reference)); + return unwrapResult(channel, result); })().then(onFulfilled, onRejected); }; }, apply(_target, _thisArg, argArray: unknown[]) { // console.log(`Calling ${reference.objectReference.objectType}.${reference.propertyName}`); - // Handle method call - const command: Command = { - method: "call", - referenceId: reference.objectReference.referenceId, - propertyName: reference.propertyName, - args: argArray, - }; - - return channel.sendCommand(command).then((result) => { - if (result.status === "error") { - throw new Error(`RPC Error: ${result.error}`); - } - - if (result.result.type === "value") { - return result.result.value; - } else if (result.result.type === "reference") { - return RpcObjectReference.create( - channel, - result.result.referenceId, - result.result.objectType, - ); - } - }); + const command = buildCallCommand( + reference.objectReference.referenceId, + reference.property, + argArray, + ); + return sendAndUnwrap(channel, command).then((result) => unwrapResult(channel, result)); }, }); } + +// Helpers +function buildGetCommand(reference: RpcPropertyReference): Command { + if (typeof reference.property === "string") { + return { + method: "get", + referenceId: reference.objectReference.referenceId, + propertyName: reference.property, + }; + } + return { + method: "get", + referenceId: reference.objectReference.referenceId, + propertySymbol: serializeSymbol(reference.property), + }; +} + +function buildCallCommand( + referenceId: ReferenceId, + property: string | PropertySymbol, + args: unknown[], +): Command { + if (typeof property === "string") { + return { method: "call", referenceId, propertyName: property, args }; + } + return { method: "call", referenceId, propertySymbol: serializeSymbol(property), args }; +} + +async function sendAndUnwrap(channel: RpcRequestChannel, command: Command) { + const response: Response = await channel.sendCommand(command); + if (response.status === "error") { + throw new Error(`RPC Error: ${response.error}`); + } + return response.result; +} + +function unwrapResult(channel: RpcRequestChannel, result: any) { + if (result.type === "value") { + return result.value; + } + return RpcObjectReference.create(channel, result.referenceId, result.objectType); +} diff --git a/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts b/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts index ed32d0062d9..9f422fbf09e 100644 --- a/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts +++ b/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts @@ -1,5 +1,7 @@ import { firstValueFrom, map, Observable } from "rxjs"; +import { Rc } from "../../../misc/reference-counting/rc"; + import { RpcClient, RpcRequestChannel } from "./client"; import { Command, Response } from "./protocol"; import { RpcObjectReference } from "./proxies"; @@ -61,6 +63,27 @@ describe("RpcServer", () => { expect(result).not.toBeInstanceOf(RpcObjectReference); expect(result).toEqual({ message: "Hello, World!", array: [1, "2", null] }); }); + + it("handles RC objects correctly", async () => { + const wasmObj = new WasmLikeObject("RC"); + const rcValue = new Rc(wasmObj); + const server = new RpcServer>(); + const client = new RpcClient>(new InMemoryChannel(server)); + server.setValue(rcValue); + + const remoteRc = await firstValueFrom(client.getRoot()); + + { + using ref = await remoteRc.take(); + const remoteWasmObj = ref.value; + const greeting = await remoteWasmObj.greet(); + expect(greeting).toBe("Hello, RC!"); + expect(wasmObj.freed).toBe(false); + } + + await remoteRc.markForDisposal(); + expect(wasmObj.freed).toBe(true); + }); }); class TestClass { @@ -85,13 +108,14 @@ class TestClass { class WasmLikeObject { ptr: number; + freed = false; constructor(private name: string) { this.ptr = 0; // Simulated pointer } free() { - // Simulated free method + this.freed = true; } async greet(): Promise { @@ -99,8 +123,8 @@ class WasmLikeObject { } } -class InMemoryChannel implements RpcRequestChannel { - constructor(private server: RpcServer) {} +class InMemoryChannel implements RpcRequestChannel { + constructor(private server: RpcServer) {} async sendCommand(command: Command): Promise { // Simulate serialization/deserialization diff --git a/libs/common/src/platform/services/sdk/rpc/server.ts b/libs/common/src/platform/services/sdk/rpc/server.ts index 22579c183d8..ea97142aba4 100644 --- a/libs/common/src/platform/services/sdk/rpc/server.ts +++ b/libs/common/src/platform/services/sdk/rpc/server.ts @@ -1,6 +1,6 @@ import { map, Observable, ReplaySubject } from "rxjs"; -import { Command, Response, Result } from "./protocol"; +import { Command, deserializeSymbol, Response, Result } from "./protocol"; import { ReferenceStore } from "./reference-store"; export class RpcServer { @@ -23,9 +23,11 @@ export class RpcServer { } try { - const propertyValue = target[command.propertyName]; + const propertyKey = + (command as any).propertyName ?? deserializeSymbol((command as any).propertySymbol); + const propertyValue = target[propertyKey]; if (typeof propertyValue === "function") { - return { status: "error", error: `[RPC] Property ${command.propertyName} is a function` }; + return { status: "error", error: `[RPC] Property ${String(propertyKey)} is a function` }; } else { return { status: "success", result: this.convertToReturnable(propertyValue) }; } @@ -41,11 +43,13 @@ export class RpcServer { } try { - const method = target[command.propertyName]; + const propertyKey = + (command as any).propertyName ?? deserializeSymbol((command as any).propertySymbol); + const method = target[propertyKey]; if (typeof method !== "function") { return { status: "error", - error: `[RPC] Property ${command.propertyName} is not a function`, + error: `[RPC] Property ${String(propertyKey)} is not a function`, }; }