Spaces:
Running
Running
| export const DEFAULT_RELAY_KEEPALIVE_MS = 240000; | |
| export const MIN_RELAY_KEEPALIVE_MS = 60000; | |
| const DEFAULT_KEEPALIVE_PATH = '/api/status'; | |
| const KEEPALIVE_TIMEOUT_MS = 10000; | |
| export function parseRelayKeepaliveMs(value, fallback = DEFAULT_RELAY_KEEPALIVE_MS) { | |
| if (String(value).trim() === '0') { | |
| return 0; | |
| } | |
| const next = Number(value); | |
| if (!Number.isFinite(next) || next < MIN_RELAY_KEEPALIVE_MS) { | |
| return fallback; | |
| } | |
| return Math.floor(next); | |
| } | |
| export function buildRelayKeepaliveUrl(relayUrl, pathname = DEFAULT_KEEPALIVE_PATH) { | |
| const url = new URL(relayUrl); | |
| if (url.username || url.password) { | |
| throw new Error('relay_keepalive_invalid_relay_url'); | |
| } | |
| if (url.protocol === 'wss:') { | |
| url.protocol = 'https:'; | |
| } else if (url.protocol === 'ws:') { | |
| url.protocol = 'http:'; | |
| } else { | |
| throw new Error('relay_keepalive_invalid_relay_url'); | |
| } | |
| url.pathname = pathname; | |
| url.search = ''; | |
| url.hash = ''; | |
| url.searchParams.set('keepalive', '1'); | |
| return url.toString(); | |
| } | |
| export function createRelayKeepalive({ | |
| relayUrl, | |
| intervalMs = DEFAULT_RELAY_KEEPALIVE_MS, | |
| fetchImpl = globalThis.fetch, | |
| logState = () => {}, | |
| setIntervalFn = setInterval, | |
| clearIntervalFn = clearInterval | |
| }) { | |
| const keepaliveUrl = buildRelayKeepaliveUrl(relayUrl); | |
| let timer = null; | |
| let pingInFlight = null; | |
| async function ping() { | |
| if (pingInFlight) { | |
| return pingInFlight; | |
| } | |
| pingInFlight = runPing().finally(() => { | |
| pingInFlight = null; | |
| }); | |
| return pingInFlight; | |
| } | |
| async function runPing() { | |
| try { | |
| const response = await fetchImpl(keepaliveUrl, { | |
| method: 'GET', | |
| headers: { | |
| accept: 'application/json', | |
| 'cache-control': 'no-store' | |
| }, | |
| signal: AbortSignal.timeout(KEEPALIVE_TIMEOUT_MS) | |
| }); | |
| await releaseResponseBody(response); | |
| if (!response.ok) { | |
| logState('keepalive_failed', `status=${response.status}`); | |
| return false; | |
| } | |
| logState('keepalive', `status=${response.status}`); | |
| return true; | |
| } catch (error) { | |
| logState('keepalive_failed', error.message || 'request_failed'); | |
| return false; | |
| } | |
| } | |
| function start() { | |
| if (!intervalMs || timer) { | |
| return; | |
| } | |
| timer = setIntervalFn(() => ping(), intervalMs); | |
| timer.unref?.(); | |
| } | |
| function stop() { | |
| if (!timer) { | |
| return; | |
| } | |
| clearIntervalFn(timer); | |
| timer = null; | |
| } | |
| async function releaseResponseBody(response) { | |
| try { | |
| await response.body?.cancel?.(); | |
| } catch { | |
| return; | |
| } | |
| } | |
| return { | |
| ping, | |
| start, | |
| stop | |
| }; | |
| } | |