Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

arena: fix UDP snap drops + add cmd/snap rate telemetry

Client side (lib/udp.mjs):
Geckos on the wire serializes via {"<event>": payload}. When the server
emits an object (as arena-manager does with snaps), the client's `on`
handler receives the payload already as a parsed object — not a string.
respond() was calling JSON.parse(content) unconditionally, which throws
on objects ("[object Object]" is invalid JSON) and silently dropped every
arena:snap. That's why remote figures appeared at their first WS-delivered
position and then went stale once UDP bound and the server stopped
falling back to WS. Guard the JSON.parse behind a typeof check.

Server side (arena-manager.mjs):
Add minimal telemetry to diagnose the matching cmd-direction issue:
log the first cmd received per handle, count cmd rx + snap tx per player,
and dump a 5-second rate summary. Cheap to leave in — ~1 line every 5s
when the arena is occupied.

Also bump sw cache to v7 so clients pick up the fresh udp.mjs on reload
instead of riding out another stale-while-revalidate cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+32 -2
+20
session-server/arena-manager.mjs
··· 206 206 receiveCmd(handle, frame) { 207 207 const rec = this.players.get(handle); 208 208 if (!rec) return; 209 + if (!rec._firstCmdLogged) { 210 + console.log(`🏟️ cmd:first handle=${handle} cmds=${(frame.cmds || []).length}`); 211 + rec._firstCmdLogged = true; 212 + } 213 + rec._cmdRxCount = (rec._cmdRxCount || 0) + 1; 209 214 rec.lastSeenMs = this.now(); 210 215 211 216 // Snap-ack: client tells us which snap they last saw. ··· 282 287 if (this.tick % TICK_RATE === 0) this.sweepStale(nowMs); 283 288 284 289 if (this.tick % SNAP_EVERY === 0) this.broadcastSnapshots(); 290 + 291 + // Periodic rate log (every 5s at TICK_RATE=60). 292 + if (this.tick % (TICK_RATE * 5) === 0 && this.players.size > 0) { 293 + const rows = []; 294 + for (const rec of this.players.values()) { 295 + const cmdRx = rec._cmdRxCount || 0; 296 + const snapTx = rec._snapTxCount || 0; 297 + const transport = rec.udpChannelId != null ? "UDP" : "WS"; 298 + rows.push(`${rec.handle}(${transport} rx=${cmdRx} tx=${snapTx})`); 299 + rec._cmdRxCount = 0; 300 + rec._snapTxCount = 0; 301 + } 302 + console.log(`🏟️ stats[5s] ${rows.join(" ")}`); 303 + } 285 304 } 286 305 287 306 sweepStale(nowMs) { ··· 403 422 if (!ok && rec.wsId != null && this.sendWS) { 404 423 this.sendWS(rec.wsId, "arena:snap", snap); 405 424 } 425 + if (ok || rec.wsId != null) rec._snapTxCount = (rec._snapTxCount || 0) + 1; 406 426 } 407 427 408 428 // Probes: always WS, full snap, messageNum=0 (they don't ack).
+11 -1
system/public/aesthetic.computer/lib/udp.mjs
··· 114 114 } 115 115 116 116 function respond(name, content) { 117 - content = JSON.parse(content); 117 + // Geckos auto-parses JSON envelopes: if the sender emitted an object, 118 + // `content` already arrives as an object; if the sender emitted a 119 + // pre-stringified payload (the pattern client→server uses), it arrives 120 + // as a string that still needs parsing. Handle both. 121 + if (typeof content === "string") { 122 + try { content = JSON.parse(content); } 123 + catch (err) { 124 + console.warn(`🩰 UDP ${name}: JSON.parse failed`, err?.message); 125 + return; 126 + } 127 + } 118 128 if (logs.udp) console.log(`🩰 UDP Received:`, content); 119 129 send({ 120 130 type: "udp:receive",
+1 -1
system/public/sw.js
··· 1 1 // Aesthetic Computer Service Worker 2 2 // Caches JavaScript modules for faster subsequent loads 3 3 4 - const CACHE_NAME = 'ac-modules-v6'; // Bump to invalidate cached disk.mjs/bios.mjs after the piece-runs telemetry shape fix 4 + const CACHE_NAME = 'ac-modules-v7'; // Bump to force fresh udp.mjs with robust respond() parsing (fixes arena:snap drops) 5 5 const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in ms (dev-friendly) 6 6 7 7 // Critical modules to precache on install