First stage renames, rpc-{core,provider,rx} (#220)

* Docs renames (& removal for re-gen)

* Rename api, api-provider & api-rx

* Apoi -> Rpc, RxApi -> RpcRx (classes)

* Add .nodoc

* Bump version

* Link updates (summary)

* Cleanup deleted (master version)

* Deleted re-instated

* More... ;)

* Types...

* Revert .nodoc
This commit is contained in:
Jaco Greeff
2018-10-10 10:01:34 +02:00
committed by GitHub
parent cd7c17b265
commit 4344f33de8
102 changed files with 257 additions and 233 deletions
@@ -0,0 +1,33 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import Ws from './index';
describe('onConnect', () => {
let mock;
beforeEach(() => {
mock = mockWs([]);
});
afterEach(() => {
if (mock) {
mock.done();
mock = null;
}
});
it('sets up the on* handlers', () => {
const ws = new Ws(TEST_WS_URL, false);
ws.connect();
expect(ws.websocket.onclose[0]).toBeDefined();
expect(ws.websocket.onerror[0]).toBeDefined();
expect(ws.websocket.onmessage[0]).toBeDefined();
expect(ws.websocket.onopen[0]).toBeDefined();
});
});
@@ -0,0 +1,32 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import Ws from './index';
let ws;
let mock;
function createWs (requests, autoConnect) {
mock = mockWs(requests);
ws = new Ws(TEST_WS_URL, autoConnect);
return ws;
}
describe('Ws', () => {
afterEach(() => {
if (mock) {
mock.done();
mock = null;
}
});
it('returns the connected state', () => {
expect(
createWs([]).isConnected()
).toEqual(false);
});
});
+316
View File
@@ -0,0 +1,316 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { Logger } from '@polkadot/util/types';
import { RpcCoder } from '../coder/json/types';
import { JsonRpcResponse, ProviderInterface, ProviderInterface$Callback, ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types';
import './polyfill';
import E3 from 'eventemitter3';
import assert from '@polkadot/util/assert';
import isNull from '@polkadot/util/is/null';
import isUndefined from '@polkadot/util/is/undefined';
import logger from '@polkadot/util/logger';
import coder from '../coder/json';
type SubscriptionHandler = {
callback: ProviderInterface$Callback,
type: string
};
type WsState$Awaiting = {
callback: ProviderInterface$Callback,
method: string,
params: Array<any>,
subscription?: SubscriptionHandler
};
type WsState$Subscription = SubscriptionHandler & {
method: string,
params: Array<any>
};
interface WSProviderInterface extends ProviderInterface {
connect (): void;
}
/**
* @name WsProvider
* @summary The WebSocket Provider allows sending requests using WebSocket to a WebSocket RPC server TCP port.
* @description Unlike the [[HttpProvider]], it does support subscriptions and allows
* listening to events such as new blocks or balance changes.
* @example
* <BR>
*
* ```javascript
* import Api from '@polkadot/rpc-core';
* import WsProvider from '@polkadot/rpc-provider/ws';
*
* const provider = new WsProvider('ws://127.0.0.1:9944');
* const api = new Api(provider);
* ```
*
* @see [[HttpProvider]]
*/
export default class WsProvider extends E3.EventEmitter implements WSProviderInterface {
private autoConnect: boolean;
private coder: RpcCoder;
private endpoint: string;
private handlers: {
[index: number]: WsState$Awaiting
};
private _isConnected: boolean;
private l: Logger;
private queued: {
[index: string]: string
};
private subscriptions: {
[index: string]: WsState$Subscription
};
private websocket: WebSocket | null;
/**
* @param {string} endpoint The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`
* @param {boolean} autoConnect Whether to connect automatically or not.
*/
constructor (endpoint: string, autoConnect: boolean = true) {
super();
assert(/^(wss|ws):\/\//.test(endpoint), `Endpoint should start with 'ws://', received '${endpoint}'`);
this.autoConnect = autoConnect;
this.coder = coder();
this.endpoint = endpoint;
this._isConnected = false;
this.handlers = {};
this.l = logger('api-ws');
this.queued = {};
this.subscriptions = {};
this.websocket = null;
if (autoConnect) {
this.connect();
}
}
/**
* @summary Manually connect
* @description The [[WsProvider]] connects automatically by default, however if you decided otherwise, you may
* connect manually using this method.
*/
connect (): void {
try {
this.websocket = new WebSocket(this.endpoint);
this.websocket.onclose = this.onSocketClose;
this.websocket.onerror = this.onSocketError;
this.websocket.onmessage = this.onSocketMessage;
this.websocket.onopen = this.onSocketOpen;
} catch (error) {
this.l.error(error);
}
}
/**
* @summary Whether the node is connected or not.
* @return {boolean} true if connected
*/
isConnected (): boolean {
return this._isConnected;
}
/**
* @summary Listens on events after having subscribed using the [[subscribe]] function.
* @param {ProviderInterface$Emitted} type Event
* @param {ProviderInterface$EmitCb} sub Callback
* @return {this} [description]
*/
on (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): this {
return super.on(type, sub);
}
/**
* @summary Send JSON data using WebSockets to configured HTTP Endpoint or queue.
*/
async send (method: string, params: Array<any>, subscription?: SubscriptionHandler): Promise<any> {
return new Promise((resolve, reject): void => {
try {
const json = this.coder.encodeJson(method, params);
const id = this.coder.getId();
const callback = (error: Error | null, result: any) => {
if (error) {
reject(error);
} else {
resolve(result);
}
};
this.l.debug(() => ['calling', method, params, json, !!subscription]);
this.handlers[id] = {
callback,
method,
params,
subscription
};
if (this.isConnected() && !isNull(this.websocket)) {
this.websocket.send(json);
} else {
this.queued[id] = json;
}
} catch (error) {
reject(error);
}
});
}
/**
* @name subscribe
* @summary Allows subscribing to a specific event.
* @param {string} type Subscription type
* @param {string} method Subscription method
* @param {Array<any>} params Parameters
* @param {ProviderInterface$Callback} callback Callback
* @return {Promise<number>} Promise resolving to the dd of the subscription you can use with [[unsubscribe]].
*
* @example
* <BR>
*
* ```javascript
* const provider = new WsProvider('ws://127.0.0.1:9944');
* const api = new Api(provider);
*
* api.state.storage([[storage.balances.freeBalance, <Address>]], (_, values) => {
* console.log(values)
* }).then((subscriptionId) => {
* console.log('balance changes subscription id: ', subscriptionId)
* })
* ```
*/
async subscribe (type: string, method: string, params: Array<any>, callback: ProviderInterface$Callback): Promise<number> {
const id = await this.send(method, params, { callback, type });
return id as number;
}
/**
* @summary Allows unsubscribing to subscriptions made with [[subscribe]].
*/
async unsubscribe (type: string, method: string, id: number): Promise<boolean> {
const subscription = `${type}::${id}`;
assert(!isUndefined(this.subscriptions[subscription]), `Unable to find active subscription=${subscription}`);
delete this.subscriptions[subscription];
const result = await this.send(method, [id]);
return result as boolean;
}
private onSocketClose = (): void => {
this.l.debug(() => ['disconnected from', this.endpoint]);
this._isConnected = false;
this.emit('disconnected');
if (this.autoConnect) {
setTimeout(() => {
this.connect();
}, 1000);
}
}
private onSocketError = (error: Event): void => {
this.l.error(error);
}
private onSocketMessage = (message: MessageEvent): void => {
this.l.debug(() => ['received', message.data]);
const response: JsonRpcResponse = JSON.parse(message.data as string);
return isUndefined(response.method)
? this.onSocketMessageResult(response)
: this.onSocketMessageSubscribe(response);
}
private onSocketMessageResult = (response: JsonRpcResponse): void => {
this.l.debug(() => ['handling: response =', response, 'id =', response.id]);
const handler = this.handlers[response.id];
if (!handler) {
this.l.error(`Unable to find handler for id=${response.id}`);
return;
}
try {
const { method, params, subscription } = handler;
const result = this.coder.decodeResponse(response);
if (subscription) {
this.subscriptions[`${subscription.type}::${result}`] = {
...subscription,
method,
params
};
}
handler.callback(null, result);
} catch (error) {
handler.callback(error, undefined);
}
delete this.handlers[response.id];
}
private onSocketMessageSubscribe = (response: JsonRpcResponse): void => {
const subscription = `${response.method}::${response.params.subscription}`;
this.l.debug(() => ['handling: response =', response, 'subscription =', subscription]);
const handler = this.subscriptions[subscription];
if (!handler) {
this.l.error(`Unable to find handler for subscription=${subscription}`);
return;
}
try {
const result = this.coder.decodeResponse(response);
handler.callback(null, result);
} catch (error) {
handler.callback(error, undefined);
}
}
private onSocketOpen = (): boolean => {
assert(!isNull(this.websocket), 'WebSocket cannot be null in onOpen');
this.l.debug(() => ['connected to', this.endpoint]);
this._isConnected = true;
this.emit('connected');
Object.keys(this.queued).forEach((id) => {
try {
// @ts-ignore checked above
this.websocket.send(
this.queued[id]
);
delete this.queued[id];
} catch (error) {
this.l.error(error);
}
});
return true;
}
}
@@ -0,0 +1,27 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import Ws from './index';
describe('onError', () => {
let ws;
let errorSpy;
beforeEach(() => {
ws = new Ws('ws://127.0.0.1:1234', false);
errorSpy = jest.spyOn(console, 'error');
});
afterEach(() => {
errorSpy.mockRestore();
});
it('logs the error', () => {
ws.onSocketError('test error');
expect(errorSpy).toHaveBeenCalledWith(
expect.anything(), expect.anything(), 'test error'
);
});
});
@@ -0,0 +1,38 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import Ws from './index';
describe('onMessageResult', () => {
let ws;
let errorSpy;
beforeEach(() => {
ws = new Ws('ws://127.0.0.1:1234', false);
errorSpy = jest.spyOn(console, 'error');
});
afterEach(() => {
errorSpy.mockRestore();
});
it('fails with log when handler not found', () => {
ws.onSocketMessage({ data: '{"id":2}' });
expect(errorSpy).toHaveBeenCalledWith(
expect.anything(), expect.anything(), 'Unable to find handler for id=2'
);
});
it('calls the handler when found', (done) => {
ws.handlers[5] = {
callback: (_, result) => {
expect(result).toEqual('test');
done();
}
};
ws.onSocketMessage({ data: '{"jsonrpc":"2.0","id":5,"result":"test"}' });
});
});
@@ -0,0 +1,61 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import Ws from './index';
describe('onMessageSubscribe', () => {
let ws;
let errorSpy;
beforeEach(() => {
ws = new Ws('ws://127.0.0.1:1234', false);
errorSpy = jest.spyOn(console, 'error');
});
afterEach(() => {
errorSpy.mockRestore();
});
it('fails with log when handler not found', () => {
ws.onSocketMessage({ data: '{"method":"test","params":{"subscription":2}}' });
expect(errorSpy).toHaveBeenCalledWith(
expect.anything(), expect.anything(), 'Unable to find handler for subscription=test::2'
);
});
it('calls the subscriber with data', (done) => {
ws.handlers[11] = {
callback: (_, id) => {},
method: 'test',
subscription: {
callback: (_, result) => {
expect(result).toEqual('test');
done();
},
type: 'test'
}
};
ws.onSocketMessage({ data: '{"jsonrpc":"2.0","id":11,"result":22}' });
ws.onSocketMessage({ data: '{"jsonrpc":"2.0","method":"test","params":{"subscription":22,"result":"test"}}' });
});
it('calls the subscriber with error', (done) => {
ws.handlers[11] = {
callback: (_, id) => {},
method: 'test',
subscription: {
callback: (error) => {
expect(error.message).toMatch(/test/);
done();
},
type: 'test'
}
};
ws.onSocketMessage({ data: '{"jsonrpc":"2.0","id":11,"result":22}' });
ws.onSocketMessage({ data: '{"jsonrpc":"2.0","method":"test","params":{"subscription":22,"error":{"message":"test"}}}' });
});
});
@@ -0,0 +1,43 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import Ws from './index';
let ws;
let mock;
function createWs (requests, autoConnect) {
mock = mockWs(requests);
ws = new Ws(TEST_WS_URL, autoConnect);
return ws;
}
describe('onOpen', () => {
afterEach(() => {
if (mock) {
mock.done();
mock = null;
}
});
it('sends messages when connected', () => {
const ws = createWs([{
id: 1,
method: 'test_queue',
reply: {
result: 'ok'
}
}], false);
const sendPromise = ws.send('test_queue', []);
ws.connect();
return sendPromise.then((result) => {
expect(result).toEqual('ok');
});
});
});
@@ -0,0 +1,26 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
describe('ws/polyfill', () => {
let origWs;
beforeEach(() => {
origWs = global.WebSocket;
global.WebSocket = null;
});
afterEach(() => {
global.WebSocket = origWs;
});
it('polyfills with no exceptions (without WebSocket)', () => {
expect(require('./polyfill')).toBeDefined();
});
it('polyfills with no exceptions (with WebSocket)', () => {
global.WebSocket = () => true;
expect(require('./polyfill')).toBeDefined();
});
});
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// tslint:disable-next-line
if (typeof WebSocket === 'undefined') {
// @ts-ignore
global.WebSocket = require('websocket').w3cwebsocket;
}
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import Ws from './index';
let ws;
let mock;
function createMock (requests) {
mock = mockWs(requests);
}
function createWs (autoConnect) {
ws = new Ws(TEST_WS_URL, autoConnect);
return ws;
}
describe('send', () => {
let globalWs;
beforeEach(() => {
globalWs = global.WebSocket;
});
afterEach(() => {
global.WebSocket = globalWs;
if (mock) {
mock.done();
mock = null;
}
});
it('handles internal errors', () => {
createMock([]);
let websocket;
global.WebSocket = class {
constructor () {
websocket = this;
}
send () {
throw new Error('send error');
}
};
ws = createWs();
websocket.onopen();
return ws
.send('test_encoding', ['param'])
.catch((error) => {
expect(error.message).toEqual('send error');
});
});
it('passes the body through correctly', () => {
createMock([{
id: 1,
method: 'test_body',
reply: {
result: 'ok'
}
}]);
return createWs()
.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', () => {
createMock([{
id: 1,
error: {
code: 666,
message: 'error'
}
}]);
return createWs()
.send('test_error', [])
.catch((error) => {
expect(error.message).toMatch(/\[666\]: error/);
});
});
it('adds subscriptions', () => {
createMock([{
id: 1,
method: 'test_sub',
reply: {
result: 1
}
}]);
return createWs()
.send('test_sub', [], () => {})
.then((id) => {
expect(id).toEqual(1);
});
});
});
@@ -0,0 +1,19 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import Ws from './index';
describe('state', () => {
it('requires an ws:// prefixed endpoint', () => {
expect(
() => new Ws('http://', false)
).toThrow(/with 'ws/);
});
it('allows wss:// endpoints', () => {
expect(
() => new Ws('wss://', false)
).not.toThrow();
});
});
@@ -0,0 +1,53 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import Ws from './index';
let ws;
let mock;
function createMock (requests) {
mock = mockWs(requests);
}
function createWs (autoConnect) {
ws = new Ws(TEST_WS_URL, autoConnect);
return ws;
}
describe('subscribe', () => {
let globalWs;
beforeEach(() => {
globalWs = global.WebSocket;
});
afterEach(() => {
global.WebSocket = globalWs;
if (mock) {
mock.done();
mock = null;
}
});
it('adds subscriptions', () => {
createMock([{
id: 1,
method: 'test_sub',
reply: {
result: 1
}
}]);
return createWs()
.subscribe('test_sub', [], () => {})
.then((id) => {
expect(id).toEqual(1);
});
});
});
@@ -0,0 +1,85 @@
// Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import Ws from './index';
let ws;
let mock;
function createMock (requests) {
mock = mockWs(requests);
}
function createWs (autoConnect) {
ws = new Ws(TEST_WS_URL, autoConnect);
return ws;
}
describe('subscribe', () => {
let globalWs;
beforeEach(() => {
globalWs = global.WebSocket;
});
afterEach(() => {
global.WebSocket = globalWs;
if (mock) {
mock.done();
mock = null;
}
});
it('removes subscriptions', () => {
createMock([
{
id: 1,
method: 'subscribe_test',
reply: {
result: 1
}
},
{
id: 2,
method: 'unsubscribe_test',
reply: {
result: true
}
}
]);
const ws = createWs();
return ws
.subscribe('test', 'subscribe_test', [], () => {})
.then((id) => {
return ws.unsubscribe('test', 'subscribe_test', id);
});
});
it('fails when sub not found', () => {
createMock([{
id: 1,
method: 'subscribe_test',
reply: {
result: 1
}
}]);
const ws = createWs();
return ws
.subscribe('test', 'subscribe_test', [], () => {})
.then((id) => {
return ws.unsubscribe('test', 'subscribe_test', 111);
})
.catch((error) => {
expect(error.message).toMatch(/find active subscription/);
});
});
});