P2PCollab.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922
  1. function P2PCollab(ui, sync, channelId)
  2. {
  3. var graph = ui.editor.graph;
  4. var encrypted = true; // global flag to encrypt all messages
  5. var sessionCount = 0;
  6. var socket = null;
  7. var colors = [
  8. //White font
  9. '#e6194b', '#3cb44b', '#4363d8', '#f58231', '#911eb4',
  10. '#f032e6', '#469990', '#9A6324', '#800000', '#808000',
  11. '#000075', '#a9a9a9',
  12. //Black font
  13. '#ffe119', '#42d4f4', '#bfef45', '#fabed4', '#dcbeff',
  14. '#fffac8', '#aaffc3', '#ffd8b1'
  15. ];
  16. var connectedSessions = {}, messageId = 1, clientLastMsgId = {}, clientsToSessions = {},
  17. connectedClient = {}, sessionColors = {};
  18. var myClientId, newClients = {}, p2pClients = {}, useSocket = true, fileJoined = false, destroyed = false;
  19. var INACTIVE_TIMEOUT = 120000; //2 min
  20. var SELECTION_OPACITY = 70; //The default opacity of 30 is not visible enough with all colors
  21. var cursorDelay = 300;
  22. // TODO: Avoid negation, move to Editor.ENABLE_P2P and use p2p=1 URL parameter
  23. // add to Editor.configure
  24. var NO_P2P = urlParams['no-p2p'] != '0';
  25. var joinInProgress = false, joinId = 0;
  26. var lastError = null;
  27. var sendReply = mxUtils.bind(this, function(action, msg)
  28. {
  29. if (destroyed) return;
  30. try
  31. {
  32. if (socket != null)
  33. {
  34. socket.send(JSON.stringify({action: action, msg: msg}));
  35. if (!NO_P2P)
  36. {
  37. EditorUi.debug('P2PCollab: sending to socket server', [action], [msg]);
  38. }
  39. }
  40. else
  41. {
  42. this.joinFile(true);
  43. }
  44. }
  45. catch (e)
  46. {
  47. lastError = e;
  48. sync.file.fireEvent(new mxEventObject('realtimeStateChanged'));
  49. EditorUi.debug('P2PCollab:', 'sendReply error', arguments, e);
  50. }
  51. });
  52. function createCursorImage(color)
  53. {
  54. return Graph.createSvgImage(8, 12, '<path d="M 4 0 L 8 12 L 4 10 L 0 12 Z" stroke="'+ color +'" fill="'+ color +'"/>').src;
  55. };
  56. function sendMessage(type, data)
  57. {
  58. try
  59. {
  60. if (destroyed) return;
  61. var user = sync.file.getCurrentUser();
  62. if (!fileJoined || user == null || user.displayName == null) return;
  63. //Converting to a string such that webRTC works also
  64. var msg = {from: myClientId, id: messageId,
  65. type: type, sessionId: sync.clientId, userId: user.id,
  66. username: user.displayName, data: data,
  67. protocol: DrawioFileSync.PROTOCOL,
  68. editor: EditorUi.VERSION};
  69. if (encrypted)
  70. {
  71. // data is needed for old server to not drop messages
  72. msg = {bytes: sync.objectToString(msg), data: 'aes'};
  73. }
  74. msg = JSON.stringify(msg);
  75. if (NO_P2P && type != 'cursor')
  76. {
  77. EditorUi.debug('P2PCollab: sending to socket server', [msg]);
  78. }
  79. messageId++;
  80. var p2pOnlyMsgs = !NO_P2P && (type == 'cursor' || type == 'selectionChange');
  81. if (useSocket && !p2pOnlyMsgs)
  82. {
  83. sendReply('message', msg);
  84. }
  85. //TODO Currently, we only send cursor & selection messages via P2P
  86. if (p2pOnlyMsgs)
  87. {
  88. for (p2pId in p2pClients)
  89. {
  90. p2pClients[p2pId].send(msg);
  91. }
  92. }
  93. }
  94. catch (e)
  95. {
  96. if (window.console != null)
  97. {
  98. console.error(e, type, data);
  99. }
  100. }
  101. };
  102. this.sendMessage = sendMessage;
  103. this.sendDiff = function(msg)
  104. {
  105. this.sendMessage('diff', (encrypted) ?
  106. {diff: msg} : {patch: encodeURIComponent(
  107. sync.objectToString(msg))});
  108. };
  109. this.sendNotification = function(msg)
  110. {
  111. this.sendMessage('notify', (encrypted) ?
  112. {msg: msg} : {data: encodeURIComponent(
  113. sync.objectToString(msg))});
  114. };
  115. this.getState = function()
  116. {
  117. return socket != null ? socket.readyState : 3 /* CLOSED */;
  118. };
  119. this.getLastError = function()
  120. {
  121. return lastError;
  122. };
  123. function debounce(func, wait)
  124. {
  125. var timeout, lastInvocation = -1;
  126. return function()
  127. {
  128. clearTimeout(timeout);
  129. var context = this, args = arguments;
  130. var later = function()
  131. {
  132. timeout = null;
  133. lastInvocation = Date.now();
  134. func.apply(context, args);
  135. };
  136. if (Date.now() - lastInvocation > wait)
  137. {
  138. later();
  139. }
  140. else
  141. {
  142. timeout = setTimeout(later, wait);
  143. }
  144. };
  145. };
  146. function sendCursor(me)
  147. {
  148. if (ui.shareCursorPosition && !graph.isMouseDown)
  149. {
  150. var offset = mxUtils.getOffset(graph.container);
  151. var tr = graph.view.translate;
  152. var s = graph.view.scale;
  153. var pageId = (ui.currentPage != null) ?
  154. ui.currentPage.getId() : null;
  155. sendMessage('cursor', {pageId: pageId,
  156. x: Math.round((me.getX() - offset.x +
  157. graph.container.scrollLeft) / s - tr.x),
  158. y: Math.round((me.getY() - offset.y +
  159. graph.container.scrollTop) / s - tr.y)});
  160. }
  161. };
  162. this.mouseListeners = {
  163. startX: 0,
  164. startY: 0,
  165. scrollLeft: 0,
  166. scrollTop: 0,
  167. mouseDown: function(sender, me) {},
  168. mouseMove: debounce(function(sender, me)
  169. {
  170. sendCursor(me);
  171. }, cursorDelay), // 5 frame/sec approx TODO with 100 milli (10 fps), the cursor is smoother
  172. mouseUp: function(sender, me)
  173. {
  174. sendCursor(me);
  175. }
  176. };
  177. graph.addMouseListener(this.mouseListeners);
  178. this.shareCursorPositionListener = function()
  179. {
  180. if (!ui.isShareCursorPosition())
  181. {
  182. sendMessage('cursor', {hide: true});
  183. }
  184. };
  185. ui.addListener('shareCursorPositionChanged', this.shareCursorPositionListener);
  186. // Clears remote selection state for large selections
  187. var selectionLimit = mxGraphHandler.prototype.maxCells;
  188. var updateThread = null;
  189. var lastSelection = {};
  190. this.selectionChangeListener = function(sender, evt)
  191. {
  192. var mapToIds = function(c)
  193. {
  194. return (c != null) ? c.id : null;
  195. };
  196. if (updateThread != null)
  197. {
  198. window.clearTimeout(updateThread);
  199. }
  200. updateThread = window.setTimeout(function()
  201. {
  202. var selection = (graph.getSelectionCount() > selectionLimit) ?
  203. [] : graph.getSelectionCells().map(mapToIds)
  204. var pageId = (ui.currentPage != null) ?
  205. ui.currentPage.getId() : null;
  206. // Computes diff between last and current selection
  207. var newSelection = {};
  208. var removed = [];
  209. var added = [];
  210. for (var i = 0; i < selection.length; i++)
  211. {
  212. var id = selection[i];
  213. if (id != null)
  214. {
  215. newSelection[id] = true;
  216. if (lastSelection[id] == null)
  217. {
  218. added.push(id);
  219. }
  220. }
  221. }
  222. for (var id in lastSelection)
  223. {
  224. if (!newSelection[id])
  225. {
  226. removed.push(id);
  227. }
  228. }
  229. lastSelection = newSelection;
  230. sendMessage('selectionChange', {pageId: pageId,
  231. removed: removed, added: added});
  232. }, 300);
  233. };
  234. graph.getSelectionModel().addListener(mxEvent.CHANGE, this.selectionChangeListener);
  235. function updateCursor(entry, transition)
  236. {
  237. var pageId = (ui.currentPage != null) ?
  238. ui.currentPage.getId() : null;
  239. if (entry != null && entry.cursor != null &&
  240. entry.lastCursor != null)
  241. {
  242. if (entry.lastCursor.hide != null ||
  243. !ui.isShowRemoteCursors() ||
  244. (entry.lastCursor.pageId != null &&
  245. entry.lastCursor.pageId != pageId))
  246. {
  247. entry.cursor.style.display = 'none';
  248. }
  249. else
  250. {
  251. var tr = graph.view.translate;
  252. var s = graph.view.scale;
  253. var x = ((tr.x + entry.lastCursor.x) * s) + 8;
  254. var y = ((tr.y + entry.lastCursor.y) * s) - 12;
  255. var img = entry.cursor.getElementsByTagName('img')[0];
  256. function setPosition()
  257. {
  258. var cx = Math.max(graph.container.scrollLeft, Math.min(graph.container.scrollLeft +
  259. graph.container.clientWidth - entry.cursor.clientWidth, x));
  260. var cy = Math.max(graph.container.scrollTop - 22, Math.min(graph.container.scrollTop +
  261. graph.container.clientHeight - entry.cursor.clientHeight, y));
  262. img.style.opacity = (cx != x || cy != y) ? 0 : 1;
  263. entry.cursor.style.left = cx + 'px';
  264. entry.cursor.style.top = cy + 'px';
  265. entry.cursor.style.display = '';
  266. };
  267. if (transition)
  268. {
  269. mxUtils.setPrefixedStyle(entry.cursor.style, 'transition', 'all ' + (3 * cursorDelay) + 'ms ease-out');
  270. mxUtils.setPrefixedStyle(img.style, 'transition', 'all ' + (3 * cursorDelay) + 'ms ease-out');
  271. window.setTimeout(setPosition, 0);
  272. }
  273. else
  274. {
  275. mxUtils.setPrefixedStyle(entry.cursor.style, 'transition', null);
  276. mxUtils.setPrefixedStyle(img.style, 'transition', null);
  277. setPosition();
  278. }
  279. }
  280. }
  281. };
  282. this.cursorHandler = mxUtils.bind(this, function()
  283. {
  284. for (var key in connectedSessions)
  285. {
  286. updateCursor(connectedSessions[key]);
  287. }
  288. });
  289. mxEvent.addListener(graph.container, 'scroll', this.cursorHandler);
  290. graph.getView().addListener(mxEvent.SCALE, this.cursorHandler);
  291. graph.getView().addListener(mxEvent.TRANSLATE, this.cursorHandler);
  292. graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, this.cursorHandler);
  293. ui.addListener('showRemoteCursorsChanged', this.cursorHandler);
  294. ui.editor.addListener('pageSelected', this.cursorHandler);
  295. function processMsg(msg, fromCId)
  296. {
  297. try
  298. {
  299. if (destroyed) return;
  300. msg = JSON.parse(msg);
  301. if (msg.bytes != null)
  302. {
  303. msg = sync.stringToObject(msg.bytes);
  304. }
  305. if (NO_P2P && msg.type != 'cursor')
  306. {
  307. EditorUi.debug('P2PCollab: msg received', [msg]);
  308. }
  309. //Exclude P2P messages from duplicate messages test since p2p can arrive before socket and interrupt delivery
  310. if (fromCId != null)
  311. {
  312. //Safeguard from duplicate messages or receiving my own messages
  313. if (msg.from == myClientId || clientLastMsgId[msg.from] >= msg.id)
  314. {
  315. EditorUi.debug('P2PCollab: Dropped Message', msg, myClientId, clientLastMsgId[msg.from])
  316. return;
  317. }
  318. clientLastMsgId[msg.from] = msg.id;
  319. }
  320. var username = msg.username? msg.username : 'Anonymous';
  321. var sessionId = msg.sessionId;
  322. var cursor, selection;
  323. function createCursor()
  324. {
  325. if (connectedSessions[sessionId] == null)
  326. {
  327. var clrIndex = sessionColors[sessionId];
  328. if (clrIndex == null)
  329. {
  330. clrIndex = sessionCount % colors.length;
  331. sessionColors[sessionId] = clrIndex;
  332. sessionCount++;
  333. }
  334. var clr = colors[clrIndex];
  335. var lblClr = clrIndex > 11? 'black' : 'white';
  336. connectedSessions[sessionId] = {
  337. cursor: document.createElement('div'),
  338. color: clr,
  339. selection: {}
  340. };
  341. clientsToSessions[fromCId] = sessionId;
  342. cursor = connectedSessions[sessionId].cursor;
  343. cursor.style.pointerEvents = 'none';
  344. cursor.style.position = 'absolute';
  345. cursor.style.display = 'none';
  346. cursor.style.opacity = '0.9';
  347. var img = document.createElement('img');
  348. mxUtils.setPrefixedStyle(img.style, 'transform', 'rotate(-45deg)translateX(-14px)');
  349. img.setAttribute('src', createCursorImage(clr));
  350. img.style.width = '10px';
  351. cursor.appendChild(img);
  352. var name = document.createElement('div');
  353. name.style.backgroundColor = clr;
  354. name.style.color = lblClr;
  355. name.style.fontSize = '9pt';
  356. name.style.padding = '3px 7px';
  357. name.style.marginTop = '8px';
  358. name.style.borderRadius = '10px';
  359. name.style.maxWidth = '100px';
  360. name.style.overflow = 'hidden';
  361. name.style.textOverflow = 'ellipsis';
  362. name.style.whiteSpace = 'nowrap';
  363. mxUtils.write(name, username);
  364. cursor.appendChild(name);
  365. ui.diagramContainer.appendChild(cursor);
  366. selection = connectedSessions[sessionId].selection;
  367. }
  368. else
  369. {
  370. cursor = connectedSessions[sessionId].cursor;
  371. selection = connectedSessions[sessionId].selection;
  372. }
  373. };
  374. if (connectedSessions[sessionId] != null)
  375. {
  376. clearTimeout(connectedSessions[sessionId].inactiveTO);
  377. connectedSessions[sessionId].inactiveTO = setTimeout(function()
  378. {
  379. clientLeft(null, sessionId);
  380. }, INACTIVE_TIMEOUT);
  381. }
  382. var msgData = msg.data;
  383. switch (msg.type)
  384. {
  385. case 'cursor':
  386. createCursor();
  387. connectedSessions[sessionId].lastCursor = msgData;
  388. updateCursor(connectedSessions[sessionId], true);
  389. break;
  390. case 'diff':
  391. try
  392. {
  393. if (msgData.patch != null)
  394. {
  395. msg = sync.stringToObject(decodeURIComponent(msgData.patch));
  396. }
  397. else
  398. {
  399. msg = msgData.diff;
  400. }
  401. sync.receiveRemoteChanges(msg.d);
  402. }
  403. catch (e)
  404. {
  405. EditorUi.debug('P2PCollab: Diff msg error', e);
  406. }
  407. break;
  408. case 'selectionChange':
  409. if (urlParams['remote-selection'] != '0')
  410. {
  411. var pageId = (ui.currentPage != null) ?
  412. ui.currentPage.getId() : null;
  413. if (pageId == null ||
  414. (msgData.pageId != null &&
  415. msgData.pageId == pageId))
  416. {
  417. createCursor();
  418. for (var i = 0; i < msgData.removed.length; i++)
  419. {
  420. var id = msgData.removed[i];
  421. if (id != null)
  422. {
  423. var handler = selection[id];
  424. delete selection[id];
  425. if (handler != null)
  426. {
  427. handler.destroy();
  428. }
  429. }
  430. }
  431. for (var i = 0; i < msgData.added.length; i++)
  432. {
  433. var id = msgData.added[i];
  434. if (id != null)
  435. {
  436. var cell = graph.model.getCell(id);
  437. if (cell != null)
  438. {
  439. selection[id] = graph.highlightCell(cell,
  440. connectedSessions[sessionId].color, 60000,
  441. SELECTION_OPACITY, 3);
  442. }
  443. }
  444. }
  445. }
  446. }
  447. break;
  448. case 'notify':
  449. if (msgData.data != null)
  450. {
  451. msg = sync.stringToObject(decodeURIComponent(msgData.data));
  452. }
  453. else
  454. {
  455. msg = msgData.msg;
  456. }
  457. sync.handleMessageData(msg.d);
  458. break;
  459. }
  460. sync.file.fireEvent(new mxEventObject('realtimeMessage', 'message', msg));
  461. }
  462. catch (e)
  463. {
  464. if (window.console != null)
  465. {
  466. console.warn(e, msg, fromCId);
  467. }
  468. }
  469. };
  470. function createPeer(id, initiator)
  471. {
  472. if (NO_P2P || !SimplePeer.WEBRTC_SUPPORT)
  473. {
  474. return;
  475. }
  476. // TODO: Move URL to Editor.STUN_SERVER_URL, add to Editor.configure
  477. var p = new SimplePeer({
  478. initiator: initiator,
  479. config: { iceServers: [{ urls: 'stun:54.89.235.160:3478' }] }
  480. });
  481. p.on('signal', function(data)
  482. {
  483. sendReply('sendSignal', {to: id, from: myClientId, signal: data});
  484. });
  485. p.on('error', function(err)
  486. {
  487. delete newClients[id];
  488. EditorUi.debug('P2PCollab: p2p socket error', err);
  489. if (!destroyed && initiator && p.destroyed && connectedClient[id]) //If a client left, don't try to reconnect
  490. {
  491. EditorUi.debug('P2PCollab: p2p socket reconnecting', id);
  492. //Reconnect
  493. createPeer(id, true);
  494. }
  495. });
  496. p.on('connect', function()
  497. {
  498. delete newClients[id];
  499. if (p2pClients[id] == null || p2pClients[id].destroyed)
  500. {
  501. p2pClients[id] = p;
  502. connectedClient[id] = true;
  503. EditorUi.debug('P2PCollab: p2p socket connected', id);
  504. // if (mxUtils.isEmptyObject(newClients))
  505. // {
  506. //TODO Enable this when all messages can be routed via P2P
  507. //useSocket = false;
  508. //sendReply('movedToP2P', '');
  509. // }
  510. }
  511. else
  512. {
  513. p.noP2PMapDel = true;
  514. p.destroy();
  515. EditorUi.debug('P2PCollab: p2p socket duplicate', id);
  516. }
  517. });
  518. p.on('close', function()
  519. {
  520. if (!p.noP2PMapDel)
  521. {
  522. EditorUi.debug('P2PCollab: p2p socket closed', id);
  523. //Remove cursor and selection
  524. removeConnectedUserUi(clientsToSessions[id]);
  525. delete p2pClients[id];
  526. }
  527. });
  528. p.on('data', processMsg);
  529. newClients[id] = p;
  530. return p;
  531. };
  532. function clientsList(data)
  533. {
  534. myClientId = data.cId;
  535. fileJoined = true;
  536. for (var i = 0; i < data.list.length; i++)
  537. {
  538. createPeer(data.list[i], true);
  539. }
  540. };
  541. function signal(data)
  542. {
  543. if (NO_P2P) return;
  544. var p;
  545. if (newClients[data.from])
  546. {
  547. p = newClients[data.from];
  548. }
  549. else
  550. {
  551. p = createPeer(data.from, false);
  552. useSocket = true;
  553. }
  554. p.signal(data.signal);
  555. };
  556. function sendSignalFailed(data)
  557. {
  558. EditorUi.debug('P2PCollab: signal failed (socket not found on server)', data);
  559. delete newClients[data.to];
  560. connectedClient[data.to] = false; //TODO Should we call clientLeft?
  561. };
  562. function newClient(clientId)
  563. {
  564. useSocket = true;
  565. };
  566. function clientLeft(clientId, sessionId)
  567. {
  568. removeConnectedUserUi(sessionId || clientsToSessions[clientId]);
  569. if (clientId != null)
  570. {
  571. delete clientsToSessions[clientId];
  572. connectedClient[clientId] = false;
  573. }
  574. };
  575. this.joinFile = function(check)
  576. {
  577. if (destroyed) return;
  578. try
  579. {
  580. if (joinInProgress)
  581. {
  582. EditorUi.debug('P2PCollab: joinInProgress on', joinInProgress);
  583. lastError = 'busy';
  584. }
  585. joinInProgress = ++joinId;
  586. try
  587. {
  588. if (socket != null && socket.readyState == 1)
  589. {
  590. EditorUi.debug('P2PCollab: force closing socket on', socket.joinId)
  591. socket.close(1000);
  592. socket = null;
  593. }
  594. }
  595. catch(e)
  596. {
  597. EditorUi.debug('P2PCollab: closing socket error', e);
  598. } //Ignore
  599. var ws = new WebSocket(window.RT_WEBSOCKET_URL + '?id=' + channelId);
  600. if (socket == null)
  601. {
  602. socket = ws;
  603. }
  604. ws.addEventListener('open', function(event)
  605. {
  606. socket = ws;
  607. socket.joinId = joinInProgress;
  608. joinInProgress = false;
  609. sync.file.fireEvent(new mxEventObject('realtimeStateChanged'));
  610. EditorUi.debug('P2PCollab: open socket', socket.joinId);
  611. // Send join message
  612. if (!Editor.enableRealtimeCache)
  613. {
  614. window.setTimeout(function()
  615. {
  616. sync.sendJoinMessage();
  617. }, 0);
  618. }
  619. if (check)
  620. {
  621. sync.scheduleCleanup();
  622. }
  623. });
  624. function messageListener(event)
  625. {
  626. try
  627. {
  628. if (!NO_P2P)
  629. {
  630. EditorUi.debug('P2PCollab: msg received', [event]);
  631. }
  632. var data = JSON.parse(event.data);
  633. if (NO_P2P && data.action != 'message')
  634. {
  635. EditorUi.debug('P2PCollab: msg received', [event]);
  636. }
  637. switch (data.action)
  638. {
  639. case 'message':
  640. processMsg(data.msg, data.from);
  641. break;
  642. case 'clientsList':
  643. clientsList(data.msg);
  644. break;
  645. case 'signal':
  646. signal(data.msg);
  647. break;
  648. case 'newClient':
  649. newClient(data.msg);
  650. break;
  651. case 'clientLeft':
  652. clientLeft(data.msg);
  653. break;
  654. case 'sendSignalFailed':
  655. sendSignalFailed(data.msg);
  656. break;
  657. }
  658. }
  659. catch (e)
  660. {
  661. if (window.console != null)
  662. {
  663. console.warn(e, event);
  664. }
  665. }
  666. };
  667. ws.addEventListener('message', messageListener);
  668. var rejoinCalled = false;
  669. ws.addEventListener('close', mxUtils.bind(this, function(event)
  670. {
  671. EditorUi.debug('P2PCollab: WebSocket closed', ws.joinId, 'reconnecting', event.code, event.reason);
  672. EditorUi.debug('P2PCollab: closing socket on', ws.joinId);
  673. if (!destroyed && event.code != 1000 && joinId == ws.joinId) //Sometimes, a delayed even sometimes is received after another socket is established
  674. {
  675. if (joinInProgress == joinId)
  676. {
  677. EditorUi.debug('P2PCollab: joinInProgress in close on', ws.joinId);
  678. joinInProgress = false;
  679. }
  680. if (!rejoinCalled)
  681. {
  682. EditorUi.debug('P2PCollab: calling rejoin on', ws.joinId);
  683. window.setTimeout(mxUtils.bind(this, function()
  684. {
  685. rejoinCalled = true;
  686. this.joinFile(true);
  687. }), 500);
  688. }
  689. }
  690. sync.file.fireEvent(new mxEventObject('realtimeStateChanged'));
  691. }));
  692. ws.addEventListener('error', mxUtils.bind(this, function(event)
  693. {
  694. EditorUi.debug('P2PCollab: WebSocket error, reconnecting', event);
  695. EditorUi.debug('P2PCollab: error socket on', ws.joinId);
  696. if (!destroyed && joinId == ws.joinId) //Sometimes, a delayed even sometimes is received after another socket is established
  697. {
  698. if (joinInProgress == joinId)
  699. {
  700. EditorUi.debug('P2PCollab: joinInProgress in error on', ws.joinId);
  701. joinInProgress = false;
  702. }
  703. if (!rejoinCalled)
  704. {
  705. EditorUi.debug('P2PCollab: calling rejoin on', ws.joinId);
  706. rejoinCalled = true;
  707. this.joinFile(true);
  708. }
  709. }
  710. sync.file.fireEvent(new mxEventObject('realtimeStateChanged'));
  711. }));
  712. sync.file.fireEvent(new mxEventObject('realtimeStateChanged'));
  713. }
  714. catch (e)
  715. {
  716. lastError = e;
  717. sync.file.fireEvent(new mxEventObject('realtimeStateChanged'));
  718. }
  719. };
  720. function removeConnectedUserUi(sessionId)
  721. {
  722. var user = connectedSessions[sessionId];
  723. if (user != null)
  724. {
  725. var selection = user.selection;
  726. for (var id in selection)
  727. {
  728. if (selection[id] != null)
  729. {
  730. selection[id].destroy();
  731. }
  732. }
  733. if (user.cursor != null && user.cursor.parentNode != null)
  734. {
  735. user.cursor.parentNode.removeChild(user.cursor);
  736. }
  737. clearTimeout(user.inactiveTO);
  738. delete connectedSessions[sessionId];
  739. }
  740. };
  741. this.destroy = function()
  742. {
  743. if (destroyed) return;
  744. EditorUi.debug('P2PCollab: destroyed');
  745. destroyed = true;
  746. //Remove selection and cursor
  747. for (sessionId in connectedSessions)
  748. {
  749. removeConnectedUserUi(sessionId);
  750. }
  751. //Remove event listeners
  752. if (this.mouseListeners != null)
  753. {
  754. graph.removeMouseListener(this.mouseListeners);
  755. }
  756. if (this.selectionChangeListener != null)
  757. {
  758. graph.getSelectionModel().removeListener(this.selectionChangeListener);
  759. }
  760. if (this.shareCursorPositionListener != null)
  761. {
  762. ui.removeListener(this.shareCursorPositionListener);
  763. }
  764. if (this.cursorHandler != null)
  765. {
  766. mxEvent.removeListener(graph.container, 'scroll', this.cursorHandler);
  767. graph.view.removeListener(this.cursorHandler);
  768. ui.editor.removeListener(this.cursorHandler);
  769. ui.removeListener(this.cursorHandler);
  770. }
  771. // Close the socket
  772. if (socket != null && socket.readyState >= 1)
  773. {
  774. socket.close(1000);
  775. socket = null;
  776. }
  777. //Close P2P sockets
  778. for (var id in p2pClients)
  779. {
  780. if (p2pClients[id] != null)
  781. {
  782. p2pClients[id].destroy();
  783. }
  784. }
  785. sync.file.fireEvent(new mxEventObject('realtimeStateChanged'));
  786. };
  787. };