feat(fastify-sse): add per-request SSE helpers

This commit is contained in:
saravanakumardb1 2026-03-29 12:38:57 -07:00
parent a8e0b7c3b0
commit c28dbc873e
4 changed files with 120 additions and 1 deletions

View File

@ -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": {

View File

@ -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';

View File

@ -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();
});
});
});

View File

@ -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();
}