diff --git a/packages/broker/chat.html b/packages/broker/chat.html
new file mode 100644
index 0000000..e87174a
--- /dev/null
+++ b/packages/broker/chat.html
@@ -0,0 +1,43 @@
+
+
+
+ Syndicate: Chat
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/broker/package.json b/packages/broker/package.json
index b14fd59..890a88f 100644
--- a/packages/broker/package.json
+++ b/packages/broker/package.json
@@ -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"
diff --git a/packages/broker/src/chat.js b/packages/broker/src/chat.js
new file mode 100644
index 0000000..438f90b
--- /dev/null
+++ b/packages/broker/src/chat.js
@@ -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(connected to {url},
+ 'state_connected');
+ on stop outputItem(disconnected from {url},
+ 'state_disconnected');
+
+ assert ToBroker(url, Present(this.nym));
+ during FromBroker(url, Present($who)) {
+ assert ui.context(who).html('#nymlist', {who});
+ }
+
+ 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(
+ {who}{what}
+ );
+ }
+
+ // on message Syndicate.WakeDetector.wakeEvent() {
+ // :: forceBrokerDisconnect(url);
+ // }
+ }
+ }
+}
+
+function outputItem(item, klass) {
+ var o = document.getElementById('chat_output');
+ o.appendChild(UI.htmlToNode(
+ {(new Date()).toGMTString()}
+ {item}
+
));
+ 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);
+}
diff --git a/packages/broker/src/client.js b/packages/broker/src/client.js
new file mode 100644
index 0000000..4c15ced
--- /dev/null
+++ b/packages/broker/src/client.js
@@ -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));
+ }
+ }
+ }
+ }
+}
diff --git a/packages/broker/src/index.js b/packages/broker/src/index.js
index ec04f16..dbe97ad 100644
--- a/packages/broker/src/index.js
+++ b/packages/broker/src/index.js
@@ -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);
diff --git a/packages/broker/src/protocol.js b/packages/broker/src/protocol.js
new file mode 100644
index 0000000..92ace4d
--- /dev/null
+++ b/packages/broker/src/protocol.js
@@ -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,
+});
diff --git a/packages/broker/style.css b/packages/broker/style.css
new file mode 100644
index 0000000..17dcfdd
--- /dev/null
+++ b/packages/broker/style.css
@@ -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;
+}
diff --git a/packages/broker/webpack.config.js b/packages/broker/webpack.config.js
new file mode 100644
index 0000000..c0c0eaf
--- /dev/null
+++ b/packages/broker/webpack.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ entry: "./lib/chat.js",
+ mode: "development",
+ externals: {
+ crypto: 'null'
+ },
+};