window.rpgRollerApi = (() => { const sessionPrefix = "rpgroller."; const stateStream = { source: null, dotNetRef: null, campaignId: null, reconnectDelayMs: 1000, reconnectTimer: null, stopped: true }; function toAppUrl(url) { if (!url || typeof url !== "string") { return url; } if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) { return url; } const relativeUrl = url.startsWith("/") ? url.slice(1) : url; return new URL(relativeUrl, document.baseURI).toString(); } function clearReconnectTimer() { if (stateStream.reconnectTimer) { clearTimeout(stateStream.reconnectTimer); stateStream.reconnectTimer = null; } } function invokeDotNet(method, ...args) { if (!stateStream.dotNetRef) { return; } stateStream.dotNetRef.invokeMethodAsync(method, ...args).catch(() => { }); } function scheduleReconnect() { if (stateStream.stopped || stateStream.reconnectTimer) { return; } const delay = stateStream.reconnectDelayMs; stateStream.reconnectTimer = setTimeout(() => { stateStream.reconnectTimer = null; if (stateStream.stopped) { return; } stateStream.reconnectDelayMs = Math.min(stateStream.reconnectDelayMs * 2, 30000); connectStateStream(); }, delay); } function connectStateStream() { if (stateStream.stopped || !stateStream.campaignId) { return; } clearReconnectTimer(); invokeDotNet("OnConnectionStateChanged", "reconnecting"); const source = new EventSource(toAppUrl(`api/events/state?campaignId=${encodeURIComponent(stateStream.campaignId)}`)); stateStream.source = source; source.onopen = () => { stateStream.reconnectDelayMs = 1000; invokeDotNet("OnConnectionStateChanged", "connected"); }; source.addEventListener("state", (event) => { try { const payload = JSON.parse(event.data); invokeDotNet("OnStateEventReceived", payload); } catch { invokeDotNet("OnStateEventReceived", { campaignId: stateStream.campaignId, totalVersion: 0, rosterVersion: 0, logVersion: 0, characterVersions: [] }); } }); source.onerror = () => { if (stateStream.source === source) { source.close(); stateStream.source = null; } if (stateStream.stopped) { return; } invokeDotNet("OnConnectionStateChanged", "reconnecting"); scheduleReconnect(); }; } function stopStateEvents() { stateStream.stopped = true; clearReconnectTimer(); if (stateStream.source) { stateStream.source.close(); stateStream.source = null; } stateStream.campaignId = null; stateStream.dotNetRef = null; } window.addEventListener("offline", () => { invokeDotNet("OnConnectionStateChanged", "offline"); }); window.addEventListener("online", () => { if (!stateStream.stopped) { connectStateStream(); } }); async function request(method, url, body) { const options = { method, credentials: "same-origin", headers: { Accept: "application/json" } }; if (body !== null && body !== undefined) { options.headers["Content-Type"] = "application/json"; options.body = JSON.stringify(body); } let response; try { response = await fetch(toAppUrl(url), options); } catch (error) { return { ok: false, status: 0, error: "Network error. Check your connection and retry." }; } let parsed = null; const text = await response.text(); if (text) { try { parsed = JSON.parse(text); } catch { parsed = null; } } if (!response.ok) { return { ok: false, status: response.status, error: parsed && typeof parsed.error === "string" ? parsed.error : "Request failed.", code: parsed && typeof parsed.code === "string" ? parsed.code : null }; } return { ok: true, status: response.status, data: parsed }; } function getSessionValue(key) { return sessionStorage.getItem(`${sessionPrefix}${key}`); } function setSessionValue(key, value) { const qualifiedKey = `${sessionPrefix}${key}`; if (value === null || value === undefined || value === "") { sessionStorage.removeItem(qualifiedKey); return; } sessionStorage.setItem(qualifiedKey, value); } function startStateEvents(campaignId, dotNetRef) { stopStateEvents(); stateStream.stopped = false; stateStream.dotNetRef = dotNetRef; stateStream.campaignId = campaignId; stateStream.reconnectDelayMs = 1000; connectStateStream(); } function scrollElementToBottom(element) { if (!element) { return; } element.scrollTop = element.scrollHeight; } function clearInputValue(element) { if (!element) { return; } element.value = ""; } return { request, getSessionValue, setSessionValue, startStateEvents, stopStateEvents, scrollElementToBottom, clearInputValue }; })();