Move to lerna repo (#19)
* Pull api-* packages into lerna repo * Bootstrap with hoist * Update jest alias config * Add api-jsonrpc * Pull in jsonrpc * Update badges * Don't hoist, use workspaces * .eslintrc to root * Cleanups, build from root * Shorthand commands * Update to @polkadot/dev 0.9.1 * Update description
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
// @flow
|
||||
|
||||
import type { ProviderInterface } from '../types';
|
||||
|
||||
require('./polyfill');
|
||||
|
||||
const JsonRpcCoder = require('../jsonRpcCoder');
|
||||
|
||||
module.exports = class HttpProvider extends JsonRpcCoder implements ProviderInterface {
|
||||
_endpoint: string;
|
||||
|
||||
constructor (endpoint: string) {
|
||||
super();
|
||||
|
||||
this._endpoint = endpoint;
|
||||
}
|
||||
|
||||
async send (method: string, params: Array<any>): Promise<any> {
|
||||
const body = this.encodeJson(method, params);
|
||||
const response = await fetch(this._endpoint, {
|
||||
body,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Length': `${body.length}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`[${response.status}]: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return this.decodeResponse(result);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
|
||||
/* global jest */
|
||||
|
||||
const { mockHttp, TEST_HTTP_URL } = require('../../test/mockHttp');
|
||||
|
||||
const Http = require('./index');
|
||||
|
||||
describe('Http', () => {
|
||||
let http;
|
||||
let mock;
|
||||
let encodeSpy;
|
||||
let decodeSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
http = new Http(TEST_HTTP_URL);
|
||||
|
||||
encodeSpy = jest.spyOn(http, 'encodeJson');
|
||||
decodeSpy = jest.spyOn(http, 'decodeResponse');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
encodeSpy.mockRestore();
|
||||
decodeSpy.mockRestore();
|
||||
|
||||
mock.done();
|
||||
});
|
||||
|
||||
describe('send', () => {
|
||||
it('encodes requests', () => {
|
||||
mock = mockHttp([{
|
||||
method: 'test_encoding',
|
||||
reply: {
|
||||
result: 'ok'
|
||||
}
|
||||
}]);
|
||||
|
||||
return http
|
||||
.send('test_encoding', ['param'])
|
||||
.then((result) => {
|
||||
expect(encodeSpy).toHaveBeenCalledWith('test_encoding', ['param']);
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes responses', () => {
|
||||
mock = mockHttp([{
|
||||
method: 'test_encoding',
|
||||
reply: {
|
||||
result: 'ok'
|
||||
}
|
||||
}]);
|
||||
|
||||
return http
|
||||
.send('test_encoding', ['param'])
|
||||
.then((result) => {
|
||||
expect(decodeSpy).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
jsonrpc: '2.0',
|
||||
result: 'ok'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the body through correctly', () => {
|
||||
mock = mockHttp([{
|
||||
method: 'test_body',
|
||||
reply: {
|
||||
result: 'ok'
|
||||
}
|
||||
}]);
|
||||
|
||||
return http
|
||||
.send('test_body', ['param'])
|
||||
.then((result) => {
|
||||
expect(mock.body['test_body']).toEqual({
|
||||
id: 1,
|
||||
jsonrpc: '2.0',
|
||||
method: 'test_body',
|
||||
params: ['param']
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error when !response.ok', () => {
|
||||
mock = mockHttp([{
|
||||
code: 500,
|
||||
method: 'test_error'
|
||||
}]);
|
||||
|
||||
return http
|
||||
.send('test_error', [])
|
||||
.catch((error) => {
|
||||
expect(error.message).toMatch(/\[500\]: Internal Server/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
// @flow
|
||||
|
||||
if (typeof fetch === 'undefined') {
|
||||
require('isomorphic-fetch');
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
|
||||
describe('http/polyfill', () => {
|
||||
let origFetch;
|
||||
|
||||
beforeEach(() => {
|
||||
origFetch = global.fetch;
|
||||
global.fetch = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = origFetch;
|
||||
});
|
||||
|
||||
it('polyfills with no exceptions (without fetch)', () => {
|
||||
expect(require('./polyfill')).toBeDefined();
|
||||
});
|
||||
|
||||
it('polyfills with no exceptions (with fetch)', () => {
|
||||
global.fetch = () => true;
|
||||
|
||||
expect(require('./polyfill')).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
// @flow
|
||||
|
||||
const Http = require('./http');
|
||||
const Ws = require('./ws');
|
||||
|
||||
module.exports = {
|
||||
Http,
|
||||
Ws
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
// @flow
|
||||
|
||||
import type { JsonRpcRequest, JsonRpcResponse } from './types';
|
||||
|
||||
const { isNumber, isUndefined } = require('@polkadot/util/is');
|
||||
|
||||
module.exports = class JsonRpcCoder {
|
||||
_id: number = 0;
|
||||
|
||||
decodeResponse (response: JsonRpcResponse): any {
|
||||
if (!response) {
|
||||
throw new Error('Empty response object received');
|
||||
}
|
||||
|
||||
if (response.jsonrpc !== '2.0') {
|
||||
throw new Error('Invalid jsonrpc field in decoded object');
|
||||
}
|
||||
|
||||
if (!isNumber(response.id)) {
|
||||
throw new Error('Invalid id field in decoded object');
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
const { code, message } = response.error;
|
||||
|
||||
throw new Error(`[${code}]: ${message}`);
|
||||
}
|
||||
|
||||
if (isUndefined(response.result)) {
|
||||
throw new Error('No result found in JsonRpc response');
|
||||
}
|
||||
|
||||
return response.result;
|
||||
}
|
||||
|
||||
encodeObject (method: string, params: Array<any>): JsonRpcRequest {
|
||||
return {
|
||||
id: ++this._id,
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params
|
||||
};
|
||||
}
|
||||
|
||||
encodeJson (method: string, params: Array<any>): string {
|
||||
return JSON.stringify(
|
||||
this.encodeObject(method, params)
|
||||
);
|
||||
}
|
||||
|
||||
get id (): number {
|
||||
return this._id;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
|
||||
const JsonRpcCoder = require('./jsonRpcCoder');
|
||||
|
||||
describe('JsonRpcCoder', () => {
|
||||
let coder;
|
||||
|
||||
beforeEach(() => {
|
||||
coder = new JsonRpcCoder();
|
||||
});
|
||||
|
||||
describe('id', () => {
|
||||
it('starts with id === 0 (nothing sent)', () => {
|
||||
expect(coder.id).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decodeResponse', () => {
|
||||
it('expects a non-empty input object', () => {
|
||||
expect(
|
||||
() => coder.decodeResponse()
|
||||
).toThrow(/Empty response/);
|
||||
});
|
||||
|
||||
it('expects a valid jsonrpc field', () => {
|
||||
expect(
|
||||
() => coder.decodeResponse({})
|
||||
).toThrow(/Invalid jsonrpc/);
|
||||
});
|
||||
|
||||
it('expects a valid id field', () => {
|
||||
expect(
|
||||
() => coder.decodeResponse({ jsonrpc: '2.0' })
|
||||
).toThrow(/Invalid id/);
|
||||
});
|
||||
|
||||
it('expects a valid result field', () => {
|
||||
expect(
|
||||
() => coder.decodeResponse({ id: 1, jsonrpc: '2.0' })
|
||||
).toThrow(/No result/);
|
||||
});
|
||||
|
||||
it('throws any error found', () => {
|
||||
expect(
|
||||
() => coder.decodeResponse({ id: 1, jsonrpc: '2.0', error: { code: 123, message: 'test error' } })
|
||||
).toThrow(/\[123\]: test error/);
|
||||
});
|
||||
|
||||
it('returns the result', () => {
|
||||
expect(
|
||||
coder.decodeResponse({ id: 1, jsonrpc: '2.0', result: 'some result' })
|
||||
).toEqual('some result');
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeObject', () => {
|
||||
it('encodes a valid JsonRPC object', () => {
|
||||
expect(
|
||||
coder.encodeObject('method', 'params')
|
||||
).toEqual({
|
||||
id: 1,
|
||||
jsonrpc: '2.0',
|
||||
method: 'method',
|
||||
params: 'params'
|
||||
});
|
||||
expect(coder.id).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeJson', () => {
|
||||
it('encodes a valid JsonRPC JSON string', () => {
|
||||
expect(
|
||||
coder.encodeJson('method', 'params')
|
||||
).toEqual('{"id":1,"jsonrpc":"2.0","method":"method","params":"params"}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
// @flow
|
||||
|
||||
export type JsonRpcObject = {
|
||||
id: number;
|
||||
jsonrpc: '2.0';
|
||||
};
|
||||
|
||||
export type JsonRpcRequest = JsonRpcObject & {
|
||||
method: string;
|
||||
params: Array<any>;
|
||||
};
|
||||
|
||||
export type JsonRpcResponseBase = {
|
||||
error?: {
|
||||
code: number,
|
||||
message: string
|
||||
};
|
||||
result?: any;
|
||||
}
|
||||
|
||||
export type JsonRpcResponse = JsonRpcObject & JsonRpcResponseBase;
|
||||
|
||||
export interface ProviderInterface {
|
||||
send (method: string, params: Array<any>): Promise<any>;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
// @flow
|
||||
|
||||
import type { JsonRpcResponse, ProviderInterface } from '../types';
|
||||
|
||||
type AwaitingType = {
|
||||
callback: (error: ?Error, result: any) => void
|
||||
};
|
||||
|
||||
type WsMessageType = {
|
||||
data: any
|
||||
};
|
||||
|
||||
const JsonRpcCoder = require('../jsonRpcCoder');
|
||||
|
||||
module.exports = class WsProvider extends JsonRpcCoder implements ProviderInterface {
|
||||
_autoConnect: boolean = true;
|
||||
_endpoint: string;
|
||||
_handlers: { [number]: AwaitingType } = {};
|
||||
_websocket: WebSocket;
|
||||
|
||||
constructor (endpoint: string, autoConnect: boolean = true) {
|
||||
super();
|
||||
|
||||
this._endpoint = endpoint;
|
||||
this._autoConnect = autoConnect;
|
||||
|
||||
if (autoConnect) {
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
connect = () => {
|
||||
this._handlers = {};
|
||||
|
||||
this._websocket = new WebSocket(this._endpoint);
|
||||
|
||||
this._websocket.onclose = this._onClose;
|
||||
this._websocket.onerror = this._onError;
|
||||
this._websocket.onmessage = this._onMessage;
|
||||
this._websocket.onopen = this._onOpen;
|
||||
}
|
||||
|
||||
_onClose = () => {
|
||||
// console.log('disconnected from', this._endpoint);
|
||||
|
||||
if (this._autoConnect) {
|
||||
setTimeout(this.connect, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
_onError = (error: Event) => {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
_onOpen = () => {
|
||||
// console.log('connected to', this._endpoint);
|
||||
}
|
||||
|
||||
_onMessage = (message: WsMessageType) => {
|
||||
const response: JsonRpcResponse = JSON.parse(message.data);
|
||||
const handler = this._handlers[response.id];
|
||||
|
||||
if (!handler) {
|
||||
console.error(`Unable to find handler for id=${response.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = this.decodeResponse(response);
|
||||
|
||||
handler.callback(null, result);
|
||||
} catch (error) {
|
||||
handler.callback(error);
|
||||
}
|
||||
|
||||
delete this._handlers[response.id];
|
||||
}
|
||||
|
||||
send (method: string, params: Array<any>): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this._websocket.send(
|
||||
this.encodeObject(method, params)
|
||||
);
|
||||
|
||||
this._handlers[this.id] = {
|
||||
callback: (error: ?Error, result: any) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
// ISC, Copyright 2017 Jaco Greeff
|
||||
|
||||
/* global jest */
|
||||
|
||||
const { mockWs, TEST_WS_URL } = require('../../test/mockWs');
|
||||
|
||||
const { isUndefined } = require('@polkadot/util/is');
|
||||
|
||||
const Ws = require('./index');
|
||||
|
||||
let ws;
|
||||
let mock;
|
||||
let encodeSpy;
|
||||
let decodeSpy;
|
||||
|
||||
function createWs (requests, autoConnect) {
|
||||
mock = autoConnect || isUndefined(autoConnect)
|
||||
? mockWs(requests)
|
||||
: null;
|
||||
|
||||
ws = new Ws(TEST_WS_URL, autoConnect);
|
||||
|
||||
encodeSpy = jest.spyOn(ws, 'encodeObject');
|
||||
decodeSpy = jest.spyOn(ws, 'decodeResponse');
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
describe('Ws', () => {
|
||||
afterEach(() => {
|
||||
encodeSpy.mockRestore();
|
||||
decodeSpy.mockRestore();
|
||||
|
||||
if (mock) {
|
||||
mock.done();
|
||||
}
|
||||
});
|
||||
|
||||
describe('websocket', () => {
|
||||
let errorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
ws = createWs([]);
|
||||
|
||||
errorSpy = jest.spyOn(console, 'error');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('sets up the on* handlers', () => {
|
||||
expect(ws._websocket.onclose[0]).toEqual(ws._onClose);
|
||||
expect(ws._websocket.onerror[0]).toEqual(ws._onError);
|
||||
expect(ws._websocket.onmessage[0]).toEqual(ws._onMessage);
|
||||
expect(ws._websocket.onopen[0]).toEqual(ws._onOpen);
|
||||
});
|
||||
|
||||
describe('_onClose', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('reconnects after delay', () => {
|
||||
ws.connect = jest.fn();
|
||||
ws._onClose();
|
||||
|
||||
expect(ws.connect).not.toHaveBeenCalled();
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(ws.connect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not reconnect when autoConnect false', () => {
|
||||
mock.done();
|
||||
ws = createWs([], false);
|
||||
|
||||
ws.connect = jest.fn();
|
||||
ws._onClose();
|
||||
|
||||
expect(ws.connect).not.toHaveBeenCalled();
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(ws.connect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onError', () => {
|
||||
it('logs the error', () => {
|
||||
ws._onError('test error');
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith('test error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onMessage', () => {
|
||||
it('fails with log when handler not found', () => {
|
||||
ws._onMessage({ data: '{"id":2}' });
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith('Unable to find handler for id=2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('send', () => {
|
||||
it('handles internal errors', () => {
|
||||
ws = createWs([], false);
|
||||
|
||||
return ws
|
||||
.send('test_encoding', ['param'])
|
||||
.catch((error) => {
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('encodes requests', () => {
|
||||
ws = createWs([{
|
||||
id: 1,
|
||||
method: 'test_encoding',
|
||||
reply: {
|
||||
result: 'ok'
|
||||
}
|
||||
}]);
|
||||
|
||||
return ws
|
||||
.send('test_encoding', ['param'])
|
||||
.then((result) => {
|
||||
expect(encodeSpy).toHaveBeenCalledWith('test_encoding', ['param']);
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes responses', () => {
|
||||
ws = createWs([{
|
||||
id: 1,
|
||||
method: 'test_encoding',
|
||||
reply: {
|
||||
result: 'ok'
|
||||
}
|
||||
}]);
|
||||
|
||||
return ws
|
||||
.send('test_encoding', ['param'])
|
||||
.then((result) => {
|
||||
expect(decodeSpy).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
jsonrpc: '2.0',
|
||||
result: 'ok'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the body through correctly', () => {
|
||||
ws = createWs([{
|
||||
id: 1,
|
||||
method: 'test_body',
|
||||
reply: {
|
||||
result: 'ok'
|
||||
}
|
||||
}]);
|
||||
|
||||
return ws
|
||||
.send('test_body', ['param'])
|
||||
.then((result) => {
|
||||
expect(mock.body['test_body']).toEqual({
|
||||
id: 1,
|
||||
jsonrpc: '2.0',
|
||||
method: 'test_body',
|
||||
params: ['param']
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error when !response.ok', () => {
|
||||
ws = createWs([{
|
||||
id: 1,
|
||||
error: {
|
||||
code: 666,
|
||||
message: 'error'
|
||||
}
|
||||
}]);
|
||||
|
||||
return ws
|
||||
.send('test_error', [])
|
||||
.catch((error) => {
|
||||
expect(error.message).toMatch(/\[666\]: error/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user