DrawioFileSync.js 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265
  1. /**
  2. * Copyright (c) 2006-2018, JGraph Ltd
  3. * Copyright (c) 2006-2018, Gaudenz Alder
  4. *
  5. * Realtime collaboration for any file.
  6. */
  7. DrawioFileSync = function(file)
  8. {
  9. mxEventSource.call(this);
  10. this.lastActivity = new Date();
  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())
  19. {
  20. this.fileChangedNotify();
  21. }
  22. });
  23. mxEvent.addListener(window, 'online', this.onlineListener);
  24. // Listens to visible state changes
  25. this.visibleListener = mxUtils.bind(this, function()
  26. {
  27. if (document.visibilityState == 'hidden')
  28. {
  29. if (this.isConnected())
  30. {
  31. this.stop();
  32. }
  33. }
  34. else
  35. {
  36. this.start();
  37. }
  38. });
  39. mxEvent.addListener(document, 'visibilitychange', this.visibleListener);
  40. // Listens to visible state changes
  41. this.activityListener = mxUtils.bind(this, function(evt)
  42. {
  43. this.lastActivity = new Date();
  44. this.start();
  45. });
  46. mxEvent.addListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.activityListener);
  47. mxEvent.addListener(document, 'keypress', this.activityListener);
  48. mxEvent.addListener(window, 'focus', this.activityListener);
  49. if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
  50. {
  51. mxEvent.addListener(document, 'touchstart', this.activityListener);
  52. mxEvent.addListener(document, 'touchmove', this.activityListener);
  53. }
  54. // Listens to errors in the pusher API
  55. this.pusherErrorListener = mxUtils.bind(this, function(err)
  56. {
  57. if (err.error != null && err.error.data != null &&
  58. err.error.data.code === 4004)
  59. {
  60. EditorUi.logError('Error: Pusher Limit', null, this.file.getId());
  61. }
  62. });
  63. // Listens to connection state changes
  64. this.connectionListener = mxUtils.bind(this, function()
  65. {
  66. this.updateOnlineState();
  67. this.updateStatus();
  68. if (this.isConnected())
  69. {
  70. if (!this.announced)
  71. {
  72. var user = this.file.getCurrentUser();
  73. var join = {a: 'join'};
  74. if (user != null)
  75. {
  76. join.name = user.displayName;
  77. join.uid = user.id;
  78. }
  79. mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
  80. '&msg=' + encodeURIComponent(this.objectToString(
  81. this.createMessage(join))));
  82. this.file.stats.msgSent++;
  83. this.announced = true;
  84. }
  85. // Catchup on any lost edits
  86. this.fileChangedNotify();
  87. }
  88. });
  89. // Listens to remove messages
  90. this.changeListener = mxUtils.bind(this, function(data)
  91. {
  92. this.file.stats.msgReceived++;
  93. this.lastActivity = new Date();
  94. if (this.enabled && !this.file.inConflictState &&
  95. !this.file.redirectDialogShowing)
  96. {
  97. try
  98. {
  99. var msg = this.stringToObject(data);
  100. if (msg != null)
  101. {
  102. EditorUi.debug('Sync.message', [this], msg, data.length, 'bytes');
  103. // Handles protocol mismatch
  104. if (msg.v > DrawioFileSync.PROTOCOL)
  105. {
  106. this.file.redirectToNewApp(mxUtils.bind(this, function()
  107. {
  108. // Callback adds cancel option
  109. }));
  110. }
  111. else if (msg.v === DrawioFileSync.PROTOCOL && msg.d != null)
  112. {
  113. this.handleMessageData(msg.d);
  114. }
  115. }
  116. }
  117. catch (e)
  118. {
  119. if (window.console != null && urlParams['test'] == '1')
  120. {
  121. console.log(e);
  122. }
  123. }
  124. }
  125. });
  126. };
  127. /**
  128. * Protocol version to be added to all communcations and diffs to check
  129. * if a client is out of date and force a refresh. Note that this must
  130. * be incremented if new messages are added or the format is changed.
  131. * This must be numeric to compare older vs newer protocol versions.
  132. */
  133. DrawioFileSync.PROTOCOL = 6;
  134. //Extends mxEventSource
  135. mxUtils.extend(DrawioFileSync, mxEventSource);
  136. /**
  137. * Maximum size in bytes for cache values.
  138. */
  139. DrawioFileSync.prototype.maxCacheEntrySize = 1000000;
  140. /**
  141. * Specifies if notifications should be sent and received for changes.
  142. */
  143. DrawioFileSync.prototype.enabled = true;
  144. /**
  145. * True if a change event is fired for a remote change.
  146. */
  147. DrawioFileSync.prototype.updateStatusInterval = 10000;
  148. /**
  149. * Holds the channel ID for sending and receiving change notifications.
  150. */
  151. DrawioFileSync.prototype.channelId = null;
  152. /**
  153. * Holds the channel ID for sending and receiving change notifications.
  154. */
  155. DrawioFileSync.prototype.channel = null;
  156. /**
  157. * Specifies if descriptor change events should be ignored.
  158. */
  159. DrawioFileSync.prototype.catchupRetryCount = 0;
  160. /**
  161. * Specifies if descriptor change events should be ignored.
  162. */
  163. DrawioFileSync.prototype.maxCatchupRetries = 15;
  164. /**
  165. * Specifies if descriptor change events should be ignored.
  166. */
  167. DrawioFileSync.prototype.maxCacheReadyRetries = 2;
  168. /**
  169. * Specifies if descriptor change events should be ignored.
  170. */
  171. DrawioFileSync.prototype.cacheReadyDelay = 500;
  172. /**
  173. * Inactivity timeout is 30 minutes.
  174. */
  175. DrawioFileSync.prototype.inactivityTimeoutSeconds = 1800;
  176. /**
  177. * Specifies if notifications should be sent and received for changes.
  178. */
  179. DrawioFileSync.prototype.lastActivity = null;
  180. /**
  181. * Adds all listeners.
  182. */
  183. DrawioFileSync.prototype.start = function()
  184. {
  185. if (this.channelId == null)
  186. {
  187. this.channelId = this.file.getChannelId();
  188. }
  189. if (this.key == null)
  190. {
  191. this.key = this.file.getChannelKey();
  192. }
  193. if (this.pusher == null && this.channelId != null &&
  194. document.visibilityState != 'hidden')
  195. {
  196. this.pusher = this.ui.getPusher();
  197. if (this.pusher != null)
  198. {
  199. try
  200. {
  201. // Error listener must be installed before trying to create channel
  202. if (this.pusher.connection != null)
  203. {
  204. this.pusher.connection.bind('error', this.pusherErrorListener);
  205. }
  206. }
  207. catch (e)
  208. {
  209. // ignore
  210. }
  211. try
  212. {
  213. this.pusher.connect();
  214. this.channel = this.pusher.subscribe(this.channelId);
  215. EditorUi.debug('Sync.start', [this]);
  216. }
  217. catch (e)
  218. {
  219. // ignore
  220. }
  221. this.installListeners();
  222. }
  223. window.setTimeout(mxUtils.bind(this, function()
  224. {
  225. this.lastModified = this.file.getLastModifiedDate();
  226. this.lastActivity = new Date();
  227. this.resetUpdateStatusThread();
  228. this.updateOnlineState();
  229. this.updateStatus();
  230. }, 0));
  231. }
  232. };
  233. /**
  234. * Draw function for the collaborator list.
  235. */
  236. DrawioFileSync.prototype.isConnected = function()
  237. {
  238. if (this.pusher != null && this.pusher.connection != null)
  239. {
  240. return this.pusher.connection.state == 'connected';
  241. }
  242. else
  243. {
  244. return false;
  245. }
  246. };
  247. /**
  248. * Draw function for the collaborator list.
  249. */
  250. DrawioFileSync.prototype.updateOnlineState = function()
  251. {
  252. var addClickHandler = mxUtils.bind(this, function(elt)
  253. {
  254. mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt)
  255. {
  256. this.enabled = !this.enabled;
  257. this.ui.updateButtonContainer();
  258. this.resetUpdateStatusThread();
  259. this.updateOnlineState();
  260. this.updateStatus();
  261. if (!this.file.inConflictState && this.enabled)
  262. {
  263. this.fileChangedNotify();
  264. }
  265. }));
  266. });
  267. if (uiTheme == 'min' && this.ui.buttonContainer != null)
  268. {
  269. if (this.collaboratorsElement == null)
  270. {
  271. var elt = document.createElement('a');
  272. elt.className = 'geToolbarButton';
  273. elt.style.cssText = 'display:inline-block;position:relative;box-sizing:border-box;margin-right:4px;cursor:pointer;float:left;';
  274. elt.style.backgroundPosition = 'center center';
  275. elt.style.backgroundRepeat = 'no-repeat';
  276. elt.style.backgroundSize = '24px 24px';
  277. elt.style.height = '24px';
  278. elt.style.width = '24px';
  279. addClickHandler(elt);
  280. this.ui.buttonContainer.appendChild(elt);
  281. this.collaboratorsElement = elt;
  282. }
  283. }
  284. else if (this.ui.toolbarContainer != null)
  285. {
  286. if (this.collaboratorsElement == null)
  287. {
  288. var elt = document.createElement('a');
  289. elt.className = 'geButton';
  290. elt.style.position = 'absolute';
  291. elt.style.display = 'inline-block';
  292. elt.style.verticalAlign = 'bottom';
  293. elt.style.color = '#666';
  294. elt.style.top = '8px';
  295. elt.style.right = (uiTheme != 'atlas') ? '70px' : '50px';
  296. elt.style.padding = '2px';
  297. elt.style.fontSize = '8pt';
  298. elt.style.verticalAlign = 'middle';
  299. elt.style.textDecoration = 'none';
  300. elt.style.backgroundPosition = 'center center';
  301. elt.style.backgroundRepeat = 'no-repeat';
  302. elt.style.backgroundSize = '16px 16px';
  303. elt.style.width = '16px';
  304. elt.style.height = '16px';
  305. mxUtils.setOpacity(elt, 60);
  306. if (uiTheme == 'dark')
  307. {
  308. elt.style.filter = 'invert(100%)';
  309. }
  310. // Prevents focus
  311. mxEvent.addListener(elt, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
  312. mxUtils.bind(this, function(evt)
  313. {
  314. evt.preventDefault();
  315. }));
  316. addClickHandler(elt);
  317. this.ui.toolbarContainer.appendChild(elt);
  318. this.collaboratorsElement = elt;
  319. }
  320. }
  321. if (this.collaboratorsElement != null)
  322. {
  323. var status = '';
  324. if (!this.enabled)
  325. {
  326. status = mxResources.get('disconnected');
  327. }
  328. else if (this.file.invalidChecksum)
  329. {
  330. status = mxResources.get('error') + ': ' + mxResources.get('checksum');
  331. }
  332. else if (this.ui.isOffline() || !this.isConnected())
  333. {
  334. status = mxResources.get('offline');
  335. }
  336. else
  337. {
  338. status = mxResources.get('online');
  339. }
  340. this.collaboratorsElement.setAttribute('title', status);
  341. this.collaboratorsElement.style.backgroundImage = 'url(' + ((!this.enabled) ? Editor.syncDisabledImage :
  342. ((!this.ui.isOffline() && this.isConnected() && !this.file.invalidChecksum) ?
  343. Editor.syncImage : Editor.syncProblemImage)) + ')';
  344. }
  345. };
  346. /**
  347. * Updates the status bar with the latest change.
  348. */
  349. DrawioFileSync.prototype.updateStatus = function()
  350. {
  351. if (this.isConnected() && this.lastActivity != null &&
  352. (new Date().getTime() - this.lastActivity.getTime()) / 1000 >
  353. this.inactivityTimeoutSeconds)
  354. {
  355. this.stop();
  356. }
  357. if (!this.file.isModified() && !this.file.inConflictState &&
  358. this.file.autosaveThread == null && !this.file.savingFile &&
  359. !this.file.redirectDialogShowing)
  360. {
  361. if (this.enabled && this.ui.statusContainer != null)
  362. {
  363. // LATER: Write out modified date for more than 2 weeks ago
  364. var str = this.ui.timeSince(new Date(this.lastModified));
  365. if (str == null)
  366. {
  367. str = mxResources.get('lessThanAMinute');
  368. }
  369. var history = this.file.isRevisionHistorySupported();
  370. // Consumed and displays last message
  371. var msg = this.lastMessage;
  372. this.lastMessage = null;
  373. if (msg != null && msg.length > 40)
  374. {
  375. msg = msg.substring(0, 40) + '...';
  376. }
  377. var label = mxResources.get('lastChange', [str]);
  378. this.ui.editor.setStatus('<div title="'+ mxUtils.htmlEntities(label) +
  379. '" style="display:inline-block;">' + mxUtils.htmlEntities(label) + '</div>' +
  380. ((msg != null) ? ' <span style="opacity:0;" title="' + mxUtils.htmlEntities(msg) +
  381. '">(' + mxUtils.htmlEntities(msg) + ')</span>' : '') +
  382. (this.file.isEditable() ? '' : '<div class="geStatusAlert" style="margin-left:8px;display:inline-block;">' +
  383. mxUtils.htmlEntities(mxResources.get('readOnly')) + '</div>') +
  384. (this.isConnected() ? '' : '<div class="geStatusAlert geBlink" style="margin-left:8px;display:inline-block;">' +
  385. mxUtils.htmlEntities(mxResources.get('disconnected')) + '</div>'));
  386. var links = this.ui.statusContainer.getElementsByTagName('div');
  387. if (links.length > 0 && history)
  388. {
  389. links[0].style.cursor = 'pointer';
  390. links[0].style.textDecoration = 'underline';
  391. mxEvent.addListener(links[0], 'click', mxUtils.bind(this, function()
  392. {
  393. this.ui.actions.get('revisionHistory').funct();
  394. }));
  395. }
  396. // Fades in/out last message
  397. var spans = this.ui.statusContainer.getElementsByTagName('span');
  398. if (spans.length > 0)
  399. {
  400. var temp = spans[0];
  401. mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 0.2s ease');
  402. window.setTimeout(mxUtils.bind(this, function()
  403. {
  404. mxUtils.setOpacity(temp, 100);
  405. mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 1s ease');
  406. window.setTimeout(mxUtils.bind(this, function()
  407. {
  408. mxUtils.setOpacity(temp, 0);
  409. }), this.updateStatusInterval / 2);
  410. }), 0);
  411. }
  412. this.resetUpdateStatusThread();
  413. }
  414. else
  415. {
  416. this.file.addAllSavedStatus();
  417. }
  418. }
  419. };
  420. /**
  421. * Resets the thread to update the status.
  422. */
  423. DrawioFileSync.prototype.resetUpdateStatusThread = function()
  424. {
  425. if (this.updateStatusThread != null)
  426. {
  427. window.clearInterval(this.updateStatusThread);
  428. }
  429. if (this.channel != null)
  430. {
  431. this.updateStatusThread = window.setInterval(mxUtils.bind(this, function()
  432. {
  433. this.updateStatus();
  434. }), this.updateStatusInterval);
  435. }
  436. };
  437. /**
  438. * Installs all required listeners for syncing the current file.
  439. */
  440. DrawioFileSync.prototype.installListeners = function()
  441. {
  442. if (this.pusher != null && this.pusher.connection != null)
  443. {
  444. this.pusher.connection.bind('state_change', this.connectionListener);
  445. }
  446. if (this.channel != null)
  447. {
  448. this.channel.bind('changed', this.changeListener);
  449. }
  450. };
  451. /**
  452. * Adds the listener for automatically saving the diagram for local changes.
  453. */
  454. DrawioFileSync.prototype.handleMessageData = function(data)
  455. {
  456. if (data.a == 'desc')
  457. {
  458. if (!this.file.savingFile)
  459. {
  460. this.reloadDescriptor();
  461. }
  462. }
  463. else if (data.a == 'join' || data.a == 'leave')
  464. {
  465. if (data.a == 'join')
  466. {
  467. this.file.stats.joined++;
  468. }
  469. if (data.name != null)
  470. {
  471. this.lastMessage = mxResources.get((data.a == 'join') ?
  472. 'userJoined' : 'userLeft', [data.name]);
  473. this.resetUpdateStatusThread();
  474. this.updateStatus();
  475. }
  476. }
  477. else if (data.m != null)
  478. {
  479. var mod = new Date(data.m);
  480. // Ignores obsolete messages
  481. if (this.lastMessageModified == null || this.lastMessageModified < mod)
  482. {
  483. this.lastMessageModified = mod;
  484. this.fileChangedNotify();
  485. }
  486. }
  487. };
  488. /**
  489. * Adds the listener for automatically saving the diagram for local changes.
  490. */
  491. DrawioFileSync.prototype.isValidState = function()
  492. {
  493. return this.ui.getCurrentFile() == this.file &&
  494. this.file.sync == this && !this.file.invalidChecksum &&
  495. !this.file.redirectDialogShowing;
  496. };
  497. /**
  498. * Adds the listener for automatically saving the diagram for local changes.
  499. */
  500. DrawioFileSync.prototype.fileChangedNotify = function()
  501. {
  502. if (this.isValidState())
  503. {
  504. if (this.file.savingFile)
  505. {
  506. this.remoteFileChanged = true;
  507. }
  508. else
  509. {
  510. // It's possible that a request never returns so override
  511. // existing requests and abort them when they are active
  512. var thread = this.fileChanged(mxUtils.bind(this, function(err)
  513. {
  514. this.updateStatus();
  515. }),
  516. mxUtils.bind(this, function(err)
  517. {
  518. this.file.handleFileError(err);
  519. }), mxUtils.bind(this, function()
  520. {
  521. return !this.file.savingFile && this.notifyThread != thread;
  522. }));
  523. }
  524. }
  525. };
  526. /**
  527. * Adds the listener for automatically saving the diagram for local changes.
  528. */
  529. DrawioFileSync.prototype.fileChanged = function(success, error, abort)
  530. {
  531. var thread = window.setTimeout(mxUtils.bind(this, function()
  532. {
  533. if (abort == null || !abort())
  534. {
  535. if (!this.isValidState())
  536. {
  537. if (error != null)
  538. {
  539. error();
  540. }
  541. }
  542. else
  543. {
  544. this.file.loadPatchDescriptor(mxUtils.bind(this, function(desc)
  545. {
  546. if (abort == null || !abort())
  547. {
  548. if (!this.isValidState())
  549. {
  550. if (error != null)
  551. {
  552. error();
  553. }
  554. }
  555. else
  556. {
  557. this.catchup(this.file.getDescriptorEtag(desc),
  558. this.file.getDescriptorSecret(desc),
  559. success, error, abort);
  560. }
  561. }
  562. }), error);
  563. }
  564. }
  565. }), 0);
  566. this.notifyThread = thread;
  567. return thread;
  568. };
  569. /**
  570. * Adds the listener for automatically saving the diagram for local changes.
  571. */
  572. DrawioFileSync.prototype.reloadDescriptor = function()
  573. {
  574. this.file.loadDescriptor(mxUtils.bind(this, function(desc)
  575. {
  576. if (desc != null)
  577. {
  578. // Forces data to be updated
  579. this.file.setDescriptorEtag(desc, this.file.getCurrentEtag());
  580. this.updateDescriptor(desc);
  581. this.fileChangedNotify();
  582. }
  583. else
  584. {
  585. this.file.inConflictState = true;
  586. this.file.handleFileError();
  587. }
  588. }), mxUtils.bind(this, function(err)
  589. {
  590. this.file.inConflictState = true;
  591. this.file.handleFileError(err);
  592. }));
  593. };
  594. /**
  595. * Adds the listener for automatically saving the diagram for local changes.
  596. */
  597. DrawioFileSync.prototype.updateDescriptor = function(desc)
  598. {
  599. this.file.setDescriptor(desc);
  600. this.file.descriptorChanged();
  601. this.start();
  602. };
  603. /**
  604. * Adds the listener for automatically saving the diagram for local changes.
  605. */
  606. DrawioFileSync.prototype.catchup = function(etag, secret, success, error, abort)
  607. {
  608. if (abort == null || !abort())
  609. {
  610. var current = this.file.getCurrentEtag();
  611. if (current == etag)
  612. {
  613. if (success != null)
  614. {
  615. success();
  616. }
  617. }
  618. else if (!this.isValidState())
  619. {
  620. if (error != null)
  621. {
  622. error();
  623. }
  624. }
  625. else
  626. {
  627. // Cache entry may not have been uploaded to cache before new
  628. // etag is visible to client so retry once after cache miss
  629. var cacheReadyRetryCount = 0;
  630. var failed = false;
  631. var doCatchup = mxUtils.bind(this, function()
  632. {
  633. if (abort == null || !abort())
  634. {
  635. // Ignores patch if shadow has changed
  636. if (current != this.file.getCurrentEtag())
  637. {
  638. if (success != null)
  639. {
  640. success();
  641. }
  642. }
  643. else if (!this.isValidState())
  644. {
  645. if (error != null)
  646. {
  647. error();
  648. }
  649. }
  650. else
  651. {
  652. var acceptResponse = true;
  653. var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
  654. {
  655. acceptResponse = false;
  656. this.reload(success, error, abort);
  657. }), this.ui.timeout);
  658. mxUtils.get(EditorUi.cacheUrl + '?id=' + encodeURIComponent(this.channelId) +
  659. '&from=' + encodeURIComponent(current) + '&to=' + encodeURIComponent(etag) +
  660. ((secret != null) ? '&secret=' + encodeURIComponent(secret) : ''),
  661. mxUtils.bind(this, function(req)
  662. {
  663. this.file.stats.bytesReceived += req.getText().length;
  664. window.clearTimeout(timeoutThread);
  665. if (acceptResponse && (abort == null || !abort()))
  666. {
  667. // Ignores patch if shadow has changed
  668. if (current != this.file.getCurrentEtag())
  669. {
  670. if (success != null)
  671. {
  672. success();
  673. }
  674. }
  675. else if (!this.isValidState())
  676. {
  677. if (error != null)
  678. {
  679. error();
  680. }
  681. }
  682. else
  683. {
  684. var checksum = null;
  685. var temp = [];
  686. if (req.getStatus() >= 200 && req.getStatus() <= 299 &&
  687. req.getText().length > 0)
  688. {
  689. try
  690. {
  691. var result = JSON.parse(req.getText());
  692. if (result != null && result.length > 0)
  693. {
  694. for (var i = 0; i < result.length; i++)
  695. {
  696. var value = this.stringToObject(result[i]);
  697. if (value.v > DrawioFileSync.PROTOCOL)
  698. {
  699. failed = true;
  700. temp = [];
  701. break;
  702. }
  703. else if (value.v === DrawioFileSync.PROTOCOL &&
  704. value.d != null)
  705. {
  706. checksum = value.d.checksum;
  707. temp.push(value.d.patch);
  708. }
  709. else
  710. {
  711. failed = true;
  712. temp = [];
  713. break;
  714. }
  715. }
  716. }
  717. }
  718. catch (e)
  719. {
  720. temp = [];
  721. if (window.console != null && urlParams['test'] == '1')
  722. {
  723. console.log(e);
  724. }
  725. }
  726. }
  727. try
  728. {
  729. if (temp.length > 0)
  730. {
  731. this.file.stats.cacheHits++;
  732. this.merge(temp, checksum, etag, success, error, abort);
  733. }
  734. // Retries if cache entry was not yet there
  735. else if (cacheReadyRetryCount <= this.maxCacheReadyRetries &&
  736. !failed && req.getStatus() != 401)
  737. {
  738. cacheReadyRetryCount++;
  739. this.file.stats.cacheMiss++;
  740. window.setTimeout(doCatchup, (cacheReadyRetryCount + 1) * this.cacheReadyDelay);
  741. }
  742. else
  743. {
  744. this.file.stats.cacheFail++;
  745. this.reload(success, error, abort);
  746. }
  747. }
  748. catch (e)
  749. {
  750. if (error != null)
  751. {
  752. error(e);
  753. }
  754. }
  755. }
  756. }
  757. }));
  758. }
  759. }
  760. });
  761. window.setTimeout(doCatchup, this.cacheReadyDelay);
  762. }
  763. }
  764. };
  765. /**
  766. * Adds the listener for automatically saving the diagram for local changes.
  767. */
  768. DrawioFileSync.prototype.reload = function(success, error, abort, shadow)
  769. {
  770. this.file.updateFile(mxUtils.bind(this, function()
  771. {
  772. this.lastModified = this.file.getLastModifiedDate();
  773. this.updateStatus();
  774. this.start();
  775. if (success != null)
  776. {
  777. success();
  778. }
  779. }), mxUtils.bind(this, function(err)
  780. {
  781. if (error != null)
  782. {
  783. error(err);
  784. }
  785. }), abort, shadow);
  786. };
  787. /**
  788. * Adds the listener for automatically saving the diagram for local changes.
  789. */
  790. DrawioFileSync.prototype.merge = function(patches, checksum, etag, success, error, abort)
  791. {
  792. try
  793. {
  794. this.file.stats.merged++;
  795. this.lastModified = new Date();
  796. this.file.shadowPages = (this.file.shadowPages != null) ?
  797. this.file.shadowPages : this.ui.getPagesForNode(
  798. mxUtils.parseXml(this.file.shadowData).documentElement)
  799. // Creates a patch for backup if the checksum fails
  800. this.file.backupPatch = (this.file.isModified()) ?
  801. this.ui.diffPages(this.file.shadowPages,
  802. this.ui.pages) : null;
  803. var ignored = this.file.ignorePatches(patches);
  804. if (!ignored)
  805. {
  806. // Patches the shadow document
  807. for (var i = 0; i < patches.length; i++)
  808. {
  809. this.file.shadowPages = this.ui.patchPages(this.file.shadowPages, patches[i]);
  810. }
  811. var current = (checksum != null) ? this.ui.getHashValueForPages(this.file.shadowPages) : null;
  812. if (urlParams['test'] == '1')
  813. {
  814. EditorUi.debug('Sync.merge', [this],
  815. 'from', this.file.getCurrentEtag(), 'to', etag,
  816. 'backup', this.file.backupPatch,
  817. 'attempt', this.catchupRetryCount,
  818. 'patches', patches,
  819. 'checksum', checksum == current, checksum);
  820. }
  821. // Compares the checksum
  822. if (checksum != null && checksum != current)
  823. {
  824. var from = this.ui.hashValue(this.file.getCurrentEtag());
  825. var to = this.ui.hashValue(etag);
  826. this.file.checksumError(error, patches, 'From: ' + from + '\nTo: ' + to +
  827. '\nChecksum: ' + checksum + '\nCurrent: ' + current, etag, 'merge');
  828. // Uses current state as shadow to compute diff since
  829. // shadowPages has been modified in-place above
  830. // LATER: Check if fallback to reload is possible
  831. // this.reload(success, error, abort, this.ui.pages);
  832. // Abnormal termination
  833. return;
  834. }
  835. else
  836. {
  837. // Patches the current document
  838. this.file.patch(patches,
  839. (DrawioFile.LAST_WRITE_WINS) ?
  840. this.file.backupPatch : null);
  841. }
  842. }
  843. this.file.invalidChecksum = false;
  844. this.file.inConflictState = false;
  845. this.file.setCurrentEtag(etag);
  846. this.file.backupPatch = null;
  847. if (success != null)
  848. {
  849. success();
  850. }
  851. }
  852. catch (e)
  853. {
  854. this.file.inConflictState = true;
  855. this.file.invalidChecksum = true;
  856. this.file.descriptorChanged();
  857. if (error != null)
  858. {
  859. error(e);
  860. }
  861. try
  862. {
  863. if (this.file.errorReportsEnabled)
  864. {
  865. var from = this.ui.hashValue(this.file.getCurrentEtag());
  866. var to = this.ui.hashValue(etag);
  867. this.file.sendErrorReport('Error in merge',
  868. 'From: ' + from + '\nTo: ' + to +
  869. '\nChecksum: ' + checksum +
  870. '\nPatches:\n' + this.file.compressReportData(
  871. JSON.stringify(patches, null, 2)), e);
  872. }
  873. else
  874. {
  875. var user = this.file.getCurrentUser();
  876. var uid = (user != null) ? user.id : 'unknown';
  877. EditorUi.logError('Error in merge', null,
  878. this.file.getMode() + '.' +
  879. this.file.getId(), uid, e);
  880. }
  881. }
  882. catch (e2)
  883. {
  884. // ignore
  885. }
  886. }
  887. };
  888. /**
  889. * Invokes after a file was saved to add cache entry (which in turn notifies
  890. * collaborators).
  891. */
  892. DrawioFileSync.prototype.descriptorChanged = function(etag)
  893. {
  894. this.lastModified = this.file.getLastModifiedDate();
  895. if (this.channelId != null)
  896. {
  897. var msg = this.objectToString(this.createMessage({a: 'desc',
  898. m: this.lastModified.getTime()}));
  899. var current = this.file.getCurrentEtag();
  900. var data = this.objectToString({});
  901. mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
  902. '&from=' + encodeURIComponent(etag) + '&to=' + encodeURIComponent(current) +
  903. '&msg=' + encodeURIComponent(msg) + '&data=' + encodeURIComponent(data));
  904. this.file.stats.bytesSent += data.length;
  905. this.file.stats.msgSent++;
  906. }
  907. this.updateStatus();
  908. };
  909. /**
  910. * Invokes after a file was saved to add cache entry (which in turn notifies
  911. * collaborators).
  912. */
  913. DrawioFileSync.prototype.objectToString = function(obj)
  914. {
  915. var data = Graph.compress(JSON.stringify(obj));
  916. if (this.key != null && typeof CryptoJS !== 'undefined')
  917. {
  918. data = CryptoJS.AES.encrypt(data, this.key).toString();
  919. }
  920. return data;
  921. };
  922. /**
  923. * Invokes after a file was saved to add cache entry (which in turn notifies
  924. * collaborators).
  925. */
  926. DrawioFileSync.prototype.stringToObject = function(data)
  927. {
  928. if (this.key != null && typeof CryptoJS !== 'undefined')
  929. {
  930. data = CryptoJS.AES.decrypt(data, this.key).toString(CryptoJS.enc.Utf8);
  931. }
  932. return JSON.parse(Graph.decompress(data));
  933. };
  934. /**
  935. * Invokes after a file was saved to add cache entry (which in turn notifies
  936. * collaborators).
  937. */
  938. DrawioFileSync.prototype.fileSaved = function(pages, lastDesc, success, error)
  939. {
  940. this.lastModified = this.file.getLastModifiedDate();
  941. this.resetUpdateStatusThread();
  942. this.catchupRetryCount = 0;
  943. if (!this.ui.isOffline() && !this.file.inConflictState && !this.file.redirectDialogShowing)
  944. {
  945. this.start();
  946. if (this.channelId != null)
  947. {
  948. // Computes diff and checksum
  949. var shadow = (this.file.shadowPages != null) ?
  950. this.file.shadowPages : this.ui.getPagesForNode(
  951. mxUtils.parseXml(this.file.shadowData).documentElement)
  952. var checksum = this.ui.getHashValueForPages(pages);
  953. var diff = this.ui.diffPages(shadow, pages);
  954. // Data is stored in cache and message is sent to all listeners
  955. var etag = this.file.getDescriptorEtag(lastDesc);
  956. var current = this.file.getCurrentEtag();
  957. var data = this.objectToString(this.createMessage({patch: diff, checksum: checksum}));
  958. var msg = this.objectToString(this.createMessage({m: this.lastModified.getTime()}));
  959. var secret = this.file.getDescriptorSecret(this.file.getDescriptor());
  960. this.file.stats.bytesSent += data.length;
  961. this.file.stats.msgSent++;
  962. mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
  963. '&from=' + encodeURIComponent(etag) + '&to=' + encodeURIComponent(current) +
  964. '&msg=' + encodeURIComponent(msg) + ((secret != null) ? '&secret=' + encodeURIComponent(secret) : '') +
  965. ((data.length < this.maxCacheEntrySize) ? '&data=' + encodeURIComponent(data) : ''),
  966. mxUtils.bind(this, function(req)
  967. {
  968. // Ignores response
  969. }));
  970. if (urlParams['test'] == '1')
  971. {
  972. EditorUi.debug('Sync.fileSaved', [this],
  973. 'from', etag, 'to', current, data.length,
  974. 'bytes', 'diff', diff, 'checksum', checksum);
  975. }
  976. }
  977. }
  978. this.file.shadowPages = pages;
  979. if (success != null)
  980. {
  981. success();
  982. }
  983. };
  984. /**
  985. * Creates the properties for the file descriptor.
  986. */
  987. DrawioFileSync.prototype.getIdParameters = function()
  988. {
  989. var result = 'id=' + this.channelId;
  990. if (this.pusher != null && this.pusher.connection != null)
  991. {
  992. result += '&sid=' + this.pusher.connection.socket_id;
  993. }
  994. return result;
  995. };
  996. /**
  997. * Creates the properties for the file descriptor.
  998. */
  999. DrawioFileSync.prototype.createMessage = function(data)
  1000. {
  1001. return {v: DrawioFileSync.PROTOCOL, d: data, c: this.clientId};
  1002. };
  1003. /**
  1004. * Creates the properties for the file descriptor.
  1005. */
  1006. DrawioFileSync.prototype.fileConflict = function(desc, success, error)
  1007. {
  1008. this.catchupRetryCount++;
  1009. if (this.catchupRetryCount < this.maxCatchupRetries)
  1010. {
  1011. this.file.stats.conflicts++;
  1012. if (desc != null)
  1013. {
  1014. var etag = this.file.getDescriptorEtag(desc);
  1015. var secret = this.file.getDescriptorSecret(desc);
  1016. this.catchup(etag, secret, success, error);
  1017. }
  1018. else
  1019. {
  1020. this.fileChanged(success, error);
  1021. }
  1022. }
  1023. else
  1024. {
  1025. this.file.stats.timeouts++;
  1026. this.catchupRetryCount = 0;
  1027. if (error != null)
  1028. {
  1029. error({message: mxResources.get('timeout')});
  1030. }
  1031. }
  1032. };
  1033. /**
  1034. * Adds the listener for automatically saving the diagram for local changes.
  1035. */
  1036. DrawioFileSync.prototype.stop = function()
  1037. {
  1038. if (this.pusher != null)
  1039. {
  1040. EditorUi.debug('Sync.stop', [this]);
  1041. if (this.pusher.connection != null)
  1042. {
  1043. this.pusher.connection.unbind('state_change', this.connectionListener);
  1044. this.pusher.connection.unbind('error', this.pusherErrorListener);
  1045. }
  1046. if (this.channel != null)
  1047. {
  1048. this.channel.unbind('changed', this.changeListener);
  1049. // See https://github.com/pusher/pusher-js/issues/75
  1050. // this.pusher.unsubscribe(this.channelId);
  1051. this.channel = null;
  1052. }
  1053. this.pusher.disconnect();
  1054. this.pusher = null;
  1055. }
  1056. this.updateOnlineState();
  1057. this.updateStatus();
  1058. };
  1059. /**
  1060. * Adds the listener for automatically saving the diagram for local changes.
  1061. */
  1062. DrawioFileSync.prototype.destroy = function()
  1063. {
  1064. if (this.channelId != null)
  1065. {
  1066. var user = this.file.getCurrentUser();
  1067. var leave = {a: 'leave'};
  1068. if (user != null)
  1069. {
  1070. leave.name = user.displayName;
  1071. leave.uid = user.id;
  1072. }
  1073. mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
  1074. '&msg=' + encodeURIComponent(this.objectToString(
  1075. this.createMessage(leave))));
  1076. this.file.stats.msgSent++;
  1077. }
  1078. this.stop();
  1079. if (this.updateStatusThread != null)
  1080. {
  1081. window.clearInterval(this.updateStatusThread);
  1082. this.updateStatusThread = null;
  1083. }
  1084. if (this.onlineListener != null)
  1085. {
  1086. mxEvent.removeListener(window, 'online', this.onlineListener);
  1087. this.onlineListener = null;
  1088. }
  1089. if (this.visibleListener != null)
  1090. {
  1091. mxEvent.removeListener(document, 'visibilitychange', this.visibleListener);
  1092. this.visibleListener = null;
  1093. }
  1094. if (this.activityListener != null)
  1095. {
  1096. mxEvent.removeListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.activityListener);
  1097. mxEvent.removeListener(document, 'keypress', this.activityListener);
  1098. mxEvent.removeListener(window, 'focus', this.activityListener);
  1099. if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
  1100. {
  1101. mxEvent.removeListener(document, 'touchstart', this.activityListener);
  1102. mxEvent.removeListener(document, 'touchmove', this.activityListener);
  1103. }
  1104. this.activityListener = null;
  1105. }
  1106. if (this.collaboratorsElement != null)
  1107. {
  1108. this.collaboratorsElement.parentNode.removeChild(this.collaboratorsElement);
  1109. this.collaboratorsElement = null;
  1110. }
  1111. };