Classify Http & Ws providers (#120)

* Class-ify HttpProvider

* Class-ify WsProvider (tests still WIP)

* Rework tests

* rx-api usage
This commit is contained in:
Jaco Greeff
2018-08-07 16:59:40 +02:00
committed by GitHub
parent f74fd9e01d
commit fe4c34defe
38 changed files with 342 additions and 531 deletions
+14 -2
View File
@@ -4,13 +4,25 @@
import { TEST_HTTP_URL } from '../../test/mockHttp'; import { TEST_HTTP_URL } from '../../test/mockHttp';
import createHttp from './index'; import Http from './index';
describe('Http', () => { describe('Http', () => {
let http; let http;
beforeEach(() => { beforeEach(() => {
http = createHttp(TEST_HTTP_URL); http = new Http(TEST_HTTP_URL);
});
it('requires an http:// prefixed endpoint', () => {
expect(
() => new Http('ws://')
).toThrow(/with 'http/);
});
it('allows https:// endpoints', () => {
expect(
() => new Http('https://')
).not.toThrow();
}); });
it('always returns isConnected true', () => { it('always returns isConnected true', () => {
+52 -19
View File
@@ -2,29 +2,62 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details. // of the ISC license. See the LICENSE file for details.
import { Logger } from '@polkadot/util/types';
import { RpcCoder } from '../coder/json/types';
import { ProviderInterface, ProviderInterface$Callback, ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types'; import { ProviderInterface, ProviderInterface$Callback, ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types';
import './polyfill'; import './polyfill';
import on from './on'; import assert from '@polkadot/util/assert';
import send from './send'; import logger from '@polkadot/util/logger';
import subscribe from './subscribe';
import state from './state';
import unsubscribe from './unsubscribe';
export default function httpProvider (endpoint: string): ProviderInterface { import coder from '../coder/json';
const self = state(endpoint);
return { export default class HttpProvider implements ProviderInterface {
isConnected: (): boolean => private coder: RpcCoder;
true, private endpoint: string;
on: (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void => private l: Logger;
on(self, type, sub),
send: (method: string, params: Array<any>): Promise<any> => constructor (endpoint: string) {
send(self, method, params), assert(/^(https|http):\/\//.test(endpoint), `Endpoint should start with 'http://', received '${endpoint}'`);
subscribe: (method: string, params: Array<any>, cb: ProviderInterface$Callback): Promise<number> =>
subscribe(self, method, params, cb), this.coder = coder();
unsubscribe: (method: string, id: number): Promise<boolean> => this.endpoint = endpoint;
unsubscribe(self, method, id) this.l = logger('api-http');
}; }
isConnected (): boolean {
return true;
}
on (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void {
this.l.error(`Provider does not have 'on' emitters for type '${type}'`);
}
async send (method: string, params: Array<any>): Promise<any> {
const body = this.coder.encodeJson(method, params);
const response = await fetch(this.endpoint, {
body,
headers: {
'Accept': 'application/json',
'Content-Length': `${body.length}`,
'Content-Type': 'application/json'
},
method: 'POST'
});
assert(response.ok, `[${response.status}]: ${response.statusText}`);
const result = await response.json();
return this.coder.decodeResponse(result);
}
async subscribe (method: string, params: Array<any>, cb: ProviderInterface$Callback): Promise<number> {
throw new Error('Subscriptions has not been implemented');
}
async unsubscribe (method: string, id: number): Promise<boolean> {
throw new Error('Subscriptions has not been implemented');
}
} }
-10
View File
@@ -1,10 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types';
import { HttpState } from './types';
export default function on (self: HttpState, type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void {
self.l.error(`Provider does not have 'on' emitters for type '${type}'`);
}
+2 -2
View File
@@ -4,14 +4,14 @@
import { mockHttp, TEST_HTTP_URL } from '../../test/mockHttp'; import { mockHttp, TEST_HTTP_URL } from '../../test/mockHttp';
import createHttp from './index'; import Http from './index';
describe('send', () => { describe('send', () => {
let http; let http;
let mock; let mock;
beforeEach(() => { beforeEach(() => {
http = createHttp(TEST_HTTP_URL); http = new Http(TEST_HTTP_URL);
}); });
afterEach(() => { afterEach(() => {
-26
View File
@@ -1,26 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { HttpState } from './types';
import assert from '@polkadot/util/assert';
export default async function send ({ coder, endpoint }: HttpState, method: string, params: Array<any>): Promise<any> {
const body = coder.encodeJson(method, params);
const response = await fetch(endpoint, {
body,
headers: {
'Accept': 'application/json',
'Content-Length': `${body.length}`,
'Content-Type': 'application/json'
},
method: 'POST'
});
assert(response.ok, `[${response.status}]: ${response.statusText}`);
const result = await response.json();
return coder.decodeResponse(result);
}
@@ -1,19 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import state from './state';
describe('state', () => {
it('requires an http:// prefixed endpoint', () => {
expect(
() => state('ws://')
).toThrow(/with 'http/);
});
it('allows https:// endpoints', () => {
expect(
state('https://')
).toBeDefined();
});
});
-22
View File
@@ -1,22 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { HttpState } from './types';
import assert from '@polkadot/util/assert';
import logger from '@polkadot/util/logger';
import coder from '../coder/json';
const l = logger('api-http');
export default function state (endpoint: string): HttpState {
assert(/^(https|http):\/\//.test(endpoint), `Endpoint should start with 'http://', received '${endpoint}'`);
return {
coder: coder(),
endpoint,
l
};
}
@@ -1,10 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { ProviderInterface$Callback } from '../types';
import { HttpState } from './types';
export default async function subscribe (self: HttpState, method: string, params: Array<any>, cb: ProviderInterface$Callback): Promise<number> {
throw new Error('Subscriptions has not been implemented');
}
@@ -1,9 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { HttpState } from './types';
export default async function unsubscribe (self: HttpState, method: string, id: number): Promise<boolean> {
throw new Error('Subscriptions has not been implemented');
}
+1 -1
View File
@@ -41,7 +41,7 @@ export type ProviderInterface$Emitted = 'connected' | 'disconnected';
export type ProviderInterface$EmitCb = (value?: any) => any; export type ProviderInterface$EmitCb = (value?: any) => any;
export type ProviderInterface = { export interface ProviderInterface {
isConnected (): boolean, isConnected (): boolean,
on (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void, on (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void,
send (method: string, params: Array<any>): Promise<any>, send (method: string, params: Array<any>): Promise<any>,
+8 -9
View File
@@ -4,10 +4,9 @@
import { mockWs, TEST_WS_URL } from '../../test/mockWs'; import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import createState from './state'; import Ws from './index';
import connect from './connect';
describe('onError', () => { describe('onConnect', () => {
let mock; let mock;
beforeEach(() => { beforeEach(() => {
@@ -22,13 +21,13 @@ describe('onError', () => {
}); });
it('sets up the on* handlers', () => { it('sets up the on* handlers', () => {
const state = createState(TEST_WS_URL, false); const ws = new Ws(TEST_WS_URL, false);
connect(state); ws.connect();
expect(state.websocket.onclose[0]).toBeDefined(); expect(ws.websocket.onclose[0]).toBeDefined();
expect(state.websocket.onerror[0]).toBeDefined(); expect(ws.websocket.onerror[0]).toBeDefined();
expect(state.websocket.onmessage[0]).toBeDefined(); expect(ws.websocket.onmessage[0]).toBeDefined();
expect(state.websocket.onopen[0]).toBeDefined(); expect(ws.websocket.onopen[0]).toBeDefined();
}); });
}); });
-23
View File
@@ -1,23 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { WsState } from './types';
import onClose from './onClose';
import onError from './onError';
import onMessage from './onMessage';
import onOpen from './onOpen';
export default function connect (self: WsState): void {
try {
self.websocket = new WebSocket(self.endpoint);
self.websocket.onclose = onClose(self);
self.websocket.onerror = onError(self);
self.websocket.onmessage = onMessage(self);
self.websocket.onopen = onOpen(self);
} catch (error) {
self.l.error(error);
}
}
+2 -2
View File
@@ -4,14 +4,14 @@
import { mockWs, TEST_WS_URL } from '../../test/mockWs'; import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import create from './index'; import Ws from './index';
let ws; let ws;
let mock; let mock;
function createWs (requests, autoConnect) { function createWs (requests, autoConnect) {
mock = mockWs(requests); mock = mockWs(requests);
ws = create(TEST_WS_URL, autoConnect); ws = new Ws(TEST_WS_URL, autoConnect);
return ws; return ws;
} }
+220 -27
View File
@@ -2,40 +2,233 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details. // of the ISC license. See the LICENSE file for details.
import { ProviderInterface, ProviderInterface$Callback, ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types'; 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 './polyfill';
import connect from './connect'; import E3 from 'eventemitter3';
import on from './on'; import assert from '@polkadot/util/assert';
import send from './send'; import isNull from '@polkadot/util/is/null';
import state from './state'; import isUndefined from '@polkadot/util/is/undefined';
import subscribe from './subscribe'; import logger from '@polkadot/util/logger';
import unsubscribe from './unsubscribe';
type WSProviderInterface = ProviderInterface & { import coder from '../coder/json';
connect (): void
type WsState$Awaiting = {
callback: ProviderInterface$Callback,
subscription?: ProviderInterface$Callback
}; };
export default function wsProvider (endpoint: string, autoConnect: boolean = true): WSProviderInterface { type WsState$Subscription = {
const self = state(endpoint, autoConnect); callback: ProviderInterface$Callback
};
if (autoConnect) { interface WSProviderInterface extends ProviderInterface {
connect(self); connect (): void;
}
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: number]: WsState$Subscription
};
private websocket: WebSocket | null;
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();
}
} }
return { connect (): void {
connect: (): void => try {
connect(self), this.websocket = new WebSocket(this.endpoint);
isConnected: (): boolean =>
self.isConnected, this.websocket.onclose = this.onSocketClose;
on: (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void => this.websocket.onerror = this.onSocketError;
on(self, type, sub), this.websocket.onmessage = this.onSocketMessage;
send: (method: string, params: Array<any>): Promise<any> => this.websocket.onopen = this.onSocketOpen;
send(self, method, params), } catch (error) {
subscribe: (method: string, params: Array<any>, cb: ProviderInterface$Callback): Promise<number> => this.l.error(error);
subscribe(self, method, params, cb), }
unsubscribe: (method: string, id: number): Promise<boolean> => }
unsubscribe(self, method, id)
}; isConnected (): boolean {
return this._isConnected;
}
on (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): this {
return super.on(type, sub);
}
async send (method: string, params: Array<any>, subscription?: ProviderInterface$Callback): 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,
subscription
};
if (this.isConnected && !isNull(this.websocket)) {
this.websocket.send(json);
} else {
this.queued[id] = json;
}
} catch (error) {
reject(error);
}
});
}
async subscribe (method: string, params: Array<any>, subscription: ProviderInterface$Callback): Promise<number> {
const id = await this.send(method, params, subscription);
return id as number;
}
async unsubscribe (method: string, id: number): Promise<boolean> {
assert(!isUndefined(this.subscriptions[id]), `Unable to find active subscription=${id}`);
delete this.subscriptions[id];
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 { subscription } = this.handlers[response.id];
const result = this.coder.decodeResponse(response);
if (subscription) {
this.subscriptions[result as number] = {
callback: subscription
};
}
handler.callback(null, result);
} catch (error) {
handler.callback(error, undefined);
}
delete this.handlers[response.id];
}
private onSocketMessageSubscribe = (response: JsonRpcResponse): void => {
this.l.debug(() => ['handling: response =', response, 'subscription =', response.params.subscription]);
const handler = this.subscriptions[response.params.subscription];
if (!handler) {
this.l.error(`Unable to find handler for subscription=${response.params.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;
}
} }
-10
View File
@@ -1,10 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types';
import { WsState } from './types';
export default function on (self: WsState, type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void {
self.emitter.on(type, sub);
}
@@ -1,42 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { TEST_WS_URL } from '../../test/mockWs';
import createState from './state';
describe('onClose', () => {
let mockConnect;
beforeEach(() => {
mockConnect = jest.fn();
jest.mock('./connect', () => mockConnect);
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('reconnects after delay', () => {
require('./onClose').default(createState(TEST_WS_URL, true))();
expect(mockConnect).not.toHaveBeenCalled();
jest.runAllTimers();
expect(mockConnect).toHaveBeenCalled();
});
it('does not reconnect when autoConnect false', () => {
require('./onClose').default(createState(TEST_WS_URL, false))();
expect(mockConnect).not.toHaveBeenCalled();
jest.runAllTimers();
expect(mockConnect).not.toHaveBeenCalled();
});
});
-22
View File
@@ -1,22 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { WsState } from './types';
import connect from './connect';
export default function onClose (self: WsState): () => void {
return (): void => {
self.l.debug(() => ['disconnected from', self.endpoint]);
self.isConnected = false;
self.emitter.emit('disconnected');
if (self.autoConnect) {
setTimeout(() => {
connect(self);
}, 1000);
}
};
}
+4 -5
View File
@@ -2,15 +2,14 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details. // of the ISC license. See the LICENSE file for details.
import onError from './onError'; import Ws from './index';
import createState from './state';
describe('onError', () => { describe('onError', () => {
let state; let ws;
let errorSpy; let errorSpy;
beforeEach(() => { beforeEach(() => {
state = createState('ws://127.0.0.1:1234', false); ws = new Ws('ws://127.0.0.1:1234', false);
errorSpy = jest.spyOn(console, 'error'); errorSpy = jest.spyOn(console, 'error');
}); });
@@ -19,7 +18,7 @@ describe('onError', () => {
}); });
it('logs the error', () => { it('logs the error', () => {
onError(state)('test error'); ws.onSocketError('test error');
expect(errorSpy).toHaveBeenCalledWith( expect(errorSpy).toHaveBeenCalledWith(
expect.anything(), expect.anything(), 'test error' expect.anything(), expect.anything(), 'test error'
-11
View File
@@ -1,11 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { WsState } from './types';
export default function onError (self: WsState): (event: Event) => void {
return (error: Event): void => {
self.l.error(error);
};
}
-23
View File
@@ -1,23 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { JsonRpcResponse } from '../types';
import { WsState } from './types';
import isUndefined from '@polkadot/util/is/undefined';
import onMessageResult from './onMessageResult';
import onMessageSubscribe from './onMessageSubscribe';
export default function onMessage (self: WsState): (event: MessageEvent) => void {
return (message: MessageEvent): void => {
self.l.debug(() => ['received', message.data]);
const response: JsonRpcResponse = JSON.parse(message.data as string);
return isUndefined(response.method)
? onMessageResult(self, response)
: onMessageSubscribe(self, response);
};
}
@@ -2,15 +2,14 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details. // of the ISC license. See the LICENSE file for details.
import onMessage from './onMessage'; import Ws from './index';
import createState from './state';
describe('onMessageResult', () => { describe('onMessageResult', () => {
let state; let ws;
let errorSpy; let errorSpy;
beforeEach(() => { beforeEach(() => {
state = createState('ws://127.0.0.1:1234', false); ws = new Ws('ws://127.0.0.1:1234', false);
errorSpy = jest.spyOn(console, 'error'); errorSpy = jest.spyOn(console, 'error');
}); });
@@ -19,7 +18,7 @@ describe('onMessageResult', () => {
}); });
it('fails with log when handler not found', () => { it('fails with log when handler not found', () => {
onMessage(state)({ data: '{"id":2}' }); ws.onSocketMessage({ data: '{"id":2}' });
expect(errorSpy).toHaveBeenCalledWith( expect(errorSpy).toHaveBeenCalledWith(
expect.anything(), expect.anything(), 'Unable to find handler for id=2' expect.anything(), expect.anything(), 'Unable to find handler for id=2'
@@ -27,13 +26,13 @@ describe('onMessageResult', () => {
}); });
it('calls the handler when found', (done) => { it('calls the handler when found', (done) => {
state.handlers[5] = { ws.handlers[5] = {
callback: (_, result) => { callback: (_, result) => {
expect(result).toEqual('test'); expect(result).toEqual('test');
done(); done();
} }
}; };
onMessage(state)({ data: '{"jsonrpc":"2.0","id":5,"result":"test"}' }); ws.onSocketMessage({ data: '{"jsonrpc":"2.0","id":5,"result":"test"}' });
}); });
}); });
@@ -1,34 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { JsonRpcResponse } from '../types';
import { WsState } from './types';
export default function onMessageResult (self: WsState, response: JsonRpcResponse): void {
self.l.debug(() => ['handling: response =', response, 'id =', response.id]);
const handler = self.handlers[response.id];
if (!handler) {
self.l.error(`Unable to find handler for id=${response.id}`);
return;
}
try {
const { subscription } = self.handlers[response.id];
const result = self.coder.decodeResponse(response);
if (subscription) {
self.subscriptions[result as number] = {
callback: subscription
};
}
handler.callback(null, result);
} catch (error) {
handler.callback(error, undefined);
}
delete self.handlers[response.id];
}
@@ -2,15 +2,14 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details. // of the ISC license. See the LICENSE file for details.
import onMessage from './onMessage'; import Ws from './index';
import createState from './state';
describe('onMessageSubscribe', () => { describe('onMessageSubscribe', () => {
let state; let ws;
let errorSpy; let errorSpy;
beforeEach(() => { beforeEach(() => {
state = createState('ws://127.0.0.1:1234', false); ws = new Ws('ws://127.0.0.1:1234', false);
errorSpy = jest.spyOn(console, 'error'); errorSpy = jest.spyOn(console, 'error');
}); });
@@ -19,7 +18,7 @@ describe('onMessageSubscribe', () => {
}); });
it('fails with log when handler not found', () => { it('fails with log when handler not found', () => {
onMessage(state)({ data: '{"method":"test","params":{"subscription":2}}' }); ws.onSocketMessage({ data: '{"method":"test","params":{"subscription":2}}' });
expect(errorSpy).toHaveBeenCalledWith( expect(errorSpy).toHaveBeenCalledWith(
expect.anything(), expect.anything(), 'Unable to find handler for subscription=2' expect.anything(), expect.anything(), 'Unable to find handler for subscription=2'
@@ -27,7 +26,7 @@ describe('onMessageSubscribe', () => {
}); });
it('calls the subscriber with data', (done) => { it('calls the subscriber with data', (done) => {
state.handlers[11] = { ws.handlers[11] = {
callback: (_, id) => {}, callback: (_, id) => {},
subscription: (_, result) => { subscription: (_, result) => {
expect(result).toEqual('test'); expect(result).toEqual('test');
@@ -35,12 +34,12 @@ describe('onMessageSubscribe', () => {
} }
}; };
onMessage(state)({ data: '{"jsonrpc":"2.0","id":11,"result":22}' }); ws.onSocketMessage({ data: '{"jsonrpc":"2.0","id":11,"result":22}' });
onMessage(state)({ data: '{"jsonrpc":"2.0","method":"test","params":{"subscription":22,"result":"test"}}' }); ws.onSocketMessage({ data: '{"jsonrpc":"2.0","method":"test","params":{"subscription":22,"result":"test"}}' });
}); });
it('calls the subscriber with error', (done) => { it('calls the subscriber with error', (done) => {
state.handlers[11] = { ws.handlers[11] = {
callback: (_, id) => {}, callback: (_, id) => {},
subscription: (error) => { subscription: (error) => {
expect(error.message).toMatch(/test/); expect(error.message).toMatch(/test/);
@@ -48,7 +47,7 @@ describe('onMessageSubscribe', () => {
} }
}; };
onMessage(state)({ data: '{"jsonrpc":"2.0","id":11,"result":22}' }); ws.onSocketMessage({ data: '{"jsonrpc":"2.0","id":11,"result":22}' });
onMessage(state)({ data: '{"jsonrpc":"2.0","method":"test","params":{"subscription":22,"error":{"message":"test"}}}' }); ws.onSocketMessage({ data: '{"jsonrpc":"2.0","method":"test","params":{"subscription":22,"error":{"message":"test"}}}' });
}); });
}); });
@@ -1,25 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { JsonRpcResponse } from '../types';
import { WsState } from './types';
export default function onMessageSubscribe (self: WsState, response: JsonRpcResponse): void {
self.l.debug(() => ['handling: response =', response, 'subscription =', response.params.subscription]);
const handler = self.subscriptions[response.params.subscription];
if (!handler) {
self.l.error(`Unable to find handler for subscription=${response.params.subscription}`);
return;
}
try {
const result = self.coder.decodeResponse(response);
handler.callback(null, result);
} catch (error) {
handler.callback(error, undefined);
}
}
+2 -2
View File
@@ -4,14 +4,14 @@
import { mockWs, TEST_WS_URL } from '../../test/mockWs'; import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import create from './index'; import Ws from './index';
let ws; let ws;
let mock; let mock;
function createWs (requests, autoConnect) { function createWs (requests, autoConnect) {
mock = mockWs(requests); mock = mockWs(requests);
ws = create(TEST_WS_URL, autoConnect); ws = new Ws(TEST_WS_URL, autoConnect);
return ws; return ws;
} }
-34
View File
@@ -1,34 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { WsState } from './types';
import assert from '@polkadot/util/assert';
import isNull from '@polkadot/util/is/null';
export default function onOpen (self: WsState): () => boolean {
return (): boolean => {
assert(!isNull(self.websocket), 'WebSocket cannot be null in onOpen');
self.l.debug(() => ['connected to', self.endpoint]);
self.isConnected = true;
self.emitter.emit('connected');
Object.keys(self.queued).forEach((id) => {
try {
// @ts-ignore checked above
self.websocket.send(
self.queued[id]
);
delete self.queued[id];
} catch (error) {
self.l.error(error);
}
});
return true;
};
}
+2 -2
View File
@@ -4,7 +4,7 @@
import { mockWs, TEST_WS_URL } from '../../test/mockWs'; import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import index from './index'; import Ws from './index';
let ws; let ws;
let mock; let mock;
@@ -14,7 +14,7 @@ function createMock (requests) {
} }
function createWs (autoConnect) { function createWs (autoConnect) {
ws = index(TEST_WS_URL, autoConnect); ws = new Ws(TEST_WS_URL, autoConnect);
return ws; return ws;
} }
-39
View File
@@ -1,39 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { ProviderInterface$Callback } from '../types';
import { WsState } from './types';
import isNull from '@polkadot/util/is/null';
export default async function send (self: WsState, method: string, params: Array<any>, subscription?: ProviderInterface$Callback): Promise<any> {
return new Promise((resolve, reject): void => {
try {
const json = self.coder.encodeJson(method, params);
const id = self.coder.getId();
const callback = (error: Error | null, result: any) => {
if (error) {
reject(error);
} else {
resolve(result);
}
};
self.l.debug(() => ['calling', method, params, json, !!subscription]);
self.handlers[id] = {
callback,
subscription
};
if (self.isConnected && !isNull(self.websocket)) {
self.websocket.send(json);
} else {
self.queued[id] = json;
}
} catch (error) {
reject(error);
}
});
}
+4 -4
View File
@@ -2,18 +2,18 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details. // of the ISC license. See the LICENSE file for details.
import state from './state'; import Ws from './index';
describe('state', () => { describe('state', () => {
it('requires an ws:// prefixed endpoint', () => { it('requires an ws:// prefixed endpoint', () => {
expect( expect(
() => state('http://') () => new Ws('http://', false)
).toThrow(/with 'ws/); ).toThrow(/with 'ws/);
}); });
it('allows wss:// endpoints', () => { it('allows wss:// endpoints', () => {
expect( expect(
state('wss://') () => new Ws('wss://', false)
).toBeDefined(); ).not.toThrow();
}); });
}); });
-30
View File
@@ -1,30 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { WsState } from './types';
import E3 from 'eventemitter3';
import assert from '@polkadot/util/assert';
import logger from '@polkadot/util/logger';
import coder from '../coder/json';
const l = logger('api-ws');
export default function state (endpoint: string, autoConnect: boolean): WsState {
assert(/^(wss|ws):\/\//.test(endpoint), `Endpoint should start with 'ws://', received '${endpoint}'`);
return {
autoConnect,
coder: coder(),
emitter: new E3.EventEmitter(),
endpoint,
handlers: {},
isConnected: false,
l,
queued: {},
subscriptions: {},
websocket: null
};
}
@@ -4,7 +4,7 @@
import { mockWs, TEST_WS_URL } from '../../test/mockWs'; import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import index from './index'; import Ws from './index';
let ws; let ws;
let mock; let mock;
@@ -14,7 +14,7 @@ function createMock (requests) {
} }
function createWs (autoConnect) { function createWs (autoConnect) {
ws = index(TEST_WS_URL, autoConnect); ws = new Ws(TEST_WS_URL, autoConnect);
return ws; return ws;
} }
-14
View File
@@ -1,14 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { ProviderInterface$Callback } from '../types';
import { WsState } from './types';
import send from './send';
export default async function subscribe (self: WsState, method: string, params: Array<any>, subscription: ProviderInterface$Callback): Promise<number> {
const id = await send(self, method, params, subscription);
return id as number;
}
@@ -4,7 +4,7 @@
import { mockWs, TEST_WS_URL } from '../../test/mockWs'; import { mockWs, TEST_WS_URL } from '../../test/mockWs';
import index from './index'; import Ws from './index';
let ws; let ws;
let mock; let mock;
@@ -14,7 +14,7 @@ function createMock (requests) {
} }
function createWs (autoConnect) { function createWs (autoConnect) {
ws = index(TEST_WS_URL, autoConnect); ws = new Ws(TEST_WS_URL, autoConnect);
return ws; return ws;
} }
@@ -1,20 +0,0 @@
// Copyright 2017-2018 @polkadot/api-provider authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { WsState } from './types';
import assert from '@polkadot/util/assert';
import isUndefined from '@polkadot/util/is/undefined';
import send from './send';
export default async function unsubscribe (self: WsState, method: string, id: number): Promise<boolean> {
assert(!isUndefined(self.subscriptions[id]), `Unable to find active subscription=${id}`);
delete self.subscriptions[id];
const result = await send(self, method, [id]);
return result as boolean;
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { ApiInterface } from '../../api/src/types'; import { ApiInterface } from '../../api/src/types';
import createApi from '../../api/src'; import createApi from '../../api/src';
import createProvider from '../src/ws'; import Ws from '../src/ws';
describe.skip('e2e', () => { describe.skip('e2e', () => {
let api: ApiInterface; let api: ApiInterface;
beforeEach(() => { beforeEach(() => {
jest.setTimeout(30000); jest.setTimeout(30000);
api = createApi(createProvider('ws://127.0.0.1:9944')); api = createApi(new Ws('ws://127.0.0.1:9944'));
}); });
it('subscribes to client_newHead', (done) => { it('subscribes to client_newHead', (done) => {
+5 -5
View File
@@ -2,11 +2,11 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details. // of the ISC license. See the LICENSE file for details.
jest.mock('@polkadot/api-provider/ws', () => () => ({ jest.mock('@polkadot/api-provider/ws', () => class {
isConnected: () => true, isConnected = () => true;
on: () => true, on = () => true;
send: () => true send = () => true;
})); });
jest.mock('./interface', () => (api, sectionName) => sectionName); jest.mock('./interface', () => (api, sectionName) => sectionName);
const createApi = require('./index').default; const createApi = require('./index').default;
+2 -2
View File
@@ -8,13 +8,13 @@ import { RxApiInterface } from './types';
import createApi from '@polkadot/api/index'; import createApi from '@polkadot/api/index';
import interfaces from '@polkadot/jsonrpc'; import interfaces from '@polkadot/jsonrpc';
import createWs from '@polkadot/api-provider/ws'; import Ws from '@polkadot/api-provider/ws';
import createExposed from './api'; import createExposed from './api';
import defaults from './defaults'; import defaults from './defaults';
import createInterface from './interface'; import createInterface from './interface';
export default function rxApi (provider: ProviderInterface = createWs(defaults.WS_URL)): RxApiInterface { export default function rxApi (provider: ProviderInterface = new Ws(defaults.WS_URL)): RxApiInterface {
const api = createApi(provider); const api = createApi(provider);
return Object.keys(interfaces).reduce((result, type) => { return Object.keys(interfaces).reduce((result, type) => {
+2 -2
View File
@@ -25,11 +25,11 @@ npm install --save @polkadot/api
Initialisation - Initialisation -
```js ```js
import Api from '@polkadot/api'; import createApi from '@polkadot/api';
import HttpProvider from '@polkadot/api-provider/http'; import HttpProvider from '@polkadot/api-provider/http';
const provider = new HttpProvider('http://127.0.0.1:9933'); const provider = new HttpProvider('http://127.0.0.1:9933');
const api = new Api(provider); const api = createApi(provider);
``` ```
Making calls - Making calls -