Implement comprehensive content localization, admin panels for editing each language, AI translation interface with automatic updates when base language version is changed. - SEO tags for all language URLs - Uses accept-language by default, ?lang=en overrides temporarily - User edits patched on top of translations - RTL language supportReviewed-on: #1
57 lines
2.3 KiB
JavaScript
57 lines
2.3 KiB
JavaScript
// Shared reconnect policy for the WebSockets (page/banner editors,
|
|
// analytics view, the activity channel). Two things trip a browser's
|
|
// WebSocket throttling, after which every socket to the host sits
|
|
// "pending" (never opens, never closes) for minutes:
|
|
//
|
|
// 1. A burst of simultaneous attempts — page load opens Vite's HMR
|
|
// socket plus several of ours at the same moment, and every refresh
|
|
// repeats the burst. socketSlot() spaces new sockets out.
|
|
// 2. Too-frequent retries — so failed attempts back off exponentially
|
|
// (a few seconds, doubling to half a minute), reset only after a
|
|
// connection stayed open long enough to count as healthy. A socket
|
|
// that closes right after opening must NOT reset the backoff.
|
|
export function reconnectPolicy({ min = 2000, max = 30000, healthyAfter = 30000 } = {}) {
|
|
let delay = min
|
|
let openedAt = 0
|
|
return {
|
|
// Stamp a socket that just opened.
|
|
opened() {
|
|
openedAt = Date.now()
|
|
},
|
|
// The socket closed: the wait before the next attempt (up to 50%
|
|
// jitter; the base doubles per failure). A healthy streak resets it.
|
|
closed() {
|
|
if (openedAt && Date.now() - openedAt >= healthyAfter) delay = min
|
|
openedAt = 0
|
|
const wait = Math.round(delay * (1 + Math.random() * 0.5))
|
|
delay = Math.min(delay * 2, max)
|
|
return wait
|
|
},
|
|
}
|
|
}
|
|
|
|
// Sockets created at the same moment (page load: Vite's HMR socket plus
|
|
// ours) read as one burst to the browser's throttling. Space new sockets
|
|
// out: each call reserves a slot a beat after the previous one.
|
|
let nextSlot = 0
|
|
export function socketSlot() {
|
|
const now = Date.now()
|
|
const wait = Math.max(0, nextSlot - now)
|
|
nextSlot = Math.max(now, nextSlot) + 300
|
|
return wait
|
|
}
|
|
|
|
// A socket still CONNECTING after this long counts as a failed attempt:
|
|
// browser throttling leaves sockets "pending" (no open, no close) for
|
|
// minutes, and without a watchdog the app would wait on one forever (the
|
|
// recurring empty editor). Closing it fires onclose, which reschedules
|
|
// through the policy's backoff — it never reconnects aggressively itself.
|
|
export function watchConnecting(ws, label) {
|
|
return setTimeout(() => {
|
|
if (ws.readyState === WebSocket.CONNECTING) {
|
|
console.warn(`[pagerite] ${label} socket stuck connecting — closing it, retrying with backoff`)
|
|
ws.close()
|
|
}
|
|
}, 10_000)
|
|
}
|