aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrandon Millman <brandon.millman@gmail.com>2018-05-17 02:15:02 +0800
committerBrandon Millman <brandon.millman@gmail.com>2018-07-12 01:17:45 +0800
commit16ddd1edfccdd7768447bfff9afec1f4a1ce014e (patch)
treeac4209c77775a1b7c326204c0f7d49b9fcab7bff
parent8fcc7aefa7651311c5a6348101eb023d28799934 (diff)
downloaddexon-sol-tools-16ddd1edfccdd7768447bfff9afec1f4a1ce014e.tar.gz
dexon-sol-tools-16ddd1edfccdd7768447bfff9afec1f4a1ce014e.tar.zst
dexon-sol-tools-16ddd1edfccdd7768447bfff9afec1f4a1ce014e.zip
Implement web browser socket
-rw-r--r--packages/connect/package.json2
-rw-r--r--packages/connect/src/browser_ws_orderbook_channel.ts140
-rw-r--r--packages/connect/src/index.ts5
-rw-r--r--packages/connect/src/node_ws_orderbook_channel.ts (renamed from packages/connect/src/ws_orderbook_channel.ts)28
-rw-r--r--packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts (renamed from packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts)4
-rw-r--r--packages/connect/src/schemas/schemas.ts4
-rw-r--r--packages/connect/src/types.ts2
-rw-r--r--packages/connect/src/utils/assert.ts25
-rw-r--r--packages/connect/src/utils/orderbook_channel_message_parser.ts8
-rw-r--r--packages/connect/test/browser_ws_orderbook_channel_test.ts61
-rw-r--r--packages/connect/test/node_ws_orderbook_channel_test.ts (renamed from packages/connect/test/ws_orderbook_channel_test.ts)6
-rw-r--r--yarn.lock7
12 files changed, 259 insertions, 33 deletions
diff --git a/packages/connect/package.json b/packages/connect/package.json
index 469d47d33..78cb3e71d 100644
--- a/packages/connect/package.json
+++ b/packages/connect/package.json
@@ -68,7 +68,7 @@
"@types/lodash": "4.14.104",
"@types/mocha": "^2.2.42",
"@types/query-string": "^5.0.1",
- "@types/websocket": "^0.0.34",
+ "@types/websocket": "^0.0.39",
"async-child-process": "^1.1.1",
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
diff --git a/packages/connect/src/browser_ws_orderbook_channel.ts b/packages/connect/src/browser_ws_orderbook_channel.ts
new file mode 100644
index 000000000..b97a82ec9
--- /dev/null
+++ b/packages/connect/src/browser_ws_orderbook_channel.ts
@@ -0,0 +1,140 @@
+import * as _ from 'lodash';
+import * as WebSocket from 'websocket';
+
+import {
+ OrderbookChannel,
+ OrderbookChannelHandler,
+ OrderbookChannelMessageTypes,
+ OrderbookChannelSubscriptionOpts,
+ WebsocketClientEventType,
+ WebsocketConnectionEventType,
+} from './types';
+import { assert } from './utils/assert';
+import { orderbookChannelMessageParser } from './utils/orderbook_channel_message_parser';
+
+interface Subscription {
+ subscriptionOpts: OrderbookChannelSubscriptionOpts;
+ handler: OrderbookChannelHandler;
+}
+
+/**
+ * This class includes all the functionality related to interacting with a websocket endpoint
+ * that implements the standard relayer API v0 in a browser environment
+ */
+export class BrowserWebSocketOrderbookChannel implements OrderbookChannel {
+ private _apiEndpointUrl: string;
+ private _clientIfExists?: WebSocket.w3cwebsocket;
+ private _subscriptions: Subscription[] = [];
+ /**
+ * Instantiates a new WebSocketOrderbookChannel instance
+ * @param url The relayer API base WS url you would like to interact with
+ * @return An instance of WebSocketOrderbookChannel
+ */
+ constructor(url: string) {
+ assert.isUri('url', url);
+ this._apiEndpointUrl = url;
+ }
+ /**
+ * Subscribe to orderbook snapshots and updates from the websocket
+ * @param subscriptionOpts An OrderbookChannelSubscriptionOpts instance describing which
+ * token pair to subscribe to
+ * @param handler An OrderbookChannelHandler instance that responds to various
+ * channel updates
+ */
+ public subscribe(subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler): void {
+ assert.isOrderbookChannelSubscriptionOpts('subscriptionOpts', subscriptionOpts);
+ assert.isOrderbookChannelHandler('handler', handler);
+ const newSubscription: Subscription = {
+ subscriptionOpts,
+ handler,
+ };
+ this._subscriptions.push(newSubscription);
+ const subscribeMessage = {
+ type: 'subscribe',
+ channel: 'orderbook',
+ requestId: this._subscriptions.length - 1,
+ payload: subscriptionOpts,
+ };
+ if (_.isUndefined(this._clientIfExists)) {
+ this._clientIfExists = new WebSocket.w3cwebsocket(this._apiEndpointUrl);
+ this._clientIfExists.onopen = () => {
+ this._sendMessage(subscribeMessage);
+ };
+ this._clientIfExists.onerror = error => {
+ this._alertAllHandlersToError(error);
+ };
+ this._clientIfExists.onclose = () => {
+ _.forEach(this._subscriptions, subscription => {
+ subscription.handler.onClose(this, subscription.subscriptionOpts);
+ });
+ };
+ this._clientIfExists.onmessage = message => {
+ this._handleWebSocketMessage(message);
+ };
+ } else {
+ this._sendMessage(subscribeMessage);
+ }
+ }
+ /**
+ * Close the websocket and stop receiving updates
+ */
+ public close(): void {
+ if (!_.isUndefined(this._clientIfExists)) {
+ this._clientIfExists.close();
+ }
+ }
+ /**
+ * Send a message to the client if it has been instantiated and it is open
+ */
+ private _sendMessage(message: any): void {
+ if (!_.isUndefined(this._clientIfExists) && this._clientIfExists.readyState === WebSocket.w3cwebsocket.OPEN) {
+ this._clientIfExists.send(JSON.stringify(message));
+ }
+ }
+ /**
+ * For use in cases where we need to alert all handlers of an error
+ */
+ private _alertAllHandlersToError(error: Error): void {
+ _.forEach(this._subscriptions, subscription => {
+ subscription.handler.onError(this, subscription.subscriptionOpts, error);
+ });
+ }
+ private _handleWebSocketMessage(message: any): void {
+ // if we get a message with no data, alert all handlers and return
+ if (_.isUndefined(message.data)) {
+ this._alertAllHandlersToError(new Error(`Message does not contain utf8Data`));
+ return;
+ }
+ // try to parse the message data and route it to the correct handler
+ try {
+ const utf8Data = message.data;
+ const parserResult = orderbookChannelMessageParser.parse(utf8Data);
+ const subscription = this._subscriptions[parserResult.requestId];
+ if (_.isUndefined(subscription)) {
+ this._alertAllHandlersToError(new Error(`Message has unknown requestId: ${utf8Data}`));
+ return;
+ }
+ const handler = subscription.handler;
+ const subscriptionOpts = subscription.subscriptionOpts;
+ switch (parserResult.type) {
+ case OrderbookChannelMessageTypes.Snapshot: {
+ handler.onSnapshot(this, subscriptionOpts, parserResult.payload);
+ break;
+ }
+ case OrderbookChannelMessageTypes.Update: {
+ handler.onUpdate(this, subscriptionOpts, parserResult.payload);
+ break;
+ }
+ default: {
+ handler.onError(
+ this,
+ subscriptionOpts,
+ new Error(`Message has unknown type parameter: ${utf8Data}`),
+ );
+ }
+ }
+ } catch (error) {
+ this._alertAllHandlersToError(error);
+ }
+ }
+}
diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts
index ef5d8683e..88b09506c 100644
--- a/packages/connect/src/index.ts
+++ b/packages/connect/src/index.ts
@@ -1,9 +1,11 @@
export { HttpClient } from './http_client';
-export { WebSocketOrderbookChannel } from './ws_orderbook_channel';
+export { BrowserWebSocketOrderbookChannel } from './browser_ws_orderbook_channel';
+export { NodeWebSocketOrderbookChannel } from './node_ws_orderbook_channel';
export {
Client,
FeesRequest,
FeesResponse,
+ NodeWebSocketOrderbookChannelConfig,
OrderbookChannel,
OrderbookChannelHandler,
OrderbookChannelSubscriptionOpts,
@@ -14,7 +16,6 @@ export {
TokenPairsItem,
TokenPairsRequestOpts,
TokenTradeInfo,
- WebSocketOrderbookChannelConfig,
} from './types';
export { Order, SignedOrder } from '@0xproject/types';
diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/node_ws_orderbook_channel.ts
index bdcc8a75d..5f61ac4c8 100644
--- a/packages/connect/src/ws_orderbook_channel.ts
+++ b/packages/connect/src/node_ws_orderbook_channel.ts
@@ -1,18 +1,17 @@
-import { assert } from '@0xproject/assert';
-import { schemas } from '@0xproject/json-schemas';
import * as _ from 'lodash';
import * as WebSocket from 'websocket';
import { schemas as clientSchemas } from './schemas/schemas';
import {
+ NodeWebSocketOrderbookChannelConfig,
OrderbookChannel,
OrderbookChannelHandler,
OrderbookChannelMessageTypes,
OrderbookChannelSubscriptionOpts,
WebsocketClientEventType,
WebsocketConnectionEventType,
- WebSocketOrderbookChannelConfig,
} from './types';
+import { assert } from './utils/assert';
import { orderbookChannelMessageParser } from './utils/orderbook_channel_message_parser';
const DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
@@ -20,9 +19,9 @@ const MINIMUM_HEARTBEAT_INTERVAL_MS = 10;
/**
* This class includes all the functionality related to interacting with a websocket endpoint
- * that implements the standard relayer API v0
+ * that implements the standard relayer API v0 in a node environment
*/
-export class WebSocketOrderbookChannel implements OrderbookChannel {
+export class NodeWebSocketOrderbookChannel implements OrderbookChannel {
private _apiEndpointUrl: string;
private _client: WebSocket.client;
private _connectionIfExists?: WebSocket.connection;
@@ -30,15 +29,15 @@ export class WebSocketOrderbookChannel implements OrderbookChannel {
private _subscriptionCounter = 0;
private _heartbeatIntervalMs: number;
/**
- * Instantiates a new WebSocketOrderbookChannel instance
+ * Instantiates a new NodeWebSocketOrderbookChannelConfig instance
* @param url The relayer API base WS url you would like to interact with
* @param config The configuration object. Look up the type for the description.
- * @return An instance of WebSocketOrderbookChannel
+ * @return An instance of NodeWebSocketOrderbookChannelConfig
*/
- constructor(url: string, config?: WebSocketOrderbookChannelConfig) {
+ constructor(url: string, config?: NodeWebSocketOrderbookChannelConfig) {
assert.isUri('url', url);
if (!_.isUndefined(config)) {
- assert.doesConformToSchema('config', config, clientSchemas.webSocketOrderbookChannelConfigSchema);
+ assert.doesConformToSchema('config', config, clientSchemas.nodeWebSocketOrderbookChannelConfigSchema);
}
this._apiEndpointUrl = url;
this._heartbeatIntervalMs =
@@ -55,15 +54,8 @@ export class WebSocketOrderbookChannel implements OrderbookChannel {
* channel updates
*/
public subscribe(subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler): void {
- assert.doesConformToSchema(
- 'subscriptionOpts',
- subscriptionOpts,
- schemas.relayerApiOrderbookChannelSubscribePayload,
- );
- assert.isFunction('handler.onSnapshot', _.get(handler, 'onSnapshot'));
- assert.isFunction('handler.onUpdate', _.get(handler, 'onUpdate'));
- assert.isFunction('handler.onError', _.get(handler, 'onError'));
- assert.isFunction('handler.onClose', _.get(handler, 'onClose'));
+ assert.isOrderbookChannelSubscriptionOpts('subscriptionOpts', subscriptionOpts);
+ assert.isOrderbookChannelHandler('handler', handler);
this._subscriptionCounter += 1;
const subscribeMessage = {
type: 'subscribe',
diff --git a/packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts b/packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts
index 81c0cac9c..c745d0b82 100644
--- a/packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts
+++ b/packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts
@@ -1,5 +1,5 @@
-export const webSocketOrderbookChannelConfigSchema = {
- id: '/WebSocketOrderbookChannelConfig',
+export const nodeWebSocketOrderbookChannelConfigSchema = {
+ id: '/NodeWebSocketOrderbookChannelConfig',
type: 'object',
properties: {
heartbeatIntervalMs: {
diff --git a/packages/connect/src/schemas/schemas.ts b/packages/connect/src/schemas/schemas.ts
index b9a8472fb..835fc7b4f 100644
--- a/packages/connect/src/schemas/schemas.ts
+++ b/packages/connect/src/schemas/schemas.ts
@@ -1,15 +1,15 @@
import { feesRequestSchema } from './fees_request_schema';
+import { nodeWebSocketOrderbookChannelConfigSchema } from './node_websocket_orderbook_channel_config_schema';
import { orderBookRequestSchema } from './orderbook_request_schema';
import { ordersRequestOptsSchema } from './orders_request_opts_schema';
import { pagedRequestOptsSchema } from './paged_request_opts_schema';
import { tokenPairsRequestOptsSchema } from './token_pairs_request_opts_schema';
-import { webSocketOrderbookChannelConfigSchema } from './websocket_orderbook_channel_config_schema';
export const schemas = {
feesRequestSchema,
+ nodeWebSocketOrderbookChannelConfigSchema,
orderBookRequestSchema,
ordersRequestOptsSchema,
pagedRequestOptsSchema,
tokenPairsRequestOptsSchema,
- webSocketOrderbookChannelConfigSchema,
};
diff --git a/packages/connect/src/types.ts b/packages/connect/src/types.ts
index f5e52f50d..5657942ee 100644
--- a/packages/connect/src/types.ts
+++ b/packages/connect/src/types.ts
@@ -18,7 +18,7 @@ export interface OrderbookChannel {
/**
* heartbeatInterval: Interval in milliseconds that the orderbook channel should ping the underlying websocket. Default: 15000
*/
-export interface WebSocketOrderbookChannelConfig {
+export interface NodeWebSocketOrderbookChannelConfig {
heartbeatIntervalMs?: number;
}
diff --git a/packages/connect/src/utils/assert.ts b/packages/connect/src/utils/assert.ts
new file mode 100644
index 000000000..f8241aacb
--- /dev/null
+++ b/packages/connect/src/utils/assert.ts
@@ -0,0 +1,25 @@
+import { assert as sharedAssert } from '@0xproject/assert';
+// We need those two unused imports because they're actually used by sharedAssert which gets injected here
+// tslint:disable-next-line:no-unused-variable
+import { Schema, schemas } from '@0xproject/json-schemas';
+// tslint:disable-next-line:no-unused-variable
+import { ECSignature } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+
+export const assert = {
+ ...sharedAssert,
+ isOrderbookChannelSubscriptionOpts(variableName: string, subscriptionOpts: any): void {
+ sharedAssert.doesConformToSchema(
+ 'subscriptionOpts',
+ subscriptionOpts,
+ schemas.relayerApiOrderbookChannelSubscribePayload,
+ );
+ },
+ isOrderbookChannelHandler(variableName: string, handler: any): void {
+ sharedAssert.isFunction(`${variableName}.onSnapshot`, _.get(handler, 'onSnapshot'));
+ sharedAssert.isFunction(`${variableName}.onUpdate`, _.get(handler, 'onUpdate'));
+ sharedAssert.isFunction(`${variableName}.onError`, _.get(handler, 'onError'));
+ sharedAssert.isFunction(`${variableName}.onClose`, _.get(handler, 'onClose'));
+ },
+};
diff --git a/packages/connect/src/utils/orderbook_channel_message_parser.ts b/packages/connect/src/utils/orderbook_channel_message_parser.ts
index 9a9ca8901..593288078 100644
--- a/packages/connect/src/utils/orderbook_channel_message_parser.ts
+++ b/packages/connect/src/utils/orderbook_channel_message_parser.ts
@@ -8,10 +8,16 @@ import { relayerResponseJsonParsers } from './relayer_response_json_parsers';
export const orderbookChannelMessageParser = {
parse(utf8Data: string): OrderbookChannelMessage {
+ // parse the message
const messageObj = JSON.parse(utf8Data);
+ // ensure we have a type parameter to switch on
const type: string = _.get(messageObj, 'type');
assert.assert(!_.isUndefined(type), `Message is missing a type parameter: ${utf8Data}`);
assert.isString('type', type);
+ // ensure we have a request id for the resulting message
+ const requestId: number = _.get(messageObj, 'requestId');
+ assert.assert(!_.isUndefined(requestId), `Message is missing a requestId parameter: ${utf8Data}`);
+ assert.isNumber('requestId', requestId);
switch (type) {
case OrderbookChannelMessageTypes.Snapshot: {
assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelSnapshotSchema);
@@ -28,7 +34,7 @@ export const orderbookChannelMessageParser = {
default: {
return {
type: OrderbookChannelMessageTypes.Unknown,
- requestId: 0,
+ requestId,
payload: undefined,
};
}
diff --git a/packages/connect/test/browser_ws_orderbook_channel_test.ts b/packages/connect/test/browser_ws_orderbook_channel_test.ts
new file mode 100644
index 000000000..2941f7086
--- /dev/null
+++ b/packages/connect/test/browser_ws_orderbook_channel_test.ts
@@ -0,0 +1,61 @@
+import * as chai from 'chai';
+import * as dirtyChai from 'dirty-chai';
+import * as _ from 'lodash';
+import 'mocha';
+
+import { BrowserWebSocketOrderbookChannel } from '../src/browser_ws_orderbook_channel';
+
+chai.config.includeStack = true;
+chai.use(dirtyChai);
+const expect = chai.expect;
+
+describe('BrowserWebSocketOrderbookChannel', () => {
+ const websocketUrl = 'ws://localhost:8080';
+ const orderbookChannel = new BrowserWebSocketOrderbookChannel(websocketUrl);
+ const subscriptionOpts = {
+ baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ quoteTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
+ snapshot: true,
+ limit: 100,
+ };
+ const emptyOrderbookChannelHandler = {
+ onSnapshot: () => {
+ _.noop();
+ },
+ onUpdate: () => {
+ _.noop();
+ },
+ onError: () => {
+ _.noop();
+ },
+ onClose: () => {
+ _.noop();
+ },
+ };
+ describe('#subscribe', () => {
+ it('throws when subscriptionOpts does not conform to schema', () => {
+ const badSubscribeCall = orderbookChannel.subscribe.bind(
+ orderbookChannel,
+ {},
+ emptyOrderbookChannelHandler,
+ );
+ expect(badSubscribeCall).throws(
+ 'Expected subscriptionOpts to conform to schema /RelayerApiOrderbookChannelSubscribePayload\nEncountered: {}\nValidation errors: instance requires property "baseTokenAddress", instance requires property "quoteTokenAddress"',
+ );
+ });
+ it('throws when handler has the incorrect members', () => {
+ const badSubscribeCall = orderbookChannel.subscribe.bind(orderbookChannel, subscriptionOpts, {});
+ expect(badSubscribeCall).throws(
+ 'Expected handler.onSnapshot to be of type function, encountered: undefined',
+ );
+ });
+ it('does not throw when inputs are of correct types', () => {
+ const goodSubscribeCall = orderbookChannel.subscribe.bind(
+ orderbookChannel,
+ subscriptionOpts,
+ emptyOrderbookChannelHandler,
+ );
+ expect(goodSubscribeCall).to.not.throw();
+ });
+ });
+});
diff --git a/packages/connect/test/ws_orderbook_channel_test.ts b/packages/connect/test/node_ws_orderbook_channel_test.ts
index ce404d934..5e5325e83 100644
--- a/packages/connect/test/ws_orderbook_channel_test.ts
+++ b/packages/connect/test/node_ws_orderbook_channel_test.ts
@@ -3,15 +3,15 @@ import * as dirtyChai from 'dirty-chai';
import * as _ from 'lodash';
import 'mocha';
-import { WebSocketOrderbookChannel } from '../src/ws_orderbook_channel';
+import { NodeWebSocketOrderbookChannel } from '../src/node_ws_orderbook_channel';
chai.config.includeStack = true;
chai.use(dirtyChai);
const expect = chai.expect;
-describe('WebSocketOrderbookChannel', () => {
+describe('NodeWebSocketOrderbookChannel', () => {
const websocketUrl = 'ws://localhost:8080';
- const orderbookChannel = new WebSocketOrderbookChannel(websocketUrl);
+ const orderbookChannel = new NodeWebSocketOrderbookChannel(websocketUrl);
const subscriptionOpts = {
baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
quoteTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
diff --git a/yarn.lock b/yarn.lock
index 9c373aaa5..a7c8a627b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -548,10 +548,11 @@
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/valid-url/-/valid-url-1.0.2.tgz#60fa435ce24bfd5ba107b8d2a80796aeaf3a8f45"
-"@types/websocket@^0.0.34":
- version "0.0.34"
- resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-0.0.34.tgz#25596764cec885eda070fdb6d19cd76fe582747c"
+"@types/websocket@^0.0.39":
+ version "0.0.39"
+ resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-0.0.39.tgz#aa971e24f9c1455fe2a57ee3e69c7d395016b12a"
dependencies:
+ "@types/events" "*"
"@types/node" "*"
"@types/yargs@^10.0.0":