client.xml 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. <?xml version="1.0" ?>
  2. <diagram author="Joeri Exelmans" name="client">
  3. <description>Example of a browser-based WebSocket client with heartbeats</description>
  4. <top></top>
  5. <inport name="in"/>
  6. <outport name="out"/>
  7. <class name="Main" default="true">
  8. <relationships>
  9. <association name="socket" class="Socket" min="1" max="1"/>
  10. </relationships>
  11. <inport name="ack"/>
  12. <inport name="socket"/>
  13. <constructor>
  14. <parameter name="uiState"/>
  15. <parameter name="myId"/>
  16. <parameter name="getMyName"/>
  17. <body>
  18. this.uiState = uiState;
  19. this.myId = myId;
  20. this.getMyName = getMyName;
  21. this.connected = false;
  22. </body>
  23. </constructor>
  24. <scxml
  25. big_step_maximality="take_many"
  26. internal_event_lifeline="next_combo_step">
  27. <parallel id="p">
  28. <state id="response_handler">
  29. <onentry>
  30. <script>
  31. this.reqHandlers = new Map();
  32. this.reqCounter = 0;
  33. this.sendReq = (obj, handler) => {
  34. const reqId = this.reqCounter++;
  35. const req = {
  36. reqId,
  37. ...obj,
  38. };
  39. if (handler !== undefined) {
  40. this.reqHandlers.set(reqId, handler);
  41. }
  42. console.log("Sending req", req);
  43. this.addEvent(new Event("send", null, [req]));
  44. // allow to cancel the request handler
  45. return () => {
  46. this.reqHandlers.delete(reqId);
  47. };
  48. }
  49. </script>
  50. </onentry>
  51. <state id="default">
  52. <transition event="received" target=".">
  53. <parameter name="parsed"/>
  54. <script>
  55. if (parsed.type === 'ack' &amp;&amp; parsed.reqId !== undefined) {
  56. const handler = this.reqHandlers.get(parsed.reqId);
  57. if (handler) {
  58. handler(parsed);
  59. }
  60. }
  61. </script>
  62. </transition>
  63. <transition event="disconnected" target=".">
  64. <script>
  65. // no way we will get a response for our pending requests
  66. this.reqHandlers.clear();
  67. this.reqCounter = 0;
  68. </script>
  69. </transition>
  70. </state>
  71. </state>
  72. <state id="mode" initial="uninitialized">
  73. <state id="uninitialized">
  74. <transition event="init_offline" port="in" target="../async_disconnected"/>
  75. <transition event="init_join" port="in" target="../can_leave/session_set/rejoin_when_online">
  76. <parameter name="sessionId"/>
  77. <script>
  78. this.sessionId = sessionId;
  79. </script>
  80. </transition>
  81. </state>
  82. <state id="async_disconnected">
  83. <onentry><script>
  84. this.uiState.setOfflineDisconnected();
  85. </script></onentry>
  86. <transition event="connected" target="../async_connected"/>
  87. </state><!-- async_disconnected -->
  88. <state id="async_connected">
  89. <onentry><script>
  90. this.uiState.setOfflineConnected();
  91. </script></onentry>
  92. <transition event="disconnected" target="../async_disconnected"/>
  93. <transition event="join" port="in" target="../can_leave/session_set/waiting_for_join_ack">
  94. <parameter name="sessionId"/>
  95. <script> this.sessionId = sessionId; </script>
  96. </transition>
  97. <transition event="new_share" port="in" target="../can_leave/waiting_for_new_share_ack">
  98. <parameter name="ops"/>
  99. <script>
  100. // console.log("new share event, ops=", ops);
  101. this.ops = ops;
  102. </script>
  103. </transition>
  104. </state><!-- async_connected -->
  105. <state id="can_leave" initial="session_set">
  106. <transition event="leave" cond='this.connected' port="in" target="../async_connected"/>
  107. <transition event="leave" cond='!this.connected' port="in" target="../async_disconnected"/>
  108. <onexit><script>
  109. this.sendReq({type:"leave"});
  110. </script></onexit>
  111. <state id="waiting_for_new_share_ack">
  112. <onentry><script>
  113. this.uiState.setCreatingShare();
  114. this.deleteNewShareHandler = this.sendReq(
  115. {type: "new_share", ops: this.ops},
  116. res => {
  117. // called during transition (as action code), so no need to 'wake up' the controller like we do with 'addInput':
  118. this.addEvent(new Event("ack_new_share", "ack", [res.sessionId]));
  119. });
  120. </script></onentry>
  121. <onexit><script>
  122. this.deleteNewShareHandler();
  123. </script></onexit>
  124. <transition event="disconnected" target="../reshare_when_online"/>
  125. <transition event="ack_new_share" port="ack" target="../session_set/joined">
  126. <parameter name="sessionId"/>
  127. <script>
  128. this.sessionId = sessionId;
  129. </script>
  130. <raise event="ack_new_share" port="out" scope="output">
  131. <parameter expr="sessionId"/>
  132. </raise>
  133. </transition>
  134. </state><!-- waiting_for_new_share_ack -->
  135. <state id="reshare_when_online">
  136. <onentry><script>
  137. this.uiState.setReconnecting();
  138. </script></onentry>
  139. <transition event="connected" target="../waiting_for_new_share_ack"/>
  140. </state>
  141. <state id="session_set" initial="waiting_for_join_ack">
  142. <onentry><script>
  143. if (!this.sessionId) {
  144. throw new Error("DEBUG: no session id");
  145. }
  146. this.uiState.setSession(this.sessionId);
  147. </script></onentry>
  148. <onexit> <script>
  149. this.uiState.unsetSession();
  150. </script> </onexit>
  151. <state id="waiting_for_join_ack">
  152. <onentry><script>
  153. this.uiState.setJoining();
  154. this.deleteJoinHandler = this.sendReq(
  155. {type: "join", sessionId: this.sessionId},
  156. res => {
  157. // called during transition (as action code), so no need to 'wake up' the controller like we do with 'addInput':
  158. this.addEvent(new Event("ack_join", "ack", [res.ops]));
  159. });
  160. </script></onentry>
  161. <onexit><script>
  162. this.deleteJoinHandler();
  163. </script></onexit>
  164. <transition event="ack_join" port="ack" target="../joined">
  165. <parameter name="ops"/>
  166. <raise event="ack_join" port="out" scope="output">
  167. <parameter expr="ops"/>
  168. </raise>
  169. </transition>
  170. <transition event="disconnected" target="../rejoin_when_online"/>
  171. </state><!-- waiting_for_join_ack -->
  172. <parallel id="joined">
  173. <state id="region_sendreceive">
  174. <state id="default">
  175. <onentry>
  176. <script>
  177. this.uiState.setOnline();
  178. </script>
  179. </onentry>
  180. <transition event="received" cond="parsed.type === 'pub_edit'" target=".">
  181. <parameter name="parsed"/>
  182. <script>
  183. if (parsed.sessionId !== this.sessionId) {
  184. throw new Error("Unexpected: received edit for another session:" + parsed.sessionId);
  185. }
  186. </script>
  187. <raise event="received_op" port="out" scope="output">
  188. <parameter expr="parsed.op"/>
  189. </raise>
  190. </transition>
  191. <transition event="new_edit" port="in" target=".">
  192. <parameter name="serialized"/>
  193. <script>
  194. this.sendReq(
  195. {type:"new_edit", sessionId: this.sessionId, op: serialized},
  196. res => {
  197. // this.unacknowledged.delete(serialized.id);
  198. });
  199. // this.unacknowledged.set(serialized.id, serialized);
  200. </script>
  201. </transition>
  202. <transition event="request_lock" port="in" target=".">
  203. <parameter name="cellIds"/>
  204. <parameter name="acquiredCallback"/>
  205. <parameter name="deniedCallback"/>
  206. <script>
  207. this.sendReq(
  208. {type:"request_lock", sessionId: this.sessionId, cellIds},
  209. res => {
  210. if (res.success) {
  211. acquiredCallback();
  212. } else {
  213. deniedCallback();
  214. }
  215. });
  216. </script>
  217. </transition>
  218. <transition event="release_lock" port="in" target=".">
  219. <parameter name="cellIds"/>
  220. <script>
  221. this.sendReq(
  222. {type:"release_lock", sessionId: this.sessionId, cellIds},
  223. res => {},
  224. );
  225. </script>
  226. </transition>
  227. <transition event="received" cond="parsed.type === 'broadcast'" target=".">
  228. <parameter name="parsed"/>
  229. <raise event="broadcast" port="out" scope="output">
  230. <parameter expr="parsed.sessionId"/>
  231. <parameter expr="parsed.msg"/>
  232. </raise>
  233. </transition>
  234. <transition event="broadcast_selection" target=".">
  235. <parameter name="selectedIds"/>
  236. <script>
  237. if (this.cursorState !== null) {
  238. this.addEvent(new Event("send", null, [{
  239. type:"broadcast",
  240. msg: {
  241. type:"update_selection",
  242. userId: this.myId,
  243. selectedIds,
  244. ...this.cursorState,
  245. },
  246. }]));
  247. }
  248. </script>
  249. </transition>
  250. </state><!-- default -->
  251. </state><!-- region_sendreceive -->
  252. <state id="region_cursor" initial="nodelay">
  253. <state id="nodelay">
  254. <transition event="broadcast_cursor" port="in" target="../delay">
  255. <parameter name="x"/>
  256. <parameter name="y"/>
  257. <raise event="send">
  258. <parameter expr='{type:"broadcast", msg: {type:"update_cursor", name: this.getMyName(), x, y, userId: this.myId}}'/>
  259. </raise>
  260. </transition>
  261. </state><!-- nodelay -->
  262. <state id="delay">
  263. <onentry><script> this.cursorState = null; </script></onentry>
  264. <state id="inner">
  265. <transition event="broadcast_cursor" port="in" target=".">
  266. <parameter name="x"/>
  267. <parameter name="y"/>
  268. <script>
  269. this.cursorState = {x,y};
  270. </script>
  271. </transition>
  272. </state><!-- inner -->
  273. <transition after="0.1" target="../nodelay">
  274. <script>
  275. if (this.cursorState !== null) {
  276. this.addEvent(new Event("send", null, [{
  277. type:"broadcast",
  278. msg: {
  279. type:"update_cursor",
  280. userId: this.myId,
  281. name: this.getMyName(),
  282. ...this.cursorState,
  283. },
  284. }]));
  285. }
  286. </script>
  287. </transition>
  288. </state><!-- delay -->
  289. </state><!-- region_cursor -->
  290. <transition event="disconnected" target="../rejoin_when_online"/>
  291. </parallel><!-- joined -->
  292. <state id="rejoin_when_online">
  293. <onentry><script>
  294. this.uiState.setReconnecting();
  295. </script></onentry>
  296. <!-- Re-join when connection is restored -->
  297. <transition event="connected" target="../waiting_for_join_ack"/>
  298. </state><!-- rejoin when online -->
  299. </state><!-- session_set -->
  300. </state><!-- can_leave -->
  301. </state><!-- mode -->
  302. <state id="socket_region" initial="disconnected">
  303. <state id="disconnected">
  304. <transition event="connect" port="in" target="../connecting_or_connected">
  305. <parameter name="addr"/>
  306. <script>
  307. console.log("received connect", addr)
  308. this.addr = addr;
  309. </script>
  310. </transition>
  311. </state>
  312. <state id="connecting_or_connected" initial="connecting">
  313. <!-- 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. -->
  314. <transition event="disconnect" port="in" target="../disconnected">
  315. <script>
  316. this.socket.close();
  317. </script>
  318. </transition>
  319. <state id="connecting">
  320. <onentry>
  321. <script>
  322. console.log("connecting...");
  323. this.socket = new WebSocket(this.addr);
  324. // Translate socket events to statechart events:
  325. this.socket.onopen = event => {
  326. this.controller.addInput("open", this.inports["socket"], [], this.controller.wallclockToSimtime());
  327. };
  328. this.socket.onmessage = event => {
  329. let parsed;
  330. try {
  331. parsed = JSON.parse(event.data);
  332. } catch (e) {
  333. console.log("received unparsable message", e);
  334. return;
  335. }
  336. this.controller.addInput("message", this.inports["socket"], [parsed], this.controller.wallclockToSimtime());
  337. };
  338. this.socket.onerror = event => {
  339. // From what I see, this event only happens when the socket could not connect.
  340. // An 'error' happening after the connection is established, will be indicated by a close event.
  341. this.controller.addInput("error", this.inports["socket"], [], this.controller.wallclockToSimtime());
  342. };
  343. this.socket.onclose = event => {
  344. this.controller.addInput("close", this.inports["socket"], [], this.controller.wallclockToSimtime());
  345. };
  346. </script>
  347. </onentry>
  348. <transition event="open" port="socket" target="../connected"/>
  349. <transition event="error" port="socket" target="../wait_reconnect">
  350. <!-- Error event - emitted when connection could not be established -->
  351. <raise event="error"/>
  352. </transition>
  353. </state>
  354. <parallel id="connected">
  355. <onentry>
  356. <raise event="connected"/>
  357. <script> this.connected = true; </script>
  358. </onentry>
  359. <onexit>
  360. <raise event="disconnected"/>
  361. <script> this.connected = false; </script>
  362. </onexit>
  363. <!-- Server closed its end -->
  364. <transition event="close" port="socket" target="../wait_reconnect"/>
  365. <state id="send_receive_region">
  366. <state id="ready">
  367. <transition event="send" target=".">
  368. <parameter name="json"/>
  369. <script>
  370. this.socket.send(JSON.stringify(json));
  371. </script>
  372. </transition>
  373. <transition event="message" port="socket" target="." cond="parsed.type !== 'pong'">
  374. <parameter name="parsed"/>
  375. <raise event="received">
  376. <parameter expr="parsed"/>
  377. </raise>
  378. </transition>
  379. </state>
  380. </state>
  381. <state id="connection_monitor_region" initial="all_good">
  382. <parallel id="all_good">
  383. <state id="send_pings">
  384. <state id="waiting">
  385. <!-- reset ping timer each time we send anything -->
  386. <transition event="send" port="in" target="."/>
  387. <!-- when not having sent anything for 1s, send a ping to let server know we're still alive -->
  388. <transition after="1" target=".">
  389. <script>
  390. this.socket.send(JSON.stringify({type:"ping"}));
  391. </script>
  392. </transition>
  393. </state>
  394. </state>
  395. <state id="receive_pongs">
  396. <state id="waiting">
  397. <!-- reset pong timer each time we receive anything -->
  398. <transition event="message" port="socket" target="."/>
  399. <!-- when not having received anything for 3s, decide the server and/or connection are (temporarily) dead -->
  400. <transition after="5" target=".">
  401. <!-- 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 -->
  402. <raise event="timeout"/>
  403. </transition>
  404. </state>
  405. </state>
  406. <!-- part of WORKAROUND, see above -->
  407. <transition event="timeout" target="../timeout">
  408. <raise event="timeout"/>
  409. </transition>
  410. </parallel>
  411. <state id="timeout">
  412. <!-- don't send pings - just wait for any observable server activity -->
  413. <transition event="message" port="socket" target="../all_good">
  414. <raise event="timeout_recovered"/>
  415. </transition>
  416. </state>
  417. </state>
  418. </parallel>
  419. <state id="wait_reconnect">
  420. <!-- This is an intermediate state where we wait for a little while, before trying to reconnect -->
  421. <transition after="1" target="../connecting"/>
  422. </state>
  423. </state>
  424. </state><!-- socket region -->
  425. </parallel>
  426. </scxml>
  427. </class>
  428. </diagram>