Broker client implementation; simple chat demo

This commit is contained in:
Tony Garnock-Jones 2018-11-19 22:22:39 +00:00
parent 16719e1d07
commit 9a5c3136f0
8 changed files with 361 additions and 32 deletions

43
packages/broker/chat.html Normal file
View File

@ -0,0 +1,43 @@
<!doctype html>
<html>
<head>
<title>Syndicate: Chat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="style.css" rel="stylesheet">
<script src="dist/main.js"></script>
</head>
<body>
<section>
<form name="nym_form">
<fieldset>
<label class="control-label" for="wsurl">Server:</label>
<input type="text" id="wsurl" name="wsurl" value="ws://localhost:8000/broker">
<label class="control-label" for="nym">Nym:</label>
<input type="text" id="nym" name="nym" value="">
</fieldset>
</form>
</section>
<section>
<section id="messages">
<h1>Messages</h1>
<div id="chat_output"></div>
</section>
<section id="active_users">
<h1>Active Users</h1>
<ul id="nymlist"></ul>
</section>
</section>
<section>
<form id="chat_form" name="chat_form">
<fieldset>
<input type="text" id="chat_input" name="chat_input" value="" autocomplete="off">
<button id="send_chat">Send</button>
</fieldset>
</form>
</section>
</body>
</html>

View File

@ -15,7 +15,10 @@
"@syndicate-lang/driver-browser-ui": "^0.0.15",
"@syndicate-lang/driver-http-node": "^0.0.14",
"@syndicate-lang/driver-tcp-node": "^0.0.4",
"@syndicate-lang/driver-timer": "^0.0.18"
"@syndicate-lang/driver-timer": "^0.0.18",
"@syndicate-lang/driver-websocket": "^0.0.9",
"webpack": "^4.23.1",
"webpack-cli": "^3.1.2"
},
"scripts": {
"prepare": "which redo >/dev/null && redo || ../../do"

View File

@ -0,0 +1,83 @@
"use strict";
const UI = activate require("@syndicate-lang/driver-browser-ui");
// @jsx UI.html
// @jsxFrag UI.htmlFragment
const { ToBroker, FromBroker, BrokerConnected } = activate require("./client");
assertion type Present(name);
assertion type Says(who, what);
spawn {
// These lines effectively preventDefault the corresponding events:
on message UI.GlobalEvent('#chat_form', 'submit', _) {}
on message UI.GlobalEvent('#nym_form', 'submit', _) {}
field this.nym;
on asserted UI.UIChangeableProperty('#nym', 'value', $v) {
if (!v) {
v = randomName();
send UI.SetProperty('#nym', 'value', v);
}
this.nym = v;
}
field this.next_chat = '';
on asserted UI.UIChangeableProperty('#chat_input', 'value', $v) this.next_chat = v;
const ui = new UI.Anchor();
during UI.UIChangeableProperty('#wsurl', 'value', $url) {
during BrokerConnected(url) {
on start outputItem(<span class="connected">connected to {url}</span>,
'state_connected');
on stop outputItem(<span class="disconnected">disconnected from {url}</span>,
'state_disconnected');
assert ToBroker(url, Present(this.nym));
during FromBroker(url, Present($who)) {
assert ui.context(who).html('#nymlist', <li><span class="nym">{who}</span></li>);
}
on message UI.GlobalEvent('#send_chat', 'click', _) {
if (this.next_chat) send ToBroker(url, Says(this.nym, this.next_chat));
send UI.SetProperty('#chat_input', 'value', '');
}
on message FromBroker(url, Says($who, $what)) {
outputItem(<span class="utterance">
<span class="nym">{who}</span><span class="utterance">{what}</span>
</span>);
}
// on message Syndicate.WakeDetector.wakeEvent() {
// :: forceBrokerDisconnect(url);
// }
}
}
}
function outputItem(item, klass) {
var o = document.getElementById('chat_output');
o.appendChild(UI.htmlToNode(<div class={klass || ''}>
<span class="timestamp">{(new Date()).toGMTString()}</span>
{item}
</div>));
o.scrollTop = o.scrollHeight;
}
///////////////////////////////////////////////////////////////////////////
// Courtesy of http://listofrandomnames.com/ :-)
const names = ['Lisa', 'Wally', 'Rivka', 'Willie', 'Marget', 'Roma', 'Aron', 'Shakita', 'Lean',
'Carson', 'Walter', 'Lan', 'Cari', 'Fredrick', 'Audra', 'Luvenia', 'Wilda', 'Raul',
'Latia', 'Shalanda', 'Samira', 'Deshawn', 'Kerstin', 'Mina', 'Sunni', 'Bev',
'Chrystal', 'Chad', 'Shaunte', 'Shonna', 'Georgann', 'Von', 'Dorothea', 'Janette',
'Krysta', 'Graig', 'Jeromy', 'Corine', 'Lue', 'Xuan', 'Kesha', 'Reyes', 'Nichol',
'Easter', 'Stephany', 'Kimber', 'Rosette', 'Onita', 'Aliza', 'Clementine'];
function randomName() {
return names[Math.floor(Math.random() * names.length)] +
'_' + Math.floor(Math.random() * 990 + 10);
}

View File

@ -0,0 +1,75 @@
"use strict";
import {
Decoder, Encoder, Bytes,
Observe, Skeleton,
genUuid,
} from "@syndicate-lang/core";
const WS = activate require("@syndicate-lang/driver-websocket");
const {
Assert, Clear, Message,
Add, Del, Msg,
makeDecoder,
} = activate require("./protocol");
assertion type ToBroker(url, assertion);
assertion type FromBroker(url, assertion);
assertion type BrokerConnection(url);
assertion type BrokerConnected(url);
message type ForceBrokerDisconnect(url);
message type _BrokerPacket(url, packet);
Object.assign(module.exports, {
ToBroker, FromBroker,
BrokerConnection, BrokerConnected,
ForceBrokerDisconnect,
});
spawn named "BrokerClientFactory" {
during ToBroker($url, _) assert BrokerConnection(url);
during Observe(FromBroker($url, _)) assert BrokerConnection(url);
during Observe(BrokerConnected($url)) assert BrokerConnection(url);
during BrokerConnection($url) spawn named ['Broker', url] {
const wsId = genUuid('broker');
during WS.WebSocket(wsId, url, {}) {
assert BrokerConnected(url);
function w(x) {
send WS.DataOut(wsId, new Encoder().push(x).contents());
}
on message WS.DataIn(wsId, $data) {
if (data instanceof Bytes) {
send _BrokerPacket(url, makeDecoder(data).next());
}
}
during ToBroker(url, $a) {
const ep = genUuid('pub');
on start w(Assert(ep, a));
on stop w(Clear(ep));
}
on message ToBroker(url, $a) w(Message(a));
during Observe(FromBroker(url, $spec)) {
const ep = genUuid('sub');
on start w(Assert(ep, Observe(spec)));
on stop w(Clear(ep));
on message _BrokerPacket(url, Add(ep, $vs)) {
react {
assert FromBroker(url, Skeleton.instantiateAssertion(spec, vs));
stop on message _BrokerPacket(url, Del(ep, vs));
}
}
on message _BrokerPacket(url, Msg(ep, $vs)) {
send FromBroker(url, Skeleton.instantiateAssertion(spec, vs));
}
}
}
}
}

View File

@ -8,32 +8,24 @@ const Http = activate require("@syndicate-lang/driver-http-node");
const Tcp = activate require("@syndicate-lang/driver-tcp-node");
import {
Set, Bytes,
Decoder, Encoder,
Discard, Capture, Observe,
Encoder, Observe,
Dataspace, Skeleton, currentFacet,
} from "@syndicate-lang/core";
const server = Http.HttpServer(null, 8000);
assertion type Connection(connId);
message type Fragment(connId, data);
message type Request(connId, body);
message type Response(connId, body);
// Internal isolation
assertion type Envelope(body);
// Client ---> Broker
message type Assert(endpointName, assertion);
message type Clear(endpointName);
message type Message(body);
// Client <--- Broker
message type Add(endpointName, captures);
message type Del(endpointName, captures);
message type Msg(endpointName, captures);
assertion type Endpoint(connId, endpointName, assertion);
const {
Assert, Clear, Message,
Add, Del, Msg,
makeDecoder,
} = activate require("./protocol");
spawn named 'serverLogger' {
on asserted Http.Request(_, server, $method, $path, $query, $req) {
@ -59,7 +51,11 @@ spawn named 'rootServer' {
spawn named 'websocketListener' {
during Http.WebSocket($reqId, server, ['broker'], _) spawn named ['wsConnection', reqId] {
assert Connection(reqId);
on message Http.DataIn(reqId, $data) send Fragment(reqId, data);
on message Http.DataIn(reqId, $data) {
if (data instanceof Bytes) {
send Request(reqId, makeDecoder(data).next());
}
}
on message Response(reqId, $resp) send Http.DataOut(reqId, new Encoder().push(resp).contents());
}
}
@ -68,7 +64,14 @@ spawn named 'tcpListener' {
during Tcp.TcpConnection($id, Tcp.TcpListener(8001)) spawn named ['tcpConnection', id] {
assert Tcp.TcpAccepted(id);
assert Connection(id);
on message Tcp.DataIn(id, $data) send Fragment(id, data);
const decoder = makeDecoder(null);
on message Tcp.DataIn(id, $data) {
decoder.write(data);
let v;
while ((v = decoder.try_next())) {
send Request(id, v);
}
}
on message Response(id, $resp) send Tcp.DataOut(id, new Encoder().push(resp).contents());
}
}
@ -80,21 +83,6 @@ spawn named 'connectionHandler' {
let endpoints = Set();
const decoder = new Decoder(null, {
shortForms: {
0: Discard.constructorInfo.label,
1: Capture.constructorInfo.label,
2: Observe.constructorInfo.label,
}
});
on message Fragment(connId, $data) {
decoder.write(data);
let v;
while ((v = decoder.try_next())) {
send Request(connId, v);
}
}
on message Request(connId, Assert($ep, $a)) {
if (!endpoints.includes(ep)) {
endpoints = endpoints.add(ep);

View File

@ -0,0 +1,29 @@
"use strict";
import { Decoder, Discard, Capture, Observe } from "@syndicate-lang/core";
// Client ---> Broker
message type Assert(endpointName, assertion);
message type Clear(endpointName);
message type Message(body);
// Client <--- Broker
message type Add(endpointName, captures);
message type Del(endpointName, captures);
message type Msg(endpointName, captures);
function makeDecoder(initialBuffer) {
return new Decoder(initialBuffer, {
shortForms: {
0: Discard.constructorInfo.label,
1: Capture.constructorInfo.label,
2: Observe.constructorInfo.label,
}
});
}
Object.assign(module.exports, {
Assert, Clear, Message,
Add, Del, Msg,
makeDecoder,
});

101
packages/broker/style.css Normal file
View File

@ -0,0 +1,101 @@
template {
display: none;
}
h1 {
background: lightgrey;
}
body > section {
display: flex;
}
body > section > section {
margin: 1em;
}
section#messages {
flex-grow: 3;
}
section#active_users {
flex-grow: 1;
}
form#chat_form {
flex: 1 100%;
}
span.timestamp {
color: #d0d0d0;
}
span.timestamp:after {
content: " ";
}
.utterance span.nym:after {
content: ": ";
}
span.arrived:after {
content: " arrived";
}
span.departed:after {
content: " departed";
}
div.notification {
background-color: #eeeeff;
}
span.state.connected, span.arrived {
color: #00c000;
}
span.state.disconnected, span.departed {
color: #c00000;
}
span.state.crashed {
color: white;
background: red;
}
span.state.crashed:after {
content: "; please reload the page";
}
div.state_disconnected {
background-color: #ffeeee;
}
div.state_connected {
background-color: #eeffee;
}
#chat_output {
height: 15em;
overflow-y: scroll;
}
#chat_input {
width: 80%;
}
.nym {
color: #00c000;
}
.nym_status:before {
content: " (";
}
.nym_status:after {
content: ")";
}
.nym_status {
font-size: smaller;
}

View File

@ -0,0 +1,7 @@
module.exports = {
entry: "./lib/chat.js",
mode: "development",
externals: {
crypto: 'null'
},
};