Skip to content

Commit a60cc06

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 4debc74 commit a60cc06

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
@@ -134,6 +134,7 @@ export class StreamableHTTPClientTransport implements Transport {
134134
private _reconnectionOptions: StreamableHTTPReconnectionOptions;
135135
private _protocolVersion?: string;
136136
private _hasCompletedAuthFlow = false; // Circuit breaker: detect auth success followed by immediate 401
137+
private _serverRetryMs?: number; // Server-provided retry delay from SSE retry field (SEP-1699)
137138

138139
onclose?: () => void;
139140
onerror?: (error: Error) => void;
@@ -202,6 +203,7 @@ export class StreamableHTTPClientTransport implements Transport {
202203

203204
private async _startOrAuthSse(options: StartSSEOptions): Promise<void> {
204205
const { resumptionToken } = options;
206+
205207
try {
206208
// Try to open an initial SSE stream with GET to listen for server messages
207209
// This is optional according to the spec - server may not support it
@@ -248,7 +250,12 @@ export class StreamableHTTPClientTransport implements Transport {
248250
* @returns Time to wait in milliseconds before next reconnection attempt
249251
*/
250252
private _getNextReconnectionDelay(attempt: number): number {
251-
// Access default values directly, ensuring they're never undefined
253+
// SEP-1699: Use server-provided retry value if available
254+
if (this._serverRetryMs !== undefined) {
255+
return this._serverRetryMs;
256+
}
257+
258+
// Fall back to exponential backoff
252259
const initialDelay = this._reconnectionOptions.initialReconnectionDelay;
253260
const growFactor = this._reconnectionOptions.reconnectionDelayGrowFactor;
254261
const maxDelay = this._reconnectionOptions.maxReconnectionDelay;
@@ -301,7 +308,12 @@ export class StreamableHTTPClientTransport implements Transport {
301308
// Create a pipeline: binary stream -> text decoder -> SSE parser
302309
const reader = stream
303310
.pipeThrough(new TextDecoderStream() as ReadableWritablePair<string, Uint8Array>)
304-
.pipeThrough(new EventSourceParserStream())
311+
.pipeThrough(new EventSourceParserStream({
312+
onRetry: (retryMs: number) => {
313+
// SEP-1699: Capture server-provided retry value for reconnection timing
314+
this._serverRetryMs = retryMs;
315+
}
316+
}))
305317
.getReader();
306318

307319
while (true) {
@@ -328,6 +340,19 @@ export class StreamableHTTPClientTransport implements Transport {
328340
}
329341
}
330342
}
343+
344+
// SEP-1699: Handle graceful server-side disconnect
345+
// Server may close connection after sending event ID and retry field
346+
if (isReconnectable && this._abortController && !this._abortController.signal.aborted) {
347+
this._scheduleReconnection(
348+
{
349+
resumptionToken: lastEventId,
350+
onresumptiontoken,
351+
replayMessageId
352+
},
353+
0
354+
);
355+
}
331356
} catch (error) {
332357
// Handle stream errors - likely a network disconnect
333358
this.onerror?.(new Error(`SSE stream disconnected: ${error}`));

0 commit comments

Comments
 (0)