support callbacks on sync request calls in PullBox OperationRouter (and remove 2sec close wait hack)

too bad this loses the nice singleTimeCallbackOnCompleteServerOpSpecsPlay var ;)
This commit is contained in:
Friedrich W. H. Kossebau 2013-09-02 02:29:04 +02:00
parent 880101b221
commit 64e8ea689b

View File

@ -57,27 +57,31 @@ define("webodf/editor/server/pullbox/OperationRouter", [], function () {
"use strict"; "use strict";
var operationFactory, var operationFactory,
singleTimeCallbackOnCompleteServerOpSpecsPlay,
/**@type{function(!ops.Operation)}*/ /**@type{function(!ops.Operation)}*/
playbackFunction, playbackFunction,
/**@type{?{active:!boolean}}*/ syncOpsTimeout = null,
pullingTimeOutFlag = null,
/**@type{!boolean}*/ /**@type{!boolean}*/
triggerPushingOpsActivated = false, isInstantSyncRequested = false,
/**@type{!boolean}*/ /**@type{!boolean}*/
playUnplayedServerOpSpecsTriggered = false, isPushingOpsTriggered = false,
/**@type{!boolean}*/ /**@type{!boolean}*/
syncLock = false, isPlayingUnplayedServerOpSpecs = false,
/**@type{!boolean}*/
isSyncCallRunning = false,
/**@type{!boolean}*/ /**@type{!boolean}*/
hasUnresolvableConflict = false, hasUnresolvableConflict = false,
/**@type{!boolean}*/ /**@type{!boolean}*/
syncingBlocked = false, syncingBlocked = false,
/** @type {!string} id of latest op stack state known on the server */ /** @type {!string} id of latest op stack state known on the server */
lastServerSeq = "", lastServerSeq = "",
/** @type {!Array.<!Function>} sync request callbacks created since the last sync call to the server */
syncRequestCallbacksQueue = [],
/** @type {!Array.<!Object>} ops created since the last sync call to the server */ /** @type {!Array.<!Object>} ops created since the last sync call to the server */
unsyncedClientOpspecQueue = [], unsyncedClientOpspecQueue = [],
/** @type {!Array.<!Object>} ops created since the last sync call to the server */ /** @type {!Array.<!Object>} ops already received from the server but not yet applied */
unplayedServerOpspecQueue = [], unplayedServerOpspecQueue = [],
/** @type {!Array.<!Function>} sync request callbacks which should be called after the received ops have been applied server */
uncalledSyncRequestCallbacksQueue = [],
/** @type {!Array.<!function(!boolean):undefined>} ops created since the last sync call to the server */ /** @type {!Array.<!function(!boolean):undefined>} ops created since the last sync call to the server */
hasLocalUnsyncedOpsStateSubscribers = [], hasLocalUnsyncedOpsStateSubscribers = [],
/**@type{!boolean}*/ /**@type{!boolean}*/
@ -149,9 +153,9 @@ runtime.log("Merged: from "+opspecs.length+" to "+result.length+" specs");
* @return {undefined} * @return {undefined}
*/ */
function doPlayUnplayedServerOpSpecs() { function doPlayUnplayedServerOpSpecs() {
var opspec, op, startTime; var opspec, op, startTime, i;
playUnplayedServerOpSpecsTriggered = false; isPlayingUnplayedServerOpSpecs = false;
// take start time // take start time
startTime = (new Date()).getTime(); startTime = (new Date()).getTime();
@ -178,19 +182,19 @@ runtime.log("Merged: from "+opspecs.length+" to "+result.length+" specs");
// still unplayed opspecs? // still unplayed opspecs?
if (unplayedServerOpspecQueue.length > 0) { if (unplayedServerOpspecQueue.length > 0) {
// let other events be handled. then continue // let other events be handled. then continue
playUnplayedServerOpSpecsTriggered = true; isPlayingUnplayedServerOpSpecs = true;
runtime.getWindow().setTimeout(doPlayUnplayedServerOpSpecs, 1); runtime.getWindow().setTimeout(doPlayUnplayedServerOpSpecs, 1);
} else { } else {
// This is such a sad hack. But there is no other way for now to inject // finally call all the callbacks waiting for that sync!
// the callback after the initial replay. for (i = 0; i < uncalledSyncRequestCallbacksQueue.length; i += 1) {
if (singleTimeCallbackOnCompleteServerOpSpecsPlay) { uncalledSyncRequestCallbacksQueue[i]();
singleTimeCallbackOnCompleteServerOpSpecsPlay();
singleTimeCallbackOnCompleteServerOpSpecsPlay = null;
} }
uncalledSyncRequestCallbacksQueue = [];
} }
} }
if (playUnplayedServerOpSpecsTriggered) { if (isPlayingUnplayedServerOpSpecs) {
return; return;
} }
doPlayUnplayedServerOpSpecs(); doPlayUnplayedServerOpSpecs();
@ -198,11 +202,13 @@ runtime.log("Merged: from "+opspecs.length+" to "+result.length+" specs");
/** /**
* @param {Array.<!Object>} opspecs * @param {Array.<!Object>} opspecs
* @param {Array.<!Function>} callbacks
* @return {undefined} * @return {undefined}
*/ */
function receiveOpSpecsFromNetwork(opspecs) { function receiveOpSpecsFromNetwork(opspecs, callbacks) {
// append to existing unplayed // append to existing unplayed
unplayedServerOpspecQueue = unplayedServerOpspecQueue.concat(opspecs); unplayedServerOpspecQueue = unplayedServerOpspecQueue.concat(opspecs);
uncalledSyncRequestCallbacksQueue = uncalledSyncRequestCallbacksQueue.concat(callbacks);
} }
/** /**
@ -245,27 +251,10 @@ runtime.log("Merged: from "+opspecs.length+" to "+result.length+" specs");
* @return {undefined} * @return {undefined}
*/ */
function syncOps() { function syncOps() {
function triggerPullingOps() { var syncedClientOpspecs,
var flag = {active: true}; syncRequestCallbacksArray;
// provide flag globally
pullingTimeOutFlag = flag;
runtime.getWindow().setTimeout(function() {
runtime.log("Pulling activated:" + flag.active);
// remove our flag
pullingTimeOutFlag = null;
if (flag.active) {
syncOps();
}
}, pullingIntervall);
}
/** if (isSyncCallRunning || hasUnresolvableConflict) {
* @return {undefined}
*/
function doSyncOps() {
var syncedClientOpspecs;
if (syncLock || hasUnresolvableConflict) {
return; return;
} }
// TODO: hack, remove // TODO: hack, remove
@ -273,11 +262,17 @@ runtime.log("Pulling activated:" + flag.active);
return; return;
} }
syncLock = true; // no more timeout or instant pull request in any case
syncOpsTimeout = null;
isInstantSyncRequested = false;
// set lock
isSyncCallRunning = true;
// take specs from queue, if any // take specs from queue, if any
syncedClientOpspecs = unsyncedClientOpspecQueue; syncedClientOpspecs = unsyncedClientOpspecQueue;
unsyncedClientOpspecQueue = []; unsyncedClientOpspecQueue = [];
syncRequestCallbacksArray = syncRequestCallbacksQueue;
syncRequestCallbacksQueue = [];
server.call({ server.call({
command: 'sync_ops', command: 'sync_ops',
@ -288,8 +283,7 @@ runtime.log("Pulling activated:" + flag.active);
client_ops: syncedClientOpspecs client_ops: syncedClientOpspecs
} }
}, function(responseData) { }, function(responseData) {
var shouldRetryInstantly = false, var response = /** @type{{result:string, head_seq:string, ops:Array.<!Object>}} */(runtime.fromJson(responseData));
response = /** @type{{result:string, head_seq:string, ops:Array.<!Object>}} */(runtime.fromJson(responseData));
// TODO: hack, remove // TODO: hack, remove
if (syncingBlocked) { if (syncingBlocked) {
@ -303,24 +297,27 @@ runtime.log("Pulling activated:" + flag.active);
if (response.ops.length > 0) { if (response.ops.length > 0) {
// no new locally in the meantime? // no new locally in the meantime?
if (unsyncedClientOpspecQueue.length === 0) { if (unsyncedClientOpspecQueue.length === 0) {
receiveOpSpecsFromNetwork(compressOpSpecs(response.ops)); receiveOpSpecsFromNetwork(compressOpSpecs(response.ops), syncRequestCallbacksArray);
} else { } else {
// transform server ops against new local ones and apply, // transform server ops against new local ones and apply,
// transform and send new local ops to server // transform and send new local ops to server
runtime.log("meh, have new ops locally meanwhile, have to do transformations."); runtime.log("meh, have new ops locally meanwhile, have to do transformations.");
hasUnresolvableConflict = !handleOpsSyncConflict(compressOpSpecs(response.ops)); hasUnresolvableConflict = !handleOpsSyncConflict(compressOpSpecs(response.ops));
syncRequestCallbacksQueue = syncRequestCallbacksArray.concat(syncRequestCallbacksQueue);
} }
// and note server state // and note server state
lastServerSeq = response.head_seq; lastServerSeq = response.head_seq;
} }
} else if (response.result === "added") { } else if (response.result === "added") {
runtime.log("All added to server"); runtime.log("All added to server");
receiveOpSpecsFromNetwork([], syncRequestCallbacksArray);
// note server state // note server state
lastServerSeq = response.head_seq; lastServerSeq = response.head_seq;
updateHasLocalUnsyncedOpsState(); updateHasLocalUnsyncedOpsState();
} else if (response.result === "conflict") { } else if (response.result === "conflict") {
// put the send ops back into the outgoing queue // put the send ops back into the outgoing queue
unsyncedClientOpspecQueue = syncedClientOpspecs.concat(unsyncedClientOpspecQueue); unsyncedClientOpspecQueue = syncedClientOpspecs.concat(unsyncedClientOpspecQueue);
syncRequestCallbacksQueue = syncRequestCallbacksArray.concat(syncRequestCallbacksQueue);
// transform server ops against new local ones and apply, // transform server ops against new local ones and apply,
// transform and request new send new local ops to server // transform and request new send new local ops to server
runtime.log("meh, server has new ops meanwhile, have to do transformations."); runtime.log("meh, server has new ops meanwhile, have to do transformations.");
@ -329,13 +326,14 @@ runtime.log("Pulling activated:" + flag.active);
lastServerSeq = response.head_seq; lastServerSeq = response.head_seq;
// try again instantly // try again instantly
if (!hasUnresolvableConflict) { if (!hasUnresolvableConflict) {
shouldRetryInstantly = true; isInstantSyncRequested = true;
} }
} else { } else {
runtime.assert(false, "Unexpected result on sync-ops call: "+response.result); runtime.assert(false, "Unexpected result on sync-ops call: "+response.result);
} }
syncLock = false; // unlock
isSyncCallRunning = false;
if (hasUnresolvableConflict) { if (hasUnresolvableConflict) {
// TODO: offer option to reload session automatically? // TODO: offer option to reload session automatically?
@ -345,46 +343,62 @@ runtime.log("Pulling activated:" + flag.active);
"Client disconnected from session, no further editing accepted.\n\n" + "Client disconnected from session, no further editing accepted.\n\n" +
"Please reconnect manually for now."); "Please reconnect manually for now.");
} else { } else {
if (shouldRetryInstantly) {
doSyncOps();
} else {
runtime.log("Preparing next: " + (unsyncedClientOpspecQueue.length === 0));
// prepare next sync // prepare next sync
// nothing to push right now? if (isInstantSyncRequested) {
if (unsyncedClientOpspecQueue.length === 0) { syncOps();
triggerPullingOps(); } else {
} syncOpsTimeout = runtime.getWindow().setTimeout(function() {
syncOpsTimeout = null;
syncOps();
}, (unsyncedClientOpspecQueue.length === 0) ? pullingIntervall : pushingIntervall);
} }
playUnplayedServerOpSpecs(); playUnplayedServerOpSpecs();
} }
}); });
} }
doSyncOps();
}
function triggerPushingOps() { function triggerPushingOps() {
if (syncLock || triggerPushingOpsActivated) { if (isSyncCallRunning || isPushingOpsTriggered) {
return; return;
} }
triggerPushingOpsActivated = true; isPushingOpsTriggered = true;
// disable current pulling timeout // disable current pulling timeout
if (pullingTimeOutFlag) { if (syncOpsTimeout) {
pullingTimeOutFlag.active = false; runtime.clearTimeout(syncOpsTimeout);
syncOpsTimeout = null;
} }
// TODO: how stupid! if the pulling timeout was close to done, this will extend it
// solution: split pulling into two timeouts, with second as short as pushing,
// and only cancel the first half
runtime.getWindow().setTimeout(function() { runtime.getWindow().setTimeout(function() {
runtime.log("Pushing activated"); runtime.log("Pushing activated");
triggerPushingOpsActivated = false; isPushingOpsTriggered = false;
syncOps(); syncOps();
}, pushingIntervall); }, pushingIntervall);
} }
this.requestReplay = function (done_cb) { /**
singleTimeCallbackOnCompleteServerOpSpecsPlay = done_cb; * @param {!Funtion} cb
* @return {undefined}
*/
function requestInstantOpsSync(cb) {
syncRequestCallbacksQueue.push(cb);
// disable current pulling timeout
if (syncOpsTimeout) {
runtime.clearTimeout(syncOpsTimeout);
syncOpsTimeout = null;
}
syncOps(); syncOps();
}; };
this.requestReplay = function (done_cb) {
requestInstantOpsSync(done_cb);
};
/** /**
* Sets the factory to use to create operation instances from operation specs. * Sets the factory to use to create operation instances from operation specs.
* *
@ -450,23 +464,24 @@ runtime.log("Pushing activated");
* A callback is called on success. * A callback is called on success.
*/ */
this.close = function (cb) { this.close = function (cb) {
function writeSessionStateToFile() {
function cbSuccess(fileData) { function cbSuccess(fileData) {
server.writeSessionStateToFile(sessionId, memberId, lastServerSeq, fileData, cb); server.writeSessionStateToFile(sessionId, memberId, lastServerSeq, fileData, cb);
};
odfContainer.createByteArray(cbSuccess, cb);
} }
// TODO: hack, rather add callback to syncOps for success and properly close things function doClose() {
syncOps();
runtime.getWindow().setTimeout(function() {
syncingBlocked = true; syncingBlocked = true;
if (hasPushedModificationOps) { if (hasPushedModificationOps) {
writeSessionStateToFile(); odfContainer.createByteArray(cbSuccess, cb);
} else { } else {
cb(); cb();
} }
}, 2000); }
if (hasLocalUnsyncedOps) {
requestInstantOpsSync(doClose);
} else {
doClose();
}
}; };
this.getHasLocalUnsyncedOpsAndUpdates = function (subscriber) { this.getHasLocalUnsyncedOpsAndUpdates = function (subscriber) {