Location
sdks/typescript/pmxt/ws-client.ts:248-304 (subscribeBatch) and lines 43-46 (the Maps)
Code
// Instance-level Maps — grow without bound during session lifetime
private dataStore: Map<string, any> = new Map(); // grows forever
private subscriptions: Map<string, WsSubscription> = new Map(); // grows forever
// (activeSubs Map is only used by subscribe(), not subscribeBatch())
async subscribeBatch(
exchange: string,
method: string,
args: any[],
credentials?: Record<string, any>,
timeoutMs = 30000,
): Promise<Record<string, any>> {
const symbols: string[] = Array.isArray(args[0]) ? args[0] : [];
await this.ensureConnected();
const requestId = `req-${Math.random().toString(36).slice(2, 14)}`; // ← fresh ID every call
const sub: WsSubscription = {
requestId,
method,
symbols,
resolve: null,
reject: null,
};
this.subscriptions.set(requestId, sub); // ← never removed
this.ws!.send(JSON.stringify(message));
await this.waitForData(requestId, timeoutMs);
// Collect per-symbol data — entries never deleted afterward
const result: Record<string, any> = {};
for (const symbol of symbols) {
const storeKey = `${requestId}:${symbol}`;
const data = this.dataStore.get(storeKey); // ← read but never deleted
if (data !== undefined) {
result[symbol] = data;
}
}
return result;
}
Growth Pattern
Unlike subscribe(), which deduplicates via activeSubs and reuses the same requestId for the same symbol set, subscribeBatch() generates a fresh UUID on every call. This means:
this.subscriptions[requestId] gets a new entry per subscribeBatch() call, never removed.
this.dataStore["${requestId}:${symbol}"] gets a new entry per symbol per call, never deleted after being read.
this.dataStore[requestId] (the fallback at line 303) also persists.
subscribeBatch is the backing transport for watchOrderBooks() in the TypeScript SDK. In the CCXT Pro pattern, users call watchOrderBooks() in a tight while (true) loop — one call per desired tick. Each iteration silently leaks entries.
This is the exact TypeScript analog of Python issue #363.
OOM Estimate
- Per
subscribeBatch() call: 1 WsSubscription object (~200 bytes) + N dataStore entries (each an order book dict, ~500 bytes per symbol)
- Watching 50 symbols, polling at 1 Hz:
- After 1 hour: 3,600 subscription entries + 180,000 dataStore entries
- Memory: 3,600 × 200 B + 180,000 × 500 B = ~90 MB after 1 hour
- After 24 hours: ~2.2 GB — OOM on typical Node.js deployments with default heap limits
- V8 GC cannot collect these entries because
this.subscriptions and this.dataStore hold live references
Suggested Fix
After subscribeBatch() collects its result, delete the consumed entries:
// After collecting result:
this.subscriptions.delete(requestId);
for (const symbol of symbols) {
this.dataStore.delete(`${requestId}:${symbol}`);
}
this.dataStore.delete(requestId);
Also add deduplication via activeSubs (same as subscribe()) so repeated batch calls for the same symbol set reuse the existing subscription instead of creating a new one each time.
Found by automated unbounded operations audit
Location
sdks/typescript/pmxt/ws-client.ts:248-304(subscribeBatch) and lines 43-46 (the Maps)Code
Growth Pattern
Unlike
subscribe(), which deduplicates viaactiveSubsand reuses the samerequestIdfor the same symbol set,subscribeBatch()generates a fresh UUID on every call. This means:this.subscriptions[requestId]gets a new entry persubscribeBatch()call, never removed.this.dataStore["${requestId}:${symbol}"]gets a new entry per symbol per call, never deleted after being read.this.dataStore[requestId](the fallback at line 303) also persists.subscribeBatchis the backing transport forwatchOrderBooks()in the TypeScript SDK. In the CCXT Pro pattern, users callwatchOrderBooks()in a tightwhile (true)loop — one call per desired tick. Each iteration silently leaks entries.This is the exact TypeScript analog of Python issue #363.
OOM Estimate
subscribeBatch()call: 1WsSubscriptionobject (~200 bytes) + N dataStore entries (each an order book dict, ~500 bytes per symbol)this.subscriptionsandthis.dataStorehold live referencesSuggested Fix
After
subscribeBatch()collects its result, delete the consumed entries:Also add deduplication via
activeSubs(same assubscribe()) so repeated batch calls for the same symbol set reuse the existing subscription instead of creating a new one each time.Found by automated unbounded operations audit