diff --git a/libs/common/src/platform/services/sdk/remote.ts b/libs/common/src/platform/services/sdk/remote.ts index bc7d25337aa..dea134110ef 100644 --- a/libs/common/src/platform/services/sdk/remote.ts +++ b/libs/common/src/platform/services/sdk/remote.ts @@ -16,6 +16,89 @@ export type RemoteValue = T extends { free(): void } ? Promise : Promise; -export type RemoteFunction any> = ( - ...args: Parameters +export type RemoteFunction any> = >( + ...args: SerializableArgs ) => RemoteValue>; + +// Serializable type rules to mirror `isSerializable` from rpc/server.ts +// - Primitives: string | number | boolean | null +// - Arrays: elements must be Serializable +// - Plain objects: all non-function properties must be Serializable +// - Everything else (functions, class instances, Date, Map, Set, etc.) is NOT serializable +type IsAny = 0 extends 1 & T ? true : false; +type IsNever = [T] extends [never] ? true : false; + +type IsFunction = T extends (...args: any[]) => any ? true : false; +type IsArray = T extends readonly any[] ? true : false; + +type IsSerializablePrimitive = [T] extends [string | number | boolean | null] ? true : false; + +type IsSerializableArray = T extends readonly (infer U)[] ? IsSerializable : false; + +type PropsAreSerializable = + Exclude< + { + [K in keyof T]-?: IsFunction extends true ? false : IsSerializable; + }[keyof T], + true + > extends never + ? true + : false; + +type IsSpecialObject = T extends + | Date + | RegExp + | Map + | Set + | WeakMap + | WeakSet + ? true + : false; + +type IsSerializableObject = + IsFunction extends true + ? false + : IsArray extends true + ? false + : IsSpecialObject extends true + ? false + : T extends object + ? PropsAreSerializable + : false; + +export type IsSerializable = + IsAny extends true + ? false // discourage any; use explicit types + : IsNever extends true + ? true + : IsSerializablePrimitive extends true + ? true + : IsArray extends true + ? IsSerializableArray + : IsSerializableObject; + +// Public helper alias for consumers +export type Serializable = IsSerializable extends true ? T : never; + +// Human-readable reason per kind +type NonSerializableReason = + IsFunction extends true + ? "functions are not serializable" + : IsArray extends true + ? "array contains non-serializable element(s)" + : IsSpecialObject extends true + ? "class instances / special objects (Date/Map/Set/RegExp/...) are not serializable" + : T extends object + ? "object contains non-serializable property" + : "type is not serializable"; + +// Tuple-literal error type so TS prints a helpful message at the callsite +type EnsureSerializableWithMessage = + IsSerializable extends true + ? T + : ["Non-serializable RPC argument", C, NonSerializableReason]; + +type IndexLabel = K extends string | number ? `${K}` : "?"; +type SerializableArgs = { + [K in keyof A]: EnsureSerializableWithMessage}]`>; +}; 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 e54ca0b5e60..ed32d0062d9 100644 --- a/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts +++ b/libs/common/src/platform/services/sdk/rpc/rpc.spec.ts @@ -2,6 +2,7 @@ import { firstValueFrom, map, Observable } from "rxjs"; import { RpcClient, RpcRequestChannel } from "./client"; import { Command, Response } from "./protocol"; +import { RpcObjectReference } from "./proxies"; import { RpcServer } from "./server"; describe("RpcServer", () => { @@ -45,8 +46,21 @@ describe("RpcServer", () => { const wasmObj = await remoteInstance.getWasmGreeting("Wasm World"); const greeting = await wasmObj.greet(); + expect(wasmObj).toBeInstanceOf(RpcObjectReference); expect(greeting).toBe("Hello, Wasm World!"); }); + + it("returns plain objects by value", async () => { + const remoteInstance = await firstValueFrom(client.getRoot()); + + const result = await remoteInstance.echo({ + message: "Hello, World!", + array: [1, "2", null], + }); + + expect(result).not.toBeInstanceOf(RpcObjectReference); + expect(result).toEqual({ message: "Hello, World!", array: [1, "2", null] }); + }); }); class TestClass { @@ -63,6 +77,10 @@ class TestClass { getWasmGreeting(name: string): WasmLikeObject { return new WasmLikeObject(name); } + + echo(obj: T): T { + return obj; + } } class WasmLikeObject {