Update websocket for SRA v2

This commit is contained in:
fragosti 2018-08-20 11:42:29 -07:00
parent f2d1d95355
commit 075e3a41c8
12 changed files with 71 additions and 74 deletions

View File

@ -1,4 +1,12 @@
[ [
{
"version": "2.0.0",
"changes": [
{
"note": "Updated for SRA v2"
}
]
},
{ {
"timestamp": 1534210131, "timestamp": 1534210131,
"version": "1.0.5", "version": "1.0.5",

View File

@ -31,7 +31,7 @@ export interface OrdersChannelHandler {
onUpdate: ( onUpdate: (
channel: OrdersChannel, channel: OrdersChannel,
subscriptionOpts: OrdersChannelSubscriptionOpts, subscriptionOpts: OrdersChannelSubscriptionOpts,
order: APIOrder, orders: APIOrder[],
) => void; ) => void;
onError: (channel: OrdersChannel, err: Error, subscriptionOpts?: OrdersChannelSubscriptionOpts) => void; onError: (channel: OrdersChannel, err: Error, subscriptionOpts?: OrdersChannelSubscriptionOpts) => void;
onClose: (channel: OrdersChannel) => void; onClose: (channel: OrdersChannel) => void;
@ -48,13 +48,13 @@ export enum OrdersChannelMessageTypes {
export interface UpdateOrdersChannelMessage { export interface UpdateOrdersChannelMessage {
type: OrdersChannelMessageTypes.Update; type: OrdersChannelMessageTypes.Update;
requestId: number; requestId: string;
payload: APIOrder; payload: APIOrder[];
} }
export interface UnknownOrdersChannelMessage { export interface UnknownOrdersChannelMessage {
type: OrdersChannelMessageTypes.Unknown; type: OrdersChannelMessageTypes.Unknown;
requestId: number; requestId: string;
payload: undefined; payload: undefined;
} }

View File

@ -15,15 +15,15 @@ export const ordersChannelMessageParser = {
assert.assert(!_.isUndefined(type), `Message is missing a type parameter: ${utf8Data}`); assert.assert(!_.isUndefined(type), `Message is missing a type parameter: ${utf8Data}`);
assert.isString('type', type); assert.isString('type', type);
// ensure we have a request id for the resulting message // ensure we have a request id for the resulting message
const requestId: number = _.get(messageObj, 'requestId'); const requestId: string = _.get(messageObj, 'requestId');
assert.assert(!_.isUndefined(requestId), `Message is missing a requestId parameter: ${utf8Data}`); assert.assert(!_.isUndefined(requestId), `Message is missing a requestId parameter: ${utf8Data}`);
assert.isNumber('requestId', requestId); assert.isString('requestId', requestId);
switch (type) { switch (type) {
case OrdersChannelMessageTypes.Update: { case OrdersChannelMessageTypes.Update: {
assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrdersChannelUpdateSchema); assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrdersChannelUpdateSchema);
const orderJson = messageObj.payload; const ordersJson = messageObj.payload;
const order = relayerResponseJsonParsers.parseAPIOrderJson(orderJson); const orders = relayerResponseJsonParsers.parseAPIOrdersJson(ordersJson);
return _.assign(messageObj, { payload: order }); return _.assign(messageObj, { payload: orders });
} }
default: { default: {
return { return {

View File

@ -22,7 +22,10 @@ export const relayerResponseJsonParsers = {
}, },
parseOrdersJson(json: any): OrdersResponse { parseOrdersJson(json: any): OrdersResponse {
assert.doesConformToSchema('relayerApiOrdersResponse', json, schemas.relayerApiOrdersResponseSchema); assert.doesConformToSchema('relayerApiOrdersResponse', json, schemas.relayerApiOrdersResponseSchema);
return { ...json, records: json.records.map(relayerResponseJsonParsers.parseAPIOrderJson.bind(relayerResponseJsonParsers)) }; return { ...json, records: relayerResponseJsonParsers.parseAPIOrdersJson(json.records) };
},
parseAPIOrdersJson(json: any): APIOrder[] {
return json.map(relayerResponseJsonParsers.parseAPIOrderJson.bind(relayerResponseJsonParsers));
}, },
parseAPIOrderJson(json: any): APIOrder { parseAPIOrderJson(json: any): APIOrder {
assert.doesConformToSchema('relayerApiOrder', json, schemas.relayerApiOrderSchema); assert.doesConformToSchema('relayerApiOrder', json, schemas.relayerApiOrderSchema);

View File

@ -9,7 +9,11 @@ import {
OrdersChannelSubscriptionOpts, OrdersChannelSubscriptionOpts,
} from './types'; } from './types';
import { assert } from './utils/assert'; import { assert } from './utils/assert';
import { ordersChannelMessageParser } from './utils/orderbook_channel_message_parser'; import { ordersChannelMessageParser } from './utils/orders_channel_message_parser';
export interface OrdersChannelSubscriptionOptsMap {
[key: string]: OrdersChannelSubscriptionOpts;
}
/** /**
* This class includes all the functionality related to interacting with a websocket endpoint * This class includes all the functionality related to interacting with a websocket endpoint
@ -18,7 +22,7 @@ import { ordersChannelMessageParser } from './utils/orderbook_channel_message_pa
export class WebSocketOrdersChannel implements OrdersChannel { export class WebSocketOrdersChannel implements OrdersChannel {
private readonly _client: WebSocket.w3cwebsocket; private readonly _client: WebSocket.w3cwebsocket;
private readonly _handler: OrdersChannelHandler; private readonly _handler: OrdersChannelHandler;
private readonly _subscriptionOptsList: OrdersChannelSubscriptionOpts[] = []; private readonly _subscriptionOptsMap: OrdersChannelSubscriptionOptsMap = {};
/** /**
* Instantiates a new WebSocketOrdersChannel instance * Instantiates a new WebSocketOrdersChannel instance
* @param client A WebSocket client * @param client A WebSocket client
@ -50,11 +54,12 @@ export class WebSocketOrdersChannel implements OrdersChannel {
public subscribe(subscriptionOpts: OrdersChannelSubscriptionOpts): void { public subscribe(subscriptionOpts: OrdersChannelSubscriptionOpts): void {
assert.isOrdersChannelSubscriptionOpts('subscriptionOpts', subscriptionOpts); assert.isOrdersChannelSubscriptionOpts('subscriptionOpts', subscriptionOpts);
assert.assert(this._client.readyState === WebSocket.w3cwebsocket.OPEN, 'WebSocket connection is closed'); assert.assert(this._client.readyState === WebSocket.w3cwebsocket.OPEN, 'WebSocket connection is closed');
this._subscriptionOptsList.push(subscriptionOpts); const requestId = uuid();
this._subscriptionOptsMap[requestId] = subscriptionOpts;
const subscribeMessage = { const subscribeMessage = {
type: 'subscribe', type: 'subscribe',
channel: 'orders', channel: 'orders',
requestId: uuid(), requestId,
payload: subscriptionOpts, payload: subscriptionOpts,
}; };
this._client.send(JSON.stringify(subscribeMessage)); this._client.send(JSON.stringify(subscribeMessage));
@ -73,7 +78,7 @@ export class WebSocketOrdersChannel implements OrdersChannel {
try { try {
const data = message.data; const data = message.data;
const parserResult = ordersChannelMessageParser.parse(data); const parserResult = ordersChannelMessageParser.parse(data);
const subscriptionOpts = this._subscriptionOptsList[parserResult.requestId]; const subscriptionOpts = this._subscriptionOptsMap[parserResult.requestId];
if (_.isUndefined(subscriptionOpts)) { if (_.isUndefined(subscriptionOpts)) {
this._handler.onError( this._handler.onError(
this, this,

View File

@ -5,6 +5,6 @@ const orderJSONString = JSON.stringify(orderResponseJSON);
export const unknownOrdersChannelMessage = `{ export const unknownOrdersChannelMessage = `{
"type": "superGoodUpdate", "type": "superGoodUpdate",
"channel": "orderbook", "channel": "orderbook",
"requestId": 1, "requestId": "6ce8c5a6-5c46-4027-a44a-51831c77b8a1",
"payload": ${orderJSONString} "payload": [${orderJSONString}]
}`; }`;

View File

@ -1,17 +0,0 @@
import * as orderResponseJSON from './order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json';
const orderJSONString = JSON.stringify(orderResponseJSON);
export const updateOrdersChannelMessage = `{
"type": "update",
"channel": "orderbook",
"requestId": 1,
"payload": ${orderJSONString}
}`;
export const malformedUpdateOrdersChannelMessage = `{
"type": "update",
"channel": "orderbook",
"requestId": 1,
"payload": {}
}`;

View File

@ -0,0 +1,17 @@
import * as apiOrderJSON from './order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json';
const apiOrderJSONString = JSON.stringify(apiOrderJSON);
export const updateOrdersChannelMessage = `{
"type": "update",
"channel": "orders",
"requestId": "5a1ce3a2-22b9-41e6-a615-68077512e9e2",
"payload": [${apiOrderJSONString}]
}`;
export const malformedUpdateOrdersChannelMessage = `{
"type": "update",
"channel": "orders",
"requestId": "4d8efcee-adde-4475-9601-f0b30962ca2b",
"payload": {}
}`;

View File

@ -191,6 +191,3 @@ describe('HttpClient', () => {
}); });
}); });
}); });
// https://example.com/fee_recipients?networkId=42&page=3&perPage=50
// https://example.com/fee_recipients?networkId=42&page=3&perPage=50

View File

@ -9,15 +9,9 @@ chai.config.includeStack = true;
chai.use(dirtyChai); chai.use(dirtyChai);
const expect = chai.expect; const expect = chai.expect;
const emptyOrdersChannelHandler = { const emptyOrdersChannelHandler = {
onUpdate: () => { onUpdate: _.noop,
_.noop(); onError: _.noop,
}, onClose: _.noop,
onError: () => {
_.noop();
},
onClose: () => {
_.noop();
},
}; };
describe('ordersChannelFactory', () => { describe('ordersChannelFactory', () => {

View File

@ -2,14 +2,14 @@ import * as chai from 'chai';
import * as dirtyChai from 'dirty-chai'; import * as dirtyChai from 'dirty-chai';
import 'mocha'; import 'mocha';
import { ordersChannelMessageParser } from '../src/utils/orderbook_channel_message_parser'; import { ordersChannelMessageParser } from '../src/utils/orders_channel_message_parser';
import { orderResponse } from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f'; import { orderResponse } from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f';
import { unknownOrdersChannelMessage } from './fixtures/standard_relayer_api/unknown_orderbook_channel_message'; import { unknownOrdersChannelMessage } from './fixtures/standard_relayer_api/unknown_orders_channel_message';
import { import {
malformedUpdateOrdersChannelMessage, malformedUpdateOrdersChannelMessage,
updateOrdersChannelMessage, updateOrdersChannelMessage,
} from './fixtures/standard_relayer_api/update_orderbook_channel_message'; } from './fixtures/standard_relayer_api/update_orders_channel_message';
chai.config.includeStack = true; chai.config.includeStack = true;
chai.use(dirtyChai); chai.use(dirtyChai);
@ -20,7 +20,7 @@ describe('ordersChannelMessageParser', () => {
it('parses update messages', () => { it('parses update messages', () => {
const updateMessage = ordersChannelMessageParser.parse(updateOrdersChannelMessage); const updateMessage = ordersChannelMessageParser.parse(updateOrdersChannelMessage);
expect(updateMessage.type).to.be.equal('update'); expect(updateMessage.type).to.be.equal('update');
expect(updateMessage.payload).to.be.deep.equal(orderResponse); expect(updateMessage.payload).to.be.deep.equal([orderResponse]);
}); });
it('returns unknown message for messages with unsupported types', () => { it('returns unknown message for messages with unsupported types', () => {
const unknownMessage = ordersChannelMessageParser.parse(unknownOrdersChannelMessage); const unknownMessage = ordersChannelMessageParser.parse(unknownOrdersChannelMessage);
@ -29,9 +29,9 @@ describe('ordersChannelMessageParser', () => {
}); });
it('throws when message does not include a type', () => { it('throws when message does not include a type', () => {
const typelessMessage = `{ const typelessMessage = `{
"channel": "orderbook", "channel": "orders",
"requestId": 1, "requestId": "4d8efcee-adde-4475-9601-f0b30962ca2b",
"payload": {} "payload": []
}`; }`;
const badCall = () => ordersChannelMessageParser.parse(typelessMessage); const badCall = () => ordersChannelMessageParser.parse(typelessMessage);
expect(badCall).throws(`Message is missing a type parameter: ${typelessMessage}`); expect(badCall).throws(`Message is missing a type parameter: ${typelessMessage}`);
@ -39,9 +39,9 @@ describe('ordersChannelMessageParser', () => {
it('throws when type is not a string', () => { it('throws when type is not a string', () => {
const messageWithBadType = `{ const messageWithBadType = `{
"type": 1, "type": 1,
"channel": "orderbook", "channel": "orders",
"requestId": 1, "requestId": "4d8efcee-adde-4475-9601-f0b30962ca2b",
"payload": {} "payload": []
}`; }`;
const badCall = () => ordersChannelMessageParser.parse(messageWithBadType); const badCall = () => ordersChannelMessageParser.parse(messageWithBadType);
expect(badCall).throws('Expected type to be of type string, encountered: 1'); expect(badCall).throws('Expected type to be of type string, encountered: 1');
@ -53,7 +53,7 @@ describe('ordersChannelMessageParser', () => {
it('throws when input message is not valid JSON', () => { it('throws when input message is not valid JSON', () => {
const nonJsonString = 'h93b{sdfs9fsd f'; const nonJsonString = 'h93b{sdfs9fsd f';
const badCall = () => ordersChannelMessageParser.parse(nonJsonString); const badCall = () => ordersChannelMessageParser.parse(nonJsonString);
expect(badCall).throws('Unexpected assetData h in JSON at position 0'); expect(badCall).throws('Unexpected token h in JSON at position 0');
}); });
}); });
}); });

View File

@ -11,18 +11,9 @@ chai.config.includeStack = true;
chai.use(dirtyChai); chai.use(dirtyChai);
const expect = chai.expect; const expect = chai.expect;
const emptyOrdersChannelHandler = { const emptyOrdersChannelHandler = {
onSnapshot: () => { onUpdate: _.noop,
_.noop(); onError: _.noop,
}, onClose: _.noop,
onUpdate: () => {
_.noop();
},
onError: () => {
_.noop();
},
onClose: () => {
_.noop();
},
}; };
describe('WebSocketOrdersChannel', () => { describe('WebSocketOrdersChannel', () => {
@ -34,15 +25,14 @@ describe('WebSocketOrdersChannel', () => {
const subscriptionOpts = { const subscriptionOpts = {
baseAssetData: '0x323b5d4c32345ced77393b3530b1eed0f346429d', baseAssetData: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
quoteAssetData: '0xef7fff64389b814a946f3e92105513705ca6b990', quoteAssetData: '0xef7fff64389b814a946f3e92105513705ca6b990',
snapshot: true,
limit: 100, limit: 100,
}; };
describe('#subscribe', () => { describe('#subscribe', () => {
it('throws when subscriptionOpts does not conform to schema', () => { it('throws when subscriptionOpts does not conform to schema', () => {
const badSubscribeCall = openOrdersChannel.subscribe.bind(openOrdersChannel, {}); const badSubscribeCall = openOrdersChannel.subscribe.bind(openOrdersChannel, {
expect(badSubscribeCall).throws( makerAssetData: 5,
'Expected subscriptionOpts to conform to schema /RelayerApiOrdersChannelSubscribePayload\nEncountered: {}\nValidation errors: instance requires property "baseAssetData", instance requires property "quoteAssetData"', });
); expect(badSubscribeCall).throws();
}); });
it('does not throw when inputs are of correct types', () => { it('does not throw when inputs are of correct types', () => {
const goodSubscribeCall = openOrdersChannel.subscribe.bind(openOrdersChannel, subscriptionOpts); const goodSubscribeCall = openOrdersChannel.subscribe.bind(openOrdersChannel, subscriptionOpts);