mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
feat: add ipc service usage docs (#16000)
This commit is contained in:
@@ -2,25 +2,136 @@ import { Observable, shareReplay } from "rxjs";
|
|||||||
|
|
||||||
import { IpcClient, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal";
|
import { IpcClient, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for inter-process communication (IPC).
|
||||||
|
*
|
||||||
|
* - {@link IpcService.init} should be called in the initialization phase of the client.
|
||||||
|
* - This service owns the underlying {@link IpcClient} lifecycle and starts it during initialization.
|
||||||
|
*
|
||||||
|
* ## Usage
|
||||||
|
*
|
||||||
|
* ### Publish / Subscribe
|
||||||
|
* There are 2 main ways of sending and receiving messages over IPC in TypeScript:
|
||||||
|
*
|
||||||
|
* #### 1. TypeScript only JSON-based messages
|
||||||
|
* This is the simplest form of IPC, where messages are sent as untyped JSON objects.
|
||||||
|
* This is useful for simple message passing without the need for Rust code.
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* // Send a message
|
||||||
|
* await ipcService.send(OutgoingMessage.new_json_payload({ my: "data" }, "BrowserBackground", "my-topic"));
|
||||||
|
*
|
||||||
|
* // Receive messages
|
||||||
|
* ipcService.messages$.subscribe((message: IncomingMessage) => {
|
||||||
|
* if (message.topic === "my-topic") {
|
||||||
|
* const data = incomingMessage.parse_payload_as_json();
|
||||||
|
* console.log("Received message:", data);
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* #### 2. Rust compatible messages
|
||||||
|
* If you need to send messages that can also be handled by Rust code you can use typed Rust structs
|
||||||
|
* together with Rust functions to send and receive messages. For more information on typed structs
|
||||||
|
* refer to `TypedOutgoingMessage` and `TypedIncomingMessage` in the SDK.
|
||||||
|
*
|
||||||
|
* For examples on how to use the RPC framework with Rust see the section below.
|
||||||
|
*
|
||||||
|
* ### RPC (Request / Response)
|
||||||
|
* The RPC functionality is more complex than simple message passing and requires Rust code
|
||||||
|
* to send and receive calls. For this reason, the service also exposes the underlying
|
||||||
|
* {@link IpcClient} so it can be passed directly into Rust code.
|
||||||
|
*
|
||||||
|
* #### Rust code
|
||||||
|
* ```rust
|
||||||
|
* #[wasm_bindgen(js_name = ipcRegisterPingHandler)]
|
||||||
|
* pub async fn ipc_register_ping_handler(ipc_client: &JsIpcClient) {
|
||||||
|
* ipc_client
|
||||||
|
* .client
|
||||||
|
* // See Rust docs for more information on how to implement a handler
|
||||||
|
* .register_rpc_handler(PingHandler::new())
|
||||||
|
* .await;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* #[wasm_bindgen(js_name = ipcRequestPing)]
|
||||||
|
* pub async fn ipc_request_ping(
|
||||||
|
* ipc_client: &JsIpcClient,
|
||||||
|
* destination: Endpoint,
|
||||||
|
* abort_signal: Option<AbortSignal>,
|
||||||
|
* ) -> Result<PingResponse, RequestError> {
|
||||||
|
* ipc_client
|
||||||
|
* .client
|
||||||
|
* .request(
|
||||||
|
* PingRequest,
|
||||||
|
* destination,
|
||||||
|
* abort_signal.map(|c| c.to_cancellation_token()),
|
||||||
|
* )
|
||||||
|
* .await
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* #### TypeScript code
|
||||||
|
* ```typescript
|
||||||
|
* import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||||
|
* import { IpcClient, ipcRegisterPingHandler, ipcRequestPing } from "@bitwarden/sdk-internal";
|
||||||
|
*
|
||||||
|
* class MyService {
|
||||||
|
* constructor(private ipcService: IpcService) {}
|
||||||
|
*
|
||||||
|
* async init() {
|
||||||
|
* await ipcRegisterPingHandler(this.ipcService.client);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* async ping(destination: Endpoint): Promise<PingResponse> {
|
||||||
|
* return await ipcRequestPing(this.ipcService.client, destination);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
export abstract class IpcService {
|
export abstract class IpcService {
|
||||||
private _client?: IpcClient;
|
private _client?: IpcClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access to the underlying {@link IpcClient} for advanced/Rust RPC usage.
|
||||||
|
*
|
||||||
|
* @throws If the service has not been initialized.
|
||||||
|
*/
|
||||||
get client(): IpcClient {
|
get client(): IpcClient {
|
||||||
if (!this._client) {
|
if (!this._client) {
|
||||||
throw new Error("IpcService not initialized");
|
throw new Error("IpcService not initialized. Call init() first.");
|
||||||
}
|
}
|
||||||
return this._client;
|
return this._client;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _messages$?: Observable<IncomingMessage>;
|
private _messages$?: Observable<IncomingMessage>;
|
||||||
protected get messages$(): Observable<IncomingMessage> {
|
|
||||||
|
/**
|
||||||
|
* Hot stream of {@link IncomingMessage} from the IPC layer.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - Uses `shareReplay({ bufferSize: 0, refCount: true })`, so no events are replayed to late subscribers.
|
||||||
|
* Subscribe early if you must not miss messages.
|
||||||
|
*
|
||||||
|
* @throws If the service has not been initialized.
|
||||||
|
*/
|
||||||
|
get messages$(): Observable<IncomingMessage> {
|
||||||
if (!this._messages$) {
|
if (!this._messages$) {
|
||||||
throw new Error("IpcService not initialized");
|
throw new Error("IpcService not initialized. Call init() first.");
|
||||||
}
|
}
|
||||||
return this._messages$;
|
return this._messages$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the service and starts the IPC client.
|
||||||
|
*/
|
||||||
abstract init(): Promise<void>;
|
abstract init(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wires the provided {@link IpcClient}, starts it, and sets up the message stream.
|
||||||
|
*
|
||||||
|
* - Starts the client via `client.start()`.
|
||||||
|
* - Subscribes to the client's receive loop and exposes it through {@link messages$}.
|
||||||
|
* - Implementations may override `init` but should call this helper exactly once.
|
||||||
|
*/
|
||||||
protected async initWithClient(client: IpcClient): Promise<void> {
|
protected async initWithClient(client: IpcClient): Promise<void> {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
await this._client.start();
|
await this._client.start();
|
||||||
@@ -47,6 +158,12 @@ export abstract class IpcService {
|
|||||||
}).pipe(shareReplay({ bufferSize: 0, refCount: true }));
|
}).pipe(shareReplay({ bufferSize: 0, refCount: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an {@link OutgoingMessage} over IPC.
|
||||||
|
*
|
||||||
|
* @param message The message to send.
|
||||||
|
* @throws If the service is not initialized or the underlying client fails to send.
|
||||||
|
*/
|
||||||
async send(message: OutgoingMessage) {
|
async send(message: OutgoingMessage) {
|
||||||
await this.client.send(message);
|
await this.client.send(message);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user