fix(@typedtrader/exchange): reconnect Alpaca WebSocket in-process instead of exiting#1176
Open
bennycode wants to merge 2 commits into
Open
fix(@typedtrader/exchange): reconnect Alpaca WebSocket in-process instead of exiting#1176bennycode wants to merge 2 commits into
bennycode wants to merge 2 commits into
Conversation
…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.
Contributor
There was a problem hiding this comment.
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 existingts-retry-promisepolicy. - 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 explicitdisconnect(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]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
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.AlpacaWebSocketnow reconnects in-process using the existingts-retry-promiseconfig (30s delay, infinite retries, non-retryable close codes 402/404/405/409), re-authenticates, and re-subscribes all previously subscribed bar topics under the sameconnectionId— consumers holding anAlpacaConnectionkeep working transparently.EventEmitterand emitsreconnecting,resubscribed, andreconnect_failed(each with theconnectionId) so hosts can log/alert on stream drops.disconnect(connectionId)closes a connection intentionally; a guard flag ensures an intentional close never triggers reconnection (mirrors theAlpacaTradingWebSocketsibling API).subscribeToBarsattached a newstream.on('message')handler on every call andunsubscribeFromBarsnever 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.AlpacaTradingWebSocket: it has no exit-on-close pattern, so no change needed there.Test plan
AlpacaWebSocket.test.tswith a stubbedAlpacaStream(vi.mock) covering:disconnect()→ no reconnectprocess.exitis never called (spy)apiKey:sourceand symbol-scoped dispatchnpm testinpackages/exchange: 9 files, 92 tests passednpm run lintinpackages/exchange: 0 errors