Building a Chat Web App - Step 2: Adding a Backend

Continuing the app we built in the previous article, we will now add a back end to enable chatting between multiple front ends.

Let’s keep it simple for the first step and have the backend store the chat data in memory. We’ll build two endpoints, one for posting a new message, and one for fetching the message history. We will skip the rooms for now and keep all messages in one global publicly available pool of messages, so everyone will see all messages from everyone. The messages for now will simply be stored in a global array of strings.

It’s important to keep in mind that Alusus and WebPlatform are multi-threaded, so global variables will need to be thread safe. So, we will use a mutex to guard the access to this global array. Our backed will look like this:

//==============================================================================
// Backend

def MAX_MESSAGES: 12;
def messages: Array[String];
def mutex: Threading.Mutex;
Threading.initMutex(mutex~ptr, 0);

@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];
    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
    Threading.lockMutex(mutex~ptr);
    if messages.getLength() >= MAX_MESSAGES messages.remove(0);
    messages.add(String(postData~ptr, postDataSize));
    Threading.unlockMutex(mutex~ptr);
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {
    Threading.lockMutex(mutex~ptr);
    def response: String = String.merge(messages, "\n");
    Threading.unlockMutex(mutex~ptr);
    Http.print(conn, "HTTP/1.1 200 Ok\r\n");
    Http.print(conn, "Content-Type: text/plain\r\n");
    Http.print(conn, "Cache-Control: no-cache\r\n");
    Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength());
    Http.print(conn, response.buf);
}

Each REST endpoint is a function marked with the @beEndpoint modifier, giving it the method and the route for that endpoint. You’ll see that in each of these two endpoints we lock the mutex before accessing the global messages variable, then we unlock it afterwards.

You can put this backend code in a separate file and then import it from chat.alusus, and you can also keep inside chat.alusus. In Alusus, you do not have to separate the backend from the front end, nor do you need to run them separately; WebPlatform takes care of all of that for you.

Now that we have the backend, we need to hook the frontend to it. We have a text entry component that isn’t listening to any events, so let’s listen to the button click and key-up events and notify the owner:

class TextEntry {
    @injection def component: Component;

    def onNewEntry: closure (String);
    def textInput: SrdRef[TextInput];

    handler this~init() {
        def self: ref[this_type](this);

        this.view = Box({}).{
            style.{
                display = Display.FLEX;
                layout = Layout.ROW;
                justify = Justify.SPACE_BETWEEN;
            };
            addChildren({
                TextInput().{
                    self.textInput = this;
                    style.{
                        flex = Flex(1);
                        margin = Length4.px(10);
                        height = Length.px(50);
                        fontSize = Length.pt(12.0);
                        borderStyle = BorderStyle.SOLID;
                        borderColor = Color("000");
                        borderWidth = Length4.px(1.5);
                    };
                    onKeyUp.connect(closure (widget: ref[TextInput], payload: ref[String]) {
                        if payload == "Shift+Enter" {
                            def newData: String = String("- ") + widget.getText().trim();
                            widget.setText(String());
                            if not self.onNewEntry.isNull() self.onNewEntry(newData);
                        }
                    });
                },
                Button(String("Send")).{
                    style.{
                        height = Length.px(50);
                        width = Length.px(50);
                        fontSize = Length.px(16.0);
                        justify = Justify.CENTER;
                        margin = Length4.px(10, 10, 10, 0);
                    };
                    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                        def newData: String = String("- ") + self.textInput.getText().trim();
                        self.textInput.setText(String());
                        if not self.onNewEntry.isNull() self.onNewEntry(newData);
                    });
                }
            });
        };
    }

    handler this_type(): SrdRef[TextEntry] {
        return SrdRef[TextEntry].construct();
    }
}

Now the owner of the component just needs to provide the onNewEntry callback. In the UI endpoint we will do two things:

  • Listen to new entries and send them over to the backend.
  • Periodically fetch the chat history from the backend and display it.
@uiEndpoint["/"]
@title["Wasm Chat"]
func main {
    def onFetch: closure (json: Json);

    Window.instance.style.{
        padding = Length4.pt(0);
        margin = Length4.pt(0);
    };
    Window.instance.setView(Box({}).{
        style.{
            height = Length.percent(100);
            justify = Justify.SPACE_BETWEEN;
            display = Display.FLEX;
            layout = Layout.COLUMN;
            background = Background(Color("aaa"));
        };
        addChildren({
            Header(),
            Text(String()).{
                style.{
                    flex = Flex(1);
                    width = Length.percent(100) - Length.px(20);
                    margin = Length4.px(10, 10, 0, 10);
                    fontSize = Length.pt(20.0);
                    borderStyle = BorderStyle.SOLID;
                    borderColor = Color("000");
                    borderWidth = Length4.px(1.5);
                    background = Background(Color("fff"));
                };
                onFetch = closure (json: Json) {
                    def status: Int[64] = json("eventData")("status");
                    if status >= 200 and status < 300 {
                        def data: String = json("eventData")("body");
                        if this.getText() != data {
                            this.setText(data);
                        }
                    }
                };
            },
            TextEntry().{
                onNewEntry = closure (newData: String) {
                    sendRequest(
                        "POST", "/messages", "Content-Type: application/text", newData, 10000,
                        closure (Json) {}
                    );
                    sendRequest("GET", "/messages", null, null, 500, onFetch);
                };
            };
        })
    });

    startTimer(500000, closure (json: Json) {
        sendRequest("GET", "/messages", null, null, 500, onFetch);
    });
    sendRequest("GET", "/messages", null, null, 500, onFetch);

    runEventLoop();
}

The rest of the app looks the same, and running the app is done the same way as shown in the previous section. The full source code now looks like this:

import "Srl/String";
import "Apm";
Apm.importFile("Alusus/Threading");
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

//==============================================================================
// Backend

def MAX_MESSAGES: 12;
def messages: Array[String];
def mutex: Threading.Mutex;
Threading.initMutex(mutex~ptr, 0);

@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];
    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
    Threading.lockMutex(mutex~ptr);
    if messages.getLength() >= MAX_MESSAGES messages.remove(0);
    messages.add(String(postData~ptr, postDataSize));
    Threading.unlockMutex(mutex~ptr);
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {
    Threading.lockMutex(mutex~ptr);
    def response: String = String.merge(messages, "\n");
    Threading.unlockMutex(mutex~ptr);
    Http.print(conn, "HTTP/1.1 200 Ok\r\n");
    Http.print(conn, "Content-Type: text/plain\r\n");
    Http.print(conn, "Cache-Control: no-cache\r\n");
    Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength());
    Http.print(conn, response.buf);
}


//==============================================================================
// Frontend Components

class Header {
    @injection def component: Component;

    handler this~init() {
        this.view = Box({}).{
            style.{
                padding = Length4.pt(4);
                borderWidth = Length4.pt(0, 0, 1.5, 0);
                borderStyle = BorderStyle.SOLID;
                borderColor = Color("000");
                justify = Justify.START;
                display = Display.FLEX;
                layout = Layout.COLUMN;
                background = Background(Color("fff"));
            };
            addChildren({
                Text(String("Wasm Chat")).{
                    style.fontSize = Length.pt(18.0);
                }
            });
        };
    }

    handler this_type(): SrdRef[Header] {
        return SrdRef[Header].construct();
    }
}

class TextEntry {
    @injection def component: Component;

    def onNewEntry: closure (String);
    def textInput: SrdRef[TextInput];

    handler this~init() {
        def self: ref[this_type](this);

        this.view = Box({}).{
            style.{
                display = Display.FLEX;
                layout = Layout.ROW;
                justify = Justify.SPACE_BETWEEN;
            };
            addChildren({
                TextInput().{
                    self.textInput = this;
                    style.{
                        flex = Flex(1);
                        margin = Length4.px(10);
                        height = Length.px(50);
                        fontSize = Length.pt(12.0);
                        borderStyle = BorderStyle.SOLID;
                        borderColor = Color("000");
                        borderWidth = Length4.px(1.5);
                    };
                    onKeyUp.connect(closure (widget: ref[TextInput], payload: ref[String]) {
                        if payload == "Shift+Enter" {
                            def newData: String = String("- ") + widget.getText().trim();
                            widget.setText(String());
                            if not self.onNewEntry.isNull() self.onNewEntry(newData);
                        }
                    });
                },
                Button(String("Send")).{
                    style.{
                        height = Length.px(50);
                        width = Length.px(50);
                        fontSize = Length.px(16.0);
                        justify = Justify.CENTER;
                        margin = Length4.px(10, 10, 10, 0);
                    };
                    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                        def newData: String = String("- ") + self.textInput.getText().trim();
                        self.textInput.setText(String());
                        if not self.onNewEntry.isNull() self.onNewEntry(newData);
                    });
                }
            });
        };
    }

    handler this_type(): SrdRef[TextEntry] {
        return SrdRef[TextEntry].construct();
    }
}

//==============================================================================
// Frontend Pages

@uiEndpoint["/"]
@title["Wasm Chat"]
func main {
    def onFetch: closure (json: Json);

    Window.instance.style.{
        padding = Length4.pt(0);
        margin = Length4.pt(0);
    };
    Window.instance.setView(Box({}).{
        style.{
            height = Length.percent(100);
            justify = Justify.SPACE_BETWEEN;
            display = Display.FLEX;
            layout = Layout.COLUMN;
            background = Background(Color("aaa"));
        };
        addChildren({
            Header(),
            Text(String()).{
                style.{
                    flex = Flex(1);
                    width = Length.percent(100) - Length.px(20);
                    margin = Length4.px(10, 10, 0, 10);
                    fontSize = Length.pt(20.0);
                    borderStyle = BorderStyle.SOLID;
                    borderColor = Color("000");
                    borderWidth = Length4.px(1.5);
                    background = Background(Color("fff"));
                };
                onFetch = closure (json: Json) {
                    def status: Int[64] = json("eventData")("status");
                    if status >= 200 and status < 300 {
                        def data: String = json("eventData")("body");
                        if this.getText() != data {
                            this.setText(data);
                        }
                    }
                };
            },
            TextEntry().{
                onNewEntry = closure (newData: String) {
                    sendRequest(
                        "POST", "/messages", "Content-Type: application/text", newData, 10000,
                        closure (Json) {}
                    );
                    sendRequest("GET", "/messages", null, null, 500, onFetch);
                };
            };
        })
    });

    startTimer(500000, closure (json: Json) {
        sendRequest("GET", "/messages", null, null, 500, onFetch);
    });
    sendRequest("GET", "/messages", null, null, 500, onFetch);

    runEventLoop();
}

//==============================================================================
// Entry Point

Console.print("Starting server on port 8000...\nURL: http://localhost:8000/\n");
buildAndRunServer(Array[CharsPtr]({ "listening_ports", "8000", "static_file_max_age", "0" }));

Now we can have multiple people chat over this simple chat application, but the data is not persisted yet, so if we restart the server all data will be gone. We will fix that in the next article.