// run: deno run --allow-net main.ts
// ─────────────────────────────────────────────
-const PORT = 8080;
+const PORT = Number(Deno.env.get("PORT")) ?? 8080;
const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
const CHUNK_SIZE = 16 * 1024; // 16KB DataChannel chunks
function generateSessionId(): string {
let id: string;
- do { id = generateId(4) + "-" + generateId(4); }
- while (sessions.has(id));
+ do {
+ id = generateId(4) + "-" + generateId(4);
+ } while (sessions.has(id));
return id;
}
const enc = new TextEncoder();
-function broadcast(session: Session, fromPeerId: string, event: string, data: unknown) {
- const payload = enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
+function broadcast(
+ session: Session,
+ fromPeerId: string,
+ event: string,
+ data: unknown,
+) {
+ const payload = enc.encode(
+ `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`,
+ );
for (const [pid, peer] of session.peers) {
if (pid !== fromPeerId) {
try {
}
}
-function sendToPeer(session: Session, toPeerId: string, event: string, data: unknown) {
+function sendToPeer(
+ session: Session,
+ toPeerId: string,
+ event: string,
+ data: unknown,
+) {
const peer = session.peers.get(toPeerId);
if (!peer) return;
try {
- peer.controller.enqueue(enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
+ peer.controller.enqueue(
+ enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`),
+ );
peer.lastSeen = Date.now();
} catch (_) {
// peer disconnected
function sendToSelf(peer: Peer, event: string, data: unknown) {
try {
- peer.controller.enqueue(enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
+ peer.controller.enqueue(
+ enc.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`),
+ );
} catch (_) { /* ignore */ }
}
for (const [id, session] of sessions) {
if (now - session.lastActivity > SESSION_TTL_MS) {
for (const peer of session.peers.values()) {
- try { peer.controller.close(); } catch (_) { /* ignore */ }
+ try {
+ peer.controller.close();
+ } catch (_) { /* ignore */ }
}
sessions.delete(id);
}
const rlStore = new Map<string, { count: number; reset: number }>();
-function rateLimit(ip: string, key: string, max: number, windowMs = 60_000): boolean {
+function rateLimit(
+ ip: string,
+ key: string,
+ max: number,
+ windowMs = 60_000,
+): boolean {
const k = ip + ":" + key;
const now = Date.now();
const e = rlStore.get(k);
- if (!e || now > e.reset) { rlStore.set(k, { count: 1, reset: now + windowMs }); return true; }
+ if (!e || now > e.reset) {
+ rlStore.set(k, { count: 1, reset: now + windowMs });
+ return true;
+ }
if (e.count >= max) return false;
e.count++;
return true;
// POST /session → create new session
if (req.method === "POST" && path === "/session") {
- if (!rateLimit(ip, "session-create", 10)) return json({ error: "Too many requests" }, 429);
+ if (!rateLimit(ip, "session-create", 10)) {
+ return json({ error: "Too many requests" }, 429);
+ }
const session = createSession();
return json({ sessionId: session.id });
}
// GET /session/:id/exists → check if session exists
if (req.method === "GET" && path.match(/^\/session\/[^/]+\/exists$/)) {
- if (!rateLimit(ip, "session-check", 30)) return json({ error: "Too many requests" }, 429);
+ if (!rateLimit(ip, "session-check", 30)) {
+ return json({ error: "Too many requests" }, 429);
+ }
const sessionId = path.split("/")[2];
return json({ exists: sessions.has(sessionId) });
}
// GET /connect/:sessionId → SSE stream, join session
if (req.method === "GET" && path.match(/^\/connect\/[^/]+$/)) {
- if (!rateLimit(ip, "connect", 10)) return json({ error: "Too many requests" }, 429);
+ if (!rateLimit(ip, "connect", 10)) {
+ return json({ error: "Too many requests" }, 429);
+ }
const sessionId = path.split("/")[2];
const session = sessions.get(sessionId);
if (!session) {
let controller!: ReadableStreamDefaultController;
let heartbeat: ReturnType<typeof setInterval>;
const stream = new ReadableStream({
- start(c) { controller = c; },
- cancel() { clearInterval(heartbeat); removePeer(session!, peerId); },
+ start(c) {
+ controller = c;
+ },
+ cancel() {
+ clearInterval(heartbeat);
+ removePeer(session!, peerId);
+ },
});
const peer: Peer = { id: peerId, controller, lastSeen: Date.now() };
touch(session);
heartbeat = setInterval(() => {
- try { peer.controller.enqueue(enc.encode("event: heartbeat\ndata: {}\n\n")); }
- catch (_) { clearInterval(heartbeat); }
+ try {
+ peer.controller.enqueue(enc.encode("event: heartbeat\ndata: {}\n\n"));
+ } catch (_) {
+ clearInterval(heartbeat);
+ }
}, 30_000);
// Send welcome
setTimeout(() => {
- sendToSelf(peer, "welcome", { peerId, sessionId, isHost, chunkSize: CHUNK_SIZE });
+ sendToSelf(peer, "welcome", {
+ peerId,
+ sessionId,
+ isHost,
+ chunkSize: CHUNK_SIZE,
+ });
if (!isHost) {
broadcast(session!, peerId, "peer-joined", { peerId });
}
// POST /signal/:sessionId → WebRTC signaling relay
if (req.method === "POST" && path.match(/^\/signal\/[^/]+$/)) {
- if (!rateLimit(ip, "signal", 200)) return json({ error: "Too many requests" }, 429);
+ if (!rateLimit(ip, "signal", 200)) {
+ return json({ error: "Too many requests" }, 429);
+ }
const sessionId = path.split("/")[2];
const session = sessions.get(sessionId);
if (!session) return json({ error: "Session not found" }, 404);
// ── Start ────────────────────────────────────
console.log(`drop. running → http://localhost:${PORT}`);
-Deno.serve({ port: PORT }, (req, info) => handler(req, (info.remoteAddr as Deno.NetAddr).hostname));
+Deno.serve(
+ { port: PORT },
+ (req, info) => handler(req, (info.remoteAddr as Deno.NetAddr).hostname),
+);
// ── HTML / CSS / JS ──────────────────────────