Websockets in MemCP

From MemCP
Revision as of 12:14, 28 August 2026 by Wikiservice (talk | contribs) (Refresh MemCP documentation: accuracy, operational guidance, performance profile and maintained API reference)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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.