Examples
Simple examples: Counter , Chat app
More convoluted demos: Gaming portal
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;
Simple examples: Counter , Chat app
More convoluted demos: Gaming portal