Skip to content

feat: add std/ws WebSocket module and std/io terminal size detection to allow multiplayer snake#377

Draft
notactuallytreyanastasio wants to merge 12 commits intodo-crimes-to-play-snakefrom
do-more-crimes-to-play-snake-multiplayer
Draft

feat: add std/ws WebSocket module and std/io terminal size detection to allow multiplayer snake#377
notactuallytreyanastasio wants to merge 12 commits intodo-crimes-to-play-snakefrom
do-more-crimes-to-play-snake-multiplayer

Conversation

@notactuallytreyanastasio
Copy link
Copy Markdown
Contributor

Add WebSocket support to Temper's standard library with 6 @connected functions (wsListen, wsAccept, wsConnect, wsSend, wsRecv, wsClose) and 2 opaque types (WsServer, WsConnection). Wired for JS (ws npm package) and Rust (tungstenite crate) backends.

Also adds terminalColumns() and terminalRows() to std/io for detecting terminal dimensions at runtime.

@ShawSumma
Copy link
Copy Markdown
Contributor

We had a decorator called @backend that we never merged, it allowed you to connect things from non-std/ code by having it be implemented by a specific piece of code in each backend... this is an Idea that keeps seeming to reached for

@notactuallytreyanastasio
Copy link
Copy Markdown
Contributor Author

@ShawSumma I could see that being pretty valuable from a DX perspective.

@ShawSumma
Copy link
Copy Markdown
Contributor

Our earlier issue for this was #130, it wasn't super developed, but there was thought into this for Filesystems specifically

notactuallytreyanastasio and others added 12 commits March 19, 2026 18:38
JsRunFileLayoutTest: add io.js and keyboard.js to expected file trees
in both std/ and temper-core/ directories.

ReplTest: allow scheduler call in Lua translation output.

Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
…nner

The Rust backend uses a SingleThreadAsyncRunner, so the blocking
crossterm::event::read() call was starving the game tick coroutine.
Spawn a real OS thread for the blocking read and complete the promise
from there, allowing other async tasks to proceed concurrently.

Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Robert Grayson <bobbbygrayson+github@gmail.com>
Rebased onto do-crimes-to-play-snake: resolved conflicts with std/keyboard,
updated Rust terminal size to use crossterm instead of libc, added ws to
stdSupportNeeders set.
The ws.js support file dynamically imports the 'ws' package, but it
wasn't listed as a dependency. After temper build regenerated the
output, npm install wouldn't install ws, causing wsListen/wsConnect
to fail silently at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename support functions to std_ws_* to avoid colliding with
  Temper-generated panic stubs (same pattern as std_sleep/std_read_line)
- Change visibility from pub(crate) to pub for cross-crate access
- Accept &dyn WsServerTrait/WsConnectionTrait instead of &WsServer to
  work with the Rust backend's interface deref codegen
- Use WsStream enum to handle both WebSocket<TcpStream> (server-accepted)
  and WebSocket<MaybeTlsStream<TcpStream>> (client-connected)
- Use temper_core::cast() for downcasting instead of manual as_any chain
- Add .into() for tungstenite 0.26 Message::Text(Utf8Bytes) change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Temper's Rust runtime uses a single-threaded task runner. Blocking
ws operations (accept, send, recv) would block the runner and
prevent other async blocks (like readLine) from processing.

Spawn dedicated threads for ws_accept, ws_send, and ws_recv instead
of going through crate::run_async. This lets the WS operations
block independently while other async work continues.

Also adds error logging to ws_recv for debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both server-accepted and client connections now have a 50ms read
timeout. This prevents the recv loop from holding the socket Mutex
indefinitely, allowing send operations to interleave. Without this,
the Rust server couldn't send frames because recv blocked the lock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Mutex-shared WebSocket with a dedicated I/O thread per
connection that communicates via mpsc channels. The I/O thread
owns the WebSocket exclusively and polls for both send and recv,
avoiding tungstenite's internal state corruption from concurrent
access. Send is now non-blocking (channel push), recv blocks on
the channel receiver in a spawned thread.

This fixes the ResetWithoutClosingHandshake error that occurred
when the server's recv loop and send loop competed for the socket.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tungstenite corrupts internal state when read/write are interleaved
from different threads, even with Mutex protection. Replace it with
a minimal WebSocket implementation (~150 lines) that:

- Does HTTP upgrade handshake byte-by-byte (avoids BufReader read-ahead)
- Uses TcpStream::try_clone() to split into independent read/write halves
- Reader thread owns the read half, writer Mutex holds the write half
- No shared internal framing state — reads and writes are independent
- SHA-1 for the accept key via sha1_smol (only external dep)

Also removes the sleep() debug logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire WebSocket support (wsListen, wsAccept, wsConnect, wsSend, wsRecv,
wsClose) and terminal size detection (terminalColumns, terminalRows)
for the Python backend.

Uses hand-rolled WebSocket over raw TCP (same approach as Rust) with
socket.dup() for read/write split and a dedicated reader thread per
connection. No external dependencies — just stdlib socket, hashlib,
base64, struct, threading.

Terminal size uses shutil.get_terminal_size().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Check readyState before calling send(), and wrap in try/catch to
prevent synchronous throws from crashing the server when a client
disconnects mid-broadcast.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@notactuallytreyanastasio notactuallytreyanastasio force-pushed the do-more-crimes-to-play-snake-multiplayer branch from 114c6e7 to 936910d Compare March 20, 2026 03:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants