rkgk/static/session.js
liquidex 5e6b84bed5 cache busting
for faster load times, and seamless updates.
because for some reason ServeDir can't do it correctly, and it tells the client "yeah hey nothing changed" even if something changed
2024-09-04 21:50:30 +02:00

312 lines
8.9 KiB
JavaScript

import { listen } from "rkgk/framework.js";
let loginStorage = JSON.parse(localStorage.getItem("rkgk.login") ?? "{}");
function saveLoginStorage() {
localStorage.setItem("rkgk.login", JSON.stringify(loginStorage));
}
let resolveLoggedInPromise;
let loggedInPromise = new Promise((resolve) => (resolveLoggedInPromise = resolve));
export function isUserLoggedIn() {
return loginStorage.userId != null;
}
export function getUserId() {
return loginStorage.userId;
}
export function getLoginSecret() {
return loginStorage.secret;
}
export function waitForLogin() {
return loggedInPromise;
}
if (isUserLoggedIn()) {
resolveLoggedInPromise();
}
export async function registerUser(nickname) {
try {
let response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ nickname }),
headers: {
"Content-Type": "application/json",
},
});
if (response.status == 500) {
console.error("login service returned 500 status", response);
return {
status: "error",
message:
"We're sorry, but we ran into some trouble registering your account. Please try again.",
};
}
let responseText = await response.text();
let responseJson = JSON.parse(responseText);
if (responseJson.status != "ok") {
console.error("registering user failed", responseJson);
return {
status: "error",
message: "Something seems to have gone wrong. Please try again.",
};
}
loginStorage.userId = responseJson.userId;
loginStorage.secret = responseJson.secret;
console.info("user registered", loginStorage.userId);
saveLoginStorage();
resolveLoggedInPromise();
return { status: "ok" };
} catch (error) {
console.error("registering user failed", error);
return {
status: "error",
message: "Something seems to have gone wrong. Please try again.",
};
}
}
class Session extends EventTarget {
#sentPing = false;
constructor(userId, secret) {
super();
this.userId = userId;
this.secret = secret;
}
async #recvJson() {
let event = await listen([this.ws, "message"]);
if (typeof event.data == "string") {
return JSON.parse(event.data);
} else {
throw new Error("received a binary message where a JSON text message was expected");
}
}
async #recvBinary() {
let event = await listen([this.ws, "message"]);
if (event.data instanceof Blob) {
return event.data;
} else {
throw new Error("received a text message where a binary message was expected");
}
}
#sendJson(object) {
this.ws.send(JSON.stringify(object));
}
#dispatchError(source, kind, message) {
this.dispatchEvent(
Object.assign(new Event("error"), {
source,
errorKind: kind,
message,
}),
);
}
async join(wallId, userInit) {
console.info("joining wall", wallId);
this.wallId = wallId;
this.ws = new WebSocket("/api/wall");
this.ws.addEventListener("error", (event) => {
console.error("WebSocket connection error", error);
this.dispatchEvent(Object.assign(new Event("error"), event));
});
this.ws.addEventListener("message", (event) => {
if (typeof event.data == "string") {
let json = JSON.parse(event.data);
if (json.error != null) {
console.error("received error from server:", json.error);
this.#dispatchError(json, "protocol", json.error);
}
}
});
this.ws.addEventListener("close", (_) => {
console.info("connection closed");
this.dispatchEvent(new Event("disconnect"));
});
try {
await listen([this.ws, "open"]);
await this.joinInner(wallId, userInit);
} catch (error) {
this.#dispatchError(error, "connection", `communication failed: ${error.toString()}`);
}
}
async joinInner(wallId, userInit) {
let secret = this.secret;
this.secret = null;
let version = await this.#recvJson();
console.info("protocol version", version.version);
// TODO: This should probably verify that the version is compatible.
// We don't have a way of sending Rust stuff to JavaScript just yet, so we don't care about it.
let init = {
brush: userInit.brush,
};
if (this.wallId == null) {
this.#sendJson({
user: this.userId,
secret,
init,
});
} else {
this.#sendJson({
user: this.userId,
secret,
wall: wallId,
init,
});
}
let loginResponse = await this.#recvJson();
if (loginResponse.response == "loggedIn") {
this.wallId = loginResponse.wall;
this.wallInfo = loginResponse.wallInfo;
this.sessionId = loginResponse.sessionId;
console.info("logged in", this.wallId, this.sessionId);
console.info("wall info:", this.wallInfo);
} else {
this.#dispatchError(
loginResponse,
loginResponse.response,
"login failed; check error kind for details",
);
return;
}
}
async eventLoop() {
this.#pingLoop();
try {
while (true) {
let event = await listen([this.ws, "message"]);
if (typeof event.data == "string") {
await this.#processNotify(JSON.parse(event.data));
} else {
console.warn("unhandled binary event", event.data);
}
}
} catch (error) {
this.#dispatchError(error, "protocol", `error in event loop: ${error.toString()}`);
}
}
#pingLoop() {
// Send small ping packets every 30 seconds to prevent reverse proxies from reaping the
// connection if the tab is in the background.
// (Browsers don't seem to send standard WebSocket pings if the tab is unfocused.)
// We don't actually use this packet for anything else, like establishing whether
// we still have a connection to the server, because the browser can handle that for us.
setInterval(() => this.sendPing(), 30000);
}
async #processNotify(notify) {
if (notify.notify == "pong") {
console.debug("pong received");
this.#sentPing = false;
}
if (notify.notify == "wall") {
this.dispatchEvent(
Object.assign(new Event("wallEvent"), {
sessionId: notify.sessionId,
wallEvent: notify.wallEvent,
}),
);
}
if (notify.notify == "chunks") {
let chunkData = await this.#recvBinary();
this.dispatchEvent(
Object.assign(new Event("chunks"), {
chunkInfo: notify.chunks,
chunkData,
hasMore: notify.hasMore,
}),
);
}
}
sendPing() {
if (!this.#sentPing) {
console.debug("ping sent; waiting for pong");
this.#sendJson({
request: "ping",
});
this.#sentPing = true;
}
}
sendCursor(x, y) {
this.#sendJson({
request: "wall",
wallEvent: {
event: "cursor",
position: { x, y },
},
});
}
sendPlot(points) {
this.#sendJson({
request: "wall",
wallEvent: {
event: "plot",
points,
},
});
}
sendSetBrush(brush) {
this.#sendJson({
request: "wall",
wallEvent: {
event: "setBrush",
brush,
},
});
}
sendViewport({ left, top, right, bottom }) {
this.#sendJson({
request: "viewport",
topLeft: { x: left, y: top },
bottomRight: { x: right, y: bottom },
});
}
sendMoreChunks() {
this.#sendJson({
request: "moreChunks",
});
}
}
export async function newSession({ userId, secret, wallId, userInit, onError, onDisconnect }) {
let session = new Session(userId, secret);
session.addEventListener("error", onError);
session.addEventListener("disconnect", onDisconnect);
await session.join(wallId, userInit);
return session;
}