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
+15
View File
@@ -0,0 +1,15 @@
ISC License (ISC)
Copyright 2017-2018 @polkadot/rpc-provider authors & contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
+32
View File
@@ -0,0 +1,32 @@
[![polkadotjs](https://img.shields.io/badge/polkadot-js-orange.svg?style=flat-square)](https://polkadot.js.org)
![isc](https://img.shields.io/badge/license-ISC-lightgrey.svg?style=flat-square)
[![style](https://img.shields.io/badge/code%20style-semistandard-lightgrey.svg?style=flat-square)](https://github.com/Flet/semistandard)
[![npm](https://img.shields.io/npm/v/@polkadot/rpc-provider.svg?style=flat-square)](https://www.npmjs.com/package/@polkadot/rpc-provider)
[![travis](https://img.shields.io/travis/polkadot-js/api.svg?style=flat-square)](https://travis-ci.org/polkadot-js/api)
[![maintainability](https://img.shields.io/codeclimate/maintainability/polkadot-js/api.svg?style=flat-square)](https://codeclimate.com/github/polkadot-js/api/maintainability)
[![coverage](https://img.shields.io/coveralls/polkadot-js/api.svg?style=flat-square)](https://coveralls.io/github/polkadot-js/api?branch=master)
[![dependency](https://david-dm.org/polkadot-js/api.svg?style=flat-square&path=packages/rpc-provider)](https://david-dm.org/polkadot-js/api?path=packages/rpc-provider)
[![devDependency](https://david-dm.org/polkadot-js/api/dev-status.svg?style=flat-square&path=packages/rpc-provider)](https://david-dm.org/polkadot-js/api?path=packages/rpc-provider#info=devDependencies)
# @polkadot/rpc-provider
Generic transport providers to handle the transport of method calls to and from Polkadot clients from applications interacting with it. Generally, unless you are operating at a low-level and taking care of encoding and decoding of parameters/results, it won't be directly used. API interfaces building on top these providers can support various transports with the same underlying interfaces.
## Usage
Installation -
```
npm install --save @polkadot/rpc-provider
```
Initialisation -
```js
import WsProvider from '@polkadot/rpc-provider/ws';
const provider = new WsProvider('http://127.0.0.1:9944');
const version = await provider.send('client_version', []);
console.log('clientVersion', version);
```
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@polkadot/rpc-provider",
"version": "0.31.0",
"description": "Transport providers for the API",
"main": "index.js",
"keywords": [
"Polkadot",
"JsonRPC"
],
"author": "Jaco Greeff <jacogr@gmail.com>",
"maintainers": [
"Jaco Greeff <jacogr@gmail.com>"
],
"contributors": [],
"license": "ISC",
"engines": {
"node": ">=6.4"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"repository": {
"type": "git",
"url": "git+https://github.com/polkadot-js/api.git"
},
"bugs": {
"url": "https://github.com/polkadot-js/api/issues"
},
"homepage": "https://github.com/polkadot-js/api/tree/master/packages/rpc-provider#readme",
"dependencies": {
"@babel/runtime": "^7.0.0",
"@polkadot/storage": "^0.31.0",
"@polkadot/util": "^0.30.7",
"@polkadot/util-crypto": "^0.30.7",
"@polkadot/util-keyring": "^0.30.7",
"@types/nock": "^9.1.3",
"eventemitter3": "^3.1.0",
"isomorphic-fetch": "^2.2.1",
"websocket": "^1.0.25"
},
"devDependencies": {
"mock-socket": "^8.0.2",
"nock": "^9.6.1"
}
}
@@ -0,0 +1,49 @@
// 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 createCoder from './index';
describe('decodeResponse', () => {
let coder;
beforeEach(() => {
coder = createCoder();
});
it('expects a non-empty input object', () => {
expect(
() => coder.decodeResponse()
).toThrow(/Empty response/);
});
it('expects a valid jsonrpc field', () => {
expect(
() => coder.decodeResponse({})
).toThrow(/Invalid jsonrpc/);
});
it('expects a valid id field', () => {
expect(
() => coder.decodeResponse({ jsonrpc: '2.0' })
).toThrow(/Invalid id/);
});
it('expects a valid result field', () => {
expect(
() => coder.decodeResponse({ id: 1, jsonrpc: '2.0' })
).toThrow(/No result/);
});
it('throws any error found', () => {
expect(
() => coder.decodeResponse({ id: 1, jsonrpc: '2.0', error: { code: 123, message: 'test error' } })
).toThrow(/\[123\]: test error/);
});
it('returns the result', () => {
expect(
coder.decodeResponse({ id: 1, jsonrpc: '2.0', result: 'some result' })
).toEqual('some result');
});
});
@@ -0,0 +1,41 @@
// 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 { JsonRpcResponse, JsonRpcResponseBase$Error } from '../../types';
import { RpcCoderState } from './types';
import assert from '@polkadot/util/assert';
import isNumber from '@polkadot/util/is/number';
import isUndefined from '@polkadot/util/is/undefined';
function checkError (error?: JsonRpcResponseBase$Error) {
if (!error) {
return;
}
const { code, message } = error;
throw new Error(`[${code}]: ${message}`);
}
export default function decodeResponse (self: RpcCoderState, response: JsonRpcResponse): any {
assert(response, 'Empty response object received');
assert(response.jsonrpc === '2.0', 'Invalid jsonrpc field in decoded object');
const isSubscription = !isUndefined(response.params) && !isUndefined(response.method);
assert(isNumber(response.id) || (isSubscription && isNumber(response.params.subscription)), 'Invalid id field in decoded object');
checkError(response.error);
assert(!isUndefined(response.result) || isSubscription, 'No result found in JsonRpc response');
if (isSubscription) {
checkError(response.params.error);
return response.params.result;
}
return response.result;
}
@@ -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 createCoder from './index';
describe('encodeJson', () => {
let coder;
beforeEach(() => {
coder = createCoder();
});
it('encodes a valid JsonRPC JSON string', () => {
expect(
coder.encodeJson('method', 'params')
).toEqual('{"id":1,"jsonrpc":"2.0","method":"method","params":"params"}');
});
});
@@ -0,0 +1,13 @@
// 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 { RpcCoderState } from './types';
import encodeObject from './encodeObject';
export default function encodeJson (self: RpcCoderState, method: string, params: Array<any>): string {
return JSON.stringify(
encodeObject(self, method, params)
);
}
@@ -0,0 +1,29 @@
// 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 createCoder from './index';
describe('encodeObject', () => {
let coder;
beforeEach(() => {
coder = createCoder();
});
it('starts with id === 0 (nothing sent)', () => {
expect(coder.getId()).toEqual(0);
});
it('encodes a valid JsonRPC object', () => {
expect(
coder.encodeObject('method', 'params')
).toEqual({
id: 1,
jsonrpc: '2.0',
method: 'method',
params: 'params'
});
expect(coder.getId()).toEqual(1);
});
});
@@ -0,0 +1,15 @@
// 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 { JsonRpcRequest } from '../../types';
import { RpcCoderState } from './types';
export default function encodeObject (self: RpcCoderState, method: string, params: Array<any>): JsonRpcRequest {
return {
id: ++self.id,
jsonrpc: '2.0',
method,
params
};
}
@@ -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 { JsonRpcRequest, JsonRpcResponse } from '../../types';
import { RpcCoder, RpcCoderState } from './types';
import decodeResponse from './decodeResponse';
import encodeJson from './encodeJson';
import encodeObject from './encodeObject';
export default function rpcCoder (): RpcCoder {
const self: RpcCoderState = {
id: 0
};
return {
decodeResponse: (response: JsonRpcResponse): any =>
decodeResponse(self, response),
encodeJson: (method: string, params: Array<any>): string =>
encodeJson(self, method, params),
encodeObject: (method: string, params: Array<any>): JsonRpcRequest =>
encodeObject(self, method, params),
getId: (): number =>
self.id
};
}
+16
View File
@@ -0,0 +1,16 @@
// 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 { JsonRpcRequest, JsonRpcResponse } from '../../types';
export type RpcCoder = {
decodeResponse: (response: JsonRpcResponse) => any,
encodeObject: (method: string, params: Array<any>) => JsonRpcRequest,
encodeJson: (method: string, params: Array<any>) => string,
getId: () => number
};
export type RpcCoderState = {
id: number
};
@@ -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 { TEST_HTTP_URL } from '../../test/mockHttp';
import Http from './index';
describe('Http', () => {
let http;
beforeEach(() => {
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', () => {
expect(http.isConnected()).toEqual(true);
});
it('does not (yet) support subscribe', () => {
return http.subscribe().catch((error) => {
expect(error.message).toMatch(/does not have subscriptions/);
});
});
it('does not (yet) support unsubscribe', () => {
return http.unsubscribe().catch((error) => {
expect(error.message).toMatch(/does not have subscriptions/);
});
});
});
+108
View File
@@ -0,0 +1,108 @@
// 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 { ProviderInterface, ProviderInterface$Callback, ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types';
import './polyfill';
import assert from '@polkadot/util/assert';
import logger from '@polkadot/util/logger';
import coder from '../coder/json';
const ERROR_SUBSCRIBE = 'HTTP Provider does not have subscriptions, use WebSockets instead';
/**
* @name HttpProvider
* @summary The HTTP Provider allows sending requests using HTTP to a HTTP RPC server TCP port.
* @description It does not support subscriptions so you won't be able to listen to events
* such as new blocks or balance changes. It is usually preferrable using the [[WsProvider]].
*
* @example
* <BR>
*
* ```javascript
* import Api from '@polkadot/rpc-core';
* import HttpProvider from '@polkadot/rpc-provider/http';
*
* const provider = new HttpProvider('http://127.0.0.1:9933');
* const api = new Api(provider);
* ```
*
* @see [[WsProvider]]
*/
export default class HttpProvider implements ProviderInterface {
private coder: RpcCoder;
private endpoint: string;
private l: Logger;
/**
* @param {string} endpoint The endpoint url starting with http://
*/
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');
}
/**
* @summary Whether the node is connected or not.
* @return {boolean} true if connected
*/
isConnected (): boolean {
return true;
}
/**
* @summary Events are not supported with the HttpProvider, see [[WsProvider]].
* @description HTTP Provider does not have 'on' emitters. WebSockets should be used instead.
*/
on (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void {
this.l.error(`HTTP Provider does not have 'on' emitters, use WebSockets instead`);
}
/**
* @summary Send HTTP POST Request with Body to configured HTTP Endpoint.
*/
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);
}
/**
* @summary Subscriptions are not supported with the HttpProvider, see [[WsProvider]].
*/
async subscribe (types: string, method: string, params: Array<any>, cb: ProviderInterface$Callback): Promise<number> {
this.l.error(ERROR_SUBSCRIBE);
throw new Error(ERROR_SUBSCRIBE);
}
/**
* @summary Subscriptions are not supported with the HttpProvider, see [[WsProvider]].
*/
async unsubscribe (type: string, method: string, id: number): Promise<boolean> {
this.l.error(ERROR_SUBSCRIBE);
throw new Error(ERROR_SUBSCRIBE);
}
}
@@ -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('http/polyfill', () => {
let origFetch;
beforeEach(() => {
origFetch = global.fetch;
global.fetch = null;
});
afterEach(() => {
global.fetch = origFetch;
});
it('polyfills with no exceptions (without fetch)', () => {
expect(require('./polyfill')).toBeDefined();
});
it('polyfills with no exceptions (with fetch)', () => {
global.fetch = () => true;
expect(require('./polyfill')).toBeDefined();
});
});
@@ -0,0 +1,8 @@
// 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 fetch === 'undefined') {
require('isomorphic-fetch');
}
@@ -0,0 +1,56 @@
// 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 { mockHttp, TEST_HTTP_URL } from '../../test/mockHttp';
import Http from './index';
describe('send', () => {
let http;
let mock;
beforeEach(() => {
http = new Http(TEST_HTTP_URL);
});
afterEach(() => {
if (mock) {
mock.done();
mock = null;
}
});
it('passes the body through correctly', () => {
mock = mockHttp([{
method: 'test_body',
reply: {
result: 'ok'
}
}]);
return http
.send('test_body', ['param'])
.then((result) => {
expect(mock.body['test_body']).toEqual({
id: 1,
jsonrpc: '2.0',
method: 'test_body',
params: ['param']
});
});
});
it('throws error when !response.ok', () => {
mock = mockHttp([{
code: 500,
method: 'test_error'
}]);
return http
.send('test_error', [])
.catch((error) => {
expect(error.message).toMatch(/\[500\]: Internal Server/);
});
});
});
+12
View File
@@ -0,0 +1,12 @@
// 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';
export type HttpState = {
coder: RpcCoder,
endpoint: string,
l: Logger
};
+6
View File
@@ -0,0 +1,6 @@
// 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.
export { default as HttpProvider } from './http';
export { default as WsProvider } from './ws';
+35
View File
@@ -0,0 +1,35 @@
// 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 { ProviderInterface, ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types';
import mocks from './mocks';
import on from './on';
import send from './send';
import state from './state';
import subscribe from './subscribe';
import unsubscribe from './unsubscribe';
/**
* A moock provider mainly used for testing.
* @return {ProviderInterface} The mock provider
*/
export default function mockProvider (): ProviderInterface {
const self = state();
mocks(self);
return {
isConnected: (): boolean =>
true,
on: (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void =>
on(self, type, sub),
send: (method: string, params: Array<any>): Promise<any> =>
send(self, method, params),
subscribe: (type: string, method: string, ...params: Array<any>): Promise<number> =>
subscribe(self, type, method, params),
unsubscribe: (type: string, method: string, id: number): Promise<boolean> =>
unsubscribe(self, type, method, id)
};
}
+84
View File
@@ -0,0 +1,84 @@
// 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.
// FIXME: This file is way too long and way too messy
import { StorageFunction } from '@polkadot/types/StorageKey';
import { KeyringPair } from '@polkadot/util-keyring/types';
import { ProviderInterface$Emitted } from '../types';
import { MockState, MockState$Db, MockState$Subscriptions } from './types';
import BN from 'bn.js';
import storage from '@polkadot/storage/static';
import Header from '@polkadot/types/Header';
import bnToU8a from '@polkadot/util/bn/toU8a';
import u8aToHex from '@polkadot/util/u8a/toHex';
import randomAsU8a from '@polkadot/util-crypto/random/asU8a';
import testKeyring from '@polkadot/util-keyring/testing';
const keyring = testKeyring();
const emitEvents: Array<ProviderInterface$Emitted> = ['connected', 'disconnected'];
let emitIndex = 0;
function makeBlockHeader (prevNumber: BN) {
const blockNumber = prevNumber.addn(1);
return {
digest: {
logs: []
},
extrinsicsRoot: randomAsU8a(),
number: blockNumber,
parentHash: prevNumber.eqn(-1)
? new Uint8Array(32)
: bnToU8a(prevNumber, 256, false),
stateRoot: bnToU8a(blockNumber, 256, false)
};
}
function updateSubs (subscriptions: MockState$Subscriptions, method: string, value: any) {
subscriptions[method].lastValue = value;
Object
.values(subscriptions[method].callbacks)
.forEach((cb) => {
try {
cb(null, value);
} catch (error) {
console.error(`Error on '${method}' subscription`, error);
}
});
}
function setStorageBn (db: MockState$Db, createKey: StorageFunction, value: BN | number, ...keyParams: Array<Uint8Array>): void {
const keyValue = u8aToHex(
createKey(...keyParams)
);
db[keyValue] = bnToU8a(value, 64, true);
}
export default function mocks ({ emitter, db, subscriptions }: MockState): void {
let newHead = makeBlockHeader(new BN(-1));
setInterval(() => {
if (++emitIndex === emitEvents.length) {
emitIndex = 0;
}
emitter.emit(emitEvents[emitIndex]);
newHead = makeBlockHeader(newHead.number);
keyring.getPairs().forEach(({ publicKey }: KeyringPair, index: number) => {
setStorageBn(db, storage.balances.freeBalance, newHead.number.muln(3).iaddn(index), publicKey());
setStorageBn(db, storage.system.accountNonce, newHead.number.addn(index), publicKey());
});
setStorageBn(db, storage.timestamp.now, Math.floor(Date.now() / 1000));
updateSubs(subscriptions, 'chain_newHead', new Header(newHead).toJSON());
}, 5000);
}
+10
View File
@@ -0,0 +1,10 @@
// 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 { ProviderInterface$Emitted, ProviderInterface$EmitCb } from '../types';
import { MockState } from './types';
export default function on (self: MockState, type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void {
self.emitter.on(type, sub);
}
+13
View File
@@ -0,0 +1,13 @@
// 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 { MockState } from './types';
export default async function send ({ requests, db }: MockState, method: string, params: Array<any>): Promise<any> {
if (!requests[method]) {
throw new Error(`provider.send: Invalid method '${method}'`);
}
return requests[method](db, params);
}
@@ -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 createState from './state';
describe('state', () => {
let state;
beforeEach(() => {
state = createState();
});
it('generates a sane initial state', () => {
expect(
state
).toMatchObject({
subscriptionId: 0,
subscriptionMap: {}
});
});
it('generates a proper section_method map', () => {
expect(
state.subscriptions
).toMatchObject({
'chain_newHead': {
callbacks: {},
lastValue: null
}
});
});
});
+58
View File
@@ -0,0 +1,58 @@
// 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 { MockState, MockState$Db, MockState$Subscriptions } from './types';
import E3 from 'eventemitter3';
import interfaces from '@polkadot/jsonrpc/index';
import u8aToHex from '@polkadot/util/u8a/toHex';
import logger from '@polkadot/util/logger';
const l = logger('api-mock');
const SUBSCRIPTIONS: string[] = Array.prototype.concat.apply(
[], Object.values(interfaces).map((area) =>
Object
.values(area.methods)
.filter((method) =>
method.isSubscription
)
.map(({ method, section }) =>
`${section}_${method}`
)
)
);
const REQUESTS = {
'state_getStorage': (storage: MockState$Db, params: Array<any>): string => {
return u8aToHex(
storage[(params[0] as string)]
);
},
'system_chain': (): string => 'mockChain',
'system_name': (): string => 'mockClient',
'system_version': (): string => '9.8.7'
};
export default function state (): MockState {
const db = {};
const subscriptions: MockState$Subscriptions = SUBSCRIPTIONS.reduce((subs, name) => {
subs[name] = {
callbacks: {},
lastValue: null
};
return subs;
}, ({} as MockState$Subscriptions));
return {
emitter: new E3.EventEmitter(),
l,
requests: Object.assign({}, REQUESTS),
db,
subscriptionId: 0,
subscriptionMap: {},
subscriptions
};
}
@@ -0,0 +1,44 @@
// 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 createState from './state';
import subscribe from './subscribe';
describe('subscribe', () => {
let state;
beforeEach(() => {
state = createState();
});
it('fails on unknown methods', () => {
return subscribe(state, 'test', 'test_notFound').catch((error) => {
expect(error.message).toMatch(/Invalid method 'test_notFound'/);
});
});
it('returns a subscription id', () => {
return subscribe(state, 'chain_newHead', 'chain_newHead', [() => void 0]).then((id) => {
expect(id).toEqual(state.subscriptionId);
});
});
it('stores the mapping values', () => {
const cb = () => void 0;
return subscribe(state, 'chain_newHead', 'chain_newHead', [cb]).then((id) => {
expect(state.subscriptionMap[id]).toEqual('chain_newHead');
expect(state.subscriptions['chain_newHead'].callbacks[id]).toEqual(cb);
});
});
it('calls back with the last known value', (done) => {
state.subscriptions['chain_newHead'].lastValue = 'testValue';
subscribe(state, 'chain_newHead', 'chain_newHead', [(_, value) => {
expect(value).toEqual('testValue');
done();
}]);
});
});
@@ -0,0 +1,23 @@
// 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 { MockState, MockState$Subscription$Callback } from './types';
export default async function subscribe (self: MockState, type: string, method: string, params: Array<any>): Promise<number> {
self.l.debug(() => ['subscribe', method, params]);
if (self.subscriptions[method]) {
const callback: MockState$Subscription$Callback = params.pop();
const id = ++self.subscriptionId;
self.subscriptions[method].callbacks[id] = callback;
self.subscriptionMap[id] = method;
callback(null, self.subscriptions[method].lastValue);
return id;
}
throw new Error(`provider.subscribe: Invalid method '${method}'`);
}
+37
View File
@@ -0,0 +1,37 @@
// 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 EventEmitter from 'eventemitter3';
import { Logger } from '@polkadot/util/types';
export type MockState$Subscription$Callback = (error: Error | null, value: any) => void;
export type MockState$Subscriptions = {
[index: string]: {
callbacks: {
[index: number]: MockState$Subscription$Callback
},
lastValue: any
}
};
export type MockState$Db = {
[index: string]: Uint8Array
};
export type MockState$Requests = {
[index: string]: (db: MockState$Db, params: Array<any>) => string
};
export type MockState = {
emitter: EventEmitter,
l: Logger,
requests: MockState$Requests,
db: MockState$Db,
subscriptionId: number,
subscriptionMap: {
[index: number]: string
},
subscriptions: MockState$Subscriptions
};
@@ -0,0 +1,45 @@
// 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 createState from './state';
import subscribe from './subscribe';
import unsubscribe from './unsubscribe';
describe('unsubscribe', () => {
let state;
let id;
beforeEach(() => {
state = createState();
return subscribe(state, 'chain_newHead', 'chain_newHead', [() => void 0]).then((_id) => {
id = _id;
});
});
it('fails on unknown ids', () => {
return unsubscribe(state, 'chain_newHead', 'chain_newHead', 5).catch((error) => {
expect(error.message).toMatch(/Unable to find/);
});
});
it('unsubscribes successfully', () => {
return unsubscribe(state, 'chain_newHead', 'chain_newHead', id);
});
it('fails on double unsubscribe', () => {
return unsubscribe(state, 'chain_newHead', 'chain_newHead', id)
.then(() => unsubscribe(state, id))
.catch((error) => {
expect(error.message).toMatch(/Unable to find/);
});
});
it('clears the subscriptions', () => {
return unsubscribe(state, 'chain_newHead', 'chain_newHead', id).then(() => {
expect(state.subscriptionMap[id]).not.toBeDefined();
expect(state.subscriptions['chain_newHead'].callbacks[id]).not.toBeDefined();
});
});
});
@@ -0,0 +1,20 @@
// 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 { MockState } from './types';
export default async function unsubscribe (self: MockState, type: string, name: string, id: number): Promise<boolean> {
const method = self.subscriptionMap[id];
self.l.debug(() => ['unsubscribe', id, method]);
if (!method) {
throw new Error(`Unable to find subscription for ${id}`);
}
delete self.subscriptionMap[id];
delete self.subscriptions[method].callbacks[id];
return true;
}
+50
View File
@@ -0,0 +1,50 @@
// 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.
export type JsonRpcObject = {
id: number;
jsonrpc: '2.0';
};
export type JsonRpcRequest = JsonRpcObject & {
method: string;
params: Array<any>;
};
export type JsonRpcResponseBase$Error = {
code: number,
message: string
};
type JsonRpcResponse$Single = {
error?: JsonRpcResponseBase$Error;
result?: any;
};
type JsonRpcResponse$Subscription = {
method?: string;
params: {
error?: JsonRpcResponseBase$Error;
result: any;
subscription: number;
}
};
export type JsonRpcResponseBase = JsonRpcResponse$Single & JsonRpcResponse$Subscription;
export type JsonRpcResponse = JsonRpcObject & JsonRpcResponseBase;
export type ProviderInterface$Callback = (error: Error | null, result: any) => void;
export type ProviderInterface$Emitted = 'connected' | 'disconnected';
export type ProviderInterface$EmitCb = (value?: any) => any;
export interface ProviderInterface {
isConnected (): boolean,
on (type: ProviderInterface$Emitted, sub: ProviderInterface$EmitCb): void,
send (method: string, params: Array<any>): Promise<any>,
subscribe (type: string, method: string, params: Array<any>, cb: ProviderInterface$Callback): Promise<number>,
unsubscribe (type: string, method: string, id: number): Promise<boolean>
}
@@ -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/);
});
});
});
+27
View File
@@ -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 nock from 'nock';
const TEST_HTTP_URL = 'http://localhost:9944';
function mockHttp (requests: any[]) {
nock.cleanAll();
return requests.reduce((scope, request: any, index: number) => {
return scope
.post('/')
.reply(request.code || 200, (uri: string, body: { id: string }) => {
scope.body = scope.body || {};
scope.body[request.method] = body;
return Object.assign({ id: body.id, jsonrpc: '2.0' }, request.reply || {});
});
}, nock(TEST_HTTP_URL));
}
export {
TEST_HTTP_URL,
mockHttp
};
+83
View File
@@ -0,0 +1,83 @@
// 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 { Server } from 'mock-socket';
const TEST_WS_URL = 'ws://localhost:9955';
let server: Server;
type ErrorDef = {
id: number,
error: {
code: number,
message: string
}
};
type ReplyDef = {
id: number,
reply: {
result: any
}
};
function createError ({ id, error: { code, message } }: ErrorDef) {
return {
id,
jsonrpc: '2.0',
error: {
code,
message
}
};
}
function createReply ({ id, reply: { result } }: ReplyDef) {
return {
id,
jsonrpc: '2.0',
result
};
}
function mockWs (requests: Array<{ method: string }>) {
server = new Server(TEST_WS_URL);
let requestCount = 0;
const scope: { body: { [index: string]: {} }, requests: number, server: Server, done: any } = {
body: {},
requests: 0,
server,
done: () =>
server.stop(() => {
// ignore
})
};
server.on('connection', (socket) => {
// @ts-ignore definitions are wrong, this is 'on', not 'onmessage'
socket.on('message', (body: {}) => {
const request = requests[requestCount];
// @ts-ignore Yes, SHOULD be fixed, this is a mess
const response = request.error
// @ts-ignore Yes, SHOULD be fixed, this is a mess
? createError(request)
// @ts-ignore Yes, SHOULD be fixed, this is a mess
: createReply(request);
scope.body[request.method] = body;
requestCount++;
socket.send(JSON.stringify(response));
});
});
return scope;
}
export {
TEST_WS_URL,
mockWs
};