227 lines
6.0 KiB
JavaScript
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
|
|
};
|
|
})();
|