Files
RpgRoller/RpgRoller/wwwroot/js/rpgroller-api.js

227 lines
6.0 KiB
JavaScript

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
};
})();