Phase 0: scaffold three-service backend (api/worker/frontend)

api/ (ElysiaJS+Bun WS gateway skeleton), worker/ (Rust, Redis dirty-chunk
stream consumer behind a swappable RenderBackend trait), frontend/
(ElysiaJS+Pug+Tailwind4+Alpine, one server-rendered page) — all three
verified running locally. docker-compose wires them up with postgres,
redis, minio (rendered tile/mesh object storage), and Caddy as reverse
proxy.
This commit is contained in:
2026-08-08 14:10:04 +02:00
commit 4c7cc26281
26 changed files with 1653 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
mod render;
use redis::AsyncCommands;
use render::{CpuRenderBackend, RenderBackend};
const DIRTY_CHUNK_STREAM: &str = "mcmapper:dirty-chunks";
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://redis:6379".into());
let client = redis::Client::open(redis_url)?;
let mut conn = client.get_multiplexed_async_connection().await?;
let backend: Box<dyn RenderBackend> = Box::new(CpuRenderBackend::new());
println!("[worker] render backend: {}", backend.name());
println!("[worker] consuming stream '{DIRTY_CHUNK_STREAM}'");
// Phase 0 skeleton: poll the dirty-chunk stream and log entries. Consumer-group setup,
// actual tile rasterization and chunk meshing land in Phase 1+ (see render::RenderBackend).
loop {
let entries: redis::streams::StreamReadReply = conn
.xread_options(
&[DIRTY_CHUNK_STREAM],
&["$"],
&redis::streams::StreamReadOptions::default().block(5000),
)
.await
.unwrap_or_default();
for key in entries.keys {
for id in key.ids {
println!("[worker] dirty chunk event {}: {:?}", id.id, id.map);
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
use super::RenderBackend;
/// CPU rendering backend, parallelized with rayon. Thread count/task-batch granularity is
/// config-tunable (`auto` via `std::thread::available_parallelism`, or a `server`/`consumer`
/// profile) — wired up once there's an actual render workload to tune (Phase 1+).
pub struct CpuRenderBackend;
impl CpuRenderBackend {
pub fn new() -> Self {
Self
}
}
impl RenderBackend for CpuRenderBackend {
fn name(&self) -> &'static str {
"cpu"
}
}
+14
View File
@@ -0,0 +1,14 @@
mod cpu;
pub use cpu::CpuRenderBackend;
/// Swappable rendering strategy (CPU via rayon / GPU via wgpu / hybrid) selected at startup
/// via the `RENDER_BACKEND` env var. Only the CPU path exists so far; GPU/hybrid land in
/// Phase 8, with automatic fallback to CPU if `gpu`/`hybrid` is requested but no compatible
/// GPU is found.
pub trait RenderBackend: Send + Sync {
fn name(&self) -> &'static str;
// Tile rasterization and chunk meshing methods land alongside their Phase 1/2 callers —
// no point defining signatures for data shapes that don't exist yet.
}