/**
* 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 = new Date();
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.fileChangedNotify();
}
});
mxEvent.addListener(window, 'online', this.onlineListener);
// 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 = new Date();
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 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)
{
var user = this.file.getCurrentUser();
var join = {a: 'join'};
if (user != null)
{
join.name = user.displayName;
join.uid = user.id;
}
mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
'&msg=' + encodeURIComponent(this.objectToString(
this.createMessage(join))));
this.file.stats.msgSent++;
this.announced = true;
}
// Catchup on any lost edits
this.fileChangedNotify();
}
});
// Listens to remove messages
this.changeListener = mxUtils.bind(this, function(data)
{
this.file.stats.msgReceived++;
this.lastActivity = new Date();
if (this.enabled && !this.file.inConflictState &&
!this.file.redirectDialogShowing)
{
try
{
var msg = this.stringToObject(data);
if (msg != null)
{
EditorUi.debug('Sync.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)
{
if (window.console != null && urlParams['test'] == '1')
{
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;
//Extends mxEventSource
mxUtils.extend(DrawioFileSync, mxEventSource);
/**
* Maximum size in bytes for cache values.
*/
DrawioFileSync.prototype.maxCacheEntrySize = 1000000;
/**
* Specifies if notifications should be sent and received for changes.
*/
DrawioFileSync.prototype.enabled = true;
/**
* True if a change event is fired for a remote change.
*/
DrawioFileSync.prototype.updateStatusInterval = 10000;
/**
* 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 = 2;
/**
* Specifies if descriptor change events should be ignored.
*/
DrawioFileSync.prototype.cacheReadyDelay = 500;
/**
* 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();
}
if (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('Sync.start', [this]);
}
catch (e)
{
// ignore
}
this.installListeners();
}
window.setTimeout(mxUtils.bind(this, function()
{
this.lastModified = this.file.getLastModifiedDate();
this.lastActivity = new Date();
this.resetUpdateStatusThread();
this.updateOnlineState();
this.updateStatus();
}, 0));
}
};
/**
* 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
{
return false;
}
};
/**
* Draw function for the collaborator list.
*/
DrawioFileSync.prototype.updateOnlineState = function()
{
var addClickHandler = mxUtils.bind(this, function(elt)
{
mxEvent.addListener(elt, 'click', mxUtils.bind(this, function(evt)
{
this.enabled = !this.enabled;
this.ui.updateButtonContainer();
this.resetUpdateStatusThread();
this.updateOnlineState();
this.updateStatus();
if (!this.file.inConflictState && this.enabled)
{
this.fileChangedNotify();
}
}));
});
if (uiTheme == 'min' && this.ui.buttonContainer != null)
{
if (this.collaboratorsElement == null)
{
var elt = document.createElement('a');
elt.className = 'geToolbarButton';
elt.style.cssText = 'display:inline-block;position:relative;box-sizing:border-box;margin-right:4px;cursor:pointer;float:left;';
elt.style.backgroundPosition = 'center center';
elt.style.backgroundRepeat = 'no-repeat';
elt.style.backgroundSize = '24px 24px';
elt.style.height = '24px';
elt.style.width = '24px';
addClickHandler(elt);
this.ui.buttonContainer.appendChild(elt);
this.collaboratorsElement = elt;
}
}
else if (this.ui.toolbarContainer != null)
{
if (this.collaboratorsElement == null)
{
var elt = document.createElement('a');
elt.className = 'geButton';
elt.style.position = 'absolute';
elt.style.display = 'inline-block';
elt.style.verticalAlign = 'bottom';
elt.style.color = '#666';
elt.style.top = '8px';
elt.style.right = (uiTheme != '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';
mxUtils.setOpacity(elt, 60);
if (uiTheme == 'dark')
{
elt.style.filter = 'invert(100%)';
}
// Prevents focus
mxEvent.addListener(elt, (mxClient.IS_POINTER) ? 'pointerdown' : 'mousedown',
mxUtils.bind(this, function(evt)
{
evt.preventDefault();
}));
addClickHandler(elt);
this.ui.toolbarContainer.appendChild(elt);
this.collaboratorsElement = elt;
}
}
if (this.collaboratorsElement != null)
{
var status = '';
if (!this.enabled)
{
status = mxResources.get('disconnected');
}
else if (this.file.invalidChecksum)
{
status = mxResources.get('error') + ': ' + mxResources.get('checksum');
}
else if (this.ui.isOffline() || !this.isConnected())
{
status = mxResources.get('offline');
}
else
{
status = mxResources.get('online');
}
this.collaboratorsElement.setAttribute('title', status);
this.collaboratorsElement.style.backgroundImage = 'url(' + ((!this.enabled) ? Editor.syncDisabledImage :
((!this.ui.isOffline() && this.isConnected() && !this.file.invalidChecksum) ?
Editor.syncImage : Editor.syncProblemImage)) + ')';
}
};
/**
* Updates the status bar with the latest change.
*/
DrawioFileSync.prototype.updateStatus = function()
{
if (this.isConnected() && this.lastActivity != null &&
(new Date().getTime() - this.lastActivity.getTime()) / 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');
}
var history = this.file.isRevisionHistorySupported();
// Consumed and displays last message
var msg = this.lastMessage;
this.lastMessage = null;
if (msg != null && msg.length > 40)
{
msg = msg.substring(0, 40) + '...';
}
var label = mxResources.get('lastChange', [str]);
this.ui.editor.setStatus('
' + mxUtils.htmlEntities(label) + '
' +
((msg != null) ? ' (' + mxUtils.htmlEntities(msg) + ')' : '') +
(this.file.isEditable() ? '' : '' +
mxUtils.htmlEntities(mxResources.get('readOnly')) + '
') +
(this.isConnected() ? '' : '' +
mxUtils.htmlEntities(mxResources.get('disconnected')) + '
'));
var links = this.ui.statusContainer.getElementsByTagName('div');
if (links.length > 0 && history)
{
links[0].style.cursor = 'pointer';
links[0].style.textDecoration = 'underline';
mxEvent.addListener(links[0], 'click', mxUtils.bind(this, function()
{
this.ui.actions.get('revisionHistory').funct();
}));
}
// Fades in/out last message
var spans = this.ui.statusContainer.getElementsByTagName('span');
if (spans.length > 0)
{
var temp = spans[0];
mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 0.2s ease');
window.setTimeout(mxUtils.bind(this, function()
{
mxUtils.setOpacity(temp, 100);
mxUtils.setPrefixedStyle(temp.style, 'transition', 'all 1s ease');
window.setTimeout(mxUtils.bind(this, function()
{
mxUtils.setOpacity(temp, 0);
}), this.updateStatusInterval / 2);
}), 0);
}
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();
}), this.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.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', [data.name]);
this.resetUpdateStatusThread();
this.updateStatus();
}
}
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.fileChangedNotify = function()
{
if (this.isValidState())
{
if (this.file.savingFile)
{
this.remoteFileChanged = true;
}
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;
}));
}
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.fileChanged = function(success, error, abort)
{
var thread = window.setTimeout(mxUtils.bind(this, function()
{
if (abort == null || !abort())
{
if (!this.isValidState())
{
if (error != null)
{
error();
}
}
else
{
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);
}
}
}), error);
}
}
}), 0);
this.notifyThread = thread;
return thread;
};
/**
* 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.setDescriptorEtag(desc, this.file.getCurrentEtag());
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.
*/
DrawioFileSync.prototype.catchup = function(desc, success, error, abort)
{
if (abort == null || !abort())
{
var secret = this.file.getDescriptorSecret(desc);
var etag = this.file.getDescriptorEtag(desc);
var current = this.file.getCurrentEtag();
if (current == etag)
{
if (success != null)
{
success();
}
}
else if (!this.isValidState())
{
if (error != null)
{
error();
}
}
else
{
// Cache entry may not have been uploaded to cache before new
// etag 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 (current != this.file.getCurrentEtag())
{
if (success != null)
{
success();
}
}
else if (!this.isValidState())
{
if (error != null)
{
error();
}
}
else
{
var acceptResponse = true;
var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
{
acceptResponse = false;
this.reload(success, error, abort);
}), this.ui.timeout);
mxUtils.get(EditorUi.cacheUrl + '?id=' + encodeURIComponent(this.channelId) +
'&from=' + encodeURIComponent(current) + '&to=' + encodeURIComponent(etag) +
((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 (current != this.file.getCurrentEtag())
{
if (success != null)
{
success();
}
}
else if (!this.isValidState())
{
if (error != null)
{
error();
}
}
else
{
var checksum = null;
var temp = [];
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;
}
}
}
}
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);
}
// Retries if cache entry was not yet there
else if (cacheReadyRetryCount <= this.maxCacheReadyRetries &&
!failed && req.getStatus() != 401)
{
cacheReadyRetryCount++;
this.file.stats.cacheMiss++;
window.setTimeout(doCatchup, (cacheReadyRetryCount + 1) * this.cacheReadyDelay);
}
else
{
this.file.stats.cacheFail++;
this.reload(success, error, abort);
}
}
catch (e)
{
if (error != null)
{
error(e);
}
}
}
}
}));
}
}
});
window.setTimeout(doCatchup, this.cacheReadyDelay);
}
}
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.reload = function(success, error, abort, shadow)
{
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);
};
/**
* Adds the listener for automatically saving the diagram for local changes.
*/
DrawioFileSync.prototype.merge = function(patches, checksum, desc, success, error, abort)
{
try
{
this.file.stats.merged++;
this.lastModified = new Date();
this.file.shadowPages = (this.file.shadowPages != null) ?
this.file.shadowPages : this.ui.getPagesForNode(
mxUtils.parseXml(this.file.shadowData).documentElement)
// Creates a patch for backup if the checksum fails
this.file.backupPatch = (this.file.isModified()) ?
this.ui.diffPages(this.file.shadowPages,
this.ui.pages) : null;
var ignored = this.file.ignorePatches(patches);
var etag = this.file.getDescriptorEtag(desc);
if (!ignored)
{
// Patches the shadow document
for (var i = 0; i < patches.length; i++)
{
this.file.shadowPages = this.ui.patchPages(this.file.shadowPages, patches[i]);
}
var current = (checksum != null) ? this.ui.getHashValueForPages(this.file.shadowPages) : null;
if (urlParams['test'] == '1')
{
EditorUi.debug('Sync.merge', [this],
'from', this.file.getCurrentEtag(), 'to', etag,
'backup', this.file.backupPatch,
'attempt', this.catchupRetryCount,
'patches', patches,
'checksum', checksum == current, checksum);
}
// Compares the checksum
if (checksum != null && checksum != current)
{
var from = this.ui.hashValue(this.file.getCurrentEtag());
var to = this.ui.hashValue(etag);
this.file.checksumError(error, patches, 'From: ' + from + '\nTo: ' + to +
'\nChecksum: ' + checksum + '\nCurrent: ' + current, etag, 'merge');
// Uses current state as shadow to compute diff since
// shadowPages has been modified in-place above
// LATER: Check if fallback to reload is possible
// this.reload(success, error, abort, this.ui.pages);
// Abnormal termination
return;
}
else
{
// Patches the current document
this.file.patch(patches,
(DrawioFile.LAST_WRITE_WINS) ?
this.file.backupPatch : null);
}
}
this.file.invalidChecksum = false;
this.file.inConflictState = false;
this.file.patchDescriptor(this.file.getDescriptor(), desc);
this.file.backupPatch = null;
if (success != null)
{
success();
}
}
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.getCurrentEtag());
var to = this.ui.hashValue(etag);
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
}
}
};
/**
* Invokes after a file was saved to add cache entry (which in turn notifies
* collaborators).
*/
DrawioFileSync.prototype.descriptorChanged = function(etag)
{
this.lastModified = this.file.getLastModifiedDate();
if (this.channelId != null)
{
var msg = this.objectToString(this.createMessage({a: 'desc',
m: this.lastModified.getTime()}));
var current = this.file.getCurrentEtag();
var data = this.objectToString({});
mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
'&from=' + encodeURIComponent(etag) + '&to=' + encodeURIComponent(current) +
'&msg=' + encodeURIComponent(msg) + '&data=' + encodeURIComponent(data));
this.file.stats.bytesSent += data.length;
this.file.stats.msgSent++;
}
this.updateStatus();
};
/**
* Invokes after a file was saved to add cache entry (which in turn notifies
* collaborators).
*/
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;
};
/**
* Invokes after a file was saved to add cache entry (which in turn notifies
* collaborators).
*/
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));
};
/**
* Invokes after a file was saved to add cache entry (which in turn notifies
* collaborators).
*/
DrawioFileSync.prototype.fileSaved = function(pages, lastDesc, success, error)
{
this.lastModified = this.file.getLastModifiedDate();
this.resetUpdateStatusThread();
this.catchupRetryCount = 0;
if (!this.ui.isOffline() && !this.file.inConflictState && !this.file.redirectDialogShowing)
{
this.start();
if (this.channelId != null)
{
// Computes diff and checksum
var shadow = (this.file.shadowPages != null) ?
this.file.shadowPages : this.ui.getPagesForNode(
mxUtils.parseXml(this.file.shadowData).documentElement)
var checksum = this.ui.getHashValueForPages(pages);
var diff = this.ui.diffPages(shadow, pages);
// Data is stored in cache and message is sent to all listeners
var etag = this.file.getDescriptorEtag(lastDesc);
var current = this.file.getCurrentEtag();
var data = this.objectToString(this.createMessage({patch: diff, checksum: checksum}));
var msg = this.objectToString(this.createMessage({m: this.lastModified.getTime()}));
var secret = this.file.getDescriptorSecret(this.file.getDescriptor());
this.file.stats.bytesSent += data.length;
this.file.stats.msgSent++;
mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
'&from=' + encodeURIComponent(etag) + '&to=' + encodeURIComponent(current) +
'&msg=' + encodeURIComponent(msg) + ((secret != null) ? '&secret=' + encodeURIComponent(secret) : '') +
((data.length < this.maxCacheEntrySize) ? '&data=' + encodeURIComponent(data) : ''),
mxUtils.bind(this, function(req)
{
// Ignores response
}));
if (urlParams['test'] == '1')
{
EditorUi.debug('Sync.fileSaved', [this],
'from', etag, 'to', current, data.length,
'bytes', 'diff', diff, 'checksum', checksum);
}
}
}
this.file.shadowPages = pages;
if (success != null)
{
success();
}
};
/**
* 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++;
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({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('Sync.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;
}
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 = user.displayName;
leave.uid = user.id;
}
mxUtils.post(EditorUi.cacheUrl, this.getIdParameters() +
'&msg=' + encodeURIComponent(this.objectToString(
this.createMessage(leave))));
this.file.stats.msgSent++;
}
this.stop();
if (this.updateStatusThread != null)
{
window.clearInterval(this.updateStatusThread);
this.updateStatusThread = null;
}
if (this.onlineListener != null)
{
mxEvent.removeListener(window, 'online', this.onlineListener);
this.onlineListener = 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;
}
};