From 16ddd1edfccdd7768447bfff9afec1f4a1ce014e Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 16 May 2018 11:15:02 -0700 Subject: Implement web browser socket --- packages/connect/package.json | 2 +- .../connect/src/browser_ws_orderbook_channel.ts | 140 +++++++++++++++++ packages/connect/src/index.ts | 5 +- packages/connect/src/node_ws_orderbook_channel.ts | 158 ++++++++++++++++++++ ...de_websocket_orderbook_channel_config_schema.ts | 10 ++ packages/connect/src/schemas/schemas.ts | 4 +- .../websocket_orderbook_channel_config_schema.ts | 10 -- packages/connect/src/types.ts | 2 +- packages/connect/src/utils/assert.ts | 25 ++++ .../src/utils/orderbook_channel_message_parser.ts | 8 +- packages/connect/src/ws_orderbook_channel.ts | 166 --------------------- .../test/browser_ws_orderbook_channel_test.ts | 61 ++++++++ .../connect/test/node_ws_orderbook_channel_test.ts | 61 ++++++++ packages/connect/test/ws_orderbook_channel_test.ts | 61 -------- 14 files changed, 469 insertions(+), 244 deletions(-) create mode 100644 packages/connect/src/browser_ws_orderbook_channel.ts create mode 100644 packages/connect/src/node_ws_orderbook_channel.ts create mode 100644 packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts delete mode 100644 packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts create mode 100644 packages/connect/src/utils/assert.ts delete mode 100644 packages/connect/src/ws_orderbook_channel.ts create mode 100644 packages/connect/test/browser_ws_orderbook_channel_test.ts create mode 100644 packages/connect/test/node_ws_orderbook_channel_test.ts delete mode 100644 packages/connect/test/ws_orderbook_channel_test.ts (limited to 'packages') 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/node_ws_orderbook_channel.ts b/packages/connect/src/node_ws_orderbook_channel.ts new file mode 100644 index 000000000..5f61ac4c8 --- /dev/null +++ b/packages/connect/src/node_ws_orderbook_channel.ts @@ -0,0 +1,158 @@ +import * as _ from 'lodash'; +import * as WebSocket from 'websocket'; + +import { schemas as clientSchemas } from './schemas/schemas'; +import { + NodeWebSocketOrderbookChannelConfig, + OrderbookChannel, + OrderbookChannelHandler, + OrderbookChannelMessageTypes, + OrderbookChannelSubscriptionOpts, + WebsocketClientEventType, + WebsocketConnectionEventType, +} from './types'; +import { assert } from './utils/assert'; +import { orderbookChannelMessageParser } from './utils/orderbook_channel_message_parser'; + +const DEFAULT_HEARTBEAT_INTERVAL_MS = 15000; +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 in a node environment + */ +export class NodeWebSocketOrderbookChannel implements OrderbookChannel { + private _apiEndpointUrl: string; + private _client: WebSocket.client; + private _connectionIfExists?: WebSocket.connection; + private _heartbeatTimerIfExists?: NodeJS.Timer; + private _subscriptionCounter = 0; + private _heartbeatIntervalMs: number; + /** + * 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 NodeWebSocketOrderbookChannelConfig + */ + constructor(url: string, config?: NodeWebSocketOrderbookChannelConfig) { + assert.isUri('url', url); + if (!_.isUndefined(config)) { + assert.doesConformToSchema('config', config, clientSchemas.nodeWebSocketOrderbookChannelConfigSchema); + } + this._apiEndpointUrl = url; + this._heartbeatIntervalMs = + _.isUndefined(config) || _.isUndefined(config.heartbeatIntervalMs) + ? DEFAULT_HEARTBEAT_INTERVAL_MS + : config.heartbeatIntervalMs; + this._client = new WebSocket.client(); + } + /** + * 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); + this._subscriptionCounter += 1; + const subscribeMessage = { + type: 'subscribe', + channel: 'orderbook', + requestId: this._subscriptionCounter, + payload: subscriptionOpts, + }; + this._getConnection((error, connection) => { + if (!_.isUndefined(error)) { + handler.onError(this, subscriptionOpts, error); + } else if (!_.isUndefined(connection) && connection.connected) { + connection.on(WebsocketConnectionEventType.Error, wsError => { + handler.onError(this, subscriptionOpts, wsError); + }); + connection.on(WebsocketConnectionEventType.Close, (_code: number, _desc: string) => { + handler.onClose(this, subscriptionOpts); + }); + connection.on(WebsocketConnectionEventType.Message, message => { + this._handleWebSocketMessage(subscribeMessage.requestId, subscriptionOpts, message, handler); + }); + connection.sendUTF(JSON.stringify(subscribeMessage)); + } + }); + } + /** + * Close the websocket and stop receiving updates + */ + public close(): void { + if (!_.isUndefined(this._connectionIfExists)) { + this._connectionIfExists.close(); + } + if (!_.isUndefined(this._heartbeatTimerIfExists)) { + clearInterval(this._heartbeatTimerIfExists); + } + } + private _getConnection(callback: (error?: Error, connection?: WebSocket.connection) => void): void { + if (!_.isUndefined(this._connectionIfExists) && this._connectionIfExists.connected) { + callback(undefined, this._connectionIfExists); + } else { + this._client.on(WebsocketClientEventType.Connect, connection => { + this._connectionIfExists = connection; + if (this._heartbeatIntervalMs >= MINIMUM_HEARTBEAT_INTERVAL_MS) { + this._heartbeatTimerIfExists = setInterval(() => { + connection.ping(''); + }, this._heartbeatIntervalMs); + } else { + callback( + new Error( + `Heartbeat interval is ${ + this._heartbeatIntervalMs + }ms which is less than the required minimum of ${MINIMUM_HEARTBEAT_INTERVAL_MS}ms`, + ), + undefined, + ); + } + callback(undefined, this._connectionIfExists); + }); + this._client.on(WebsocketClientEventType.ConnectFailed, error => { + callback(error, undefined); + }); + this._client.connect(this._apiEndpointUrl); + } + } + private _handleWebSocketMessage( + requestId: number, + subscriptionOpts: OrderbookChannelSubscriptionOpts, + message: WebSocket.IMessage, + handler: OrderbookChannelHandler, + ): void { + if (!_.isUndefined(message.utf8Data)) { + try { + const utf8Data = message.utf8Data; + const parserResult = orderbookChannelMessageParser.parse(utf8Data); + if (parserResult.requestId === requestId) { + 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 missing a type parameter: ${utf8Data}`), + ); + } + } + } + } catch (error) { + handler.onError(this, subscriptionOpts, error); + } + } else { + handler.onError(this, subscriptionOpts, new Error(`Message does not contain utf8Data`)); + } + } +} diff --git a/packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts b/packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts new file mode 100644 index 000000000..c745d0b82 --- /dev/null +++ b/packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts @@ -0,0 +1,10 @@ +export const nodeWebSocketOrderbookChannelConfigSchema = { + id: '/NodeWebSocketOrderbookChannelConfig', + type: 'object', + properties: { + heartbeatIntervalMs: { + type: 'number', + minimum: 10, + }, + }, +}; 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/schemas/websocket_orderbook_channel_config_schema.ts b/packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts deleted file mode 100644 index 81c0cac9c..000000000 --- a/packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const webSocketOrderbookChannelConfigSchema = { - id: '/WebSocketOrderbookChannelConfig', - type: 'object', - properties: { - heartbeatIntervalMs: { - type: 'number', - minimum: 10, - }, - }, -}; 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/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts deleted file mode 100644 index bdcc8a75d..000000000 --- a/packages/connect/src/ws_orderbook_channel.ts +++ /dev/null @@ -1,166 +0,0 @@ -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 { - OrderbookChannel, - OrderbookChannelHandler, - OrderbookChannelMessageTypes, - OrderbookChannelSubscriptionOpts, - WebsocketClientEventType, - WebsocketConnectionEventType, - WebSocketOrderbookChannelConfig, -} from './types'; -import { orderbookChannelMessageParser } from './utils/orderbook_channel_message_parser'; - -const DEFAULT_HEARTBEAT_INTERVAL_MS = 15000; -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 - */ -export class WebSocketOrderbookChannel implements OrderbookChannel { - private _apiEndpointUrl: string; - private _client: WebSocket.client; - private _connectionIfExists?: WebSocket.connection; - private _heartbeatTimerIfExists?: NodeJS.Timer; - private _subscriptionCounter = 0; - private _heartbeatIntervalMs: number; - /** - * Instantiates a new WebSocketOrderbookChannel 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 - */ - constructor(url: string, config?: WebSocketOrderbookChannelConfig) { - assert.isUri('url', url); - if (!_.isUndefined(config)) { - assert.doesConformToSchema('config', config, clientSchemas.webSocketOrderbookChannelConfigSchema); - } - this._apiEndpointUrl = url; - this._heartbeatIntervalMs = - _.isUndefined(config) || _.isUndefined(config.heartbeatIntervalMs) - ? DEFAULT_HEARTBEAT_INTERVAL_MS - : config.heartbeatIntervalMs; - this._client = new WebSocket.client(); - } - /** - * 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.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')); - this._subscriptionCounter += 1; - const subscribeMessage = { - type: 'subscribe', - channel: 'orderbook', - requestId: this._subscriptionCounter, - payload: subscriptionOpts, - }; - this._getConnection((error, connection) => { - if (!_.isUndefined(error)) { - handler.onError(this, subscriptionOpts, error); - } else if (!_.isUndefined(connection) && connection.connected) { - connection.on(WebsocketConnectionEventType.Error, wsError => { - handler.onError(this, subscriptionOpts, wsError); - }); - connection.on(WebsocketConnectionEventType.Close, (_code: number, _desc: string) => { - handler.onClose(this, subscriptionOpts); - }); - connection.on(WebsocketConnectionEventType.Message, message => { - this._handleWebSocketMessage(subscribeMessage.requestId, subscriptionOpts, message, handler); - }); - connection.sendUTF(JSON.stringify(subscribeMessage)); - } - }); - } - /** - * Close the websocket and stop receiving updates - */ - public close(): void { - if (!_.isUndefined(this._connectionIfExists)) { - this._connectionIfExists.close(); - } - if (!_.isUndefined(this._heartbeatTimerIfExists)) { - clearInterval(this._heartbeatTimerIfExists); - } - } - private _getConnection(callback: (error?: Error, connection?: WebSocket.connection) => void): void { - if (!_.isUndefined(this._connectionIfExists) && this._connectionIfExists.connected) { - callback(undefined, this._connectionIfExists); - } else { - this._client.on(WebsocketClientEventType.Connect, connection => { - this._connectionIfExists = connection; - if (this._heartbeatIntervalMs >= MINIMUM_HEARTBEAT_INTERVAL_MS) { - this._heartbeatTimerIfExists = setInterval(() => { - connection.ping(''); - }, this._heartbeatIntervalMs); - } else { - callback( - new Error( - `Heartbeat interval is ${ - this._heartbeatIntervalMs - }ms which is less than the required minimum of ${MINIMUM_HEARTBEAT_INTERVAL_MS}ms`, - ), - undefined, - ); - } - callback(undefined, this._connectionIfExists); - }); - this._client.on(WebsocketClientEventType.ConnectFailed, error => { - callback(error, undefined); - }); - this._client.connect(this._apiEndpointUrl); - } - } - private _handleWebSocketMessage( - requestId: number, - subscriptionOpts: OrderbookChannelSubscriptionOpts, - message: WebSocket.IMessage, - handler: OrderbookChannelHandler, - ): void { - if (!_.isUndefined(message.utf8Data)) { - try { - const utf8Data = message.utf8Data; - const parserResult = orderbookChannelMessageParser.parse(utf8Data); - if (parserResult.requestId === requestId) { - 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 missing a type parameter: ${utf8Data}`), - ); - } - } - } - } catch (error) { - handler.onError(this, subscriptionOpts, error); - } - } else { - handler.onError(this, subscriptionOpts, new Error(`Message does not contain utf8Data`)); - } - } -} 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/node_ws_orderbook_channel_test.ts b/packages/connect/test/node_ws_orderbook_channel_test.ts new file mode 100644 index 000000000..5e5325e83 --- /dev/null +++ b/packages/connect/test/node_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 { NodeWebSocketOrderbookChannel } from '../src/node_ws_orderbook_channel'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +const expect = chai.expect; + +describe('NodeWebSocketOrderbookChannel', () => { + const websocketUrl = 'ws://localhost:8080'; + const orderbookChannel = new NodeWebSocketOrderbookChannel(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/ws_orderbook_channel_test.ts deleted file mode 100644 index ce404d934..000000000 --- a/packages/connect/test/ws_orderbook_channel_test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as chai from 'chai'; -import * as dirtyChai from 'dirty-chai'; -import * as _ from 'lodash'; -import 'mocha'; - -import { WebSocketOrderbookChannel } from '../src/ws_orderbook_channel'; - -chai.config.includeStack = true; -chai.use(dirtyChai); -const expect = chai.expect; - -describe('WebSocketOrderbookChannel', () => { - const websocketUrl = 'ws://localhost:8080'; - const orderbookChannel = new WebSocketOrderbookChannel(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(); - }); - }); -}); -- cgit v1.2.3 From 47debf0134b5864046831321b8eeeeb9aaaaf0a8 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Thu, 24 May 2018 17:19:27 -0700 Subject: Initial implementation of OrderbookChannelFactory --- .../connect/src/browser_ws_orderbook_channel.ts | 49 ++++----- packages/connect/src/index.ts | 1 + packages/connect/src/orderbook_channel_factory.ts | 33 ++++++ .../test/browser_ws_orderbook_channel_test.ts | 118 +++++++++++---------- 4 files changed, 113 insertions(+), 88 deletions(-) create mode 100644 packages/connect/src/orderbook_channel_factory.ts (limited to 'packages') diff --git a/packages/connect/src/browser_ws_orderbook_channel.ts b/packages/connect/src/browser_ws_orderbook_channel.ts index b97a82ec9..599b4f0be 100644 --- a/packages/connect/src/browser_ws_orderbook_channel.ts +++ b/packages/connect/src/browser_ws_orderbook_channel.ts @@ -1,5 +1,4 @@ import * as _ from 'lodash'; -import * as WebSocket from 'websocket'; import { OrderbookChannel, @@ -22,17 +21,16 @@ interface Subscription { * that implements the standard relayer API v0 in a browser environment */ export class BrowserWebSocketOrderbookChannel implements OrderbookChannel { - private _apiEndpointUrl: string; - private _clientIfExists?: WebSocket.w3cwebsocket; + private _client: WebSocket; 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; + constructor(client: WebSocket) { + // assert.isUri('url', url); + this._client = client; } /** * Subscribe to orderbook snapshots and updates from the websocket @@ -55,40 +53,31 @@ export class BrowserWebSocketOrderbookChannel implements OrderbookChannel { 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); - } + this._client.onerror = () => { + this._alertAllHandlersToError(new Error('hello')); + }; + this._client.onclose = () => { + _.forEach(this._subscriptions, subscription => { + subscription.handler.onClose(this, subscription.subscriptionOpts); + }); + }; + this._client.onmessage = message => { + this._handleWebSocketMessage(message); + }; + this._sendMessage(subscribeMessage); } /** * Close the websocket and stop receiving updates */ public close(): void { - if (!_.isUndefined(this._clientIfExists)) { - this._clientIfExists.close(); - } + this._client.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)); + if (this._client.readyState === WebSocket.OPEN) { + this._client.send(JSON.stringify(message)); } } /** diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index 88b09506c..30ce57aea 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -1,6 +1,7 @@ export { HttpClient } from './http_client'; export { BrowserWebSocketOrderbookChannel } from './browser_ws_orderbook_channel'; export { NodeWebSocketOrderbookChannel } from './node_ws_orderbook_channel'; +export { orderbookChannelFactory } from './orderbook_channel_factory'; export { Client, FeesRequest, diff --git a/packages/connect/src/orderbook_channel_factory.ts b/packages/connect/src/orderbook_channel_factory.ts new file mode 100644 index 000000000..cb00212e7 --- /dev/null +++ b/packages/connect/src/orderbook_channel_factory.ts @@ -0,0 +1,33 @@ +// import * as WebSocket from 'websocket'; + +import { BrowserWebSocketOrderbookChannel } from './browser_ws_orderbook_channel'; +import { NodeWebSocketOrderbookChannel } from './node_ws_orderbook_channel'; + +export const orderbookChannelFactory = { + async createBrowserOrderbookChannelAsync(url: string): Promise { + return new Promise((resolve, reject) => { + const client = new WebSocket(url); + console.log(client); + client.onopen = () => { + const orderbookChannel = new BrowserWebSocketOrderbookChannel(client); + console.log(orderbookChannel); + resolve(orderbookChannel); + }; + client.onerror = err => { + reject(err); + }; + }); + }, + // async createNodeOrderbookChannelAsync(url: string): Promise { + // return new Promise((resolve, reject) => { + // const client = new WebSocket.w3cwebsocket(url); + // client.onopen = () => { + // const orderbookChannel = new BrowserWebSocketOrderbookChannel(client); + // resolve(orderbookChannel); + // }; + // client.onerror = err => { + // reject(err); + // }; + // }); + // }, +}; diff --git a/packages/connect/test/browser_ws_orderbook_channel_test.ts b/packages/connect/test/browser_ws_orderbook_channel_test.ts index 2941f7086..d6a7af5c0 100644 --- a/packages/connect/test/browser_ws_orderbook_channel_test.ts +++ b/packages/connect/test/browser_ws_orderbook_channel_test.ts @@ -1,61 +1,63 @@ -import * as chai from 'chai'; -import * as dirtyChai from 'dirty-chai'; -import * as _ from 'lodash'; -import 'mocha'; +// import * as chai from 'chai'; +// import * as dirtyChai from 'dirty-chai'; +// import * as _ from 'lodash'; +// import 'mocha'; +// import * as WebSocket from 'websocket'; -import { BrowserWebSocketOrderbookChannel } from '../src/browser_ws_orderbook_channel'; +// import { BrowserWebSocketOrderbookChannel } from '../src/browser_ws_orderbook_channel'; -chai.config.includeStack = true; -chai.use(dirtyChai); -const expect = chai.expect; +// 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(); - }); - }); -}); +// describe('BrowserWebSocketOrderbookChannel', () => { +// const websocketUrl = 'ws://localhost:8080'; +// const client = new WebSocket.w3cwebsocket(websocketUrl); +// const orderbookChannel = new BrowserWebSocketOrderbookChannel(client); +// 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(); +// }); +// }); +// }); -- cgit v1.2.3 From a4b6112a311332df2c00799857463a646df78e25 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 25 May 2018 16:08:15 -0700 Subject: Consolidate back to one channel and expose only the factory --- .../connect/src/browser_ws_orderbook_channel.ts | 129 ----------------- packages/connect/src/index.ts | 3 - packages/connect/src/node_ws_orderbook_channel.ts | 158 --------------------- packages/connect/src/orderbook_channel_factory.ts | 35 ++--- ...de_websocket_orderbook_channel_config_schema.ts | 10 -- packages/connect/src/schemas/schemas.ts | 2 - packages/connect/src/types.ts | 7 - packages/connect/src/ws_orderbook_channel.ts | 132 +++++++++++++++++ .../test/browser_ws_orderbook_channel_test.ts | 63 -------- .../connect/test/node_ws_orderbook_channel_test.ts | 61 -------- .../connect/test/orderbook_channel_factory_test.ts | 26 ++++ packages/connect/test/ws_orderbook_channel_test.ts | 63 ++++++++ 12 files changed, 235 insertions(+), 454 deletions(-) delete mode 100644 packages/connect/src/browser_ws_orderbook_channel.ts delete mode 100644 packages/connect/src/node_ws_orderbook_channel.ts delete mode 100644 packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts create mode 100644 packages/connect/src/ws_orderbook_channel.ts delete mode 100644 packages/connect/test/browser_ws_orderbook_channel_test.ts delete mode 100644 packages/connect/test/node_ws_orderbook_channel_test.ts create mode 100644 packages/connect/test/orderbook_channel_factory_test.ts create mode 100644 packages/connect/test/ws_orderbook_channel_test.ts (limited to 'packages') diff --git a/packages/connect/src/browser_ws_orderbook_channel.ts b/packages/connect/src/browser_ws_orderbook_channel.ts deleted file mode 100644 index 599b4f0be..000000000 --- a/packages/connect/src/browser_ws_orderbook_channel.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as _ from 'lodash'; - -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 _client: WebSocket; - 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(client: WebSocket) { - // assert.isUri('url', url); - this._client = client; - } - /** - * 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, - }; - this._client.onerror = () => { - this._alertAllHandlersToError(new Error('hello')); - }; - this._client.onclose = () => { - _.forEach(this._subscriptions, subscription => { - subscription.handler.onClose(this, subscription.subscriptionOpts); - }); - }; - this._client.onmessage = message => { - this._handleWebSocketMessage(message); - }; - this._sendMessage(subscribeMessage); - } - /** - * Close the websocket and stop receiving updates - */ - public close(): void { - this._client.close(); - } - /** - * Send a message to the client if it has been instantiated and it is open - */ - private _sendMessage(message: any): void { - if (this._client.readyState === WebSocket.OPEN) { - this._client.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 30ce57aea..7f5eb8ed3 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -1,12 +1,9 @@ export { HttpClient } from './http_client'; -export { BrowserWebSocketOrderbookChannel } from './browser_ws_orderbook_channel'; -export { NodeWebSocketOrderbookChannel } from './node_ws_orderbook_channel'; export { orderbookChannelFactory } from './orderbook_channel_factory'; export { Client, FeesRequest, FeesResponse, - NodeWebSocketOrderbookChannelConfig, OrderbookChannel, OrderbookChannelHandler, OrderbookChannelSubscriptionOpts, diff --git a/packages/connect/src/node_ws_orderbook_channel.ts b/packages/connect/src/node_ws_orderbook_channel.ts deleted file mode 100644 index 5f61ac4c8..000000000 --- a/packages/connect/src/node_ws_orderbook_channel.ts +++ /dev/null @@ -1,158 +0,0 @@ -import * as _ from 'lodash'; -import * as WebSocket from 'websocket'; - -import { schemas as clientSchemas } from './schemas/schemas'; -import { - NodeWebSocketOrderbookChannelConfig, - OrderbookChannel, - OrderbookChannelHandler, - OrderbookChannelMessageTypes, - OrderbookChannelSubscriptionOpts, - WebsocketClientEventType, - WebsocketConnectionEventType, -} from './types'; -import { assert } from './utils/assert'; -import { orderbookChannelMessageParser } from './utils/orderbook_channel_message_parser'; - -const DEFAULT_HEARTBEAT_INTERVAL_MS = 15000; -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 in a node environment - */ -export class NodeWebSocketOrderbookChannel implements OrderbookChannel { - private _apiEndpointUrl: string; - private _client: WebSocket.client; - private _connectionIfExists?: WebSocket.connection; - private _heartbeatTimerIfExists?: NodeJS.Timer; - private _subscriptionCounter = 0; - private _heartbeatIntervalMs: number; - /** - * 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 NodeWebSocketOrderbookChannelConfig - */ - constructor(url: string, config?: NodeWebSocketOrderbookChannelConfig) { - assert.isUri('url', url); - if (!_.isUndefined(config)) { - assert.doesConformToSchema('config', config, clientSchemas.nodeWebSocketOrderbookChannelConfigSchema); - } - this._apiEndpointUrl = url; - this._heartbeatIntervalMs = - _.isUndefined(config) || _.isUndefined(config.heartbeatIntervalMs) - ? DEFAULT_HEARTBEAT_INTERVAL_MS - : config.heartbeatIntervalMs; - this._client = new WebSocket.client(); - } - /** - * 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); - this._subscriptionCounter += 1; - const subscribeMessage = { - type: 'subscribe', - channel: 'orderbook', - requestId: this._subscriptionCounter, - payload: subscriptionOpts, - }; - this._getConnection((error, connection) => { - if (!_.isUndefined(error)) { - handler.onError(this, subscriptionOpts, error); - } else if (!_.isUndefined(connection) && connection.connected) { - connection.on(WebsocketConnectionEventType.Error, wsError => { - handler.onError(this, subscriptionOpts, wsError); - }); - connection.on(WebsocketConnectionEventType.Close, (_code: number, _desc: string) => { - handler.onClose(this, subscriptionOpts); - }); - connection.on(WebsocketConnectionEventType.Message, message => { - this._handleWebSocketMessage(subscribeMessage.requestId, subscriptionOpts, message, handler); - }); - connection.sendUTF(JSON.stringify(subscribeMessage)); - } - }); - } - /** - * Close the websocket and stop receiving updates - */ - public close(): void { - if (!_.isUndefined(this._connectionIfExists)) { - this._connectionIfExists.close(); - } - if (!_.isUndefined(this._heartbeatTimerIfExists)) { - clearInterval(this._heartbeatTimerIfExists); - } - } - private _getConnection(callback: (error?: Error, connection?: WebSocket.connection) => void): void { - if (!_.isUndefined(this._connectionIfExists) && this._connectionIfExists.connected) { - callback(undefined, this._connectionIfExists); - } else { - this._client.on(WebsocketClientEventType.Connect, connection => { - this._connectionIfExists = connection; - if (this._heartbeatIntervalMs >= MINIMUM_HEARTBEAT_INTERVAL_MS) { - this._heartbeatTimerIfExists = setInterval(() => { - connection.ping(''); - }, this._heartbeatIntervalMs); - } else { - callback( - new Error( - `Heartbeat interval is ${ - this._heartbeatIntervalMs - }ms which is less than the required minimum of ${MINIMUM_HEARTBEAT_INTERVAL_MS}ms`, - ), - undefined, - ); - } - callback(undefined, this._connectionIfExists); - }); - this._client.on(WebsocketClientEventType.ConnectFailed, error => { - callback(error, undefined); - }); - this._client.connect(this._apiEndpointUrl); - } - } - private _handleWebSocketMessage( - requestId: number, - subscriptionOpts: OrderbookChannelSubscriptionOpts, - message: WebSocket.IMessage, - handler: OrderbookChannelHandler, - ): void { - if (!_.isUndefined(message.utf8Data)) { - try { - const utf8Data = message.utf8Data; - const parserResult = orderbookChannelMessageParser.parse(utf8Data); - if (parserResult.requestId === requestId) { - 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 missing a type parameter: ${utf8Data}`), - ); - } - } - } - } catch (error) { - handler.onError(this, subscriptionOpts, error); - } - } else { - handler.onError(this, subscriptionOpts, new Error(`Message does not contain utf8Data`)); - } - } -} diff --git a/packages/connect/src/orderbook_channel_factory.ts b/packages/connect/src/orderbook_channel_factory.ts index cb00212e7..4b363365f 100644 --- a/packages/connect/src/orderbook_channel_factory.ts +++ b/packages/connect/src/orderbook_channel_factory.ts @@ -1,16 +1,21 @@ -// import * as WebSocket from 'websocket'; +import * as WebSocket from 'websocket'; -import { BrowserWebSocketOrderbookChannel } from './browser_ws_orderbook_channel'; -import { NodeWebSocketOrderbookChannel } from './node_ws_orderbook_channel'; +import { OrderbookChannel, WebsocketClientEventType } from './types'; +import { assert } from './utils/assert'; +import { WebSocketOrderbookChannel } from './ws_orderbook_channel'; export const orderbookChannelFactory = { - async createBrowserOrderbookChannelAsync(url: string): Promise { - return new Promise((resolve, reject) => { - const client = new WebSocket(url); - console.log(client); + /** + * Instantiates a new WebSocketOrderbookChannel instance + * @param url The relayer API base WS url you would like to interact with + * @return An OrderbookChannel Promise + */ + async createWebSocketOrderbookChannelAsync(url: string): Promise { + assert.isUri('url', url); + return new Promise((resolve, reject) => { + const client = new WebSocket.w3cwebsocket(url); client.onopen = () => { - const orderbookChannel = new BrowserWebSocketOrderbookChannel(client); - console.log(orderbookChannel); + const orderbookChannel = new WebSocketOrderbookChannel(client); resolve(orderbookChannel); }; client.onerror = err => { @@ -18,16 +23,4 @@ export const orderbookChannelFactory = { }; }); }, - // async createNodeOrderbookChannelAsync(url: string): Promise { - // return new Promise((resolve, reject) => { - // const client = new WebSocket.w3cwebsocket(url); - // client.onopen = () => { - // const orderbookChannel = new BrowserWebSocketOrderbookChannel(client); - // resolve(orderbookChannel); - // }; - // client.onerror = err => { - // reject(err); - // }; - // }); - // }, }; diff --git a/packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts b/packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts deleted file mode 100644 index c745d0b82..000000000 --- a/packages/connect/src/schemas/node_websocket_orderbook_channel_config_schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const nodeWebSocketOrderbookChannelConfigSchema = { - id: '/NodeWebSocketOrderbookChannelConfig', - type: 'object', - properties: { - heartbeatIntervalMs: { - type: 'number', - minimum: 10, - }, - }, -}; diff --git a/packages/connect/src/schemas/schemas.ts b/packages/connect/src/schemas/schemas.ts index 835fc7b4f..0b8b798a9 100644 --- a/packages/connect/src/schemas/schemas.ts +++ b/packages/connect/src/schemas/schemas.ts @@ -1,5 +1,4 @@ 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'; @@ -7,7 +6,6 @@ import { tokenPairsRequestOptsSchema } from './token_pairs_request_opts_schema'; export const schemas = { feesRequestSchema, - nodeWebSocketOrderbookChannelConfigSchema, orderBookRequestSchema, ordersRequestOptsSchema, pagedRequestOptsSchema, diff --git a/packages/connect/src/types.ts b/packages/connect/src/types.ts index 5657942ee..5ea114371 100644 --- a/packages/connect/src/types.ts +++ b/packages/connect/src/types.ts @@ -15,13 +15,6 @@ export interface OrderbookChannel { close: () => void; } -/** - * heartbeatInterval: Interval in milliseconds that the orderbook channel should ping the underlying websocket. Default: 15000 - */ -export interface NodeWebSocketOrderbookChannelConfig { - heartbeatIntervalMs?: number; -} - /** * baseTokenAddress: The address of token designated as the baseToken in the currency pair calculation of price * quoteTokenAddress: The address of token designated as the quoteToken in the currency pair calculation of price diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts new file mode 100644 index 000000000..f90d9ac30 --- /dev/null +++ b/packages/connect/src/ws_orderbook_channel.ts @@ -0,0 +1,132 @@ +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 + */ +export class WebSocketOrderbookChannel implements OrderbookChannel { + private _client: 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(client: WebSocket.w3cwebsocket) { + this._client = client; + this._client.onerror = err => { + this._alertAllHandlersToError(err); + }; + this._client.onclose = () => { + this._alertAllHandlersToClose(); + }; + this._client.onmessage = message => { + this._handleWebSocketMessage(message); + }; + } + /** + * 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, + }; + this._sendMessage(subscribeMessage); + } + /** + * Close the websocket and stop receiving updates + */ + public close(): void { + this._client.close(); + } + /** + * Send a message to the client if it has been instantiated and it is open + */ + private _sendMessage(message: any): void { + if (this._client.readyState === WebSocket.w3cwebsocket.OPEN) { + this._client.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 _alertAllHandlersToClose(): void { + _.forEach(this._subscriptions, subscription => { + subscription.handler.onClose(this, subscription.subscriptionOpts); + }); + } + 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/test/browser_ws_orderbook_channel_test.ts b/packages/connect/test/browser_ws_orderbook_channel_test.ts deleted file mode 100644 index d6a7af5c0..000000000 --- a/packages/connect/test/browser_ws_orderbook_channel_test.ts +++ /dev/null @@ -1,63 +0,0 @@ -// import * as chai from 'chai'; -// import * as dirtyChai from 'dirty-chai'; -// import * as _ from 'lodash'; -// import 'mocha'; -// import * as WebSocket from 'websocket'; - -// 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 client = new WebSocket.w3cwebsocket(websocketUrl); -// const orderbookChannel = new BrowserWebSocketOrderbookChannel(client); -// 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/node_ws_orderbook_channel_test.ts b/packages/connect/test/node_ws_orderbook_channel_test.ts deleted file mode 100644 index 5e5325e83..000000000 --- a/packages/connect/test/node_ws_orderbook_channel_test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as chai from 'chai'; -import * as dirtyChai from 'dirty-chai'; -import * as _ from 'lodash'; -import 'mocha'; - -import { NodeWebSocketOrderbookChannel } from '../src/node_ws_orderbook_channel'; - -chai.config.includeStack = true; -chai.use(dirtyChai); -const expect = chai.expect; - -describe('NodeWebSocketOrderbookChannel', () => { - const websocketUrl = 'ws://localhost:8080'; - const orderbookChannel = new NodeWebSocketOrderbookChannel(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/orderbook_channel_factory_test.ts b/packages/connect/test/orderbook_channel_factory_test.ts new file mode 100644 index 000000000..fd84332cc --- /dev/null +++ b/packages/connect/test/orderbook_channel_factory_test.ts @@ -0,0 +1,26 @@ +import * as chai from 'chai'; +import * as dirtyChai from 'dirty-chai'; +import * as _ from 'lodash'; +import 'mocha'; +import * as WebSocket from 'websocket'; + +import { orderbookChannelFactory } from '../src/orderbook_channel_factory'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +const expect = chai.expect; + +describe('orderbookChannelFactory', () => { + const websocketUrl = 'ws://localhost:8080'; + + describe('#createWebSocketOrderbookChannelAsync', () => { + it('throws when input is not a url', () => { + const badInput = 54; + const badSubscribeCall = orderbookChannelFactory.createWebSocketOrderbookChannelAsync.bind( + orderbookChannelFactory, + badInput, + ); + expect(orderbookChannelFactory.createWebSocketOrderbookChannelAsync(badInput as any)).to.be.rejected(); + }); + }); +}); diff --git a/packages/connect/test/ws_orderbook_channel_test.ts b/packages/connect/test/ws_orderbook_channel_test.ts new file mode 100644 index 000000000..79100d0e2 --- /dev/null +++ b/packages/connect/test/ws_orderbook_channel_test.ts @@ -0,0 +1,63 @@ +import * as chai from 'chai'; +import * as dirtyChai from 'dirty-chai'; +import * as _ from 'lodash'; +import 'mocha'; +import * as WebSocket from 'websocket'; + +import { WebSocketOrderbookChannel } from '../src/ws_orderbook_channel'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +const expect = chai.expect; + +describe('WebSocketOrderbookChannel', () => { + const websocketUrl = 'ws://localhost:8080'; + const client = new WebSocket.w3cwebsocket(websocketUrl); + const orderbookChannel = new WebSocketOrderbookChannel(client); + 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(); + }); + }); +}); -- cgit v1.2.3 From 0efe6df416de83fe508245326ab69ad3d37ee510 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Fri, 25 May 2018 16:50:10 -0700 Subject: Add CHANGELOG entry --- packages/connect/CHANGELOG.json | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'packages') diff --git a/packages/connect/CHANGELOG.json b/packages/connect/CHANGELOG.json index c426f974b..562b9e9e6 100644 --- a/packages/connect/CHANGELOG.json +++ b/packages/connect/CHANGELOG.json @@ -1,4 +1,12 @@ [ + { + "version": "0.7.0", + "changes": [ + { + "note": "Remove WebSocketOrderbookChannel from the public interface and replace with orderbookChannelFactory" + } + ] + }, { "timestamp": 1531149657, "version": "0.6.16", -- cgit v1.2.3 From cab6829df9063c698f91d2fb03bdbd81999843d1 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 29 May 2018 10:37:35 -0700 Subject: Remove unused import --- packages/connect/src/orderbook_channel_factory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'packages') diff --git a/packages/connect/src/orderbook_channel_factory.ts b/packages/connect/src/orderbook_channel_factory.ts index 4b363365f..1b5625840 100644 --- a/packages/connect/src/orderbook_channel_factory.ts +++ b/packages/connect/src/orderbook_channel_factory.ts @@ -1,13 +1,13 @@ import * as WebSocket from 'websocket'; -import { OrderbookChannel, WebsocketClientEventType } from './types'; +import { OrderbookChannel } from './types'; import { assert } from './utils/assert'; import { WebSocketOrderbookChannel } from './ws_orderbook_channel'; export const orderbookChannelFactory = { /** * Instantiates a new WebSocketOrderbookChannel instance - * @param url The relayer API base WS url you would like to interact with + * @param url The relayer API base WS url you would like to interact with * @return An OrderbookChannel Promise */ async createWebSocketOrderbookChannelAsync(url: string): Promise { -- cgit v1.2.3 From 0c120cb7a3a7796504e577a99452ea8909989cfc Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 29 May 2018 10:38:08 -0700 Subject: Assert that connection is opening before attempting to subscribe --- packages/connect/src/ws_orderbook_channel.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) (limited to 'packages') diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts index f90d9ac30..45d1b35d7 100644 --- a/packages/connect/src/ws_orderbook_channel.ts +++ b/packages/connect/src/ws_orderbook_channel.ts @@ -51,6 +51,7 @@ export class WebSocketOrderbookChannel implements OrderbookChannel { public subscribe(subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler): void { assert.isOrderbookChannelSubscriptionOpts('subscriptionOpts', subscriptionOpts); assert.isOrderbookChannelHandler('handler', handler); + assert.assert(this._client.readyState === WebSocket.w3cwebsocket.OPEN, 'WebSocket connection is closed'); const newSubscription: Subscription = { subscriptionOpts, handler, @@ -62,7 +63,7 @@ export class WebSocketOrderbookChannel implements OrderbookChannel { requestId: this._subscriptions.length - 1, payload: subscriptionOpts, }; - this._sendMessage(subscribeMessage); + this._client.send(JSON.stringify(subscribeMessage)); } /** * Close the websocket and stop receiving updates @@ -70,14 +71,6 @@ export class WebSocketOrderbookChannel implements OrderbookChannel { public close(): void { this._client.close(); } - /** - * Send a message to the client if it has been instantiated and it is open - */ - private _sendMessage(message: any): void { - if (this._client.readyState === WebSocket.w3cwebsocket.OPEN) { - this._client.send(JSON.stringify(message)); - } - } /** * For use in cases where we need to alert all handlers of an error */ -- cgit v1.2.3 From 3e7ee1f0900f7d365ce73f62c353c7d653a35861 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 29 May 2018 11:16:10 -0700 Subject: Add explicit HACK comment when we import assert --- packages/connect/src/utils/assert.ts | 2 +- packages/contract-wrappers/src/utils/assert.ts | 2 +- packages/order-utils/src/assert.ts | 4 ++-- packages/order-watcher/src/utils/assert.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) (limited to 'packages') diff --git a/packages/connect/src/utils/assert.ts b/packages/connect/src/utils/assert.ts index f8241aacb..b5d5283bd 100644 --- a/packages/connect/src/utils/assert.ts +++ b/packages/connect/src/utils/assert.ts @@ -1,5 +1,5 @@ import { assert as sharedAssert } from '@0xproject/assert'; -// We need those two unused imports because they're actually used by sharedAssert which gets injected here +// HACK: 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 diff --git a/packages/contract-wrappers/src/utils/assert.ts b/packages/contract-wrappers/src/utils/assert.ts index da6697b08..842b16fa0 100644 --- a/packages/contract-wrappers/src/utils/assert.ts +++ b/packages/contract-wrappers/src/utils/assert.ts @@ -1,5 +1,5 @@ import { assert as sharedAssert } from '@0xproject/assert'; -// We need those two unused imports because they're actually used by sharedAssert which gets injected here +// HACK: We need those two unused imports because they're actually used by sharedAssert which gets injected here import { Schema } from '@0xproject/json-schemas'; // tslint:disable-line:no-unused-variable import { isValidSignatureAsync } from '@0xproject/order-utils'; import { ECSignature } from '@0xproject/types'; // tslint:disable-line:no-unused-variable diff --git a/packages/order-utils/src/assert.ts b/packages/order-utils/src/assert.ts index b4b57d02a..15b4a024f 100644 --- a/packages/order-utils/src/assert.ts +++ b/packages/order-utils/src/assert.ts @@ -1,6 +1,6 @@ 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:no-unused-variable +// HACK: 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 } from '@0xproject/json-schemas'; import { ECSignature, SignatureType } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; diff --git a/packages/order-watcher/src/utils/assert.ts b/packages/order-watcher/src/utils/assert.ts index 5d7f72716..fc85de958 100644 --- a/packages/order-watcher/src/utils/assert.ts +++ b/packages/order-watcher/src/utils/assert.ts @@ -1,6 +1,6 @@ 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:no-unused-variable +// HACK: 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 } from '@0xproject/json-schemas'; import { ECSignature } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; -- cgit v1.2.3 From 6ecda647ad8f1fc9620098c0bd7eb87defc8829f Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 29 May 2018 11:20:01 -0700 Subject: Add TODO comment for switching requestIds to strings --- packages/connect/src/ws_orderbook_channel.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'packages') diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts index 45d1b35d7..ed31bc833 100644 --- a/packages/connect/src/ws_orderbook_channel.ts +++ b/packages/connect/src/ws_orderbook_channel.ts @@ -57,6 +57,7 @@ export class WebSocketOrderbookChannel implements OrderbookChannel { handler, }; this._subscriptions.push(newSubscription); + // TODO: update requestId management to use UUIDs for v2 const subscribeMessage = { type: 'subscribe', channel: 'orderbook', -- cgit v1.2.3 From c500cc095f5c5adb6da3aaf85ad245531531f215 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 29 May 2018 11:30:36 -0700 Subject: Enforce one handler per channel --- packages/connect/src/types.ts | 4 +- packages/connect/src/ws_orderbook_channel.ts | 70 +++++++++------------------- 2 files changed, 25 insertions(+), 49 deletions(-) (limited to 'packages') diff --git a/packages/connect/src/types.ts b/packages/connect/src/types.ts index 5ea114371..d7d7a96d0 100644 --- a/packages/connect/src/types.ts +++ b/packages/connect/src/types.ts @@ -39,8 +39,8 @@ export interface OrderbookChannelHandler { subscriptionOpts: OrderbookChannelSubscriptionOpts, order: SignedOrder, ) => void; - onError: (channel: OrderbookChannel, subscriptionOpts: OrderbookChannelSubscriptionOpts, err: Error) => void; - onClose: (channel: OrderbookChannel, subscriptionOpts: OrderbookChannelSubscriptionOpts) => void; + onError: (channel: OrderbookChannel, err: Error) => void; + onClose: (channel: OrderbookChannel) => void; } export type OrderbookChannelMessage = diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts index ed31bc833..01aaf4fd2 100644 --- a/packages/connect/src/ws_orderbook_channel.ts +++ b/packages/connect/src/ws_orderbook_channel.ts @@ -12,30 +12,32 @@ import { 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 */ export class WebSocketOrderbookChannel implements OrderbookChannel { private _client: WebSocket.w3cwebsocket; - private _subscriptions: Subscription[] = []; + private _handler: OrderbookChannelHandler; + private _subscriptionOptsList: OrderbookChannelSubscriptionOpts[] = []; /** * Instantiates a new WebSocketOrderbookChannel instance - * @param url The relayer API base WS url you would like to interact with + * @param client A WebSocket client + * @param handler An OrderbookChannelHandler instance that responds to various + * channel updates * @return An instance of WebSocketOrderbookChannel */ - constructor(client: WebSocket.w3cwebsocket) { + constructor(client: WebSocket.w3cwebsocket, handler: OrderbookChannelHandler) { + assert.isOrderbookChannelHandler('handler', handler); + // set private members this._client = client; + this._handler = handler; + // attach client callbacks this._client.onerror = err => { - this._alertAllHandlersToError(err); + this._handler.onError(this, err); }; this._client.onclose = () => { - this._alertAllHandlersToClose(); + this._handler.onClose(this); }; this._client.onmessage = message => { this._handleWebSocketMessage(message); @@ -45,23 +47,16 @@ export class WebSocketOrderbookChannel implements OrderbookChannel { * 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 { + public subscribe(subscriptionOpts: OrderbookChannelSubscriptionOpts): void { assert.isOrderbookChannelSubscriptionOpts('subscriptionOpts', subscriptionOpts); - assert.isOrderbookChannelHandler('handler', handler); assert.assert(this._client.readyState === WebSocket.w3cwebsocket.OPEN, 'WebSocket connection is closed'); - const newSubscription: Subscription = { - subscriptionOpts, - handler, - }; - this._subscriptions.push(newSubscription); + this._subscriptionOptsList.push(subscriptionOpts); // TODO: update requestId management to use UUIDs for v2 const subscribeMessage = { type: 'subscribe', channel: 'orderbook', - requestId: this._subscriptions.length - 1, + requestId: this._subscriptionOptsList.length - 1, payload: subscriptionOpts, }; this._client.send(JSON.stringify(subscribeMessage)); @@ -72,55 +67,36 @@ export class WebSocketOrderbookChannel implements OrderbookChannel { public close(): void { this._client.close(); } - /** - * 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 _alertAllHandlersToClose(): void { - _.forEach(this._subscriptions, subscription => { - subscription.handler.onClose(this, subscription.subscriptionOpts); - }); - } 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`)); + this._handler.onError(this, 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}`)); + const subscriptionOpts = this._subscriptionOptsList[parserResult.requestId]; + if (_.isUndefined(subscriptionOpts)) { + this._handler.onError(this, 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); + this._handler.onSnapshot(this, subscriptionOpts, parserResult.payload); break; } case OrderbookChannelMessageTypes.Update: { - handler.onUpdate(this, subscriptionOpts, parserResult.payload); + this._handler.onUpdate(this, subscriptionOpts, parserResult.payload); break; } default: { - handler.onError( - this, - subscriptionOpts, - new Error(`Message has unknown type parameter: ${utf8Data}`), - ); + this._handler.onError(this, new Error(`Message has unknown type parameter: ${utf8Data}`)); } } } catch (error) { - this._alertAllHandlersToError(error); + this._handler.onError(this, error); } } } -- cgit v1.2.3 From c403dcdabf4b98ea0ecde1ab0e7e9dafa97d797f Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 29 May 2018 11:36:19 -0700 Subject: Update tests --- packages/connect/test/ws_orderbook_channel_test.ts | 36 +++++++++------------- 1 file changed, 15 insertions(+), 21 deletions(-) (limited to 'packages') diff --git a/packages/connect/test/ws_orderbook_channel_test.ts b/packages/connect/test/ws_orderbook_channel_test.ts index 79100d0e2..f4ad67ba3 100644 --- a/packages/connect/test/ws_orderbook_channel_test.ts +++ b/packages/connect/test/ws_orderbook_channel_test.ts @@ -9,31 +9,31 @@ import { WebSocketOrderbookChannel } from '../src/ws_orderbook_channel'; chai.config.includeStack = true; chai.use(dirtyChai); const expect = chai.expect; +const emptyOrderbookChannelHandler = { + onSnapshot: () => { + _.noop(); + }, + onUpdate: () => { + _.noop(); + }, + onError: () => { + _.noop(); + }, + onClose: () => { + _.noop(); + }, +}; describe('WebSocketOrderbookChannel', () => { const websocketUrl = 'ws://localhost:8080'; const client = new WebSocket.w3cwebsocket(websocketUrl); - const orderbookChannel = new WebSocketOrderbookChannel(client); + const orderbookChannel = new WebSocketOrderbookChannel(client, emptyOrderbookChannelHandler); 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( @@ -45,12 +45,6 @@ describe('WebSocketOrderbookChannel', () => { '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, -- cgit v1.2.3 From af395eccda95f12ba14f40f9261443d24e48f455 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 29 May 2018 12:54:53 -0700 Subject: Update orderbook channel and factory tests --- packages/connect/package.json | 2 ++ packages/connect/src/orderbook_channel_factory.ts | 14 ++++++--- packages/connect/src/types.ts | 2 +- .../connect/test/orderbook_channel_factory_test.ts | 34 +++++++++++++++++----- packages/connect/test/ws_orderbook_channel_test.ts | 26 +++++++++-------- 5 files changed, 54 insertions(+), 24 deletions(-) (limited to 'packages') diff --git a/packages/connect/package.json b/packages/connect/package.json index 78cb3e71d..cc68d34f4 100644 --- a/packages/connect/package.json +++ b/packages/connect/package.json @@ -59,6 +59,7 @@ "isomorphic-fetch": "^2.2.1", "lodash": "^4.17.4", "query-string": "^5.0.1", + "sinon": "^4.0.0", "websocket": "^1.0.25" }, "devDependencies": { @@ -68,6 +69,7 @@ "@types/lodash": "4.14.104", "@types/mocha": "^2.2.42", "@types/query-string": "^5.0.1", + "@types/sinon": "^2.2.2", "@types/websocket": "^0.0.39", "async-child-process": "^1.1.1", "chai": "^4.0.1", diff --git a/packages/connect/src/orderbook_channel_factory.ts b/packages/connect/src/orderbook_channel_factory.ts index 1b5625840..5134af323 100644 --- a/packages/connect/src/orderbook_channel_factory.ts +++ b/packages/connect/src/orderbook_channel_factory.ts @@ -1,21 +1,27 @@ import * as WebSocket from 'websocket'; -import { OrderbookChannel } from './types'; +import { OrderbookChannel, OrderbookChannelHandler } from './types'; import { assert } from './utils/assert'; import { WebSocketOrderbookChannel } from './ws_orderbook_channel'; export const orderbookChannelFactory = { /** * Instantiates a new WebSocketOrderbookChannel instance - * @param url The relayer API base WS url you would like to interact with + * @param url The relayer API base WS url you would like to interact with + * @param handler An OrderbookChannelHandler instance that responds to various + * channel updates * @return An OrderbookChannel Promise */ - async createWebSocketOrderbookChannelAsync(url: string): Promise { + async createWebSocketOrderbookChannelAsync( + url: string, + handler: OrderbookChannelHandler, + ): Promise { assert.isUri('url', url); + assert.isOrderbookChannelHandler('handler', handler); return new Promise((resolve, reject) => { const client = new WebSocket.w3cwebsocket(url); client.onopen = () => { - const orderbookChannel = new WebSocketOrderbookChannel(client); + const orderbookChannel = new WebSocketOrderbookChannel(client, handler); resolve(orderbookChannel); }; client.onerror = err => { diff --git a/packages/connect/src/types.ts b/packages/connect/src/types.ts index d7d7a96d0..7347beb0b 100644 --- a/packages/connect/src/types.ts +++ b/packages/connect/src/types.ts @@ -11,7 +11,7 @@ export interface Client { } export interface OrderbookChannel { - subscribe: (subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler) => void; + subscribe: (subscriptionOpts: OrderbookChannelSubscriptionOpts) => void; close: () => void; } diff --git a/packages/connect/test/orderbook_channel_factory_test.ts b/packages/connect/test/orderbook_channel_factory_test.ts index fd84332cc..d2140bfa6 100644 --- a/packages/connect/test/orderbook_channel_factory_test.ts +++ b/packages/connect/test/orderbook_channel_factory_test.ts @@ -9,18 +9,38 @@ import { orderbookChannelFactory } from '../src/orderbook_channel_factory'; chai.config.includeStack = true; chai.use(dirtyChai); const expect = chai.expect; +const emptyOrderbookChannelHandler = { + onSnapshot: () => { + _.noop(); + }, + onUpdate: () => { + _.noop(); + }, + onError: () => { + _.noop(); + }, + onClose: () => { + _.noop(); + }, +}; describe('orderbookChannelFactory', () => { const websocketUrl = 'ws://localhost:8080'; - describe('#createWebSocketOrderbookChannelAsync', () => { it('throws when input is not a url', () => { - const badInput = 54; - const badSubscribeCall = orderbookChannelFactory.createWebSocketOrderbookChannelAsync.bind( - orderbookChannelFactory, - badInput, - ); - expect(orderbookChannelFactory.createWebSocketOrderbookChannelAsync(badInput as any)).to.be.rejected(); + const badUrlInput = 54; + expect( + orderbookChannelFactory.createWebSocketOrderbookChannelAsync( + badUrlInput as any, + emptyOrderbookChannelHandler, + ), + ).to.be.rejected(); + }); + it('throws when handler has the incorrect members', () => { + const badHandlerInput = {}; + expect( + orderbookChannelFactory.createWebSocketOrderbookChannelAsync(websocketUrl, badHandlerInput as any), + ).to.be.rejected(); }); }); }); diff --git a/packages/connect/test/ws_orderbook_channel_test.ts b/packages/connect/test/ws_orderbook_channel_test.ts index f4ad67ba3..fed4f2217 100644 --- a/packages/connect/test/ws_orderbook_channel_test.ts +++ b/packages/connect/test/ws_orderbook_channel_test.ts @@ -2,6 +2,7 @@ import * as chai from 'chai'; import * as dirtyChai from 'dirty-chai'; import * as _ from 'lodash'; import 'mocha'; +import * as Sinon from 'sinon'; import * as WebSocket from 'websocket'; import { WebSocketOrderbookChannel } from '../src/ws_orderbook_channel'; @@ -26,8 +27,10 @@ const emptyOrderbookChannelHandler = { describe('WebSocketOrderbookChannel', () => { const websocketUrl = 'ws://localhost:8080'; - const client = new WebSocket.w3cwebsocket(websocketUrl); - const orderbookChannel = new WebSocketOrderbookChannel(client, emptyOrderbookChannelHandler); + const openClient = new WebSocket.w3cwebsocket(websocketUrl); + Sinon.stub(openClient, 'readyState').get(() => WebSocket.w3cwebsocket.OPEN); + Sinon.stub(openClient, 'send').callsFake(_.noop); + const openOrderbookChannel = new WebSocketOrderbookChannel(openClient, emptyOrderbookChannelHandler); const subscriptionOpts = { baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', quoteTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', @@ -36,22 +39,21 @@ describe('WebSocketOrderbookChannel', () => { }; describe('#subscribe', () => { it('throws when subscriptionOpts does not conform to schema', () => { - const badSubscribeCall = orderbookChannel.subscribe.bind( - orderbookChannel, - {}, - emptyOrderbookChannelHandler, - ); + const badSubscribeCall = openOrderbookChannel.subscribe.bind(openOrderbookChannel, {}); expect(badSubscribeCall).throws( 'Expected subscriptionOpts to conform to schema /RelayerApiOrderbookChannelSubscribePayload\nEncountered: {}\nValidation errors: instance requires property "baseTokenAddress", instance requires property "quoteTokenAddress"', ); }); it('does not throw when inputs are of correct types', () => { - const goodSubscribeCall = orderbookChannel.subscribe.bind( - orderbookChannel, - subscriptionOpts, - emptyOrderbookChannelHandler, - ); + const goodSubscribeCall = openOrderbookChannel.subscribe.bind(openOrderbookChannel, subscriptionOpts); expect(goodSubscribeCall).to.not.throw(); }); + it('throws when client is closed', () => { + const closedClient = new WebSocket.w3cwebsocket(websocketUrl); + Sinon.stub(closedClient, 'readyState').get(() => WebSocket.w3cwebsocket.CLOSED); + const closedOrderbookChannel = new WebSocketOrderbookChannel(closedClient, emptyOrderbookChannelHandler); + const badSubscribeCall = closedOrderbookChannel.subscribe.bind(closedOrderbookChannel, subscriptionOpts); + expect(badSubscribeCall).throws('WebSocket connection is closed'); + }); }); }); -- cgit v1.2.3 From e12f7c3026dd513a6444092ea7654598c71548ae Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 29 May 2018 12:59:09 -0700 Subject: Remove outdated comments --- packages/connect/src/ws_orderbook_channel.ts | 2 -- 1 file changed, 2 deletions(-) (limited to 'packages') diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts index 01aaf4fd2..e6a1322d2 100644 --- a/packages/connect/src/ws_orderbook_channel.ts +++ b/packages/connect/src/ws_orderbook_channel.ts @@ -68,12 +68,10 @@ export class WebSocketOrderbookChannel implements OrderbookChannel { this._client.close(); } private _handleWebSocketMessage(message: any): void { - // if we get a message with no data, alert all handlers and return if (_.isUndefined(message.data)) { this._handler.onError(this, 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); -- cgit v1.2.3 From 17c34716f99616066d3a4cec3256cdc4d0b96667 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 11 Jul 2018 11:16:45 -0700 Subject: Provide subscriptionOpts in error callback and include url in error messages --- packages/connect/src/types.ts | 2 +- packages/connect/src/ws_orderbook_channel.ts | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) (limited to 'packages') diff --git a/packages/connect/src/types.ts b/packages/connect/src/types.ts index 7347beb0b..fc7a4b24d 100644 --- a/packages/connect/src/types.ts +++ b/packages/connect/src/types.ts @@ -39,7 +39,7 @@ export interface OrderbookChannelHandler { subscriptionOpts: OrderbookChannelSubscriptionOpts, order: SignedOrder, ) => void; - onError: (channel: OrderbookChannel, err: Error) => void; + onError: (channel: OrderbookChannel, err: Error, subscriptionOpts?: OrderbookChannelSubscriptionOpts) => void; onClose: (channel: OrderbookChannel) => void; } diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts index e6a1322d2..e5d31607a 100644 --- a/packages/connect/src/ws_orderbook_channel.ts +++ b/packages/connect/src/ws_orderbook_channel.ts @@ -69,15 +69,18 @@ export class WebSocketOrderbookChannel implements OrderbookChannel { } private _handleWebSocketMessage(message: any): void { if (_.isUndefined(message.data)) { - this._handler.onError(this, new Error(`Message does not contain utf8Data`)); + this._handler.onError(this, new Error(`Message does not contain data. Url: ${this._client.url}`)); return; } try { - const utf8Data = message.data; - const parserResult = orderbookChannelMessageParser.parse(utf8Data); + const data = message.data; + const parserResult = orderbookChannelMessageParser.parse(data); const subscriptionOpts = this._subscriptionOptsList[parserResult.requestId]; if (_.isUndefined(subscriptionOpts)) { - this._handler.onError(this, new Error(`Message has unknown requestId: ${utf8Data}`)); + this._handler.onError( + this, + new Error(`Message has unknown requestId. Url: ${this._client.url} Message: ${data}`), + ); return; } switch (parserResult.type) { @@ -90,7 +93,11 @@ export class WebSocketOrderbookChannel implements OrderbookChannel { break; } default: { - this._handler.onError(this, new Error(`Message has unknown type parameter: ${utf8Data}`)); + this._handler.onError( + this, + new Error(`Message has unknown type parameter. Url: ${this._client.url} Message: ${data}`), + subscriptionOpts, + ); } } } catch (error) { -- cgit v1.2.3 From e67d67419f893782fc379a9554580896b4f0cdab Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 11 Jul 2018 11:29:34 -0700 Subject: Prettier --- packages/connect/CHANGELOG.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'packages') diff --git a/packages/connect/CHANGELOG.json b/packages/connect/CHANGELOG.json index 562b9e9e6..10019a882 100644 --- a/packages/connect/CHANGELOG.json +++ b/packages/connect/CHANGELOG.json @@ -3,7 +3,8 @@ "version": "0.7.0", "changes": [ { - "note": "Remove WebSocketOrderbookChannel from the public interface and replace with orderbookChannelFactory" + "note": + "Remove WebSocketOrderbookChannel from the public interface and replace with orderbookChannelFactory" } ] }, -- cgit v1.2.3 From e5617dfe615da42d794a43924cb1fa9240af1f0d Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 11 Jul 2018 11:46:28 -0700 Subject: Lint fixes --- packages/connect/src/utils/assert.ts | 3 ++- packages/connect/src/ws_orderbook_channel.ts | 2 -- packages/connect/test/orderbook_channel_factory_test.ts | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) (limited to 'packages') diff --git a/packages/connect/src/utils/assert.ts b/packages/connect/src/utils/assert.ts index b5d5283bd..a0fd12fbd 100644 --- a/packages/connect/src/utils/assert.ts +++ b/packages/connect/src/utils/assert.ts @@ -4,6 +4,7 @@ import { assert as sharedAssert } from '@0xproject/assert'; import { Schema, schemas } from '@0xproject/json-schemas'; // tslint:disable-next-line:no-unused-variable import { ECSignature } from '@0xproject/types'; +// tslint:disable-next-line:no-unused-variable import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; @@ -11,7 +12,7 @@ export const assert = { ...sharedAssert, isOrderbookChannelSubscriptionOpts(variableName: string, subscriptionOpts: any): void { sharedAssert.doesConformToSchema( - 'subscriptionOpts', + variableName, subscriptionOpts, schemas.relayerApiOrderbookChannelSubscribePayload, ); diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts index e5d31607a..e1c55cce3 100644 --- a/packages/connect/src/ws_orderbook_channel.ts +++ b/packages/connect/src/ws_orderbook_channel.ts @@ -6,8 +6,6 @@ import { OrderbookChannelHandler, OrderbookChannelMessageTypes, OrderbookChannelSubscriptionOpts, - WebsocketClientEventType, - WebsocketConnectionEventType, } from './types'; import { assert } from './utils/assert'; import { orderbookChannelMessageParser } from './utils/orderbook_channel_message_parser'; diff --git a/packages/connect/test/orderbook_channel_factory_test.ts b/packages/connect/test/orderbook_channel_factory_test.ts index d2140bfa6..2ce361bd2 100644 --- a/packages/connect/test/orderbook_channel_factory_test.ts +++ b/packages/connect/test/orderbook_channel_factory_test.ts @@ -2,7 +2,6 @@ import * as chai from 'chai'; import * as dirtyChai from 'dirty-chai'; import * as _ from 'lodash'; import 'mocha'; -import * as WebSocket from 'websocket'; import { orderbookChannelFactory } from '../src/orderbook_channel_factory'; -- cgit v1.2.3 From 33f92b6bcf0f5ee64864b9b412eeebb530a2d48d Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 11 Jul 2018 13:15:28 -0700 Subject: Fix lint for order-utils --- packages/order-utils/src/assert.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages') diff --git a/packages/order-utils/src/assert.ts b/packages/order-utils/src/assert.ts index 15b4a024f..f8db7ac63 100644 --- a/packages/order-utils/src/assert.ts +++ b/packages/order-utils/src/assert.ts @@ -1,6 +1,6 @@ import { assert as sharedAssert } from '@0xproject/assert'; // HACK: We need those two unused imports because they're actually used by sharedAssert which gets injected here -// tslint:disable-next-line:no-unused-variable +// tslint:disable:no-unused-variable import { Schema } from '@0xproject/json-schemas'; import { ECSignature, SignatureType } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; -- cgit v1.2.3 From ed3aeb7997196225dda060e9261b93d300a7d0ab Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 11 Jul 2018 13:35:21 -0700 Subject: Fix lint for order watcher --- packages/order-watcher/src/utils/assert.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages') diff --git a/packages/order-watcher/src/utils/assert.ts b/packages/order-watcher/src/utils/assert.ts index fc85de958..9c992d9b4 100644 --- a/packages/order-watcher/src/utils/assert.ts +++ b/packages/order-watcher/src/utils/assert.ts @@ -1,6 +1,6 @@ import { assert as sharedAssert } from '@0xproject/assert'; // HACK: We need those two unused imports because they're actually used by sharedAssert which gets injected here -// tslint:disable-next-line:no-unused-variable +// tslint:disable:no-unused-variable import { Schema } from '@0xproject/json-schemas'; import { ECSignature } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; -- cgit v1.2.3 From 6190ac7791cec92b6f4be0735a5914ce2d418ab5 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Wed, 11 Jul 2018 14:31:39 -0700 Subject: Change version to 1.0.0 --- packages/connect/CHANGELOG.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages') diff --git a/packages/connect/CHANGELOG.json b/packages/connect/CHANGELOG.json index 10019a882..7b747f167 100644 --- a/packages/connect/CHANGELOG.json +++ b/packages/connect/CHANGELOG.json @@ -1,6 +1,6 @@ [ { - "version": "0.7.0", + "version": "1.0.0", "changes": [ { "note": -- cgit v1.2.3