diff --git a/packages/api-provider/src/http/index.spec.js b/packages/api-provider/src/http/index.spec.js index 14cb4ab64f..f454b05c7d 100644 --- a/packages/api-provider/src/http/index.spec.js +++ b/packages/api-provider/src/http/index.spec.js @@ -4,13 +4,25 @@ import { TEST_HTTP_URL } from '../../test/mockHttp'; -import createHttp from './index'; +import Http from './index'; describe('Http', () => { let http; 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', () => { diff --git a/packages/api-provider/src/http/index.ts b/packages/api-provider/src/http/index.ts index 0261029ea0..85fbeb6efd 100644 --- a/packages/api-provider/src/http/index.ts +++ b/packages/api-provider/src/http/index.ts @@ -2,29 +2,62 @@ // 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 { ProviderInterface, ProviderInterface$Callback, ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types'; import './polyfill'; -import on from './on'; -import send from './send'; -import subscribe from './subscribe'; -import state from './state'; -import unsubscribe from './unsubscribe'; +import assert from '@polkadot/util/assert'; +import logger from '@polkadot/util/logger'; -export default function httpProvider (endpoint: string): ProviderInterface { - const self = state(endpoint); +import coder from '../coder/json'; - return { - isConnected: (): boolean => - true, - on: (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void => - on(self, type, sub), - send: (method: string, params: Array): Promise => - send(self, method, params), - subscribe: (method: string, params: Array, cb: ProviderInterface$Callback): Promise => - subscribe(self, method, params, cb), - unsubscribe: (method: string, id: number): Promise => - unsubscribe(self, method, id) - }; +export default class HttpProvider implements ProviderInterface { + private coder: RpcCoder; + private endpoint: string; + private l: Logger; + + constructor (endpoint: string) { + assert(/^(https|http):\/\//.test(endpoint), `Endpoint should start with 'http://', received '${endpoint}'`); + + this.coder = coder(); + this.endpoint = endpoint; + 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): Promise { + 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, cb: ProviderInterface$Callback): Promise { + throw new Error('Subscriptions has not been implemented'); + } + + async unsubscribe (method: string, id: number): Promise { + throw new Error('Subscriptions has not been implemented'); + } } diff --git a/packages/api-provider/src/http/on.ts b/packages/api-provider/src/http/on.ts deleted file mode 100644 index 3f45580706..0000000000 --- a/packages/api-provider/src/http/on.ts +++ /dev/null @@ -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}'`); -} diff --git a/packages/api-provider/src/http/send.spec.js b/packages/api-provider/src/http/send.spec.js index 9af60fe53a..ee47d29a93 100644 --- a/packages/api-provider/src/http/send.spec.js +++ b/packages/api-provider/src/http/send.spec.js @@ -4,14 +4,14 @@ import { mockHttp, TEST_HTTP_URL } from '../../test/mockHttp'; -import createHttp from './index'; +import Http from './index'; describe('send', () => { let http; let mock; beforeEach(() => { - http = createHttp(TEST_HTTP_URL); + http = new Http(TEST_HTTP_URL); }); afterEach(() => { diff --git a/packages/api-provider/src/http/send.ts b/packages/api-provider/src/http/send.ts deleted file mode 100644 index 57be65d98e..0000000000 --- a/packages/api-provider/src/http/send.ts +++ /dev/null @@ -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): Promise { - 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); -} diff --git a/packages/api-provider/src/http/state.spec.js b/packages/api-provider/src/http/state.spec.js deleted file mode 100644 index 0d804ce92b..0000000000 --- a/packages/api-provider/src/http/state.spec.js +++ /dev/null @@ -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(); - }); -}); diff --git a/packages/api-provider/src/http/state.ts b/packages/api-provider/src/http/state.ts deleted file mode 100644 index cd56f9340e..0000000000 --- a/packages/api-provider/src/http/state.ts +++ /dev/null @@ -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 - }; -} diff --git a/packages/api-provider/src/http/subscribe.ts b/packages/api-provider/src/http/subscribe.ts deleted file mode 100644 index 747ee22aeb..0000000000 --- a/packages/api-provider/src/http/subscribe.ts +++ /dev/null @@ -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, cb: ProviderInterface$Callback): Promise { - throw new Error('Subscriptions has not been implemented'); -} diff --git a/packages/api-provider/src/http/unsubscribe.ts b/packages/api-provider/src/http/unsubscribe.ts deleted file mode 100644 index 57d857e35a..0000000000 --- a/packages/api-provider/src/http/unsubscribe.ts +++ /dev/null @@ -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 { - throw new Error('Subscriptions has not been implemented'); -} diff --git a/packages/api-provider/src/types.d.ts b/packages/api-provider/src/types.d.ts index 81aa9260ee..1d137ef66e 100644 --- a/packages/api-provider/src/types.d.ts +++ b/packages/api-provider/src/types.d.ts @@ -41,7 +41,7 @@ export type ProviderInterface$Emitted = 'connected' | 'disconnected'; export type ProviderInterface$EmitCb = (value?: any) => any; -export type ProviderInterface = { +export interface ProviderInterface { isConnected (): boolean, on (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void, send (method: string, params: Array): Promise, diff --git a/packages/api-provider/src/ws/connect.spec.js b/packages/api-provider/src/ws/connect.spec.js index b2bd778bb4..7c13e6bd5d 100644 --- a/packages/api-provider/src/ws/connect.spec.js +++ b/packages/api-provider/src/ws/connect.spec.js @@ -4,10 +4,9 @@ import { mockWs, TEST_WS_URL } from '../../test/mockWs'; -import createState from './state'; -import connect from './connect'; +import Ws from './index'; -describe('onError', () => { +describe('onConnect', () => { let mock; beforeEach(() => { @@ -22,13 +21,13 @@ describe('onError', () => { }); 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(state.websocket.onerror[0]).toBeDefined(); - expect(state.websocket.onmessage[0]).toBeDefined(); - expect(state.websocket.onopen[0]).toBeDefined(); + expect(ws.websocket.onclose[0]).toBeDefined(); + expect(ws.websocket.onerror[0]).toBeDefined(); + expect(ws.websocket.onmessage[0]).toBeDefined(); + expect(ws.websocket.onopen[0]).toBeDefined(); }); }); diff --git a/packages/api-provider/src/ws/connect.ts b/packages/api-provider/src/ws/connect.ts deleted file mode 100644 index a7c1982815..0000000000 --- a/packages/api-provider/src/ws/connect.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/api-provider/src/ws/index.spec.js b/packages/api-provider/src/ws/index.spec.js index cf31faf676..b01b1309fe 100644 --- a/packages/api-provider/src/ws/index.spec.js +++ b/packages/api-provider/src/ws/index.spec.js @@ -4,14 +4,14 @@ import { mockWs, TEST_WS_URL } from '../../test/mockWs'; -import create from './index'; +import Ws from './index'; let ws; let mock; function createWs (requests, autoConnect) { mock = mockWs(requests); - ws = create(TEST_WS_URL, autoConnect); + ws = new Ws(TEST_WS_URL, autoConnect); return ws; } diff --git a/packages/api-provider/src/ws/index.ts b/packages/api-provider/src/ws/index.ts index c30d564d35..8e0a4770e5 100644 --- a/packages/api-provider/src/ws/index.ts +++ b/packages/api-provider/src/ws/index.ts @@ -2,40 +2,233 @@ // This software may be modified and distributed under the terms // 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 connect from './connect'; -import on from './on'; -import send from './send'; -import state from './state'; -import subscribe from './subscribe'; -import unsubscribe from './unsubscribe'; +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'; -type WSProviderInterface = ProviderInterface & { - connect (): void +import coder from '../coder/json'; + +type WsState$Awaiting = { + callback: ProviderInterface$Callback, + subscription?: ProviderInterface$Callback }; -export default function wsProvider (endpoint: string, autoConnect: boolean = true): WSProviderInterface { - const self = state(endpoint, autoConnect); +type WsState$Subscription = { + callback: ProviderInterface$Callback +}; - if (autoConnect) { - connect(self); +interface WSProviderInterface extends ProviderInterface { + 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(self), - isConnected: (): boolean => - self.isConnected, - on: (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void => - on(self, type, sub), - send: (method: string, params: Array): Promise => - send(self, method, params), - subscribe: (method: string, params: Array, cb: ProviderInterface$Callback): Promise => - subscribe(self, method, params, cb), - unsubscribe: (method: string, id: number): Promise => - unsubscribe(self, method, id) - }; + 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); + } + } + + isConnected (): boolean { + return this._isConnected; + } + + on (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): this { + return super.on(type, sub); + } + + async send (method: string, params: Array, subscription?: ProviderInterface$Callback): Promise { + 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, subscription: ProviderInterface$Callback): Promise { + const id = await this.send(method, params, subscription); + + return id as number; + } + + async unsubscribe (method: string, id: number): Promise { + 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; + } } diff --git a/packages/api-provider/src/ws/on.ts b/packages/api-provider/src/ws/on.ts deleted file mode 100644 index 8bc009015d..0000000000 --- a/packages/api-provider/src/ws/on.ts +++ /dev/null @@ -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); -} diff --git a/packages/api-provider/src/ws/onClose.spec.js b/packages/api-provider/src/ws/onClose.spec.js deleted file mode 100644 index 5de32cf9cf..0000000000 --- a/packages/api-provider/src/ws/onClose.spec.js +++ /dev/null @@ -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(); - }); -}); diff --git a/packages/api-provider/src/ws/onClose.ts b/packages/api-provider/src/ws/onClose.ts deleted file mode 100644 index 675cf1424f..0000000000 --- a/packages/api-provider/src/ws/onClose.ts +++ /dev/null @@ -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); - } - }; -} diff --git a/packages/api-provider/src/ws/onError.spec.js b/packages/api-provider/src/ws/onError.spec.js index be964dab91..2766ff7b0f 100644 --- a/packages/api-provider/src/ws/onError.spec.js +++ b/packages/api-provider/src/ws/onError.spec.js @@ -2,15 +2,14 @@ // This software may be modified and distributed under the terms // of the ISC license. See the LICENSE file for details. -import onError from './onError'; -import createState from './state'; +import Ws from './index'; describe('onError', () => { - let state; + let ws; let errorSpy; 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'); }); @@ -19,7 +18,7 @@ describe('onError', () => { }); it('logs the error', () => { - onError(state)('test error'); + ws.onSocketError('test error'); expect(errorSpy).toHaveBeenCalledWith( expect.anything(), expect.anything(), 'test error' diff --git a/packages/api-provider/src/ws/onError.ts b/packages/api-provider/src/ws/onError.ts deleted file mode 100644 index 4385b344d8..0000000000 --- a/packages/api-provider/src/ws/onError.ts +++ /dev/null @@ -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); - }; -} diff --git a/packages/api-provider/src/ws/onMessage.ts b/packages/api-provider/src/ws/onMessage.ts deleted file mode 100644 index 3aa745c5cc..0000000000 --- a/packages/api-provider/src/ws/onMessage.ts +++ /dev/null @@ -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); - }; -} diff --git a/packages/api-provider/src/ws/onMessageResult.spec.js b/packages/api-provider/src/ws/onMessageResult.spec.js index a3c2a92176..cbe11a8032 100644 --- a/packages/api-provider/src/ws/onMessageResult.spec.js +++ b/packages/api-provider/src/ws/onMessageResult.spec.js @@ -2,15 +2,14 @@ // This software may be modified and distributed under the terms // of the ISC license. See the LICENSE file for details. -import onMessage from './onMessage'; -import createState from './state'; +import Ws from './index'; describe('onMessageResult', () => { - let state; + let ws; let errorSpy; 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'); }); @@ -19,7 +18,7 @@ describe('onMessageResult', () => { }); it('fails with log when handler not found', () => { - onMessage(state)({ data: '{"id":2}' }); + ws.onSocketMessage({ data: '{"id":2}' }); expect(errorSpy).toHaveBeenCalledWith( expect.anything(), expect.anything(), 'Unable to find handler for id=2' @@ -27,13 +26,13 @@ describe('onMessageResult', () => { }); it('calls the handler when found', (done) => { - state.handlers[5] = { + ws.handlers[5] = { callback: (_, result) => { expect(result).toEqual('test'); done(); } }; - onMessage(state)({ data: '{"jsonrpc":"2.0","id":5,"result":"test"}' }); + ws.onSocketMessage({ data: '{"jsonrpc":"2.0","id":5,"result":"test"}' }); }); }); diff --git a/packages/api-provider/src/ws/onMessageResult.ts b/packages/api-provider/src/ws/onMessageResult.ts deleted file mode 100644 index f0e4ac27ae..0000000000 --- a/packages/api-provider/src/ws/onMessageResult.ts +++ /dev/null @@ -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]; -} diff --git a/packages/api-provider/src/ws/onMessageSubscribe.spec.js b/packages/api-provider/src/ws/onMessageSubscribe.spec.js index 2719621965..dccc2dc06d 100644 --- a/packages/api-provider/src/ws/onMessageSubscribe.spec.js +++ b/packages/api-provider/src/ws/onMessageSubscribe.spec.js @@ -2,15 +2,14 @@ // This software may be modified and distributed under the terms // of the ISC license. See the LICENSE file for details. -import onMessage from './onMessage'; -import createState from './state'; +import Ws from './index'; describe('onMessageSubscribe', () => { - let state; + let ws; let errorSpy; 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'); }); @@ -19,7 +18,7 @@ describe('onMessageSubscribe', () => { }); 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.anything(), expect.anything(), 'Unable to find handler for subscription=2' @@ -27,7 +26,7 @@ describe('onMessageSubscribe', () => { }); it('calls the subscriber with data', (done) => { - state.handlers[11] = { + ws.handlers[11] = { callback: (_, id) => {}, subscription: (_, result) => { expect(result).toEqual('test'); @@ -35,12 +34,12 @@ describe('onMessageSubscribe', () => { } }; - onMessage(state)({ 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","id":11,"result":22}' }); + ws.onSocketMessage({ data: '{"jsonrpc":"2.0","method":"test","params":{"subscription":22,"result":"test"}}' }); }); it('calls the subscriber with error', (done) => { - state.handlers[11] = { + ws.handlers[11] = { callback: (_, id) => {}, subscription: (error) => { expect(error.message).toMatch(/test/); @@ -48,7 +47,7 @@ describe('onMessageSubscribe', () => { } }; - onMessage(state)({ 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","id":11,"result":22}' }); + ws.onSocketMessage({ data: '{"jsonrpc":"2.0","method":"test","params":{"subscription":22,"error":{"message":"test"}}}' }); }); }); diff --git a/packages/api-provider/src/ws/onMessageSubscribe.ts b/packages/api-provider/src/ws/onMessageSubscribe.ts deleted file mode 100644 index 1a955386d0..0000000000 --- a/packages/api-provider/src/ws/onMessageSubscribe.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/api-provider/src/ws/onOpen.spec.js b/packages/api-provider/src/ws/onOpen.spec.js index e833d0db0d..279919e379 100644 --- a/packages/api-provider/src/ws/onOpen.spec.js +++ b/packages/api-provider/src/ws/onOpen.spec.js @@ -4,14 +4,14 @@ import { mockWs, TEST_WS_URL } from '../../test/mockWs'; -import create from './index'; +import Ws from './index'; let ws; let mock; function createWs (requests, autoConnect) { mock = mockWs(requests); - ws = create(TEST_WS_URL, autoConnect); + ws = new Ws(TEST_WS_URL, autoConnect); return ws; } diff --git a/packages/api-provider/src/ws/onOpen.ts b/packages/api-provider/src/ws/onOpen.ts deleted file mode 100644 index 953808ebcf..0000000000 --- a/packages/api-provider/src/ws/onOpen.ts +++ /dev/null @@ -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; - }; -} diff --git a/packages/api-provider/src/ws/send.spec.js b/packages/api-provider/src/ws/send.spec.js index 8ec8b8873a..e0695608cb 100644 --- a/packages/api-provider/src/ws/send.spec.js +++ b/packages/api-provider/src/ws/send.spec.js @@ -4,7 +4,7 @@ import { mockWs, TEST_WS_URL } from '../../test/mockWs'; -import index from './index'; +import Ws from './index'; let ws; let mock; @@ -14,7 +14,7 @@ function createMock (requests) { } function createWs (autoConnect) { - ws = index(TEST_WS_URL, autoConnect); + ws = new Ws(TEST_WS_URL, autoConnect); return ws; } diff --git a/packages/api-provider/src/ws/send.ts b/packages/api-provider/src/ws/send.ts deleted file mode 100644 index 12a6906f5d..0000000000 --- a/packages/api-provider/src/ws/send.ts +++ /dev/null @@ -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, subscription?: ProviderInterface$Callback): Promise { - 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); - } - }); -} diff --git a/packages/api-provider/src/ws/state.spec.js b/packages/api-provider/src/ws/state.spec.js index 1be95acadd..c594826632 100644 --- a/packages/api-provider/src/ws/state.spec.js +++ b/packages/api-provider/src/ws/state.spec.js @@ -2,18 +2,18 @@ // This software may be modified and distributed under the terms // of the ISC license. See the LICENSE file for details. -import state from './state'; +import Ws from './index'; describe('state', () => { it('requires an ws:// prefixed endpoint', () => { expect( - () => state('http://') + () => new Ws('http://', false) ).toThrow(/with 'ws/); }); it('allows wss:// endpoints', () => { expect( - state('wss://') - ).toBeDefined(); + () => new Ws('wss://', false) + ).not.toThrow(); }); }); diff --git a/packages/api-provider/src/ws/state.ts b/packages/api-provider/src/ws/state.ts deleted file mode 100644 index a320dca258..0000000000 --- a/packages/api-provider/src/ws/state.ts +++ /dev/null @@ -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 - }; -} diff --git a/packages/api-provider/src/ws/subscribe.spec.js b/packages/api-provider/src/ws/subscribe.spec.js index e113766b9c..ad7412cd3c 100644 --- a/packages/api-provider/src/ws/subscribe.spec.js +++ b/packages/api-provider/src/ws/subscribe.spec.js @@ -4,7 +4,7 @@ import { mockWs, TEST_WS_URL } from '../../test/mockWs'; -import index from './index'; +import Ws from './index'; let ws; let mock; @@ -14,7 +14,7 @@ function createMock (requests) { } function createWs (autoConnect) { - ws = index(TEST_WS_URL, autoConnect); + ws = new Ws(TEST_WS_URL, autoConnect); return ws; } diff --git a/packages/api-provider/src/ws/subscribe.ts b/packages/api-provider/src/ws/subscribe.ts deleted file mode 100644 index bd14edd4b3..0000000000 --- a/packages/api-provider/src/ws/subscribe.ts +++ /dev/null @@ -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, subscription: ProviderInterface$Callback): Promise { - const id = await send(self, method, params, subscription); - - return id as number; -} diff --git a/packages/api-provider/src/ws/unsubscribe.spec.js b/packages/api-provider/src/ws/unsubscribe.spec.js index 87ee48ca2a..9d97cefec2 100644 --- a/packages/api-provider/src/ws/unsubscribe.spec.js +++ b/packages/api-provider/src/ws/unsubscribe.spec.js @@ -4,7 +4,7 @@ import { mockWs, TEST_WS_URL } from '../../test/mockWs'; -import index from './index'; +import Ws from './index'; let ws; let mock; @@ -14,7 +14,7 @@ function createMock (requests) { } function createWs (autoConnect) { - ws = index(TEST_WS_URL, autoConnect); + ws = new Ws(TEST_WS_URL, autoConnect); return ws; } diff --git a/packages/api-provider/src/ws/unsubscribe.ts b/packages/api-provider/src/ws/unsubscribe.ts deleted file mode 100644 index cd78bdb3ab..0000000000 --- a/packages/api-provider/src/ws/unsubscribe.ts +++ /dev/null @@ -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 { - 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; -} diff --git a/packages/api-provider/test/e2e.test.ts b/packages/api-provider/test/e2e.test.ts index d4c167159f..62ee6b5fef 100644 --- a/packages/api-provider/test/e2e.test.ts +++ b/packages/api-provider/test/e2e.test.ts @@ -5,14 +5,14 @@ import { ApiInterface } from '../../api/src/types'; import createApi from '../../api/src'; -import createProvider from '../src/ws'; +import Ws from '../src/ws'; describe.skip('e2e', () => { let api: ApiInterface; beforeEach(() => { 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) => { diff --git a/packages/api-rx/src/index.spec.js b/packages/api-rx/src/index.spec.js index 370da3d9e6..3ed1e8009b 100644 --- a/packages/api-rx/src/index.spec.js +++ b/packages/api-rx/src/index.spec.js @@ -2,11 +2,11 @@ // This software may be modified and distributed under the terms // of the ISC license. See the LICENSE file for details. -jest.mock('@polkadot/api-provider/ws', () => () => ({ - isConnected: () => true, - on: () => true, - send: () => true -})); +jest.mock('@polkadot/api-provider/ws', () => class { + isConnected = () => true; + on = () => true; + send = () => true; +}); jest.mock('./interface', () => (api, sectionName) => sectionName); const createApi = require('./index').default; diff --git a/packages/api-rx/src/index.ts b/packages/api-rx/src/index.ts index 0a1ea4d4b5..009b766a53 100644 --- a/packages/api-rx/src/index.ts +++ b/packages/api-rx/src/index.ts @@ -8,13 +8,13 @@ import { RxApiInterface } from './types'; import createApi from '@polkadot/api/index'; import interfaces from '@polkadot/jsonrpc'; -import createWs from '@polkadot/api-provider/ws'; +import Ws from '@polkadot/api-provider/ws'; import createExposed from './api'; import defaults from './defaults'; 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); return Object.keys(interfaces).reduce((result, type) => { diff --git a/packages/api/README.md b/packages/api/README.md index 97ac7eee59..22f0e8ea6a 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -25,11 +25,11 @@ npm install --save @polkadot/api Initialisation - ```js -import Api from '@polkadot/api'; +import createApi from '@polkadot/api'; import HttpProvider from '@polkadot/api-provider/http'; const provider = new HttpProvider('http://127.0.0.1:9933'); -const api = new Api(provider); +const api = createApi(provider); ``` Making calls -