Procházet zdrojové kódy

Implemented ghost cursors.

Joeri Exelmans před 4 roky
rodič
revize
aa8b621672

+ 48 - 0
lib/versioning/DisabledCells.js

@@ -0,0 +1,48 @@
+
+class DisabledCells {
+  constructor() {
+    this.disabledCells = new Set();
+  }
+
+  add(cell) {
+    this.disabledCells.add(cell);
+  }
+
+  delete(cell) {
+    this.disabledCells.delete(cell);
+  }
+
+  install(graph) {
+    const self = this;
+    // part #1: Intercepting mxGraph.fireMouseEvent
+    const oldFireMouseEvent = graph.fireMouseEvent;
+    graph.fireMouseEvent = function(evtName, me, sender) {
+      if (me.state && self.disabledCells.has(me.state.cell.id)) {
+        // clicked shape is disabled
+        return;
+      }
+      oldFireMouseEvent.apply(this, arguments);
+    }
+
+    // part #2: Ignore double clicks on disabled cells
+    const oldDblClick = graph.dblClick;
+    graph.dblClick = function(evt, cell) {
+      if (cell && self.disabledCells.has(cell.id)) {
+        // clicked shape is disabled
+        return;
+      }
+      oldDblClick.apply(this, arguments);
+    }
+    // part #3: Protect disabled cells from ever being selected
+    const oldMxSelectionChange = mxSelectionChange; // override constructor :)
+    mxSelectionChange = function(selectionModel, added, removed) {
+      oldMxSelectionChange.apply(this, arguments);
+      if (this.added) {
+        this.added = this.added.filter(cell => !self.disabledCells.has(cell.id));
+      }
+    }
+    mxSelectionChange.prototype = oldMxSelectionChange.prototype;
+  }
+}
+
+module.exports = { DisabledCells };

+ 801 - 0
lib/versioning/client.js

@@ -0,0 +1,801 @@
+/*
+Date: Mon Sep 20 13:24:37 2021
+
+Model author: Joeri Exelmans
+Model name: client
+Model description:
+Example of a browser-based WebSocket client with heartbeats
+*/
+
+
+// package client
+let client = {};
+(function() {
+
+let Main = function(controller, uiState) {
+    RuntimeClassBase.call(this, controller);
+    
+    this.inports["ack"] = controller.addInputPort("ack", this);
+    this.inports["socket"] = controller.addInputPort("socket", this);
+    
+    this.semantics.bigStepMaximality = StatechartSemantics.TakeMany;
+    this.semantics.internalEventLifeline = StatechartSemantics.NextComboStep;
+    this.semantics.inputEventLifeline = StatechartSemantics.FirstComboStep;
+    this.semantics.priority = StatechartSemantics.SourceParent;
+    this.semantics.concurrency = StatechartSemantics.Single;
+    
+    // build Statechart structure
+    this.buildStatechartStructure();
+    
+    // call user defined constructor
+    Main.prototype.userDefinedConstructor.call(this, uiState);
+};
+Main.prototype = new Object();
+(function() {
+    var proto = new RuntimeClassBase();
+    for (let prop in proto) {
+        Main.prototype[prop] = proto[prop];
+    }
+})();
+
+Main.prototype.userDefinedConstructor = function(uiState) {
+    console.log("Main constructor");
+    this.uiState = uiState;
+    this.connected = false;
+};
+
+Main.prototype.userDefinedDestructor = function() {
+};
+
+
+// builds Statechart structure
+Main.prototype.buildStatechartStructure = function() {
+    
+    // state <root>
+    this.states[""] = new State(0, "", this);
+    
+    // state /p
+    this.states["/p"] = new ParallelState(1, "/p", this);
+    
+    // state /p/response_handler
+    this.states["/p/response_handler"] = new State(2, "/p/response_handler", this);
+    this.states["/p/response_handler"].setEnter(this.PResponseHandlerEnter);
+    
+    // state /p/response_handler/default
+    this.states["/p/response_handler/default"] = new State(3, "/p/response_handler/default", this);
+    
+    // state /p/mode
+    this.states["/p/mode"] = new State(4, "/p/mode", this);
+    
+    // state /p/mode/uninitialized
+    this.states["/p/mode/uninitialized"] = new State(5, "/p/mode/uninitialized", this);
+    
+    // state /p/mode/async_disconnected
+    this.states["/p/mode/async_disconnected"] = new State(6, "/p/mode/async_disconnected", this);
+    this.states["/p/mode/async_disconnected"].setEnter(this.PModeAsyncDisconnectedEnter);
+    
+    // state /p/mode/async_connected
+    this.states["/p/mode/async_connected"] = new State(7, "/p/mode/async_connected", this);
+    this.states["/p/mode/async_connected"].setEnter(this.PModeAsyncConnectedEnter);
+    
+    // state /p/mode/can_leave
+    this.states["/p/mode/can_leave"] = new State(8, "/p/mode/can_leave", this);
+    this.states["/p/mode/can_leave"].setExit(this.PModeCanLeaveExit);
+    
+    // state /p/mode/can_leave/waiting_for_new_share_ack
+    this.states["/p/mode/can_leave/waiting_for_new_share_ack"] = new State(9, "/p/mode/can_leave/waiting_for_new_share_ack", this);
+    this.states["/p/mode/can_leave/waiting_for_new_share_ack"].setEnter(this.PModeCanLeaveWaitingForNewShareAckEnter);
+    this.states["/p/mode/can_leave/waiting_for_new_share_ack"].setExit(this.PModeCanLeaveWaitingForNewShareAckExit);
+    
+    // state /p/mode/can_leave/reshare_when_online
+    this.states["/p/mode/can_leave/reshare_when_online"] = new State(10, "/p/mode/can_leave/reshare_when_online", this);
+    this.states["/p/mode/can_leave/reshare_when_online"].setEnter(this.PModeCanLeaveReshareWhenOnlineEnter);
+    
+    // state /p/mode/can_leave/session_set
+    this.states["/p/mode/can_leave/session_set"] = new State(11, "/p/mode/can_leave/session_set", this);
+    this.states["/p/mode/can_leave/session_set"].setEnter(this.PModeCanLeaveSessionSetEnter);
+    this.states["/p/mode/can_leave/session_set"].setExit(this.PModeCanLeaveSessionSetExit);
+    
+    // state /p/mode/can_leave/session_set/waiting_for_join_ack
+    this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"] = new State(12, "/p/mode/can_leave/session_set/waiting_for_join_ack", this);
+    this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"].setEnter(this.PModeCanLeaveSessionSetWaitingForJoinAckEnter);
+    this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"].setExit(this.PModeCanLeaveSessionSetWaitingForJoinAckExit);
+    
+    // state /p/mode/can_leave/session_set/joined
+    this.states["/p/mode/can_leave/session_set/joined"] = new ParallelState(13, "/p/mode/can_leave/session_set/joined", this);
+    
+    // state /p/mode/can_leave/session_set/joined/region_sendreceive
+    this.states["/p/mode/can_leave/session_set/joined/region_sendreceive"] = new State(14, "/p/mode/can_leave/session_set/joined/region_sendreceive", this);
+    
+    // state /p/mode/can_leave/session_set/joined/region_sendreceive/default
+    this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"] = new State(15, "/p/mode/can_leave/session_set/joined/region_sendreceive/default", this);
+    this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"].setEnter(this.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefaultEnter);
+    this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"].setExit(this.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefaultExit);
+    
+    // state /p/mode/can_leave/session_set/joined/region_cursor
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor"] = new State(16, "/p/mode/can_leave/session_set/joined/region_cursor", this);
+    
+    // state /p/mode/can_leave/session_set/joined/region_cursor/nodelay
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/nodelay"] = new State(17, "/p/mode/can_leave/session_set/joined/region_cursor/nodelay", this);
+    
+    // state /p/mode/can_leave/session_set/joined/region_cursor/delay
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"] = new State(18, "/p/mode/can_leave/session_set/joined/region_cursor/delay", this);
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"].setEnter(this.PModeCanLeaveSessionSetJoinedRegionCursorDelayEnter);
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"].setExit(this.PModeCanLeaveSessionSetJoinedRegionCursorDelayExit);
+    
+    // state /p/mode/can_leave/session_set/joined/region_cursor/delay/inner
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay/inner"] = new State(19, "/p/mode/can_leave/session_set/joined/region_cursor/delay/inner", this);
+    
+    // state /p/mode/can_leave/session_set/rejoin_when_online
+    this.states["/p/mode/can_leave/session_set/rejoin_when_online"] = new State(20, "/p/mode/can_leave/session_set/rejoin_when_online", this);
+    this.states["/p/mode/can_leave/session_set/rejoin_when_online"].setEnter(this.PModeCanLeaveSessionSetRejoinWhenOnlineEnter);
+    
+    // state /p/socket_region
+    this.states["/p/socket_region"] = new State(21, "/p/socket_region", this);
+    
+    // state /p/socket_region/disconnected
+    this.states["/p/socket_region/disconnected"] = new State(22, "/p/socket_region/disconnected", this);
+    
+    // state /p/socket_region/connecting_or_connected
+    this.states["/p/socket_region/connecting_or_connected"] = new State(23, "/p/socket_region/connecting_or_connected", this);
+    
+    // state /p/socket_region/connecting_or_connected/connecting
+    this.states["/p/socket_region/connecting_or_connected/connecting"] = new State(24, "/p/socket_region/connecting_or_connected/connecting", this);
+    this.states["/p/socket_region/connecting_or_connected/connecting"].setEnter(this.PSocketRegionConnectingOrConnectedConnectingEnter);
+    
+    // state /p/socket_region/connecting_or_connected/connected
+    this.states["/p/socket_region/connecting_or_connected/connected"] = new ParallelState(25, "/p/socket_region/connecting_or_connected/connected", this);
+    this.states["/p/socket_region/connecting_or_connected/connected"].setEnter(this.PSocketRegionConnectingOrConnectedConnectedEnter);
+    this.states["/p/socket_region/connecting_or_connected/connected"].setExit(this.PSocketRegionConnectingOrConnectedConnectedExit);
+    
+    // state /p/socket_region/connecting_or_connected/connected/send_receive_region
+    this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region"] = new State(26, "/p/socket_region/connecting_or_connected/connected/send_receive_region", this);
+    
+    // state /p/socket_region/connecting_or_connected/connected/send_receive_region/ready
+    this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region/ready"] = new State(27, "/p/socket_region/connecting_or_connected/connected/send_receive_region/ready", this);
+    
+    // state /p/socket_region/connecting_or_connected/connected/connection_monitor_region
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region"] = new State(28, "/p/socket_region/connecting_or_connected/connected/connection_monitor_region", this);
+    
+    // state /p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good"] = new ParallelState(29, "/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good", this);
+    
+    // state /p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings"] = new State(30, "/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings", this);
+    
+    // state /p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"] = new State(31, "/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting", this);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"].setEnter(this.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaitingEnter);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"].setExit(this.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaitingExit);
+    
+    // state /p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs"] = new State(32, "/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs", this);
+    
+    // state /p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"] = new State(33, "/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting", this);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"].setEnter(this.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaitingEnter);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"].setExit(this.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaitingExit);
+    
+    // state /p/socket_region/connecting_or_connected/connected/connection_monitor_region/timeout
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/timeout"] = new State(34, "/p/socket_region/connecting_or_connected/connected/connection_monitor_region/timeout", this);
+    
+    // state /p/socket_region/connecting_or_connected/wait_reconnect
+    this.states["/p/socket_region/connecting_or_connected/wait_reconnect"] = new State(35, "/p/socket_region/connecting_or_connected/wait_reconnect", this);
+    this.states["/p/socket_region/connecting_or_connected/wait_reconnect"].setEnter(this.PSocketRegionConnectingOrConnectedWaitReconnectEnter);
+    this.states["/p/socket_region/connecting_or_connected/wait_reconnect"].setExit(this.PSocketRegionConnectingOrConnectedWaitReconnectExit);
+    
+    // add children
+    this.states[""].addChild(this.states["/p"]);
+    this.states["/p"].addChild(this.states["/p/response_handler"]);
+    this.states["/p"].addChild(this.states["/p/mode"]);
+    this.states["/p"].addChild(this.states["/p/socket_region"]);
+    this.states["/p/response_handler"].addChild(this.states["/p/response_handler/default"]);
+    this.states["/p/mode"].addChild(this.states["/p/mode/uninitialized"]);
+    this.states["/p/mode"].addChild(this.states["/p/mode/async_disconnected"]);
+    this.states["/p/mode"].addChild(this.states["/p/mode/async_connected"]);
+    this.states["/p/mode"].addChild(this.states["/p/mode/can_leave"]);
+    this.states["/p/mode/can_leave"].addChild(this.states["/p/mode/can_leave/waiting_for_new_share_ack"]);
+    this.states["/p/mode/can_leave"].addChild(this.states["/p/mode/can_leave/reshare_when_online"]);
+    this.states["/p/mode/can_leave"].addChild(this.states["/p/mode/can_leave/session_set"]);
+    this.states["/p/mode/can_leave/session_set"].addChild(this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"]);
+    this.states["/p/mode/can_leave/session_set"].addChild(this.states["/p/mode/can_leave/session_set/joined"]);
+    this.states["/p/mode/can_leave/session_set"].addChild(this.states["/p/mode/can_leave/session_set/rejoin_when_online"]);
+    this.states["/p/mode/can_leave/session_set/joined"].addChild(this.states["/p/mode/can_leave/session_set/joined/region_sendreceive"]);
+    this.states["/p/mode/can_leave/session_set/joined"].addChild(this.states["/p/mode/can_leave/session_set/joined/region_cursor"]);
+    this.states["/p/mode/can_leave/session_set/joined/region_sendreceive"].addChild(this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"]);
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor"].addChild(this.states["/p/mode/can_leave/session_set/joined/region_cursor/nodelay"]);
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor"].addChild(this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"]);
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"].addChild(this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay/inner"]);
+    this.states["/p/socket_region"].addChild(this.states["/p/socket_region/disconnected"]);
+    this.states["/p/socket_region"].addChild(this.states["/p/socket_region/connecting_or_connected"]);
+    this.states["/p/socket_region/connecting_or_connected"].addChild(this.states["/p/socket_region/connecting_or_connected/connecting"]);
+    this.states["/p/socket_region/connecting_or_connected"].addChild(this.states["/p/socket_region/connecting_or_connected/connected"]);
+    this.states["/p/socket_region/connecting_or_connected"].addChild(this.states["/p/socket_region/connecting_or_connected/wait_reconnect"]);
+    this.states["/p/socket_region/connecting_or_connected/connected"].addChild(this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region"]);
+    this.states["/p/socket_region/connecting_or_connected/connected"].addChild(this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region"]);
+    this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region"].addChild(this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region/ready"]);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region"].addChild(this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good"]);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region"].addChild(this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/timeout"]);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good"].addChild(this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings"]);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good"].addChild(this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs"]);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings"].addChild(this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"]);
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs"].addChild(this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"]);
+    this.states[""].fixTree();
+    this.states[""].defaultState = this.states["/p"];
+    this.states["/p/response_handler"].defaultState = this.states["/p/response_handler/default"];
+    this.states["/p/mode"].defaultState = this.states["/p/mode/uninitialized"];
+    this.states["/p/mode/can_leave"].defaultState = this.states["/p/mode/can_leave/session_set"];
+    this.states["/p/mode/can_leave/session_set"].defaultState = this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"];
+    this.states["/p/mode/can_leave/session_set/joined/region_sendreceive"].defaultState = this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"];
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor"].defaultState = this.states["/p/mode/can_leave/session_set/joined/region_cursor/nodelay"];
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"].defaultState = this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay/inner"];
+    this.states["/p/socket_region"].defaultState = this.states["/p/socket_region/disconnected"];
+    this.states["/p/socket_region/connecting_or_connected"].defaultState = this.states["/p/socket_region/connecting_or_connected/connecting"];
+    this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region"].defaultState = this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region/ready"];
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region"].defaultState = this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good"];
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings"].defaultState = this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"];
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs"].defaultState = this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"];
+    
+    // transition /p/response_handler/default
+    var PResponseHandlerDefault_0 = new Transition(this, this.states["/p/response_handler/default"], [this.states["/p/response_handler/default"]]);
+    PResponseHandlerDefault_0.setAction(this.PResponseHandlerDefault_0Exec);
+    PResponseHandlerDefault_0.setTrigger(new Event("received", null));
+    this.states["/p/response_handler/default"].addTransition(PResponseHandlerDefault_0);
+    var PResponseHandlerDefault_1 = new Transition(this, this.states["/p/response_handler/default"], [this.states["/p/response_handler/default"]]);
+    PResponseHandlerDefault_1.setAction(this.PResponseHandlerDefault_1Exec);
+    PResponseHandlerDefault_1.setTrigger(new Event("disconnected", null));
+    this.states["/p/response_handler/default"].addTransition(PResponseHandlerDefault_1);
+    
+    // transition /p/mode/uninitialized
+    var PModeUninitialized_0 = new Transition(this, this.states["/p/mode/uninitialized"], [this.states["/p/mode/async_disconnected"]]);
+    PModeUninitialized_0.setTrigger(new Event("init_offline", this.getInPortName("in")));
+    this.states["/p/mode/uninitialized"].addTransition(PModeUninitialized_0);
+    var PModeUninitialized_1 = new Transition(this, this.states["/p/mode/uninitialized"], [this.states["/p/mode/can_leave/session_set/rejoin_when_online"]]);
+    PModeUninitialized_1.setAction(this.PModeUninitialized_1Exec);
+    PModeUninitialized_1.setTrigger(new Event("init_join", this.getInPortName("in")));
+    this.states["/p/mode/uninitialized"].addTransition(PModeUninitialized_1);
+    
+    // transition /p/mode/async_disconnected
+    var PModeAsyncDisconnected_0 = new Transition(this, this.states["/p/mode/async_disconnected"], [this.states["/p/mode/async_connected"]]);
+    PModeAsyncDisconnected_0.setTrigger(new Event("connected", null));
+    this.states["/p/mode/async_disconnected"].addTransition(PModeAsyncDisconnected_0);
+    
+    // transition /p/mode/async_connected
+    var PModeAsyncConnected_0 = new Transition(this, this.states["/p/mode/async_connected"], [this.states["/p/mode/async_disconnected"]]);
+    PModeAsyncConnected_0.setTrigger(new Event("disconnected", null));
+    this.states["/p/mode/async_connected"].addTransition(PModeAsyncConnected_0);
+    var PModeAsyncConnected_1 = new Transition(this, this.states["/p/mode/async_connected"], [this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"]]);
+    PModeAsyncConnected_1.setAction(this.PModeAsyncConnected_1Exec);
+    PModeAsyncConnected_1.setTrigger(new Event("join", this.getInPortName("in")));
+    this.states["/p/mode/async_connected"].addTransition(PModeAsyncConnected_1);
+    var PModeAsyncConnected_2 = new Transition(this, this.states["/p/mode/async_connected"], [this.states["/p/mode/can_leave/waiting_for_new_share_ack"]]);
+    PModeAsyncConnected_2.setAction(this.PModeAsyncConnected_2Exec);
+    PModeAsyncConnected_2.setTrigger(new Event("new_share", this.getInPortName("in")));
+    this.states["/p/mode/async_connected"].addTransition(PModeAsyncConnected_2);
+    
+    // transition /p/mode/can_leave/waiting_for_new_share_ack
+    var PModeCanLeaveWaitingForNewShareAck_0 = new Transition(this, this.states["/p/mode/can_leave/waiting_for_new_share_ack"], [this.states["/p/mode/can_leave/reshare_when_online"]]);
+    PModeCanLeaveWaitingForNewShareAck_0.setTrigger(new Event("disconnected", null));
+    this.states["/p/mode/can_leave/waiting_for_new_share_ack"].addTransition(PModeCanLeaveWaitingForNewShareAck_0);
+    var PModeCanLeaveWaitingForNewShareAck_1 = new Transition(this, this.states["/p/mode/can_leave/waiting_for_new_share_ack"], [this.states["/p/mode/can_leave/session_set/joined"]]);
+    PModeCanLeaveWaitingForNewShareAck_1.setAction(this.PModeCanLeaveWaitingForNewShareAck_1Exec);
+    PModeCanLeaveWaitingForNewShareAck_1.setTrigger(new Event("ack_new_share", this.getInPortName("ack")));
+    this.states["/p/mode/can_leave/waiting_for_new_share_ack"].addTransition(PModeCanLeaveWaitingForNewShareAck_1);
+    
+    // transition /p/mode/can_leave/reshare_when_online
+    var PModeCanLeaveReshareWhenOnline_0 = new Transition(this, this.states["/p/mode/can_leave/reshare_when_online"], [this.states["/p/mode/can_leave/waiting_for_new_share_ack"]]);
+    PModeCanLeaveReshareWhenOnline_0.setTrigger(new Event("connected", null));
+    this.states["/p/mode/can_leave/reshare_when_online"].addTransition(PModeCanLeaveReshareWhenOnline_0);
+    
+    // transition /p/mode/can_leave/session_set/waiting_for_join_ack
+    var PModeCanLeaveSessionSetWaitingForJoinAck_0 = new Transition(this, this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"], [this.states["/p/mode/can_leave/session_set/joined"]]);
+    PModeCanLeaveSessionSetWaitingForJoinAck_0.setAction(this.PModeCanLeaveSessionSetWaitingForJoinAck_0Exec);
+    PModeCanLeaveSessionSetWaitingForJoinAck_0.setTrigger(new Event("ack_join", this.getInPortName("ack")));
+    this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"].addTransition(PModeCanLeaveSessionSetWaitingForJoinAck_0);
+    var PModeCanLeaveSessionSetWaitingForJoinAck_1 = new Transition(this, this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"], [this.states["/p/mode/can_leave/session_set/rejoin_when_online"]]);
+    PModeCanLeaveSessionSetWaitingForJoinAck_1.setTrigger(new Event("disconnected", null));
+    this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"].addTransition(PModeCanLeaveSessionSetWaitingForJoinAck_1);
+    
+    // transition /p/mode/can_leave/session_set/joined/region_sendreceive/default
+    var PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_0 = new Transition(this, this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"], [this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"]]);
+    PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_0.setAction(this.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_0Exec);
+    PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_0.setTrigger(new Event("received", null));
+    PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_0.setGuard(this.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_0Guard);
+    this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"].addTransition(PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_0);
+    var PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_1 = new Transition(this, this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"], [this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"]]);
+    PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_1.setAction(this.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_1Exec);
+    PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_1.setTrigger(new Event("new_edit", this.getInPortName("in")));
+    this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"].addTransition(PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_1);
+    var PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_2 = new Transition(this, this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"], [this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"]]);
+    PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_2.setAction(this.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_2Exec);
+    PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_2.setTrigger(new Event("received", null));
+    PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_2.setGuard(this.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_2Guard);
+    this.states["/p/mode/can_leave/session_set/joined/region_sendreceive/default"].addTransition(PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_2);
+    
+    // transition /p/mode/can_leave/session_set/joined/region_cursor/nodelay
+    var PModeCanLeaveSessionSetJoinedRegionCursorNodelay_0 = new Transition(this, this.states["/p/mode/can_leave/session_set/joined/region_cursor/nodelay"], [this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"]]);
+    PModeCanLeaveSessionSetJoinedRegionCursorNodelay_0.setAction(this.PModeCanLeaveSessionSetJoinedRegionCursorNodelay_0Exec);
+    PModeCanLeaveSessionSetJoinedRegionCursorNodelay_0.setTrigger(new Event("update_cursor", this.getInPortName("in")));
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/nodelay"].addTransition(PModeCanLeaveSessionSetJoinedRegionCursorNodelay_0);
+    
+    // transition /p/mode/can_leave/session_set/joined/region_cursor/delay/inner
+    var PModeCanLeaveSessionSetJoinedRegionCursorDelayInner_0 = new Transition(this, this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay/inner"], [this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay/inner"]]);
+    PModeCanLeaveSessionSetJoinedRegionCursorDelayInner_0.setAction(this.PModeCanLeaveSessionSetJoinedRegionCursorDelayInner_0Exec);
+    PModeCanLeaveSessionSetJoinedRegionCursorDelayInner_0.setTrigger(new Event("update_cursor", this.getInPortName("in")));
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay/inner"].addTransition(PModeCanLeaveSessionSetJoinedRegionCursorDelayInner_0);
+    
+    // transition /p/mode/can_leave/session_set/rejoin_when_online
+    var PModeCanLeaveSessionSetRejoinWhenOnline_0 = new Transition(this, this.states["/p/mode/can_leave/session_set/rejoin_when_online"], [this.states["/p/mode/can_leave/session_set/waiting_for_join_ack"]]);
+    PModeCanLeaveSessionSetRejoinWhenOnline_0.setTrigger(new Event("connected", null));
+    this.states["/p/mode/can_leave/session_set/rejoin_when_online"].addTransition(PModeCanLeaveSessionSetRejoinWhenOnline_0);
+    
+    // transition /p/socket_region/disconnected
+    var PSocketRegionDisconnected_0 = new Transition(this, this.states["/p/socket_region/disconnected"], [this.states["/p/socket_region/connecting_or_connected"]]);
+    PSocketRegionDisconnected_0.setAction(this.PSocketRegionDisconnected_0Exec);
+    PSocketRegionDisconnected_0.setTrigger(new Event("connect", this.getInPortName("in")));
+    this.states["/p/socket_region/disconnected"].addTransition(PSocketRegionDisconnected_0);
+    
+    // transition /p/socket_region/connecting_or_connected/connecting
+    var PSocketRegionConnectingOrConnectedConnecting_0 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connecting"], [this.states["/p/socket_region/connecting_or_connected/connected"]]);
+    PSocketRegionConnectingOrConnectedConnecting_0.setTrigger(new Event("open", this.getInPortName("socket")));
+    this.states["/p/socket_region/connecting_or_connected/connecting"].addTransition(PSocketRegionConnectingOrConnectedConnecting_0);
+    var PSocketRegionConnectingOrConnectedConnecting_1 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connecting"], [this.states["/p/socket_region/connecting_or_connected/wait_reconnect"]]);
+    PSocketRegionConnectingOrConnectedConnecting_1.setAction(this.PSocketRegionConnectingOrConnectedConnecting_1Exec);
+    PSocketRegionConnectingOrConnectedConnecting_1.setTrigger(new Event("error", this.getInPortName("socket")));
+    this.states["/p/socket_region/connecting_or_connected/connecting"].addTransition(PSocketRegionConnectingOrConnectedConnecting_1);
+    
+    // transition /p/socket_region/connecting_or_connected/connected/send_receive_region/ready
+    var PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_0 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region/ready"], [this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region/ready"]]);
+    PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_0.setAction(this.PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_0Exec);
+    PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_0.setTrigger(new Event("send", null));
+    this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region/ready"].addTransition(PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_0);
+    var PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_1 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region/ready"], [this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region/ready"]]);
+    PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_1.setAction(this.PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_1Exec);
+    PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_1.setTrigger(new Event("message", this.getInPortName("socket")));
+    PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_1.setGuard(this.PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_1Guard);
+    this.states["/p/socket_region/connecting_or_connected/connected/send_receive_region/ready"].addTransition(PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_1);
+    
+    // transition /p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting
+    var PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaiting_0 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"], [this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"]]);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaiting_0.setTrigger(new Event("send", this.getInPortName("in")));
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"].addTransition(PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaiting_0);
+    var PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaiting_1 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"], [this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"]]);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaiting_1.setAction(this.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaiting_1Exec);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaiting_1.setTrigger(new Event("event2After"));
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/send_pings/waiting"].addTransition(PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaiting_1);
+    
+    // transition /p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting
+    var PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaiting_0 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"], [this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"]]);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaiting_0.setTrigger(new Event("message", this.getInPortName("socket")));
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"].addTransition(PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaiting_0);
+    var PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaiting_1 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"], [this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"]]);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaiting_1.setAction(this.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaiting_1Exec);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaiting_1.setTrigger(new Event("event3After"));
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good/receive_pongs/waiting"].addTransition(PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaiting_1);
+    
+    // transition /p/socket_region/connecting_or_connected/connected/connection_monitor_region/timeout
+    var PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionTimeout_0 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/timeout"], [this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good"]]);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionTimeout_0.setAction(this.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionTimeout_0Exec);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionTimeout_0.setTrigger(new Event("message", this.getInPortName("socket")));
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/timeout"].addTransition(PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionTimeout_0);
+    
+    // transition /p/socket_region/connecting_or_connected/wait_reconnect
+    var PSocketRegionConnectingOrConnectedWaitReconnect_0 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/wait_reconnect"], [this.states["/p/socket_region/connecting_or_connected/connecting"]]);
+    PSocketRegionConnectingOrConnectedWaitReconnect_0.setTrigger(new Event("event4After"));
+    this.states["/p/socket_region/connecting_or_connected/wait_reconnect"].addTransition(PSocketRegionConnectingOrConnectedWaitReconnect_0);
+    
+    // transition /p/mode/can_leave
+    var PModeCanLeave_0 = new Transition(this, this.states["/p/mode/can_leave"], [this.states["/p/mode/async_connected"]]);
+    PModeCanLeave_0.setTrigger(new Event("leave", this.getInPortName("in")));
+    PModeCanLeave_0.setGuard(this.PModeCanLeave_0Guard);
+    this.states["/p/mode/can_leave"].addTransition(PModeCanLeave_0);
+    var PModeCanLeave_1 = new Transition(this, this.states["/p/mode/can_leave"], [this.states["/p/mode/async_disconnected"]]);
+    PModeCanLeave_1.setTrigger(new Event("leave", this.getInPortName("in")));
+    PModeCanLeave_1.setGuard(this.PModeCanLeave_1Guard);
+    this.states["/p/mode/can_leave"].addTransition(PModeCanLeave_1);
+    
+    // transition /p/mode/can_leave/session_set/joined
+    var PModeCanLeaveSessionSetJoined_0 = new Transition(this, this.states["/p/mode/can_leave/session_set/joined"], [this.states["/p/mode/can_leave/session_set/rejoin_when_online"]]);
+    PModeCanLeaveSessionSetJoined_0.setTrigger(new Event("disconnected", null));
+    this.states["/p/mode/can_leave/session_set/joined"].addTransition(PModeCanLeaveSessionSetJoined_0);
+    
+    // transition /p/mode/can_leave/session_set/joined/region_cursor/delay
+    var PModeCanLeaveSessionSetJoinedRegionCursorDelay_0 = new Transition(this, this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"], [this.states["/p/mode/can_leave/session_set/joined/region_cursor/nodelay"]]);
+    PModeCanLeaveSessionSetJoinedRegionCursorDelay_0.setAction(this.PModeCanLeaveSessionSetJoinedRegionCursorDelay_0Exec);
+    PModeCanLeaveSessionSetJoinedRegionCursorDelay_0.setTrigger(new Event("event0After"));
+    PModeCanLeaveSessionSetJoinedRegionCursorDelay_0.setGuard(this.PModeCanLeaveSessionSetJoinedRegionCursorDelay_0Guard);
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"].addTransition(PModeCanLeaveSessionSetJoinedRegionCursorDelay_0);
+    var PModeCanLeaveSessionSetJoinedRegionCursorDelay_1 = new Transition(this, this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"], [this.states["/p/mode/can_leave/session_set/joined/region_cursor/nodelay"]]);
+    PModeCanLeaveSessionSetJoinedRegionCursorDelay_1.setTrigger(new Event("event1After"));
+    PModeCanLeaveSessionSetJoinedRegionCursorDelay_1.setGuard(this.PModeCanLeaveSessionSetJoinedRegionCursorDelay_1Guard);
+    this.states["/p/mode/can_leave/session_set/joined/region_cursor/delay"].addTransition(PModeCanLeaveSessionSetJoinedRegionCursorDelay_1);
+    
+    // transition /p/socket_region/connecting_or_connected
+    var PSocketRegionConnectingOrConnected_0 = new Transition(this, this.states["/p/socket_region/connecting_or_connected"], [this.states["/p/socket_region/disconnected"]]);
+    PSocketRegionConnectingOrConnected_0.setAction(this.PSocketRegionConnectingOrConnected_0Exec);
+    PSocketRegionConnectingOrConnected_0.setTrigger(new Event("disconnect", this.getInPortName("in")));
+    this.states["/p/socket_region/connecting_or_connected"].addTransition(PSocketRegionConnectingOrConnected_0);
+    
+    // transition /p/socket_region/connecting_or_connected/connected
+    var PSocketRegionConnectingOrConnectedConnected_0 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connected"], [this.states["/p/socket_region/connecting_or_connected/wait_reconnect"]]);
+    PSocketRegionConnectingOrConnectedConnected_0.setTrigger(new Event("close", this.getInPortName("socket")));
+    this.states["/p/socket_region/connecting_or_connected/connected"].addTransition(PSocketRegionConnectingOrConnectedConnected_0);
+    
+    // transition /p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good
+    var PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGood_0 = new Transition(this, this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good"], [this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/timeout"]]);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGood_0.setAction(this.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGood_0Exec);
+    PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGood_0.setTrigger(new Event("timeout", null));
+    this.states["/p/socket_region/connecting_or_connected/connected/connection_monitor_region/all_good"].addTransition(PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGood_0);
+};
+
+Main.prototype.PResponseHandlerEnter = function() {
+    this.reqHandlers = new Map();
+    this.reqCounter = 0;
+    this.sendReq = (obj, handler) => {
+      const reqId = this.reqCounter++;
+      const req = {
+        reqId,
+        ...obj,
+      };
+      console.log("Sending req", req);
+      this.reqHandlers.set(reqId, handler);
+      this.addEvent(new Event("send", null, [req]));
+    
+      // allow to cancel the request handler
+      return () => {
+        this.reqHandlers.delete(reqId);
+      };
+    }
+};
+
+Main.prototype.PModeCanLeaveExit = function() {
+    this.sendReq({type:"leave"});
+};
+
+Main.prototype.PModeCanLeaveSessionSetEnter = function() {
+    if (!this.sessionId) {
+      throw new Error("DEBUG: no session id");
+    }
+    this.uiState.setSession(this.sessionId);
+};
+
+Main.prototype.PModeCanLeaveSessionSetExit = function() {
+    this.uiState.unsetSession();
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionCursorDelayEnter = function() {
+    this.cursorMsg = null; 
+    this.addTimer(0, 0.1);
+    this.addTimer(1, 0.1);
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionCursorDelayExit = function() {
+    this.removeTimer(0);
+    this.removeTimer(1);
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedEnter = function() {
+    this.raiseInternalEvent(new Event("connected", null, []));
+    this.connected = true; 
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedExit = function() {
+    this.raiseInternalEvent(new Event("disconnected", null, []));
+    this.connected = false; 
+};
+
+Main.prototype.PModeAsyncDisconnectedEnter = function() {
+    this.uiState.setOfflineDisconnected();
+};
+
+Main.prototype.PModeAsyncConnectedEnter = function() {
+    this.uiState.setOfflineConnected();
+};
+
+Main.prototype.PModeCanLeaveWaitingForNewShareAckEnter = function() {
+    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]));
+      });
+};
+
+Main.prototype.PModeCanLeaveWaitingForNewShareAckExit = function() {
+    this.deleteNewShareHandler();
+};
+
+Main.prototype.PModeCanLeaveReshareWhenOnlineEnter = function() {
+    this.uiState.setReconnecting();
+};
+
+Main.prototype.PModeCanLeaveSessionSetWaitingForJoinAckEnter = function() {
+    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]));
+      });
+};
+
+Main.prototype.PModeCanLeaveSessionSetWaitingForJoinAckExit = function() {
+    this.deleteJoinHandler();
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefaultEnter = function() {
+    this.uiState.setOnline();
+    this.bigStep.outputEvent(new Event("joined", this.getOutPortName("out"), []));
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefaultExit = function() {
+    this.bigStep.outputEvent(new Event("left", this.getOutPortName("out"), []));
+};
+
+Main.prototype.PModeCanLeaveSessionSetRejoinWhenOnlineEnter = function() {
+    this.uiState.setReconnecting();
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectingEnter = function() {
+    this.socket = new WebSocket(this.addr);
+    
+    // Translate socket events to statechart events:
+    
+    this.socket.onopen = event => {
+      this.controller.addInput("open", this.inports["socket"], []);
+    };
+    
+    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.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.socket.onclose = event => {
+      this.controller.addInput("close", this.inports["socket"], []);
+    };
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaitingEnter = function() {
+    this.addTimer(2, 500);
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaitingExit = function() {
+    this.removeTimer(2);
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaitingEnter = function() {
+    this.addTimer(3, 7000);
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaitingExit = function() {
+    this.removeTimer(3);
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedWaitReconnectEnter = function() {
+    this.addTimer(4, 1);
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedWaitReconnectExit = function() {
+    this.removeTimer(4);
+};
+
+Main.prototype.PModeCanLeave_0Guard = function(parameters) {
+    return this.connected;
+};
+
+Main.prototype.PModeCanLeave_1Guard = function(parameters) {
+    return this.disconnected;
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionCursorDelay_0Exec = function(parameters) {
+    this.raiseInternalEvent(new Event("send", null, [this.cursorMsg]));
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionCursorDelay_0Guard = function(parameters) {
+    return this.cursorMsg !== null;
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionCursorDelay_1Guard = function(parameters) {
+    return this.cursorMsg === null;
+};
+
+Main.prototype.PSocketRegionConnectingOrConnected_0Exec = function(parameters) {
+    this.socket.close();
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGood_0Exec = function(parameters) {
+    this.raiseInternalEvent(new Event("timeout", null, []));
+};
+
+Main.prototype.PResponseHandlerDefault_0Exec = function(parameters) {
+    var parsed = parameters[0];
+    if (parsed.type === 'ack') {
+      const handler = this.reqHandlers.get(parsed.reqId);
+      if (handler) {
+        handler(parsed);
+      }
+    }
+};
+
+Main.prototype.PResponseHandlerDefault_1Exec = function(parameters) {
+    // no way we will get a response for our pending requests
+    this.reqHandlers.clear();
+    this.reqCounter = 0;
+};
+
+Main.prototype.PModeUninitialized_1Exec = function(parameters) {
+    var sessionId = parameters[0];
+    this.sessionId = sessionId;
+};
+
+Main.prototype.PModeAsyncConnected_1Exec = function(parameters) {
+    var sessionId = parameters[0];
+    this.sessionId = sessionId; 
+};
+
+Main.prototype.PModeAsyncConnected_2Exec = function(parameters) {
+    var ops = parameters[0];
+    // console.log("new share event, ops=", ops);
+    this.ops = ops;
+};
+
+Main.prototype.PModeCanLeaveWaitingForNewShareAck_1Exec = function(parameters) {
+    var sessionId = parameters[0];
+    this.sessionId = sessionId;
+    this.bigStep.outputEvent(new Event("ack_new_share", this.getOutPortName("out"), [sessionId]));
+};
+
+Main.prototype.PModeCanLeaveSessionSetWaitingForJoinAck_0Exec = function(parameters) {
+    var ops = parameters[0];
+    this.bigStep.outputEvent(new Event("ack_join", this.getOutPortName("out"), [ops]));
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_0Exec = function(parameters) {
+    var parsed = parameters[0];
+    if (parsed.sessionId !== this.sessionId) {
+      throw new Error("Unexpected: received edit for another session:" +  parsed.sessionId);
+    }
+    this.bigStep.outputEvent(new Event("received_op", this.getOutPortName("out"), [parsed.op]));
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_0Guard = function(parameters) {
+    var parsed = parameters[0];
+    return parsed.type === 'pub_edit';
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_1Exec = function(parameters) {
+    var serialized = parameters[0];
+    // console.log("sending sessionId", this.sessionId);
+    this.sendReq(
+      {type:"new_edit", sessionId: this.sessionId, op: serialized},
+      res => {
+        // this.unacknowledged.delete(serialized.id);
+      });
+    // this.unacknowledged.set(serialized.id, serialized);
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_2Exec = function(parameters) {
+    var parsed = parameters[0];
+    this.bigStep.outputEvent(new Event("update_cursor", this.getOutPortName("out"), [parsed]));
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionSendreceiveDefault_2Guard = function(parameters) {
+    var parsed = parameters[0];
+    return parsed.type === 'update_cursor';
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionCursorNodelay_0Exec = function(parameters) {
+    var msg = parameters[0];
+    this.raiseInternalEvent(new Event("send", null, [msg]));
+};
+
+Main.prototype.PModeCanLeaveSessionSetJoinedRegionCursorDelayInner_0Exec = function(parameters) {
+    var msg = parameters[0];
+    this.cursorMsg = msg;
+};
+
+Main.prototype.PSocketRegionDisconnected_0Exec = function(parameters) {
+    var addr = parameters[0];
+    console.log("received connect", addr)
+    this.addr = addr;
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnecting_1Exec = function(parameters) {
+    this.raiseInternalEvent(new Event("error", null, []));
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_0Exec = function(parameters) {
+    var json = parameters[0];
+    this.socket.send(JSON.stringify(json));
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_1Exec = function(parameters) {
+    var parsed = parameters[0];
+    this.raiseInternalEvent(new Event("received", null, [parsed]));
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedSendReceiveRegionReady_1Guard = function(parameters) {
+    var parsed = parameters[0];
+    return parsed.type !== 'pong';
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodSendPingsWaiting_1Exec = function(parameters) {
+    this.socket.send(JSON.stringify({type:"ping"}));
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionAllGoodReceivePongsWaiting_1Exec = function(parameters) {
+    this.raiseInternalEvent(new Event("timeout", null, []));
+};
+
+Main.prototype.PSocketRegionConnectingOrConnectedConnectedConnectionMonitorRegionTimeout_0Exec = function(parameters) {
+    this.raiseInternalEvent(new Event("timeout_recovered", null, []));
+};
+
+Main.prototype.initializeStatechart = function() {
+    // enter default state
+    this.defaultTargets = this.states["/p"].getEffectiveTargetStates();
+    RuntimeClassBase.prototype.initializeStatechart.call(this);
+};
+
+// Add symbol 'Main' to package 'client'
+client.Main = Main;
+
+let ObjectManager = function(controller) {
+    ObjectManagerBase.call(this, controller);
+};
+ObjectManager.prototype = new Object();
+(function() {
+    var proto = new ObjectManagerBase();
+    for (let prop in proto) {
+        ObjectManager.prototype[prop] = proto[prop];
+    }
+})();
+
+ObjectManager.prototype.instantiate = function(className, constructParams) {
+    if (className === "Main") {
+        var instance = new Main(this.controller, constructParams[0]);
+        instance.associations = {};
+        instance.associations["socket"] = new Association("Socket", 1, 1);
+    } else  {
+        throw new Error("Cannot instantiate class " + className);
+    }
+    return instance;
+};
+
+// Add symbol 'ObjectManager' to package 'client'
+client.ObjectManager = ObjectManager;
+
+let Controller = function(uiState, eventLoopCallbacks, finishedCallback, behindScheduleCallback) {
+    if (finishedCallback === undefined) finishedCallback = null;
+    if (behindScheduleCallback === undefined) behindScheduleCallback = null;
+    EventLoopControllerBase.call(this, new ObjectManager(this), eventLoopCallbacks, finishedCallback, behindScheduleCallback);
+    this.addInputPort("in");
+    this.addOutputPort("out");
+    this.objectManager.createInstance("Main", [uiState]);
+};
+Controller.prototype = new Object();
+(function() {
+    var proto = new EventLoopControllerBase();
+    for (let prop in proto) {
+        Controller.prototype[prop] = proto[prop];
+    }
+})();
+
+// Add symbol 'Controller' to package 'client'
+client.Controller = Controller;
+})();

+ 71 - 34
lib/versioning/client.xml

@@ -181,42 +181,79 @@
                   <transition event="disconnected" target="../rejoin_when_online"/>
                 </state><!-- waiting_for_join_ack -->
 
-                <state id="joined">
-                  <onentry>
-                    <script>
-                      this.uiState.setOnline();
-                    </script>
-                    <raise event="joined" port="out" scope="output"/>
-                  </onentry>
-                  <onexit>
-                    <raise event="left" port="out" scope="output"/>
-                  </onexit>
-                  <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>
-                      // console.log("sending sessionId", this.sessionId);
-                      this.sendReq(
-                        {type:"new_edit", sessionId: this.sessionId, op: serialized},
-                        res => {
-                          // this.unacknowledged.delete(serialized.id);
-                        });
-                      // this.unacknowledged.set(serialized.id, serialized);
-                    </script>
-                  </transition>
+                <parallel id="joined">
+                  <state id="region_sendreceive">
+                    <state id="default">
+                      <onentry>
+                        <script>
+                          this.uiState.setOnline();
+                        </script>
+                        <raise event="joined" port="out" scope="output"/>
+                      </onentry>
+                      <onexit>
+                        <raise event="left" port="out" scope="output"/>
+                      </onexit>
+                      <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>
+                          // console.log("sending sessionId", this.sessionId);
+                          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="received" cond="parsed.type === 'update_cursor'" target=".">
+                        <parameter name="parsed"/>
+                        <raise event="update_cursor" port="out" scope="output">
+                          <parameter expr="parsed"/>
+                        </raise>
+                      </transition>
+                    </state><!-- default -->
+                  </state><!-- region_sendreceive -->
+                  <state id="region_cursor" initial="nodelay">
+                    <state id="nodelay">
+                      <transition event="update_cursor" port="in" target="../delay">
+                        <parameter name="msg"/>
+                        <raise event="send">
+                          <parameter expr="msg"/>
+                        </raise>
+                      </transition>
+                    </state><!-- nodelay -->
+                    <state id="delay">
+                      <onentry><script> this.cursorMsg = null; </script></onentry>
+                      <state id="inner">
+                        <transition event="update_cursor" port="in" target=".">
+                          <parameter name="msg"/>
+                          <script>
+                            this.cursorMsg = msg;
+                          </script>
+                        </transition>
+                      </state><!-- inner -->
+                      <transition after="0.1" cond="this.cursorMsg !== null" target="../nodelay">
+                        <raise event="send">
+                          <parameter expr="this.cursorMsg"/>
+                        </raise>
+                      </transition>
+                      <transition after="0.1" cond="this.cursorMsg === null" target="../nodelay"/>
+                    </state><!-- delay -->
+                  </state><!-- region_cursor -->
 
                   <transition event="disconnected" target="../rejoin_when_online"/>
-                </state><!-- joined -->
+                </parallel><!-- joined -->
                 <state id="rejoin_when_online">
                   <onentry><script>
                     this.uiState.setReconnecting();

+ 71 - 0
lib/versioning/resources/cursor.svg

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="Layer_1"
+   x="0px"
+   y="0px"
+   viewBox="0 0 11.6 18.200001"
+   enable-background="new 0 0 28 28"
+   xml:space="preserve"
+   sodipodi:docname="cursor.svg"
+   width="11.6"
+   height="18.200001"
+   inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><metadata
+   id="metadata897"><rdf:RDF><cc:Work
+       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+   id="defs895" /><sodipodi:namedview
+   pagecolor="#ffffff"
+   bordercolor="#666666"
+   borderopacity="1"
+   objecttolerance="10"
+   gridtolerance="10"
+   guidetolerance="10"
+   inkscape:pageopacity="1"
+   inkscape:pageshadow="2"
+   inkscape:window-width="2560"
+   inkscape:window-height="1540"
+   id="namedview893"
+   showgrid="false"
+   fit-margin-top="0"
+   fit-margin-left="0"
+   fit-margin-right="0"
+   fit-margin-bottom="0"
+   inkscape:pagecheckerboard="true"
+   inkscape:zoom="17.197847"
+   inkscape:cx="7.2142334"
+   inkscape:cy="14.516473"
+   inkscape:window-x="0"
+   inkscape:window-y="32"
+   inkscape:window-maximized="1"
+   inkscape:current-layer="Layer_1" />
+<polygon
+   fill="#ffffff"
+   points="19.8,16.5 13,16.5 12.6,16.6 8.2,20.9 8.2,4.9 "
+   id="polygon884"
+   transform="translate(-8.2,-4.9)" />
+<polygon
+   fill="#ffffff"
+   points="9,12 12.7,10.5 17.3,21.6 13.7,23.1 "
+   id="polygon886"
+   transform="translate(-8.2,-4.9)" />
+<rect
+   x="-1.0260303"
+   y="9.7560711"
+   transform="rotate(-22.772792)"
+   width="2.0001149"
+   height="8.0004597"
+   id="rect888"
+   style="stroke-width:1.00006" />
+<polygon
+   points="12.2,15.6 12.6,15.5 17.4,15.5 9.2,7.3 9.2,18.5 "
+   id="polygon890"
+   transform="translate(-8.2,-4.9)" />
+</svg>

+ 23 - 3
lib/versioning/run_server.js

@@ -187,15 +187,35 @@ async function startServer() {
           })();
           return {type: "ack", reqId};
         }
+        else if (req.type === "update_cursor") {
+          mySubscriptions.forEach(sessionId => {
+            const sockets = subscriptions.get(sessionId) || new Set();
+            const stringified = JSON.stringify(req);
+            sockets.forEach(s => {
+              if (s === socket) {
+                return;
+              }
+              try {
+                s.send(stringified);
+              } catch (e) {
+                console.log("Error:", e, "Closing socket...")
+                s.close(); // in case of an error, it is the client's responsibility to re-subscribe and 'catch up'.
+              }
+            });
+          });
+        }
       }
+
     }
 
     socket.on('message', function(data) {
       const req = JSON.parse(data);
       handle(req).then(json => {
-        if (json.type !== 'pong')
-          console.log("<- ", json);
-        socket.send(JSON.stringify(json));
+        if (json) {
+          if (json.type !== 'pong')
+            console.log("<- ", json);
+          socket.send(JSON.stringify(json));
+        }
       }).catch(e => {
         console.log("Error handling", req, e);
       })

+ 181 - 27
src/main/webapp/plugins/cdf/versioning.browser.js

@@ -838,6 +838,55 @@ function version(uuid) {
 var _default = version;
 exports.default = _default;
 },{"./validate.js":14}],16:[function(require,module,exports){
+
+class DisabledCells {
+  constructor() {
+    this.disabledCells = new Set();
+  }
+
+  add(cell) {
+    this.disabledCells.add(cell);
+  }
+
+  delete(cell) {
+    this.disabledCells.delete(cell);
+  }
+
+  install(graph) {
+    const self = this;
+    // part #1: Intercepting mxGraph.fireMouseEvent
+    const oldFireMouseEvent = graph.fireMouseEvent;
+    graph.fireMouseEvent = function(evtName, me, sender) {
+      if (me.state && self.disabledCells.has(me.state.cell.id)) {
+        // clicked shape is disabled
+        return;
+      }
+      oldFireMouseEvent.apply(this, arguments);
+    }
+
+    // part #2: Ignore double clicks on disabled cells
+    const oldDblClick = graph.dblClick;
+    graph.dblClick = function(evt, cell) {
+      if (cell && self.disabledCells.has(cell.id)) {
+        // clicked shape is disabled
+        return;
+      }
+      oldDblClick.apply(this, arguments);
+    }
+    // part #3: Protect disabled cells from ever being selected
+    const oldMxSelectionChange = mxSelectionChange; // override constructor :)
+    mxSelectionChange = function(selectionModel, added, removed) {
+      oldMxSelectionChange.apply(this, arguments);
+      if (this.added) {
+        this.added = this.added.filter(cell => !self.disabledCells.has(cell.id));
+      }
+    }
+    mxSelectionChange.prototype = oldMxSelectionChange.prototype;
+  }
+}
+
+module.exports = { DisabledCells };
+},{}],17:[function(require,module,exports){
 "use strict";
 
 const { v4: uuidv4 } = require("uuid");
@@ -1089,18 +1138,31 @@ class History {
 
 module.exports = { Context, History, uuidv4 };
 
-},{"uuid":1}],17:[function(require,module,exports){
+},{"uuid":1}],18:[function(require,module,exports){
 // Build this plugin with 'browserify':
 //   browserify versioning.js > versioning.browser.js
 
 Draw.loadPlugin(async function(ui) {
-  window.ui = ui;
+
+  const names = ["Billy", "James", "Robert", "John", "Mary", "Patricia", "Jennifer", "Barbara"];
+  let me = window.localStorage.getItem('drawio-username');
+  if (!me) {
+    me = names[Math.floor(Math.random()*names.length)];
+  }
+
   const graph = ui.editor.graph;
   const model = graph.model;
 
+  // Interactive debugging
+  window.ui = ui;
+  window.graph = graph;
+  window.model = model;
+
   await loadScript("../../../../../sccd/docs/runtimes/javascript/statecharts_core.js");
   await loadScript("../../../../../lib/versioning/client.js");
 
+  const {DisabledCells} = require("../../../../../lib/versioning/DisabledCells.js")
+
   class EnabledState {
     constructor() {
       this.enabled = false;
@@ -1253,29 +1315,30 @@ Draw.loadPlugin(async function(ui) {
   const xmlSerializer = new XMLSerializer();
   const xmlParser = new DOMParser();
 
+  function getCell(cellId) {
+    let cell = model.cells[cellId];
+    if (cell === undefined) {
+      // throw new Error("NO SUCH CELL:", cellId);
+    }
+    return cell;
+  }
+
+  function createCell(cellId, isVertex, isEdge) {
+    cell = new mxCell("");
+    cell.setId(cellId);
+    cell.setVertex(isVertex);
+    cell.setEdge(isEdge);
+    model.cells[cellId] = cell; // HACK!
+    return cell;
+  }
+
   const setState = (key, value) => {
     // console.log("setState", key, value);
 
-    function getCell(cellId) {
-      let cell = model.cells[cellId];
-      if (cell === undefined) {
-        // throw new Error("NO SUCH CELL:", cellId);
-      }
-      return cell;
-    }
-
-    function createCell(cellId, isVertex, isEdge) {
-      cell = new mxCell("");
-      cell.setId(cellId);
-      cell.setVertex(isVertex);
-      cell.setEdge(isEdge);
-      model.cells[cellId] = cell; // HACK!
-      return cell;
-    }
 
     try {
       // temporarily disable listener:
-      listenForEdits = false;
+      // listenForEdits = false;
 
       if (key === "root") {
         // the only global 'key'
@@ -1371,7 +1434,7 @@ Draw.loadPlugin(async function(ui) {
       }
     } finally {
       // re-enable:
-      listenForEdits = true;
+      // listenForEdits = true;
     }
   };
 
@@ -1391,7 +1454,16 @@ Draw.loadPlugin(async function(ui) {
   function queuedMerge(serializedOp) {
     mergePromise = mergePromise.then(() => {
       return history.context.receiveOperation(serializedOp).then(op => {
-        history.autoMerge(op);
+        try {
+          listenForEdits++;
+
+          model.beginUpdate();
+          history.autoMerge(op);
+          model.endUpdate();
+        }
+        finally {
+          listenForEdits--;
+        }
         console.log("Merged ", op.id);
       });
     });
@@ -1412,7 +1484,7 @@ Draw.loadPlugin(async function(ui) {
         const [ops] = event.parameters;
         console.log("Outevent ack_join", ops.length, "ops..");
         try {
-          listenForEdits = false;
+          listenForEdits++;
           model.clear();
           resetHistory();
           for (const op of ops) {
@@ -1420,7 +1492,7 @@ Draw.loadPlugin(async function(ui) {
           }
           leaveOnError();
         } finally {
-          listenForEdits = true;
+          listenForEdits--;
         }
       }
       else if (event.name === "ack_new_share") {
@@ -1435,9 +1507,84 @@ Draw.loadPlugin(async function(ui) {
       else if (event.name === "left") {
 
       }
+      else if (event.name === "update_cursor") {
+        const [msg] = event.parameters;
+        ghostOverlays.put(msg.userId, msg.x, msg.y);
+        // const [msg] = event.parameters;
+        // let obj = ghostOverlays.get(msg.userId);
+        // if (!obj) {
+        //   obj = { x: msg.x, y: msg.y, cell: null };
+        //   ghostOverlays.set(msg.userId, obj);
+        // }
+        // ghostOverlays.set(msg.userId, obj);
+        // updateGhostCursors();
+      }
     }
   });
 
+  const disabledCells = new DisabledCells();
+  disabledCells.install(graph);
+
+  class GhostOverlays {
+    constructor(ui) {
+      this.map = new Map();
+      this.canvas = ui.editor.graph.view.canvas;
+      this.svg = this.canvas.parentElement;
+    }
+
+    // Update or create ghost cursor. Idempotent.
+    put(userId, x, y) {
+      let state = this.map.get(userId);
+      if (state === undefined) {
+        const g = document.createElementNS("http://www.w3.org/2000/svg", 'g');
+        const image = document.createElementNS("http://www.w3.org/2000/svg", 'image');
+        const text = document.createElementNS("http://www.w3.org/2000/svg", 'text');
+        text.style.fontSize = "10px";
+        text.setAttribute('y', 30);
+        const textNode = document.createTextNode(userId);
+        text.appendChild(textNode);
+        image.setAttribute('href', "/lib/versioning/resources/cursor.svg");
+        image.setAttribute('width', 11.6);
+        image.setAttribute('height', 18.2);
+        g.appendChild(image);
+        g.appendChild(text);
+        this.canvas.appendChild(g);
+        const transform = this.svg.createSVGTransform();
+        g.transform.baseVal.appendItem(transform);
+        state = {transform, timeout: null};
+        this.map.set(userId, state);
+      }
+      state.transform.setTranslate(
+        (x + graph.view.translate.x) * graph.view.scale,
+        (y + graph.view.translate.y) * graph.view.scale,
+      );
+      if (state.timeout) {
+        clearTimeout(state.timeout);
+      }
+      state.timeout = setTimeout(() => state.transform.setTranslate(-50, -50), 5000);
+    }
+  }
+
+  const ghostOverlays = new GhostOverlays(ui);
+
+  graph.addMouseListener({
+    mouseDown(graph, event) {
+
+    },
+    mouseMove(graph, event) {
+      const msg = {
+        type: "update_cursor",
+        userId: me,
+        x: event.graphX / graph.view.scale - graph.view.translate.x,
+        y: event.graphY / graph.view.scale - graph.view.translate.y,
+      }
+
+      controller.addInput("update_cursor", "in", [msg]);
+    },
+    mouseUp(graph, event) {
+
+    },
+  });
   
   {
     // Upon loading the page, if a 'sessionId' URL parameter is given, automatically join the given session once the connection with the server is established.
@@ -1449,7 +1596,7 @@ Draw.loadPlugin(async function(ui) {
       controller.addInput("init_offline", "in", []);
     }
   }
-  
+
   let websocketAddress;
   if (window.location.protocol === "http:") {
     websocketAddress = "ws:";
@@ -1568,9 +1715,9 @@ Draw.loadPlugin(async function(ui) {
   }
 
   // Fired when a local change happens
-  let listenForEdits = true;
+  let listenForEdits = 0;
   model.addListener(mxEvent.NOTIFY, function(sender, event) {
-    if (listenForEdits) {
+    if (listenForEdits === 0) {
       console.log("NOTIFY:", event.properties.edit.changes);
 
       const delta = {};
@@ -1603,6 +1750,13 @@ Draw.loadPlugin(async function(ui) {
       const seq = history.getOpsSequence();
       console.log(seq.map(op => op.serialize()));
     }
+
+    if (e.code === 'KeyU') {
+      const u = window.prompt("Choose a user name:", me);
+      if (u) {
+        me = u;
+      }
+    }
   })
 });
-},{"../../../../../lib/versioning/History.js":16}]},{},[17]);
+},{"../../../../../lib/versioning/DisabledCells.js":16,"../../../../../lib/versioning/History.js":17}]},{},[18]);

+ 207 - 120
src/main/webapp/plugins/cdf/versioning.js

@@ -2,13 +2,26 @@
 //   browserify versioning.js > versioning.browser.js
 
 Draw.loadPlugin(async function(ui) {
-  window.ui = ui;
+
+  const names = ["Billy", "James", "Robert", "John", "Mary", "Patricia", "Jennifer", "Barbara"];
+  let me = window.localStorage.getItem('drawio-username');
+  if (!me) {
+    me = names[Math.floor(Math.random()*names.length)];
+  }
+
   const graph = ui.editor.graph;
   const model = graph.model;
 
+  // Interactive debugging
+  window.ui = ui;
+  window.graph = graph;
+  window.model = model;
+
   await loadScript("../../../../../sccd/docs/runtimes/javascript/statecharts_core.js");
   await loadScript("../../../../../lib/versioning/client.js");
 
+  // const {DisabledCells} = require("../../../../../lib/versioning/DisabledCells.js")
+
   class EnabledState {
     constructor() {
       this.enabled = false;
@@ -161,125 +174,116 @@ Draw.loadPlugin(async function(ui) {
   const xmlSerializer = new XMLSerializer();
   const xmlParser = new DOMParser();
 
-  const setState = (key, value) => {
-    // console.log("setState", key, value);
-
-    function getCell(cellId) {
-      let cell = model.cells[cellId];
-      if (cell === undefined) {
-        // throw new Error("NO SUCH CELL:", cellId);
-      }
-      return cell;
-    }
-
-    function createCell(cellId, isVertex, isEdge) {
-      cell = new mxCell("");
-      cell.setId(cellId);
-      cell.setVertex(isVertex);
-      cell.setEdge(isEdge);
-      model.cells[cellId] = cell; // HACK!
-      return cell;
+  function getCell(cellId) {
+    let cell = model.cells[cellId];
+    if (cell === undefined) {
+      // throw new Error("NO SUCH CELL:", cellId);
     }
+    return cell;
+  }
 
-    try {
-      // temporarily disable listener:
-      listenForEdits = false;
+  function createCell(cellId, isVertex, isEdge) {
+    cell = new mxCell("");
+    cell.setId(cellId);
+    cell.setVertex(isVertex);
+    cell.setEdge(isEdge);
+    model.cells[cellId] = cell; // HACK!
+    return cell;
+  }
 
-      if (key === "root") {
-        // the only global 'key'
+  const setState = (key, value) => {
+    // console.log("setState", key, value);
+    if (key === "root") {
+      // the only global 'key'
 
-        const rootId = value;
-        const root = getCell(rootId) || createCell(rootId, false, false);
-        model.setRoot(root);
-      }
-      else {
-        // key is concatenation: <cellId> + '_' + <attribute>
-        const splitPoint = key.lastIndexOf('_');
-        const attribute = key.substring(splitPoint+1);
-        const cellId = key.substring(0, splitPoint-2);
-        const isVertex = key[splitPoint-1] === 'V';
-        const isEdge = key[splitPoint-1] === 'E';
-
-        const cell = getCell(cellId) || createCell(cellId, isVertex, isEdge);
-
-        const attrHandlers = {
-          geometry: () => {
-            new mxGeometryChange(model, )
-            const serializedGeometry = value;
-            const parsed = xmlParser.parseFromString(serializedGeometry, 'text/xml');
-            const geometry = codec.decode(parsed.documentElement);
-            model.setGeometry(cell, geometry);
-          },
-          style: () => {
-            const style = value;
-            model.setStyle(cell, style);
-          },
-          parent: () => {
-            const cellId = value;
-            const parent = model.cells[cellId];
-            // The following appears to create a mxChildChange object, indicating that it is this the correct way to set the parent...
-            if (parent) {
-              model.add(parent, cell, null);
-            } else {
-              model.remove(cell);
-            }
-          },
-          value: () => {
-            let v;
-            if (typeof value === 'object') {
-              const {xmlEncoded} = value;
-              v = xmlParser.parseFromString(xmlEncoded, 'text/xml');
-            } else {
-              v = value;
-            }
-            model.setValue(cell, v);
-          },
-          source: () => {
-            const sourceId = value;
-            if (sourceId === null) {
-              model.setTerminal(cell, null, true);
-            } else {
-              const source = getCell(sourceId);
-              if (source === undefined) {
-                throw new Error("NO SUCH CELL:", cellId);
-              }
-              model.setTerminal(cell, source, true);
+      const rootId = value;
+      const root = getCell(rootId) || createCell(rootId, false, false);
+      model.setRoot(root);
+    }
+    else {
+      // key is concatenation: <cellId> + '_' + <attribute>
+      const splitPoint = key.lastIndexOf('_');
+      const attribute = key.substring(splitPoint+1);
+      const cellId = key.substring(0, splitPoint-2);
+      const isVertex = key[splitPoint-1] === 'V';
+      const isEdge = key[splitPoint-1] === 'E';
+
+      const cell = getCell(cellId) || createCell(cellId, isVertex, isEdge);
+
+      const attrHandlers = {
+        geometry: () => {
+          new mxGeometryChange(model, )
+          const serializedGeometry = value;
+          const parsed = xmlParser.parseFromString(serializedGeometry, 'text/xml');
+          const geometry = codec.decode(parsed.documentElement);
+          model.setGeometry(cell, geometry);
+        },
+        style: () => {
+          const style = value;
+          model.setStyle(cell, style);
+        },
+        parent: () => {
+          const cellId = value;
+          const parent = model.cells[cellId];
+          // The following appears to create a mxChildChange object, indicating that it is this the correct way to set the parent...
+          if (parent) {
+            model.add(parent, cell, null);
+          } else {
+            model.remove(cell);
+          }
+        },
+        value: () => {
+          let v;
+          if (typeof value === 'object') {
+            const {xmlEncoded} = value;
+            v = xmlParser.parseFromString(xmlEncoded, 'text/xml');
+          } else {
+            v = value;
+          }
+          model.setValue(cell, v);
+        },
+        source: () => {
+          const sourceId = value;
+          if (sourceId === null) {
+            model.setTerminal(cell, null, true);
+          } else {
+            const source = getCell(sourceId);
+            if (source === undefined) {
+              throw new Error("NO SUCH CELL:", cellId);
             }
-          },
-          target: () => {
-            const targetId = value;
-            if (targetId === null) {
-              model.setTerminal(cell, null, false);
-            } else {
-              const target = getCell(targetId);
-              if (target === undefined) {
-                throw new Error("NO SUCH CELL:", cellId);
-              }
-              model.setTerminal(cell, target, false);
+            model.setTerminal(cell, source, true);
+          }
+        },
+        target: () => {
+          const targetId = value;
+          if (targetId === null) {
+            model.setTerminal(cell, null, false);
+          } else {
+            const target = getCell(targetId);
+            if (target === undefined) {
+              throw new Error("NO SUCH CELL:", cellId);
             }
-          },
-          collapsed: () => {
-            const collapsed = value;
-            model.setCollapsed(cell, collapsed);
-          },
-          visible: () => {
-            const visible = value;
-            model.setVisible(cell, visible);
-          },
-          vertex: () => {
-            const vertex = value;
-            cell.setVertex(vertex);
-          },
-          edge: () => {
-            const edge = value;
-            cell.setEdge(edge);
-          },
-        };
-        attrHandlers[attribute]();
-      }
-    } finally {
-      // re-enable:
-      listenForEdits = true;
+            model.setTerminal(cell, target, false);
+          }
+        },
+        collapsed: () => {
+          const collapsed = value;
+          model.setCollapsed(cell, collapsed);
+        },
+        visible: () => {
+          const visible = value;
+          model.setVisible(cell, visible);
+        },
+        vertex: () => {
+          const vertex = value;
+          cell.setVertex(vertex);
+        },
+        edge: () => {
+          const edge = value;
+          cell.setEdge(edge);
+        },
+      };
+      attrHandlers[attribute]();
     }
   };
 
@@ -299,7 +303,16 @@ Draw.loadPlugin(async function(ui) {
   function queuedMerge(serializedOp) {
     mergePromise = mergePromise.then(() => {
       return history.context.receiveOperation(serializedOp).then(op => {
-        history.autoMerge(op);
+        try {
+          listenForEdits++;
+
+          model.beginUpdate();
+          history.autoMerge(op);
+          model.endUpdate();
+        }
+        finally {
+          listenForEdits--;
+        }
         console.log("Merged ", op.id);
       });
     });
@@ -320,7 +333,7 @@ Draw.loadPlugin(async function(ui) {
         const [ops] = event.parameters;
         console.log("Outevent ack_join", ops.length, "ops..");
         try {
-          listenForEdits = false;
+          listenForEdits++;
           model.clear();
           resetHistory();
           for (const op of ops) {
@@ -328,7 +341,7 @@ Draw.loadPlugin(async function(ui) {
           }
           leaveOnError();
         } finally {
-          listenForEdits = true;
+          listenForEdits--;
         }
       }
       else if (event.name === "ack_new_share") {
@@ -343,9 +356,76 @@ Draw.loadPlugin(async function(ui) {
       else if (event.name === "left") {
 
       }
+      else if (event.name === "update_cursor") {
+        const [msg] = event.parameters;
+        ghostOverlays.put(msg.userId, msg.x, msg.y);
+      }
     }
   });
 
+  // const disabledCells = new DisabledCells();
+  // disabledCells.install(graph);
+
+  class GhostOverlays {
+    constructor(ui) {
+      this.map = new Map();
+      this.canvas = ui.editor.graph.view.canvas;
+      this.svg = this.canvas.parentElement;
+    }
+
+    // Update or create ghost cursor. Idempotent.
+    put(userId, x, y) {
+      let state = this.map.get(userId);
+      if (state === undefined) {
+        const g = document.createElementNS("http://www.w3.org/2000/svg", 'g');
+        const image = document.createElementNS("http://www.w3.org/2000/svg", 'image');
+        const text = document.createElementNS("http://www.w3.org/2000/svg", 'text');
+        text.style.fontSize = "10px";
+        text.setAttribute('y', 30);
+        const textNode = document.createTextNode(userId);
+        text.appendChild(textNode);
+        image.setAttribute('href', "/lib/versioning/resources/cursor.svg");
+        image.setAttribute('width', 11.6);
+        image.setAttribute('height', 18.2);
+        g.appendChild(image);
+        g.appendChild(text);
+        this.canvas.appendChild(g);
+        const transform = this.svg.createSVGTransform();
+        g.transform.baseVal.appendItem(transform);
+        state = {transform, timeout: null};
+        this.map.set(userId, state);
+      }
+      state.transform.setTranslate(
+        (x + graph.view.translate.x) * graph.view.scale,
+        (y + graph.view.translate.y) * graph.view.scale,
+      );
+      if (state.timeout) {
+        clearTimeout(state.timeout);
+      }
+      state.timeout = setTimeout(() => state.transform.setTranslate(-50, -50), 5000);
+    }
+  }
+
+  const ghostOverlays = new GhostOverlays(ui);
+
+  graph.addMouseListener({
+    mouseDown(graph, event) {
+
+    },
+    mouseMove(graph, event) {
+      const msg = {
+        type: "update_cursor",
+        userId: me,
+        x: event.graphX / graph.view.scale - graph.view.translate.x,
+        y: event.graphY / graph.view.scale - graph.view.translate.y,
+      }
+
+      controller.addInput("update_cursor", "in", [msg]);
+    },
+    mouseUp(graph, event) {
+
+    },
+  });
   
   {
     // Upon loading the page, if a 'sessionId' URL parameter is given, automatically join the given session once the connection with the server is established.
@@ -357,7 +437,7 @@ Draw.loadPlugin(async function(ui) {
       controller.addInput("init_offline", "in", []);
     }
   }
-  
+
   let websocketAddress;
   if (window.location.protocol === "http:") {
     websocketAddress = "ws:";
@@ -476,9 +556,9 @@ Draw.loadPlugin(async function(ui) {
   }
 
   // Fired when a local change happens
-  let listenForEdits = true;
+  let listenForEdits = 0;
   model.addListener(mxEvent.NOTIFY, function(sender, event) {
-    if (listenForEdits) {
+    if (listenForEdits === 0) {
       console.log("NOTIFY:", event.properties.edit.changes);
 
       const delta = {};
@@ -511,5 +591,12 @@ Draw.loadPlugin(async function(ui) {
       const seq = history.getOpsSequence();
       console.log(seq.map(op => op.serialize()));
     }
+
+    if (e.code === 'KeyU') {
+      const u = window.prompt("Choose a user name:", me);
+      if (u) {
+        me = u;
+      }
+    }
   })
 });