src/ui/session.js
Rama main · réplica de lectura del repositorio original.
const WIZARD_SESSION_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
const WIZARD_ROUTES = new Set(["/", "/assistant", "/local", "/supergrok-vps", "/claude-vps", "/local-model-vps", "/hosting"]);
const fragmentSessionWindows = new WeakSet();
const pendingBrowserTransfer = globalThis.__relmioWizardSessionReady;
if (pendingBrowserTransfer && typeof pendingBrowserTransfer.then === "function") {
try { await pendingBrowserTransfer; } catch { /* The bootstrap fails closed. */ }
try { delete globalThis.__relmioWizardSessionReady; } catch {
globalThis.__relmioWizardSessionReady = null;
}
}
function validWizardSession(value) {
return typeof value === "string" && WIZARD_SESSION_PATTERN.test(value);
}
function cleanWizardLocation(location) {
return `${location.pathname}${location.hash ?? ""}`;
}
function preserveFragmentSession(browserWindow, session) {
if (
!validWizardSession(session) ||
!browserWindow ||
(typeof browserWindow !== "object" && typeof browserWindow !== "function") ||
typeof browserWindow.addEventListener !== "function" ||
fragmentSessionWindows.has(browserWindow)
) return;
fragmentSessionWindows.add(browserWindow);
const documentPath = browserWindow.location.pathname;
// Navigation carries the private capability through history before reloading.
// Back/Forward can restore that same-document entry with the old route's DOM.
browserWindow.addEventListener("popstate", () => {
if (browserWindow.location.pathname !== documentPath) {
browserWindow.location.reload();
}
});
browserWindow.addEventListener("pageshow", (event) => {
if (event.persisted) browserWindow.location.reload();
});
browserWindow.addEventListener("hashchange", () => {
browserWindow.history.replaceState(
{ relmioWizardSession: session },
"",
cleanWizardLocation(browserWindow.location),
);
});
}
export function readWizardSession(browserWindow = window) {
const { history, location } = browserWindow;
const historySession = validWizardSession(
history.state?.relmioWizardSession,
)
? history.state.relmioWizardSession
: null;
const session = historySession;
const cleanLocation = cleanWizardLocation(location);
history.replaceState(
session ? { relmioWizardSession: session } : null,
"",
cleanLocation,
);
preserveFragmentSession(browserWindow, session);
return session;
}
export function bindWizardNavigation(
link,
path,
session,
browserWindow = window,
) {
if (!link?.setAttribute || !link?.addEventListener) {
throw new TypeError("A wizard navigation link is required.");
}
if (!WIZARD_ROUTES.has(path)) {
throw new TypeError("The wizard navigation route is invalid.");
}
link.setAttribute("href", path);
link.addEventListener("click", (event) => {
const target = link.getAttribute?.("target") || link.target;
const modified =
event.button !== 0 ||
event.altKey ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey;
if (
event.defaultPrevented ||
modified ||
(target && target.toLowerCase() !== "_self") ||
!validWizardSession(session)
) {
return;
}
event.preventDefault();
browserWindow.history.pushState(
{ relmioWizardSession: session },
"",
path,
);
browserWindow.location.reload();
});
}
// The failure a wizard API reported, with the fields error help reads. `recovery`
// is left out on purpose: that table is ChatGPT sign-in advice.
export function wizardApiError(result, status, fallback = "The operation could not be completed.") {
const error = new Error(typeof result?.error === "string" && result.error ? result.error : fallback);
error.status = status;
for (const key of ["code", "param"]) {
if (typeof result?.[key] === "string" && result[key] && result[key].length <= 128) error[key] = result[key];
}
for (const flag of ["remoteOutcomeUnknown", "retryBlocked"]) {
if (result?.[flag] === true) error[flag] = true;
}
return error;
}
const copiedTimers = new WeakMap();
// Copy feedback: marks the button with data-copied for a moment, and the kit
// shows a check. The accessible name never changes; the caller announces the
// copy through its polite status line.
export function flashCopied(button) {
button.toggleAttribute("data-copied", true);
clearTimeout(copiedTimers.get(button));
copiedTimers.set(button, setTimeout(() => button.removeAttribute("data-copied"), 1_800));
}
// Runs a poll tick now, or once when the tab is shown again if it is hidden,
// so a background tab sends no requests.
export function whenVisible(tick, page = document) {
if (!page.hidden) {
tick();
return;
}
page.addEventListener("visibilitychange", function shown() {
if (page.hidden) return;
page.removeEventListener("visibilitychange", shown);
tick();
});
}