mirror of
https://github.com/bitwarden/browser
synced 2026-02-02 09:43:29 +00:00
feat: replace everything with new proxies
This commit is contained in:
@@ -35,12 +35,12 @@ export class BrowserRemoteSdkService implements RemoteSdkService {
|
||||
);
|
||||
},
|
||||
sendCommand: async (command) => {
|
||||
this.logService.debug("[RemoteSdkService]: Sending command", command);
|
||||
const response = (await BrowserApi.sendMessageWithResponse("sdk.request", {
|
||||
const jsonResponse = (await BrowserApi.sendMessageWithResponse("sdk.request", {
|
||||
type: "RemoteSdkRequest",
|
||||
command,
|
||||
} satisfies RemoteSdkRequest)) as Response;
|
||||
this.logService.debug("[RemoteSdkService]: Received response", response);
|
||||
command: JSON.stringify(command),
|
||||
} satisfies RemoteSdkRequest)) as string;
|
||||
this.logService.debug("[RemoteSdkService]: Request:", command, "Response:", jsonResponse);
|
||||
const response = JSON.parse(jsonResponse) as Response;
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Command, Response, Result } from "@bitwarden/common/platform/services/sdk/rpc/protocol";
|
||||
import { Result } from "@bitwarden/common/platform/services/sdk/rpc/protocol";
|
||||
|
||||
export type RemoteSdkRequest = {
|
||||
type: "RemoteSdkRequest";
|
||||
command: Command;
|
||||
command: string;
|
||||
};
|
||||
|
||||
export type RemoteSdkResponse = {
|
||||
type: "RemoteSdkResponse";
|
||||
response: Response;
|
||||
response: string;
|
||||
};
|
||||
|
||||
export type RemoteSdkResendRootRequest = {
|
||||
@@ -21,37 +21,22 @@ export type RemoteSdkRoot = {
|
||||
|
||||
export function isRemoteSdkRequest(message: unknown): message is RemoteSdkRequest {
|
||||
return (
|
||||
typeof message === "object" &&
|
||||
message !== null &&
|
||||
(message as any).type === "RemoteSdkRequest" &&
|
||||
typeof (message as any).command === "object"
|
||||
typeof message === "object" && message !== null && (message as any).type === "RemoteSdkRequest"
|
||||
);
|
||||
}
|
||||
|
||||
export function isRemoteSdkResponse(message: unknown): message is RemoteSdkResponse {
|
||||
return (
|
||||
typeof message === "object" &&
|
||||
message !== null &&
|
||||
(message as any).type === "RemoteSdkResponse" &&
|
||||
typeof (message as any).response === "object"
|
||||
typeof message === "object" && message !== null && (message as any).type === "RemoteSdkResponse"
|
||||
);
|
||||
}
|
||||
|
||||
export function isRemoteSdkResendRootRequest(
|
||||
message: unknown,
|
||||
): message is RemoteSdkResendRootRequest {
|
||||
return (
|
||||
typeof message === "object" &&
|
||||
message !== null &&
|
||||
(message as any).type === "RemoteSdkResendRootRequest"
|
||||
);
|
||||
return message !== null && (message as any).type === "RemoteSdkResendRootRequest";
|
||||
}
|
||||
|
||||
export function isRemoteSdkRoot(message: unknown): message is RemoteSdkRoot {
|
||||
return (
|
||||
typeof message === "object" &&
|
||||
message !== null &&
|
||||
(message as any).type === "RemoteSdkRoot" &&
|
||||
typeof (message as any).result === "object"
|
||||
);
|
||||
return message !== null && (message as any).type === "RemoteSdkRoot";
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ export class RemoteSdkServerService {
|
||||
"sdk.request",
|
||||
(message: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
|
||||
if (isRemoteSdkRequest(message)) {
|
||||
void this.server.handle(message.command).then((response) => {
|
||||
sendResponse(response);
|
||||
void this.server.handle(JSON.parse(message.command)).then((response) => {
|
||||
sendResponse(JSON.stringify(response));
|
||||
});
|
||||
return true; // Indicate that we will send a response asynchronously
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ export const DefaultFeatureFlagValue = {
|
||||
|
||||
/* Vault */
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: true,
|
||||
[FeatureFlag.PM22134SdkCipherListView]: true,
|
||||
[FeatureFlag.PM22136_SdkCipherEncryption]: true,
|
||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||
[FeatureFlag.PM22134SdkCipherListView]: FALSE,
|
||||
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM22110_DisableAlternateLoginMethods]: FALSE,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { Rc } from "../../misc/reference-counting/rc";
|
||||
|
||||
import { Remote } from "./remote";
|
||||
import { Remote } from "./rpc/remote";
|
||||
|
||||
export abstract class RemoteSdkService {
|
||||
abstract remoteClient$: Observable<Remote<Rc<BitwardenClient> | null>>;
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { ChainablePromise } from "./chainable-promise";
|
||||
|
||||
export type Remote<T> = {
|
||||
[K in keyof T as K extends typeof Symbol.dispose ? never : K]: RemoteProperty<T[K]>;
|
||||
} & (typeof Symbol.dispose extends keyof T ? { [Symbol.asyncDispose](): Promise<void> } : object);
|
||||
|
||||
type Resolved<T> = T extends Promise<infer U> ? U : T;
|
||||
// type HasFree<T> = T extends { free(): void } ? true : false;
|
||||
|
||||
/**
|
||||
* Maps remote object fields to RPC-exposed types.
|
||||
*
|
||||
* Property access (non-function):
|
||||
* - If the value is serializable (see IsSerializable), returns Promise<Resolved<T>>.
|
||||
* - If not serializable (e.g., class instance, Wasm object), returns Remote<Resolved<T>> (a live reference).
|
||||
* Note: properties do NOT expose `.await`; they are direct remote references.
|
||||
*
|
||||
* Function call:
|
||||
* - If the return value is serializable, returns Promise<Resolved<R>>.
|
||||
* - If not serializable, returns ChainablePromise<Remote<Resolved<R>>> so callers can use `.await`
|
||||
* for ergonomic chaining, e.g. remote.vault().await.totp().await.generate(...).
|
||||
*/
|
||||
export type RemoteProperty<T> = T extends (...args: any[]) => any
|
||||
? RemoteFunction<T>
|
||||
: RemoteReference<Resolved<T>>;
|
||||
// : HasFree<Resolved<T>> extends true
|
||||
// ? RemoteReference<Resolved<T>>
|
||||
// : Promise<Resolved<T>>;
|
||||
|
||||
export type Transfer<T> = {
|
||||
/**
|
||||
* Force a by-value snapshot transfer of this remote reference. Resolves to a serializable value.
|
||||
* If the object is not serializable at runtime, this will throw.
|
||||
*/
|
||||
transfer: Promise<T>;
|
||||
};
|
||||
|
||||
export type RemoteReference<T> = Remote<T> &
|
||||
Transfer<T> & {
|
||||
/**
|
||||
* Force a by-value snapshot transfer of this remote reference. Resolves to a serializable value.
|
||||
* If the object is not serializable at runtime, this will throw.
|
||||
*
|
||||
* OLD: Remove
|
||||
*/
|
||||
by_value(): Promise<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* RemoteFunction arguments must be Serializable at compile time. For non-serializable
|
||||
* return types, we expose ChainablePromise<Remote<...>> to enable Rust-like `.await` chaining.
|
||||
*/
|
||||
export type RemoteFunction<T extends (...args: any[]) => any> = (
|
||||
...args: Parameters<T>
|
||||
) => ChainablePromise<RemoteReference<Resolved<ReturnType<T>>>> & Transfer<Resolved<ReturnType<T>>>;
|
||||
// ) => Resolved<ReturnType<T>> extends object
|
||||
// ? ChainablePromise<RemoteReference<Resolved<ReturnType<T>>>>
|
||||
// : Promise<Resolved<ReturnType<T>>>;
|
||||
|
||||
// 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
|
||||
// Serializability checks removed: transport and server decide value vs reference.
|
||||
@@ -1,168 +0,0 @@
|
||||
import { RpcRequestChannel } from "./client";
|
||||
import { ReferenceId, PropertySymbol, BatchCommand, serializeSymbol } from "./protocol";
|
||||
|
||||
export type BatchingProxy<T> = {
|
||||
[ProxyInfo]: T & {
|
||||
proxyType: "RpcObjectReference" | "RpcPendingObjectReference";
|
||||
};
|
||||
};
|
||||
|
||||
export const ProxyInfo = Symbol("ProxyInfo");
|
||||
|
||||
export function isProxy(obj: any): obj is BatchingProxy<any> {
|
||||
return obj && typeof obj === "function" && obj[ProxyInfo] !== undefined;
|
||||
}
|
||||
|
||||
export function isReferenceProxy(obj: any): obj is BatchingProxy<RpcObjectReference> {
|
||||
return isProxy(obj) && obj[ProxyInfo].proxyType === "RpcObjectReference";
|
||||
}
|
||||
|
||||
export function isPendingReferenceProxy(obj: any): obj is BatchingProxy<RpcPendingObjectReference> {
|
||||
return isProxy(obj) && obj[ProxyInfo].proxyType === "RpcPendingObjectReference";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<RpcObjectReference>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<RpcPendingObjectReference>;
|
||||
}
|
||||
|
||||
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" && commands.length === 0) {
|
||||
// This means we awaited a RpcObjectReference which resolves to itself
|
||||
// We don't support transfering references to Promises themselves, we'll
|
||||
// automatically await them before returning
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (property === "then" && commands.length > 0) {
|
||||
return BatchCommandExecutor(channel, reference.referenceId, commands);
|
||||
}
|
||||
|
||||
if (property === "await") {
|
||||
return RpcPendingObjectReference(channel, {
|
||||
reference,
|
||||
commands: [...commands, { method: "await" }],
|
||||
});
|
||||
}
|
||||
|
||||
if (property === "transfer") {
|
||||
return RpcPendingObjectReference(channel, {
|
||||
reference,
|
||||
commands: [...commands, { method: "transfer" }],
|
||||
});
|
||||
}
|
||||
|
||||
return RpcPendingObjectReference(channel, {
|
||||
reference,
|
||||
commands:
|
||||
typeof property === "string"
|
||||
? [...commands, { method: "get", propertyName: property }]
|
||||
: [...commands, { method: "get", propertySymbol: serializeSymbol(property) }],
|
||||
});
|
||||
},
|
||||
|
||||
apply(_target: any, _thisArg: any, argArray?: any): any {
|
||||
return RpcPendingObjectReference(channel, {
|
||||
reference,
|
||||
commands: [...commands, { method: "apply", args: argArray }],
|
||||
});
|
||||
},
|
||||
} satisfies ProxyHandler<any>;
|
||||
}
|
||||
|
||||
function BatchCommandExecutor(
|
||||
channel: RpcRequestChannel,
|
||||
referenceId: ReferenceId,
|
||||
commands: BatchCommand[],
|
||||
): (onFulfilled: (value: any) => void, onRejected: (reason: any) => void) => void {
|
||||
const command = {
|
||||
method: "batch",
|
||||
referenceId,
|
||||
commands: commands.filter((cmd) => cmd.method !== "await"),
|
||||
} as const;
|
||||
|
||||
return (onFulfilled, onRejected) => {
|
||||
(async () => {
|
||||
const result = await channel.sendCommand(command);
|
||||
|
||||
if (result.status === "error") {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (result.result.type === "value") {
|
||||
return result.result.value;
|
||||
}
|
||||
|
||||
return RpcObjectReference(channel, {
|
||||
referenceId: result.result.referenceId,
|
||||
objectType: result.result.objectType,
|
||||
});
|
||||
})().then(onFulfilled, onRejected);
|
||||
};
|
||||
}
|
||||
|
||||
function commandsToString(commands: BatchCommand[]): string {
|
||||
return commands
|
||||
.map((cmd) => {
|
||||
if (cmd.method === "get") {
|
||||
const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol;
|
||||
return `${String(prop)}`;
|
||||
} else if (cmd.method === "apply") {
|
||||
const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol;
|
||||
return `${String(prop)}()`;
|
||||
}
|
||||
|
||||
return "???";
|
||||
})
|
||||
.join(".");
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { Remote } from "../remote";
|
||||
|
||||
import { RpcObjectReference } from "./batch-proxies";
|
||||
import { Command, Response } from "./protocol";
|
||||
import { RpcObjectReference } from "./proxies";
|
||||
import { Remote } from "./remote";
|
||||
|
||||
export interface RpcRequestChannel {
|
||||
sendCommand(command: Command): Promise<Response>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { executeBatchCommands } from "./batch-executor";
|
||||
import { executeBatchCommands } from "./executor";
|
||||
import { RpcError } from "./error";
|
||||
import { BatchCommand, serializeSymbol } from "./protocol";
|
||||
import { ReferenceStore } from "./reference-store";
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { ProxyInfo, RpcObjectReference } from "./batch-proxies";
|
||||
import { ProxyInfo, RpcObjectReference } from "./proxies";
|
||||
import { RpcRequestChannel } from "./client";
|
||||
import { Command, Response } from "./protocol";
|
||||
|
||||
@@ -1,215 +1,173 @@
|
||||
import { chain } from "../chainable-promise";
|
||||
|
||||
import { RpcRequestChannel } from "./client";
|
||||
import { Command, PropertySymbol, ReferenceId, Response, serializeSymbol } from "./protocol";
|
||||
import { RpcError } from "./error";
|
||||
import { ReferenceId, PropertySymbol, BatchCommand, serializeSymbol } from "./protocol";
|
||||
|
||||
export type BatchingProxy<T> = {
|
||||
[ProxyInfo]: T & {
|
||||
proxyType: "RpcObjectReference" | "RpcPendingObjectReference";
|
||||
};
|
||||
};
|
||||
|
||||
export const ProxyInfo = Symbol("ProxyInfo");
|
||||
|
||||
export function isProxy(obj: any): obj is BatchingProxy<any> {
|
||||
return obj && typeof obj === "function" && obj[ProxyInfo] !== undefined;
|
||||
}
|
||||
|
||||
export function isReferenceProxy(obj: any): obj is BatchingProxy<RpcObjectReference> {
|
||||
return isProxy(obj) && obj[ProxyInfo].proxyType === "RpcObjectReference";
|
||||
}
|
||||
|
||||
export function isPendingReferenceProxy(obj: any): obj is BatchingProxy<RpcPendingObjectReference> {
|
||||
return isProxy(obj) && obj[ProxyInfo].proxyType === "RpcPendingObjectReference";
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to a remote object.
|
||||
*/
|
||||
export class RpcObjectReference {
|
||||
static create(
|
||||
channel: RpcRequestChannel,
|
||||
referenceId: ReferenceId,
|
||||
objectType?: string,
|
||||
): RpcObjectReference & { by_value(): Promise<any> } {
|
||||
return ProxiedReference(channel, new RpcObjectReference(referenceId, objectType)) as any;
|
||||
}
|
||||
export type RpcObjectReference = {
|
||||
referenceId: ReferenceId;
|
||||
objectType?: string;
|
||||
};
|
||||
|
||||
private constructor(
|
||||
public referenceId: ReferenceId,
|
||||
public 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<RpcObjectReference>;
|
||||
}
|
||||
|
||||
function ProxiedReference(
|
||||
/**
|
||||
* 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<RpcPendingObjectReference>;
|
||||
}
|
||||
|
||||
function proxyHandler(
|
||||
target: any,
|
||||
channel: RpcRequestChannel,
|
||||
reference: RpcObjectReference,
|
||||
): RpcObjectReference & { by_value(): Promise<any> } {
|
||||
return new Proxy(reference as any, {
|
||||
get(target, property: string | PropertySymbol) {
|
||||
if (property === "then") {
|
||||
// Allow awaiting the proxy itself
|
||||
commands: BatchCommand[],
|
||||
): any {
|
||||
return {
|
||||
get(target: any, property: string | PropertySymbol) {
|
||||
if ((property as any) === ProxyInfo) {
|
||||
return (target as any)[ProxyInfo];
|
||||
}
|
||||
|
||||
if (property === "then" && commands.length === 0) {
|
||||
// This means we awaited a RpcObjectReference which resolves to itself
|
||||
// We don't support transfering references to Promises themselves, we'll
|
||||
// automatically await them before returning
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (property === "by_value") {
|
||||
return async () => {
|
||||
const result = await sendAndUnwrap(channel, {
|
||||
method: "by_value",
|
||||
referenceId: reference.referenceId,
|
||||
} as Command);
|
||||
if (result.type !== "value") {
|
||||
throw new Error(
|
||||
`[RPC] by_value() expected a value but got a reference for ${reference.objectType}`,
|
||||
);
|
||||
}
|
||||
return result.value;
|
||||
};
|
||||
if (property === "then" && commands.length > 0) {
|
||||
return BatchCommandExecutor(channel, reference.referenceId, commands);
|
||||
}
|
||||
|
||||
// console.log(`Accessing ${reference.objectType}.${String(propertyName)}`);
|
||||
return RpcPropertyReference(channel, { objectReference: target as any, property });
|
||||
if (property === "await") {
|
||||
return RpcPendingObjectReference(channel, {
|
||||
reference,
|
||||
commands: [...commands, { method: "await" }],
|
||||
});
|
||||
}
|
||||
|
||||
if (property === "transfer") {
|
||||
return RpcPendingObjectReference(channel, {
|
||||
reference,
|
||||
commands: [...commands, { method: "transfer" }],
|
||||
});
|
||||
}
|
||||
|
||||
return RpcPendingObjectReference(channel, {
|
||||
reference,
|
||||
commands:
|
||||
typeof property === "string"
|
||||
? [...commands, { method: "get", propertyName: property }]
|
||||
: [...commands, { method: "get", propertySymbol: serializeSymbol(property) }],
|
||||
});
|
||||
},
|
||||
}) as any;
|
||||
|
||||
apply(_target: any, _thisArg: any, argArray?: any): any {
|
||||
return RpcPendingObjectReference(channel, {
|
||||
reference,
|
||||
commands: [...commands, { method: "apply", args: argArray }],
|
||||
});
|
||||
},
|
||||
} satisfies ProxyHandler<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to a specific property on a remote object.
|
||||
*/
|
||||
type RpcPropertyReference = {
|
||||
objectReference: RpcObjectReference;
|
||||
property: string | PropertySymbol;
|
||||
};
|
||||
function BatchCommandExecutor(
|
||||
channel: RpcRequestChannel,
|
||||
referenceId: ReferenceId,
|
||||
commands: BatchCommand[],
|
||||
): (onFulfilled: (value: any) => void, onRejected: (reason: any) => void) => void {
|
||||
const command = {
|
||||
method: "batch",
|
||||
referenceId,
|
||||
commands: commands.filter((cmd) => cmd.method !== "await"),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* A reference to a specific property on a remote object.
|
||||
*/
|
||||
// export class RpcPropertyReference {
|
||||
// static create(
|
||||
// channel: RpcRequestChannel,
|
||||
// objectReference: RpcObjectReference,
|
||||
// propertyName: string,
|
||||
// ): RpcPropertyReference {
|
||||
// return ProxiedReferenceProperty(
|
||||
// channel,
|
||||
// new RpcPropertyReference(objectReference, propertyName),
|
||||
// );
|
||||
// }
|
||||
return (onFulfilled, onRejected) => {
|
||||
(async () => {
|
||||
const result = await channel.sendCommand(command);
|
||||
|
||||
// private constructor(
|
||||
// public objectReference: RpcObjectReference,
|
||||
// public propertyName: string,
|
||||
// ) {}
|
||||
// }
|
||||
|
||||
/**
|
||||
* A sub-proxy for a specific property of a proxied reference
|
||||
* This is because we need to handle property accesses differently than method calls
|
||||
* but we don't know which type it is until it gets consumed.
|
||||
*
|
||||
* If this references a method then the `apply` trap will be called on this proxy.
|
||||
* If this references a property then they'll try to await the value, triggering the `get` trap
|
||||
* when they access the `then` property.
|
||||
*/
|
||||
function RpcPropertyReference(channel: RpcRequestChannel, reference: RpcPropertyReference) {
|
||||
const target = () => {};
|
||||
Object.defineProperty(target, "name", { value: `RpcPropertyReference`, configurable: true });
|
||||
(target as any).objectReference = reference.objectReference;
|
||||
(target as any).property = reference.property;
|
||||
|
||||
return new Proxy(target, {
|
||||
get(_target, propertyName: string) {
|
||||
// console.log(
|
||||
// `Accessing ${reference.objectReference.objectType}.${reference.propertyName}.${propertyName}`,
|
||||
// );
|
||||
|
||||
// Allow Function.prototype.call/apply/bind to be used by TS helpers and wrappers (e.g., disposables, chainable await)
|
||||
if (propertyName === "call") {
|
||||
return Function.prototype.call;
|
||||
}
|
||||
if (propertyName === "apply") {
|
||||
return Function.prototype.apply;
|
||||
}
|
||||
if (propertyName === "bind") {
|
||||
return Function.prototype.bind;
|
||||
if (result === null || result === undefined) {
|
||||
throw new RpcError("RPC returned null or undefined response");
|
||||
}
|
||||
|
||||
if (propertyName !== "then") {
|
||||
// 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[]) => {
|
||||
const p = (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),
|
||||
);
|
||||
if (callResult.type === "value") {
|
||||
return callResult.value;
|
||||
}
|
||||
return RpcObjectReference.create(
|
||||
channel,
|
||||
callResult.referenceId,
|
||||
callResult.objectType,
|
||||
);
|
||||
})();
|
||||
return chain(p as Promise<any>);
|
||||
};
|
||||
if (result.status === "error") {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return (onFulfilled: (value: any) => void, onRejected: (error: any) => void) => {
|
||||
(async () => {
|
||||
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}`);
|
||||
if (result.result.type === "value") {
|
||||
return result.result.value;
|
||||
}
|
||||
|
||||
const command = buildCallCommand(
|
||||
reference.objectReference.referenceId,
|
||||
reference.property,
|
||||
argArray,
|
||||
);
|
||||
const p = (async () => {
|
||||
const result = await sendAndUnwrap(channel, command);
|
||||
if (result.type === "value") {
|
||||
return result.value;
|
||||
}
|
||||
return RpcObjectReference.create(channel, result.referenceId, result.objectType);
|
||||
})();
|
||||
return chain(p as Promise<any>);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 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),
|
||||
return RpcObjectReference(channel, {
|
||||
referenceId: result.result.referenceId,
|
||||
objectType: result.result.objectType,
|
||||
});
|
||||
})().then(onFulfilled, onRejected);
|
||||
};
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
function commandsToString(commands: BatchCommand[]): string {
|
||||
return commands
|
||||
.map((cmd) => {
|
||||
if (cmd.method === "get") {
|
||||
const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol;
|
||||
return `${String(prop)}`;
|
||||
} else if (cmd.method === "apply") {
|
||||
const prop = (cmd as any).propertyName ?? (cmd as any).propertySymbol;
|
||||
return `${String(prop)}()`;
|
||||
}
|
||||
|
||||
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);
|
||||
return "???";
|
||||
})
|
||||
.join(".");
|
||||
}
|
||||
|
||||
32
libs/common/src/platform/services/sdk/rpc/remote.ts
Normal file
32
libs/common/src/platform/services/sdk/rpc/remote.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ChainablePromise } from "./chainable-promise";
|
||||
|
||||
export type Remote<T> = {
|
||||
[K in keyof T as K extends typeof Symbol.dispose ? never : K]: RemoteProperty<T[K]>;
|
||||
} & (typeof Symbol.dispose extends keyof T ? { [Symbol.asyncDispose](): Promise<void> } : object);
|
||||
|
||||
type Resolved<T> = T extends Promise<infer U> ? U : T;
|
||||
|
||||
/**
|
||||
* Maps remote object fields to RPC-exposed types.
|
||||
*/
|
||||
export type RemoteProperty<T> = T extends (...args: any[]) => any
|
||||
? RemoteFunction<T>
|
||||
: RemoteReference<Resolved<T>>;
|
||||
|
||||
export type Transfer<T> = {
|
||||
/**
|
||||
* Force a by-value snapshot transfer of this remote reference. Resolves to a serializable value.
|
||||
* If the object is not serializable at runtime, this will throw.
|
||||
*/
|
||||
transfer: Promise<T>;
|
||||
};
|
||||
|
||||
export type RemoteReference<T> = Remote<T> & Transfer<T>;
|
||||
|
||||
/**
|
||||
* RemoteFunction arguments must be Serializable at compile time. For non-serializable
|
||||
* return types, we expose ChainablePromise<Remote<...>> to enable Rust-like `.await` chaining.
|
||||
*/
|
||||
export type RemoteFunction<T extends (...args: any[]) => any> = (
|
||||
...args: Parameters<T>
|
||||
) => ChainablePromise<RemoteReference<Resolved<ReturnType<T>>>> & Transfer<Resolved<ReturnType<T>>>;
|
||||
@@ -2,9 +2,9 @@ import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { Rc } from "../../../misc/reference-counting/rc";
|
||||
|
||||
import { isReferenceProxy, RpcObjectReference } from "./batch-proxies";
|
||||
import { RpcClient, RpcRequestChannel } from "./client";
|
||||
import { Command, Response } from "./protocol";
|
||||
import { isReferenceProxy, RpcObjectReference } from "./proxies";
|
||||
import { RpcServer } from "./server";
|
||||
|
||||
describe("RpcServer", () => {
|
||||
@@ -66,7 +66,7 @@ describe("RpcServer", () => {
|
||||
expect(result).toEqual({ message: "Hello, World!", array: [1, "2", null] });
|
||||
});
|
||||
|
||||
it("handles RC objects correctly", async () => {
|
||||
it.only("handles RC objects correctly", async () => {
|
||||
const wasmObj = new WasmLikeObject("RC");
|
||||
const rcValue = new Rc(wasmObj);
|
||||
const server = new RpcServer<Rc<WasmLikeObject>>();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { map, Observable, ReplaySubject } from "rxjs";
|
||||
|
||||
import { executeBatchCommands } from "./batch-executor";
|
||||
import { executeBatchCommands } from "./executor";
|
||||
import { Command, Response, Result } from "./protocol";
|
||||
import { ReferenceStore } from "./reference-store";
|
||||
|
||||
@@ -30,70 +30,6 @@ export class RpcServer<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// if (command.method === "get") {
|
||||
// const target = this.references.get<any>(command.referenceId);
|
||||
// if (!target) {
|
||||
// return { status: "error", error: `[RPC] Reference ID ${command.referenceId} not found` };
|
||||
// }
|
||||
|
||||
// try {
|
||||
// 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 ${String(propertyKey)} is a function` };
|
||||
// } else {
|
||||
// return { status: "success", result: this.convertToReturnable(propertyValue) };
|
||||
// }
|
||||
// } catch (error) {
|
||||
// return { status: "error", error };
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (command.method === "by_value") {
|
||||
// const target = this.references.get<any>(command.referenceId);
|
||||
// if (!target) {
|
||||
// return { status: "error", error: `[RPC] Reference ID ${command.referenceId} not found` };
|
||||
// }
|
||||
|
||||
// // try {
|
||||
// // Not a dependable check
|
||||
// // if (!isSerializable(target)) {
|
||||
// // return {
|
||||
// // status: "error",
|
||||
// // error: `[RPC] by_value() not supported for non-serializable object of type ${target?.constructor?.name}`,
|
||||
// // };
|
||||
// // }
|
||||
// return { status: "success", result: { type: "value", value: target } };
|
||||
// // } catch (error) {
|
||||
// // return { status: "error", error };
|
||||
// // }
|
||||
// }
|
||||
|
||||
// if (command.method === "call") {
|
||||
// const target = this.references.get<any>(command.referenceId);
|
||||
// if (!target) {
|
||||
// return { status: "error", error: `[RPC] Reference ID ${command.referenceId} not found` };
|
||||
// }
|
||||
|
||||
// try {
|
||||
// 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 ${String(propertyKey)} is not a function of ${target.constructor.name}`,
|
||||
// };
|
||||
// }
|
||||
|
||||
// const result = await method.apply(target, command.args);
|
||||
// return { status: "success", result: this.convertToReturnable(result) };
|
||||
// } catch (error) {
|
||||
// return { status: "error", error };
|
||||
// }
|
||||
// }
|
||||
|
||||
return { status: "error", error: `Unknown command method: ${command.method}` };
|
||||
}
|
||||
|
||||
|
||||
@@ -233,12 +233,12 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
||||
throw new Error("SDK is undefined");
|
||||
}
|
||||
|
||||
using ref = await sdk.take();
|
||||
await using ref = await sdk.take();
|
||||
|
||||
const ciphersClient = await ref.value.vault().await.ciphers();
|
||||
const result: DecryptCipherListResult = await ciphersClient
|
||||
.decrypt_list_with_failures(ciphers.map((cipher) => cipher.toSdkCipher()))
|
||||
.await.by_value();
|
||||
const result: DecryptCipherListResult = await ciphersClient.decrypt_list_with_failures(
|
||||
ciphers.map((cipher) => cipher.toSdkCipher()),
|
||||
).transfer;
|
||||
|
||||
const decryptedCiphers = result.successes;
|
||||
const failedCiphers: Cipher[] = result.failures
|
||||
|
||||
@@ -39,10 +39,10 @@ export class TotpService implements TotpServiceAbstraction {
|
||||
// Using remote SDK service to generate TOTP
|
||||
return this.remoteSdkService.remoteClient$.pipe(
|
||||
switchMap(async (sdk) => {
|
||||
using ref = await sdk!.take();
|
||||
await using ref = await sdk!.take();
|
||||
const totp = await ref.value.vault().await.totp();
|
||||
// Force by-value transfer for the TOTP response
|
||||
return totp.generate_totp(key).await.by_value();
|
||||
// Transfer the TOTP response
|
||||
return totp.generate_totp(key).transfer;
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user