Broker client implementation; simple chat demo
This commit is contained in:
parent
16719e1d07
commit
9a5c3136f0
|
@ -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>
|
|
@ -15,7 +15,10 @@
|
||||||
"@syndicate-lang/driver-browser-ui": "^0.0.15",
|
"@syndicate-lang/driver-browser-ui": "^0.0.15",
|
||||||
"@syndicate-lang/driver-http-node": "^0.0.14",
|
"@syndicate-lang/driver-http-node": "^0.0.14",
|
||||||
"@syndicate-lang/driver-tcp-node": "^0.0.4",
|
"@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": {
|
"scripts": {
|
||||||
"prepare": "which redo >/dev/null && redo || ../../do"
|
"prepare": "which redo >/dev/null && redo || ../../do"
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,32 +8,24 @@ const Http = activate require("@syndicate-lang/driver-http-node");
|
||||||
const Tcp = activate require("@syndicate-lang/driver-tcp-node");
|
const Tcp = activate require("@syndicate-lang/driver-tcp-node");
|
||||||
import {
|
import {
|
||||||
Set, Bytes,
|
Set, Bytes,
|
||||||
Decoder, Encoder,
|
Encoder, Observe,
|
||||||
Discard, Capture, Observe,
|
|
||||||
Dataspace, Skeleton, currentFacet,
|
Dataspace, Skeleton, currentFacet,
|
||||||
} from "@syndicate-lang/core";
|
} from "@syndicate-lang/core";
|
||||||
|
|
||||||
const server = Http.HttpServer(null, 8000);
|
const server = Http.HttpServer(null, 8000);
|
||||||
|
|
||||||
assertion type Connection(connId);
|
assertion type Connection(connId);
|
||||||
message type Fragment(connId, data);
|
|
||||||
message type Request(connId, body);
|
message type Request(connId, body);
|
||||||
message type Response(connId, body);
|
message type Response(connId, body);
|
||||||
|
|
||||||
// Internal isolation
|
// Internal isolation
|
||||||
assertion type Envelope(body);
|
assertion type Envelope(body);
|
||||||
|
|
||||||
// Client ---> Broker
|
const {
|
||||||
message type Assert(endpointName, assertion);
|
Assert, Clear, Message,
|
||||||
message type Clear(endpointName);
|
Add, Del, Msg,
|
||||||
message type Message(body);
|
makeDecoder,
|
||||||
|
} = activate require("./protocol");
|
||||||
// Client <--- Broker
|
|
||||||
message type Add(endpointName, captures);
|
|
||||||
message type Del(endpointName, captures);
|
|
||||||
message type Msg(endpointName, captures);
|
|
||||||
|
|
||||||
assertion type Endpoint(connId, endpointName, assertion);
|
|
||||||
|
|
||||||
spawn named 'serverLogger' {
|
spawn named 'serverLogger' {
|
||||||
on asserted Http.Request(_, server, $method, $path, $query, $req) {
|
on asserted Http.Request(_, server, $method, $path, $query, $req) {
|
||||||
|
@ -59,7 +51,11 @@ spawn named 'rootServer' {
|
||||||
spawn named 'websocketListener' {
|
spawn named 'websocketListener' {
|
||||||
during Http.WebSocket($reqId, server, ['broker'], _) spawn named ['wsConnection', reqId] {
|
during Http.WebSocket($reqId, server, ['broker'], _) spawn named ['wsConnection', reqId] {
|
||||||
assert Connection(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());
|
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] {
|
during Tcp.TcpConnection($id, Tcp.TcpListener(8001)) spawn named ['tcpConnection', id] {
|
||||||
assert Tcp.TcpAccepted(id);
|
assert Tcp.TcpAccepted(id);
|
||||||
assert Connection(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());
|
on message Response(id, $resp) send Tcp.DataOut(id, new Encoder().push(resp).contents());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,21 +83,6 @@ spawn named 'connectionHandler' {
|
||||||
|
|
||||||
let endpoints = Set();
|
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)) {
|
on message Request(connId, Assert($ep, $a)) {
|
||||||
if (!endpoints.includes(ep)) {
|
if (!endpoints.includes(ep)) {
|
||||||
endpoints = endpoints.add(ep);
|
endpoints = endpoints.add(ep);
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
entry: "./lib/chat.js",
|
||||||
|
mode: "development",
|
||||||
|
externals: {
|
||||||
|
crypto: 'null'
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in New Issue