/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*
* Realtime collaboration for any file.
*/
DrawioFileSync = function(file)
{
mxEventSource.call(this);
this.lastActivity = Date.now();
this.clientId = Editor.guid();
this.ui = file.ui;
this.file = file;
// Listens to online state changes
this.onlineListener = mxUtils.bind(this, function()
{
this.updateOnlineState();
if (this.isConnected() && !this.ui.isOffline(true))
{
this.fileChangedNotify();
}
else
{
this.updateStatus();
}
});
mxEvent.addListener(window, 'offline', this.onlineListener);
mxEvent.addListener(window, 'online', this.onlineListener);
// Listens to realtime state changes
this.realtimeListener = mxUtils.bind(this, function()
{
this.updateOnlineState();
});
this.file.addListener('realtimeStateChanged', this.realtimeListener);
// Listens to autosave changes to update the realtime collab socket
this.autosaveListener = mxUtils.bind(this, function()
{
this.updateRealtime();
});
this.ui.editor.addListener('autosaveChanged', this.autosaveListener);
// Listens to visible state changes
this.visibleListener = mxUtils.bind(this, function()
{
if (document.visibilityState == 'hidden')
{
if (this.isConnected())
{
this.stop();
}
}
else
{
this.start();
}
});
mxEvent.addListener(document, 'visibilitychange', this.visibleListener);
// Listens to visible state changes
this.activityListener = mxUtils.bind(this, function(evt)
{
this.lastActivity = Date.now();
this.start();
});
mxEvent.addListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.activityListener);
mxEvent.addListener(document, 'keypress', this.activityListener);
mxEvent.addListener(window, 'focus', this.activityListener);
if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
{
mxEvent.addListener(document, 'touchstart', this.activityListener);
mxEvent.addListener(document, 'touchmove', this.activityListener);
}
// Listens to fast sync activitiy
this.file.addListener('realtimeMessage', this.activityListener);
// Listens to errors in the pusher API
this.pusherErrorListener = mxUtils.bind(this, function(err)
{
if (err.error != null && err.error.data != null &&
err.error.data.code === 4004)
{
EditorUi.logError('Error: Pusher Limit', null, this.file.getId());
}
});
// Listens to connection state changes
this.connectionListener = mxUtils.bind(this, function()
{
this.updateOnlineState();
this.updateStatus();
if (this.isConnected())
{
if (!this.announced && Editor.enableRealtimeCache &&
!Editor.p2pSyncNotify)
{
this.sendJoinMessage();
}
else if (this.announced)
{
// Catchup on any lost edits
this.fileChangedNotify(null, true);
}
}
});
// Listens to messages
this.changeListener = mxUtils.bind(this, function(data)
{
this.file.stats.msgReceived++;
this.lastActivity = Date.now();
if (this.enabled && !this.file.inConflictState &&
!this.file.redirectDialogShowing)
{
try
{
var msg = this.stringToObject(data);
if (msg != null)
{
EditorUi.debug('DrawioFileSync.message', [this], msg, data.length, 'bytes');
// Handles protocol mismatch
if (msg.v > DrawioFileSync.PROTOCOL)
{
this.file.redirectToNewApp(mxUtils.bind(this, function()
{
// Callback adds cancel option
}));
}
else if (msg.v === DrawioFileSync.PROTOCOL && msg.d != null)
{
this.handleMessageData(msg.d);
}
}
}
catch (e)
{
// Checks if file was changed
if (this.isConnected())
{
this.fileChangedNotify();
}
// NOTE: Probably UTF16 in username for join/leave message causing this
// var len = (data != null) ? data.length : 'null';
//
// EditorUi.logError('Protocol Error ' + e.message,
// null, 'data_' + len + '_file_' + this.file.getHash() +
// '_client_' + this.clientId);
//
// if (window.console != null)
// {
// console.log(e);
// }
}
}
});
};
/**
* Protocol version to be added to all communcations and diffs to check
* if a client is out of date and force a refresh. Note that this must
* be incremented if new messages are added or the format is changed.
* This must be numeric to compare older vs newer protocol versions.
*/
DrawioFileSync.PROTOCOL = 6;
/**
* Enables socket connections.
*/
DrawioFileSync.ENABLE_SOCKETS = urlParams['sockets'] != '0';
//Extends mxEventSource
mxUtils.extend(DrawioFileSync, mxEventSource);
/**
* Maximum size in bytes for cache values.
*/
DrawioFileSync.prototype.maxCacheEntrySize = 1000000;
/**
* Maximum size in bytes for fast sync messages via Pusher.
* Use 0 to disable message size check. Default is 9KB.
*/
DrawioFileSync.prototype.maxSyncMessageSize = 9000;
/**
* Delay for fast sync message sending in ms. Larger
* values help to group sending out changes, smaller
* values reduce latency.
*/
DrawioFileSync.prototype.syncSendMessageDelay = 300;
/**
* Delay for received sync message processing in ms.
* Larger values help to sort and merge messages,
* smaller values reduce latency.
*/
DrawioFileSync.prototype.syncReceiveMessageDelay = 50;
/**
* Inactivity time to undo remote changes that have not been saved
* to the file. Larger values give time to save, smaller values
* require less inactivity time by the user. (Conflict handling
* for a local and remote save takes around 15 seconds.)
*/
DrawioFileSync.prototype.cleanupDelay = 15000;
/**
* Counter for local message IDs.
*/
DrawioFileSync.prototype.syncChangeCounter = 0;
/**
* Specifies if notifications should be sent and received for changes.
*/
DrawioFileSync.prototype.enabled = true;
/**
* Holds the channel ID for sending and receiving change notifications.
*/
DrawioFileSync.prototype.channelId = null;
/**
* Holds the channel ID for sending and receiving change notifications.
*/
DrawioFileSync.prototype.channel = null;
/**
* Specifies if descriptor change events should be ignored.
*/
DrawioFileSync.prototype.catchupRetryCount = 0;
/**
* Specifies if descriptor change events should be ignored.
*/
DrawioFileSync.prototype.maxCatchupRetries = 15;
/**
* Specifies if descriptor change events should be ignored.
*/
DrawioFileSync.prototype.maxCacheReadyRetries = 1;
/**
* Specifies if descriptor change events should be ignored.
*/
DrawioFileSync.prototype.cacheReadyDelay = 700;
/**
* Specifies if descriptor change events should be ignored.
*/
DrawioFileSync.prototype.maxOptimisticRetries = 6;
/**
* Inactivity timeout is 30 minutes.
*/
DrawioFileSync.prototype.inactivityTimeoutSeconds = 1800;
/**
* Specifies if notifications should be sent and received for changes.
*/
DrawioFileSync.prototype.lastActivity = null;
/**
* Adds all listeners.
*/
DrawioFileSync.prototype.start = function()
{
if (this.channelId == null)
{
this.channelId = this.file.getChannelId();
}
if (this.key == null)
{
this.key = this.file.getChannelKey();
}
var updateStatus = false;
if (DrawioFileSync.PULLING_MODE && this.puller == null &&
document.visibilityState != 'hidden')
{
if (this.puller == null)
{
this.puller = new DrawioFilePuller(this.file, this);
}
this.puller.start(this.file.getPullingInterval());
EditorUi.debug('DrawioFileSync.start (Pulling)', [this],
'version', DrawioFileSync.PROTOCOL,
'rev', this.file.getCurrentRevisionId());
updateStatus = true;
}
else if (!DrawioFileSync.PULLING_MODE && this.pusher == null &&
this.channelId != null && document.visibilityState != 'hidden')
{
this.pusher = this.ui.getPusher();
if (this.pusher != null)
{
try
{
// Error listener must be installed before trying to create channel
if (this.pusher.connection != null)
{
this.pusher.connection.bind('error', this.pusherErrorListener);
}
}
catch (e)
{
// ignore
}
try
{
this.pusher.connect();
this.channel = this.pusher.subscribe(this.channelId);
EditorUi.debug('DrawioFileSync.start', [this],
'version', DrawioFileSync.PROTOCOL,
'rev', this.file.getCurrentRevisionId());
}
catch (e)
{
// ignore
}
this.installListeners();
}
updateStatus = true;
}
if (updateStatus)
{
window.setTimeout(mxUtils.bind(this, function()
{
this.lastModified = this.file.getLastModifiedDate();
this.lastActivity = Date.now();
this.resetUpdateStatusThread();
this.updateOnlineState();
this.updateStatus();
}, 0));
}
this.updateRealtime();
};
/**
* Draw function for the collaborator list.
*/
DrawioFileSync.prototype.updateRealtime = function()
{
if (this.isValidState())
{
if (this.file.isRealtimeEnabled() &&
this.file.isRealtimeSupported() &&
this.isRealtimeActive())
{
if (!this.file.isRealtime())
{
this.initRealtime();
}
}
else if (this.file.isRealtime())
{
this.resetRealtime();
}
if (DrawioFileSync.ENABLE_SOCKETS && this.file.isRealtime() &&
this.p2pCollab == null && this.channelId != null)
{
this.p2pCollab = new P2PCollab(this.ui, this, this.channelId);
this.p2pCollab.joinFile();
}
else if (!this.file.isRealtime() && this.p2pCollab != null)
{
this.p2pCollab.destroy();
this.p2pCollab = null;
}
}
};
/**
* Initializes the realtime model.
*/
DrawioFileSync.prototype.initRealtime = function()
{
this.file.theirPages = this.ui.clonePages(
this.ui.pages);
this.file.ownPages = this.ui.clonePages(
this.ui.pages);
this.snapshot = this.file.ownPages;
};
/**
* Resets the realtime model.
*/
DrawioFileSync.prototype.resetRealtime = function()
{
var shadow = this.file.getShadowPages();
if (shadow != null)
{
var patch = this.ui.diffPages(
shadow, this.file.ownPages);
this.file.patch([patch]);
}
this.sendLocalChanges();
this.cleanup();
this.file.theirPages = null;
this.file.ownPages = null;
this.snapshot = null;
};
/**
* Draw function for the collaborator list.
*/
DrawioFileSync.prototype.isConnected = function()
{
if (this.pusher != null && this.pusher.connection != null)
{
return this.pusher.connection.state == 'connected';
}
else if (this.puller != null)
{
return this.puller.isConnected();
}
else
{
return false;
}
};
/**
* Draw function for the collaborator list.
*/
DrawioFileSync.prototype.updateOnlineState = function()
{
//For RT in embeded mode, we don't need this icon
if (urlParams['embedRT'] == '1')
{
return;
}
if (this.ui.toolbarContainer != null && this.collaboratorsElement == null)
{
this.collaboratorsElement = this.createCollaboratorsElement();
this.ui.toolbarContainer.appendChild(this.collaboratorsElement);
}
this.updateCollaboratorsElement();
};
/**
* Updates the status bar with the latest change.
*/
DrawioFileSync.prototype.updateCollaboratorsElement = function()
{
if (this.collaboratorsElement != null)
{
var status = this.ui.getNetworkStatus();
if (status != null)
{
this.collaboratorsElement.style.backgroundImage = 'url(' +
Editor.syncProblemImage + ')';
this.collaboratorsElement.style.display = 'inline-block';
this.collaboratorsElement.setAttribute('title', status);
}
else
{
this.collaboratorsElement.style.display = 'none';
}
}
};
/**
* Updates the status bar with the latest change.
*/
DrawioFileSync.prototype.createCollaboratorsElement = function()
{
var elt = document.createElement('a');
elt.className = 'geButton geAdaptiveAsset';
elt.style.position = 'absolute';
elt.style.display = 'inline-block';
elt.style.verticalAlign = 'bottom';
elt.style.color = '#666';
elt.style.top = '6px';
elt.style.right = (Editor.currentTheme != 'atlas') ? '70px' : '50px';
elt.style.padding = '2px';
elt.style.fontSize = '8pt';
elt.style.verticalAlign = 'middle';
elt.style.textDecoration = 'none';
elt.style.backgroundPosition = 'center center';
elt.style.backgroundRepeat = 'no-repeat';
elt.style.backgroundSize = '16px 16px';
elt.style.width = '16px';
elt.style.height = '16px';
elt.style.opacity = '0.6';
// Prevents focus
mxEvent.addListener(elt, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
mxUtils.bind(this, function(evt)
{
evt.preventDefault();
}));
mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt)
{
if (this.file.isRealtimeEnabled() && this.file.isRealtimeSupported())
{
var status = this.ui.getNetworkStatus();
this.ui.showError(mxResources.get('realtimeCollaboration'),
mxUtils.htmlEntities((status != null) ? status :
mxResources.get('online')));
}
else
{
this.enabled = !this.enabled;
this.ui.updateButtonContainer();
this.resetUpdateStatusThread();
this.updateOnlineState();
this.updateStatus();
if (!this.file.inConflictState && this.enabled)
{
this.fileChangedNotify();
}
}
}));
return elt;
};
/**
* Updates the status bar with the latest change.
*/
DrawioFileSync.prototype.updateStatus = function()
{
if (this.isConnected() && this.lastActivity != null &&
(Date.now() - this.lastActivity) / 1000 >
this.inactivityTimeoutSeconds)
{
this.stop();
}
if (!this.file.isModified() && !this.file.inConflictState &&
this.file.autosaveThread == null && !this.file.savingFile &&
!this.file.redirectDialogShowing)
{
if (this.enabled && this.ui.statusContainer != null)
{
// LATER: Write out modified date for more than 2 weeks ago
var str = this.ui.timeSince(new Date(this.lastModified));
if (str == null)
{
str = mxResources.get('lessThanAMinute');
}
// Consumes and displays last message
var msg = this.lastMessage;
this.lastMessage = null;
if (msg != null && msg.length > 40)
{
msg = msg.substring(0, 40) + '...';
}
var status = this.ui.getNetworkStatus();
var label = mxResources.get('lastChange', [str]);
var rev = (this.file.isRevisionHistorySupported()) ? 'data-action="revisionHistory" ' : '';
this.ui.editor.setStatus('
' + mxUtils.htmlEntities(label) + '
' +
(!this.file.isEditable() ? '' +
mxUtils.htmlEntities(mxResources.get('readOnly')) + '
' : '') +
(status != null ? '' +
mxUtils.htmlEntities(status) + '
' : '') +
((msg != null) ? ' ' +
mxUtils.htmlEntities(msg) + '
' : ''));
this.resetUpdateStatusThread();
}
else
{
this.file.addAllSavedStatus();
}
}
};
/**
* Resets the thread to update the status.
*/
DrawioFileSync.prototype.resetUpdateStatusThread = function()
{
if (this.updateStatusThread != null)
{
window.clearInterval(this.updateStatusThread);
}
if (this.channel != null)
{
this.updateStatusThread = window.setInterval(mxUtils.bind(this, function()
{
this.updateStatus();
}), Editor.updateStatusInterval);
}
};
/**
* Installs all required listeners for syncing the current file.
*/
DrawioFileSync.prototype.installListeners = function()
{
if (this.pusher != null && this.pusher.connection != null)
{
this.pusher.connection.bind('state_change', this.connectionListener);
}
if (this.channel != null)
{
this.channel.bind('changed', this.changeListener);
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.notify = function(msg)
{
this.file.stats.msgSent++;
if (Editor.enableRealtimeCache && !Editor.p2pSyncNotify)
{
mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
'&msg=' + encodeURIComponent(this.objectToString(msg)));
}
else if (this.p2pCollab != null)
{
this.p2pCollab.sendNotification(msg);
}
EditorUi.debug('DrawioFileSync.notify', [this],
'enableRealtimeCache', Editor.enableRealtimeCache,
'p2pSyncNotify', Editor.p2pSyncNotify,
'msg', msg);
};
/**
*
*/
DrawioFileSync.prototype.sendJoinMessage = function()
{
if (!this.announced)
{
var user = this.file.getCurrentUser();
var join = {a: 'join'};
if (user != null)
{
join.name = encodeURIComponent(user.displayName);
join.uid = user.id;
}
this.notify(this.createMessage(join));
this.announced = true;
}
}
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.handleMessageData = function(data)
{
if (data.a == 'desc')
{
if (!this.file.savingFile)
{
this.reloadDescriptor();
}
}
else if (data.a == 'join' || data.a == 'leave')
{
if (data.a == 'join')
{
this.file.stats.joined++;
}
if (data.name != null)
{
this.lastMessage = mxResources.get((data.a == 'join') ?
'userJoined' : 'userLeft', [decodeURIComponent(data.name)]);
this.resetUpdateStatusThread();
this.updateStatus();
}
}
else if (data.a == 'change')
{
this.receiveRemoteChanges(data);
}
else if (data.m != null)
{
var mod = new Date(data.m);
// Ignores obsolete messages
if (this.lastMessageModified == null ||
this.lastMessageModified < mod)
{
this.lastMessageModified = mod;
this.fileChangedNotify();
}
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.isValidState = function()
{
return this.ui.getCurrentFile() == this.file &&
this.file.sync == this && !this.file.invalidChecksum &&
!this.file.redirectDialogShowing;
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.optimisticSync = function(count)
{
if (this.reloadThread == null)
{
count = (count != null) ? count : 0;
if (count < this.maxOptimisticRetries)
{
this.reloadThread = window.setTimeout(mxUtils.bind(this, function()
{
EditorUi.debug('DrawioFileSync.optimisticSync', [this],
'attempt', count, 'of', this.maxOptimisticRetries,
'remoteFileChanged', this.remoteFileChanged);
this.remoteFileChanged = false;
this.file.getLatestVersion(mxUtils.bind(this, function(latestFile)
{
this.reloadThread = null;
if (latestFile != null)
{
var source = this.file.getCurrentRevisionId();
var target = latestFile.getCurrentRevisionId();
// Retries if the file has not changed
if (source == target)
{
this.optimisticSync(count + 1);
}
else
{
this.file.mergeFile(latestFile, mxUtils.bind(this, function()
{
this.lastModified = this.file.getLastModifiedDate();
this.updateStatus();
}));
}
}
}), mxUtils.bind(this, function()
{
this.reloadThread = null;
}));
}), (count + 1) * this.file.optimisticSyncDelay);
}
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
* Immediate is passed through to scheduleCleanup.
*/
DrawioFileSync.prototype.fileChangedNotify = function(data, immediate)
{
if (this.isValidState())
{
EditorUi.debug('DrawioFileSync.fileChangedNotify', [this],
'data', [data], 'immediate', immediate,
'saving', this.file.savingFile);
if (this.file.savingFile)
{
this.remoteFileChanged = true;
}
else
{
if (data != null && data.type == 'optimistic')
{
this.optimisticSync();
}
else
{
// It's possible that a request never returns so override
// existing requests and abort them when they are active
var thread = this.fileChanged(mxUtils.bind(this, function(err)
{
this.updateStatus();
}), mxUtils.bind(this, function(err)
{
this.file.handleFileError(err);
}), mxUtils.bind(this, function()
{
return !this.file.savingFile && this.notifyThread != thread;
}), true, immediate);
}
}
}
};
/**
* Called after the file was changed locally to mark the file as changed.
*/
DrawioFileSync.prototype.localFileChanged = function()
{
if (this.file.isRealtime())
{
window.clearTimeout(this.triggerSendThread);
this.localFileWasChanged = true;
this.scheduleCleanup(true);
this.triggerSendThread = window.setTimeout(mxUtils.bind(this, function()
{
this.sendLocalChanges();
}), Math.min(this.file.autosaveDelay, this.syncSendMessageDelay - 20));
}
};
/**
* Sends the given changes too all collaborators.
*/
DrawioFileSync.prototype.doSendLocalChanges = function(changes)
{
if (!this.file.ignorePatches(changes))
{
var changeId = this.clientId + '.' + (this.syncChangeCounter++);
var msg = this.createMessage({a: 'change', c: changes,
id: changeId, t: Date.now()});
var skipped = false;
if (this.p2pCollab != null)
{
this.p2pCollab.sendDiff(msg);
}
else if (urlParams['dev'] == '1')
{
var data = encodeURIComponent(this.objectToString(msg));
if (this.maxSyncMessageSize == 0 ||
data.length < this.maxSyncMessageSize)
{
mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() + '&msg=' + data);
}
else
{
skipped = true;
}
}
else
{
skipped = true;
}
EditorUi.debug('DrawioFileSync.doSendLocalChanges', [this],
'changes', changes, skipped ? '(skipped)' : '');
}
};
/**
* Handles the given remote changes.
*/
DrawioFileSync.prototype.receiveRemoteChanges = function(data)
{
var changes = data.c;
if (!this.file.ignorePatches(changes))
{
if (this.receivedData == null)
{
this.receivedData = [data];
window.setTimeout(mxUtils.bind(this, function()
{
if (this.ui.getCurrentFile() == this.file)
{
// Skips additional processing for single change
if (this.receivedData.length == 1)
{
this.doReceiveRemoteChanges(this.receivedData[0].c);
}
else
{
// Sorts by sender and remote counter
this.receivedData.sort(function(a, b)
{
if (a.id < b.id)
{
return -1;
}
else if (a.id > b.id)
{
return 1;
}
else
{
return 0;
}
});
var lastDiff = null;
// Processes changes
for (var i = 0; i < this.receivedData.length; i++)
{
// Ignores consecutive duplicates
var currentDiff = JSON.stringify(this.receivedData[i].c);
if (currentDiff != lastDiff)
{
this.doReceiveRemoteChanges(this.receivedData[i].c);
}
lastDiff = currentDiff;
}
}
}
this.receivedData = null;
}), this.syncReceiveMessageDelay);
}
else
{
this.receivedData.push(data);
}
}
};
/**
* Schedules a new cleanup if not lazy or one is pending
*/
DrawioFileSync.prototype.scheduleCleanup = function(lazy)
{
var delay = (lazy == false) ? 0 : this.cleanupDelay;
var prev = this.cleanupThread;
if (lazy != true || this.cleanupThread != null)
{
window.clearTimeout(this.cleanupThread);
this.cleanupThread = window.setTimeout(mxUtils.bind(this, function()
{
this.cleanup(null, mxUtils.bind(this, function(err)
{
this.file.handleFileError(err);
}));
}), delay);
}
EditorUi.debug('DrawioFileSync.scheduleCleanup', [this],
'lazy', lazy, 'delay', delay, 'prev', prev,
'thread', this.cleanupThread);
};
/**
* Removes remote changes that have not been saved and merges
* the latest version of the file if checkFile is true.
*/
DrawioFileSync.prototype.cleanup = function(success, error, checkFile)
{
var thread = this.cleanupThread;
window.clearTimeout(this.cleanupThread);
this.cleanupThread = null;
if (this.isValidState() && !this.file.inConflictState &&
this.file.isRealtime() && !this.file.isModified())
{
var patches = [this.ui.diffPages(this.ui.pages,
this.file.ownPages)];
this.file.theirPages = this.ui.clonePages(
this.file.ownPages);
if (urlParams['test'] == '1')
{
EditorUi.debug('DrawioFileSync.cleanup',
[this], 'thread', thread, 'patches', patches,
'checkFile', checkFile, 'checksum',
this.ui.getHashValueForPages(this.ui.pages));
}
if (!this.file.ignorePatches(patches))
{
this.file.patch(patches);
}
if (!checkFile)
{
if (!document.hidden && urlParams['test'] == '1' &&
urlParams['checksum'] == '1')
{
this.testChecksum();
}
if (success != null)
{
success();
}
}
else
{
this.file.getLatestVersion(mxUtils.bind(this, function(newFile)
{
try
{
if (this.isValidState() && !this.file.inConflictState &&
this.file.isRealtime())
{
var pages = newFile.getShadowPages();
patches = [this.ui.diffPages(this.ui.pages, pages),
this.ui.diffPages(pages, this.file.ownPages)];
if (!this.file.ignorePatches(patches))
{
this.file.patch(patches);
}
EditorUi.debug('DrawioFileSync.cleanup',
[this], 'newFile', newFile,
'patches', patches);
}
if (success != null)
{
success();
}
}
catch (e)
{
if (error != null)
{
error(e);
}
}
}), error);
}
}
else if (success != null)
{
success();
EditorUi.debug('DrawioFileSync.cleanup',
[this], 'checkFile', checkFile,
'modified', this.file.isModified());
}
};
/**
* Extracts local changes by diffing remote pages and patched remote pages.
*/
DrawioFileSync.prototype.testChecksum = function()
{
var localChecksum = this.ui.getHashValueForPages(this.ui.pages);
var localRev = this.file.getCurrentRevisionId();
this.file.getLatestVersion(mxUtils.bind(this, function(latestFile)
{
if (!document.hidden)
{
var remoteChecksum = this.ui.getHashValueForPages(
latestFile.getShadowPages());
var descChecksum = latestFile.getDescriptorChecksum(
latestFile.getDescriptor());
var remoteRev = latestFile.getCurrentRevisionId();
EditorUi.debug('DrawioFileSync.testChecksum',
'local', [this.file], 'modified', this.file.isModified(),
'inConflictState', this.file.inConflictState,
'autosaveThread', this.file.autosaveThread,
'savingFile', this.file.savingFile,
'localFileWasChanged', this.localFileWasChanged,
'remoteFileChanged', this.remoteFileChanged,
'cleanup', this.cleanupThread,
'checksum', localChecksum);
EditorUi.debug('DrawioFileSync.testChecksum',
'remote', [latestFile],
'rev', remoteRev == localRev,
'desc', descChecksum == remoteChecksum,
'checksum', remoteChecksum);
if (remoteChecksum != localChecksum)
{
EditorUi.debug('DrawioFileSync.testChecksum',
[this], 'checksums do not match');
this.ui.alert('Checksums do not match');
}
else
{
EditorUi.debug('DrawioFileSync.testChecksum',
[this], 'checksums match');
}
}
}), mxUtils.bind(this, function(err)
{
EditorUi.debug('DrawioFileSync.testChecksum',
[this], 'checksum test error', err);
}));
};
/**
* Extracts local changes by diffing remote pages and patched remote pages.
*/
DrawioFileSync.prototype.extractLocal = function(patch)
{
return (mxUtils.isEmptyObject(patch)) ? {} : this.ui.diffPages(
this.file.theirPages, this.ui.patchPages(this.ui.clonePages(
this.file.theirPages), patch));
};
/**
* Extracts remove operations for pages and cells from the given patch.
*/
DrawioFileSync.prototype.extractRemove = function(patch)
{
var result = {};
if (patch[EditorUi.DIFF_REMOVE] != null)
{
result[EditorUi.DIFF_REMOVE] =
patch[EditorUi.DIFF_REMOVE];
}
if (patch[EditorUi.DIFF_UPDATE] != null)
{
for (var id in patch[EditorUi.DIFF_UPDATE])
{
var diff = patch[EditorUi.DIFF_UPDATE][id];
if (diff.cells != null && diff.cells
[EditorUi.DIFF_REMOVE] != null)
{
if (result[EditorUi.DIFF_UPDATE] == null)
{
result[EditorUi.DIFF_UPDATE] = {};
}
result[EditorUi.DIFF_UPDATE][id] = {};
var temp = result[EditorUi.DIFF_UPDATE][id];
temp.cells = {};
temp.cells[EditorUi.DIFF_REMOVE] =
diff.cells[EditorUi.DIFF_REMOVE];
}
}
}
return result;
};
/**
* Updates the realtime models and saves pending local changes.
* Immediate is passed through to scheduleCleanup.
*/
DrawioFileSync.prototype.patchRealtime = function(patches, backup, own, immediate)
{
var all = null;
if (this.file.isRealtime())
{
// Gets pending changes that must be saved after remote
// changes are applied, ie. local remove of remote shape.
// TODO: Currently only implemented for pending removes as
// remote changes are not received in the order in which
// they are finally saved in the file.
all = this.extractRemove(this.ui.diffPages(
this.file.getShadowPages(), this.ui.pages));
var local = this.extractRemove(this.extractLocal(all));
// Applies incoming, own and local changes to own pages
var applied = ((own == null) ? patches :
patches.concat(own)).concat([local]);
this.file.ownPages = this.ui.applyPatches(
this.file.ownPages, applied, true,
backup);
// Triggers a file change to save pending local
// changes or updates the UI and schedules a
// cleanup with no pending local changes.
if (!mxUtils.isEmptyObject(local))
{
this.file.fileChanged(false);
}
else
{
this.scheduleCleanup((immediate != null) ?
false : null);
}
EditorUi.debug('DrawioFileSync.patchRealtime', [this],
'patches', patches, 'backup', backup, 'own', own,
'all', all, 'local', local, 'applied', applied,
'immediate', immediate);
}
return all;
};
/**
* Computes and sends the local changes if the file was changed.
*/
DrawioFileSync.prototype.isRealtimeActive = function()
{
return this.ui.editor.autosave;
};
/**
* Computes and sends the local changes if the file was changed.
*/
DrawioFileSync.prototype.sendLocalChanges = function()
{
try
{
if (this.file.isRealtime() && this.localFileWasChanged)
{
var snapshot = this.ui.clonePages(this.ui.pages);
var patch = this.ui.diffPages(this.snapshot, snapshot);
this.file.ownPages = this.ui.patchPages(
this.file.ownPages, patch, true);
this.snapshot = snapshot;
// Creates patch for cross references
var resolve = this.ui.resolveCrossReferences(
patch, this.ui.diffPages(this.file.ownPages,
this.ui.pages));
// Patches own pages to resolve cross references
this.file.ownPages = this.ui.patchPages(
this.file.ownPages, resolve, true);
if (this.isRealtimeActive())
{
this.doSendLocalChanges([resolve, patch]);
}
}
this.localFileWasChanged = false;
}
catch (e)
{
var user = this.file.getCurrentUser();
var uid = (user != null) ? user.id : 'unknown';
EditorUi.logError('Error in sendLocalChanges', null,
this.file.getMode() + '.' +
this.file.getId(), uid, e);
}
};
/**
* Sends the given changes too all collaborators.
*/
DrawioFileSync.prototype.doReceiveRemoteChanges = function(changes)
{
if (this.file.isRealtime() && this.isRealtimeActive())
{
this.sendLocalChanges();
this.file.patch(changes);
this.file.theirPages = this.ui.applyPatches(
this.file.theirPages, changes);
this.scheduleCleanup();
EditorUi.debug('DrawioFileSync.doReceiveRemoteChanges',
[this], 'changes', changes);
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
* Immediate is passed through to scheduleCleanup.
*/
DrawioFileSync.prototype.merge = function(patches, checksum, desc, success, error, abort, immediate)
{
try
{
this.file.stats.merged++;
this.lastModified = new Date();
var target = this.file.getDescriptorRevisionId(desc);
var ignored = this.file.ignorePatches(patches);
if (!ignored)
{
this.sendLocalChanges();
// Creates a patch for backup if the checksum fails
var shadow = this.ui.clonePages(this.file.getShadowPages());
this.file.backupPatch = (this.file.isModified() &&
!this.file.isRealtime()) ? this.ui.diffPages(
shadow, this.ui.pages) : null;
var pending = (!this.file.isRealtime()) ? null :
this.ui.diffPages(shadow, this.file.ownPages);
shadow = this.ui.applyPatches(shadow, patches);
var current = (checksum == null) ? null :
this.ui.getHashValueForPages(shadow);
EditorUi.debug('DrawioFileSync.merge', [this], 'patches', patches,
'backup', this.file.backupPatch, 'pending', pending, 'checksum',
checksum, 'current', current, 'valid', checksum == current,
'attempt', this.catchupRetryCount, 'of', this.maxCatchupRetries,
'from', this.file.getCurrentRevisionId(), 'to', target,
'etag', this.file.getDescriptorEtag(desc),
'immediate', immediate);
// Compares the checksum
if (checksum != null && checksum != current)
{
// Logs checksum error
var logError = mxUtils.bind(this, function(failed)
{
try
{
var user = this.file.getCurrentUser();
var uid = (user != null) ? user.id : 'unknown';
var id = (this.file.getId() != '') ? this.file.getId() :
('(' + this.ui.hashValue(this.file.getTitle()) + ')');
var bytes = JSON.stringify(patches).length;
EditorUi.logError('Merge checksum fallback ' + (failed ?
'failed' : 'success') + ' ' + id, null,
this.file.getMode() + '.' + this.file.getId(),
'user_' + uid + '-client_' + this.clientId +
'-bytes_' + bytes + '-patches_' + patches.length +
'-size_' + this.file.getSize() +
((checksum != null) ? ('-expected_' + checksum) : '') +
((current != null) ? ('-current_' + current) : '') +
'-from_' + this.ui.hashValue(this.file.getCurrentRevisionId()) +
'-to_' + this.ui.hashValue(target));
}
catch (e)
{
// ignore
}
});
// Fallback to full reload with logging
this.reload(mxUtils.bind(this, function()
{
if (success != null)
{
success();
}
}), mxUtils.bind(this, function()
{
if (error != null)
{
error();
}
}), abort, null, immediate);
// Abnormal termination
return;
}
else
{
this.file.setShadowPages(shadow);
// Patches the current document and own pages
if (this.patchRealtime(patches, null, pending, immediate) == null)
{
this.file.patch(patches,
(DrawioFile.LAST_WRITE_WINS) ?
this.file.backupPatch : null);
}
// Logs successull patch
// try
// {
// var user = this.file.getCurrentUser();
// var uid = (user != null) ? user.id : 'unknown';
//
// EditorUi.logEvent({category: 'PATCH-SYNC-FILE-' + this.file.getHash(),
// action: uid + '-patches-' + patches.length + '-recvd-' +
// this.file.stats.bytesReceived + '-msgs-' + this.file.stats.msgReceived,
// label: this.clientId});
// }
// catch (e)
// {
// // ignore
// }
}
}
this.file.invalidChecksum = false;
this.file.inConflictState = false;
this.file.patchDescriptor(this.file.getDescriptor(), desc);
this.file.backupPatch = null;
if (success != null)
{
success(true);
}
}
catch (e)
{
this.file.inConflictState = true;
this.file.invalidChecksum = true;
this.file.descriptorChanged();
if (error != null)
{
error(e);
}
try
{
if (this.file.errorReportsEnabled)
{
var from = this.ui.hashValue(this.file.getCurrentRevisionId());
var to = this.ui.hashValue(target);
this.file.sendErrorReport('Error in merge',
'From: ' + from + '\nTo: ' + to +
'\nChecksum: ' + checksum +
'\nPatches:\n' + this.file.compressReportData(
JSON.stringify(patches, null, 2)), e);
}
else
{
var user = this.file.getCurrentUser();
var uid = (user != null) ? user.id : 'unknown';
EditorUi.logError('Error in merge', null,
this.file.getMode() + '.' +
this.file.getId(), uid, e);
}
}
catch (e2)
{
// ignore
}
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
* Immediate is passed through to scheduleCleanup.
*/
DrawioFileSync.prototype.fileChanged = function(success, error, abort, lazy, immediate)
{
var thread = window.setTimeout(mxUtils.bind(this, function()
{
if (abort == null || !abort())
{
EditorUi.debug('DrawioFileSync.fileChanged', [this],
'lazy', lazy, 'immediate', immediate,
'remoteFileChanged', this.remoteFileChanged,
'valid', this.isValidState());
if (!this.isValidState())
{
if (error != null)
{
error();
}
}
else
{
this.remoteFileChanged = false;
this.file.loadPatchDescriptor(mxUtils.bind(this, function(desc)
{
if (abort == null || !abort())
{
if (!this.isValidState())
{
if (error != null)
{
error();
}
}
else
{
this.catchup(desc, success, error, abort, immediate);
}
}
}), error);
}
}
}), (lazy) ? this.cacheReadyDelay : 0);
this.notifyThread = thread;
return thread;
};
/**
* Fast-forward to the current editor state.
*/
DrawioFileSync.prototype.fastForward = function(desc)
{
this.file.patchDescriptor(this.file.getDescriptor(), desc);
this.file.setShadowPages(this.ui.clonePages(this.ui.pages));
this.file.theirPages = this.ui.clonePages(this.ui.pages);
this.file.ownPages = this.ui.clonePages(this.ui.pages);
var thread = this.cleanupThread;
window.clearTimeout(this.cleanupThread);
this.cleanupThread = null;
if (urlParams['test'] == '1')
{
EditorUi.debug('DrawioFileSync.fastForward',
[this], 'desc', [desc], 'cleanup', thread, 'checksum',
this.ui.getHashValueForPages(this.ui.pages));
}
if (!document.hidden && urlParams['test'] == '1' &&
urlParams['checksum'] == '1' &&
this.cleanupThread == null)
{
this.testChecksum();
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.reloadDescriptor = function()
{
this.file.loadDescriptor(mxUtils.bind(this, function(desc)
{
if (desc != null)
{
// Forces data to be updated
this.file.setDescriptorRevisionId(desc,
this.file.getCurrentRevisionId());
this.updateDescriptor(desc);
this.fileChangedNotify();
}
else
{
this.file.inConflictState = true;
this.file.handleFileError();
}
}), mxUtils.bind(this, function(err)
{
this.file.inConflictState = true;
this.file.handleFileError(err);
}));
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.updateDescriptor = function(desc)
{
this.file.setDescriptor(desc);
this.file.descriptorChanged();
this.start();
};
/**
* Adds the listener for automatically saving the diagram for local changes.
* Immediate is passed through to scheduleCleanup.
*/
DrawioFileSync.prototype.catchup = function(desc, success, error, abort, immediate)
{
if (desc != null && (abort == null || !abort()))
{
var source = this.file.getCurrentRevisionId();
var target = this.file.getDescriptorRevisionId(desc);
EditorUi.debug('DrawioFileSync.catchup', [this],
'desc', [desc], 'from', source, 'to', target,
'immediate', immediate, 'valid',
this.isValidState());
if (source == target)
{
this.file.patchDescriptor(this.file.getDescriptor(), desc);
if (urlParams['test'] == '1')
{
EditorUi.debug('DrawioFileSync.catchup', [this],
'up to date', 'cleanup', this.cleanupThread,
'checksum', this.ui.getHashValueForPages(this.ui.pages));
}
if (!document.hidden && urlParams['test'] == '1' &&
urlParams['checksum'] == '1' &&
this.cleanupThread == null)
{
this.testChecksum();
}
if (success != null)
{
success(true);
}
}
else if (!this.isValidState())
{
if (error != null)
{
error();
}
}
else
{
var checksum = this.file.getDescriptorChecksum(desc)
var secret = this.file.getDescriptorSecret(desc);
if (checksum != null &&
checksum == this.ui.getHashValueForPages(this.ui.pages))
{
// Fast-forward to current state if checksum matches
this.fastForward(desc);
if (success != null)
{
success(true);
}
}
else if (!Editor.enableRealtimeCache || secret == null ||
urlParams['lockdown'] == '1')
{
this.reload(success, error, abort, null, immediate);
}
else
{
// Cache entry may not have been uploaded to cache before new
// file is visible to client so retry once after cache miss
var cacheReadyRetryCount = 0;
var failed = false;
var doCatchup = mxUtils.bind(this, function()
{
if (abort == null || !abort())
{
// Ignores patch if shadow has changed
if (source != this.file.getCurrentRevisionId())
{
if (success != null)
{
success(true);
}
}
else if (!this.isValidState())
{
if (error != null)
{
error();
}
}
else
{
this.scheduleCleanup(true);
var acceptResponse = true;
var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
{
acceptResponse = false;
this.reload(success, error, abort, null, immediate);
}), this.ui.timeout);
mxUtils.get(EditorUi.cacheUrl + '?id=' + encodeURIComponent(this.channelId) +
'&from=' + encodeURIComponent(source) + '&to=' + encodeURIComponent(target) +
((secret != null) ? '&secret=' + encodeURIComponent(secret) : ''),
mxUtils.bind(this, function(req)
{
this.file.stats.bytesReceived += req.getText().length;
window.clearTimeout(timeoutThread);
if (acceptResponse && (abort == null || !abort()))
{
// Ignores patch if shadow has changed
if (source != this.file.getCurrentRevisionId())
{
if (success != null)
{
success(true);
}
}
else if (!this.isValidState())
{
if (error != null)
{
error();
}
}
else
{
var checksum = null;
var temp = [];
EditorUi.debug('DrawioFileSync.doCatchup',
[this], 'request', [req], 'status', req.getStatus(),
'cacheReadyRetryCount', cacheReadyRetryCount,
'maxCacheReadyRetries', this.maxCacheReadyRetries);
if (req.getStatus() >= 200 && req.getStatus() <= 299 &&
req.getText().length > 0)
{
try
{
var result = JSON.parse(req.getText());
if (result != null && result.length > 0)
{
for (var i = 0; i < result.length; i++)
{
var value = this.stringToObject(result[i]);
if (value.v > DrawioFileSync.PROTOCOL)
{
failed = true;
temp = [];
break;
}
else if (value.v === DrawioFileSync.PROTOCOL &&
value.d != null)
{
checksum = value.d.checksum;
temp.push(value.d.patch);
}
else
{
failed = true;
temp = [];
break;
}
}
}
EditorUi.debug('DrawioFileSync.doCatchup', [this],
'response', [result], 'status',
(failed ? 'failed' : 'ok'),
'temp', temp, 'checksum', checksum);
}
catch (e)
{
temp = [];
if (window.console != null && urlParams['test'] == '1')
{
console.log(e);
}
}
}
try
{
if (temp.length > 0)
{
this.file.stats.cacheHits++;
this.merge(temp, checksum, desc,
success, error, abort, immediate);
}
// Retries if cache entry was not yet there
else if (cacheReadyRetryCount <= this.maxCacheReadyRetries - 1 &&
!failed && req.getStatus() != 401 && req.getStatus() != 503 &&
req.getStatus() != 410)
{
cacheReadyRetryCount++;
this.file.stats.cacheMiss++;
window.setTimeout(doCatchup, (cacheReadyRetryCount + 1) *
this.cacheReadyDelay);
}
else
{
this.file.stats.cacheFail++;
this.reload(success, error, abort, null, immediate);
}
}
catch (e)
{
if (error != null)
{
error(e);
}
}
}
}
}), error);
}
}
});
window.setTimeout(doCatchup, this.cacheReadyDelay);
}
}
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
* Immediate is passed through to scheduleCleanup.
*/
DrawioFileSync.prototype.reload = function(success, error, abort, shadow, immediate)
{
EditorUi.debug('DrawioFileSync.reload',
[this], 'immediate', immediate);
this.file.updateFile(mxUtils.bind(this, function()
{
this.lastModified = this.file.getLastModifiedDate();
this.updateStatus();
this.start();
if (success != null)
{
success();
}
}), mxUtils.bind(this, function(err)
{
if (error != null)
{
error(err);
}
}), abort, shadow, immediate);
};
/**
* Invokes when the file descriptor was changed.
*/
DrawioFileSync.prototype.descriptorChanged = function(source)
{
this.lastModified = this.file.getLastModifiedDate();
if (this.channelId != null)
{
var msg = this.objectToString(this.createMessage({a: 'desc',
m: this.lastModified.getTime()}));
var target = this.file.getCurrentRevisionId();
var data = this.objectToString({});
mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
'&from=' + encodeURIComponent(source) + '&to=' + encodeURIComponent(target) +
'&msg=' + encodeURIComponent(msg) + '&data=' + encodeURIComponent(data));
this.file.stats.bytesSent += data.length;
this.file.stats.msgSent++;
EditorUi.debug('DrawioFileSync.descriptorChanged',
[this], 'from', source, 'to', target);
}
this.updateStatus();
};
/**
* Converts the given object to an encrypted string.
*/
DrawioFileSync.prototype.objectToString = function(obj)
{
var data = Graph.compress(JSON.stringify(obj));
if (this.key != null && typeof CryptoJS !== 'undefined')
{
data = CryptoJS.AES.encrypt(data, this.key).toString();
}
return data;
};
/**
* Converts the given encrypted string to an object.
*/
DrawioFileSync.prototype.stringToObject = function(data)
{
if (this.key != null && typeof CryptoJS !== 'undefined')
{
data = CryptoJS.AES.decrypt(data, this.key).toString(CryptoJS.enc.Utf8);
}
return JSON.parse(Graph.decompress(data));
};
/**
* Requests a token for the given sec
*/
DrawioFileSync.prototype.createToken = function(secret, success, error)
{
var acceptResponse = true;
var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
{
acceptResponse = false;
error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')});
}), this.ui.timeout);
mxUtils.get(EditorUi.cacheUrl + '?id=' + encodeURIComponent(this.channelId) +
'&secret=' + encodeURIComponent(secret), mxUtils.bind(this, function(req)
{
window.clearTimeout(timeoutThread);
if (acceptResponse)
{
if (req.getStatus() >= 200 && req.getStatus() <= 299)
{
success(req.getText());
}
else
{
error({code: req.getStatus(), message: 'Token Error ' + req.getStatus()});
}
}
}), error);
};
/**
* Invoked when a save request for a file was sent regardless of the response.
*/
DrawioFileSync.prototype.fileSaving = function()
{
if (this.file.isOptimisticSync())
{
this.notify(this.createMessage({
m: Date.now(), type: 'optimistic'}));
}
EditorUi.debug('DrawioFileSync.fileSaving', [this],
'optimistic', this.file.isOptimisticSync());
};
/**
* Invoked when the file data was updated for saving.
*/
DrawioFileSync.prototype.fileDataUpdated = function()
{
this.scheduleCleanup(true);
EditorUi.debug('DrawioFileSync.fileDataUpdated', [this]);
};
/**
* Invoked after a file was saved to add cache entry (which in turn notifies
* collaborators).
*/
DrawioFileSync.prototype.fileSaved = function(pages, lastDesc, success, error, token, checksum)
{
this.lastModified = this.file.getLastModifiedDate();
this.resetUpdateStatusThread();
this.catchupRetryCount = 0;
if (!this.ui.isOffline(true) && !this.file.inConflictState &&
!this.file.redirectDialogShowing)
{
this.start();
if (this.channelId != null)
{
// Computes diff and checksum
var secret = this.file.getDescriptorSecret(this.file.getDescriptor());
var msg = this.createMessage({m: this.lastModified.getTime()});
var source = this.file.getDescriptorRevisionId(lastDesc);
var target = this.file.getCurrentRevisionId();
if (secret == null || token == null ||
urlParams['lockdown'] == '1' ||
!Editor.enableRealtimeCache)
{
this.notify(msg);
if (success != null)
{
success();
}
EditorUi.debug('DrawioFileSync.fileSaved', [this],
'from', source, 'to', target, 'etag',
this.file.getCurrentEtag());
}
else
{
var diff = this.ui.diffPages(this.file.getShadowPages(), pages);
var lastSecret = this.file.getDescriptorSecret(lastDesc);
checksum = (checksum != null) ? checksum : this.ui.getHashValueForPages(pages);
// Data is stored in cache and message is sent to all listeners
var data = this.objectToString(this.createMessage(
{patch: diff, checksum: checksum}));
this.file.stats.bytesSent += data.length;
this.file.stats.msgSent++;
var acceptResponse = true;
var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
{
acceptResponse = false;
error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')});
}), this.ui.timeout);
mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
'&from=' + encodeURIComponent(source) + '&to=' + encodeURIComponent(target) +
(!Editor.p2pSyncNotify ? '&msg=' + encodeURIComponent(this.objectToString(msg)) : '') +
((secret != null) ? '&secret=' + encodeURIComponent(secret) : '') +
((lastSecret != null) ? '&last-secret=' + encodeURIComponent(lastSecret) : '') +
((data.length < this.maxCacheEntrySize) ? '&data=' + encodeURIComponent(data) : '') +
((token != null) ? '&token=' + encodeURIComponent(token) : ''),
mxUtils.bind(this, function(req)
{
window.clearTimeout(timeoutThread);
if (acceptResponse)
{
if (req.getStatus() >= 200 && req.getStatus() <= 299)
{
if (Editor.p2pSyncNotify)
{
this.notify(msg);
}
if (success != null)
{
success();
}
}
else
{
error({message: mxResources.get('realtimeCollaboration') +
((req.getStatus() != 0) ? ': ' + req.getStatus() : '')});
}
}
}));
EditorUi.debug('DrawioFileSync.fileSaved', [this],
'from', source, 'to', target, 'etag',
this.file.getCurrentEtag(), 'diff',
diff, data.length, 'bytes',
'checksum', checksum);
}
// Logs successull diff
// try
// {
// var user = this.file.getCurrentUser();
// var uid = (user != null) ? user.id : 'unknown';
//
// EditorUi.logEvent({category: 'DIFF-SYNC-FILE-' + this.file.getHash(),
// action: uid + '-diff-' + data.length + '-sent-' +
// this.file.stats.bytesSent + '-msgs-' +
// this.file.stats.msgSent, label: this.clientId});
// }
// catch (e)
// {
// // ignore
// }
}
}
// Ignores cache response as clients
// load file if cache entry failed
this.file.setShadowPages(pages);
this.scheduleCleanup();
};
/**
* Creates the properties for the file descriptor.
*/
DrawioFileSync.prototype.getIdParameters = function()
{
var result = 'id=' + this.channelId;
if (this.pusher != null && this.pusher.connection != null &&
this.pusher.connection.socket_id != null)
{
result += '&sid=' + this.pusher.connection.socket_id;
}
return result;
};
/**
* Creates the properties for the file descriptor.
*/
DrawioFileSync.prototype.createMessage = function(data)
{
return {v: DrawioFileSync.PROTOCOL, d: data, c: this.clientId};
};
/**
* Creates the properties for the file descriptor.
*/
DrawioFileSync.prototype.fileConflict = function(desc, success, error)
{
this.catchupRetryCount++;
EditorUi.debug('DrawioFileSync.fileConflict', [this], 'desc', [desc],
'catchupRetryCount', this.catchupRetryCount,
'maxCatchupRetries', this.maxCatchupRetries);
if (this.catchupRetryCount < this.maxCatchupRetries)
{
this.file.stats.conflicts++;
if (desc != null)
{
this.catchup(desc, success, error);
}
else
{
this.fileChanged(success, error);
}
}
else
{
this.file.stats.timeouts++;
this.catchupRetryCount = 0;
if (error != null)
{
error({code: App.ERROR_TIMEOUT, message: mxResources.get('timeout')});
}
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.stop = function()
{
if (this.pusher != null)
{
EditorUi.debug('DrawioFileSync.stop', [this]);
if (this.pusher.connection != null)
{
this.pusher.connection.unbind('state_change', this.connectionListener);
this.pusher.connection.unbind('error', this.pusherErrorListener);
}
if (this.channel != null)
{
this.channel.unbind('changed', this.changeListener);
// See https://github.com/pusher/pusher-js/issues/75
// this.pusher.unsubscribe(this.channelId);
this.channel = null;
}
this.pusher.disconnect();
this.pusher = null;
if (this.p2pCollab != null)
{
this.p2pCollab.destroy();
this.p2pCollab = null;
}
}
else if (this.puller != null)
{
EditorUi.debug('DrawioFileSync.stop (Pulling)', [this]);
this.puller.stop();
this.puller = null;
}
this.updateOnlineState();
this.updateStatus();
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.destroy = function()
{
if (this.channelId != null)
{
var user = this.file.getCurrentUser();
var leave = {a: 'leave'};
if (user != null)
{
leave.name = encodeURIComponent(user.displayName);
leave.uid = user.id;
}
this.notify(this.createMessage(leave));
}
this.stop();
if (this.onlineListener != null)
{
mxEvent.removeListener(window, 'offline', this.onlineListener);
mxEvent.removeListener(window, 'online', this.onlineListener);
this.onlineListener = null;
}
if (this.autosaveListener != null)
{
this.ui.editor.addListener('autosaveChanged', this.autosaveListener);
this.autosaveListener = null;
}
if (this.visibleListener != null)
{
mxEvent.removeListener(document, 'visibilitychange', this.visibleListener);
this.visibleListener = null;
}
if (this.activityListener != null)
{
mxEvent.removeListener(document, (mxClient.IS_POINTER) ? 'pointermove' : 'mousemove', this.activityListener);
mxEvent.removeListener(document, 'keypress', this.activityListener);
mxEvent.removeListener(window, 'focus', this.activityListener);
if (!mxClient.IS_POINTER && mxClient.IS_TOUCH)
{
mxEvent.removeListener(document, 'touchstart', this.activityListener);
mxEvent.removeListener(document, 'touchmove', this.activityListener);
}
this.activityListener = null;
}
if (this.collaboratorsElement != null)
{
this.collaboratorsElement.parentNode.removeChild(this.collaboratorsElement);
this.collaboratorsElement = null;
}
// This is not needed now as stop already destroyed it
if (this.p2pCollab != null)
{
this.p2pCollab.destroy();
}
};