Skip to content

Commit

Permalink
use an object with functions as "RPC message" type (#1257)
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas authored Mar 12, 2024
1 parent 9c2ee89 commit c56c8e6
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 65 deletions.
84 changes: 55 additions & 29 deletions src/extension/__tests__/rpc.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { MessageAdapter } from "../messageAdapters";
import { ApolloClientDevtoolsRPCMessage, MessageType } from "../messages";
import {
RPCMessage,
createRPCBridge,
createRpcClient,
createRpcHandler,
} from "../rpc";
import { createRPCBridge, createRpcClient, createRpcHandler } from "../rpc";

interface TestAdapter
extends MessageAdapter<
Expand Down Expand Up @@ -67,7 +62,9 @@ function createBridge(adapter1: TestAdapter, adapter2: TestAdapter) {
}

test("can send and receive rpc messages", async () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};
// Since these are sent over separate instances in the real world, we want to
// simulate that as best as we can with separate adapters
const handlerAdapter = createTestAdapter();
Expand All @@ -85,7 +82,9 @@ test("can send and receive rpc messages", async () => {
});

test("resolves async handlers", async () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};
// Since these are sent over separate instances in the real world, we want to
// simulate that as best as we can with separate adapters
const handlerAdapter = createTestAdapter();
Expand All @@ -109,7 +108,9 @@ test("resolves async handlers", async () => {
});

test("does not mistakenly handle messages from different rpc calls", async () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};
const clientAdapter = createTestAdapter();
const client = createRpcClient<Message>(clientAdapter);

Expand All @@ -132,7 +133,9 @@ test("does not mistakenly handle messages from different rpc calls", async () =>
});

test("rejects when handler throws error", async () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};
// Since these are sent over separate instances in the real world, we want to
// simulate that as best as we can with separate adapters
const handlerAdapter = createTestAdapter();
Expand All @@ -152,7 +155,9 @@ test("rejects when handler throws error", async () => {
});

test("rejects when async handler rejects", async () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};
// Since these are sent over separate instances in the real world, we want to
// simulate that as best as we can with separate adapters
const handlerAdapter = createTestAdapter();
Expand All @@ -170,9 +175,12 @@ test("rejects when async handler rejects", async () => {
});

test("can handle multiple rpc messages", async () => {
type Message =
| RPCMessage<"add", { x: number; y: number }, number>
| RPCMessage<"shout", { text: string }, string>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
// while we're at it, let's have this one return a Promise in the definition
// it should not matter for the implementation
shout({ text }: { text: string }): Promise<string>;
};

const handlerAdapter = createTestAdapter();
const clientAdapter = createTestAdapter();
Expand All @@ -192,7 +200,9 @@ test("can handle multiple rpc messages", async () => {
});

test("only allows one handler per type", async () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};

const handle = createRpcHandler<Message>(createTestAdapter());

Expand All @@ -204,7 +214,9 @@ test("only allows one handler per type", async () => {
});

test("ignores messages that don't originate from devtools", () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};

const adapter = createTestAdapter();
const handle = createRpcHandler<Message>(adapter);
Expand All @@ -221,7 +233,9 @@ test("ignores messages that don't originate from devtools", () => {
// actor message type collides with an rpc message type, we want to ignore the
// actor message type.
test("ignores messages that aren't rpc messages", () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};

const adapter = createTestAdapter();
const handle = createRpcHandler<Message>(adapter);
Expand All @@ -239,7 +253,9 @@ test("ignores messages that aren't rpc messages", () => {
});

test("does not add listener to adapter until first subscribed handler", () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};

const adapter = createTestAdapter();
const handle = createRpcHandler<Message>(adapter);
Expand All @@ -252,10 +268,11 @@ test("does not add listener to adapter until first subscribed handler", () => {
});

test("adds a single listener regardless of active handlers", () => {
type Message =
| RPCMessage<"add", { x: number; y: number }, number>
| RPCMessage<"subtract", { x: number; y: number }, number>
| RPCMessage<"shout", { text: string }, string>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
subtract({ x, y }: { x: number; y: number }): number;
shout({ text }: { text: string }): string;
};

const adapter = createTestAdapter();
const handle = createRpcHandler<Message>(adapter);
Expand All @@ -270,7 +287,9 @@ test("adds a single listener regardless of active handlers", () => {
});

test("can unsubscribe from a handler by calling the returned function", () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};

const adapter = createTestAdapter();
const handle = createRpcHandler<Message>(adapter);
Expand All @@ -297,9 +316,10 @@ test("can unsubscribe from a handler by calling the returned function", () => {
});

test("removes listener on adapter when unsubscribing from last handler", () => {
type Message =
| RPCMessage<"add", { x: number; y: number }, number>
| RPCMessage<"shout", { text: string }, string>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
shout({ text }: { text: string }): string;
};

const adapter = createTestAdapter();
const handle = createRpcHandler<Message>(adapter);
Expand All @@ -315,7 +335,9 @@ test("removes listener on adapter when unsubscribing from last handler", () => {
});

test("re-adds listener on adapter when subscribing after unsubscribing", () => {
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};

const adapter = createTestAdapter();
const handle = createRpcHandler<Message>(adapter);
Expand All @@ -332,7 +354,9 @@ test("re-adds listener on adapter when subscribing after unsubscribing", () => {

test("times out if no message received within default timeout", async () => {
jest.useFakeTimers();
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};

const adapter = createTestAdapter();
const client = createRpcClient<Message>(adapter);
Expand All @@ -350,7 +374,9 @@ test("times out if no message received within default timeout", async () => {

test("times out if no message received within configured timeout", async () => {
jest.useFakeTimers();
type Message = RPCMessage<"add", { x: number; y: number }, number>;
type Message = {
add({ x, y }: { x: number; y: number }): number;
};

const adapter = createTestAdapter();
const client = createRpcClient<Message>(adapter);
Expand Down
2 changes: 1 addition & 1 deletion src/extension/devtools/devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function startRequestInterval(ms = 500) {
let id: NodeJS.Timeout;

async function getClientData() {
const payload = await rpcClient.request("getClientOperations", {});
const payload = await rpcClient.request("getClientOperations", undefined);

if (panelWindow) {
panelWindow.send({ type: "update", payload });
Expand Down
9 changes: 3 additions & 6 deletions src/extension/messages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ExplorerResponse } from "../types";
import { GetStates, GetContext } from "../application/stateMachine";
import { DevtoolsMachine } from "../application/machines";
import { RPCMessage } from "./rpc";

export interface MessageFormat {
type: string;
Expand Down Expand Up @@ -76,11 +75,9 @@ export type PanelMessage =
| { type: "devtoolsStateChanged"; state: GetStates<DevtoolsMachine> }
| { type: "update"; payload: GetContext<DevtoolsMachine>["clientContext"] };

export type DevtoolsRPCMessage = RPCMessage<
"getClientOperations",
Record<string, never>,
GetContext<DevtoolsMachine>["clientContext"]
>;
export type DevtoolsRPCMessage = {
getClientOperations(): GetContext<DevtoolsMachine>["clientContext"];
};

export function isApolloClientDevtoolsMessage<
Message extends Record<string, unknown>,
Expand Down
43 changes: 14 additions & 29 deletions src/extension/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,46 +17,34 @@ type RPCResponseMessageFormat =
| { sourceId: number; result: unknown }
| { sourceId: number; error: unknown };

export type RPCMessage<
Name extends string,
Params extends RPCParams,
ReturnType,
> = {
__name: Name;
__params: Params;
__returnType: ReturnType;
};
type MessageCollection = Record<string, (...parameters: [SafeAny]) => SafeAny>;

export interface RpcClient<
Messages extends RPCMessage<string, RPCParams, SafeAny>,
> {
request: <TName extends Messages["__name"]>(
export interface RpcClient<Messages extends MessageCollection> {
request: <TName extends keyof Messages & string>(
name: TName,
params: Extract<Messages, { __name: TName }>["__params"],
params: Parameters<Messages[TName]>[0],
options?: { timeoutMs?: number }
) => Promise<Extract<Messages, { __name: TName }>["__returnType"]>;
) => Promise<Awaited<ReturnType<Messages[TName]>>>;
}

let nextMessageId = 0;
const DEFAULT_TIMEOUT = 30_000;

export function createRpcClient<
Messages extends RPCMessage<string, RPCParams, SafeAny>,
>(
export function createRpcClient<Messages extends MessageCollection>(
adapter: MessageAdapter<
ApolloClientDevtoolsRPCMessage<RPCRequestMessageFormat>
>
): RpcClient<Messages> {
return {
request: (name, params, options) => {
return Promise.race([
new Promise((_, reject) => {
new Promise<never>((_, reject) => {
setTimeout(
() => reject(new Error("Timeout waiting for message")),
options?.timeoutMs ?? DEFAULT_TIMEOUT
);
}),
new Promise((resolve, reject) => {
new Promise<SafeAny>((resolve, reject) => {
const id = ++nextMessageId;

const removeListener = adapter.addListener((message) => {
Expand Down Expand Up @@ -87,9 +75,7 @@ export function createRpcClient<
};
}

export function createRpcHandler<
Messages extends RPCMessage<string, RPCParams, SafeAny>,
>(
export function createRpcHandler<Messages extends MessageCollection>(
adapter: MessageAdapter<
ApolloClientDevtoolsRPCMessage<RPCResponseMessageFormat>
>
Expand Down Expand Up @@ -119,14 +105,13 @@ export function createRpcHandler<
}
}

return function <
TName extends Messages["__name"],
TReturnType = Extract<Messages, { __name: TName }>["__returnType"],
>(
return function <TName extends keyof Messages & string>(
name: TName,
execute: (
params: Extract<Messages, { __name: TName }>["__params"]
) => NoInfer<TReturnType> | Promise<NoInfer<TReturnType>>
params: Parameters<Messages[TName]>[0]
) =>
| NoInfer<Awaited<ReturnType<Messages[TName]>>>
| Promise<NoInfer<Awaited<ReturnType<Messages[TName]>>>>
) {
if (listeners.has(name)) {
throw new Error("Only one rpc handler can be registered per type");
Expand Down

0 comments on commit c56c8e6

Please sign in to comment.