DrawioFileSync.js 29 KB

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