diff --git a/app.py b/app.py index 0ed3319..9bf09f5 100644 --- a/app.py +++ b/app.py @@ -389,6 +389,9 @@ $("#padname").textContent = "/"+docId+"/"; let ws, ver = 0, clientId = Math.random().toString(36).slice(2), debounce; let isProtected = false, isAuthed = false; +// dirty: local edits the server has not acknowledged. Guards the reconnect path +// from overwriting text typed while the socket was down. +let dirty = false, reconnectTimer = null, reconnectDelay = 500; const urlPw = new URLSearchParams(location.search).get("pw") || ""; // --- Line numbers --- @@ -572,6 +575,7 @@ function connect(){ $("#status").classList.remove("connected"); ws = new WebSocket(`${proto}://${location.host}/ws/${encodeURIComponent(docId)}`); ws.onopen = () => { + reconnectDelay = 500; $("#status").textContent = "connected"; $("#status").classList.add("connected"); }; @@ -590,7 +594,14 @@ function connect(){ } else { isAuthed = true; hideOverlay(); - ver = msg.ver; ta.value = msg.text; refresh(); + ver = msg.ver; + if (dirty && ta.value !== msg.text) { + // Reconnected with unsent edits: push them rather than lose them. + ws.send(JSON.stringify({type: "edit", ver, text: ta.value, clientId})); + } else { + ta.value = msg.text; + } + refresh(); applyLang(msg.lang || ""); // ?pw= on an unprotected pad sets the password instead of unlocking. // Guard on !isProtected: the server re-sends init after a successful @@ -634,8 +645,31 @@ function connect(){ $("#status").textContent = "disconnected"; $("#status").classList.remove("connected"); $("#peers").style.display = "none"; + scheduleReconnect(); }; } + +function scheduleReconnect() { + if (reconnectTimer) return; + reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); }, reconnectDelay); + reconnectDelay = Math.min(reconnectDelay * 2, 10000); // back off, cap at 10s +} + +// Reconnect if the socket died while we were not looking. A pad restored from the +// browser's back/forward cache resumes with its socket already closed and no close +// event delivered, which is why leaving a pad and pressing Back left it dead. +function ensureConnected() { + if (!ws || ws.readyState === 2 || ws.readyState === 3) { + clearTimeout(reconnectTimer); reconnectTimer = null; + reconnectDelay = 500; + connect(); + } +} +window.addEventListener("pageshow", (e) => { if (e.persisted) ensureConnected(); }); +document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "visible") ensureConnected(); +}); +window.addEventListener("online", ensureConnected); $("#newpad").addEventListener("click", (e) => { e.preventDefault(); window.open("/" + rand() + "/", "_blank"); }); // Copy to clipboard function @@ -688,10 +722,12 @@ ta.addEventListener("keydown", (e) => { // Send edits (debounced) ta.addEventListener("input", () => { + dirty = true; clearTimeout(debounce); debounce = setTimeout(() => { if (ws?.readyState === 1 && isAuthed) { ws.send(JSON.stringify({type:"edit", ver, text: ta.value, clientId})); + dirty = false; } }, 120); });