smartapi-typescript
Version:
TypeScript library for Angel One SmartAPI broker API
873 lines • 37.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SmartAPIWebSocket = exports.ExchangeType = exports.FeedMode = void 0;
const ws_1 = __importDefault(require("ws"));
const events_1 = require("events");
const apiUrls_1 = require("../constants/apiUrls");
/**
* WebSocket feed modes according to WebSocket Streaming 2.0 specification
*/
var FeedMode;
(function (FeedMode) {
FeedMode[FeedMode["LTP"] = 1] = "LTP";
FeedMode[FeedMode["QUOTE"] = 2] = "QUOTE";
FeedMode[FeedMode["SNAP_QUOTE"] = 3] = "SNAP_QUOTE";
FeedMode[FeedMode["DEPTH_20"] = 4] = "DEPTH_20"; // 20 depth (only for NSE_CM)
})(FeedMode || (exports.FeedMode = FeedMode = {}));
/**
* Exchange types for WebSocket API
*/
var ExchangeType;
(function (ExchangeType) {
ExchangeType[ExchangeType["NSE_CM"] = 1] = "NSE_CM";
ExchangeType[ExchangeType["NSE_FO"] = 2] = "NSE_FO";
ExchangeType[ExchangeType["BSE_CM"] = 3] = "BSE_CM";
ExchangeType[ExchangeType["BSE_FO"] = 4] = "BSE_FO";
ExchangeType[ExchangeType["MCX_FO"] = 5] = "MCX_FO";
ExchangeType[ExchangeType["NCX_FO"] = 7] = "NCX_FO";
ExchangeType[ExchangeType["CDE_FO"] = 13] = "CDE_FO"; // CDE Futures & Options
})(ExchangeType || (exports.ExchangeType = ExchangeType = {}));
/**
* Action types for WebSocket commands
*/
var Action;
(function (Action) {
Action[Action["UNSUBSCRIBE"] = 0] = "UNSUBSCRIBE";
Action[Action["SUBSCRIBE"] = 1] = "SUBSCRIBE";
})(Action || (Action = {}));
/**
* WebSocket connection states
*/
var ConnectionState;
(function (ConnectionState) {
ConnectionState[ConnectionState["CONNECTING"] = 0] = "CONNECTING";
ConnectionState[ConnectionState["CONNECTED"] = 1] = "CONNECTED";
ConnectionState[ConnectionState["DISCONNECTING"] = 3] = "DISCONNECTING";
ConnectionState[ConnectionState["DISCONNECTED"] = 4] = "DISCONNECTED";
ConnectionState[ConnectionState["ERROR"] = 5] = "ERROR";
})(ConnectionState || (ConnectionState = {}));
/**
* SmartAPI WebSocket client for real-time market data
* Implementation based on WebSocket Streaming 2.0 specification
*/
class SmartAPIWebSocket extends events_1.EventEmitter {
/**
* Initialize a new SmartAPIWebSocket client
* @param apiKey API key provided by Angel One
* @param clientCode Client code/user id for Angel One
* @param feedToken Feed token for WebSocket authentication
* @param jwtToken JWT token from login API (auth token)
* @param debug Enable debug logging
*/
constructor(apiKey, clientCode, feedToken, jwtToken, debug = false) {
super();
this.ws = null;
this.connectionState = ConnectionState.DISCONNECTED;
this.subscriptions = new Map();
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectInterval = 5000; // 5 seconds
this.heartbeatInterval = null;
this.debug = false;
this.pendingSubscriptions = [];
this.isBrowser = false;
this.apiKey = apiKey;
this.clientCode = clientCode;
this.feedToken = feedToken;
this.jwtToken = jwtToken;
this.wsUrl = apiUrls_1.API_URLS.WEBSOCKET;
this.debug = debug;
// Detect if we're running in a browser environment
this.isBrowser = typeof window !== 'undefined';
if (this.isBrowser) {
// For browser environments, we need to append authentication as query parameters
this.wsUrl = `${this.wsUrl}?clientCode=${this.clientCode}&feedToken=${this.feedToken}&apiKey=${this.apiKey}`;
}
}
/**
* Log debug messages if debug mode is enabled
* @param message Message to log
* @param data Optional data to log
*/
log(message, data) {
if (this.debug) {
console.log(`[SmartAPIWebSocket] ${message}`);
if (data !== undefined) {
console.log(data);
}
}
}
/**
* Connect to the WebSocket server
*/
connect() {
this.log('Connecting to WebSocket server', this.wsUrl);
return new Promise((resolve, reject) => {
if (this.connectionState === ConnectionState.CONNECTED) {
this.log('Already connected');
resolve(true);
return;
}
this.connectionState = ConnectionState.CONNECTING;
try {
// In node.js environment, we use headers for authentication
const options = this.isBrowser ? {} : {
headers: {
'Authorization': this.jwtToken,
'x-api-key': this.apiKey,
'x-client-code': this.clientCode,
'x-feed-token': this.feedToken
}
};
this.ws = new ws_1.default(this.wsUrl, options);
this.ws.on('open', () => {
this.connectionState = ConnectionState.CONNECTED;
this.log('WebSocket connected');
this.reconnectAttempts = 0;
this.setupHeartbeat();
// Process pending subscriptions after successful connection
this.processPendingSubscriptions();
this.emit('connected');
resolve(true);
});
this.ws.on('message', (data) => {
try {
// First check if it's a text message
if (typeof data === 'string') {
// Check if it's a heartbeat response
if (data === 'pong') {
this.log('Received heartbeat response');
return;
}
// Try to parse as JSON (could be an error response)
try {
const message = JSON.parse(data);
if (message.errorCode || message.errorMessage) {
this.log('Received error', message);
this.emit('error', message);
}
else {
this.log('Received text message', message);
this.emit('message', message);
}
}
catch (e) {
this.log('Unknown text message', data);
}
}
else {
// Binary data - needs to be parsed based on subscription mode
this.parseBinaryMessage(data);
}
}
catch (e) {
this.log('Failed to process WebSocket message', e);
}
});
this.ws.on('error', (error) => {
this.log('WebSocket error', error);
this.connectionState = ConnectionState.ERROR;
this.emit('error', error);
reject(error);
});
this.ws.on('close', (code, reason) => {
this.log('WebSocket closed', { code, reason });
this.clearHeartbeat();
this.connectionState = ConnectionState.DISCONNECTED;
this.emit('disconnect', { code, reason });
// Attempt reconnection
this.attemptReconnect();
});
}
catch (error) {
this.log('Failed to create WebSocket connection', error);
this.connectionState = ConnectionState.ERROR;
this.emit('error', error);
reject(error);
}
});
}
/**
* Attempt to reconnect to the WebSocket server
*/
attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.log('Maximum reconnect attempts reached');
this.emit('reconnect_failed');
return;
}
this.reconnectAttempts++;
this.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
this.emit('reconnecting', this.reconnectAttempts);
setTimeout(() => {
this.connect().then(() => {
// Resubscribe to all active subscriptions
this.resubscribe();
}).catch(() => {
// Error will be emitted by the connect method
});
}, this.reconnectInterval);
}
/**
* Set up heartbeat to keep the connection alive
*/
setupHeartbeat() {
this.clearHeartbeat();
this.heartbeatInterval = setInterval(() => {
var _a;
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
this.log('Sending heartbeat');
this.ws.send('ping'); // Send 'ping' as text every 30 seconds
}
}, 30000); // 30 seconds
}
/**
* Clear the heartbeat interval
*/
clearHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* Parse binary message received from WebSocket
* @param data Binary data buffer
*/
parseBinaryMessage(data) {
try {
// First byte indicates the subscription mode
const mode = data.readInt8(0);
// Second byte is the exchange type
const exchangeType = data.readInt8(1);
// Token is a string (up to 25 bytes, null-terminated)
let token = '';
for (let i = 0; i < 25; i++) {
const charCode = data.readUInt8(2 + i);
if (charCode === 0)
break; // null terminator
token += String.fromCharCode(charCode);
}
// Create a base tick object
const tick = {
mode,
exchangeType,
token,
};
// Sequence number
if (data.length >= 35) {
tick.sequenceNumber = data.readBigInt64LE(27);
}
// Exchange timestamp
if (data.length >= 43) {
tick.exchangeTimestamp = Number(data.readBigInt64LE(35));
}
// Parse the rest based on the mode
if (mode === FeedMode.LTP) {
// LTP mode - packet size = 51 bytes
if (data.length >= 51) {
tick.lastTradedPrice = Number(data.readInt32LE(43));
}
}
else if (mode === FeedMode.QUOTE) {
// Quote mode - packet size = 123 bytes
if (data.length >= 51) {
tick.lastTradedPrice = Number(data.readInt32LE(43));
}
if (data.length >= 59) {
tick.lastTradedQuantity = Number(data.readBigInt64LE(51));
}
if (data.length >= 67) {
tick.averageTradedPrice = Number(data.readBigInt64LE(59));
}
if (data.length >= 75) {
tick.volumeTradedForTheDay = Number(data.readBigInt64LE(67));
}
if (data.length >= 83) {
tick.totalBuyQuantity = data.readDoubleLE(75);
}
if (data.length >= 91) {
tick.totalSellQuantity = data.readDoubleLE(83);
}
if (data.length >= 99) {
tick.openPrice = Number(data.readBigInt64LE(91));
}
if (data.length >= 107) {
tick.highPrice = Number(data.readBigInt64LE(99));
}
if (data.length >= 115) {
tick.lowPrice = Number(data.readBigInt64LE(107));
}
if (data.length >= 123) {
tick.closePrice = Number(data.readBigInt64LE(115));
}
}
else if (mode === FeedMode.SNAP_QUOTE) {
// Snap Quote mode - packet size = 379 bytes
if (data.length >= 51) {
tick.lastTradedPrice = Number(data.readInt32LE(43));
}
if (data.length >= 59) {
tick.lastTradedQuantity = Number(data.readBigInt64LE(51));
}
if (data.length >= 67) {
tick.averageTradedPrice = Number(data.readBigInt64LE(59));
}
if (data.length >= 75) {
tick.volumeTradedForTheDay = Number(data.readBigInt64LE(67));
}
if (data.length >= 83) {
tick.totalBuyQuantity = data.readDoubleLE(75);
}
if (data.length >= 91) {
tick.totalSellQuantity = data.readDoubleLE(83);
}
if (data.length >= 99) {
tick.openPrice = Number(data.readBigInt64LE(91));
}
if (data.length >= 107) {
tick.highPrice = Number(data.readBigInt64LE(99));
}
if (data.length >= 115) {
tick.lowPrice = Number(data.readBigInt64LE(107));
}
if (data.length >= 123) {
tick.closePrice = Number(data.readBigInt64LE(115));
}
if (data.length >= 131) {
tick.lastTradedTimestamp = Number(data.readBigInt64LE(123));
}
if (data.length >= 139) {
tick.openInterest = Number(data.readBigInt64LE(131));
}
if (data.length >= 147) {
// Open interest change % is a dummy field according to docs
tick.openInterestChangePercent = data.readDoubleLE(139);
}
// Best 5 data - 10 packets of 20 bytes each (5 buy + 5 sell)
if (data.length >= 347) {
tick.bestBids = [];
tick.bestAsks = [];
for (let i = 0; i < 10; i++) {
const offset = 147 + (i * 20);
const isBuy = data.readInt16LE(offset) === 1;
const quantity = Number(data.readBigInt64LE(offset + 2));
const price = Number(data.readBigInt64LE(offset + 10));
const numOrders = data.readInt16LE(offset + 18);
const entry = {
quantity,
price,
numOrders
};
if (isBuy) {
tick.bestBids.push(entry);
}
else {
tick.bestAsks.push(entry);
}
}
}
if (data.length >= 355) {
tick.upperCircuitLimit = Number(data.readBigInt64LE(347));
}
if (data.length >= 363) {
tick.lowerCircuitLimit = Number(data.readBigInt64LE(355));
}
if (data.length >= 371) {
tick.fiftyTwoWeekHighPrice = Number(data.readBigInt64LE(363));
}
if (data.length >= 379) {
tick.fiftyTwoWeekLowPrice = Number(data.readBigInt64LE(371));
}
}
else if (mode === FeedMode.DEPTH_20) {
// Depth 20 mode - only available for NSE_CM
if (exchangeType === ExchangeType.NSE_CM && data.length >= 443) {
if (data.length >= 35) {
tick.exchangeTimestamp = Number(data.readBigInt64LE(27));
}
if (data.length >= 43) {
// dummy placeholder field
tick.dummyPlaceholder = Number(data.readBigInt64LE(35));
}
// Best 20 bids and asks
tick.bestBids = [];
tick.bestAsks = [];
// Parse best 20 bids (43 + 200 = 243)
for (let i = 0; i < 20; i++) {
const offset = 43 + (i * 10);
const quantity = data.readInt32LE(offset);
const price = data.readInt32LE(offset + 4);
const numOrders = data.readInt16LE(offset + 8);
tick.bestBids.push({
quantity,
price,
numOrders
});
}
// Parse best 20 asks (243 + 200 = 443)
for (let i = 0; i < 20; i++) {
const offset = 243 + (i * 10);
const quantity = data.readInt32LE(offset);
const price = data.readInt32LE(offset + 4);
const numOrders = data.readInt16LE(offset + 8);
tick.bestAsks.push({
quantity,
price,
numOrders
});
}
}
}
// Normalize price values (divide by 100 or 10000000 as per documentation)
if (tick.lastTradedPrice !== undefined) {
// For currencies, divide by 10000000 (7 zeros), for everything else divide by 100
const divisor = exchangeType === ExchangeType.CDE_FO ? 10000000 : 100;
tick.lastTradedPrice = tick.lastTradedPrice / divisor;
if (tick.openPrice !== undefined)
tick.openPrice = tick.openPrice / divisor;
if (tick.highPrice !== undefined)
tick.highPrice = tick.highPrice / divisor;
if (tick.lowPrice !== undefined)
tick.lowPrice = tick.lowPrice / divisor;
if (tick.closePrice !== undefined)
tick.closePrice = tick.closePrice / divisor;
if (tick.upperCircuitLimit !== undefined)
tick.upperCircuitLimit = tick.upperCircuitLimit / divisor;
if (tick.lowerCircuitLimit !== undefined)
tick.lowerCircuitLimit = tick.lowerCircuitLimit / divisor;
if (tick.fiftyTwoWeekHighPrice !== undefined)
tick.fiftyTwoWeekHighPrice = tick.fiftyTwoWeekHighPrice / divisor;
if (tick.fiftyTwoWeekLowPrice !== undefined)
tick.fiftyTwoWeekLowPrice = tick.fiftyTwoWeekLowPrice / divisor;
// Normalize prices in best bids/asks
if (tick.bestBids) {
tick.bestBids.forEach((bid) => {
bid.price = bid.price / divisor;
});
}
if (tick.bestAsks) {
tick.bestAsks.forEach((ask) => {
ask.price = ask.price / divisor;
});
}
}
// Emit the parsed tick
this.log('Parsed tick data', tick);
this.emit('tick', tick);
// Emit on subscription-specific channel
const key = this.getSubscriptionKey(exchangeType, token);
const subscription = this.subscriptions.get(key);
if (subscription) {
this.emit(`tick:${subscription.correlationId}`, tick);
}
}
catch (error) {
this.log('Error parsing binary message:', error);
}
}
/**
* Process any pending subscriptions after successful connection
*/
processPendingSubscriptions() {
if (this.pendingSubscriptions.length === 0) {
return;
}
this.log(`Processing ${this.pendingSubscriptions.length} pending subscriptions`);
// Group pending subscriptions by mode and exchange
const subscriptionGroups = new Map();
for (const sub of this.pendingSubscriptions) {
const groupKey = `${sub.feedMode}`;
if (!subscriptionGroups.has(groupKey)) {
subscriptionGroups.set(groupKey, []);
}
subscriptionGroups.get(groupKey).push(sub);
}
// Process each group
for (const [groupKey, subs] of subscriptionGroups.entries()) {
const correlationId = `pending-${groupKey}-${Date.now()}`;
const feedMode = subs[0].feedMode;
// Group by exchange type
const exchangeTokens = new Map();
for (const sub of subs) {
if (!exchangeTokens.has(sub.exchangeType)) {
exchangeTokens.set(sub.exchangeType, []);
}
exchangeTokens.get(sub.exchangeType).push(sub.token);
// Save subscription
const key = this.getSubscriptionKey(sub.exchangeType, sub.token);
this.subscriptions.set(key, sub);
}
// Build token list
const tokenList = Array.from(exchangeTokens.entries()).map(([exchangeType, tokens]) => ({
exchangeType,
tokens
}));
// Send subscription request
this.sendSubscriptionRequest(correlationId, feedMode, tokenList);
}
// Clear pending subscriptions
this.pendingSubscriptions = [];
}
/**
* Send subscription/unsubscription request
* @param correlationId Correlation ID for the request
* @param feedMode Feed mode
* @param tokenList List of exchange and token pairs
* @param action Action (SUBSCRIBE or UNSUBSCRIBE)
*/
sendSubscriptionRequest(correlationId, feedMode, tokenList, action = Action.SUBSCRIBE) {
// Check connection
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
this.log('Cannot send request - WebSocket not connected');
return;
}
// Check for depth20 limitations
if (feedMode === FeedMode.DEPTH_20) {
let totalTokens = 0;
for (const item of tokenList) {
if (item.exchangeType === ExchangeType.NSE_CM) {
totalTokens += item.tokens.length;
}
}
if (totalTokens > 50) {
this.log('Error: Depth20 mode has a limit of 50 tokens per connection');
this.emit('error', {
errorCode: 'E1002',
errorMessage: 'Invalid Request. Subscription Limit Exceeded.',
correlationID: correlationId
});
return;
}
}
// Create request payload
const request = {
correlationID: correlationId,
action: action,
params: {
mode: feedMode,
tokenList: tokenList.map(item => ({
exchangeType: item.exchangeType,
tokens: item.tokens
}))
}
};
this.log('Sending request', request);
try {
this.ws.send(JSON.stringify(request));
}
catch (error) {
this.log('Error sending request', error);
}
}
/**
* Generate a subscription key from exchange type and token
* @param exchangeType Exchange type
* @param token Symbol token
* @returns Subscription key
*/
getSubscriptionKey(exchangeType, token) {
return `${exchangeType}:${token}`;
}
/**
* Subscribe to market data for a symbol
* @param correlationId Client-defined correlation ID for this subscription
* @param exchangeType Exchange type
* @param token Symbol token
* @param feedMode Feed mode (LTP, QUOTE, SNAP_QUOTE, DEPTH_20)
* @returns Subscription result
*/
subscribe(correlationId, exchangeType, token, feedMode = FeedMode.LTP) {
return new Promise((resolve, reject) => {
// Check if we're attempting to use DEPTH_20 for non-NSE_CM exchange
if (feedMode === FeedMode.DEPTH_20 && exchangeType !== ExchangeType.NSE_CM) {
reject({
status: false,
message: '20-Depth Mode is available only for NSE_CM segment'
});
return;
}
// If not connected yet, queue the subscription for later
if (this.connectionState !== ConnectionState.CONNECTED) {
this.log('Not connected yet, queuing subscription', {
correlationId, exchangeType, token, feedMode
});
this.pendingSubscriptions.push({
correlationId,
exchangeType,
token,
feedMode
});
// If not connected, initiate connection
if (this.connectionState === ConnectionState.DISCONNECTED) {
this.connect().catch(error => {
reject(error);
});
}
resolve({ status: true, message: 'Subscription queued' });
return;
}
const key = this.getSubscriptionKey(exchangeType, token);
// Save subscription data
this.subscriptions.set(key, {
correlationId,
exchangeType,
token,
feedMode
});
// Send subscription request
this.sendSubscriptionRequest(correlationId, feedMode, [{ exchangeType, tokens: [token] }]);
resolve({ status: true, message: 'Subscription request sent' });
});
}
/**
* Subscribe to multiple symbols at once
* @param correlationId Client-defined correlation ID for this subscription batch
* @param symbols Array of {exchangeType, token} objects
* @param feedMode Feed mode (LTP, QUOTE, SNAP_QUOTE, DEPTH_20)
* @returns Subscription result
*/
subscribeMultiple(correlationId, symbols, feedMode = FeedMode.LTP) {
return new Promise((resolve, reject) => {
// If DEPTH_20 mode, ensure all symbols are NSE_CM
if (feedMode === FeedMode.DEPTH_20) {
const nonNseSymbol = symbols.find(s => s.exchangeType !== ExchangeType.NSE_CM);
if (nonNseSymbol) {
reject({
status: false,
message: '20-Depth Mode is available only for NSE_CM segment'
});
return;
}
if (symbols.length > 50) {
reject({
status: false,
message: 'For 20-Depth Mode, the maximum limit is 50 tokens per connection'
});
return;
}
}
if (this.connectionState !== ConnectionState.CONNECTED) {
this.log('Not connected yet, queuing multiple subscriptions');
// Queue individual subscriptions
for (const symbol of symbols) {
this.pendingSubscriptions.push({
correlationId: `${correlationId}-${symbol.exchangeType}-${symbol.token}`,
exchangeType: symbol.exchangeType,
token: symbol.token,
feedMode
});
}
// If not connected, initiate connection
if (this.connectionState === ConnectionState.DISCONNECTED) {
this.connect().catch(error => {
reject(error);
});
}
resolve({ status: true, message: 'Multiple subscriptions queued' });
return;
}
// Group symbols by exchange type
const exchangeTokens = new Map();
for (const symbol of symbols) {
const key = this.getSubscriptionKey(symbol.exchangeType, symbol.token);
// Save subscription
this.subscriptions.set(key, {
correlationId: `${correlationId}-${symbol.exchangeType}-${symbol.token}`,
exchangeType: symbol.exchangeType,
token: symbol.token,
feedMode
});
if (!exchangeTokens.has(symbol.exchangeType)) {
exchangeTokens.set(symbol.exchangeType, []);
}
exchangeTokens.get(symbol.exchangeType).push(symbol.token);
}
// Build token list
const tokenList = Array.from(exchangeTokens.entries()).map(([exchangeType, tokens]) => ({
exchangeType,
tokens
}));
// Send subscription request
this.sendSubscriptionRequest(correlationId, feedMode, tokenList);
resolve({ status: true, message: 'Multiple subscription request sent' });
});
}
/**
* Unsubscribe from market data for a symbol
* @param correlationId Client-defined correlation ID for this unsubscription
* @param exchangeType Exchange type
* @param token Symbol token
* @returns Unsubscription result
*/
unsubscribe(correlationId, exchangeType, token) {
return new Promise((resolve, reject) => {
if (this.connectionState !== ConnectionState.CONNECTED) {
const error = { status: false, message: 'WebSocket is not connected' };
reject(error);
return;
}
const key = this.getSubscriptionKey(exchangeType, token);
// Check if subscription exists
if (!this.subscriptions.has(key)) {
// According to the docs, unsubscribing from tokens which are not subscribed
// will be gracefully ignored by the server, so we'll just log a warning
this.log('Unsubscription warning - not explicitly subscribed');
}
// Send unsubscription request
this.sendSubscriptionRequest(correlationId, FeedMode.LTP, // doesn't matter for unsubscription
[{ exchangeType, tokens: [token] }], Action.UNSUBSCRIBE);
// Remove from subscriptions map
this.subscriptions.delete(key);
resolve({ status: true, message: 'Unsubscription request sent' });
});
}
/**
* Unsubscribe from multiple symbols at once
* @param correlationId Client-defined correlation ID for this unsubscription batch
* @param symbols Array of {exchangeType, token} objects
* @returns Unsubscription result
*/
unsubscribeMultiple(correlationId, symbols) {
return new Promise((resolve, reject) => {
if (this.connectionState !== ConnectionState.CONNECTED) {
const error = { status: false, message: 'WebSocket is not connected' };
reject(error);
return;
}
// Group symbols by exchange type
const exchangeTokens = new Map();
for (const symbol of symbols) {
const key = this.getSubscriptionKey(symbol.exchangeType, symbol.token);
// Remove from subscriptions map
this.subscriptions.delete(key);
if (!exchangeTokens.has(symbol.exchangeType)) {
exchangeTokens.set(symbol.exchangeType, []);
}
exchangeTokens.get(symbol.exchangeType).push(symbol.token);
}
// Build token list
const tokenList = Array.from(exchangeTokens.entries()).map(([exchangeType, tokens]) => ({
exchangeType,
tokens
}));
// Send unsubscription request
this.sendSubscriptionRequest(correlationId, FeedMode.LTP, // doesn't matter for unsubscription
tokenList, Action.UNSUBSCRIBE);
resolve({ status: true, message: 'Multiple unsubscription request sent' });
});
}
/**
* Resubscribe to all active subscriptions
*/
resubscribe() {
if (this.subscriptions.size === 0) {
return;
}
this.log(`Resubscribing to ${this.subscriptions.size} symbols`);
// Group subscriptions by feed mode for efficiency
const subscriptionsByMode = new Map();
for (const subscription of this.subscriptions.values()) {
const { feedMode, exchangeType, token } = subscription;
if (!subscriptionsByMode.has(feedMode)) {
subscriptionsByMode.set(feedMode, new Map());
}
const exchangeMap = subscriptionsByMode.get(feedMode);
if (!exchangeMap.has(exchangeType)) {
exchangeMap.set(exchangeType, []);
}
exchangeMap.get(exchangeType).push(token);
}
// Subscribe to each group by mode
for (const [mode, exchangeMap] of subscriptionsByMode.entries()) {
// For DEPTH_20 mode, we need to enforce the 50 token limit
if (mode === FeedMode.DEPTH_20) {
// Count NSE_CM tokens
let nseCmTokens = [];
if (exchangeMap.has(ExchangeType.NSE_CM)) {
nseCmTokens = exchangeMap.get(ExchangeType.NSE_CM);
}
// If we have more than 50, split them
if (nseCmTokens.length > 50) {
for (let i = 0; i < nseCmTokens.length; i += 50) {
const batch = nseCmTokens.slice(i, i + 50);
const batchId = `resubscribe-depth20-${i / 50}`;
this.sendSubscriptionRequest(batchId, mode, [{ exchangeType: ExchangeType.NSE_CM, tokens: batch }]);
}
}
else if (nseCmTokens.length > 0) {
this.sendSubscriptionRequest(`resubscribe-depth20`, mode, [{ exchangeType: ExchangeType.NSE_CM, tokens: nseCmTokens }]);
}
}
else {
// For other modes, we can batch by exchange type
const tokenList = Array.from(exchangeMap.entries()).map(([exchangeType, tokens]) => ({
exchangeType,
tokens
}));
// Each subscription can have at most 1000 tokens, but we'll batch by 200 to be safe
const batchSize = 200;
let batchCounter = 0;
while (tokenList.length > 0) {
const batch = [];
let totalTokenCount = 0;
// Take exchanges one by one until we hit the batch size
while (tokenList.length > 0 && totalTokenCount < batchSize) {
const exchange = tokenList[0];
if (exchange.tokens.length + totalTokenCount <= batchSize) {
// Can take the whole exchange
batch.push(tokenList.shift());
totalTokenCount += exchange.tokens.length;
}
else {
// Need to split the exchange
const tokensForBatch = exchange.tokens.splice(0, batchSize - totalTokenCount);
batch.push({
exchangeType: exchange.exchangeType,
tokens: tokensForBatch
});
totalTokenCount += tokensForBatch.length;
}
}
if (batch.length > 0) {
const batchId = `resubscribe-mode${mode}-${batchCounter++}`;
this.sendSubscriptionRequest(batchId, mode, batch);
}
}
}
}
}
/**
* Disconnect from the WebSocket server
*/
disconnect() {
if (this.connectionState === ConnectionState.DISCONNECTED) {
this.log('Already disconnected');
return;
}
this.log('Disconnecting');
this.connectionState = ConnectionState.DISCONNECTING;
this.clearHeartbeat();
// Clear pending subscriptions
this.pendingSubscriptions = [];
if (this.ws) {
// Clear subscriptions
this.subscriptions.clear();
// Close the connection
try {
this.ws.close();
}
catch (error) {
this.log('Error closing WebSocket', error);
}
this.ws = null;
this.connectionState = ConnectionState.DISCONNECTED;
this.emit('disconnect', { reason: 'User initiated disconnect' });
}
}
}
exports.SmartAPIWebSocket = SmartAPIWebSocket;
//# sourceMappingURL=WebSocket.js.map