Shared editor language selection; paced WebSocket reconnects
- LangSelect.vue: one language selector for the page and structure tabs (small flag, clean dropdown), v-modeled on the shell-wide editorLang — switching in either panel switches both AND the page preview. While the panel is open the selection overrides the normal language preferences (swapdoc.setLangOverride: loadPlain and pagerite.js fetches/prefetches pin ?lang=, the primary language by its own code); closing restores. - reconnect.js: every socket (page/banner editors, analytics view, the activity channel) now connects through a staggered slot — simultaneous attempts at page load (Vite's HMR socket plus ours, repeated on every refresh) trip the browser's WebSocket throttling, leaving all sockets to the host "pending" for minutes, which was the recurring empty editor. A watchdog closes sockets stuck CONNECTING so they reschedule through the policy (jittered exponential backoff, reset only by a healthy connection) instead of hanging forever; a reconnected editor re-opens its document when the previous open died with its socket.
This commit is contained in:
+24
-11
@@ -6,6 +6,7 @@
|
||||
// support from the article itself and are re-applied after each swap.
|
||||
import { OverlayScrollbars } from "overlayscrollbars";
|
||||
import "overlayscrollbars/overlayscrollbars.css";
|
||||
import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect";
|
||||
|
||||
(() => {
|
||||
// Overlay scrollbars: the native kind reserves a strip of layout (or
|
||||
@@ -50,13 +51,21 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
url.searchParams.delete("lang");
|
||||
history.replaceState(history.state, "", url);
|
||||
}
|
||||
// The session language. While the editor panel is open, its language
|
||||
// selection overrides the normal preference (swapdoc.setLangOverride):
|
||||
// internal fetches and prefetches follow it until the panel closes and
|
||||
// the override clears (null restores the initial ?lang=, if any).
|
||||
let sessionLang = langParam;
|
||||
addEventListener("pagerite:session-lang", (ev) => {
|
||||
sessionLang = ev.detail?.lang || langParam;
|
||||
});
|
||||
// An internal URL as fetched: carries the session's ?lang= unless the
|
||||
// link already pins a language of its own. With no ?lang= on the initial
|
||||
// load nothing is ever added.
|
||||
const pageUrl = (url) => {
|
||||
const u = new URL(url, location.href);
|
||||
if (langParam && u.origin === location.origin && !u.searchParams.has("lang")) {
|
||||
u.searchParams.set("lang", langParam);
|
||||
if (sessionLang && u.origin === location.origin && !u.searchParams.has("lang")) {
|
||||
u.searchParams.set("lang", sessionLang);
|
||||
}
|
||||
return u;
|
||||
};
|
||||
@@ -394,10 +403,9 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
// conditional request); it enters the cache when navigated to.
|
||||
const pageCache = new Map(); // rawKey/cacheKey(url) -> HTML text
|
||||
addEventListener("pagerite:page-fetched", (ev) => {
|
||||
// The editors' re-renders fetch the plain URL (no ?lang=); key by the
|
||||
// URL as announced. Adding the session language would cache that
|
||||
// header-language copy under the translated page's key and serve it
|
||||
// back on navigation.
|
||||
// Key by the URL as announced, exactly as the editor fetched it: a
|
||||
// copy pinned to a language (?lang=) caches under its own key, where
|
||||
// navigation with the same session language finds it.
|
||||
pageCache.set(rawKey(ev.detail.url), ev.detail.html);
|
||||
});
|
||||
|
||||
@@ -551,8 +559,12 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
// failing in the background.
|
||||
let ws = null;
|
||||
const wsQueue = [];
|
||||
let wsReconnectMs = 1000;
|
||||
let wsNotBefore = 0;
|
||||
const wsPolicy = reconnectPolicy({ min: 1000 });
|
||||
// The first attempt is staggered too: page load opens several sockets at
|
||||
// once (Vite's HMR socket, the editors), and the burst trips the browser's
|
||||
// WebSocket throttling (sockets then sit "pending" for minutes).
|
||||
let wsNotBefore = Date.now() + socketSlot();
|
||||
let wsWatchdog = null;
|
||||
|
||||
function activityWs() {
|
||||
if (ws || Date.now() < wsNotBefore) return;
|
||||
@@ -563,15 +575,16 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
clearTimeout(wsWatchdog);
|
||||
wsWatchdog = watchConnecting(ws, "activity");
|
||||
ws.onopen = () => {
|
||||
wsReconnectMs = 1000;
|
||||
wsPolicy.opened();
|
||||
for (const msg of wsQueue.splice(0)) ws.send(JSON.stringify(msg));
|
||||
};
|
||||
ws.onclose = () => {
|
||||
ws = null;
|
||||
// No timer here: the next user activity retries, after the backoff.
|
||||
wsNotBefore = Date.now() + wsReconnectMs;
|
||||
wsReconnectMs = Math.min(wsReconnectMs * 2, 30_000);
|
||||
wsNotBefore = Date.now() + wsPolicy.closed();
|
||||
};
|
||||
ws.onerror = () => ws.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user