Examples

Chat app

Vue code (client):

<template>
  <div>
    <div class="chat-app">
      <div class="left">
        <div ref="messages" class="messages">
          <div v-for="(msg, idx) in messages" :key="idx">
            <strong>{{ msg.username }}:</strong>
            {{ msg.message }}
          </div>
        </div>
        <form class="input" @submit.prevent="send">
          <div class="my-username">
            {{ username }}:
          </div>
          <input ref="input" v-model.trim="input" type="text" maxlength="250">
          <button type="submit" :disabled="!input">
            Send
          </button>
        </form>
      </div>
      <div class="user-list">
        <div v-for="(value, user, idx) in state.users" :key="idx">
          {{ user }}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import BS from '@/libs/BS';

export default {
  data: () => ({
    input: '',
    state: { users: {} },
    username: '',
    messages: []
  }),
  created () {
    this.ch = BS.joinChannel('chat')
    this.ch.on('state', (state) => {
      this.state = state
    })
    this.ch.on('initialState', () => {
      this.getUsername()
    })
    this.ch.on('event', async (event) => {
      this.messages.push(event)
      // scroll to bottom
      await this.$nextTick()
      this.$refs.messages.scrollTo(0, this.$refs.messages.scrollHeight)
    })
  },
  mounted () {
    this.$refs.input.focus()
  },
  beforeDestroy () {
    this.ch.leave()
  },
  methods: {
    send () {
      this.ch.doAction('say', this.input)
      this.input = ''
    },
    async getUsername () {
      this.username = await this.ch.doRequest('whoami')
    }
  }
}
</script>

<style lang="scss" scoped>
.chat-app {
  border: 3px solid black;
  height: 400px;
  display: flex;
}

.left {
  flex: 3 0 0;
  display: flex;
  flex-direction: column;

  .messages {
    padding: 3px 5px;
    flex: 1 1 0;
    border-bottom: 3px solid black;
    overflow-y: auto;
  }

  form.input {
    padding: 3px 5px;
    margin: 0;
    display: flex;
    align-items: center;

    .my-username {
      padding: 0 5px 0 3px;
    }

    input[type=text] {
      flex: 1 0 auto;
    }
  }
}

.user-list {
  padding: 3px 5px;
  flex: 1 0 0;
  border-left: 3px solid black;
  overflow-y: auto;
}
</style>

Perl code (server):

package MyApp::Plugin::DemoChat;

use Mojo::Base 'Mojolicious::Plugin', -signatures;

my $DICT = '/usr/share/dict/words';

use constant CHANNEL_NAME => 'chat';

sub random_word {
    -e $DICT or die "Missing file $DICT";
    my $size = -s $DICT;
    my $random_point = int((0.8 * rand() + 0.1) * $size);
    open my $fh, '<', $DICT or die "Couldn't open file $DICT for reading: $!";
    seek $fh, $random_point, 0 or die "Couldn't seek in $DICT";
    <$fh>;
    chomp(my $word = <$fh>);
    $word =~ s/\'s\z//;
    return lc $word;
}

sub register ($self, $app, $config) {
    $app->bs->create_channel(CHANNEL_NAME, {users => {}});

    $app->bs->on_join(CHANNEL_NAME, sub ($channel_name, $c, $attrs) {
        # first login
        my $username = $c->stash->{'bsdemo.chat.username'} //= random_word;

        $c->bs->lock_state($channel_name, sub ($state) {
            $state->{users}{$username} = $c->bs->worker_uuid;

            return undef, $state, 1;
        });

        return {
            limit => $attrs->{is_reconnect} ? 100 : 10,
        };
    });

    $app->bs->on_leave(CHANNEL_NAME, sub ($channel_name, $c) {
        my $username = $c->stash->{'bsdemo.chat.username'};
        defined $username or return;

        $c->bs->lock_state($channel_name, sub ($state) {
            delete $state->{users}{$username};

            return undef, $state, -1;
        });
    });

    $app->bs->on_request(CHANNEL_NAME, 'whoami', sub ($channel_name, $c, $payload) {
        return $c->stash('bsdemo.chat.username');
    });

    $app->bs->on_action(CHANNEL_NAME, 'say', sub ($channel_name, $c, $payload) {
        0 < length $payload <= 1024 or return;

        $c->bs->lock_state($channel_name, sub ($state) {
            my $username = $c->stash->{'bsdemo.chat.username'};
            my $msg = "" . $payload;
            my $event = {
                username => $username,
                message  => $msg,
            };

            return $event, undef;
        });
    });

    $app->bs->on_cleanup(CHANNEL_NAME, sub ($channel_name, $txn, $alive_workers) {
        $txn->lock_state($channel_name, sub ($state) {
            foreach my $username (keys $state->{users}->%*) {
                my $worker_uuid = $state->{users}{$username};
                if (! $alive_workers->{$worker_uuid}) {
                    delete $state->{users}{$username};
                }
            }

            return undef, $state;
        });
    });
}

1;