Skip to content

Commit 8725815

Browse files
committed
fix(core): end caught-up tracking on all terminal stream paths
caughtUp() is documented to reject when the stream ends before reaching the tail, but cancel(), non-retryable HTTP responses, and the user-abort branches closed or errored the stream without ending the tracker, so a caller awaiting caughtUp() could stay pending forever. End the tracker on every terminal path. Also removes a stray closing tag from the changeset and applies oxfmt formatting.
1 parent 940d60f commit 8725815

4 files changed

Lines changed: 28 additions & 6 deletions

File tree

.changeset/chat-session-caught-up-resume.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,3 @@
44
---
55

66
Chat sessions now close a resumed stream as soon as it has caught up to the latest output, instead of holding the connection open for the full long-poll window. Reloading or reconnecting to an idle chat settles faster.
7-
</content>

packages/core/src/v3/apiClient/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1432,7 +1432,10 @@ export class ApiClient {
14321432

14331433
const stream = await subscription.subscribe();
14341434
if (options?.onCaughtUp) {
1435-
subscription.caughtUp().then(options.onCaughtUp).catch(() => {});
1435+
subscription
1436+
.caughtUp()
1437+
.then(options.onCaughtUp)
1438+
.catch(() => {});
14361439
}
14371440
const onPart = options?.onPart;
14381441
const onControl = options?.onControl;

packages/core/src/v3/apiClient/runStream.test.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,12 @@ describe("SSEStreamSubscription caught-up tracking", () => {
609609
vi.restoreAllMocks();
610610
});
611611

612-
type Rec = { body: string; seq_num: number; timestamp: number; headers?: Array<[string, string]> };
612+
type Rec = {
613+
body: string;
614+
seq_num: number;
615+
timestamp: number;
616+
headers?: Array<[string, string]>;
617+
};
613618
type Tail = { seq_num: number; timestamp: number };
614619

615620
function dataRec(seq: number): Rec {
@@ -677,7 +682,9 @@ describe("SSEStreamSubscription caught-up tracking", () => {
677682
it("stays behind when the batch does not reach the tail", async () => {
678683
globalThis.fetch = vi
679684
.fn()
680-
.mockResolvedValue(makeEventsResponse([batchEvent([dataRec(0)], { seq_num: 3, timestamp: 1 })]));
685+
.mockResolvedValue(
686+
makeEventsResponse([batchEvent([dataRec(0)], { seq_num: 3, timestamp: 1 })])
687+
);
681688
const sub = new SSEStreamSubscription("http://x", { maxRetries: 0 });
682689
const stream = await sub.subscribe();
683690
await drain(stream);
@@ -703,7 +710,12 @@ describe("SSEStreamSubscription caught-up tracking", () => {
703710
batchEvent(
704711
[
705712
dataRec(0),
706-
{ body: "", seq_num: 1, timestamp: 1, headers: [["trigger-control", "turn-complete"]] },
713+
{
714+
body: "",
715+
seq_num: 1,
716+
timestamp: 1,
717+
headers: [["trigger-control", "turn-complete"]],
718+
},
707719
{ body: "AAAAAAAAAAQ=", seq_num: 2, timestamp: 1, headers: [["", "trim"]] },
708720
],
709721
{ seq_num: 3, timestamp: 1 }
@@ -723,7 +735,9 @@ describe("SSEStreamSubscription caught-up tracking", () => {
723735
it("never reaches caught-up when the wire carries no tail (feature-detect fallback)", async () => {
724736
globalThis.fetch = vi
725737
.fn()
726-
.mockResolvedValue(makeEventsResponse([batchEvent([dataRec(0), dataRec(1), dataRec(2)]), pingEvent()]));
738+
.mockResolvedValue(
739+
makeEventsResponse([batchEvent([dataRec(0), dataRec(1), dataRec(2)]), pingEvent()])
740+
);
727741
const sub = new SSEStreamSubscription("http://x", { maxRetries: 0 });
728742
const stream = await sub.subscribe();
729743
await drain(stream);

packages/core/src/v3/apiClient/runStream.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ export class SSEStreamSubscription implements StreamSubscription {
309309
await self.connectStream(controller);
310310
},
311311
cancel() {
312+
self.caughtUpTracker.end();
312313
self.options.onComplete?.();
313314
},
314315
});
@@ -373,6 +374,7 @@ export class SSEStreamSubscription implements StreamSubscription {
373374
);
374375
this.options.onError?.(error);
375376
if (this.nonRetryableStatuses.has(response.status)) {
377+
this.caughtUpTracker.end();
376378
controller.error(error);
377379
return;
378380
}
@@ -514,6 +516,7 @@ export class SSEStreamSubscription implements StreamSubscription {
514516
if (this.options.signal?.aborted) {
515517
reader.cancel();
516518
reader.releaseLock();
519+
this.caughtUpTracker.end();
517520
controller.close();
518521
this.options.onComplete?.();
519522
return;
@@ -529,6 +532,7 @@ export class SSEStreamSubscription implements StreamSubscription {
529532
} catch (error) {
530533
if (this.options.signal?.aborted) {
531534
// User cancel — exit cleanly, don't retry.
535+
this.caughtUpTracker.end();
532536
controller.close();
533537
this.options.onComplete?.();
534538
return;
@@ -555,6 +559,7 @@ export class SSEStreamSubscription implements StreamSubscription {
555559
error?: Error
556560
): Promise<void> {
557561
if (this.options.signal?.aborted) {
562+
this.caughtUpTracker.end();
558563
controller.close();
559564
this.options.onComplete?.();
560565
return;
@@ -598,6 +603,7 @@ export class SSEStreamSubscription implements StreamSubscription {
598603
this.retryNowController = null;
599604

600605
if (this.options.signal?.aborted) {
606+
this.caughtUpTracker.end();
601607
controller.close();
602608
this.options.onComplete?.();
603609
return;

0 commit comments

Comments
 (0)