Skip to content

Commit d47ade2

Browse files
committed
refactor(rpc-methods): Improve type safety and overhaul types
1 parent 4a32b25 commit d47ade2

File tree

16 files changed

+271
-307
lines changed

16 files changed

+271
-307
lines changed

eslint.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ const config = createConfig([
129129
},
130130
},
131131

132+
{
133+
files: ['**/test/**/*', '**/*.test.ts', '**/*.test.tsx'],
134+
rules: {
135+
'@typescript-eslint/explicit-function-return-type': 'off',
136+
},
137+
},
138+
132139
{
133140
files: ['**/*.types.test.ts'],
134141
rules: {

packages/cli/test/bundles.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export const invalidTestBundleNames = ['bad-vat.fails'];
1111

1212
const testRoot = new URL('.', import.meta.url).pathname;
1313

14-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
1514
const makeTestBundleRoot = async () => {
1615
const stageRoot = resolve(tmpdir(), 'test');
1716

@@ -32,7 +31,6 @@ const makeTestBundleRoot = async () => {
3231
return stageBundleRoot;
3332
};
3433

35-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
3634
export const makeTestBundleStage = async () => {
3735
const stageBundleRoot = await makeTestBundleRoot();
3836

@@ -44,7 +42,6 @@ export const makeTestBundleStage = async () => {
4442
return join(stageBundleRoot, `${bundleName}.js`);
4543
};
4644

47-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
4845
const getTestBundleSpecs = (testBundleNames: string[]) =>
4946
testBundleNames.map((bundleName) => ({
5047
name: bundleName,

packages/cli/test/integration/serve.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ describe('serve', async () => {
5353
});
5454

5555
describe('server', () => {
56-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
5756
const makeServer = (root: string = testBundleRoot) => {
5857
const port = getServerPort();
5958
const { listen } = getServer({

packages/create-package/src/cli.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,7 @@ describe('create-package/cli', () => {
7474
tsConfig: {},
7575
tsConfigBuild: {},
7676
nodeVersions: '>=18.0.0',
77-
// TODO: Replace `any` with type
78-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
79-
} as any);
77+
} as utils.MonorepoFileData);
8078
vi.spyOn(utils, 'finalizeAndWriteData').mockResolvedValue();
8179

8280
expect(
@@ -100,9 +98,7 @@ describe('create-package/cli', () => {
10098
tsConfig: {},
10199
tsConfigBuild: {},
102100
nodeVersions: '>=18.0.0',
103-
// TODO: Replace `any` with type
104-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
105-
} as any);
101+
} as utils.MonorepoFileData);
106102
vi.spyOn(utils, 'finalizeAndWriteData').mockResolvedValue();
107103

108104
expect(

packages/extension/src/kernel-integration/ui-connections.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ class MockBroadcastChannel {
8585

8686
vi.stubGlobal('BroadcastChannel', MockBroadcastChannel);
8787

88-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
8988
const makeMockLogger = () =>
9089
({
9190
...console,

packages/kernel-test/src/supervisor.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const makeVatSupervisor = async ({
1414
}: {
1515
handleWrite?: (input: unknown) => void | Promise<void>;
1616
vatPowers?: Record<string, unknown>;
17-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
1817
}) => {
1918
const commandStream = await TestDuplexStream.make<
2019
VatCommand,

packages/rpc-methods/src/RpcClient.test.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
import { string } from '@metamask/superstruct';
21
import { jsonrpc2 } from '@metamask/utils';
32
import { describe, it, vi, expect } from 'vitest';
43

54
import { RpcClient } from './RpcClient.ts';
6-
7-
const getMethods = () =>
8-
({
9-
method1: {
10-
method: 'method1',
11-
result: string(),
12-
},
13-
}) as const;
5+
import { getMethods } from '../test/methods.ts';
146

157
describe('RpcClient', () => {
168
describe('constructor', () => {
@@ -23,19 +15,19 @@ describe('RpcClient', () => {
2315
describe('call', () => {
2416
it('should call a method', async () => {
2517
const client = new RpcClient(getMethods(), vi.fn(), 'test');
26-
const resultP = client.call('method1', 'test');
18+
const resultP = client.call('method1', ['test']);
2719
client.handleResponse('test:1', {
2820
jsonrpc: jsonrpc2,
2921
id: 'test:1',
30-
result: 'test',
22+
result: null,
3123
});
3224

33-
expect(await resultP).toBe('test');
25+
expect(await resultP).toBeNull();
3426
});
3527

3628
it('should throw an error for error responses', async () => {
3729
const client = new RpcClient(getMethods(), vi.fn(), 'test');
38-
const resultP = client.call('method1', 'test');
30+
const resultP = client.call('method1', ['test']);
3931
client.handleResponse('test:1', {
4032
jsonrpc: jsonrpc2,
4133
id: 'test:1',
@@ -50,20 +42,20 @@ describe('RpcClient', () => {
5042

5143
it('should throw an error for invalid results', async () => {
5244
const client = new RpcClient(getMethods(), vi.fn(), 'test');
53-
const resultP = client.call('method1', 'test');
45+
const resultP = client.call('method1', ['test']);
5446
client.handleResponse('test:1', {
5547
jsonrpc: jsonrpc2,
5648
id: 'test:1',
5749
result: 42,
5850
});
5951
await expect(resultP).rejects.toThrow(
60-
'Invalid result: Expected a string, but received: 42',
52+
'Invalid result: Expected the literal `null`, but received: 42',
6153
);
6254
});
6355

6456
it('should throw an error for invalid responses', async () => {
6557
const client = new RpcClient(getMethods(), vi.fn(), 'test');
66-
const resultP = client.call('method1', 'test');
58+
const resultP = client.call('method1', ['test']);
6759
client.handleResponse('test:1', 'invalid');
6860
await expect(resultP).rejects.toThrow('Invalid JSON-RPC response:');
6961
});
@@ -83,8 +75,8 @@ describe('RpcClient', () => {
8375
describe('rejectAll', () => {
8476
it('should reject all unresolved messages', async () => {
8577
const client = new RpcClient(getMethods(), vi.fn(), 'test');
86-
const p1 = client.call('method1', 'test');
87-
const p2 = client.call('method1', 'test');
78+
const p1 = client.call('method1', ['test']);
79+
const p2 = client.call('method1', ['test']);
8880
client.rejectAll(new Error('test error'));
8981
await expect(p1).rejects.toThrow('test error');
9082
await expect(p2).rejects.toThrow('test error');
Lines changed: 38 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,35 @@
11
import { makePromiseKit } from '@endo/promise-kit';
2-
import type { Struct } from '@metamask/superstruct';
32
import { assert as assertStruct } from '@metamask/superstruct';
43
import { assertIsJsonRpcResponse, isJsonRpcFailure } from '@metamask/utils';
5-
import type { Json, JsonRpcParams } from '@metamask/utils';
4+
import type { JsonRpcParams } from '@metamask/utils';
65
import { makeCounter } from '@ocap/utils';
76
import type { PromiseCallbacks } from '@ocap/utils';
87

9-
export type MethodSignature<
10-
Method extends string,
11-
Params extends JsonRpcParams,
12-
Result extends Json,
13-
> = (method: Method, params: Params) => Promise<Result>;
14-
15-
type ExtractMethodName<
16-
Methods extends MethodSignature<string, JsonRpcParams, Json>,
17-
> = Methods extends (
18-
method: infer Method,
19-
params: JsonRpcParams,
20-
) => Promise<Json>
21-
? Method
22-
: never;
23-
24-
type ExtractParams<
25-
Methods extends MethodSignature<string, JsonRpcParams, Json>,
26-
> = Methods extends (method: string, params: infer Params) => Promise<Json>
27-
? Params
28-
: never;
29-
30-
type ExtractResult<
31-
Methods extends MethodSignature<string, JsonRpcParams, Json>,
32-
> = Methods extends (
33-
method: string,
34-
params: JsonRpcParams,
35-
) => Promise<infer Result>
36-
? Result
37-
: never;
38-
39-
export type MethodSpec<Method extends string, Result extends Json> = {
40-
method: Method;
41-
result: Struct<Result>;
42-
};
43-
44-
type MethodSpecs<Methods extends MethodSignature<string, JsonRpcParams, Json>> =
45-
Record<
46-
ExtractMethodName<Methods>,
47-
MethodSpec<ExtractMethodName<Methods>, ExtractResult<Methods>>
48-
>;
8+
import type {
9+
MethodSpec,
10+
ExtractParams,
11+
ExtractResult,
12+
ExtractMethod,
13+
MethodSpecRecord,
14+
} from './utils.ts';
4915

5016
type RpcPayload = {
5117
method: string;
5218
params: JsonRpcParams;
5319
};
5420

55-
type SendMessage = (messageId: string, payload: RpcPayload) => Promise<void>;
21+
export type SendMessage = (
22+
messageId: string,
23+
payload: RpcPayload,
24+
) => Promise<void>;
5625

57-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
58-
export class RpcClient<Methods extends MethodSignature<string, any, any>> {
59-
readonly #methods: MethodSpecs<Methods>;
26+
export class RpcClient<
27+
// The class picks up its type from the `methods` argument,
28+
// so using `any` in this constraint is safe.
29+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
30+
Methods extends MethodSpecRecord<MethodSpec<string, any, any>>,
31+
> {
32+
readonly #methods: Methods;
6033

6134
readonly #prefix: string;
6235

@@ -66,20 +39,16 @@ export class RpcClient<Methods extends MethodSignature<string, any, any>> {
6639

6740
readonly #sendMessage: SendMessage;
6841

69-
constructor(
70-
methods: MethodSpecs<Methods>,
71-
sendMessage: SendMessage,
72-
prefix: string,
73-
) {
42+
constructor(methods: Methods, sendMessage: SendMessage, prefix: string) {
7443
this.#methods = methods;
7544
this.#sendMessage = sendMessage;
7645
this.#prefix = prefix;
7746
}
7847

79-
async call<Method extends Methods>(
80-
method: ExtractMethodName<Method>,
81-
params: ExtractParams<Method>,
82-
): Promise<ExtractResult<Method>> {
48+
async call<Method extends ExtractMethod<Methods>>(
49+
method: Method,
50+
params: ExtractParams<Method, Methods>,
51+
): Promise<ExtractResult<Method, Methods>> {
8352
const response = await this.#createMessage({
8453
method,
8554
params,
@@ -90,10 +59,23 @@ export class RpcClient<Methods extends MethodSignature<string, any, any>> {
9059
throw new Error(`${response.error.message}`);
9160
}
9261

93-
assertResult(response.result, this.#methods[method].result);
62+
this.#assertResult(method, response.result);
9463
return response.result;
9564
}
9665

66+
#assertResult<Method extends ExtractMethod<Methods>>(
67+
method: Method,
68+
result: unknown,
69+
): asserts result is ExtractResult<Method, Methods> {
70+
try {
71+
// @ts-expect-error - TODO: For unknown reasons, TypeScript fails to recognize that
72+
// `Method` must be a key of `this.#methods`.
73+
assertStruct(result, this.#methods[method].result);
74+
} catch (error) {
75+
throw new Error(`Invalid result: ${(error as Error).message}`);
76+
}
77+
}
78+
9779
async #createMessage(payload: RpcPayload): Promise<unknown> {
9880
const { promise, reject, resolve } = makePromiseKit<unknown>();
9981
const messageId = this.#nextMessageId();
@@ -128,19 +110,3 @@ export class RpcClient<Methods extends MethodSignature<string, any, any>> {
128110
return `${this.#prefix}:${this.#messageCounter()}`;
129111
}
130112
}
131-
132-
/**
133-
* @param result - The result to assert.
134-
* @param struct - The struct to assert the result against.
135-
* @throws If the result is invalid.
136-
*/
137-
function assertResult<Result extends Json>(
138-
result: unknown,
139-
struct: Struct<Result>,
140-
): asserts result is Result {
141-
try {
142-
assertStruct(result, struct);
143-
} catch (error) {
144-
throw new Error(`Invalid result: ${(error as Error).message}`);
145-
}
146-
}

packages/rpc-methods/src/RpcService.test.ts

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,7 @@
1-
import { number, string, tuple } from '@metamask/superstruct';
21
import { describe, it, expect } from 'vitest';
32

43
import { RpcService } from './RpcService.ts';
5-
import type { Handler } from './RpcService.ts';
6-
7-
const getHooks = () =>
8-
({
9-
hook1: () => undefined,
10-
hook2: () => undefined,
11-
hook3: () => undefined,
12-
}) as const;
13-
14-
type Hooks = ReturnType<typeof getHooks>;
15-
16-
const getHandlers = () =>
17-
({
18-
method1: {
19-
method: 'method1',
20-
implementation: () => null,
21-
params: tuple([string()]),
22-
hooks: ['hook1', 'hook2'] as const,
23-
} as Handler<Hooks, 'method1', [string], null>,
24-
method2: {
25-
method: 'method2',
26-
implementation: (hooks, [value]) => {
27-
hooks.hook3();
28-
return value * 2;
29-
},
30-
params: tuple([number()]),
31-
hooks: ['hook3'] as const,
32-
} as Handler<Hooks, 'method2', [number], number>,
33-
}) as const;
4+
import { getHandlers, getHooks } from '../test/methods.ts';
345

356
describe('RpcService', () => {
367
describe('constructor', () => {

0 commit comments

Comments
 (0)