@dxfeed/dxlink-feed
Advanced tools
| import { DXLinkChannel, DXLinkClient, DXLinkLogLevel, DXLinkChannelState, DXLinkChannelStateChangeListener } from '@dxfeed/dxlink-core'; | ||
| declare enum FeedContract { | ||
| 'AUTO' = "AUTO", | ||
| 'TICKER' = "TICKER", | ||
| 'HISTORY' = "HISTORY", | ||
| 'STREAM' = "STREAM" | ||
| } | ||
| declare enum FeedDataFormat { | ||
| 'FULL' = "FULL", | ||
| 'COMPACT' = "COMPACT" | ||
| } | ||
| interface FeedEventFields { | ||
| [eventType: string]: string[]; | ||
| } | ||
| type Subscription = { | ||
| readonly type: string; | ||
| readonly symbol: string; | ||
| }; | ||
| type TimeSeriesSubscription = { | ||
| readonly type: string; | ||
| readonly symbol: string; | ||
| readonly fromTime: number; | ||
| }; | ||
| type IndexedEventSubscription = { | ||
| readonly type: string; | ||
| readonly symbol: string; | ||
| readonly source: string; | ||
| }; | ||
| type FeedEventValue = number | string | boolean; | ||
| interface FeedEventData { | ||
| [key: string]: FeedEventValue; | ||
| } | ||
| /** | ||
| * Prefered configuration for the feed channel. | ||
| * Server can ignore some of the parameters and use own defaults. | ||
| * @see {DXLinkFeed.configure} | ||
| */ | ||
| interface FeedAcceptConfig { | ||
| /** | ||
| * Aggregation period in seconds. | ||
| * If not specified, the channel will use the default value. | ||
| * If specified as 0, the channel will try not aggregate events. | ||
| */ | ||
| acceptAggregationPeriod?: number; | ||
| /** | ||
| * Data format to be used for received events. | ||
| * If not specified, the channel will use the default value `FULL`. | ||
| */ | ||
| acceptDataFormat?: FeedDataFormat; | ||
| /** | ||
| * Event fields to be included in received events. | ||
| * If not specified, the channel will use the default value. | ||
| * If specified as an empty array, the channel will try to send events with default fields. | ||
| */ | ||
| acceptEventFields?: FeedEventFields; | ||
| } | ||
| /** | ||
| * Configuration of the feed channel. | ||
| */ | ||
| interface FeedConfig { | ||
| /** | ||
| * Aggregation period in seconds. | ||
| * @example 0.5 - 500 milliseconds. | ||
| * @default `NaN` | ||
| * @see {FeedAcceptConfig.acceptAggregationPeriod} | ||
| */ | ||
| readonly aggregationPeriod: number; | ||
| /** | ||
| * Data format to be used for received events. | ||
| * @example `FULL` - object with keys and values. | ||
| * @example `COMPACT` - array of values. | ||
| * @default `FULL` | ||
| * @see {FeedAcceptConfig.acceptDataFormat} | ||
| */ | ||
| readonly dataFormat: FeedDataFormat; | ||
| /** | ||
| * Event fields to be included in received events. | ||
| * You can specify fields for all event types or for specific event types @see {FeedAcceptConfig.acceptEventFields}. | ||
| * @example ```json | ||
| * { "Quote": ["eventSymbol", "askPrice", "bidPrice"] } | ||
| * ``` | ||
| * @default `{}` | ||
| */ | ||
| readonly eventFields: FeedEventFields; | ||
| } | ||
| /** | ||
| * Listener for the feed channel config changes. | ||
| */ | ||
| type DXLinkFeedConfigChangeListener = (config: FeedConfig) => void; | ||
| /** | ||
| * Subscription type by the contract. | ||
| */ | ||
| type SubscriptionByContract = { | ||
| [FeedContract.AUTO]: Subscription | TimeSeriesSubscription | IndexedEventSubscription; | ||
| [FeedContract.TICKER]: Subscription; | ||
| [FeedContract.HISTORY]: TimeSeriesSubscription | IndexedEventSubscription; | ||
| [FeedContract.STREAM]: Subscription | TimeSeriesSubscription | IndexedEventSubscription; | ||
| }; | ||
| /** | ||
| * Listener for the feed channel events received from the channel. | ||
| */ | ||
| type DXLinkFeedEventListener = (event: FeedEventData[]) => void; | ||
| /** | ||
| * dxLink FEED service instance for the specified {@link FeedContract}. | ||
| */ | ||
| interface DXLinkFeedRequester<Contract extends FeedContract = FeedContract.AUTO> { | ||
| /** | ||
| * Unique identifier of the feed channel. | ||
| */ | ||
| readonly id: number; | ||
| /** | ||
| * Contract of the feed channel. | ||
| * @see {FeedContract} | ||
| */ | ||
| readonly contract: Contract; | ||
| /** | ||
| * Get current channel of the feed. | ||
| * Note: inaproppriate usage of the channel can lead to unexpected behavior. | ||
| * @see {DXLinkChannel} | ||
| */ | ||
| getChannel(): DXLinkChannel; | ||
| /** | ||
| * Configure desired configuration of the feed channel. | ||
| * @see {FeedAcceptConfig} | ||
| */ | ||
| configure(acceptConfig: FeedAcceptConfig): void; | ||
| /** | ||
| * Get current configuration of the feed channel as received from the channel. | ||
| */ | ||
| getConfig(): FeedConfig; | ||
| /** | ||
| * Add a listener for the feed channel config changes. | ||
| */ | ||
| addConfigChangeListener(listener: DXLinkFeedConfigChangeListener): void; | ||
| /** | ||
| * Remove a listener for the feed channel config changes. | ||
| */ | ||
| removeConfigChangeListener(listener: DXLinkFeedConfigChangeListener): void; | ||
| /** | ||
| * Add subscriptions to the feed channel. | ||
| * @param subscriptions - Subscriptions to be added. | ||
| */ | ||
| addSubscriptions(subscriptions: SubscriptionByContract[Contract][]): void; | ||
| /** | ||
| * Add subscriptions to the feed channel. | ||
| * @param subscriptions - Subscriptions to be added. | ||
| */ | ||
| addSubscriptions(...subscriptions: SubscriptionByContract[Contract][]): void; | ||
| /** | ||
| * Remove subscriptions from the feed channel. | ||
| * @param subscriptions - Subscriptions to be removed. | ||
| */ | ||
| removeSubscriptions(subscriptions: SubscriptionByContract[Contract][]): void; | ||
| /** | ||
| * Remove subscriptions from the feed channel. | ||
| * @param subscriptions - Subscriptions to be removed. | ||
| */ | ||
| removeSubscriptions(...subscriptions: SubscriptionByContract[Contract][]): void; | ||
| /** | ||
| * Remove all active subscriptions from the feed channel. | ||
| */ | ||
| clearSubscriptions(): void; | ||
| /** | ||
| * Add a listener for the feed channel events received from the channel. | ||
| */ | ||
| addEventListener(listener: DXLinkFeedEventListener): void; | ||
| /** | ||
| * Remove a listener for the feed channel events received from the channel. | ||
| */ | ||
| removeEventListener(listener: DXLinkFeedEventListener): void; | ||
| /** | ||
| * Close the feed channel. | ||
| */ | ||
| close(): void; | ||
| } | ||
| /** | ||
| * Options for the {@link DXLinkFeed} instance. | ||
| */ | ||
| interface DXLinkFeedOptions { | ||
| /** | ||
| * Space to be used for the service. | ||
| */ | ||
| space?: string; | ||
| /** | ||
| * Feed name to be used for the service. | ||
| */ | ||
| feed?: string; | ||
| /** | ||
| * Time in milliseconds to wait for more pending subscriptions before sending them to the channel. | ||
| */ | ||
| batchSubscriptionsTime: number; | ||
| /** | ||
| * Maximum size of the subscription chunk to be sent to the channel. | ||
| */ | ||
| maxSendSubscriptionChunkSize: number; | ||
| /** | ||
| * Log level for the feed. | ||
| */ | ||
| logLevel: DXLinkLogLevel; | ||
| } | ||
| /** | ||
| * dxLink FEED provides access to the real-time and historical data of dxFeed. | ||
| */ | ||
| declare class DXLinkFeed<Contract extends FeedContract> implements DXLinkFeedRequester<Contract> { | ||
| /** | ||
| * Unique identifier of the feed channel. | ||
| */ | ||
| readonly id: number; | ||
| /** | ||
| * Contract of the feed channel. | ||
| * @see {FeedContract} | ||
| */ | ||
| contract: Contract; | ||
| private readonly options; | ||
| /** | ||
| * dxLink channel instance. | ||
| */ | ||
| private readonly channel; | ||
| /** | ||
| * Current accept config of the feed channel. | ||
| */ | ||
| private acceptConfig; | ||
| /** | ||
| * Current config of the feed channel. | ||
| */ | ||
| private config; | ||
| private readonly configListeners; | ||
| private readonly eventListeners; | ||
| /** | ||
| * Pending add subscriptions to be sent to the channel. | ||
| */ | ||
| private readonly pendingAdd; | ||
| /** | ||
| * Pending remove subscriptions to be sent to the channel. | ||
| */ | ||
| private readonly pendingRemove; | ||
| /** | ||
| * Pending reset flag to be sent to the channel. | ||
| */ | ||
| private pengingReset; | ||
| /** | ||
| * List of active subscriptions. | ||
| * Used to avoid sending the same subscription twice and re-subscribe on the channel re-open. | ||
| */ | ||
| private readonly subscriptions; | ||
| /** | ||
| * List of event types which schema was sent to the channel. | ||
| */ | ||
| private readonly touchedEvents; | ||
| /** | ||
| * Scheduler for scheduling subscriptions sending to the channel. | ||
| */ | ||
| private readonly scheduler; | ||
| private readonly processPendingsSchedulerKey; | ||
| private readonly logger; | ||
| /** | ||
| * Allows to create {@link DXLinkFeed} instance with the specified {@link FeedContract} for the given {@link DXLinkWebSocketClient}. | ||
| */ | ||
| constructor(client: DXLinkClient, contract: Contract, options?: Partial<DXLinkFeedOptions>); | ||
| getChannel: () => DXLinkChannel; | ||
| getState: () => DXLinkChannelState; | ||
| addStateChangeListener: (listener: DXLinkChannelStateChangeListener) => void; | ||
| removeStateChangeListener: (listener: DXLinkChannelStateChangeListener) => void; | ||
| getConfig: () => FeedConfig; | ||
| addConfigChangeListener: (listener: DXLinkFeedConfigChangeListener) => void; | ||
| removeConfigChangeListener: (listener: DXLinkFeedConfigChangeListener) => void; | ||
| close: () => void; | ||
| configure: (acceptConfig: FeedAcceptConfig) => void; | ||
| addSubscriptions(subscriptions: SubscriptionByContract[Contract][]): void; | ||
| addSubscriptions(...subscriptions: SubscriptionByContract[Contract][]): void; | ||
| removeSubscriptions(subscriptions: SubscriptionByContract[Contract][]): void; | ||
| removeSubscriptions(...subscriptions: SubscriptionByContract[Contract][]): void; | ||
| clearSubscriptions: () => void; | ||
| addEventListener: (listener: DXLinkFeedEventListener) => void; | ||
| removeEventListener: (listener: DXLinkFeedEventListener) => void; | ||
| /** | ||
| * Clean the subscription from the fields which are not allowed for the specified contract. | ||
| * Note: coze of the TypeScript limitations, we need to clean the subscription from the fields which are not allowed for the specified contract. | ||
| */ | ||
| private cleanSubscription; | ||
| /** | ||
| * Process message received in the channel. | ||
| */ | ||
| private processMessage; | ||
| /** | ||
| * Process config received from the channel. | ||
| */ | ||
| private processConfig; | ||
| /** | ||
| * Parse data received from the channel. | ||
| */ | ||
| private parseEventData; | ||
| /** | ||
| * Process data received from the channel. | ||
| */ | ||
| private processData; | ||
| /** | ||
| * Process channel status changes from the channel. | ||
| */ | ||
| private processStatus; | ||
| /** | ||
| * Send the subscription chunk to the channel. | ||
| * @param chunk Subscription chunk to be sent to the channel. | ||
| * @param newTouchedEvents List of event types which schema should be sent to the channel before the chunk. | ||
| * @returns | ||
| */ | ||
| private sendSubscriptionChunkAndSchema; | ||
| /** | ||
| * Process error received from the channel. | ||
| */ | ||
| private processError; | ||
| /** | ||
| * Resubscribe to the feed channel subscriptions after the channel re-open. | ||
| */ | ||
| private resubscribe; | ||
| /** | ||
| * Schedule sending pending subscriptions to the channel to batch them together to reduce the number of messages. | ||
| */ | ||
| private scheduleProcessPendings; | ||
| /** | ||
| * Process pending subscriptions and send them to the channel. | ||
| */ | ||
| private processPendings; | ||
| /** | ||
| * Send the `FEED_SETUP` message to the channel with the event fields for the specified event types. | ||
| * @param eventTypes List of event type fields to be sent to the channel. | ||
| * @param force If `true`, the config will be sent to the channel even if there is no event fields to send. | ||
| */ | ||
| private sendAcceptConfig; | ||
| } | ||
| export { DXLinkFeed, type DXLinkFeedConfigChangeListener, type DXLinkFeedEventListener, type DXLinkFeedOptions, type DXLinkFeedRequester, type FeedAcceptConfig, type FeedConfig, FeedContract, FeedDataFormat, type FeedEventData, type FeedEventFields, type FeedEventValue, type IndexedEventSubscription, type Subscription, type SubscriptionByContract, type TimeSeriesSubscription }; |
+401
| // src/messages.ts | ||
| import "@dxfeed/dxlink-core"; | ||
| var FeedContract = /* @__PURE__ */ ((FeedContract2) => { | ||
| FeedContract2["AUTO"] = "AUTO"; | ||
| FeedContract2["TICKER"] = "TICKER"; | ||
| FeedContract2["HISTORY"] = "HISTORY"; | ||
| FeedContract2["STREAM"] = "STREAM"; | ||
| return FeedContract2; | ||
| })(FeedContract || {}); | ||
| var FeedDataFormat = /* @__PURE__ */ ((FeedDataFormat2) => { | ||
| FeedDataFormat2["FULL"] = "FULL"; | ||
| FeedDataFormat2["COMPACT"] = "COMPACT"; | ||
| return FeedDataFormat2; | ||
| })(FeedDataFormat || {}); | ||
| var isFeedFullData = (data) => typeof data[0] === "object"; | ||
| var isFeedCompactData = (data) => data.length >= 2 && typeof data[0] === "string" && Array.isArray(data[1]); | ||
| var isFeedMessage = (message) => message.type === "FEED_SETUP" || message.type === "FEED_CONFIG" || message.type === "FEED_SUBSCRIPTION" || message.type === "FEED_DATA"; | ||
| // src/feed.ts | ||
| import { | ||
| DXLinkChannelState, | ||
| DXLinkLogLevel, | ||
| Logger | ||
| } from "@dxfeed/dxlink-core"; | ||
| var getSubscriptionKey = (subscription) => `${subscription.type}${"source" in subscription ? `#${subscription.source}` : ""}:${subscription.symbol}`; | ||
| var FEED_SERVICE_NAME = "FEED"; | ||
| var DXLinkFeed = class _DXLinkFeed { | ||
| /** | ||
| * Allows to create {@link DXLinkFeed} instance with the specified {@link FeedContract} for the given {@link DXLinkWebSocketClient}. | ||
| */ | ||
| constructor(client, contract, options = {}) { | ||
| /** | ||
| * Current accept config of the feed channel. | ||
| */ | ||
| this.acceptConfig = {}; | ||
| /** | ||
| * Current config of the feed channel. | ||
| */ | ||
| this.config = { | ||
| aggregationPeriod: NaN, | ||
| dataFormat: "FULL" /* FULL */, | ||
| eventFields: {} | ||
| }; | ||
| // Listeners | ||
| this.configListeners = /* @__PURE__ */ new Set(); | ||
| this.eventListeners = /* @__PURE__ */ new Set(); | ||
| /** | ||
| * Pending add subscriptions to be sent to the channel. | ||
| */ | ||
| this.pendingAdd = /* @__PURE__ */ new Map(); | ||
| /** | ||
| * Pending remove subscriptions to be sent to the channel. | ||
| */ | ||
| this.pendingRemove = /* @__PURE__ */ new Map(); | ||
| /** | ||
| * Pending reset flag to be sent to the channel. | ||
| */ | ||
| this.pengingReset = false; | ||
| /** | ||
| * List of active subscriptions. | ||
| * Used to avoid sending the same subscription twice and re-subscribe on the channel re-open. | ||
| */ | ||
| this.subscriptions = /* @__PURE__ */ new Map(); | ||
| /** | ||
| * List of event types which schema was sent to the channel. | ||
| */ | ||
| this.touchedEvents = /* @__PURE__ */ new Set(); | ||
| this.getChannel = () => this.channel; | ||
| this.getState = () => this.channel.getState(); | ||
| this.addStateChangeListener = (listener) => this.channel.addStateChangeListener(listener); | ||
| this.removeStateChangeListener = (listener) => this.channel.removeStateChangeListener(listener); | ||
| this.getConfig = () => this.config; | ||
| this.addConfigChangeListener = (listener) => { | ||
| this.configListeners.add(listener); | ||
| }; | ||
| this.removeConfigChangeListener = (listener) => { | ||
| this.configListeners.delete(listener); | ||
| }; | ||
| this.close = () => { | ||
| this.acceptConfig = {}; | ||
| this.configListeners.clear(); | ||
| this.eventListeners.clear(); | ||
| this.pendingAdd.clear(); | ||
| this.pendingRemove.clear(); | ||
| this.touchedEvents.clear(); | ||
| this.subscriptions.clear(); | ||
| this.touchedEvents.clear(); | ||
| this.scheduler.cancel(this.processPendingsSchedulerKey); | ||
| this.channel.close(); | ||
| }; | ||
| this.configure = (acceptConfig) => { | ||
| this.acceptConfig = acceptConfig; | ||
| if (this.channel.getState() === DXLinkChannelState.OPENED) { | ||
| this.sendAcceptConfig(this.touchedEvents); | ||
| } | ||
| }; | ||
| this.clearSubscriptions = () => { | ||
| this.pendingAdd.clear(); | ||
| this.pendingRemove.clear(); | ||
| this.pengingReset = true; | ||
| this.scheduleProcessPendings(); | ||
| }; | ||
| this.addEventListener = (listener) => { | ||
| this.eventListeners.add(listener); | ||
| }; | ||
| this.removeEventListener = (listener) => { | ||
| this.eventListeners.delete(listener); | ||
| }; | ||
| /** | ||
| * Clean the subscription from the fields which are not allowed for the specified contract. | ||
| * Note: coze of the TypeScript limitations, we need to clean the subscription from the fields which are not allowed for the specified contract. | ||
| */ | ||
| this.cleanSubscription = (subscription) => { | ||
| if (this.contract === "TICKER" /* TICKER */) { | ||
| const { type, symbol, ...other } = subscription; | ||
| if (Object.keys(other).length > 0) { | ||
| this.logger.warn( | ||
| "Subscription for the TICKER contract should not have any additional fields", | ||
| subscription | ||
| ); | ||
| } | ||
| return { type, symbol }; | ||
| } | ||
| return subscription; | ||
| }; | ||
| /** | ||
| * Process message received in the channel. | ||
| */ | ||
| this.processMessage = (message) => { | ||
| if (isFeedMessage(message)) { | ||
| switch (message.type) { | ||
| case "FEED_CONFIG": | ||
| this.processConfig(message); | ||
| return; | ||
| case "FEED_DATA": | ||
| this.processData(message); | ||
| return; | ||
| } | ||
| } | ||
| this.logger.warn("Unknown message", message); | ||
| }; | ||
| /** | ||
| * Process config received from the channel. | ||
| */ | ||
| this.processConfig = (config) => { | ||
| const newConfig = { | ||
| aggregationPeriod: config.aggregationPeriod ?? this.config.aggregationPeriod, | ||
| dataFormat: config.dataFormat ?? this.config.dataFormat, | ||
| eventFields: { | ||
| ...this.config.eventFields ?? {}, | ||
| ...config.eventFields ?? {} | ||
| } | ||
| }; | ||
| this.config = newConfig; | ||
| for (const listener of this.configListeners) { | ||
| try { | ||
| listener(newConfig); | ||
| } catch (error) { | ||
| this.logger.error("Error in config listener", error); | ||
| } | ||
| } | ||
| }; | ||
| /** | ||
| * Parse data received from the channel. | ||
| */ | ||
| this.parseEventData = (data) => { | ||
| const { dataFormat, eventFields } = this.config; | ||
| if (dataFormat === "FULL") { | ||
| if (isFeedFullData(data)) { | ||
| return data; | ||
| } | ||
| } else if (isFeedCompactData(data)) { | ||
| const events = []; | ||
| const [eventType, values] = data; | ||
| const eventFieldsForType = eventFields[eventType]; | ||
| if (eventFieldsForType === void 0) { | ||
| throw new Error("Cannot find event fields for event type in the config"); | ||
| } | ||
| let cursor = 0; | ||
| while (cursor < values.length) { | ||
| const event = { | ||
| eventType | ||
| }; | ||
| for (const field of eventFieldsForType) { | ||
| const value = values[cursor]; | ||
| if (value === void 0) { | ||
| throw new Error("Not enough values in compact event"); | ||
| } | ||
| event[field] = value; | ||
| cursor++; | ||
| } | ||
| events.push(event); | ||
| } | ||
| return events; | ||
| } | ||
| throw new Error("Incoming data does not match the format specified in the config"); | ||
| }; | ||
| /** | ||
| * Process data received from the channel. | ||
| */ | ||
| this.processData = ({ data }) => { | ||
| try { | ||
| const events = this.parseEventData(data); | ||
| for (const listener of this.eventListeners) { | ||
| try { | ||
| listener(events); | ||
| } catch (error) { | ||
| this.logger.error("Error in event listener", error); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| this.logger.error("Cannot parse data", error); | ||
| return; | ||
| } | ||
| }; | ||
| /** | ||
| * Process channel status changes from the channel. | ||
| */ | ||
| this.processStatus = (processStatus) => { | ||
| switch (processStatus) { | ||
| case DXLinkChannelState.OPENED: { | ||
| this.sendAcceptConfig(this.touchedEvents, true); | ||
| return this.resubscribe(); | ||
| } | ||
| case DXLinkChannelState.REQUESTED: { | ||
| this.scheduler.cancel(this.processPendingsSchedulerKey); | ||
| return; | ||
| } | ||
| case DXLinkChannelState.CLOSED: | ||
| return this.close(); | ||
| } | ||
| }; | ||
| /** | ||
| * Send the subscription chunk to the channel. | ||
| * @param chunk Subscription chunk to be sent to the channel. | ||
| * @param newTouchedEvents List of event types which schema should be sent to the channel before the chunk. | ||
| * @returns | ||
| */ | ||
| this.sendSubscriptionChunkAndSchema = (chunk, newTouchedEvents) => { | ||
| if (this.channel.getState() !== DXLinkChannelState.OPENED) return; | ||
| if (newTouchedEvents !== void 0 && newTouchedEvents.size > 0) { | ||
| this.sendAcceptConfig(newTouchedEvents); | ||
| } | ||
| this.channel.send({ | ||
| type: "FEED_SUBSCRIPTION", | ||
| ...chunk | ||
| }); | ||
| }; | ||
| /** | ||
| * Process error received from the channel. | ||
| */ | ||
| this.processError = (processError) => { | ||
| this.logger.error("Error in channel", processError); | ||
| }; | ||
| /** | ||
| * Process pending subscriptions and send them to the channel. | ||
| */ | ||
| this.processPendings = () => { | ||
| const newTouchedEvents = /* @__PURE__ */ new Set(); | ||
| let chunk = {}; | ||
| let chunkSize = 0; | ||
| if (this.pengingReset) { | ||
| chunk.reset = true; | ||
| chunkSize += 13; | ||
| this.pengingReset = false; | ||
| } | ||
| for (const [key, subscription] of this.pendingRemove.entries()) { | ||
| ; | ||
| (chunk.remove ?? (chunk.remove = [])).push(subscription); | ||
| chunkSize += key.length + ("fromTime" in subscription ? 34 : 21); | ||
| this.subscriptions.delete(getSubscriptionKey(subscription)); | ||
| if (chunkSize >= this.options.maxSendSubscriptionChunkSize) { | ||
| this.sendSubscriptionChunkAndSchema(chunk); | ||
| chunk = {}; | ||
| chunkSize = 0; | ||
| } | ||
| } | ||
| this.pendingRemove.clear(); | ||
| for (const [key, subscription] of this.pendingAdd.entries()) { | ||
| ; | ||
| (chunk.add ?? (chunk.add = [])).push(subscription); | ||
| chunkSize += key.length + ("fromTime" in subscription ? 34 : 21); | ||
| if (!this.touchedEvents.has(subscription.type)) { | ||
| newTouchedEvents.add(subscription.type); | ||
| this.touchedEvents.add(subscription.type); | ||
| } | ||
| this.subscriptions.set(key, subscription); | ||
| if (chunkSize >= this.options.maxSendSubscriptionChunkSize) { | ||
| this.sendSubscriptionChunkAndSchema(chunk, newTouchedEvents); | ||
| newTouchedEvents.clear(); | ||
| chunk = {}; | ||
| chunkSize = 0; | ||
| } | ||
| } | ||
| this.pendingAdd.clear(); | ||
| if (chunkSize > 0) { | ||
| this.sendSubscriptionChunkAndSchema(chunk, newTouchedEvents); | ||
| } | ||
| }; | ||
| /** | ||
| * Send the `FEED_SETUP` message to the channel with the event fields for the specified event types. | ||
| * @param eventTypes List of event type fields to be sent to the channel. | ||
| * @param force If `true`, the config will be sent to the channel even if there is no event fields to send. | ||
| */ | ||
| this.sendAcceptConfig = (eventTypes, force = false) => { | ||
| let acceptEventFields; | ||
| if (this.acceptConfig.acceptEventFields !== void 0) { | ||
| for (const eventType of eventTypes) { | ||
| const eventFields = this.acceptConfig.acceptEventFields[eventType]; | ||
| if (eventFields !== void 0) { | ||
| acceptEventFields ?? (acceptEventFields = {}); | ||
| acceptEventFields[eventType] = eventFields; | ||
| } | ||
| } | ||
| } | ||
| const { acceptAggregationPeriod, acceptDataFormat } = this.acceptConfig; | ||
| if (acceptEventFields === void 0 && acceptAggregationPeriod === void 0 && acceptDataFormat === void 0) { | ||
| return; | ||
| } | ||
| if (force || acceptEventFields !== void 0 || acceptAggregationPeriod !== void 0 && acceptAggregationPeriod !== this.config.aggregationPeriod || acceptDataFormat !== void 0 && acceptDataFormat !== this.config.dataFormat) { | ||
| this.channel.send({ | ||
| type: "FEED_SETUP", | ||
| acceptAggregationPeriod, | ||
| acceptDataFormat, | ||
| acceptEventFields | ||
| }); | ||
| } | ||
| }; | ||
| this.options = { | ||
| logLevel: DXLinkLogLevel.WARN, | ||
| batchSubscriptionsTime: 100, | ||
| maxSendSubscriptionChunkSize: 4096 * 2, | ||
| ...options | ||
| }; | ||
| this.scheduler = client.getScheduler(); | ||
| this.channel = client.openChannel(FEED_SERVICE_NAME, { | ||
| contract, | ||
| // Optional parameters for FEED source | ||
| space: options.space, | ||
| feed: options.feed | ||
| }); | ||
| this.id = this.channel.id; | ||
| this.contract = contract; | ||
| this.channel.addMessageListener(this.processMessage); | ||
| this.channel.addStateChangeListener(this.processStatus); | ||
| this.channel.addErrorListener(this.processError); | ||
| this.addSubscriptions = this.addSubscriptions.bind(this); | ||
| this.removeSubscriptions = this.removeSubscriptions.bind(this); | ||
| this.logger = new Logger(`${_DXLinkFeed.name}#${this.id}`, this.options.logLevel); | ||
| this.processPendingsSchedulerKey = `${FEED_SERVICE_NAME}#${this.id}:PROCESS_PENDINGS`; | ||
| } | ||
| addSubscriptions(...args) { | ||
| const inputs = Array.isArray(args[0]) ? args[0] : args; | ||
| for (const input of inputs) { | ||
| const subscription = this.cleanSubscription(input); | ||
| this.pendingAdd.set(getSubscriptionKey(subscription), subscription); | ||
| } | ||
| this.scheduleProcessPendings(); | ||
| } | ||
| removeSubscriptions(...args) { | ||
| const inputs = Array.isArray(args[0]) ? args[0] : args; | ||
| for (const input of inputs) { | ||
| const subscription = this.cleanSubscription(input); | ||
| const key = getSubscriptionKey(subscription); | ||
| this.pendingRemove.set(key, subscription); | ||
| this.pendingAdd.delete(key); | ||
| } | ||
| this.scheduleProcessPendings(); | ||
| } | ||
| /** | ||
| * Resubscribe to the feed channel subscriptions after the channel re-open. | ||
| */ | ||
| resubscribe() { | ||
| for (const [key, subscription] of this.subscriptions) { | ||
| this.pendingAdd.set(key, subscription); | ||
| } | ||
| if (this.pendingAdd.size === 0) return; | ||
| this.pendingRemove.clear(); | ||
| this.pengingReset = true; | ||
| this.processPendings(); | ||
| } | ||
| /** | ||
| * Schedule sending pending subscriptions to the channel to batch them together to reduce the number of messages. | ||
| */ | ||
| scheduleProcessPendings() { | ||
| if (this.scheduler.has(this.processPendingsSchedulerKey)) { | ||
| return; | ||
| } | ||
| this.scheduler.schedule( | ||
| this.processPendings, | ||
| this.options.batchSubscriptionsTime, | ||
| this.processPendingsSchedulerKey | ||
| ); | ||
| } | ||
| }; | ||
| export { | ||
| DXLinkFeed, | ||
| FeedContract, | ||
| FeedDataFormat | ||
| }; | ||
| //# sourceMappingURL=index.mjs.map |
| {"version":3,"sources":["../src/messages.ts","../src/feed.ts"],"sourcesContent":["import { type DXLinkChannelMessage } from '@dxfeed/dxlink-core'\n\nexport enum FeedContract {\n 'AUTO' = 'AUTO',\n 'TICKER' = 'TICKER',\n 'HISTORY' = 'HISTORY',\n 'STREAM' = 'STREAM',\n}\n\nexport enum FeedDataFormat {\n 'FULL' = 'FULL',\n 'COMPACT' = 'COMPACT',\n}\n\nexport interface FeedParameters {\n readonly contract: FeedContract\n}\n\nexport interface FeedEventFields {\n [eventType: string]: string[]\n}\n\nexport interface FeedSetupMessage {\n readonly type: 'FEED_SETUP'\n readonly acceptAggregationPeriod?: number\n readonly acceptDataFormat?: FeedDataFormat\n readonly acceptEventFields?: FeedEventFields\n}\n\nexport interface FeedConfigMessage {\n readonly type: 'FEED_CONFIG'\n readonly aggregationPeriod: number\n readonly dataFormat: FeedDataFormat\n readonly eventFields?: FeedEventFields\n}\n\nexport type Subscription = {\n readonly type: string\n readonly symbol: string\n}\n\nexport type TimeSeriesSubscription = {\n readonly type: string\n readonly symbol: string\n readonly fromTime: number\n}\n\nexport type IndexedEventSubscription = {\n readonly type: string\n readonly symbol: string\n readonly source: string\n}\n\nexport interface FeedSubscriptionMessage {\n readonly type: 'FEED_SUBSCRIPTION'\n readonly add?: (Subscription | TimeSeriesSubscription | IndexedEventSubscription)[]\n readonly remove?: (Subscription | TimeSeriesSubscription | IndexedEventSubscription)[]\n readonly reset?: boolean\n}\n\nexport type FeedEventValue = number | string | boolean\n\nexport interface FeedEventData {\n [key: string]: FeedEventValue\n}\n\nexport type FeedCompactEventData = [string, FeedEventValue[]]\n\nexport interface FeedDataMessage {\n readonly type: 'FEED_DATA'\n readonly data: FeedEventData[] | FeedCompactEventData\n}\n\nexport type FeedMessage =\n | FeedSetupMessage\n | FeedConfigMessage\n | FeedSubscriptionMessage\n | FeedDataMessage\n\nexport const isFeedFullData = (\n data: FeedEventData[] | FeedCompactEventData\n): data is FeedEventData[] => typeof data[0] === 'object'\n\nexport const isFeedCompactData = (\n data: FeedEventData[] | FeedCompactEventData\n): data is FeedCompactEventData =>\n data.length >= 2 && typeof data[0] === 'string' && Array.isArray(data[1])\n\nexport const isFeedMessage = (message: DXLinkChannelMessage): message is FeedMessage =>\n message.type === 'FEED_SETUP' ||\n message.type === 'FEED_CONFIG' ||\n message.type === 'FEED_SUBSCRIPTION' ||\n message.type === 'FEED_DATA'\n","import {\n type DXLinkChannel,\n type DXLinkChannelMessage,\n DXLinkChannelState,\n type DXLinkChannelStateChangeListener,\n type DXLinkError,\n type DXLinkClient,\n DXLinkLogLevel,\n type DXLinkLogger,\n Logger,\n type DXLinkScheduler,\n} from '@dxfeed/dxlink-core'\n\nimport {\n type FeedEventFields,\n FeedContract,\n FeedDataFormat,\n type IndexedEventSubscription,\n type Subscription,\n type TimeSeriesSubscription,\n type FeedSetupMessage,\n type FeedSubscriptionMessage,\n isFeedMessage,\n isFeedFullData,\n type FeedDataMessage,\n isFeedCompactData,\n type FeedEventData,\n type FeedConfigMessage,\n} from './messages'\n\n/**\n * Prefered configuration for the feed channel.\n * Server can ignore some of the parameters and use own defaults.\n * @see {DXLinkFeed.configure}\n */\nexport interface FeedAcceptConfig {\n /**\n * Aggregation period in seconds.\n * If not specified, the channel will use the default value.\n * If specified as 0, the channel will try not aggregate events.\n */\n acceptAggregationPeriod?: number\n /**\n * Data format to be used for received events.\n * If not specified, the channel will use the default value `FULL`.\n */\n acceptDataFormat?: FeedDataFormat\n /**\n * Event fields to be included in received events.\n * If not specified, the channel will use the default value.\n * If specified as an empty array, the channel will try to send events with default fields.\n */\n acceptEventFields?: FeedEventFields\n}\n\n/**\n * Configuration of the feed channel.\n */\nexport interface FeedConfig {\n /**\n * Aggregation period in seconds.\n * @example 0.5 - 500 milliseconds.\n * @default `NaN`\n * @see {FeedAcceptConfig.acceptAggregationPeriod}\n */\n readonly aggregationPeriod: number\n /**\n * Data format to be used for received events.\n * @example `FULL` - object with keys and values.\n * @example `COMPACT` - array of values.\n * @default `FULL`\n * @see {FeedAcceptConfig.acceptDataFormat}\n */\n readonly dataFormat: FeedDataFormat\n /**\n * Event fields to be included in received events.\n * You can specify fields for all event types or for specific event types @see {FeedAcceptConfig.acceptEventFields}.\n * @example ```json\n * { \"Quote\": [\"eventSymbol\", \"askPrice\", \"bidPrice\"] }\n * ```\n * @default `{}`\n */\n readonly eventFields: FeedEventFields\n}\n\n/**\n * Listener for the feed channel config changes.\n */\nexport type DXLinkFeedConfigChangeListener = (config: FeedConfig) => void\n\ntype AnySubscription = Subscription | TimeSeriesSubscription | IndexedEventSubscription\n\n/**\n * Get a unique key for the subscription.\n */\nconst getSubscriptionKey = (subscription: AnySubscription) =>\n `${subscription.type}${'source' in subscription ? `#${subscription.source}` : ''}:${\n subscription.symbol\n }`\n\n/**\n * Subscription type by the contract.\n */\nexport type SubscriptionByContract = {\n [FeedContract.AUTO]: Subscription | TimeSeriesSubscription | IndexedEventSubscription\n [FeedContract.TICKER]: Subscription\n [FeedContract.HISTORY]: TimeSeriesSubscription | IndexedEventSubscription\n [FeedContract.STREAM]: Subscription | TimeSeriesSubscription | IndexedEventSubscription\n}\n\n/**\n * Listener for the feed channel events received from the channel.\n */\nexport type DXLinkFeedEventListener = (event: FeedEventData[]) => void\n\n/**\n * Chunk of the subscriptions to be sent to the channel.\n */\ninterface FeedSubscriptionChunk {\n add?: AnySubscription[]\n remove?: AnySubscription[]\n reset?: boolean\n}\n\n/**\n * dxLink FEED service instance for the specified {@link FeedContract}.\n */\nexport interface DXLinkFeedRequester<Contract extends FeedContract = FeedContract.AUTO> {\n /**\n * Unique identifier of the feed channel.\n */\n readonly id: number\n /**\n * Contract of the feed channel.\n * @see {FeedContract}\n */\n readonly contract: Contract\n\n /**\n * Get current channel of the feed.\n * Note: inaproppriate usage of the channel can lead to unexpected behavior.\n * @see {DXLinkChannel}\n */\n getChannel(): DXLinkChannel\n\n /**\n * Configure desired configuration of the feed channel.\n * @see {FeedAcceptConfig}\n */\n configure(acceptConfig: FeedAcceptConfig): void\n\n /**\n * Get current configuration of the feed channel as received from the channel.\n */\n getConfig(): FeedConfig\n /**\n * Add a listener for the feed channel config changes.\n */\n addConfigChangeListener(listener: DXLinkFeedConfigChangeListener): void\n /**\n * Remove a listener for the feed channel config changes.\n */\n removeConfigChangeListener(listener: DXLinkFeedConfigChangeListener): void\n\n /**\n * Add subscriptions to the feed channel.\n * @param subscriptions - Subscriptions to be added.\n */\n addSubscriptions(subscriptions: SubscriptionByContract[Contract][]): void\n /**\n * Add subscriptions to the feed channel.\n * @param subscriptions - Subscriptions to be added.\n */\n addSubscriptions(...subscriptions: SubscriptionByContract[Contract][]): void\n /**\n * Remove subscriptions from the feed channel.\n * @param subscriptions - Subscriptions to be removed.\n */\n removeSubscriptions(subscriptions: SubscriptionByContract[Contract][]): void\n /**\n * Remove subscriptions from the feed channel.\n * @param subscriptions - Subscriptions to be removed.\n */\n removeSubscriptions(...subscriptions: SubscriptionByContract[Contract][]): void\n /**\n * Remove all active subscriptions from the feed channel.\n */\n clearSubscriptions(): void\n\n /**\n * Add a listener for the feed channel events received from the channel.\n */\n addEventListener(listener: DXLinkFeedEventListener): void\n /**\n * Remove a listener for the feed channel events received from the channel.\n */\n removeEventListener(listener: DXLinkFeedEventListener): void\n\n /**\n * Close the feed channel.\n */\n close(): void\n}\n\n/**\n * Options for the {@link DXLinkFeed} instance.\n */\nexport interface DXLinkFeedOptions {\n /**\n * Space to be used for the service.\n */\n space?: string\n /**\n * Feed name to be used for the service.\n */\n feed?: string\n /**\n * Time in milliseconds to wait for more pending subscriptions before sending them to the channel.\n */\n batchSubscriptionsTime: number\n /**\n * Maximum size of the subscription chunk to be sent to the channel.\n */\n maxSendSubscriptionChunkSize: number\n /**\n * Log level for the feed.\n */\n logLevel: DXLinkLogLevel\n}\n\nconst FEED_SERVICE_NAME = 'FEED'\n\n/**\n * dxLink FEED provides access to the real-time and historical data of dxFeed.\n */\nexport class DXLinkFeed<Contract extends FeedContract> implements DXLinkFeedRequester<Contract> {\n /**\n * Unique identifier of the feed channel.\n */\n public readonly id: number\n\n /**\n * Contract of the feed channel.\n * @see {FeedContract}\n */\n public contract: Contract\n\n private readonly options: DXLinkFeedOptions\n\n /**\n * dxLink channel instance.\n */\n private readonly channel: DXLinkChannel\n\n /**\n * Current accept config of the feed channel.\n */\n private acceptConfig: FeedAcceptConfig = {}\n\n /**\n * Current config of the feed channel.\n */\n private config: FeedConfig = {\n aggregationPeriod: NaN,\n dataFormat: FeedDataFormat.FULL,\n eventFields: {},\n }\n\n // Listeners\n private readonly configListeners = new Set<DXLinkFeedConfigChangeListener>()\n private readonly eventListeners = new Set<DXLinkFeedEventListener>()\n\n /**\n * Pending add subscriptions to be sent to the channel.\n */\n private readonly pendingAdd = new Map<string, AnySubscription>()\n /**\n * Pending remove subscriptions to be sent to the channel.\n */\n private readonly pendingRemove = new Map<string, AnySubscription>()\n /**\n * Pending reset flag to be sent to the channel.\n */\n private pengingReset = false\n\n /**\n * List of active subscriptions.\n * Used to avoid sending the same subscription twice and re-subscribe on the channel re-open.\n */\n private readonly subscriptions = new Map<string, AnySubscription>()\n\n /**\n * List of event types which schema was sent to the channel.\n */\n private readonly touchedEvents = new Set<string>()\n\n /**\n * Scheduler for scheduling subscriptions sending to the channel.\n */\n private readonly scheduler: DXLinkScheduler\n private readonly processPendingsSchedulerKey: string\n\n private readonly logger: DXLinkLogger\n\n /**\n * Allows to create {@link DXLinkFeed} instance with the specified {@link FeedContract} for the given {@link DXLinkWebSocketClient}.\n */\n constructor(client: DXLinkClient, contract: Contract, options: Partial<DXLinkFeedOptions> = {}) {\n this.options = {\n logLevel: DXLinkLogLevel.WARN,\n batchSubscriptionsTime: 100,\n maxSendSubscriptionChunkSize: 4096 * 2,\n ...options,\n }\n this.scheduler = client.getScheduler()\n\n this.channel = client.openChannel(FEED_SERVICE_NAME, {\n contract,\n // Optional parameters for FEED source\n space: options.space,\n feed: options.feed,\n })\n this.id = this.channel.id\n this.contract = contract\n this.channel.addMessageListener(this.processMessage)\n this.channel.addStateChangeListener(this.processStatus)\n this.channel.addErrorListener(this.processError)\n\n this.addSubscriptions = this.addSubscriptions.bind(this)\n this.removeSubscriptions = this.removeSubscriptions.bind(this)\n\n this.logger = new Logger(`${DXLinkFeed.name}#${this.id}`, this.options.logLevel)\n\n this.processPendingsSchedulerKey = `${FEED_SERVICE_NAME}#${this.id}:PROCESS_PENDINGS`\n }\n\n getChannel = () => this.channel\n\n getState = () => this.channel.getState()\n addStateChangeListener = (listener: DXLinkChannelStateChangeListener) =>\n this.channel.addStateChangeListener(listener)\n removeStateChangeListener = (listener: DXLinkChannelStateChangeListener) =>\n this.channel.removeStateChangeListener(listener)\n\n getConfig = () => this.config\n addConfigChangeListener = (listener: DXLinkFeedConfigChangeListener) => {\n this.configListeners.add(listener)\n }\n removeConfigChangeListener = (listener: DXLinkFeedConfigChangeListener) => {\n this.configListeners.delete(listener)\n }\n\n close = () => {\n this.acceptConfig = {}\n\n this.configListeners.clear()\n this.eventListeners.clear()\n\n this.pendingAdd.clear()\n this.pendingRemove.clear()\n this.touchedEvents.clear()\n this.subscriptions.clear()\n this.touchedEvents.clear()\n\n this.scheduler.cancel(this.processPendingsSchedulerKey)\n\n this.channel.close()\n }\n\n configure = (acceptConfig: FeedAcceptConfig) => {\n this.acceptConfig = acceptConfig\n\n // Update touched events list\n if (this.channel.getState() === DXLinkChannelState.OPENED) {\n this.sendAcceptConfig(this.touchedEvents)\n }\n }\n\n addSubscriptions(subscriptions: SubscriptionByContract[Contract][]): void\n addSubscriptions(...subscriptions: SubscriptionByContract[Contract][]): void\n addSubscriptions(...args: unknown[]): void {\n const inputs: SubscriptionByContract[Contract][] = Array.isArray(args[0]) ? args[0] : args\n\n for (const input of inputs) {\n const subscription = this.cleanSubscription(input)\n\n this.pendingAdd.set(getSubscriptionKey(subscription), subscription)\n }\n\n this.scheduleProcessPendings()\n }\n\n removeSubscriptions(subscriptions: SubscriptionByContract[Contract][]): void\n removeSubscriptions(...subscriptions: SubscriptionByContract[Contract][]): void\n removeSubscriptions(...args: unknown[]): void {\n const inputs: SubscriptionByContract[Contract][] = Array.isArray(args[0]) ? args[0] : args\n\n for (const input of inputs) {\n const subscription = this.cleanSubscription(input)\n\n const key = getSubscriptionKey(subscription)\n this.pendingRemove.set(key, subscription)\n this.pendingAdd.delete(key)\n }\n\n this.scheduleProcessPendings()\n }\n\n clearSubscriptions = () => {\n this.pendingAdd.clear()\n this.pendingRemove.clear()\n this.pengingReset = true\n\n this.scheduleProcessPendings()\n }\n\n addEventListener = (listener: DXLinkFeedEventListener) => {\n this.eventListeners.add(listener)\n }\n removeEventListener = (listener: DXLinkFeedEventListener) => {\n this.eventListeners.delete(listener)\n }\n\n /**\n * Clean the subscription from the fields which are not allowed for the specified contract.\n * Note: coze of the TypeScript limitations, we need to clean the subscription from the fields which are not allowed for the specified contract.\n */\n private cleanSubscription = (\n subscription: SubscriptionByContract[Contract]\n ): SubscriptionByContract[Contract] => {\n if (this.contract === FeedContract.TICKER) {\n const { type, symbol, ...other } = subscription\n if (Object.keys(other).length > 0) {\n this.logger.warn(\n 'Subscription for the TICKER contract should not have any additional fields',\n subscription\n )\n }\n return { type, symbol } as unknown as SubscriptionByContract[Contract]\n }\n\n return subscription\n }\n\n /**\n * Process message received in the channel.\n */\n private processMessage = (message: DXLinkChannelMessage) => {\n // Parse message\n if (isFeedMessage(message)) {\n switch (message.type) {\n case 'FEED_CONFIG':\n this.processConfig(message)\n return\n case 'FEED_DATA':\n this.processData(message)\n return\n }\n }\n\n this.logger.warn('Unknown message', message)\n }\n\n /**\n * Process config received from the channel.\n */\n private processConfig = (config: FeedConfigMessage) => {\n // Update config with the new values from the channel\n const newConfig: FeedConfig = {\n aggregationPeriod: config.aggregationPeriod ?? this.config.aggregationPeriod,\n dataFormat: config.dataFormat ?? this.config.dataFormat,\n eventFields: {\n ...(this.config.eventFields ?? {}),\n ...(config.eventFields ?? {}),\n },\n }\n\n this.config = newConfig\n\n // Notify listeners\n for (const listener of this.configListeners) {\n try {\n listener(newConfig)\n } catch (error) {\n this.logger.error('Error in config listener', error)\n }\n }\n }\n\n /**\n * Parse data received from the channel.\n */\n private parseEventData = (data: FeedDataMessage['data']) => {\n const { dataFormat, eventFields } = this.config\n if (dataFormat === 'FULL') {\n if (isFeedFullData(data)) {\n // If data is already in the full format, just return it\n return data\n }\n } else if (isFeedCompactData(data)) {\n // If data is in the compact format, parse it\n const events: FeedEventData[] = []\n\n const [eventType, values] = data\n const eventFieldsForType = eventFields[eventType]\n if (eventFieldsForType === undefined) {\n throw new Error('Cannot find event fields for event type in the config')\n }\n\n // Split values into events\n let cursor = 0\n while (cursor < values.length) {\n const event: FeedEventData = {\n eventType,\n }\n\n // Build event object from the values\n for (const field of eventFieldsForType) {\n const value = values[cursor]\n if (value === undefined) {\n throw new Error('Not enough values in compact event')\n }\n event[field] = value\n cursor++\n }\n events.push(event)\n }\n\n return events\n }\n\n throw new Error('Incoming data does not match the format specified in the config')\n }\n\n /**\n * Process data received from the channel.\n */\n private processData = ({ data }: FeedDataMessage) => {\n try {\n const events = this.parseEventData(data)\n\n for (const listener of this.eventListeners) {\n try {\n listener(events)\n } catch (error) {\n this.logger.error('Error in event listener', error)\n }\n }\n } catch (error) {\n this.logger.error('Cannot parse data', error)\n return\n }\n }\n\n /**\n * Process channel status changes from the channel.\n */\n private processStatus = (processStatus: DXLinkChannelState) => {\n switch (processStatus) {\n case DXLinkChannelState.OPENED: {\n this.sendAcceptConfig(this.touchedEvents, true)\n\n return this.resubscribe()\n }\n case DXLinkChannelState.REQUESTED: {\n // Clear the timeout if it is set to avoid sending the subscriptions while the channel is not ready\n this.scheduler.cancel(this.processPendingsSchedulerKey)\n return\n }\n case DXLinkChannelState.CLOSED:\n // Destroy the channel if it is closed by the channel\n return this.close()\n }\n }\n\n /**\n * Send the subscription chunk to the channel.\n * @param chunk Subscription chunk to be sent to the channel.\n * @param newTouchedEvents List of event types which schema should be sent to the channel before the chunk.\n * @returns\n */\n private sendSubscriptionChunkAndSchema = (\n chunk: FeedSubscriptionChunk,\n newTouchedEvents?: Set<string>\n ) => {\n if (this.channel.getState() !== DXLinkChannelState.OPENED) return // If the channel is not ready, just exit\n\n if (newTouchedEvents !== undefined && newTouchedEvents.size > 0) {\n this.sendAcceptConfig(newTouchedEvents)\n }\n\n this.channel.send({\n type: 'FEED_SUBSCRIPTION',\n ...chunk,\n } satisfies FeedSubscriptionMessage)\n }\n\n /**\n * Process error received from the channel.\n */\n private processError = (processError: DXLinkError) => {\n this.logger.error('Error in channel', processError)\n }\n\n /**\n * Resubscribe to the feed channel subscriptions after the channel re-open.\n */\n private resubscribe() {\n // Merge pending add subscriptions with current subscriptions\n for (const [key, subscription] of this.subscriptions) {\n this.pendingAdd.set(key, subscription)\n }\n\n if (this.pendingAdd.size === 0) return // If there is no subscriptions to send, just exit\n\n // Clear pending remove subscriptions\n this.pendingRemove.clear()\n this.pengingReset = true\n\n // Send the subscriptions to the channel imidiately\n this.processPendings()\n }\n\n /**\n * Schedule sending pending subscriptions to the channel to batch them together to reduce the number of messages.\n */\n private scheduleProcessPendings() {\n if (this.scheduler.has(this.processPendingsSchedulerKey)) {\n return\n }\n\n this.scheduler.schedule(\n this.processPendings,\n this.options.batchSubscriptionsTime,\n this.processPendingsSchedulerKey\n )\n }\n\n /**\n * Process pending subscriptions and send them to the channel.\n */\n private processPendings = () => {\n const newTouchedEvents = new Set<string>() // New events to be sent to the channel\n let chunk: FeedSubscriptionChunk = {} // Chunk to be sent to the channel\n let chunkSize = 0 // Approximate size of the chunk in bytes\n\n // Add `reset` flag to the chunk\n if (this.pengingReset) {\n chunk.reset = true\n chunkSize += 13 // ',\"reset\":true'.length\n this.pengingReset = false\n }\n\n // Add `remove` subscriptions to the chunk\n for (const [key, subscription] of this.pendingRemove.entries()) {\n ;(chunk.remove ??= []).push(subscription)\n chunkSize += key.length + ('fromTime' in subscription ? 34 : 21) // Approximate size of the subscription in bytes\n\n // Remove the subscription from the active subscriptions\n this.subscriptions.delete(getSubscriptionKey(subscription))\n\n // Send the chunk if it is too big already\n if (chunkSize >= this.options.maxSendSubscriptionChunkSize) {\n this.sendSubscriptionChunkAndSchema(chunk)\n chunk = {}\n chunkSize = 0\n }\n }\n this.pendingRemove.clear()\n\n // Add `add` subscriptions to the chunk\n for (const [key, subscription] of this.pendingAdd.entries()) {\n ;(chunk.add ??= []).push(subscription)\n chunkSize += key.length + ('fromTime' in subscription ? 34 : 21) // Approximate size of the subscription in bytes\n\n // Add the event type to the new touched events if it is not touched yet\n if (!this.touchedEvents.has(subscription.type)) {\n newTouchedEvents.add(subscription.type)\n this.touchedEvents.add(subscription.type)\n }\n\n // Add the subscription to the active subscriptions\n this.subscriptions.set(key, subscription)\n\n // Send the chunk if it is too big already\n if (chunkSize >= this.options.maxSendSubscriptionChunkSize) {\n this.sendSubscriptionChunkAndSchema(chunk, newTouchedEvents)\n newTouchedEvents.clear()\n chunk = {}\n chunkSize = 0\n }\n }\n this.pendingAdd.clear()\n\n // Send the last chunk\n if (chunkSize > 0) {\n this.sendSubscriptionChunkAndSchema(chunk, newTouchedEvents)\n }\n }\n\n /**\n * Send the `FEED_SETUP` message to the channel with the event fields for the specified event types.\n * @param eventTypes List of event type fields to be sent to the channel.\n * @param force If `true`, the config will be sent to the channel even if there is no event fields to send.\n */\n private sendAcceptConfig = (eventTypes: Set<string>, force: boolean = false) => {\n let acceptEventFields: FeedEventFields | undefined\n\n // Get event fields for the specified event types\n if (this.acceptConfig.acceptEventFields !== undefined) {\n for (const eventType of eventTypes) {\n const eventFields = this.acceptConfig.acceptEventFields[eventType]\n if (eventFields !== undefined) {\n acceptEventFields ??= {}\n acceptEventFields[eventType] = eventFields\n }\n }\n }\n\n const { acceptAggregationPeriod, acceptDataFormat } = this.acceptConfig\n\n // Check if there is anything to send\n if (\n acceptEventFields === undefined &&\n acceptAggregationPeriod === undefined &&\n acceptDataFormat === undefined\n ) {\n return // Nothing to send\n }\n\n // Send the config to the channel if there is event fields to send or if it is forced to send\n // or if the aggregation period or data format was changed\n if (\n force ||\n acceptEventFields !== undefined ||\n (acceptAggregationPeriod !== undefined &&\n acceptAggregationPeriod !== this.config.aggregationPeriod) ||\n (acceptDataFormat !== undefined && acceptDataFormat !== this.config.dataFormat)\n ) {\n this.channel.send({\n type: 'FEED_SETUP',\n acceptAggregationPeriod,\n acceptDataFormat,\n acceptEventFields,\n } satisfies FeedSetupMessage)\n }\n }\n}\n"],"mappings":"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","names":["FeedContract","FeedDataFormat"]} |
+2
-2
| { | ||
| "name": "@dxfeed/dxlink-feed", | ||
| "version": "0.8.1", | ||
| "version": "0.9.0", | ||
| "private": false, | ||
@@ -30,3 +30,3 @@ "sideEffects": false, | ||
| "dependencies": { | ||
| "@dxfeed/dxlink-core": "0.8.1" | ||
| "@dxfeed/dxlink-core": "0.9.0" | ||
| }, | ||
@@ -33,0 +33,0 @@ "author": "Dmitry Petrov <dmitry.petrov@devexperts.com>", |
159509
58.85%11
37.5%833
91.94%+ Added
- Removed
Updated