Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional logging; update example app #44

Merged
merged 2 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions examples/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const Layout = observer((props: any) => {
<Toaster
toastOptions={{
duration: 5000,
position: 'bottom-right',
success: {
duration: 10000
}
Expand Down
10 changes: 6 additions & 4 deletions examples/contexts/context.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
// src/context/state.ts
import { createContext } from 'react';
import TastytradeClient, { MarketDataStreamer } from "tastytrade-api"
import TastytradeClient, { LogLevel, MarketDataStreamer } from "tastytrade-api"
import { makeAutoObservable } from 'mobx';
import _ from 'lodash'

const SANDBOX_BASE_URL = 'https://api.cert.tastyworks.com'
const SANDBOX_STREAMER_URL = 'wss://streamer.cert.tastyworks.com'

class TastytradeContext {
static Instance = new TastytradeContext('https://api.cert.tastyworks.com', 'wss://streamer.cert.tastyworks.com');
public tastytradeApi: TastytradeClient
public accountNumbers: string[] = []
public readonly marketDataStreamer: MarketDataStreamer = new MarketDataStreamer()

constructor(baseUrl: string, accountStreamerUrl: string) {
makeAutoObservable(this)
this.tastytradeApi = new TastytradeClient(baseUrl, accountStreamerUrl)
this.tastytradeApi = new TastytradeClient({ baseUrl, accountStreamerUrl, logger: console, logLevel: LogLevel.INFO })
makeAutoObservable(this.tastytradeApi.session)
}

Expand All @@ -21,6 +23,6 @@ class TastytradeContext {
}
}

const AppContext = createContext(TastytradeContext.Instance)
const AppContext = createContext<TastytradeContext>(new TastytradeContext(SANDBOX_BASE_URL, SANDBOX_STREAMER_URL))

export { AppContext, TastytradeContext };
16 changes: 9 additions & 7 deletions examples/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
23 changes: 15 additions & 8 deletions lib/account-streamer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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,
Expand Down Expand Up @@ -42,16 +43,17 @@
}

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

Check warning on line 49 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 49 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type

Check warning on line 49 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 49 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type
private requestCounter: number = 0

Check warning on line 50 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Type number trivially inferred from a number literal, remove type annotation

Check warning on line 50 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Type number trivially inferred from a number literal, remove type annotation

Check warning on line 50 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Type number trivially inferred from a number literal, remove type annotation

Check warning on line 50 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Type number trivially inferred from a number literal, remove type annotation
private queued: string[] = []

private heartbeatTimerId: number | NodeJS.Timeout | null = null

lastCloseEvent: any = null

Check warning on line 55 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 55 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type

Check warning on line 55 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 55 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type
lastErrorEvent: any = null

Check warning on line 56 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 56 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type

Check warning on line 56 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 56 in lib/account-streamer.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type
private _streamerState: STREAMER_STATE = STREAMER_STATE.Closed

private readonly streamerStateObservers: StreamerStateObserver[] = []
Expand All @@ -65,13 +67,17 @@
[(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
Expand Down Expand Up @@ -237,6 +243,7 @@
// Queue up and send on open
this.queued.push(message)
} else {
this.logger.info('Sending message: ', message)
websocket.send(message)
}

Expand Down Expand Up @@ -304,12 +311,12 @@
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
Expand All @@ -320,7 +327,7 @@
}

private readonly handleClose = (event: WebSocket.CloseEvent) => {
this.logger.info('AccountStreamer closed', event)
this.logger.info('AccountStreamer closed')
if (this.websocket === null) {
return
}
Expand All @@ -335,7 +342,7 @@
return
}

this.logger.warn('AccountStreamer error', event)
this.logger.error('AccountStreamer error', event)

this.lastErrorEvent = event
this.streamerState = STREAMER_STATE.Error
Expand Down Expand Up @@ -370,7 +377,7 @@
}

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))
Expand Down
48 changes: 48 additions & 0 deletions lib/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import _ from 'lodash'

export default interface Logger {
error(...data: any[]): void;

Check warning on line 4 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 4 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type

Check warning on line 4 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 4 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type
info(...data: any[]): void;

Check warning on line 5 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 5 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type

Check warning on line 5 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 5 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type
warn(...data: any[]): void;

Check warning on line 6 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 6 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type

Check warning on line 6 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 6 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type
}

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 {

Check warning on line 24 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 24 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type

Check warning on line 24 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 24 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type
if (this.shouldLog(LogLevel.ERROR)) {
this.logger!.error(...data)
}
}

info(...data: any[]): void {

Check warning on line 30 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 30 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type

Check warning on line 30 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 30 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type
if (this.shouldLog(LogLevel.INFO)) {
this.logger!.info(...data)
}
}

warn(...data: any[]): void {

Check warning on line 36 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 36 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type

Check warning on line 36 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 36 in lib/logger.ts

View workflow job for this annotation

GitHub Actions / build (21.x)

Unexpected any. Specify a different type
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]
}
}
8 changes: 6 additions & 2 deletions lib/services/tastytrade-http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ 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) {
return qs.stringify(queryParams, { arrayFormat: 'brackets' })
}
}

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()
}

Expand Down Expand Up @@ -47,6 +50,7 @@ export default class TastytradeHttpClient{
paramsSerializer: ParamsSerializer
}, _.isEmpty)

this.logger?.info('Making request', config)
return axios.request(config)
}

Expand Down
19 changes: 16 additions & 3 deletions lib/tastytrade-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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 }
Loading