diff --git a/CHANGELOG.md b/CHANGELOG.md index acf20a3..06bccf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.0.0] - 2024-09-17 + +The `TastytradeClient` constructor now takes a single config object instead of 2 urls. + +### Changed + +- Add optional logging; update example app [#44](https://github.com/tastytrade/tastytrade-api-js/pull/44) +- Add future option quote streaming example to README + +### Fixed + +- Update README.md [#42](https://github.com/tastytrade/tastytrade-api-js/pull/35) +- README corrections +- typo on json account node name [#39](https://github.com/tastytrade/tastytrade-api-js/pull/39) + ## [4.0.0] - 2024-03-12 ### Changed diff --git a/examples/components/layout.tsx b/examples/components/layout.tsx index 71877ab..58df467 100644 --- a/examples/components/layout.tsx +++ b/examples/components/layout.tsx @@ -39,6 +39,7 @@ const Layout = observer((props: any) => { (new TastytradeContext(SANDBOX_BASE_URL, SANDBOX_STREAMER_URL)) export { AppContext, TastytradeContext }; diff --git a/examples/package-lock.json b/examples/package-lock.json index 33b32ff..5e4d763 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -37,7 +37,7 @@ }, "..": { "name": "@tastytrade/api", - "version": "3.1.0", + "version": "4.0.0", "license": "MIT", "dependencies": { "@types/lodash": "^4.14.182", @@ -58,9 +58,10 @@ "@typescript-eslint/parser": "^5.57.1", "dotenv": "^16.0.3", "eslint": "^8.14.0", - "jest": "^29.5.0", - "ts-jest": "^29.0.5", - "typescript": "4.6.3" + "jest": "^29.7.0", + "nock": "^13.5.4", + "ts-jest": "^29.1.2", + "typescript": "^5.4.2" }, "engines": { "node": ">=20.0.0", @@ -11457,11 +11458,12 @@ "dotenv": "^16.0.3", "eslint": "^8.14.0", "isomorphic-ws": "^5.0.0", - "jest": "^29.5.0", + "jest": "^29.7.0", "lodash": "^4.17.21", + "nock": "^13.5.4", "qs": "^6.11.1", - "ts-jest": "^29.0.5", - "typescript": "4.6.3", + "ts-jest": "^29.1.2", + "typescript": "^5.4.2", "uuid": "^9.0.0", "ws": "^8.13.0" } diff --git a/examples/pages/_app.tsx b/examples/pages/_app.tsx index a196040..a2df930 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.cert.tastyworks.com'), + () => new TastytradeContext('https://api.cert.tastyworks.com', 'wss://streamer.cert.tastyworks.com'), [] ); return ( diff --git a/lib/account-streamer.ts b/lib/account-streamer.ts index 39c7990..d6915e1 100644 --- a/lib/account-streamer.ts +++ b/lib/account-streamer.ts @@ -4,6 +4,7 @@ import type { JsonMap, JsonValue } from './utils/json-util.js' import { JsonBuilder } from './utils/json-util.js' import TastytradeSession from './models/tastytrade-session.js' import { MinTlsVersion } from './utils/constants.js' +import type Logger from './logger.js' export enum STREAMER_STATE { Open = 0, @@ -42,6 +43,7 @@ function removeElement(array: T[], element: T): void { } export class AccountStreamer { + private readonly logger: Logger private websocket: WebSocket | null = null private startResolve: ((result: boolean) => void) | null = null private startReject: ((reason?: any) => void) | null = null @@ -65,13 +67,17 @@ export class AccountStreamer { [(status: string) => void, (error: string) => void] > = new Map() - private readonly logger = console - /** * * @param url Url of the account streamer service */ - constructor(private readonly url: string, private readonly session: TastytradeSession) {} + constructor( + private readonly url: string, + private readonly session: TastytradeSession, + logger: Logger + ) { + this.logger = logger + } get streamerState(): STREAMER_STATE { return this._streamerState @@ -237,6 +243,7 @@ export class AccountStreamer { // Queue up and send on open this.queued.push(message) } else { + this.logger.info('Sending message: ', message) websocket.send(message) } @@ -304,12 +311,12 @@ export class AccountStreamer { this.queued = [] } - private readonly handleOpen = (event: WebSocket.Event) => { + private readonly handleOpen = (_event: WebSocket.Event) => { if (this.startResolve === null) { return } - this.logger.info('AccountStreamer opened', event) + this.logger.info('AccountStreamer opened') this.startResolve(true) this.startResolve = this.startReject = null @@ -320,7 +327,7 @@ export class AccountStreamer { } private readonly handleClose = (event: WebSocket.CloseEvent) => { - this.logger.info('AccountStreamer closed', event) + this.logger.info('AccountStreamer closed') if (this.websocket === null) { return } @@ -335,7 +342,7 @@ export class AccountStreamer { return } - this.logger.warn('AccountStreamer error', event) + this.logger.error('AccountStreamer error', event) this.lastErrorEvent = event this.streamerState = STREAMER_STATE.Error @@ -370,7 +377,7 @@ export class AccountStreamer { } private readonly handleOneMessage = (json: JsonMap) => { - this.logger.info(json) + this.logger.info('Message received: ', json) const action = json.action as string this.streamerMessageObservers.forEach(observer => observer(json)) diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 0000000..3273868 --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,48 @@ +import _ from 'lodash' + +export default interface Logger { + error(...data: any[]): void; + info(...data: any[]): void; + warn(...data: any[]): void; +} + +export enum LogLevel { + INFO = 1, + WARN = 2, + ERROR = 3 +} + +export class TastytradeLogger implements Logger { + public logLevel: LogLevel + private logger: Logger | null = null + + constructor(logger?: Logger, logLevel?: LogLevel) { + this.logger = logger ?? null + this.logLevel = logLevel ?? LogLevel.ERROR + } + + error(...data: any[]): void { + if (this.shouldLog(LogLevel.ERROR)) { + this.logger!.error(...data) + } + } + + info(...data: any[]): void { + if (this.shouldLog(LogLevel.INFO)) { + this.logger!.info(...data) + } + } + + warn(...data: any[]): void { + if (this.shouldLog(LogLevel.WARN)) { + this.logger!.warn(...data) + } + } + + private shouldLog(level: LogLevel) { + if (_.isNil(this.logger)) { + return false + } + return LogLevel[level] >= LogLevel[this.logLevel] + } +} \ No newline at end of file diff --git a/lib/services/tastytrade-http-client.ts b/lib/services/tastytrade-http-client.ts index 7f92940..3b61cee 100644 --- a/lib/services/tastytrade-http-client.ts +++ b/lib/services/tastytrade-http-client.ts @@ -3,6 +3,7 @@ import axios from "axios" import qs from 'qs' import { recursiveDasherizeKeys } from "../utils/json-util.js" import _ from 'lodash' +import type Logger from "../logger.js" const ParamsSerializer = { serialize: function (queryParams: object) { @@ -10,10 +11,12 @@ const ParamsSerializer = { } } -export default class TastytradeHttpClient{ +export default class TastytradeHttpClient { + private readonly logger?: Logger public readonly session: TastytradeSession - constructor(private readonly baseUrl: string) { + constructor(private readonly baseUrl: string, logger?: Logger) { + this.logger = logger this.session = new TastytradeSession() } @@ -47,6 +50,7 @@ export default class TastytradeHttpClient{ paramsSerializer: ParamsSerializer }, _.isEmpty) + this.logger?.info('Making request', config) return axios.request(config) } diff --git a/lib/tastytrade-api.ts b/lib/tastytrade-api.ts index 21994fe..56316e6 100644 --- a/lib/tastytrade-api.ts +++ b/lib/tastytrade-api.ts @@ -17,8 +17,18 @@ import SymbolSearchService from "./services/symbol-search-service.js" import TransactionsService from "./services/transactions-service.js" import WatchlistsService from "./services/watchlists-service.js" import TastytradeSession from "./models/tastytrade-session.js" +import type Logger from "./logger.js" +import { TastytradeLogger, LogLevel } from "./logger.js" + +export type ClientConfig = { + baseUrl: string, + accountStreamerUrl: string, + logger?: Logger, + logLevel?: LogLevel +} export default class TastytradeClient { + public readonly logger: TastytradeLogger public readonly httpClient: TastytradeHttpClient public readonly accountStreamer: AccountStreamer @@ -37,9 +47,10 @@ export default class TastytradeClient { public readonly transactionsService: TransactionsService public readonly watchlistsService: WatchlistsService - constructor(readonly baseUrl: string, readonly accountStreamerUrl: string) { - this.httpClient = new TastytradeHttpClient(baseUrl) - this.accountStreamer = new AccountStreamer(accountStreamerUrl, this.session) + constructor(config: ClientConfig) { + this.logger = new TastytradeLogger(config.logger, config.logLevel) + this.httpClient = new TastytradeHttpClient(config.baseUrl, this.logger) + this.accountStreamer = new AccountStreamer(config.accountStreamerUrl, this.session, this.logger) this.sessionService = new SessionService(this.httpClient) this.accountStatusService = new AccountStatusService(this.httpClient) @@ -63,3 +74,5 @@ export default class TastytradeClient { export { MarketDataStreamer, MarketDataSubscriptionType, type MarketDataListener, type CandleSubscriptionOptions, CandleType } export { AccountStreamer, STREAMER_STATE, type Disposer, type StreamerStateObserver } +export { TastytradeLogger, LogLevel } +export type { Logger } \ No newline at end of file