Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
moon version --all
moonrun --version

- name: moon update
run: |
moon update

- name: moon check
run: |
moon check --deny-warn
Expand Down Expand Up @@ -75,6 +79,10 @@ jobs:
moon version --all
moonrun --version

- name: moon update
run: |
moon update

- name: moon check
run: |
moon check
Expand Down Expand Up @@ -105,6 +113,10 @@ jobs:
moon version --all
moonrun --version

- name: moon update
run: |
moon update

- name: format diff
run: |
moon fmt
Expand All @@ -128,6 +140,10 @@ jobs:
moon version --all
moonrun --version

- name: moon update
run: |
moon update

- name: moon info
run: |
moon info
Expand All @@ -149,6 +165,10 @@ jobs:
moon version --all
moonrun --version

- name: moon update
run: |
moon update

- name: disable mimalloc
run: |
echo "" >dummy_libmoonbitrun.c
Expand Down Expand Up @@ -181,6 +201,10 @@ jobs:
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
echo "$HOME/.moon/bin" >> $GITHUB_PATH

- name: moon update
run: |
moon update

- name: moon test
run: moon test --enable-coverage

Expand Down
114 changes: 114 additions & 0 deletions examples/websocket_client/main.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// WebSocket client example - Autobahn Test Suite Client
///
/// This implements the Autobahn test suite client logic:
/// 1. Get case count
/// 2. Run each test case (echo all messages)
/// 3. Update reports
async fn main {
run_autobahn_tests()
}

///|
/// Run Autobahn WebSocket test suite
pub async fn run_autobahn_tests() -> Unit {
let host = @sys.get_env_vars()
.get("WS_TEST_HOST")
.unwrap_or("127.0.0.1".to_string())
let port = 9001
let agent = "moonbit-async-websocket"

// Step 1: Get case count
@stdio.stdout.write("Getting case count from \{host}:\{port}...\n")
let case_count = get_case_count(host, port)
@stdio.stdout.write("Ok, will run \{case_count} cases\n\n")

// Step 2: Run each test case
for case_id = 1; case_id <= case_count; case_id = case_id + 1 {
@stdio.stdout.write(
"Running test case \{case_id}/\{case_count} as user agent \{agent}\n",
)
run_test_case(host, port, case_id, agent)
}

// Step 3: Update reports
@stdio.stdout.write("\nUpdating reports...\n")
update_reports(host, port, agent)
@stdio.stdout.write("All tests completed!\n")
}

///|
/// Get the total number of test cases
async fn get_case_count(host : String, port : Int) -> Int {
let client = @websocket.Client::connect(host, "/getCaseCount", port~)
let message = client.receive()
client.close()
match message {
@websocket.Message::Text(text) => {
let count_str = text.to_string()
@strconv.parse_int(count_str)
}
_ => {
@stdio.stdout.write("Error: Expected text message with case count\n")
0
}
}
}

///|
/// Run a single test case - echo all messages back to server
async fn run_test_case(
host : String,
port : Int,
case_id : Int,
agent : String,
) -> Unit {
let path = "/runCase?case=\{case_id}&agent=\{agent}"
let client = @websocket.Client::connect(host, path, port~)
for {
let message = client.receive() catch {
@websocket.ConnectionClosed(_, _) =>
// Test case completed
break
err => {
@stdio.stdout.write("Error in case \{case_id}: \{err}\n")
break
}
}
// Echo the message back (core test logic)
match message {
@websocket.Message::Text(text) => client.send_text(text)
@websocket.Message::Binary(data) => client.send_binary(data)
}
}
client.close()
}

///|
/// Update test reports on the server
async fn update_reports(host : String, port : Int, agent : String) -> Unit {
let path = "/updateReports?agent=\{agent}"
let client = @websocket.Client::connect(host, path, port~)
// Wait for server to close the connection
ignore(
client.receive() catch {
@websocket.ConnectionClosed(_, _) => @websocket.Message::Text("")
_ => @websocket.Message::Text("")
},
)
client.close()
}
10 changes: 10 additions & 0 deletions examples/websocket_client/moon.pkg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"import": [
"moonbitlang/async/websocket",
"moonbitlang/async",
"moonbitlang/async/stdio",
"moonbitlang/async/io",
"moonbitlang/x/sys"
],
"is-main": true
}
20 changes: 20 additions & 0 deletions examples/websocket_client/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_a_WebSocket_server_in_JavaScript_Deno
Deno.serve({
port: 8080,
handler(request) {
if (request.headers.get("upgrade") !== "websocket") {
return new Response(null, { status: 200 });
}
const { socket, response } = Deno.upgradeWebSocket(request);
socket.onopen = () => {
console.log("CONNECTED");
};
socket.onmessage = (event) => {
console.log("MESSAGE RECEIVED: ", event.data);
socket.send("pong");
};
socket.onclose = () => console.log("DISCONNECTED");
socket.onerror = (err) => console.error("ERROR: ", err);
return response;
},
});
15 changes: 15 additions & 0 deletions examples/websocket_echo_server/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const socket = new WebSocket("ws://localhost:9001");
socket.addEventListener("open", (event) => {
console.log("Connection opened");
socket.send("Hello, Server!");
});
socket.addEventListener("message", (event) => {
console.log("Message from server: ", event.data);
socket.close();
});
socket.addEventListener("close", (event) => {
console.log("Connection closed");
});
socket.addEventListener("error", (event) => {
console.error("WebSocket error: ", event);
});
74 changes: 74 additions & 0 deletions examples/websocket_echo_server/main.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Simple WebSocket echo server example
///
/// This server accepts WebSocket connections on localhost:9001
/// and echoes back any messages it receives.
///
/// You can test it with a JavaScript client in a web browser:
/// ```javascript
/// const ws = new WebSocket('ws://localhost:9001');
/// ws.onopen = function() {
/// console.log('Connected');
/// ws.send('Hello, WebSocket!');
/// };
/// ws.onmessage = function(event) {
/// console.log('Received:', event.data);
/// };
/// ```
async fn main {
start_echo_server()
}

///|
/// Start the WebSocket echo server
/// This function starts a server that listens on localhost:9001
/// and echoes back any messages it receives from clients
pub async fn start_echo_server() -> Unit {
println("Starting WebSocket echo server on localhost:9001")
@websocket.run_server(
@socket.Addr::parse("0.0.0.0:9001"),
"/ws",
async fn(ws, client_addr) {
println("New WebSocket connection from \{client_addr}")

// Simple echo loop - receive and echo back
// Connection errors will automatically close the handler
try {
for {
let msg = ws.receive()
match msg {
@websocket.Text(text) => {
println("Received text \{text.char_length()} chars")
ws.send_text(text.to_string())
}
@websocket.Binary(data) => {
println("Received binary data (\{data.length()} bytes)")
ws.send_binary(data.to_bytes())
}
}
}
} catch {
@websocket.ConnectionClosed(e, reason) =>
println(
"Client \{client_addr} disconnected with \{e}, reason: \{reason}",
)
e => println("Error with client \{client_addr}: \{e}")
}
},
allow_failure=true,
)
}
8 changes: 8 additions & 0 deletions examples/websocket_echo_server/moon.pkg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"import": [
"moonbitlang/async/socket",
"moonbitlang/async/websocket",
"moonbitlang/async"
],
"is-main": true
}
5 changes: 4 additions & 1 deletion moon.mod.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"name": "moonbitlang/async",
"version": "0.13.1",
"deps": {
"moonbitlang/x": "0.4.36"
},
"readme": "README.md",
"repository": "https://github.com/moonbitlang/async",
"license": "Apache-2.0",
"keywords": [],
"description": "Asynchronous programming library for MoonBit",
"source": "src",
"preferred-target": "native"
}
}
6 changes: 6 additions & 0 deletions src/tls/ffi.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,9 @@ fn err_get_error() -> String {
let len = err_get_error_ffi(buf)
@bytes_util.ascii_to_string(buf[:len])
}

///|
/// Generate cryptographically secure random bytes using OpenSSL's RAND_bytes
/// Returns 1 on success, 0 on failure
#borrow(buf)
pub extern "C" fn rand_bytes(buf : FixedArray[Byte], num : Int) -> Int = "moonbitlang_async_tls_rand_bytes"
1 change: 1 addition & 0 deletions src/tls/pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import(
)

// Values
fn rand_bytes(FixedArray[Byte], Int) -> Int

// Errors
pub suberror ConnectionClosed
Expand Down
5 changes: 5 additions & 0 deletions src/tls/stub.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ typedef struct SSL_METHOD SSL_METHOD;
IMPORT_FUNC(int, SSL_CTX_set_default_verify_paths, (SSL_CTX *ctx))\
IMPORT_FUNC(unsigned long, ERR_get_error, (void))\
IMPORT_FUNC(char *, ERR_error_string, (unsigned long e, char *buf))\
IMPORT_FUNC(int, RAND_bytes, (unsigned char *buf, int num))\

#define IMPORT_FUNC(ret, name, params) static ret (*name) params;
IMPORTED_OPEN_SSL_FUNCTIONS
Expand Down Expand Up @@ -250,3 +251,7 @@ int moonbitlang_async_tls_get_error(void *buf) {
ERR_error_string(code, buf);
return strlen(buf);
}

int moonbitlang_async_tls_rand_bytes(unsigned char *buf, int num) {
return RAND_bytes(buf, num);
}
Loading
Loading