Skip to content

fix(@typedtrader/exchange): reconnect Alpaca WebSocket in-process instead of exiting#1176

Open
bennycode wants to merge 2 commits into
mainfrom
fix/alpaca-websocket-reconnect
Open

fix(@typedtrader/exchange): reconnect Alpaca WebSocket in-process instead of exiting#1176
bennycode wants to merge 2 commits into
mainfrom
fix/alpaca-websocket-reconnect

Conversation

@bennycode

Copy link
Copy Markdown
Owner

Summary

  • The Alpaca market-data WebSocket close handler called process.exit(1) so an external orchestrator could restart the stream. Library code must not kill the process — it takes down every other trading session, price watch, and the Telegram bot sharing the same process.
  • On an unexpected close, AlpacaWebSocket now reconnects in-process using the existing ts-retry-promise config (30s delay, infinite retries, non-retryable close codes 402/404/405/409), re-authenticates, and re-subscribes all previously subscribed bar topics under the same connectionId — consumers holding an AlpacaConnection keep working transparently.
  • The manager now extends EventEmitter and emits reconnecting, resubscribed, and reconnect_failed (each with the connectionId) so hosts can log/alert on stream drops.
  • New disconnect(connectionId) closes a connection intentionally; a guard flag ensures an intentional close never triggers reconnection (mirrors the AlpacaTradingWebSocket sibling API).
  • Fixes a latent listener leak: subscribeToBars attached a new stream.on('message') handler on every call and unsubscribeFromBars never removed it, so repeated subscribe cycles emitted duplicate candles. Each connection now has exactly one message handler that dispatches to tracked per-symbol callbacks — which is also what makes resubscription after a reconnect possible.
  • Checked the sibling AlpacaTradingWebSocket: it has no exit-on-close pattern, so no change needed there.

Test plan

  • New AlpacaWebSocket.test.ts with a stubbed AlpacaStream (vi.mock) covering:
    • unexpected close → reconnect + resubscribe of prior bar topics (and bars keep flowing afterwards)
    • intentional disconnect() → no reconnect
    • subscribe/unsubscribe/subscribe cycle → exactly one candle emission per message
    • process.exit is never called (spy)
    • singleton connection reuse per apiKey:source and symbol-scoped dispatch
  • npm test in packages/exchange: 9 files, 92 tests passed
  • npm run lint in packages/exchange: 0 errors

…tead of exiting

The market-data WebSocket close handler called process.exit(1) so an
external orchestrator could restart the stream. That kills every other
trading session, price watch, and bot sharing the process. The socket
now reconnects in-process using the existing retry config (30s delay,
infinite retries, non-retryable close codes 402/404/405/409),
re-authenticates, and re-subscribes all previously subscribed bar
topics under the same connection ID. An intentional disconnect() is
guarded by a flag and never triggers reconnection. The manager now
extends EventEmitter and emits 'reconnecting', 'resubscribed', and
'reconnect_failed' so hosts can log/alert.

Also fixes a listener leak: subscribeToBars added a new stream message
handler on every call and unsubscribeFromBars never removed it, so
repeated subscribe cycles emitted duplicate candles. Each connection
now has exactly one message handler that dispatches to tracked
per-symbol callbacks, which also enables resubscription after a
reconnect.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Alpaca market-data WebSocket manager to avoid terminating the entire Node.js process on unexpected socket closes, and instead reconnects in-process while preserving the same connectionId and bar subscriptions for existing consumers.

Changes:

  • Replace process.exit(1) on unexpected close with in-process reconnect + re-auth + resubscribe using existing ts-retry-promise policy.
  • Rework bar subscription handling to avoid message-listener leaks by dispatching through a single per-connection message handler and tracked per-symbol callbacks.
  • Add lifecycle events (reconnecting, resubscribed, reconnect_failed) and an explicit disconnect(connectionId) API, plus a new Vitest suite covering reconnect and subscription behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/exchange/src/broker/alpaca/AlpacaWebSocket.ts Implements reconnect-in-process, event emission, intentional disconnect guard, and subscription dispatch refactor.
packages/exchange/src/broker/alpaca/AlpacaWebSocket.test.ts Adds tests for reconnect/resubscribe behavior, intentional disconnect, listener leak regression, and “no process.exit” guarantee.
Comments suppressed due to low confidence (1)

packages/exchange/src/broker/alpaca/AlpacaWebSocket.ts:74

  • If the stream emits an error during connection establishment, the promise rejects but the underlying WebSocket is left open. With retry() this can leave orphaned sockets between attempts and potentially contribute to connection-limit issues. Close the stream before rejecting so failed attempts are cleaned up.
      stream.on('error', (error: unknown) => {
        console.error(`WebSocket streaming failed for ID "${connectionId}".`, error);
        reject(error);
      });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 199 to 201
const allSymbols = Array.from(subscriptions.keys());
connection.stream.unsubscribe('bars', allSymbols);
connection.stream.subscribe('bars', allSymbols);
Comment on lines 210 to 212
this.#subscriptions.get(connectionId)?.delete(symbol);

connection.stream.unsubscribe('bars', [symbol]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants