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 + + + + + + +
+
+
+ + + + + +
+
+
+ +
+
+

Messages

+
+
+
+

Active Users

+ +
+
+ +
+
+
+ + +
+
+
+ + 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' + }, +};