DrawioFileSync.js 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225
  1. /**
  2. * Copyright (c) 2006-2024, draw.io AG
  3. * Copyright (c) 2006-2024, JGraph Ltd
  4. *
  5. * Realtime collaboration for any file.
  6. */
  7. DrawioFileSync = function(file)
  8. {
  9. mxEventSource.call(this);
  10. this.lastActivity = Date.now();
  11. this.clientId = Editor.guid();
  12. this.ui = file.ui;
  13. this.file = file;
  14. // Listens to online state changes
  15. this.onlineListener = mxUtils.bind(this, function()
  16. {
  17. this.updateOnlineState();
  18. if (this.isConnected() && !this.ui.isOffline(true))
  19. {
  20. this.fileChangedNotify();
  21. }
  22. else
  23. {
  24. this.updateStatus();
  25. }
  26. });
  27. mxEvent.addListener(window, 'offline', this.onlineListener);
  28. mxEvent.addListener(window, 'online', this.onlineListener);
  29. // Listens to realtime state changes
  30. this.realtimeListener = mxUtils.bind(this, function()
  31. {
  32. this.updateOnlineState();
  33. });
  34. this.file.addListener('realtimeStateChanged', this.realtimeListener);
  35. // Listens to autosave changes to update the realtime collab socket
  36. this.autosaveListener = mxUtils.bind(this, function()
  37. {
  38. this.updateRealtime();
  39. });
  40. this.ui.editor.addListener('autosaveChanged', this.autosaveListener);
  41. // Listens to visible state changes
  42. this.visibleListener = mxUtils.bind(this, function()
  43. {
  44. if (document.visibilityState == 'hidden')
  45. {
  46. if (this.isConnected())
  47. {
  48. this.stop();
  49. }
  50. }
  51. else
  52. {
  53. this.start();
  54. }
  55. });
  56. mxEvent.addListener(document, 'visibilitychange', this.visibleListener);
  57. // Listens to visible state changes
  58. this.activityListener = mxUtils.bind(this, function(evt)
  59. {
  60. this.lastActivity = Date.now();
  61. this.start();
  62. });
  63. mxEvent.addListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.activityListener);
  64. mxEvent.addListener(document, 'keypress', this.activityListener);
  65. mxEvent.addListener(window, 'focus', this.activityListener);
  66. if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
  67. {
  68. mxEvent.addListener(document, 'touchstart', this.activityListener);
  69. mxEvent.addListener(document, 'touchmove', this.activityListener);
  70. }
  71. // Listens to fast sync activitiy
  72. this.file.addListener('realtimeMessage', this.activityListener);
  73. // Listens to errors in the pusher API
  74. this.pusherErrorListener = mxUtils.bind(this, function(err)
  75. {
  76. if (err.error != null && err.error.data != null &&
  77. err.error.data.code === 4004)
  78. {
  79. EditorUi.logError('Error: Pusher Limit', null, this.file.getId());
  80. }
  81. });
  82. // Listens to connection state changes
  83. this.connectionListener = mxUtils.bind(this, function()
  84. {
  85. this.updateOnlineState();
  86. this.updateStatus();
  87. if (this.isConnected())
  88. {
  89. if (!this.announced && Editor.enableRealtimeCache &&
  90. !Editor.p2pSyncNotify)
  91. {
  92. this.sendJoinMessage();
  93. }
  94. else if (this.announced)
  95. {
  96. // Catchup on any lost edits
  97. this.fileChangedNotify(null, true);
  98. }
  99. }
  100. });
  101. // Listens to messages
  102. this.changeListener = mxUtils.bind(this, function(data)
  103. {
  104. this.file.stats.msgReceived++;
  105. this.lastActivity = Date.now();
  106. if (this.enabled && !this.file.inConflictState &&
  107. !this.file.redirectDialogShowing)
  108. {
  109. try
  110. {
  111. var msg = this.stringToObject(data);
  112. if (msg != null)
  113. {
  114. EditorUi.debug('DrawioFileSync.message', [this], msg, data.length, 'bytes');
  115. // Handles protocol mismatch
  116. if (msg.v > DrawioFileSync.PROTOCOL)
  117. {
  118. this.file.redirectToNewApp(mxUtils.bind(this, function()
  119. {
  120. // Callback adds cancel option
  121. }));
  122. }
  123. else if (msg.v === DrawioFileSync.PROTOCOL && msg.d != null)
  124. {
  125. this.handleMessageData(msg.d);
  126. }
  127. }
  128. }
  129. catch (e)
  130. {
  131. // Checks if file was changed
  132. if (this.isConnected())
  133. {
  134. this.fileChangedNotify();
  135. }
  136. // NOTE: Probably UTF16 in username for join/leave message causing this
  137. // var len = (data != null) ? data.length : 'null';
  138. //
  139. // EditorUi.logError('Protocol Error ' + e.message,
  140. // null, 'data_' + len + '_file_' + this.file.getHash() +
  141. // '_client_' + this.clientId);
  142. //
  143. // if (window.console != null)
  144. // {
  145. // console.log(e);
  146. // }
  147. }
  148. }
  149. });
  150. };
  151. /**
  152. * Protocol version to be added to all communcations and diffs to check
  153. * if a client is out of date and force a refresh. Note that this must
  154. * be incremented if new messages are added or the format is changed.
  155. * This must be numeric to compare older vs newer protocol versions.
  156. */
  157. DrawioFileSync.PROTOCOL = 6;
  158. /**
  159. * Enables socket connections.
  160. */
  161. DrawioFileSync.ENABLE_SOCKETS = urlParams['sockets'] != '0';
  162. //Extends mxEventSource
  163. mxUtils.extend(DrawioFileSync, mxEventSource);
  164. /**
  165. * Maximum size in bytes for cache values.
  166. */
  167. DrawioFileSync.prototype.maxCacheEntrySize = 1000000;
  168. /**
  169. * Maximum size in bytes for fast sync messages via Pusher.
  170. * Use 0 to disable message size check. Default is 9KB.
  171. */
  172. DrawioFileSync.prototype.maxSyncMessageSize = 9000;
  173. /**
  174. * Delay for fast sync message sending in ms. Larger
  175. * values help to group sending out changes, smaller
  176. * values reduce latency.
  177. */
  178. DrawioFileSync.prototype.syncSendMessageDelay = 300;
  179. /**
  180. * Delay for received sync message processing in ms.
  181. * Larger values help to sort and merge messages,
  182. * smaller values reduce latency.
  183. */
  184. DrawioFileSync.prototype.syncReceiveMessageDelay = 50;
  185. /**
  186. * Inactivity time to undo remote changes that have not been saved
  187. * to the file. Larger values give time to save, smaller values
  188. * require less inactivity time by the user. (Conflict handling
  189. * for a local and remote save takes around 15 seconds.)
  190. */
  191. DrawioFileSync.prototype.cleanupDelay = 15000;
  192. /**
  193. * Counter for local message IDs.
  194. */
  195. DrawioFileSync.prototype.syncChangeCounter = 0;
  196. /**
  197. * Specifies if notifications should be sent and received for changes.
  198. */
  199. DrawioFileSync.prototype.enabled = true;
  200. /**
  201. * Holds the channel ID for sending and receiving change notifications.
  202. */
  203. DrawioFileSync.prototype.channelId = null;
  204. /**
  205. * Holds the channel ID for sending and receiving change notifications.
  206. */
  207. DrawioFileSync.prototype.channel = null;
  208. /**
  209. * Specifies if descriptor change events should be ignored.
  210. */
  211. DrawioFileSync.prototype.catchupRetryCount = 0;
  212. /**
  213. * Specifies if descriptor change events should be ignored.
  214. */
  215. DrawioFileSync.prototype.maxCatchupRetries = 15;
  216. /**
  217. * Specifies if descriptor change events should be ignored.
  218. */
  219. DrawioFileSync.prototype.maxCacheReadyRetries = 1;
  220. /**
  221. * Specifies if descriptor change events should be ignored.
  222. */
  223. DrawioFileSync.prototype.cacheReadyDelay = 700;
  224. /**
  225. * Specifies if descriptor change events should be ignored.
  226. */
  227. DrawioFileSync.prototype.maxOptimisticRetries = 6;
  228. /**
  229. * Inactivity timeout is 30 minutes.
  230. */
  231. DrawioFileSync.prototype.inactivityTimeoutSeconds = 1800;
  232. /**
  233. * Specifies if notifications should be sent and received for changes.
  234. */
  235. DrawioFileSync.prototype.lastActivity = null;
  236. /**
  237. * Adds all listeners.
  238. */
  239. DrawioFileSync.prototype.start = function()
  240. {
  241. if (this.channelId == null)
  242. {
  243. this.channelId = this.file.getChannelId();
  244. }
  245. if (this.key == null)
  246. {
  247. this.key = this.file.getChannelKey();
  248. }
  249. var updateStatus = false;
  250. if (this.file.isPolling())
  251. {
  252. if (document.visibilityState != 'hidden')
  253. {
  254. if (this.polling == null)
  255. {
  256. this.polling = new DrawioFilePolling(this.file, this);
  257. }
  258. this.polling.start(this.file.getPollingInterval());
  259. updateStatus = true;
  260. }
  261. }
  262. else if (this.pusher == null && this.channelId != null &&
  263. document.visibilityState != 'hidden')
  264. {
  265. this.pusher = this.ui.getPusher();
  266. if (this.pusher != null)
  267. {
  268. try
  269. {
  270. // Error listener must be installed before trying to create channel
  271. if (this.pusher.connection != null)
  272. {
  273. this.pusher.connection.bind('error', this.pusherErrorListener);
  274. }
  275. }
  276. catch (e)
  277. {
  278. // ignore
  279. }
  280. try
  281. {
  282. this.pusher.connect();
  283. this.channel = this.pusher.subscribe(this.channelId);
  284. EditorUi.debug('DrawioFileSync.start', [this],
  285. 'version', DrawioFileSync.PROTOCOL,
  286. 'rev', this.file.getCurrentRevisionId());
  287. }
  288. catch (e)
  289. {
  290. // ignore
  291. }
  292. this.installListeners();
  293. }
  294. updateStatus = true;
  295. }
  296. if (updateStatus)
  297. {
  298. window.setTimeout(mxUtils.bind(this, function()
  299. {
  300. this.lastModified = this.file.getLastModifiedDate();
  301. this.lastActivity = Date.now();
  302. this.resetUpdateStatusThread();
  303. this.updateOnlineState();
  304. this.updateStatus();
  305. }, 0));
  306. }
  307. this.updateRealtime();
  308. };
  309. /**
  310. * Draw function for the collaborator list.
  311. */
  312. DrawioFileSync.prototype.updateRealtime = function()
  313. {
  314. if (this.isValidState())
  315. {
  316. if (this.file.isRealtimeEnabled() &&
  317. this.file.isRealtimeSupported() &&
  318. this.isRealtimeActive())
  319. {
  320. if (!this.file.isRealtime())
  321. {
  322. this.initRealtime();
  323. }
  324. }
  325. else if (this.file.isRealtime())
  326. {
  327. this.resetRealtime();
  328. }
  329. if (DrawioFileSync.ENABLE_SOCKETS && this.file.isRealtime() &&
  330. this.p2pCollab == null && this.channelId != null)
  331. {
  332. this.p2pCollab = new P2PCollab(this.ui, this, this.channelId);
  333. this.p2pCollab.joinFile();
  334. }
  335. else if (!this.file.isRealtime() && this.p2pCollab != null)
  336. {
  337. this.p2pCollab.destroy();
  338. this.p2pCollab = null;
  339. }
  340. }
  341. };
  342. /**
  343. * Initializes the realtime model.
  344. */
  345. DrawioFileSync.prototype.initRealtime = function()
  346. {
  347. this.file.theirPages = this.ui.clonePages(
  348. this.ui.pages);
  349. this.file.ownPages = this.ui.clonePages(
  350. this.ui.pages);
  351. this.snapshot = this.file.ownPages;
  352. };
  353. /**
  354. * Resets the realtime model.
  355. */
  356. DrawioFileSync.prototype.resetRealtime = function()
  357. {
  358. var shadow = this.file.getShadowPages();
  359. if (shadow != null)
  360. {
  361. var patch = this.ui.diffPages(
  362. shadow, this.file.ownPages);
  363. this.file.patch([patch]);
  364. }
  365. this.sendLocalChanges();
  366. this.cleanup();
  367. this.file.theirPages = null;
  368. this.file.ownPages = null;
  369. this.snapshot = null;
  370. };
  371. /**
  372. * Draw function for the collaborator list.
  373. */
  374. DrawioFileSync.prototype.isConnected = function()
  375. {
  376. if (this.pusher != null && this.pusher.connection != null)
  377. {
  378. return this.pusher.connection.state == 'connected';
  379. }
  380. else if (this.polling != null)
  381. {
  382. return this.polling.isConnected();
  383. }
  384. else
  385. {
  386. return false;
  387. }
  388. };
  389. /**
  390. * Draw function for the collaborator list.
  391. */
  392. DrawioFileSync.prototype.updateOnlineState = function()
  393. {
  394. //For RT in embeded mode, we don't need this icon
  395. if (urlParams['embedRT'] == '1')
  396. {
  397. return;
  398. }
  399. if (this.ui.toolbarContainer != null && this.collaboratorsElement == null)
  400. {
  401. this.collaboratorsElement = this.createCollaboratorsElement();
  402. this.ui.toolbarContainer.appendChild(this.collaboratorsElement);
  403. }
  404. this.updateCollaboratorsElement();
  405. };
  406. /**
  407. * Updates the status bar with the latest change.
  408. */
  409. DrawioFileSync.prototype.updateCollaboratorsElement = function()
  410. {
  411. if (this.collaboratorsElement != null)
  412. {
  413. var status = this.ui.getNetworkStatus();
  414. if (status != null)
  415. {
  416. this.collaboratorsElement.style.backgroundImage = 'url(' +
  417. Editor.syncProblemImage + ')';
  418. this.collaboratorsElement.style.display = 'inline-block';
  419. this.collaboratorsElement.setAttribute('title', status);
  420. }
  421. else
  422. {
  423. this.collaboratorsElement.style.display = 'none';
  424. }
  425. }
  426. };
  427. /**
  428. * Updates the status bar with the latest change.
  429. */
  430. DrawioFileSync.prototype.createCollaboratorsElement = function()
  431. {
  432. var elt = document.createElement('a');
  433. elt.className = 'geButton geAdaptiveAsset';
  434. elt.style.position = 'absolute';
  435. elt.style.display = 'inline-block';
  436. elt.style.verticalAlign = 'bottom';
  437. elt.style.color = '#666';
  438. elt.style.top = '6px';
  439. elt.style.right = (Editor.currentTheme != 'atlas') ? '70px' : '50px';
  440. elt.style.padding = '2px';
  441. elt.style.fontSize = '8pt';
  442. elt.style.verticalAlign = 'middle';
  443. elt.style.textDecoration = 'none';
  444. elt.style.backgroundPosition = 'center center';
  445. elt.style.backgroundRepeat = 'no-repeat';
  446. elt.style.backgroundSize = '16px 16px';
  447. elt.style.width = '16px';
  448. elt.style.height = '16px';
  449. elt.style.opacity = '0.6';
  450. // Prevents focus
  451. mxEvent.addListener(elt, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
  452. mxUtils.bind(this, function(evt)
  453. {
  454. evt.preventDefault();
  455. }));
  456. mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt)
  457. {
  458. if (this.file.isRealtimeEnabled() && this.file.isRealtimeSupported())
  459. {
  460. var status = this.ui.getNetworkStatus();
  461. this.ui.showError(mxResources.get('realtimeCollaboration'),
  462. mxUtils.htmlEntities((status != null) ? status :
  463. mxResources.get('online')));
  464. }
  465. else
  466. {
  467. this.enabled = !this.enabled;
  468. this.ui.updateButtonContainer();
  469. this.resetUpdateStatusThread();
  470. this.updateOnlineState();
  471. this.updateStatus();
  472. if (!this.file.inConflictState && this.enabled)
  473. {
  474. this.fileChangedNotify();
  475. }
  476. }
  477. }));
  478. return elt;
  479. };
  480. /**
  481. * Updates the status bar with the latest change.
  482. */
  483. DrawioFileSync.prototype.updateStatus = function()
  484. {
  485. if (this.isConnected() && this.lastActivity != null &&
  486. (Date.now() - this.lastActivity) / 1000 >
  487. this.inactivityTimeoutSeconds)
  488. {
  489. this.stop();
  490. }
  491. if (!this.file.isModified() && !this.file.inConflictState &&
  492. this.file.autosaveThread == null && !this.file.savingFile &&
  493. !this.file.redirectDialogShowing)
  494. {
  495. if (this.enabled && this.ui.statusContainer != null)
  496. {
  497. // LATER: Write out modified date for more than 2 weeks ago
  498. var str = this.ui.timeSince(new Date(this.lastModified));
  499. if (str == null)
  500. {
  501. str = mxResources.get('lessThanAMinute');
  502. }
  503. // Consumes and displays last message
  504. var msg = this.lastMessage;
  505. this.lastMessage = null;
  506. if (msg != null && msg.length > 40)
  507. {
  508. msg = msg.substring(0, 40) + '...';
  509. }
  510. var status = this.ui.getNetworkStatus();
  511. var label = mxResources.get('lastChange', [str]);
  512. var rev = (this.file.isRevisionHistorySupported()) ? 'data-action="revisionHistory" ' : '';
  513. this.ui.editor.setStatus('<div ' + rev + 'title="'+ mxUtils.htmlEntities(label) + '">' + mxUtils.htmlEntities(label) + '</div>' +
  514. (!this.file.isEditable() ? '<div class="geStatusBox" title="' +
  515. mxUtils.htmlEntities(mxResources.get('readOnly')) + '">' +
  516. mxUtils.htmlEntities(mxResources.get('readOnly')) + '</div>' :
  517. (this.file.isLocked() ? ' <img class="geToolbarButton geAdaptiveAsset" data-action="properties" ' +
  518. 'style="margin-left:4px;flex-shrink:0;" src="' + Editor.lockedImage + '"/>' : '')) +
  519. (status != null ? '<div class="geStatusBox" title="' + mxUtils.htmlEntities(status) + '">' +
  520. mxUtils.htmlEntities(status) + '</div>' : '') +
  521. ((msg != null) ? ' <div class="geStatusBox" data-effect="fade" title="' + mxUtils.htmlEntities(msg) + '">' +
  522. mxUtils.htmlEntities(msg) + '</div>' : ''));
  523. this.resetUpdateStatusThread();
  524. }
  525. else
  526. {
  527. this.file.addAllSavedStatus();
  528. }
  529. }
  530. };
  531. /**
  532. * Resets the thread to update the status.
  533. */
  534. DrawioFileSync.prototype.resetUpdateStatusThread = function()
  535. {
  536. if (this.updateStatusThread != null)
  537. {
  538. window.clearInterval(this.updateStatusThread);
  539. }
  540. if (this.channel != null)
  541. {
  542. this.updateStatusThread = window.setInterval(mxUtils.bind(this, function()
  543. {
  544. this.updateStatus();
  545. }), Editor.updateStatusInterval);
  546. }
  547. };
  548. /**
  549. * Installs all required listeners for syncing the current file.
  550. */
  551. DrawioFileSync.prototype.installListeners = function()
  552. {
  553. if (this.pusher != null && this.pusher.connection != null)
  554. {
  555. this.pusher.connection.bind('state_change', this.connectionListener);
  556. }
  557. if (this.channel != null)
  558. {
  559. this.channel.bind('changed', this.changeListener);
  560. }
  561. };
  562. /**
  563. * Adds the listener for automatically saving the diagram for local changes.
  564. */
  565. DrawioFileSync.prototype.notify = function(msg)
  566. {
  567. this.file.stats.msgSent++;
  568. // Skips notifications in polling mode
  569. if (this.polling == null)
  570. {
  571. if (Editor.enableRealtimeCache && !Editor.p2pSyncNotify)
  572. {
  573. mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
  574. '&msg=' + encodeURIComponent(this.objectToString(msg)));
  575. }
  576. else if (this.p2pCollab != null)
  577. {
  578. this.p2pCollab.sendNotification(msg);
  579. }
  580. }
  581. EditorUi.debug('DrawioFileSync.notify', [this],
  582. 'enableRealtimeCache', Editor.enableRealtimeCache,
  583. 'p2pSyncNotify', Editor.p2pSyncNotify,
  584. 'msg', msg);
  585. };
  586. /**
  587. *
  588. */
  589. DrawioFileSync.prototype.sendJoinMessage = function()
  590. {
  591. if (!this.announced)
  592. {
  593. var user = this.file.getCurrentUser();
  594. var join = {a: 'join'};
  595. if (user != null)
  596. {
  597. join.name = encodeURIComponent(user.displayName);
  598. join.uid = user.id;
  599. }
  600. this.notify(this.createMessage(join));
  601. this.announced = true;
  602. }
  603. }
  604. /**
  605. * Adds the listener for automatically saving the diagram for local changes.
  606. */
  607. DrawioFileSync.prototype.handleMessageData = function(data)
  608. {
  609. if (data.a == 'desc')
  610. {
  611. if (!this.file.savingFile)
  612. {
  613. this.reloadDescriptor();
  614. }
  615. }
  616. else if (data.a == 'join' || data.a == 'leave')
  617. {
  618. if (data.a == 'join')
  619. {
  620. this.file.stats.joined++;
  621. }
  622. if (data.name != null)
  623. {
  624. this.lastMessage = mxResources.get((data.a == 'join') ?
  625. 'userJoined' : 'userLeft', [decodeURIComponent(data.name)]);
  626. this.resetUpdateStatusThread();
  627. this.updateStatus();
  628. }
  629. }
  630. else if (data.a == 'change')
  631. {
  632. this.receiveRemoteChanges(data);
  633. }
  634. else if (data.m != null)
  635. {
  636. var mod = new Date(data.m);
  637. // Ignores obsolete messages
  638. if (this.lastMessageModified == null ||
  639. this.lastMessageModified < mod)
  640. {
  641. this.lastMessageModified = mod;
  642. this.fileChangedNotify();
  643. }
  644. }
  645. };
  646. /**
  647. * Adds the listener for automatically saving the diagram for local changes.
  648. */
  649. DrawioFileSync.prototype.isValidState = function()
  650. {
  651. return this.ui.getCurrentFile() == this.file &&
  652. this.file.sync == this && !this.file.invalidChecksum &&
  653. !this.file.redirectDialogShowing;
  654. };
  655. /**
  656. * Adds the listener for automatically saving the diagram for local changes.
  657. */
  658. DrawioFileSync.prototype.optimisticSync = function(count)
  659. {
  660. if (this.reloadThread == null)
  661. {
  662. count = (count != null) ? count : 0;
  663. if (count < this.maxOptimisticRetries)
  664. {
  665. this.reloadThread = window.setTimeout(mxUtils.bind(this, function()
  666. {
  667. EditorUi.debug('DrawioFileSync.optimisticSync', [this],
  668. 'attempt', count, 'of', this.maxOptimisticRetries,
  669. 'remoteFileChanged', this.remoteFileChanged);
  670. this.remoteFileChanged = false;
  671. this.file.getLatestVersion(mxUtils.bind(this, function(latestFile)
  672. {
  673. this.reloadThread = null;
  674. if (latestFile != null)
  675. {
  676. var source = this.file.getCurrentRevisionId();
  677. var target = latestFile.getCurrentRevisionId();
  678. // Retries if the file has not changed
  679. if (source == target)
  680. {
  681. this.optimisticSync(count + 1);
  682. }
  683. else
  684. {
  685. this.file.mergeFile(latestFile, mxUtils.bind(this, function()
  686. {
  687. this.lastModified = this.file.getLastModifiedDate();
  688. this.updateStatus();
  689. }));
  690. }
  691. }
  692. }), mxUtils.bind(this, function()
  693. {
  694. this.reloadThread = null;
  695. }));
  696. }), (count + 1) * this.file.optimisticSyncDelay);
  697. }
  698. }
  699. };
  700. /**
  701. * Adds the listener for automatically saving the diagram for local changes.
  702. * Immediate is passed through to scheduleCleanup.
  703. */
  704. DrawioFileSync.prototype.fileChangedNotify = function(data, immediate)
  705. {
  706. if (this.isValidState())
  707. {
  708. EditorUi.debug('DrawioFileSync.fileChangedNotify', [this],
  709. 'data', [data], 'immediate', immediate,
  710. 'saving', this.file.savingFile);
  711. if (this.file.savingFile)
  712. {
  713. this.remoteFileChanged = true;
  714. }
  715. else
  716. {
  717. if (data != null && data.type == 'optimistic')
  718. {
  719. this.optimisticSync();
  720. }
  721. else
  722. {
  723. // It's possible that a request never returns so override
  724. // existing requests and abort them when they are active
  725. var thread = this.fileChanged(mxUtils.bind(this, function(err)
  726. {
  727. this.updateStatus();
  728. }), mxUtils.bind(this, function(err)
  729. {
  730. this.file.handleFileError(err);
  731. }), mxUtils.bind(this, function()
  732. {
  733. return !this.file.savingFile && this.notifyThread != thread;
  734. }), true, immediate);
  735. }
  736. }
  737. }
  738. };
  739. /**
  740. * Called after the file was changed locally to mark the file as changed.
  741. */
  742. DrawioFileSync.prototype.localFileChanged = function()
  743. {
  744. if (this.file.isRealtime())
  745. {
  746. window.clearTimeout(this.triggerSendThread);
  747. this.localFileWasChanged = true;
  748. this.scheduleCleanup(true);
  749. this.triggerSendThread = window.setTimeout(mxUtils.bind(this, function()
  750. {
  751. this.sendLocalChanges();
  752. }), Math.min(this.file.autosaveDelay, this.syncSendMessageDelay - 20));
  753. }
  754. };
  755. /**
  756. * Sends the given changes too all collaborators.
  757. */
  758. DrawioFileSync.prototype.doSendLocalChanges = function(changes)
  759. {
  760. if (!this.file.ignorePatches(changes))
  761. {
  762. var changeId = this.clientId + '.' + (this.syncChangeCounter++);
  763. var msg = this.createMessage({a: 'change', c: changes,
  764. id: changeId, t: Date.now()});
  765. var skipped = false;
  766. if (this.p2pCollab != null)
  767. {
  768. this.p2pCollab.sendDiff(msg);
  769. }
  770. else if (urlParams['dev'] == '1')
  771. {
  772. var data = encodeURIComponent(this.objectToString(msg));
  773. if (this.maxSyncMessageSize == 0 ||
  774. data.length < this.maxSyncMessageSize)
  775. {
  776. mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() + '&msg=' + data);
  777. }
  778. else
  779. {
  780. skipped = true;
  781. }
  782. }
  783. else
  784. {
  785. skipped = true;
  786. }
  787. EditorUi.debug('DrawioFileSync.doSendLocalChanges', [this],
  788. 'changes', changes, skipped ? '(skipped)' : '');
  789. }
  790. };
  791. /**
  792. * Handles the given remote changes.
  793. */
  794. DrawioFileSync.prototype.receiveRemoteChanges = function(data)
  795. {
  796. var changes = data.c;
  797. if (!this.file.ignorePatches(changes))
  798. {
  799. if (this.receivedData == null)
  800. {
  801. this.receivedData = [data];
  802. window.setTimeout(mxUtils.bind(this, function()
  803. {
  804. if (this.ui.getCurrentFile() == this.file)
  805. {
  806. // Skips additional processing for single change
  807. if (this.receivedData.length == 1)
  808. {
  809. this.doReceiveRemoteChanges(this.receivedData[0].c);
  810. }
  811. else
  812. {
  813. // Sorts by sender and remote counter
  814. this.receivedData.sort(function(a, b)
  815. {
  816. if (a.id < b.id)
  817. {
  818. return -1;
  819. }
  820. else if (a.id > b.id)
  821. {
  822. return 1;
  823. }
  824. else
  825. {
  826. return 0;
  827. }
  828. });
  829. var lastDiff = null;
  830. // Processes changes
  831. for (var i = 0; i < this.receivedData.length; i++)
  832. {
  833. // Ignores consecutive duplicates
  834. var currentDiff = JSON.stringify(this.receivedData[i].c);
  835. if (currentDiff != lastDiff)
  836. {
  837. this.doReceiveRemoteChanges(this.receivedData[i].c);
  838. }
  839. lastDiff = currentDiff;
  840. }
  841. }
  842. }
  843. this.receivedData = null;
  844. }), this.syncReceiveMessageDelay);
  845. }
  846. else
  847. {
  848. this.receivedData.push(data);
  849. }
  850. }
  851. };
  852. /**
  853. * Schedules a new cleanup if not lazy or one is pending
  854. */
  855. DrawioFileSync.prototype.scheduleCleanup = function(lazy)
  856. {
  857. // Adds 2 secs per 10MB of file size to allow for remote save with
  858. // local fastForward before cleanup is triggered
  859. var sizeDelaySec = Math.min(15, Math.floor(this.file.getSize() / 5000000));
  860. var delay = (lazy == false) ? 0 : this.cleanupDelay + sizeDelaySec * 1000;
  861. var prev = this.cleanupThread;
  862. if (lazy != true || this.cleanupThread != null)
  863. {
  864. window.clearTimeout(this.cleanupThread);
  865. this.cleanupThread = window.setTimeout(mxUtils.bind(this, function()
  866. {
  867. this.cleanup(null, mxUtils.bind(this, function(err)
  868. {
  869. this.file.handleFileError(err);
  870. }));
  871. }), delay);
  872. }
  873. EditorUi.debug('DrawioFileSync.scheduleCleanup', [this],
  874. 'lazy', lazy, 'delay', delay, 'prev', prev,
  875. 'thread', this.cleanupThread);
  876. };
  877. /**
  878. * Removes remote changes that have not been saved and merges
  879. * the latest version of the file if checkFile is true.
  880. */
  881. DrawioFileSync.prototype.cleanup = function(success, error, checkFile)
  882. {
  883. var thread = this.cleanupThread;
  884. window.clearTimeout(this.cleanupThread);
  885. this.cleanupThread = null;
  886. if (this.isValidState() && !this.file.inConflictState &&
  887. this.file.isRealtime() && !this.file.isModified())
  888. {
  889. var patches = [this.ui.diffPages(this.ui.pages,
  890. this.file.ownPages)];
  891. this.file.theirPages = this.ui.clonePages(
  892. this.file.ownPages);
  893. if (urlParams['test'] == '1')
  894. {
  895. EditorUi.debug('DrawioFileSync.cleanup',
  896. [this], 'thread', thread, 'patches', patches,
  897. 'checkFile', checkFile, 'checksum',
  898. this.ui.getHashValueForPages(this.ui.pages));
  899. }
  900. if (!this.file.ignorePatches(patches))
  901. {
  902. this.file.patch(patches);
  903. }
  904. if (!checkFile)
  905. {
  906. if (!document.hidden && urlParams['test'] == '1' &&
  907. urlParams['checksum'] == '1')
  908. {
  909. this.testChecksum();
  910. }
  911. if (success != null)
  912. {
  913. success();
  914. }
  915. }
  916. else
  917. {
  918. this.file.getLatestVersion(mxUtils.bind(this, function(newFile)
  919. {
  920. try
  921. {
  922. if (this.isValidState() && !this.file.inConflictState &&
  923. this.file.isRealtime())
  924. {
  925. var pages = newFile.getShadowPages();
  926. patches = [this.ui.diffPages(this.ui.pages, pages),
  927. this.ui.diffPages(pages, this.file.ownPages)];
  928. if (!this.file.ignorePatches(patches))
  929. {
  930. this.file.patch(patches);
  931. }
  932. EditorUi.debug('DrawioFileSync.cleanup',
  933. [this], 'newFile', newFile,
  934. 'patches', patches);
  935. }
  936. if (success != null)
  937. {
  938. success();
  939. }
  940. }
  941. catch (e)
  942. {
  943. if (error != null)
  944. {
  945. error(e);
  946. }
  947. }
  948. }), error);
  949. }
  950. }
  951. else if (success != null)
  952. {
  953. success();
  954. EditorUi.debug('DrawioFileSync.cleanup',
  955. [this], 'checkFile', checkFile,
  956. 'modified', this.file.isModified());
  957. }
  958. };
  959. /**
  960. * Extracts local changes by diffing remote pages and patched remote pages.
  961. */
  962. DrawioFileSync.prototype.testChecksum = function()
  963. {
  964. var localChecksum = this.ui.getHashValueForPages(this.ui.pages);
  965. var localRev = this.file.getCurrentRevisionId();
  966. this.file.getLatestVersion(mxUtils.bind(this, function(latestFile)
  967. {
  968. if (!document.hidden)
  969. {
  970. var remoteChecksum = this.ui.getHashValueForPages(
  971. latestFile.getShadowPages());
  972. var descChecksum = latestFile.getDescriptorChecksum(
  973. latestFile.getDescriptor());
  974. var remoteRev = latestFile.getCurrentRevisionId();
  975. EditorUi.debug('DrawioFileSync.testChecksum',
  976. 'local', [this.file], 'modified', this.file.isModified(),
  977. 'inConflictState', this.file.inConflictState,
  978. 'autosaveThread', this.file.autosaveThread,
  979. 'savingFile', this.file.savingFile,
  980. 'localFileWasChanged', this.localFileWasChanged,
  981. 'remoteFileChanged', this.remoteFileChanged,
  982. 'cleanup', this.cleanupThread,
  983. 'checksum', localChecksum);
  984. EditorUi.debug('DrawioFileSync.testChecksum',
  985. 'remote', [latestFile],
  986. 'rev', remoteRev == localRev,
  987. 'desc', descChecksum == remoteChecksum,
  988. 'checksum', remoteChecksum);
  989. if (remoteChecksum != localChecksum)
  990. {
  991. EditorUi.debug('DrawioFileSync.testChecksum',
  992. [this], 'checksums do not match');
  993. this.ui.alert('Checksums do not match');
  994. }
  995. else
  996. {
  997. EditorUi.debug('DrawioFileSync.testChecksum',
  998. [this], 'checksums match');
  999. }
  1000. }
  1001. }), mxUtils.bind(this, function(err)
  1002. {
  1003. EditorUi.debug('DrawioFileSync.testChecksum',
  1004. [this], 'checksum test error', err);
  1005. }));
  1006. };
  1007. /**
  1008. * Extracts local changes by diffing remote pages and patched remote pages.
  1009. */
  1010. DrawioFileSync.prototype.extractLocal = function(patch)
  1011. {
  1012. return (mxUtils.isEmptyObject(patch)) ? {} : this.ui.diffPages(
  1013. this.file.theirPages, this.ui.patchPages(this.ui.clonePages(
  1014. this.file.theirPages), patch));
  1015. };
  1016. /**
  1017. * Extracts remove operations for pages and cells from the given patch.
  1018. */
  1019. DrawioFileSync.prototype.extractRemove = function(patch)
  1020. {
  1021. var result = {};
  1022. if (patch[EditorUi.DIFF_REMOVE] != null)
  1023. {
  1024. result[EditorUi.DIFF_REMOVE] =
  1025. patch[EditorUi.DIFF_REMOVE];
  1026. }
  1027. if (patch[EditorUi.DIFF_UPDATE] != null)
  1028. {
  1029. for (var id in patch[EditorUi.DIFF_UPDATE])
  1030. {
  1031. var diff = patch[EditorUi.DIFF_UPDATE][id];
  1032. if (diff.cells != null && diff.cells
  1033. [EditorUi.DIFF_REMOVE] != null)
  1034. {
  1035. if (result[EditorUi.DIFF_UPDATE] == null)
  1036. {
  1037. result[EditorUi.DIFF_UPDATE] = {};
  1038. }
  1039. result[EditorUi.DIFF_UPDATE][id] = {};
  1040. var temp = result[EditorUi.DIFF_UPDATE][id];
  1041. temp.cells = {};
  1042. temp.cells[EditorUi.DIFF_REMOVE] =
  1043. diff.cells[EditorUi.DIFF_REMOVE];
  1044. }
  1045. }
  1046. }
  1047. return result;
  1048. };
  1049. /**
  1050. * Updates the realtime models and saves pending local changes.
  1051. * Immediate is passed through to scheduleCleanup.
  1052. */
  1053. DrawioFileSync.prototype.patchRealtime = function(patches, backup, own, immediate)
  1054. {
  1055. var all = null;
  1056. if (this.file.isRealtime())
  1057. {
  1058. // Gets pending changes that must be saved after remote
  1059. // changes are applied, ie. local remove of remote shape.
  1060. // TODO: Currently only implemented for pending removes as
  1061. // remote changes are not received in the order in which
  1062. // they are finally saved in the file.
  1063. all = this.extractRemove(this.ui.diffPages(
  1064. this.file.getShadowPages(), this.ui.pages));
  1065. var local = this.extractRemove(this.extractLocal(all));
  1066. // Applies incoming, own and local changes to own pages
  1067. var applied = ((own == null) ? patches :
  1068. patches.concat(own)).concat([local]);
  1069. this.file.ownPages = this.ui.applyPatches(
  1070. this.file.ownPages, applied, true,
  1071. backup);
  1072. // Triggers a file change to save pending local
  1073. // changes or updates the UI and schedules a
  1074. // cleanup with no pending local changes.
  1075. if (!mxUtils.isEmptyObject(local))
  1076. {
  1077. this.file.fileChanged(false);
  1078. }
  1079. else
  1080. {
  1081. this.scheduleCleanup((immediate != null) ?
  1082. false : null);
  1083. }
  1084. EditorUi.debug('DrawioFileSync.patchRealtime', [this],
  1085. 'patches', patches, 'backup', backup, 'own', own,
  1086. 'all', all, 'local', local, 'applied', applied,
  1087. 'immediate', immediate);
  1088. }
  1089. return all;
  1090. };
  1091. /**
  1092. * Computes and sends the local changes if the file was changed.
  1093. */
  1094. DrawioFileSync.prototype.isRealtimeActive = function()
  1095. {
  1096. return this.ui.editor.autosave;
  1097. };
  1098. /**
  1099. * Computes and sends the local changes if the file was changed.
  1100. */
  1101. DrawioFileSync.prototype.sendLocalChanges = function()
  1102. {
  1103. try
  1104. {
  1105. if (this.file.isRealtime() && this.localFileWasChanged)
  1106. {
  1107. var snapshot = this.ui.clonePages(this.ui.pages);
  1108. var patch = this.ui.diffPages(this.snapshot, snapshot);
  1109. this.file.ownPages = this.ui.patchPages(
  1110. this.file.ownPages, patch, true);
  1111. this.snapshot = snapshot;
  1112. // Creates patch for cross references
  1113. var resolve = this.ui.resolveCrossReferences(
  1114. patch, this.ui.diffPages(this.file.ownPages,
  1115. this.ui.pages));
  1116. // Patches own pages to resolve cross references
  1117. this.file.ownPages = this.ui.patchPages(
  1118. this.file.ownPages, resolve, true);
  1119. if (this.isRealtimeActive())
  1120. {
  1121. this.doSendLocalChanges([resolve, patch]);
  1122. }
  1123. }
  1124. this.localFileWasChanged = false;
  1125. }
  1126. catch (e)
  1127. {
  1128. var user = this.file.getCurrentUser();
  1129. var uid = (user != null) ? user.id : 'unknown';
  1130. EditorUi.logError('Error in sendLocalChanges', null,
  1131. this.file.getMode() + '.' +
  1132. this.file.getId(), uid, e);
  1133. }
  1134. };
  1135. /**
  1136. * Sends the given changes too all collaborators.
  1137. */
  1138. DrawioFileSync.prototype.doReceiveRemoteChanges = function(changes)
  1139. {
  1140. if (this.file.isRealtime() && this.isRealtimeActive())
  1141. {
  1142. this.sendLocalChanges();
  1143. this.file.patch(changes);
  1144. this.file.theirPages = this.ui.applyPatches(
  1145. this.file.theirPages, changes);
  1146. this.scheduleCleanup();
  1147. EditorUi.debug('DrawioFileSync.doReceiveRemoteChanges',
  1148. [this], 'changes', changes);
  1149. }
  1150. };
  1151. /**
  1152. * Adds the listener for automatically saving the diagram for local changes.
  1153. * Immediate is passed through to scheduleCleanup.
  1154. */
  1155. DrawioFileSync.prototype.merge = function(patches, checksum, desc, success, error, abort, immediate)
  1156. {
  1157. try
  1158. {
  1159. this.file.stats.merged++;
  1160. this.lastModified = new Date();
  1161. var target = this.file.getDescriptorRevisionId(desc);
  1162. var ignored = this.file.ignorePatches(patches);
  1163. if (!ignored)
  1164. {
  1165. this.sendLocalChanges();
  1166. // Computes local changes
  1167. var shadow = this.ui.clonePages(this.file.getShadowPages());
  1168. var changes = (this.file.isModified() &&
  1169. !this.file.isRealtime()) ? this.ui.diffPages(
  1170. shadow, this.ui.pages) : null;
  1171. var pending = (!this.file.isRealtime()) ? null :
  1172. this.ui.diffPages(shadow, this.file.ownPages);
  1173. shadow = this.ui.applyPatches(shadow, patches);
  1174. var current = (checksum == null) ? null :
  1175. this.ui.getHashValueForPages(shadow);
  1176. EditorUi.debug('DrawioFileSync.merge', [this], 'patches', patches,
  1177. 'changes', changes, 'pending', pending, 'checksum',
  1178. checksum, 'current', current, 'valid', checksum == current,
  1179. 'attempt', this.catchupRetryCount, 'of', this.maxCatchupRetries,
  1180. 'from', this.file.getCurrentRevisionId(), 'to', target,
  1181. 'etag', this.file.getDescriptorEtag(desc),
  1182. 'immediate', immediate);
  1183. // Compares the checksum
  1184. if (checksum != null && checksum != current)
  1185. {
  1186. // Fallback to full reload with mergeFile
  1187. this.reload(mxUtils.bind(this, function()
  1188. {
  1189. if (success != null)
  1190. {
  1191. success();
  1192. }
  1193. }), mxUtils.bind(this, function()
  1194. {
  1195. if (error != null)
  1196. {
  1197. error();
  1198. }
  1199. }), abort, null, immediate);
  1200. // Abnormal termination
  1201. return;
  1202. }
  1203. else
  1204. {
  1205. this.file.setShadowPages(shadow);
  1206. // Patches the current document and own pages
  1207. if (this.patchRealtime(patches, null, pending, immediate) == null)
  1208. {
  1209. this.file.patch(patches,
  1210. (DrawioFile.LAST_WRITE_WINS) ?
  1211. changes : null);
  1212. }
  1213. // Logs successull patch
  1214. // try
  1215. // {
  1216. // var user = this.file.getCurrentUser();
  1217. // var uid = (user != null) ? user.id : 'unknown';
  1218. //
  1219. // EditorUi.logEvent({category: 'PATCH-SYNC-FILE-' + this.file.getHash(),
  1220. // action: uid + '-patches-' + patches.length + '-recvd-' +
  1221. // this.file.stats.bytesReceived + '-msgs-' + this.file.stats.msgReceived,
  1222. // label: this.clientId});
  1223. // }
  1224. // catch (e)
  1225. // {
  1226. // // ignore
  1227. // }
  1228. }
  1229. }
  1230. this.file.invalidChecksum = false;
  1231. this.file.inConflictState = false;
  1232. this.file.patchDescriptor(this.file.getDescriptor(), desc);
  1233. if (success != null)
  1234. {
  1235. success(true);
  1236. }
  1237. }
  1238. catch (e)
  1239. {
  1240. this.file.inConflictState = true;
  1241. this.file.invalidChecksum = true;
  1242. this.file.descriptorChanged();
  1243. if (error != null)
  1244. {
  1245. error(e);
  1246. }
  1247. try
  1248. {
  1249. var user = this.file.getCurrentUser();
  1250. var uid = (user != null) ? user.id : 'unknown';
  1251. EditorUi.logError('Error in merge', null,
  1252. this.file.getMode() + '.' +
  1253. this.file.getId(), uid, e);
  1254. }
  1255. catch (e2)
  1256. {
  1257. // ignore
  1258. }
  1259. }
  1260. };
  1261. /**
  1262. * Adds the listener for automatically saving the diagram for local changes.
  1263. * Immediate is passed through to scheduleCleanup.
  1264. */
  1265. DrawioFileSync.prototype.fileChanged = function(success, error, abort, lazy, immediate)
  1266. {
  1267. var thread = window.setTimeout(mxUtils.bind(this, function()
  1268. {
  1269. if (abort == null || !abort())
  1270. {
  1271. EditorUi.debug('DrawioFileSync.fileChanged', [this],
  1272. 'lazy', lazy, 'immediate', immediate,
  1273. 'remoteFileChanged', this.remoteFileChanged,
  1274. 'valid', this.isValidState());
  1275. if (!this.isValidState())
  1276. {
  1277. if (error != null)
  1278. {
  1279. error();
  1280. }
  1281. }
  1282. else
  1283. {
  1284. this.remoteFileChanged = false;
  1285. this.file.loadPatchDescriptor(mxUtils.bind(this, function(desc)
  1286. {
  1287. if (abort == null || !abort())
  1288. {
  1289. if (!this.isValidState())
  1290. {
  1291. if (error != null)
  1292. {
  1293. error();
  1294. }
  1295. }
  1296. else
  1297. {
  1298. this.catchup(desc, success, error, abort, immediate);
  1299. }
  1300. }
  1301. }), error);
  1302. }
  1303. }
  1304. }), (lazy) ? this.cacheReadyDelay : 0);
  1305. this.notifyThread = thread;
  1306. return thread;
  1307. };
  1308. /**
  1309. * Fast-forward to the current editor state.
  1310. */
  1311. DrawioFileSync.prototype.fastForward = function(desc)
  1312. {
  1313. this.file.patchDescriptor(this.file.getDescriptor(), desc);
  1314. this.file.setShadowPages(this.ui.clonePages(this.ui.pages));
  1315. this.file.theirPages = this.ui.clonePages(this.ui.pages);
  1316. // Forces update of internal page state for remote changes
  1317. // Note that clonePages does not clone the needsUpdate flag
  1318. var prevOwnPages = this.file.ownPages;
  1319. this.file.ownPages = this.ui.clonePages(this.ui.pages);
  1320. for (var i = 0; i < this.file.ownPages.length; i++)
  1321. {
  1322. if (prevOwnPages[i] != null &&
  1323. (this.ui.getHashValueForPages([this.file.ownPages[i]]) !=
  1324. this.ui.getHashValueForPages([prevOwnPages[i]])))
  1325. {
  1326. this.file.ownPages[i].needsUpdate = true;
  1327. }
  1328. }
  1329. var thread = this.cleanupThread;
  1330. window.clearTimeout(this.cleanupThread);
  1331. this.cleanupThread = null;
  1332. if (urlParams['test'] == '1')
  1333. {
  1334. EditorUi.debug('DrawioFileSync.fastForward',
  1335. [this], 'desc', [desc], 'cleanup', thread, 'checksum',
  1336. this.ui.getHashValueForPages(this.ui.pages));
  1337. }
  1338. if (!document.hidden && urlParams['test'] == '1' &&
  1339. urlParams['checksum'] == '1' &&
  1340. this.cleanupThread == null)
  1341. {
  1342. this.testChecksum();
  1343. }
  1344. };
  1345. /**
  1346. * Adds the listener for automatically saving the diagram for local changes.
  1347. */
  1348. DrawioFileSync.prototype.reloadDescriptor = function()
  1349. {
  1350. this.file.loadDescriptor(mxUtils.bind(this, function(desc)
  1351. {
  1352. if (desc != null)
  1353. {
  1354. // Forces data to be updated
  1355. this.file.setDescriptorRevisionId(desc,
  1356. this.file.getCurrentRevisionId());
  1357. this.updateDescriptor(desc);
  1358. this.fileChangedNotify();
  1359. }
  1360. else
  1361. {
  1362. this.file.inConflictState = true;
  1363. this.file.handleFileError();
  1364. }
  1365. }), mxUtils.bind(this, function(err)
  1366. {
  1367. this.file.inConflictState = true;
  1368. this.file.handleFileError(err);
  1369. }));
  1370. };
  1371. /**
  1372. * Adds the listener for automatically saving the diagram for local changes.
  1373. */
  1374. DrawioFileSync.prototype.updateDescriptor = function(desc)
  1375. {
  1376. this.file.setDescriptor(desc);
  1377. this.file.descriptorChanged();
  1378. this.start();
  1379. };
  1380. /**
  1381. * Adds the listener for automatically saving the diagram for local changes.
  1382. * Immediate is passed through to scheduleCleanup.
  1383. */
  1384. DrawioFileSync.prototype.catchup = function(desc, success, error, abort, immediate)
  1385. {
  1386. if (desc != null && (abort == null || !abort()))
  1387. {
  1388. var source = this.file.getCurrentRevisionId();
  1389. var target = this.file.getDescriptorRevisionId(desc);
  1390. EditorUi.debug('DrawioFileSync.catchup', [this],
  1391. 'desc', [desc], 'from', source, 'to', target,
  1392. 'immediate', immediate, 'valid',
  1393. this.isValidState());
  1394. if (source == target)
  1395. {
  1396. this.file.patchDescriptor(this.file.getDescriptor(), desc);
  1397. if (urlParams['test'] == '1')
  1398. {
  1399. EditorUi.debug('DrawioFileSync.catchup', [this],
  1400. 'up to date', 'cleanup', this.cleanupThread,
  1401. 'checksum', this.ui.getHashValueForPages(this.ui.pages));
  1402. }
  1403. if (!document.hidden && urlParams['test'] == '1' &&
  1404. urlParams['checksum'] == '1' &&
  1405. this.cleanupThread == null)
  1406. {
  1407. this.testChecksum();
  1408. }
  1409. if (success != null)
  1410. {
  1411. success(true);
  1412. }
  1413. }
  1414. else if (!this.isValidState())
  1415. {
  1416. if (error != null)
  1417. {
  1418. error();
  1419. }
  1420. }
  1421. else
  1422. {
  1423. var checksum = this.file.getDescriptorChecksum(desc)
  1424. var secret = this.file.getDescriptorSecret(desc);
  1425. if (checksum != null &&
  1426. checksum == this.ui.getHashValueForPages(this.ui.pages))
  1427. {
  1428. // Fast-forward to current state if checksum matches
  1429. this.fastForward(desc);
  1430. if (success != null)
  1431. {
  1432. success(true);
  1433. }
  1434. }
  1435. else if (!Editor.enableRealtimeCache || secret == null ||
  1436. urlParams['lockdown'] == '1')
  1437. {
  1438. this.reload(success, error, abort, null, immediate);
  1439. }
  1440. else
  1441. {
  1442. // Cache entry may not have been uploaded to cache before new
  1443. // file is visible to client so retry once after cache miss
  1444. var cacheReadyRetryCount = 0;
  1445. var failed = false;
  1446. var doCatchup = mxUtils.bind(this, function()
  1447. {
  1448. if (abort == null || !abort())
  1449. {
  1450. // Ignores patch if shadow has changed
  1451. if (source != this.file.getCurrentRevisionId())
  1452. {
  1453. if (success != null)
  1454. {
  1455. success(true);
  1456. }
  1457. }
  1458. else if (!this.isValidState())
  1459. {
  1460. if (error != null)
  1461. {
  1462. error();
  1463. }
  1464. }
  1465. else
  1466. {
  1467. this.scheduleCleanup(true);
  1468. var acceptResponse = true;
  1469. var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  1470. {
  1471. acceptResponse = false;
  1472. this.reload(success, error, abort, null, immediate);
  1473. }), this.ui.timeout);
  1474. mxUtils.get(EditorUi.cacheUrl + '?id=' + encodeURIComponent(this.channelId) +
  1475. '&from=' + encodeURIComponent(source) + '&to=' + encodeURIComponent(target) +
  1476. ((secret != null) ? '&secret=' + encodeURIComponent(secret) : ''),
  1477. mxUtils.bind(this, function(req)
  1478. {
  1479. this.file.stats.bytesReceived += req.getText().length;
  1480. window.clearTimeout(timeoutThread);
  1481. if (acceptResponse && (abort == null || !abort()))
  1482. {
  1483. // Ignores patch if shadow has changed
  1484. if (source != this.file.getCurrentRevisionId())
  1485. {
  1486. if (success != null)
  1487. {
  1488. success(true);
  1489. }
  1490. }
  1491. else if (!this.isValidState())
  1492. {
  1493. if (error != null)
  1494. {
  1495. error();
  1496. }
  1497. }
  1498. else
  1499. {
  1500. var checksum = null;
  1501. var temp = [];
  1502. EditorUi.debug('DrawioFileSync.doCatchup',
  1503. [this], 'request', [req], 'status', req.getStatus(),
  1504. 'cacheReadyRetryCount', cacheReadyRetryCount,
  1505. 'maxCacheReadyRetries', this.maxCacheReadyRetries);
  1506. if (req.getStatus() >= 200 && req.getStatus() <= 299 &&
  1507. req.getText().length > 0)
  1508. {
  1509. try
  1510. {
  1511. var result = JSON.parse(req.getText());
  1512. if (result != null && result.length > 0)
  1513. {
  1514. for (var i = 0; i < result.length; i++)
  1515. {
  1516. var value = this.stringToObject(result[i]);
  1517. if (value.v > DrawioFileSync.PROTOCOL)
  1518. {
  1519. failed = true;
  1520. temp = [];
  1521. break;
  1522. }
  1523. else if (value.v === DrawioFileSync.PROTOCOL &&
  1524. value.d != null)
  1525. {
  1526. checksum = value.d.checksum;
  1527. temp.push(value.d.patch);
  1528. }
  1529. else
  1530. {
  1531. failed = true;
  1532. temp = [];
  1533. break;
  1534. }
  1535. }
  1536. }
  1537. EditorUi.debug('DrawioFileSync.doCatchup', [this],
  1538. 'response', [result], 'status',
  1539. (failed ? 'failed' : 'ok'),
  1540. 'temp', temp, 'checksum', checksum);
  1541. }
  1542. catch (e)
  1543. {
  1544. temp = [];
  1545. if (window.console != null && urlParams['test'] == '1')
  1546. {
  1547. console.log(e);
  1548. }
  1549. }
  1550. }
  1551. try
  1552. {
  1553. if (temp.length > 0)
  1554. {
  1555. this.file.stats.cacheHits++;
  1556. this.merge(temp, checksum, desc,
  1557. success, error, abort, immediate);
  1558. }
  1559. // Retries if cache entry was not yet there
  1560. else if (cacheReadyRetryCount <= this.maxCacheReadyRetries - 1 &&
  1561. !failed && req.getStatus() != 401 && req.getStatus() != 503 &&
  1562. req.getStatus() != 410)
  1563. {
  1564. cacheReadyRetryCount++;
  1565. this.file.stats.cacheMiss++;
  1566. window.setTimeout(doCatchup, (cacheReadyRetryCount + 1) *
  1567. this.cacheReadyDelay);
  1568. }
  1569. else
  1570. {
  1571. this.file.stats.cacheFail++;
  1572. this.reload(success, error, abort, null, immediate);
  1573. }
  1574. }
  1575. catch (e)
  1576. {
  1577. if (error != null)
  1578. {
  1579. error(e);
  1580. }
  1581. }
  1582. }
  1583. }
  1584. }), error);
  1585. }
  1586. }
  1587. });
  1588. window.setTimeout(doCatchup, this.cacheReadyDelay);
  1589. }
  1590. }
  1591. }
  1592. };
  1593. /**
  1594. * Adds the listener for automatically saving the diagram for local changes.
  1595. * Immediate is passed through to scheduleCleanup.
  1596. */
  1597. DrawioFileSync.prototype.reload = function(success, error, abort, shadow, immediate)
  1598. {
  1599. EditorUi.debug('DrawioFileSync.reload', [this], 'immediate', immediate);
  1600. this.file.updateFile(mxUtils.bind(this, function()
  1601. {
  1602. this.lastModified = this.file.getLastModifiedDate();
  1603. this.updateStatus();
  1604. this.start();
  1605. if (success != null)
  1606. {
  1607. success();
  1608. }
  1609. }), mxUtils.bind(this, function(err)
  1610. {
  1611. if (error != null)
  1612. {
  1613. error(err);
  1614. }
  1615. }), abort, shadow, immediate);
  1616. };
  1617. /**
  1618. * Invokes when the file descriptor was changed.
  1619. */
  1620. DrawioFileSync.prototype.descriptorChanged = function(source)
  1621. {
  1622. this.lastModified = this.file.getLastModifiedDate();
  1623. if (this.channelId != null)
  1624. {
  1625. var msg = this.objectToString(this.createMessage({a: 'desc',
  1626. m: this.lastModified.getTime()}));
  1627. var target = this.file.getCurrentRevisionId();
  1628. var data = this.objectToString({});
  1629. mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
  1630. '&from=' + encodeURIComponent(source) + '&to=' + encodeURIComponent(target) +
  1631. '&msg=' + encodeURIComponent(msg) + '&data=' + encodeURIComponent(data));
  1632. this.file.stats.bytesSent += data.length;
  1633. this.file.stats.msgSent++;
  1634. EditorUi.debug('DrawioFileSync.descriptorChanged',
  1635. [this], 'from', source, 'to', target);
  1636. }
  1637. this.updateStatus();
  1638. };
  1639. /**
  1640. * Converts the given object to an encrypted string.
  1641. */
  1642. DrawioFileSync.prototype.objectToString = function(obj)
  1643. {
  1644. var data = Graph.compress(JSON.stringify(obj));
  1645. if (this.key != null && typeof CryptoJS !== 'undefined')
  1646. {
  1647. data = CryptoJS.AES.encrypt(data, this.key).toString();
  1648. }
  1649. return data;
  1650. };
  1651. /**
  1652. * Converts the given encrypted string to an object.
  1653. */
  1654. DrawioFileSync.prototype.stringToObject = function(data)
  1655. {
  1656. if (this.key != null && typeof CryptoJS !== 'undefined')
  1657. {
  1658. data = CryptoJS.AES.decrypt(data, this.key).toString(CryptoJS.enc.Utf8);
  1659. }
  1660. return JSON.parse(Graph.decompress(data));
  1661. };
  1662. /**
  1663. * Requests a token for the given sec
  1664. */
  1665. DrawioFileSync.prototype.createToken = function(secret, success, error)
  1666. {
  1667. var acceptResponse = true;
  1668. var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  1669. {
  1670. acceptResponse = false;
  1671. error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')});
  1672. }), this.ui.timeout);
  1673. mxUtils.get(EditorUi.cacheUrl + '?id=' + encodeURIComponent(this.channelId) +
  1674. '&secret=' + encodeURIComponent(secret), mxUtils.bind(this, function(req)
  1675. {
  1676. window.clearTimeout(timeoutThread);
  1677. if (acceptResponse)
  1678. {
  1679. if (req.getStatus() >= 200 && req.getStatus() <= 299)
  1680. {
  1681. success(req.getText());
  1682. }
  1683. else
  1684. {
  1685. error({code: req.getStatus(), message: 'Token Error ' + req.getStatus()});
  1686. }
  1687. }
  1688. }), error);
  1689. };
  1690. /**
  1691. * Invoked when a save request for a file was sent regardless of the response.
  1692. */
  1693. DrawioFileSync.prototype.fileSaving = function()
  1694. {
  1695. if (this.file.isOptimisticSync())
  1696. {
  1697. this.notify(this.createMessage({
  1698. m: Date.now(), type: 'optimistic'}));
  1699. }
  1700. EditorUi.debug('DrawioFileSync.fileSaving', [this],
  1701. 'optimistic', this.file.isOptimisticSync());
  1702. };
  1703. /**
  1704. * Invoked when the file data was updated for saving.
  1705. */
  1706. DrawioFileSync.prototype.fileDataUpdated = function()
  1707. {
  1708. this.scheduleCleanup(true);
  1709. EditorUi.debug('DrawioFileSync.fileDataUpdated', [this]);
  1710. };
  1711. /**
  1712. * Invoked after a file was saved to add cache entry (which in turn notifies
  1713. * collaborators).
  1714. */
  1715. DrawioFileSync.prototype.fileSaved = function(pages, lastDesc, success, error, token, checksum)
  1716. {
  1717. this.lastModified = this.file.getLastModifiedDate();
  1718. this.resetUpdateStatusThread();
  1719. this.catchupRetryCount = 0;
  1720. if (!this.ui.isOffline(true) && !this.file.inConflictState &&
  1721. !this.file.redirectDialogShowing)
  1722. {
  1723. this.start();
  1724. if (this.channelId != null)
  1725. {
  1726. // Computes diff and checksum
  1727. var secret = this.file.getDescriptorSecret(this.file.getDescriptor());
  1728. var msg = this.createMessage({m: this.lastModified.getTime()});
  1729. var source = this.file.getDescriptorRevisionId(lastDesc);
  1730. var target = this.file.getCurrentRevisionId();
  1731. if (secret == null || token == null ||
  1732. urlParams['lockdown'] == '1' ||
  1733. !Editor.enableRealtimeCache)
  1734. {
  1735. this.notify(msg);
  1736. if (success != null)
  1737. {
  1738. success();
  1739. }
  1740. EditorUi.debug('DrawioFileSync.fileSaved', [this],
  1741. 'from', source, 'to', target, 'etag',
  1742. this.file.getCurrentEtag());
  1743. }
  1744. else
  1745. {
  1746. var diff = this.ui.diffPages(this.file.getShadowPages(), pages);
  1747. var lastSecret = this.file.getDescriptorSecret(lastDesc);
  1748. checksum = (checksum != null) ? checksum : this.ui.getHashValueForPages(pages);
  1749. // Data is stored in cache and message is sent to all listeners
  1750. var data = this.objectToString(this.createMessage(
  1751. {patch: diff, checksum: checksum}));
  1752. this.file.stats.bytesSent += data.length;
  1753. this.file.stats.msgSent++;
  1754. var acceptResponse = true;
  1755. var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  1756. {
  1757. acceptResponse = false;
  1758. error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')});
  1759. }), this.ui.timeout);
  1760. mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
  1761. '&from=' + encodeURIComponent(source) + '&to=' + encodeURIComponent(target) +
  1762. (!Editor.p2pSyncNotify ? '&msg=' + encodeURIComponent(this.objectToString(msg)) : '') +
  1763. ((secret != null) ? '&secret=' + encodeURIComponent(secret) : '') +
  1764. ((lastSecret != null) ? '&last-secret=' + encodeURIComponent(lastSecret) : '') +
  1765. ((data.length < this.maxCacheEntrySize) ? '&data=' + encodeURIComponent(data) : '') +
  1766. ((token != null) ? '&token=' + encodeURIComponent(token) : ''),
  1767. mxUtils.bind(this, function(req)
  1768. {
  1769. window.clearTimeout(timeoutThread);
  1770. if (acceptResponse)
  1771. {
  1772. if (req.getStatus() >= 200 && req.getStatus() <= 299)
  1773. {
  1774. if (Editor.p2pSyncNotify)
  1775. {
  1776. this.notify(msg);
  1777. }
  1778. if (success != null)
  1779. {
  1780. success();
  1781. }
  1782. }
  1783. else
  1784. {
  1785. error({message: mxResources.get('realtimeCollaboration') +
  1786. ((req.getStatus() != 0) ? ': ' + req.getStatus() : '')});
  1787. }
  1788. }
  1789. }));
  1790. EditorUi.debug('DrawioFileSync.fileSaved', [this],
  1791. 'from', source, 'to', target, 'etag',
  1792. this.file.getCurrentEtag(), 'diff',
  1793. diff, data.length, 'bytes',
  1794. 'checksum', checksum);
  1795. }
  1796. // Logs successull diff
  1797. // try
  1798. // {
  1799. // var user = this.file.getCurrentUser();
  1800. // var uid = (user != null) ? user.id : 'unknown';
  1801. //
  1802. // EditorUi.logEvent({category: 'DIFF-SYNC-FILE-' + this.file.getHash(),
  1803. // action: uid + '-diff-' + data.length + '-sent-' +
  1804. // this.file.stats.bytesSent + '-msgs-' +
  1805. // this.file.stats.msgSent, label: this.clientId});
  1806. // }
  1807. // catch (e)
  1808. // {
  1809. // // ignore
  1810. // }
  1811. }
  1812. }
  1813. // Ignores cache response as clients
  1814. // load file if cache entry failed
  1815. this.file.setShadowPages(pages);
  1816. this.scheduleCleanup();
  1817. };
  1818. /**
  1819. * Creates the properties for the file descriptor.
  1820. */
  1821. DrawioFileSync.prototype.getIdParameters = function()
  1822. {
  1823. var result = 'id=' + this.channelId;
  1824. if (this.pusher != null && this.pusher.connection != null &&
  1825. this.pusher.connection.socket_id != null)
  1826. {
  1827. result += '&sid=' + this.pusher.connection.socket_id;
  1828. }
  1829. return result;
  1830. };
  1831. /**
  1832. * Creates the properties for the file descriptor.
  1833. */
  1834. DrawioFileSync.prototype.createMessage = function(data)
  1835. {
  1836. return {v: DrawioFileSync.PROTOCOL, d: data, c: this.clientId};
  1837. };
  1838. /**
  1839. * Creates the properties for the file descriptor.
  1840. */
  1841. DrawioFileSync.prototype.fileConflict = function(desc, success, error)
  1842. {
  1843. this.catchupRetryCount++;
  1844. EditorUi.debug('DrawioFileSync.fileConflict', [this], 'desc', [desc],
  1845. 'catchupRetryCount', this.catchupRetryCount,
  1846. 'maxCatchupRetries', this.maxCatchupRetries);
  1847. if (this.catchupRetryCount < this.maxCatchupRetries)
  1848. {
  1849. this.file.stats.conflicts++;
  1850. if (desc != null)
  1851. {
  1852. this.catchup(desc, success, error);
  1853. }
  1854. else
  1855. {
  1856. this.fileChanged(success, error);
  1857. }
  1858. }
  1859. else
  1860. {
  1861. this.file.stats.timeouts++;
  1862. this.catchupRetryCount = 0;
  1863. if (error != null)
  1864. {
  1865. error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')});
  1866. }
  1867. }
  1868. };
  1869. /**
  1870. * Adds the listener for automatically saving the diagram for local changes.
  1871. */
  1872. DrawioFileSync.prototype.stop = function()
  1873. {
  1874. if (this.pusher != null)
  1875. {
  1876. if (this.pusher.connection != null)
  1877. {
  1878. this.pusher.connection.unbind('state_change', this.connectionListener);
  1879. this.pusher.connection.unbind('error', this.pusherErrorListener);
  1880. }
  1881. if (this.channel != null)
  1882. {
  1883. this.channel.unbind('changed', this.changeListener);
  1884. // See https://github.com/pusher/pusher-js/issues/75
  1885. // this.pusher.unsubscribe(this.channelId);
  1886. this.channel = null;
  1887. }
  1888. this.pusher.disconnect();
  1889. this.pusher = null;
  1890. if (this.p2pCollab != null)
  1891. {
  1892. this.p2pCollab.destroy();
  1893. this.p2pCollab = null;
  1894. }
  1895. EditorUi.debug('DrawioFileSync.stop', [this]);
  1896. }
  1897. else if (this.polling != null)
  1898. {
  1899. this.polling.stop();
  1900. this.polling = null;
  1901. }
  1902. this.updateOnlineState();
  1903. this.updateStatus();
  1904. };
  1905. /**
  1906. * Adds the listener for automatically saving the diagram for local changes.
  1907. */
  1908. DrawioFileSync.prototype.destroy = function()
  1909. {
  1910. if (this.channelId != null)
  1911. {
  1912. var user = this.file.getCurrentUser();
  1913. var leave = {a: 'leave'};
  1914. if (user != null)
  1915. {
  1916. leave.name = encodeURIComponent(user.displayName);
  1917. leave.uid = user.id;
  1918. }
  1919. this.notify(this.createMessage(leave));
  1920. }
  1921. this.stop();
  1922. if (this.onlineListener != null)
  1923. {
  1924. mxEvent.removeListener(window, 'offline', this.onlineListener);
  1925. mxEvent.removeListener(window, 'online', this.onlineListener);
  1926. this.onlineListener = null;
  1927. }
  1928. if (this.autosaveListener != null)
  1929. {
  1930. this.ui.editor.addListener('autosaveChanged', this.autosaveListener);
  1931. this.autosaveListener = null;
  1932. }
  1933. if (this.visibleListener != null)
  1934. {
  1935. mxEvent.removeListener(document, 'visibilitychange', this.visibleListener);
  1936. this.visibleListener = null;
  1937. }
  1938. if (this.activityListener != null)
  1939. {
  1940. mxEvent.removeListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.activityListener);
  1941. mxEvent.removeListener(document, 'keypress', this.activityListener);
  1942. mxEvent.removeListener(window, 'focus', this.activityListener);
  1943. if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
  1944. {
  1945. mxEvent.removeListener(document, 'touchstart', this.activityListener);
  1946. mxEvent.removeListener(document, 'touchmove', this.activityListener);
  1947. }
  1948. this.activityListener = null;
  1949. }
  1950. if (this.collaboratorsElement != null)
  1951. {
  1952. this.collaboratorsElement.parentNode.removeChild(this.collaboratorsElement);
  1953. this.collaboratorsElement = null;
  1954. }
  1955. // This is not needed now as stop already destroyed it
  1956. if (this.p2pCollab != null)
  1957. {
  1958. this.p2pCollab.destroy();
  1959. }
  1960. };