PUBG drop map: live squad map with WebSocket rooms
All checks were successful
Deploy production / deploy (push) Successful in 1m15s

- server.js wraps Bun.serve to add /ws/<channel> upgrades (same server and port) and
  serve the map tiles from the host mount; Dockerfile now runs bun ./server.js
- ws hub (channel registry, origin check) + pubg-map channel: rooms by unguessable
  code, validated + rate-limited messages, per-room write queue, presence
- games.pubg_rooms / pubg_matches / pubg_markers; per-match sessions with history
- /trackers/games/pubg/map hub (owner: rooms, landings over time, history) and
  /trackers/games/pubg/map/<code> fullscreen Leaflet map (all 11 maps)
- deploy-remote.sh bind-mounts ~/pubg-maps; scripts/build-pubg-tiles.py rebuilds it
This commit is contained in:
2026-10-25 22:46:24 +00:00
parent d93374bca2
commit 0fe0d36f87
26 changed files with 39201 additions and 3 deletions

74
server.js Normal file
View File

@@ -0,0 +1,74 @@
/* global Bun */
// Production entry point: `bun ./server.js` instead of `bun ./build`. The adapter-bun build
// starts Bun.serve itself without WebSocket handlers, so this wraps Bun.serve before loading
// it and adds, on the same server and port:
// /ws/<channel> WebSocket upgrades, handled by #lib/server/ws/hub (registered in
// hooks.server.ts init, published on globalThis.__siteWs)
// /pubg-tiles/… the PUBG map tiles, straight from the bind-mounted PUBG_TILES_ROOT
// (immutable, so Cloudflare and browsers cache them; no session lookup)
// Everything else goes to SvelteKit unchanged.
import { resolve, sep } from 'node:path';
const TILE_RE = /^\/pubg-tiles\/([a-z-]{3,20})\/(?:(\d)\/(\d{1,2})\/(\d{1,2})|thumb)\.webp$/;
const tilesRoot = resolve(process.env.PUBG_TILES_ROOT || '/pubg-maps');
function clientIp(request, server) {
const h = request.headers;
return (
h.get('cf-connecting-ip') ||
h.get('x-forwarded-for')?.split(',')[0]?.trim() ||
server.requestIP(request)?.address ||
'unknown'
);
}
async function tile(pathname) {
const m = TILE_RE.exec(pathname);
if (!m) return new Response('Not Found', { status: 404 });
const rel = m[2] === undefined ? `${m[1]}/thumb.webp` : `${m[1]}/${m[2]}/${m[3]}/${m[4]}.webp`;
const path = resolve(tilesRoot, rel);
if (!path.startsWith(tilesRoot + sep)) return new Response('Not Found', { status: 404 });
const file = Bun.file(path);
if (!(await file.exists())) return new Response('Not Found', { status: 404 });
return new Response(file, {
headers: {
'content-type': 'image/webp',
'cache-control': 'public, max-age=31536000, immutable',
'access-control-allow-origin': '*'
}
});
}
const serve = Bun.serve.bind(Bun);
Bun.serve = (options) => {
const app = options.fetch;
options.fetch = async (request, server) => {
const { pathname } = new URL(request.url);
if (pathname.startsWith('/pubg-tiles/')) return tile(pathname);
if (pathname.startsWith('/ws/')) {
const hub = globalThis.__siteWs;
if (!hub) return new Response('Service Unavailable', { status: 503 });
if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket')
return new Response('Upgrade Required', { status: 426 });
const data = await hub.authorize(request, new URL(request.url), clientIp(request, server));
if (data instanceof Response) return data;
if (server.upgrade(request, { data })) return undefined;
return new Response('Upgrade Required', { status: 426 });
}
return app(request, server);
};
options.websocket = {
// Messages are small JSON; Bun pings idle sockets so proxies keep them open.
maxPayloadLength: 16 * 1024,
idleTimeout: 60,
sendPings: true,
backpressureLimit: 1024 * 1024,
closeOnBackpressureLimit: true,
open: (ws) => globalThis.__siteWs?.open(ws),
message: (ws, message) => globalThis.__siteWs?.message(ws, message),
close: (ws) => globalThis.__siteWs?.close(ws)
};
return serve(options);
};
await import('./build/index.js');