rkgk/static/session.js

302 lines
8.7 KiB
JavaScript
Raw Normal View History

import { listen } from "rkgk/framework.js";
2024-08-10 23:13:20 +02:00
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;
}
2024-08-10 23:13:20 +02:00
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;
2024-08-10 23:13:20 +02:00
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) {
2024-08-10 23:13:20 +02:00
super();
this.userId = userId;
this.secret = secret;
2024-08-10 23:13:20 +02:00
}
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");
}
}
2024-08-15 20:01:23 +02:00
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");
}
}
2024-08-10 23:13:20 +02:00
#sendJson(object) {
this.ws.send(JSON.stringify(object));
}
#dispatchError(source, kind, message) {
this.dispatchEvent(
Object.assign(new Event("error"), {
source,
errorKind: kind,
message,
}),
);
}
2024-08-15 20:01:23 +02:00
async join(wallId, userInit) {
2024-08-10 23:13:20 +02:00
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"));
});
2024-08-10 23:13:20 +02:00
try {
await listen([this.ws, "open"]);
2024-08-15 20:01:23 +02:00
await this.joinInner(wallId, userInit);
2024-08-10 23:13:20 +02:00
} catch (error) {
this.#dispatchError(error, "connection", `communication failed: ${error.toString()}`);
}
}
2024-08-15 20:01:23 +02:00
async joinInner(wallId, userInit) {
let secret = this.secret;
this.secret = null;
2024-08-10 23:13:20 +02:00
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.
2024-08-15 20:01:23 +02:00
let init = {
brush: userInit.brush,
};
2024-08-10 23:13:20 +02:00
if (this.wallId == null) {
2024-08-15 20:01:23 +02:00
this.#sendJson({
user: this.userId,
secret,
2024-08-15 20:01:23 +02:00
init,
});
2024-08-10 23:13:20 +02:00
} else {
2024-08-15 20:01:23 +02:00
this.#sendJson({
user: this.userId,
secret,
2024-08-15 20:01:23 +02:00
wall: wallId,
init,
});
2024-08-10 23:13:20 +02:00
}
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();
2024-08-10 23:13:20 +02:00
try {
while (true) {
let event = await listen([this.ws, "message"]);
if (typeof event.data == "string") {
2024-08-15 20:01:23 +02:00
await this.#processNotify(JSON.parse(event.data));
2024-08-10 23:13:20 +02:00
} else {
2024-08-15 20:01:23 +02:00
console.warn("unhandled binary event", event.data);
2024-08-10 23:13:20 +02:00
}
}
} 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);
}
2024-08-15 20:01:23 +02:00
async #processNotify(notify) {
if (notify.notify == "pong") {
console.debug("pong received");
this.#sentPing = false;
}
2024-08-15 20:01:23 +02:00
if (notify.notify == "wall") {
2024-08-10 23:13:20 +02:00
this.dispatchEvent(
2024-08-15 20:01:23 +02:00
Object.assign(new Event("wallEvent"), {
sessionId: notify.sessionId,
wallEvent: notify.wallEvent,
2024-08-10 23:13:20 +02:00
}),
);
}
2024-08-15 20:01:23 +02:00
if (notify.notify == "chunks") {
let chunkData = await this.#recvBinary();
this.dispatchEvent(
Object.assign(new Event("chunks"), {
chunkInfo: notify.chunks,
chunkData,
hasMore: notify.hasMore,
2024-08-15 20:01:23 +02:00
}),
);
}
}
sendPing() {
if (!this.#sentPing) {
console.debug("ping sent; waiting for pong");
this.#sendJson({
request: "ping",
});
this.#sentPing = true;
}
}
2024-08-15 20:01:23 +02:00
sendCursor(x, y) {
this.#sendJson({
request: "wall",
wallEvent: {
event: "cursor",
position: { x, y },
},
});
}
sendInteraction(interactions) {
2024-08-15 20:01:23 +02:00
this.#sendJson({
request: "wall",
wallEvent: {
event: "interact",
interactions,
2024-08-15 20:01:23 +02:00
},
});
}
sendViewport({ left, top, right, bottom }) {
this.#sendJson({
request: "viewport",
topLeft: { x: left, y: top },
bottomRight: { x: right, y: bottom },
});
2024-08-10 23:13:20 +02:00
}
2024-08-15 20:01:23 +02:00
sendMoreChunks() {
2024-08-10 23:13:20 +02:00
this.#sendJson({
2024-08-15 20:01:23 +02:00
request: "moreChunks",
2024-08-10 23:13:20 +02:00
});
}
}
export async function newSession({ userId, secret, wallId, userInit, onError, onDisconnect }) {
let session = new Session(userId, secret);
session.addEventListener("error", onError);
session.addEventListener("disconnect", onDisconnect);
2024-08-15 20:01:23 +02:00
await session.join(wallId, userInit);
2024-08-10 23:13:20 +02:00
return session;
}