DrawioFileSync.js 32 KB

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