Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/webview/lib/actions/clipboard.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { openErrorDialog } from "@/webview/lib/actions";
import { rpc } from "@/webview/lib/rpc/rpc-client";
import { rpcClient } from "@/webview/lib/rpc/rpc-client";

export async function copyToClipboard(type: string, data: string) {
try {
const success = await rpc.call("clipboard.copy", data);
const success = await rpcClient.request("clipboard.copy", data);

if (!success) {
openErrorDialog(window.l10n.unableToCopyToClipboard.replace("{0}", type));
Expand Down
18 changes: 11 additions & 7 deletions src/webview/lib/dispatcher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { ResponseMessage } from "@/types";
import { handleRpcResponse } from "@/webview/lib/rpc/rpc-client";
import { handleRpcNotification } from "@/webview/lib/rpc/rpc-notify";

import { handleActionResult } from "./handler/action-result";
import { handleCommitDetails } from "./handler/commit-details";
Expand Down Expand Up @@ -38,16 +36,22 @@ const handlers: Handlers = {

export function initDispatcher() {
window.addEventListener("message", (e: MessageEvent<unknown>) => {
if (handleRpcResponse(e.data)) {
if (!isResponseMessage(e.data)) {
return;
}
if (handleRpcNotification(e.data)) {
return;
}
dispatch(e.data as ResponseMessage);
dispatch(e.data);
});
}

function isResponseMessage(message: unknown): message is ResponseMessage {
return (
typeof message === "object" &&
message !== null &&
"command" in message &&
typeof message.command === "string"
);
}

function dispatch(msg: ResponseMessage): void {
const handle = handlers[msg.command] as ((m: ResponseMessage) => void) | undefined;

Expand Down
65 changes: 14 additions & 51 deletions src/webview/lib/rpc/rpc-client.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import type { RpcMethod, RpcMethodMap, RpcRequest, RpcResponse } from "@/types";
import type { RpcMethod, RpcMethodMap, RpcRequest } from "@/types";
import { initRpcHandler, type PendingRpcRequest } from "@/webview/lib/rpc/rpc-handler";
import { vscode } from "@/webview/lib/vscode";

const RPC_TIMEOUT_MS = 30_000;

type PendingRequest = {
resolve: (value: unknown) => void;
reject: (value: unknown) => void;
timeout: ReturnType<typeof setTimeout>;
};
const requests = new Map<string, PendingRequest>();
const requests = new Map<string, PendingRpcRequest>();
let initialized = false;

export const rpcClient = {
init(): void {
if (initialized) {
return;
}

export const rpc = {
call<M extends RpcMethod>(
initialized = true;
initRpcHandler(requests);
},
request<M extends RpcMethod>(
method: M,
params: RpcMethodMap[M]["params"]
): Promise<RpcMethodMap[M]["result"]> {
Expand Down Expand Up @@ -47,45 +52,3 @@ export const rpc = {
});
}
};

export function handleRpcResponse(message: unknown): boolean {
if (!isRpcResponse(message)) {
return false;
}

const request = requests.get(message.id);
if (request === undefined) {
return true;
}
requests.delete(message.id);
clearTimeout(request.timeout);

if (message.success) {
request.resolve(message.result);
} else {
request.reject(new Error(message.error));
}

return true;
}

function isRpcResponse(message: unknown): message is RpcResponse {
if (
typeof message !== "object" ||
message === null ||
!("kind" in message) ||
message.kind !== "rpc.response" ||
!("id" in message) ||
typeof message.id !== "string" ||
!("success" in message) ||
typeof message.success !== "boolean"
) {
return false;
}

if (message.success) {
return "result" in message;
}

return "error" in message && typeof message.error === "string";
}
89 changes: 89 additions & 0 deletions src/webview/lib/rpc/rpc-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { RpcNotification, RpcResponse } from "@/types";
import { refresh } from "@/webview/lib/actions";
import { loadRepoList } from "@/webview/lib/load-repos";
import { selectedRepo } from "@/webview/lib/stores";
import { repoListStore } from "@/webview/lib/stores/repo-list.store";

export type PendingRpcRequest = {
resolve: (value: unknown) => void;
reject: (value: unknown) => void;
timeout: ReturnType<typeof setTimeout>;
};

export function initRpcHandler(requests: Map<string, PendingRpcRequest>): void {
window.addEventListener("message", (event: MessageEvent<unknown>) => {
if (isRpcResponse(event.data)) {
handleRpcResponse(event.data, requests);
return;
}

if (isRpcNotification(event.data)) {
handleRpcNotification(event.data);
}
});
}

function handleRpcResponse(message: RpcResponse, requests: Map<string, PendingRpcRequest>): void {
const request = requests.get(message.id);
if (request === undefined) {
return;
}
requests.delete(message.id);
clearTimeout(request.timeout);

if (message.success) {
request.resolve(message.result);
} else {
request.reject(new Error(message.error));
}
}

function handleRpcNotification(message: RpcNotification): void {
switch (message.name) {
case "repo.changed":
repoListStore.apply(message.message);
return;
case "repo.rescan":
void loadRepoList();
return;
case "repo.updated":
if (message.message.path === selectedRepo.value) {
refresh();
}
}
}

function isRpcResponse(message: unknown): message is RpcResponse {
if (
typeof message !== "object" ||
message === null ||
!("kind" in message) ||
message.kind !== "rpc.response" ||
!("id" in message) ||
typeof message.id !== "string" ||
!("success" in message) ||
typeof message.success !== "boolean"
) {
return false;
}

if (message.success) {
return "result" in message;
}

return "error" in message && typeof message.error === "string";
}

function isRpcNotification(message: unknown): message is RpcNotification {
return (
typeof message === "object" &&
message !== null &&
"kind" in message &&
message.kind === "rpc.notify" &&
"id" in message &&
typeof message.id === "string" &&
"name" in message &&
typeof message.name === "string" &&
"message" in message
);
}
41 changes: 0 additions & 41 deletions src/webview/lib/rpc/rpc-notify.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/webview/lib/stores/repo-list.store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { signal } from "@preact/signals";

import type { GitRepo, RepoChange } from "@/types";
import { rpc } from "@/webview/lib/rpc/rpc-client";
import { rpcClient } from "@/webview/lib/rpc/rpc-client";

const repoList = signal<Array<GitRepo> | undefined>(undefined);

Expand All @@ -11,7 +11,7 @@ export const repoListStore = {
},
load: async (): Promise<Array<GitRepo>> => {
repoList.value = undefined;
const result = await rpc.call("repo.scan", null);
const result = await rpcClient.request("repo.scan", null);
repoList.value = result.repos;
return result.repos;
},
Expand Down
5 changes: 3 additions & 2 deletions src/webview/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Button } from "./components/ui/Button";
import { selectRepo } from "./lib/actions";
import { initDispatcher } from "./lib/dispatcher";
import { loadRepoList, repoListError } from "./lib/load-repos";
import { rpc } from "./lib/rpc/rpc-client";
import { rpcClient } from "./lib/rpc/rpc-client";
import { initializeStores, selectedRepo } from "./lib/stores";
import { repoListStore } from "./lib/stores/repo-list.store";
import { initializeWebviewConfig } from "./lib/webview-config";
Expand All @@ -17,6 +17,7 @@ import { NoRepoPage } from "./pages/NoRepoPage";

const root = document.getElementById("app")!;

rpcClient.init();
initDispatcher();
render(<LoadingPage />, root);

Expand All @@ -30,7 +31,7 @@ void main().catch((error: unknown) => {
});

async function main() {
const { l10n, config } = await rpc.call("webview.initialize", null);
const { l10n, config } = await rpcClient.request("webview.initialize", null);
window.l10n = l10n;
initializeWebviewConfig(config);
initializeStores(config.initialLoadCommits);
Expand Down
4 changes: 2 additions & 2 deletions src/webview/pages/NoRepoPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState } from "preact/hooks";

import { Button } from "@/webview/components/ui/Button";
import { Icon } from "@/webview/components/ui/Icons";
import { rpc } from "@/webview/lib/rpc/rpc-client";
import { rpcClient } from "@/webview/lib/rpc/rpc-client";

export function NoRepoPage() {
const [initializing, setInitializing] = useState(false);
Expand All @@ -13,7 +13,7 @@ export function NoRepoPage() {
setError(undefined);

try {
await rpc.call("git.init", null);
await rpcClient.request("git.init", null);
} catch (reason: unknown) {
const message = reason instanceof Error ? reason.message : String(reason);
setError(window.l10n.unableToInitializeRepo.replace("{0}", message));
Expand Down
4 changes: 2 additions & 2 deletions tests/webview/lib/rpc-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, expect, it, vi } from "vitest";

import { rpc } from "@/webview/lib/rpc/rpc-client";
import { rpcClient } from "@/webview/lib/rpc/rpc-client";

import { vscodeApi } from "@tests/webview/setup";

Expand All @@ -14,7 +14,7 @@ afterEach(() => {

it("rejects a request that times out", async () => {
vi.useFakeTimers();
const result = rpc.call("clipboard.copy", "commit");
const result = rpcClient.request("clipboard.copy", "commit");
const rejection = expect(result).rejects.toThrow("RPC request timed out: clipboard.copy");
await vi.runAllTimersAsync();

Expand Down
2 changes: 2 additions & 0 deletions tests/webview/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { LocalizedStrings } from "@/old-extension/l10n/webviewL10n";
import type { WebviewConfig } from "@/types";
import { initDispatcher } from "@/webview/lib/dispatcher";
import { rpcClient } from "@/webview/lib/rpc/rpc-client";
import { initializeWebviewConfig } from "@/webview/lib/webview-config";

const config: WebviewConfig = {
Expand All @@ -23,6 +24,7 @@ export function setupWebviewTest({ dispatchMessages = false } = {}) {
});

if (dispatchMessages) {
rpcClient.init();
initDispatcher();
}
}