From 348dd09f1fce4d8a31ef414ec919e2838bca71db Mon Sep 17 00:00:00 2001 From: Ronan-Yann Lorin Date: Sat, 7 Dec 2024 12:11:12 +0100 Subject: [PATCH] Implement getMarketRule and minor improvements --- package.json | 6 +- src/api-next/api-next.ts | 177 ++++++---- src/api-next/common/error.ts | 3 + src/api/contract/priceIncrement.ts | 7 + src/common/errorCode.ts | 30 +- src/core/api-next/subscription-registry.ts | 15 +- src/core/api-next/subscription.ts | 10 +- src/core/io/controller.ts | 12 +- src/core/io/decoder.ts | 15 +- src/core/io/encoder.ts | 131 +++---- src/core/io/socket.ts | 44 +-- src/index.ts | 3 +- .../get-contract-details.test.ts | 25 +- .../api-next-live/get-current-time.test.ts | 77 ++++ .../get-historical-ticks.test.ts | 7 +- .../get-managed-accounts.test.ts | 55 +++ .../api-next-live/get-market-rule.test.ts | 73 ++++ .../unit/api-next-live/get-user-info.test.ts | 59 ++++ .../subscription-registry.test.ts | 15 +- .../unit/api-next/get-account-summary.test.ts | 2 +- .../unit/api-next/get-account-updates.test.ts | 149 ++++++++ .../unit/api-next/get-historical-data.test.ts | 3 +- .../unit/api-next/next-valid-order-id.test.ts | 4 +- src/tests/unit/api/historical-data.test.ts | 329 +++++++++--------- src/tests/unit/api/market-scanner.test.ts | 49 ++- src/tests/unit/api/order/cancelOrder.test.ts | 204 +++++------ yarn.lock | 314 ++--------------- 27 files changed, 1008 insertions(+), 810 deletions(-) create mode 100644 src/api/contract/priceIncrement.ts create mode 100644 src/tests/unit/api-next-live/get-current-time.test.ts create mode 100644 src/tests/unit/api-next-live/get-managed-accounts.test.ts create mode 100644 src/tests/unit/api-next-live/get-market-rule.test.ts create mode 100644 src/tests/unit/api-next-live/get-user-info.test.ts create mode 100644 src/tests/unit/api-next/get-account-updates.test.ts diff --git a/package.json b/package.json index 8149e7b2..f5a3edd1 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ }, "devDependencies": { "@types/jest": "^29.5.14", - "@types/node": "^18.19.64", + "@types/node": "^18.19.67", "@types/source-map-support": "^0.5.10", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", @@ -87,8 +87,8 @@ "jest-environment-node": "^29.7.0", "jest-junit": "^16.0.0", "ts-jest": "^29.2.5", - "typedoc": "^0.26.11", - "typescript": "^5.6.3" + "typedoc": "^0.27.2", + "typescript": "^5.7.2" }, "engines": { "node": ">=18.0.0" diff --git a/src/api-next/api-next.ts b/src/api-next/api-next.ts index d17c4322..2091d9e7 100644 --- a/src/api-next/api-next.ts +++ b/src/api-next/api-next.ts @@ -25,6 +25,7 @@ import { OrderBookUpdate, OrderCancel, OrderState, + PriceIncrement, ScannerSubscription, SecType, TagValue, @@ -32,6 +33,7 @@ import { } from "../"; import LogLevel from "../api/data/enum/log-level"; import OrderStatus from "../api/order/enum/order-status"; +import { isNonFatalError } from "../common/errorCode"; import { MutableAccountSummaries, MutableAccountSummaryTagValues, @@ -73,13 +75,6 @@ import { MarketScannerUpdate, } from "./market-scanner/market-scanner"; -/** - * @internal - * - * An invalid request id. - */ -const INVALID_REQ_ID = -1; - /** * @internal * @@ -94,6 +89,19 @@ const LOG_TAG = "IBApiNext"; */ const TWS_LOG_TAG = "TWS"; +function filterMap( + map: Map, // eslint-disable-line @typescript-eslint/no-explicit-any + pred: (k: number, v: any) => boolean, // eslint-disable-line @typescript-eslint/no-explicit-any +) { + const result = new Map(); + for (const [k, v] of map) { + if (pred(k, v)) { + result.set(k, v); + } + } + return result; +} + /** * Input arguments on the [[IBApiNext]] constructor. */ @@ -205,22 +213,9 @@ export class IBApiNext { reqId, advancedOrderReject, }; - // handle warnings - they are also reported on TWS error callback, but we DO NOT want to emit - // it as error into the subject (and cancel the subscription). - if ( - (code >= 2100 && code < 3000) || - code === ErrorCode.PART_OF_REQUESTED_DATA_NOT_SUBSCRIBED || - code === ErrorCode.DISPLAYING_DELAYED_DATA - ) { - this.logger.warn( - TWS_LOG_TAG, - `${error.message} - Code: ${code} - ReqId: ${reqId}`, - ); - return; - } // emit to the subscription subject - if (reqId !== INVALID_REQ_ID) { - this.subscriptions.dispatchError(reqId, apiError); + if (reqId !== ErrorCode.NO_VALID_ID && !isNonFatalError(code, error)) { + this.subscriptions.dispatchError(apiError); } // emit to global error subject this.errorSubject.next(apiError); @@ -353,7 +348,7 @@ export class IBApiNext { }, undefined, [[EventName.currentTime, this.onCurrentTime]], - "getCurrentTime", // use same instance id each time, to make sure there is only 1 pending request at time + "getCurrentTime", // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: number }) => v.all)), { @@ -386,7 +381,7 @@ export class IBApiNext { }, undefined, [[EventName.managedAccounts, this.onManagedAccts]], - "getManagedAccounts", // use same instance id each time, to make sure there is only 1 pending request at time + "getManagedAccounts", // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: string[] }) => v.all)), { @@ -534,7 +529,7 @@ export class IBApiNext { [EventName.accountSummary, this.onAccountSummary], [EventName.accountSummaryEnd, this.onAccountSummaryEnd], ], - `${group}:${tags}`, + `getAccountSummary+${group}:${tags}`, // Use the same instance ID each time to ensure there is only one pending request at a time. ); } @@ -548,6 +543,8 @@ export class IBApiNext { * @param currency the currency of the value. * * @see [[reqAccountUpdates]] + * + * @todo Filter subscriptions notifications in callbacks using instanceId to finish this implementation */ private readonly onUpdateAccountValue = ( subscriptions: Map>, @@ -556,11 +553,12 @@ export class IBApiNext { currency: string, account: string, ): void => { - this.logger.debug( - LOG_TAG, - `onUpdateAccountValue(${tag}, ${value}, ${currency}, ${account})`, - ); - subscriptions.forEach((subscription) => { + filterMap( + subscriptions, + (_k: number, v: IBApiNextSubscription) => + v.instanceId === "getAccountUpdates" || + v.instanceId === `getAccountUpdates+${account}`, + ).forEach((subscription) => { // update latest value on cache const all: AccountUpdate = subscription.lastAllValue ?? {}; const cached = all?.value ?? new MutableAccountSummaries(); @@ -617,6 +615,8 @@ export class IBApiNext { * @param account The IBKR account Id. * * @see [[reqAccountUpdates]] + * + * @todo Filter subscriptions notifications in callbacks using instanceId to finish this implementation */ private readonly onUpdatePortfolio = ( subscriptions: Map>, @@ -629,10 +629,6 @@ export class IBApiNext { realizedPNL: number, account: string, ): void => { - this.logger.debug( - LOG_TAG, - `onUpdatePortfolio(${contract.symbol}, ${pos}, ${marketPrice}, ${marketValue}, ${avgCost}, ${unrealizedPNL}, ${realizedPNL}, ${account})`, - ); const updatedPosition: Position = { account, contract, @@ -644,7 +640,12 @@ export class IBApiNext { realizedPNL, }; // notify all subscribers - subscriptions.forEach((subscription) => { + filterMap( + subscriptions, + (_k: number, v: IBApiNextSubscription) => + v.instanceId === "getAccountUpdates" || + v.instanceId === `getAccountUpdates+${account}`, + ).forEach((subscription) => { // update latest value on cache let hasAdded = false; @@ -714,7 +715,6 @@ export class IBApiNext { subscriptions: Map>, timeStamp: string, ): void => { - this.logger.debug(LOG_TAG, `onUpdateAccountTime(${timeStamp})`); subscriptions.forEach((sub) => { const changed: AccountUpdate = { timestamp: timeStamp }; const all: AccountUpdate = sub.lastAllValue ?? {}; @@ -733,14 +733,20 @@ export class IBApiNext { * @param accountName the account name * * @see [[reqAccountUpdates]] + * + * @todo Filter subscriptions notifications in callbacks using instanceId to finish this implementation */ private readonly onAccountDownloadEnd = ( subscriptions: Map>, accountName: string, ): void => { - this.logger.debug(LOG_TAG, `onAccountDownloadEnd(${accountName})`); // notify all subscribers - subscriptions.forEach((subscription) => { + filterMap( + subscriptions, + (_k: number, v: IBApiNextSubscription) => + v.instanceId === "getAccountUpdates" || + v.instanceId === `getAccountUpdates+${accountName}`, + ).forEach((subscription) => { const all: AccountUpdate = subscription.lastAllValue ?? {}; subscription.endEventReceived = true; subscription.next({ all }); @@ -756,9 +762,10 @@ export class IBApiNext { * @param acctCode the specific account to retrieve. * * @see [[reqAccountUpdates]], [[reqGlobalCancel]] + * + * @todo Filter subscriptions notifications in callbacks using instanceId to finish this implementation */ getAccountUpdates(acctCode?: string): Observable { - this.logger.debug(LOG_TAG, `getAccountUpdates(${acctCode})`); return this.subscriptions.register( () => { this.api.reqAccountUpdates(true, acctCode); @@ -769,10 +776,10 @@ export class IBApiNext { [ [EventName.updateAccountValue, this.onUpdateAccountValue], [EventName.updatePortfolio, this.onUpdatePortfolio], - [EventName.updateAccountTime, this.onUpdateAccountTime], [EventName.accountDownloadEnd, this.onAccountDownloadEnd], + [EventName.updateAccountTime, this.onUpdateAccountTime], ], - acctCode ? `getAccountUpdates+${acctCode}` : "getAccountUpdates", // use same instance id each time, to make sure there is only 1 pending request at time + acctCode ? `getAccountUpdates+${acctCode}` : "getAccountUpdates", // Use the same instance ID each time to ensure there is only one pending request at a time. ); } @@ -864,7 +871,7 @@ export class IBApiNext { [EventName.position, this.onPosition], [EventName.positionEnd, this.onPositionEnd], ], - "getPositions", + "getPositions", // Use the same instance ID each time to ensure there is only one pending request at a time. ); } @@ -1071,7 +1078,7 @@ export class IBApiNext { this.api.cancelPnL(reqId); }, [[EventName.pnl, this.onPnL]], - `${account}:${modelCode}`, + `getPnl+${account}:${modelCode}`, // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: PnL }) => v.all)); } @@ -1127,7 +1134,7 @@ export class IBApiNext { this.api.cancelPnLSingle(reqId); }, [[EventName.pnlSingle, this.onPnLSingle]], - `${account}:${modelCode}:${conId}`, + `getPnLSingle+${account}:${modelCode}:${conId}`, // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: PnLSingle }) => v.all)); } @@ -1551,9 +1558,9 @@ export class IBApiNext { [EventName.tickOptionComputation, this.onTickOptionComputation], [EventName.tickSnapshotEnd, this.onTickSnapshotEnd], ], - snapshot || regulatorySnapshot - ? undefined - : `${JSON.stringify(contract)}:${genericTickList}`, + `getMarketData+${JSON.stringify( + contract, + )}:${genericTickList}:${snapshot}:${regulatorySnapshot}`, // Use the same instance ID each time to ensure there is only one pending request at a time. ); } @@ -1640,7 +1647,9 @@ export class IBApiNext { this.api.cancelHeadTimestamp(reqId); }, [[EventName.headTimestamp, this.onHeadTimestamp]], - `${JSON.stringify(contract)}:${whatToShow}:${useRTH}:${formatDate}`, + `getHeadTimestamp+${JSON.stringify( + contract, + )}:${whatToShow}:${useRTH}:${formatDate}`, // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: string }) => v.all)), { @@ -1779,7 +1788,6 @@ export class IBApiNext { }, undefined, [[EventName.historicalData, this.onHistoricalData]], - undefined, ) .pipe(map((v: { all: Bar[] }) => v.all)), { @@ -1885,7 +1893,7 @@ export class IBApiNext { [[EventName.historicalDataUpdate, this.onHistoricalDataUpdate]], `${JSON.stringify( contract, - )}:${barSizeSetting}:${whatToShow}:${formatDate}`, + )}:${barSizeSetting}:${whatToShow}:${formatDate}`, // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: Bar }) => v.all)); } @@ -1953,7 +1961,6 @@ export class IBApiNext { }, undefined, [[EventName.historicalTicks, this.onHistoricalTicks]], - undefined, ) .pipe(map((v: { all: HistoricalTick[] }) => v.all)); } @@ -2023,7 +2030,6 @@ export class IBApiNext { }, undefined, [[EventName.historicalTicksBidAsk, this.onHistoricalTicksBidAsk]], - undefined, ) .pipe(map((v: { all: HistoricalTickBidAsk[] }) => v.all)); } @@ -2089,7 +2095,6 @@ export class IBApiNext { }, undefined, [[EventName.historicalTicksLast, this.onHistoricalTicksLast]], - undefined, ) .pipe(map((v: { all: HistoricalTickLast[] }) => v.all)); } @@ -2122,7 +2127,7 @@ export class IBApiNext { }, undefined, [[EventName.mktDepthExchanges, this.onMktDepthExchanges]], - "getMarketDepthExchanges", // use same instance id each time, to make sure there is only 1 pending request at time + "getMarketDepthExchanges", // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: DepthMktDataDescription[] }) => v.all)), { @@ -2339,7 +2344,7 @@ export class IBApiNext { ], `${JSON.stringify( contract, - )}:${numRows}:${isSmartDepth}:${mktDepthOptions}`, + )}:${numRows}:${isSmartDepth}:${mktDepthOptions}`, // Use the same instance ID each time to ensure there is only one pending request at a time. ); } @@ -2365,7 +2370,7 @@ export class IBApiNext { }, undefined, [[EventName.scannerParameters, this.onScannerParameters]], - "getScannerParameters", // use same instance id each time, to make sure there is only 1 pending request at time + "getScannerParameters", // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: string }) => v.all)), { @@ -2486,7 +2491,6 @@ export class IBApiNext { [EventName.scannerData, this.onScannerData], [EventName.scannerDataEnd, this.onScannerDataEnd], ], - `getMarketScanner-${JSON.stringify(scannerSubscription)}`, ); } @@ -2537,7 +2541,9 @@ export class IBApiNext { this.api.cancelHistogramData(reqId); }, [[EventName.histogramData, this.onHistogramData]], - `${JSON.stringify(contract)}:${useRTH}:${duration}:${durationUnit}`, + `getHistogramData+${JSON.stringify( + contract, + )}:${useRTH}:${duration}:${durationUnit}`, // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: HistogramEntry[] }) => v.all)), { @@ -2754,7 +2760,7 @@ export class IBApiNext { [EventName.orderBound, this.onOrderBound], [EventName.openOrderEnd, this.onOpenOrderComplete], ], - "getAllOpenOrders", // use same instance id each time, to make sure there is only 1 pending request at time + "getAllOpenOrders", // Use the same instance ID each time to ensure there is only one pending request at a time. ) .pipe(map((v: { all: OpenOrder[] }) => v.all)), { @@ -2779,7 +2785,7 @@ export class IBApiNext { [EventName.orderBound, this.onOrderBound], [EventName.openOrderEnd, this.onOpenOrderEnd], ], - "getOpenOrders", // use same instance id each time, to make sure there is only 1 pending request at time + "getOpenOrders", // Use the same instance ID each time to ensure there is only one pending request at a time. ); } @@ -2803,7 +2809,7 @@ export class IBApiNext { [EventName.orderBound, this.onOrderBound], [EventName.openOrderEnd, this.onOpenOrderEnd], ], - "getAutoOpenOrders", // use same instance id each time, to make sure there is only 1 pending request at time + "getAutoOpenOrders", // Use the same instance ID each time to ensure there is only one pending request at a time. ); } @@ -2986,7 +2992,7 @@ export class IBApiNext { } /** - * Get commication reports details of all executed trades. + * Get commissions reports details of all executed trades. * @param filter filter trade data on [[ExecutionFilter]] */ getCommissionReport(filter: ExecutionFilter): Promise { @@ -2998,8 +3004,8 @@ export class IBApiNext { }, undefined, [ - [EventName.execDetailsEnd, this.onExecDetailsEnd], [EventName.commissionReport, this.onComissionReport], + [EventName.execDetailsEnd, this.onExecDetailsEnd], ], ) .pipe(map((v: { all: CommissionReport[] }) => v.all)), @@ -3070,7 +3076,7 @@ export class IBApiNext { }, undefined, [[EventName.userInfo, this.onUserInfo]], - "getUserInfo", // use same instance id each time, to make sure there is only 1 pending request at time + "getUserInfo", ) .pipe(map((v: { all: string }) => v.all)), { @@ -3078,4 +3084,47 @@ export class IBApiNext { }, ); } + + /** marketRule event handler. */ + private readonly onMarketRule = ( + subscriptions: Map>, + marketRuleId: number, + priceIncrements: PriceIncrement[], + ): void => { + filterMap( + subscriptions, + (_k: number, v: IBApiNextSubscription) => + v.instanceId === `getMarketRule+${marketRuleId}`, + ).forEach((sub) => { + sub.next({ all: priceIncrements }); + sub.complete(); + }); + }; + + /** + * Get details about a given market rule. + * The market rule for an instrument on a particular exchange provides details about how the minimum price increment + * changes with price. A list of market rule ids can be obtained by invoking reqContractDetails on a particular + * contract. The returned market rule ID list will provide the market rule ID for the instrument in the correspond + * valid exchange list in contractDetails. + * + * @param marketRuleId The id of market rule. + */ + getMarketRule(marketRuleId: number): Promise { + return lastValueFrom( + this.subscriptions + .register( + () => { + this.api.reqMarketRule(marketRuleId); + }, + undefined, + [[EventName.marketRule, this.onMarketRule]], + `getMarketRule+${marketRuleId}`, + ) + .pipe(map((v: { all: PriceIncrement[] }) => v.all)), + { + defaultValue: undefined, + }, + ); + } } diff --git a/src/api-next/common/error.ts b/src/api-next/common/error.ts index d38e9328..b404e206 100644 --- a/src/api-next/common/error.ts +++ b/src/api-next/common/error.ts @@ -15,4 +15,7 @@ export interface IBApiNextError { /** Additional information in case of order rejection */ advancedOrderReject?: unknown; + + /** Warning/information only message, not an error */ + // isWarning: boolean; } diff --git a/src/api/contract/priceIncrement.ts b/src/api/contract/priceIncrement.ts new file mode 100644 index 00000000..b61be4a4 --- /dev/null +++ b/src/api/contract/priceIncrement.ts @@ -0,0 +1,7 @@ +/** + * Type describing price increment info + */ +export interface PriceIncrement { + lowEdge: number; + increment: number; +} diff --git a/src/common/errorCode.ts b/src/common/errorCode.ts index 88ae7956..37d6b254 100644 --- a/src/common/errorCode.ts +++ b/src/common/errorCode.ts @@ -1,8 +1,8 @@ /** * [[IBApi]] error event codes. */ -/* eslint-disable @typescript-eslint/no-duplicate-enum-values */ +/* eslint-disable @typescript-eslint/no-duplicate-enum-values */ export enum ErrorCode { /** No request id associated to this error */ NO_VALID_ID = -1, @@ -10,11 +10,11 @@ export enum ErrorCode { /** Order Canceled - reason: */ ORDER_CANCELLED = 202, - /** ou must subscribe for additional permissions to obtain precise results for scanner.Parameter:Most Active,Filter:Price;Real-Time Market Data:Pink Sheets */ - SCANNER_LOW_PRECISION = 492, + /** Order Message: BUY 1 SPY ARCA Warning: your order will not be placed at the exchange until 2024-12-02 04:00:00 US/Eastern (#399) */ + ORDER_MESSAGE = 399, - /** Already connected. */ - ALREADY_CONNECTED = 501, + /** You must subscribe for additional permissions to obtain precise results for scanner.Parameter:Most Active,Filter:Price;Real-Time Market Data:Pink Sheets */ + SCANNER_LOW_PRECISION = 492, /** Requested market data is not subscribed. Delayed market data is not available. */ REQ_MKT_DATA_NOT_AVAIL = 354, @@ -22,6 +22,9 @@ export enum ErrorCode { /** No trading permissions. */ NO_TRADING_PERMISSIONS = 460, + /** Already connected. */ + ALREADY_CONNECTED = 501, + /** * Couldn't connect to TWS. * @@ -301,3 +304,20 @@ export enum ErrorCode { /* News feed is not allowed. */ NEWS_FEED_NOT_ALLOWED = 10276, } + +/** + * Check if an error message is fatal (to the request execution) or only a warning/information message + */ +export const isNonFatalError = (code: ErrorCode, error: Error): boolean => { + if (code >= 2100 && code < 3000) return true; + if (error.message.includes("Warning:")) return true; + switch (code) { + case ErrorCode.PART_OF_REQUESTED_DATA_NOT_SUBSCRIBED: + case ErrorCode.DISPLAYING_DELAYED_DATA: + case ErrorCode.ORDER_MESSAGE: + case ErrorCode.SCANNER_LOW_PRECISION: + return true; + default: + return false; + } +}; diff --git a/src/core/api-next/subscription-registry.ts b/src/core/api-next/subscription-registry.ts index f57de1a9..7dae7cee 100644 --- a/src/core/api-next/subscription-registry.ts +++ b/src/core/api-next/subscription-registry.ts @@ -69,7 +69,7 @@ export class IBApiNextSubscriptionRegistry { ) {} /** A Map containing the subscription registry, with event name as key. */ - private readonly entires = new IBApiNextMap(); + private readonly entries = new IBApiNextMap(); /** * Register a subscription. @@ -81,8 +81,7 @@ export class IBApiNextSubscriptionRegistry { * the subscription instance. This can be used to avoid creation of multiple subscriptions, * that will end up on same TWS request (i.e. request same market data multiple times), but an * existing subscription instance will be re-used if same instanceId does already exist. - * As a general rule: don't use instanceId when there is no reqId or when you want to return - * a Promise (single emitted value). Use it everywhere else. + * As a general rule: don't use instanceId when there is a reqId. Use it everywhere else. */ register( requestFunction: (reqId: number) => void, @@ -102,7 +101,7 @@ export class IBApiNextSubscriptionRegistry { eventHandler.forEach((handler) => { const eventName = handler[0]; const callback = handler[1]; - const entry = this.entires.getOrAdd(eventName, () => { + const entry = this.entries.getOrAdd(eventName, () => { const entry = new RegistryEntry(eventName, callback); this.apiNext.logger.debug( LOG_TAG, @@ -156,7 +155,7 @@ export class IBApiNextSubscriptionRegistry { LOG_TAG, `Remove RegistryEntry for EventName.${entry.eventName}.`, ); - this.entires.delete(entry.eventName); + this.entries.delete(entry.eventName); } }); @@ -185,9 +184,9 @@ export class IBApiNextSubscriptionRegistry { /** * Dispatch an error into the subscription that owns the given request id. */ - dispatchError(reqId: number, error: IBApiNextError): void { - this.entires.forEach((entry) => { - entry.subscriptions.get(reqId)?.error(error); + dispatchError(error: IBApiNextError): void { + this.entries.forEach((entry) => { + entry.subscriptions.get(error.reqId)?.error(error); }); } } diff --git a/src/core/api-next/subscription.ts b/src/core/api-next/subscription.ts index cd0c0d8c..d3f547fb 100644 --- a/src/core/api-next/subscription.ts +++ b/src/core/api-next/subscription.ts @@ -87,7 +87,7 @@ export class IBApiNextSubscription { */ error(error: IBApiNextError): void { delete this._lastAllValue; - this.endEventReceived = false; + // this.endEventReceived = false; this.hasError = true; this.subject.error(error); this.cancelTwsSubscription(); @@ -124,17 +124,17 @@ export class IBApiNextSubscription { // request from TWS if first subscriber - if (this.observersCount === 0) { + if (this.observersCount++ === 0) { this.requestTwsSubscription(); } - this.observersCount++; + // this.observersCount++; moved into "if" condition above // handle unsubscribe return (): void => { subscription$.unsubscribe(); - this.observersCount--; - if (this.observersCount <= 0) { + // this.observersCount--; moved into "if" condition below + if (--this.observersCount <= 0) { this.cancelTwsSubscription(); this.cleanupFunction(); } diff --git a/src/core/io/controller.ts b/src/core/io/controller.ts index 0e37a14f..d78ab3e4 100644 --- a/src/core/io/controller.ts +++ b/src/core/io/controller.ts @@ -219,15 +219,14 @@ export class Controller implements EncoderCallbacks, DecoderCallbacks { emitError( errMsg: string, code: number, - reqId: number, + reqId?: number, advancedOrderReject?: unknown, ): void { - // if (advancedOrderReject) errMsg += ", advancedOrderReject: " + JSON.stringify(advancedOrderReject); this.emitEvent( EventName.error, new Error(errMsg), code, - reqId, + reqId ?? ErrorCode.NO_VALID_ID, advancedOrderReject, ); } @@ -254,10 +253,9 @@ export class Controller implements EncoderCallbacks, DecoderCallbacks { if (!this.socket.connected) { this.socket.connect(clientId); } else { - this.emitError( + this.emitInfo( "Cannot connect if already connected.", - ErrorCode.CONNECT_FAIL, - -1, + ErrorCode.ALREADY_CONNECTED, ); } } @@ -292,7 +290,7 @@ export class Controller implements EncoderCallbacks, DecoderCallbacks { this.emitError( "Cannot send data when disconnected.", ErrorCode.NOT_CONNECTED, - -1, + ErrorCode.NO_VALID_ID, ); } } diff --git a/src/core/io/decoder.ts b/src/core/io/decoder.ts index 70d5cbe5..574fa6c3 100644 --- a/src/core/io/decoder.ts +++ b/src/core/io/decoder.ts @@ -1,4 +1,4 @@ -import { IneligibilityReason } from "../.."; +import { IneligibilityReason, PriceIncrement } from "../.."; import { Contract } from "../../api/contract/contract"; import { ContractDescription } from "../../api/contract/contractDescription"; import { ContractDetails } from "../../api/contract/contractDetails"; @@ -107,7 +107,7 @@ export interface DecoderCallbacks { emitError( errMsg: string, code: number, - reqId: number, + reqId?: number, advancedOrderReject?: unknown, ): void; @@ -339,7 +339,6 @@ export class Decoder { this.callback.emitError( `No parser implementation found for token: ${IN_MSG_ID[msgId]} (${msgId}).`, ErrorCode.UNKNOWN_ID, - -1, ); } } @@ -386,7 +385,6 @@ export class Decoder { this.dataQueue, )}). Please report to https://github.com/stoqey/ib`, ErrorCode.UNKNOWN_ID, - -1, ); } @@ -401,7 +399,6 @@ export class Decoder { this.callback.emitError( `Underrun error on ${IN_MSG_ID[msgId]}: ${e.message} Please report to https://github.com/stoqey/ib`, ErrorCode.UNKNOWN_ID, - -1, ); } @@ -710,7 +707,7 @@ export class Decoder { const version = this.readInt(); if (version < 2) { const errorMsg = this.readStr(); - this.callback.emitError(errorMsg, -1, -1); + this.callback.emitError(errorMsg, ErrorCode.UNKNOWN_ID); } else { const id = this.readInt(); const code = this.readInt(); @@ -728,7 +725,7 @@ export class Decoder { } } - if (id === -1) { + if (id === ErrorCode.NO_VALID_ID) { this.callback.emitInfo(msg, code); } else { this.callback.emitError(msg, code, id, advancedOrderReject); @@ -1415,7 +1412,7 @@ export class Decoder { private decodeMsg_MARKET_RULE(): void { const marketRuleId = this.readInt(); const nPriceIncrements = this.readInt(); - const priceIncrements = new Array(nPriceIncrements); + const priceIncrements = new Array(nPriceIncrements); for (let i = 0; i < nPriceIncrements; i++) { priceIncrements[i] = { @@ -1529,7 +1526,7 @@ export class Decoder { * Decode a SCANNER_PARAMETERS message from data queue and emit a scannerParameters event. */ private decodeMsg_SCANNER_PARAMETERS(): void { - this.readInt(); // version + const _version = this.readInt(); const xml = this.readStr(); this.emit(EventName.scannerParameters, xml); diff --git a/src/core/io/encoder.ts b/src/core/io/encoder.ts index 94d5d059..ede5cb5a 100644 --- a/src/core/io/encoder.ts +++ b/src/core/io/encoder.ts @@ -202,11 +202,11 @@ export class Encoder { * @param errMsg The error test message. * @param data Additional error data (optional). */ - private emitError(errMsg: string, code: ErrorCode, reqId: number): void { + private emitError(errMsg: string, code: ErrorCode, reqId?: number): void { this.callback.emitError( `Server Version ${this.serverVersion}: ${errMsg}`, code, - reqId, + reqId ?? ErrorCode.NO_VALID_ID, ); } @@ -466,38 +466,38 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { /** * Encode a CANCEL_HISTORICAL_DATA message to an array of tokens. */ - cancelHistoricalData(tickerId: number): void { + cancelHistoricalData(reqId: number): void { if (this.serverVersion < 24) { return this.emitError( "It does not support historical data query cancellation.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } const version = 1; - this.sendMsg(OUT_MSG_ID.CANCEL_HISTORICAL_DATA, version, tickerId); + this.sendMsg(OUT_MSG_ID.CANCEL_HISTORICAL_DATA, version, reqId); } /** * Encode a CANCEL_MKT_DATA message to an array of tokens. */ - cancelMktData(tickerId: number): void { + cancelMktData(reqId: number): void { const version = 1; - this.sendMsg(OUT_MSG_ID.CANCEL_MKT_DATA, version, tickerId); + this.sendMsg(OUT_MSG_ID.CANCEL_MKT_DATA, version, reqId); } /** * Encode a CANCEL_MKT_DEPTH message to an array of tokens. */ - cancelMktDepth(tickerId: number, isSmartDepth: boolean): void { + cancelMktDepth(reqId: number, isSmartDepth: boolean): void { if (this.serverVersion < 6) { return this.emitError( "This feature is only available for versions of TWS >=6.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -505,13 +505,13 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support SMART depth cancel.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } const version = 1; - const tokens: unknown[] = [OUT_MSG_ID.CANCEL_MKT_DEPTH, version, tickerId]; + const tokens: unknown[] = [OUT_MSG_ID.CANCEL_MKT_DEPTH, version, reqId]; if (this.serverVersion >= MIN_SERVER_VER.SMART_DEPTH) { tokens.push(isSmartDepth); @@ -595,7 +595,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support position cancellation.", ErrorCode.UPDATE_TWS, - -1, ); } @@ -607,42 +606,42 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { /** * Encode a CANCEL_REAL_TIME_BARS message to an array of tokens. */ - cancelRealTimeBars(tickerId: number): void { + cancelRealTimeBars(reqId: number): void { if (this.serverVersion < MIN_SERVER_VER.REAL_TIME_BARS) { return this.emitError( "It does not support realtime bar data query cancellation.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } const version = 1; - this.sendMsg(OUT_MSG_ID.CANCEL_REAL_TIME_BARS, version, tickerId); + this.sendMsg(OUT_MSG_ID.CANCEL_REAL_TIME_BARS, version, reqId); } /** * Encode a CANCEL_SCANNER_SUBSCRIPTION message to an array of tokens. */ - cancelScannerSubscription(tickerId: number): void { + cancelScannerSubscription(reqId: number): void { if (this.serverVersion < 24) { return this.emitError( "It does not support API scanner subscription.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } const version = 1; - this.sendMsg(OUT_MSG_ID.CANCEL_SCANNER_SUBSCRIPTION, version, tickerId); + this.sendMsg(OUT_MSG_ID.CANCEL_SCANNER_SUBSCRIPTION, version, reqId); } /** * Encode a EXERCISE_OPTIONS message to an array of tokens. */ exerciseOptions( - tickerId: number, + reqId: number, contract: Contract, exerciseAction: OptionExerciseAction, exerciseQuantity: number, @@ -658,7 +657,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support options exercise from the API.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -667,7 +666,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support conId and tradingClass parameters in exerciseOptions.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } } @@ -679,7 +678,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support manual order time parameter in exerciseOptions.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -688,7 +687,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support customer account parameter in exerciseOptions.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } } @@ -698,12 +697,12 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support professional customer parameter in exerciseOptions.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } } - const tokens: unknown[] = [OUT_MSG_ID.EXERCISE_OPTIONS, version, tickerId]; + const tokens: unknown[] = [OUT_MSG_ID.EXERCISE_OPTIONS, version, reqId]; // send contract fields if (this.serverVersion >= MIN_SERVER_VER.TRADING_CLASS) { @@ -2123,7 +2122,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support current time requests.", ErrorCode.UPDATE_TWS, - -1, ); } @@ -2224,7 +2222,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support globalCancel requests.", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } @@ -2236,7 +2233,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support ext operator and manual order indicator parameters", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } } @@ -2262,7 +2258,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { * Encode a REQ_HISTORICAL_DATA message. */ reqHistoricalData( - tickerId: number, + reqId: number, contract: Contract, endDateTime: string, durationStr: string, @@ -2279,7 +2275,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support historical data backfill.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -2288,7 +2284,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support conId and tradingClass parameters in reqHistoricalData.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } } @@ -2301,7 +2297,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support requesting of historical schedule.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } } @@ -2312,7 +2308,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { tokens.push(version); } - tokens.push(tickerId); + tokens.push(reqId); // send contract fields if (this.serverVersion >= MIN_SERVER_VER.TRADING_CLASS) { @@ -2380,7 +2376,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { * Encode a REQ_HISTORICAL_TICKS message. */ reqHistoricalTicks( - tickerId: number, + reqId: number, contract: Contract, startDateTime: string, endDateTime: string, @@ -2394,13 +2390,13 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support historical ticks request.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } const args: unknown[] = [ OUT_MSG_ID.REQ_HISTORICAL_TICKS, - tickerId, + reqId, ...this.encodeContract(contract), ]; args.push(startDateTime); @@ -2523,7 +2519,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support marketDataType requests.", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } @@ -2536,7 +2531,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { * Encode a REQ_MKT_DATA message. */ reqMktData( - tickerId: number, + reqId: number, contract: Contract, genericTickList: string, snapshot: boolean, @@ -2546,7 +2541,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support snapshot market data requests.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -2554,7 +2549,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support delta-neutral orders.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -2562,7 +2557,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support conId parameter.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -2571,14 +2566,14 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support tradingClass parameter in reqMarketData.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } } const version = 11; - const args: unknown[] = [OUT_MSG_ID.REQ_MKT_DATA, version, tickerId]; + const args: unknown[] = [OUT_MSG_ID.REQ_MKT_DATA, version, reqId]; // send contract fields if (this.serverVersion >= MIN_SERVER_VER.REQ_MKT_DATA_CONID) { @@ -2668,7 +2663,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { * Encode a REQ_MKT_DEPTH message. */ reqMktDepth( - tickerId: number, + reqId: number, contract: Contract, numRows: number, isSmartDepth: boolean, @@ -2678,7 +2673,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "This feature is only available for versions of TWS >=6", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -2687,7 +2682,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support conId and tradingClass parameters in reqMktDepth.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } } @@ -2696,7 +2691,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support SMART depth request.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -2707,14 +2702,14 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support primaryExch parameter in reqMktDepth.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } const version = 5; // send req mkt data msg - const tokens: unknown[] = [OUT_MSG_ID.REQ_MKT_DEPTH, version, tickerId]; + const tokens: unknown[] = [OUT_MSG_ID.REQ_MKT_DEPTH, version, reqId]; // send contract fields if (this.serverVersion >= MIN_SERVER_VER.TRADING_CLASS) { @@ -2785,7 +2780,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support position requests.", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } @@ -2842,7 +2836,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { * Encode a REQ_REAL_TIME_BARS message. */ reqRealTimeBars( - tickerId: number, + reqId: number, contract: Contract, barSize: number, whatToShow: WhatToShow, @@ -2853,7 +2847,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support real time bars.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } @@ -2862,7 +2856,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support conId and tradingClass parameters in reqRealTimeBars.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } } @@ -2870,11 +2864,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { const version = 3; // send req mkt data msg - const tokens: unknown[] = [ - OUT_MSG_ID.REQ_REAL_TIME_BARS, - version, - tickerId, - ]; + const tokens: unknown[] = [OUT_MSG_ID.REQ_REAL_TIME_BARS, version, reqId]; // send contract fields if (this.serverVersion >= MIN_SERVER_VER.TRADING_CLASS) { @@ -2916,7 +2906,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support API scanner subscription.", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } @@ -3013,7 +3002,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "FA Profile is not supported anymore, use FA Group instead.", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } @@ -3118,7 +3106,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support family codes request.", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } @@ -3148,7 +3135,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support market depth exchanges request.", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } @@ -3209,7 +3195,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support smart components request.", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } @@ -3259,7 +3244,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { * Encode a REQ_HISTOGRAM_DATA message. */ reqHistogramData( - tickerId: number, + reqId: number, contract: Contract, useRTH: boolean, timePeriod: string, @@ -3268,13 +3253,13 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support histogram requests.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } this.sendMsg( OUT_MSG_ID.REQ_HISTOGRAM_DATA, - tickerId, + reqId, this.encodeContract(contract), useRTH ? 1 : 0, timePeriod, @@ -3284,31 +3269,31 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { /** * Encode a CANCEL_HISTOGRAM_DATA message. */ - cancelHistogramData(tickerId: number): void { + cancelHistogramData(reqId: number): void { if (this.serverVersion < MIN_SERVER_VER.REQ_HISTOGRAM) { return this.emitError( "It does not support head time stamp requests.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } - this.sendMsg(OUT_MSG_ID.CANCEL_HISTOGRAM_DATA, tickerId); + this.sendMsg(OUT_MSG_ID.CANCEL_HISTOGRAM_DATA, reqId); } /** * Encode a CANCEL_HISTOGRAM_DATA message. */ - cancelHeadTimestamp(tickerId: number): void { + cancelHeadTimestamp(reqId: number): void { if (this.serverVersion < MIN_SERVER_VER.CANCEL_HEADTIMESTAMP) { return this.emitError( "It does not support head time stamp requests canceling.", ErrorCode.UPDATE_TWS, - tickerId, + reqId, ); } - this.sendMsg(OUT_MSG_ID.CANCEL_HEAD_TIMESTAMP, tickerId); + this.sendMsg(OUT_MSG_ID.CANCEL_HEAD_TIMESTAMP, reqId); } /** @@ -3319,7 +3304,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support market rule requests.", ErrorCode.UPDATE_TWS, - marketRuleId, ); } @@ -3334,7 +3318,6 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { return this.emitError( "It does not support completed orders requests.", ErrorCode.UPDATE_TWS, - ErrorCode.NO_VALID_ID, ); } diff --git a/src/core/io/socket.ts b/src/core/io/socket.ts index cea5e48e..091873f8 100644 --- a/src/core/io/socket.ts +++ b/src/core/io/socket.ts @@ -188,6 +188,7 @@ export class Socket { // disconnect TCP socket. this.client?.end(); + this.client?.destroy(); } /** @@ -246,47 +247,32 @@ export class Socket { */ private onData(data: Buffer): void { if (this.useV100Plus) { - let dataToParse = data; - if (this._v100MessageBuffer.length > 0) { - dataToParse = Buffer.concat([this._v100MessageBuffer, data]); - } - if (dataToParse.length > MAX_V100_MESSAGE_LENGTH) { + this._v100MessageBuffer = Buffer.concat([this._v100MessageBuffer, data]); + if (this._v100MessageBuffer.length > MAX_V100_MESSAGE_LENGTH) { // At this point we have buffered enough data that we have exceeded the max known message length, // at which point this is likely an unrecoverable state and we should discard all prior data, // and disconnect the socket + const size = this._v100MessageBuffer.length; this._v100MessageBuffer = Buffer.alloc(0); this.onError( new Error( - `Message of size ${dataToParse.length} exceeded max message length ${MAX_V100_MESSAGE_LENGTH}`, + `Message of size ${size} exceeded max message length ${MAX_V100_MESSAGE_LENGTH}`, ), ); this.disconnect(); return; } - let messageBufferOffset = 0; - while (messageBufferOffset + 4 < dataToParse.length) { - let currentMessageOffset = messageBufferOffset; - const msgSize = dataToParse.readInt32BE(currentMessageOffset); - currentMessageOffset += 4; - if (currentMessageOffset + msgSize <= dataToParse.length) { - const segment = dataToParse.slice( - currentMessageOffset, - currentMessageOffset + msgSize, - ); - currentMessageOffset += msgSize; + while (this._v100MessageBuffer.length > 4) { + const msgSize = this._v100MessageBuffer.readInt32BE(); + if (this._v100MessageBuffer.length >= 4 + msgSize) { + const segment = this._v100MessageBuffer.slice(4, 4 + msgSize); + this._v100MessageBuffer = this._v100MessageBuffer.slice(4 + msgSize); this.onMessage(segment.toString("utf8")); - messageBufferOffset = currentMessageOffset; } else { - // We can't parse further, the message is incomplete - break; + // else keep data for later + return; } } - if (messageBufferOffset != dataToParse.length) { - // There is data left in the buffer, save it for the next data packet - this._v100MessageBuffer = dataToParse.slice(messageBufferOffset); - } else { - this._v100MessageBuffer = Buffer.alloc(0); - } } else { this.onMessage(data.toString()); } @@ -356,9 +342,8 @@ export class Socket { ) { this.disconnect(); this.controller.emitError( - "Unsupported Version", + `Unsupported Version ${this._serverVersion}`, ErrorCode.UNSUPPORTED_VERSION, - -1, ); return; } @@ -368,7 +353,6 @@ export class Socket { this.controller.emitError( "The TWS is out of date and must be upgraded.", ErrorCode.UPDATE_TWS, - -1, ); return; } @@ -447,7 +431,7 @@ export class Socket { * Called when an error occurred on the TCP socket connection. */ private onError(err: Error): void { - this.controller.emitError(err.message, ErrorCode.CONNECT_FAIL, -1); + this.controller.emitError(err.message, ErrorCode.CONNECT_FAIL); } /** diff --git a/src/index.ts b/src/index.ts index b397a999..cb3c2ac2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,7 @@ import { IBApi } from "./api/api"; export { IBApi, IBApiCreationOptions } from "./api/api"; -export { ErrorCode } from "./common/errorCode"; +export { ErrorCode, isNonFatalError } from "./common/errorCode"; // export contract types @@ -50,6 +50,7 @@ export { Future } from "./api/contract/future"; export { Index } from "./api/contract/ind"; export { IneligibilityReason } from "./api/contract/ineligibilityReason"; export { Option } from "./api/contract/option"; +export { PriceIncrement } from "./api/contract/priceIncrement"; export { Stock } from "./api/contract/stock"; export { WshEventData } from "./api/contract/wsh"; diff --git a/src/tests/unit/api-next-live/get-contract-details.test.ts b/src/tests/unit/api-next-live/get-contract-details.test.ts index ec6f7c0d..b643926d 100644 --- a/src/tests/unit/api-next-live/get-contract-details.test.ts +++ b/src/tests/unit/api-next-live/get-contract-details.test.ts @@ -4,6 +4,7 @@ import { Subscription } from "rxjs"; import { IBApiNext, IBApiNextError } from "../../.."; +import logger from "../../../common/logger"; import { sample_bond, sample_crypto, @@ -26,9 +27,9 @@ describe("ApiNext: getContractDetails()", () => { if (!error$) { error$ = api.errorSubject.subscribe((error) => { if (error.reqId === -1) { - console.warn(`${error.error.message} (Error #${error.code})`); + logger.warn(`${error.error.message} (Error #${error.code})`); } else { - console.error( + logger.error( `${error.error.message} (Error #${error.code}) ${ error.advancedOrderReject ? error.advancedOrderReject : "" }`, @@ -40,7 +41,7 @@ describe("ApiNext: getContractDetails()", () => { try { api.connect(clientId); } catch (error) { - console.error(error.message); + logger.error(error.message); } }); @@ -140,22 +141,4 @@ describe("ApiNext: getContractDetails()", () => { ); }); }); - - test("Crypto contract details", (done) => { - const ref_contract = sample_crypto; - - api - .getContractDetails(ref_contract) - .then((result) => { - expect(result.length).toBeGreaterThan(0); - expect(result[0].contract.symbol).toEqual(ref_contract.symbol); - expect(result[0].contract.secType).toEqual(ref_contract.secType); - done(); - }) - .catch((err: IBApiNextError) => { - done( - `getContractDetails failed with '${err.error.message}' (Error #${err.code})`, - ); - }); - }); }); diff --git a/src/tests/unit/api-next-live/get-current-time.test.ts b/src/tests/unit/api-next-live/get-current-time.test.ts new file mode 100644 index 00000000..f16b1271 --- /dev/null +++ b/src/tests/unit/api-next-live/get-current-time.test.ts @@ -0,0 +1,77 @@ +/** + * This file implements tests for the [[IBApiNext.getCurrentTime]] function. + */ + +import { Subscription } from "rxjs"; +import { IBApiNext, IBApiNextError, isNonFatalError } from "../../.."; +import logger from "../../../common/logger"; + +describe("ApiNext: getCurrentTime()", () => { + jest.setTimeout(5_000); + + let clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client + + const api: IBApiNext = new IBApiNext(); + + const _error$: Subscription = api.errorSubject.subscribe((error) => { + if (isNonFatalError(error.code, error.error)) { + logger.warn(`${error.error.message} (Error #${error.code})`); + } else { + logger.error( + `${error.error.message} (Error #${error.code}) ${ + error.advancedOrderReject ? error.advancedOrderReject : "" + }`, + ); + } + }); + + beforeEach(() => { + api.connect(clientId++); + }); + + afterEach(() => { + api.disconnect(); + }); + + test("getCurrentTime once", (done) => { + api + .getCurrentTime() + .then((result) => { + logger.info(result); + done(); + }) + .catch((err: IBApiNextError) => { + done( + `getCurrentTime failed with '${err.error.message}' (Error #${err.code})`, + ); + }); + }); + + test("getCurrentTime twice", (done) => { + const p1 = api.getCurrentTime(); + const p2 = api.getCurrentTime(); + + Promise.all([p1, p2]) + .then((result) => { + logger.info(result[0], result[1]); + done(); + }) + .catch((err: IBApiNextError) => { + done( + `getCurrentTime failed with '${err.error?.message}' (Error #${err.code})`, + ); + }); + }); + + test("getCurrentTime n times", (done) => { + const n = 10; + const p: Promise[] = []; + for (let i = 0; i < n; i++) p.push(api.getCurrentTime()); + + Promise.all(p).then((result) => { + logger.info(result); + expect(result.length).toBe(n); + done(); + }); + }); +}); diff --git a/src/tests/unit/api-next-live/get-historical-ticks.test.ts b/src/tests/unit/api-next-live/get-historical-ticks.test.ts index 1cd2815d..cec96537 100644 --- a/src/tests/unit/api-next-live/get-historical-ticks.test.ts +++ b/src/tests/unit/api-next-live/get-historical-ticks.test.ts @@ -4,6 +4,7 @@ import { Subscription } from "rxjs"; import { IBApiNext } from "../../.."; +import logger from "../../../common/logger"; import { sample_etf } from "../sample-data/contracts"; describe("ApiNext: getContractDetails()", () => { @@ -20,9 +21,9 @@ describe("ApiNext: getContractDetails()", () => { if (!error$) { error$ = api.errorSubject.subscribe((error) => { if (error.reqId === -1) { - console.warn(`${error.error.message} (Error #${error.code})`); + logger.warn(`${error.error.message} (Error #${error.code})`); } else { - console.error( + logger.error( `${error.error.message} (Error #${error.code}) ${ error.advancedOrderReject ? error.advancedOrderReject : "" }`, @@ -34,7 +35,7 @@ describe("ApiNext: getContractDetails()", () => { try { api.connect(clientId); } catch (error) { - console.error(error.message); + logger.error(error.message); } }); diff --git a/src/tests/unit/api-next-live/get-managed-accounts.test.ts b/src/tests/unit/api-next-live/get-managed-accounts.test.ts new file mode 100644 index 00000000..05b0a30e --- /dev/null +++ b/src/tests/unit/api-next-live/get-managed-accounts.test.ts @@ -0,0 +1,55 @@ +/** + * This file implements tests for the [[IBApiNext.getManagedAccounts]] function. + */ + +import { Subscription } from "rxjs"; +import { IBApiNext, isNonFatalError } from "../../.."; +import logger from "../../../common/logger"; + +describe("ApiNext: getManagedAccounts()", () => { + jest.setTimeout(5_000); + + let clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client + + const api: IBApiNext = new IBApiNext(); + + const _error$: Subscription = api.errorSubject.subscribe((error) => { + if (isNonFatalError(error.code, error.error)) { + logger.warn(`${error.error.message} (Error #${error.code})`); + } else { + logger.error( + `${error.error.message} (Error #${error.code}) ${ + error.advancedOrderReject ? error.advancedOrderReject : "" + }`, + ); + } + }); + + beforeEach(() => { + api.connect(clientId++); + }); + + afterEach(() => { + api.disconnect(); + }); + + test("getManagedAccounts once", (done) => { + api.getManagedAccounts().then((result) => { + expect(result.length).toBeGreaterThan(0); + logger.info(result); + done(); + }); + }); + + test("getManagedAccounts n times", (done) => { + const n = 10; + const p: Promise[] = []; + for (let i = 0; i < n; i++) p.push(api.getManagedAccounts()); + + Promise.all(p).then((result) => { + logger.info(result); + expect(result.length).toBe(n); + done(); + }); + }); +}); diff --git a/src/tests/unit/api-next-live/get-market-rule.test.ts b/src/tests/unit/api-next-live/get-market-rule.test.ts new file mode 100644 index 00000000..59653e3d --- /dev/null +++ b/src/tests/unit/api-next-live/get-market-rule.test.ts @@ -0,0 +1,73 @@ +/** + * This file implements tests for the [[IBApiNext.getMarketRule]] function. + */ + +import { Subscription } from "rxjs"; +import { IBApiNext, isNonFatalError, PriceIncrement } from "../../.."; +import logger from "../../../common/logger"; + +describe("ApiNext: getMarketRule()", () => { + jest.setTimeout(5_000); + + let clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client + + const api: IBApiNext = new IBApiNext(); + + const _error$: Subscription = api.errorSubject.subscribe((error) => { + if (isNonFatalError(error.code, error.error)) { + logger.warn(`${error.error.message} (Error #${error.code})`); + } else { + logger.error( + `${error.error.message} (Error #${error.code}) ${ + error.advancedOrderReject ? error.advancedOrderReject : "" + }`, + ); + } + }); + + beforeEach(() => { + api.connect(clientId++); + }); + + afterEach(() => { + api.disconnect(); + }); + + test("getMarketRule once", (done) => { + const p: Promise[] = []; + + p.push(api.getMarketRule(26)); + p.push(api.getMarketRule(32)); + p.push(api.getMarketRule(67)); + p.push(api.getMarketRule(635)); + p.push(api.getMarketRule(2806)); + + Promise.all(p).then((result) => { + expect(result.length).toBe(5); + expect(result[0][0].lowEdge).toBe(0); + expect(result[0][0].increment).toBe(0.01); + expect(result[1][0].lowEdge).toBe(0); + expect(result[1][0].increment).toBe(0.01); + expect(result[2][0].lowEdge).toBe(0); + expect(result[2][0].increment).toBe(0.25); + expect(result[3][0].lowEdge).toBe(0); + expect(result[3][0].increment).toBe(0.0001); + expect(result[4][0].lowEdge).toBe(0); + expect(result[4][0].increment).toBe(0.25); + logger.info(result); + done(); + }); + }); + + test("getMarketRule n times", (done) => { + const n = 10; + const p: Promise[] = []; + for (let i = 0; i < n; i++) p.push(api.getMarketRule(26)); + + Promise.all(p).then((result) => { + logger.info(result); + expect(result.length).toBe(n); + done(); + }); + }); +}); diff --git a/src/tests/unit/api-next-live/get-user-info.test.ts b/src/tests/unit/api-next-live/get-user-info.test.ts new file mode 100644 index 00000000..963faf17 --- /dev/null +++ b/src/tests/unit/api-next-live/get-user-info.test.ts @@ -0,0 +1,59 @@ +/** + * This file implements tests for the [[IBApiNext.getUserInfo]] function. + */ + +import { Subscription } from "rxjs"; +import { IBApiNext, isNonFatalError } from "../../.."; +import logger from "../../../common/logger"; + +describe("ApiNext: getManagedAccounts()", () => { + jest.setTimeout(5_000); + + let clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client + + const api: IBApiNext = new IBApiNext(); + + const _error$: Subscription = api.errorSubject.subscribe((error) => { + if (isNonFatalError(error.code, error.error)) { + logger.warn(`${error.error.message} (Error #${error.code})`); + } else { + logger.error( + `${error.error.message} (Error #${error.code}) ${ + error.advancedOrderReject ? error.advancedOrderReject : "" + }`, + ); + } + }); + + beforeEach(() => { + api.connect(clientId++); + }); + + afterEach(() => { + api.disconnect(); + }); + + test("getUserInfo once", (done) => { + const p: Promise[] = []; + + p.push(api.getUserInfo()); + + Promise.all(p).then((result) => { + expect(result.length).toBeGreaterThan(0); + logger.info(result); + done(); + }); + }); + + test("getUserInfo n times", (done) => { + const n = 10; + const p: Promise[] = []; + for (let i = 0; i < n; i++) p.push(api.getUserInfo()); + + Promise.all(p).then((result) => { + logger.info(result); + expect(result.length).toBe(n); + done(); + }); + }); +}); diff --git a/src/tests/unit/api-next-live/subscription-registry.test.ts b/src/tests/unit/api-next-live/subscription-registry.test.ts index 0898796d..e9aae51d 100644 --- a/src/tests/unit/api-next-live/subscription-registry.test.ts +++ b/src/tests/unit/api-next-live/subscription-registry.test.ts @@ -1,5 +1,6 @@ import { Subscription } from "rxjs"; import { IBApiNext, IBApiNextError } from "../../.."; +import logger from "../../../common/logger"; describe("Subscription registry Tests", () => { jest.setTimeout(2_000); @@ -16,9 +17,9 @@ describe("Subscription registry Tests", () => { if (!error$) { error$ = api.errorSubject.subscribe((error) => { if (error.reqId === -1) { - console.warn(`${error.error.message} (Error #${error.code})`); + logger.warn(`${error.error.message} (Error #${error.code})`); } else { - console.error( + logger.error( `${error.error.message} (Error #${error.code}) ${ error.advancedOrderReject ? error.advancedOrderReject : "" }`, @@ -30,7 +31,7 @@ describe("Subscription registry Tests", () => { try { api.connect(clientId); } catch (error) { - console.error(error.message); + logger.error(error.message); } }); @@ -48,22 +49,22 @@ describe("Subscription registry Tests", () => { // console.log(data); }, complete: () => { - console.log("getOpenOrders completed."); + logger.info("getOpenOrders completed."); done(); }, error: (err: IBApiNextError) => { - console.error(`getOpenOrders failed with '${err.error.message}'`); + logger.error(`getOpenOrders failed with '${err.error.message}'`); }, }); api .getAllOpenOrders() .then((orders) => { - console.log(orders); + logger.info(orders); subscription$.unsubscribe(); }) .catch((err: IBApiNextError) => { - console.error(`getAllOpenOrders failed with '${err}'`); + logger.error(`getAllOpenOrders failed with '${err}'`); }); }); }); diff --git a/src/tests/unit/api-next/get-account-summary.test.ts b/src/tests/unit/api-next/get-account-summary.test.ts index 44e1d093..661451cb 100644 --- a/src/tests/unit/api-next/get-account-summary.test.ts +++ b/src/tests/unit/api-next/get-account-summary.test.ts @@ -37,7 +37,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { // testing values const accountId1 = "DU123456"; - const accountId2 = "DU123456"; + const accountId2 = "DU654321"; const currency = "USD"; const testValueReqId1 = "1111111"; diff --git a/src/tests/unit/api-next/get-account-updates.test.ts b/src/tests/unit/api-next/get-account-updates.test.ts new file mode 100644 index 00000000..862d9414 --- /dev/null +++ b/src/tests/unit/api-next/get-account-updates.test.ts @@ -0,0 +1,149 @@ +/** + * This file implements tests for the [[IBApiNext.getAccountSummary]] function. + */ + +import { EventName, IBApi, IBApiNext, IBApiNextError } from "../../.."; +import { sample_etf, sample_stock } from "../sample-data/contracts"; + +describe("RxJS Wrapper: getAccountUpdates()", () => { + test("Update multicast", (done) => { + const apiNext = new IBApiNext(); + const api = (apiNext as unknown as Record).api as IBApi; + + // testing values + const accountId1 = "DU123456"; + const accountId2 = "DU654321"; + const currency = "USD"; + + apiNext + .getAccountUpdates() + // eslint-disable-next-line rxjs/no-ignored-subscription + .subscribe({ + next: (update) => { + if (update.changed?.timestamp == "now") { + expect(update.all.value.get(accountId1)).toBeDefined(); + expect( + update.all.value.get(accountId1).get("tag").get(currency).value, + ).toBe("value1"); + expect(update.all.portfolio.get(accountId1)).toBeDefined(); + expect(update.all.portfolio.get(accountId1)[0].account).toBe( + accountId1, + ); + expect( + update.all.portfolio.get(accountId1)[0].contract.symbol, + ).toBe(sample_etf.symbol); + + expect(update.all.value.get(accountId2)).toBeDefined(); + expect( + update.all.value.get(accountId2).get("tag").get(currency).value, + ).toBe("value2"); + expect(update.all.portfolio.get(accountId2)).toBeDefined(); + expect(update.all.portfolio.get(accountId2)[0].account).toBe( + accountId2, + ); + expect( + update.all.portfolio.get(accountId2)[0].contract.symbol, + ).toBe(sample_stock.symbol); + } else if (update.changed?.timestamp == "later") done(); + }, + error: (error: IBApiNextError) => { + fail(error.error.message); + }, + }); + + apiNext + .getAccountUpdates(accountId1) + // eslint-disable-next-line rxjs/no-ignored-subscription + .subscribe({ + next: (update) => { + if (update.changed?.timestamp == "now") { + expect(update.all.value.get(accountId1)).toBeDefined(); + expect( + update.all.value.get(accountId1).get("tag").get(currency).value, + ).toBe("value1"); + expect(update.all.portfolio.get(accountId1)).toBeDefined(); + expect(update.all.portfolio.get(accountId1)[0].account).toBe( + accountId1, + ); + expect( + update.all.portfolio.get(accountId1)[0].contract.symbol, + ).toBe(sample_etf.symbol); + + expect(update.all.value.get(accountId2)).toBeUndefined(); + } + }, + error: (error: IBApiNextError) => { + fail(error.error.message); + }, + }); + + apiNext + .getAccountUpdates(accountId2) + // eslint-disable-next-line rxjs/no-ignored-subscription + .subscribe({ + next: (update) => { + if (update.changed?.timestamp == "now") { + expect(update.all.value.get(accountId1)).toBeUndefined(); + + expect(update.all.value.get(accountId2)).toBeDefined(); + expect( + update.all.value.get(accountId2).get("tag").get(currency).value, + ).toBe("value2"); + expect(update.all.portfolio.get(accountId2)).toBeDefined(); + expect(update.all.portfolio.get(accountId2)[0].account).toBe( + accountId2, + ); + expect( + update.all.portfolio.get(accountId2)[0].contract.symbol, + ).toBe(sample_stock.symbol); + } + }, + error: (error: IBApiNextError) => { + fail(error.error.message); + }, + }); + + api.emit( + EventName.updateAccountValue, + "tag", + "value1", + currency, + accountId1, + ); + api.emit( + EventName.updatePortfolio, + sample_etf, + 1, + 10, + 100, + 9, + 10, + 0, + accountId1, + ); + api.emit(EventName.accountDownloadEnd, accountId1); + + api.emit( + EventName.updateAccountValue, + "tag", + "value2", + currency, + accountId2, + ); + api.emit( + EventName.updatePortfolio, + sample_stock, + 1, + 10, + 100, + 9, + 10, + 0, + accountId2, + ); + api.emit(EventName.accountDownloadEnd, accountId2); + + api.emit(EventName.updateAccountTime, "now"); + api.emit(EventName.updateAccountTime, "later"); + }); +}); diff --git a/src/tests/unit/api-next/get-historical-data.test.ts b/src/tests/unit/api-next/get-historical-data.test.ts index bfb37068..7a1609f3 100644 --- a/src/tests/unit/api-next/get-historical-data.test.ts +++ b/src/tests/unit/api-next/get-historical-data.test.ts @@ -4,6 +4,7 @@ import { Bar, + ErrorCode, EventName, IBApi, IBApiNext, @@ -135,6 +136,6 @@ describe("RxJS Wrapper: getHistoricalData()", () => { done(); }); - api.emit(EventName.error, {}, -1, 1); + api.emit(EventName.error, new Error(), ErrorCode.UNSUPPORTED_VERSION, 1); }); }); diff --git a/src/tests/unit/api-next/next-valid-order-id.test.ts b/src/tests/unit/api-next/next-valid-order-id.test.ts index 8e6b737c..a790d682 100644 --- a/src/tests/unit/api-next/next-valid-order-id.test.ts +++ b/src/tests/unit/api-next/next-valid-order-id.test.ts @@ -2,7 +2,7 @@ * This file implements tests for the [[IBApiNext.getCurrentTime]] function. */ -import { IBApi, IBApiNext, IBApiNextError, EventName } from "../../.."; +import { EventName, IBApi, IBApiNext, IBApiNextError } from "../../.."; describe("RxJS Wrapper: getNextValidOrderId()", () => { test("Promise result", (done) => { @@ -13,7 +13,7 @@ describe("RxJS Wrapper: getNextValidOrderId()", () => { // emit a EventName.nextValidId and verify RxJS result - const testValue = Math.random(); + const testValue = Math.ceil(Math.random() * 10_000); apiNext .getNextValidOrderId() diff --git a/src/tests/unit/api/historical-data.test.ts b/src/tests/unit/api/historical-data.test.ts index f284ebb4..91918211 100644 --- a/src/tests/unit/api/historical-data.test.ts +++ b/src/tests/unit/api/historical-data.test.ts @@ -5,11 +5,13 @@ import { BarSizeSetting, EventName, IBApi, + isNonFatalError, Option, OptionType, WhatToShow, } from "../../.."; import configuration from "../../../common/configuration"; +import logger from "../../../common/logger"; import { sample_etf } from "../sample-data/contracts"; describe("IBApi Historical data Tests", () => { @@ -24,7 +26,6 @@ describe("IBApi Historical data Tests", () => { port: configuration.ib_port, clientId, }); - // logger.info("IBApi created"); }); afterEach(() => { @@ -32,7 +33,6 @@ describe("IBApi Historical data Tests", () => { ib.disconnect(); ib = undefined; } - // logger.info("IBApi disconnected"); }); it("Stock market data", (done) => { @@ -51,52 +51,53 @@ describe("IBApi Historical data Tests", () => { 2, false, ); - }) - .on( - EventName.historicalData, - ( - reqId: number, - time: string, - open: number, - high: number, - low: number, - close: number, - volume: number, - count: number | undefined, - WAP: number, - ) => { - // console.log( - // counter, - // time, - // open, - // high, - // low, - // close, - // volume, - // count, - // WAP, - // ); - expect(reqId).toEqual(refId); - if (time.startsWith("finished")) { - expect(counter).toEqual(30); - done(); - } else if (counter++ == 29) { - expect(time).toEqual("1696622399"); - expect(open).toEqual(429.5); - expect(high).toEqual(429.6); - expect(low).toEqual(429.47); - expect(close).toEqual(429.51); - expect(volume).toEqual(3487.38); - expect(count).toEqual(1090); - expect(WAP).toEqual(429.532); - } - }, - ) - .on(EventName.error, (err, code, reqId) => { - if (reqId == refId) done(`[${reqId}] ${err.message} (#${code})`); - }); + }).on( + EventName.historicalData, + ( + reqId: number, + time: string, + open: number, + high: number, + low: number, + close: number, + volume: number, + count: number | undefined, + WAP: number, + ) => { + // console.log( + // counter, + // time, + // open, + // high, + // low, + // close, + // volume, + // count, + // WAP, + // ); + expect(reqId).toEqual(refId); + if (time.startsWith("finished")) { + expect(counter).toEqual(30); + done(); + } else if (counter++ == 29) { + expect(time).toEqual("1696622399"); + expect(open).toEqual(429.5); + expect(high).toEqual(429.6); + expect(low).toEqual(429.47); + expect(close).toEqual(429.51); + expect(volume).toEqual(3487.38); + expect(count).toEqual(1090); + expect(WAP).toEqual(429.532); + } + }, + ); - ib.connect(); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) ? logger.warn(msg) : done(msg); + }) + .connect(); }); test("Option market data", (done) => { @@ -152,11 +153,10 @@ describe("IBApi Historical data Tests", () => { }, ); - ib.on(EventName.disconnected, () => done()) - .on(EventName.info, (msg, code) => console.info("INFO", code, msg)) - .on(EventName.error, (err, code, reqId) => { - if (reqId > 0) done(`[${reqId}] ${err.message} (#${code})`); - else console.error("ERROR", err.message, code, reqId); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) ? logger.warn(msg) : done(msg); }) .connect(); }); @@ -177,52 +177,53 @@ describe("IBApi Historical data Tests", () => { 2, false, ); - }) - .on( - EventName.historicalData, - ( - reqId: number, - time: string, - open: number, - high: number, - low: number, - close: number, - volume: number, - count: number | undefined, - WAP: number, - ) => { - // console.log( - // counter, - // time, - // open, - // high, - // low, - // close, - // volume, - // count, - // WAP, - // ); - expect(reqId).toEqual(refId); - if (time.startsWith("finished")) { - expect(counter).toEqual(5); - done(); - } else if (counter++ == 4) { - expect(time).toEqual("20230901"); - expect(open).toEqual(437.3); - expect(high).toEqual(453.67); - expect(low).toEqual(437.3); - expect(close).toEqual(450.92); - expect(volume).toEqual(2771783.24); - expect(count).toEqual(1393264); - expect(WAP).toEqual(448.476); - } - }, - ) - .on(EventName.error, (err, code, reqId) => { - if (reqId == refId) done(`[${reqId}] ${err.message} (#${code})`); - }); + }).on( + EventName.historicalData, + ( + reqId: number, + time: string, + open: number, + high: number, + low: number, + close: number, + volume: number, + count: number | undefined, + WAP: number, + ) => { + // console.log( + // counter, + // time, + // open, + // high, + // low, + // close, + // volume, + // count, + // WAP, + // ); + expect(reqId).toEqual(refId); + if (time.startsWith("finished")) { + expect(counter).toEqual(5); + done(); + } else if (counter++ == 4) { + expect(time).toEqual("20230901"); + expect(open).toEqual(437.3); + expect(high).toEqual(453.67); + expect(low).toEqual(437.3); + expect(close).toEqual(450.92); + expect(volume).toEqual(2771783.24); + expect(count).toEqual(1393264); + expect(WAP).toEqual(448.476); + } + }, + ); - ib.connect(); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) ? logger.warn(msg) : done(msg); + }) + .connect(); }); it("Monthly market data", (done) => { @@ -241,81 +242,79 @@ describe("IBApi Historical data Tests", () => { 2, false, ); - }) - .on( - EventName.historicalData, - ( - reqId: number, - time: string, - open: number, - high: number, - low: number, - close: number, - volume: number, - count: number | undefined, - WAP: number, - ) => { - // console.log( - // counter, - // time, - // open, - // high, - // low, - // close, - // volume, - // count, - // WAP, - // ); - expect(reqId).toEqual(refId); - if (time.startsWith("finished")) { - expect(counter).toEqual(13); - done(); - } else if (counter++ == 12) { - expect(time).toEqual("20230901"); - expect(open).toEqual(451.53); - expect(high).toEqual(453.67); - expect(low).toEqual(449.68); - expect(close).toEqual(450.92); - expect(volume).toEqual(474058.9); - expect(count).toEqual(248346); - expect(WAP).toEqual(451.3); - } - }, - ) - .on(EventName.error, (err, code, reqId) => { - if (reqId == refId) done(`[${reqId}] ${err.message} (#${code})`); - }); + }).on( + EventName.historicalData, + ( + reqId: number, + time: string, + open: number, + high: number, + low: number, + close: number, + volume: number, + count: number | undefined, + WAP: number, + ) => { + // console.log( + // counter, + // time, + // open, + // high, + // low, + // close, + // volume, + // count, + // WAP, + // ); + expect(reqId).toEqual(refId); + if (time.startsWith("finished")) { + expect(counter).toEqual(13); + done(); + } else if (counter++ == 12) { + expect(time).toEqual("20230901"); + expect(open).toEqual(451.53); + expect(high).toEqual(453.67); + expect(low).toEqual(449.68); + expect(close).toEqual(450.92); + expect(volume).toEqual(474058.9); + expect(count).toEqual(248346); + expect(WAP).toEqual(451.3); + } + }, + ); - ib.connect(); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) ? logger.warn(msg) : done(msg); + }) + .connect(); }); it("Test request tick history", (done) => { const refId = 45; - let isConnected = false; ib.on(EventName.connected, () => { - isConnected = true; - }) - .on(EventName.historicalTicksLast, (reqId: number, ticks: []) => { - expect(ticks.length).toBeGreaterThan(0); - if (isConnected) { - ib.disconnect(); - } - done(); - }) - .on(EventName.error, (err, code, reqId) => { - if (reqId == refId) done(`[${reqId}] ${err.message} (#${code})`); - }); + ib.reqHistoricalTicks( + refId, + sample_etf, + "20240508-17:00:00", + null, + 10, + WhatToShow.TRADES, + 0, + true, + ); + }).on(EventName.historicalTicksLast, (reqId: number, ticks: []) => { + expect(ticks.length).toBeGreaterThan(0); + done(); + }); - ib.connect().reqHistoricalTicks( - refId, - sample_etf, - "20240508-17:00:00", - null, - 10, - WhatToShow.TRADES, - 0, - true, - ); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) ? logger.warn(msg) : done(msg); + }) + .connect(); }); }); diff --git a/src/tests/unit/api/market-scanner.test.ts b/src/tests/unit/api/market-scanner.test.ts index e743234d..1ccecb26 100644 --- a/src/tests/unit/api/market-scanner.test.ts +++ b/src/tests/unit/api/market-scanner.test.ts @@ -1,16 +1,17 @@ import { ContractDetails, - ErrorCode, EventName, IBApi, Instrument, + isNonFatalError, LocationCode, ScanCode, } from "../../.."; import configuration from "../../../common/configuration"; +import logger from "../../../common/logger"; describe("IBApi market scanner tests", () => { - jest.setTimeout(10 * 1000); + jest.setTimeout(10_000); let ib: IBApi; const clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client @@ -24,31 +25,29 @@ describe("IBApi market scanner tests", () => { }); afterEach(() => { - if (ib) { - ib.disconnect(); - ib = undefined; - } + ib.disconnect(); }); test("Scanner parameters", (done) => { - ib.on(EventName.scannerParameters, (xml: string) => { + ib.once(EventName.connected, () => { + ib.reqScannerParameters(); + }).on(EventName.scannerParameters, (xml: string) => { const match = ''; // eslint-disable-line quotes expect(xml.substring(0, match.length)).toEqual(match); - ib.disconnect(); - }) - .on(EventName.disconnected, () => { - done(); - }) - .on(EventName.error, (err, code: ErrorCode, reqId) => { - if (reqId !== -1) done(`[${reqId}] ${err.message} (#${code})`); - }); + done(); + }); - ib.connect().reqScannerParameters(); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) ? logger.warn(msg) : logger.error(msg); + }) + .connect(); }); test("Most active US stocks", (done) => { const refId = 1; - ib.once(EventName.nextValidId, (_reqId) => { + ib.once(EventName.connected, () => { ib.reqScannerSubscription(refId, { abovePrice: 1, scanCode: ScanCode.MOST_ACTIVE, @@ -73,18 +72,14 @@ describe("IBApi market scanner tests", () => { ) .on(EventName.scannerDataEnd, (reqId) => { expect(reqId).toEqual(refId); - if (ib) ib.disconnect(); - }) - .on(EventName.disconnected, () => { done(); - }) - .on(EventName.error, (err, code, reqId) => { - if (reqId == refId) { - if (code == ErrorCode.SCANNER_LOW_PRECISION) return; - done(`[${reqId}] ${err.message} (#${code})`); - } }); - ib.connect(); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) ? logger.warn(msg) : logger.error(msg); + }) + .connect(); }); }); diff --git a/src/tests/unit/api/order/cancelOrder.test.ts b/src/tests/unit/api/order/cancelOrder.test.ts index ed0b4bda..f4019b88 100644 --- a/src/tests/unit/api/order/cancelOrder.test.ts +++ b/src/tests/unit/api/order/cancelOrder.test.ts @@ -6,6 +6,7 @@ import { ErrorCode, EventName, IBApi, + isNonFatalError, Order, OrderAction, OrderStatus, @@ -58,7 +59,7 @@ describe("CancelOrder", () => { let isDone = false; ib.once(EventName.nextValidId, (orderId: number) => { refId = orderId; - ib.placeOrder(refId, contract, order); + ib.reqOpenOrders().placeOrder(refId, contract, order); }) .on( EventName.orderStatus, @@ -100,37 +101,30 @@ describe("CancelOrder", () => { } }, ) - .on( - EventName.error, - ( - error: Error, - code: ErrorCode, - reqId: number, - _advancedOrderReject?: unknown, - ) => { - if (reqId === -1) { - logger.info(error.message); - } else { - const msg = `[${reqId}] ${error.message} (Error #${code})`; - if (error.message.includes("Warning:")) { - logger.warn(msg); - } else if ( - code == ErrorCode.ORDER_CANCELLED && - reqId == refId && - isCancelling - ) { - // isDone = true; - logger.info(msg); - // done(); - } else { - isDone = true; - done(msg); - } - } - }, - ); + .on(EventName.error, (error: Error, code: ErrorCode, reqId: number) => { + if ( + code == ErrorCode.ORDER_CANCELLED && + reqId == refId && + isCancelling + ) { + // Alright, we can safely ignore + } else { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isDone = true; + done(msg); + } + }); - ib.connect().reqOpenOrders(); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) + ? logger.warn(msg) + : isCancelling + ? logger.info(msg) + : logger.error(msg); + }) + .connect(); }); test("cancelOrder immediate", (done) => { @@ -140,110 +134,118 @@ describe("CancelOrder", () => { let isDone = false; ib.once(EventName.nextValidId, (orderId: number) => { refId = orderId; - ib.placeOrder(refId, contract, order); + ib.reqOpenOrders().placeOrder(refId, contract, order); }) - .on(EventName.orderStatus, (orderId) => { + .on(EventName.orderStatus, (orderId, status) => { + // console.log(orderId, status, isCancelling, isDone); if (orderId === refId) { + // console.log(orderId, status); if (isDone) { // ignore any message } else if (!isCancelling) { + // [OrderStatus.PreSubmitted, OrderStatus.Submitted].includes( + // status as OrderStatus, + // ) isCancelling = true; ib.cancelOrder(orderId, ""); - } - } - }) - .on( - EventName.error, - ( - error: Error, - code: ErrorCode, - reqId: number, - _advancedOrderReject?: unknown, - ) => { - if (reqId === -1) { - logger.info(error.message); } else { - const msg = `[${reqId}] ${error.message} (Error #${code})`; - if (error.message.includes("Warning:")) { - logger.warn(msg); - } else if ( - code == ErrorCode.ORDER_CANCELLED && - reqId == refId && - isCancelling + if ( + [ + OrderStatus.PendingCancel, + OrderStatus.ApiCancelled, + OrderStatus.Cancelled, + ].includes(status as OrderStatus) ) { isDone = true; - logger.info(msg); done(); - } else { - isDone = true; - done(msg); } } - }, - ); + } + }) + .on(EventName.error, (error: Error, code: ErrorCode, reqId: number) => { + if ( + code == ErrorCode.ORDER_CANCELLED && + reqId == refId && + isCancelling + ) { + // Alright, we can safely ignore + } else { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isDone = true; + done(msg); + } + }); - ib.connect().reqOpenOrders(); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) + ? logger.warn(msg) + : isCancelling + ? logger.info(msg) + : logger.error(msg); + }) + .connect(); }); test("cancelOrder later", (done) => { - // NOTE: this test is not correctly written, but the API doesn't behave as expected neither + // NOTE: this test is not correctly written, but the API doesn't behave as *I* expected neither let refId: number; let isCancelling = false; let isDone = false; ib.once(EventName.nextValidId, (orderId: number) => { refId = orderId; - ib.placeOrder(refId, contract, { ...order, goodAfterTime: undefined }); + ib.reqOpenOrders().placeOrder(refId, contract, order); }) .on(EventName.orderStatus, (orderId, status) => { + // console.log(orderId, status, isCancelling, isDone); if (orderId === refId) { if (isDone) { // ignore any message } else if (!isCancelling) { isCancelling = true; - ib.cancelOrder(orderId, "20260101-23:59:59"); - } else { - if ( - [ - OrderStatus.PendingCancel, - OrderStatus.ApiCancelled, - OrderStatus.Cancelled, - ].includes(status as OrderStatus) - ) { - isDone = true; - done(); - } + ib.cancelOrder(orderId, "20310101-23:59:59"); + } else if ( + [ + OrderStatus.PendingCancel, + OrderStatus.ApiCancelled, + OrderStatus.Cancelled, + ].includes(status as OrderStatus) + ) { + isDone = true; + done(); } } }) - .on( - EventName.error, - ( - error: Error, - code: ErrorCode, - reqId: number, - _advancedOrderReject?: unknown, - ) => { - if (reqId === -1) { - logger.info(error.message); - } else { - const msg = `[${reqId}] ${error.message} (Error #${code})`; - if (error.message.includes("Warning:")) { - logger.warn(msg); - } else if ( - code == ErrorCode.ORDER_CANCELLED && - reqId == refId && - isCancelling - ) { - logger.info(msg); - } else { - isDone = true; - done(msg); - } + .on(EventName.error, (error: Error, code: ErrorCode, reqId: number) => { + if (isDone) { + // ignore any message + } else if ( + code == ErrorCode.ORDER_CANCELLED && + reqId == refId && + isCancelling + ) { + if (!isDone) { + isDone = true; + done(); } - }, - ); + } else if (!isNonFatalError(code, error)) { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isDone = true; + done(msg); + } + }); - ib.connect().reqOpenOrders(); + ib.on(EventName.info, (msg, code) => logger.info(code, msg)) + .on(EventName.error, (error, code, reqId) => { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + isNonFatalError(code, error) + ? logger.warn(msg) + : isCancelling + ? logger.info(msg) + : logger.error(msg); + }) + .connect(); }); }); diff --git a/yarn.lock b/yarn.lock index d2cd3363..e2656745 100644 --- a/yarn.lock +++ b/yarn.lock @@ -442,6 +442,15 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@gerrit0/mini-shiki@^1.24.0": + version "1.24.1" + resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-1.24.1.tgz#60ef10f4e2cfac7a9223e10b88c128438aa44fd8" + integrity sha512-PNP/Gjv3VqU7z7DjRgO3F9Ok5frTKqtpV+LJW1RzMcr2zpRk0ulhEWnbcNGXzPC7BZyWMIHrkfQX2GZRfxrn6Q== + dependencies: + "@shikijs/engine-oniguruma" "^1.24.0" + "@shikijs/types" "^1.24.0" + "@shikijs/vscode-textmate" "^9.3.0" + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -734,39 +743,18 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@shikijs/core@1.22.2": - version "1.22.2" - resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.22.2.tgz#9c22bd4cc8a4d6c062461cfd35e1faa6c617ca25" - integrity sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg== +"@shikijs/engine-oniguruma@^1.24.0": + version "1.24.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.0.tgz#4e6f49413fbc96dabfa30cb232ca1acf5ca1a446" + integrity sha512-Eua0qNOL73Y82lGA4GF5P+G2+VXX9XnuUxkiUuwcxQPH4wom+tE39kZpBFXfUuwNYxHSkrSxpB1p4kyRW0moSg== dependencies: - "@shikijs/engine-javascript" "1.22.2" - "@shikijs/engine-oniguruma" "1.22.2" - "@shikijs/types" "1.22.2" + "@shikijs/types" "1.24.0" "@shikijs/vscode-textmate" "^9.3.0" - "@types/hast" "^3.0.4" - hast-util-to-html "^9.0.3" -"@shikijs/engine-javascript@1.22.2": - version "1.22.2" - resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-1.22.2.tgz#62e90dbd2ed1d78b972ad7d0a1f8ffaaf5e43279" - integrity sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw== - dependencies: - "@shikijs/types" "1.22.2" - "@shikijs/vscode-textmate" "^9.3.0" - oniguruma-to-js "0.4.3" - -"@shikijs/engine-oniguruma@1.22.2": - version "1.22.2" - resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.2.tgz#b12a44e3faf486e19fbcf8952f4b56b9b9b8d9b8" - integrity sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA== - dependencies: - "@shikijs/types" "1.22.2" - "@shikijs/vscode-textmate" "^9.3.0" - -"@shikijs/types@1.22.2": - version "1.22.2" - resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.22.2.tgz#695a283f19963fe0638fc2646862ba5cfc4623a8" - integrity sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg== +"@shikijs/types@1.24.0", "@shikijs/types@^1.24.0": + version "1.24.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.24.0.tgz#a1755b125cb8fb1780a876a0a57242939eafd79f" + integrity sha512-aptbEuq1Pk88DMlCe+FzXNnBZ17LCiLIGWAeCWhoFDzia5Q5Krx3DgnULLiouSdd6+LUM39XwXGppqYE0Ghtug== dependencies: "@shikijs/vscode-textmate" "^9.3.0" "@types/hast" "^3.0.4" @@ -835,7 +823,7 @@ dependencies: "@types/node" "*" -"@types/hast@^3.0.0", "@types/hast@^3.0.4": +"@types/hast@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== @@ -874,22 +862,15 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== -"@types/mdast@^4.0.0": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" - integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== - dependencies: - "@types/unist" "*" - "@types/node@*": version "18.16.19" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.19.tgz#cb03fca8910fdeb7595b755126a8a78144714eea" integrity sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA== -"@types/node@^18.19.64": - version "18.19.64" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.64.tgz#122897fb79f2a9ec9c979bded01c11461b2b1478" - integrity sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ== +"@types/node@^18.19.67": + version "18.19.67" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.67.tgz#77c4b01641a1e3e1509aff7e10d39e4afd5ae06d" + integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ== dependencies: undici-types "~5.26.4" @@ -915,11 +896,6 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== -"@types/unist@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" - integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== - "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -1113,7 +1089,7 @@ "@typescript-eslint/types" "8.13.0" eslint-visitor-keys "^3.4.3" -"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": +"@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== @@ -1364,11 +1340,6 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -ccount@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" - integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== - chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1391,16 +1362,6 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -character-entities-html4@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" - integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== - -character-entities-legacy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" - integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== - ci-info@^3.2.0: version "3.8.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" @@ -1459,11 +1420,6 @@ colors@^1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -comma-separated-tokens@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" - integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== - command-buffer@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/command-buffer/-/command-buffer-0.1.0.tgz#ced3f40f77289314db91cb77ef3b00c332e8e268" @@ -1547,23 +1503,11 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -dequal@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -devlop@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" - integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== - dependencies: - dequal "^2.0.0" - diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -2064,40 +2008,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hast-util-to-html@^9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz#a9999a0ba6b4919576a9105129fead85d37f302b" - integrity sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - ccount "^2.0.0" - comma-separated-tokens "^2.0.0" - hast-util-whitespace "^3.0.0" - html-void-elements "^3.0.0" - mdast-util-to-hast "^13.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - stringify-entities "^4.0.0" - zwitch "^2.0.4" - -hast-util-whitespace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" - integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== - dependencies: - "@types/hast" "^3.0.0" - html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -html-void-elements@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" - integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -2850,21 +2765,6 @@ markdown-it@^14.1.0: punycode.js "^2.3.1" uc.micro "^2.1.0" -mdast-util-to-hast@^13.0.0: - version "13.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4" - integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== - dependencies: - "@types/hast" "^3.0.0" - "@types/mdast" "^4.0.0" - "@ungap/structured-clone" "^1.0.0" - devlop "^1.0.0" - micromark-util-sanitize-uri "^2.0.0" - trim-lines "^3.0.0" - unist-util-position "^5.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - mdurl@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" @@ -2880,38 +2780,6 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromark-util-character@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1" - integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== - dependencies: - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" - integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== - -micromark-util-sanitize-uri@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" - integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-encode "^2.0.0" - micromark-util-symbol "^2.0.0" - -micromark-util-symbol@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" - integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== - -micromark-util-types@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" - integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== - micromatch@^4.0.4: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -3004,13 +2872,6 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -oniguruma-to-js@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz#8d899714c21f5c7d59a3c0008ca50e848086d740" - integrity sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ== - dependencies: - regex "^4.3.2" - optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -3156,11 +3017,6 @@ prompts@^2.0.1, prompts@~2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -property-information@^6.0.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" - integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== - punycode.js@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" @@ -3186,11 +3042,6 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -regex@^4.3.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/regex/-/regex-4.4.0.tgz#cb731e2819f230fad69089e1bd854fef7569e90a" - integrity sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ== - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -3315,18 +3166,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shiki@^1.16.2: - version "1.22.2" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.22.2.tgz#ed109a3d0850504ad5a1edf8496470a2121c5b7b" - integrity sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA== - dependencies: - "@shikijs/core" "1.22.2" - "@shikijs/engine-javascript" "1.22.2" - "@shikijs/engine-oniguruma" "1.22.2" - "@shikijs/types" "1.22.2" - "@shikijs/vscode-textmate" "^9.3.0" - "@types/hast" "^3.0.4" - signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -3355,11 +3194,6 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -space-separated-tokens@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" - integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -3389,14 +3223,6 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -stringify-entities@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" - integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== - dependencies: - character-entities-html4 "^2.0.0" - character-entities-legacy "^3.0.0" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -3476,11 +3302,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -trim-lines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" - integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== - ts-api-utils@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" @@ -3548,21 +3369,21 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -typedoc@^0.26.11: - version "0.26.11" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.26.11.tgz#124b43a5637b7f3237b8c721691b44738c5c9dc9" - integrity sha512-sFEgRRtrcDl2FxVP58Ze++ZK2UQAEvtvvH8rRlig1Ja3o7dDaMHmaBfvJmdGnNEFaLTpQsN8dpvZaTqJSu/Ugw== +typedoc@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.2.tgz#8a4e0303f4c49174af21e981e0b60e8a637d8167" + integrity sha512-C2ima5TZJHU3ecnRIz50lKd1BsYck5LhYQIy7MRPmjuSEJreUEAt+uAVcZgY7wZsSORzEI7xW8miZIdxv/cbmw== dependencies: + "@gerrit0/mini-shiki" "^1.24.0" lunr "^2.3.9" markdown-it "^14.1.0" minimatch "^9.0.5" - shiki "^1.16.2" - yaml "^2.5.1" + yaml "^2.6.1" -typescript@^5.6.3: - version "5.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" - integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== +typescript@^5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" + integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" @@ -3574,44 +3395,6 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -unist-util-is@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" - integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-position@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" - integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-stringify-position@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" - integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-visit-parents@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" - integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - -unist-util-visit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" - integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - unist-util-visit-parents "^6.0.0" - update-browserslist-db@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" @@ -3641,22 +3424,6 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" -vfile-message@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" - integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" - -vfile@^6.0.0: - version "6.0.3" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" - integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== - dependencies: - "@types/unist" "^3.0.0" - vfile-message "^4.0.0" - walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" @@ -3713,10 +3480,10 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^2.5.1: - version "2.6.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.0.tgz#14059ad9d0b1680d0f04d3a60fe00f3a857303c3" - integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== +yaml@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773" + integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg== yargs-parser@^21.1.1: version "21.1.1" @@ -3740,8 +3507,3 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zwitch@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" - integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==