diff --git a/README.md b/README.md index e804678..d206eeb 100644 --- a/README.md +++ b/README.md @@ -18,30 +18,36 @@ const accountPositions = await tastytradeClient.balancesAndPositionsService.getP ### Market Data ```js -import TastytradeClient, { QuoteStreamer } from "@tastytrade-api" +import TastytradeClient, { MarketDataStreamer, MarketDataSubscriptionType } from "@tastytrade-api" const tastytradeClient = new TastytradeClient(baseUrl, accountStreamerUrl) await tastytradeClient.sessionService.login(usernameOrEmail, pasword) -const tokenResponse = await tastytradeClient.AccountsAndCustomersService.getQuoteStreamerTokens() -const quoteStreamer = new QuoteStreamer(tokenResponse.token, `${tokenResponse['websocket-url']}/cometd`) -quoteStreamer.connect() +const tokenResponse = await tastytradeClient.AccountsAndCustomersService.getApiQuoteToken() +const streamer = new MarketDataStreamer() +streamer.connect(tokenResponse['dxlink-url'], tokenResponse.token) -function handleMarketDataReceived(event) { +function handleMarketDataReceived(data) { // Triggers every time market data event occurs - console.log(event) + console.log(data) } + +// Add a listener for incoming market data. Returns a remove() function that removes the listener from the quote streamer +const removeDataListener = streamer.addDataListener(handleMarketDataReceived) + // Subscribe to a single equity quote -quoteStreamer.subscribe('AAPL', handleMarketDataReceived) +streamer.addSubscription('AAPL') +// Optionally specify which market data events you want to subscribe to +streamer.addSubscription('SPY', { subscriptionTypes: [MarketDataSubscriptionType.Quote] }) // Subscribe to a single equity option quote const optionChain = await tastytradeClient.instrumentsService.getOptionChain('AAPL') -quoteStreamer.subscribe(optionChain[0]['streamer-symbol'], handleMarketDataReceived) +streamer.addSubscription(optionChain[0]['streamer-symbol']) ``` ### Account Streamer ```js const TastytradeApi = require("@tastytrade/api") const TastytradeClient = TastytradeApi.default -const { AccountStreamer, QuoteStreamer } = TastytradeApi +const { AccountStreamer } = TastytradeApi const _ = require('lodash') function handleStreamerMessage(json) { diff --git a/examples/components/custom-table.tsx b/examples/components/custom-table.tsx index a90b22b..2d48eb9 100644 --- a/examples/components/custom-table.tsx +++ b/examples/components/custom-table.tsx @@ -7,10 +7,16 @@ interface Props{ } export default function CustomTable(props:Props) { - return ( -
- {props.rows.map(props.renderItem)} + const renderRow = (item: any, index: number) => { +
+ {props.renderItem(item, index)}
+ } + + return ( + <> + {props.rows.map(renderRow)} + ) } diff --git a/examples/components/subscribed-symbol.tsx b/examples/components/subscribed-symbol.tsx index 05bebd7..719d7f2 100644 --- a/examples/components/subscribed-symbol.tsx +++ b/examples/components/subscribed-symbol.tsx @@ -1,8 +1,7 @@ import { useContext, useEffect, useState } from 'react' import _ from 'lodash' import { AppContext } from '../contexts/context' -import Button from './button' -import { EventType } from '@dxfeed/api' +import { MarketDataSubscriptionType } from "tastytrade-api" export default function SubscribedSymbol(props: any) { const [bidPrice, setBidPrice] = useState(NaN) @@ -10,17 +9,24 @@ export default function SubscribedSymbol(props: any) { const appContext = useContext(AppContext) - const handleEvent = (event: any) => { - console.log(event) - if (event.eventType === EventType.Quote) { - setBidPrice(event.bidPrice) - setAskPrice(event.askPrice) - } - } - useEffect(() => { - const unsubscribe = appContext.quoteStreamer!.subscribe(props.symbol, handleEvent) - return unsubscribe + const removeListener = appContext.marketDataStreamer.addDataListener((event: any) => { + const eventData = _.find(event.data, data => + data.eventType === MarketDataSubscriptionType.Quote && + data.eventSymbol === props.symbol + ) + + if (!_.isNil(eventData)) { + setBidPrice(eventData.bidPrice) + setAskPrice(eventData.askPrice) + } + }) + + appContext.marketDataStreamer.addSubscription(props.symbol) + return () => { + appContext.marketDataStreamer.removeSubscription(props.symbol) + removeListener() + } }, []); return ( diff --git a/examples/contexts/context.ts b/examples/contexts/context.ts index c45dc80..3aa581d 100644 --- a/examples/contexts/context.ts +++ b/examples/contexts/context.ts @@ -1,14 +1,14 @@ // src/context/state.ts import { createContext } from 'react'; -import TastytradeClient, { QuoteStreamer } from "tastytrade-api" +import TastytradeClient, { MarketDataStreamer } from "tastytrade-api" import { makeAutoObservable } from 'mobx'; import _ from 'lodash' class TastytradeContext { - static Instance = new TastytradeContext('https://api.tastyworks.com', 'wss://streamer.tastyworks.com'); + static Instance = new TastytradeContext('https://api.cert.tastyworks.com', 'wss://streamer.cert.tastyworks.com'); public tastytradeApi: TastytradeClient public accountNumbers: string[] = [] - public quoteStreamer: QuoteStreamer | null = null + public readonly marketDataStreamer: MarketDataStreamer = new MarketDataStreamer() constructor(baseUrl: string, accountStreamerUrl: string) { makeAutoObservable(this) @@ -16,14 +16,6 @@ class TastytradeContext { makeAutoObservable(this.tastytradeApi.session) } - setupQuoteStreamer(token: string, url: string) { - if (_.isNil(this.quoteStreamer)) { - this.quoteStreamer = new QuoteStreamer(token, url) - } - - return this.quoteStreamer - } - get isLoggedIn() { return this.tastytradeApi.session.isValid } diff --git a/examples/next.config.js b/examples/next.config.js index a843cbe..53e2dc0 100644 --- a/examples/next.config.js +++ b/examples/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, + reactStrictMode: false } module.exports = nextConfig diff --git a/examples/package-lock.json b/examples/package-lock.json index 222b75a..fe8deca 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -37,19 +37,22 @@ }, "..": { "name": "@tastytrade/api", - "version": "0.0.1", + "version": "1.0.0", "license": "MIT", "dependencies": { - "@dxfeed/api": "^1.1.0", "@types/lodash": "^4.14.182", "@types/qs": "^6.9.7", "axios": "^1.3.4", + "isomorphic-ws": "^5.0.0", "lodash": "^4.17.21", - "qs": "^6.11.1" + "qs": "^6.11.1", + "uuid": "^9.0.0", + "ws": "^8.13.0" }, "devDependencies": { "@types/jest": "^29.5.0", "@types/node": "17.0.27", + "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", "dotenv": "^16.0.3", @@ -11433,21 +11436,24 @@ "tastytrade-api": { "version": "file:..", "requires": { - "@dxfeed/api": "^1.1.0", "@types/jest": "^29.5.0", "@types/lodash": "^4.14.182", "@types/node": "17.0.27", "@types/qs": "^6.9.7", + "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", "axios": "^1.3.4", "dotenv": "^16.0.3", "eslint": "^8.14.0", + "isomorphic-ws": "^5.0.0", "jest": "^29.5.0", "lodash": "^4.17.21", "qs": "^6.11.1", "ts-jest": "^29.0.5", - "typescript": "4.6.3" + "typescript": "4.6.3", + "uuid": "^9.0.0", + "ws": "^8.13.0" } }, "test-exclude": { diff --git a/examples/pages/_app.tsx b/examples/pages/_app.tsx index a31d9e9..a196040 100644 --- a/examples/pages/_app.tsx +++ b/examples/pages/_app.tsx @@ -7,7 +7,7 @@ import { useMemo } from 'react' function MyApp({ Component, pageProps }: AppProps) { const context = useMemo( - () => new TastytradeContext('https://api.tastyworks.com', 'wss://streamer.tastyworks.com'), + () => new TastytradeContext('https://api.tastyworks.com', 'wss://streamer.cert.tastyworks.com'), [] ); return ( diff --git a/examples/pages/quote-streamer.tsx b/examples/pages/quote-streamer.tsx index 9d6b876..0c56ce5 100644 --- a/examples/pages/quote-streamer.tsx +++ b/examples/pages/quote-streamer.tsx @@ -13,18 +13,17 @@ const Home: NextPage = () => { const appContext = useContext(AppContext) useEffect(() => { - appContext.tastytradeApi.accountsAndCustomersService.getQuoteStreamerTokens() + appContext.tastytradeApi.accountsAndCustomersService.getApiQuoteToken() .then(response => { - const url = `${response['websocket-url']}/cometd` - const quoteStreamer = appContext.setupQuoteStreamer(response.token, url) - quoteStreamer.connect() + if (appContext.marketDataStreamer.isConnected) { + return + } + appContext.marketDataStreamer.connect(response['dxlink-url'], response.token) setLoading(false) }) return () => { - if (!_.isNil(appContext.quoteStreamer)) { - appContext.quoteStreamer.disconnect() - } + appContext.marketDataStreamer.disconnect() } }, []); @@ -48,7 +47,7 @@ const Home: NextPage = () => { return (

- DxFeed Quotes Demo + DxLink Quotes Demo

Type a symbol into the input and click 'Add Symbol'
diff --git a/lib/market-data-streamer.ts b/lib/market-data-streamer.ts new file mode 100644 index 0000000..404cabf --- /dev/null +++ b/lib/market-data-streamer.ts @@ -0,0 +1,273 @@ +import WebSocket from 'isomorphic-ws' +import _ from 'lodash' +import { v4 as uuidv4 } from 'uuid' + +export enum MarketDataSubscriptionType { + Quote = 'Quote', + Trade = 'Trade', + Summary = 'Summary', + Profile = 'Profile', + Greeks = 'Greeks', + Underlying = 'Underlying' +} + +export type MarketDataListener = (data: any) => void + +type QueuedSubscription = { symbol: string, subscriptionTypes: MarketDataSubscriptionType[] } +type SubscriptionOptions = { subscriptionTypes: MarketDataSubscriptionType[], channelId: number } + +const AllSubscriptionTypes = Object.values(MarketDataSubscriptionType) + +const KeepaliveInterval = 30000 // 30 seconds + +const DefaultChannelId = 1 + +export default class MarketDataStreamer { + private webSocket: WebSocket | null = null + private token = '' + private keepaliveIntervalId: any | null = null + private dataListeners = new Map() + private openChannels = new Set() + private subscriptionsQueue: Map = new Map() + private authState = '' + + addDataListener(dataListener: MarketDataListener) { + if (_.isNil(dataListener)) { + return _.noop + } + const guid = uuidv4() + this.dataListeners.set(guid, dataListener) + + return () => this.dataListeners.delete(guid) + } + + connect(url: string, token: string) { + if (this.isConnected) { + throw new Error('MarketDataStreamer is attempting to connect when an existing websocket is already connected') + } + + this.token = token + this.webSocket = new WebSocket(url) + this.webSocket.onopen = this.onOpen.bind(this) + this.webSocket.onerror = this.onError.bind(this) + this.webSocket.onmessage = this.handleMessageReceived.bind(this) + this.webSocket.onclose = this.onClose.bind(this) + } + + disconnect() { + if (_.isNil(this.webSocket)) { + return + } + + this.clearKeepalive() + + this.webSocket.close() + this.webSocket = null + + this.openChannels.clear() + this.subscriptionsQueue.clear() + this.authState = '' + } + + // TODO: add listener to options, return unsubscriber + addSubscription(symbol: string, options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }) { + const { subscriptionTypes, channelId } = options + const isOpen = this.isChannelOpened(channelId) + if (isOpen) { + this.sendSubscriptionMessage(symbol, subscriptionTypes, channelId, 'add') + } else { + this.queueSubscription(symbol, options) + } + } + + removeSubscription(symbol: string, options = { subscriptionTypes: AllSubscriptionTypes, channelId: DefaultChannelId }) { + const { subscriptionTypes, channelId } = options + const isOpen = this.isChannelOpened(channelId) + if (isOpen) { + this.sendSubscriptionMessage(symbol, subscriptionTypes, channelId, 'remove') + } else { + this.dequeueSubscription(symbol, options) + } + } + + removeAllSubscriptions(channelId = DefaultChannelId) { + const isOpen = this.isChannelOpened(channelId) + if (isOpen) { + this.sendMessage({ "type": "FEED_SUBSCRIPTION", "channel": channelId, reset: true }) + } else { + this.subscriptionsQueue.set(channelId, []) + } + } + + openFeedChannel(channelId: number) { + if (!this.isDxLinkAuthorized) { + throw new Error(`Unable to open channel ${channelId} due to DxLink authorization state: ${this.authState}`) + } + + if (this.isChannelOpened(channelId)) { + return + } + + this.sendMessage({ + "type": "CHANNEL_REQUEST", + "channel": channelId, + "service": "FEED", + "parameters": { + "contract": "AUTO" + } + }) + } + + isChannelOpened(channelId: number) { + return this.isConnected && this.openChannels.has(channelId) + } + + get isConnected() { + return !_.isNil(this.webSocket) + } + + private scheduleKeepalive() { + this.keepaliveIntervalId = setInterval(this.sendKeepalive, KeepaliveInterval) + } + + private sendKeepalive() { + if (_.isNil(this.keepaliveIntervalId)) { + return + } + + this.sendMessage({ + "type": "KEEPALIVE", + "channel": 0 + }) + } + + private queueSubscription(symbol: string, options: SubscriptionOptions) { + const { subscriptionTypes, channelId } = options + let queue = this.subscriptionsQueue.get(options.channelId) + if (_.isNil(queue)) { + queue = [] + this.subscriptionsQueue.set(channelId, queue) + } + + queue.push({ symbol, subscriptionTypes }) + } + + private dequeueSubscription(symbol: string, options: SubscriptionOptions) { + const queue = this.subscriptionsQueue.get(options.channelId) + if (_.isNil(queue) || _.isEmpty(queue)) { + return + } + + _.remove(queue, (queueItem: any) => queueItem.symbol === symbol) + } + + private sendQueuedSubscriptions(channelId: number) { + const queuedSubscriptions = this.subscriptionsQueue.get(channelId) + if (_.isNil(queuedSubscriptions)) { + return + } + + // Clear out queue immediately + this.subscriptionsQueue.set(channelId, []) + queuedSubscriptions.forEach(subscription => { + this.sendSubscriptionMessage(subscription.symbol, subscription.subscriptionTypes, channelId, 'add') + }) + } + + /** + * + * @param {*} symbol + * @param {*} subscriptionTypes + * @param {*} channelId + * @param {*} direction add or remove + */ + private sendSubscriptionMessage(symbol: string, subscriptionTypes: MarketDataSubscriptionType[], channelId: number, direction: string) { + const subscriptions = subscriptionTypes.map(type => ({ "symbol": symbol, "type": type })) + this.sendMessage({ + "type": "FEED_SUBSCRIPTION", + "channel": channelId, + [direction]: subscriptions + }) + } + + private onError(error: any) { + console.error('Error received: ', error) + } + + private onOpen() { + this.openChannels.clear() + + this.sendMessage({ + "type": "SETUP", + "channel": 0, + "keepaliveTimeout": KeepaliveInterval, + "acceptKeepaliveTimeout": KeepaliveInterval, + "version": "0.1-js/1.0.0" + }) + + this.scheduleKeepalive() + } + + private onClose() { + this.webSocket = null + this.clearKeepalive() + } + + private clearKeepalive() { + if (!_.isNil(this.keepaliveIntervalId)) { + clearInterval(this.keepaliveIntervalId) + } + + this.keepaliveIntervalId = null + } + + get isDxLinkAuthorized() { + return this.authState === 'AUTHORIZED' + } + + private handleAuthStateMessage(data: any) { + this.authState = data.state + if (this.isDxLinkAuthorized) { + this.openFeedChannel(DefaultChannelId) + } else { + this.sendMessage({ + "type": "AUTH", + "channel": 0, + "token": this.token + }) + } + } + + private handleChannelOpened(jsonData: any) { + this.openChannels.add(jsonData.channel) + this.sendQueuedSubscriptions(jsonData.channel) + } + + private notifyListeners(jsonData: any) { + this.dataListeners.forEach(listener => listener(jsonData)) + } + + private handleMessageReceived(data: string) { + const messageData = _.get(data, 'data', data) + const jsonData = JSON.parse(messageData) + switch (jsonData.type) { + case 'AUTH_STATE': + this.handleAuthStateMessage(jsonData) + break + case 'CHANNEL_OPENED': + this.handleChannelOpened(jsonData) + break + case 'FEED_DATA': + this.notifyListeners(jsonData) + break + } + } + + private sendMessage(json: object) { + if (_.isNil(this.webSocket)) { + return + } + + this.webSocket.send(JSON.stringify(json)) + } +} \ No newline at end of file diff --git a/lib/quote-streamer.ts b/lib/quote-streamer.ts deleted file mode 100644 index e299379..0000000 --- a/lib/quote-streamer.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Feed, { EventType, IEvent } from '@dxfeed/api' -import _ from 'lodash' - -export const SupportedEventTypes = [ - EventType.Quote, - EventType.Trade, - EventType.Summary, - EventType.Greeks, - EventType.Profile -] - -export default class QuoteStreamer { - private feed: Feed | null = null - - constructor(private readonly token: string, private readonly url: string) {} - - connect() { - this.feed = new Feed() - this.feed.setAuthToken(this.token) - this.feed.connect(this.url) - } - - disconnect() { - if (!_.isNil(this.feed)) { - this.feed.disconnect() - } - } - - subscribe(dxfeedSymbol: string, eventHandler: (event: IEvent) => void): () => void { - if (_.isNil(this.feed)) { - return _.noop - } - - return this.feed.subscribe( - SupportedEventTypes, - [dxfeedSymbol], - eventHandler - ) - } -} diff --git a/lib/services/accounts-and-customers-service.ts b/lib/services/accounts-and-customers-service.ts index 1a6de8d..940c07a 100644 --- a/lib/services/accounts-and-customers-service.ts +++ b/lib/services/accounts-and-customers-service.ts @@ -26,10 +26,9 @@ export default class AccountsAndCustomersService { return extractResponseData(fullCustomerAccountResource) } - //Quote-streamer-tokens: Operations about quote-streamer-tokens - async getQuoteStreamerTokens(){ - //Returns the appropriate quote streamer endpoint, level and identification token for the current customer to receive market data. - const quoteStreamerTokens = (await this.httpClient.getData('/quote-streamer-tokens', {}, {})) - return extractResponseData(quoteStreamerTokens) + //Returns the appropriate quote streamer endpoint, level and identification token for the current customer to receive market data. + async getApiQuoteToken() { + const apiQuoteToken = (await this.httpClient.getData('/api-quote-tokens', {}, {})) + return extractResponseData(apiQuoteToken) } } diff --git a/lib/tastytrade-api.ts b/lib/tastytrade-api.ts index f4920bf..f68664d 100644 --- a/lib/tastytrade-api.ts +++ b/lib/tastytrade-api.ts @@ -1,6 +1,6 @@ import TastytradeHttpClient from "./services/tastytrade-http-client" import { AccountStreamer, STREAMER_STATE, Disposer, StreamerStateObserver } from './account-streamer' -import QuoteStreamer from "./quote-streamer" +import MarketDataStreamer, { MarketDataSubscriptionType, MarketDataListener } from "./market-data-streamer" //Services: import SessionService from "./services/session-service" @@ -61,5 +61,5 @@ export default class TastytradeClient { } } -export { QuoteStreamer } +export { MarketDataStreamer, MarketDataSubscriptionType, MarketDataListener } export { AccountStreamer, STREAMER_STATE, Disposer, StreamerStateObserver } diff --git a/package-lock.json b/package-lock.json index 3998ae5..dde9d74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,19 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@dxfeed/api": "^1.1.0", "@types/lodash": "^4.14.182", "@types/qs": "^6.9.7", "axios": "^1.3.4", + "isomorphic-ws": "^5.0.0", "lodash": "^4.17.21", - "qs": "^6.11.1" + "qs": "^6.11.1", + "uuid": "^9.0.0", + "ws": "^8.13.0" }, "devDependencies": { "@types/jest": "^29.5.0", "@types/node": "17.0.27", + "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", "dotenv": "^16.0.3", @@ -654,15 +657,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@dxfeed/api": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@dxfeed/api/-/api-1.1.0.tgz", - "integrity": "sha512-U8agkpCaVXCQF6DZab0pXou8jQnOcN9ApriRnpwib3Jp0AQOzO8F8047yr/iCvTij9/XovcQyRUyWQaVudS1pw==", - "dependencies": { - "@types/cometd": "^4.0.7", - "cometd": "^5.0.1" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1263,11 +1257,6 @@ "@babel/types": "^7.3.0" } }, - "node_modules/@types/cometd": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@types/cometd/-/cometd-4.0.11.tgz", - "integrity": "sha512-bn27WenWkEyCH1ynpZZdSA6gnYW30bLWVUIu+EluxEaFNDPor6EnVJtTCVDX/wzUdriaXRcIYjOTnIP1fEuGeQ==" - }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -1351,6 +1340,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/uuid": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", + "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -2032,11 +2027,6 @@ "node": ">= 0.8" } }, - "node_modules/cometd": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/cometd/-/cometd-5.0.15.tgz", - "integrity": "sha512-zEFBRuE1Roa5C4rJGNYUnT7AuKiYIpsWDbbBNo16vpOfMoSOY2Y+zANBxHpChTwytc7crAclBh8fNqfja/DAUw==" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2942,6 +2932,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -4732,6 +4730,14 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -4827,6 +4833,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -5357,15 +5383,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@dxfeed/api": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@dxfeed/api/-/api-1.1.0.tgz", - "integrity": "sha512-U8agkpCaVXCQF6DZab0pXou8jQnOcN9ApriRnpwib3Jp0AQOzO8F8047yr/iCvTij9/XovcQyRUyWQaVudS1pw==", - "requires": { - "@types/cometd": "^4.0.7", - "cometd": "^5.0.1" - } - }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -5851,11 +5868,6 @@ "@babel/types": "^7.3.0" } }, - "@types/cometd": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@types/cometd/-/cometd-4.0.11.tgz", - "integrity": "sha512-bn27WenWkEyCH1ynpZZdSA6gnYW30bLWVUIu+EluxEaFNDPor6EnVJtTCVDX/wzUdriaXRcIYjOTnIP1fEuGeQ==" - }, "@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -5939,6 +5951,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/uuid": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", + "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==", + "dev": true + }, "@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -6397,11 +6415,6 @@ "delayed-stream": "~1.0.0" } }, - "cometd": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/cometd/-/cometd-5.0.15.tgz", - "integrity": "sha512-zEFBRuE1Roa5C4rJGNYUnT7AuKiYIpsWDbbBNo16vpOfMoSOY2Y+zANBxHpChTwytc7crAclBh8fNqfja/DAUw==" - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7059,6 +7072,12 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "requires": {} + }, "istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -8363,6 +8382,11 @@ "punycode": "^2.1.0" } }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -8439,6 +8463,12 @@ "signal-exit": "^3.0.7" } }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "requires": {} + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 564f81d..a9d9dee 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,19 @@ "postpack": "git tag -a $npm_package_version -m $npm_package_version && git push origin $npm_package_version" }, "dependencies": { - "@dxfeed/api": "^1.1.0", "@types/lodash": "^4.14.182", "@types/qs": "^6.9.7", "axios": "^1.3.4", + "isomorphic-ws": "^5.0.0", "lodash": "^4.17.21", - "qs": "^6.11.1" + "qs": "^6.11.1", + "uuid": "^9.0.0", + "ws": "^8.13.0" }, "devDependencies": { "@types/jest": "^29.5.0", "@types/node": "17.0.27", + "@types/uuid": "^9.0.2", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", "dotenv": "^16.0.3", diff --git a/tests/integration/service/accounts-and-customers-service.test.ts b/tests/integration/service/accounts-and-customers-service.test.ts index e1f8f81..b9b1133 100644 --- a/tests/integration/service/accounts-and-customers-service.test.ts +++ b/tests/integration/service/accounts-and-customers-service.test.ts @@ -46,9 +46,9 @@ describe('getFullCustomerAccountResource', () => { }) }) -describe('getQuoteStreamerTokens', () => { +describe('getApiQuoteToken', () => { it('responds with the correct data', async function() { - const response = await accountsAndCustomersService.getQuoteStreamerTokens() + const response = await accountsAndCustomersService.getApiQuoteToken() expect(response.token).toBeDefined() }) })