Packages

Gleam Bindings to browser WebSocket client

Current section

Files

Jump to
stratocumulus src stratocumulus.gleam
Raw

src/stratocumulus.gleam

import gleam/dynamic.{type Dynamic}
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/uri.{type Uri}
pub type WebSocket
/// Builders are used to construct the desired WebSocket. Use `new`, `protocols`
/// and `open` to create your `Builder` and opening your `WebSocket`.
pub opaque type Builder {
Builder(
uri: Uri,
protocols: List(String),
on_open: Option(fn(Dynamic) -> Nil),
on_close: Option(fn(Dynamic) -> Nil),
on_error: Option(fn(Dynamic) -> Nil),
on_text: Option(fn(String, Dynamic) -> Nil),
on_bytes: Option(fn(BitArray, Dynamic) -> Nil),
)
}
/// Return the number of bytes of data that have been queued using calls to
/// `send` but not yet transmitted to the network. This value resets to zero
/// once all queued data has been sent. This value does not reset to zero when
/// the connection is closed; if you keep calling `send`,
/// this will continue to climb.
@external(javascript, "./stratocumulus.ffi.mjs", "bufferedAmount")
pub fn buffered_amount(websocket: WebSocket) -> Int
/// Return the extensions selected by the server. This is currently only the
/// empty string or a list of extensions as negotiated by the connection.
@external(javascript, "./stratocumulus.ffi.mjs", "extensions")
pub fn extensions(websocket: WebSocket) -> String
/// Return the name of the sub-protocol the server selected; this will be
/// one of the strings specified in the protocols parameter when creating the
/// WebSocket, or the empty string if no connection is established.
@external(javascript, "./stratocumulus.ffi.mjs", "protocol")
pub fn protocol(websocket: WebSocket) -> String
/// State of the WebSocket. A WebSocket is _always_ in one of those four states.
pub type ReadyState {
/// Socket has been created. The connection is not yet open.
Connecting
/// The connection is open and ready to communicate.
Open
/// The connection is in the process of closing.
Closing
/// The connection is closed or couldn't be opened.
Closed
}
/// Return the current state of the WebSocket connection.
@external(javascript, "./stratocumulus.ffi.mjs", "readyState")
pub fn ready_state(websocket: WebSocket) -> ReadyState
/// Return the absolute URI of the WebSocket as resolved by the WebSocket itself.
pub fn uri(websocket: WebSocket) -> Uri {
let url = url(websocket)
let assert Ok(uri) = uri.parse(url)
uri
}
/// Init a new WebSocket Builder. A Builder is the equivalent of a blueprint,
/// on which we can spawn an infinite amount of WebSockets.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// let assert Ok(ws) = stratocumulus.new(endpoint) |> stratocumulus.open
/// let assert Ok(_) = stratocumulus.send(ws, "Hello world!")
/// ```
pub fn new(uri: Uri) -> Builder {
Builder(
uri:,
protocols: [],
on_open: None,
on_close: None,
on_error: None,
on_text: None,
on_bytes: None,
)
}
/// A single string or an array of strings representing the
/// [sub-protocol(s)](https://developer.mozilla.org/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#subprotocols)
/// that the client would like to use, in order of preference. If it is omitted,
/// an empty list is used by default, i.e., `[]`.
///
/// A single server can implement multiple WebSocket sub-protocols, and handle
/// different types of interactions depending on the specified value. Note
/// however that only one sub-protocol can be selected per connection.
///
/// The allowed values are those that can be specified in the
/// [`Sec-WebSocket-Protocol`](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Sec-WebSocket-Protocol)
/// HTTP header. These are values selected from the
/// [IANA WebSocket Subprotocol Name Registry](https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name),
/// such as `soap`, `wamp`, `ship` and so on, or may be a custom name jointly
/// understood by the client and the server.
///
/// [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols)
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// stratocumulus.new(endpoint)
/// |> stratocumulus.protocols(["soap"])
/// |> stratocumulus.open
/// ```
pub fn protocols(builder: Builder, protocols: List(String)) -> Builder {
Builder(..builder, protocols:)
}
/// Create & open the WebSocket from its Builder. This is the equivalent to
/// creating the WebSocket with `new WebSocket` in JavaScript.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// stratocumulus.new(endpoint)
/// |> stratocumulus.protocols(["soap"])
/// |> stratocumulus.open
/// ```
pub fn open(builder: Builder) -> Result(WebSocket, OpenError) {
let endpoint = uri.to_string(builder.uri)
create(endpoint, builder.protocols)
|> maybe(builder.on_open, fn(ws, l) { add_event_listener(ws, "open", l) })
|> maybe(builder.on_close, fn(ws, l) { add_event_listener(ws, "close", l) })
|> maybe(builder.on_error, fn(ws, l) { add_event_listener(ws, "error", l) })
|> maybe(builder.on_text, fn(ws, l) { add_text_listener(ws, l) })
|> maybe(builder.on_bytes, fn(ws, l) { add_bit_array_listener(ws, l) })
}
pub type OpenError {
/// Happens when
/// - `uri` has a scheme other than ws, wss, http, or https
/// - `uri` has a [fragment](https://developer.mozilla.org/docs/Web/URI/Reference/Fragment)
/// - any of the values in `protocols` occur more than once, or otherwise fail
/// to match the requirements for elements that comprise the value of
/// [`Sec-WebSocket-Protocol`](https://developer.mozilla.org/docs/Web/HTTP/Guides/Protocol_upgrade_mechanism#sec-websocket-protocol) fields
/// as defined by the WebSocket Protocol specification
OpenSyntaxError
}
/// Close an opened WebSocket.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// let assert Ok(ws) = stratocumulus.new(endpoint) |> stratocumulus.open
/// let assert Ok(_) = stratocumulus.close(ws, code: 1000, reason: "normal")
/// ```
@external(javascript, "./stratocumulus.ffi.mjs", "close")
pub fn close(
websocket: WebSocket,
code code: Int,
reason reason: String,
) -> Result(Nil, CloseError)
/// Possible errors that can be received after calling [`close`](#close).
pub type CloseError {
/// `code` is neither an integer equal to `1000`
/// nor an integer in the range `3000``4999`.
InvalidAccessError
/// The UTF-8-encoded `reason` value is longer than 123 bytes.
ReasonSyntaxError
}
/// Send a text frame on the WebSocket.
/// The WebSocket will be returned on success.
/// An error will be returned in case the WebSocket state is `Connecting`.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// let assert Ok(ws) = stratocumulus.new(endpoint) |> stratocumulus.open
/// let assert Ok(_) = stratocumulus.send(ws, "Hello world!")
/// ```
@external(javascript, "./stratocumulus.ffi.mjs", "send")
pub fn send(ws: WebSocket, content: String) -> Result(WebSocket, SendError)
/// Possible errors that can be received after calling [`send`](#send)
/// or [`send_bytes`](#send_bytes).
pub type SendError {
/// Thrown if [`ready_state`](#ready_state) is `Connecting`.
InvalidStateError
}
/// Send a binary frame on the WebSocket.
/// The WebSocket will be returned on success.
/// An error will be returned in case the WebSocket state is `Connecting`.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// let assert Ok(ws) = stratocumulus.new(endpoint) |> stratocumulus.open
/// let assert Ok(_) = stratocumulus.send(ws, <<"Hello world!">>)
/// ```
@external(javascript, "./stratocumulus.ffi.mjs", "send")
pub fn send_bytes(
websocket: WebSocket,
content: BitArray,
) -> Result(WebSocket, SendError)
/// Subscribe to the `"open"` event. `"open"` will be emitted after the Socket
/// has been opened with the remote server. The argument received is a generic
/// [`Event`](https://developer.mozilla.org/docs/Web/API/Event).
/// `on_open` returns the original WebSocket to continue chaining commands on
/// the WebSocket.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// stratocumulus.new(endpoint)
/// |> stratocumulus.on_open(fn (data) { Nil })
/// |> stratocumulus.on_close(fn (data) { Nil })
/// |> stratocumulus.on_error(fn (data) { Nil })
/// ```
pub fn on_open(builder: Builder, on_open: fn(Dynamic) -> Nil) -> Builder {
let on_open = Some(on_open)
Builder(..builder, on_open:)
}
/// Subscribe to the `"close"` event. `"close"` will be emitted after the Socket
/// has been closed with the remote server. The argument received is a
/// [`CloseEvent`](https://developer.mozilla.org/docs/Web/API/CloseEvent).
/// `on_close` returns the original WebSocket to continue chaining commands on
/// the WebSocket.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// stratocumulus.new(endpoint)
/// |> stratocumulus.on_open(fn (data) { Nil })
/// |> stratocumulus.on_close(fn (data) { Nil })
/// |> stratocumulus.on_error(fn (data) { Nil })
/// ```
pub fn on_close(builder: Builder, on_close: fn(Dynamic) -> Nil) -> Builder {
let on_close = Some(on_close)
Builder(..builder, on_close:)
}
/// Subscribe to the `"error"` event. `"error"` will be emitted when an error
/// has occur, like some data that could not be sent. The argument received is a
/// generic [`Event`](https://developer.mozilla.org/docs/Web/API/Event).
/// `on_error` returns the original WebSocket to continue chaining commands on
/// the WebSocket.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// stratocumulus.new(endpoint)
/// |> stratocumulus.on_open(fn (data) { Nil })
/// |> stratocumulus.on_close(fn (data) { Nil })
/// |> stratocumulus.on_error(fn (data) { Nil })
/// ```
pub fn on_error(builder: Builder, on_error: fn(Dynamic) -> Nil) -> Builder {
let on_error = Some(on_error)
Builder(..builder, on_error:)
}
/// Subscribe to text messages received. Everytime the WebSocket receives
/// a textual message, an event is received with the text as content.
/// The second argument is the event itself, in case it is needed for other usages.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// stratocumulus.new(endpoint)
/// |> stratocumulus.on_open(fn (data) { Nil })
/// |> stratocumulus.on_close(fn (data) { Nil })
/// |> stratocumulus.on_error(fn (data) { Nil })
/// |> stratocumulus.on_text(fn (content: String, event) { Nil })
/// ```
pub fn on_text(builder: Builder, on_text: fn(String, Dynamic) -> Nil) -> Builder {
let on_text = Some(on_text)
Builder(..builder, on_text:)
}
/// Subscribe to bytes messages received. Everytime the WebSocket receives
/// a binary message, an event is received with the bytes as content.
/// The second argument is the event itself, in case it is needed for other usages.
///
/// ```gleam
/// let assert Ok(endpoint) = uri.parse("...")
/// stratocumulus.new(endpoint)
/// |> stratocumulus.on_open(fn (data) { Nil })
/// |> stratocumulus.on_close(fn (data) { Nil })
/// |> stratocumulus.on_error(fn (data) { Nil })
/// |> stratocumulus.on_bytes(fn (content: BitArray, event) { Nil })
/// ```
pub fn on_bytes(
builder: Builder,
on_bytes: fn(BitArray, Dynamic) -> Nil,
) -> Builder {
let on_bytes = Some(on_bytes)
Builder(..builder, on_bytes:)
}
// Internal
@external(javascript, "./stratocumulus.ffi.mjs", "addStringMessageListener")
fn add_text_listener(
websocket: WebSocket,
handler: fn(String, Dynamic) -> Nil,
) -> WebSocket
@external(javascript, "./stratocumulus.ffi.mjs", "addBitArrayMessageListener")
fn add_bit_array_listener(
websocket: WebSocket,
handler: fn(BitArray, Dynamic) -> Nil,
) -> WebSocket
@external(javascript, "./stratocumulus.ffi.mjs", "addEventListener")
fn add_event_listener(
websocket: WebSocket,
event: String,
handler: fn(Dynamic) -> Nil,
) -> WebSocket
@external(javascript, "./stratocumulus.ffi.mjs", "create")
fn create(url: String, protocols: List(String)) -> Result(WebSocket, OpenError)
@external(javascript, "./stratocumulus.ffi.mjs", "url")
fn url(websocket: WebSocket) -> String
fn maybe(
websocket: Result(WebSocket, OpenError),
data: Option(a),
mapper: fn(WebSocket, a) -> WebSocket,
) -> Result(WebSocket, OpenError) {
use websocket <- result.map(websocket)
case data {
None -> websocket
Some(data) -> mapper(websocket, data)
}
}