v1.0.0 — Hexagonal architecture rewrite
All checks were successful
CI / ci (push) Successful in 7m16s
All checks were successful
CI / ci (push) Successful in 7m16s
Restructure the monolithic 252-line main.rs into a 10-crate workspace with clean hexagonal architecture, swappable adapters, and a production- ready deployment pipeline. Backend architecture: - domain: Canvas, Color/Position/PixelUpdate value objects, port traits (CanvasStore, CanvasPersistence, EventBroadcaster), BroadcastEvent - application: use cases (place_pixel, get_state, save/restore snapshot, connect/disconnect), AppState with Arc snapshot cache - config: AppConfig structs + ConfigSource trait - api-types: shared DTOs, event name constants - adapters: config-env, canvas-file, http-axum (rust-embed), socketio, websocket — all behind port traits, swappable via feature flags - server: composition root with graceful shutdown (SIGTERM/SIGINT) Frontend: - Transport abstraction: Socket.IO and native WebSocket via VITE_TRANSPORT - Canvas zoom/pan with mouse wheel, pinch-to-zoom, and +/- buttons - ImageData rendering (~50x faster than fillRect loop) - Touch support, responsive CSS scaling, mobile-friendly layout - OG/Twitter Card meta tags for rich link previews Production: - Docker: musl static build on scratch — 2.73MB image - CI workflows for Gitea and GitHub Actions (fmt, clippy, test, Docker push) - deploy.sh for private registry - 39 unit tests across domain and application - Zero unwraps, zero unsafe, graceful error handling with tracing - Periodic canvas snapshots with rotation, restored on startup - All config via environment variables with typed defaults
This commit is contained in:
@@ -1,58 +1,88 @@
|
||||
import { connectToWS } from "./socket.js";
|
||||
import "./canvas.js";
|
||||
import "./counter.js";
|
||||
import { updateCountdown } from "./counter.js";
|
||||
import { checkEndpoint, pixelSize } from "./constants.js";
|
||||
import { handleSocketEvents } from "./canvas.js";
|
||||
import "./challenge.js"
|
||||
import { createSocketConnection } from "./infrastructure/socket-client.js";
|
||||
import { checkServer } from "./infrastructure/api.js";
|
||||
import { createCanvasRenderer } from "./ui/canvas-renderer.js";
|
||||
import { createColorPalette } from "./ui/color-palette.js";
|
||||
import { createPixelPlacer } from "./ui/pixel-placer.js";
|
||||
import { startCooldownDisplay } from "./ui/cooldown-display.js";
|
||||
import { createCanvasViewport } from "./ui/canvas-viewport.js";
|
||||
import { setPixel } from "./domain/canvas-state.js";
|
||||
import { getCanvasCoords } from "./domain/coords.js";
|
||||
|
||||
const isDebug = import.meta.env.VITE_IS_DEBUG === "true";
|
||||
|
||||
const currentSoldiersSpan = document.getElementById("current-soldiers");
|
||||
|
||||
let coords = [];
|
||||
const canvas = document.getElementById("canvas");
|
||||
const canvasEl = document.getElementById("canvas");
|
||||
const coordsText = document.getElementById("coords");
|
||||
const ogCanvasStyle = canvas.style.display;
|
||||
canvas.style.display = "none";
|
||||
const currentSoldiersSpan = document.getElementById("current-soldiers");
|
||||
const statusEl = document.getElementById("connection-status");
|
||||
|
||||
fetch(checkEndpoint)
|
||||
const savedDisplay = canvasEl.style.display;
|
||||
canvasEl.style.display = "none";
|
||||
|
||||
let canvasState = [];
|
||||
|
||||
const renderer = createCanvasRenderer(canvasEl);
|
||||
const palette = createColorPalette();
|
||||
|
||||
startCooldownDisplay();
|
||||
createCanvasViewport(canvasEl);
|
||||
|
||||
canvasEl.addEventListener("mousemove", (event) => {
|
||||
const { x, y } = getCanvasCoords(event, canvasEl);
|
||||
coordsText.textContent = `${x}, ${y}`;
|
||||
});
|
||||
|
||||
document.getElementById("save-canvas").addEventListener("click", () => {
|
||||
const a = document.createElement("a");
|
||||
a.href = renderer.toDataURL();
|
||||
a.download = "canvas.png";
|
||||
a.click();
|
||||
});
|
||||
|
||||
const showStatus = (message, isError) => {
|
||||
if (!statusEl) return;
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = isError
|
||||
? "text-red-500 text-sm"
|
||||
: "text-green-500 text-sm";
|
||||
};
|
||||
|
||||
checkServer()
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
const socket = connectToWS();
|
||||
if (!response.ok) throw new Error("Server unavailable");
|
||||
|
||||
socket.on("connect", () => {
|
||||
canvas.style.display = ogCanvasStyle;
|
||||
console.log("connect");
|
||||
});
|
||||
const socket = createSocketConnection();
|
||||
|
||||
socket.on("error", (message) => {
|
||||
alert(message);
|
||||
});
|
||||
socket.on("connect", () => {
|
||||
canvasEl.style.display = savedDisplay;
|
||||
showStatus("Connected", false);
|
||||
});
|
||||
|
||||
socket.on("current_soldiers", (currentSoldiers) => {
|
||||
currentSoldiersSpan.textContent = currentSoldiers;
|
||||
});
|
||||
socket.on("canvas_state", (data) => {
|
||||
canvasState = data;
|
||||
renderer.drawState(data);
|
||||
});
|
||||
|
||||
handleSocketEvents(socket);
|
||||
socket.on("error", (message) => showStatus(message, true));
|
||||
|
||||
requestAnimationFrame(updateCountdown);
|
||||
socket.on("current_soldiers", (count) => {
|
||||
currentSoldiersSpan.textContent = count;
|
||||
});
|
||||
|
||||
window.addEventListener("mousemove", (event) => {
|
||||
// get coordinates of the mouse inside the canvas
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = Math.floor((event.clientX - rect.left) / pixelSize);
|
||||
const y = Math.floor((event.clientY - rect.top) / pixelSize);
|
||||
coords = [x, y];
|
||||
socket.on("pixel-updated", (update) => {
|
||||
renderer.drawPixel(update.x, update.y, update.color);
|
||||
setPixel(canvasState, update.x, update.y, update.color);
|
||||
});
|
||||
|
||||
coordsText.textContent = `${x}, ${y}`;
|
||||
});
|
||||
} else {
|
||||
throw new Error("Can't connect to the server");
|
||||
}
|
||||
socket.on("disconnect", () => {
|
||||
showStatus("Disconnected — reconnecting...", true);
|
||||
});
|
||||
|
||||
createPixelPlacer({
|
||||
canvas: canvasEl,
|
||||
renderer,
|
||||
getColor: palette.getColor,
|
||||
getState: () => canvasState,
|
||||
socket,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(
|
||||
"You have already connected to the server from another tab or window. Please close the other tab or window and refresh this page."
|
||||
);
|
||||
.catch(() => {
|
||||
showStatus("Cannot connect to server", true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user