Current section
Files
Jump to
Current section
Files
src/smol.gleam
//// <style>
//// small {
//// font-size: 0.7em;
//// opacity: 0.75;
//// }
//// h4 {
//// margin-bottom: 0;
//// + p {
//// margin-top: 0;
//// }
//// }
//// </style>
//// <script>
//// (callback => document.readyState !== 'loading' ? callback() : document.addEventListener('DOMContentLoaded', callback, { once: true }))(() => {
//// const list = document.querySelector('.sidebar > ul:last-of-type')
//// const sortedLists = document.createDocumentFragment()
//// const sortedMembers = document.createDocumentFragment()
////
//// for (const header of document.querySelectorAll('main > h4')) {
//// sortedLists.append((() => {
//// const node = document.createElement('h3')
//// node.append(header.textContent)
//// return node
//// })())
//// sortedMembers.append((() => {
//// const node = document.createElement('h2')
//// node.append(header.textContent)
//// return node
//// })())
////
//// const sortedList = document.createElement('ul')
//// sortedLists.append(sortedList)
////
//// for (const anchor of header.nextElementSibling.querySelectorAll('a')) {
//// const href = anchor.getAttribute('href')
//// const member = document.querySelector(`.member:has(h2 > a[href="${href}"])`)
//// const sidebar = list.querySelector(`li:has(a[href="${href}"])`)
//// sortedList.append(sidebar)
//// sortedMembers.append(member)
//// }
//// }
////
//// document.querySelector('.sidebar').insertBefore(sortedLists, list)
//// document.querySelector('.module-members:has(#module-functions)').insertBefore(sortedMembers, document.querySelector('#module-functions').nextSibling)
//// })
//// </script>
////
//// **Tip:** Hover the links for short summaries!
////
//// #### Builder
//// [new](#new "Create a new builder for a handler"),
//// [port](#port "Set the port"),
//// [random_port](#random_port "Choose a random port"),
//// [bind](#bind "Bind to a network interface"),
//// [bind_all](#bind_all "Bind to all network interfaces"),
//// [after_start](#after_start "Set a callback once the server is started")
////
//// #### Server Operations
//// [start](#start "Start the server")
//// [get_port](#get_port "Get the port the server is listening on"),
//// [get_interface](#get_interface "Get the interface the server is listening on")
//// [stop](#stop "Stop the server")
////
//// #### Request Handling
//// [read_bytes_tree](#read_bytes_tree "Read the body as a BytesTree"),
//// [read_bytes](#read_bytes "Read the body as a BitArray"),
//// [read_string](#read_string "Read the body as a String"),
//// [read_json](#read_json "Read and decode the body as JSON")
////
//// #### Response Creation
//// [send_file](#send_file "Send a file response"),
//// [send_bytes](#send_bytes "Send binary data"),
//// [send_chunked](#send_chunked "Send a streamed rseponse"),
//// [send_html](#send_html "Send html response"),
//// [send_json](#send_json "Send JSON response"),
//// [send_string](#send_string "Send text response"),
//// [send_status](#send_status "Send a status code response"),
//// [server_sent_events](#server_sent_events "Send a SSE stream response"),
//// [websocket](#websocket "Open a websocket connection"),
//// [send](#send "Helper for chunked/SSE/Websocket responses"),
//// [step](#step "Helper for chunked/SSE/Websocket responses"),
//// [close](#close "Helper for chunked/SSE/Websocket responses"),
//// [dispatch](#dispatch "Send a message to a running websocket")
////
//// #### Advanced
//// [adapt](#adapt "Convert a Gleam handler to a fetch handler"),
//// [detect_runtime](#detect_runtime "Detect JavaScript runtime environment"),
//// [status_text](#status_text "Convert a status code into a string")
import filepath
import gleam/bit_array
import gleam/bytes_tree.{type BytesTree}
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode.{type Decoder}
import gleam/http/request.{type Request}
import gleam/http/response.{type Response, Response}
import gleam/int
import gleam/io
import gleam/javascript/promise.{type Promise}
import gleam/json.{type Json}
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
import marceau
// -- BUILDER ------------------------------------------------------------------
pub opaque type Builder {
Builder(
port: Int,
interface: String,
handler: fn(Request(ReadableStream)) -> Promise(Response(ReadableStream)),
after_start: fn(Int, String) -> Nil,
)
}
/// Creates a new server builder with the given request handler.
///
/// The server listens on `http://localhost:4000` by default. See the examples
/// pages for example on how to use this function.
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn new(
handler: fn(Request(ReadableStream)) -> Promise(Response(ReadableStream)),
) -> Builder {
Builder(
port: 4000,
interface: "127.0.0.1",
handler: handler,
after_start: default_after_start,
)
}
fn default_after_start(port: Int, interface: String) {
io.println(
"smol: Listening on http://" <> interface <> ":" <> int.to_string(port),
)
}
/// Sets the port for the server to listen on.
///
/// By default smol tries to listen on port `4000`.
///
/// ## Example
///
/// ```gleam
/// smol.new(handler)
/// |> smol.port(8080)
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn port(builder: Builder, port: Int) -> Builder {
Builder(..builder, port:)
}
/// Configures the server to use a random available port.
///
/// Useful for testing to avoid port conflicts.
///
/// ## Example
///
/// ```gleam
/// smol.new(handler)
/// |> smol.random_port()
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn random_port(builder: Builder) -> Builder {
Builder(..builder, port: 0)
}
/// Sets the network interface to listen on.
///
/// By default smol listens on `"127.0.0.1"` (localhost).
///
/// ## Example
///
/// ```gleam
/// smol.new(handler)
/// |> smol.bind("192.168.1.100")
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn bind(builder: Builder, interface: String) -> Builder {
Builder(..builder, interface:)
}
/// Configures the server to listen on all network interfaces (`0.0.0.0`).
///
/// ## Example
///
/// ```gleam
/// smol.new(handler)
/// |> smol.bind_all()
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn bind_all(builder: Builder) -> Builder {
Builder(..builder, interface: "0.0.0.0")
}
/// Sets a callback function to be called after the server starts.
///
/// The function receives the actual port and interface the server is listening
/// on. By default, a simple `Listening on...` message is printed.
///
/// ## Example
///
/// ```gleam
/// fn log_start(port, interface) {
/// io.println("Server started on " <> interface <> ":" <> int.to_string(port))
/// }
///
/// smol.new(handler)
/// |> smol.after_start(log_start)
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn after_start(
builder: Builder,
after_start: fn(Int, String) -> Nil,
) -> Builder {
Builder(..builder, after_start:)
}
// -- FETCH REQUEST RESPONSE BODIES --------------------------------------------
/// smol uses a standard web `ReadableStream` under the hood on all runtimes,
/// including Node.js!
pub type ReadableStream
pub type ReadError {
ExcessBody
MalformedBody
}
pub type FileError {
IsDir
NoAccess
NoEntry
UnknownFileError
RuntimeNotSupportedFileError
}
/// Reads the request body as a BytesTree.
///
/// The `max_body_limit` parameter sets a limit on the size of the body that
/// can be read.
///
/// ### Example
///
/// ```gleam
/// use body <- promise.await(smol.read_bytes_tree(request, 1024 * 1024))
/// case body {
/// Ok(bytes_tree) -> {
/// // ... do something with the body ...
/// let response = smol.send_string("Received bytes")
/// promise.resolve(response)
/// }
/// Error(error) -> {
/// let response = smol.send_status(400)
/// promise.resolve(response)
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
@external(javascript, "./smol.ffi.mjs", "read")
pub fn read_bytes_tree(
request: Request(ReadableStream),
max_body_limit: Int,
) -> Promise(Result(BytesTree, ReadError))
/// Reads the request body as a BitArray.
///
/// The `max_body_limit` parameter sets a limit on the size of the body that
/// can be read.
///
///
/// ## Example
///
/// ```gleam
/// // a simple echo server!
/// use body <- promise.await(smol.read_bytes(request, max_body_limit: 1024 * 1024))
/// case body {
/// Ok(bytes) -> {
/// let response = smol.send_bytes(bytes)
/// promise.resolve(response)
/// }
/// Error(error) -> {
/// let response = smol.send_status(400)
/// promise.resolve(response)
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn read_bytes(
request: Request(ReadableStream),
max_body_limit max_body_limit: Int,
) -> Promise(Result(BitArray, ReadError)) {
use body <- try(read_bytes_tree(request, max_body_limit))
return(bytes_tree.to_bit_array(body))
}
/// Reads the request body as a UTF-8 encoded String.
///
/// The `max_body_limit` parameter sets a limit on the size of the body that
/// can be read.
///
/// ## Example
///
/// ```gleam
/// use body <- promise.await(smol.read_string(request, max_body_limit: 1024 * 1024))
/// case body {
/// Ok(text) -> {
/// let response = smol.send_string("You sent: " <> text)
/// promise.resolve(response)
/// }
/// Error(error) -> {
/// let response = smol.send_status(400)
/// promise.resolve(response)
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn read_string(
request: Request(ReadableStream),
max_body_limit max_body_limit: Int,
) -> Promise(Result(String, ReadError)) {
use body <- try(read_bytes(request, max_body_limit))
case bit_array.to_string(body) {
Ok(text) -> return(text)
Error(_) -> fail(MalformedBody)
}
}
/// Reads and decodes a JSON request body.
///
/// The `decoder` parameter is used to decode the JSON into a specific type.
/// The `max_body_limit` parameter sets a limit on the size of the body that
/// can be read.
///
/// ## Example
///
/// ```gleam
/// type User {
/// User(name: String, age: Int)
/// }
///
/// fn user_decoder() {
/// use name <- decode.field("name", decode.string)
/// use age <- decode.field("age", decode.int)
/// decode.success(User(name:, age:))
/// }
///
/// fn handler(request) {
/// use user <- promise.await(smol.read_json(
/// request,
/// using: user_decoder(),
/// max_body_limit: 1024 * 1024,
/// ))
///
/// case user {
/// Ok(user) -> {
/// // Process the user
/// let response = smol.send_string("Hello, " <> user.name)
/// promise.resolve(response)
/// }
/// Error(error) -> {
/// let response = smol.send_status(400)
/// promise.resolve(response)
/// }
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn read_json(
request: Request(ReadableStream),
using decoder: Decoder(a),
max_body_limit max_body_limit: Int,
) -> Promise(Result(a, json.DecodeError)) {
use body <- promise.await(read_string(request, max_body_limit))
case body {
Ok(body) ->
case json.parse(from: body, using: decoder) {
Error(error) -> fail(error)
Ok(result) -> return(result)
}
Error(_) -> fail(json.UnexpectedEndOfInput)
}
}
/// Sends a file as the response.
///
/// The `path` parameter is the file path to send. The path will be joined to
/// the current working directory. Make sure the path cannot escape the folder
/// you are trying to serve from!
///
/// The `offset` parameter specifies the starting byte offset (default 0).
/// The `limit` parameter specifies the maximum number of bytes to send
/// (default is the entire file).
///
/// `Content-Length` and `Content-Type` headers will be set automatically based
/// on the file name and size.
///
/// ## Example
///
/// ```gleam
/// fn handler(request) {
/// use response <- promise.await(smol.send_file(
/// "./priv/public/index.html",
/// offset: 0,
/// limit: option.None,
/// ))
///
/// case response {
/// Ok(response) -> promise.resolve(response)
/// Error(error) -> promise.resolve(smol.send_status(404))
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn send_file(
path: String,
offset offset: Int,
limit limit: Option(Int),
) -> Promise(Result(Response(ReadableStream), FileError)) {
let limit = option.unwrap(limit, 0)
use file <- try(from_file(path, offset, limit))
let content_length = case limit > 0 {
True -> int.clamp(file.size - offset, 0, limit)
False -> int.max(file.size - offset, 0)
}
let content_type = case file.mime {
option.None ->
filepath.extension(path)
|> result.map(marceau.extension_to_mime_type)
|> result.unwrap("application/octet-stream")
option.Some(mime) -> mime
}
return(
Response(status: 200, body: file.stream, headers: [
#("content-length", int.to_string(content_length)),
#("content-type", content_type),
]),
)
}
@internal
pub type File {
File(stream: ReadableStream, size: Int, mime: option.Option(String))
}
@external(javascript, "./smol.ffi.mjs", "from_file")
fn from_file(
path: String,
offset: Int,
limit: Int,
) -> Promise(Result(File, FileError))
/// Creates a response with binary data.
///
/// The response will have content-type `application/octet-stream`.
///
/// ## Example
///
/// ```gleam
/// fn handler(request) {
/// let binary_data = <<1, 2, 3, 4, 5>>
/// let response = smol.send_bytes(binary_data)
/// promise.resolve(response)
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn send_bytes(bytes: BitArray) -> Response(ReadableStream) {
Response(
status: 200,
headers: [
#("content-type", "application/octet-stream"),
#("content-length", int.to_string(bit_array.byte_size(bytes))),
],
body: from_bytes(bytes),
)
}
/// Creates a plain text response with the given string.
///
/// The response will have context-type: `text/plain; charset=utf-8`.
///
///
/// ## Example
///
/// ```gleam
/// fn handler(request) {
/// let response = smol.send_string("Hello, World!")
/// promise.resolve(response)
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn send_string(text: String) -> Response(ReadableStream) {
let body = <<text:utf8>>
Response(
status: 200,
headers: [
#("content-type", "text/plain; charset=utf-8"),
#("content-length", int.to_string(bit_array.byte_size(body))),
],
body: from_bytes(body),
)
}
/// Creates a html response with the given string.
///
/// The response will have context-type: `text/html; charset=utf-8`.
///
///
/// ## Example
///
/// ```gleam
/// fn handler(request) {
/// let response = smol.send_html("<!doctype html><h1>Hello, World!")
/// promise.resolve(response)
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
pub fn send_html(text: String) -> Response(ReadableStream) {
let body = <<text:utf8>>
Response(
status: 200,
headers: [
#("content-type", "text/html; charset=utf-8"),
#("content-length", int.to_string(bit_array.byte_size(body))),
],
body: from_bytes(body),
)
}
/// A small helper to create a response with the given status code and a
/// standard status message.
///
/// ## Example
///
/// ```gleam
/// fn handler(request) {
/// let response = smol.send_status(404) // Response with "Not Found" body
/// promise.resolve(response)
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn send_status(status: Int) -> Response(ReadableStream) {
let body = <<status_text(status):utf8>>
Response(
status:,
headers: [
#("content-type", "text/plain; charset=utf-8"),
#("content-length", int.to_string(bit_array.byte_size(body))),
],
body: from_bytes(body),
)
}
/// Creates a JSON response with the given JSON data.
///
/// The response will have Content-Type: application/json; charset=utf-8.
///
/// ```gleam
/// fn handler(request) {
/// let user = User(name: "Ada", age: 4)
/// let data = json.object([
/// #("name", json.string(user.name)),
/// #("age", json.int(user.age)),
/// ])
///
/// let response = smol.send_json(data)
/// promise.resolve(response)
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn send_json(json: Json) -> Response(ReadableStream) {
let body = <<json.to_string(json):utf8>>
Response(
status: 200,
headers: [
#("content-type", "application/json; charset=utf-8"),
#("content-length", int.to_string(bit_array.byte_size(body))),
],
body: from_bytes(body),
)
}
/// A type used in various functions as a return type from "unfold" or "update"
/// - style functions, usually used in combination with a `Promise`, similar to
/// `actor.Next` or `yielder.Step`.
///
/// Represents the result of a stateful iteration that can send messages back to
/// a client.
pub type Step(state, output) {
/// Stop iterating, close the connection. We are done sending.
Close
/// Loop, without sending anything.
Step(state: state)
/// Loop, sending `returning` to the client.
Send(state: state, sending: output)
}
/// A convenience method useful for async functions.
///
/// Equivalent to `promise.resolve(Close)`
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn close() -> Promise(Step(state, output)) {
promise.resolve(Close)
}
/// A convenience method useful for async functions.
///
/// Equivalent to `promise.resolve(Step(state))`
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn step(state state: state) -> Promise(Step(state, output)) {
promise.resolve(Step(state))
}
/// A convenience method useful for async functions.
///
/// Equivalent to `promise.resolve(Send(state:, returning:))`
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn send(
state state: state,
sending sending: output,
) -> Promise(Step(state, output)) {
promise.resolve(Send(state:, sending:))
}
/// Creates a chunked transfer-encoded HTTP response from a yielder function.
///
/// The yielder function produces chunks of data over time, allowing efficient
/// streaming of large responses without loading everything into memory at once.
///
/// Note that file responses are automatically streamed.
///
/// ## Example
///
/// ```gleam
/// // Stream a large file in chunks
/// fn stream_handler(request) {
/// send_chunked(
/// from: #(0, file_size),
/// with: fn(state) {
/// let #(offset, total) = state
///
/// case offset >= total {
/// // we are done!
/// True -> smol.close()
/// False -> {
/// // Read next chunk (10KB at a time)
/// let chunk_size = int.min(10240, total - offset)
/// use chunk <- promise.await(read_chunk(path, offset, chunk_size))
///
/// // Return chunk and advance to next offset
/// smol.send(
/// state: #(offset + chunk_size, total),
/// sending: chunk,
/// )
/// }
/// }
/// }
/// )
/// |> promise.resolve
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn send_chunked(
from state: state,
with fun: fn(state) -> Promise(Step(state, BitArray)),
) -> Response(ReadableStream) {
let body = from_unfold(state, fun)
Response(
status: 200,
headers: [
#("conent-type", "application/octet-stream"),
#("transfer-encoding", "chunked"),
#("connection", "keep-alive"),
],
body: body,
)
}
fn from_bytes(bytes: BitArray) -> ReadableStream {
use has_sent <- from_unfold(False)
case has_sent {
False -> send(state: True, sending: bytes)
True -> close()
}
}
@external(javascript, "./smol.ffi.mjs", "from_unfold")
fn from_unfold(
state: state,
unfold: fn(state) -> Promise(Step(state, BitArray)),
) -> ReadableStream
// -- SERVER-SENT EVENTS -------------------------------------------------------
pub type SseEvent {
SseEvent(
event: Option(String),
data: String,
id: Option(String),
retry: Option(Int),
)
}
fn format_sse_event(event: SseEvent) -> BitArray {
let str = case event.event {
Some(event_type) -> "event: " <> event_type <> "\n"
None -> ""
}
let str = case event.id {
Some(id) -> str <> "id: " <> id <> "\n"
None -> str
}
let str = case event.retry {
Some(ms) -> str <> "retry: " <> int.to_string(ms) <> "\n"
None -> str
}
// Add data (split multi-line data with multiple data: lines)
let str = {
use str, line <- list.fold(string.split(event.data, on: "\n"), str)
str <> "data: " <> line <> "\n"
}
let str = str <> "\n"
<<str:utf8>>
}
/// Creates a Server-Sent Events (SSE) response, producing events using an
/// async yielder function.
///
/// The unfold function takes a state and returns either:
/// - `Ok(#(new_state, event))` to emit an event and continue with new state
/// - `Error(Nil)` to end the stream
///
/// ## Example
///
/// ```gleam
/// fn sse_handler(request) {
/// // Create a counter that generates events every second
/// use count <- server_sent_events(0)
/// use _ <- promise.await(promise.wait(1000))
/// case count < 10 {
/// True -> {
/// let event = SseEvent(
/// event: Some("update"),
/// data: "Count is " <> int.to_string(count),
/// id: None,
/// retry: None,
/// )
/// smol.send(state: count + 1, sending: event)
/// }
/// False -> smol.close()
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn server_sent_events(
from state: state,
with fun: fn(state) -> Promise(Step(state, SseEvent)),
) -> Response(ReadableStream) {
let fun = fn(state) {
use result <- promise.await(fun(state))
case result {
Send(new_state, event) -> send(new_state, format_sse_event(event))
Step(new_state) -> step(new_state)
Close -> close()
}
}
send_chunked(state, fun)
|> response.set_header("content-type", "text/event-stream")
|> response.set_header("cache-control", "no-cache")
}
// -- WEBSOCKETS ---------------------------------------------------------------
pub type WebsocketMessage {
Binary(bytes: BitArray)
Text(text: String)
}
pub opaque type WebsocketRuntime(msg) {
WebsocketRuntime(send: fn(msg) -> Nil)
}
/// Attempt to open a websocket connection in response to this request.
///
/// The client has to have made a valid `Upgrade` request, otherwise smol will
/// respond with status 426.
///
/// When the upgrade succeeds, an actor-like process is started, sending and
/// receiving messages from the client.
///
/// ```gleam
/// type Msg {
/// ConnectionOpened
/// ReceivedMessage(smol.WebsocketMessage)
/// ConnectionClosed
/// }
///
/// fn websockets_handler(request) {
/// use count, msg <- smol.websocket(
/// init: 1,
/// on_open: ConnectionOpened,
/// on_message: ReceivedMessage,
/// on_close: ConnectionClosed
/// )
///
/// case msg {
/// ConnectionClosed -> smol.close()
/// ReceivedMessage(smol.Text(text:)) -> {
/// let response = "Message No. " <> int.to_string(count) <> ": " <> text
/// smol.send(state: count + 1, sending: smol.Text(response))
/// }
/// ReceivedMessage(smol.Binary(_)) -> smol.step(state: count + 1)
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn websocket(
init init: state,
update update: fn(state, msg) -> Promise(Step(state, WebsocketMessage)),
on_open handle_open: msg,
on_message handle_message: fn(WebsocketMessage) -> msg,
on_close handle_close: msg,
) -> #(Response(ReadableStream), WebsocketRuntime(msg)) {
// NOTE: both the body and response are kinda fake here -
// the body is empty and is used as an object holding a reference to the actor spec
// the response is closer to reality, but upgrade will be handled by the
// server runtime again, and headers like sec-websocket-accept are missing..
let #(body, send) =
from_upgrade(
init:,
update:,
on_open: handle_open,
on_message: handle_message,
on_close: handle_close,
)
// We construct an Upgrade required response here to indicate that the server
// _wanted_ to use websockets, but those were not properly implemented in one
// way or another.
let response = Response(status: 426, headers: [], body: body)
let runtime = WebsocketRuntime(send)
#(response, runtime)
}
@external(javascript, "./smol.ffi.mjs", "from_upgrade")
fn from_upgrade(
init init: state,
update update: fn(state, msg) -> Promise(Step(state, WebsocketMessage)),
on_open handle_open: msg,
on_message handle_message: fn(WebsocketMessage) -> msg,
on_close handle_close: msg,
) -> #(ReadableStream, fn(msg) -> Nil)
/// Send a custom message to a running websocket actor.
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn dispatch(runtime: WebsocketRuntime(msg), msg: msg) -> Nil {
runtime.send(msg)
}
// -- START --------------------------------------------------------------------
pub opaque type Server {
Server(port: Int, interface: String, stop: fn() -> promise.Promise(Nil))
}
@internal
pub fn started(
port: Int,
interface: String,
stop: fn() -> promise.Promise(Nil),
) -> Result(Server, _) {
Ok(Server(port:, interface:, stop:))
}
/// Adapts a smol handler function to work with standard Web
/// Request/Response objects.
///
/// This is useful for integrating with other JavaScript frameworks or
/// environments that use the standard Fetch API, like serverless platforms or
/// service workers.
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
@external(javascript, "./adapt.ffi.mjs", "adapt")
pub fn adapt(
handler: fn(Request(ReadableStream)) -> Promise(Response(ReadableStream)),
) -> fn(Dynamic) -> Promise(Dynamic)
/// Starts the server with the configured options.
///
/// The returned promise resolves once the server has been started or failed
/// to start.
///
/// ## Example
///
/// ```gleam
/// let handler = fn(request) {
/// let response = smol.send_string("Hello, Joe!")
/// promise.resolve(response)
/// }
///
/// use server <- promise.await(
/// smol.new(handler)
/// |> smol.port(8080)
/// |> smol.start()
/// )
///
/// case server {
/// Ok(server) -> {
/// io.println("Server started!")
/// }
/// Error(_) -> {
/// io.println("Could not start the server")
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
@external(javascript, "./smol.ffi.mjs", "start")
pub fn start(builder: Builder) -> Promise(Result(Server, Nil))
/// Gets the port that the server is listening on.
///
/// This is especially useful when using `random_port()` to determine which
/// port was assigned.
///
/// ## Example
///
/// ```gleam
/// use server <- promise.await(smol.start(builder))
/// use server <- result.try(server)
/// let port = smol.get_port(server)
/// io.println("Server listening on port " <> int.to_string(port))
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn get_port(server: Server) -> Int {
server.port
}
/// Gets the network interface that the server is listening on.
///
/// ## Example
///
/// ```gleam
/// use server <- promise.await(smol.start(builder))
/// use server <- result.try(server)
/// let interface = smol.get_interface(server)
/// io.println("Server listening on interface " <> interface)
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn get_interface(server: Server) -> String {
server.interface
}
/// Stops the server gracefully.
///
/// Returns a Promise that resolves when the server has fully stopped.
///
/// ## Example
///
/// ```gleam
/// use server <- promise.await(smol.start(builder))
/// case server {
/// Ok(server) -> {
/// io.println("Server started, stopping in 10 seconds...")
/// use _ <- promise.await(promise.wait(10_0000))
/// io.println("Stopping the server...")
/// use _ <- promise.await(smol.stop(server))
/// io.println("Server stopped.")
/// }
/// Error(_) -> {
/// io.println("Could not start the server!")
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn stop(server: Server) -> Promise(Nil) {
server.stop()
}
// -- HELPERS ------------------------------------------------------------------
pub type Runtime {
Node
Deno
Bun
}
/// Detects the current JavaScript runtime environment.
///
/// Returns a Result containing the Runtime or an error if the runtime couldn't
/// be detected.
///
/// ## Example
///
/// ```gleam
/// fn main() {
/// case smol.detect_runtime() {
/// Ok(smol.Node) -> io.println("Running on Node.js")
/// Ok(smol.Deno) -> io.println("Running on Deno")
/// Ok(smol.Bun) -> io.println("Running on Bun")
/// Error(_) -> io.println("Unknown runtime")
/// }
/// }
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
@external(javascript, "./smol.ffi.mjs", "detect_runtime")
pub fn detect_runtime() -> Result(Runtime, Nil)
/// Returns the status text, given a status code.
///
/// If the status code is not known, return the code as a string instead.
///
/// ## Example
///
/// ```gleam
/// smol.status_text(418)
/// // --> "I'm a teapot"
///
/// smol.status_text(911)
/// // --> "911"
/// ```
///
/// <div style="text-align: right;">
/// <a href="#">
/// <small>Back to top ↑</small>
/// </a>
/// </div>
pub fn status_text(status: Int) -> String {
case status {
100 -> "Continue"
101 -> "Switching Protocols"
103 -> "Early Hints"
200 -> "OK"
201 -> "Created"
202 -> "Accepted"
203 -> "Non-Authoritative Information"
204 -> "No Content"
205 -> "Reset Content"
206 -> "Partial Content"
300 -> "Multiple Choices"
301 -> "Moved Permanently"
302 -> "Found"
303 -> "See Other"
304 -> "Not Modified"
307 -> "Temporary Redirect"
308 -> "Permanent Redirect"
400 -> "Bad Request"
401 -> "Unauthorized"
402 -> "Payment Required"
403 -> "Forbidden"
404 -> "Not Found"
405 -> "Method Not Allowed"
406 -> "Not Acceptable"
407 -> "Proxy Authentication Required"
408 -> "Request Timeout"
409 -> "Conflict"
410 -> "Gone"
411 -> "Length Required"
412 -> "Precondition Failed"
413 -> "Payload Too Large"
414 -> "URI Too Long"
415 -> "Unsupported Media Type"
416 -> "Range Not Satisfiable"
417 -> "Expectation Failed"
418 -> "I'm a teapot"
422 -> "Unprocessable Entity"
425 -> "Too Early"
426 -> "Upgrade Required"
428 -> "Precondition Required"
429 -> "Too Many Requests"
431 -> "Request Header Fields Too Large"
451 -> "Unavailable For Legal Reasons"
500 -> "Internal Server Error"
501 -> "Not Implemented"
502 -> "Bad Gateway"
503 -> "Service Unavailable"
504 -> "Gateway Timeout"
505 -> "HTTP Version Not Supported"
506 -> "Variant Also Negotiates"
507 -> "Insufficient Storage"
508 -> "Loop Detected"
510 -> "Not Extended"
511 -> "Network Authentication Required"
_ -> int.to_string(status)
}
}
fn try(
promise: Promise(Result(a, e)),
then continue_with: fn(a) -> Promise(Result(b, e)),
) -> Promise(Result(b, e)) {
use result <- promise.await(promise)
case result {
Ok(value) -> continue_with(value)
Error(e) -> promise.resolve(Error(e))
}
}
fn return(value: a) -> Promise(Result(a, e)) {
promise.resolve(Ok(value))
}
fn fail(error: e) -> Promise(Result(a, e)) {
promise.resolve(Error(error))
}