Merge pull request #248 from CosmWasm/247-fork-jsonrpc
Fork @iov/jsonrpc
This commit is contained in:
commit
06142599ff
2
NOTICE
2
NOTICE
@ -19,6 +19,8 @@ on 2020-06-05.
|
||||
|
||||
The code in packages/tendermint-rpc and scripts/tendermint was forked from the folders packages/iov-tendermint-rpc and scripts/tendermint respectively of https://github.com/iov-one/iov-core at tag v2.5.0 on 2020-06-15.
|
||||
|
||||
The code in packages/json-rpc was forked from https://github.com/iov-one/iov-core/tree/v2.5.0/packages/iov-jsonrpc, with additional code from https://github.com/iov-one/iov-core/tree/v2.5.0/packages/iov-encoding on 2020-06-24.
|
||||
|
||||
Copyright 2018-2020 IOV SAS
|
||||
Copyright 2020 Confio UO
|
||||
Copyright 2020 Simon Warta
|
||||
|
||||
1
packages/json-rpc/.eslintignore
Symbolic link
1
packages/json-rpc/.eslintignore
Symbolic link
@ -0,0 +1 @@
|
||||
../../.eslintignore
|
||||
3
packages/json-rpc/.gitignore
vendored
Normal file
3
packages/json-rpc/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
build/
|
||||
dist/
|
||||
docs/
|
||||
12
packages/json-rpc/README.md
Normal file
12
packages/json-rpc/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# @cosmjs/json-rpc
|
||||
|
||||
[](https://www.npmjs.com/package/@cosmjs/json-rpc)
|
||||
|
||||
This package provides a light framework for implementing a
|
||||
[JSON-RPC 2.0 API](https://www.jsonrpc.org/specification).
|
||||
|
||||
## License
|
||||
|
||||
This package is part of the cosmjs repository, licensed under the Apache License
|
||||
2.0 (see [NOTICE](https://github.com/CosmWasm/cosmjs/blob/master/NOTICE) and
|
||||
[LICENSE](https://github.com/CosmWasm/cosmjs/blob/master/LICENSE)).
|
||||
26
packages/json-rpc/jasmine-testrunner.js
Executable file
26
packages/json-rpc/jasmine-testrunner.js
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require("source-map-support").install();
|
||||
const defaultSpecReporterConfig = require("../../jasmine-spec-reporter.config.json");
|
||||
|
||||
// setup Jasmine
|
||||
const Jasmine = require("jasmine");
|
||||
const jasmine = new Jasmine();
|
||||
jasmine.loadConfig({
|
||||
spec_dir: "build",
|
||||
spec_files: ["**/*.spec.js"],
|
||||
helpers: [],
|
||||
random: false,
|
||||
seed: null,
|
||||
stopSpecOnExpectationFailure: false,
|
||||
});
|
||||
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 1000;
|
||||
|
||||
// setup reporter
|
||||
const { SpecReporter } = require("jasmine-spec-reporter");
|
||||
const reporter = new SpecReporter({ ...defaultSpecReporterConfig });
|
||||
|
||||
// initialize and execute
|
||||
jasmine.env.clearReporters();
|
||||
jasmine.addReporter(reporter);
|
||||
jasmine.execute();
|
||||
56
packages/json-rpc/karma.conf.js
Normal file
56
packages/json-rpc/karma.conf.js
Normal file
@ -0,0 +1,56 @@
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: ".",
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ["jasmine"],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
"dist/web/tests.js",
|
||||
{
|
||||
pattern: "dist/web/dummyservice.worker.js",
|
||||
included: false,
|
||||
served: true,
|
||||
watched: false,
|
||||
nocache: true,
|
||||
},
|
||||
],
|
||||
|
||||
client: {
|
||||
jasmine: {
|
||||
random: false,
|
||||
timeoutInterval: 15000,
|
||||
},
|
||||
},
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ["progress", "kjhtml"],
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false,
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ["Firefox"],
|
||||
|
||||
browserNoActivityTimeout: 30000,
|
||||
|
||||
// Keep brower open for debugging. This is overridden by yarn scripts
|
||||
singleRun: false,
|
||||
});
|
||||
};
|
||||
1
packages/json-rpc/nonces/README.txt
Normal file
1
packages/json-rpc/nonces/README.txt
Normal file
@ -0,0 +1 @@
|
||||
Directory used to trigger lerna package updates for all packages
|
||||
49
packages/json-rpc/package.json
Normal file
49
packages/json-rpc/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@cosmjs/json-rpc",
|
||||
"version": "0.20.0",
|
||||
"description": "Framework for implementing a JSON-RPC 2.0 API",
|
||||
"contributors": [
|
||||
"IOV SAS <admin@iov.one>",
|
||||
"Confio UO <hello@confio.tech>",
|
||||
"Will Clark <willclarktech@users.noreply.github.com>"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "build/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"build/",
|
||||
"types/",
|
||||
"*.md",
|
||||
"!*.spec.*",
|
||||
"!**/testdata/"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CosmWasm/cosmjs/tree/master/packages/json-rpc"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"docs": "shx rm -rf docs && typedoc --options typedoc.js",
|
||||
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\"",
|
||||
"lint-fix": "eslint --max-warnings 0 \"**/*.{js,ts}\" --fix",
|
||||
"format": "prettier --write --loglevel warn \"./src/**/*.ts\"",
|
||||
"format-text": "prettier --write --prose-wrap always --print-width 80 \"./*.md\"",
|
||||
"test-node": "node jasmine-testrunner.js",
|
||||
"test-edge": "yarn pack-web && karma start --single-run --browsers Edge",
|
||||
"test-firefox": "yarn pack-web && karma start --single-run --browsers Firefox",
|
||||
"test-chrome": "yarn pack-web && karma start --single-run --browsers ChromeHeadless",
|
||||
"test-safari": "yarn pack-web && karma start --single-run --browsers Safari",
|
||||
"test": "yarn build-or-skip && yarn test-node",
|
||||
"move-types": "shx rm -r ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts",
|
||||
"format-types": "prettier --write --loglevel warn \"./types/**/*.d.ts\"",
|
||||
"build": "shx rm -rf ./build && tsc && tsc -p tsconfig.workers.json && yarn move-types && yarn format-types",
|
||||
"build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build",
|
||||
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iov/stream": "^2.3.2",
|
||||
"xstream": "^11.10.0"
|
||||
}
|
||||
}
|
||||
125
packages/json-rpc/src/compatibility.spec.ts
Normal file
125
packages/json-rpc/src/compatibility.spec.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { isJsonCompatibleArray, isJsonCompatibleDictionary, isJsonCompatibleValue } from "./compatibility";
|
||||
|
||||
describe("json", () => {
|
||||
function sum(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
describe("isJsonCompatibleValue", () => {
|
||||
it("returns true for primitive types", () => {
|
||||
expect(isJsonCompatibleValue(null)).toEqual(true);
|
||||
expect(isJsonCompatibleValue(0)).toEqual(true);
|
||||
expect(isJsonCompatibleValue(1)).toEqual(true);
|
||||
expect(isJsonCompatibleValue("abc")).toEqual(true);
|
||||
expect(isJsonCompatibleValue(true)).toEqual(true);
|
||||
expect(isJsonCompatibleValue(false)).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true for arrays", () => {
|
||||
expect(isJsonCompatibleValue([1, 2, 3])).toEqual(true);
|
||||
expect(isJsonCompatibleValue([1, "2", true, null])).toEqual(true);
|
||||
expect(isJsonCompatibleValue([1, "2", true, null, [1, "2", true, null]])).toEqual(true);
|
||||
expect(isJsonCompatibleValue([{ a: 123 }])).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true for simple dicts", () => {
|
||||
expect(isJsonCompatibleValue({ a: 123 })).toEqual(true);
|
||||
expect(isJsonCompatibleValue({ a: "abc" })).toEqual(true);
|
||||
expect(isJsonCompatibleValue({ a: true })).toEqual(true);
|
||||
expect(isJsonCompatibleValue({ a: null })).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true for dict with array", () => {
|
||||
expect(isJsonCompatibleValue({ a: [1, 2, 3] })).toEqual(true);
|
||||
expect(isJsonCompatibleValue({ a: [1, "2", true, null] })).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true for nested dicts", () => {
|
||||
expect(isJsonCompatibleValue({ a: { b: 123 } })).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns false for functions", () => {
|
||||
expect(isJsonCompatibleValue(sum)).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns true for empty dicts", () => {
|
||||
expect(isJsonCompatibleValue({})).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isJsonCompatibleArray", () => {
|
||||
it("returns false for primitive types", () => {
|
||||
expect(isJsonCompatibleArray(null)).toEqual(false);
|
||||
expect(isJsonCompatibleArray(undefined)).toEqual(false);
|
||||
expect(isJsonCompatibleArray(0)).toEqual(false);
|
||||
expect(isJsonCompatibleArray(1)).toEqual(false);
|
||||
expect(isJsonCompatibleArray("abc")).toEqual(false);
|
||||
expect(isJsonCompatibleArray(true)).toEqual(false);
|
||||
expect(isJsonCompatibleArray(false)).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns true for arrays", () => {
|
||||
expect(isJsonCompatibleArray([1, 2, 3])).toEqual(true);
|
||||
expect(isJsonCompatibleArray([1, "2", true, null])).toEqual(true);
|
||||
expect(isJsonCompatibleArray([1, "2", true, null, [1, "2", true, null]])).toEqual(true);
|
||||
expect(isJsonCompatibleArray([{ a: 123 }])).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns false for dicts", () => {
|
||||
expect(isJsonCompatibleArray({ a: 123 })).toEqual(false);
|
||||
expect(isJsonCompatibleArray({ a: "abc" })).toEqual(false);
|
||||
expect(isJsonCompatibleArray({ a: true })).toEqual(false);
|
||||
expect(isJsonCompatibleArray({ a: null })).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false for functions", () => {
|
||||
expect(isJsonCompatibleArray(sum)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isJsonCompatibleDictionary", () => {
|
||||
it("returns false for primitive types", () => {
|
||||
expect(isJsonCompatibleDictionary(null)).toEqual(false);
|
||||
expect(isJsonCompatibleDictionary(undefined)).toEqual(false);
|
||||
expect(isJsonCompatibleDictionary(0)).toEqual(false);
|
||||
expect(isJsonCompatibleDictionary(1)).toEqual(false);
|
||||
expect(isJsonCompatibleDictionary("abc")).toEqual(false);
|
||||
expect(isJsonCompatibleDictionary(true)).toEqual(false);
|
||||
expect(isJsonCompatibleDictionary(false)).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false for other objects", () => {
|
||||
expect(isJsonCompatibleDictionary(new Uint8Array([0x00]))).toEqual(false);
|
||||
expect(isJsonCompatibleDictionary(/123/)).toEqual(false);
|
||||
expect(isJsonCompatibleDictionary(new Date())).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false for arrays", () => {
|
||||
expect(isJsonCompatibleDictionary([1, 2, 3])).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false for functions", () => {
|
||||
expect(isJsonCompatibleDictionary(sum)).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns true for empty dicts", () => {
|
||||
expect(isJsonCompatibleDictionary({})).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true for simple dicts", () => {
|
||||
expect(isJsonCompatibleDictionary({ a: 123 })).toEqual(true);
|
||||
expect(isJsonCompatibleDictionary({ a: "abc" })).toEqual(true);
|
||||
expect(isJsonCompatibleDictionary({ a: true })).toEqual(true);
|
||||
expect(isJsonCompatibleDictionary({ a: null })).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true for dict with array", () => {
|
||||
expect(isJsonCompatibleDictionary({ a: [1, 2, 3] })).toEqual(true);
|
||||
expect(isJsonCompatibleDictionary({ a: [1, "2", true, null] })).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns true for nested dicts", () => {
|
||||
expect(isJsonCompatibleDictionary({ a: { b: 123 } })).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
73
packages/json-rpc/src/compatibility.ts
Normal file
73
packages/json-rpc/src/compatibility.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* A single JSON value. This is the missing return type of JSON.parse().
|
||||
*/
|
||||
export type JsonCompatibleValue =
|
||||
| JsonCompatibleDictionary
|
||||
| JsonCompatibleArray
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
|
||||
/**
|
||||
* An array of JsonCompatibleValue
|
||||
*/
|
||||
// Use interface extension instead of type alias to make circular declaration possible.
|
||||
export interface JsonCompatibleArray extends ReadonlyArray<JsonCompatibleValue> {}
|
||||
|
||||
/**
|
||||
* A string to json value dictionary.
|
||||
*/
|
||||
export interface JsonCompatibleDictionary {
|
||||
readonly [key: string]: JsonCompatibleValue | readonly JsonCompatibleValue[];
|
||||
}
|
||||
|
||||
export function isJsonCompatibleValue(value: unknown): value is JsonCompatibleValue {
|
||||
if (
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean" ||
|
||||
value === null ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
isJsonCompatibleArray(value) ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
isJsonCompatibleDictionary(value)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isJsonCompatibleArray(value: unknown): value is JsonCompatibleArray {
|
||||
if (!Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const item of value) {
|
||||
if (!isJsonCompatibleValue(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// all items okay
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isJsonCompatibleDictionary(data: unknown): data is JsonCompatibleDictionary {
|
||||
if (typeof data !== "object" || data === null) {
|
||||
// data must be a non-null object
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude special kind of objects like Array, Date or Uint8Array
|
||||
// Object.prototype.toString() returns a specified value:
|
||||
// http://www.ecma-international.org/ecma-262/7.0/index.html#sec-object.prototype.tostring
|
||||
if (Object.prototype.toString.call(data) !== "[object Object]") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: replace with Object.values when available (ES2017+)
|
||||
const values = Object.getOwnPropertyNames(data).map((key) => (data as any)[key]);
|
||||
return values.every(isJsonCompatibleValue);
|
||||
}
|
||||
21
packages/json-rpc/src/id.spec.ts
Normal file
21
packages/json-rpc/src/id.spec.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { makeJsonRpcId } from "./id";
|
||||
|
||||
describe("id", () => {
|
||||
describe("makeJsonRpcId", () => {
|
||||
it("returns a string or number", () => {
|
||||
const id = makeJsonRpcId();
|
||||
expect(["string", "number"]).toContain(typeof id);
|
||||
});
|
||||
|
||||
it("returns unique values", () => {
|
||||
const ids = new Set([
|
||||
makeJsonRpcId(),
|
||||
makeJsonRpcId(),
|
||||
makeJsonRpcId(),
|
||||
makeJsonRpcId(),
|
||||
makeJsonRpcId(),
|
||||
]);
|
||||
expect(ids.size).toEqual(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
13
packages/json-rpc/src/id.ts
Normal file
13
packages/json-rpc/src/id.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// Start with 10001 to avoid possible collisions with all hand-selected values like e.g. 1,2,3,42,100
|
||||
let counter = 10000;
|
||||
|
||||
/**
|
||||
* Creates a new ID to be used for creating a JSON-RPC request.
|
||||
*
|
||||
* Multiple calls of this produce unique values.
|
||||
*
|
||||
* The output may be any value compatible to JSON-RPC request IDs with an undefined output format and generation logic.
|
||||
*/
|
||||
export function makeJsonRpcId(): number {
|
||||
return (counter += 1);
|
||||
}
|
||||
20
packages/json-rpc/src/index.ts
Normal file
20
packages/json-rpc/src/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export { makeJsonRpcId } from "./id";
|
||||
export { JsonRpcClient, SimpleMessagingConnection } from "./jsonrpcclient";
|
||||
export {
|
||||
parseJsonRpcId,
|
||||
parseJsonRpcRequest,
|
||||
parseJsonRpcResponse,
|
||||
parseJsonRpcErrorResponse,
|
||||
parseJsonRpcSuccessResponse,
|
||||
} from "./parse";
|
||||
export {
|
||||
isJsonRpcErrorResponse,
|
||||
isJsonRpcSuccessResponse,
|
||||
JsonRpcError,
|
||||
JsonRpcErrorResponse,
|
||||
JsonRpcId,
|
||||
JsonRpcRequest,
|
||||
JsonRpcResponse,
|
||||
JsonRpcSuccessResponse,
|
||||
jsonRpcCode,
|
||||
} from "./types";
|
||||
86
packages/json-rpc/src/jsonrpcclient.spec.ts
Normal file
86
packages/json-rpc/src/jsonrpcclient.spec.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/// <reference lib="dom" />
|
||||
|
||||
import { Producer, Stream } from "xstream";
|
||||
|
||||
import { JsonRpcClient, SimpleMessagingConnection } from "./jsonrpcclient";
|
||||
import { parseJsonRpcResponse } from "./parse";
|
||||
import { JsonRpcRequest, JsonRpcResponse } from "./types";
|
||||
|
||||
function pendingWithoutWorker(): void {
|
||||
if (typeof Worker === "undefined") {
|
||||
pending("Environment without WebWorker support detected. Marked as pending.");
|
||||
}
|
||||
}
|
||||
|
||||
function makeSimpleMessagingConnection(
|
||||
worker: Worker,
|
||||
): SimpleMessagingConnection<JsonRpcRequest, JsonRpcResponse> {
|
||||
const producer: Producer<JsonRpcResponse> = {
|
||||
start: (listener) => {
|
||||
worker.onmessage = (event) => {
|
||||
listener.next(parseJsonRpcResponse(event.data));
|
||||
};
|
||||
},
|
||||
stop: () => {
|
||||
worker.onmessage = null;
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
responseStream: Stream.create(producer),
|
||||
sendRequest: (request) => worker.postMessage(request),
|
||||
};
|
||||
}
|
||||
|
||||
describe("JsonRpcClient", () => {
|
||||
const dummyserviceKarmaUrl = "/base/dist/web/dummyservice.worker.js";
|
||||
|
||||
it("can be constructed with a Worker", () => {
|
||||
pendingWithoutWorker();
|
||||
|
||||
const worker = new Worker(dummyserviceKarmaUrl);
|
||||
const client = new JsonRpcClient(makeSimpleMessagingConnection(worker));
|
||||
expect(client).toBeTruthy();
|
||||
worker.terminate();
|
||||
});
|
||||
|
||||
it("can communicate with worker", async () => {
|
||||
pendingWithoutWorker();
|
||||
|
||||
const worker = new Worker(dummyserviceKarmaUrl);
|
||||
|
||||
const client = new JsonRpcClient(makeSimpleMessagingConnection(worker));
|
||||
const response = await client.run({
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
method: "getIdentities",
|
||||
params: ["Who are you?"],
|
||||
});
|
||||
expect(response.jsonrpc).toEqual("2.0");
|
||||
expect(response.id).toEqual(123);
|
||||
expect(response.result).toEqual(`Called getIdentities("Who are you?")`);
|
||||
|
||||
worker.terminate();
|
||||
});
|
||||
|
||||
it("supports params as dictionary", async () => {
|
||||
pendingWithoutWorker();
|
||||
|
||||
const worker = new Worker(dummyserviceKarmaUrl);
|
||||
|
||||
const client = new JsonRpcClient(makeSimpleMessagingConnection(worker));
|
||||
const response = await client.run({
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
method: "getIdentities",
|
||||
params: {
|
||||
a: "Who are you?",
|
||||
},
|
||||
});
|
||||
expect(response.jsonrpc).toEqual("2.0");
|
||||
expect(response.id).toEqual(123);
|
||||
expect(response.result).toEqual(`Called getIdentities({"a":"Who are you?"})`);
|
||||
|
||||
worker.terminate();
|
||||
});
|
||||
});
|
||||
37
packages/json-rpc/src/jsonrpcclient.ts
Normal file
37
packages/json-rpc/src/jsonrpcclient.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { firstEvent } from "@iov/stream";
|
||||
import { Stream } from "xstream";
|
||||
|
||||
import { isJsonRpcErrorResponse, JsonRpcRequest, JsonRpcResponse, JsonRpcSuccessResponse } from "./types";
|
||||
|
||||
export interface SimpleMessagingConnection<Request, Response> {
|
||||
readonly responseStream: Stream<Response>;
|
||||
readonly sendRequest: (request: Request) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A thin wrapper that is used to bring together requests and responses by ID.
|
||||
*
|
||||
* Using this class is only advised for continous communication channels like
|
||||
* WebSockets or WebWorker messaging.
|
||||
*/
|
||||
export class JsonRpcClient {
|
||||
private readonly connection: SimpleMessagingConnection<JsonRpcRequest, JsonRpcResponse>;
|
||||
|
||||
public constructor(connection: SimpleMessagingConnection<JsonRpcRequest, JsonRpcResponse>) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public async run(request: JsonRpcRequest): Promise<JsonRpcSuccessResponse> {
|
||||
const filteredStream = this.connection.responseStream.filter((r) => r.id === request.id);
|
||||
const pendingResponses = firstEvent(filteredStream);
|
||||
this.connection.sendRequest(request);
|
||||
|
||||
const response = await pendingResponses;
|
||||
if (isJsonRpcErrorResponse(response)) {
|
||||
const error = response.error;
|
||||
throw new Error(`JSON RPC error: code=${error.code}; message='${error.message}'`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
429
packages/json-rpc/src/parse.spec.ts
Normal file
429
packages/json-rpc/src/parse.spec.ts
Normal file
@ -0,0 +1,429 @@
|
||||
import {
|
||||
parseJsonRpcErrorResponse,
|
||||
parseJsonRpcId,
|
||||
parseJsonRpcResponse,
|
||||
parseJsonRpcSuccessResponse,
|
||||
} from "./parse";
|
||||
import { jsonRpcCode, JsonRpcErrorResponse, JsonRpcRequest, JsonRpcSuccessResponse } from "./types";
|
||||
|
||||
describe("parse", () => {
|
||||
describe("parseJsonRpcId", () => {
|
||||
it("works for number IDs", () => {
|
||||
const request: JsonRpcRequest = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
method: "foo",
|
||||
params: {},
|
||||
};
|
||||
expect(parseJsonRpcId(request)).toEqual(123);
|
||||
});
|
||||
|
||||
it("works for string IDs", () => {
|
||||
const request: JsonRpcRequest = {
|
||||
jsonrpc: "2.0",
|
||||
id: "329fg3b",
|
||||
method: "foo",
|
||||
params: {},
|
||||
};
|
||||
expect(parseJsonRpcId(request)).toEqual("329fg3b");
|
||||
});
|
||||
|
||||
it("returns null for invaid IDs", () => {
|
||||
// unset
|
||||
{
|
||||
const request = {
|
||||
jsonrpc: "2.0",
|
||||
method: "foo",
|
||||
params: {},
|
||||
};
|
||||
expect(parseJsonRpcId(request)).toBeNull();
|
||||
}
|
||||
// wrong type (object)
|
||||
{
|
||||
const request = {
|
||||
jsonrpc: "2.0",
|
||||
id: { content: 123 },
|
||||
method: "foo",
|
||||
params: {},
|
||||
};
|
||||
expect(parseJsonRpcId(request)).toBeNull();
|
||||
}
|
||||
// wrong type (Array)
|
||||
{
|
||||
const request = {
|
||||
jsonrpc: "2.0",
|
||||
id: [1, 2, 3],
|
||||
method: "foo",
|
||||
params: {},
|
||||
};
|
||||
expect(parseJsonRpcId(request)).toBeNull();
|
||||
}
|
||||
// wrong type (null)
|
||||
{
|
||||
const request = {
|
||||
jsonrpc: "2.0",
|
||||
id: null,
|
||||
method: "foo",
|
||||
params: {},
|
||||
};
|
||||
expect(parseJsonRpcId(request)).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseJsonRpcErrorResponse", () => {
|
||||
it("works for valid error", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
data: [2, 3, 4],
|
||||
},
|
||||
};
|
||||
expect(parseJsonRpcErrorResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("works for error with string ID", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: "a3g4g34g",
|
||||
error: {
|
||||
code: jsonRpcCode.parseError,
|
||||
message: "Could not parse request ID",
|
||||
},
|
||||
};
|
||||
expect(parseJsonRpcErrorResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("works for error with null ID", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: null,
|
||||
error: {
|
||||
code: jsonRpcCode.parseError,
|
||||
message: "Could not parse request ID",
|
||||
},
|
||||
};
|
||||
expect(parseJsonRpcErrorResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("works for error with null data", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
data: null,
|
||||
},
|
||||
};
|
||||
expect(parseJsonRpcErrorResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("works for error with unset data", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
},
|
||||
};
|
||||
expect(parseJsonRpcErrorResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("throws for invalid type", () => {
|
||||
const expectedError = /data must be JSON compatible dictionary/i;
|
||||
expect(() => parseJsonRpcErrorResponse(undefined)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcErrorResponse(null)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcErrorResponse(false)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcErrorResponse("error")).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcErrorResponse(42)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcErrorResponse(() => true)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcErrorResponse({ foo: () => true })).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcErrorResponse({ foo: () => new Uint8Array([]) })).toThrowError(expectedError);
|
||||
});
|
||||
|
||||
it("throws for invalid version", () => {
|
||||
// wrong type
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: 2.0,
|
||||
id: 123,
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
},
|
||||
};
|
||||
expect(() => parseJsonRpcErrorResponse(response)).toThrowError(/got unexpected jsonrpc version/i);
|
||||
}
|
||||
// wrong version
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: "1.0",
|
||||
id: 123,
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
},
|
||||
};
|
||||
expect(() => parseJsonRpcErrorResponse(response)).toThrowError(/got unexpected jsonrpc version/i);
|
||||
}
|
||||
// unset
|
||||
{
|
||||
const response: any = {
|
||||
id: 123,
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
},
|
||||
};
|
||||
expect(() => parseJsonRpcErrorResponse(response)).toThrowError(/got unexpected jsonrpc version/i);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws for invalid ID", () => {
|
||||
// wrong type
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: [1, 2, 3],
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
},
|
||||
};
|
||||
expect(() => parseJsonRpcErrorResponse(response)).toThrowError(/invalid id field/i);
|
||||
}
|
||||
// unset
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
},
|
||||
};
|
||||
expect(() => parseJsonRpcErrorResponse(response)).toThrowError(/invalid id field/i);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws for success response", () => {
|
||||
const response: JsonRpcSuccessResponse = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcErrorResponse(response)).toThrowError(/invalid error field/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseJsonRpcSuccessResponse", () => {
|
||||
it("works for response with dict result", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
result: {
|
||||
foo: "bar",
|
||||
},
|
||||
};
|
||||
expect(parseJsonRpcSuccessResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("works for response with null result", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
result: null,
|
||||
};
|
||||
expect(parseJsonRpcSuccessResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("works for response with number ID", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
result: {},
|
||||
};
|
||||
expect(parseJsonRpcSuccessResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("works for response with string ID", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: "40gfh408g",
|
||||
result: {},
|
||||
};
|
||||
expect(parseJsonRpcSuccessResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("throws for invalid type", () => {
|
||||
const expectedError = /data must be JSON compatible dictionary/i;
|
||||
expect(() => parseJsonRpcSuccessResponse(undefined)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcSuccessResponse(null)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcSuccessResponse(false)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcSuccessResponse("success")).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcSuccessResponse(42)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcSuccessResponse(() => true)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcSuccessResponse({ foo: () => true })).toThrowError(expectedError);
|
||||
});
|
||||
|
||||
it("throws for invalid version", () => {
|
||||
// wrong type
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: 2.0,
|
||||
id: 123,
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcSuccessResponse(response)).toThrowError(/got unexpected jsonrpc version/i);
|
||||
}
|
||||
// wrong version
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: "1.0",
|
||||
id: 123,
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcSuccessResponse(response)).toThrowError(/got unexpected jsonrpc version/i);
|
||||
}
|
||||
// unset
|
||||
{
|
||||
const response: any = {
|
||||
id: 123,
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcSuccessResponse(response)).toThrowError(/got unexpected jsonrpc version/i);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws for invalid ID", () => {
|
||||
// wrong type
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: [1, 2, 3],
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcSuccessResponse(response)).toThrowError(/invalid id field/i);
|
||||
}
|
||||
// wrong type
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: null,
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcSuccessResponse(response)).toThrowError(/invalid id field/i);
|
||||
}
|
||||
// unset
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcSuccessResponse(response)).toThrowError(/invalid id field/i);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws for error response", () => {
|
||||
const response: JsonRpcErrorResponse = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
error: {
|
||||
code: jsonRpcCode.parseError,
|
||||
message: "Could not parse request ID",
|
||||
},
|
||||
};
|
||||
expect(() => parseJsonRpcSuccessResponse(response)).toThrowError(/invalid result field/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseJsonRpcResponse", () => {
|
||||
it("works for success response", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
result: 3000,
|
||||
};
|
||||
expect(parseJsonRpcResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("works for error response", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
data: [2, 3, 4],
|
||||
},
|
||||
};
|
||||
expect(parseJsonRpcResponse(response)).toEqual(response);
|
||||
});
|
||||
|
||||
it("favours error if response is error and success at the same time", () => {
|
||||
const response: any = {
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
result: 3000,
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
},
|
||||
};
|
||||
expect(parseJsonRpcResponse(response)).toEqual({
|
||||
jsonrpc: "2.0",
|
||||
id: 123,
|
||||
error: {
|
||||
code: jsonRpcCode.serverError.default,
|
||||
message: "Something bad happened",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("throws for invalid type", () => {
|
||||
const expectedError = /data must be JSON compatible dictionary/i;
|
||||
expect(() => parseJsonRpcResponse(undefined)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcResponse(null)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcResponse(false)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcResponse("error")).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcResponse(42)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcResponse(() => true)).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcResponse({ foo: () => true })).toThrowError(expectedError);
|
||||
expect(() => parseJsonRpcResponse({ foo: () => new Uint8Array([]) })).toThrowError(expectedError);
|
||||
});
|
||||
|
||||
it("throws for invalid version", () => {
|
||||
const expectedError = /got unexpected jsonrpc version/i;
|
||||
// wrong type
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: 2.0,
|
||||
id: 123,
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcResponse(response)).toThrowError(expectedError);
|
||||
}
|
||||
// wrong version
|
||||
{
|
||||
const response: any = {
|
||||
jsonrpc: "1.0",
|
||||
id: 123,
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcResponse(response)).toThrowError(expectedError);
|
||||
}
|
||||
// unset
|
||||
{
|
||||
const response: any = {
|
||||
id: 123,
|
||||
result: 3000,
|
||||
};
|
||||
expect(() => parseJsonRpcResponse(response)).toThrowError(expectedError);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
157
packages/json-rpc/src/parse.ts
Normal file
157
packages/json-rpc/src/parse.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import {
|
||||
isJsonCompatibleArray,
|
||||
isJsonCompatibleDictionary,
|
||||
isJsonCompatibleValue,
|
||||
JsonCompatibleDictionary,
|
||||
JsonCompatibleValue,
|
||||
} from "./compatibility";
|
||||
import {
|
||||
JsonRpcError,
|
||||
JsonRpcErrorResponse,
|
||||
JsonRpcId,
|
||||
JsonRpcRequest,
|
||||
JsonRpcResponse,
|
||||
JsonRpcSuccessResponse,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Extracts ID field from request or response object.
|
||||
*
|
||||
* Returns `null` when no valid ID was found.
|
||||
*/
|
||||
export function parseJsonRpcId(data: unknown): JsonRpcId | null {
|
||||
if (!isJsonCompatibleDictionary(data)) {
|
||||
throw new Error("Data must be JSON compatible dictionary");
|
||||
}
|
||||
|
||||
const id = data.id;
|
||||
if (typeof id !== "number" && typeof id !== "string") {
|
||||
return null;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function parseJsonRpcRequest(data: unknown): JsonRpcRequest {
|
||||
if (!isJsonCompatibleDictionary(data)) {
|
||||
throw new Error("Data must be JSON compatible dictionary");
|
||||
}
|
||||
|
||||
if (data.jsonrpc !== "2.0") {
|
||||
throw new Error(`Got unexpected jsonrpc version: ${data.jsonrpc}`);
|
||||
}
|
||||
|
||||
const id = parseJsonRpcId(data);
|
||||
if (id === null) {
|
||||
throw new Error("Invalid id field");
|
||||
}
|
||||
|
||||
const method = data.method;
|
||||
if (typeof method !== "string") {
|
||||
throw new Error("Invalid method field");
|
||||
}
|
||||
|
||||
if (!isJsonCompatibleArray(data.params) && !isJsonCompatibleDictionary(data.params)) {
|
||||
throw new Error("Invalid params field");
|
||||
}
|
||||
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: id,
|
||||
method: method,
|
||||
params: data.params,
|
||||
};
|
||||
}
|
||||
|
||||
function parseError(error: JsonCompatibleDictionary): JsonRpcError {
|
||||
if (typeof error.code !== "number") {
|
||||
throw new Error("Error property 'code' is not a number");
|
||||
}
|
||||
|
||||
if (typeof error.message !== "string") {
|
||||
throw new Error("Error property 'message' is not a string");
|
||||
}
|
||||
|
||||
let maybeUndefinedData: JsonCompatibleValue | undefined;
|
||||
|
||||
if (error.data === undefined) {
|
||||
maybeUndefinedData = undefined;
|
||||
} else if (isJsonCompatibleValue(error.data)) {
|
||||
maybeUndefinedData = error.data;
|
||||
} else {
|
||||
throw new Error("Error property 'data' is defined but not a JSON compatible value.");
|
||||
}
|
||||
|
||||
return {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
...(maybeUndefinedData !== undefined ? { data: maybeUndefinedData } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Throws if data is not a JsonRpcErrorResponse */
|
||||
export function parseJsonRpcErrorResponse(data: unknown): JsonRpcErrorResponse {
|
||||
if (!isJsonCompatibleDictionary(data)) {
|
||||
throw new Error("Data must be JSON compatible dictionary");
|
||||
}
|
||||
|
||||
if (data.jsonrpc !== "2.0") {
|
||||
throw new Error(`Got unexpected jsonrpc version: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const id = data.id;
|
||||
if (typeof id !== "number" && typeof id !== "string" && id !== null) {
|
||||
throw new Error("Invalid id field");
|
||||
}
|
||||
|
||||
if (typeof data.error === "undefined" || !isJsonCompatibleDictionary(data.error)) {
|
||||
throw new Error("Invalid error field");
|
||||
}
|
||||
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: id,
|
||||
error: parseError(data.error),
|
||||
};
|
||||
}
|
||||
|
||||
/** Throws if data is not a JsonRpcSuccessResponse */
|
||||
export function parseJsonRpcSuccessResponse(data: unknown): JsonRpcSuccessResponse {
|
||||
if (!isJsonCompatibleDictionary(data)) {
|
||||
throw new Error("Data must be JSON compatible dictionary");
|
||||
}
|
||||
|
||||
if (data.jsonrpc !== "2.0") {
|
||||
throw new Error(`Got unexpected jsonrpc version: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const id = data.id;
|
||||
if (typeof id !== "number" && typeof id !== "string") {
|
||||
throw new Error("Invalid id field");
|
||||
}
|
||||
|
||||
if (typeof data.result === "undefined") {
|
||||
throw new Error("Invalid result field");
|
||||
}
|
||||
|
||||
const result = data.result;
|
||||
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: id,
|
||||
result: result,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JsonRpcErrorResponse if input can be parsed as a JSON-RPC error. Otherwise parses
|
||||
* input as JsonRpcSuccessResponse. Throws if input is neither a valid error nor success response.
|
||||
*/
|
||||
export function parseJsonRpcResponse(data: unknown): JsonRpcResponse {
|
||||
let response: JsonRpcResponse;
|
||||
try {
|
||||
response = parseJsonRpcErrorResponse(data);
|
||||
} catch (_) {
|
||||
response = parseJsonRpcSuccessResponse(data);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
59
packages/json-rpc/src/types.ts
Normal file
59
packages/json-rpc/src/types.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { JsonCompatibleArray, JsonCompatibleDictionary, JsonCompatibleValue } from "./compatibility";
|
||||
|
||||
export type JsonRpcId = number | string;
|
||||
|
||||
export interface JsonRpcRequest {
|
||||
readonly jsonrpc: "2.0";
|
||||
readonly id: JsonRpcId;
|
||||
readonly method: string;
|
||||
readonly params: JsonCompatibleArray | JsonCompatibleDictionary;
|
||||
}
|
||||
|
||||
export interface JsonRpcSuccessResponse {
|
||||
readonly jsonrpc: "2.0";
|
||||
readonly id: JsonRpcId;
|
||||
readonly result: any;
|
||||
}
|
||||
|
||||
export interface JsonRpcError {
|
||||
readonly code: number;
|
||||
readonly message: string;
|
||||
readonly data?: JsonCompatibleValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* And error object as described in https://www.jsonrpc.org/specification#error_object
|
||||
*/
|
||||
export interface JsonRpcErrorResponse {
|
||||
readonly jsonrpc: "2.0";
|
||||
readonly id: JsonRpcId | null;
|
||||
readonly error: JsonRpcError;
|
||||
}
|
||||
|
||||
export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
|
||||
|
||||
export function isJsonRpcErrorResponse(response: JsonRpcResponse): response is JsonRpcErrorResponse {
|
||||
return typeof (response as JsonRpcErrorResponse).error === "object";
|
||||
}
|
||||
|
||||
export function isJsonRpcSuccessResponse(response: JsonRpcResponse): response is JsonRpcSuccessResponse {
|
||||
return !isJsonRpcErrorResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error codes as specified in JSON-RPC 2.0
|
||||
*
|
||||
* @see https://www.jsonrpc.org/specification#error_object
|
||||
*/
|
||||
export const jsonRpcCode = {
|
||||
parseError: -32700,
|
||||
invalidRequest: -32600,
|
||||
methodNotFound: -32601,
|
||||
invalidParams: -32602,
|
||||
internalError: -32603,
|
||||
// server error (Reserved for implementation-defined server-errors.):
|
||||
// -32000 to -32099
|
||||
serverError: {
|
||||
default: -32000,
|
||||
},
|
||||
};
|
||||
67
packages/json-rpc/src/workers/dummyservice.worker.ts
Normal file
67
packages/json-rpc/src/workers/dummyservice.worker.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
// for testing only
|
||||
|
||||
import { isJsonCompatibleDictionary } from "../compatibility";
|
||||
import { parseJsonRpcId, parseJsonRpcRequest } from "../parse";
|
||||
import {
|
||||
jsonRpcCode,
|
||||
JsonRpcErrorResponse,
|
||||
JsonRpcRequest,
|
||||
JsonRpcResponse,
|
||||
JsonRpcSuccessResponse,
|
||||
} from "../types";
|
||||
|
||||
function handleRequest(event: MessageEvent): JsonRpcResponse {
|
||||
let request: JsonRpcRequest;
|
||||
try {
|
||||
request = parseJsonRpcRequest(event.data);
|
||||
} catch (error) {
|
||||
const requestId = parseJsonRpcId(event.data);
|
||||
const errorResponse: JsonRpcErrorResponse = {
|
||||
jsonrpc: "2.0",
|
||||
id: requestId,
|
||||
error: {
|
||||
code: jsonRpcCode.invalidRequest,
|
||||
message: error.toString(),
|
||||
},
|
||||
};
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
let paramsString: string;
|
||||
if (isJsonCompatibleDictionary(request.params)) {
|
||||
paramsString = JSON.stringify(request.params);
|
||||
} else {
|
||||
paramsString = request.params
|
||||
.map((p) => {
|
||||
if (typeof p === "number") {
|
||||
return p;
|
||||
} else if (p === null) {
|
||||
return `null`;
|
||||
} else if (typeof p === "string") {
|
||||
return `"${p}"`;
|
||||
} else {
|
||||
return p.toString();
|
||||
}
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
const response: JsonRpcSuccessResponse = {
|
||||
jsonrpc: "2.0",
|
||||
id: request.id,
|
||||
result: `Called ${request.method}(${paramsString})`,
|
||||
};
|
||||
return response;
|
||||
}
|
||||
|
||||
onmessage = (event) => {
|
||||
// filter out empty {"isTrusted":true} events
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = handleRequest(event);
|
||||
setTimeout(() => postMessage(response), 50);
|
||||
};
|
||||
15
packages/json-rpc/tsconfig.json
Normal file
15
packages/json-rpc/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"declarationDir": "build/types",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"src/workers/**/*"
|
||||
]
|
||||
}
|
||||
12
packages/json-rpc/tsconfig.workers.json
Normal file
12
packages/json-rpc/tsconfig.workers.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"declarationDir": "build/types",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/workers/**/*"
|
||||
]
|
||||
}
|
||||
14
packages/json-rpc/typedoc.js
Normal file
14
packages/json-rpc/typedoc.js
Normal file
@ -0,0 +1,14 @@
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
src: ["./src"],
|
||||
out: "docs",
|
||||
exclude: "**/*.spec.ts",
|
||||
target: "es6",
|
||||
name: `${packageJson.name} Documentation`,
|
||||
readme: "README.md",
|
||||
mode: "file",
|
||||
excludeExternals: true,
|
||||
excludeNotExported: true,
|
||||
excludePrivate: true,
|
||||
};
|
||||
23
packages/json-rpc/types/compatibility.d.ts
vendored
Normal file
23
packages/json-rpc/types/compatibility.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* A single JSON value. This is the missing return type of JSON.parse().
|
||||
*/
|
||||
export declare type JsonCompatibleValue =
|
||||
| JsonCompatibleDictionary
|
||||
| JsonCompatibleArray
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
/**
|
||||
* An array of JsonCompatibleValue
|
||||
*/
|
||||
export interface JsonCompatibleArray extends ReadonlyArray<JsonCompatibleValue> {}
|
||||
/**
|
||||
* A string to json value dictionary.
|
||||
*/
|
||||
export interface JsonCompatibleDictionary {
|
||||
readonly [key: string]: JsonCompatibleValue | readonly JsonCompatibleValue[];
|
||||
}
|
||||
export declare function isJsonCompatibleValue(value: unknown): value is JsonCompatibleValue;
|
||||
export declare function isJsonCompatibleArray(value: unknown): value is JsonCompatibleArray;
|
||||
export declare function isJsonCompatibleDictionary(data: unknown): data is JsonCompatibleDictionary;
|
||||
8
packages/json-rpc/types/id.d.ts
vendored
Normal file
8
packages/json-rpc/types/id.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Creates a new ID to be used for creating a JSON-RPC request.
|
||||
*
|
||||
* Multiple calls of this produce unique values.
|
||||
*
|
||||
* The output may be any value compatible to JSON-RPC request IDs with an undefined output format and generation logic.
|
||||
*/
|
||||
export declare function makeJsonRpcId(): number;
|
||||
20
packages/json-rpc/types/index.d.ts
vendored
Normal file
20
packages/json-rpc/types/index.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
export { makeJsonRpcId } from "./id";
|
||||
export { JsonRpcClient, SimpleMessagingConnection } from "./jsonrpcclient";
|
||||
export {
|
||||
parseJsonRpcId,
|
||||
parseJsonRpcRequest,
|
||||
parseJsonRpcResponse,
|
||||
parseJsonRpcErrorResponse,
|
||||
parseJsonRpcSuccessResponse,
|
||||
} from "./parse";
|
||||
export {
|
||||
isJsonRpcErrorResponse,
|
||||
isJsonRpcSuccessResponse,
|
||||
JsonRpcError,
|
||||
JsonRpcErrorResponse,
|
||||
JsonRpcId,
|
||||
JsonRpcRequest,
|
||||
JsonRpcResponse,
|
||||
JsonRpcSuccessResponse,
|
||||
jsonRpcCode,
|
||||
} from "./types";
|
||||
17
packages/json-rpc/types/jsonrpcclient.d.ts
vendored
Normal file
17
packages/json-rpc/types/jsonrpcclient.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
import { Stream } from "xstream";
|
||||
import { JsonRpcRequest, JsonRpcResponse, JsonRpcSuccessResponse } from "./types";
|
||||
export interface SimpleMessagingConnection<Request, Response> {
|
||||
readonly responseStream: Stream<Response>;
|
||||
readonly sendRequest: (request: Request) => void;
|
||||
}
|
||||
/**
|
||||
* A thin wrapper that is used to bring together requests and responses by ID.
|
||||
*
|
||||
* Using this class is only advised for continous communication channels like
|
||||
* WebSockets or WebWorker messaging.
|
||||
*/
|
||||
export declare class JsonRpcClient {
|
||||
private readonly connection;
|
||||
constructor(connection: SimpleMessagingConnection<JsonRpcRequest, JsonRpcResponse>);
|
||||
run(request: JsonRpcRequest): Promise<JsonRpcSuccessResponse>;
|
||||
}
|
||||
23
packages/json-rpc/types/parse.d.ts
vendored
Normal file
23
packages/json-rpc/types/parse.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
import {
|
||||
JsonRpcErrorResponse,
|
||||
JsonRpcId,
|
||||
JsonRpcRequest,
|
||||
JsonRpcResponse,
|
||||
JsonRpcSuccessResponse,
|
||||
} from "./types";
|
||||
/**
|
||||
* Extracts ID field from request or response object.
|
||||
*
|
||||
* Returns `null` when no valid ID was found.
|
||||
*/
|
||||
export declare function parseJsonRpcId(data: unknown): JsonRpcId | null;
|
||||
export declare function parseJsonRpcRequest(data: unknown): JsonRpcRequest;
|
||||
/** Throws if data is not a JsonRpcErrorResponse */
|
||||
export declare function parseJsonRpcErrorResponse(data: unknown): JsonRpcErrorResponse;
|
||||
/** Throws if data is not a JsonRpcSuccessResponse */
|
||||
export declare function parseJsonRpcSuccessResponse(data: unknown): JsonRpcSuccessResponse;
|
||||
/**
|
||||
* Returns a JsonRpcErrorResponse if input can be parsed as a JSON-RPC error. Otherwise parses
|
||||
* input as JsonRpcSuccessResponse. Throws if input is neither a valid error nor success response.
|
||||
*/
|
||||
export declare function parseJsonRpcResponse(data: unknown): JsonRpcResponse;
|
||||
46
packages/json-rpc/types/types.d.ts
vendored
Normal file
46
packages/json-rpc/types/types.d.ts
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
import { JsonCompatibleArray, JsonCompatibleDictionary, JsonCompatibleValue } from "./compatibility";
|
||||
export declare type JsonRpcId = number | string;
|
||||
export interface JsonRpcRequest {
|
||||
readonly jsonrpc: "2.0";
|
||||
readonly id: JsonRpcId;
|
||||
readonly method: string;
|
||||
readonly params: JsonCompatibleArray | JsonCompatibleDictionary;
|
||||
}
|
||||
export interface JsonRpcSuccessResponse {
|
||||
readonly jsonrpc: "2.0";
|
||||
readonly id: JsonRpcId;
|
||||
readonly result: any;
|
||||
}
|
||||
export interface JsonRpcError {
|
||||
readonly code: number;
|
||||
readonly message: string;
|
||||
readonly data?: JsonCompatibleValue;
|
||||
}
|
||||
/**
|
||||
* And error object as described in https://www.jsonrpc.org/specification#error_object
|
||||
*/
|
||||
export interface JsonRpcErrorResponse {
|
||||
readonly jsonrpc: "2.0";
|
||||
readonly id: JsonRpcId | null;
|
||||
readonly error: JsonRpcError;
|
||||
}
|
||||
export declare type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
|
||||
export declare function isJsonRpcErrorResponse(response: JsonRpcResponse): response is JsonRpcErrorResponse;
|
||||
export declare function isJsonRpcSuccessResponse(
|
||||
response: JsonRpcResponse,
|
||||
): response is JsonRpcSuccessResponse;
|
||||
/**
|
||||
* Error codes as specified in JSON-RPC 2.0
|
||||
*
|
||||
* @see https://www.jsonrpc.org/specification#error_object
|
||||
*/
|
||||
export declare const jsonRpcCode: {
|
||||
parseError: number;
|
||||
invalidRequest: number;
|
||||
methodNotFound: number;
|
||||
invalidParams: number;
|
||||
internalError: number;
|
||||
serverError: {
|
||||
default: number;
|
||||
};
|
||||
};
|
||||
2
packages/json-rpc/types/workers/dummyservice.worker.d.ts
vendored
Normal file
2
packages/json-rpc/types/workers/dummyservice.worker.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference lib="webworker" />
|
||||
export {};
|
||||
28
packages/json-rpc/webpack.web.config.js
Normal file
28
packages/json-rpc/webpack.web.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
const glob = require("glob");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
|
||||
const target = "web";
|
||||
const distdir = path.join(__dirname, "dist", "web");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
// bundle for WebWorker tests
|
||||
target: target,
|
||||
entry: "./build/workers/dummyservice.worker.js",
|
||||
output: {
|
||||
path: distdir,
|
||||
filename: "dummyservice.worker.js",
|
||||
},
|
||||
},
|
||||
{
|
||||
// bundle used for Karma tests
|
||||
target: target,
|
||||
entry: glob.sync("./build/**/*.spec.js"),
|
||||
output: {
|
||||
path: distdir,
|
||||
filename: "tests.js",
|
||||
},
|
||||
plugins: [new webpack.EnvironmentPlugin([])],
|
||||
},
|
||||
];
|
||||
@ -45,8 +45,8 @@
|
||||
"dependencies": {
|
||||
"@cosmjs/crypto": "^0.20.0",
|
||||
"@cosmjs/encoding": "^0.20.0",
|
||||
"@cosmjs/json-rpc": "^0.20.0",
|
||||
"@cosmjs/math": "^0.20.0",
|
||||
"@iov/jsonrpc": "^2.3.2",
|
||||
"@iov/socket": "^2.3.2",
|
||||
"axios": "^0.19.0",
|
||||
"readonly-date": "^1.0.0",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc";
|
||||
|
||||
import * as requests from "./requests";
|
||||
import * as responses from "./responses";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { JsonRpcRequest } from "@iov/jsonrpc";
|
||||
import { JsonRpcRequest } from "@cosmjs/json-rpc";
|
||||
|
||||
const numbers = "0123456789";
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import {
|
||||
JsonRpcRequest,
|
||||
JsonRpcSuccessResponse,
|
||||
parseJsonRpcResponse,
|
||||
} from "@iov/jsonrpc";
|
||||
} from "@cosmjs/json-rpc";
|
||||
import axios from "axios";
|
||||
|
||||
import { hasProtocol, RpcClient } from "./rpcclient";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc";
|
||||
import { Stream } from "xstream";
|
||||
|
||||
/**
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
JsonRpcResponse,
|
||||
JsonRpcSuccessResponse,
|
||||
parseJsonRpcResponse,
|
||||
} from "@iov/jsonrpc";
|
||||
} from "@cosmjs/json-rpc";
|
||||
import { ConnectionStatus, ReconnectingSocket, SocketWrapperMessageEvent } from "@iov/socket";
|
||||
import { firstEvent } from "@iov/stream";
|
||||
import { Listener, Producer, Stream, Subscription } from "xstream";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Types in this file are exported outside of the @iov/tendermint-rpc package,
|
||||
// Types in this file are exported outside of the @cosmjs/tendermint-rpc package,
|
||||
// e.g. as part of a request or response
|
||||
|
||||
import { As } from "type-tagger";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { toHex } from "@cosmjs/encoding";
|
||||
import { JsonRpcRequest } from "@iov/jsonrpc";
|
||||
import { JsonRpcRequest } from "@cosmjs/json-rpc";
|
||||
|
||||
import { assertNotEmpty, Base64, Base64String, HexString, Integer, IntegerString, may } from "../encodings";
|
||||
import { createJsonRpcRequest } from "../jsonrpc";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { fromHex } from "@cosmjs/encoding";
|
||||
import { JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||
import { JsonRpcSuccessResponse } from "@cosmjs/json-rpc";
|
||||
|
||||
import {
|
||||
assertArray,
|
||||
|
||||
2
packages/tendermint-rpc/types/adaptor.d.ts
vendored
2
packages/tendermint-rpc/types/adaptor.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc";
|
||||
import * as requests from "./requests";
|
||||
import * as responses from "./responses";
|
||||
import { SubscriptionEvent } from "./rpcclients";
|
||||
|
||||
2
packages/tendermint-rpc/types/jsonrpc.d.ts
vendored
2
packages/tendermint-rpc/types/jsonrpc.d.ts
vendored
@ -1,3 +1,3 @@
|
||||
import { JsonRpcRequest } from "@iov/jsonrpc";
|
||||
import { JsonRpcRequest } from "@cosmjs/json-rpc";
|
||||
/** Creates a JSON-RPC request with random ID */
|
||||
export declare function createJsonRpcRequest(method: string, params?: {}): JsonRpcRequest;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc";
|
||||
import { RpcClient } from "./rpcclient";
|
||||
export declare class HttpClient implements RpcClient {
|
||||
protected readonly url: string;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc";
|
||||
import { Stream } from "xstream";
|
||||
/**
|
||||
* An event emitted from Tendermint after subscribing via RPC.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { JsonRpcId, JsonRpcRequest, JsonRpcResponse, JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||
import { JsonRpcId, JsonRpcRequest, JsonRpcResponse, JsonRpcSuccessResponse } from "@cosmjs/json-rpc";
|
||||
import { Stream } from "xstream";
|
||||
import { RpcStreamingClient, SubscriptionEvent } from "./rpcclient";
|
||||
export declare class WebsocketClient implements RpcStreamingClient {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { JsonRpcRequest } from "@iov/jsonrpc";
|
||||
import { JsonRpcRequest } from "@cosmjs/json-rpc";
|
||||
import * as requests from "../requests";
|
||||
export declare class Params {
|
||||
static encodeAbciInfo(req: requests.AbciInfoRequest): JsonRpcRequest;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||
import { JsonRpcSuccessResponse } from "@cosmjs/json-rpc";
|
||||
import * as responses from "../responses";
|
||||
import { SubscriptionEvent } from "../rpcclients";
|
||||
export declare class Responses {
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@ -92,25 +92,6 @@
|
||||
unique-filename "^1.1.1"
|
||||
which "^1.3.1"
|
||||
|
||||
"@iov/encoding@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@iov/encoding/-/encoding-2.3.2.tgz#4b37966af0345a6bc904bb58189dc1ea9d14ad9b"
|
||||
integrity sha512-viioqo1flTkG4Oxb0PvoBXGozHq9fObAgAL4dRHJe9zmChE77EBX2Y5u0nabd2JwAhEbir56AtsrUe4dOrtd5w==
|
||||
dependencies:
|
||||
base64-js "^1.3.0"
|
||||
bech32 "^1.1.4"
|
||||
bn.js "^4.11.8"
|
||||
readonly-date "^1.0.0"
|
||||
|
||||
"@iov/jsonrpc@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@iov/jsonrpc/-/jsonrpc-2.3.2.tgz#5cdfa56333741073cc00f17d54efb9f526b9705a"
|
||||
integrity sha512-fPryTYZ4na1F/K0AF4eRjf1mwg97s8N/AgvELHyqpm7Bq9zvV0/ZBPvzjV1mqmPMfONx91qLIkpDguwmwEb8NA==
|
||||
dependencies:
|
||||
"@iov/encoding" "^2.3.2"
|
||||
"@iov/stream" "^2.3.2"
|
||||
xstream "^11.10.0"
|
||||
|
||||
"@iov/socket@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@iov/socket/-/socket-2.3.2.tgz#adc8ef389bafc5380e1c7415fb21f9a890d79195"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user