1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-16 08:34:39 +00:00

feat: fix serialization/deserialization quirks

This commit is contained in:
Andreas Coroiu
2025-10-27 10:08:56 +01:00
parent 2de3c690f3
commit bd8033cdd5
3 changed files with 91 additions and 9 deletions

View File

@@ -112,7 +112,11 @@ function RpcPropertyReference(channel: RpcRequestChannel, reference: RpcProperty
if (result.result.type === "value") {
return result.result.value;
} else if (result.result.type === "reference") {
return RpcObjectReference.create(channel, result.result.referenceId);
return RpcObjectReference.create(
channel,
result.result.referenceId,
result.result.objectType,
);
}
})().then(onFulfilled, onRejected);
};
@@ -136,7 +140,11 @@ function RpcPropertyReference(channel: RpcRequestChannel, reference: RpcProperty
if (result.result.type === "value") {
return result.result.value;
} else if (result.result.type === "reference") {
return RpcObjectReference.create(channel, result.result.referenceId);
return RpcObjectReference.create(
channel,
result.result.referenceId,
result.result.objectType,
);
}
});
},

View File

@@ -1,7 +1,7 @@
import { firstValueFrom, map, Observable } from "rxjs";
import { RpcClient, RpcRequestChannel } from "./client";
import { Response } from "./protocol";
import { Command, Response } from "./protocol";
import { RpcServer } from "./server";
describe("RpcServer", () => {
@@ -38,6 +38,15 @@ describe("RpcServer", () => {
expect(result).toBe("Hello, Async World!");
});
it("references Wasm-like object with pointer and free method", async () => {
const remoteInstance = await firstValueFrom(client.getRoot());
const wasmObj = await remoteInstance.getWasmGreeting("Wasm World");
const greeting = await wasmObj.greet();
expect(greeting).toBe("Hello, Wasm World!");
});
});
class TestClass {
@@ -50,13 +59,38 @@ class TestClass {
async greetAsync(name: string): Promise<string> {
return `Hello, ${name}!`;
}
getWasmGreeting(name: string): WasmLikeObject {
return new WasmLikeObject(name);
}
}
class WasmLikeObject {
ptr: number;
constructor(private name: string) {
this.ptr = 0; // Simulated pointer
}
free() {
// Simulated free method
}
async greet(): Promise<string> {
return `Hello, ${this.name}!`;
}
}
class InMemoryChannel implements RpcRequestChannel {
constructor(private server: RpcServer<TestClass>) {}
async sendCommand(command: any): Promise<any> {
return this.server.handle(command);
async sendCommand(command: Command): Promise<Response> {
// Simulate serialization/deserialization
command = JSON.parse(JSON.stringify(command));
let response = await this.server.handle(command);
// Simulate serialization/deserialization
response = JSON.parse(JSON.stringify(response));
return response;
}
subscribeToRoot(): Observable<Response> {

View File

@@ -15,7 +15,7 @@ export class RpcServer<T> {
constructor() {}
handle(command: Command): Response {
async handle(command: Command): Promise<Response> {
if (command.method === "get") {
const target = this.references.get<any>(command.referenceId);
if (!target) {
@@ -27,7 +27,7 @@ export class RpcServer<T> {
if (typeof propertyValue === "function") {
return { status: "error", error: `[RPC] Property ${command.propertyName} is a function` };
} else {
return { status: "success", result: { type: "value", value: propertyValue } };
return { status: "success", result: this.convertToReturnable(propertyValue) };
}
} catch (error) {
return { status: "error", error };
@@ -49,8 +49,8 @@ export class RpcServer<T> {
};
}
const result = method.apply(target, command.args);
return { status: "success", result: { type: "value", value: result } };
const result = await method.apply(target, command.args);
return { status: "success", result: this.convertToReturnable(result) };
} catch (error) {
return { status: "error", error };
}
@@ -62,4 +62,44 @@ export class RpcServer<T> {
setValue(value: T) {
this._value$.next(value);
}
private convertToReturnable(value: any): Result {
// Return a reference for objects with a 'free' method, otherwise return the value directly
// This causes objects in WASM memory to be referenced rather than serialized.
// TODO: Consider checking for 'ptr' instead
if (isSerializable(value)) {
return { type: "value", value };
}
const referenceId = this.references.store(value);
return { type: "reference", referenceId, objectType: value?.constructor?.name };
}
}
function isSerializable(value: any): boolean {
// Primitives are serializable
if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
return true;
}
// Arrays are serializable if all elements are
if (Array.isArray(value)) {
return value.every(isSerializable);
}
// Only plain objects (object literals) are serializable. Class instances should be returned by reference.
if (isPlainObject(value)) {
return Object.values(value).every(isSerializable);
}
// Everything else (functions, dates, maps, sets, class instances, etc.) should be referenced
return false;
}
function isPlainObject(value: any): boolean {
if (value === null || typeof value !== "object") {
return false;
}
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}