一键导入
server-sent-events
Server-Sent Events (SSE) for real-time progress updates in Next.js/Vercel. TransformStream patterns, client consumption, progress tracking.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Server-Sent Events (SSE) for real-time progress updates in Next.js/Vercel. TransformStream patterns, client consumption, progress tracking.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Use when implementing structured logging, setting up Axiom observability, migrating from console.* to a centralized logger, or configuring Vercel Log Drains for log collection in Next.js apps
Psychological principles and implementation patterns for AI chat interfaces that feel natural and engaging. Covers Weber-Fechner timing, beta-distribution delays, character-by-character typing simulation, and model response padding.
CSS and React patterns for building WhatsApp-style chat interfaces with pinned input, bottom-anchored messages, scroll management, and streaming support. Covers the CSSence double-reverse pattern, min-h-0 grid/flex fixes, h-dvh viewport locking, and mobile adaptations.
Use when upgrading Zod 3→4, Vitest 2→4, Tailwind CSS 3→4. Covers non-obvious gotchas that cause runtime failures - things NOT prominently documented.
Use when deploying to Vercel - covers Fluid Compute (timeout issues, 60s bug), vercel.json configuration, maxDuration settings, cron jobs, environment variables (printf gotcha), monorepo setup, Next.js issues, and common build errors
DuckDB, MotherDuck cloud analytics, and Parquet file handling for Node.js/TypeScript applications
基于 SOC 职业分类
| name | server-sent-events |
| description | Server-Sent Events (SSE) for real-time progress updates in Next.js/Vercel. TransformStream patterns, client consumption, progress tracking. |
| triggers | ["sse","server-sent events","streaming","progress updates","real-time","event stream"] |
Real-time server-to-client streaming for progress updates, notifications, and live data feeds.
| Technology | Use When | Limitations |
|---|---|---|
| SSE | Server → Client only, progress updates, live feeds | Unidirectional, limited connections |
| WebSockets | Bidirectional chat, gaming, collaboration | More complex, stateful |
| Polling | Simple updates, wide compatibility | Inefficient, latency |
| HTTP Streaming | Large file downloads | No structured events |
Choose SSE for: Progress bars, status updates, notifications, log streaming, live dashboards.
// app/api/progress/route.ts
import { NextRequest } from 'next/server'
interface SSEMessage {
progress: number
message: string
status: 'progress' | 'complete' | 'error'
}
export async function POST(req: NextRequest) {
const body = await req.json()
// SSE Response Headers
const headers = {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
}
// Create transform stream for SSE
const stream = new TransformStream()
const writer = stream.writable.getWriter()
const encoder = new TextEncoder()
// Helper to send SSE messages
async function sendSSE(data: SSEMessage) {
await writer.write(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
}
// Execute work with progress updates (async, don't await)
;(async () => {
try {
await sendSSE({ progress: 0, message: 'Starting...', status: 'progress' })
// Simulate work with progress updates
for (let i = 1; i <= 10; i++) {
await doSomeWork(i)
await sendSSE({
progress: i * 10,
message: `Processing step ${i} of 10...`,
status: 'progress'
})
}
await sendSSE({ progress: 100, message: 'Complete!', status: 'complete' })
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
await sendSSE({ progress: 0, message, status: 'error' })
} finally {
await writer.close()
}
})()
return new Response(stream.readable, { headers })
}
// React component
'use client'
import { useState } from 'react'
interface ProgressState {
progress: number
message: string
status: 'idle' | 'progress' | 'complete' | 'error'
}
export function ProgressTracker() {
const [state, setState] = useState<ProgressState>({
progress: 0,
message: '',
status: 'idle'
})
async function startProcess() {
setState({ progress: 0, message: 'Connecting...', status: 'progress' })
const response = await fetch('/api/progress', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ /* request data */ })
})
if (!response.body) {
setState({ progress: 0, message: 'No response body', status: 'error' })
return
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
// Parse SSE messages from buffer
const lines = buffer.split('\n\n')
buffer = lines.pop() || '' // Keep incomplete message in buffer
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
setState({
progress: data.progress,
message: data.message,
status: data.status
})
}
}
}
}
return (
<div>
<button onClick={startProcess} disabled={state.status === 'progress'}>
Start Process
</button>
<progress value={state.progress} max={100} />
<p>{state.message}</p>
</div>
)
}
data: {"key": "value"}\n\n
Rules:
\n\n (double newline)data: event:, id:, retry: fields// Server
async function sendNamedEvent(event: string, data: object) {
await writer.write(encoder.encode(
`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
))
}
// Send different event types
await sendNamedEvent('progress', { percent: 50 })
await sendNamedEvent('log', { message: 'Processing...' })
await sendNamedEvent('complete', { result: 'success' })
// Client with EventSource (for GET endpoints)
const source = new EventSource('/api/events')
source.addEventListener('progress', (e) => {
const data = JSON.parse(e.data)
console.log('Progress:', data.percent)
})
source.addEventListener('complete', (e) => {
const data = JSON.parse(e.data)
console.log('Done:', data.result)
source.close()
})
// CORRECT: Stream keeps function alive until closed
;(async () => {
try {
// Do all work here
for (const item of items) {
await processItem(item)
await sendSSE({ progress: calculateProgress(item) })
}
await sendSSE({ status: 'complete' })
} finally {
await writer.close() // MUST close to end function
}
})()
return new Response(stream.readable, { headers })
| Plan | Max Duration |
|---|---|
| Hobby | 10 seconds |
| Pro | 60 seconds |
| Enterprise | 900 seconds |
For long operations, consider:
interface PercentageProgress {
progress: number // 0-100
message: string
status: 'progress' | 'complete' | 'error'
}
// Calculate progress for known item count
const total = items.length
for (let i = 0; i < items.length; i++) {
await processItem(items[i])
await sendSSE({
progress: Math.floor(((i + 1) / total) * 100),
message: `Processing ${i + 1} of ${total}...`,
status: 'progress'
})
}
interface PhaseProgress {
phase: string
phaseProgress: number
overallProgress: number
message: string
}
const phases = [
{ name: 'validate', weight: 10 },
{ name: 'process', weight: 70 },
{ name: 'finalize', weight: 20 },
]
// Track progress across phases
let overallProgress = 0
for (const phase of phases) {
await sendSSE({
phase: phase.name,
phaseProgress: 0,
overallProgress,
message: `Starting ${phase.name}...`
})
// Do phase work...
overallProgress += phase.weight
}
interface LogProgress {
type: 'log' | 'warning' | 'error' | 'complete'
message: string
timestamp: string
}
// For operations where progress can't be calculated
await sendSSE({
type: 'log',
message: 'Connecting to external service...',
timestamp: new Date().toISOString()
})
;(async () => {
try {
await riskyOperation()
await sendSSE({ status: 'complete', message: 'Done!' })
} catch (error) {
// Always send error to client before closing
await sendSSE({
status: 'error',
message: error instanceof Error ? error.message : 'Unknown error',
progress: 0
})
} finally {
// ALWAYS close the stream
await writer.close()
}
})()
async function consumeSSE(url: string, body: object) {
try {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(body)
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
// Process stream...
} catch (error) {
// Handle network errors, aborts, etc.
if (error.name === 'AbortError') {
console.log('Request cancelled')
} else {
console.error('SSE failed:', error)
}
}
}
// Client with AbortController
const controller = new AbortController()
const response = await fetch('/api/progress', {
method: 'POST',
body: JSON.stringify(data),
signal: controller.signal
})
// Cancel button handler
function handleCancel() {
controller.abort()
}
// app/api/batch/route.ts
export async function POST(req: NextRequest) {
const { items } = await req.json()
const stream = new TransformStream()
const writer = stream.writable.getWriter()
const encoder = new TextEncoder()
const sendSSE = async (data: object) => {
await writer.write(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
}
;(async () => {
const results = { success: 0, failed: 0, errors: [] as string[] }
try {
await sendSSE({
status: 'progress',
progress: 0,
message: `Processing ${items.length} items...`
})
for (let i = 0; i < items.length; i++) {
try {
await processItem(items[i])
results.success++
} catch (err) {
results.failed++
results.errors.push(`Item ${i}: ${err.message}`)
}
await sendSSE({
status: 'progress',
progress: Math.floor(((i + 1) / items.length) * 100),
message: `Processed ${i + 1} of ${items.length}`,
results: { ...results }
})
}
await sendSSE({
status: 'complete',
progress: 100,
message: `Complete: ${results.success} succeeded, ${results.failed} failed`,
results
})
} catch (error) {
await sendSSE({
status: 'error',
message: error instanceof Error ? error.message : 'Batch failed',
results
})
} finally {
await writer.close()
}
})()
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
}
})
}
// Minimal SSE endpoint
export async function POST(req: NextRequest) {
const stream = new TransformStream()
const writer = stream.writable.getWriter()
const encoder = new TextEncoder()
;(async () => {
await writer.write(encoder.encode(`data: {"message":"Hello"}\n\n`))
await writer.close()
})()
return new Response(stream.readable, {
headers: { 'Content-Type': 'text/event-stream' }
})
}
// Minimal client consumption
const res = await fetch('/api/sse', { method: 'POST' })
const reader = res.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
console.log(decoder.decode(value))
}