From 42905ed83a91efc38e4e8e83ac52d55386ca34b7 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Fri, 31 Oct 2025 13:58:51 +0100 Subject: [PATCH] feat: start adding new batched proxies --- .../services/sdk/rpc/batching-proxies.spec.ts | 68 +++++++++++++ .../services/sdk/rpc/batching-proxies.ts | 98 +++++++++++++++++++ .../src/platform/services/sdk/rpc/protocol.ts | 11 +++ 3 files changed, 177 insertions(+) create mode 100644 libs/common/src/platform/services/sdk/rpc/batching-proxies.spec.ts create mode 100644 libs/common/src/platform/services/sdk/rpc/batching-proxies.ts diff --git a/libs/common/src/platform/services/sdk/rpc/batching-proxies.spec.ts b/libs/common/src/platform/services/sdk/rpc/batching-proxies.spec.ts new file mode 100644 index 00000000000..14a592a510f --- /dev/null +++ b/libs/common/src/platform/services/sdk/rpc/batching-proxies.spec.ts @@ -0,0 +1,68 @@ +import { Observable } from "rxjs"; + +import { ProxyInfo, RpcObjectReference } from "./batching-proxies"; +import { RpcRequestChannel } from "./client"; +import { Command, Response } from "./protocol"; + +describe("Batching proxies", () => { + let channel: RpcChannel; + beforeEach(() => { + channel = new RpcChannel(); + }); + + it("creates an object reference proxy", () => { + const reference = { referenceId: 1, objectType: "TestObject" }; + const proxy = RpcObjectReference(channel, reference); + expect(proxy[ProxyInfo]).toEqual({ + referenceId: 1, + objectType: "TestObject", + proxyType: "RpcObjectReference", + }); + }); + + // Not sure what await itself should do yet + // it("should allow awaiting the proxy itself", async () => { + // const reference = { + // referenceId: 2, + // objectType: "AwaitableObject", + // }; + // const proxy = RpcObjectReference(channel, reference); + // const awaited = await proxy; + // expect(awaited).toBe(proxy); + // }); + + it("returns a pending object reference proxy when accesing a property", async () => { + const reference = { referenceId: 1, objectType: "TestObject" }; + const proxy = RpcObjectReference(channel, reference) as any; + + const someProperty = proxy.someProperty; + + expect(someProperty[ProxyInfo].proxyType).toBe("RpcPendingObjectReference"); + expect(someProperty[ProxyInfo].commands).toEqual([ + { method: "get", propertyName: "someProperty" }, + ]); + }); +}); + +class RpcChannel implements RpcRequestChannel { + outgoing: Command[] = []; + waitingResponses: Array<(response: Response) => void> = []; + + sendCommand(command: Command): Promise { + return new Promise((resolve) => { + this.waitingResponses.push(resolve); + this.outgoing.push(command); + }); + } + + subscribeToRoot(): Observable { + throw new Error("Method not implemented."); + } + + respond(response: Response) { + const resolver = this.waitingResponses.shift(); + if (resolver) { + resolver(response); + } + } +} diff --git a/libs/common/src/platform/services/sdk/rpc/batching-proxies.ts b/libs/common/src/platform/services/sdk/rpc/batching-proxies.ts new file mode 100644 index 00000000000..ca39e040e34 --- /dev/null +++ b/libs/common/src/platform/services/sdk/rpc/batching-proxies.ts @@ -0,0 +1,98 @@ +import { RpcRequestChannel } from "./client"; +import { ReferenceId, PropertySymbol, BatchCommand, serializeSymbol } from "./protocol"; + +export type BatchingProxy = { + [ProxyInfo]: T & { + proxyType: "RpcObjectReference" | "RpcPendingObjectReference"; + }; +}; + +export const ProxyInfo = Symbol("ProxyInfo"); + +/** + * A reference to a remote object. + */ +export type RpcObjectReference = { + referenceId: ReferenceId; + objectType?: string; +}; + +export function RpcObjectReference(channel: RpcRequestChannel, reference: RpcObjectReference) { + const target = () => {}; + Object.defineProperty(target, "name", { value: `RpcObjectReference`, configurable: true }); + (target as any)[ProxyInfo] = { ...reference, proxyType: "RpcObjectReference" }; + + return new Proxy( + target, + proxyHandler(target, channel, reference, []), + ) as any as BatchingProxy; +} + +/** + * A pending reference to a remote object. + */ +export type RpcPendingObjectReference = { + reference: RpcObjectReference; + commands: BatchCommand[]; +}; + +export function RpcPendingObjectReference( + channel: RpcRequestChannel, + reference: RpcPendingObjectReference, +) { + const target = () => {}; + Object.defineProperty(target, "name", { + value: `RpcPendingObjectReference(${reference.reference.objectType}.${commandsToString(reference.commands)})`, + configurable: true, + }); + (target as any)[ProxyInfo] = { ...reference, proxyType: "RpcPendingObjectReference" }; + + return new Proxy( + target, + proxyHandler(target, channel, reference.reference, reference.commands), + ) as any as BatchingProxy; +} + +function proxyHandler( + target: any, + channel: RpcRequestChannel, + reference: RpcObjectReference, + commands: BatchCommand[], +): any { + return { + get(target: any, property: string | PropertySymbol) { + if ((property as any) === ProxyInfo) { + return (target as any)[ProxyInfo]; + } + + if (property === "then") { + // Allow awaiting the proxy itself + return undefined; + } + + return RpcPendingObjectReference(channel, { + reference, + commands: + typeof property === "string" + ? [...commands, { method: "get", propertyName: property }] + : [...commands, { method: "get", propertySymbol: serializeSymbol(property) }], + }); + }, + }; +} + +function commandsToString(commands: BatchCommand[]): string { + return commands + .map((cmd) => { + if (cmd.method === "get") { + const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol; + return `get(${String(prop)})`; + } else if (cmd.method === "call") { + const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol; + return `call(${String(prop)})`; + } + + return "???"; + }) + .join("."); +} diff --git a/libs/common/src/platform/services/sdk/rpc/protocol.ts b/libs/common/src/platform/services/sdk/rpc/protocol.ts index bdd6b6a3ebd..b61a619065f 100644 --- a/libs/common/src/platform/services/sdk/rpc/protocol.ts +++ b/libs/common/src/platform/services/sdk/rpc/protocol.ts @@ -3,6 +3,16 @@ export type ReferenceId = number; export type PropertySymbol = keyof typeof PropertySymbolMap; export type SerializedPropertySymbol = (typeof PropertySymbolMap)[keyof typeof PropertySymbolMap]; +export type BatchCommand = + | { method: "get"; propertyName: string } + | { method: "get"; propertySymbol: SerializedPropertySymbol } + | { method: "call"; propertyName: string; args: unknown[] } + | { + method: "call"; + propertySymbol: SerializedPropertySymbol; + args: unknown[]; + }; + export type Command = | { method: "get"; referenceId: ReferenceId; propertyName: string } | { method: "get"; referenceId: ReferenceId; propertySymbol: SerializedPropertySymbol } @@ -13,6 +23,7 @@ export type Command = propertySymbol: SerializedPropertySymbol; args: unknown[]; } + | { method: "batch"; referenceId: ReferenceId; commands: BatchCommand[] } | { method: "by_value"; referenceId: ReferenceId } | { method: "release"; referenceId: ReferenceId };