├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── package-lock.json ├── package.json ├── src ├── index.spec.ts └── index.ts ├── tsconfig.es2015.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_size = 2 7 | indent_style = space 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .DS_Store 4 | npm-debug.log 5 | dist/ 6 | dist.es2015/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | node_js: 9 | - "10" 10 | - stable 11 | 12 | after_script: 13 | - npm install coveralls@2 14 | - cat ./coverage/lcov.info | coveralls 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Blake Embrey (hello@blakeembrey.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON RPC 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![NPM downloads][downloads-image]][downloads-url] 5 | [![Build status][travis-image]][travis-url] 6 | [![Test coverage][coveralls-image]][coveralls-url] 7 | 8 | > Tiny, mostly compliant [JSON-RPC 2.0](https://www.jsonrpc.org/specification) implementation. 9 | 10 | This package intentionally doesn't implement the "arguments" form of request parameters. This is when the input `params` can be an object or an ordered array representing the object. Instead, you can pass _any_ JSON params over the wire. 11 | 12 | ## Installation 13 | 14 | ```sh 15 | npm install @borderless/json-rpc --save 16 | ``` 17 | 18 | ## Usage 19 | 20 | This package makes no assumptions about the transportation layer, for client or server. 21 | 22 | ### Methods 23 | 24 | ```ts 25 | type Methods = { 26 | hello: { 27 | request: {}; 28 | response: string; 29 | }; 30 | echo: { 31 | request: { arg: string }; 32 | response: string; 33 | }; 34 | }; 35 | ``` 36 | 37 | ### Server 38 | 39 | The server accepts a dictionary of resolvers. 40 | 41 | ```ts 42 | import { createServer } from "@borderless/json-rpc"; 43 | 44 | const server = createServer({ 45 | hello: (_) => "Hello World!", 46 | echo: ({ arg }) => arg, 47 | }); 48 | 49 | const res = await server({ 50 | jsonrpc: "2.0", 51 | id: "test", 52 | method: "hello", 53 | }); //=> { jsonrpc: "2.0", id: "test", result: "Hello World!" } 54 | ``` 55 | 56 | ### Client 57 | 58 | The client accepts a function to `send` the JSON-RPC request. 59 | 60 | ```ts 61 | import { createClient } from "@borderless/json-rpc"; 62 | 63 | const client = createClient(async (payload) => { 64 | const res = await fetch("...", { 65 | method: "POST", 66 | body: JSON.stringify(payload), 67 | headers: { 68 | "Content-Type": "application/json", 69 | }, 70 | }); 71 | 72 | return res.json(); 73 | }); 74 | 75 | const result = await client({ 76 | method: "hello", 77 | params: {}, 78 | }); //=> "Hello World!" 79 | 80 | const results = await client.many([ 81 | { 82 | method: "hello", 83 | params: {}, 84 | }, 85 | { 86 | method: "echo", 87 | params: { arg: "Test" }, 88 | }, 89 | ]); //=> ["Hello World!", "Test"] 90 | ``` 91 | 92 | ## License 93 | 94 | MIT 95 | 96 | [npm-image]: https://img.shields.io/npm/v/@borderless/json-rpc.svg?style=flat 97 | [npm-url]: https://npmjs.org/package/@borderless/json-rpc 98 | [downloads-image]: https://img.shields.io/npm/dm/@borderless/json-rpc.svg?style=flat 99 | [downloads-url]: https://npmjs.org/package/@borderless/json-rpc 100 | [travis-image]: https://img.shields.io/travis/borderless/json-rpc.svg?style=flat 101 | [travis-url]: https://travis-ci.org/borderless/json-rpc 102 | [coveralls-image]: https://img.shields.io/coveralls/borderless/json-rpc.svg?style=flat 103 | [coveralls-url]: https://coveralls.io/r/borderless/json-rpc?branch=master 104 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security contact information 4 | 5 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@borderless/json-rpc", 3 | "version": "3.0.1", 4 | "description": "Tiny, type-safe JSON-RPC 2.0 implementation", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "module": "dist.es2015/index.js", 8 | "sideEffects": false, 9 | "jsnext:main": "dist.es2015/index.js", 10 | "files": [ 11 | "dist/", 12 | "dist.es2015/" 13 | ], 14 | "scripts": { 15 | "lint": "ts-scripts lint", 16 | "format": "ts-scripts format", 17 | "specs": "ts-scripts specs", 18 | "test": "ts-scripts test", 19 | "prepare": "ts-scripts install && ts-scripts build" 20 | }, 21 | "ts-scripts": { 22 | "project": [ 23 | "tsconfig.json", 24 | "tsconfig.es2015.json" 25 | ], 26 | "dist": [ 27 | "dist", 28 | "dist.es2015" 29 | ] 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/borderless/json-rpc.git" 34 | }, 35 | "keywords": [ 36 | "json", 37 | "rpc", 38 | "type", 39 | "safe", 40 | "typescript" 41 | ], 42 | "author": { 43 | "name": "Blake Embrey", 44 | "email": "hello@blakeembrey.com", 45 | "url": "http://blakeembrey.me" 46 | }, 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/borderless/json-rpc/issues" 50 | }, 51 | "homepage": "https://github.com/borderless/json-rpc", 52 | "publishConfig": { 53 | "access": "public" 54 | }, 55 | "engines": { 56 | "node": ">=10" 57 | }, 58 | "devDependencies": { 59 | "@borderless/ts-scripts": "^0.0.8", 60 | "@types/jest": "^26.0.20", 61 | "@types/node": "^14.14.27", 62 | "ts-expect": "^1.3.0", 63 | "typescript": "^4.1.5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { expectType, TypeEqual } from "ts-expect"; 2 | import { 3 | createServer, 4 | parseJSON, 5 | createClient, 6 | RpcError, 7 | Resolvers, 8 | ClientRequest, 9 | ClientResponse, 10 | } from "./index"; 11 | 12 | describe("json rpc", () => { 13 | type Methods = { 14 | hello: { 15 | request: undefined; 16 | response: string; 17 | }; 18 | echo: { 19 | request: { arg: string }; 20 | response: string; 21 | }; 22 | }; 23 | 24 | const resolvers: Resolvers = { 25 | hello: () => "Hello World!", 26 | echo: ({ arg }) => arg, 27 | }; 28 | 29 | const server = createServer(resolvers); 30 | const client = createClient((x) => server(x, undefined)); 31 | 32 | describe("types", () => { 33 | type Requests = ClientRequest; 34 | type Responses = ClientResponse; 35 | 36 | expectType>(true); 37 | }); 38 | 39 | describe("parse", () => { 40 | it("should parse json", () => { 41 | expect(() => parseJSON("{}")).not.toThrow(); 42 | }); 43 | 44 | it("should fail to parse malformed json", () => { 45 | expect(() => parseJSON("[")).toThrow(RpcError); 46 | }); 47 | }); 48 | 49 | describe("server", () => { 50 | describe("object", () => { 51 | it("should respond", async () => { 52 | const res = await server({ 53 | jsonrpc: "2.0", 54 | id: "test", 55 | method: "hello", 56 | }); 57 | 58 | expect(res).toEqual({ 59 | jsonrpc: "2.0", 60 | id: "test", 61 | result: "Hello World!", 62 | }); 63 | }); 64 | 65 | it("should not respond to notification", async () => { 66 | const res = await server({ 67 | jsonrpc: "2.0", 68 | method: "hello", 69 | }); 70 | 71 | expect(res).toEqual(undefined); 72 | }); 73 | 74 | it("should accept an object of parameters", async () => { 75 | const res = await server({ 76 | jsonrpc: "2.0", 77 | id: "test", 78 | method: "echo", 79 | params: { arg: "test" }, 80 | }); 81 | 82 | expect(res).toEqual({ 83 | jsonrpc: "2.0", 84 | id: "test", 85 | result: "test", 86 | }); 87 | }); 88 | }); 89 | 90 | describe("array", () => { 91 | it("should respond", async () => { 92 | const res = await server([ 93 | { 94 | jsonrpc: "2.0", 95 | id: 1, 96 | method: "hello", 97 | }, 98 | { 99 | jsonrpc: "2.0", 100 | id: 2, 101 | method: "hello", 102 | }, 103 | ]); 104 | 105 | expect(res).toEqual([ 106 | { 107 | jsonrpc: "2.0", 108 | id: 1, 109 | result: "Hello World!", 110 | }, 111 | { 112 | jsonrpc: "2.0", 113 | id: 2, 114 | result: "Hello World!", 115 | }, 116 | ]); 117 | }); 118 | }); 119 | 120 | describe("invalid", () => { 121 | it("should fail on malformed request", async () => { 122 | const res = await server(123); 123 | 124 | expect(res).toEqual({ 125 | jsonrpc: "2.0", 126 | id: null, 127 | error: { 128 | code: -32600, 129 | message: "Invalid request", 130 | }, 131 | }); 132 | }); 133 | 134 | it("should fail on malformed request object", async () => { 135 | const res = await server({}); 136 | 137 | expect(res).toEqual({ 138 | jsonrpc: "2.0", 139 | id: null, 140 | error: { 141 | code: -32600, 142 | message: "Invalid request", 143 | }, 144 | }); 145 | }); 146 | 147 | it("should fail on empty batch request", async () => { 148 | const res = await server([]); 149 | 150 | expect(res).toEqual({ 151 | jsonrpc: "2.0", 152 | id: null, 153 | error: { 154 | code: -32600, 155 | message: "Invalid request", 156 | }, 157 | }); 158 | }); 159 | 160 | it("should fail on invalid request rpc id", async () => { 161 | const res = await server({ 162 | jsonrpc: "2.0", 163 | id: {}, 164 | }); 165 | 166 | expect(res).toEqual({ 167 | jsonrpc: "2.0", 168 | id: null, 169 | error: { 170 | code: -32600, 171 | message: "Invalid request", 172 | }, 173 | }); 174 | }); 175 | 176 | it("should fail on fractional numeric request rpc id", async () => { 177 | const res = await server({ 178 | jsonrpc: "2.0", 179 | id: 123.5, 180 | }); 181 | 182 | expect(res).toEqual({ 183 | jsonrpc: "2.0", 184 | id: null, 185 | error: { 186 | code: -32600, 187 | message: "Invalid request", 188 | }, 189 | }); 190 | }); 191 | 192 | it("should fail on method not found", async () => { 193 | const res = await server({ 194 | jsonrpc: "2.0", 195 | id: "test", 196 | method: "missing", 197 | }); 198 | 199 | expect(res).toEqual({ 200 | jsonrpc: "2.0", 201 | id: "test", 202 | error: { 203 | code: -32601, 204 | message: "Method not found", 205 | }, 206 | }); 207 | }); 208 | }); 209 | }); 210 | 211 | describe("client", () => { 212 | describe("request", () => { 213 | it("should make a request", async () => { 214 | const result = await client({ method: "hello", params: undefined }); 215 | 216 | expect(result).toEqual("Hello World!"); 217 | }); 218 | 219 | it("should make a notification request", async () => { 220 | const result = await client({ 221 | method: "hello", 222 | params: undefined, 223 | async: true, 224 | }); 225 | 226 | expect(result).toEqual(undefined); 227 | }); 228 | 229 | it("should throw rpc errors", async () => { 230 | await expect( 231 | client({ 232 | method: "echo", 233 | params: {} as any, 234 | }) 235 | ).rejects.toBeInstanceOf(RpcError); 236 | }); 237 | 238 | describe("with send context", () => { 239 | const client = createClient(async (_, context: string) => ({ 240 | result: context, 241 | })); 242 | 243 | it("should accept options", async () => { 244 | const result = await client( 245 | { method: "hello", params: undefined }, 246 | "Test" 247 | ); 248 | 249 | expect(result).toEqual("Test"); 250 | }); 251 | }); 252 | }); 253 | 254 | describe("many", () => { 255 | it("should make a many request", async () => { 256 | const result = await client.many([ 257 | { method: "hello", params: undefined }, 258 | { method: "echo", params: { arg: "test" } }, 259 | ]); 260 | 261 | expect(result).toEqual(["Hello World!", "test"]); 262 | }); 263 | 264 | it("should make a many notification request", async () => { 265 | const result = await client.many([ 266 | { method: "hello", params: undefined, async: true }, 267 | { method: "echo", params: { arg: "test" }, async: true }, 268 | ]); 269 | 270 | expect(result).toEqual([undefined, undefined]); 271 | }); 272 | 273 | it("should handle mixed many responses", async () => { 274 | const result = await client.many([ 275 | { method: "hello", params: undefined, async: true }, 276 | { method: "echo", params: { arg: "test" } }, 277 | { method: "hello", params: undefined, async: true }, 278 | ]); 279 | 280 | expect(result).toEqual([undefined, "test", undefined]); 281 | }); 282 | 283 | it("should return rpc errors", async () => { 284 | const result = await client.many([ 285 | { 286 | method: "echo", 287 | params: {} as any, 288 | }, 289 | ]); 290 | 291 | expect(result.length).toEqual(1); 292 | expect(result[0]).toBeInstanceOf(RpcError); 293 | }); 294 | }); 295 | }); 296 | }); 297 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from "make-error"; 2 | 3 | export interface Method { 4 | request: T; 5 | response: U; 6 | } 7 | 8 | export type Methods = Record>; 9 | 10 | /** 11 | * Metadata for JSON-RPC requests. 12 | */ 13 | export interface Metadata { 14 | id: JsonRpcId; 15 | isNotification: boolean; 16 | } 17 | 18 | /** 19 | * RPC method resolver. 20 | */ 21 | export type Resolver = ( 22 | req: T, 23 | context: C, 24 | meta: Metadata 25 | ) => U | Promise; 26 | 27 | /** 28 | * Implementation of methods. 29 | */ 30 | export type Resolvers = { 31 | [K in keyof T]: Resolver; 32 | }; 33 | 34 | /** 35 | * JSON RPC request. 36 | */ 37 | export interface JsonRpcRequest { 38 | jsonrpc: "2.0"; 39 | id?: JsonRpcId; 40 | method: T; 41 | params: U; 42 | } 43 | 44 | /** 45 | * JSON RPC error. 46 | */ 47 | export interface JsonRpcError { 48 | code: number; 49 | message: string; 50 | data: T; 51 | } 52 | 53 | /** 54 | * Valid JSON RPC IDs (`undefined` denotes no notification). 55 | */ 56 | export type JsonRpcId = string | number | null | undefined; 57 | 58 | /** 59 | * JSON RPC response. 60 | */ 61 | export interface JsonRpcResponse { 62 | jsonrpc: "2.0"; 63 | id: JsonRpcId; 64 | } 65 | 66 | /** 67 | * The JSON RPC success interface defines a response object. 68 | */ 69 | export interface JsonRpcSuccess extends JsonRpcResponse { 70 | result: T; 71 | } 72 | 73 | /** 74 | * The JSON RPC failure interface occurs on error. 75 | */ 76 | export interface JsonRpcFailure extends JsonRpcResponse { 77 | error: JsonRpcError; 78 | } 79 | 80 | /** 81 | * Create a custom RPC error to report issues. 82 | */ 83 | export class RpcError extends BaseError implements JsonRpcError { 84 | constructor(public message: string, public code = -32603, public data: T) { 85 | super(message); 86 | } 87 | } 88 | 89 | export class InvalidRequestError extends RpcError { 90 | constructor() { 91 | super("Invalid request", -32600); 92 | } 93 | } 94 | 95 | export class MethodNotFoundError extends RpcError { 96 | constructor() { 97 | super("Method not found", -32601); 98 | } 99 | } 100 | 101 | export class ParseError extends RpcError { 102 | constructor(message: string) { 103 | super(message, -32700); 104 | } 105 | } 106 | 107 | /** 108 | * Parse raw input into JSON. 109 | */ 110 | export function parseJSON(input: string): unknown { 111 | try { 112 | return JSON.parse(input); 113 | } catch (err) { 114 | throw new ParseError(err.message); 115 | } 116 | } 117 | 118 | /** 119 | * Check if JSON RPC ID is valid. 120 | */ 121 | function isJsonRpcId(input: unknown): input is JsonRpcId { 122 | if (input === null || input === undefined) return true; 123 | if (typeof input === "string") return true; 124 | if (typeof input === "number") return input % 1 === 0; 125 | return false; 126 | } 127 | 128 | /** 129 | * Wrap a result in JSON RPC success response. 130 | */ 131 | function success(result: T, id: JsonRpcId): JsonRpcSuccess | undefined { 132 | if (id === undefined) return; 133 | return { jsonrpc: "2.0", result, id }; 134 | } 135 | 136 | /** 137 | * Wrap an error in JSON RPC failure response. 138 | */ 139 | function failure( 140 | error: JsonRpcError, 141 | id: JsonRpcId 142 | ): JsonRpcFailure | undefined { 143 | if (id === undefined) return; 144 | const { code, message, data } = error; 145 | return { jsonrpc: "2.0", error: { code, message, data }, id }; 146 | } 147 | 148 | /** 149 | * Validate RPC message is correctly formatted and type-safe. 150 | */ 151 | async function processRequest( 152 | resolvers: Resolvers, 153 | message: unknown, 154 | context: C 155 | ): Promise | JsonRpcFailure | undefined> { 156 | if (message === null || typeof message !== "object") { 157 | return failure(new InvalidRequestError(), null); 158 | } 159 | 160 | const { jsonrpc, method, id, params } = message as Record; 161 | const isNotification = id === undefined; 162 | 163 | if (!isJsonRpcId(id)) { 164 | return failure(new InvalidRequestError(), null); 165 | } 166 | 167 | if (jsonrpc !== "2.0" || typeof method !== "string") { 168 | return failure(new InvalidRequestError(), id ?? null); 169 | } 170 | 171 | if (!(method in resolvers)) { 172 | return failure(new MethodNotFoundError(), id); 173 | } 174 | 175 | // Metadata object used for request information. 176 | const metadata: Metadata = { id, isNotification }; 177 | 178 | try { 179 | const data = await resolvers[method](params, context, metadata); 180 | if (isNotification) return; // Do not encode response for notifications. 181 | return success(data, id); 182 | } catch (err) { 183 | return failure( 184 | { 185 | code: typeof err.code === "number" ? err.code : -32603, 186 | message: err.message || "Internal error", 187 | data: err.data, 188 | }, 189 | id 190 | ); 191 | } 192 | } 193 | 194 | /** 195 | * Create a JSON RPC request handler. 196 | */ 197 | export function createServer( 198 | resolvers: Resolvers 199 | ) { 200 | return async function rpcServer(payload: unknown, context: C) { 201 | if (Array.isArray(payload)) { 202 | if (payload.length === 0) { 203 | return failure(new InvalidRequestError(), null); 204 | } 205 | 206 | const results = await Promise.all( 207 | payload.map((x) => processRequest(resolvers, x, context)) 208 | ); 209 | 210 | return results.filter((x): x is 211 | | JsonRpcSuccess 212 | | JsonRpcFailure => { 213 | return x !== undefined; 214 | }); 215 | } 216 | 217 | return processRequest(resolvers, payload, context); 218 | }; 219 | } 220 | 221 | /** 222 | * Map methods to valid client methods. 223 | */ 224 | export type ClientRequest = { 225 | [K in keyof T]: { 226 | method: K; 227 | params: T[K]["request"]; 228 | async?: boolean; 229 | }; 230 | }[keyof T & string]; 231 | 232 | /** 233 | * Map client requests to response types. 234 | */ 235 | export type ClientResponse< 236 | T extends Methods, 237 | P extends ClientRequest 238 | > = P["async"] extends true ? undefined : T[P["method"]]["response"]; 239 | 240 | /** 241 | * Create a JSON RPC request client. 242 | */ 243 | export function createClient( 244 | send: ( 245 | data: JsonRpcRequest | JsonRpcRequest[], 246 | context: C 247 | ) => Promise 248 | ) { 249 | let counter = 0; 250 | const jsonrpc = "2.0"; 251 | 252 | function prepare>(payload: U) { 253 | const { method, params, async } = payload; 254 | const id = async ? undefined : counter++; 255 | 256 | const process = (body: unknown): unknown => { 257 | if (body === undefined) { 258 | if (async) return undefined; 259 | 260 | return new RpcError("Invalid response", -1, undefined); 261 | } 262 | 263 | if (body === null || typeof body !== "object" || Array.isArray(body)) { 264 | return new RpcError("Invalid response", -1, undefined); 265 | } 266 | 267 | const { result, error } = body as Record; 268 | 269 | if (result === undefined && error === undefined) { 270 | return new RpcError("Invalid response", -1, undefined); 271 | } 272 | 273 | if (error !== undefined) { 274 | return new RpcError( 275 | String(error?.message || "Error"), 276 | Number(error?.code) || 0, 277 | error?.data 278 | ); 279 | } 280 | 281 | return result; 282 | }; 283 | 284 | return { method, params, id, process }; 285 | } 286 | 287 | async function rpcClient

>( 288 | payload: P, 289 | context: C 290 | ): Promise> { 291 | const { params, id, method, process } = prepare(payload); 292 | const data = await send({ jsonrpc, method, params, id }, context); 293 | const response = process(data) as any; 294 | if (response instanceof RpcError) throw response; // Throw RPC errors. 295 | return response; 296 | } 297 | 298 | rpcClient.many = async

[]>( 299 | payload: P, 300 | context: C 301 | ): Promise< 302 | { 303 | [K in keyof P]: K extends number 304 | ? ClientResponse | RpcError 305 | : P[K]; 306 | } 307 | > => { 308 | const items = payload.map(prepare); 309 | 310 | const data = await send( 311 | items.map( 312 | ({ method, params, id }): JsonRpcRequest => ({ 313 | jsonrpc, 314 | method, 315 | params, 316 | id, 317 | }) 318 | ), 319 | context 320 | ); 321 | 322 | if (!Array.isArray(data)) { 323 | throw new RpcError("Invalid response", -1, undefined); 324 | } 325 | 326 | // Return items in the order they were sent. 327 | const lookup = new Map(data.map((data) => [data.id, data])); 328 | return items.map((item) => item.process(lookup.get(item.id))) as any; 329 | }; 330 | 331 | return rpcClient; 332 | } 333 | -------------------------------------------------------------------------------- /tsconfig.es2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist.es2015", 5 | "module": "es2015", 6 | "declaration": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@borderless/ts-scripts/configs/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "types": ["node", "jest"] 7 | } 8 | } 9 | --------------------------------------------------------------------------------