From 51c2c2d80fe1c9443368396f7bd3c584d11dd0ca Mon Sep 17 00:00:00 2001 From: nathaninline Date: Wed, 22 Jul 2026 09:15:15 +0200 Subject: [PATCH] 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). --- conversation.go | 15 ++++++++++++++- e2e.go | 4 +++- main.go | 2 +- resource_windows_amd64.syso | Bin 8340 -> 8340 bytes resource_windows_arm64.syso | Bin 8340 -> 8340 bytes versioninfo.json | 6 +++--- web.go | 6 ++++-- 7 files changed, 25 insertions(+), 8 deletions(-) diff --git a/conversation.go b/conversation.go index b6bf3c5..0a993c3 100644 --- a/conversation.go +++ b/conversation.go @@ -311,6 +311,16 @@ func (c *Conversation) Subscribe(ctx context.Context, from int, emit func(map[st 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). c.mu.Lock() 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 } } - 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 } diff --git a/e2e.go b/e2e.go index 5e12fa0..0f711d7 100644 --- a/e2e.go +++ b/e2e.go @@ -157,7 +157,9 @@ func handleE2EChat(w http.ResponseWriter, r *http.Request) { return } 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") flusher, _ := w.(http.Flusher) mu, stop := sseHeartbeat(w, flusher) diff --git a/main.go b/main.go index 8b276a5..581cbf8 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( "strings" ) -const Version = "0.4.2" +const Version = "0.4.3" func main() { // Migration one-shot des anciens skills (SKILLS//SKILL.md) vers la diff --git a/resource_windows_amd64.syso b/resource_windows_amd64.syso index ba9f97a2903bd26d8ab431dd1f0bccec8832a847..2971cd3c9aeaccfefc3cc3fbe9bd9e0883d1faa7 100644 GIT binary patch delta 36 ncmbQ@IK^>;lnf^`0}BHPFmG0t5oBUC-fSut%m}19<+&LFgVP4m delta 36 ncmbQ@IK^>;lnf^m0}BHPFl|;lnf^`0}BHPFmG0t5oBUC-fSut%m}19<+&LFgVP4m delta 36 ncmbQ@IK^>;lnf^m0}BHPFl|