Last commit before realtime drop

This commit is contained in:
benweet 2014-04-12 23:30:18 +01:00
parent 3b30813e3d
commit 5f380b461a
8 changed files with 163 additions and 106 deletions

View File

@ -374,7 +374,7 @@ define([
if(conflictList.length) { if(conflictList.length) {
eventMgr.onMessage('"' + remoteTitle + '" has conflicts that you have to review.'); eventMgr.onMessage('"' + remoteTitle + '" has conflicts that you have to review.');
} }
}), this); }, this));
} }
// Return remote CRCs // Return remote CRCs

View File

@ -198,7 +198,7 @@ define([
// Refresh conversation if popover is open // Refresh conversation if popover is open
var context = currentContext; var context = currentContext;
if(context.discussionIndex) { if(context.discussionIndex) {
context.popoverElt.querySelector('.discussion-comment-list').innerHTML = getDiscussionComments(); context.getPopoverElt().querySelector('.discussion-comment-list').innerHTML = getDiscussionComments();
} }
try { try {
cssApplier.undoToRange(context.rangyRange); cssApplier.undoToRange(context.rangyRange);

View File

@ -47,7 +47,6 @@ define([
var $removeButtonElt = $(removeButtonElt); var $removeButtonElt = $(removeButtonElt);
var syncAttributes = fileDesc.syncLocations[$removeButtonElt.data('syncIndex')]; var syncAttributes = fileDesc.syncLocations[$removeButtonElt.data('syncIndex')];
$removeButtonElt.click(function() { $removeButtonElt.click(function() {
synchronizer.tryStopRealtimeSync();
fileDesc.removeSyncLocation(syncAttributes); fileDesc.removeSyncLocation(syncAttributes);
eventMgr.onSyncRemoved(fileDesc, syncAttributes); eventMgr.onSyncRemoved(fileDesc, syncAttributes);
}); });

View File

@ -562,6 +562,30 @@ define([
dataType: file.isRealtime ? 'json' : 'text', dataType: file.isRealtime ? 'json' : 'text',
timeout: constants.AJAX_TIMEOUT timeout: constants.AJAX_TIMEOUT
}).done(function(data) { }).done(function(data) {
if(file.isRealtime) {
data = data.data.value;
data = {
content: data.content.value,
discussionList: (function() {
var discussionList = {};
data.discussionList && _.each(data.discussionList.value, function(discussionObject) {
var discussion = {
discussionIndex: discussionObject.value.discussionIndex.json,
selectionStart: discussionObject.value.selectionStart.json,
selectionEnd: discussionObject.value.selectionEnd.json,
};
var type = (discussionObject.value.type || {}).json;
type && (discussion.type = type);
var commentList = (discussionObject.value.commentList || {}).value || [];
commentList.length && (discussion.commentList = commentList.map(function(commentObject) {
return commentObject.json;
}));
discussionList[discussion.discussionIndex] = discussion;
});
return discussionList;
})()
};
}
file.content = data; file.content = data;
objects.shift(); objects.shift();
task.chain(recursiveDownloadContent); task.chain(recursiveDownloadContent);

View File

@ -290,12 +290,32 @@ define([
// Realtime closure // Realtime closure
var realtimeContext; var realtimeContext;
(function() { (function() {
var inCompoundOperation = false;
function modelChangeWrapper(cb) {
realtimeContext.isChanging = true;
try { cb(); }
finally {
if(inCompoundOperation) {
try { realtimeContext.model.endCompoundOperation(); }
catch(e) {}
inCompoundOperation = false;
}
realtimeContext.isChanging = false;
}
}
function compound(cb) {
if(!inCompoundOperation) {
realtimeContext.model.beginCompoundOperation();
inCompoundOperation = true;
}
cb();
}
function toRealtimeDiscussion(context, discussion) { function toRealtimeDiscussion(context, discussion) {
var realtimeCommentList = context.model.createList(); var realtimeCommentList = context.model.createList();
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, modelEventListener); realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, onModelChange);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, modelEventListener); realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, onModelChange);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, modelEventListener); realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, onModelChange);
discussion.commentList && discussion.commentList.forEach(function(comment) { discussion.commentList && discussion.commentList.forEach(function(comment) {
realtimeCommentList.push({ realtimeCommentList.push({
author: comment.author, author: comment.author,
@ -322,6 +342,10 @@ define([
} }
function fromRealtimeDiscussion(realtimeDiscussion) { function fromRealtimeDiscussion(realtimeDiscussion) {
var realtimeCommentList = realtimeDiscussion.get('commentList');
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, onModelChange);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, onModelChange);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, onModelChange);
var discussion = { var discussion = {
discussionIndex: realtimeDiscussion.get('discussionIndex'), discussionIndex: realtimeDiscussion.get('discussionIndex'),
selectionStart: realtimeDiscussion.get('selectionStart'), selectionStart: realtimeDiscussion.get('selectionStart'),
@ -329,7 +353,7 @@ define([
}; };
var type = realtimeDiscussion.get('type'); var type = realtimeDiscussion.get('type');
type && (discussion.type = type); type && (discussion.type = type);
var commentList = realtimeDiscussion.get('commentList').asArray(); var commentList = realtimeCommentList.asArray();
commentList.length && (discussion.commentList = commentList); commentList.length && (discussion.commentList = commentList);
return discussion; return discussion;
} }
@ -344,12 +368,14 @@ define([
return localDiscussionList; return localDiscussionList;
} }
function mergeDiscussion(localDiscussion, realtimeDiscussion, isServerChange) { function mergeDiscussion(localDiscussion, realtimeDiscussion, isModelChange) {
var commentsChanged = false; var commentsChanged = false;
// We only pay attention to local selection modifications // We don't pay attention to model selection modifications
if(!isServerChange) { if(!isModelChange) {
realtimeDiscussion.set('selectionStart', localDiscussion.selectionStart); compound(function() {
realtimeDiscussion.set('selectionEnd', localDiscussion.selectionEnd); realtimeDiscussion.set('selectionStart', localDiscussion.selectionStart);
realtimeDiscussion.set('selectionEnd', localDiscussion.selectionEnd);
});
} }
function isInDiscussion(comment, commentList) { function isInDiscussion(comment, commentList) {
return commentList.some(function(commentInDiscussion) { return commentList.some(function(commentInDiscussion) {
@ -362,21 +388,25 @@ define([
var localCommentList = localDiscussion.commentList; var localCommentList = localDiscussion.commentList;
function checkLocalComment(comment, index) { function checkLocalComment(comment, index) {
if(!isInDiscussion(comment, realtimeCommentList.asArray())) { if(!isInDiscussion(comment, realtimeCommentList.asArray())) {
if(isServerChange) { if(isModelChange) {
localCommentList.splice(index, 1); localCommentList.splice(index, 1);
commentsChanged = true; commentsChanged = true;
return true; return true;
} }
else { else {
realtimeCommentList.push(comment); compound(function() {
realtimeCommentList.push(comment);
});
} }
} }
} }
while(localCommentList.some(checkLocalComment)) {} while(localCommentList.some(checkLocalComment)) {}
function checkRealtimeComment(comment, index) { function checkRealtimeComment(comment, index) {
if(!isInDiscussion(comment, localCommentList)) { if(!isInDiscussion(comment, localCommentList)) {
if(!isServerChange) { if(!isModelChange) {
realtimeCommentList.remove(index); compound(function() {
realtimeCommentList.remove(index);
});
return true; return true;
} }
else { else {
@ -389,17 +419,19 @@ define([
return commentsChanged; return commentsChanged;
} }
function mergeDiscussionList(context, isServerChange) { function mergeDiscussionList(context, isModelChange) {
var commentsChanged = false; var commentsChanged = false;
var localDiscussionList = context.fileDesc.discussionList; var localDiscussionList = context.fileDesc.discussionList;
_.values(localDiscussionList).forEach(function(localDiscussion) { _.values(localDiscussionList).forEach(function(localDiscussion) {
var realtimeDiscussion = context.realtimeDiscussionList.get(localDiscussion.discussionIndex); var realtimeDiscussion = context.realtimeDiscussionList.get(localDiscussion.discussionIndex);
if(realtimeDiscussion) { if(realtimeDiscussion) {
commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isServerChange); commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isModelChange);
} }
else if(!isServerChange) { else if(!isModelChange) {
realtimeDiscussion = toRealtimeDiscussion(context, localDiscussion); compound(function() {
context.realtimeDiscussionList.set(localDiscussion.discussionIndex, realtimeDiscussion); realtimeDiscussion = toRealtimeDiscussion(context, localDiscussion);
context.realtimeDiscussionList.set(localDiscussion.discussionIndex, realtimeDiscussion);
});
} }
else { else {
delete localDiscussionList[localDiscussion.discussionIndex]; delete localDiscussionList[localDiscussion.discussionIndex];
@ -410,15 +442,17 @@ define([
var realtimeDiscussion = context.realtimeDiscussionList.get(discussionIndex); var realtimeDiscussion = context.realtimeDiscussionList.get(discussionIndex);
var localDiscussion = localDiscussionList[discussionIndex]; var localDiscussion = localDiscussionList[discussionIndex];
if(localDiscussion) { if(localDiscussion) {
commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isServerChange); commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isModelChange);
} }
else if(isServerChange) { else if(isModelChange) {
var discussion = fromRealtimeDiscussion(realtimeDiscussion); var discussion = fromRealtimeDiscussion(realtimeDiscussion);
localDiscussionList[discussionIndex] = discussion; localDiscussionList[discussionIndex] = discussion;
eventMgr.onDiscussionCreated(context.fileDesc, discussion); eventMgr.onDiscussionCreated(context.fileDesc, discussion);
} }
else { else {
context.realtimeDiscussionList.delete(discussionIndex); compound(function() {
context.realtimeDiscussionList.delete(discussionIndex);
});
} }
}); });
context.fileDesc.discussionList = localDiscussionList; // Write in localStorage context.fileDesc.discussionList = localDiscussionList; // Write in localStorage
@ -451,59 +485,55 @@ define([
} }
var onChange = (function() { var onChange = (function() {
var debouncedOnChange = _.debounce(function() { var debouncedOnChange = utils.debounce(function() {
var context = realtimeContext; var context = realtimeContext;
if(!context) { if(!context) {
return; return;
} }
if(context.isServerChange) { if(context.isModelChange) {
logger.log('Realtime syncing remote changes'); logger.log('Realtime syncing model changes');
} }
else { else {
// Model is supposed to be updated on local modifications
context.model.beginCompoundOperation();
logger.log('Realtime syncing local changes'); logger.log('Realtime syncing local changes');
} }
// Check content modifications modelChangeWrapper(function() {
var localContent = context.fileDesc.content; // Check content modifications
var remoteContent = context.realtimeString.getText(); var localContent = context.fileDesc.content;
var contentChanged = localContent != remoteContent; var remoteContent = context.realtimeString.getText();
if(contentChanged) { var contentChanged = localContent != remoteContent;
if(context.isServerChange) { if(contentChanged) {
editor.setValue(remoteContent); if(context.isModelChange) {
editor.setValue(remoteContent);
}
else {
compound(function() {
context.realtimeString.setText(localContent);
});
}
} }
else {
context.realtimeString.setText(localContent); // Check discussion modifications
mergeDiscussionList(context, context.isModelChange);
// For local changes, CRCs are updated on "save success" event
if(context.isModelChange) {
updateStatus();
} }
} context.isModelChange = false;
});
// Check discussion modifications });
mergeDiscussionList(context, context.isServerChange);
// For local changes, CRCs are updated on "save success" event
if(context.isServerChange) {
updateStatus();
}
else {
context.model.endCompoundOperation();
}
context.isServerChange = false;
}, 0);
return function(fileDesc) { return function(fileDesc) {
if(realtimeContext && realtimeContext.fileDesc === fileDesc) { if(realtimeContext && realtimeContext.fileDesc === fileDesc) {
debouncedOnChange(); debouncedOnChange();
} }
}; };
})(); })();
function modelEventListener(evt) { function onModelChange() {
if(!realtimeContext) { if(!realtimeContext || realtimeContext.isChanging) {
return; return;
} }
if(evt.isLocal === false) { realtimeContext.isModelChange = true;
realtimeContext.isServerChange = true;
}
onChange(realtimeContext.fileDesc); onChange(realtimeContext.fileDesc);
} }
eventMgr.addListener('onContentChanged', onChange); eventMgr.addListener('onContentChanged', onChange);
@ -513,6 +543,9 @@ define([
// Start realtime synchronization // Start realtime synchronization
gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) { gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) {
if(realtimeContext !== undefined) {
return;
}
var context = { var context = {
fileDesc: fileDesc, fileDesc: fileDesc,
syncAttributes: syncAttributes syncAttributes: syncAttributes
@ -520,6 +553,9 @@ define([
realtimeContext = context; realtimeContext = context;
googleHelper.loadRealtime(syncAttributes.id, accountId, function(err, doc) { googleHelper.loadRealtime(syncAttributes.id, accountId, function(err, doc) {
if(err || !doc) { if(err || !doc) {
if(context === realtimeContext) {
realtimeContext = undefined;
}
return; return;
} }
@ -542,8 +578,8 @@ define([
} }
context.realtimeString = realtimeString; context.realtimeString = realtimeString;
// Listen to content modifications // Listen to content modifications
realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, modelEventListener); realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, onModelChange);
realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, modelEventListener); realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, onModelChange);
// Get or create discussion map // Get or create discussion map
var realtimeDiscussionList = model.getRoot().get('discussionList'); var realtimeDiscussionList = model.getRoot().get('discussionList');
@ -554,14 +590,14 @@ define([
} }
context.realtimeDiscussionList = realtimeDiscussionList; context.realtimeDiscussionList = realtimeDiscussionList;
// Listen to discussion modifications // Listen to discussion modifications
realtimeDiscussionList.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, modelEventListener); realtimeDiscussionList.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, onModelChange);
realtimeDiscussionList.keys().forEach(function(discussionIndex) { realtimeDiscussionList.keys().forEach(function(discussionIndex) {
var realtimeDiscussion = context.realtimeDiscussionList.get(discussionIndex); var realtimeDiscussion = context.realtimeDiscussionList.get(discussionIndex);
var realtimeCommentList = realtimeDiscussion.get('commentList'); var realtimeCommentList = realtimeDiscussion.get('commentList');
// Listen to comment modifications in every discussion // Listen to comment modifications in every discussion
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, modelEventListener); realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, onModelChange);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, modelEventListener); realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, onModelChange);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, modelEventListener); realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, onModelChange);
}); });
// Also listen to "save success" event // Also listen to "save success" event
@ -577,6 +613,7 @@ define([
var remoteDiscussionList = fromRealtimeDiscussionList(realtimeDiscussionList); var remoteDiscussionList = fromRealtimeDiscussionList(realtimeDiscussionList);
var remoteDiscussionListJSON = JSON.stringify(remoteDiscussionList); var remoteDiscussionListJSON = JSON.stringify(remoteDiscussionList);
gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON); gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON);
onChange(context.fileDesc);
// Save undo/redo buttons default actions // Save undo/redo buttons default actions
undoExecute = pagedownEditor.uiManager.buttons.undo.execute; undoExecute = pagedownEditor.uiManager.buttons.undo.execute;
@ -584,8 +621,8 @@ define([
setUndoRedoButtonStates = pagedownEditor.uiManager.setUndoRedoButtonStates; setUndoRedoButtonStates = pagedownEditor.uiManager.setUndoRedoButtonStates;
// Set temporary actions for undo/redo buttons // Set temporary actions for undo/redo buttons
pagedownEditor.uiManager.buttons.undo.execute = model.undo; pagedownEditor.uiManager.buttons.undo.execute = _.bind(model.undo, model);
pagedownEditor.uiManager.buttons.redo.execute = model.redo; pagedownEditor.uiManager.buttons.redo.execute = _.bind(model.redo, model);
// Add event handler for model's UndoRedoStateChanged events // Add event handler for model's UndoRedoStateChanged events
pagedownEditor.uiManager.setUndoRedoButtonStates = _.debounce(function() { pagedownEditor.uiManager.setUndoRedoButtonStates = _.debounce(function() {
@ -609,7 +646,7 @@ define([
gdriveProvider.stopRealtimeSync(); gdriveProvider.stopRealtimeSync();
} }
else if(err.isFatal) { else if(err.isFatal) {
eventMgr.onError('Real time synchronization is temporarily unavailable.'); // Retry will be attempted shortly...
gdriveProvider.stopRealtimeSync(); gdriveProvider.stopRealtimeSync();
} }
}); });
@ -619,6 +656,8 @@ define([
gdriveProvider.stopRealtimeSync = function() { gdriveProvider.stopRealtimeSync = function() {
logger.log("Stopping Google Drive realtime synchronization"); logger.log("Stopping Google Drive realtime synchronization");
if(realtimeContext !== undefined) { if(realtimeContext !== undefined) {
try { realtimeContext.model.endCompoundOperation(); }
catch(e) {}
realtimeContext.document && realtimeContext.document.close(); realtimeContext.document && realtimeContext.document.close();
realtimeContext = undefined; realtimeContext = undefined;
} }

View File

@ -63,8 +63,8 @@ define([
// Clean fields from deleted files in local storage // Clean fields from deleted files in local storage
Object.keys(storage).forEach(function(key) { Object.keys(storage).forEach(function(key) {
var match = key.match(/(publish\.\S+?)\.\S+/); var match = key.match(/publish\.\S+/);
if(match && !publishIndexMap.hasOwnProperty(match[1])) { if(match && !publishIndexMap.hasOwnProperty(match[0])) {
storage.removeItem(key); storage.removeItem(key);
} }
}); });

View File

@ -52,8 +52,8 @@ define([
// Clean fields from deleted files in local storage // Clean fields from deleted files in local storage
Object.keys(storage).forEach(function(key) { Object.keys(storage).forEach(function(key) {
var match = key.match(/(sync\.\S+?)\.\S+/); var match = key.match(/sync\.\S+/);
if(match && !syncIndexMap.hasOwnProperty(match[1])) { if(match && !syncIndexMap.hasOwnProperty(match[0])) {
storage.removeItem(key); storage.removeItem(key);
} }
}); });
@ -241,28 +241,16 @@ define([
* Realtime synchronization * Realtime synchronization
**************************************************************************/ **************************************************************************/
var realtimeFileDesc;
var realtimeSyncAttributes;
var isOnline = true; var isOnline = true;
// Determines if open file has real time sync location and tries to start
// real time sync
function onFileOpen(fileDesc) {
realtimeFileDesc = _.some(fileDesc.syncLocations, function(syncAttributes) {
realtimeSyncAttributes = syncAttributes;
return syncAttributes.isRealtime;
}) ? fileDesc : undefined;
tryStartRealtimeSync();
}
// Tries to start/stop real time sync on online/offline event // Tries to start/stop real time sync on online/offline event
function onOfflineChanged(isOfflineParam) { function onOfflineChanged(isOfflineParam) {
if(isOfflineParam === false) { if(isOfflineParam === false) {
isOnline = true; isOnline = true;
tryStartRealtimeSync(); startRealtimeSync();
} }
else { else {
synchronizer.tryStopRealtimeSync(); stopRealtimeSync();
isOnline = false; isOnline = false;
} }
} }
@ -270,24 +258,36 @@ define([
// Starts real time synchronization if: // Starts real time synchronization if:
// 1. current file has real time sync location // 1. current file has real time sync location
// 2. we are online // 2. we are online
function tryStartRealtimeSync() { function startRealtimeSync() {
if(realtimeFileDesc !== undefined && isOnline === true) { var fileDesc = fileMgr.currentFile;
realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc, realtimeSyncAttributes); _.each(fileDesc.syncLocations, function(syncAttributes) {
} syncAttributes.isRealtime && syncAttributes.provider.startRealtimeSync(fileDesc, syncAttributes);
});
} }
// Stops previously started synchronization if any // Stops previously started synchronization if any
synchronizer.tryStopRealtimeSync = function() { function stopRealtimeSync() {
if(realtimeFileDesc !== undefined && isOnline === true) { _.each(providerMap, function(provider) {
realtimeSyncAttributes.provider.stopRealtimeSync(); provider.stopRealtimeSync && provider.stopRealtimeSync();
} });
}; }
// Triggers realtime synchronization from eventMgr events // Triggers realtime synchronization from eventMgr events
if(window.viewerMode === false) { if(window.viewerMode === false) {
eventMgr.addListener("onFileOpen", onFileOpen); // On file open, try to start realtime sync
eventMgr.addListener("onFileClosed", synchronizer.tryStopRealtimeSync); eventMgr.addListener("onFileOpen", startRealtimeSync);
// On new sync location, try to start realtime sync
eventMgr.addListener("onSyncExportSuccess", startRealtimeSync);
// On file close, stop any active realtime synchronization
eventMgr.addListener("onFileClosed", stopRealtimeSync);
// Start/stop realtime sync depending on network status
eventMgr.addListener("onOfflineChanged", onOfflineChanged); eventMgr.addListener("onOfflineChanged", onOfflineChanged);
// Try to start realtime sync every 15 sec in case of error
eventMgr.addListener("onPeriodicRun", _.throttle(startRealtimeSync, 15000));
// Stop realtime sync if synchronized location is removed
eventMgr.addListener("onSyncRemoved", function(fileDesc, syncAttributes) {
fileDesc === fileMgr.currentFile && syncAttributes.isRealtime && syncAttributes.provider.stopRealtimeSync();
});
} }
/*************************************************************************** /***************************************************************************
@ -368,11 +368,6 @@ define([
syncAttributes.isRealtime = true; syncAttributes.isRealtime = true;
fileDesc.addSyncLocation(syncAttributes); fileDesc.addSyncLocation(syncAttributes);
eventMgr.onSyncExportSuccess(fileDesc, syncAttributes); eventMgr.onSyncExportSuccess(fileDesc, syncAttributes);
// Start the real time sync
realtimeFileDesc = fileDesc;
realtimeSyncAttributes = syncAttributes;
tryStartRealtimeSync();
}); });
} }
else { else {

View File

@ -13,7 +13,7 @@ define([
// Faster than setTimeout (see http://dbaron.org/log/20100309-faster-timeouts) // Faster than setTimeout (see http://dbaron.org/log/20100309-faster-timeouts)
utils.defer = (function() { utils.defer = (function() {
var timeouts = []; var timeouts = [];
var messageName = "delay"; var messageName = "deferMsg";
window.addEventListener("message", function(evt) { window.addEventListener("message", function(evt) {
if(evt.source == window && evt.data == messageName) { if(evt.source == window && evt.data == messageName) {
evt.stopPropagation(); evt.stopPropagation();