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:
Jaco Greeff
2017-11-28 18:03:48 +01:00
committed by GitHub
parent bf6880d5b0
commit e8ccc9aa79
64 changed files with 2378 additions and 136 deletions
+39
View File
@@ -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();
});
});
+10
View File
@@ -0,0 +1,10 @@
// ISC, Copyright 2017 Jaco Greeff
// @flow
const Http = require('./http');
const Ws = require('./ws');
module.exports = {
Http,
Ws
};
+55
View File
@@ -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"}');
});
});
});
+26
View File
@@ -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>;
}
+101
View File
@@ -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);
}
});
}
};
+195
View File
@@ -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/);
});
});
});
});