From c3edb40257ccb775967b4aaddd49ab6056be036f Mon Sep 17 00:00:00 2001 From: ZackFan Date: Tue, 4 Aug 2026 17:11:37 +0800 Subject: [PATCH] feat(client): expose the resolved endpoint on rest and websocket clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since baseUrl became host-and-prefix only, the SDK — not the caller — decides the version segment. That left no way to answer "which version am I actually on": options is protected, so even our own tests reached for it through @ts-ignore. WebSocketClient gains `url` (the full endpoint, version segment included) and RestClient gains `baseUrl` (the prefix requests are built on). Both are read-only, available before connecting, and inherited by the stock and futopt subclasses. The existing URL assertions now read through these accessors instead of @ts-ignore, which drops that escape hatch from ~20 places to the two that genuinely poke at private internals. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VGBMNGKfgHPyNGMkHMcaJH --- src/rest/client.ts | 11 +++++++++++ src/websocket/client.ts | 12 ++++++++++++ test/rest-client.spec.ts | 21 +++++++-------------- test/websocket-client.spec.ts | 24 ++++++++---------------- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/rest/client.ts b/src/rest/client.ts index 9d15796..421953e 100644 --- a/src/rest/client.ts +++ b/src/rest/client.ts @@ -13,6 +13,17 @@ export interface RestClientOptions { export abstract class RestClient { constructor(protected readonly options: RestClientOptions) {} + /** + * The prefix every request from this client is built on, fully resolved — + * host, version segment and product. Endpoints are appended to it. + * + * The version segment is chosen by the SDK rather than written by the + * caller, so this is the only way to see what a client actually resolved to. + */ + public get baseUrl(): string { + return this.options.baseUrl; + } + protected request = async (endpoint: string, params?: Record) => { const url = queryString.stringifyUrl({ url: `${this.options.baseUrl}/${endpoint}`, query: params }); diff --git a/src/websocket/client.ts b/src/websocket/client.ts index 36e714d..74b19e6 100644 --- a/src/websocket/client.ts +++ b/src/websocket/client.ts @@ -32,6 +32,18 @@ export class WebSocketClient extends events.EventEmitter { super(); } + /** + * The endpoint this client connects to, fully resolved — host, version + * segment, product and `/streaming`. + * + * The version segment is chosen by the SDK from the `version` option rather + * than written by the caller, so this is the only way to see which version a + * client actually ended up on. + */ + public get url(): string { + return this.options.url; + } + public connect() { this.socket = new WebSocket(this.options.url); this.socket.onopen = () => this.emit(CONNECT_EVENT); diff --git a/test/rest-client.spec.ts b/test/rest-client.spec.ts index 83fdc37..88206c6 100644 --- a/test/rest-client.spec.ts +++ b/test/rest-client.spec.ts @@ -66,16 +66,14 @@ describe('RestClient', () => { const client = new RestClient({ apiKey: 'api-key', baseUrl: 'https://custom-api.example.com' }); const stock = client.stock; expect(stock).toBeInstanceOf(RestStockClient); - // @ts-ignore - accessing private property for testing - expect(stock.options.baseUrl).toBe('https://custom-api.example.com/v1.0/stock'); + expect(stock.baseUrl).toBe('https://custom-api.example.com/v1.0/stock'); }); it('should use custom baseUrl for futopt client', () => { const client = new RestClient({ apiKey: 'api-key', baseUrl: 'https://custom-api.example.com' }); const futopt = client.futopt; expect(futopt).toBeInstanceOf(RestFutOptClient); - // @ts-ignore - accessing private property for testing - expect(futopt.options.baseUrl).toBe('https://custom-api.example.com/v1.0/futopt'); + expect(futopt.baseUrl).toBe('https://custom-api.example.com/v1.0/futopt'); }); describe('.intraday', () => { @@ -931,33 +929,28 @@ describe('RestClient', () => { it('should handle baseUrl without trailing slash', () => { const client = new RestClient({ apiKey: 'api-key', baseUrl: 'https://api.example.com/marketdata' }); const stock = client.stock; - // @ts-ignore - accessing private property for testing - expect(stock.options.baseUrl).toBe('https://api.example.com/marketdata/v1.0/stock'); + expect(stock.baseUrl).toBe('https://api.example.com/marketdata/v1.0/stock'); }); it('should handle baseUrl with single trailing slash', () => { const client = new RestClient({ apiKey: 'api-key', baseUrl: 'https://api.example.com/marketdata/' }); const stock = client.stock; - // @ts-ignore - accessing private property for testing - expect(stock.options.baseUrl).toBe('https://api.example.com/marketdata/v1.0/stock'); + expect(stock.baseUrl).toBe('https://api.example.com/marketdata/v1.0/stock'); }); it('should handle baseUrl with multiple trailing slashes', () => { const client = new RestClient({ apiKey: 'api-key', baseUrl: 'https://api.example.com/marketdata///' }); const stock = client.stock; - // @ts-ignore - accessing private property for testing - expect(stock.options.baseUrl).toBe('https://api.example.com/marketdata/v1.0/stock'); + expect(stock.baseUrl).toBe('https://api.example.com/marketdata/v1.0/stock'); }); it('should treat a path segment that is not a vX.Y version as part of the prefix', () => { const client = new RestClient({ apiKey: 'api-key', baseUrl: 'https://api.example.com/api/v2/' }); const stock = client.stock; - // @ts-ignore - accessing private property for testing - expect(stock.options.baseUrl).toBe('https://api.example.com/api/v2/v1.0/stock'); + expect(stock.baseUrl).toBe('https://api.example.com/api/v2/v1.0/stock'); const futopt = client.futopt; - // @ts-ignore - accessing private property for testing - expect(futopt.options.baseUrl).toBe('https://api.example.com/api/v2/v1.0/futopt'); + expect(futopt.baseUrl).toBe('https://api.example.com/api/v2/v1.0/futopt'); }); it('should reject a baseUrl carrying its own version segment', () => { diff --git a/test/websocket-client.spec.ts b/test/websocket-client.spec.ts index 5ba9e93..e89f68c 100644 --- a/test/websocket-client.spec.ts +++ b/test/websocket-client.spec.ts @@ -84,16 +84,14 @@ describe('WebSocketClient', () => { const client = new WebSocketClient({ apiKey: 'api-key', baseUrl: 'wss://custom-ws.example.com' }); const stock = client.stock; expect(stock).toBeInstanceOf(WebSocketStockClient); - // @ts-ignore - accessing private property for testing - expect(stock.options.url).toBe('wss://custom-ws.example.com/v1.0/stock/streaming'); + expect(stock.url).toBe('wss://custom-ws.example.com/v1.0/stock/streaming'); }); it('should use custom baseUrl for futopt client', () => { const client = new WebSocketClient({ apiKey: 'api-key', baseUrl: 'wss://custom-ws.example.com' }); const futopt = client.futopt; expect(futopt).toBeInstanceOf(WebSocketFutOptClient); - // @ts-ignore - accessing private property for testing - expect(futopt.options.url).toBe('wss://custom-ws.example.com/v1.1/futopt/streaming'); + expect(futopt.url).toBe('wss://custom-ws.example.com/v1.1/futopt/streaming'); }); describe('.connect()', () => { @@ -469,39 +467,33 @@ describe('WebSocketClient', () => { it('should handle baseUrl without trailing slash', () => { const client = new WebSocketClient({ apiKey: 'api-key', baseUrl: 'wss://ws.example.com/marketdata' }); const stock = client.stock; - // @ts-ignore - accessing private property for testing - expect(stock.options.url).toBe('wss://ws.example.com/marketdata/v1.0/stock/streaming'); + expect(stock.url).toBe('wss://ws.example.com/marketdata/v1.0/stock/streaming'); }); it('should handle baseUrl with single trailing slash', () => { const client = new WebSocketClient({ apiKey: 'api-key', baseUrl: 'wss://ws.example.com/marketdata/' }); const stock = client.stock; - // @ts-ignore - accessing private property for testing - expect(stock.options.url).toBe('wss://ws.example.com/marketdata/v1.0/stock/streaming'); + expect(stock.url).toBe('wss://ws.example.com/marketdata/v1.0/stock/streaming'); }); it('should handle baseUrl with multiple trailing slashes', () => { const client = new WebSocketClient({ apiKey: 'api-key', baseUrl: 'wss://ws.example.com/marketdata///' }); const stock = client.stock; - // @ts-ignore - accessing private property for testing - expect(stock.options.url).toBe('wss://ws.example.com/marketdata/v1.0/stock/streaming'); + expect(stock.url).toBe('wss://ws.example.com/marketdata/v1.0/stock/streaming'); }); it('should treat a path segment that is not a vX.Y version as part of the prefix', () => { const client = new WebSocketClient({ apiKey: 'api-key', baseUrl: 'wss://ws.example.com/api/v2/' }); const stock = client.stock; - // @ts-ignore - accessing private property for testing - expect(stock.options.url).toBe('wss://ws.example.com/api/v2/v1.0/stock/streaming'); + expect(stock.url).toBe('wss://ws.example.com/api/v2/v1.0/stock/streaming'); const futopt = client.futopt; - // @ts-ignore - accessing private property for testing - expect(futopt.options.url).toBe('wss://ws.example.com/api/v2/v1.1/futopt/streaming'); + expect(futopt.url).toBe('wss://ws.example.com/api/v2/v1.1/futopt/streaming'); }); }); describe('streaming version', () => { - // @ts-ignore - accessing private property for testing - const urlOf = (client: WebSocketClient, product: 'stock' | 'futopt') => client[product].options.url; + const urlOf = (client: WebSocketClient, product: 'stock' | 'futopt') => client[product].url; it('should default each product to its latest version', () => { const client = new WebSocketClient({ apiKey: 'api-key' });