programming-game
Version:
The client for programming game, an mmorpg that you interact with entirely through code.
173 lines (162 loc) • 4.83 kB
text/typescript
import { BatchedEvents, OnTick } from "./types";
import { version } from "./package.json";
import { BaseClient, OnEvent } from "./base-client";
type ConnectProps = {
credentials: {
id: string;
key: string;
};
onTick: OnTick;
onEvent?: OnEvent;
/**
* Controls how often onTick is called when there's no new data from the server.
*/
tickInterval?: number;
};
class InnerSocket {
private socket: WebSocket;
private closed: boolean = false;
private handlers = new Map<string, Set<(data: any) => void>>();
private credentials: ConnectProps["credentials"];
private reconnecting: boolean = false;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
constructor(credentials: ConnectProps["credentials"]) {
this.credentials = credentials;
this.socket = this.reconnect();
}
private reconnect() {
console.log("reconnect call...");
const socket = new WebSocket(
process.env.NODE_ENV === "development"
? "ws://localhost:3001"
: `wss://programming-game.com`
);
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnecting = false;
}
this.reconnecting = true;
socket.addEventListener("open", () => {
this.reconnecting = false;
console.log("connected to server!");
socket.send(
JSON.stringify({
type: "credentials",
value: this.credentials,
version,
})
);
});
const attemptReconnect = () => {
if (this.closed) return;
if (this.reconnecting) return;
this.reconnecting = true;
this.reconnectTimer = setTimeout(() => {
console.log("attempting to reconnect...");
this.socket = this.reconnect();
}, 1000);
};
socket.addEventListener("error", (event) => {
this.reconnecting = false;
console.log("WebSocket error...", Date.now());
attemptReconnect();
});
// @ts-ignore
socket.addEventListener("close", (e, b) => {
console.log("WebSocket closed", e, b);
this.triggerHandlers("close", null);
attemptReconnect();
});
socket.addEventListener("message", (event) => {
try {
const data = JSON.parse(event.data.toString());
switch (data.type) {
case "version": {
return this.triggerHandlers("version", data.value);
}
case "events": {
return this.triggerHandlers("events", data.value);
}
default: {
console.error("Unknown message type:", data.type);
}
}
} catch (e) {
console.log("Error parsing message:", e);
}
});
return socket;
}
private triggerHandlers(event: string, data: any) {
this.handlers.get(event)?.forEach((handler) => {
handler(data);
});
}
emit(event: string, data: any) {
this.socket.send(JSON.stringify({ type: event, value: data }));
}
on(event: "close", callback: () => void): void;
on(event: "version", callback: (version: string) => void): void;
on(event: "events", callback: (events: BatchedEvents) => void): void;
on(event: string, callback: (data: any) => void) {
let set = this.handlers.get(event) || new Set<(data: any) => void>();
if (!this.handlers.has(event)) {
this.handlers.set(event, set);
}
set.add(callback);
}
close() {
this.closed = true;
this.socket.close();
}
}
export const connect = ({
credentials,
onTick,
onEvent,
tickInterval = 300,
}: ConnectProps): (() => void) => {
if (typeof WebSocket === "undefined") {
throw new Error(
"No global WebSocket, please upgrade to Node 22, play in the browser, or polyfill w/ a compatible library."
);
}
const socket = new InnerSocket(credentials);
const versionHandler = (serverVersion: string) => {
const clientVersion = version;
if (serverVersion !== clientVersion) {
const lines: string[] = [];
lines.push("There's an updated version of programming-game.");
lines.push(
`Your version: ${clientVersion}, latest version: ${serverVersion}`
);
console.log("\n" + lines.join("\n") + "\n");
} else {
console.log(
`\nYou're on the latest version of programming-game, enjoy!.\n`
);
}
};
socket.on("version", versionHandler);
const baseClient = new BaseClient({
onTick,
onEvent,
setIntent: (intent) => {
socket.emit("setIntent", intent);
},
tickInterval,
});
// @ts-ignore
socket.on("error", (error) => {
console.log("socket error", error);
});
socket.on("events", baseClient.eventsHandler.bind(baseClient));
// @ts-ignore
socket.on("close", (error) => {
baseClient.clearState();
});
return () => {
socket.close();
baseClient.clearState();
};
};