mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-08-09 16:35:02 +02:00
W.I.P of collaborative odf editor:
- Add wodocollabtexteditor library
This commit is contained in:
593
api/js/webodf/collab/backend/pullbox/OperationRouter.js
Normal file
593
api/js/webodf/collab/backend/pullbox/OperationRouter.js
Normal file
@ -0,0 +1,593 @@
|
||||
/**
|
||||
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
|
||||
*
|
||||
* @licstart
|
||||
* This file is part of WebODF.
|
||||
*
|
||||
* WebODF is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License (GNU AGPL)
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* WebODF is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
|
||||
* @licend
|
||||
*
|
||||
* @source: http://www.webodf.org/
|
||||
* @source: https://github.com/kogmbh/WebODF/
|
||||
*/
|
||||
|
||||
/*global define, runtime, core, ops*/
|
||||
|
||||
define("webodf/editor/backend/pullbox/OperationRouter", [], function () {
|
||||
"use strict";
|
||||
|
||||
// TODO: these eventid strings should be defined at OperationRouter interface
|
||||
var /**@const @type {!string}*/
|
||||
EVENT_BEFORESAVETOFILE = "beforeSaveToFile",
|
||||
/**@const @type {!string}*/
|
||||
EVENT_SAVEDTOFILE = "savedToFile",
|
||||
/**@const @type {!string}*/
|
||||
EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED = "hasLocalUnsyncedOperationsChanged",
|
||||
/**@const @type {!string}*/
|
||||
EVENT_HASSESSIONHOSTCONNECTIONCHANGED = "hasSessionHostConnectionChanged";
|
||||
|
||||
runtime.loadClass("ops.OperationTransformer");
|
||||
runtime.loadClass("core.EventNotifier");
|
||||
|
||||
/**
|
||||
* route operations in a networked collaborative manner.
|
||||
*
|
||||
* incoming operations (from controller) are sent to a server,
|
||||
* who will distribute them.
|
||||
*
|
||||
* incoming operations (from the server are played on the DOM.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements ops.OperationRouter
|
||||
*/
|
||||
return function PullBoxOperationRouter(sessionId, memberId, server, odfContainer, errorCallback) {
|
||||
|
||||
var operationFactory,
|
||||
/**@type{function(!ops.Operation):boolean}*/
|
||||
playbackFunction,
|
||||
idleTimeout = null,
|
||||
syncOpsTimeout = null,
|
||||
/**@type{!boolean}*/
|
||||
isInstantSyncRequested = false,
|
||||
/**@type{!boolean}*/
|
||||
isPlayingUnplayedServerOpSpecs = false,
|
||||
/**@type{!boolean}*/
|
||||
isSyncCallRunning = false,
|
||||
/**@type{!boolean}*/
|
||||
hasError = false,
|
||||
/**@type{!boolean}*/
|
||||
syncingBlocked = false,
|
||||
/** @type {!string} id of latest op stack state known on the server */
|
||||
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 */
|
||||
unsyncedClientOpspecQueue = [],
|
||||
/** @type {!Array.<!Object>} ops already received from the server but not yet applied */
|
||||
unplayedServerOpspecQueue = [],
|
||||
/** @type {!Array.<!Function>} sync request callbacks which should be called after the received ops have been applied server */
|
||||
uncalledSyncRequestCallbacksQueue = [],
|
||||
/**@type{!boolean}*/
|
||||
hasLocalUnsyncedOps = false,
|
||||
/**@type{!boolean}*/
|
||||
hasSessionHostConnection = true,
|
||||
eventNotifier = new core.EventNotifier([
|
||||
EVENT_BEFORESAVETOFILE,
|
||||
EVENT_SAVEDTOFILE,
|
||||
EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED,
|
||||
EVENT_HASSESSIONHOSTCONNECTIONCHANGED,
|
||||
ops.OperationRouter.signalProcessingBatchStart,
|
||||
ops.OperationRouter.signalProcessingBatchEnd
|
||||
]),
|
||||
/**@type{!boolean} tells if any local ops have been modifying ops */
|
||||
hasPushedModificationOps = false,
|
||||
operationTransformer = new ops.OperationTransformer(),
|
||||
/**@const*/replayTime = 500,
|
||||
/**@const*/syncOpsDelay = 3000,
|
||||
/**@const*/idleDelay = 5000;
|
||||
|
||||
/**
|
||||
* @return {undefined}
|
||||
*/
|
||||
function updateHasLocalUnsyncedOpsState() {
|
||||
var hasLocalUnsyncedOpsNow = (unsyncedClientOpspecQueue.length > 0);
|
||||
|
||||
// no change?
|
||||
if (hasLocalUnsyncedOps === hasLocalUnsyncedOpsNow) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasLocalUnsyncedOps = hasLocalUnsyncedOpsNow;
|
||||
eventNotifier.emit(EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, hasLocalUnsyncedOps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!boolean} hasConnection
|
||||
* @return {undefined}
|
||||
*/
|
||||
function updateHasSessionHostConnectionState(hasConnection) {
|
||||
// no change?
|
||||
if (hasSessionHostConnection === hasConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasSessionHostConnection = hasConnection;
|
||||
eventNotifier.emit(EVENT_HASSESSIONHOSTCONNECTIONCHANGED, hasSessionHostConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {undefined}
|
||||
*/
|
||||
function playUnplayedServerOpSpecs() {
|
||||
/**
|
||||
* @return {undefined}
|
||||
*/
|
||||
function doPlayUnplayedServerOpSpecs() {
|
||||
var opspec, op, startTime, i;
|
||||
|
||||
isPlayingUnplayedServerOpSpecs = false;
|
||||
|
||||
// take start time
|
||||
startTime = Date.now();
|
||||
|
||||
eventNotifier.emit(ops.OperationRouter.signalProcessingBatchStart, {});
|
||||
|
||||
// apply as much as possible in the given time
|
||||
while (unplayedServerOpspecQueue.length > 0) {
|
||||
// time over?
|
||||
if (Date.now() - startTime > replayTime) {
|
||||
break;
|
||||
}
|
||||
|
||||
opspec = unplayedServerOpspecQueue.shift();
|
||||
|
||||
// use factory to create an instance, and playback!
|
||||
op = operationFactory.create(opspec);
|
||||
runtime.log(" op in: "+runtime.toJson(opspec));
|
||||
if (op !== null) {
|
||||
if (!playbackFunction(op)) {
|
||||
eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {});
|
||||
hasError = true;
|
||||
errorCallback("opExecutionFailure");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {});
|
||||
hasError = true;
|
||||
runtime.log("ignoring invalid incoming opspec: " + opspec);
|
||||
errorCallback("unknownOpReceived");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {});
|
||||
|
||||
// still unplayed opspecs?
|
||||
if (unplayedServerOpspecQueue.length > 0) {
|
||||
// let other events be handled. then continue
|
||||
isPlayingUnplayedServerOpSpecs = true;
|
||||
runtime.getWindow().setTimeout(doPlayUnplayedServerOpSpecs, 1);
|
||||
} else {
|
||||
// finally call all the callbacks waiting for that sync!
|
||||
for (i = 0; i < uncalledSyncRequestCallbacksQueue.length; i += 1) {
|
||||
uncalledSyncRequestCallbacksQueue[i]();
|
||||
}
|
||||
|
||||
uncalledSyncRequestCallbacksQueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (isPlayingUnplayedServerOpSpecs) {
|
||||
return;
|
||||
}
|
||||
doPlayUnplayedServerOpSpecs();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array.<!Object>} opspecs
|
||||
* @param {Array.<!Function>} callbacks
|
||||
* @return {undefined}
|
||||
*/
|
||||
function receiveOpSpecsFromNetwork(opspecs, callbacks) {
|
||||
// append to existing unplayed
|
||||
unplayedServerOpspecQueue = unplayedServerOpspecQueue.concat(opspecs);
|
||||
uncalledSyncRequestCallbacksQueue = uncalledSyncRequestCallbacksQueue.concat(callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the unsynced client ops and the server ops,
|
||||
* applies the server ops after transformation
|
||||
* @param {Array.<!Object>} serverOpspecs
|
||||
* @return {!boolean}
|
||||
*/
|
||||
function handleOpsSyncConflict(serverOpspecs) {
|
||||
var i,
|
||||
transformResult;
|
||||
|
||||
if (! serverOpspecs) {
|
||||
// TODO: proper error message, stop working
|
||||
runtime.BrowserRuntime$assert(false, "no opspecs received!");
|
||||
return false;
|
||||
} // TODO: more checking of proper content in serverOpspecs
|
||||
|
||||
transformResult = operationTransformer.transform(unsyncedClientOpspecQueue, /**@type{!Array.<!Object>}*/(serverOpspecs));
|
||||
|
||||
if (!transformResult) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// store transformed server ops
|
||||
for (i = 0; i < transformResult.opSpecsB.length; i += 1) {
|
||||
unplayedServerOpspecQueue.push(transformResult.opSpecsB[i]);
|
||||
}
|
||||
|
||||
// store opspecs of all transformed client opspecs
|
||||
unsyncedClientOpspecQueue = [];
|
||||
for (i = 0; i < transformResult.opSpecsA.length; i += 1) {
|
||||
unsyncedClientOpspecQueue.push(transformResult.opSpecsA[i]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {undefined}
|
||||
*/
|
||||
function syncOps() {
|
||||
var syncedClientOpspecs,
|
||||
syncRequestCallbacksArray;
|
||||
|
||||
/**
|
||||
* @return {undefined}
|
||||
*/
|
||||
function startSyncOpsTimeout() {
|
||||
idleTimeout = null;
|
||||
syncOpsTimeout = runtime.getWindow().setTimeout(function() {
|
||||
syncOpsTimeout = null;
|
||||
syncOps();
|
||||
}, syncOpsDelay);
|
||||
}
|
||||
|
||||
if (isSyncCallRunning || hasError) {
|
||||
return;
|
||||
}
|
||||
// TODO: hack, remove
|
||||
if (syncingBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.log("OperationRouter: sending sync_ops call");
|
||||
// no more instant pull request in any case
|
||||
isInstantSyncRequested = false;
|
||||
// set lock
|
||||
isSyncCallRunning = true;
|
||||
|
||||
// take specs from queue, if any
|
||||
syncedClientOpspecs = unsyncedClientOpspecQueue;
|
||||
unsyncedClientOpspecQueue = [];
|
||||
syncRequestCallbacksArray = syncRequestCallbacksQueue;
|
||||
syncRequestCallbacksQueue = [];
|
||||
|
||||
server.call({
|
||||
command: 'sync_ops',
|
||||
args: {
|
||||
es_id: sessionId,
|
||||
member_id: memberId,
|
||||
seq_head: String(lastServerSeq),
|
||||
client_ops: syncedClientOpspecs
|
||||
}
|
||||
}, function(responseData) {
|
||||
var response,
|
||||
/**@type{!boolean}*/
|
||||
hasUnresolvableConflict = false;
|
||||
|
||||
updateHasSessionHostConnectionState(true);
|
||||
|
||||
if (syncingBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
response = /** @type{{result:string, head_seq:string, ops:Array.<!Object>}} */(runtime.fromJson(responseData));
|
||||
} catch (e) {
|
||||
hasError = true;
|
||||
runtime.log("Could not parse reply: "+responseData);
|
||||
errorCallback("unknownServerReply");
|
||||
return;
|
||||
}
|
||||
// TODO: hack, remove
|
||||
runtime.log("sync_ops reply: " + responseData);
|
||||
|
||||
// just new ops?
|
||||
if (response.result === "new_ops") {
|
||||
if (response.ops.length > 0) {
|
||||
// no new locally in the meantime?
|
||||
if (unsyncedClientOpspecQueue.length === 0) {
|
||||
receiveOpSpecsFromNetwork(response.ops, syncRequestCallbacksArray);
|
||||
} else {
|
||||
// transform server ops against new local ones and apply,
|
||||
// transform and send new local ops to server
|
||||
runtime.log("meh, have new ops locally meanwhile, have to do transformations.");
|
||||
hasUnresolvableConflict = !handleOpsSyncConflict(response.ops);
|
||||
syncRequestCallbacksQueue = syncRequestCallbacksArray.concat(syncRequestCallbacksQueue);
|
||||
}
|
||||
// and note server state
|
||||
lastServerSeq = response.head_seq;
|
||||
} else {
|
||||
receiveOpSpecsFromNetwork([], syncRequestCallbacksArray);
|
||||
}
|
||||
} else if (response.result === "added") {
|
||||
runtime.log("All added to server");
|
||||
receiveOpSpecsFromNetwork([], syncRequestCallbacksArray);
|
||||
// note server state
|
||||
lastServerSeq = response.head_seq;
|
||||
updateHasLocalUnsyncedOpsState();
|
||||
} else if (response.result === "conflict") {
|
||||
// put the send ops back into the outgoing queue
|
||||
unsyncedClientOpspecQueue = syncedClientOpspecs.concat(unsyncedClientOpspecQueue);
|
||||
syncRequestCallbacksQueue = syncRequestCallbacksArray.concat(syncRequestCallbacksQueue);
|
||||
// transform server ops against new local ones and apply,
|
||||
// transform and request new send new local ops to server
|
||||
runtime.log("meh, server has new ops meanwhile, have to do transformations.");
|
||||
hasUnresolvableConflict = !handleOpsSyncConflict(response.ops);
|
||||
// and note server state
|
||||
lastServerSeq = response.head_seq;
|
||||
// try again instantly
|
||||
if (!hasUnresolvableConflict) {
|
||||
isInstantSyncRequested = true;
|
||||
}
|
||||
} else if (response.result === "error") {
|
||||
runtime.log("server reports an error: "+response.error);
|
||||
hasError = true;
|
||||
errorCallback(
|
||||
response.error === "ENOSESSION" ? "sessionDoesNotExist":
|
||||
response.error === "ENOMEMBER" ? "notMemberOfSession":
|
||||
"unknownServerReply"
|
||||
);
|
||||
} else {
|
||||
hasError = true;
|
||||
runtime.log("Unexpected result on sync-ops call: "+response.result);
|
||||
errorCallback("unknownServerReply");
|
||||
}
|
||||
if (hasError) {
|
||||
return;
|
||||
}
|
||||
|
||||
// unlock
|
||||
isSyncCallRunning = false;
|
||||
|
||||
if (hasUnresolvableConflict) {
|
||||
hasError = true;
|
||||
errorCallback("unresolvableConflictingOps");
|
||||
} else {
|
||||
// prepare next sync
|
||||
if (isInstantSyncRequested) {
|
||||
syncOps();
|
||||
} else {
|
||||
// nothing on client to sync?
|
||||
if (unsyncedClientOpspecQueue.length === 0) {
|
||||
idleTimeout = runtime.getWindow().setTimeout(startSyncOpsTimeout, idleDelay);
|
||||
} else {
|
||||
startSyncOpsTimeout();
|
||||
}
|
||||
}
|
||||
playUnplayedServerOpSpecs();
|
||||
}
|
||||
}, function() {
|
||||
runtime.log("meh, server cannot be reached ATM.");
|
||||
// signal connection problem, but do not give up for now
|
||||
updateHasSessionHostConnectionState(false);
|
||||
// put the (not) send ops back into the outgoing queue
|
||||
unsyncedClientOpspecQueue = syncedClientOpspecs.concat(unsyncedClientOpspecQueue);
|
||||
syncRequestCallbacksQueue = syncRequestCallbacksArray.concat(syncRequestCallbacksQueue);
|
||||
// unlock
|
||||
isSyncCallRunning = false;
|
||||
// nothing on client to sync?
|
||||
if (unsyncedClientOpspecQueue.length === 0) {
|
||||
idleTimeout = runtime.getWindow().setTimeout(startSyncOpsTimeout, idleDelay);
|
||||
} else {
|
||||
startSyncOpsTimeout();
|
||||
}
|
||||
playUnplayedServerOpSpecs();
|
||||
});
|
||||
}
|
||||
|
||||
function triggerPushingOps() {
|
||||
// disable any idle timeout
|
||||
if (idleTimeout) {
|
||||
runtime.clearTimeout(idleTimeout);
|
||||
idleTimeout = null;
|
||||
}
|
||||
|
||||
// enable syncOps timeout, if needed
|
||||
if (!syncOpsTimeout && !isSyncCallRunning) {
|
||||
runtime.log("OperationRouter: opsSync requested for pushing");
|
||||
syncOpsTimeout = runtime.getWindow().setTimeout(function() {
|
||||
syncOpsTimeout = null;
|
||||
syncOps();
|
||||
}, syncOpsDelay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Funtion} cb
|
||||
* @return {undefined}
|
||||
*/
|
||||
function requestInstantOpsSync(cb) {
|
||||
// register callback
|
||||
syncRequestCallbacksQueue.push(cb);
|
||||
|
||||
// disable any idle timeout
|
||||
if (idleTimeout) {
|
||||
runtime.clearTimeout(idleTimeout);
|
||||
idleTimeout = null;
|
||||
}
|
||||
|
||||
// disable any syncOps timeout
|
||||
if (syncOpsTimeout) {
|
||||
runtime.clearTimeout(syncOpsTimeout);
|
||||
syncOpsTimeout = null;
|
||||
}
|
||||
|
||||
runtime.log("OperationRouter: instant opsSync requested");
|
||||
isInstantSyncRequested = true;
|
||||
syncOps();
|
||||
}
|
||||
|
||||
this.requestReplay = function (done_cb) {
|
||||
requestInstantOpsSync(done_cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the factory to use to create operation instances from operation specs.
|
||||
*
|
||||
* @param {!ops.OperationFactory} f
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.setOperationFactory = function (f) {
|
||||
operationFactory = f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the method which should be called to apply operations.
|
||||
*
|
||||
* @param {!function(!ops.Operation):boolean} playback_func
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.setPlaybackFunction = function (playback_func) {
|
||||
playbackFunction = playback_func;
|
||||
};
|
||||
|
||||
/**
|
||||
* Brings the locally created operations into the game.
|
||||
*
|
||||
* @param {!Array.<!ops.Operation>} operations
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.push = function (operations) {
|
||||
var i, op, opspec,
|
||||
timestamp = Date.now();
|
||||
|
||||
if (hasError) {
|
||||
return;
|
||||
}
|
||||
// TODO: should be an assert in the future
|
||||
// there needs to be a flag telling that processing is happening,
|
||||
// and thus any input should be dropped in the sessioncontroller
|
||||
// ideally also have some UI element showing the processing state
|
||||
if (unplayedServerOpspecQueue.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventNotifier.emit(ops.OperationRouter.signalProcessingBatchStart, {});
|
||||
|
||||
for (i = 0; i < operations.length; i += 1) {
|
||||
op = operations[i];
|
||||
opspec = op.spec();
|
||||
|
||||
// note if any local ops modified
|
||||
hasPushedModificationOps = hasPushedModificationOps || op.isEdit;
|
||||
|
||||
// add timestamp TODO: improve the useless recreation of the op
|
||||
opspec.timestamp = timestamp;
|
||||
op = operationFactory.create(opspec);
|
||||
|
||||
// apply locally
|
||||
if (!playbackFunction(op)) {
|
||||
hasError = true;
|
||||
errorCallback("opExecutionFailure");
|
||||
return;
|
||||
}
|
||||
|
||||
// send to server
|
||||
unsyncedClientOpspecQueue.push(opspec);
|
||||
}
|
||||
|
||||
triggerPushingOps();
|
||||
|
||||
updateHasLocalUnsyncedOpsState();
|
||||
|
||||
eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests a gracefull shutdown of the Operation Router.
|
||||
* Buffered operations shall be sent to the server.
|
||||
* A callback is called on success.
|
||||
*/
|
||||
this.close = function (cb) {
|
||||
function cbDoneSaving(err) {
|
||||
eventNotifier.emit(EVENT_SAVEDTOFILE, null);
|
||||
cb(err);
|
||||
}
|
||||
|
||||
function cbSuccess(fileData) {
|
||||
server.writeSessionStateToFile(sessionId, memberId, lastServerSeq, fileData, cbDoneSaving);
|
||||
}
|
||||
|
||||
function doClose() {
|
||||
syncingBlocked = true;
|
||||
if (hasPushedModificationOps) {
|
||||
eventNotifier.emit(EVENT_BEFORESAVETOFILE, null);
|
||||
|
||||
odfContainer.createByteArray(cbSuccess, cbDoneSaving);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
cb();
|
||||
} else if (hasLocalUnsyncedOps) {
|
||||
requestInstantOpsSync(doClose);
|
||||
} else {
|
||||
doClose();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} eventId
|
||||
* @param {!Function} cb
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.subscribe = function (eventId, cb) {
|
||||
eventNotifier.subscribe(eventId, cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} eventId
|
||||
* @param {!Function} cb
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.unsubscribe = function (eventId, cb) {
|
||||
eventNotifier.unsubscribe(eventId, cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {!boolean}
|
||||
*/
|
||||
this.hasLocalUnsyncedOps = function () {
|
||||
return hasLocalUnsyncedOps;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {!boolean}
|
||||
*/
|
||||
this.hasSessionHostConnection = function () {
|
||||
return hasSessionHostConnection;
|
||||
};
|
||||
};
|
||||
});
|
291
api/js/webodf/collab/backend/pullbox/Server.js
Normal file
291
api/js/webodf/collab/backend/pullbox/Server.js
Normal file
@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
|
||||
*
|
||||
* @licstart
|
||||
* This file is part of WebODF.
|
||||
*
|
||||
* WebODF is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License (GNU AGPL)
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* WebODF is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
|
||||
* @licend
|
||||
*
|
||||
* @source: http://www.webodf.org/
|
||||
* @source: https://github.com/kogmbh/WebODF/
|
||||
*/
|
||||
|
||||
/*global define, runtime, core, XMLHttpRequest*/
|
||||
|
||||
define("webodf/editor/backend/pullbox/Server", [], function () {
|
||||
"use strict";
|
||||
|
||||
runtime.loadClass("core.Base64");
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements ops.Server
|
||||
* @param {{url:string}} args
|
||||
*/
|
||||
return function PullBoxServer(args) {
|
||||
|
||||
var self = this,
|
||||
token,
|
||||
/**@const*/serverCallTimeout = 10000,
|
||||
base64 = new core.Base64();
|
||||
|
||||
args = args || {};
|
||||
args.url = args.url || "/WSER";
|
||||
args.sessionStateToFileUrl = args.sessionStateToFileUrl || "/SS2F";
|
||||
|
||||
this.getGenesisUrl = function (sessionId) {
|
||||
return args.genesisUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!Object} message
|
||||
* @param {!function(!string)} cb
|
||||
* @param {!function(!number,!string)} cbError passes the status number
|
||||
* and the statustext, or -1 if there was an exception on sending
|
||||
* @return {undefined}
|
||||
*/
|
||||
function call(message, cb, cbError) {
|
||||
var xhr = new XMLHttpRequest(),
|
||||
messageString = JSON.stringify(message);
|
||||
|
||||
function handleResult() {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
// report error
|
||||
runtime.log("Status " + String(xhr.status) + ": " +
|
||||
xhr.responseText || xhr.statusText);
|
||||
cbError(xhr.status, xhr.statusText);
|
||||
} else {
|
||||
runtime.log("Status " + String(xhr.status) + ": " +
|
||||
xhr.responseText || xhr.statusText);
|
||||
cb(xhr.responseText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime.log("Sending message to server: " + messageString);
|
||||
// create body data for request from metadata and payload
|
||||
|
||||
// do the request
|
||||
xhr.open('POST', args.url, true);
|
||||
if (token) {
|
||||
xhr.setRequestHeader("requesttoken", token);
|
||||
}
|
||||
xhr.setRequestHeader("content-type", "application/json");
|
||||
xhr.onreadystatechange = handleResult;
|
||||
xhr.timeout = serverCallTimeout;
|
||||
// TODO: seems handleResult is called on timeout as well, with xhr.status === 0
|
||||
// xhr.ontimeout = handleTimeout;
|
||||
try {
|
||||
xhr.send(messageString);
|
||||
} catch (e) {
|
||||
runtime.log("Problem with calling server: " + e);
|
||||
cbError(-1, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
this.call = call;
|
||||
|
||||
this.getToken = function () {
|
||||
return token;
|
||||
};
|
||||
|
||||
/**
|
||||
* for pre-authenticated use
|
||||
*/
|
||||
this.setToken = function (a_token) {
|
||||
token = a_token;
|
||||
};
|
||||
|
||||
|
||||
/*jslint unparam: true*/
|
||||
/**
|
||||
* @param {!number} timeout in milliseconds
|
||||
* @param {!function(!string)} callback
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.connect = function (timeout, callback) {
|
||||
/*
|
||||
var accumulatedWaitingTime = 0;
|
||||
|
||||
// already tried connecting?
|
||||
if (self.networkStatus() === "ready") {
|
||||
return;
|
||||
}
|
||||
*/
|
||||
callback("ready");
|
||||
};
|
||||
/*jslint unparam: false*/
|
||||
|
||||
/**
|
||||
* @return {!string}
|
||||
*/
|
||||
this.networkStatus = function () {
|
||||
return "ready";
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} login
|
||||
* @param {!string} password
|
||||
* @param {function(!Object)} successCb
|
||||
* @param {function(!string)} failCb
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.login = function (login, password, successCb, failCb) {
|
||||
successCb ({
|
||||
'full_name' :'Hadi Nategh',
|
||||
'uid' : 'sysop',
|
||||
'securityToken' : '10000',
|
||||
'token' : '10000',
|
||||
'sessiondata_list' : [],
|
||||
'success' : true,
|
||||
'member_id' : 'sysop'
|
||||
});
|
||||
return;
|
||||
call({
|
||||
command: "login",
|
||||
args: {
|
||||
login: base64.toBase64(login),
|
||||
password: base64.toBase64(password)
|
||||
}
|
||||
}, function(responseData) {
|
||||
var response = /**@type {{token:string}}*/(runtime.fromJson(responseData));
|
||||
runtime.log("Login reply: " + responseData);
|
||||
|
||||
if (response.hasOwnProperty("token")) {
|
||||
token = response.token;
|
||||
runtime.log("Caching token: " + self.getToken());
|
||||
successCb(response);
|
||||
} else {
|
||||
failCb(responseData);
|
||||
}
|
||||
}, failCb);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} userId
|
||||
* @param {!string} sessionId
|
||||
* @param {!function(!string)} successCb
|
||||
* @param {!function()} failCb
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.joinSession = function (userId, sessionId, successCb, failCb) {
|
||||
successCb('sysop');
|
||||
return;
|
||||
call({
|
||||
command: "join_session",
|
||||
args: {
|
||||
user_id: userId,
|
||||
es_id: sessionId
|
||||
}
|
||||
}, function(responseData) {
|
||||
var response = /**@type {{success:string, member_id:string}}*/(runtime.fromJson(responseData));
|
||||
runtime.log("join_session reply: " + responseData);
|
||||
|
||||
if (response.hasOwnProperty("success") && response.success) {
|
||||
successCb(response.member_id);
|
||||
} else {
|
||||
failCb();
|
||||
}
|
||||
}, failCb);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} sessionId
|
||||
* @param {!string} memberId
|
||||
* @param {!function()} successCb
|
||||
* @param {!function()} failCb
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.leaveSession = function (sessionId, memberId, successCb, failCb) {
|
||||
call({
|
||||
command: "leave_session",
|
||||
args: {
|
||||
es_id: sessionId,
|
||||
member_id: memberId
|
||||
}
|
||||
}, function(responseData) {
|
||||
var response = /**@type {{success:string, member_id:string}}*/(runtime.fromJson(responseData));
|
||||
runtime.log("leave_session reply: " + responseData);
|
||||
|
||||
if (response.hasOwnProperty("success") && response.success) {
|
||||
successCb();
|
||||
} else {
|
||||
failCb();
|
||||
}
|
||||
}, failCb);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} sessionId
|
||||
* @param {!string} memberId
|
||||
* @param {!string} seqHead
|
||||
* @param {!function(!Object=)} callback
|
||||
* @return {undefined}
|
||||
*/
|
||||
this.writeSessionStateToFile = function(sessionId, memberId, seqHead, fileData, callback) {
|
||||
// code copied from BrowserRuntime.writeFile and adapted
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
function handleResult() {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 0 && !xhr.responseText) {// TODO: check makes sense here as well?
|
||||
// for local files there is no difference between missing
|
||||
// and empty files, so empty files are considered as errors
|
||||
runtime.log("File " + args.sessionStateToFileUrl + " is empty.");
|
||||
} else if ((xhr.status >= 200 && xhr.status < 300) ||
|
||||
xhr.status === 0) {
|
||||
// report success
|
||||
runtime.log(null);
|
||||
} else {
|
||||
// report error
|
||||
runtime.log("Status " + String(xhr.status) + ": " +
|
||||
xhr.responseText || xhr.statusText);
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// do the request
|
||||
xhr.open('POST', args.sessionStateToFileUrl, true);
|
||||
if (token) {
|
||||
xhr.setRequestHeader("requesttoken", token);
|
||||
}
|
||||
xhr.setRequestHeader("webodf-session-id", sessionId);
|
||||
xhr.setRequestHeader("webodf-member-id", memberId);
|
||||
xhr.setRequestHeader("webodf-session-revision", seqHead);
|
||||
xhr.onreadystatechange = handleResult;
|
||||
// ArrayBufferView will have an ArrayBuffer property, in WebKit, XHR can send()
|
||||
// an ArrayBuffer, In Firefox, one must use sendAsBinary with a string
|
||||
if (fileData.buffer && !xhr.sendAsBinary) {
|
||||
fileData = fileData.buffer; // webkit supports sending an ArrayBuffer
|
||||
} else {
|
||||
// encode into a string, this works in FireFox >= 3
|
||||
fileData = runtime.byteArrayToString(fileData, "binary");
|
||||
}
|
||||
try {
|
||||
if (xhr.sendAsBinary) {
|
||||
xhr.sendAsBinary(fileData);
|
||||
} else {
|
||||
xhr.send(fileData);
|
||||
}
|
||||
} catch (e) {
|
||||
runtime.log("Problem with calling \"writeSessionStateToFile\" on server");
|
||||
callback(e.message);
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
49
api/js/webodf/collab/backend/pullbox/ServerFactory.js
Normal file
49
api/js/webodf/collab/backend/pullbox/ServerFactory.js
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
|
||||
*
|
||||
* @licstart
|
||||
* This file is part of WebODF.
|
||||
*
|
||||
* WebODF is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License (GNU AGPL)
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* WebODF is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
|
||||
* @licend
|
||||
*
|
||||
* @source: http://www.webodf.org/
|
||||
* @source: https://github.com/kogmbh/WebODF/
|
||||
*/
|
||||
|
||||
/*global define, document, require, runtime, ops */
|
||||
|
||||
define("webodf/editor/backend/pullbox/ServerFactory", [
|
||||
"webodf/editor/backend/pullbox/Server",
|
||||
"webodf/editor/backend/pullbox/SessionBackend",
|
||||
"webodf/editor/backend/pullbox/SessionList"],
|
||||
function (PullBoxServer, PullBoxSessionBackend, PullBoxSessionList) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements ServerFactory
|
||||
*/
|
||||
return function PullBoxServerFactory() {
|
||||
this.createServer = function (args) {
|
||||
return new PullBoxServer(args);
|
||||
};
|
||||
this.createSessionBackend = function (sid, mid, server) {
|
||||
return new PullBoxSessionBackend(sid, mid, server);
|
||||
};
|
||||
this.createSessionList = function (server) {
|
||||
return new PullBoxSessionList(server);
|
||||
};
|
||||
};
|
||||
});
|
64
api/js/webodf/collab/backend/pullbox/SessionBackend.js
Normal file
64
api/js/webodf/collab/backend/pullbox/SessionBackend.js
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (C) 2014 KO GmbH <copyright@kogmbh.com>
|
||||
*
|
||||
* @licstart
|
||||
* This file is part of WebODF.
|
||||
*
|
||||
* WebODF is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License (GNU AGPL)
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* WebODF is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
|
||||
* @licend
|
||||
*
|
||||
* @source: http://www.webodf.org/
|
||||
* @source: https://github.com/kogmbh/WebODF/
|
||||
*/
|
||||
|
||||
/*global define, ops*/
|
||||
|
||||
|
||||
define("webodf/editor/backend/pullbox/SessionBackend", [
|
||||
"webodf/editor/backend/pullbox/OperationRouter"],
|
||||
function (PullBoxOperationRouter) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements SessionBackend
|
||||
*/
|
||||
function PullBoxSessionBackend(sessionId, memberId, server) {
|
||||
|
||||
/**
|
||||
* @return {!string}
|
||||
*/
|
||||
this.getMemberId = function () {
|
||||
return memberId;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!odf.OdfContainer} odfContainer
|
||||
* @param {!function(!Object)} errorCallback
|
||||
* @return {!ops.OperationRouter}
|
||||
*/
|
||||
this.createOperationRouter = function (odfContainer, errorCallback) {
|
||||
return new PullBoxOperationRouter(sessionId, memberId, server, odfContainer, errorCallback);
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {!string}
|
||||
*/
|
||||
this.getGenesisUrl = function () {
|
||||
return server.getGenesisUrl(sessionId);
|
||||
};
|
||||
}
|
||||
|
||||
return PullBoxSessionBackend;
|
||||
});
|
173
api/js/webodf/collab/backend/pullbox/SessionList.js
Normal file
173
api/js/webodf/collab/backend/pullbox/SessionList.js
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
|
||||
*
|
||||
* @licstart
|
||||
* This file is part of WebODF.
|
||||
*
|
||||
* WebODF is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License (GNU AGPL)
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* WebODF is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
|
||||
* @licend
|
||||
*
|
||||
* @source: http://www.webodf.org/
|
||||
* @source: https://github.com/kogmbh/WebODF/
|
||||
*/
|
||||
|
||||
/*global define, ops, runtime */
|
||||
|
||||
define("webodf/editor/backend/pullbox/SessionList", [], function () {
|
||||
"use strict";
|
||||
|
||||
return function PullBoxSessionList(server) {
|
||||
var cachedSessionData = {},
|
||||
subscribers = [],
|
||||
serverPullingTimeoutId = null,
|
||||
pullingActive = true;
|
||||
|
||||
function onSessionData(sessionData) {
|
||||
var i,
|
||||
isNew = !cachedSessionData.hasOwnProperty(sessionData.id);
|
||||
|
||||
// extend data with download url
|
||||
sessionData.fileUrl = "/session/" + sessionData.id + "/last/" + sessionData.filename;
|
||||
// cache
|
||||
cachedSessionData[sessionData.id] = sessionData;
|
||||
runtime.log("get session data for:" + sessionData.title + ", is new:" + isNew);
|
||||
|
||||
for (i = 0; i < subscribers.length; i += 1) {
|
||||
if (isNew) {
|
||||
subscribers[i].onCreated(sessionData);
|
||||
} else {
|
||||
subscribers[i].onUpdated(sessionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onSessionRemoved(sessionId) {
|
||||
var i;
|
||||
|
||||
if (cachedSessionData.hasOwnProperty(sessionId)) {
|
||||
delete cachedSessionData[sessionId];
|
||||
|
||||
for (i = 0; i < subscribers.length; i += 1) {
|
||||
subscribers[i].onRemoved(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pullSessionList() {
|
||||
serverPullingTimeoutId = null;
|
||||
|
||||
server.call({
|
||||
command: "query_sessiondata_list"
|
||||
}, function (responseData) {
|
||||
var response = runtime.fromJson(responseData),
|
||||
sessionList, i,
|
||||
unupdatedSessions = {};
|
||||
|
||||
// stopped meanwhile? TODO: support for cancelling calls
|
||||
if (!pullingActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.log("query_sessiondata_list reply: " + responseData);
|
||||
|
||||
if (response.hasOwnProperty("sessiondata_list")) {
|
||||
// collect known sessions
|
||||
for (i in cachedSessionData) {
|
||||
if (cachedSessionData.hasOwnProperty(i)) {
|
||||
unupdatedSessions[i] = ""; // some dummy value, unused
|
||||
}
|
||||
}
|
||||
|
||||
// add/update with all delivered sessions
|
||||
sessionList = response.sessiondata_list;
|
||||
for (i = 0; i < sessionList.length; i += 1) {
|
||||
if (unupdatedSessions.hasOwnProperty(sessionList[i].id)) {
|
||||
delete unupdatedSessions[sessionList[i].id];
|
||||
}
|
||||
onSessionData(sessionList[i]);
|
||||
}
|
||||
|
||||
// remove unupdated sessions
|
||||
for (i in unupdatedSessions) {
|
||||
if (unupdatedSessions.hasOwnProperty(i)) {
|
||||
onSessionRemoved(i);
|
||||
}
|
||||
}
|
||||
|
||||
// next update in 5 secs
|
||||
serverPullingTimeoutId = runtime.getWindow().setTimeout(pullSessionList, 5000);
|
||||
} else {
|
||||
runtime.log("Meh, sessionlist data broken: " + responseData);
|
||||
}
|
||||
}, function (e) {
|
||||
runtime.log(e);
|
||||
});
|
||||
}
|
||||
|
||||
this.getSessions = function (subscriber) {
|
||||
var i,
|
||||
sessionList = [];
|
||||
|
||||
if (subscriber) {
|
||||
subscribers.push(subscriber);
|
||||
}
|
||||
|
||||
for (i in cachedSessionData) {
|
||||
if (cachedSessionData.hasOwnProperty(i)) {
|
||||
sessionList.push(cachedSessionData[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return sessionList;
|
||||
};
|
||||
|
||||
this.unsubscribe = function (subscriber) {
|
||||
var i;
|
||||
|
||||
for (i = 0; i < subscribers.length; i += 1) {
|
||||
if (subscribers[i] === subscriber) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
runtime.BrowserRuntime$assert((i < subscribers.length),
|
||||
"tried to unsubscribe when not subscribed.");
|
||||
|
||||
subscribers.splice(i, 1);
|
||||
};
|
||||
|
||||
this.setUpdatesEnabled = function (enabled) {
|
||||
if (pullingActive === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
pullingActive = enabled;
|
||||
if (pullingActive) {
|
||||
pullSessionList();
|
||||
} else {
|
||||
// cancel any running pulling timeout
|
||||
if (serverPullingTimeoutId !== null) {
|
||||
runtime.clearTimeout(serverPullingTimeoutId);
|
||||
serverPullingTimeoutId = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function init() {
|
||||
pullSessionList();
|
||||
}
|
||||
|
||||
init();
|
||||
};
|
||||
});
|
Reference in New Issue
Block a user