mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-26 09:35:52 +00:00
- Run go fmt on all Go files (10 files formatted) - Fix 50+ errcheck issues by adding proper error handling - Fix 3 staticcheck issues (empty branches, error string capitalization) - Remove 2 unused struct fields - Install and configure golangci-lint v2.1.6 for Go 1.24 compatibility - All linting now passes with 0 issues 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
166 lines
3.8 KiB
Go
166 lines
3.8 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/vibetunnel/linux/pkg/protocol"
|
|
"github.com/vibetunnel/linux/pkg/session"
|
|
)
|
|
|
|
type MultiSSEStreamer struct {
|
|
w http.ResponseWriter
|
|
manager *session.Manager
|
|
sessionIDs []string
|
|
flusher http.Flusher
|
|
done chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func NewMultiSSEStreamer(w http.ResponseWriter, manager *session.Manager, sessionIDs []string) *MultiSSEStreamer {
|
|
flusher, _ := w.(http.Flusher)
|
|
return &MultiSSEStreamer{
|
|
w: w,
|
|
manager: manager,
|
|
sessionIDs: sessionIDs,
|
|
flusher: flusher,
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (m *MultiSSEStreamer) Stream() {
|
|
m.w.Header().Set("Content-Type", "text/event-stream")
|
|
m.w.Header().Set("Cache-Control", "no-cache")
|
|
m.w.Header().Set("Connection", "keep-alive")
|
|
m.w.Header().Set("X-Accel-Buffering", "no")
|
|
|
|
// Start a goroutine for each session
|
|
for _, sessionID := range m.sessionIDs {
|
|
m.wg.Add(1)
|
|
go m.streamSession(sessionID)
|
|
}
|
|
|
|
// Wait for all streams to complete
|
|
m.wg.Wait()
|
|
}
|
|
|
|
func (m *MultiSSEStreamer) streamSession(sessionID string) {
|
|
defer m.wg.Done()
|
|
|
|
sess, err := m.manager.GetSession(sessionID)
|
|
if err != nil {
|
|
if err := m.sendError(sessionID, fmt.Sprintf("Session not found: %v", err)); err != nil {
|
|
// Log error but continue - client might have disconnected
|
|
log.Printf("[ERROR] MultiStream: Failed to send error for session %s: %v", sessionID, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
streamPath := sess.StreamOutPath()
|
|
file, err := os.Open(streamPath)
|
|
if err != nil {
|
|
if err := m.sendError(sessionID, fmt.Sprintf("Failed to open stream: %v", err)); err != nil {
|
|
log.Printf("Failed to send error message: %v", err)
|
|
}
|
|
return
|
|
}
|
|
defer func() {
|
|
if err := file.Close(); err != nil {
|
|
log.Printf("Failed to close stream file: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Seek to end for live streaming
|
|
if _, err := file.Seek(0, io.SeekEnd); err != nil {
|
|
log.Printf("Failed to seek to end of stream file: %v", err)
|
|
}
|
|
|
|
reader := protocol.NewStreamReader(file)
|
|
ticker := time.NewTicker(100 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-m.done:
|
|
return
|
|
case <-ticker.C:
|
|
for {
|
|
event, err := reader.Next()
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
if err := m.sendError(sessionID, fmt.Sprintf("Stream read error: %v", err)); err != nil {
|
|
log.Printf("Failed to send stream error to client: %v", err)
|
|
}
|
|
return
|
|
}
|
|
break
|
|
}
|
|
|
|
if err := m.sendEvent(sessionID, event); err != nil {
|
|
return
|
|
}
|
|
|
|
if event.Type == "end" {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *MultiSSEStreamer) sendEvent(sessionID string, event *protocol.StreamEvent) error {
|
|
// Match Rust format: send raw arrays for terminal events
|
|
if event.Type == "event" && event.Event != nil {
|
|
// For terminal events, send as raw array
|
|
data := []interface{}{
|
|
event.Event.Time,
|
|
string(event.Event.Type),
|
|
event.Event.Data,
|
|
}
|
|
|
|
jsonData, err := json.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Match Rust multistream format: sessionID:event_json
|
|
prefixedEvent := fmt.Sprintf("%s:%s", sessionID, jsonData)
|
|
|
|
if _, err := fmt.Fprintf(m.w, "data: %s\n\n", prefixedEvent); err != nil {
|
|
return err // Client disconnected
|
|
}
|
|
} else {
|
|
// For other event types, serialize the event
|
|
jsonData, err := json.Marshal(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Match Rust multistream format: sessionID:event_json
|
|
prefixedEvent := fmt.Sprintf("%s:%s", sessionID, jsonData)
|
|
|
|
if _, err := fmt.Fprintf(m.w, "data: %s\n\n", prefixedEvent); err != nil {
|
|
return err // Client disconnected
|
|
}
|
|
}
|
|
|
|
if m.flusher != nil {
|
|
m.flusher.Flush()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *MultiSSEStreamer) sendError(sessionID string, message string) error {
|
|
event := &protocol.StreamEvent{
|
|
Type: "error",
|
|
Message: message,
|
|
}
|
|
return m.sendEvent(sessionID, event)
|
|
}
|