fix(FN-2013): flush buffered SSE events at stream end
- Remove the trailing-newline guard when flushing pending SSE buffer data in streamChatResponse - Re-enable the regression test covering final done events without a trailing blank line - Add parser coverage for chunk-split events, newline-terminated streams, and incomplete trailing fragments - Add a patch changeset for @gsxdsm/fusion documenting the SSE final-buffer flush fix
This commit is contained in:
@@ -3524,11 +3524,7 @@ describe("streamChatResponse", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// TODO(FN-1719): Skipped because this test exposes a pre-existing bug where the
|
||||
// implementation doesn't flush pending events when the stream ends without a
|
||||
// trailing newline. The condition `buffer.endsWith("\n")` should just be
|
||||
// `buffer.length > 0`. Also flaky in CI due to stream timing issues.
|
||||
it.skip("flushes a final complete event when the stream ends without a trailing blank line", async () => {
|
||||
it("flushes a final complete event when the stream ends without a trailing blank line", async () => {
|
||||
await withStreamResult(
|
||||
[
|
||||
"event: text\ndata: \"tail\"\n\nevent: done\ndata: {\"messageId\":\"msg-tail\"}",
|
||||
@@ -3540,6 +3536,64 @@ describe("streamChatResponse", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("flushes events built from partial chunks when stream ends without trailing newline", async () => {
|
||||
await withStreamResult(
|
||||
[
|
||||
"event: text\ndata: \"partial",
|
||||
" chunk\"\n\nevent: done\ndata: {\"messageId\":\"msg-x\"}",
|
||||
],
|
||||
(callbacks) => {
|
||||
expect(callbacks.text).toEqual(["partial chunk"]);
|
||||
expect(callbacks.done).toEqual([{ messageId: "msg-x" }]);
|
||||
expect(callbacks.error).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("still delivers events normally when stream ends with proper newlines", async () => {
|
||||
await withStreamResult(
|
||||
[
|
||||
"event: text\ndata: \"hello\"\n\nevent: done\ndata: {\"messageId\":\"msg-n\"}\n\n",
|
||||
],
|
||||
(callbacks) => {
|
||||
expect(callbacks.text).toEqual(["hello"]);
|
||||
expect(callbacks.done).toEqual([{ messageId: "msg-n" }]);
|
||||
expect(callbacks.error).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not dispatch when stream ends mid-event with incomplete data", async () => {
|
||||
const callbacks = {
|
||||
thinking: [] as string[],
|
||||
text: [] as string[],
|
||||
done: [] as Array<{ messageId: string }>,
|
||||
error: [] as string[],
|
||||
connectionStates: [] as string[],
|
||||
};
|
||||
|
||||
globalThis.fetch = vi.fn().mockImplementation(() =>
|
||||
createStreamResponse([
|
||||
'event: text\ndata: "complete"\n\ndata: "incomp',
|
||||
]),
|
||||
);
|
||||
|
||||
const stream = streamChatResponse("chat-1", "hello", {
|
||||
onThinking: (data) => callbacks.thinking.push(data),
|
||||
onText: (data) => callbacks.text.push(data),
|
||||
onDone: (data) => callbacks.done.push(data),
|
||||
onError: (data) => callbacks.error.push(data),
|
||||
onConnectionStateChange: (state) => callbacks.connectionStates.push(state),
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
stream.close();
|
||||
|
||||
expect(callbacks.text).toEqual(["complete"]);
|
||||
expect(callbacks.done).toEqual([]);
|
||||
expect(callbacks.error).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchMemoryBackendStatus", () => {
|
||||
|
||||
@@ -5679,10 +5679,9 @@ export function streamChatResponse(
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
// Only push complete SSE lines (ending with \n) to lines for processing.
|
||||
// Incomplete lines (no trailing \n) are intentionally left in the buffer
|
||||
// to be continued by the next chunk or properly handled at stream end.
|
||||
if (flushPendingEvent && buffer.length > 0 && buffer.endsWith("\n")) {
|
||||
// At stream end, flush any remaining buffered line so complete trailing
|
||||
// events are parsed even when the payload has no final newline.
|
||||
if (flushPendingEvent && buffer.length > 0) {
|
||||
lines.push(buffer);
|
||||
buffer = "";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user