Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e7e5e60
implement websocket
afsalthaj Mar 20, 2026
79c0a4a
implement websocket
afsalthaj Mar 20, 2026
881d377
delete comments
afsalthaj Mar 20, 2026
24b1906
Merge branch 'main' into web_socket
afsalthaj Mar 22, 2026
7e0fad8
implement websocket stream, rexport all in node http, and enable vend…
afsalthaj Mar 23, 2026
e50bf74
Merge branch 'main' into web_socket
afsalthaj Mar 23, 2026
014be2b
Merge branch 'main' into web_socket
afsalthaj Mar 25, 2026
3c3a632
add golden files
afsalthaj Mar 25, 2026
26da5ea
try fixing ci errors again
afsalthaj Mar 25, 2026
2bf961f
try fixing ci errors again
afsalthaj Mar 25, 2026
aa7e860
use golem rust api
afsalthaj Mar 25, 2026
2548f27
avoid changes in golem-rust
afsalthaj Mar 25, 2026
9c1f8bd
separate crate for golem-websocket
afsalthaj Mar 26, 2026
bbe488d
reformat code
afsalthaj Mar 26, 2026
71c0526
reformat code
afsalthaj Mar 26, 2026
f717e4c
remove assistant mistakes
afsalthaj Mar 26, 2026
cf3bd16
reformat and add docs
afsalthaj Mar 26, 2026
ec0a1ed
avoid golem feature gate for websocket
afsalthaj Mar 26, 2026
35b7f3d
remove other pointless code
afsalthaj Mar 26, 2026
3789eac
mock websocket
afsalthaj Mar 26, 2026
cc0a8ce
reformat code
afsalthaj Mar 26, 2026
8b46903
remove stub for websocket
afsalthaj Mar 26, 2026
cd08044
revert
afsalthaj Mar 26, 2026
8aeb0bd
Add define_unknown_imports_as_traps to handle WebSocket and other Gol…
afsalthaj Mar 26, 2026
4ca1fd0
Revert "Add define_unknown_imports_as_traps to handle WebSocket and o…
afsalthaj Mar 26, 2026
8a38448
add mock
afsalthaj Mar 26, 2026
121516a
Merge remote-tracking branch 'origin/web_socket' into web_socket
afsalthaj Mar 26, 2026
a24e2ca
reformat code
afsalthaj Mar 26, 2026
acbb74c
Skip test-websocket-disabled.js as WebSocket is now always available
afsalthaj Mar 27, 2026
c5058a2
update README
afsalthaj Mar 27, 2026
e999d01
update README
afsalthaj Mar 27, 2026
f467974
remove comments
afsalthaj Mar 27, 2026
f785ee5
enable vendor tests
afsalthaj Mar 27, 2026
fffdccd
fix re-export
afsalthaj Mar 27, 2026
a9c36bc
try fixing tests
afsalthaj Mar 27, 2026
299ce64
try fixing tests
afsalthaj Mar 27, 2026
71dbe00
try fixing tests
afsalthaj Mar 27, 2026
3b3c50e
fix more ci errors
afsalthaj Mar 27, 2026
d84d77c
Fix node:http WebSocket re-exports using lazy getters
afsalthaj Mar 27, 2026
bcfa1fb
reduce the chance of stack overflow
afsalthaj Mar 27, 2026
bb47559
Merge branch 'main' into web_socket
afsalthaj Mar 27, 2026
b238c5e
make golem-websocket under golem feature flag
afsalthaj Mar 27, 2026
bb22031
revert feature gate
afsalthaj Mar 27, 2026
f9abeab
Merge branch 'main' into web_socket
vigoo Mar 31, 2026
f6f7cf7
Feature gated websockets and updated to the latest golem wit interfaces
vigoo Mar 31, 2026
29b9624
Proper async receive loop
vigoo Mar 31, 2026
2f8a2e4
Fixes
vigoo Mar 31, 2026
4a2264a
Git dependency for golem-websockets temporarily
vigoo Apr 1, 2026
ce126bc
Optimization
vigoo Apr 1, 2026
c3b4423
Fix
vigoo Apr 1, 2026
a99c707
Regenerated dts
vigoo Apr 1, 2026
71dd581
Fix
vigoo Apr 1, 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
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ exclude = [
"crates/wasm-rquickjs/skeleton",
"tmp"
]
members = ["crates/wasi-logging", "crates/wasm-rquickjs"]
members = ["crates/wasi-logging", "crates/wasm-rquickjs", "crates/golem-websocket"]

[workspace.package]
version = "0.0.0"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ Requires the `http` feature flag. Client requests use `wasi:http` (TLS handled t
- `node:_http_common` — `_checkIsHttpToken`, `_checkInvalidHeaderChar`
- Supported features: keep-alive connections, chunked transfer encoding, content-length bodies, sequential request pipelining, idle connection cleanup

**Not yet supported:** HTTP Upgrade/WebSocket, 1xx informational events, server-side timeout enforcement, `https.createServer()` / HTTPS server, client `lookup` / `autoSelectFamily` options.
**Not yet supported:** HTTP Upgrade, 1xx informational events, server-side timeout enforcement, `https.createServer()` / HTTPS server, client `lookup` / `autoSelectFamily` options.

</details>

Expand Down
16 changes: 16 additions & 0 deletions crates/golem-websocket/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "golem-websocket"
version = "0.0.2"
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
description = "Packages golem:websocket as a simple Rust crate"

[dependencies]
wit-bindgen-rt = { version = "0.42.1" }
wit-bindgen = { version = "0.51.0", default-features = false, features = ["macros"] }
wasip2 = "1.0"

[package.metadata.component.target.dependencies]
"golem:websocket" = { path = "wit/deps/golem-websocket" }
15 changes: 15 additions & 0 deletions crates/golem-websocket/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#[allow(unsafe_op_in_unsafe_fn)]
mod bindings {
wit_bindgen::generate!({
world: "golem-websocket",
path: "wit",
generate_all,
pub_export_macro: true,
default_bindings_module: "crate::bindings",
with: {
"wasi:io/poll@0.2.3": wasip2::io::poll,
},
});
}

pub use bindings::golem::websocket::client::{CloseInfo, Error, Message, WebsocketConnection};
51 changes: 51 additions & 0 deletions crates/golem-websocket/wit/deps/golem-websocket/websocket.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package golem:websocket@1.5.0;

interface client {
use wasi:io/poll@0.2.3.{pollable};

variant error {
connection-failure(string),
send-failure(string),
receive-failure(string),
protocol-error(string),
closed(option<close-info>),
other(string),
}

record close-info {
code: u16,
reason: string,
}

/// A WebSocket message — text or binary
variant message {
text(string),
binary(list<u8>),
}

/// A WebSocket connection resource
resource websocket-connection {
/// Connect to a WebSocket server at the given URL (ws:// or wss://)
/// Optional headers for auth, subprotocols, etc.
connect: static func(
url: string,
headers: option<list<tuple<string, string>>>
) -> result<websocket-connection, error>;

/// Send a message (text or binary)
send: func(message: message) -> result<_, error>;

/// Receive the next message (blocks until available)
receive: func() -> result<message, error>;

/// Receive the next message with a timeout in milliseconds.
/// Returns none if the timeout expires before a message arrives.
receive-with-timeout: func(timeout-ms: u64) -> result<option<message>, error>;

/// Send a close frame with optional code and reason
close: func(code: option<u16>, reason: option<string>) -> result<_, error>;

/// Returns a pollable that resolves when a message is available to read
subscribe: func() -> pollable;
}
}
47 changes: 47 additions & 0 deletions crates/golem-websocket/wit/deps/io/poll.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package wasi:io@0.2.3;

/// A poll API intended to let users wait for I/O events on multiple handles
/// at once.
@since(version = 0.2.0)
interface poll {
/// `pollable` represents a single I/O event which may be ready, or not.
@since(version = 0.2.0)
resource pollable {

/// Return the readiness of a pollable. This function never blocks.
///
/// Returns `true` when the pollable is ready, and `false` otherwise.
@since(version = 0.2.0)
ready: func() -> bool;

/// `block` returns immediately if the pollable is ready, and otherwise
/// blocks until ready.
///
/// This function is equivalent to calling `poll.poll` on a list
/// containing only this pollable.
@since(version = 0.2.0)
block: func();
}

/// Poll for completion on a set of pollables.
///
/// This function takes a list of pollables, which identify I/O sources of
/// interest, and waits until one or more of the events is ready for I/O.
///
/// The result `list<u32>` contains one or more indices of handles in the
/// argument list that is ready for I/O.
///
/// This function traps if either:
/// - the list is empty, or:
/// - the list contains more elements than can be indexed with a `u32` value.
///
/// A timeout can be implemented by adding a pollable from the
/// wasi-clocks API to the list.
///
/// This function does not return a `result`; polling in itself does not
/// do any I/O so it doesn't fail. If any of the I/O sources identified by
/// the pollables has an error, it is indicated by marking the source as
/// being ready for I/O.
@since(version = 0.2.0)
poll: func(in: list<borrow<pollable>>) -> list<u32>;
}
5 changes: 5 additions & 0 deletions crates/golem-websocket/wit/golem-websocket.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package golem-websocket-rust:golem-websocket;

world golem-websocket {
import golem:websocket/client@1.5.0;
}
6 changes: 4 additions & 2 deletions crates/wasm-rquickjs/skeleton/Cargo.toml_
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ full-no-logging = ["lite", "node-http", "crypto", "zlib", "encoding", "crypto-fu
# Individual capabilities
fetch = ["dep:golem-wasi-http"]
node-http = []

logging = ["dep:wasi-logging"]
encoding = ["dep:encoding_rs"]
zlib = ["dep:flate2", "dep:crc32fast"]
Expand Down Expand Up @@ -52,7 +53,7 @@ crypto-full = [

sqlite = ["dep:rusqlite"]
timezone = ["dep:chrono-tz"]
golem = ["normal", "dep:golem-rust"]
golem = ["dep:golem-rust", "dep:golem-websocket"]

[dependencies]
# Core dependencies
Expand Down Expand Up @@ -121,7 +122,8 @@ wasi-logging = { version = "0.0.1", optional = true }
rusqlite = { version = "0.38", default-features = false, features = ["bundled", "wasm32-wasi-vfs", "functions", "session", "backup", "serialize"], optional = true }

# Golem
golem-rust = { version = "1.7", default-features = false, optional = true }
golem-rust = { git = "https://github.com/golemcloud/golem.git", branch = "main", default-features = false, optional = true }
golem-websocket = { git = "https://github.com/golemcloud/wasm-rquickjs.git", rev = "2f8a2e468985dd13516c3c13603dbb8ea5ce9534", optional = true }

# Patch rusqlite and libsqlite3-sys together for wasm32-wasi: the fork carries
# both the serialize/deserialize flag typing fix and the session-enabled WASI
Expand Down
32 changes: 23 additions & 9 deletions crates/wasm-rquickjs/skeleton/src/builtin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ mod web_crypto {
pub use super::web_crypto_lite::*;
}

#[cfg(feature = "golem")]
mod websocket;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be feature gated with golem

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I actually tried it, but golemcloud/golem#3076 was failing with "Websocket not found". I guess the generated crate during base-wasm generation in Golem has only "normal" features included. I will try and understand a bit more soon

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, #83 (comment) answered that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mod webstreams;
mod worker_threads;

Expand Down Expand Up @@ -260,7 +262,9 @@ pub fn add_module_resolvers(
#[cfg(feature = "golem")]
let resolver = resolver
.with_module("__wasm_rquickjs_builtin/diagnostics_channel_native")
.with_module("__wasm_rquickjs_builtin/diagnostics_channel_golem");
.with_module("__wasm_rquickjs_builtin/diagnostics_channel_golem")
.with_module("__wasm_rquickjs_builtin/websocket_native")
.with_module("__wasm_rquickjs_builtin/websocket");

internal::add_to_resolver(resolver)
}
Expand Down Expand Up @@ -336,10 +340,15 @@ pub fn module_loader() -> (
);

#[cfg(feature = "golem")]
let native_loader = native_loader.with_module(
"__wasm_rquickjs_builtin/diagnostics_channel_native",
diagnostics_channel::js_native_module,
);
let native_loader = native_loader
.with_module(
"__wasm_rquickjs_builtin/diagnostics_channel_native",
diagnostics_channel::js_native_module,
)
.with_module(
"__wasm_rquickjs_builtin/websocket_native",
websocket::js_native_module,
);

let builtin_loader = rquickjs::loader::BuiltinLoader::default()
.with_module(
Expand Down Expand Up @@ -489,10 +498,12 @@ pub fn module_loader() -> (
.with_module("node:sqlite", sqlite::SQLITE_JS);

#[cfg(feature = "golem")]
let builtin_loader = builtin_loader.with_module(
"__wasm_rquickjs_builtin/diagnostics_channel_golem",
diagnostics_channel::DIAGNOSTICS_CHANNEL_GOLEM_JS,
);
let builtin_loader = builtin_loader
.with_module(
"__wasm_rquickjs_builtin/diagnostics_channel_golem",
diagnostics_channel::DIAGNOSTICS_CHANNEL_GOLEM_JS,
)
.with_module("__wasm_rquickjs_builtin/websocket", websocket::WEBSOCKET_JS);

(native_loader, builtin_loader, internal::module_loader())
}
Expand Down Expand Up @@ -524,6 +535,9 @@ pub fn wire_builtins() -> String {
#[cfg(feature = "golem")]
writeln!(result, "{}", diagnostics_channel::GOLEM_WIRE_JS).unwrap();

#[cfg(feature = "golem")]
writeln!(result, "{}", websocket::WIRE_JS).unwrap();

result
}

Expand Down
20 changes: 19 additions & 1 deletion crates/wasm-rquickjs/skeleton/src/builtin/node_http.js
Original file line number Diff line number Diff line change
Expand Up @@ -2635,9 +2635,18 @@ export function get(url, options, callback) {
return req;
}

// ===== WebSocket re-exports (per Node.js convention) =====
// These are lazily read from globalThis because the websocket WIRE_JS init script
// sets them after module evaluation but before any user code runs.
export const WebSocket = globalThis.WebSocket;
export const WebSocketStream = globalThis.WebSocketStream;
export const MessageEvent = globalThis.MessageEvent;
export const CloseEvent = globalThis.CloseEvent;
export const ErrorEvent = globalThis.ErrorEvent;

// ===== Default export =====

export default {
const _default = {
METHODS,
STATUS_CODES,
maxHeaderSize,
Expand All @@ -2654,3 +2663,12 @@ export default {
request,
get,
};
// Add WebSocket properties as lazy getters so they resolve after WIRE_JS runs
Object.defineProperties(_default, {
WebSocket: { get() { return globalThis.WebSocket; }, enumerable: true },
WebSocketStream: { get() { return globalThis.WebSocketStream; }, enumerable: true },
MessageEvent: { get() { return globalThis.MessageEvent; }, enumerable: true },
CloseEvent: { get() { return globalThis.CloseEvent; }, enumerable: true },
ErrorEvent: { get() { return globalThis.ErrorEvent; }, enumerable: true },
});
export default _default;
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ export function createServer() { throw NOT_SUPPORTED_ERROR; }
export function request() { throw NOT_SUPPORTED_ERROR; }
export function get() { throw NOT_SUPPORTED_ERROR; }

export const WebSocket = globalThis.WebSocket;
export const WebSocketStream = globalThis.WebSocketStream;

export default {
METHODS, STATUS_CODES, maxHeaderSize,
validateHeaderName, validateHeaderValue,
Agent, globalAgent,
ClientRequest, IncomingMessage,
Server, ServerResponse,
createServer, request, get,
WebSocket, WebSocketStream,
};
Loading
Loading