spawnfest / arizona

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Arizona

Arizona is a Web Framework for Erlang.

Table of contents

Goal

The goal of this project is to have a web framework based on the Phoenix LiveView, where users connect to the server and send and receive events in realtime via WebSocket.

Motivation

Phoenix has an important role in the Elixir's popularity and community growth, and when the community grows, the language also grows. Erlang has a wonderful community and a strong and rock-solid structure, but I wanna see Erlang also growing fast and becoming more and more popular to make this world a better and peaceful place. This is the reason and motivation for this project.

Roadmap

  • Server side
    • Server
    • Request handler
    • WebSocket
    • JSON parser
    • Template compiler
    • Router
  • Client side
    • Broker
    • WebWorker
    • Event bus
    • Real-time updates
    • Patch only the diff on real-time updates
    • Persist state on reconnect
  • Example project
  • Create a rebar3 template

Components

The components should be pluggable and do not rely directly to any library. The pluggable components are:

  • Server
  • JSON parser and generator
  • Router
  • Template compiler

All of them can be configured via a config file or setting them via arizona environment variables. A config file example:

[{arizona, [
    {app, myarizona}, % your app name. This is used to expose the priv files.
    {server, #{
        adapter => arizona_server_adapter_cowboy,
        args => #{
            url => #{
                schema => http, % this is the only schema supported yet.
                ip => {127, 0, 0, 1},
                port => 8080
            }
        }
    }},
    {router, #{
        adapter => myarizona_web_router_example,
        args => #{
            % no optional args defined for this config yet.
        }
    }},
    {template, #{
        adapter => arizona_template_adapter_eel,
        args => #{
            % no optional args defined for this config yet.
        }
    }},
    {json, #{
        adapter => arizona_json_adapter_euneus,
        args => #{
            % no optional args defined for this config yet.
        }
    }}
]}]

Note

The config file is usually located at ./config/sys.config.

How it works

Let's look at the arizona_example folder and make it up and running via:

$ cd arizona_template
$ make serve

If all is good, the console will indicate that Arizona is running at http://127.0.0.1:8080.

Warning

There is a bug visiting the link for the first time after the server is up and running. You must reload the page 2 or 3 times for the page content appear. I need to investigate this. See the GIF below:

Up and running bug

Visiting the link you can see a counter label and a +1 button. Clicking the button, the counter number will be increased by one, like this:

Up and running

Nothing special here, but let's see what is happening under the hood.

Note The explanation below is very simplistic due to the event timeout.

The server receives the request, call the handler that calls the router plug to resolve this request. In the example project, the router is configured to call the arizona_example_web_live_home controller in the home page (aka /). This controller is a live controller, which needs to respect the arizona_live_view behavior. First, the mount function is called to resolve the initial setup of the socket, and then then request process to the client.

The client receives the rendered page and connects to the server via WebSocket, the server renders the page again only in the server side and store the information in a process. If the connection is lost, the client sends some information that the server knows how to restore the state.

In a nutshell, this is the project setup:

  • Router: arizona_example_web_router
    match(get, [ ]) ->
        LiveOpts = #{template => arizona_example_web_template_root},
        {{live, arizona_example_web_live_home, LiveOpts }, #{}}.
    % Note: get is the method and [ ] is the "/" path. The path needs to be improved, but it is the path split globally by slashes, e.g. <<"foo/bar/123">> =:= [ <<"foo">>, <<"bar">>, <<"123">> ]
  • Controller: arizona_example_web_live_home
    mount(_Params, Socket0) ->
        Socket = arizona_socket:bind(count, 0, Socket0),
        {ok, Socket}.
    
    render(Bindings) ->
        ?LV(<<"
        <div>Count: <span id=\"counter\"><%= @count .%></span></div>
        <button type=\"button\" arz-click=\"+1\">+1</button>
        ">>).
    
    handle_event(<<"+1">>, _Payload, Socket0) ->
        #{count := Count} = arizona_socket:get_bindings(Socket0),
        Socket = arizona_socket:bind(count, Count+1, Socket0),
        {ok, Socket}.

    Note

    • The template uses the eel lib convention;
    • The button has the arz-click attribute, where arz is an acronym for arizona. This attribute automatically calls the event in the view via handle_event/3 when the button is clicked.
  • Template: arizona_example_web_template_root
    mount(_Params, Socket0) ->
        Socket = arizona_socket:bind(#{
            title => <<"arizona_example">>
        }, Socket0),
        {ok, Socket}.
    
    render(Bindings) ->
        ?LV(<<"
        <!DOCTYPE html>
        <html lang=\"en\">
        <head>
            <meta charset=\"UTF-8\">
            <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">
            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
            <title><%= @title .%></title>
            <script src=\"assets/arizona/js/arizona.js\"></script>
            <script src=\"assets/arizona/js/morphdom.min.js\"></script>
            <script src=\"assets/js/main.js\"></script>
        </head>
        <body>
            <%= @inner_content .%>
        </body>
        </html>
        ">>).

    Note The morphdom lib is the same lib used by Phoenix to apply diffs and update the page content/nodes.

  • Client: javascript
    "use strict"
    
    const arizona = arizonaFactory()
    
    const params = { }
    
    arizona.connect(params, () => {
        console.log("Arizona is connected")
    })

    Note

    To send an event from client to server, just call via JS:

    arizona.send("myevent", { foo: "bar" })
    

    On the server side, the view will receive this event on the handle_event/3 callback, where the first argument is <<"myevent">>, the second #{<<"foo">> => <<"bar">>}, and third the socket.

    The client can subscribe/unsubscribe to any event, e.g.:

    arizona.on("myevent", (payload) => {
        console.log("myevent received", payload)
    })

    or once:

    arizona.once("myevent", (payload) => {
        console.log("myevent received", payload)
    })

Message transfer

The idea is to transfer the minimum amount of data via client and server. When connected, the full content of the page is received by the client:

Connect data

But then, only the data that is needed to update the page is sent/received:

Patch data

Notes

There are a lot of debugs in Javascript and Erlang files that need to be removed, but they help to understand what's happening under the hood.

Template

There is a rebar3 template under the arizona_template directory. Please see the README for instructions. After installed, just call the below command to create a pre-configured project:

$ rebar3 new arizona name=myarizona
===> Writing myarizona/config/sys.config
===> Writing myarizona/priv/static/assets/js/main.js
===> Writing myarizona/priv/static/favicon.ico
===> Writing myarizona/priv/static/robots.txt
===> Writing myarizona/src/web/controller/myarizona_web_controller_error.erl
===> Writing myarizona/src/web/live/myarizona_web_live_home.erl
===> Writing myarizona/src/web/template/myarizona_web_template_root.erl
===> Writing myarizona/src/web/myarizona_web_router.erl
===> Writing myarizona/src/myarizona_app.erl
===> Writing myarizona/src/myarizona_sup.erl
===> Writing myarizona/src/myarizona.app.src
===> Writing myarizona/.gitignore
===> Writing myarizona/LICENSE.md
===> Writing myarizona/Makefile
===> Writing myarizona/README.md
===> Writing myarizona/rebar.config
$ cd myarizona

Dependencies

Maintained by me

  • eel: Template compiler;
  • euneus: JSON parser and generator.

Third-party

Event notes

There are huge improvements/work to do and infinite possibilities for a web framework. This is just the beginning. I'll be very happy to see any progress after this 48 hours of SpawnFest coding to this lib.

Feel free to contact me. Let's improve this lib Erlang Web Framework together 💪

My special thanks to all of the folks involved in this event.

Wish luck to all participants o/

See you all next time 🚀

Next steps

  • Compile the templates. Currently, all the time that a page is visited the template is compiled;
  • A database integration, like Ecto for Elixir. I have some ideas implemented on this project;
  • Simplify the CRUD creation using a mechanism like the Changeset for Ecto/Elixir. I have a project also called Changeset that can be used as an initial idea;
  • CLI (Command Line Interface) with useful commands, maybe via a rebar3 plugin.

License

Copyright (c) 2023 William Fank Thomé

Arizona is 100% open source and community-driven. All components are available under the Apache 2 License on GitHub.

See LICENSE.md for more information.

About

License:Apache License 2.0


Languages

Language:Erlang 87.6%Language:JavaScript 11.9%Language:Makefile 0.5%