2024-09-04 21:48:42 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-08-23 19:45:07 +02:00
|
|
|
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;
|
2024-08-23 19:45:07 +02:00
|
|
|
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 {
|
2024-09-02 23:17:00 +02:00
|
|
|
#sentPing = false;
|
|
|
|
|
2024-08-23 19:45:07 +02:00
|
|
|
constructor(userId, secret) {
|
2024-08-10 23:13:20 +02:00
|
|
|
super();
|
|
|
|
this.userId = userId;
|
2024-08-23 19:45:07 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-08-25 12:53:53 +02:00
|
|
|
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) {
|
2024-08-23 19:45:07 +02:00
|
|
|
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,
|
2024-08-23 19:45:07 +02:00
|
|
|
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,
|
2024-08-23 19:45:07 +02:00
|
|
|
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() {
|
2024-09-02 23:17:00 +02:00
|
|
|
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()}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-02 23:17:00 +02:00
|
|
|
#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) {
|
2024-09-02 23:17:00 +02:00
|
|
|
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,
|
2024-08-17 22:16:31 +02:00
|
|
|
hasMore: notify.hasMore,
|
2024-08-15 20:01:23 +02:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-02 23:17:00 +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 },
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-09-08 13:53:29 +02:00
|
|
|
sendInteraction(interactions) {
|
|
|
|
console.log(interactions);
|
2024-08-15 20:01:23 +02:00
|
|
|
this.#sendJson({
|
|
|
|
request: "wall",
|
|
|
|
wallEvent: {
|
2024-09-08 13:53:29 +02:00
|
|
|
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
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-25 12:53:53 +02:00
|
|
|
export async function newSession({ userId, secret, wallId, userInit, onError, onDisconnect }) {
|
2024-08-23 19:45:07 +02:00
|
|
|
let session = new Session(userId, secret);
|
2024-08-25 12:53:53 +02:00
|
|
|
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;
|
|
|
|
}
|