WebSocket API
In Kinode OS, WebSocket connects are made with a Rust warp
server in the core http_server:distro:sys
process.
Each connection is assigned a channel_id
that can be bound to a given process using a WsRegister
message.
The process receives the channel_id
for pushing data into the WebSocket, and any subsequent messages from that client will be forwarded to the bound process.
Opening a WebSocket Channel from a Client
To open a WebSocket channel, connect to the main route on the node /
and send a WsRegister
message as either text or bytes.
The simplest way to connect from a browser is to use the @kinode/client-api
like so:
const api = new KinodeEncryptorApi({
nodeId: window.our.node, // this is set if the /our.js script is present in index.html
processId: "my_package:my_package:template.os",
onOpen: (_event, api) => {
console.log('Connected to Kinode')
// Send a message to the node via WebSocket
api.send({ data: 'Hello World' })
},
})
@kinode/client-api
is available here: https://www.npmjs.com/package/@kinode/client-api
Simple JavaScript/JSON example:
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith(name)) {
return cookie.substring(name.length + 1);
}
}
}
const websocket = new WebSocket("http://localhost:8080/");
const message = JSON.stringify({
"auth_token": getCookie(`kinode-auth_${nodeId}`),
"target_process": "my_package:my_package:template.os",
"encrypted": false,
});
websocket.send(message);
Handling Incoming WebSocket Messages
Incoming WebSocket messages will be enums of HttpServerRequest
with type WebSocketOpen
, WebSocketPush
, or WebSocketClose
.
You will want to store the channel_id
that comes in with WebSocketOpen
so that you can push data to that WebSocket.
If you expect to have more than one client connected at a time, then you will most likely want to store the channel IDs in a Set (Rust HashSet
).
With a WebSocketPush
, the incoming message will be on the LazyLoadBlob
, accessible with get_blob()
.
WebSocketClose
will have the channel_id
of the closed channel, so that you can remove it from wherever you are storing it.
A full example:
fn handle_http_server_request(
our: &Address,
message_archive: &mut MessageArchive,
source: &Address,
body: &[u8],
channel_ids: &mut HashSet,
) -> anyhow::Result<()> {
let Ok(server_request) = serde_json::from_slice::<HttpServerRequest>(body) else {
// Fail silently if we can't parse the request
return Ok(());
};
match server_request {
HttpServerRequest::WebSocketOpen { channel_id, .. } => {
// Set our channel_id to the newly opened channel
// Note: this code could be improved to support multiple channels
channel_ids.insert(channel_id);
}
HttpServerRequest::WebSocketPush { .. } => {
let Some(blob) = get_blob() else {
return Ok(());
};
handle_chat_request(
our,
message_archive,
our_channel_id,
source,
&blob.bytes,
false,
)?;
}
HttpServerRequest::WebSocketClose(_channel_id) => {
channel_ids.remove(channel_id);
}
HttpServerRequest::Http(IncomingHttpRequest { method, url, bound_path, .. }) => {
// Handle incoming HTTP requests here
}
};
Ok(())
}
Pushing Data to a Client via WebSocket
Pushing data to a connected WebSocket is very simple. Call the send_ws_push
function from process_lib
:
pub fn send_ws_push(
node: String,
channel_id: u32,
message_type: WsMessageType,
blob: LazyLoadBlob,
) -> anyhow::Result<()>
node
will usually be our.node
(although you can also send a WS push to another node's http_server
!), channel_id
is the client you want to send to, message_type
will be either WsMessageType::Text
or WsMessageType::Binary
, and blob
will be a standard LazyLoadBlob
with an optional mime
field and required bytes
field.
If you would prefer to send the request without the helper function, this is that what send_ws_push
looks like under the hood:
Request::new()
.target(Address::new(
node,
ProcessId::from_str("http_server:distro:sys").unwrap(),
))
.body(
serde_json::json!(HttpServerRequest::WebSocketPush {
channel_id,
message_type,
})
.to_string()
.as_bytes()
.to_vec(),
)
.blob(blob)
.send()?;