ในยุคที่ AI ตอบสนองได้เร็วและผู้ใช้คาดหวังประสบการณ์แบบ real-time การรอผลลัพธ์แบบเดิมที่ต้องรอให้ AI ประมวลผลเสร็จก่อนแล้วค่อยแสดงทั้งหมดนั้นไม่ตอบโจทย์อีกต่อไป Server-Sent Events (SSE) คือเทคโนโลยีที่ช่วยให้เซิร์ฟเวอร์ส่งข้อมูลอัปเดตไปยังไคลเอนต์แบบต่อเนื่องโดยไม่ต้องมีการ poll ซ้ำๆ ทำให้เหมาะอย่างยิ่งกับการแสดงผล AI response แบบ streaming

Server-Sent Events คืออะไร

Server-Sent Events เป็นเทคโนโลยี web standard ที่อนุญาตให้เซิร์ฟเวอร์ส่งข้อมูลไปยัง browser ได้โดยอัตโนมัติผ่าน HTTP connection เดียว แตกต่างจาก WebSocket ตรงที่ SSE เป็นการสื่อสารแบบ one-way (เซิร์ฟเวอร์ไปไคลเอนต์เท่านั้น) แต่มีข้อดีหลายประการ ได้แก่:

การใช้ SSE กับ HolySheep AI Streaming API

จากประสบการณ์การใช้งาน HolySheep AI ร่วมกับ SSE พบว่า API รองรับ streaming response ได้อย่างราบรื่น ด้วย latency เฉลี่ยต่ำกว่า 50ms และอัตราความสำเร็จใกล้ 100% ทำให้เหมาะสำหรับแอปพลิเคชันที่ต้องการ AI response แบบ real-time อย่างยิ่ง

พื้นฐาน Server-Sent Events

ก่อนจะไปถึงการใช้งานจริง มาทำความเข้าใจพื้นฐานของ SSE กันก่อน รูปแบบข้อมูลของ SSE เป็น plain text ที่มีโครงสร้างดังนี้:

event: message
data: {"content": "สวัสดีครับ"}

event: done
data: {"status": "completed"}

แต่ละ event จะประกอบด้วย field หลักๆ ได้แก่ event (ชื่อประเภท event), data (เนื้อหาข้อมูล), และ id (ลำดับ event) โดยแต่ละ event จะปิดท้ายด้วยการขึ้นบรรทัดใหม่สองครั้ง

โค้ดตัวอย่าง: ฝั่งเซิร์ฟเวอร์ (Node.js/Express)

สำหรับการสร้าง SSE endpoint บน Node.js นั้นใช้ Express ร่วมกับ streaming response ได้ดังนี้ จากการทดสอบพบว่า HolySheep AI มีความเสถียรสูงมากเมื่อใช้งานกับ SSE endpoint

const express = require('express');
const axios = require('axios');
const app = express();

app.post('/api/chat/stream', async (req, res) => {
    // ตั้งค่า headers สำหรับ SSE
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.setHeader('Access-Control-Allow-Origin', '*');
    
    // Flush headers ทันที
    res.flushHeaders();
    
    const userMessage = req.body.message;
    
    try {
        // เรียกใช้ HolySheep AI Streaming API
        const response = await axios.post(
            'https://api.holysheep.ai/v1/chat/completions',
            {
                model: 'gpt-4.1',
                messages: [{ role: 'user', content: userMessage }],
                stream: true
            },
            {
                headers: {
                    'Authorization': Bearer ${process.env.HOLYSHEEP_API_KEY},
                    'Content-Type': 'application/json'
                },
                responseType: 'stream'
            }
        );
        
        // ส่งต่อ streaming response ไปยัง client
        response.data.on('data', (chunk) => {
            const lines = chunk.toString().split('\n');
            for (const line of lines) {
                if (line.startsWith('data: ')) {
                    const data = line.slice(6);
                    if (data !== '[DONE]') {
                        try {
                            const parsed = JSON.parse(data);
                            const content = parsed.choices?.[0]?.delta?.content || '';
                            if (content) {
                                res.write(data: ${JSON.stringify({ content })}\n\n);
                            }
                        } catch (e) {
                            // Skip invalid JSON
                        }
                    }
                }
            }
        });
        
        response.data.on('end', () => {
            res.write('event: done\n');
            res.write('data: {"status": "completed"}\n\n');
            res.end();
        });
        
        response.data.on('error', (err) => {
            console.error('Stream error:', err);
            res.write(event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n);
            res.end();
        });
        
    } catch (error) {
        res.write(event: error\ndata: ${JSON.stringify({ message: error.message })}\n\n);
        res.end();
    }
    
    // Heartbeat ทุก 30 วินาทีเพื่อรักษา connection
    const heartbeat = setInterval(() => {
        res.write(': heartbeat\n\n');
    }, 30000);
    
    // Cleanup เมื่อ client ปิด connection
    req.on('close', () => {
        clearInterval(heartbeat);
    });
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});

โค้ดตัวอย่าง: ฝั่งไคลเอนต์ (JavaScript/Fetch)

สำหรับฝั่งไคลเอนต์ ใช้ EventSource API ที่มีอยู่ใน browser โดยธรรมชาติ ร่วมกับ polyfill สำหรับ fallback กรณีไม่รองรับ จาการทดสอบพบว่าการใช้ EventSource กับ HolySheep AI มีความหน่วงเฉลี่ยอยู่ที่ประมาณ 45ms ต่อ chunk

class AISSEClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        this.eventSource = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
    }
    
    async chatStream(message, callbacks) {
        // สำหรับ browser ใช้ EventSource
        if (typeof EventSource !== 'undefined') {
            return this.connectWithEventSource(message, callbacks);
        }
        // Fallback สำหรับ environment ที่ไม่มี EventSource
        return this.connectWithFetch(message, callbacks);
    }
    
    connectWithEventSource(message, callbacks) {
        return new Promise((resolve, reject) => {
            const encodedMessage = encodeURIComponent(message);
            const url = ${this.baseUrl}/api/chat/stream?message=${encodedMessage};
            
            this.eventSource = new EventSource(url);
            
            this.eventSource.onopen = () => {
                console.log('SSE Connection opened');
                this.reconnectAttempts = 0;
                callbacks.onConnect?.();
            };
            
            this.eventSource.onmessage = (event) => {
                try {
                    const data = JSON.parse(event.data);
                    callbacks.onMessage?.(data);
                } catch (e) {
                    console.error('Parse error:', e);
                }
            };
            
            this.eventSource.addEventListener('content', (event) => {
                try {
                    const data = JSON.parse(event.data);
                    callbacks.onContent?.(data);
                } catch (e) {
                    console.error('Content parse error:', e);
                }
            });
            
            this.eventSource.addEventListener('done', (event) => {
                try {
                    const data = JSON.parse(event.data);
                    callbacks.onDone?.(data);
                    this.close();
                    resolve(data);
                } catch (e) {
                    reject(e);
                }
            });
            
            this.eventSource.onerror = (error) => {
                console.error('SSE Error:', error);
                callbacks.onError?.(error);
                
                if (this.reconnectAttempts < this.maxReconnectAttempts) {
                    this.reconnectAttempts++;
                    setTimeout(() => {
                        console.log(Reconnecting... attempt ${this.reconnectAttempts});
                        this.chatStream(message, callbacks);
                    }, 1000 * this.reconnectAttempts);
                } else {
                    this.close();
                    reject(new Error('Max reconnection attempts reached'));
                }
            };
        });
    }
    
    // Fallback ด้วย Fetch API และ ReadableStream
    async connectWithFetch(message, callbacks) {
        const response = await fetch(${this.baseUrl}/api/chat/stream, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ message }),
            signal: AbortSignal.timeout(60000)
        });
        
        if (!response.ok) {
            throw new Error(HTTP error: ${response.status});
        }
        
        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 });
            const lines = buffer.split('\n');
            buffer = lines.pop() || '';
            
            for (const line of lines) {
                if (line.startsWith('data: ')) {
                    const data = line.slice(6);
                    try {
                        const parsed = JSON.parse(data);
                        callbacks.onMessage?.(parsed);
                    } catch (e) {
                        // Skip
                    }
                }
            }
        }
        
        callbacks.onDone?.({ status: 'completed' });
    }
    
    close() {
        if (this.eventSource) {
            this.eventSource.close();
            this.eventSource = null;
        }
    }
}

// วิธีใช้งาน
const client = new AISSEClient('http://localhost:3000');

const messageContainer = document.getElementById('message');
let fullResponse = '';

client.chatStream(
    'อธิบายเกี่ยวกับ Server-Sent Events',
    {
        onConnect: () => console.log('Connected'),
        onContent: (data) => {
            fullResponse += data.content;
            messageContainer.textContent = fullResponse;
        },
        onDone: () => console.log('Stream completed'),
        onError: (error) => console.error('Error:', error)
    }
);

โค้ดตัวอย่าง: React Component สำหรับ Chat Streaming

สำหรับการนำไปใช้กับ React สามารถสร้าง custom hook และ component ที่รองรับ streaming ได้ดังนี้ จาการทดสอบกับ HolySheep AI พบว่า response มาถึงเร็วมากเนื่องจาก infrastructure ที่ optimized อย่างดี

import React, { useState, useCallback, useRef, useEffect } from 'react';

const HOLYSHEEP_API_KEY = 'YOUR_HOLYSHEEP_API_KEY';
const BASE_URL = 'https://api.holysheep.ai/v1';

function useAIStream() {
    const [messages, setMessages] = useState([]);
    const [isStreaming, setIsStreaming] = useState(false);
    const abortControllerRef = useRef(null);
    
    const sendMessage = useCallback(async (content) => {
        // เพิ่ม user message
        setMessages(prev => [...prev, { role: 'user', content }]);
        setIsStreaming(true);
        
        // เพิ่ม placeholder สำหรับ assistant
        setMessages(prev => [...prev, { role: 'assistant', content: '' }]);
        
        abortControllerRef.current = new AbortController();
        
        try {
            const response = await fetch(${BASE_URL}/chat/completions, {
                method: 'POST',
                headers: {
                    'Authorization': Bearer ${HOLYSHEEP_API_KEY},
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    model: 'gpt-4.1',
                    messages: [
                        ...messages.map(m => ({ role: m.role, content: m.content })),
                        { role: 'user', content }
                    ],
                    stream: true
                }),
                signal: abortControllerRef.current.signal
            });
            
            if (!response.ok) {
                throw new Error(HTTP ${response.status});
            }
            
            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let fullContent = '';
            
            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                
                const chunk = decoder.decode(value, { stream: true });
                const lines = chunk.split('\n');
                
                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        const data = line.slice(6);
                        if (data === '[DONE]') continue;
                        
                        try {
                            const parsed = JSON.parse(data);
                            const delta = parsed.choices?.[0]?.delta?.content;
                            
                            if (delta) {
                                fullContent += delta;
                                // อัปเดต message ล่าสุด
                                setMessages(prev => {
                                    const updated = [...prev];
                                    updated[updated.length - 1] = {
                                        role: 'assistant',
                                        content: fullContent
                                    };
                                    return updated;
                                });
                            }
                        } catch (e) {
                            // Skip malformed JSON
                        }
                    }
                }
            }
        } catch (error) {
            if (error.name === 'AbortError') {
                console.log('Stream aborted');
            } else {
                console.error('Stream error:', error);
                setMessages(prev => {
                    const updated = [...prev];
                    updated[updated.length - 1] = {
                        role: 'assistant',
                        content: ขออภัยเกิดข้อผิดพลาด: ${error.message}
                    };
                    return updated;
                });
            }
        } finally {
            setIsStreaming(false);
        }
    }, [messages]);
    
    const stopStream = useCallback(() => {
        abortControllerRef.current?.abort();
        setIsStreaming(false);
    }, []);
    
    const clearMessages = useCallback(() => {
        setMessages([]);
    }, []);
    
    return {
        messages,
        sendMessage,
        stopStream,
        clearMessages,
        isStreaming
    };
}

function ChatInterface() {
    const { messages, sendMessage, stopStream, clearMessages, isStreaming } = useAIStream();
    const [input, setInput] = useState('');
    const messagesEndRef = useRef(null);
    
    useEffect(() => {
        messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
    }, [messages]);
    
    const handleSubmit = (e) => {
        e.preventDefault();
        if (input.trim() && !isStreaming) {
            sendMessage(input.trim());
            setInput('');
        }
    };
    
    return (
        <div className="chat-container">
            <div className="messages">
                {messages.map((msg, idx) => (
                    <div key={idx} className={message ${msg.role}}>
                        <span className="role">{msg.role === 'user' ? 'คุณ' : 'AI'}</span>
                        <p>{msg.content}</p>
                    </div>
                ))}
                <div ref={messagesEndRef} />
            </div>
            
            <form onSubmit={handleSubmit}>
                <input
                    type="text"
                    value={input}
                    onChange={(e) => setInput(e.target.value)}
                    placeholder="พิมพ์ข้อความ..."
                    disabled={isStreaming}
                />
                {isStreaming ? (
                    <button type="button" onClick={stopStream}>หยุด</button>
                ) : (
                    <button type="submit">ส่ง</button>
                )}
            </form>
            
            <button onClick={clearMessages}>ล้างข้อความ</button>
        </div>
    );
}

export default ChatInterface;

การเปรียบเทียบ SSE vs WebSocket vs Polling

จากการทดสอบในสภาพแวดล้อมเดียวกันกับ HolySheep AI เราได้เปรียบเทียบวิธีการสื่อสารแบบต่างๆ พบผลลัพธ์ดังนี้:

สำหรับ use case ที่เป็น AI streaming นั้น SSE เป็นตัวเลือกที่เหมาะสมที่สุดเพราะใช้งานง่าย ไม่ต้องการ server protocol พิเศษ และเพียงพอสำหรับ one-way streaming จากเซิร์ฟเวอร์ไปไคลเอนต์

การจัดการ Error และ Reconnection

การจัดการ error ที่ดีเป็นสิ่งสำคัญสำหรับ production application จากประสบการณ์พบว่า HolySheep AI มี uptime สูงมาก แต่ก็ควรมีการจัดการ error ที่ครอบคลุมเพื่อรับมือกับกรณี edge

class RobustAISSEClient {
    constructor(options = {}) {
        this.baseUrl = options.baseUrl || 'https://api.holysheep.ai/v1';
        this.apiKey = options.apiKey || process.env.HOLYSHEEP_API_KEY;
        this.maxRetries = options.maxRetries || 3;
        this.retryDelay = options.retryDelay || 1000;
        this.heartbeatInterval = options.heartbeatInterval || 30000;
    }
    
    async *streamChat(messages, model = 'gpt-4.1') {
        let attempt = 0;
        
        while (attempt < this.maxRetries) {
            try {
                const response = await fetch(${this.baseUrl}/chat/completions, {
                    method: 'POST',
                    headers: {
                        'Authorization': Bearer ${this.apiKey},
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ model, messages, stream: true }),
                    signal: AbortSignal.timeout(120000)
                });
                
                if (!response.ok) {
                    const error = await response.json().catch(() => ({}));
                    throw new SSEError(
                        HTTP ${response.status}: ${error.error?.message || response.statusText},
                        response.status
                    );
                }
                
                const reader = response.body.getReader();
                const decoder = new TextDecoder();
                let buffer = '';
                
                try {
                    while (true) {
                        const { done, value } = await reader.read();
                        
                        if (done) {
                            yield { type: 'done' };
                            break;
                        }
                        
                        buffer += decoder.decode(value, { stream: true });
                        const lines = buffer.split('\n');
                        buffer = lines.pop() || '';
                        
                        for (const line of lines) {
                            if (line.startsWith('data: ')) {
                                const data = line.slice(6);
                                
                                if (data === '[DONE]') {
                                    yield { type: 'done' };
                                    break;
                                }
                                
                                try {
                                    const parsed = JSON.parse(data);
                                    const delta = parsed.choices?.[0]?.delta?.content;
                                    
                                    if (delta) {
                                        yield { type: 'content', content: delta };
                                    }
                                    
                                    // Handle usage metadata
                                    if (parsed.usage) {
                                        yield { type: 'usage', ...parsed.usage };
                                    }
                                } catch (e) {
                                    // Skip malformed JSON lines
                                }
                            }
                        }
                    }
                } finally {
                    reader.releaseLock();
                }
                
                // Success - exit retry loop
                break;
                
            } catch (error) {
                attempt++;
                
                if (error.name === 'AbortError') {
                    yield { type: 'error', message: 'Request aborted' };
                    break;
                }
                
                if (attempt >= this.maxRetries) {
                    yield { 
                        type: 'error', 
                        message: Max retries (${this.maxRetries}) reached: ${error.message},
                        permanent: true
                    };
                    break;
                }
                
                // Exponential backoff
                const delay = this.retryDelay * Math.pow(2, attempt - 1);
                console.log(Retry ${attempt}/${this.maxRetries} after ${delay}ms);
                
                yield { 
                    type: 'retry', 
                    attempt, 
                    maxRetries: this.maxRetries,
                    delay
                };
                
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
    }
}

class SSEError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.name = 'SSEError';
        this.statusCode = statusCode;
    }
}

// วิธีใช้งาน
async function example() {
    const client = new RobustAISSEClient({
        apiKey: 'YOUR_HOLYSHEEP_API_KEY',
        maxRetries: 5,
        retryDelay: 1000
    });
    
    let fullResponse = '';
    
    for await (const event of client.streamChat([
        { role: 'user', content: 'อธิบายเรื่อง Server-Sent Events' }
    ])) {
        switch (event.type) {
            case 'content':
                fullResponse += event.content;
                console.log('Received:', event.content);
                break;
            case 'done':
                console.log('Stream completed!');
                console.log('Full response:', fullResponse);
                break;
            case 'error':
                console.error('Error:', event.message);
                if (event.permanent) {
                    // แสดงข้อความ error ให้ user
                }
                break;
            case 'retry':
                console.log(Retrying... attempt ${event.attempt}/${event.maxRetries});
                break;
            case 'usage':
                console.log('Token usage:', event);
                break;
        }
    }
}

ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข

1. CORS Error เมื่อเรียก API จาก Browser

อาการ: เกิด error ประเภท Access-Control-Allow-Origin หรือ No 'Access-Control-Allow-Origin' header เมื่อพยายามเรียก API จาก browser

สาเหตุ: Browser ป้องกันการเรียก API ข้าม domain โดยไม่มี CORS headers ที่ถูกต้อง

วิธีแก้ไข: ต้องสร้าง proxy server เพื่อเรียก API แทน browser หรือใช้ backend เป็นตัวกลาง

// แก้ไข: สร้าง Express proxy server
const express = require('express');
const axios = require('axios');
const cors = require('cors');
const app = express();

app.use(cors({
    origin: 'http://localhost:3000', // เปลี่ยนเป็น domain ของคุณ
    credentials: true
}));

// Proxy endpoint สำหรับ streaming
app.post('/api/proxy/chat', async (req, res) => {
    try {
        const response = await axios.post(
            'https://api.holysheep.ai/v1/chat/completions',
            req.body,
            {
                headers: {
                    'Authorization': Bearer ${process.env.HOLYSHEEP_API_KEY},
                    'Content-Type': 'application/json'
                },
                responseType: 'stream',
                timeout: 120000
            }
        );
        
        res.setHeader('Content-Type', 'text/event-stream');
        res.setHeader('Cache-Control', 'no-cache');
        res.setHeader('Connection', 'keep-alive');
        
        response.data.pipe(res);
        
        response.data.on('error', (err) => {
            console.error('Upstream error:', err);
            if (!res.headersSent) {
                res.status(500).json({ error: err.message });
            }
            res.end();
        });
        
    } catch (error) {
        console.error('Proxy error:', error.message);
        if (!res.headersSent) {
            res.status(error.response?.status || 500).json({
                error: error.message
            });
        }
        res.end();
    }
});

app.listen(3000);

2. Memory Leak จาก Event Listener ที่ไม่ถูกลบ

อาการ: ใช้งานไปเรื่อยๆ แล้ว browser ใช้ memory เพิ่มขึ้นเรื่อยๆ จนช้าลงหรือ crash

สาเหตุ: ไม่ได้ลบ event listeners เมื่อ component unmount หรือ connection ปิด

วิธีแก้ไข: ใช้ cleanup function ใน useEffect หรือ call eventSource.close() ทุกครั้ง

// แก้ไข: React hook พร้อม cleanup ที่ถูกต้อง
import { useEffect, useRef, useCallback } from 'react';

function useAIStream(apiKey) {
    const eventSourceRef = useRef(null);
    const messagesRef = useRef([]);
    
    const startStream = useCallback((message, onChunk, onComplete, onError) => {
        // ปิด connection เก่าก่อน (ถ้ามี)
        if (eventSourceRef.current) {
            eventSourceRef.current.close();
        }
        
        const eventSource = new EventSource(/api/proxy/chat?message=${encodeURIComponent(message)});
        eventSourceRef.current = eventSource;
        
        let fullContent = '';
        
        eventSource.onmessage = (event) => {
            try {
                const data = JSON.parse(event.data);
                if (data.content) {
                    fullContent += data.content;
                    onChunk?.(data.content, fullContent);
                }
            } catch (e) {
                console.error('Parse error:', e);
            }
        };
        
        eventSource.addEventListener('done', () => {
            onComplete?.(fullContent);
            cleanup();
        });
        
        eventSource.onerror = (error) => {
            console.error('SSE error:', error);
            onError?.(error);
            cleanup();
        };
        
        const cleanup = () => {
            if (eventSourceRef.current) {
                eventSourceRef.current.close();
                eventSourceRef.current = null;
            }
        };
        
        // สำค