DrawioFileSync.js 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253
  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, data.length, 'bytes');
  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 details = [];
  646. var temp = [];
  647. if (req.getStatus() >= 200 && req.getStatus() <= 299 &&
  648. req.getText().length > 0)
  649. {
  650. try
  651. {
  652. var result = JSON.parse(req.getText());
  653. if (result != null && result.length > 0)
  654. {
  655. for (var i = 0; i < result.length; i++)
  656. {
  657. var value = this.stringToObject(result[i]);
  658. if (value.v > DrawioFileSync.PROTOCOL)
  659. {
  660. this.file.redirectToNewApp(error);
  661. temp = [];
  662. break;
  663. }
  664. else if (value.v === DrawioFileSync.PROTOCOL &&
  665. value.d != null)
  666. {
  667. checksum = value.d.checksum;
  668. temp.push(value.d.patch);
  669. if (value.d.details != null)
  670. {
  671. value.d.details.checksum = checksum;
  672. details.push(JSON.stringify(value.d.details));
  673. }
  674. }
  675. else
  676. {
  677. temp = [];
  678. break;
  679. }
  680. }
  681. }
  682. }
  683. catch (e)
  684. {
  685. temp = [];
  686. if (window.console != null && urlParams['test'] == '1')
  687. {
  688. console.log(e);
  689. }
  690. }
  691. }
  692. try
  693. {
  694. if (temp.length > 0)
  695. {
  696. this.file.stats.cacheHits++;
  697. this.merge(temp, checksum, etag, success, error, details);
  698. }
  699. // Retries if cache entry was not yet there
  700. else if (cacheReadyRetryCount <= this.maxCacheReadyRetries &&
  701. req.getStatus() != 401)
  702. {
  703. cacheReadyRetryCount++;
  704. window.setTimeout(doCatchup, this.cacheReadyDelay);
  705. }
  706. else
  707. {
  708. this.file.stats.cacheMiss++;
  709. this.reload(success, error, abort);
  710. }
  711. }
  712. catch (e)
  713. {
  714. if (error != null)
  715. {
  716. error(e);
  717. }
  718. }
  719. }
  720. }));
  721. }
  722. });
  723. window.setTimeout(doCatchup, this.cacheReadyDelay);
  724. }
  725. };
  726. /**
  727. * Adds the listener for automatically saving the diagram for local changes.
  728. */
  729. DrawioFileSync.prototype.reload = function(success, error, abort)
  730. {
  731. this.file.updateFile(mxUtils.bind(this, function()
  732. {
  733. if (this.channelId == null)
  734. {
  735. // Checks channel ID and starts sync
  736. this.start();
  737. }
  738. this.lastModified = this.file.getLastModifiedDate();
  739. this.updateStatus();
  740. if (success != null)
  741. {
  742. success();
  743. }
  744. }), mxUtils.bind(this, function(err)
  745. {
  746. if (error != null)
  747. {
  748. error(err);
  749. }
  750. }), abort);
  751. };
  752. /**
  753. * Adds the listener for automatically saving the diagram for local changes.
  754. */
  755. DrawioFileSync.prototype.merge = function(patches, checksum, etag, success, error, details)
  756. {
  757. try
  758. {
  759. this.lastModified = new Date();
  760. this.file.shadowPages = (this.file.shadowPages != null) ?
  761. this.file.shadowPages : this.ui.getPagesForNode(
  762. mxUtils.parseXml(this.file.shadowData).documentElement)
  763. this.file.checkShadow(this.file.shadowPages);
  764. // Creates a patch for backup if the checksum fails
  765. this.file.backupPatch = (this.file.isModified()) ?
  766. this.ui.diffPages(this.file.shadowPages,
  767. this.ui.pages) : null;
  768. if (!this.file.ignorePatches(patches))
  769. {
  770. this.file.stats.merged++;
  771. // Patches the shadow document
  772. for (var i = 0; i < patches.length; i++)
  773. {
  774. this.file.shadowPages = this.ui.patchPages(this.file.shadowPages, patches[i]);
  775. }
  776. var currentDetails = {};
  777. var current = (checksum != null) ? this.ui.getHashValueForPages(
  778. this.file.shadowPages, currentDetails) : null;
  779. if (urlParams['test'] == '1')
  780. {
  781. EditorUi.debug('Sync.merge', [this],
  782. 'from', this.file.getCurrentEtag(), 'to', etag,
  783. 'backup', this.file.backupPatch,
  784. 'attempt', this.catchupRetryCount,
  785. 'details', details,
  786. 'patches', patches,
  787. 'checksum', checksum == current, checksum);
  788. }
  789. // Compares the checksum
  790. if (checksum != null && checksum != current)
  791. {
  792. this.file.checksumError(error, patches,
  793. 'Checksum: ' + checksum +
  794. ((details != null && details.length > 0) ? ('\nDetails: ' +
  795. details.join(', ')) : '') +
  796. '\nCurrent: ' + current +
  797. ((currentDetails != null) ? ('\nCurrent Details: ' +
  798. JSON.stringify(currentDetails)) : ''));
  799. // Abnormal termination
  800. return;
  801. }
  802. else
  803. {
  804. // Patches the current document
  805. this.file.patch(patches,
  806. (DrawioFile.LAST_WRITE_WINS) ?
  807. this.file.backupPatch : null);
  808. }
  809. }
  810. this.file.invalidChecksum = false;
  811. this.file.inConflictState = false;
  812. this.file.setCurrentEtag(etag);
  813. this.file.backupPatch = null;
  814. this.file.checkPages();
  815. if (success != null)
  816. {
  817. success();
  818. }
  819. }
  820. catch (e)
  821. {
  822. this.file.inConflictState = true;
  823. this.file.invalidChecksum = true;
  824. if (error != null)
  825. {
  826. error(e);
  827. }
  828. try
  829. {
  830. this.file.sendErrorReport('Error in merge', null, e);
  831. }
  832. catch (e2)
  833. {
  834. // ignore
  835. }
  836. }
  837. };
  838. /**
  839. * Invokes after a file was saved to add cache entry (which in turn notifies
  840. * collaborators).
  841. */
  842. DrawioFileSync.prototype.descriptorChanged = function(etag)
  843. {
  844. this.lastModified = this.file.getLastModifiedDate();
  845. if (this.isConnected())
  846. {
  847. var msg = this.objectToString(this.createMessage({a: 'desc',
  848. m: this.lastModified.getTime()}));
  849. var current = this.file.getCurrentEtag();
  850. var data = this.objectToString({});
  851. mxUtils.post(this.cacheUrl, this.getIdParameters() +
  852. '&from=' + encodeURIComponent(etag) + '&to=' + encodeURIComponent(current) +
  853. '&msg=' + encodeURIComponent(msg) + '&data=' + encodeURIComponent(data));
  854. this.file.stats.bytesSent += data.length;
  855. this.file.stats.msgSent++;
  856. }
  857. this.updateStatus();
  858. };
  859. /**
  860. * Invokes after a file was saved to add cache entry (which in turn notifies
  861. * collaborators).
  862. */
  863. DrawioFileSync.prototype.objectToString = function(obj)
  864. {
  865. var data = this.ui.editor.graph.compress(JSON.stringify(obj));
  866. if (this.key != null && typeof CryptoJS !== 'undefined')
  867. {
  868. data = CryptoJS.AES.encrypt(data, this.key).toString();
  869. }
  870. return data;
  871. };
  872. /**
  873. * Invokes after a file was saved to add cache entry (which in turn notifies
  874. * collaborators).
  875. */
  876. DrawioFileSync.prototype.stringToObject = function(data)
  877. {
  878. if (this.key != null && typeof CryptoJS !== 'undefined')
  879. {
  880. data = CryptoJS.AES.decrypt(data, this.key).toString(CryptoJS.enc.Utf8);
  881. }
  882. return JSON.parse(this.ui.editor.graph.decompress(data));
  883. };
  884. /**
  885. * Invokes after a file was saved to add cache entry (which in turn notifies
  886. * collaborators).
  887. */
  888. DrawioFileSync.prototype.fileSaved = function(pages, lastDesc, success, error)
  889. {
  890. this.lastModified = this.file.getLastModifiedDate();
  891. this.resetUpdateStatusThread();
  892. this.catchupRetryCount = 0;
  893. if (this.isConnected() && !this.file.inConflictState && !this.redirectDialogShowing)
  894. {
  895. // Computes diff and checksum
  896. var shadow = (this.file.shadowPages != null) ?
  897. this.file.shadowPages : this.ui.getPagesForNode(
  898. mxUtils.parseXml(this.file.shadowData).documentElement)
  899. var details = {v: EditorUi.VERSION, t: new Date().toISOString(), ua: navigator.userAgent};
  900. var checksum = this.ui.getHashValueForPages(pages, details);
  901. var diff = this.ui.diffPages(shadow, pages);
  902. // Data is stored in cache and message is sent to all listeners
  903. var data = this.objectToString(this.createMessage({patch: diff, checksum: checksum, details: details}));
  904. var msg = this.objectToString(this.createMessage({m: this.lastModified.getTime()}));
  905. var secret = this.file.getDescriptorSecret(this.file.getDescriptor());
  906. var etag = this.file.getDescriptorEtag(lastDesc);
  907. var current = this.file.getCurrentEtag();
  908. this.file.shadowPages = pages;
  909. mxUtils.post(this.cacheUrl, this.getIdParameters() +
  910. '&from=' + encodeURIComponent(etag) + '&to=' + encodeURIComponent(current) +
  911. '&msg=' + encodeURIComponent(msg) + ((secret != null) ? '&secret=' + encodeURIComponent(secret) : '') +
  912. ((data.length < this.maxCacheEntrySize) ? '&data=' + encodeURIComponent(data) : ''),
  913. mxUtils.bind(this, function(req)
  914. {
  915. if (req.getStatus() >= 200 && req.getStatus() <= 299)
  916. {
  917. if (success != null)
  918. {
  919. success();
  920. }
  921. }
  922. else if (error != null)
  923. {
  924. error({message: req.getStatus()});
  925. }
  926. }));
  927. if (urlParams['test'] == '1')
  928. {
  929. EditorUi.debug('Sync.fileSaved', [this],
  930. 'from', etag, 'to', current, data.length,
  931. 'bytes', 'diff', diff, 'checksum', checksum);
  932. }
  933. this.file.stats.bytesSent += data.length;
  934. this.file.stats.msgSent++;
  935. }
  936. else
  937. {
  938. this.file.shadowPages = pages;
  939. if (this.channelId == null)
  940. {
  941. // Checks channel ID and starts sync
  942. this.start();
  943. }
  944. if (success != null)
  945. {
  946. success();
  947. }
  948. }
  949. };
  950. /**
  951. * Creates the properties for the file descriptor.
  952. */
  953. DrawioFileSync.prototype.getIdParameters = function()
  954. {
  955. var result = 'id=' + this.channelId;
  956. if (this.pusher != null && this.pusher.connection != null)
  957. {
  958. result += '&sid=' + this.pusher.connection.socket_id;
  959. }
  960. return result;
  961. };
  962. /**
  963. * Creates the properties for the file descriptor.
  964. */
  965. DrawioFileSync.prototype.createMessage = function(data)
  966. {
  967. return {v: DrawioFileSync.PROTOCOL, d: data, c: this.clientId};
  968. };
  969. /**
  970. * Creates the properties for the file descriptor.
  971. */
  972. DrawioFileSync.prototype.fileConflict = function(desc, success, error)
  973. {
  974. this.catchupRetryCount++;
  975. if (this.catchupRetryCount < this.maxCatchupRetries)
  976. {
  977. this.file.stats.conflicts++;
  978. if (desc != null)
  979. {
  980. var etag = this.file.getDescriptorEtag(desc);
  981. var secret = this.file.getDescriptorSecret(desc);
  982. this.catchup(etag, secret, success, error);
  983. }
  984. else
  985. {
  986. this.fileChanged(success, error);
  987. }
  988. }
  989. else
  990. {
  991. this.catchupRetryCount = 0;
  992. this.file.stats.timeouts++;
  993. if (error != null)
  994. {
  995. error({message: mxResources.get('timeout')});
  996. }
  997. }
  998. };
  999. /**
  1000. * Adds the listener for automatically saving the diagram for local changes.
  1001. */
  1002. DrawioFileSync.prototype.stop = function()
  1003. {
  1004. EditorUi.debug('Sync.stop', [this]);
  1005. if (this.changeListener != null && this.channel != null)
  1006. {
  1007. this.channel.unbind('changed', this.changeListener);
  1008. this.changeListener = null;
  1009. }
  1010. if (this.connectionListener != null)
  1011. {
  1012. if (this.pusher != null && this.pusher.connection != null)
  1013. {
  1014. this.pusher.connection.unbind('state_change', this.connectionListener);
  1015. }
  1016. this.connectionListener = null;
  1017. }
  1018. if (this.pusherErrorListener != null)
  1019. {
  1020. if (this.pusher != null && this.pusher.connection != null)
  1021. {
  1022. this.pusher.connection.unbind('error', this.pusherErrorListener);
  1023. }
  1024. this.pusherErrorListener = null;
  1025. }
  1026. if (this.pusher != null && this.channel != null && this.channelId != null)
  1027. {
  1028. // See https://github.com/pusher/pusher-js/issues/75
  1029. //this.pusher.unsubscribe(this.channelId);
  1030. this.channel = null;
  1031. }
  1032. if (this.pusher != null)
  1033. {
  1034. this.pusher.disconnect();
  1035. }
  1036. this.channelId = null;
  1037. this.pusher = null;
  1038. this.paused = true;
  1039. this.updateOnlineState();
  1040. this.updateStatus();
  1041. };
  1042. /**
  1043. * Adds the listener for automatically saving the diagram for local changes.
  1044. */
  1045. DrawioFileSync.prototype.destroy = function()
  1046. {
  1047. if (this.isConnected())
  1048. {
  1049. var user = this.file.getCurrentUser();
  1050. var leave = {a: 'leave'};
  1051. if (user != null)
  1052. {
  1053. leave.name = user.displayName;
  1054. leave.uid = user.id;
  1055. }
  1056. mxUtils.post(this.cacheUrl, this.getIdParameters() +
  1057. '&msg=' + encodeURIComponent(this.objectToString(
  1058. this.createMessage(leave))));
  1059. this.file.stats.msgSent++;
  1060. }
  1061. this.stop();
  1062. if (this.updateStatusThread != null)
  1063. {
  1064. window.clearInterval(this.updateStatusThread);
  1065. this.updateStatusThread = null;
  1066. }
  1067. if (this.onlineListener != null)
  1068. {
  1069. mxEvent.removeListener(window, 'online', this.onlineListener);
  1070. this.onlineListener = null;
  1071. }
  1072. if (this.visibleListener != null)
  1073. {
  1074. mxEvent.removeListener(document, 'visibilitychange', this.visibleListener);
  1075. this.visibleListener = null;
  1076. }
  1077. if (this.activityListener != null)
  1078. {
  1079. mxEvent.removeListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.activityListener);
  1080. mxEvent.removeListener(document, 'keypress', this.activityListener);
  1081. mxEvent.removeListener(window, 'focus', this.activityListener);
  1082. if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
  1083. {
  1084. mxEvent.removeListener(document, 'touchstart', this.activityListener);
  1085. mxEvent.removeListener(document, 'touchmove', this.activityListener);
  1086. }
  1087. this.activityListener = null;
  1088. }
  1089. if (this.collaboratorsElement != null)
  1090. {
  1091. this.collaboratorsElement.parentNode.removeChild(this.collaboratorsElement);
  1092. this.collaboratorsElement = null;
  1093. }
  1094. };