@drift-labs/common
Version:
Common functions for Drift
311 lines • 11.8 kB
JavaScript
"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