image:[License,link="https://github.com/hyperpolymath/palimpsest-license"]
Type-safe WebSocket client and server bindings for ReScript using Deno’s native WebSocket API.
Part of the ReScript Full Stack ecosystem.
rescript-websocket provides zero-cost FFI bindings to the standard WebSocket API, designed to work seamlessly with Deno’s runtime. It offers:
-
Type-safe Client API - Connect to WebSocket servers with full type safety
-
Server-side Support - Upgrade HTTP connections to WebSocket using Deno.serve
-
Broadcast Utilities - Helper functions for multi-client messaging
-
Promise-based Flow -
waitForOpenfor clean async connection handling -
Zero Runtime Overhead - Compiles to direct JavaScript WebSocket calls
Add to your deno.json imports:
{
"imports": {
"@hyperpolymath/rescript-websocket": "jsr:@hyperpolymath/rescript-websocket@^0.1.0"
}
}open WebSocket
// Create a connection
let ws = Client.make("wss://echo.websocket.org")
// Set up event handlers
ws->Client.onOpen(() => {
Console.log("Connected!")
ws->Client.send("Hello, WebSocket!")
})
ws->Client.onMessage(event => {
Console.log("Received: " ++ event.data)
})
ws->Client.onClose(event => {
Console.log(`Closed: ${event.code->Int.toString} - ${event.reason}`)
})
ws->Client.onError(error => {
Console.error("Error: " ++ error.message)
})open WebSocket
// Track connected clients
let clients: ref<array<Client.t>> = ref([])
// Create WebSocket handler
let wsHandler = Server.makeHandler(
~onConnect=socket => {
Console.log("Client connected")
clients := clients.contents->Array.concat([socket])
},
~onMessage=(socket, data) => {
Console.log("Received: " ++ data)
// Echo back to sender
socket->Client.send("Echo: " ++ data)
// Or broadcast to all clients
broadcast(clients.contents, data)
},
~onClose=(_socket, event) => {
Console.log(`Client disconnected: ${event.code->Int.toString}`)
},
)
// Start server
let _ = Server.serve(
{
port: 8080,
onListen: info => Console.log(`Server running on ${info["hostname"]}:${info["port"]->Int.toString}`),
},
async request => {
switch wsHandler(request) {
| Some(response) => response
| None => Fetch.Response.make("Not a WebSocket request")
}
},
)type readyState =
| Connecting // 0 - Connection not yet open
| Open // 1 - Connection is open
| Closing // 2 - Connection is closing
| Closed // 3 - Connection is closed
type t // WebSocket instance
type closeEvent = {
code: int,
reason: string,
wasClean: bool,
}
type messageEvent = { data: string }
type errorEvent = { message: string }| Function | Description | Signature |
|---|---|---|
|
Create a new WebSocket connection |
|
|
Create with subprotocol negotiation |
|
|
Get current connection state |
|
|
Get the WebSocket URL |
|
|
Get negotiated subprotocol |
|
|
Get bytes queued for sending |
|
|
Send a text message |
|
|
Send binary data |
|
|
Send JSON data (serialized) |
|
|
Close the connection |
|
|
Close with code and reason |
|
|
Set connection opened handler |
|
|
Set connection closed handler |
|
|
Set message received handler |
|
|
Set error handler |
|
|
Check if connection is open |
|
|
Check if still connecting |
|
|
Check if connection is closed |
|
|
Promise that resolves when open |
|
type socket = Client.t // Server-side WebSocket is same as client
type server // Deno HTTP server handle
type request // Incoming HTTP request
type serveOptions = {
port?: int,
hostname?: string,
onListen?: {"port": int, "hostname": string} => unit,
}
type handler = request => promise<Fetch.Response.t>| Function | Description |
|---|---|
|
Upgrade HTTP request to WebSocket (returns socket + response) |
|
Start HTTP/WebSocket server with options |
|
Start server with just a handler function |
|
Gracefully shutdown the server |
|
Get URL from request object |
|
Create a WebSocket handler with callbacks |
Standard WebSocket close codes:
| Code | Meaning |
|---|---|
1000 |
Normal closure |
1001 |
Going away (e.g., page navigation) |
1002 |
Protocol error |
1003 |
Unsupported data type |
1006 |
Abnormal closure (no close frame) |
1007 |
Invalid frame payload data |
1008 |
Policy violation |
1009 |
Message too big |
1010 |
Mandatory extension missing |
1011 |
Internal server error |
1015 |
TLS handshake failure |
let rec connect = (url, attempt) => {
let ws = Client.make(url)
ws->Client.onClose(_ => {
let delay = Math.min(1000.0 *. Math.pow(2.0, attempt->Int.toFloat), 30000.0)
Console.log(`Reconnecting in ${delay->Float.toString}ms...`)
let _ = setTimeout(() => connect(url, attempt + 1), delay->Float.toInt)
})
ws->Client.onOpen(() => {
Console.log("Connected!")
// Reset attempt counter on successful connection
})
ws
}
let ws = connect("wss://example.com/ws", 0)type message =
| Ping
| Pong
| Chat({user: string, text: string})
| Join({room: string})
let encodeMessage = msg => {
switch msg {
| Ping => JSON.Encode.object([("type", JSON.Encode.string("ping"))])
| Pong => JSON.Encode.object([("type", JSON.Encode.string("pong"))])
| Chat({user, text}) =>
JSON.Encode.object([
("type", JSON.Encode.string("chat")),
("user", JSON.Encode.string(user)),
("text", JSON.Encode.string(text)),
])
| Join({room}) =>
JSON.Encode.object([
("type", JSON.Encode.string("join")),
("room", JSON.Encode.string(room)),
])
}
}
ws->Client.sendJson(encodeMessage(Chat({user: "alice", text: "Hello!"})))This library is part of the ReScript Full Stack ecosystem:
-
rescript-websocket - WebSocket client/server (this library)
-
rescript-fetch - HTTP client bindings
-
rescript-deno - Deno runtime bindings
-
rescript-json-schema - JSON Schema validation
See CONTRIBUTING.adoc for guidelines.
-
Sign all commits (DCO required)
-
Follow conventional commits
-
ReScript only - no TypeScript
AGPL-3.0-or-later
Copyright © 2025 Hyperpolymath
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
See LICENSE.txt for full text.