WebSockets were never actually connecting from Eric's browser (CF logs
showed zero WebSocket Upgrade requests during testing). Even when they
do connect, they're unreliable: CF DO hibernation, bg-tab throttling,
dev bundle env issues, network blips all kill them.
HTTP polling: dead simple, works everywhere, survives bg tabs, no
hibernation issue, no env-var dependency.
Server (deployed):
- POST /presence body: {id, name, color, world, x, y, z, yaw, ...}
Returns: {presences (others in world), chats (since lastChatTs), online, byWorld, peakToday, ts}
- POST /chat body: {id, name, color, world, text, ts}
- POST /leave body: {id} (called via navigator.sendBeacon on unload)
Client:
- New 1Hz polling loop in use-playground-multiplayer that POSTs presence
and processes the snapshot response. Hardcoded fallback URL so it works
even with stale dev bundles.
- sendChat now fans out to BroadcastChannel + WS + HTTP for redundancy.
- WS still attempted for low-latency presence updates between polls,
but no longer required for MP to work.
- navigator.sendBeacon on beforeunload/pagehide so explicit leaves
propagate even when the tab is closing.
The actual root cause: the DO worker was hibernating after ~10s of
inactivity (CF default), which silently killed every live WebSocket.
Clients reconnected but presence map was reset, count went to 0, all
avatars disappeared from each other's screens.
Fix: state.acceptWebSocket() lets the DO hibernate WITHOUT killing the
WebSockets. Messages route to webSocketMessage/Close/Error class methods.
Presence + chat ring are now persisted to storage so they survive
hibernation cleanly.
Server build is now 'hermes.playground.cf-worker.v2-hibernation'.
Deployed: version fc4a2e58-c7e2-44f5-8629-a266da423c7b.
This eliminates the 'avatar disappeared then came back' flicker that was
happening every time a tab momentarily lost the WebSocket (CF DO hibernation,
bg-tab throttling, network blip). Now socket close just ages the presence;
if the client reconnects within STALE_AFTER_MS (12s) the avatar persists
seamlessly. Explicit 'leave' messages from the client (sent on beforeunload)
still propagate immediately as before.
Worker (deployed):
- STALE_AFTER_MS 5000 -> 12000 to forgive bg-tab throttling.
Player avatar:
- Muscled cuirass chest plate + glowing winged Hermes sigil disc on the chest
- Tasset (4 armored skirt strips) at the pelvis
- Steel gauntlets on the forearms
- Greaves (shin armor) on each leg
- Shoulder pauldron stud kept; helmet/cape/weapon variants unchanged
Result: avatars now read as actual knights, not blobs.
Client (use-playground-multiplayer.ts):
- Drop presence to 5 Hz (200ms, was 100ms). Halves bandwidth, identical look.
- Skip-send when player static (POS_EPSILON 0.04, YAW_EPSILON ~1.4°).
- Avatar config sent only on signature change, not every tick.
- Position-delta gate before re-render (RENDER_POS_EPSILON 0.03).
- World-scoped local rendering (visibleRemotes) — never see remotes from
other worlds.
- Connection state ('offline' | 'broadcast' | 'ws' | 'both') exposed for HUD.
- Server-pushed count consumed via 'count' wire kind (no /stats polling).
Worker (playground-ws-worker/src/worker.ts) v1:
- World-scoped fan-out: only broadcast to recipients in same world.
- Push 'count' messages on every join/leave (zero poll cost on clients).
- Per-socket token bucket rate limit: 30 msgs/sec.
- 50ms presence dedupe per player (drops floods).
- Stale prune at 5s (matches client).
- Send live count baseline on connect handshake.
HUD (playground-online-chip.tsx):
- Consume server-pushed 'hermes-playground-count' CustomEvent.
- /stats fetch only as 3s fallback if no push arrives.
- Connection-state dot: green (live), yellow (local-only), red (offline).
- Tooltip shows peak + per-world breakdown.
- Pulsing animation when fully connected.
World-3d:
- Coalesced position poll to 200ms (matches presence cadence).
- Surface transport + serverCount on window for HUD chip.
Verified: pnpm build clean, worker deploy clean, WS handshake + count
push + byWorld breakdown all working against the live worker.
- Replace floating PNG portraits with portrait-chip nameplates above heads
(NPCs, player, bots, remote players). Voxel body becomes the character;
PNG identifies them at a glance without the chimera-y face hover.
- Add Cloudflare Workers + Durable Objects port of scripts/playground-ws.mjs
in playground-ws-worker/ — same wire protocol so client connects unchanged.
Includes /stats endpoint with online + byWorld + peakToday.
- Add PlaygroundOnlineChip HUD component that polls /stats and renders a
live 'N agents online' badge. Hidden when VITE_PLAYGROUND_STATS_URL unset.
- Drop unused Billboard / useTexture imports from world-3d.
Build: pnpm build clean. No new deps. Worker deps install separately.