UNPKG

@drift-labs/common

Version:

Common functions for Drift

422 lines 20.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MarketDataFeed = exports.TradeSubscriberSubscription = exports.CandleSubscriberSubscription = void 0; const rxjs_1 = require("rxjs"); const dataApiWsClient_1 = require("./dataApiWsClient"); const assert_1 = __importDefault(require("assert")); const tiny_invariant_1 = __importDefault(require("tiny-invariant")); const DEFAULT_CANDLE_RESOLUTION_FOR_TRADE_SUBSCRIPTIONS = '1'; class SubscriberSubscription { get observable() { return this._subject.asObservable(); } constructor(id, subject) { this.id = id; this._subject = subject; } } class CandleSubscriberSubscription extends SubscriberSubscription { constructor(id, subject) { super(id, subject); } } exports.CandleSubscriberSubscription = CandleSubscriberSubscription; class TradeSubscriberSubscription extends SubscriberSubscription { constructor(id, subject) { super(id, subject); } } exports.TradeSubscriberSubscription = TradeSubscriberSubscription; class _Subscriber { get subscription() { return this._apiSubscription; } generateSubscriberId() { var _a, _b, _c; return `${(_a = this.config) === null || _a === void 0 ? void 0 : _a.type}:${(_b = this.config) === null || _b === void 0 ? void 0 : _b.marketSymbol}:${(_c = this.config) === null || _c === void 0 ? void 0 : _c.env.key}:(${_Subscriber.idIncrementer++})`; } constructor(config) { this.config = { ...config }; // Make a copy of the config to avoid mutability issues this.id = this.generateSubscriberId(); } setSubscription(subscription) { this._apiSubscription = subscription; } next(data) { this.subject.next(data); } } _Subscriber.idIncrementer = 0; class CandleSubscriber extends _Subscriber { constructor(config) { super(config); this.subject = new rxjs_1.Subject(); this.subscriberSubscription = new CandleSubscriberSubscription(this.id, this.subject); } } class TradeSubscriber extends _Subscriber { constructor(config) { super(config); this.subject = new rxjs_1.Subject(); this.subscriberSubscription = new TradeSubscriberSubscription(this.id, this.subject); } } function getCompatibleCandleSubscriptionLookupKey(config) { return `${config.marketSymbol}:${config.resolution}:${config.env.key}`; } function getCompatibleTradeSubscriptionLookupKey(config) { return `${config.marketSymbol}:${config.env.key}`; } class ApiSubscription { generateSubscriptionId() { return `${getCompatibleCandleSubscriptionLookupKey(this.config)}(${ApiSubscription.idIncrementer++})`; } /** * Any trade subscription with a matching key can use this ApiSubscription. */ get tradeSubscriptionsLookupKey() { return getCompatibleTradeSubscriptionLookupKey(this.config); } /** * Any candle subscription with a matching key can use this ApiSubscription. */ get candleSubscriptionsLookupKey() { return getCompatibleCandleSubscriptionLookupKey(this.config); } constructor(resolution, initialSubscriber, onNoMoreSubscribers) { const marketSymbol = initialSubscriber.config.marketSymbol; const env = initialSubscriber.config.env; this.config = { marketSymbol, resolution, env }; this.id = this.generateSubscriptionId(); this.onNoMoreSubscribers = () => onNoMoreSubscribers(this.id); console.log(`marketDataFeed::creating_new_api_subscription:${this.id}`); const initialSubscriberType = initialSubscriber.config.type; this.apiClient = new dataApiWsClient_1.DataApiWsClient({ marketSymbol: this.config.marketSymbol, resolution: this.config.resolution, env: this.config.env, }); initialSubscriber.setSubscription(this); switch (initialSubscriberType) { case 'candles': { this.candleSubscribers = new Map([ [initialSubscriber.id, initialSubscriber], ]); this.tradeSubscribers = new Map(); break; } case 'trades': { this.candleSubscribers = new Map(); this.tradeSubscribers = new Map([ [initialSubscriber.id, initialSubscriber], ]); break; } default: { const _never = initialSubscriberType; throw new Error(`Unknown subscription type: ${_never}`); } } this.subscribeToApi(); } async subscribeToApi() { await this.apiClient.subscribe(); this.apiClient.candlesObservable.subscribe((candle) => { this.candleSubscribers.forEach((subscriber) => { subscriber.next(candle); }); }); this.apiClient.tradesObservable.subscribe((trades) => { this.tradeSubscribers.forEach((subscriber) => { subscriber.next(trades); }); }); } attachNewCandleSubscriberToExistingSubscription(subscriber) { subscriber.setSubscription(this); this.candleSubscribers.set(subscriber.id, subscriber); } attachNewTradeSubscriberToExistingSubscription(subscriber) { subscriber.setSubscription(this); this.tradeSubscribers.set(subscriber.id, subscriber); } unsubscribeFromApi() { console.log(`marketDataFeed::unsubscribing_api_subscription:${this.id}`); this.apiClient.unsubscribe(); } removeSubscriber(subscriberId) { this.candleSubscribers.delete(subscriberId); this.tradeSubscribers.delete(subscriberId); // Handle the case where there are no more subscribers for this subscription if (this.candleSubscribers.size === 0 && this.tradeSubscribers.size === 0) { this.unsubscribeFromApi(); this.onNoMoreSubscribers(); } } } ApiSubscription.idIncrementer = 0; class SubscriptionLookup { has(subscriptionLookupKey) { return !!this.get(subscriptionLookupKey); } } class TradeSubscriptionLookup extends SubscriptionLookup { constructor() { super(); this.subscriptions = new Map(); } add(apiSubscription) { const subscriptions = this.subscriptions.get(apiSubscription.tradeSubscriptionsLookupKey); if (!subscriptions) { this.subscriptions.set(apiSubscription.tradeSubscriptionsLookupKey, [ apiSubscription, ]); } else { subscriptions.push(apiSubscription); } } get(subscriptionLookupKey) { var _a, _b; return (_b = (_a = this.subscriptions.get(subscriptionLookupKey)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null; } getAll(subscriptionLookupKey) { var _a; return (_a = this.subscriptions.get(subscriptionLookupKey)) !== null && _a !== void 0 ? _a : []; } remove(apiSubscription) { const subscriptionLookupKey = apiSubscription.tradeSubscriptionsLookupKey; const apiSubscriptionId = apiSubscription.id; const subscriptions = this.subscriptions.get(subscriptionLookupKey); if (!subscriptions) return; const index = subscriptions.findIndex((subscription) => subscription.id === apiSubscriptionId); if (index === -1) return; subscriptions.splice(index, 1); // Clean up empty arrays to prevent memory leaks and ensure has() works correctly if (subscriptions.length === 0) { this.subscriptions.delete(subscriptionLookupKey); } } has(subscriptionLookupKey) { return super.has(subscriptionLookupKey); } } class CandleSubscriptionLookup extends SubscriptionLookup { constructor() { super(); this.subscriptions = new Map(); } add(apiSubscription) { this.subscriptions.set(apiSubscription.candleSubscriptionsLookupKey, apiSubscription); } get(subscriptionLookupKey) { var _a; return (_a = this.subscriptions.get(subscriptionLookupKey)) !== null && _a !== void 0 ? _a : null; } remove(apiSubscription) { const subscriptionLookupKey = apiSubscription.candleSubscriptionsLookupKey; this.subscriptions.delete(subscriptionLookupKey); } has(subscriptionLookupKey) { return super.has(subscriptionLookupKey); } } /** * Internal class that manages the complex subscription orchestration logic. * This handles subscription sharing, transfer logic, and lifecycle management. */ class SubscriptionManager { constructor() { // Stores all current subscribers this.subscribers = new Map(); // Stores all current subscriptions :: subscribers:subscriptions is N:M (Subscribers may share a subscription but not vice versa) this.apiSubscriptions = new Map(); // Lookup table for any existing subscriptions which have a compatible configuration for a trade subscriber. Can be multiple compatible subscriptions for a trade subscriber. this.compatibleTradeSubscriptionLookup = new TradeSubscriptionLookup(); // Lookup table for any existing subscriptions which have a compatible configuration for a candle subscriber. Should only be one compatible subscription for a candle subscriber. this.compatibleCandleSubscriptionLookup = new CandleSubscriptionLookup(); } /** * Handles a new subscription by adding it to the necessary lookup tables * @param subscription */ handleNewApiSubscription(subscription) { this.apiSubscriptions.set(subscription.id, subscription); // Add to the trade subscription compatibility lookup this.compatibleTradeSubscriptionLookup.add(subscription); // Add to the candle subscription compatibility lookup if a previous one suiting this subscription doesn't already exist if (!this.compatibleCandleSubscriptionLookup.has(subscription.candleSubscriptionsLookupKey)) { this.compatibleCandleSubscriptionLookup.add(subscription); } this.checkForTradeSubscriptionTransferForNewSubscription(subscription); } transferTradeSubscribersToNewSubscription(existingTradeSubscription, newSubscription) { (0, tiny_invariant_1.default)(existingTradeSubscription.id !== newSubscription.id, 'Expected different subscriptions when transferring trade subscribers'); (0, tiny_invariant_1.default)(existingTradeSubscription.candleSubscribers.size === 0, 'Expected existing trade subscription to have no candle subscribers when transferring trade subscribers'); // Transfer the subscribers to the new subscription const tradeSubscribers = Array.from(existingTradeSubscription.tradeSubscribers.values()); if (tradeSubscribers.length === 0) return; // Skip early if there are no trade subscribers to transfer console.log(`marketDataFeed::transferring_previous_trade_subscribers_to_new_subscription`); tradeSubscribers.forEach((tradeSubscriber) => { newSubscription.attachNewTradeSubscriberToExistingSubscription(tradeSubscriber); existingTradeSubscription.removeSubscriber(tradeSubscriber.id); }); } /** * When we have a new subscription, we want to check if there is an existing subscription with trade subscribers which should be transferred to the new subscription. * * Reasoning: * - If a TRADE SUBSCRIBER caused a new subscription to be created * - and then a following CANDLE SUBSCRIBER is created for the same market * => then it is wasteful to keep the previous trade subscription open, because the new candle subscription can collect the data for both subscribers * * @param newSubscription */ checkForTradeSubscriptionTransferForNewSubscription(newSubscription) { const tradeSubscriptionLookupKey = newSubscription.tradeSubscriptionsLookupKey; (0, tiny_invariant_1.default)(!!this.compatibleTradeSubscriptionLookup.get(tradeSubscriptionLookupKey), `Expect a matching trade subscription when checking for transfers for a new subscription`); const allCompatibleTradeSubscriptions = this.compatibleTradeSubscriptionLookup.getAll(tradeSubscriptionLookupKey); for (const existingTradeSubscription of allCompatibleTradeSubscriptions) { if (existingTradeSubscription.id === newSubscription.id) continue; // If the subscription is the current subscription, then there are no subscribers to transfer. if (existingTradeSubscription.candleSubscribers.size > 0) continue; // If the existing subscription has candle subscribers, skip doing any transfers because it's not any extra efficient to do so. this.transferTradeSubscribersToNewSubscription(existingTradeSubscription, newSubscription); } } /** * When a subscriber unsubscribes and causes the subscription to have some remaining trade subscribers, but no candle subscribers, we want to check if there is another compatible subscription to transfer the trade subscribers to. * @param apiSubscription */ checkForSubscriptionTransferOnUnsubscribe(apiSubscription) { if (apiSubscription.tradeSubscribers.size === 0) return; // Skip early if there are no trade subscribers to transfer if (apiSubscription.candleSubscribers.size > 0) return; // Skip early if there are candle subscribers // Look for another compatible subscription to transfer the trade subscribers to const tradeSubscriptionLookupKey = apiSubscription.tradeSubscriptionsLookupKey; // Get all compatible subscriptions for this market const compatibleSubscriptions = this.compatibleTradeSubscriptionLookup.getAll(tradeSubscriptionLookupKey); if (!compatibleSubscriptions || compatibleSubscriptions.length <= 1) { // No other compatible subscriptions available, or only this subscription exists return; } // Find a different subscription that can accept the trade subscribers const targetSubscription = compatibleSubscriptions.find((subscription) => subscription.id !== apiSubscription.id); if (!targetSubscription) { // No suitable target subscription found return; } console.log(`marketDataFeed::transferring_trade_subscribers_on_unsubscribe`); // Transfer all trade subscribers to the target subscription this.transferTradeSubscribersToNewSubscription(apiSubscription, targetSubscription); } handleNewCandleSubscriber(subscriber) { const candleSubscriptionLookupKey = getCompatibleCandleSubscriptionLookupKey(subscriber.config); const hasExistingSuitableSubscription = this.compatibleCandleSubscriptionLookup.has(candleSubscriptionLookupKey); if (hasExistingSuitableSubscription) { console.log(`marketDataFeed::attaching_new_candle_subscriber_to_existing_subscription`); const existingSubscription = this.compatibleCandleSubscriptionLookup.get(candleSubscriptionLookupKey); existingSubscription.attachNewCandleSubscriberToExistingSubscription(subscriber); } else { console.log(`marketDataFeed::creating_new_candle_subscription`); const newSubscription = new ApiSubscription(subscriber.config.resolution, subscriber, this.cleanupApiSubscription.bind(this)); this.handleNewApiSubscription(newSubscription); } (0, assert_1.default)(this.subscribers.has(subscriber.id), 'Subscriber should be added to subscribers map'); } handleNewTradeSubscriber(subscriber) { // Check if any existing subscriptions already suit the new subscriber const tradeSubscriptionLookupKey = getCompatibleTradeSubscriptionLookupKey(subscriber.config); const hasExistingSuitableSubscription = this.compatibleTradeSubscriptionLookup.has(tradeSubscriptionLookupKey); if (hasExistingSuitableSubscription) { console.log(`marketDataFeed::attaching_new_trade_subscriber_to_existing_subscription`); const existingSubscription = this.compatibleTradeSubscriptionLookup.get(tradeSubscriptionLookupKey); existingSubscription.attachNewTradeSubscriberToExistingSubscription(subscriber); } else { console.log(`marketDataFeed::creating_new_trade_subscription`); const newSubscription = new ApiSubscription(DEFAULT_CANDLE_RESOLUTION_FOR_TRADE_SUBSCRIPTIONS, subscriber, this.cleanupApiSubscription.bind(this)); this.handleNewApiSubscription(newSubscription); } (0, assert_1.default)(this.subscribers.has(subscriber.id), 'Subscriber should be added to subscribers map'); } /** * This method gets called when we no longer need an ApiSubscription and can close it down completely. * @param apiSubscriptionId */ cleanupApiSubscription(apiSubscriptionId) { const apiSubscription = this.apiSubscriptions.get(apiSubscriptionId); (0, tiny_invariant_1.default)(apiSubscription.candleSubscribers.size === 0, 'Expected api subscription to have no candle subscribers when cleaning up'); (0, tiny_invariant_1.default)(apiSubscription.tradeSubscribers.size === 0, 'Expected api subscription to have no trade subscribers when cleaning up'); // Remove the subscription from the lookup tables this.compatibleTradeSubscriptionLookup.remove(apiSubscription); this.compatibleCandleSubscriptionLookup.remove(apiSubscription); // We can delete the subscription from the lookup table when we're cleaning up an ApiSubscription this.apiSubscriptions.delete(apiSubscriptionId); } /** * Creates a new subscriber and manages its subscription lifecycle */ subscribe(config) { switch (config.type) { case 'candles': { // Create the new Subscriber const newSubscriber = new CandleSubscriber(config); // Add to current subscribers this.subscribers.set(newSubscriber.id, newSubscriber); this.handleNewCandleSubscriber(newSubscriber); return newSubscriber.subscriberSubscription; } case 'trades': { // Create the new Subscriber const newSubscriber = new TradeSubscriber(config); // Add to current subscribers this.subscribers.set(newSubscriber.id, newSubscriber); this.handleNewTradeSubscriber(newSubscriber); return newSubscriber.subscriberSubscription; } default: { const _never = config; throw new Error(`Unknown subscription type: ${_never}`); } } } /** * Removes a subscriber and cleans up resources as needed */ unsubscribe(subscriberId) { const subscriber = this.subscribers.get(subscriberId); if (!subscriber) { throw new Error(`Subscriber not found: ${subscriberId}`); } const apiSubscription = subscriber.subscription; apiSubscription.removeSubscriber(subscriberId); this.checkForSubscriptionTransferOnUnsubscribe(apiSubscription); this.subscribers.delete(subscriberId); } } /** * This class will handle subscribing to market data from the Drift Data API's websocket. See https://data.api.drift.trade/playground for more information about the API. * * It currently supports subscribing to candles and to trades. */ class MarketDataFeed { constructor() { } static subscribe(config) { return this.subscriptionManager.subscribe(config); } static unsubscribe(subscriberId) { this.subscriptionManager.unsubscribe(subscriberId); } } exports.MarketDataFeed = MarketDataFeed; MarketDataFeed.subscriptionManager = new SubscriptionManager(); //# sourceMappingURL=marketDataFeed.js.map