From c28dbc873ea15d762aba064624a8d55a09543c28 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 29 Mar 2026 12:38:57 -0700 Subject: [PATCH] feat(fastify-sse): add per-request SSE helpers --- packages/fastify-sse/package.json | 2 +- packages/fastify-sse/src/index.ts | 1 + packages/fastify-sse/src/per-request.test.ts | 87 ++++++++++++++++++++ packages/fastify-sse/src/per-request.ts | 31 +++++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 packages/fastify-sse/src/per-request.test.ts create mode 100644 packages/fastify-sse/src/per-request.ts diff --git a/packages/fastify-sse/package.json b/packages/fastify-sse/package.json index 84ece4fc..29fadf65 100644 --- a/packages/fastify-sse/package.json +++ b/packages/fastify-sse/package.json @@ -1,6 +1,6 @@ { "name": "@bytelyst/fastify-sse", - "version": "0.1.0", + "version": "0.2.0", "description": "Fastify plugin for Server-Sent Events (SSE) — real-time push for ByteLyst product backends", "type": "module", "exports": { diff --git a/packages/fastify-sse/src/index.ts b/packages/fastify-sse/src/index.ts index bb76a9c2..a5f37283 100644 --- a/packages/fastify-sse/src/index.ts +++ b/packages/fastify-sse/src/index.ts @@ -1,3 +1,4 @@ export { SSEHub } from './hub.js'; export { ssePlugin } from './plugin.js'; export type { SSEPluginOptions, SSEClient } from './plugin.js'; +export { startSSE, sendSSEData, sendSSEEvent, endSSE } from './per-request.js'; diff --git a/packages/fastify-sse/src/per-request.test.ts b/packages/fastify-sse/src/per-request.test.ts new file mode 100644 index 00000000..de5cb632 --- /dev/null +++ b/packages/fastify-sse/src/per-request.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, vi } from 'vitest'; +import { startSSE, sendSSEData, sendSSEEvent, endSSE } from './per-request.js'; +import type { FastifyReply } from 'fastify'; + +function mockReply(): FastifyReply { + const chunks: string[] = []; + return { + raw: { + writeHead: vi.fn(), + write: vi.fn((chunk: string) => { + chunks.push(chunk); + return true; + }), + end: vi.fn(), + _chunks: chunks, + }, + hijack: vi.fn(), + } as unknown as FastifyReply; +} + +describe('per-request SSE helpers', () => { + describe('startSSE', () => { + it('sets correct SSE headers', () => { + const reply = mockReply(); + startSSE(reply); + expect(reply.raw.writeHead).toHaveBeenCalledWith(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }); + }); + + it('hijacks the reply', () => { + const reply = mockReply(); + startSSE(reply); + expect(reply.hijack).toHaveBeenCalled(); + }); + }); + + describe('sendSSEData', () => { + it('formats object data as JSON', () => { + const reply = mockReply(); + sendSSEData(reply, { hello: true }); + expect(reply.raw.write).toHaveBeenCalledWith('data: {"hello":true}\n\n'); + }); + + it('formats string data without double-encoding', () => { + const reply = mockReply(); + sendSSEData(reply, 'plain text'); + expect(reply.raw.write).toHaveBeenCalledWith('data: plain text\n\n'); + }); + + it('formats number data as JSON', () => { + const reply = mockReply(); + sendSSEData(reply, 42); + expect(reply.raw.write).toHaveBeenCalledWith('data: 42\n\n'); + }); + }); + + describe('sendSSEEvent', () => { + it('formats named event with object data', () => { + const reply = mockReply(); + sendSSEEvent(reply, 'token', { text: 'hi' }); + expect(reply.raw.write).toHaveBeenCalledWith('event: token\ndata: {"text":"hi"}\n\n'); + }); + + it('formats named event with string data', () => { + const reply = mockReply(); + sendSSEEvent(reply, 'status', 'done'); + expect(reply.raw.write).toHaveBeenCalledWith('event: status\ndata: done\n\n'); + }); + }); + + describe('endSSE', () => { + it('sends the [DONE] sentinel', () => { + const reply = mockReply(); + endSSE(reply); + expect(reply.raw.write).toHaveBeenCalledWith('data: [DONE]\n\n'); + }); + + it('ends the stream', () => { + const reply = mockReply(); + endSSE(reply); + expect(reply.raw.end).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/fastify-sse/src/per-request.ts b/packages/fastify-sse/src/per-request.ts new file mode 100644 index 00000000..216bd10d --- /dev/null +++ b/packages/fastify-sse/src/per-request.ts @@ -0,0 +1,31 @@ +/** + * Per-request SSE helpers — single-request streaming pattern. + * Used by chat streaming and model comparison endpoints where + * one route handler streams SSE to one client. + */ + +import type { FastifyReply } from 'fastify'; + +export function startSSE(reply: FastifyReply): void { + reply.raw.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }); + reply.hijack(); +} + +export function sendSSEEvent(reply: FastifyReply, event: string, data: unknown): void { + const payload = typeof data === 'string' ? data : JSON.stringify(data); + reply.raw.write(`event: ${event}\ndata: ${payload}\n\n`); +} + +export function sendSSEData(reply: FastifyReply, data: unknown): void { + const payload = typeof data === 'string' ? data : JSON.stringify(data); + reply.raw.write(`data: ${payload}\n\n`); +} + +export function endSSE(reply: FastifyReply): void { + reply.raw.write('data: [DONE]\n\n'); + reply.raw.end(); +}