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
122 lines
3.4 KiB
Markdown
122 lines
3.4 KiB
Markdown
# Painter
|
|
|
|
A collaborative pixel canvas inspired by r/place. Users connect in real-time and place colored pixels on a shared 500x500 canvas.
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# Install frontend dependencies and build
|
|
cd painter-js && bun install && bun run build && cd ..
|
|
|
|
# Run the server (serves embedded frontend)
|
|
cargo run --release
|
|
```
|
|
|
|
Open `http://localhost:3000` in your browser.
|
|
|
|
## Configuration
|
|
|
|
All configuration is via environment variables (or `.env` file):
|
|
|
|
| Variable | Default | Description |
|
|
|---|---|---|
|
|
| `ADDRESS` | `0.0.0.0` | Bind address |
|
|
| `PORT` | `3000` | Bind port |
|
|
| `ENABLE_CORS` | `true` | Enable CORS headers |
|
|
| `CANVAS_WIDTH` | `500` | Canvas width in pixels |
|
|
| `CANVAS_HEIGHT` | `500` | Canvas height in pixels |
|
|
| `COOLDOWN_SECS` | `10` | Seconds between pixel placements per user |
|
|
| `RATE_LIMIT_BURST` | `10` | HTTP rate limit burst size |
|
|
| `RATE_LIMIT_PER_SECOND` | `10` | HTTP rate limit per second |
|
|
| `BROADCAST_CAPACITY` | `1024` | Broadcast channel buffer size |
|
|
| `SNAPSHOT_ENABLED` | `true` | Enable periodic canvas snapshots |
|
|
| `SNAPSHOT_INTERVAL_SECS` | `300` | Seconds between snapshots |
|
|
| `SNAPSHOT_MAX` | `5` | Maximum snapshot files to keep |
|
|
| `SNAPSHOT_DIR` | `snapshots/` | Snapshot storage directory |
|
|
|
|
## Transport Adapters
|
|
|
|
The server supports two real-time transport protocols, selected at compile time:
|
|
|
|
```bash
|
|
# Socket.IO (default)
|
|
cargo run --release
|
|
|
|
# Native WebSocket
|
|
cargo run --release --no-default-features --features websocket
|
|
```
|
|
|
|
The frontend auto-detects via `VITE_TRANSPORT` environment variable (`socketio` or `websocket`).
|
|
|
|
## Docker
|
|
|
|
```bash
|
|
# Build
|
|
docker build -t painter .
|
|
|
|
# Run
|
|
docker run -p 3000:3000 painter
|
|
|
|
# With custom config
|
|
docker run -p 3000:3000 \
|
|
-e CANVAS_WIDTH=1000 \
|
|
-e CANVAS_HEIGHT=1000 \
|
|
-e COOLDOWN_SECS=30 \
|
|
-v ./snapshots:/app/snapshots \
|
|
painter
|
|
```
|
|
|
|
The Docker image is a single statically-linked binary on `scratch` — **~3MB** total, no OS, no runtime dependencies. The frontend is embedded at compile time.
|
|
|
|
The server shuts down gracefully on SIGTERM/SIGINT (Ctrl+C) — in-flight connections are drained and a final canvas snapshot is saved before exit.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
# Backend (watches for changes)
|
|
RUST_LOG=debug cargo run
|
|
|
|
# Frontend (Vite dev server with HMR)
|
|
cd painter-js
|
|
VITE_IS_DEBUG=true bun run dev
|
|
|
|
# Run all checks (fmt + clippy + tests)
|
|
make check
|
|
|
|
# Format
|
|
make fmt
|
|
cd painter-js && bun run fmt
|
|
```
|
|
|
|
## Architecture
|
|
|
|
Hexagonal architecture with clean dependency boundaries. See [architecture.mmd](architecture.mmd) for the full diagram.
|
|
|
|
```
|
|
crates/
|
|
config/ Config structs, ConfigSource trait
|
|
domain/ Canvas, value objects, port traits, events
|
|
application/ Use cases, AppState, InProcessBroadcaster
|
|
api-types/ Shared DTOs, event constants
|
|
adapters/
|
|
config-env/ Environment variable config loader
|
|
canvas-file/ File-based snapshot persistence
|
|
http-axum/ HTTP routes, rate limiting, embedded static files
|
|
socketio/ Socket.IO transport adapter
|
|
websocket/ Native WebSocket transport adapter
|
|
server/ Composition root
|
|
```
|
|
|
|
All infrastructure is behind port traits:
|
|
|
|
| Port | Adapter | Swappable to |
|
|
|---|---|---|
|
|
| `CanvasStore` | In-memory | Redis, mmap'd file |
|
|
| `CanvasPersistence` | File system | S3, SQLite |
|
|
| `EventBroadcaster` | tokio broadcast | NATS, Redis pub/sub |
|
|
| `ConfigSource` | Env vars | TOML, JSON, remote API |
|
|
|
|
## License
|
|
|
|
[MIT](LICENSE)
|