feat(fastify-sse): add per-request SSE helpers
This commit is contained in:
parent
a8e0b7c3b0
commit
c28dbc873e
@ -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": {
|
||||
|
||||
@ -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';
|
||||
|
||||
87
packages/fastify-sse/src/per-request.test.ts
Normal file
87
packages/fastify-sse/src/per-request.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
31
packages/fastify-sse/src/per-request.ts
Normal file
31
packages/fastify-sse/src/per-request.ts
Normal 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();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user