UNPKG

@drift-labs/common

Version:

Common functions for Drift

311 lines 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PollingDlob = exports.POLLING_DEPTHS = exports.POLLING_INTERVALS = void 0; const rxjs_1 = require("rxjs"); const MarketId_1 = require("../../../types/MarketId"); const blockchain_1 = require("../constants/blockchain"); const dlobServer_1 = require("../../base/actions/trade/openPerpOrder/dlobServer"); // Predefined interval multipliers from the original React hook exports.POLLING_INTERVALS = { LIVE_MARKET: 1, BACKGROUND_DEEP: 3, // Can be configured to 2 for interim usage BACKGROUND_SHALLOW: 30, IDLE_1: 30, IDLE_2: 60, }; exports.POLLING_DEPTHS = { SHALLOW: 1, DEEP: 1, ORDERBOOK: 100, }; /** * PollingDlob - A configurable market data polling system. * The Drift DLOB (decentralized limit orderbook) server stores the current live state of the orderbook * across all Drift markets. This class is used to poll the DLOB server for the markets' current mark price, * while oracle price data is also provided alongside. * * Example usage: * ```typescript * import { PollingDlob, MarketId } from '@drift/common'; * * const pollingDlob = new PollingDlob({ * dlobServerHttpUrl: 'https://dlob.drift.trade', * indicativeLiquidityEnabled: true * }); * * // Add different polling intervals * pollingDlob.addInterval('live', 1, 100); // Every 1s with depth 100 * pollingDlob.addInterval('background', 3, 1); // Every 3s with depth 1 * pollingDlob.addInterval('idle', 30, 1); // Every 30s with depth 1 * * // Add markets to intervals * const perpMarket = MarketId.createPerpMarket(0); * const spotMarket = MarketId.createSpotMarket(0); * * pollingDlob.addMarketToInterval('live', perpMarket); * pollingDlob.addMarketToInterval('background', spotMarket); * * // Subscribe to data updates * pollingDlob.onData().subscribe(marketData => { * marketData.forEach(({ marketId, data, intervalId }) => { * console.log(`Market ${marketId.key} data from ${intervalId}:`, data); * }); * }); * * // Subscribe to errors * pollingDlob.onError().subscribe(error => { * console.error('Polling error:', error); * }); * * // Start polling * pollingDlob.start(); * * // Stop when done * // pollingDlob.stop(); * ``` */ class PollingDlob { constructor(config) { this.baseTickIntervalMs = 1000; this.intervals = new Map(); this._marketToIntervalMap = new Map(); this.dataSubject = new rxjs_1.Subject(); this.errorSubject = new rxjs_1.Subject(); this.isStarted = false; this.intervalHandle = null; this.tickCounter = 0; this.consecutiveEmptyResponseCount = 0; this.consecutiveErrorCount = 0; this.maxConsecutiveEmptyResponses = 3; this.maxConsecutiveErrors = 5; this.config = { indicativeLiquidityEnabled: true, ...config, }; } getPollingIntervalForMarket(marketKey) { const intervalId = this._marketToIntervalMap.get(marketKey); if (!intervalId) { return undefined; } return this.intervals.get(intervalId); } addInterval(id, intervalMultiplier, depth) { if (this.intervals.has(id)) { throw new Error(`Interval with id '${id}' already exists`); } this.intervals.set(id, { id, intervalMultiplier, depth, markets: new Set(), newlyAddedMarkets: new Set(), }); } removeInterval(id) { const interval = this.intervals.get(id); if (!interval) { return; } // Remove all markets from this interval interval.markets.forEach((market) => { this._marketToIntervalMap.delete(market); }); this.intervals.delete(id); } /** * Add a market to an interval. * If the market is already in an interval, it will be removed from the existing interval. * Newly added markets will be polled on the next tick regardless of interval multiplier. */ addMarketToInterval(intervalId, marketKey) { const interval = this.intervals.get(intervalId); if (!interval) { throw new Error(`Interval with id '${intervalId}' does not exist`); } // Remove market from any existing interval first const existingIntervalId = this._marketToIntervalMap.get(marketKey); if (existingIntervalId === intervalId) { // market is already in the interval return; } if (existingIntervalId) { this.removeMarketFromInterval(existingIntervalId, marketKey); } interval.markets.add(marketKey); // Mark as newly added so it gets polled on the next tick interval.newlyAddedMarkets.add(marketKey); this._marketToIntervalMap.set(marketKey, intervalId); } addMarketsToInterval(intervalId, marketKeys) { for (const marketKey of marketKeys) { this.addMarketToInterval(intervalId, marketKey); } } removeMarketFromInterval(intervalId, marketKey) { const interval = this.intervals.get(intervalId); if (!interval) { return; } interval.markets.delete(marketKey); interval.newlyAddedMarkets.delete(marketKey); this._marketToIntervalMap.delete(marketKey); } getMarketInterval(marketKey) { return this._marketToIntervalMap.get(marketKey); } onData() { return this.dataSubject.asObservable(); } onError() { return this.errorSubject.asObservable(); } start() { if (this.isStarted) { return Promise.resolve(); } this.isStarted = true; this.tickCounter = 0; const firstTickPromise = this.tick(); this.intervalHandle = setInterval(() => { this.tick(); }, this.baseTickIntervalMs); return firstTickPromise; } stop() { if (!this.isStarted) { return; } this.isStarted = false; if (this.intervalHandle) { clearInterval(this.intervalHandle); this.intervalHandle = null; } } isRunning() { return this.isStarted; } getConfig() { return { ...this.config }; } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } getMarketCount() { return this._marketToIntervalMap.size; } getIntervalCount() { return this.intervals.size; } getAllMarkets() { const allMarkets = []; this.intervals.forEach((interval) => { allMarkets.push(...Array.from(interval.markets)); }); return allMarkets; } getMarketsForInterval(intervalId) { const interval = this.intervals.get(intervalId); return interval ? Array.from(interval.markets) : []; } getStats() { return { isRunning: this.isStarted, tickCounter: this.tickCounter, intervalCount: this.intervals.size, marketCount: this._marketToIntervalMap.size, consecutiveEmptyResponses: this.consecutiveEmptyResponseCount, consecutiveErrors: this.consecutiveErrorCount, }; } resetErrorCounters() { this.consecutiveEmptyResponseCount = 0; this.consecutiveErrorCount = 0; } /** * Factory method to create a PollingDlob with common interval configurations */ static createWithCommonIntervals(config) { const pollingDlob = new PollingDlob(config); // Add common intervals based on the original React hook pollingDlob.addInterval(blockchain_1.PollingCategory.SELECTED_MARKET, exports.POLLING_INTERVALS.LIVE_MARKET, exports.POLLING_DEPTHS.ORDERBOOK); pollingDlob.addInterval(blockchain_1.PollingCategory.USER_INVOLVED, exports.POLLING_INTERVALS.BACKGROUND_DEEP, exports.POLLING_DEPTHS.DEEP); pollingDlob.addInterval(blockchain_1.PollingCategory.USER_NOT_INVOLVED, exports.POLLING_INTERVALS.BACKGROUND_SHALLOW, exports.POLLING_DEPTHS.SHALLOW); return pollingDlob; } async tick() { this.tickCounter++; // Find intervals that should be polled this tick const intervalsToPoll = Array.from(this.intervals.values()).filter((interval) => { const hasMarkets = interval.markets.size > 0; const hasNewlyAddedMarkets = interval.newlyAddedMarkets.size > 0; const isFirstTick = this.tickCounter === 1; const isRegularInterval = this.tickCounter % interval.intervalMultiplier === 0; return (hasMarkets && (isFirstTick || isRegularInterval || hasNewlyAddedMarkets)); }); if (intervalsToPoll.length === 0) { return; } try { const allMarketPollingData = []; // Combine all markets from different intervals into a single request const combinedMarketRequests = []; for (const interval of intervalsToPoll) { const marketsArray = Array.from(interval.markets); if (marketsArray.length === 0) { continue; } for (const marketKey of marketsArray) { combinedMarketRequests.push({ marketId: MarketId_1.MarketId.getMarketIdFromKey(marketKey), depth: interval.depth, intervalMultiplier: interval.intervalMultiplier, }); } } if (combinedMarketRequests.length === 0) { return; } // Make a single bulk fetch for all markets const l2Data = await (0, dlobServer_1.fetchBulkMarketsDlobL2Data)(this.config.driftDlobServerHttpUrl, combinedMarketRequests.map((req) => ({ marketId: req.marketId, depth: req.depth, }))); // Map the results back to MarketPollingData with correct interval IDs const intervalData = l2Data.map((data, index) => ({ marketId: combinedMarketRequests[index].marketId, data, })); allMarketPollingData.push(...intervalData); if (allMarketPollingData.length > 0) { this.consecutiveEmptyResponseCount = 0; this.consecutiveErrorCount = 0; this.dataSubject.next(allMarketPollingData); // Clear newly added markets flags for intervals that were polled intervalsToPoll.forEach((interval) => { interval.newlyAddedMarkets.clear(); }); } else { this.consecutiveEmptyResponseCount++; if (this.consecutiveEmptyResponseCount >= this.maxConsecutiveEmptyResponses) { this.errorSubject.next(new Error(`Received ${this.maxConsecutiveEmptyResponses} consecutive empty responses`)); } } } catch (error) { this.consecutiveErrorCount++; const errorInstance = error instanceof Error ? error : new Error(String(error)); if (this.consecutiveErrorCount >= this.maxConsecutiveErrors) { this.errorSubject.next(new Error(`Received ${this.maxConsecutiveErrors} consecutive errors. Latest: ${errorInstance.message}`)); } else { this.errorSubject.next(errorInstance); } } } } exports.PollingDlob = PollingDlob; //# sourceMappingURL=PollingDlob.js.map