Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

websockets #72

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mist = ">= 0.13.0 and < 2.0.0"
simplifile = ">= 1.4.0 and != 1.6.0 and < 2.0.0"
marceau = ">= 1.1.0 and < 2.0.0"
logging = ">= 1.0.0 and < 2.0.0"
gleam_otp = ">= 0.10.0 and < 1.0.0"

[dev-dependencies]
gleeunit = "~> 1.0"
1 change: 1 addition & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ gleam_crypto = { version = ">= 1.0.0 and < 2.0.0" }
gleam_erlang = { version = ">= 0.21.0 and < 2.0.0" }
gleam_http = { version = ">= 3.5.0 and < 4.0.0" }
gleam_json = { version = ">= 0.6.0 and < 2.0.0" }
gleam_otp = { version = ">= 0.10.0 and < 1.0.0"}
gleam_stdlib = { version = ">= 0.29.0 and < 2.0.0" }
gleeunit = { version = "~> 1.0" }
logging = { version = ">= 1.0.0 and < 2.0.0" }
Expand Down
118 changes: 116 additions & 2 deletions src/wisp.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import gleam/dict.{type Dict}
import gleam/dynamic.{type Dynamic}
import gleam/erlang
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}
Expand All @@ -17,6 +18,7 @@ import gleam/int
import gleam/json
import gleam/list
import gleam/option.{type Option}
import gleam/otp/actor
import gleam/result
import gleam/string
import gleam/string_builder.{type StringBuilder}
Expand Down Expand Up @@ -52,7 +54,12 @@ pub fn mist_handler(
secret_key_base: String,
) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) {
fn(request: HttpRequest(_)) {
let connection = make_connection(mist_body_reader(request), secret_key_base)
let connection =
make_connection(
mist_body_reader(request),
secret_key_base,
Socket(request.body),
)
let request = request.set_body(request, connection)

use <- exception.defer(fn() {
Expand Down Expand Up @@ -95,6 +102,7 @@ fn mist_response(response: Response) -> HttpResponse(mist.ResponseData) {
Text(text) -> mist.Bytes(bytes_builder.from_string_builder(text))
Bytes(bytes) -> mist.Bytes(bytes)
File(path) -> mist_send_file(path)
Websocket(x) -> mist.Websocket(x)
}
response
|> response.set_body(body)
Expand Down Expand Up @@ -145,6 +153,8 @@ pub type Body {
/// in place of any with an empty body.
///
Empty
/// An newly established websocket connection.
Websocket(process.Selector(process.ProcessDown))
bgwdotdev marked this conversation as resolved.
Show resolved Hide resolved
}

/// An alias for a HTTP response containing a `Body`.
Expand Down Expand Up @@ -605,10 +615,15 @@ pub opaque type Connection {
read_chunk_size: Int,
secret_key_base: String,
temporary_directory: String,
socket: Socket,
)
}

fn make_connection(body_reader: Reader, secret_key_base: String) -> Connection {
fn make_connection(
body_reader: Reader,
secret_key_base: String,
socket: Socket,
) -> Connection {
// TODO: replace `/tmp` with appropriate for the OS
let prefix = "/tmp/gleam-wisp/"
let temporary_directory = join_path(prefix, random_slug())
Expand All @@ -619,6 +634,7 @@ fn make_connection(body_reader: Reader, secret_key_base: String) -> Connection {
read_chunk_size: 1_000_000,
temporary_directory: temporary_directory,
secret_key_base: secret_key_base,
socket: socket,
)
}

Expand Down Expand Up @@ -1832,5 +1848,103 @@ pub fn create_canned_connection(
make_connection(
fn(_size) { Ok(Chunk(body, fn(_size) { Ok(ReadingFinished) })) },
secret_key_base,
NoSocket,
)
}

//
// Websockets
//

// TODO(bgw): doc more once fleshed out

/// The messages possible to receive to and from a websocket.
pub type WebsocketMessage(a) {
WSText(String)
WSBinary(BitArray)
WSClosed
WSShutdown
WSCustom(a)
bgwdotdev marked this conversation as resolved.
Show resolved Hide resolved
}

fn from_mist_websocket_message(
msg: mist.WebsocketMessage(a),
) -> WebsocketMessage(a) {
case msg {
mist.Text(x) -> WSText(x)
mist.Binary(x) -> WSBinary(x)
mist.Closed -> WSClosed
mist.Shutdown -> WSShutdown
mist.Custom(x) -> WSCustom(x)
}
}

pub opaque type WebsocketConnection {
WebsocketConnection(mist.WebsocketConnection)
bgwdotdev marked this conversation as resolved.
Show resolved Hide resolved
}

/// Sends text to a websocket connection
pub fn send_text(connection: WebsocketConnection, text: String) {
let conn = case connection {
WebsocketConnection(conn) -> conn
}
mist.send_text_frame(conn, text)
}

/// Sends binary data to a websocket connection
pub fn send_binary(connection: WebsocketConnection, binary: BitArray) {
let conn = case connection {
WebsocketConnection(conn) -> conn
}
mist.send_binary_frame(conn, binary)
}

pub opaque type Socket {
Socket(mist.Connection)
// TODO: can delete if we handle create_canned_connection somehow?
NoSocket
}

// TODO: heavily doc this
pub fn websocket(
req: Request,
handler handler: fn(a, WebsocketConnection, WebsocketMessage(b)) ->
actor.Next(b, a),
on_init on_init: fn(WebsocketConnection) -> #(a, Option(process.Selector(b))),
on_close on_close: fn(a) -> Nil,
) -> Response {
bgwdotdev marked this conversation as resolved.
Show resolved Hide resolved
let handler = fn(
state: a,
conn: mist.WebsocketConnection,
msg: mist.WebsocketMessage(b),
) {
let msg = msg |> from_mist_websocket_message
let conn = WebsocketConnection(conn)
handler(state, conn, msg)
}
let on_init = fn(conn: mist.WebsocketConnection) {
let conn = WebsocketConnection(conn)
on_init(conn)
}
mist_websocket(req, handler, on_init, on_close)
}

fn mist_websocket(
req: Request,
handler handler: fn(a, mist.WebsocketConnection, mist.WebsocketMessage(b)) ->
actor.Next(b, a),
on_init on_init: fn(mist.WebsocketConnection) ->
#(a, Option(process.Selector(b))),
on_close on_close: fn(a) -> Nil,
) -> Response {
let assert Socket(x) = req.body.socket
let req = request.set_body(req, x)
let resp = mist.websocket(req, handler, on_init(_), on_close)
case resp.status, resp.body {
200, mist.Websocket(x) ->
ok()
|> set_body(Websocket(x))
400, _ -> bad_request()
_, _ -> internal_server_error()
}
}
6 changes: 3 additions & 3 deletions src/wisp/testing.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import gleam/string
import gleam/string_builder
import gleam/uri
import simplifile
import wisp.{type Request, type Response, Bytes, Empty, File, Text}
import wisp.{type Request, type Response, Bytes, Empty, File, Text, Websocket}

/// The default secret key base used for test requests.
/// This should never be used outside of tests.
Expand Down Expand Up @@ -226,7 +226,7 @@ pub fn patch_json(
///
pub fn string_body(response: Response) -> String {
case response.body {
Empty -> ""
Empty | Websocket(_) -> ""
Text(builder) -> string_builder.to_string(builder)
Bytes(bytes) -> {
let data = bytes_builder.to_bit_array(bytes)
Expand All @@ -249,7 +249,7 @@ pub fn string_body(response: Response) -> String {
///
pub fn bit_array_body(response: Response) -> BitArray {
case response.body {
Empty -> <<>>
Empty | Websocket(_) -> <<>>
Bytes(builder) -> bytes_builder.to_bit_array(builder)
Text(builder) ->
bytes_builder.to_bit_array(bytes_builder.from_string_builder(builder))
Expand Down