UNPKG

programming-game

Version:

The client for programming game, an mmorpg that you interact with entirely through code.

173 lines (162 loc) 4.83 kB
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(); }; };