From f0230d4ba20f7fedfde2a2f207bbed0488c7adcc Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:24:34 -0700 Subject: [PATCH] fix: Honor urlBuilder on React Native EventSource reconnect The shared FDv2 streaming source passes a urlBuilder to createEventSource so each (re)connection replays the current basis selector. The browser SDK honors it, but the React Native EventSource captured the URL once and reused it on reconnect, so FDv2 stream reconnects sent an empty basis. Thread urlBuilder through EventSourceOptions, PlatformRequests, and the EventSource so the URL is recomputed on each connection, matching the browser SDK. --- .../react-native-sse/EventSource.test.ts | 21 +++++++++++++++++++ .../react-native-sse/EventSource.ts | 8 +++++++ .../fromExternal/react-native-sse/types.ts | 7 +++++++ .../src/platform/PlatformRequests.ts | 1 + 4 files changed, 37 insertions(+) diff --git a/packages/sdk/react-native/__tests__/fromExternal/react-native-sse/EventSource.test.ts b/packages/sdk/react-native/__tests__/fromExternal/react-native-sse/EventSource.test.ts index 2073d1a0fc..84fa7788d6 100644 --- a/packages/sdk/react-native/__tests__/fromExternal/react-native-sse/EventSource.test.ts +++ b/packages/sdk/react-native/__tests__/fromExternal/react-native-sse/EventSource.test.ts @@ -114,4 +114,25 @@ describe('EventSource', () => { expect(mockXhr.open).toHaveBeenCalledTimes(2); expect(eventSource.onclose).toHaveBeenCalledTimes(1); }); + + test('recomputes the connection URL via urlBuilder on each connection', () => { + let basis: string | undefined; + const urlBuilder = jest.fn(() => (basis ? `${uri}?basis=${basis}` : uri)); + const es = new EventSource(uri, { logger, urlBuilder }); + es.onretrying = jest.fn(); + + // Initial connection asks the builder for the URL (no selector known yet). + jest.runAllTimers(); + expect(urlBuilder).toHaveBeenCalled(); + expect(mockXhr.open).toHaveBeenLastCalledWith('GET', uri, true); + + // Once a selector is known, a reconnect must replay it (e.g. FDv2 basis) + // rather than reuse the original URL. + basis = 'initial'; + // @ts-ignore - force a reconnect + es._tryConnect(); + jest.runAllTimers(); + + expect(mockXhr.open).toHaveBeenLastCalledWith('GET', `${uri}?basis=initial`, true); + }); }); diff --git a/packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.ts b/packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.ts index 9ba565582a..1d04733303 100644 --- a/packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.ts +++ b/packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.ts @@ -68,6 +68,7 @@ export default class EventSource { private _initialRetryDelayMillis: number = 1000; private _retryCount: number = 0; private _logger?: any; + private _urlBuilder?: () => string; constructor(url: string, options?: EventSourceOptions) { const opts = { @@ -84,6 +85,7 @@ export default class EventSource { this._retryAndHandleError = opts.retryAndHandleError; this._initialRetryDelayMillis = opts.initialRetryDelayMillis!; this._logger = opts.logger; + this._urlBuilder = opts.urlBuilder; this._tryConnect(true); } @@ -116,6 +118,12 @@ export default class EventSource { try { this._lastIndexProcessed = 0; this._status = this.CONNECTING; + // Recompute the URL on each (re)connection so that reconnects pick up + // state that changes over the connection's lifetime (for example the + // FDv2 `basis` selector, which must be replayed on reconnect). + if (this._urlBuilder) { + this._url = this._urlBuilder(); + } this._xhr.open(this._method, this._url, true); if (this._withCredentials) { diff --git a/packages/sdk/react-native/src/fromExternal/react-native-sse/types.ts b/packages/sdk/react-native/src/fromExternal/react-native-sse/types.ts index 3c828189b8..465b150fdf 100644 --- a/packages/sdk/react-native/src/fromExternal/react-native-sse/types.ts +++ b/packages/sdk/react-native/src/fromExternal/react-native-sse/types.ts @@ -54,6 +54,13 @@ export interface EventSourceOptions { retryAndHandleError?: (err: any) => boolean; initialRetryDelayMillis?: number; logger?: any; + /** + * Called before each (re)connection to compute the URL to connect to. When + * provided, this takes precedence over the static URL so that reconnections + * pick up state that changes over the connection's lifetime (for example the + * FDv2 `basis` selector, which must be replayed on reconnect). + */ + urlBuilder?: () => string; } type BuiltInEventMap = { diff --git a/packages/sdk/react-native/src/platform/PlatformRequests.ts b/packages/sdk/react-native/src/platform/PlatformRequests.ts index 4cbe4f6da9..c84fee726f 100644 --- a/packages/sdk/react-native/src/platform/PlatformRequests.ts +++ b/packages/sdk/react-native/src/platform/PlatformRequests.ts @@ -21,6 +21,7 @@ export default class PlatformRequests implements Requests { body: eventSourceInitDict.body, retryAndHandleError: eventSourceInitDict.errorFilter, logger: this._logger, + urlBuilder: eventSourceInitDict.urlBuilder, }); }