mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 23:18:13 +02:00
v0.4.3 : anti-buffering SSE (Cloudflare) - fin des derniers messages en retard au refresh
Le chat E2E d'app.ajean.link traverse Cloudflare (ajean.link, proxy orange) qui bufferise le flux SSE par lots : la queue du replay arrivait 20-30s apres le reste, de facon intermittente (surtout Safari iOS). Corrige par la technique standard : padding d'amorcage en tete de flux + padding sur caught_up (force le flush de la queue), Cache-Control: no-transform (interdit a Cloudflare de bufferiser/compresser), et heartbeat 15s->4s (borne le pire cas).
This commit is contained in:
1 parent
7d104e3213
commit
51c2c2d80f
7 files changed
+25
-8
No files matched your search
+14
-1
@@ -311,6 +311,16 @@ func (c *Conversation) Subscribe(ctx context.Context, from int, emit func(map[st
|
|||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// 0. Amorçage anti-buffering. Le chat E2E d'app.ajean.link traverse Cloudflare
|
||||||
|
// (ajean.link, proxy orange) : tant qu'un proxy intermédiaire n'a pas reçu assez
|
||||||
|
// d'octets, il bufferise la réponse et ne la relaie qu'en retard (symptôme :
|
||||||
|
// derniers messages qui arrivent 20-30 s après le reste, de façon intermittente
|
||||||
|
// sur Safari). Envoyer d'emblée un gros événement de padding force le proxy à
|
||||||
|
// basculer en mode streaming tout de suite. Le client ignore la clé `pad`.
|
||||||
|
if !emit(map[string]any{"pad": strings.Repeat("·", 2048)}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Replay coalescé (snapshot hors verrou pour ne pas bloquer la génération).
|
// 1. Replay coalescé (snapshot hors verrou pour ne pas bloquer la génération).
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
snapshot := append([]LogEvent(nil), c.Log...)
|
snapshot := append([]LogEvent(nil), c.Log...)
|
||||||
@@ -328,7 +338,10 @@ func (c *Conversation) Subscribe(ctx context.Context, from int, emit func(map[st
|
|||||||
last = s
|
last = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !emit(map[string]any{"caught_up": true}) {
|
// Padding aussi sur caught_up : force le flush de la QUEUE du replay à travers un
|
||||||
|
// proxy qui re-bufferise par lots (Cloudflare) → les derniers messages arrivent
|
||||||
|
// avec le reste, plus 20-30 s après.
|
||||||
|
if !emit(map[string]any{"caught_up": true, "pad": strings.Repeat("·", 2048)}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,9 @@ func handleE2EChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
// no-transform : interdit à Cloudflare (proxy orange sur ajean.link) de
|
||||||
|
// bufferiser/compresser le flux — sinon les événements arrivent en retard.
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-transform")
|
||||||
w.Header().Set("X-Accel-Buffering", "no")
|
w.Header().Set("X-Accel-Buffering", "no")
|
||||||
flusher, _ := w.(http.Flusher)
|
flusher, _ := w.(http.Flusher)
|
||||||
mu, stop := sseHeartbeat(w, flusher)
|
mu, stop := sseHeartbeat(w, flusher)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "0.4.2"
|
const Version = "0.4.3"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Migration one-shot des anciens skills (SKILLS/<nom>/SKILL.md) vers la
|
// Migration one-shot des anciens skills (SKILLS/<nom>/SKILL.md) vers la
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+3
-3
@@ -3,13 +3,13 @@
|
|||||||
"FileVersion": {
|
"FileVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 4,
|
"Minor": 4,
|
||||||
"Patch": 2,
|
"Patch": 3,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"ProductVersion": {
|
"ProductVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 4,
|
"Minor": 4,
|
||||||
"Patch": 2,
|
"Patch": 3,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
||||||
"OriginalFilename": "jean.exe",
|
"OriginalFilename": "jean.exe",
|
||||||
"ProductName": "Jean",
|
"ProductName": "Jean",
|
||||||
"ProductVersion": "0.4.2",
|
"ProductVersion": "0.4.3",
|
||||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
@@ -830,7 +830,9 @@ func sseHeartbeat(w http.ResponseWriter, flusher http.Flusher) (*sync.Mutex, fun
|
|||||||
mu := &sync.Mutex{}
|
mu := &sync.Mutex{}
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
t := time.NewTicker(15 * time.Second)
|
// 4 s (et non 15) : borne le temps qu'un dernier bout de flux peut rester
|
||||||
|
// coincé dans un buffer proxy (Cloudflare) faute d'octets pour le pousser.
|
||||||
|
t := time.NewTicker(4 * time.Second)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -907,7 +909,7 @@ func handleChat(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache, no-transform")
|
||||||
w.Header().Set("X-Accel-Buffering", "no")
|
w.Header().Set("X-Accel-Buffering", "no")
|
||||||
flusher, _ := w.(http.Flusher)
|
flusher, _ := w.(http.Flusher)
|
||||||
mu, stop := sseHeartbeat(w, flusher)
|
mu, stop := sseHeartbeat(w, flusher)
|
||||||
|
|||||||
Reference in new issue
Block a user