Using WebSockets
WebSockets give a Jarvis application a two-way, persistent connection to a client. Unlike an ordinary HTTP request, where the client asks and the server answers once, a WebSocket stays open: either side can send a message at any time, until the connection is closed. This suits interactive, low-latency exchanges — live updates in both directions, chat, collaborative editing, streaming commands and results.
If you only need the server to push a stream of events to the client, and the client never sends anything back over that channel, Server-Sent Events are simpler, work over plain HTTP, and reconnect automatically.
WebSocket support works in both the JSON and REST paradigms. It is off by default; see WebSocket Settings.
A first example
⍝ in CodeLocation
∇ r←WsReceive req ⍝ OnWsReceiveFn
{}(req.##.conx)req.##.Server.WsSend 'You said: ',req.Payload
r←0 ⍝ 0 keeps the connection open
∇
j←Jarvis.New ''
j.CodeLocation←#
j.EnableWebSockets←1
j.OnWsReceiveFn←'WsReceive'
j.Start
In a page served from the same Jarvis:
const ws = new WebSocket("ws://localhost:8080/");
ws.onmessage = e => console.log(e.data); // "You said: hello"
ws.onopen = () => ws.send("hello");
The connection lifecycle
A WebSocket connection goes through three stages, each with its own hook.
Upgrade
A client opens an HTTP connection and sends an Upgrade: websocket request. How Jarvis handles it depends on WsAutoUpgrade:
WsAutoUpgrade←1(the default):Jarvisaccepts the upgrade automatically, then callsOnWsUpgradeFnwith the connection namespace. Returning a non-zero result closes the connection.WsAutoUpgrade←0:JarviscallsOnWsUpgradeReqFnwith the connection namespace before accepting. Return0to accept the upgrade, or a non-zero value to reject it. IfOnWsUpgradeReqFnis not defined, the upgrade is accepted. Your function can inspect the request (Headers,Path,PeerAddr) and can add headers to the acceptance by setting the connection namespace'sAcceptHeaders.
Messages
Once upgraded, each message the client sends arrives as a call to OnWsReceiveFn (see Receiving messages). Your application sends messages to a client with WsSend. A message can be sent from inside OnWsReceiveFn (a reply) or from any other code that has the connection, such as a background thread broadcasting to many clients.
Close and error
When the client closes the connection, Jarvis calls OnWsCloseFn with the connection namespace, then removes the connection. If an error occurs on the connection, Jarvis logs it, calls OnWsErrorFn, and removes the connection. Use these hooks to release any per-connection state your application is holding.
The connection namespace
The upgrade, close and error hooks are each passed a connection namespace: a namespace Jarvis keeps for the life of the connection. Useful fields are:
| -- | -- |
conx |
The Conga connection name. Pass it (or the namespace) to WsSend. |
Server |
The Jarvis instance, for calling WsSend. |
Headers |
The upgrade request's headers, as a 2-column matrix of lower-case name and value. |
Path |
The path from the upgrade request's URL. |
PeerAddr |
The client's IP address. |
IsWebSocket |
1. |
IsAuthenticated |
1 once WsAuthenticateFn has accepted the connection. |
Keep a reference to the connection (its conx, or the namespace) if you want to send to it later, for example from a broadcast thread.
Receiving messages
When OnWsReceiveFn is defined, Jarvis calls it for each message with a message namespace, which we'll call req. Its fields are:
| -- | -- |
Payload |
The message content. |
DataType |
The WebSocket data type. |
Complete |
Whether the message is complete. |
req.## |
The connection namespace (the message namespace's parent). |
So req.##.conx is the connection, req.##.Server is the Jarvis instance, and req.##.Headers is the upgrade request's headers.
OnWsReceiveFn's result controls the connection: 0 keeps it open, and any non-zero value closes it.
Sending messages
{r}←where j.WsSend what
where is one or more connections to send to. Each can be:
- a connection name (a
conx, as found in the connection namespace), or - a connection namespace
what is the message:
- a namespace is sent as JSON
- any other array is sent as it is
r is one Conga return code per connection: 0 for success, non-zero for a send failure (which is also logged). A reply from inside OnWsReceiveFn:
∇ r←WsReceive req;ns
ns←req.## ⍝ the connection namespace
{}(ns.conx)ns.Server.WsSend 'ack: ',req.Payload
r←0
∇
To send to several connections, collect their conx names (for example in OnWsUpgradeFn) and pass them all to WsSend. A background thread started by AppInitFn can broadcast this way; keep a reference to the Jarvis instance, which a monadic AppInitFn receives as its argument.
The built-in message handler
If OnWsReceiveFn is not defined, and the HTML interface is enabled (which it is by default in JSON mode), Jarvis uses a built-in message handler. It treats each WebSocket message as a JSON request for an endpoint:
{"Endpoint":"/myEndpoint","Payload":<argument>}
Jarvis runs the named function in CodeLocation with Payload as its argument and sends the result back as JSON. This is what the built-in HTML test page's "Send via WebSocket" button uses, and it's a quick way to try an endpoint over a WebSocket.
The built-in handler applies the same restrictions as the HTTP interface: a function is run only if it passes IncludeFns / ExcludeFns and is not a hook function. A message naming any other function gets Invalid Endpoint, and a message that isn't valid JSON gets a WSReceive Error whose detail follows ErrorInfoLevel.
Because the built-in handler is active whenever WebSockets are enabled without an OnWsReceiveFn, define OnWsReceiveFn for any application that has its own WebSocket protocol, so messages go to your code instead.
Authentication
Set WsAuthenticateFn to authenticate a connection. When OnWsReceiveFn is defined, Jarvis calls WsAuthenticateFn with the connection namespace before dispatching the first message. Return 0 to accept (the connection is then marked authenticated and the function isn't called again for it), or a non-zero value to reject, in which case Jarvis closes the connection without dispatching the message.
The browser's WebSocket API can't add headers to its request, so a token is usually carried another way:
- in a cookie, which the browser sends with the upgrade request (available in the connection namespace's
Headers) - in the upgrade request's query string (
new WebSocket("ws://host/path?token=…")), available inPath
A non-browser client (for example an APL HTTP client) can send any headers on the upgrade request.
Things to bear in mind
- Each open WebSocket is a connection that stays open. WebSocket connections are not subject to
ConnectionTimeout. Jarvisuses Conga, which supports HTTP/1.x and its WebSocket upgrade; it does not implement WebSocket extensions such as per-message compression.- Each message is handled on its own thread, so a slow handler for one message doesn't block others; conversely, don't assume replies to rapid successive messages arrive in a fixed order relative to later sends.
OnWsCloseFnis called on a WebSocket close handshake (what a browser'sws.close()performs). A connection that simply drops is reported as a close and cleaned up, but may not invokeOnWsCloseFn.