Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6cfc3fc
Add WebSocket client and server
Nicell Apr 9, 2026
3c6a98a
fix client close
Nicell Apr 9, 2026
7ed4a48
remove unused state
Nicell Apr 9, 2026
b8cdbaa
remove boolean response from client send return
Nicell Apr 9, 2026
a9d952a
do better backpressure/dropped/sent for server
Nicell Apr 9, 2026
a1ee675
use buffers for binary data
Nicell Apr 9, 2026
fdf024f
close with error on error
Nicell Apr 9, 2026
d52a055
don't busy wait on client
Nicell Apr 9, 2026
dfc2f47
drop the procol option
Nicell Apr 9, 2026
cdfb85f
make server handle upgrade work everywhere
Nicell Apr 9, 2026
9474d14
client fix partials
Nicell Apr 9, 2026
83b3c19
use uv for receiving on client
Nicell Apr 10, 2026
64df983
finish send moved over to libuv poll
Nicell Apr 10, 2026
0cae3ef
make client send fire and forget
Nicell Apr 10, 2026
4a21e4c
reuse userdata, remove mutex
Nicell Apr 10, 2026
ef30635
small server tweaks
Nicell Apr 10, 2026
c49637a
remove receiving limits
Nicell Apr 10, 2026
445af75
unused???
Nicell Apr 10, 2026
4922a6e
unused is annoying ngl
Nicell Apr 13, 2026
8db2451
print not warn
Nicell Apr 13, 2026
ec3c73f
Merge branch 'primary' into websockets-take-two
Nicell Apr 13, 2026
864b7c0
split client websocket into its own file
Nicell Apr 13, 2026
db34c4d
reduce diff
Nicell Apr 13, 2026
c76ad0a
decompose setUpAndListen and prepareServerHandlerThread
Nicell Apr 13, 2026
14c20f2
unused again
Nicell Apr 13, 2026
32eeb9a
Merge branch 'primary' into websockets-take-two
Nicell Apr 13, 2026
7cb6a05
fix example
Nicell Apr 14, 2026
79ee3d6
remove redundant token in client code
Nicell Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions definitions/net/client.luau
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,21 @@ function client.request(url: string, metadata: Metadata?): Response
error("not implemented")
end

export type WebSocketOptions = {
headers: { [string]: string }?,
onopen: (() -> ())?,
onmessage: ((message: string | buffer) -> ())?,
onclose: ((code: number, reason: string) -> ())?,
onerror: ((error: string) -> ())?,
}

export type WebSocket = {
send: (self: WebSocket, data: string | buffer) -> (),
close: (self: WebSocket) -> (),
}

function client.websocket(url: string, options: WebSocketOptions?): WebSocket
error("not implemented")
end

return client
4 changes: 4 additions & 0 deletions definitions/net/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ local net = {}

export type Metadata = client.Metadata
export type Response = client.Response
export type WebSocketOptions = client.WebSocketOptions
export type WebSocket = client.WebSocket
export type ReceivedRequest = server.ReceivedRequest
export type ServerResponse = server.ServerResponse
export type Handler = server.Handler
export type Configuration = server.Configuration
export type Server = server.Server
export type ServerWebSocket = server.ServerWebSocket
export type WebSocketHandlers = server.WebSocketHandlers

net.client = client
net.server = server
Expand Down
28 changes: 21 additions & 7 deletions definitions/net/server.luau
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,34 @@ export type ServerResponse = string | {
headers: { [string]: string }?,
}

export type Handler = (request: ReceivedRequest) -> ServerResponse
export type ServerWebSocket = {
send: (self: ServerWebSocket, data: string | buffer) -> number,
close: (self: ServerWebSocket, code: number?, message: string?) -> (),
}

export type Configuration = {
hostname: string?,
port: number?,
reuseport: boolean?,
tls: { certfilename: string, keyfilename: string, passphrase: string?, cafilename: string? }?,
handler: Handler,
export type WebSocketHandlers = {
open: ((ws: ServerWebSocket) -> ())?,
message: ((ws: ServerWebSocket, message: string | buffer) -> ())?,
close: ((ws: ServerWebSocket, code: number, message: string) -> ())?,
drain: ((ws: ServerWebSocket) -> ())?,
}

export type Server = {
hostname: string,
port: number,
close: () -> (),
upgrade: (self: Server, req: ReceivedRequest) -> boolean,
}

export type Handler = (request: ReceivedRequest, server: Server) -> ServerResponse?

export type Configuration = {
hostname: string?,
port: number?,
reuseport: boolean?,
tls: { certfilename: string, keyfilename: string, passphrase: string?, cafilename: string? }?,
handler: Handler?,
websocket: WebSocketHandlers?,
}

function server.serve(config: Handler | Configuration): Server
Expand Down
43 changes: 43 additions & 0 deletions examples/serve_websocket.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
local server = require("@lute/net/server")

print("starting server on ws://127.0.0.1:3000")

local instance = server.serve({
hostname = "127.0.0.1",
port = 3000,
handler = function(req: server.ReceivedRequest, instance: server.Server): server.ServerResponse?
if instance:upgrade(req) then
return nil
end

return {
status = 200,
body = "Hello over HTTP. Try websocket upgrade.",
}
end,
websocket = {
open = function(ws: server.ServerWebSocket)
print("ws open")
ws:send("welcome")
end,
message = function(ws, message)
if type(message) == "buffer" then
print("ws binary message len:", buffer.len(message))
else
print("ws message:", message)
if message == "close" then
ws:close()
end
end
ws:send(message)
end,
close = function(_ws, code, message)
print("ws close:", code, message)
end,
drain = function(_ws)
print("ws drain")
end,
},
})

print(`listening on {instance.hostname}:{instance.port}`)
37 changes: 37 additions & 0 deletions examples/websocket_echo.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
local client = require("@lute/net/client")

print("connecting to echo server...")

local _ws: client.WebSocket?
_ws = client.websocket("wss://echo.websocket.org", {
onopen = function()
print("websocket opened")
if _ws then
_ws:send("hello from lute over websockets")
_ws:send(buffer.create(4))
_ws:send("close")
end
end,
onmessage = function(message)
if type(message) == "buffer" then
print("received binary message len:", buffer.len(message))
return
end

print("received:", message)
if message == "close" then
print("closing...")
if _ws then
_ws:close()
end
end
end,
onclose = function()
print("websocket closed")
end,
onerror = function(err)
print("websocket error:", err)
end,
})

print("connected, waiting for messages...")
1 change: 1 addition & 0 deletions lute/net/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ target_sources(Lute.Net PRIVATE
include/lute/net.h

src/client.cpp
src/client_websocket.cpp
src/net.cpp
src/server.cpp
)
Expand Down
57 changes: 55 additions & 2 deletions lute/net/src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "lute/common.h"
#include "lute/runtime.h"
#include "lute/userdatas.h"

#include "Luau/DenseHash.h"

Expand All @@ -10,14 +11,22 @@

#include "curl/curl.h"

#include <cstring>
#include <memory>
#include <string>
#include <utility>
#include <vector>

namespace
namespace net::client
{
struct WebSocketHandle;
int websocket(lua_State* L);
int ws_send(lua_State* L);
int ws_close(lua_State* L);
}

namespace
{
struct CurlHolder
{
CurlHolder()
Expand All @@ -37,6 +46,48 @@ static CurlHolder& globalCurlInit()
return holder;
}

static void initializeNetClient(lua_State* L)
{
luaL_newmetatable(L, "WebSocketHandle");

lua_pushcfunction(
L,
[](lua_State* L)
{
const char* index = luaL_checkstring(L, -1);

if (strcmp(index, "send") == 0)
{
lua_pushcfunction(L, net::client::ws_send, "WebSocketHandle.send");
return 1;
}

if (strcmp(index, "close") == 0)
{
lua_pushcfunction(L, net::client::ws_close, "WebSocketHandle.close");
return 1;
}

return 0;
},
"WebSocketHandle.__index"
);
lua_setfield(L, -2, "__index");

lua_pushstring(L, "WebSocketHandle");
lua_setfield(L, -2, "__type");

lua_setuserdatadtor(
L,
kWebSocketHandleTag,
[](lua_State*, void* ud)
{
std::destroy_at(static_cast<std::shared_ptr<net::client::WebSocketHandle>*>(ud));
}
);

lua_setuserdatametatable(L, kWebSocketHandleTag);
}
} // namespace

namespace net::client
Expand Down Expand Up @@ -197,7 +248,7 @@ int request(lua_State* L)

// TODO: add cancellations
token->runtime->runInWorkQueue(
[=]
[url = std::move(url), method = std::move(method), body = std::move(body), headers = std::move(headers), token]
{
CurlResponse resp = requestData(url, method, body, headers);
if (!resp.error.empty())
Expand Down Expand Up @@ -248,12 +299,14 @@ const char* const NetClient::properties[] = {nullptr};

const luaL_Reg NetClient::lib[] = {
{"request", net::client::request},
{"websocket", net::client::websocket},
{nullptr, nullptr},
};

int NetClient::pushLibrary(lua_State* L)
{
globalCurlInit();
initializeNetClient(L);

lua_createtable(L, 0, std::size(NetClient::lib));

Expand Down
Loading
Loading