Websockets in MemCP: Difference between revisions
Wikiservice (talk | contribs) (Refresh MemCP documentation: accuracy, operational guidance, performance profile and maintained API reference) |
Wikiservice (talk | contribs) (Refresh MemCP documentation: accuracy, operational guidance, performance profile and maintained API reference) |
||
| Line 9: | Line 9: | ||
== Server-side upgrade == | == Server-side upgrade == | ||
< | <pre> | ||
(set send ((res "websocket") | (set send ((res "websocket") | ||
(lambda (message) (print "message: " message)) | (lambda (message) (print "message: " message)) | ||
(lambda () (print "connection closed")))) | (lambda () (print "connection closed")))) | ||
(send 1 "Hello from MemCP") | (send 1 "Hello from MemCP") | ||
</ | </pre> | ||
The numeric first argument is the WebSocket frame opcode used by the current response API. Keep the sending function only for the connection lifetime. | The numeric first argument is the WebSocket frame opcode used by the current response API. Keep the sending function only for the connection lifetime. | ||
| Line 20: | Line 20: | ||
A browser client can connect to the routed endpoint with the standard API: | A browser client can connect to the routed endpoint with the standard API: | ||
< | <pre> | ||
const socket = new WebSocket("wss://example.test/minigame/ws"); | const socket = new WebSocket("wss://example.test/minigame/ws"); | ||
socket.addEventListener("open", () => socket.send(JSON.stringify({type: "hello"}))); | socket.addEventListener("open", () => socket.send(JSON.stringify({type: "hello"}))); | ||
socket.addEventListener("message", event => console.log(JSON.parse(event.data))); | socket.addEventListener("message", event => console.log(JSON.parse(event.data))); | ||
</ | </pre> | ||
== Complete routed example == | == Complete routed example == | ||
| Line 30: | Line 30: | ||
The repository's <code>apps/minigame.scm</code> shows the complete routing pattern: preserve the previous handler, serve an HTML/JavaScript client below one prefix, and upgrade only the WebSocket path. | The repository's <code>apps/minigame.scm</code> shows the complete routing pattern: preserve the previous handler, serve an HTML/JavaScript client below one prefix, and upgrade only the WebSocket path. | ||
< | <pre> | ||
(define http_handler (begin | (define http_handler (begin | ||
(set old_handler http_handler) | (set old_handler http_handler) | ||
| Line 46: | Line 46: | ||
((res "print") "WebSocket client assets belong here")) | ((res "print") "WebSocket client assets belong here")) | ||
(old_handler req res)))))) | (old_handler req res)))))) | ||
</ | </pre> | ||
When this module is imported by <code>lib/main.scm</code>, the existing server uses the wrapped handler. A standalone application can call <code>serve</code> explicitly. Keep the receive callback and its connection-local <code>send</code> closure together; never store that closure after close. | When this module is imported by <code>lib/main.scm</code>, the existing server uses the wrapped handler. A standalone application can call <code>serve</code> explicitly. Keep the receive callback and its connection-local <code>send</code> closure together; never store that closure after close. | ||
Latest revision as of 12:14, 28 August 2026
Websockets in MemCP
An embedded HTTP handler can upgrade a request through the response object's WebSocket facility and receive messages through a callback. The apps/minigame.scm example in the repository demonstrates the current API and routing pattern.
WebSockets keep one bidirectional connection open for live dashboards, notifications, collaborative state, games, or change feeds. They are a delivery channel, not a durable message queue: reconnect, replay, acknowledgement, ordering, and missed-message semantics belong to the application.
Server-side upgrade
(set send ((res "websocket") (lambda (message) (print "message: " message)) (lambda () (print "connection closed")))) (send 1 "Hello from MemCP")
The numeric first argument is the WebSocket frame opcode used by the current response API. Keep the sending function only for the connection lifetime.
A browser client can connect to the routed endpoint with the standard API:
const socket = new WebSocket("wss://example.test/minigame/ws");
socket.addEventListener("open", () => socket.send(JSON.stringify({type: "hello"})));
socket.addEventListener("message", event => console.log(JSON.parse(event.data)));
Complete routed example
The repository's apps/minigame.scm shows the complete routing pattern: preserve the previous handler, serve an HTML/JavaScript client below one prefix, and upgrade only the WebSocket path.
(define http_handler (begin (set old_handler http_handler) (lambda (req res) (begin (match (req "path") "/minigame/ws" (begin (set send ((res "websocket") (lambda (message) (begin (print "message: " message) (send 1 (concat "echo: " message)))))) (send 1 "Hello from MemCP")) (regex "^/minigame/(.*)$" path asset) (begin ((res "header") "Content-Type" "text/plain") ((res "status") 200) ((res "print") "WebSocket client assets belong here")) (old_handler req res))))))
When this module is imported by lib/main.scm, the existing server uses the wrapped handler. A standalone application can call serve explicitly. Keep the receive callback and its connection-local send closure together; never store that closure after close.
Prefer JSON messages with an explicit type and version. Validate the decoded shape before reading fields, cap message size, and define how the client resubscribes or resumes after reconnect.
Database-backed publish/subscribe
Prepare fixed queries during module startup, create a session per connection, and bind authenticated identity and subscription state there. A receive callback can update state atomically; query results can be emitted through send. On close, release subscriptions and other references so they do not keep sessions or sending functions alive.
For fan-out, use bounded per-client queues and a policy for slow consumers (drop/coalesce messages or disconnect). A database table can persist events for replay, but a memory or cache table cannot provide durable delivery after restart.
The earlier hardlife.io example used this structure at larger scale: prepare position-update, listener-membership and dirty-list queries once; authenticate a player handle with a message authentication code; keep the connection's player/location state in a session; mark affected listeners dirty after a position change; send only changed avatars; and remove listener state on disconnect. The complete historical listing coupled credentials, global sloppy defaults and application-specific tables, so reproducing it as a copy-and-paste server would be unsafe. The pipeline remains a useful design example.
Hosting the handler beside the database removes an application/database socket hop and repeated parsing for prepared shapes. That is a performance opportunity, not proof that every embedded WebSocket service is faster than a separate Node.js or Go service; benchmark the authenticated end-to-end path and account for the shared failure domain.
Production considerations
Authenticate the upgrade, validate browser Origin, limit message and queue sizes, apply backpressure, and remove subscriptions when the connection closes. A slow client must not create an unbounded in-memory backlog. Long database operations should observe cancellation. Use TLS at a trusted proxy or transport layer and do not expose default credentials.
Storage ENGINE choice is independent of WebSocket delivery: sloppy can lose recent writes, and memory/cache are not durable. See In-Database WebApps and REST Services and Persistency and Performance Guarantees.