diff --git a/README.md b/README.md index d1a91dd..ceae3c3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# Real-Time Chat +# Real-Time Gleam Chat + +> [!NOTE] +> This is the repository associated to the article [Building a real-time chat in Gleam](https://gautier.dev/articles/real-time-gleam-chat). Start the server with `gleam run` and that's it! ✨ diff --git a/src/index.html b/src/index.html index a98dd72..2e9e3b2 100644 --- a/src/index.html +++ b/src/index.html @@ -58,27 +58,27 @@

Real-Time Gleam Chat

diff --git a/src/real_time_chat.gleam b/src/real_time_chat.gleam index df035e6..e5a0b5b 100644 --- a/src/real_time_chat.gleam +++ b/src/real_time_chat.gleam @@ -24,7 +24,7 @@ type PubSubMessage { /// A client has disconnected and should no longer receive messages. Unsubscribe(client: Subject(String)) /// A message to forward to all connected clients. - Message(String) + Publish(String) } /// This is the pubsub loop function, which receives messages and produces @@ -35,21 +35,21 @@ type PubSubMessage { fn pubsub_loop(message: PubSubMessage, clients: List(Subject(String))) { case message { // When the pubsub receives a Subscribe message with a client in it, - // continue running the actor loop with the client added to the state. + // continue running the actor loop with the client added to the state Subscribe(client) -> { io.println("➕ Client connected") [client, ..clients] |> actor.continue } // When the pubsub receives a Unsubscribe message with a client in it, - // produce a new state with the client removed and continue running. + // produce a new state with the client removed and continue running Unsubscribe(client) -> { io.println("➖ Client disconnected") clients |> list.filter(fn(c) { c != client }) |> actor.continue } - // Finally, when the pubsub receives a Message, forward it to clients. - Message(message) -> { + // Finally, when the pubsub receives a message, forward it to clients + Publish(message) -> { io.println("💬 " <> message) clients |> list.each(process.send(_, message)) clients |> actor.continue @@ -64,16 +64,16 @@ fn new_response(status: Int, body: String) { } pub fn main() { - // Start the pubsub with an empty list of clients in its own process. + // Start the pubsub with an empty list of clients in its own process let assert Ok(pubsub) = actor.start([], pubsub_loop) let assert Ok(_) = mist.new( - // HTTP server handler: it takes a request and returns a response. + // HTTP server handler: it takes a request and returns a response fn(request) { - // Basic router matching the request method and path of the request. + // Basic router matching the request method and path of the request let response = case request.method, request.path { - // On 'GET /', read the index.html file and return it. + // On 'GET /', read the index.html file and return it http.Get, "/" -> { use index <- result.try( simplifile.read("src/index.html") @@ -82,15 +82,15 @@ pub fn main() { new_response(200, index) |> Ok } - // On 'POST /post', read the body and send it to the pubsub. + // On 'POST /post', read the body and send it to the pubsub http.Post, "/post" -> { - // Read the first 128 bytes of the request. + // Read the first 128 bytes of the request use request <- result.try( request |> mist.read_body(128) |> result.replace_error("Could not read request body."), ) - // Transform the bytes into a string. + // Transform the bytes into a string use message <- result.try( request.body |> bit_array.to_string @@ -99,10 +99,10 @@ pub fn main() { ), ) - // Send the message to the pubsub. - process.send(pubsub, Message(message)) + // Send the message to the pubsub + process.send(pubsub, Publish(message)) - // Respond with a success message. + // Respond with a success message new_response(200, "Submitted: " <> message) |> Ok } @@ -110,25 +110,25 @@ pub fn main() { // The SSE loop runs in a separate process, we will use the pubsub // to send and receive messages. http.Get, "/sse" -> - request - |> mist.server_sent_events( + mist.server_sent_events( + request, response.new(200), - // Initialization function of the SSE loop. + // Initialization function of the SSE loop init: fn() { - // Create a new subject for the client to receive messages. + // Create a new subject for the client to receive messages let client = process.new_subject() - // Send this new client to the pubsub. + // Send this new client to the pubsub process.send(pubsub, Subscribe(client)) // Define on what messages the SSE loop function should run: - // on every message send to the `client` subject. + // on every message send to the `client` subject let selector = process.new_selector() |> process.selecting(client, function.identity) // Start the loop with the client as state and a selector - // pointing to the client subject. + // pointing to the client subject actor.Ready(client, selector) }, // This loop function is called every time the `client` subject @@ -137,16 +137,16 @@ pub fn main() { // SSE connection, and the third is the loop state, which, in this // case is always the client subject. loop: fn(message, connection, client) { - // Forward the message to the web client. + // Forward the message to the web client case mist.send_event( connection, message |> string_builder.from_string |> mist.event, ) { - // If it succeeds, continue the process. + // If it succeeds, continue the process Ok(_) -> actor.continue(client) - // If it fails, disconnect the client and stop the process. + // If it fails, disconnect the client and stop the process Error(_) -> { process.send(pubsub, Unsubscribe(client)) actor.Stop(process.Normal) @@ -156,11 +156,11 @@ pub fn main() { ) |> Ok - // In case of any other request, return a 404. + // In case of any other request, return a 404 _, _ -> new_response(404, "Not found") |> Ok } - // Simple error-handling mechanism. + // Simple error-handling mechanism case response { Ok(response) -> response Error(error) -> { @@ -170,10 +170,10 @@ pub fn main() { } }, ) - // Create and start an HTTP server using this handler. + // Create and start an HTTP server using this handler |> mist.port(3000) |> mist.start_http - // Everything runs in separate processes, keep the main process alive. + // Everything runs in separate processes, keep the main process alive process.sleep_forever() }