Web Sockets: Chat



examples/lite/web_sockets_chat.pl
use Mojolicious::Lite;
use DateTime;
use Data::Dumper qw(Dumper);
# Chrome seems to reach timeout after 15 sec
# Firefox seems to reach timeout after 35-40 sec
# When I enabled the heartbeat from JavaScript the timeout went down in FF to 15 sec


my %clients;

websocket '/chat' => sub {
    my $c = shift;
    my $tx_id = sprintf "%s", $c->tx;
    $clients{$tx_id} = { ws => $c->tx };

    $c->on(json => sub {
        my ($ws, $hash) = @_;
        $c->app->log->debug("From $ws received " . Dumper $hash);

        if ($hash->{heartbeat}) {
            $ws->send({json => {heartbeat => $hash->{heartbeat}}});
            return;
        }

        my $dt   = DateTime->now( time_zone => 'Asia/Tokyo');

        if ($hash->{login}) {
            $clients{$tx_id}{user_name} = $hash->{login};
            $ws->send({json => {login => 'ok'}});
            # TODO: notify everyone who joined the conversation
            foreach my $ws (keys %clients) {
                next if $ws eq $tx_id;
                $clients{$ws}{ws}->send({json => {
                    msg => $dt->hms . " $clients{$tx_id}{user_name} has joined the conversation"
                }});
            }

            return;
        }

        foreach my $ws (keys %clients) {
            my $msg = $dt->hms . ($ws eq $tx_id ? " $hash->{msg}" : " $clients{$tx_id}{user_name}: $hash->{msg}");
            $clients{$ws}{ws}->send({json => {
                msg => $msg,
            }});
        }

        return;
    });

    $c->on(finish => sub {
        my ($ws, $code, $reason) = @_;
        $c->app->log->debug( "Finished $ws Code $code reason: '" . ( $reason // '' ) .  "'");
        # code was 1006
        # reason was undef
        # TODO: notify everyone who left the conversation

        my $dt   = DateTime->now( time_zone => 'Asia/Tokyo');
        foreach my $ws (keys %clients) {
            next if $ws eq $tx_id;
            $clients{$ws}{ws}->send({json => {
                msg => $dt->hms . " $clients{$tx_id}{user_name} has left the conversation"
            }});
        }

        delete $clients{$ws};
        return;
    });
};

get '/' => 'index';

app->start;
__DATA__

@@ index.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title>Chat</title>
    <script>
        var ws;
        var user_name;
        var heartbeat_interval = null;
        function login(e) {
            if (e.keyCode !== 13) {
                  return false;
            }
            user_name = document.getElementById('name').value;
            //console.log(user_name);
            document.getElementById('login_name').innerHTML = user_name;
            document.getElementById('login').style.display = 'none';
            document.getElementById('chat').style.display = 'block';

            setup();
        }

        function setup() {
            ws = new WebSocket('<%= url_for('chat')->to_abs %>');

            ws.onmessage = function (event) {
                var data = JSON.parse(event.data);
                if (! data.msg) {
                    return;
                }
                console.log('Received', data.msg);
                document.getElementById('output').innerHTML = data.msg + '<br>' + document.getElementById('output').innerHTML;
            };

            ws.onopen = function (event) {
                ws.send(JSON.stringify({login: user_name}));

                // heartbeat: not based on http://django-websocket-redis.readthedocs.io/en/latest/heartbeats.html
                if (heartbeat_interval === null) {
                    heartbeat_interval = setInterval(function() {
                        ws.send(JSON.stringify({heartbeat: user_name}));
                    }, 14000);
                }
            };

            ws.onclose = function(){
                if (heartbeat_interval !== null) {
                    clearInterval(heartbeat_interval);
                }
                setTimeout(setup, 1000);
            };
        };

        function send(e) {
            if (e.keyCode !== 13) {
                  return false;
            }
            var msg = document.getElementById('msg').value;
            document.getElementById('msg').value = '';
            console.log('send', msg);
            ws.send(JSON.stringify({msg: msg}));
        }
        
        function onload() {
            //console.log('onload');
            document.getElementById('name').addEventListener('keypress', login);
            document.getElementById('msg').addEventListener('keypress', send);
            document.getElementById('msg').focus();
        }
    </script>

    <style>
      #chat {
        display: none;
      }
    </style>
  </head>
  <body onload="onload()">
    <div id="login">Your name: <input type="text" id="name"></div>
    <div id="chat">
        Logged in as <span id="login_name"></span><br>
        <input type="text" id="msg" placeholder="Your message">
        <div id="output"></div>
    </div>
  </body>
</html>