mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
UI : plus de requetes suspendues indefiniment (saturation de connexions)
Chaque onglet Jean ouvert monopolise une connexion SSE permanente ; au-dela du plafond navigateur (~6 par domaine) toute nouvelle requete part en file d'attente sans jamais aboutir NI echouer : UI figee sur << chargement... >> alors que le serveur repond en quelques ms (verifie en curl). - un onglet cache relache son flux SSE et le reprend au retour (le replay depuis lastSeq rattrape ce qui a ete manque) - timeout de 30 s sur les appels /api/* ordinaires, avec un message explicite au lieu d'une attente infinie
This commit is contained in:
1 parent
e7d837d17e
commit
43ea4ad6c7
3 files changed
+74
-2
No files matched your search
@@ -895,11 +895,30 @@ function authHeaders(h){ h = Object.assign({}, h||{}); if(TOKEN) h['Authorizatio
|
|||||||
// Base d'URL : "" en local (page à "/"), "/u/<id>" servie via le relais ajean.link.
|
// Base d'URL : "" en local (page à "/"), "/u/<id>" servie via le relais ajean.link.
|
||||||
// Garde les chemins absolus (/api/…) corrects derrière le tunnel.
|
// Garde les chemins absolus (/api/…) corrects derrière le tunnel.
|
||||||
const API_BASE = location.pathname.replace(/\/(index\.html)?$/, '');
|
const API_BASE = location.pathname.replace(/\/(index\.html)?$/, '');
|
||||||
|
// Délai maximal d'un appel /api/* ordinaire. Sans lui, une requête que le
|
||||||
|
// navigateur met en file d'attente (plafond de ~6 connexions par domaine, atteint
|
||||||
|
// dès qu'on laisse traîner plusieurs onglets Jean) reste suspendue POUR TOUJOURS :
|
||||||
|
// ni réponse, ni erreur, et une UI figée sur « chargement… » sans rien à afficher.
|
||||||
|
// Mieux vaut une erreur franche. Les flux longs (SSE) passent leur propre signal
|
||||||
|
// et ne sont donc jamais concernés.
|
||||||
|
const API_TIMEOUT_MS = 30000;
|
||||||
async function jfetch(u, opts){
|
async function jfetch(u, opts){
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
opts.headers = authHeaders(opts.headers);
|
opts.headers = authHeaders(opts.headers);
|
||||||
if(u.charAt(0) === '/') u = API_BASE + u;
|
if(u.charAt(0) === '/') u = API_BASE + u;
|
||||||
let r = await fetch(u, opts);
|
let timer=null;
|
||||||
|
if(!opts.signal){
|
||||||
|
const ac=new AbortController();
|
||||||
|
opts.signal=ac.signal;
|
||||||
|
timer=setTimeout(()=>ac.abort(), API_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
let r;
|
||||||
|
try{ r = await fetch(u, opts); }
|
||||||
|
catch(e){
|
||||||
|
if(e && e.name==='AbortError'){ throw new Error('requête sans réponse après 30 s (trop d\'onglets Jean ouverts ?)'); }
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
finally{ if(timer) clearTimeout(timer); }
|
||||||
if(r.status === 401){
|
if(r.status === 401){
|
||||||
const k = await askPrompt('Clé de pilotage jean requise :', {title:'Authentification', placeholder:'clé…'});
|
const k = await askPrompt('Clé de pilotage jean requise :', {title:'Authentification', placeholder:'clé…'});
|
||||||
if(k){ TOKEN = k.trim(); localStorage.setItem('jean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
if(k){ TOKEN = k.trim(); localStorage.setItem('jean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
||||||
@@ -1861,8 +1880,25 @@ function handleDelta(d){
|
|||||||
}
|
}
|
||||||
// Flux d'abonnement permanent + reconnexion auto (from=lastSeq → pas de
|
// Flux d'abonnement permanent + reconnexion auto (from=lastSeq → pas de
|
||||||
// re-téléchargement complet après une coupure / bascule d'appareil).
|
// re-téléchargement complet après une coupure / bascule d'appareil).
|
||||||
|
// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet Jean laissé
|
||||||
|
// ouvert monopolise une des ~6 connexions simultanées autorisées par domaine :
|
||||||
|
// au-delà, toute requête (journal, installation du moteur…) reste en file
|
||||||
|
// d'attente sans jamais partir ni échouer — un blocage silencieux très
|
||||||
|
// déroutant. Au retour de l'onglet on se reconnecte, et le replay depuis
|
||||||
|
// lastSeq rattrape tout ce qui s'est passé entre-temps.
|
||||||
|
let streamPaused=false;
|
||||||
|
document.addEventListener('visibilitychange', ()=>{
|
||||||
|
if(document.hidden){
|
||||||
|
streamPaused=true;
|
||||||
|
if(streamAbort) try{ streamAbort.abort(); }catch(e){}
|
||||||
|
} else if(streamPaused){
|
||||||
|
streamPaused=false;
|
||||||
|
}
|
||||||
|
});
|
||||||
async function connectStream(){
|
async function connectStream(){
|
||||||
while(true){
|
while(true){
|
||||||
|
// Onglet en arrière-plan : on n'ouvre aucune connexion, on attend le retour.
|
||||||
|
while(document.hidden){ await new Promise(res=>setTimeout(res, 500)); }
|
||||||
streamAbort=new AbortController();
|
streamAbort=new AbortController();
|
||||||
try{
|
try{
|
||||||
const r=await jfetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({from:lastSeq}),signal:streamAbort.signal});
|
const r=await jfetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({from:lastSeq}),signal:streamAbort.signal});
|
||||||
|
|||||||
@@ -5,11 +5,30 @@ function authHeaders(h){ h = Object.assign({}, h||{}); if(TOKEN) h['Authorizatio
|
|||||||
// Base d'URL : "" en local (page à "/"), "/u/<id>" servie via le relais ajean.link.
|
// Base d'URL : "" en local (page à "/"), "/u/<id>" servie via le relais ajean.link.
|
||||||
// Garde les chemins absolus (/api/…) corrects derrière le tunnel.
|
// Garde les chemins absolus (/api/…) corrects derrière le tunnel.
|
||||||
const API_BASE = location.pathname.replace(/\/(index\.html)?$/, '');
|
const API_BASE = location.pathname.replace(/\/(index\.html)?$/, '');
|
||||||
|
// Délai maximal d'un appel /api/* ordinaire. Sans lui, une requête que le
|
||||||
|
// navigateur met en file d'attente (plafond de ~6 connexions par domaine, atteint
|
||||||
|
// dès qu'on laisse traîner plusieurs onglets Jean) reste suspendue POUR TOUJOURS :
|
||||||
|
// ni réponse, ni erreur, et une UI figée sur « chargement… » sans rien à afficher.
|
||||||
|
// Mieux vaut une erreur franche. Les flux longs (SSE) passent leur propre signal
|
||||||
|
// et ne sont donc jamais concernés.
|
||||||
|
const API_TIMEOUT_MS = 30000;
|
||||||
async function jfetch(u, opts){
|
async function jfetch(u, opts){
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
opts.headers = authHeaders(opts.headers);
|
opts.headers = authHeaders(opts.headers);
|
||||||
if(u.charAt(0) === '/') u = API_BASE + u;
|
if(u.charAt(0) === '/') u = API_BASE + u;
|
||||||
let r = await fetch(u, opts);
|
let timer=null;
|
||||||
|
if(!opts.signal){
|
||||||
|
const ac=new AbortController();
|
||||||
|
opts.signal=ac.signal;
|
||||||
|
timer=setTimeout(()=>ac.abort(), API_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
let r;
|
||||||
|
try{ r = await fetch(u, opts); }
|
||||||
|
catch(e){
|
||||||
|
if(e && e.name==='AbortError'){ throw new Error('requête sans réponse après 30 s (trop d\'onglets Jean ouverts ?)'); }
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
finally{ if(timer) clearTimeout(timer); }
|
||||||
if(r.status === 401){
|
if(r.status === 401){
|
||||||
const k = await askPrompt('Clé de pilotage jean requise :', {title:'Authentification', placeholder:'clé…'});
|
const k = await askPrompt('Clé de pilotage jean requise :', {title:'Authentification', placeholder:'clé…'});
|
||||||
if(k){ TOKEN = k.trim(); localStorage.setItem('jean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
if(k){ TOKEN = k.trim(); localStorage.setItem('jean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
||||||
|
|||||||
@@ -96,8 +96,25 @@ function handleDelta(d){
|
|||||||
}
|
}
|
||||||
// Flux d'abonnement permanent + reconnexion auto (from=lastSeq → pas de
|
// Flux d'abonnement permanent + reconnexion auto (from=lastSeq → pas de
|
||||||
// re-téléchargement complet après une coupure / bascule d'appareil).
|
// re-téléchargement complet après une coupure / bascule d'appareil).
|
||||||
|
// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet Jean laissé
|
||||||
|
// ouvert monopolise une des ~6 connexions simultanées autorisées par domaine :
|
||||||
|
// au-delà, toute requête (journal, installation du moteur…) reste en file
|
||||||
|
// d'attente sans jamais partir ni échouer — un blocage silencieux très
|
||||||
|
// déroutant. Au retour de l'onglet on se reconnecte, et le replay depuis
|
||||||
|
// lastSeq rattrape tout ce qui s'est passé entre-temps.
|
||||||
|
let streamPaused=false;
|
||||||
|
document.addEventListener('visibilitychange', ()=>{
|
||||||
|
if(document.hidden){
|
||||||
|
streamPaused=true;
|
||||||
|
if(streamAbort) try{ streamAbort.abort(); }catch(e){}
|
||||||
|
} else if(streamPaused){
|
||||||
|
streamPaused=false;
|
||||||
|
}
|
||||||
|
});
|
||||||
async function connectStream(){
|
async function connectStream(){
|
||||||
while(true){
|
while(true){
|
||||||
|
// Onglet en arrière-plan : on n'ouvre aucune connexion, on attend le retour.
|
||||||
|
while(document.hidden){ await new Promise(res=>setTimeout(res, 500)); }
|
||||||
streamAbort=new AbortController();
|
streamAbort=new AbortController();
|
||||||
try{
|
try{
|
||||||
const r=await jfetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({from:lastSeq}),signal:streamAbort.signal});
|
const r=await jfetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({from:lastSeq}),signal:streamAbort.signal});
|
||||||
|
|||||||
Reference in new issue
Block a user