From 4edb771701ae8a08b0fad0d64f7becac22d895e7 Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sun, 13 Sep 2026 13:28:09 -0500 Subject: [PATCH] Fix TopstepX login loop on bundled builds: signalr's dynamic require MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @microsoft/signalr does an unconditional require('ws')/require('eventsource') when constructing a connection; bundled (turbopack) builds throw 'dynamic usage of require is not supported', which the login chain mislabelled as a login failure and retried every 60s. Externalize the package and never let hub setup fail the login — REST polling covers everything the hub does. Co-Authored-By: Claude Fable 5 --- lib/topstepx-class.ts | 15 ++++++++++++++- next.config.ts | 5 ++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/topstepx-class.ts b/lib/topstepx-class.ts index 0102e82..c3a964a 100644 --- a/lib/topstepx-class.ts +++ b/lib/topstepx-class.ts @@ -172,7 +172,14 @@ export class TopstepXClient implements PlatformClient { if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = null; } this.syncComplete = false; - await this.startHub(); + // Hub failure must never read as a login failure: REST polling + // carries every feature (slower), and subscribeAll retries the + // hub on each sync tick. + try { + await this.startHub(); + } catch (err) { + console.error('[topstepx] hub setup failed — running on REST polling only', err instanceof Error ? err.message : err); + } await this.requestSync(); this.syncInterval = setInterval(() => void this.requestSync(), 60_000); @@ -240,6 +247,12 @@ export class TopstepXClient implements PlatformClient { } this.subscribedAccountIds.clear(); + // signalr's Node path does an unconditional dynamic require('ws') / + // require('eventsource') when constructing the connection — bundled + // builds throw "dynamic usage of require is not supported" on it, so + // the package is listed in next.config serverExternalPackages and any + // residual failure here is caught by the caller (REST polling covers + // everything the hub does, just slower). const hub = new signalR.HubConnectionBuilder() .withUrl(USER_HUB, { skipNegotiation: true, diff --git a/next.config.ts b/next.config.ts index 40988d5..baf59c1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,10 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - serverExternalPackages: ['better-sqlite3'], + // @microsoft/signalr does dynamic require('ws')/require('eventsource') for + // its Node transports — bundling it (turbopack production builds) throws + // "dynamic usage of require is not supported" the moment a hub is built. + serverExternalPackages: ['better-sqlite3', '@microsoft/signalr'], }; export default nextConfig;