Phase 1: chunk store, tile rendering pipeline, and Leaflet viewer
api: WS gateway with token auth (Postgres-backed servers table, seeded via `bun run seed`), column-granularity chunk store (hand-written SQL migrations, no drizzle-kit CLI — its config loader needs esbuild, which doesn't install cleanly here), dirty-chunk Redis stream producer with per-flush dedup, and a tile-serving route. worker: consumes the dirty-chunk stream via a proper consumer group, rasterizes each chunk's columns into a single-resolution top-down PNG (static pre-Flattening block-id palette), uploads to MinIO, and upserts the tile pointer. frontend: barebones Leaflet 2D viewer (CRS.Simple, one native zoom level) wired to /api/servers and /api/tiles. Object storage: tiles live in a dedicated `mcmapper-tiles` bucket on the existing shared MinIO instance (devstack-minio on octo-winsrv) instead of a per-stack container, via a scoped access key limited to that one bucket — see README's "Object storage" section. MINIO_SECRET_KEY is real and is deliberately not committed; docker-compose layers an untracked .env over .env.example for it. Full pipeline verified end-to-end against live containers: WS auth -> Postgres upsert -> deduped Redis dirty-chunk event -> worker rasterize -> MinIO upload -> tile fetch through the api route, including from the actual mod-side WS client (see MCMapper-Mod's matching commit).
This commit is contained in:
@@ -22,13 +22,34 @@ Three independently-deployable services, each its own docker-compose service:
|
||||
browser's own live map/chat/tile traffic talks to `api` directly, not proxied through here.
|
||||
|
||||
Plus `postgres` (source chunk data, accounts, chat history, config, render-artifact metadata
|
||||
pointers), `redis` (dirty-chunk queue, pub/sub, link-code TTLs), `minio` (S3-compatible object
|
||||
storage for the actual rendered tile PNGs and mesh binaries — kept out of Postgres so backups
|
||||
stay free of large binaries and remote `worker` instances have a shared place to write output),
|
||||
and `caddy` (reverse proxy: `/ws` + `/api/*` → `api`, everything else → `frontend`).
|
||||
pointers), `redis` (dirty-chunk queue, pub/sub, link-code TTLs), and `caddy` (reverse proxy:
|
||||
`/ws` + `/api/*` → `api`, everything else → `frontend`).
|
||||
|
||||
Postgres/Redis/MinIO are never exposed publicly — only reachable on the compose network or a
|
||||
private network (VPN/Tailscale/LAN) for remote `worker` instances.
|
||||
Postgres/Redis are never exposed publicly — only reachable on the compose network or a private
|
||||
network (VPN/Tailscale/LAN) for remote `worker` instances.
|
||||
|
||||
### Object storage
|
||||
|
||||
Rendered tile PNGs and mesh binaries live in a `mcmapper-tiles` bucket on an existing shared
|
||||
MinIO instance (`devstack-minio` on octo-winsrv) rather than a per-stack `minio` container —
|
||||
kept out of Postgres so backups stay free of large binaries, and any `worker` instance (local or
|
||||
remote) has a shared place to write output. `api`/`worker` authenticate with a dedicated
|
||||
`mcmapper` access key whose policy (`mcmapper-tiles-rw`) only grants
|
||||
`GetObject`/`PutObject`/`DeleteObject`/`ListBucket` on that one bucket — it can't see or touch
|
||||
anything else on the shared instance. Provisioned via:
|
||||
|
||||
```
|
||||
mc mb myminio/mcmapper-tiles
|
||||
mc admin policy create myminio mcmapper-tiles-rw mcmapper-policy.json # see git history for the policy JSON
|
||||
mc admin user add myminio mcmapper <generated secret>
|
||||
mc admin policy attach myminio mcmapper-tiles-rw --user mcmapper
|
||||
```
|
||||
|
||||
`api/.env.example`/`worker/.env.example` point `MINIO_ENDPOINT`/`MINIO_PORT` at that instance's
|
||||
LAN address; swap to the public gateway (`s3.octoturge.com:443`, `MINIO_USE_SSL=true`) if a
|
||||
stack isn't on the same LAN. `MINIO_SECRET_KEY` is a real credential and is deliberately **not**
|
||||
committed — set it in an untracked `./api/.env` / `./worker/.env` (docker-compose layers those on
|
||||
top of the tracked `.env.example`, see `docker-compose.yml`).
|
||||
|
||||
## Running
|
||||
|
||||
@@ -36,8 +57,24 @@ private network (VPN/Tailscale/LAN) for remote `worker` instances.
|
||||
docker compose up
|
||||
```
|
||||
|
||||
`api` on :3000, `frontend` on :3001, both behind Caddy on :80. First boot pulls/builds all
|
||||
images; `worker` starts polling an empty Redis stream until Phase 1 gives it something to do.
|
||||
`api` on :3000, `frontend` on :3001, both behind Caddy on :80. `api` applies its Postgres
|
||||
migrations on startup (see `api/src/db/migrate.ts`); `worker` consumes the `mcmapper:dirty-chunks`
|
||||
Redis stream via a consumer group (`mcmapper-workers`) so multiple instances split work safely.
|
||||
|
||||
### Phase 1: connecting a mod instance
|
||||
|
||||
There's no admin registration API yet (Phase 6) — seed one server row by hand, then point the
|
||||
mod's `MapperConfig` at the same token:
|
||||
|
||||
```
|
||||
docker compose run --rm api bun run seed
|
||||
```
|
||||
|
||||
reads `MCMAPPER_SEED_SERVER_NAME`/`MCMAPPER_SEED_SERVER_TOKEN` from `api/.env.example` (edit
|
||||
those first, or override with `-e`). Once the mod connects and sends its initial chunk backfill,
|
||||
tiles appear at `GET /api/tiles/:serverId/:dimension/:zoom/:tileX/:tileY.png` (zoom is always `0`
|
||||
for now — see `worker/src/render/cpu.rs`) and the frontend's Leaflet viewer picks them up
|
||||
automatically from `GET /api/servers`.
|
||||
|
||||
## Attribution
|
||||
|
||||
|
||||
+17
-3
@@ -1,7 +1,21 @@
|
||||
PORT=3000
|
||||
DATABASE_URL=postgres://mcmapper:mcmapper@postgres:5432/mcmapper
|
||||
REDIS_URL=redis://redis:6379
|
||||
MINIO_ENDPOINT=minio
|
||||
MINIO_PORT=9000
|
||||
|
||||
# Tiles/meshes live in a dedicated `mcmapper-tiles` bucket on a shared MinIO instance rather
|
||||
# than a per-stack container — see README.md's "Object storage" section for how the bucket and
|
||||
# this scoped access key (read/write on mcmapper-tiles only, nothing else) were provisioned.
|
||||
# 192.168.0.3:28205 is that instance's LAN address; swap for the public gateway
|
||||
# (MINIO_ENDPOINT=s3.octoturge.com, MINIO_PORT=443, MINIO_USE_SSL=true) if this stack isn't on
|
||||
# the same LAN. MINIO_SECRET_KEY is real and must NOT be committed — set it in an untracked
|
||||
# `.env` next to this file (docker-compose layers it on top, see docker-compose.yml).
|
||||
MINIO_ENDPOINT=192.168.0.3
|
||||
MINIO_PORT=28205
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY=mcmapper
|
||||
MINIO_SECRET_KEY=mcmapper-dev-only
|
||||
MINIO_SECRET_KEY=changeme-set-in-untracked-env
|
||||
|
||||
# Only read by `bun run seed` (see scripts/seed-server.ts) — Phase 6 replaces this with a real
|
||||
# admin registration API. Point the mod's MapperConfig#serverToken at the same value.
|
||||
MCMAPPER_SEED_SERVER_NAME=dev-server
|
||||
MCMAPPER_SEED_SERVER_TOKEN=changeme
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ RUN bun install
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["bun", "run", "start"]
|
||||
CMD ["sh", "-c", "bun run migrate && bun run start"]
|
||||
|
||||
+265
@@ -6,10 +6,15 @@
|
||||
"name": "@mcmapper/api",
|
||||
"dependencies": {
|
||||
"cookie": "^2.0.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"elysia": "^1.1.0",
|
||||
"ioredis": "^6.0.0",
|
||||
"minio": "^8.0.7",
|
||||
"postgres": "^3.4.9",
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"typescript": "^5.5.0",
|
||||
},
|
||||
},
|
||||
@@ -17,6 +22,68 @@
|
||||
"packages": {
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||
|
||||
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@2.0.0", "", {}, "sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg=="],
|
||||
|
||||
"@nodable/entities": ["@nodable/entities@3.0.0", "", {}, "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.52", "", {}, "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||
@@ -25,38 +92,236 @@
|
||||
|
||||
"@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
|
||||
|
||||
"anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="],
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="],
|
||||
|
||||
"browser-or-node": ["browser-or-node@2.1.1", "", {}, "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg=="],
|
||||
|
||||
"buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
|
||||
|
||||
"cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
|
||||
|
||||
"elysia": ["elysia@1.4.29", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-GwMRGGwSdjfPt+w3LA0fqTuYJtS8uVRJicvoar98/HrO5qdFKDc9CwjIb6Kja+v39lkY+58hr2JvdR9jQzlUuA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"fast-xml-builder": ["fast-xml-builder@1.3.0", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@5.10.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw=="],
|
||||
|
||||
"file-type": ["file-type@22.0.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA=="],
|
||||
|
||||
"filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.14.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ioredis": ["ioredis@6.0.0", "", { "dependencies": { "@ioredis/commands": "2.0.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "standard-as-callback": "2.1.0" } }, "sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@2.5.0", "", {}, "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w=="],
|
||||
|
||||
"is-unsafe": ["is-unsafe@2.0.0", "", {}, "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA=="],
|
||||
|
||||
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||
|
||||
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"minio": ["minio@8.0.7", "", { "dependencies": { "async": "^3.2.4", "block-stream2": "^2.1.0", "browser-or-node": "^2.1.1", "buffer-crc32": "^1.0.0", "eventemitter3": "^5.0.1", "fast-xml-parser": "^5.3.4", "ipaddr.js": "^2.0.1", "lodash": "^4.17.21", "mime-types": "^2.1.35", "query-string": "^7.1.3", "stream-json": "^1.8.0", "through2": "^4.0.2", "xml2js": "^0.5.0 || ^0.6.2" } }, "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="],
|
||||
|
||||
"postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="],
|
||||
|
||||
"query-string": ["query-string@7.1.3", "", { "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"stream-chain": ["stream-chain@2.2.5", "", {}, "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA=="],
|
||||
|
||||
"stream-json": ["stream-json@1.9.1", "", { "dependencies": { "stream-chain": "^2.2.5" } }, "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw=="],
|
||||
|
||||
"strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strnum": ["strnum@2.4.1", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
|
||||
|
||||
"through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="],
|
||||
|
||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||
|
||||
"tsx": ["tsx@4.23.11", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="],
|
||||
|
||||
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"elysia/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
CREATE TABLE IF NOT EXISTS "servers" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"token" text NOT NULL UNIQUE,
|
||||
"auth_mode" text NOT NULL DEFAULT 'offline',
|
||||
"created_at" timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "chunk_columns" (
|
||||
"server_id" uuid NOT NULL REFERENCES "servers"("id") ON DELETE CASCADE,
|
||||
"dimension" integer NOT NULL,
|
||||
"x" integer NOT NULL,
|
||||
"z" integer NOT NULL,
|
||||
"block_id" integer NOT NULL,
|
||||
"block_meta" integer NOT NULL,
|
||||
"height" smallint NOT NULL,
|
||||
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("server_id", "dimension", "x", "z")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "tile_pointers" (
|
||||
"server_id" uuid NOT NULL REFERENCES "servers"("id") ON DELETE CASCADE,
|
||||
"dimension" integer NOT NULL,
|
||||
"zoom" integer NOT NULL DEFAULT 0,
|
||||
"tile_x" integer NOT NULL,
|
||||
"tile_z" integer NOT NULL,
|
||||
"storage_key" text NOT NULL,
|
||||
"content_hash" text NOT NULL,
|
||||
"rendered_at" timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("server_id", "dimension", "zoom", "tile_x", "tile_z")
|
||||
);
|
||||
|
||||
-- Chunk-granularity lookup: a live delta touches individual columns, but the worker re-renders
|
||||
-- a whole chunk's 16x16 columns at once, keyed by floor(x/16), floor(z/16).
|
||||
CREATE INDEX IF NOT EXISTS "chunk_columns_chunk_idx" ON "chunk_columns" (
|
||||
"server_id", "dimension", (floor("x" / 16.0)), (floor("z" / 16.0))
|
||||
);
|
||||
+9
-2
@@ -5,14 +5,21 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.ts",
|
||||
"start": "bun run src/index.ts"
|
||||
"start": "bun run src/index.ts",
|
||||
"migrate": "bun run src/db/migrate.ts",
|
||||
"seed": "bun run scripts/seed-server.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": "^2.0.1",
|
||||
"elysia": "^1.1.0"
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"elysia": "^1.1.0",
|
||||
"ioredis": "^6.0.0",
|
||||
"minio": "^8.0.7",
|
||||
"postgres": "^3.4.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { db } from "../src/db/client";
|
||||
import { servers } from "../src/db/schema";
|
||||
import { sql } from "../src/db/client";
|
||||
|
||||
// Phase 6 will add a real admin registration API/UI. Until then, Phase 1 needs at least one
|
||||
// server row to hand a token to the mod — run with MCMAPPER_SEED_SERVER_NAME/_TOKEN set.
|
||||
async function main() {
|
||||
const name = process.env.MCMAPPER_SEED_SERVER_NAME;
|
||||
const token = process.env.MCMAPPER_SEED_SERVER_TOKEN;
|
||||
const authMode = process.env.MCMAPPER_SEED_SERVER_AUTH_MODE ?? "offline";
|
||||
|
||||
if (!name || !token) {
|
||||
console.error("[seed] set MCMAPPER_SEED_SERVER_NAME and MCMAPPER_SEED_SERVER_TOKEN");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [row] = await db
|
||||
.insert(servers)
|
||||
.values({ name, token, authMode })
|
||||
.onConflictDoUpdate({ target: servers.token, set: { name, authMode } })
|
||||
.returning();
|
||||
|
||||
console.log(`[seed] server ready: id=${row.id} name=${row.name}`);
|
||||
await sql.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("[seed] failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import postgres from "postgres";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const sql = postgres(process.env.DATABASE_URL!);
|
||||
export const db = drizzle(sql, { schema });
|
||||
export { sql };
|
||||
@@ -0,0 +1,33 @@
|
||||
import { readdirSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { sql } from "./client";
|
||||
|
||||
// Hand-written SQL migrations (see drizzle/*.sql) applied in filename order, tracked in a
|
||||
// `_migrations` table. No drizzle-kit CLI involved — its config loader needs esbuild, which
|
||||
// doesn't install cleanly in this environment; three tables don't need a migration framework.
|
||||
async function main() {
|
||||
await sql`CREATE TABLE IF NOT EXISTS "_migrations" ("name" text PRIMARY KEY, "applied_at" timestamptz NOT NULL DEFAULT now())`;
|
||||
|
||||
const dir = join(import.meta.dir, "..", "..", "drizzle");
|
||||
const files = readdirSync(dir).filter((f) => f.endsWith(".sql")).sort();
|
||||
|
||||
for (const file of files) {
|
||||
const [{ exists }] = await sql`SELECT EXISTS(SELECT 1 FROM "_migrations" WHERE "name" = ${file})`;
|
||||
if (exists) {
|
||||
console.log(`[migrate] skipping already-applied ${file}`);
|
||||
continue;
|
||||
}
|
||||
console.log(`[migrate] applying ${file}`);
|
||||
const contents = readFileSync(join(dir, file), "utf-8");
|
||||
await sql.unsafe(contents);
|
||||
await sql`INSERT INTO "_migrations" ("name") VALUES (${file})`;
|
||||
}
|
||||
|
||||
console.log("[migrate] up to date");
|
||||
await sql.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("[migrate] failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { pgTable, uuid, text, integer, smallint, timestamp, primaryKey } from "drizzle-orm/pg-core";
|
||||
|
||||
// One row per registered MC server. Phase 1 has no admin registration API yet (Phase 6) —
|
||||
// rows are created by `bun run seed` from MCMAPPER_SEED_SERVER_NAME/_TOKEN env vars.
|
||||
export const servers = pgTable("servers", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
authMode: text("auth_mode").notNull().default("offline"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// Column-granularity world state: the topmost non-air block per (dimension, x, z), plus its
|
||||
// height. This is deliberately not full per-voxel storage — Phase 1 only needs enough to
|
||||
// rasterize a top-down 2D tile and to derive marker Y later. Full block/section data for 3D
|
||||
// meshing is a Phase 2 extension of this table, not built now (see plan's phased scope).
|
||||
export const chunkColumns = pgTable(
|
||||
"chunk_columns",
|
||||
{
|
||||
serverId: uuid("server_id")
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: "cascade" }),
|
||||
dimension: integer("dimension").notNull(),
|
||||
x: integer("x").notNull(),
|
||||
z: integer("z").notNull(),
|
||||
blockId: integer("block_id").notNull(),
|
||||
blockMeta: integer("block_meta").notNull(),
|
||||
height: smallint("height").notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.serverId, table.dimension, table.x, table.z] })],
|
||||
);
|
||||
|
||||
// Metadata pointer to a rendered tile PNG in MinIO — the binary itself never touches Postgres.
|
||||
// One row per (server, dimension, zoom, tileX, tileZ); zoom is always 0 until Phase 2 adds
|
||||
// multi-resolution tiles.
|
||||
export const tilePointers = pgTable(
|
||||
"tile_pointers",
|
||||
{
|
||||
serverId: uuid("server_id")
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: "cascade" }),
|
||||
dimension: integer("dimension").notNull(),
|
||||
zoom: integer("zoom").notNull().default(0),
|
||||
tileX: integer("tile_x").notNull(),
|
||||
tileZ: integer("tile_z").notNull(),
|
||||
storageKey: text("storage_key").notNull(),
|
||||
contentHash: text("content_hash").notNull(),
|
||||
renderedAt: timestamp("rendered_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.serverId, table.dimension, table.zoom, table.tileX, table.tileZ] }),
|
||||
],
|
||||
);
|
||||
+48
-12
@@ -1,20 +1,56 @@
|
||||
import { Elysia } from "elysia";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { db } from "./db/client";
|
||||
import { servers, tilePointers } from "./db/schema";
|
||||
import { wsGateway } from "./ws-gateway";
|
||||
import { minio, TILE_BUCKET, ensureTileBucket } from "./minio";
|
||||
|
||||
await ensureTileBucket();
|
||||
|
||||
// Phase 0 skeleton: health check + a bare WS gateway endpoint that mod instances will
|
||||
// eventually authenticate against with a per-server token (see plan's "WS Gateway" module).
|
||||
// Delta/chat/link-request routing lands in Phase 1+.
|
||||
const app = new Elysia()
|
||||
.get("/health", () => ({ status: "ok" }))
|
||||
.get("/api/servers", async () => {
|
||||
const rows = await db.select({ id: servers.id, name: servers.name }).from(servers);
|
||||
return rows;
|
||||
})
|
||||
// :zoom is Leaflet's zoom level (Phase 1 only ever renders zoom 0 — see worker — but the
|
||||
// frontend's tileLayer always requests a zoom, so the route needs the segment even though it's
|
||||
// constant for now). :tileX is chunkX; :tileY is `-chunkZ` (the frontend's CRS.Simple setup
|
||||
// flips Z so north renders up — see frontend/src/public/js/map.js) and gets negated back here.
|
||||
.get("/api/tiles/:serverId/:dimension/:zoom/:tileX/:tileY", async ({ params, set }) => {
|
||||
const serverId = params.serverId;
|
||||
const dimension = Number(params.dimension);
|
||||
const zoom = Number(params.zoom);
|
||||
const tileX = Number(params.tileX);
|
||||
const tileZ = -Number(params.tileY.replace(/\.png$/, ""));
|
||||
|
||||
const [pointer] = await db
|
||||
.select()
|
||||
.from(tilePointers)
|
||||
.where(
|
||||
and(
|
||||
eq(tilePointers.serverId, serverId),
|
||||
eq(tilePointers.dimension, dimension),
|
||||
eq(tilePointers.zoom, zoom),
|
||||
eq(tilePointers.tileX, tileX),
|
||||
eq(tilePointers.tileZ, tileZ),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!pointer) {
|
||||
set.status = 404;
|
||||
return { error: "tile_not_rendered" };
|
||||
}
|
||||
|
||||
const stream = await minio.getObject(TILE_BUCKET, pointer.storageKey);
|
||||
set.headers["content-type"] = "image/png";
|
||||
return new Response(stream as any);
|
||||
})
|
||||
.ws("/ws", {
|
||||
open(ws) {
|
||||
console.log(`[ws] connection opened: ${ws.id}`);
|
||||
},
|
||||
message(ws, message) {
|
||||
console.log(`[ws] message from ${ws.id}:`, message);
|
||||
},
|
||||
close(ws) {
|
||||
console.log(`[ws] connection closed: ${ws.id}`);
|
||||
},
|
||||
open: wsGateway.open,
|
||||
message: wsGateway.message,
|
||||
close: wsGateway.close,
|
||||
})
|
||||
.listen(Number(process.env.PORT ?? 3000));
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Client } from "minio";
|
||||
|
||||
export const TILE_BUCKET = "mcmapper-tiles";
|
||||
|
||||
export const minio = new Client({
|
||||
endPoint: process.env.MINIO_ENDPOINT ?? "minio",
|
||||
port: Number(process.env.MINIO_PORT ?? 9000),
|
||||
useSSL: process.env.MINIO_USE_SSL === "true",
|
||||
accessKey: process.env.MINIO_ACCESS_KEY ?? "mcmapper",
|
||||
secretKey: process.env.MINIO_SECRET_KEY ?? "mcmapper-dev-only",
|
||||
});
|
||||
|
||||
export async function ensureTileBucket() {
|
||||
const exists = await minio.bucketExists(TILE_BUCKET).catch(() => false);
|
||||
if (!exists) {
|
||||
await minio.makeBucket(TILE_BUCKET);
|
||||
console.log(`[minio] created bucket ${TILE_BUCKET}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
export const DIRTY_CHUNK_STREAM = "mcmapper:dirty-chunks";
|
||||
|
||||
export const redis = new Redis(process.env.REDIS_URL ?? "redis://redis:6379");
|
||||
|
||||
export async function markChunkDirty(serverId: string, dimension: number, chunkX: number, chunkZ: number) {
|
||||
await redis.xadd(
|
||||
DIRTY_CHUNK_STREAM,
|
||||
"*",
|
||||
"serverId",
|
||||
serverId,
|
||||
"dimension",
|
||||
String(dimension),
|
||||
"chunkX",
|
||||
String(chunkX),
|
||||
"chunkZ",
|
||||
String(chunkZ),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "./db/client";
|
||||
import { chunkColumns, servers } from "./db/schema";
|
||||
import { markChunkDirty } from "./redis";
|
||||
|
||||
// Wire protocol (mod <-> api), one JSON object per WS text frame:
|
||||
//
|
||||
// mod -> api {"type":"hello","token":"<serverToken>"}
|
||||
// api -> mod {"type":"hello_ack","ok":true,"serverId":"<uuid>"}
|
||||
// {"type":"hello_ack","ok":false,"error":"..."} (connection closed after)
|
||||
//
|
||||
// mod -> api {"type":"columns","dimension":0,"columns":[{"x":..,"z":..,"height":..,"blockId":..,"blockMeta":..}]}
|
||||
//
|
||||
// `columns` doubles as both initial backfill (one message per loaded chunk, 256 columns) and
|
||||
// live deltas (one message per flush tick, just the columns that changed) — both are just "here
|
||||
// is the current topmost block + height for these XZ columns", the mod recomputes it from its
|
||||
// own world access rather than the api trying to infer a post-break top block from a raw diff.
|
||||
// Full per-voxel data (needed for Phase 2 3D meshing) is a natural extension of this same
|
||||
// connection once the chunk store grows a full block-data column.
|
||||
|
||||
interface Column {
|
||||
x: number;
|
||||
z: number;
|
||||
height: number;
|
||||
blockId: number;
|
||||
blockMeta: number;
|
||||
}
|
||||
|
||||
interface ConnState {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
const connections = new Map<string | number, ConnState>();
|
||||
|
||||
function chunkOf(coord: number): number {
|
||||
return Math.floor(coord / 16);
|
||||
}
|
||||
|
||||
export const wsGateway = {
|
||||
async open() {
|
||||
// Nothing to do until `hello` arrives — see message().
|
||||
},
|
||||
|
||||
async message(ws: any, raw: unknown) {
|
||||
let msg: any;
|
||||
try {
|
||||
msg = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||
} catch {
|
||||
ws.send(JSON.stringify({ type: "error", error: "invalid_json" }));
|
||||
return;
|
||||
}
|
||||
|
||||
const state = connections.get(ws.id);
|
||||
|
||||
if (msg.type === "hello") {
|
||||
const [row] = await db.select().from(servers).where(eq(servers.token, msg.token)).limit(1);
|
||||
if (!row) {
|
||||
ws.send(JSON.stringify({ type: "hello_ack", ok: false, error: "invalid_token" }));
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
connections.set(ws.id, { serverId: row.id });
|
||||
ws.send(JSON.stringify({ type: "hello_ack", ok: true, serverId: row.id }));
|
||||
console.log(`[ws] server '${row.name}' (${row.id}) authenticated`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
ws.send(JSON.stringify({ type: "error", error: "not_authenticated" }));
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "columns") {
|
||||
const dimension: number = msg.dimension;
|
||||
const columns: Column[] = msg.columns ?? [];
|
||||
if (columns.length === 0) return;
|
||||
|
||||
await db
|
||||
.insert(chunkColumns)
|
||||
.values(
|
||||
columns.map((c) => ({
|
||||
serverId: state.serverId,
|
||||
dimension,
|
||||
x: c.x,
|
||||
z: c.z,
|
||||
blockId: c.blockId,
|
||||
blockMeta: c.blockMeta,
|
||||
height: c.height,
|
||||
})),
|
||||
)
|
||||
.onConflictDoUpdate({
|
||||
target: [chunkColumns.serverId, chunkColumns.dimension, chunkColumns.x, chunkColumns.z],
|
||||
set: {
|
||||
blockId: sqlExcluded("block_id"),
|
||||
blockMeta: sqlExcluded("block_meta"),
|
||||
height: sqlExcluded("height"),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const dirtyChunks = new Set<string>();
|
||||
for (const c of columns) dirtyChunks.add(`${chunkOf(c.x)},${chunkOf(c.z)}`);
|
||||
for (const key of dirtyChunks) {
|
||||
const [cx, cz] = key.split(",").map(Number);
|
||||
await markChunkDirty(state.serverId, dimension, cx, cz);
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
close(ws: any) {
|
||||
connections.delete(ws.id);
|
||||
},
|
||||
};
|
||||
|
||||
// Small helper: drizzle's onConflictDoUpdate `set` needs a raw `excluded.<col>` reference for
|
||||
// "use the value that would have been inserted" — drizzle-orm doesn't expose a typed helper for
|
||||
// this on postgres-js yet, so build the sql fragment directly.
|
||||
import { sql } from "drizzle-orm";
|
||||
function sqlExcluded(column: string) {
|
||||
return sql.raw(`excluded.${column}`);
|
||||
}
|
||||
+14
-17
@@ -1,11 +1,15 @@
|
||||
services:
|
||||
api:
|
||||
build: ./api
|
||||
env_file: ./api/.env.example
|
||||
# .env.example holds safe defaults (committed); an untracked ./api/.env layered on top
|
||||
# supplies real secrets (MINIO_SECRET_KEY) — see api/.env.example.
|
||||
env_file:
|
||||
- ./api/.env.example
|
||||
- path: ./api/.env
|
||||
required: false
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- minio
|
||||
expose:
|
||||
- "3000"
|
||||
restart: unless-stopped
|
||||
@@ -21,11 +25,13 @@ services:
|
||||
|
||||
worker:
|
||||
build: ./worker
|
||||
env_file: ./worker/.env.example
|
||||
env_file:
|
||||
- ./worker/.env.example
|
||||
- path: ./worker/.env
|
||||
required: false
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- minio
|
||||
restart: unless-stopped
|
||||
# Scale render capacity locally with, e.g.: docker compose up --scale worker=3
|
||||
# A `worker` can also run standalone on a separate machine, pointed at the same
|
||||
@@ -50,18 +56,10 @@ services:
|
||||
restart: unless-stopped
|
||||
# Same rule as postgres: no public ports.
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: mcmapper
|
||||
MINIO_ROOT_PASSWORD: mcmapper-dev-only
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
restart: unless-stopped
|
||||
# Same rule as postgres/redis: no public ports in production. The console (9001) and
|
||||
# S3 API (9000) are reachable on the compose network for api/worker; expose to your
|
||||
# private network only, same as postgres/redis, if you need the console remotely.
|
||||
# No `minio` service here: rendered tiles/meshes are stored in a dedicated bucket
|
||||
# (`mcmapper-tiles`) on an existing shared MinIO instance instead of a per-stack container —
|
||||
# see api/.env.example and worker/.env.example for the endpoint/credentials, and README.md
|
||||
# for how that bucket + scoped access key were provisioned.
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
@@ -79,5 +77,4 @@ services:
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
minio-data:
|
||||
caddy-data:
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"@types/pug": "^2.0.10",
|
||||
"bun-types": "latest",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.5.0",
|
||||
@@ -103,6 +104,8 @@
|
||||
|
||||
"@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
|
||||
|
||||
"@types/pug": ["@types/pug@2.0.10", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="],
|
||||
|
||||
"acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="],
|
||||
|
||||
"asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
"pug": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"@types/pug": "^2.0.10",
|
||||
"bun-types": "latest",
|
||||
"typescript": "^5.5.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"@tailwindcss/cli": "^4.0.0"
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { Elysia } from "elysia";
|
||||
import pug from "pug";
|
||||
import { join } from "path";
|
||||
|
||||
// Phase 0 skeleton: one server-rendered page. The real map viewer (Leaflet 2D + Babylon 3D
|
||||
// canvases, server selector, chat box, marker tool) lands starting Phase 1 — this just proves
|
||||
// the Pug+Tailwind+Alpine rendering pipeline end to end.
|
||||
// Phase 1: a barebones Leaflet 2D viewer (see public/js/map.js). Babylon 3D canvas, chat box,
|
||||
// marker tool, and admin panel land in later phases per the plan's phased delivery.
|
||||
const renderIndex = pug.compileFile(join(import.meta.dir, "views/index.pug"));
|
||||
|
||||
const app = new Elysia()
|
||||
.get("/", () => new Response(renderIndex({}), { headers: { "Content-Type": "text/html" } }))
|
||||
.get("/health", () => ({ status: "ok" }))
|
||||
.get("/css/tailwind.css", () => Bun.file(join(import.meta.dir, "public/css/tailwind.css")))
|
||||
.get("/js/map.js", () => Bun.file(join(import.meta.dir, "public/js/map.js")))
|
||||
.listen(Number(process.env.PORT ?? 3001));
|
||||
|
||||
console.log(`[frontend] listening on :${app.server?.port}`);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Barebones Leaflet viewer (Phase 1). No auth/marker/chat UI yet — those are Phase 3/4.
|
||||
//
|
||||
// Tiles are one Minecraft chunk (16x16 blocks) each, upscaled to 256px, at a single native
|
||||
// zoom level (see api's tile route + worker/src/render/cpu.rs) — Leaflet stretches that one
|
||||
// native zoom to whatever zoom the user picks via `maxNativeZoom`/`minNativeZoom`.
|
||||
//
|
||||
// Coordinate mapping: Leaflet's CRS.Simple treats [lat, lng] as [y, x] with y growing upward
|
||||
// on screen. Minecraft's Z grows south (visually "down" on a conventional north-up map), so
|
||||
// tile y = -chunkZ here; the api negates it back to chunkZ when looking up the tile pointer
|
||||
// (see api/src/index.ts's /api/tiles route comment).
|
||||
function mapmapper() {
|
||||
return {
|
||||
loading: true,
|
||||
server: null,
|
||||
leaflet: null,
|
||||
|
||||
async init() {
|
||||
const servers = await fetch("/api/servers").then((r) => r.json());
|
||||
this.loading = false;
|
||||
this.server = servers[0] ?? null;
|
||||
|
||||
this.leaflet = L.map("map", { crs: L.CRS.Simple, minZoom: -4, maxZoom: 6 });
|
||||
this.leaflet.setView([0, 0], 0);
|
||||
|
||||
if (this.server) {
|
||||
L.tileLayer(`/api/tiles/${this.server.id}/0/{z}/{x}/{y}.png`, {
|
||||
tileSize: 256,
|
||||
minNativeZoom: 0,
|
||||
maxNativeZoom: 0,
|
||||
noWrap: true,
|
||||
errorTileUrl:
|
||||
"data:image/svg+xml;base64," +
|
||||
btoa('<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256"><rect width="256" height="256" fill="#1e1e28"/></svg>'),
|
||||
}).addTo(this.leaflet);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -5,8 +5,19 @@ html(lang="en")
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1")
|
||||
title MCMapper
|
||||
link(rel="stylesheet" href="/css/tailwind.css")
|
||||
link(rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css")
|
||||
script(defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js")
|
||||
body.bg-neutral-900.text-neutral-100.min-h-screen.flex.items-center.justify-center
|
||||
div(x-data="{ ready: true }")
|
||||
h1.text-2xl.font-semibold MCMapper
|
||||
p.text-neutral-400(x-show="ready") Backend scaffold is up. Map viewer lands in Phase 1.
|
||||
script(src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js")
|
||||
style.
|
||||
html, body, #map { height: 100%; margin: 0; }
|
||||
body.bg-neutral-900.text-neutral-100
|
||||
div#app.h-screen.flex.flex-col(x-data="mapmapper()" x-init="init()")
|
||||
header.px-4.py-2.flex.items-center.gap-4.border-b.border-neutral-700
|
||||
h1.text-lg.font-semibold MCMapper
|
||||
span.text-sm.text-neutral-400(x-show="!loading && server")
|
||||
| Viewing:
|
||||
span(x-text="server?.name")
|
||||
span.text-sm.text-neutral-500(x-show="loading") loading servers…
|
||||
span.text-sm.text-red-400(x-show="!loading && !server") No server registered yet — see backend README (bun run seed).
|
||||
div#map.flex-1
|
||||
script(src="/js/map.js")
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["bun-types"]
|
||||
"types": ["bun-types", "pug"]
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -1,8 +1,13 @@
|
||||
DATABASE_URL=postgres://mcmapper:mcmapper@postgres:5432/mcmapper
|
||||
REDIS_URL=redis://redis:6379
|
||||
MINIO_ENDPOINT=minio
|
||||
MINIO_PORT=9000
|
||||
|
||||
# See api/.env.example for what this points at and why — same shared MinIO instance/bucket,
|
||||
# same rule: MINIO_SECRET_KEY is real, set it in an untracked `.env` next to this file, never here.
|
||||
MINIO_ENDPOINT=192.168.0.3
|
||||
MINIO_PORT=28205
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY=mcmapper
|
||||
MINIO_SECRET_KEY=mcmapper-dev-only
|
||||
MINIO_SECRET_KEY=changeme-set-in-untracked-env
|
||||
|
||||
# cpu | gpu | hybrid — see render::backend. Only `cpu` exists so far (Phase 8 adds gpu/hybrid).
|
||||
RENDER_BACKEND=cpu
|
||||
|
||||
Generated
+2625
-9
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,12 @@ tokio = { version = "1", features = ["full"] }
|
||||
redis = { version = "0.27", features = ["tokio-comp"] }
|
||||
rayon = "1.10"
|
||||
anyhow = "1"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid"] }
|
||||
uuid = { version = "1", features = ["serde"] }
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
aws-sdk-s3 = "1"
|
||||
aws-config = "1"
|
||||
aws-credential-types = "1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn connect(database_url: &str) -> anyhow::Result<PgPool> {
|
||||
Ok(PgPoolOptions::new().max_connections(5).connect(database_url).await?)
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct StoredColumn {
|
||||
pub x: i32,
|
||||
pub z: i32,
|
||||
pub block_id: i32,
|
||||
pub block_meta: i32,
|
||||
}
|
||||
|
||||
pub async fn fetch_chunk_columns(
|
||||
pool: &PgPool,
|
||||
server_id: Uuid,
|
||||
dimension: i32,
|
||||
chunk_x: i32,
|
||||
chunk_z: i32,
|
||||
) -> anyhow::Result<Vec<StoredColumn>> {
|
||||
let min_x = chunk_x * 16;
|
||||
let min_z = chunk_z * 16;
|
||||
let rows = sqlx::query_as::<_, StoredColumn>(
|
||||
r#"SELECT x, z, block_id, block_meta FROM chunk_columns
|
||||
WHERE server_id = $1 AND dimension = $2
|
||||
AND x >= $3 AND x < $3 + 16
|
||||
AND z >= $4 AND z < $4 + 16"#,
|
||||
)
|
||||
.bind(server_id)
|
||||
.bind(dimension)
|
||||
.bind(min_x)
|
||||
.bind(min_z)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn upsert_tile_pointer(
|
||||
pool: &PgPool,
|
||||
server_id: Uuid,
|
||||
dimension: i32,
|
||||
zoom: i32,
|
||||
tile_x: i32,
|
||||
tile_z: i32,
|
||||
storage_key: &str,
|
||||
content_hash: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query(
|
||||
r#"INSERT INTO tile_pointers (server_id, dimension, zoom, tile_x, tile_z, storage_key, content_hash, rendered_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, now())
|
||||
ON CONFLICT (server_id, dimension, zoom, tile_x, tile_z)
|
||||
DO UPDATE SET storage_key = excluded.storage_key, content_hash = excluded.content_hash, rendered_at = now()"#,
|
||||
)
|
||||
.bind(server_id)
|
||||
.bind(dimension)
|
||||
.bind(zoom)
|
||||
.bind(tile_x)
|
||||
.bind(tile_z)
|
||||
.bind(storage_key)
|
||||
.bind(content_hash)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
+116
-15
@@ -1,36 +1,137 @@
|
||||
mod db;
|
||||
mod palette;
|
||||
mod render;
|
||||
mod storage;
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use redis::streams::{StreamReadOptions, StreamReadReply};
|
||||
use redis::AsyncCommands;
|
||||
use render::{CpuRenderBackend, RenderBackend};
|
||||
use render::{ColumnPixel, CpuRenderBackend, RenderBackend};
|
||||
use uuid::Uuid;
|
||||
|
||||
const DIRTY_CHUNK_STREAM: &str = "mcmapper:dirty-chunks";
|
||||
const CONSUMER_GROUP: &str = "mcmapper-workers";
|
||||
|
||||
#[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 database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
let minio_scheme = if std::env::var("MINIO_USE_SSL").as_deref() == Ok("true") {
|
||||
"https"
|
||||
} else {
|
||||
"http"
|
||||
};
|
||||
let minio_endpoint = format!(
|
||||
"{minio_scheme}://{}:{}",
|
||||
std::env::var("MINIO_ENDPOINT").unwrap_or_else(|_| "minio".into()),
|
||||
std::env::var("MINIO_PORT").unwrap_or_else(|_| "9000".into()),
|
||||
);
|
||||
let minio_access_key = std::env::var("MINIO_ACCESS_KEY").unwrap_or_else(|_| "mcmapper".into());
|
||||
let minio_secret_key =
|
||||
std::env::var("MINIO_SECRET_KEY").unwrap_or_else(|_| "mcmapper-dev-only".into());
|
||||
let consumer_name =
|
||||
std::env::var("HOSTNAME").unwrap_or_else(|_| format!("worker-{}", std::process::id()));
|
||||
|
||||
let pg_pool = db::connect(&database_url).await?;
|
||||
let s3_client = storage::connect(&minio_endpoint, &minio_access_key, &minio_secret_key);
|
||||
storage::ensure_bucket(&s3_client).await?;
|
||||
|
||||
let redis_client = redis::Client::open(redis_url)?;
|
||||
let mut conn = redis_client.get_multiplexed_async_connection().await?;
|
||||
|
||||
// Idempotent: BUSYGROUP just means another worker instance already created it.
|
||||
let _: redis::RedisResult<()> = conn
|
||||
.xgroup_create_mkstream(DIRTY_CHUNK_STREAM, CONSUMER_GROUP, "$")
|
||||
.await;
|
||||
|
||||
// Only `cpu` exists so far — `RENDER_BACKEND=gpu`/`hybrid` fall back with a warning until
|
||||
// Phase 8 adds the wgpu path.
|
||||
let requested_backend = std::env::var("RENDER_BACKEND").unwrap_or_else(|_| "cpu".into());
|
||||
if requested_backend != "cpu" {
|
||||
eprintln!(
|
||||
"[worker] RENDER_BACKEND={requested_backend} not implemented yet, falling back to cpu"
|
||||
);
|
||||
}
|
||||
let backend: Box<dyn RenderBackend> = Box::new(CpuRenderBackend::new());
|
||||
println!("[worker] render backend: {}", backend.name());
|
||||
println!("[worker] consuming stream '{DIRTY_CHUNK_STREAM}'");
|
||||
println!("[worker] consuming stream '{DIRTY_CHUNK_STREAM}' as '{consumer_name}'");
|
||||
|
||||
// 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),
|
||||
)
|
||||
let opts = StreamReadOptions::default()
|
||||
.group(CONSUMER_GROUP, &consumer_name)
|
||||
.count(10)
|
||||
.block(5000);
|
||||
|
||||
let reply: StreamReadReply = conn
|
||||
.xread_options(&[DIRTY_CHUNK_STREAM], &[">"], &opts)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
for key in entries.keys {
|
||||
for id in key.ids {
|
||||
println!("[worker] dirty chunk event {}: {:?}", id.id, id.map);
|
||||
for key in reply.keys {
|
||||
for entry in key.ids {
|
||||
if let Err(err) = process_entry(&pg_pool, &s3_client, backend.as_ref(), &entry).await {
|
||||
eprintln!("[worker] failed to process {}: {err:#}", entry.id);
|
||||
continue;
|
||||
}
|
||||
let _: redis::RedisResult<()> =
|
||||
conn.xack(DIRTY_CHUNK_STREAM, CONSUMER_GROUP, &[&entry.id]).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn field(entry: &redis::streams::StreamId, name: &str) -> anyhow::Result<String> {
|
||||
match entry.map.get(name) {
|
||||
Some(redis::Value::BulkString(bytes)) => Ok(String::from_utf8(bytes.clone())?),
|
||||
Some(other) => Ok(redis::from_redis_value::<String>(other)?),
|
||||
None => anyhow::bail!("missing field '{name}'"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_entry(
|
||||
pool: &sqlx::PgPool,
|
||||
s3_client: &aws_sdk_s3::Client,
|
||||
backend: &dyn RenderBackend,
|
||||
entry: &redis::streams::StreamId,
|
||||
) -> anyhow::Result<()> {
|
||||
let server_id: Uuid = field(entry, "serverId")?.parse()?;
|
||||
let dimension: i32 = field(entry, "dimension")?.parse()?;
|
||||
let chunk_x: i32 = field(entry, "chunkX")?.parse()?;
|
||||
let chunk_z: i32 = field(entry, "chunkZ")?.parse()?;
|
||||
|
||||
let columns = db::fetch_chunk_columns(pool, server_id, dimension, chunk_x, chunk_z).await?;
|
||||
let pixels: Vec<ColumnPixel> = columns
|
||||
.iter()
|
||||
.map(|c| ColumnPixel {
|
||||
local_x: (c.x - chunk_x * 16) as u8,
|
||||
local_z: (c.z - chunk_z * 16) as u8,
|
||||
block_id: c.block_id as u16,
|
||||
block_meta: c.block_meta as u8,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let png_bytes = backend.rasterize_tile(&pixels)?;
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
png_bytes.hash(&mut hasher);
|
||||
let content_hash = format!("{:x}", hasher.finish());
|
||||
|
||||
let storage_key = format!("{server_id}/{dimension}/0/{chunk_x}/{chunk_z}.png");
|
||||
storage::put_tile(s3_client, &storage_key, png_bytes).await?;
|
||||
db::upsert_tile_pointer(
|
||||
pool,
|
||||
server_id,
|
||||
dimension,
|
||||
0,
|
||||
chunk_x,
|
||||
chunk_z,
|
||||
&storage_key,
|
||||
&content_hash,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("[worker] rendered tile {storage_key} ({} columns)", pixels.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/// Static numeric-block-id -> RGB color table for pre-Flattening worlds (1.7.10/1.12.2), where
|
||||
/// a block's full identity is `(id, meta)` — see the mod's ChunkAdapter comment on why
|
||||
/// `blockStateId` for those leaves is just `(id << 4) | meta` rather than a dynamic palette.
|
||||
/// Covers common overworld terrain/ore blocks; anything unmapped falls back to a visually
|
||||
/// obvious placeholder so gaps are easy to spot while iterating rather than silently blending in.
|
||||
const UNKNOWN_COLOR: [u8; 3] = [204, 102, 204];
|
||||
const AIR_COLOR: [u8; 3] = [30, 30, 40];
|
||||
|
||||
pub fn color_for(block_id: u16, meta: u8) -> [u8; 3] {
|
||||
match block_id {
|
||||
0 => AIR_COLOR,
|
||||
1 => [125, 125, 125], // stone
|
||||
2 => [95, 159, 53], // grass block
|
||||
3 => [134, 96, 67], // dirt
|
||||
4 => [122, 122, 122], // cobblestone
|
||||
5 => match meta {
|
||||
1 => [166, 128, 78], // spruce planks
|
||||
2 => [196, 179, 123], // birch planks
|
||||
3 => [170, 122, 79], // jungle planks
|
||||
_ => [162, 130, 78], // oak planks
|
||||
},
|
||||
7 => [40, 40, 40], // bedrock
|
||||
8 | 9 => [63, 118, 228], // water (flowing/still)
|
||||
10 | 11 => [207, 92, 32], // lava (flowing/still)
|
||||
12 => [219, 211, 160], // sand
|
||||
13 => [136, 126, 126], // gravel
|
||||
14 => [252, 238, 75], // gold ore
|
||||
15 => [216, 175, 147], // iron ore
|
||||
16 => [77, 77, 77], // coal ore
|
||||
17 | 162 => [92, 68, 41], // logs
|
||||
18 | 161 => [60, 100, 40], // leaves
|
||||
20 => [220, 236, 240], // glass
|
||||
24 => [219, 207, 163], // sandstone
|
||||
35 => wool_color(meta),
|
||||
41 => [246, 238, 92], // gold block
|
||||
42 => [220, 220, 220], // iron block
|
||||
45 => [151, 96, 90], // bricks
|
||||
48 => [90, 108, 90], // mossy cobblestone
|
||||
49 => [24, 20, 36], // obsidian
|
||||
56 => [141, 209, 202], // diamond ore
|
||||
73 | 74 => [132, 32, 32], // redstone ore
|
||||
78 => [240, 250, 255], // snow layer
|
||||
79 => [140, 180, 230], // ice
|
||||
80 => [248, 248, 248], // snow block
|
||||
82 => [160, 164, 177], // clay
|
||||
86 => [200, 128, 32], // pumpkin
|
||||
87 => [110, 54, 48], // netherrack
|
||||
88 => [84, 64, 51], // soul sand
|
||||
89 => [186, 148, 92], // glowstone
|
||||
110 => [92, 84, 108], // mycelium
|
||||
121 => [221, 223, 165], // end stone
|
||||
123 | 124 => [171, 129, 85], // nether wart block-ish glow (placeholder)
|
||||
129 => [79, 195, 161], // emerald ore
|
||||
133 => [46, 190, 120], // emerald block
|
||||
159 => [200, 130, 100], // stained clay (approx, ignores meta)
|
||||
_ => UNKNOWN_COLOR,
|
||||
}
|
||||
}
|
||||
|
||||
fn wool_color(meta: u8) -> [u8; 3] {
|
||||
match meta {
|
||||
0 => [233, 236, 236],
|
||||
1 => [240, 118, 19],
|
||||
2 => [189, 68, 179],
|
||||
3 => [107, 138, 201],
|
||||
4 => [194, 173, 24],
|
||||
5 => [65, 174, 56],
|
||||
6 => [208, 132, 153],
|
||||
7 => [64, 64, 64],
|
||||
8 => [154, 161, 161],
|
||||
9 => [46, 110, 137],
|
||||
10 => [126, 61, 181],
|
||||
11 => [46, 56, 141],
|
||||
12 => [79, 50, 31],
|
||||
13 => [53, 70, 27],
|
||||
14 => [150, 52, 48],
|
||||
15 => [25, 22, 22],
|
||||
_ => UNKNOWN_COLOR,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,20 @@
|
||||
use super::RenderBackend;
|
||||
use image::{ImageEncoder, Rgb, RgbImage};
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::{ColumnPixel, RenderBackend};
|
||||
use crate::palette::color_for;
|
||||
|
||||
/// Native tile resolution (one pixel per block within a chunk) before upscaling for display.
|
||||
const CHUNK_SIZE: u32 = 16;
|
||||
/// Upscale factor applied so tiles are a reasonable size for a Leaflet `tileSize: 256` layer —
|
||||
/// Phase 1 has one native zoom level (see api's tile route), so this is purely cosmetic, not a
|
||||
/// multi-resolution pyramid (that's Phase 2's job).
|
||||
const UPSCALE: u32 = 16;
|
||||
|
||||
/// 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+).
|
||||
/// profile) — wired up once there's a multi-tile batch workload to tune across (Phase 7);
|
||||
/// a single 16x16 tile is too small for rayon to help within itself.
|
||||
pub struct CpuRenderBackend;
|
||||
|
||||
impl CpuRenderBackend {
|
||||
@@ -15,4 +27,32 @@ impl RenderBackend for CpuRenderBackend {
|
||||
fn name(&self) -> &'static str {
|
||||
"cpu"
|
||||
}
|
||||
|
||||
fn rasterize_tile(&self, columns: &[ColumnPixel]) -> anyhow::Result<Vec<u8>> {
|
||||
let mut base = RgbImage::from_pixel(CHUNK_SIZE, CHUNK_SIZE, Rgb([30, 30, 40]));
|
||||
for col in columns {
|
||||
if col.local_x as u32 >= CHUNK_SIZE || col.local_z as u32 >= CHUNK_SIZE {
|
||||
continue;
|
||||
}
|
||||
let color = color_for(col.block_id, col.block_meta);
|
||||
base.put_pixel(col.local_x as u32, col.local_z as u32, Rgb(color));
|
||||
}
|
||||
|
||||
let mut upscaled = RgbImage::new(CHUNK_SIZE * UPSCALE, CHUNK_SIZE * UPSCALE);
|
||||
for y in 0..upscaled.height() {
|
||||
for x in 0..upscaled.width() {
|
||||
let src = base.get_pixel(x / UPSCALE, y / UPSCALE);
|
||||
upscaled.put_pixel(x, y, *src);
|
||||
}
|
||||
}
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
image::codecs::png::PngEncoder::new(&mut Cursor::new(&mut bytes)).write_image(
|
||||
upscaled.as_raw(),
|
||||
upscaled.width(),
|
||||
upscaled.height(),
|
||||
image::ExtendedColorType::Rgb8,
|
||||
)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,14 @@ mod cpu;
|
||||
|
||||
pub use cpu::CpuRenderBackend;
|
||||
|
||||
/// One rendered column within a chunk, in chunk-local coordinates (0..16).
|
||||
pub struct ColumnPixel {
|
||||
pub local_x: u8,
|
||||
pub local_z: u8,
|
||||
pub block_id: u16,
|
||||
pub block_meta: u8,
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -9,6 +17,7 @@ pub use cpu::CpuRenderBackend;
|
||||
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.
|
||||
/// Rasterize one chunk's worth of columns (up to 256, sparse if the chunk isn't fully
|
||||
/// synced yet) into a single-resolution top-down PNG tile, returned as encoded bytes.
|
||||
fn rasterize_tile(&self, columns: &[ColumnPixel]) -> anyhow::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use aws_sdk_s3::config::{BehaviorVersion, Builder, Credentials, Region};
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use aws_sdk_s3::Client;
|
||||
|
||||
pub const TILE_BUCKET: &str = "mcmapper-tiles";
|
||||
|
||||
pub fn connect(endpoint: &str, access_key: &str, secret_key: &str) -> Client {
|
||||
let creds = Credentials::new(access_key, secret_key, None, None, "mcmapper-worker");
|
||||
let config = Builder::new()
|
||||
.region(Region::new("us-east-1"))
|
||||
.endpoint_url(endpoint)
|
||||
.credentials_provider(creds)
|
||||
.force_path_style(true)
|
||||
.behavior_version(BehaviorVersion::latest())
|
||||
.build();
|
||||
Client::from_conf(config)
|
||||
}
|
||||
|
||||
/// Bucket creation is normally the `api` service's job (see api/src/minio.ts) — this only
|
||||
/// covers the case where `worker` starts before `api` has had a chance to.
|
||||
pub async fn ensure_bucket(client: &Client) -> anyhow::Result<()> {
|
||||
if client.head_bucket().bucket(TILE_BUCKET).send().await.is_err() {
|
||||
client.create_bucket().bucket(TILE_BUCKET).send().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn put_tile(client: &Client, key: &str, bytes: Vec<u8>) -> anyhow::Result<()> {
|
||||
client
|
||||
.put_object()
|
||||
.bucket(TILE_BUCKET)
|
||||
.key(key)
|
||||
.content_type("image/png")
|
||||
.body(ByteStream::from(bytes))
|
||||
.send()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user