UNPKG

rsocket-browser

Version:

Browser-first RSocket client over WebSocket or WebTransport with reconnect and Resume.

187 lines (186 loc) 5.87 kB
import { observeAbort } from "rsocket-core-ts"; //#region src/reconnect/browser.ts /** * Browser lifecycle signals used to make reconnect more resilient on mobile. */ /** * Shared browser signal source. */ var browserReconnectSignals = { isAvailable, waitUntilAvailable, onWake }; /** Shared resolved promise for the common already-online reconnect path. */ var ONLINE_PROMISE = Promise.resolve(); /** Logical sockets multiplexed over one browser-global listener set. */ var wakeListeners = /* @__PURE__ */ new Set(); /** Native listener cleanups retained only while at least one socket is interested. */ var wakeCleanups; /** Host methods used by the active native listener set. */ var wakeEnvironment; /** * Checks the browser network hint without treating missing APIs as offline. */ function isAvailable() { return globalThis.navigator?.onLine !== false; } /** * Waits until browser events suggest the network can be tried again. */ function waitUntilAvailable(signal) { if (signal?.aborted) return Promise.reject(reconnectAbortError()); if (isAvailable()) return ONLINE_PROMISE; return new Promise((resolve, reject) => { let settled = false; let unsubscribe; let releaseAbort; const cleanup = () => { try { unsubscribe?.(); } catch {} const release = releaseAbort; releaseAbort = void 0; release?.(); }; const finish = (callback) => { if (settled) return; settled = true; cleanup(); callback(); }; const tryResolve = () => { if (isAvailable()) finish(resolve); }; const onAbort = () => { finish(() => reject(reconnectAbortError())); }; try { const releaseWake = onWake(tryResolve); if (settled) releaseWake(); else unsubscribe = releaseWake; const release = observeAbort(signal, onAbort); if (settled) release(); else releaseAbort = release; } catch (error) { finish(() => reject(error)); return; } if (!settled) tryResolve(); }); } /** * Creates a standard abort error for reconnect waits. */ function reconnectAbortError() { return new DOMException("Reconnect wait aborted", "AbortError"); } /** * Registers wake listeners on the browser globals that exist in this runtime. */ function onWake(listener) { const environment = currentWakeEnvironment(); if (wakeEnvironment !== void 0 && !sameWakeEnvironment(wakeEnvironment, environment)) { const staleCleanups = wakeCleanups; wakeCleanups = void 0; wakeEnvironment = void 0; wakeListeners.clear(); if (staleCleanups !== void 0) releaseWakeListeners(staleCleanups); } const subscription = { listener }; wakeListeners.add(subscription); if (wakeListeners.size === 1) try { wakeCleanups = installWakeListeners(environment); wakeEnvironment = environment; } catch (error) { wakeListeners.delete(subscription); wakeCleanups = void 0; wakeEnvironment = void 0; throw error; } let active = true; return () => { if (!active) return; active = false; wakeListeners.delete(subscription); if (wakeListeners.size !== 0) return; const cleanups = wakeCleanups; wakeCleanups = void 0; wakeEnvironment = void 0; if (cleanups !== void 0) releaseWakeListeners(cleanups); }; } /** Installs the single native listener set shared by every browser RSocket. */ function installWakeListeners(environment) { const { globalTarget, documentTarget } = environment; if (globalTarget === void 0 && documentTarget === void 0) return []; const cleanups = []; try { for (const type of [ "online", "pageshow", "focus" ]) if (globalTarget !== void 0) cleanups.push(addListener(globalTarget, type, dispatchWake)); if (documentTarget !== void 0) cleanups.push(addListener(documentTarget, "visibilitychange", () => { if (globalThis.document?.visibilityState !== "hidden") dispatchWake(); })); } catch (error) { releaseWakeListeners(cleanups); throw error; } return cleanups; } /** Captures enough host identity to detect browser realm or test-environment replacement. */ function currentWakeEnvironment() { const globalTarget = eventTarget(globalThis); const documentTarget = eventTarget(globalThis.document); return { globalTarget, documentTarget, globalAdd: globalTarget?.addEventListener, globalRemove: globalTarget?.removeEventListener, documentAdd: documentTarget?.addEventListener, documentRemove: documentTarget?.removeEventListener }; } /** Compares native listener operations without allocating an environment key. */ function sameWakeEnvironment(left, right) { return left.globalTarget === right.globalTarget && left.documentTarget === right.documentTarget && left.globalAdd === right.globalAdd && left.globalRemove === right.globalRemove && left.documentAdd === right.documentAdd && left.documentRemove === right.documentRemove; } /** Notifies the current logical listeners while isolating application callbacks. */ function dispatchWake() { const listeners = [...wakeListeners]; for (const subscription of listeners) try { subscription.listener(); } catch {} } /** Attempts every registered wake-listener cleanup exactly once. */ function releaseWakeListeners(cleanups) { let cleanup; while ((cleanup = cleanups.pop()) !== void 0) try { cleanup(); } catch {} } /** * Narrows browser-like globals to the event target operations we need. */ function eventTarget(value) { if (value !== void 0 && value !== null && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function") return value; } /** * Adds one listener and returns its cleanup callback. */ function addListener(target, type, listener) { try { target.addEventListener(type, listener); } catch (error) { try { target.removeEventListener(type, listener); } catch {} throw error; } return () => target.removeEventListener(type, listener); } //#endregion export { browserReconnectSignals }; //# sourceMappingURL=browser.js.map