mirror of
https://github.com/bitwarden/browser
synced 2026-01-31 00:33:33 +00:00
feat: add support for symbols
This commit is contained in:
@@ -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, SerializedPropertySymbol>([
|
||||
[Symbol.dispose, PropertySymbolMap[Symbol.dispose]],
|
||||
]);
|
||||
const StringToSymbol = new Map<SerializedPropertySymbol, symbol>([
|
||||
[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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<Rc<WasmLikeObject>>();
|
||||
const client = new RpcClient<Rc<WasmLikeObject>>(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<string> {
|
||||
@@ -99,8 +123,8 @@ class WasmLikeObject {
|
||||
}
|
||||
}
|
||||
|
||||
class InMemoryChannel implements RpcRequestChannel {
|
||||
constructor(private server: RpcServer<TestClass>) {}
|
||||
class InMemoryChannel<T> implements RpcRequestChannel {
|
||||
constructor(private server: RpcServer<T>) {}
|
||||
|
||||
async sendCommand(command: Command): Promise<Response> {
|
||||
// Simulate serialization/deserialization
|
||||
|
||||
@@ -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<T> {
|
||||
@@ -23,9 +23,11 @@ export class RpcServer<T> {
|
||||
}
|
||||
|
||||
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<T> {
|
||||
}
|
||||
|
||||
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`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user