@ fastify/ sse (帮助)
Fastify 的服务器- Sent Evolution 插件 。 提供一等SSE支持,提供干净的API集成,会话管理,和流出能力.
特征
- 🚀 清洁 API: 路由级 SSE 支持
{ sse: true } - 📡 流线: 对 Node.js 流和同步执行器的原生支持
- 🔄 重新连接: 内置信件重放
Last-Event-ID - 💓 心跳动:可配置的保有机制
- 🎯 生命周期: 与快取钩和出错处理完全整合
- 📝 类型脚本: 包含完整类型定义
- ⚡ 业绩: 高效后压处理
安装
npm i @fastify/sse
快速启动
const fastify = require('fastify')({ logger: true })
// Register the plugin
await fastify.register(require('@fastify/sse'))
// Create an SSE endpoint
fastify.get('/events', { sse: true }, async (request, reply) => {
// Send a message
await reply.sse.send({ data: 'Hello SSE!' })
// Send with full options
await reply.sse.send({
id: '123',
event: 'update',
data: { message: 'Hello World' },
retry: 1000
})
})
await fastify.listen({ port: 3000 })
API 密码
插件注册
await fastify.register(require('@fastify/sse'), {
// Optional: heartbeat interval in milliseconds (default: 30000)
heartbeatInterval: 30000,
// Optional: default serializer (default: JSON.stringify)
serializer: (data) => JSON.stringify(data)
})
路由配置
路由选择通过设置 SSE 处理 sse 字段。 有三个
表格,每个表格描述路径服务内容:
// SSE-only route: handler always streams. Clients that explicitly refuse
// SSE (e.g. `Accept: application/json`) receive 406 Not Acceptable.
// Ambiguous Accept headers (*/*, text/*, missing) admit SSE per RFC 9110.
fastify.get('/events', { sse: 'only' }, handler)
// Dual-mode route: the same handler serves both SSE and non-SSE clients.
// Only an explicit `text/event-stream` token routes to SSE; everything
// else falls through to the handler with `reply.sse` undefined, so the
// handler is expected to produce a non-SSE response in that branch.
fastify.get('/data', { sse: 'dual' }, handler)
// Manual route: no Accept negotiation. `reply.sse` is always attached and
// the handler decides at runtime whether to stream (by calling reply.sse.*)
// or to return a normal response. Useful for streaming APIs that signal
// streaming via the request body (e.g. OpenAI-style `{ stream: true }`)
// rather than the Accept header.
fastify.post('/v1/chat', { sse: 'manual' }, handler)
// Back-compat: `sse: true` behaves like `'dual'` for routing. If the
// handler is actually SSE-only and trips a TypeError on `reply.sse`
// because of a wildcard Accept, the plugin rethrows with a message
// that names `sse: 'only'` as the fix.
fastify.get('/legacy', { sse: true }, handler)
// Object form (supports per-route options):
fastify.get('/events', {
sse: {
kind: 'only', // 'only' | 'dual' | 'manual' — omit for back-compat
heartbeat: false, // Disable heartbeat for this route
serializer: customSerializer // Custom serializer for this route
}
}, handler)
回复( sse. send) (来源)
发送SSE消息. 接受各种源类型 :
单个信件
// Simple data
await reply.sse.send({ data: 'hello' })
// Full SSE message
await reply.sse.send({
id: '123',
event: 'update',
data: { message: 'Hello' },
retry: 1000
})
// Plain string
await reply.sse.send('plain text message')
流源
// Async generator
async function* generateEvents() {
for (let i = 0; i < 10; i++) {
yield { id: i, data: `Event ${i}` }
await sleep(1000)
}
}
await reply.sse.send(generateEvents())
// Node.js Readable stream
const stream = fs.createReadStream('data.jsonl')
await reply.sse.send(stream)
// Transform existing stream
const transformStream = new Transform({
transform(chunk, encoding, callback) {
callback(null, { data: chunk.toString() })
}
})
someSource.pipe(transformStream)
await reply.sse.send(transformStream)
回复.se.stream ()
创建用于管道运营的变换流 :
// Use with pipeline for efficient streaming
const { pipeline } = require('stream/promises')
const fs = require('fs')
fastify.get('/file-stream', { sse: true }, async (request, reply) => {
const fileStream = fs.createReadStream('data.jsonl')
// Parse each line as JSON and convert to SSE format
const parseTransform = new Transform({
transform(chunk, encoding, callback) {
const lines = chunk.toString().split('\n').filter(Boolean)
for (const line of lines) {
try {
const data = JSON.parse(line)
this.push({ id: data.id, data })
} catch (err) {
// Skip invalid JSON lines
}
}
callback()
}
})
// Stream file data through SSE
await pipeline(
fileStream,
parseTransform,
reply.sse.stream(),
reply.raw,
{ end: false }
)
})
连接管理
fastify.get('/live', { sse: true }, async (request, reply) => {
// Keep connection alive (prevents automatic close)
reply.sse.keepAlive()
// Send initial message
await reply.sse.send({ data: 'Connected' })
// Check if keepAlive was called
console.log('Keep alive status:', reply.sse.shouldKeepAlive) // true
// Set up periodic updates
const interval = setInterval(async () => {
if (reply.sse.isConnected) {
await reply.sse.send({ data: 'ping' })
} else {
clearInterval(interval)
}
}, 1000)
// Clean up when connection closes
reply.sse.onClose(() => {
clearInterval(interval)
console.log('Connection closed')
})
})
信件重放
处理客户端重新连接 Last-Event-ID数字 :
const messageHistory = []
fastify.get('/events', { sse: true }, async (request, reply) => {
// Handle replay on reconnection
await reply.sse.replay(async (lastEventId) => {
// Find messages after the last received ID
const startIndex = messageHistory.findIndex(msg => msg.id === lastEventId)
const messagesToReplay = startIndex !== -1
? messageHistory.slice(startIndex + 1)
: messageHistory
// Send missed messages
for (const message of messagesToReplay) {
await reply.sse.send(message)
}
})
// Send new message
const newMessage = { id: Date.now(), data: 'New event' }
messageHistory.push(newMessage)
await reply.sse.send(newMessage)
})
属性和方法
属性
reply.sse.lastEventId: 客户端最后收到的事件ID(字符串 QQ 无效)reply.sse.isConnected连接状态 (布尔)reply.sse.shouldKeepAlive: 处理器完成后, 连接是否应该保持活性( boolean)
方法
reply.sse.send(source): 从各种来源类型发送 SSE 信件reply.sse.stream(): 创建输油管业务转换流reply.sse.keepAlive(): 处理器返回后防止自动关闭连接reply.sse.close(): 手动关闭连接reply.sse.replay(callback): 使用 Last-Event ID 处理消息重放reply.sse.onClose(callback): 连接关闭时的注册回调reply.sse.sendHeaders(statusCode?): 手动发送信头(称为在首字母上自动发送)
自定义响应状态代码
SSE 响应默认为 200 OK。要使用不同的状态,请
reply.code() 在第一个 SSE 写入之前,或将状态直接传递给一个
手动 reply.sse.sendHeaders() 电话 :
fastify.get('/events', { sse: 'only' }, async (request, reply) => {
reply.code(201)
await reply.sse.send({ data: 'created' })
})
fastify.get('/events-explicit', { sse: 'only' }, async (request, reply) => {
reply.sse.sendHeaders(201)
await reply.sse.send({ data: 'created' })
})
明确 sendHeaders() 参数优先于 reply.code()。 。 。 .
一旦第一个 SSE 写入 响应头, 稍后 reply.code()
或者说 sendHeaders() 电话没有效果.
高级使用
接受信头谈判
插件解析 Accept 每页眉 RFC 9110 第12.5.1节
决定是否将请求作为SSE。 决定取决于
路线被宣布类型 :
| 路线类型 | 接受信头 | 行为 |
|---|---|---|
sse: 'only' | text/event-stream | 证监会 |
sse: 'only' | */*, (中文(简体) ), text/*缺少 | 证监会 |
sse: 'only' | application/json 包括:. | 406 无法接受 |
sse: 'only' | …, text/event-stream;q=0 | 406 无法接受 |
sse: 'dual' | text/event-stream | SSE 分支 |
sse: 'dual' | */*, (中文(简体) ), text/*缺少 | 非SSE分支 |
sse: 'dual' | application/json | 非SSE分支 |
sse: 'manual' | (任何/缺失) | 运行时处理器决定 |
sse: true | (路线与 'dual'(中文(简体) ) | + 关于滥用的解释性错误 |
简而言之:明确 text/event-stream 总是通往SSE; 模糊
通配符到SSE的路由只有在您已经声明了路由 SSE 时才会使用.
一直这样 curl, (中文(简体) ), fetch(),邮递员默认只与 SSE 合作
结束点,同时保留双模式的预期,即
通配符更偏好更丰富的非SSE代表.
回落到定期答复(sse: 'dual'(中文(简体) )
双模式路线从一个单一的SSE和非SSE客户端服务
处理者。 插件附加 reply.sse 只有客户明确
请求 text/event-stream; 否则不使用
reply.sse 并且预计将产生正常的反应。 联合国
存在 :
fastify.get('/data', { sse: 'dual' }, async (request, reply) => {
const data = await getData()
if (reply.sse) {
// SSE client — stream the data
await reply.sse.send({ data })
} else {
// Regular client — return JSON
return { data }
}
})
SSE 特定响应标题( :Content-Type: text/event-stream
(a) 在第一次审议之前,不得犯 reply.sse.send() 编号 :
reply.sse.stream() 呼叫,所以一个处理器 装饰 reply.sse 不过
然后返回掉入到 Fastify 正常值的纯值
串行路径,而不破坏响应.
基于请求逻辑的动态流线( R)sse: 'manual'(中文(简体) )
一些API根据请求决定是否流 机构 相当
比起 Accept 头曰. OpenAI 兼容和其他 LLM 端点,用于
示例,返回 a text/event-stream 请求提出时的反应
{ "stream": true } 和单一的JSON反对——客户端
不发送 Accept: text/event-stream 头头左右.
sse: 'manual' 盖上这个:它完全跳过接受者谈判,
总是附加 reply.sse,并让操作者在运行时决定
是否要流出 。 一样 'dual',SSE信头 懒惰地在
首个 reply.sse.send() 编号 : reply.sse.stream() 呼叫,所以一个处理器
永不流出 快速化的正常序列.
fastify.post('/v1/chat/completions', { sse: 'manual' }, async (request, reply) => {
const completion = await model.run(request.body)
if (request.body.stream) {
// Stream tokens as SSE — headers commit on the first send().
await reply.sse.send(completion.tokens) // async iterable / Readable / messages
return
}
// Non-streaming client — return a normal JSON response.
return completion.toJSON()
})
当处理器解决或
掷出(呼叫) reply.sse.keepAlive() 保持开放 用于出带
发送,那么 reply.sse.close() 完成时). 因为只有心跳
一旦响应作为 SSE 执行, 即非流回落
反应不会被心跳所破坏.
处理错误
fastify.get('/stream', { sse: true }, async (request, reply) => {
try {
async function* riskyGenerator() {
yield { data: 'before error' }
throw new Error('Something went wrong')
}
await reply.sse.send(riskyGenerator())
} catch (error) {
// Handle errors gracefully
await reply.sse.send({
event: 'error',
data: { message: 'Stream error occurred' }
})
}
})
自定义序列化
// Plugin-level serializer
await fastify.register(require('@fastify/sse'), {
serializer: (data) => {
// Custom serialization logic
return typeof data === 'string' ? data : JSON.stringify(data)
}
})
// Route-level serializer
fastify.get('/custom', {
sse: {
serializer: (data) => `CUSTOM:${JSON.stringify(data)}`
}
}, async (request, reply) => {
await reply.sse.send({ data: 'test' }) // Outputs: "CUSTOM:\"test\""
})
测试
测试SSE端点用标准快活注射简化:
const response = await fastify.inject({
method: 'GET',
url: '/events',
headers: {
accept: 'text/event-stream'
}
})
assert.strictEqual(response.statusCode, 200)
assert.strictEqual(response.headers['content-type'], 'text/event-stream')
assert.ok(response.body.includes('data: "Hello SSE!"'))
客户端用户端
<!DOCTYPE html>
<html>
<head>
<title>SSE Client</title>
</head>
<body>
<div id="messages"></div>
<script>
const eventSource = new EventSource('/events')
const messagesDiv = document.getElementById('messages')
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data)
messagesDiv.innerHTML += '<div>' + JSON.stringify(data) + '</div>'
}
eventSource.addEventListener('update', function(event) {
console.log('Update event:', JSON.parse(event.data))
})
eventSource.onerror = function(event) {
console.error('SSE error:', event)
}
</script>
</body>
</html>
类型脚本
全部类型脚本支持包括:
import fastify from 'fastify'
import fastifySSE, { SSEMessage } from '@fastify/sse'
const app = fastify()
await app.register(fastifySSE)
app.get('/events', { sse: true }, async (request, reply) => {
const message: SSEMessage = {
id: '123',
event: 'test',
data: { hello: 'world' }
}
await reply.sse.send(message)
// TypeScript knows about SSE properties
console.log(reply.sse.isConnected) // boolean
console.log(reply.sse.lastEventId) // string | null
console.log(reply.sse.shouldKeepAlive) // boolean
})
实例
见 实例 完整工作示例目录 :
与 se- v2 的比较
| 特性 | 钉入- sse- v2 | @ fastify/ sse (帮助) |
|---|---|---|
| 基础SSE | ✅ | ✅ |
| 同步显示器 | ✅ | ✅ |
| 流支持 | ✅ | 增强 |
| 会话管理 | ❌ | ✅ |
| 最后活动ID | ❌ | ✅ |
| 连接保健 | ❌ | ✅ |
| 快速整合 | 有限 | 满分 |
| 测试支持 | ❌ | ✅ |