Skip to content

Commit 6f18a0a

Browse files
committed
Configure encode/decode, no throw on batch
1 parent 0b1971f commit 6f18a0a

File tree

4 files changed

+86
-45
lines changed

4 files changed

+86
-45
lines changed

.eslintrc.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
module.exports = {
22
parser: "@typescript-eslint/parser",
33
extends: [
4-
"plugin:react/recommended",
54
"plugin:@typescript-eslint/recommended",
65
"prettier/@typescript-eslint",
76
"plugin:prettier/recommended"

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@
8282
"eslint": "^6.8.0",
8383
"eslint-config-prettier": "^6.9.0",
8484
"eslint-plugin-prettier": "^3.1.2",
85-
"eslint-plugin-react": "^7.17.0",
8685
"fp-ts": "^2.4.0",
8786
"husky": "^3.0.0",
8887
"io-ts": "^2.0.4",

src/index.spec.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as t from "io-ts";
2-
import { createServer, parse, createClient } from "./index";
3-
import { isLeft, isRight, either } from "fp-ts/lib/Either";
2+
import { createServer, parse, createClient, RpcError } from "./index";
3+
import { isLeft, isRight } from "fp-ts/lib/Either";
44

55
describe("json rpc", () => {
66
const methods = {
@@ -291,6 +291,15 @@ describe("json rpc", () => {
291291

292292
expect(result).toEqual(undefined);
293293
});
294+
295+
it("should throw rpc errors", async () => {
296+
await expect(
297+
client({
298+
method: "echo",
299+
params: {} as any
300+
})
301+
).rejects.toBeInstanceOf(RpcError);
302+
});
294303
});
295304

296305
describe("batch", () => {
@@ -321,6 +330,16 @@ describe("json rpc", () => {
321330

322331
expect(result).toEqual([undefined, "test", undefined]);
323332
});
333+
334+
it("should return rpc errors", async () => {
335+
const result = await client.batch({
336+
method: "echo",
337+
params: {} as any
338+
});
339+
340+
expect(result.length).toEqual(1);
341+
expect(result[0]).toBeInstanceOf(RpcError);
342+
});
324343
});
325344
});
326345
});

src/index.ts

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BaseError } from "make-error";
2-
import { TypeOf, Any, TypeC, Errors } from "io-ts";
2+
import { TypeOf, Any, TypeC } from "io-ts";
33
import { PathReporter } from "io-ts/lib/PathReporter";
44
import { either } from "fp-ts";
55

@@ -90,23 +90,18 @@ const METHOD_NOT_FOUND = { code: -32601, message: "Method not found" };
9090
const PARSE_ERROR = { code: -32700, message: "Parse error" };
9191

9292
/**
93-
* Create a custom RPC error to report issues.
93+
* Parse raw input into JSON.
9494
*/
95-
export class RpcError<T = void> extends BaseError {
96-
constructor(public message: string, public code = -32603, public data: T) {
97-
super(message);
98-
}
95+
export function parse(input: string): either.Either<JsonRpcError, unknown> {
96+
return either.parseJSON(input, () => PARSE_ERROR);
9997
}
10098

10199
/**
102-
* Parse raw input into JSON.
100+
* Create a custom RPC error to report issues.
103101
*/
104-
export function parse(input: string): either.Either<JsonRpcError, unknown> {
105-
try {
106-
const value = JSON.parse(input);
107-
return either.right(value);
108-
} catch (err) {
109-
return either.left(PARSE_ERROR);
102+
export class RpcError<T = void> extends BaseError {
103+
constructor(public message: string, public code = -32603, public data: T) {
104+
super(message);
110105
}
111106
}
112107

@@ -139,21 +134,15 @@ function failure<T = never>(
139134
return { jsonrpc: "2.0", error, id };
140135
}
141136

142-
/**
143-
* Ensure `key` is a property of `obj` in type-safe compatible way.
144-
*/
145-
function has<T extends string>(obj: Record<T, any>, key: string): key is T {
146-
return Object.prototype.hasOwnProperty.call(obj, key);
147-
}
148-
149137
/**
150138
* Validate RPC message is correctly formatted and type-safe.
151139
*/
152140
async function processRequest<T extends Methods, C = void>(
153141
methods: T,
154142
resolvers: Resolvers<T, C>,
155143
message: unknown,
156-
context: C
144+
context: C,
145+
options: ServerOptions
157146
): Promise<JsonRpcSuccess<any> | JsonRpcFailure<any> | undefined> {
158147
if (message === null || typeof message !== "object") {
159148
return failure(INVALID_REQUEST, null);
@@ -170,7 +159,7 @@ async function processRequest<T extends Methods, C = void>(
170159
return failure(INVALID_REQUEST, id ?? null);
171160
}
172161

173-
if (!has(methods, method)) {
162+
if (!(method in methods)) {
174163
return failure(METHOD_NOT_FOUND, id);
175164
}
176165

@@ -188,7 +177,8 @@ async function processRequest<T extends Methods, C = void>(
188177
return failure(INVALID_REQUEST, id);
189178
}
190179

191-
const input = request.decode(data);
180+
const input =
181+
options.decode === false ? either.right(data) : request.decode(data);
192182

193183
if (either.isLeft(input)) {
194184
return failure(
@@ -206,7 +196,7 @@ async function processRequest<T extends Methods, C = void>(
206196
try {
207197
const data = await resolvers[method](input.right, context, metadata);
208198
if (isNotification) return; // Do not encode response for notifications.
209-
return success(response.encode(data), id);
199+
return success(options.encode === false ? data : response.encode(data), id);
210200
} catch (err) {
211201
return failure(
212202
{
@@ -219,12 +209,23 @@ async function processRequest<T extends Methods, C = void>(
219209
}
220210
}
221211

212+
/**
213+
* Configure the server options.
214+
*/
215+
export interface ServerOptions {
216+
// Decodes the request before processing by resolvers.
217+
decode?: boolean;
218+
// Encodes the response before returning to client.
219+
encode?: boolean;
220+
}
221+
222222
/**
223223
* Create a JSON RPC request handler.
224224
*/
225225
export function createServer<T extends Methods, C = void>(
226226
methods: T,
227-
resolvers: Resolvers<T, C>
227+
resolvers: Resolvers<T, C>,
228+
options: ServerOptions = {}
228229
) {
229230
return async function rpcServer(payload: unknown, context: C) {
230231
if (Array.isArray(payload)) {
@@ -233,7 +234,9 @@ export function createServer<T extends Methods, C = void>(
233234
}
234235

235236
const results = await Promise.all(
236-
payload.map(x => processRequest(methods, resolvers, x, context))
237+
payload.map(x =>
238+
processRequest(methods, resolvers, x, context, options)
239+
)
237240
);
238241

239242
return results.filter((x): x is
@@ -243,7 +246,7 @@ export function createServer<T extends Methods, C = void>(
243246
});
244247
}
245248

246-
return processRequest(methods, resolvers, payload, context);
249+
return processRequest(methods, resolvers, payload, context, options);
247250
};
248251
}
249252

@@ -258,14 +261,23 @@ type ClientMethods<T extends Methods> = {
258261
};
259262
}[keyof T & string];
260263

264+
/**
265+
* Configure client options.
266+
*/
267+
export interface ClientOptions {
268+
encode?: boolean;
269+
decode?: boolean;
270+
}
271+
261272
/**
262273
* Create a JSON RPC request client.
263274
*/
264275
export function createClient<T extends Methods>(
265276
methods: T,
266277
send: (
267278
rpc: JsonRpcRequest<string, unknown> | JsonRpcRequest<string, unknown>[]
268-
) => Promise<unknown>
279+
) => Promise<unknown>,
280+
options: ClientOptions = {}
269281
) {
270282
let counter = 0;
271283
const jsonrpc = "2.0";
@@ -276,26 +288,29 @@ export function createClient<T extends Methods>(
276288

277289
return {
278290
method,
279-
params: request.encode(params),
291+
params: options.encode === false ? params : request.encode(params),
280292
id: async ? undefined : counter++,
281293
process: (body: unknown): unknown => {
282294
if (body === undefined) {
283295
if (async) return undefined;
284296

285-
throw new RpcError("Invalid response", -1, undefined);
297+
return new RpcError("Invalid response", -1, undefined);
286298
}
287299

288300
if (body === null || typeof body !== "object" || Array.isArray(body)) {
289-
throw new RpcError("Invalid response", -1, undefined);
301+
return new RpcError("Invalid response", -1, undefined);
290302
}
291303

292-
const { result, error } = body as Record<string, unknown>;
304+
const { result, error } = body as Record<string, any>;
293305

294306
if (result) {
295-
const output = response.decode(result);
307+
const output =
308+
options.decode === false
309+
? either.right(result)
310+
: response.decode(result);
296311

297312
if (either.isLeft(output)) {
298-
throw new RpcError(
313+
return new RpcError(
299314
PathReporter.report(output).join("; "),
300315
-2,
301316
output.left
@@ -310,11 +325,14 @@ export function createClient<T extends Methods>(
310325
typeof error !== "object" ||
311326
Array.isArray(error)
312327
) {
313-
throw new RpcError("Invalid response", -1, undefined);
328+
return new RpcError("Invalid response", -1, undefined);
314329
}
315330

316-
const { message, code, data } = error as Record<string, unknown>;
317-
throw new RpcError(String(message || "Error"), Number(code) || 0, data);
331+
return new RpcError(
332+
String(error.message || "Error"),
333+
Number(error.code) || 0,
334+
error.data
335+
);
318336
}
319337
};
320338
}
@@ -326,17 +344,19 @@ export function createClient<T extends Methods>(
326344
> {
327345
const { params, id, method, process } = prepare(payload);
328346
const data = await send({ jsonrpc, method, params, id });
329-
return process(data) as any;
347+
const response = process(data) as any;
348+
if (response instanceof RpcError) throw response; // Throw RPC errors.
349+
return response;
330350
}
331351

332-
rpcClient.batch = async <U extends ClientMethods<T>[]>(
333-
...payload: U
352+
rpcClient.many = async <U extends ClientMethods<T>[]>(
353+
payload: U
334354
): Promise<
335355
{
336356
[K in keyof U]: U[K] extends ClientMethods<T>
337357
? U[K]["async"] extends true
338358
? undefined
339-
: TypeOf<T[U[K]["method"]]["response"]>
359+
: TypeOf<T[U[K]["method"]]["response"]> | RpcError
340360
: U[K];
341361
}
342362
> => {
@@ -362,5 +382,9 @@ export function createClient<T extends Methods>(
362382
return items.map(item => item.process(lookup.get(item.id))) as any;
363383
};
364384

385+
rpcClient.batch = async <U extends ClientMethods<T>[]>(...payload: U) => {
386+
return rpcClient.many(payload);
387+
};
388+
365389
return rpcClient;
366390
}

0 commit comments

Comments
 (0)