DrawioFileSync.js 31 KB

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