| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- <?xml version="1.0" ?>
- <diagram author="Joeri Exelmans" name="client">
- <description>Example of a browser-based WebSocket client with heartbeats</description>
- <top></top>
- <inport name="in"/>
- <outport name="out"/>
- <class name="Main" default="true">
- <relationships>
- <association name="socket" class="Socket" min="1" max="1"/>
- </relationships>
- <inport name="ack"/>
- <inport name="socket"/>
- <constructor>
- <parameter name="uiState"/>
- <parameter name="myId"/>
- <parameter name="getMyName"/>
- <body>
- this.uiState = uiState;
- this.myId = myId;
- this.getMyName = getMyName;
- this.connected = false;
- </body>
- </constructor>
- <scxml
- big_step_maximality="take_many"
- internal_event_lifeline="next_combo_step">
- <parallel id="p">
- <state id="response_handler">
- <onentry>
- <script>
- this.reqHandlers = new Map();
- this.reqCounter = 0;
- this.sendReq = (obj, handler) => {
- const reqId = this.reqCounter++;
- const req = {
- reqId,
- ...obj,
- };
- if (handler !== undefined) {
- this.reqHandlers.set(reqId, handler);
- }
- console.log("Sending req", req);
- this.addEvent(new Event("send", null, [req]));
- // allow to cancel the request handler
- return () => {
- this.reqHandlers.delete(reqId);
- };
- }
- </script>
- </onentry>
- <state id="default">
- <transition event="received" target=".">
- <parameter name="parsed"/>
- <script>
- if (parsed.type === 'ack' && parsed.reqId !== undefined) {
- const handler = this.reqHandlers.get(parsed.reqId);
- if (handler) {
- handler(parsed);
- }
- }
- </script>
- </transition>
- <transition event="disconnected" target=".">
- <script>
- // no way we will get a response for our pending requests
- this.reqHandlers.clear();
- this.reqCounter = 0;
- </script>
- </transition>
- </state>
- </state>
- <state id="mode" initial="uninitialized">
- <state id="uninitialized">
- <transition event="init_offline" port="in" target="../async_disconnected"/>
- <transition event="init_join" port="in" target="../can_leave/session_set/rejoin_when_online">
- <parameter name="sessionId"/>
- <script>
- this.sessionId = sessionId;
- </script>
- </transition>
- </state>
- <state id="async_disconnected">
- <onentry><script>
- this.uiState.setOfflineDisconnected();
- </script></onentry>
- <transition event="connected" target="../async_connected"/>
- </state><!-- async_disconnected -->
- <state id="async_connected">
- <onentry><script>
- this.uiState.setOfflineConnected();
- </script></onentry>
- <transition event="disconnected" target="../async_disconnected"/>
- <transition event="join" port="in" target="../can_leave/session_set/waiting_for_join_ack">
- <parameter name="sessionId"/>
- <script> this.sessionId = sessionId; </script>
- </transition>
- <transition event="new_share" port="in" target="../can_leave/waiting_for_new_share_ack">
- <parameter name="ops"/>
- <script>
- // console.log("new share event, ops=", ops);
- this.ops = ops;
- </script>
- </transition>
- </state><!-- async_connected -->
- <state id="can_leave" initial="session_set">
- <transition event="leave" cond='this.connected' port="in" target="../async_connected"/>
- <transition event="leave" cond='!this.connected' port="in" target="../async_disconnected"/>
- <onexit><script>
- this.sendReq({type:"leave"});
- </script></onexit>
- <state id="waiting_for_new_share_ack">
- <onentry><script>
- this.uiState.setCreatingShare();
- this.deleteNewShareHandler = this.sendReq(
- {type: "new_share", ops: this.ops},
- res => {
- // called during transition (as action code), so no need to 'wake up' the controller like we do with 'addInput':
- this.addEvent(new Event("ack_new_share", "ack", [res.sessionId]));
- });
- </script></onentry>
- <onexit><script>
- this.deleteNewShareHandler();
- </script></onexit>
- <transition event="disconnected" target="../reshare_when_online"/>
- <transition event="ack_new_share" port="ack" target="../session_set/joined">
- <parameter name="sessionId"/>
- <script>
- this.sessionId = sessionId;
- </script>
- <raise event="ack_new_share" port="out" scope="output">
- <parameter expr="sessionId"/>
- </raise>
- </transition>
- </state><!-- waiting_for_new_share_ack -->
- <state id="reshare_when_online">
- <onentry><script>
- this.uiState.setReconnecting();
- </script></onentry>
- <transition event="connected" target="../waiting_for_new_share_ack"/>
- </state>
- <state id="session_set" initial="waiting_for_join_ack">
- <onentry><script>
- if (!this.sessionId) {
- throw new Error("DEBUG: no session id");
- }
- this.uiState.setSession(this.sessionId);
- </script></onentry>
- <onexit> <script>
- this.uiState.unsetSession();
- </script> </onexit>
- <state id="waiting_for_join_ack">
- <onentry><script>
- this.uiState.setJoining();
- this.deleteJoinHandler = this.sendReq(
- {type: "join", sessionId: this.sessionId},
- res => {
- // called during transition (as action code), so no need to 'wake up' the controller like we do with 'addInput':
- this.addEvent(new Event("ack_join", "ack", [res.ops]));
- });
- </script></onentry>
- <onexit><script>
- this.deleteJoinHandler();
- </script></onexit>
- <transition event="ack_join" port="ack" target="../joined">
- <parameter name="ops"/>
- <raise event="ack_join" port="out" scope="output">
- <parameter expr="ops"/>
- </raise>
- </transition>
- <transition event="disconnected" target="../rejoin_when_online"/>
- </state><!-- waiting_for_join_ack -->
- <parallel id="joined">
- <state id="region_sendreceive">
- <state id="default">
- <onentry>
- <script>
- this.uiState.setOnline();
- </script>
- </onentry>
- <transition event="received" cond="parsed.type === 'pub_edit'" target=".">
- <parameter name="parsed"/>
- <script>
- if (parsed.sessionId !== this.sessionId) {
- throw new Error("Unexpected: received edit for another session:" + parsed.sessionId);
- }
- </script>
- <raise event="received_op" port="out" scope="output">
- <parameter expr="parsed.op"/>
- </raise>
- </transition>
- <transition event="new_edit" port="in" target=".">
- <parameter name="serialized"/>
- <script>
- this.sendReq(
- {type:"new_edit", sessionId: this.sessionId, op: serialized},
- res => {
- // this.unacknowledged.delete(serialized.id);
- });
- // this.unacknowledged.set(serialized.id, serialized);
- </script>
- </transition>
- <transition event="request_lock" port="in" target=".">
- <parameter name="cellIds"/>
- <parameter name="acquiredCallback"/>
- <parameter name="deniedCallback"/>
- <script>
- this.sendReq(
- {type:"request_lock", sessionId: this.sessionId, cellIds},
- res => {
- if (res.success) {
- acquiredCallback();
- } else {
- deniedCallback();
- }
- });
- </script>
- </transition>
- <transition event="release_lock" port="in" target=".">
- <parameter name="cellIds"/>
- <script>
- this.sendReq(
- {type:"release_lock", sessionId: this.sessionId, cellIds},
- res => {},
- );
- </script>
- </transition>
- <transition event="received" cond="parsed.type === 'broadcast'" target=".">
- <parameter name="parsed"/>
- <raise event="broadcast" port="out" scope="output">
- <parameter expr="parsed.sessionId"/>
- <parameter expr="parsed.msg"/>
- </raise>
- </transition>
- <transition event="broadcast_selection" target=".">
- <parameter name="selectedIds"/>
- <script>
- if (this.cursorState !== null) {
- this.addEvent(new Event("send", null, [{
- type:"broadcast",
- msg: {
- type:"update_selection",
- userId: this.myId,
- selectedIds,
- ...this.cursorState,
- },
- }]));
- }
- </script>
- </transition>
- </state><!-- default -->
- </state><!-- region_sendreceive -->
- <state id="region_cursor" initial="nodelay">
- <state id="nodelay">
- <transition event="broadcast_cursor" port="in" target="../delay">
- <parameter name="x"/>
- <parameter name="y"/>
- <raise event="send">
- <parameter expr='{type:"broadcast", msg: {type:"update_cursor", name: this.getMyName(), x, y, userId: this.myId}}'/>
- </raise>
- </transition>
- </state><!-- nodelay -->
- <state id="delay">
- <onentry><script> this.cursorState = null; </script></onentry>
- <state id="inner">
- <transition event="broadcast_cursor" port="in" target=".">
- <parameter name="x"/>
- <parameter name="y"/>
- <script>
- this.cursorState = {x,y};
- </script>
- </transition>
- </state><!-- inner -->
- <transition after="0.1" target="../nodelay">
- <script>
- if (this.cursorState !== null) {
- this.addEvent(new Event("send", null, [{
- type:"broadcast",
- msg: {
- type:"update_cursor",
- userId: this.myId,
- name: this.getMyName(),
- ...this.cursorState,
- },
- }]));
- }
- </script>
- </transition>
- </state><!-- delay -->
- </state><!-- region_cursor -->
- <transition event="disconnected" target="../rejoin_when_online"/>
- </parallel><!-- joined -->
- <state id="rejoin_when_online">
- <onentry><script>
- this.uiState.setReconnecting();
- </script></onentry>
- <!-- Re-join when connection is restored -->
- <transition event="connected" target="../waiting_for_join_ack"/>
- </state><!-- rejoin when online -->
- </state><!-- session_set -->
- </state><!-- can_leave -->
- </state><!-- mode -->
- <state id="socket_region" initial="disconnected">
- <state id="disconnected">
- <transition event="connect" port="in" target="../connecting_or_connected">
- <parameter name="addr"/>
- <script>
- console.log("received connect", addr)
- this.addr = addr;
- </script>
- </transition>
- </state>
- <state id="connecting_or_connected" initial="connecting">
- <!-- Within this state, we do our best to establish a connection, and also attempt to re-establish a connection if an error occurs or if the server closes its end. -->
- <transition event="disconnect" port="in" target="../disconnected">
- <script>
- this.socket.close();
- </script>
- </transition>
- <state id="connecting">
- <onentry>
- <script>
- console.log("connecting...");
- this.socket = new WebSocket(this.addr);
- // Translate socket events to statechart events:
- this.socket.onopen = event => {
- this.controller.addInput("open", this.inports["socket"], [], this.controller.wallclockToSimtime());
- };
- this.socket.onmessage = event => {
- let parsed;
- try {
- parsed = JSON.parse(event.data);
- } catch (e) {
- console.log("received unparsable message", e);
- return;
- }
- this.controller.addInput("message", this.inports["socket"], [parsed], this.controller.wallclockToSimtime());
- };
- this.socket.onerror = event => {
- // From what I see, this event only happens when the socket could not connect.
- // An 'error' happening after the connection is established, will be indicated by a close event.
- this.controller.addInput("error", this.inports["socket"], [], this.controller.wallclockToSimtime());
- };
- this.socket.onclose = event => {
- this.controller.addInput("close", this.inports["socket"], [], this.controller.wallclockToSimtime());
- };
- </script>
- </onentry>
- <transition event="open" port="socket" target="../connected"/>
- <transition event="error" port="socket" target="../wait_reconnect">
- <!-- Error event - emitted when connection could not be established -->
- <raise event="error"/>
- </transition>
- </state>
- <parallel id="connected">
- <onentry>
- <raise event="connected"/>
- <script> this.connected = true; </script>
- </onentry>
- <onexit>
- <raise event="disconnected"/>
- <script> this.connected = false; </script>
- </onexit>
- <!-- Server closed its end -->
- <transition event="close" port="socket" target="../wait_reconnect"/>
- <state id="send_receive_region">
- <state id="ready">
- <transition event="send" target=".">
- <parameter name="json"/>
- <script>
- this.socket.send(JSON.stringify(json));
- </script>
- </transition>
- <transition event="message" port="socket" target="." cond="parsed.type !== 'pong'">
- <parameter name="parsed"/>
- <raise event="received">
- <parameter expr="parsed"/>
- </raise>
- </transition>
- </state>
- </state>
- <state id="connection_monitor_region" initial="all_good">
- <parallel id="all_good">
- <state id="send_pings">
- <state id="waiting">
- <!-- reset ping timer each time we send anything -->
- <transition event="send" port="in" target="."/>
- <!-- when not having sent anything for 1s, send a ping to let server know we're still alive -->
- <transition after="1" target=".">
- <script>
- this.socket.send(JSON.stringify({type:"ping"}));
- </script>
- </transition>
- </state>
- </state>
- <state id="receive_pongs">
- <state id="waiting">
- <!-- reset pong timer each time we receive anything -->
- <transition event="message" port="socket" target="."/>
- <!-- when not having received anything for 3s, decide the server and/or connection are (temporarily) dead -->
- <transition after="5" target=".">
- <!-- WORKAROUND: for some reason, target cannot be '../../../timeout' (limitation of 'main' SCCD, fixed in 'joeri' branch), so we generate an internal event to make the transition from higher up -->
- <raise event="timeout"/>
- </transition>
- </state>
- </state>
- <!-- part of WORKAROUND, see above -->
- <transition event="timeout" target="../timeout">
- <raise event="timeout"/>
- </transition>
- </parallel>
- <state id="timeout">
- <!-- don't send pings - just wait for any observable server activity -->
- <transition event="message" port="socket" target="../all_good">
- <raise event="timeout_recovered"/>
- </transition>
- </state>
- </state>
- </parallel>
- <state id="wait_reconnect">
- <!-- This is an intermediate state where we wait for a little while, before trying to reconnect -->
- <transition after="1" target="../connecting"/>
- </state>
- </state>
- </state><!-- socket region -->
- </parallel>
- </scxml>
- </class>
- </diagram>
|