json-rpc: Fork from @iov/jsonrpc

This commit is contained in:
willclarktech 2020-06-24 15:46:20 +02:00
parent 186cac31fa
commit a9a12d5fbc
No known key found for this signature in database
GPG Key ID: 551A86E2E398ADF7
79 changed files with 1224 additions and 0 deletions

View File

@ -0,0 +1 @@
../../.eslintignore

3
packages/json-rpc/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/
dist/
docs/

View File

@ -0,0 +1,12 @@
# @iov/jsonrpc
[![npm version](https://img.shields.io/npm/v/@iov/jsonrpc.svg)](https://www.npmjs.com/package/@iov/jsonrpc)
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 IOV-Core repository, licensed under the Apache
License 2.0 (see
[NOTICE](https://github.com/iov-one/iov-core/blob/master/NOTICE) and
[LICENSE](https://github.com/iov-one/iov-core/blob/master/LICENSE)).

View 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();

View 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,
});
};

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@ -0,0 +1 @@
Directory used to trigger lerna package updates for all packages

View File

@ -0,0 +1,44 @@
{
"name": "@iov/jsonrpc",
"version": "2.5.0",
"description": "Framework for implementing a JSON-RPC 2.0 API",
"author": "IOV SAS <admin@iov.one>",
"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/iov-one/iov-core/tree/master/packages/iov-jsonrpc"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"docs": "shx rm -rf docs && typedoc --options typedoc.js",
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\" && tslint -t verbose --project .",
"format": "prettier --write --loglevel warn \"./src/**/*.ts\"",
"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/encoding": "^2.5.0",
"@iov/stream": "^2.3.2",
"xstream": "^11.10.0"
}
}

View 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);
});
});
});

View 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);
}

View 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";

View File

@ -0,0 +1,88 @@
/// <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) => {
// tslint:disable-next-line:no-object-mutation
worker.onmessage = (event) => {
listener.next(parseJsonRpcResponse(event.data));
};
},
stop: () => {
// tslint:disable-next-line:no-object-mutation
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();
});
});

View 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;
}
}

View 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);
}
});
});
});

View File

@ -0,0 +1,158 @@
import {
isJsonCompatibleArray,
isJsonCompatibleDictionary,
isJsonCompatibleValue,
JsonCompatibleDictionary,
JsonCompatibleValue,
} from "@iov/encoding";
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;
}

View File

@ -0,0 +1,59 @@
import { JsonCompatibleArray, JsonCompatibleDictionary, JsonCompatibleValue } from "@iov/encoding";
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,
},
};

View File

@ -0,0 +1,68 @@
/// <reference lib="webworker" />
// for testing only
import { isJsonCompatibleDictionary } from "@iov/encoding";
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);
};

View File

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"declarationDir": "build/types",
"rootDir": "src"
},
"include": [
"src/**/*"
],
"exclude": [
"src/workers/**/*"
]
}

View File

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"declarationDir": "build/types",
"rootDir": "src"
},
"include": [
"src/workers/**/*"
]
}

View File

@ -0,0 +1,3 @@
{
"extends": "../../tslint.json"
}

View 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,
};

8
packages/json-rpc/types/id.d.ts vendored Normal file
View 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
View 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";

View 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
View 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
View File

@ -0,0 +1,46 @@
import { JsonCompatibleArray, JsonCompatibleDictionary, JsonCompatibleValue } from "@iov/encoding";
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;
};
};

View File

@ -0,0 +1,2 @@
/// <reference lib="webworker" />
export {};

View 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([])],
},
];