Skip to content

Commit 8e761b3

Browse files
feat: support SSE retry field for reconnection timing (SEP-1699)
Add support for the SSE retry field to control client reconnection timing. When a server sends a retry field in an SSE event, the client now uses that value instead of exponential backoff for scheduling reconnections. Also adds support for reconnecting on graceful stream close, allowing servers to implement polling behavior by closing connections after sending event IDs. Changes: - Capture retry field via EventSourceParserStream onRetry callback - Use server-provided retry value in _getNextReconnectionDelay - Schedule reconnection on graceful stream end (not just errors)
1 parent 5bcf53f commit 8e761b3

File tree

1 file changed

+27
-2
lines changed

1 file changed

+27
-2
lines changed

src/client/streamableHttp.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export class StreamableHTTPClientTransport implements Transport {
133133
private _reconnectionOptions: StreamableHTTPReconnectionOptions;
134134
private _protocolVersion?: string;
135135
private _hasCompletedAuthFlow = false; // Circuit breaker: detect auth success followed by immediate 401
136+
private _serverRetryMs?: number; // Server-provided retry delay from SSE retry field (SEP-1699)
136137

137138
onclose?: () => void;
138139
onerror?: (error: Error) => void;
@@ -200,6 +201,7 @@ export class StreamableHTTPClientTransport implements Transport {
200201

201202
private async _startOrAuthSse(options: StartSSEOptions): Promise<void> {
202203
const { resumptionToken } = options;
204+
203205
try {
204206
// Try to open an initial SSE stream with GET to listen for server messages
205207
// This is optional according to the spec - server may not support it
@@ -246,7 +248,12 @@ export class StreamableHTTPClientTransport implements Transport {
246248
* @returns Time to wait in milliseconds before next reconnection attempt
247249
*/
248250
private _getNextReconnectionDelay(attempt: number): number {
249-
// Access default values directly, ensuring they're never undefined
251+
// SEP-1699: Use server-provided retry value if available
252+
if (this._serverRetryMs !== undefined) {
253+
return this._serverRetryMs;
254+
}
255+
256+
// Fall back to exponential backoff
250257
const initialDelay = this._reconnectionOptions.initialReconnectionDelay;
251258
const growFactor = this._reconnectionOptions.reconnectionDelayGrowFactor;
252259
const maxDelay = this._reconnectionOptions.maxReconnectionDelay;
@@ -313,7 +320,12 @@ export class StreamableHTTPClientTransport implements Transport {
313320
// Create a pipeline: binary stream -> text decoder -> SSE parser
314321
const reader = stream
315322
.pipeThrough(new TextDecoderStream() as ReadableWritablePair<string, Uint8Array>)
316-
.pipeThrough(new EventSourceParserStream())
323+
.pipeThrough(new EventSourceParserStream({
324+
onRetry: (retryMs: number) => {
325+
// SEP-1699: Capture server-provided retry value for reconnection timing
326+
this._serverRetryMs = retryMs;
327+
}
328+
}))
317329
.getReader();
318330

319331
while (true) {
@@ -340,6 +352,19 @@ export class StreamableHTTPClientTransport implements Transport {
340352
}
341353
}
342354
}
355+
356+
// SEP-1699: Handle graceful server-side disconnect
357+
// Server may close connection after sending event ID and retry field
358+
if (isReconnectable && this._abortController && !this._abortController.signal.aborted) {
359+
this._scheduleReconnection(
360+
{
361+
resumptionToken: lastEventId,
362+
onresumptiontoken,
363+
replayMessageId
364+
},
365+
0
366+
);
367+
}
343368
} catch (error) {
344369
// Handle stream errors - likely a network disconnect
345370
this.onerror?.(new Error(`SSE stream disconnected: ${error}`));

0 commit comments

Comments
 (0)