diff --git a/examples/manifest.toml b/examples/manifest.toml
index 53e6dd4..5a24a9e 100644
--- a/examples/manifest.toml
+++ b/examples/manifest.toml
@@ -12,6 +12,8 @@ packages = [
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
{ name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" },
{ name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
+ { name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" },
+ { name = "gleam_stdlib", version = "0.64.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "EA2E13FC4E65750643E078487D5EF360BEBCA5EBBBA12042FB589C19F53E35C0" },
{ name = "gleam_time", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "DCDDC040CE97DA3D2A925CDBBA08D8A78681139745754A83998641C8A3F6587E" },
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
{ name = "glearray", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "5E272F7CB278CC05A929C58DEB58F5D5AC6DB5B879A681E71138658D0061C38A" },
diff --git a/examples/src/working_with_websockets/app.gleam b/examples/src/working_with_websockets/app.gleam
new file mode 100644
index 0000000..1827996
--- /dev/null
+++ b/examples/src/working_with_websockets/app.gleam
@@ -0,0 +1,19 @@
+import gleam/erlang/process
+import mist
+import wisp
+import wisp/wisp_mist
+import working_with_websockets/app/router
+
+pub fn main() {
+ wisp.configure_logger()
+ let secret_key_base = wisp.random_string(64)
+
+ let assert Ok(_) =
+ router.handle_request
+ |> wisp_mist.handler(secret_key_base)
+ |> mist.new
+ |> mist.port(8001)
+ |> mist.start
+
+ process.sleep_forever()
+}
diff --git a/examples/src/working_with_websockets/app/router.gleam b/examples/src/working_with_websockets/app/router.gleam
new file mode 100644
index 0000000..5391ced
--- /dev/null
+++ b/examples/src/working_with_websockets/app/router.gleam
@@ -0,0 +1,146 @@
+import gleam/int
+import gleam/option
+import wisp.{type Request, type Response}
+import wisp/websocket
+
+pub fn handle_request(request: Request) -> Response {
+ use <- wisp.log_request(request)
+
+ case wisp.path_segments(request) {
+ [] -> home_page()
+ ["websocket"] -> websocket_handler(request)
+ _ -> wisp.not_found()
+ }
+}
+
+fn home_page() -> Response {
+ let html =
+ "
+
+
+ WebSocket Echo Example
+
+
+
+ WebSocket Echo Example
+ This example demonstrates WebSocket support in Wisp.
+
+
+
+
+ Disconnected
+
+
+
+
+
+
+
+
+
+
+
+"
+
+ wisp.html_response(html, 200)
+}
+
+fn websocket_handler(request: Request) -> Response {
+ wisp.websocket(
+ request,
+ on_init: fn(_connection) { #(0, option.None) },
+ on_message: fn(state, message, connection) {
+ case message {
+ websocket.Text(text) -> {
+ let count = state + 1
+ let response = "Echo #" <> int.to_string(count) <> ": " <> text
+ case websocket.send_text(connection, response) {
+ Ok(_) -> websocket.Continue(count)
+ Error(_) -> websocket.StopWithError("Failed to send message")
+ }
+ }
+ websocket.Binary(binary) -> {
+ case websocket.send_binary(connection, binary) {
+ Ok(_) -> websocket.Continue(state)
+ Error(_) -> websocket.StopWithError("Failed to send binary message")
+ }
+ }
+ websocket.Closed -> websocket.Stop
+ websocket.Shutdown -> websocket.Stop
+ websocket.Custom(_) -> websocket.Stop
+ }
+ },
+ on_close: fn(state) {
+ wisp.log_info(
+ "Connection closed after: " <> int.to_string(state) <> " messages",
+ )
+ },
+ )
+}
diff --git a/examples/test/working_with_websockets/app_test.gleam b/examples/test/working_with_websockets/app_test.gleam
new file mode 100644
index 0000000..603b755
--- /dev/null
+++ b/examples/test/working_with_websockets/app_test.gleam
@@ -0,0 +1,138 @@
+import gleam/http
+import wisp
+import wisp/simulate
+import working_with_websockets/app/router
+
+pub fn get_home_page_test() {
+ let request = simulate.browser_request(http.Get, "/")
+ let response = router.handle_request(request)
+
+ assert response.status == 200
+ assert response.headers == [#("content-type", "text/html; charset=utf-8")]
+}
+
+pub fn page_not_found_test() {
+ let request = simulate.browser_request(http.Get, "/nothing-here")
+ let response = router.handle_request(request)
+
+ assert response.status == 404
+}
+
+pub fn websocket_upgrade_test() {
+ let request = simulate.websocket_request(http.Get, "/websocket")
+ let response = router.handle_request(request)
+
+ let assert wisp.WebSocket(_) = response.body
+}
+
+pub fn websocket_text_echo_test() {
+ let request = simulate.websocket_request(http.Get, "/websocket")
+ let response = router.handle_request(request)
+
+ let assert wisp.WebSocket(upgrade) = response.body
+ let handler = wisp.recover(upgrade)
+ let assert Ok(ws) = simulate.create_websocket(handler)
+
+ // Send first text message
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "Hello")
+ let assert ["Echo #1: Hello"] = simulate.websocket_sent_text_messages(ws)
+ let assert [] = simulate.websocket_sent_binary_messages(ws)
+
+ // Send second text message
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "World")
+ let assert ["Echo #1: Hello", "Echo #2: World"] =
+ simulate.websocket_sent_text_messages(ws)
+
+ // Send third text message
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "!")
+ let assert ["Echo #1: Hello", "Echo #2: World", "Echo #3: !"] =
+ simulate.websocket_sent_text_messages(ws)
+}
+
+pub fn websocket_binary_echo_test() {
+ let request = simulate.websocket_request(http.Get, "/websocket")
+ let response = router.handle_request(request)
+
+ let assert wisp.WebSocket(upgrade) = response.body
+ let handler = wisp.recover(upgrade)
+ let assert Ok(ws) = simulate.create_websocket(handler)
+
+ // Send binary message
+ let assert Ok(ws) = simulate.send_websocket_binary(ws, <<1, 2, 3>>)
+ let assert [<<1, 2, 3>>] = simulate.websocket_sent_binary_messages(ws)
+ let assert [] = simulate.websocket_sent_text_messages(ws)
+
+ // Send another binary message
+ let assert Ok(ws) = simulate.send_websocket_binary(ws, <<4, 5, 6>>)
+ let assert [<<1, 2, 3>>, <<4, 5, 6>>] =
+ simulate.websocket_sent_binary_messages(ws)
+}
+
+pub fn websocket_mixed_messages_test() {
+ let request = simulate.websocket_request(http.Get, "/websocket")
+ let response = router.handle_request(request)
+
+ let assert wisp.WebSocket(upgrade) = response.body
+ let handler = wisp.recover(upgrade)
+ let assert Ok(ws) = simulate.create_websocket(handler)
+
+ // Send text message
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "Text message")
+ let assert ["Echo #1: Text message"] =
+ simulate.websocket_sent_text_messages(ws)
+
+ // Send binary message (doesn't increment count)
+ let assert Ok(ws) = simulate.send_websocket_binary(ws, <<7, 8, 9>>)
+ let assert [<<7, 8, 9>>] = simulate.websocket_sent_binary_messages(ws)
+
+ // Send another text message (count should be 2)
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "Another text")
+ let assert ["Echo #1: Text message", "Echo #2: Another text"] =
+ simulate.websocket_sent_text_messages(ws)
+}
+
+pub fn websocket_close_test() {
+ let request = simulate.websocket_request(http.Get, "/websocket")
+ let response = router.handle_request(request)
+
+ let assert wisp.WebSocket(upgrade) = response.body
+ let handler = wisp.recover(upgrade)
+ let assert Ok(ws) = simulate.create_websocket(handler)
+
+ // Send some messages
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "First")
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "Second")
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "Third")
+
+ let assert ["Echo #1: First", "Echo #2: Second", "Echo #3: Third"] =
+ simulate.websocket_sent_text_messages(ws)
+
+ // Close the websocket - should succeed
+ let assert Ok(Nil) = simulate.close_websocket(ws)
+}
+
+pub fn websocket_closed_ignores_messages_test() {
+ let request = simulate.websocket_request(http.Get, "/websocket")
+ let response = router.handle_request(request)
+
+ let assert wisp.WebSocket(upgrade) = response.body
+ let handler = wisp.recover(upgrade)
+ let assert Ok(ws) = simulate.create_websocket(handler)
+
+ // Send a message
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "Before close")
+ let assert ["Echo #1: Before close"] =
+ simulate.websocket_sent_text_messages(ws)
+
+ // Close the websocket
+ let assert Ok(Nil) = simulate.close_websocket(ws)
+
+ // Try to send messages after closing
+ let assert Ok(ws) = simulate.send_websocket_text(ws, "After close")
+ let assert Ok(ws) = simulate.send_websocket_binary(ws, <<10, 11, 12>>)
+
+ // Messages should not be processed
+ let assert ["Echo #1: Before close"] =
+ simulate.websocket_sent_text_messages(ws)
+ let assert [] = simulate.websocket_sent_binary_messages(ws)
+}
diff --git a/gleam.toml b/gleam.toml
index 4321100..1ad2e29 100644
--- a/gleam.toml
+++ b/gleam.toml
@@ -21,6 +21,7 @@ logging = ">= 1.2.0 and < 2.0.0"
directories = ">= 1.0.0 and < 2.0.0"
houdini = ">= 1.0.0 and < 2.0.0"
filepath = ">= 1.1.2 and < 2.0.0"
+gleam_otp = ">= 1.1.0 and < 2.0.0"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
diff --git a/manifest.toml b/manifest.toml
index 93a7183..af442d2 100644
--- a/manifest.toml
+++ b/manifest.toml
@@ -34,6 +34,7 @@ gleam_crypto = { version = ">= 1.0.0 and < 2.0.0" }
gleam_erlang = { version = ">= 1.0.0 and < 2.0.0" }
gleam_http = { version = ">= 3.5.0 and < 5.0.0" }
gleam_json = { version = ">= 3.0.0 and < 4.0.0" }
+gleam_otp = { version = ">= 1.1.0 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.50.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
houdini = { version = ">= 1.0.0 and < 2.0.0" }
diff --git a/src/wisp.gleam b/src/wisp.gleam
index 2aeffcc..3b87c57 100644
--- a/src/wisp.gleam
+++ b/src/wisp.gleam
@@ -9,6 +9,7 @@ import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/erlang/application
import gleam/erlang/atom.{type Atom}
+import gleam/erlang/process
import gleam/http.{type Method}
import gleam/http/cookie
import gleam/http/request.{type Request as HttpRequest}
@@ -28,6 +29,7 @@ import logging
import marceau
import simplifile
import wisp/internal
+import wisp/websocket
//
// Responses
@@ -66,8 +68,28 @@ pub type Body {
/// The maximum number of bytes to send. Set to `None` for the whole file.
limit: Option(Int),
)
+ /// A WebSocket upgrade response.
+ ///
+ /// This will upgrade the HTTP connection to a WebSocket connection.
+ /// The upgrade is handled by the underlying HTTP server adapter.
+ ///
+ WebSocket(WebSocketUpgrade)
}
+pub type WebSocketUpgrade
+
+@internal
+pub type Unknown
+
+@external(erlang, "gleam@function", "identity")
+fn erase(callbacks: websocket.WebSocket(state, message)) -> WebSocketUpgrade
+
+@external(erlang, "gleam@function", "identity")
+@internal
+pub fn recover(
+ upgrade: WebSocketUpgrade,
+) -> websocket.WebSocket(Unknown, Unknown)
+
/// An alias for a HTTP response containing a `Body`.
pub type Response =
HttpResponse(Body)
@@ -1991,6 +2013,63 @@ pub fn get_cookie(
bit_array.to_string(value)
}
+//
+// WebSocket
+//
+
+/// Upgrade a HTTP request to a WebSocket connection.
+///
+/// This function creates a response that will upgrade the connection to
+/// WebSocket. The actual WebSocket protocol handling is done by the
+/// web server adapter (such as wisp_mist).
+///
+/// # Examples
+///
+/// ```gleam
+/// fn handle_request(request: Request) -> Response {
+/// case wisp.path_segments(request) {
+/// ["websocket"] -> {
+/// wisp.websocket(
+/// request,
+/// on_init: fn(connection) {
+/// 0
+/// },
+/// on_message: fn(state, message, connection) {
+/// case message {
+/// websocket.Text(text) -> {
+/// websocket.send_text(connection, "Echo: " <> text)
+/// websocket.continue(state)
+/// }
+/// websocket.Closed -> websocket.stop()
+/// _ -> websocket.continue(state)
+/// }
+/// },
+/// on_close: fn(_state) { Nil }
+/// )
+/// }
+/// _ -> wisp.not_found()
+/// }
+/// }
+/// ```
+///
+pub fn websocket(
+ request _request: Request,
+ on_init on_init: fn(websocket.Connection) ->
+ #(state, Option(process.Selector(message))),
+ on_message on_message: fn(
+ state,
+ websocket.Message(custom),
+ websocket.Connection,
+ ) ->
+ websocket.Next(state),
+ on_close on_close: fn(state) -> Nil,
+) -> Response {
+ let ws = websocket.new(on_init, on_message, on_close)
+
+ response(200)
+ |> set_body(WebSocket(erase(ws)))
+}
+
//
// Testing
//
diff --git a/src/wisp/simulate.gleam b/src/wisp/simulate.gleam
index fc790cd..629cbbc 100644
--- a/src/wisp/simulate.gleam
+++ b/src/wisp/simulate.gleam
@@ -1,16 +1,19 @@
import gleam/bit_array
import gleam/bytes_tree
import gleam/crypto
+import gleam/erlang/process
import gleam/http
import gleam/http/request
import gleam/json.{type Json}
import gleam/list
import gleam/option.{None, Some}
+import gleam/otp/actor
import gleam/result
import gleam/string
import gleam/uri
import simplifile
import wisp.{type Request, type Response, Bytes, File, Text}
+import wisp/websocket
/// Create a test request that can be used to test your request handler
/// functions.
@@ -279,6 +282,9 @@ pub fn read_body(response: Response) -> String {
as "the body file range was not valid UTF-8"
string
}
+ wisp.WebSocket(_) -> {
+ panic as "Cannot read body of WebSocket response - use a WebSocket client instead"
+ }
}
}
@@ -306,6 +312,9 @@ pub fn read_body_bits(response: Response) -> BitArray {
as "the body was a file, but the limit and offset were invalid"
sliced
}
+ wisp.WebSocket(_) -> {
+ panic as "Cannot read body of WebSocket response - use a WebSocket client instead"
+ }
}
}
@@ -349,3 +358,427 @@ pub const default_browser_headers: List(#(String, String)) = [
#("origin", "https://" <> default_host),
#("host", default_host),
]
+
+/// Create a websocket upgrade request with proper headers.
+///
+/// This creates a request that simulates a WebSocket upgrade handshake by
+/// setting the necessary headers: `connection`, `upgrade`, `sec-websocket-key`,
+/// and `sec-websocket-version`.
+///
+/// ## Example
+///
+/// ```gleam
+/// let request = simulate.websocket_request(http.Get, "/chat")
+/// let response = handle_request(request)
+///
+/// case response.body {
+/// wisp.WebSocket(upgrade) -> {
+/// let handler = wisp.upgrade_to_websocket(upgrade)
+/// // Test the websocket handler
+/// }
+/// _ -> panic as "Expected WebSocket upgrade"
+/// }
+/// ```
+///
+pub fn websocket_request(method: http.Method, path: String) -> Request {
+ request(method, path)
+ |> header("connection", "Upgrade")
+ |> header("upgrade", "websocket")
+ |> header("sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ==")
+ |> header("sec-websocket-version", "13")
+}
+
+/// A websocket mock for testing websocket handlers.
+///
+/// This opaque type represents a test websocket connection that captures all
+/// messages sent by the handler. It maintains the handler's state and tracks
+/// all text and binary messages sent through the connection.
+///
+/// You cannot construct this type directly - use `create_websocket` to create
+/// a test websocket from a websocket handler.
+///
+/// ## Functions
+///
+/// - `create_websocket` - Create a new test websocket
+/// - `send_websocket_text` - Send a text message to the handler
+/// - `send_websocket_binary` - Send a binary message to the handler
+/// - `websocket_sent_text_messages` - Get all text messages sent by the handler
+/// - `websocket_sent_binary_messages` - Get all binary messages sent by the handler
+/// - `reset_websocket` - Reset to initial state
+/// - `close_websocket` - Close the connection
+///
+pub opaque type WebSocket(selector_message, state) {
+ WebSocket(
+ websocket: websocket.WebSocket(selector_message, state),
+ connection: websocket.Connection,
+ state: websocket.State(state),
+ subject: process.Subject(WebSocketMessage(state)),
+ )
+}
+
+/// Internal state for the websocket mock actor
+type WebSocketState(state) {
+ State(
+ state: option.Option(websocket.State(state)),
+ sent_text_messages: List(String),
+ sent_binary_messages: List(BitArray),
+ closed: Bool,
+ )
+}
+
+/// Messages that can be sent to the mock websocket actor
+type WebSocketMessage(state) {
+ SendText(String)
+ SendBinary(BitArray)
+ Close(websocket.State(state))
+ GetSentTextMessages(reply_with: process.Subject(List(String)))
+ GetSentBinaryMessages(reply_with: process.Subject(List(BitArray)))
+ Reset(state: websocket.State(state))
+ SetState(state: websocket.State(state))
+ GetState(reply_with: process.Subject(websocket.State(state)))
+ IsClosed(reply_with: process.Subject(Bool))
+}
+
+/// Create a new websocket mock for testing.
+///
+/// This function creates a test websocket that captures all messages sent by the
+/// handler, allowing you to verify the handler's behavior without needing a real
+/// WebSocket connection. The mock automatically tracks text and binary messages
+/// sent through the connection.
+///
+/// ## Example
+///
+/// ```gleam
+/// let handler = websocket.new(
+/// on_init: fn(_conn) { 0 },
+/// on_message: fn(state, message, connection) {
+/// case message {
+/// websocket.Text(text) -> {
+/// websocket.send_text(connection, "Echo: " <> text)
+/// websocket.Continue(state + 1)
+/// }
+/// _ -> websocket.Continue(state)
+/// }
+/// },
+/// on_close: fn(_state) { Nil },
+/// )
+///
+/// let assert Ok(ws) = simulate.create_websocket(handler)
+/// let assert Ok(ws) = simulate.send_websocket_text(ws, "Hello")
+/// let assert ["Echo: Hello"] = simulate.websocket_sent_text_messages(ws)
+/// ```
+///
+/// ## Returns
+///
+/// - `Ok(WebSocket)` - A test websocket that can be used with other simulate functions
+/// - `Error(actor.StartError)` - If the underlying actor fails to start
+///
+pub fn create_websocket(
+ handler websocket: websocket.WebSocket(selector_message, state),
+) -> Result(WebSocket(selector_message, state), actor.StartError) {
+ let #(init, _, stop) = websocket.extract_callbacks(websocket)
+
+ use started <- result.try(
+ actor.new(State(
+ state: option.None,
+ sent_text_messages: [],
+ sent_binary_messages: [],
+ closed: False,
+ ))
+ |> actor.on_message(handle_message)
+ |> actor.start,
+ )
+
+ let connection =
+ websocket.make_connection(
+ fn(text) {
+ process.send(started.data, SendText(text))
+ Ok(Nil)
+ },
+ fn(binary) {
+ process.send(started.data, SendBinary(binary))
+ Ok(Nil)
+ },
+ fn() {
+ let state = process.call(started.data, 1000, GetState)
+ stop(state)
+ Ok(Nil)
+ },
+ )
+ let #(state, _selector) = init(connection)
+ process.send(started.data, SetState(state))
+ let ws_instance =
+ WebSocket(
+ websocket: websocket,
+ connection: connection,
+ state: state,
+ subject: started.data,
+ )
+ Ok(ws_instance)
+}
+
+/// Handle messages sent to the mock websocket actor
+fn handle_message(
+ state: WebSocketState(state),
+ message: WebSocketMessage(state),
+) -> actor.Next(WebSocketState(state), WebSocketMessage(state)) {
+ case message {
+ SendText(text) -> {
+ let new_state = case state.closed {
+ True -> state
+ False ->
+ State(..state, sent_text_messages: [text, ..state.sent_text_messages])
+ }
+ actor.continue(new_state)
+ }
+ SendBinary(binary) -> {
+ let new_state = case state.closed {
+ True -> state
+ False ->
+ State(..state, sent_binary_messages: [
+ binary,
+ ..state.sent_binary_messages
+ ])
+ }
+ actor.continue(new_state)
+ }
+ Close(_) -> {
+ let new_state = State(..state, closed: True)
+ actor.continue(new_state)
+ }
+ GetSentTextMessages(reply_with) -> {
+ process.send(reply_with, list.reverse(state.sent_text_messages))
+ actor.continue(state)
+ }
+ GetSentBinaryMessages(reply_with) -> {
+ process.send(reply_with, list.reverse(state.sent_binary_messages))
+ actor.continue(state)
+ }
+ Reset(new_state) -> {
+ let new_state =
+ State(
+ state: option.Some(new_state),
+ sent_text_messages: [],
+ sent_binary_messages: [],
+ closed: False,
+ )
+ actor.continue(new_state)
+ }
+ SetState(new_state) -> {
+ let new_state = State(..state, state: Some(new_state))
+ actor.continue(new_state)
+ }
+ GetState(reply_with:) -> {
+ let assert Some(s) = state.state
+ process.send(reply_with, s)
+ actor.continue(state)
+ }
+ IsClosed(reply_with:) -> {
+ process.send(reply_with, state.closed)
+ actor.continue(state)
+ }
+ }
+}
+
+/// Get all text messages that have been sent by the websocket handler.
+///
+/// Messages are returned in the order they were sent. This is useful for
+/// verifying that your handler sends the expected messages in response to
+/// incoming messages.
+///
+/// ## Example
+///
+/// ```gleam
+/// let assert Ok(ws) = simulate.create_websocket(handler)
+/// let assert Ok(ws) = simulate.send_websocket_text(ws, "Hello")
+/// let assert Ok(ws) = simulate.send_websocket_text(ws, "World")
+///
+/// let messages = simulate.websocket_sent_text_messages(ws)
+/// assert messages == ["Response 1", "Response 2"]
+/// ```
+///
+pub fn websocket_sent_text_messages(
+ websocket: WebSocket(selector_message, state),
+) -> List(String) {
+ process.call(websocket.subject, 1000, GetSentTextMessages)
+}
+
+/// Get all binary messages that have been sent by the websocket handler.
+///
+/// Messages are returned in the order they were sent. This is useful for
+/// verifying that your handler sends the expected binary data in response to
+/// incoming messages.
+///
+/// ## Example
+///
+/// ```gleam
+/// let assert Ok(ws) = simulate.create_websocket(handler)
+/// let assert Ok(ws) = simulate.send_websocket_binary(ws, <<1, 2, 3>>)
+///
+/// let messages = simulate.websocket_sent_binary_messages(ws)
+/// assert messages == [<<1, 2, 3>>]
+/// ```
+///
+pub fn websocket_sent_binary_messages(
+ websocket: WebSocket(selector_message, state),
+) -> List(BitArray) {
+ process.call(websocket.subject, 1000, GetSentBinaryMessages)
+}
+
+/// Reset the websocket to its initial state, clearing all captured messages.
+///
+/// This calls the handler's `on_init` callback again and clears the list of
+/// sent messages. The websocket is also marked as not closed, allowing you to
+/// send messages again after a close.
+///
+/// ## Example
+///
+/// ```gleam
+/// let assert Ok(ws) = simulate.create_websocket(handler)
+/// let assert Ok(ws) = simulate.send_websocket_text(ws, "Hello")
+/// let assert ["Response"] = simulate.websocket_sent_text_messages(ws)
+///
+/// // Reset to initial state
+/// let ws = simulate.reset_websocket(ws)
+/// let assert [] = simulate.websocket_sent_text_messages(ws)
+///
+/// // Can send messages again from a clean slate
+/// let assert Ok(ws) = simulate.send_websocket_text(ws, "Hello again")
+/// ```
+///
+pub fn reset_websocket(
+ websocket: WebSocket(selector_message, state),
+) -> WebSocket(selector_message, state) {
+ let WebSocket(websocket: internal_websocket, connection:, state: _, subject:) =
+ websocket
+ let #(init, _, _) = websocket.extract_callbacks(internal_websocket)
+ let #(state, _selector) = init(connection)
+ process.send(subject, Reset(state))
+ WebSocket(websocket: internal_websocket, connection:, state:, subject:)
+}
+
+/// Simulate sending a text message to the websocket handler.
+///
+/// This calls the handler's `on_message` callback with a `Text` message. The
+/// handler's state is updated based on the callback's response. Any messages
+/// sent by the handler can be retrieved using `websocket_sent_text_messages`
+/// or `websocket_sent_binary_messages`.
+///
+/// If the websocket has been closed, this function returns the websocket
+/// unchanged without calling the handler.
+///
+/// ## Example
+///
+/// ```gleam
+/// let assert Ok(ws) = simulate.create_websocket(handler)
+/// let assert Ok(ws) = simulate.send_websocket_text(ws, "Hello")
+/// let assert ["Echo: Hello"] = simulate.websocket_sent_text_messages(ws)
+/// ```
+///
+/// ## Returns
+///
+/// - `Ok(WebSocket)` - The websocket with updated state
+/// - `Error(Nil)` - If the handler returns `Stop` or `StopWithError`
+///
+pub fn send_websocket_text(
+ ws: WebSocket(selector_message, state),
+ message: String,
+) -> Result(WebSocket(selector_message, state), Nil) {
+ let WebSocket(websocket:, state:, connection:, subject:) = ws
+ let is_closed = process.call(subject, 1000, IsClosed)
+ case is_closed {
+ True -> Ok(ws)
+ False -> {
+ let #(_, handle, _) = websocket.extract_callbacks(websocket)
+ case handle(state, websocket.Text(message), connection) {
+ websocket.Continue(state) -> {
+ process.send(subject, SetState(state))
+ Ok(WebSocket(websocket:, state:, connection:, subject:))
+ }
+ websocket.Stop -> Error(Nil)
+ websocket.StopWithError(_) -> Error(Nil)
+ }
+ }
+ }
+}
+
+/// Simulate sending a binary message to the websocket handler.
+///
+/// This calls the handler's `on_message` callback with a `Binary` message. The
+/// handler's state is updated based on the callback's response. Any messages
+/// sent by the handler can be retrieved using `websocket_sent_text_messages`
+/// or `websocket_sent_binary_messages`.
+///
+/// If the websocket has been closed, this function returns the websocket
+/// unchanged without calling the handler.
+///
+/// ## Example
+///
+/// ```gleam
+/// let assert Ok(ws) = simulate.create_websocket(handler)
+/// let assert Ok(ws) = simulate.send_websocket_binary(ws, <<1, 2, 3>>)
+/// let assert [<<1, 2, 3>>] = simulate.websocket_sent_binary_messages(ws)
+/// ```
+///
+/// ## Returns
+///
+/// - `Ok(WebSocket)` - The websocket with updated state
+/// - `Error(Nil)` - If the handler returns `Stop` or `StopWithError`
+///
+pub fn send_websocket_binary(
+ ws: WebSocket(selector_message, state),
+ message: BitArray,
+) -> Result(WebSocket(selector_message, state), Nil) {
+ let WebSocket(websocket:, state:, connection:, subject:) = ws
+ let is_closed = process.call(subject, 1000, IsClosed)
+ case is_closed {
+ True -> Ok(ws)
+ False -> {
+ let #(_, handle, _) = websocket.extract_callbacks(websocket)
+ case handle(state, websocket.Binary(message), connection) {
+ websocket.Continue(state) -> {
+ process.send(subject, SetState(state))
+ Ok(WebSocket(websocket:, state:, connection:, subject:))
+ }
+ websocket.Stop -> Error(Nil)
+ websocket.StopWithError(_) -> Error(Nil)
+ }
+ }
+ }
+}
+
+/// Simulate closing the websocket connection.
+///
+/// This calls the handler's `on_close` callback with the current handler state.
+/// After closing, any subsequent calls to `send_websocket_text` or
+/// `send_websocket_binary` will be ignored without calling the handler.
+///
+/// Use `reset_websocket` to re-open the connection for further testing.
+///
+/// ## Example
+///
+/// ```gleam
+/// let assert Ok(ws) = simulate.create_websocket(handler)
+/// let assert Ok(ws) = simulate.send_websocket_text(ws, "Hello")
+///
+/// // Close the connection
+/// let assert Ok(Nil) = simulate.close_websocket(ws)
+///
+/// // Further messages are ignored
+/// let assert Ok(ws) = simulate.send_websocket_text(ws, "After close")
+/// let assert ["Response 1"] = simulate.websocket_sent_text_messages(ws)
+/// ```
+///
+/// ## Returns
+///
+/// - `Ok(Nil)` - If the connection was closed successfully
+/// - `Error(WebSocketError)` - If closing the connection failed
+///
+pub fn close_websocket(
+ websocket_arg: WebSocket(selector_message, state),
+) -> Result(Nil, websocket.WebSocketError) {
+ let WebSocket(websocket: _, state: _, connection:, subject:) = websocket_arg
+ let current_state = process.call(subject, 1000, GetState)
+ process.send(subject, Close(current_state))
+ websocket.close_connection(connection)
+}
diff --git a/src/wisp/websocket.gleam b/src/wisp/websocket.gleam
new file mode 100644
index 0000000..e5683c8
--- /dev/null
+++ b/src/wisp/websocket.gleam
@@ -0,0 +1,340 @@
+import gleam/erlang/process.{type Selector}
+import gleam/option.{type Option}
+
+/// Represents a WebSocket connection that can be used to send messages.
+///
+/// This opaque type is passed to your handler's callbacks, allowing you to
+/// send messages to the client using `send_text` and `send_binary`.
+///
+/// You cannot construct this type directly - it is provided by the framework
+/// when your handler is called.
+///
+pub opaque type Connection {
+ WebSocketConnection(
+ send_text: fn(String) -> Result(Nil, WebSocketError),
+ send_binary: fn(BitArray) -> Result(Nil, WebSocketError),
+ close: fn() -> Result(Nil, WebSocketError),
+ )
+}
+
+/// Errors that can occur when working with WebSockets.
+///
+pub type WebSocketError {
+ /// The WebSocket connection has been closed.
+ ConnectionClosed
+ /// Failed to send a message over the WebSocket.
+ SendFailed
+ /// The message format was invalid.
+ InvalidMessage
+ /// A custom WebSocket error with a description.
+ WebSocketError(String)
+}
+
+/// Messages that can be received from a WebSocket client.
+///
+/// Your `on_message` callback will receive one of these variants to handle
+/// different types of incoming messages.
+///
+pub type Message(custom) {
+ /// A text message received from the client.
+ Text(String)
+ /// A binary message received from the client.
+ Binary(BitArray)
+ /// The client has closed the connection.
+ Closed
+ /// The server is shutting down the connection.
+ Shutdown
+ Custom(custom)
+}
+
+/// The result of handling a WebSocket message.
+///
+/// Return this from your `on_message` callback to indicate whether the
+/// connection should continue or stop.
+///
+pub type Next(state) {
+ /// Continue handling messages with the updated state.
+ Continue(state)
+ /// Stop the WebSocket connection gracefully.
+ Stop
+ /// Stop the WebSocket connection with an error message.
+ StopWithError(String)
+}
+
+/// A WebSocket handler that defines the behavior of a WebSocket connection.
+///
+/// This opaque type is created using `new` and encapsulates the initialization,
+/// message handling, and cleanup logic for a WebSocket connection.
+///
+/// You cannot construct this type directly - use `new` to create a handler.
+///
+pub opaque type WebSocket(selector_message, state) {
+ WebSocket(
+ init: fn(Connection) -> #(State(state), Option(Selector(selector_message))),
+ handle: fn(State(state), Message(state), Connection) ->
+ WebSocketResult(state),
+ close: fn(State(state)) -> Nil,
+ )
+}
+
+/// The internal state of a WebSocket handler.
+///
+/// This opaque type represents the current state of your WebSocket handler and
+/// is managed internally by the framework. Your handler's state (defined in
+/// `on_init` and updated in `on_message`) is stored within this type.
+///
+pub opaque type State(any) {
+ WebSocketState(step: fn(WebSocketAction(any)) -> WebSocketResult(any))
+}
+
+type WebSocketAction(any) {
+ HandleMessage(Message(any), Connection)
+ Close
+}
+
+type WebSocketResult(any) {
+ ContinueWith(State(any))
+ StopNow
+ StopWithErrorResult(String)
+}
+
+@internal
+pub fn make_connection(
+ send_text: fn(String) -> Result(Nil, WebSocketError),
+ send_binary: fn(BitArray) -> Result(Nil, WebSocketError),
+ close: fn() -> Result(Nil, WebSocketError),
+) -> Connection {
+ WebSocketConnection(
+ send_text: send_text,
+ send_binary: send_binary,
+ close: close,
+ )
+}
+
+/// Send a text message to the WebSocket client.
+///
+/// This function sends a UTF-8 text message over the WebSocket connection.
+/// Call this from your `on_message` or `on_init` callback to send messages
+/// to the client.
+///
+/// ## Example
+///
+/// ```gleam
+/// websocket.new(
+/// on_init: fn(_conn) { 0 },
+/// on_message: fn(state, message, connection) {
+/// case message {
+/// websocket.Text(text) -> {
+/// let response = "You said: " <> text
+/// case websocket.send_text(connection, response) {
+/// Ok(_) -> websocket.Continue(state)
+/// Error(_) -> websocket.StopWithError("Failed to send message")
+/// }
+/// }
+/// _ -> websocket.Continue(state)
+/// }
+/// },
+/// on_close: fn(_) { Nil },
+/// )
+/// ```
+///
+/// ## Returns
+///
+/// - `Ok(Nil)` - The message was sent successfully
+/// - `Error(WebSocketError)` - Failed to send the message
+///
+pub fn send_text(
+ connection: Connection,
+ message: String,
+) -> Result(Nil, WebSocketError) {
+ connection.send_text(message)
+}
+
+/// Send a binary message to the WebSocket client.
+///
+/// This function sends raw binary data over the WebSocket connection. Use this
+/// when you need to send non-text data like images, audio, or custom binary
+/// protocols.
+///
+/// ## Example
+///
+/// ```gleam
+/// websocket.new(
+/// on_init: fn(_conn) { 0 },
+/// on_message: fn(state, message, connection) {
+/// case message {
+/// websocket.Binary(data) -> {
+/// // Echo the binary data back
+/// case websocket.send_binary(connection, data) {
+/// Ok(_) -> websocket.Continue(state)
+/// Error(_) -> websocket.StopWithError("Failed to send binary")
+/// }
+/// }
+/// _ -> websocket.Continue(state)
+/// }
+/// },
+/// on_close: fn(_) { Nil },
+/// )
+/// ```
+///
+/// ## Returns
+///
+/// - `Ok(Nil)` - The message was sent successfully
+/// - `Error(WebSocketError)` - Failed to send the message
+///
+pub fn send_binary(
+ connection: Connection,
+ message: BitArray,
+) -> Result(Nil, WebSocketError) {
+ connection.send_binary(message)
+}
+
+@internal
+pub fn close_connection(connection: Connection) -> Result(Nil, WebSocketError) {
+ connection.close()
+}
+
+/// Create a new WebSocket handler.
+///
+/// This function defines the behavior of a WebSocket connection by providing
+/// three callbacks:
+///
+/// - `on_init`: Called when the connection is established. Return a tuple with
+/// the initial state for this connection and an optional process selector for
+/// handling custom messages.
+/// - `on_message`: Called when a message is received. Return `Continue(state)`
+/// to keep the connection open with updated state, `Stop` to close gracefully,
+/// or `StopWithError(reason)` to close with an error.
+/// - `on_close`: Called when the connection is closed. Use this for cleanup.
+///
+/// ## Example
+///
+/// ```gleam
+/// // A simple echo server that counts messages
+/// websocket.new(
+/// on_init: fn(_connection) {
+/// // Initialize with a count of 0, no custom selector
+/// #(0, option.None)
+/// },
+/// on_message: fn(count, message, connection) {
+/// case message {
+/// websocket.Text(text) -> {
+/// let new_count = count + 1
+/// let response = "Message #" <> int.to_string(new_count) <> ": " <> text
+/// case websocket.send_text(connection, response) {
+/// Ok(_) -> websocket.Continue(new_count)
+/// Error(_) -> websocket.StopWithError("Send failed")
+/// }
+/// }
+/// websocket.Binary(data) -> {
+/// websocket.send_binary(connection, data)
+/// websocket.Continue(count)
+/// }
+/// websocket.Closed | websocket.Shutdown -> {
+/// websocket.Stop
+/// }
+/// }
+/// },
+/// on_close: fn(count) {
+/// io.println("Connection closed after " <> int.to_string(count) <> " messages")
+/// },
+/// )
+/// ```
+///
+/// ## Usage with Wisp
+///
+/// Use this with `wisp.websocket` to handle WebSocket upgrade requests:
+///
+/// ```gleam
+/// pub fn handle_request(request: wisp.Request) -> wisp.Response {
+/// case wisp.path_segments(request) {
+/// ["ws"] -> {
+/// wisp.websocket(
+/// request,
+/// on_init: fn(_conn) { #(MyState(...), option.None) },
+/// on_message: handle_ws_message,
+/// on_close: fn(_state) { Nil },
+/// )
+/// }
+/// _ -> wisp.not_found()
+/// }
+/// }
+/// ```
+///
+pub fn new(
+ on_init: fn(Connection) -> #(state, Option(Selector(message))),
+ on_message: fn(state, Message(any), Connection) -> Next(state),
+ on_close: fn(state) -> Nil,
+) -> WebSocket(message, any) {
+ WebSocket(
+ init: fn(connection) {
+ let #(state, selector) = on_init(connection)
+ #(new_state(state, on_message, on_close), selector)
+ },
+ handle:,
+ close:,
+ )
+}
+
+fn handle(
+ state: State(any),
+ message: Message(any),
+ connection: Connection,
+) -> WebSocketResult(any) {
+ state.step(HandleMessage(message, connection))
+}
+
+fn close(state: State(any)) -> Nil {
+ case state.step(Close) {
+ _ -> Nil
+ }
+}
+
+fn new_state(
+ state: state,
+ on_message: fn(state, Message(any), Connection) -> Next(state),
+ on_close: fn(state) -> Nil,
+) -> State(any) {
+ WebSocketState(step: fn(action) {
+ case action {
+ HandleMessage(message, connection) -> {
+ case on_message(state, message, connection) {
+ Continue(n_state) ->
+ ContinueWith(new_state(n_state, on_message, on_close))
+ Stop -> StopNow
+ StopWithError(error) -> StopWithErrorResult(error)
+ }
+ }
+ Close -> {
+ on_close(state)
+ StopNow
+ }
+ }
+ })
+}
+
+@internal
+pub fn extract_callbacks(
+ ws: WebSocket(message, any),
+) -> #(
+ fn(Connection) -> #(State(any), Option(Selector(message))),
+ fn(State(any), Message(any), Connection) -> Next(State(any)),
+ fn(State(any)) -> Nil,
+) {
+ #(
+ ws.init,
+ fn(state, message, connection) {
+ ws.handle(state, message, connection)
+ |> result_to_next
+ },
+ ws.close,
+ )
+}
+
+fn result_to_next(result: WebSocketResult(any)) -> Next(State(any)) {
+ case result {
+ ContinueWith(state) -> Continue(state)
+ StopNow -> Stop
+ StopWithErrorResult(error) -> StopWithError(error)
+ }
+}
diff --git a/src/wisp/wisp_mist.gleam b/src/wisp/wisp_mist.gleam
index 5e80dec..cc763b6 100644
--- a/src/wisp/wisp_mist.gleam
+++ b/src/wisp/wisp_mist.gleam
@@ -8,6 +8,7 @@ import gleam/string
import mist
import wisp
import wisp/internal
+import wisp/websocket
//
// Running the server
@@ -43,18 +44,24 @@ pub fn handler(
fn(request: HttpRequest(_)) {
let connection =
internal.make_connection(mist_body_reader(request), secret_key_base)
- let request = request.set_body(request, connection)
+ let wisp_request = request.set_body(request, connection)
use <- exception.defer(fn() {
- let assert Ok(_) = wisp.delete_temporary_files(request)
+ let assert Ok(_) = wisp.delete_temporary_files(wisp_request)
})
- let response =
- request
- |> handler
- |> mist_response
+ let response = handler(wisp_request)
- response
+ case response.body {
+ wisp.WebSocket(upgrade) -> {
+ mist_websocket_upgrade(request, upgrade)
+ }
+ wisp.Text(text) ->
+ response.set_body(response, mist.Bytes(bytes_tree.from_string(text)))
+ wisp.Bytes(bytes) -> response.set_body(response, mist.Bytes(bytes))
+ wisp.File(path:, offset:, limit:) ->
+ response.set_body(response, mist_send_file(path, offset, limit))
+ }
}
}
@@ -79,16 +86,6 @@ fn wrap_mist_chunk(
})
}
-fn mist_response(response: wisp.Response) -> HttpResponse(mist.ResponseData) {
- let body = case response.body {
- wisp.Text(text) -> mist.Bytes(bytes_tree.from_string(text))
- wisp.Bytes(bytes) -> mist.Bytes(bytes)
- wisp.File(path:, offset:, limit:) -> mist_send_file(path, offset, limit)
- }
- response
- |> response.set_body(body)
-}
-
fn mist_send_file(
path: String,
offset: Int,
@@ -103,3 +100,60 @@ fn mist_send_file(
}
}
}
+
+fn mist_websocket_upgrade(
+ request: HttpRequest(mist.Connection),
+ upgrade: wisp.WebSocketUpgrade,
+) -> HttpResponse(mist.ResponseData) {
+ let ws = wisp.recover(upgrade)
+ let #(on_init, on_message, on_close) = websocket.extract_callbacks(ws)
+
+ mist.websocket(
+ request: request,
+ on_init: fn(connection) {
+ let wisp_connection =
+ websocket.make_connection(
+ fn(text) {
+ mist.send_text_frame(connection, text)
+ |> result.replace_error(websocket.SendFailed)
+ },
+ fn(binary) {
+ mist.send_binary_frame(connection, binary)
+ |> result.replace_error(websocket.SendFailed)
+ },
+ fn() { Ok(Nil) },
+ )
+
+ on_init(wisp_connection)
+ },
+ handler: fn(user_state, message, connection) {
+ let wisp_connection =
+ websocket.make_connection(
+ fn(text) {
+ mist.send_text_frame(connection, text)
+ |> result.replace_error(websocket.SendFailed)
+ },
+ fn(binary) {
+ mist.send_binary_frame(connection, binary)
+ |> result.replace_error(websocket.SendFailed)
+ },
+ fn() { Ok(Nil) },
+ )
+
+ let wisp_message = case message {
+ mist.Text(text) -> websocket.Text(text)
+ mist.Binary(binary) -> websocket.Binary(binary)
+ mist.Closed -> websocket.Closed
+ mist.Shutdown -> websocket.Shutdown
+ mist.Custom(inner) -> websocket.Custom(inner)
+ }
+ let result = on_message(user_state, wisp_message, wisp_connection)
+ case result {
+ websocket.Continue(new_state) -> mist.continue(new_state)
+ websocket.Stop -> mist.stop()
+ websocket.StopWithError(reason) -> mist.stop_abnormal(reason)
+ }
+ },
+ on_close:,
+ )
+}
diff --git a/test/wisp/simulate_test.gleam b/test/wisp/simulate_test.gleam
index f7d871f..e5faf29 100644
--- a/test/wisp/simulate_test.gleam
+++ b/test/wisp/simulate_test.gleam
@@ -1,3 +1,4 @@
+import gleam/erlang/process
import gleam/http
import gleam/http/response
import gleam/json
@@ -7,6 +8,7 @@ import gleam/string
import simplifile
import wisp
import wisp/simulate
+import wisp/websocket
pub fn request_test() {
let request = simulate.request(http.Patch, "/wibble/woo")
@@ -261,3 +263,72 @@ pub fn multipart_generation_validation_test() {
<> "--\r\n"
assert body == <>
}
+
+pub fn websocket_handler_test() {
+ let state_subject = process.new_subject()
+ let handler =
+ websocket.new(
+ fn(_conn) {
+ let initial_state = "Initial State"
+ process.send(state_subject, initial_state)
+ #(initial_state, option.None)
+ },
+ fn(state, message, connection) {
+ case message {
+ websocket.Text(text) -> {
+ let message = "Echo: " <> text
+ let new_state = state <> " | " <> message
+ let assert Ok(_) = websocket.send_text(connection, message)
+ process.send(state_subject, new_state)
+ websocket.Continue(new_state)
+ }
+ websocket.Binary(data) -> {
+ let new_state = state <> " | " <> "Binary"
+ let assert Ok(_) = websocket.send_binary(connection, data)
+ process.send(state_subject, new_state)
+ websocket.Continue(new_state)
+ }
+ websocket.Closed | websocket.Shutdown -> {
+ websocket.Stop
+ }
+ websocket.Custom(_) -> {
+ let new_state = state <> " | Custom"
+ process.send(state_subject, new_state)
+ websocket.Continue(new_state)
+ }
+ }
+ },
+ fn(state) { process.send(state_subject, state) },
+ )
+
+ let assert Ok(websocket) = simulate.create_websocket(handler)
+ let assert Ok("Initial State") = process.receive(state_subject, 1000)
+
+ let assert Ok(websocket) = simulate.send_websocket_text(websocket, "Hello")
+ let assert [] = simulate.websocket_sent_binary_messages(websocket)
+ let assert ["Echo: Hello"] = simulate.websocket_sent_text_messages(websocket)
+ let assert Ok("Initial State | Echo: Hello") =
+ process.receive(state_subject, 1000)
+
+ let assert Ok(websocket) =
+ simulate.send_websocket_binary(websocket, <<1, 2, 3>>)
+ let assert [<<1, 2, 3>>] = simulate.websocket_sent_binary_messages(websocket)
+ let assert ["Echo: Hello"] = simulate.websocket_sent_text_messages(websocket)
+ let assert Ok("Initial State | Echo: Hello | Binary") =
+ process.receive(state_subject, 1000)
+
+ let assert Ok(Nil) = simulate.close_websocket(websocket)
+ let assert Ok("Initial State | Echo: Hello | Binary") =
+ process.receive(state_subject, 1000)
+
+ let assert Ok(websocket) =
+ simulate.send_websocket_binary(websocket, <<4, 5, 6>>)
+ let assert [<<1, 2, 3>>] = simulate.websocket_sent_binary_messages(websocket)
+
+ let websocket = simulate.reset_websocket(websocket)
+ let assert Ok("Initial State") = process.receive(state_subject, 1000)
+ let assert Ok(websocket) =
+ simulate.send_websocket_binary(websocket, <<6, 7, 8>>)
+ let assert [<<6, 7, 8>>] = simulate.websocket_sent_binary_messages(websocket)
+ let assert Ok("Initial State | Binary") = process.receive(state_subject, 1000)
+}