Update to current webodf: saves on closing the session/document (needs server support still)
saves only is the user has edited somethingpull/1/head
parent
abdd34ca50
commit
e74535bdf6
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
|
||||||
|
*
|
||||||
|
* @licstart
|
||||||
|
* The JavaScript code in this page 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. The code is distributed
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
|
||||||
|
*
|
||||||
|
* As additional permission under GNU AGPL version 3 section 7, you
|
||||||
|
* may distribute non-source (e.g., minimized or compacted) forms of
|
||||||
|
* that code without the copy of the GNU GPL normally required by
|
||||||
|
* section 4, provided you include this license notice and a URL
|
||||||
|
* through which recipients can access the Corresponding Source.
|
||||||
|
*
|
||||||
|
* As a special exception to the AGPL, any HTML file which merely makes function
|
||||||
|
* calls to this code, and for that purpose includes it by reference shall be
|
||||||
|
* deemed a separate work for copyright law purposes. In addition, the copyright
|
||||||
|
* holders of this code give you permission to combine this code with free
|
||||||
|
* software libraries that are released under the GNU LGPL. You may copy and
|
||||||
|
* distribute such a system following the terms of the GNU AGPL for this code
|
||||||
|
* and the LGPL for the libraries. If you modify this code, you may extend this
|
||||||
|
* exception to your version of the code, but you are not obligated to do so.
|
||||||
|
* If you do not wish to do so, delete this exception statement from your
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* This license applies to this entire compilation.
|
||||||
|
* @licend
|
||||||
|
* @source: http://www.webodf.org/
|
||||||
|
* @source: http://gitorious.org/webodf/webodf/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*global ops, runtime */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A model which provides information about sessions.
|
||||||
|
* @interface
|
||||||
|
*/
|
||||||
|
SessionList = function SessionList() {"use strict"; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{onCreated:function(!Object),
|
||||||
|
* onUpdated:function(!Object),
|
||||||
|
* onRemoved:function(!string) }} subscriber
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
SessionList.prototype.getSessions = function (subscriber) {"use strict"; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{onCreated:function(!Object),
|
||||||
|
* onUpdated:function(!Object),
|
||||||
|
* onRemoved:function(!string) }} subscriber
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
SessionList.prototype.unsubscribe = function (subscriber) {"use strict"; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per default updates are enabled.
|
||||||
|
* @param {!boolean} enabled
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
SessionList.prototype.setUpdatesEnabled = function (enabled) {"use strict"; };
|
@ -1,462 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
|
|
||||||
*
|
|
||||||
* @licstart
|
|
||||||
* The JavaScript code in this page 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. The code is distributed
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
|
|
||||||
*
|
|
||||||
* As additional permission under GNU AGPL version 3 section 7, you
|
|
||||||
* may distribute non-source (e.g., minimized or compacted) forms of
|
|
||||||
* that code without the copy of the GNU GPL normally required by
|
|
||||||
* section 4, provided you include this license notice and a URL
|
|
||||||
* through which recipients can access the Corresponding Source.
|
|
||||||
*
|
|
||||||
* As a special exception to the AGPL, any HTML file which merely makes function
|
|
||||||
* calls to this code, and for that purpose includes it by reference shall be
|
|
||||||
* deemed a separate work for copyright law purposes. In addition, the copyright
|
|
||||||
* holders of this code give you permission to combine this code with free
|
|
||||||
* software libraries that are released under the GNU LGPL. You may copy and
|
|
||||||
* distribute such a system following the terms of the GNU AGPL for this code
|
|
||||||
* and the LGPL for the libraries. If you modify this code, you may extend this
|
|
||||||
* exception to your version of the code, but you are not obligated to do so.
|
|
||||||
* If you do not wish to do so, delete this exception statement from your
|
|
||||||
* version.
|
|
||||||
*
|
|
||||||
* This license applies to this entire compilation.
|
|
||||||
* @licend
|
|
||||||
* @source: http://www.webodf.org/
|
|
||||||
* @source: http://gitorious.org/webodf/webodf/
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* bootstrap the editor in different ways.
|
|
||||||
* this file is meant to be included from HTML and used
|
|
||||||
* by users who do not want to know much about the inner
|
|
||||||
* complexity.
|
|
||||||
* so we need to make it really easy.
|
|
||||||
*
|
|
||||||
* including this file will result in the namespace/object
|
|
||||||
* "webodfEditor" to be available from the HTML side.
|
|
||||||
* calling webodfEditor.boot() will start the editor.
|
|
||||||
* the method can also take some parameters to specify
|
|
||||||
* behaviour. see documentation of that method.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*global runtime, require, document, alert, net, window, SessionListView, ops */
|
|
||||||
|
|
||||||
// define the namespace/object we want to provide
|
|
||||||
// this is the first line of API, the user gets.
|
|
||||||
var webodfEditor = (function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
runtime.currentDirectory = function () {
|
|
||||||
return "../../webodf/lib";
|
|
||||||
};
|
|
||||||
runtime.libraryPaths = function () {
|
|
||||||
return [ runtime.currentDirectory() ];
|
|
||||||
};
|
|
||||||
|
|
||||||
var editorInstance = null,
|
|
||||||
serverFactory = null,
|
|
||||||
server = null,
|
|
||||||
booting = false,
|
|
||||||
loadedFilename;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* wait for a network connection through nowjs to establish.
|
|
||||||
* call the callback when done, when finally failed, or
|
|
||||||
* when a timeout reached.
|
|
||||||
* the parameter to the callback is a string with the possible
|
|
||||||
* values:
|
|
||||||
* "unavailable", "timeout", "ready"
|
|
||||||
*
|
|
||||||
* @param {!function(!string)} callback
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
|
||||||
function connectNetwork(backend, callback) {
|
|
||||||
function createServer(ServerFactory) {
|
|
||||||
serverFactory = new ServerFactory();
|
|
||||||
server = serverFactory.createServer();
|
|
||||||
server.connect(8000, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (backend) {
|
|
||||||
case "pullbox":
|
|
||||||
require({ }, ["webodf/editor/server/pullbox/serverFactory"], createServer);
|
|
||||||
break;
|
|
||||||
case "nowjs":
|
|
||||||
require({ }, ["webodf/editor/server/nowjs/serverFactory"], createServer);
|
|
||||||
break;
|
|
||||||
case "owncloud":
|
|
||||||
require({ }, ["webodf/editor/server/pullbox/serverFactory"], function (ServerFactory) {
|
|
||||||
serverFactory = new ServerFactory();
|
|
||||||
server = serverFactory.createServer({url: "./documents/ajax/otpoll.php"});
|
|
||||||
server.getGenesisUrl = function(sid) {
|
|
||||||
// what a dirty hack :)
|
|
||||||
return OC.Router.generate('documents_genesis')+'/' +sid;
|
|
||||||
};
|
|
||||||
server.connect(8000, callback);
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
callback("unavailable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* try to auto-sense the server-backend.
|
|
||||||
*
|
|
||||||
* NOT IMPLEMENTED / MIGHT BE NICE TO HAVE...
|
|
||||||
* for now: try to connect to pullbox backend
|
|
||||||
*
|
|
||||||
* @param {!function(!string)} callback
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
|
||||||
function detectNetwork(callback) {
|
|
||||||
connectNetwork("pullbox", callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* extract document url from the url-fragment
|
|
||||||
*
|
|
||||||
* @return {?string}
|
|
||||||
*/
|
|
||||||
function guessDocUrl() {
|
|
||||||
var pos, docUrl = String(document.location);
|
|
||||||
// If the URL has a fragment (#...), try to load the file it represents
|
|
||||||
pos = docUrl.indexOf('#');
|
|
||||||
if (pos !== -1) {
|
|
||||||
docUrl = docUrl.substr(pos + 1);
|
|
||||||
} else {
|
|
||||||
docUrl = "welcome.odt";
|
|
||||||
}
|
|
||||||
return docUrl || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileSelectHandler(evt) {
|
|
||||||
var file, files, reader;
|
|
||||||
files = (evt.target && evt.target.files) ||
|
|
||||||
(evt.dataTransfer && evt.dataTransfer.files);
|
|
||||||
function onloadend() {
|
|
||||||
if (reader.readyState === 2) {
|
|
||||||
runtime.registerFile(file.name, reader.result);
|
|
||||||
loadedFilename = file.name;
|
|
||||||
editorInstance.loadDocument(file.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (files && files.length === 1) {
|
|
||||||
file = files[0];
|
|
||||||
reader = new FileReader();
|
|
||||||
reader.onloadend = onloadend;
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
} else {
|
|
||||||
alert("File could not be opened in this browser.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function enhanceRuntime() {
|
|
||||||
var openedFiles = {},
|
|
||||||
read = runtime.read,
|
|
||||||
getFileSize = runtime.getFileSize;
|
|
||||||
runtime.read = function (path, offset, length, callback) {
|
|
||||||
var array;
|
|
||||||
if (openedFiles.hasOwnProperty(path)) {
|
|
||||||
array = new Uint8Array(openedFiles[path], offset, length);
|
|
||||||
callback(undefined, array);
|
|
||||||
} else {
|
|
||||||
return read(path, offset, length, callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
runtime.getFileSize = function (path, callback) {
|
|
||||||
if (openedFiles.hasOwnProperty(path)) {
|
|
||||||
return callback(openedFiles[path].byteLength);
|
|
||||||
} else {
|
|
||||||
return getFileSize(path, callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
runtime.registerFile = function (path, data) {
|
|
||||||
openedFiles[path] = data;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFileLoadForm() {
|
|
||||||
var form = document.createElement("form"),
|
|
||||||
input = document.createElement("input");
|
|
||||||
form.appendChild(input);
|
|
||||||
form.style.display = "none";
|
|
||||||
input.id = "fileloader";
|
|
||||||
input.setAttribute("type", "file");
|
|
||||||
input.addEventListener("change", fileSelectHandler, false);
|
|
||||||
document.body.appendChild(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
var form = document.getElementById("fileloader");
|
|
||||||
if (!form) {
|
|
||||||
enhanceRuntime();
|
|
||||||
createFileLoadForm();
|
|
||||||
form = document.getElementById("fileloader");
|
|
||||||
}
|
|
||||||
form.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
editorInstance.saveDocument(loadedFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create a new editor instance, and start the editor with
|
|
||||||
* the given document.
|
|
||||||
*
|
|
||||||
* @param {!string} docUrl
|
|
||||||
* @param {?Object} editorOptions
|
|
||||||
* @param {?function(!Object)} editorReadyCallback
|
|
||||||
*/
|
|
||||||
function createLocalEditor(docUrl, editorOptions, editorReadyCallback) {
|
|
||||||
var pos;
|
|
||||||
booting = true;
|
|
||||||
editorOptions = editorOptions || {};
|
|
||||||
editorOptions.memberid = "localuser";
|
|
||||||
editorOptions.loadCallback = load;
|
|
||||||
editorOptions.saveCallback = save;
|
|
||||||
|
|
||||||
if (docUrl === undefined) {
|
|
||||||
docUrl = guessDocUrl();
|
|
||||||
}
|
|
||||||
runtime.assert(docUrl, "docUrl needs to be specified");
|
|
||||||
runtime.assert(editorInstance === null, "cannot boot with instanciated editor");
|
|
||||||
|
|
||||||
document.getElementById("mainContainer").style.display = "";
|
|
||||||
|
|
||||||
require({ }, ["webodf/editor/Editor"],
|
|
||||||
function (Editor) {
|
|
||||||
editorInstance = new Editor(editorOptions);
|
|
||||||
editorInstance.initAndLoadDocument(docUrl, function (editorSession) {
|
|
||||||
editorSession.startEditing();
|
|
||||||
editorReadyCallback(editorInstance);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* assume the network connection is established, create a new editor instance,
|
|
||||||
* and start the editor on the network.
|
|
||||||
*
|
|
||||||
* @param {!string} sessionId
|
|
||||||
* @param {!string} memberId
|
|
||||||
* @param {?string} token
|
|
||||||
* @param {?Object} editorOptions
|
|
||||||
* @param {?function(!Object)} editorReadyCallback
|
|
||||||
*/
|
|
||||||
function createNetworkedEditor(sessionId, memberId, token, editorOptions, editorReadyCallback) {
|
|
||||||
|
|
||||||
runtime.assert(sessionId, "sessionId needs to be specified");
|
|
||||||
runtime.assert(memberId, "memberId needs to be specified");
|
|
||||||
runtime.assert(editorInstance === null, "cannot boot with instanciated editor");
|
|
||||||
|
|
||||||
editorOptions = editorOptions || {};
|
|
||||||
editorOptions.memberid = memberId;
|
|
||||||
editorOptions.networked = true;
|
|
||||||
editorOptions.networkSecurityToken = token;
|
|
||||||
|
|
||||||
require({ }, ["webodf/editor/Editor"],
|
|
||||||
function (Editor) {
|
|
||||||
// TODO: the networkSecurityToken needs to be retrieved via now.login
|
|
||||||
// (but this is to be implemented later)
|
|
||||||
editorInstance = new Editor(editorOptions, server, serverFactory);
|
|
||||||
|
|
||||||
// load the document and get called back when it's live
|
|
||||||
editorInstance.loadSession(sessionId, function (editorSession) {
|
|
||||||
editorSession.startEditing();
|
|
||||||
editorReadyCallback(editorInstance);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* start the login process by offering a login/password prompt.
|
|
||||||
* the login is validated via nowjs namespace.
|
|
||||||
* on success a list of sessions is offered.
|
|
||||||
* when the user selects a session the callback is called
|
|
||||||
* with the sessionId as parameter
|
|
||||||
*
|
|
||||||
* @param {!function(!string, !string, ?string)} callback
|
|
||||||
* @returns {undefined}
|
|
||||||
*/
|
|
||||||
function startLoginProcess(callback) {
|
|
||||||
var userid, token;
|
|
||||||
|
|
||||||
booting = true;
|
|
||||||
|
|
||||||
runtime.assert(editorInstance === null, "cannot boot with instanciated editor");
|
|
||||||
|
|
||||||
function enterSession(selectedSessionId) {
|
|
||||||
document.getElementById("sessionListContainer").style.display = "none";
|
|
||||||
document.getElementById("mainContainer").style.display = "";
|
|
||||||
|
|
||||||
callback(selectedSessionId, userid, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSessions() {
|
|
||||||
require({ }, ["webodf/editor/SessionListView"],
|
|
||||||
function (SessionListView) {
|
|
||||||
var sessionListDiv = document.getElementById("sessionList"),
|
|
||||||
sessionList = new serverFactory.createSessionList(server),
|
|
||||||
sessionListView = new SessionListView(sessionList, sessionListDiv, enterSession);
|
|
||||||
|
|
||||||
// hide login view
|
|
||||||
document.getElementById("loginContainer").style.display = "none";
|
|
||||||
|
|
||||||
// show session list
|
|
||||||
document.getElementById("sessionListContainer").style.display = "";
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loginSuccess(userData) {
|
|
||||||
runtime.log("connected:" + userData.full_name);
|
|
||||||
userid = userData.uid;
|
|
||||||
token = userData.securityToken || null;
|
|
||||||
|
|
||||||
showSessions();
|
|
||||||
}
|
|
||||||
|
|
||||||
function loginFail(result) {
|
|
||||||
alert("Login failed: " + result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLoginSubmit() {
|
|
||||||
server.login(document.loginForm.login.value, document.loginForm.password.value, loginSuccess, loginFail);
|
|
||||||
|
|
||||||
// block the submit button, we already dealt with the input
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bring up the login form
|
|
||||||
document.loginForm.Submit.onclick = onLoginSubmit;
|
|
||||||
document.getElementById("loginContainer").style.display = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* make a guess about the document (# in URL)
|
|
||||||
* also guess about local/collaborative (depending on nowjs)
|
|
||||||
*
|
|
||||||
* @param {?Object} args
|
|
||||||
*
|
|
||||||
* args:
|
|
||||||
*
|
|
||||||
* collaborative: if set to true: connect to the network and start a
|
|
||||||
* collaborative editor. in that case the document url
|
|
||||||
* is ignored. and user needs to select a session.
|
|
||||||
*
|
|
||||||
* if set to the string "auto": it will try the above
|
|
||||||
* but fall back to non-collaborative mode [default]
|
|
||||||
*
|
|
||||||
* docUrl: if given it is used as the url to the document to load
|
|
||||||
*
|
|
||||||
* callback: callback to be called as soon as the document is loaded
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function boot(args) {
|
|
||||||
var editorOptions = {},
|
|
||||||
loginProcedure = startLoginProcess;
|
|
||||||
runtime.assert(!booting, "editor creation already in progress");
|
|
||||||
|
|
||||||
args = args || {};
|
|
||||||
|
|
||||||
if (args.collaborative === undefined) {
|
|
||||||
args.collaborative = "standalone";
|
|
||||||
} else {
|
|
||||||
args.collaborative = String(args.collaborative).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.saveCallback) {
|
|
||||||
editorOptions.saveCallback = args.saveCallback;
|
|
||||||
}
|
|
||||||
if (args.cursorAddedCallback) {
|
|
||||||
editorOptions.cursorAddedCallback = args.cursorAddedCallback;
|
|
||||||
}
|
|
||||||
if (args.cursorRemovedCallback) {
|
|
||||||
editorOptions.cursorRemovedCallback = args.cursorRemovedCallback;
|
|
||||||
}
|
|
||||||
if (args.registerCallbackForShutdown) {
|
|
||||||
editorOptions.registerCallbackForShutdown = args.registerCallbackForShutdown;
|
|
||||||
} else {
|
|
||||||
editorOptions.registerCallbackForShutdown = function (callback) {
|
|
||||||
window.onunload = callback;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.loginProcedure) {
|
|
||||||
loginProcedure = args.loginProcedure;
|
|
||||||
}
|
|
||||||
|
|
||||||
// start the editor with network
|
|
||||||
function handleNetworkedSituation() {
|
|
||||||
var joinSession = server.joinSession;
|
|
||||||
|
|
||||||
if (args.joinSession) {
|
|
||||||
joinSession = args.joinSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
loginProcedure(function (sessionId, userId, token) {
|
|
||||||
// if pre-authentication has happened:
|
|
||||||
if (token) {
|
|
||||||
server.setToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
joinSession(userId, sessionId, function(memberId) {
|
|
||||||
createNetworkedEditor(sessionId, memberId, token, editorOptions, function (ed) {
|
|
||||||
if (args.callback) {
|
|
||||||
args.callback(ed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// start the editor without network
|
|
||||||
function handleNonNetworkedSituation() {
|
|
||||||
createLocalEditor(args.docUrl, editorOptions, function (editor) {
|
|
||||||
if (args.callback) {
|
|
||||||
args.callback(editor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.collaborative === "auto") {
|
|
||||||
runtime.log("detecting network...");
|
|
||||||
detectNetwork(function (state) {
|
|
||||||
if (state === "ready") {
|
|
||||||
runtime.log("... network available.");
|
|
||||||
handleNetworkedSituation();
|
|
||||||
} else {
|
|
||||||
runtime.log("... no network available (" + state + ").");
|
|
||||||
handleNonNetworkedSituation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if ((args.collaborative === "pullbox") ||
|
|
||||||
(args.collaborative === "owncloud")) {
|
|
||||||
runtime.log("starting collaborative editor for ["+args.collaborative+"].");
|
|
||||||
connectNetwork(args.collaborative, function (state) {
|
|
||||||
if (state === "ready") {
|
|
||||||
handleNetworkedSituation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
runtime.log("starting local editor.");
|
|
||||||
handleNonNetworkedSituation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// exposed API
|
|
||||||
return { boot: boot };
|
|
||||||
}());
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
|
|
||||||
*
|
|
||||||
* @licstart
|
|
||||||
* The JavaScript code in this page 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. The code is distributed
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
|
|
||||||
*
|
|
||||||
* As additional permission under GNU AGPL version 3 section 7, you
|
|
||||||
* may distribute non-source (e.g., minimized or compacted) forms of
|
|
||||||
* that code without the copy of the GNU GPL normally required by
|
|
||||||
* section 4, provided you include this license notice and a URL
|
|
||||||
* through which recipients can access the Corresponding Source.
|
|
||||||
*
|
|
||||||
* As a special exception to the AGPL, any HTML file which merely makes function
|
|
||||||
* calls to this code, and for that purpose includes it by reference shall be
|
|
||||||
* deemed a separate work for copyright law purposes. In addition, the copyright
|
|
||||||
* holders of this code give you permission to combine this code with free
|
|
||||||
* software libraries that are released under the GNU LGPL. You may copy and
|
|
||||||
* distribute such a system following the terms of the GNU AGPL for this code
|
|
||||||
* and the LGPL for the libraries. If you modify this code, you may extend this
|
|
||||||
* exception to your version of the code, but you are not obligated to do so.
|
|
||||||
* If you do not wish to do so, delete this exception statement from your
|
|
||||||
* version.
|
|
||||||
*
|
|
||||||
* This license applies to this entire compilation.
|
|
||||||
* @licend
|
|
||||||
* @source: http://www.webodf.org/
|
|
||||||
* @source: http://gitorious.org/webodf/webodf/
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*global define, ops, runtime */
|
|
||||||
|
|
||||||
define("webodf/editor/server/nowjs/sessionList", [], function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
return function NowjsSessionList(nowjsServer) {
|
|
||||||
|
|
||||||
var cachedSessionData = {},
|
|
||||||
subscribers = [];
|
|
||||||
|
|
||||||
function onSessionData(sessionData) {
|
|
||||||
var i,
|
|
||||||
isNew = ! cachedSessionData.hasOwnProperty(sessionData.id);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.assert((i < subscribers.length),
|
|
||||||
"tried to unsubscribe when not subscribed.");
|
|
||||||
|
|
||||||
subscribers.splice(i,1);
|
|
||||||
};
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
var nowObject = nowjsServer.getNowObject();
|
|
||||||
nowObject.onSessionAdded = onSessionData;
|
|
||||||
nowObject.onSessionChanged = onSessionData;
|
|
||||||
nowObject.onSessionRemoved = onSessionRemoved;
|
|
||||||
|
|
||||||
nowObject.getSessionList( function(sessionList) {
|
|
||||||
var idx;
|
|
||||||
runtime.log("get sessions on init:"+sessionList.length);
|
|
||||||
for (idx=0; idx<sessionList.length; idx+=1) {
|
|
||||||
onSessionData(sessionList[idx])
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
};
|
|
||||||
});
|
|
@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
|
||||||
|
*
|
||||||
|
* @licstart
|
||||||
|
* The JavaScript code in this page 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. The code is distributed
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
|
||||||
|
*
|
||||||
|
* As additional permission under GNU AGPL version 3 section 7, you
|
||||||
|
* may distribute non-source (e.g., minimized or compacted) forms of
|
||||||
|
* that code without the copy of the GNU GPL normally required by
|
||||||
|
* section 4, provided you include this license notice and a URL
|
||||||
|
* through which recipients can access the Corresponding Source.
|
||||||
|
*
|
||||||
|
* As a special exception to the AGPL, any HTML file which merely makes function
|
||||||
|
* calls to this code, and for that purpose includes it by reference shall be
|
||||||
|
* deemed a separate work for copyright law purposes. In addition, the copyright
|
||||||
|
* holders of this code give you permission to combine this code with free
|
||||||
|
* software libraries that are released under the GNU LGPL. You may copy and
|
||||||
|
* distribute such a system following the terms of the GNU AGPL for this code
|
||||||
|
* and the LGPL for the libraries. If you modify this code, you may extend this
|
||||||
|
* exception to your version of the code, but you are not obligated to do so.
|
||||||
|
* If you do not wish to do so, delete this exception statement from your
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* This license applies to this entire compilation.
|
||||||
|
* @licend
|
||||||
|
* @source: http://www.webodf.org/
|
||||||
|
* @source: http://gitorious.org/webodf/webodf/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*global runtime, ops*/
|
||||||
|
|
||||||
|
define("webodf/editor/server/pullbox/MemberModel", [], function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @implements ops.MemberModel
|
||||||
|
*/
|
||||||
|
return function PullBoxMemberModel(sessionId, server) {
|
||||||
|
|
||||||
|
var cachedMemberData = {},
|
||||||
|
memberDataSubscribers = {},
|
||||||
|
isServerPullingActivated = false,
|
||||||
|
isServerPullingOpen = true,
|
||||||
|
serverPullingTimeoutId = null,
|
||||||
|
isInstantPullingRequested = false,
|
||||||
|
isPulling = false,
|
||||||
|
/**@const*/pullingIntervall = 20000;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Object} memberData
|
||||||
|
*/
|
||||||
|
function cacheMemberDatum(memberData) {
|
||||||
|
var subscribers,
|
||||||
|
i;
|
||||||
|
|
||||||
|
// notify all subscribers who are interested in this data
|
||||||
|
subscribers = memberDataSubscribers[memberData.memberid];
|
||||||
|
if (subscribers) {
|
||||||
|
// cache
|
||||||
|
cachedMemberData[memberData.memberid] = memberData;
|
||||||
|
|
||||||
|
for (i = 0; i < subscribers.length; i += 1) {
|
||||||
|
subscribers[i](memberData.memberid, memberData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pullMemberData() {
|
||||||
|
var i,
|
||||||
|
memberIds = Object.keys(memberDataSubscribers);
|
||||||
|
|
||||||
|
if (!isServerPullingOpen || isPulling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no more timeout or instant pull request in any case
|
||||||
|
serverPullingTimeoutId = null;
|
||||||
|
isInstantPullingRequested = false;
|
||||||
|
// set lock
|
||||||
|
isPulling = true;
|
||||||
|
|
||||||
|
runtime.log("member-list request for : " + memberIds.join(","));
|
||||||
|
|
||||||
|
server.call({
|
||||||
|
command: 'query_memberdata_list',
|
||||||
|
args: {
|
||||||
|
es_id: sessionId,
|
||||||
|
member_ids: memberIds
|
||||||
|
}
|
||||||
|
}, function(responseData) {
|
||||||
|
var response = /**@type {{memberdata_list:Array.<{uid,member_id,display_name,avatar_url,color}>}}*/(runtime.fromJson(responseData)),
|
||||||
|
memberDataList,
|
||||||
|
newMemberData, oldMemberData;
|
||||||
|
|
||||||
|
// unlock
|
||||||
|
isPulling = false;
|
||||||
|
|
||||||
|
// meanwhile closed/disactivated?
|
||||||
|
if (!isServerPullingOpen || !isServerPullingActivated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.log("member-list reply: " + responseData);
|
||||||
|
|
||||||
|
if (response.hasOwnProperty("memberdata_list")) {
|
||||||
|
|
||||||
|
// add/update with all delivered memberdata
|
||||||
|
memberDataList = response.memberdata_list;
|
||||||
|
for (i = 0; i < memberDataList.length; i+=1) {
|
||||||
|
newMemberData = {
|
||||||
|
memberid: memberDataList[i].member_id,
|
||||||
|
fullname: memberDataList[i].display_name,
|
||||||
|
imageurl: memberDataList[i].avatar_url,
|
||||||
|
color: memberDataList[i].color
|
||||||
|
};
|
||||||
|
|
||||||
|
oldMemberData = cachedMemberData.hasOwnProperty(newMemberData.memberid) ? cachedMemberData[newMemberData.memberid] : null;
|
||||||
|
if (!oldMemberData ||
|
||||||
|
oldMemberData.fullname !== newMemberData.fullname ||
|
||||||
|
oldMemberData.imageurl !== newMemberData.imageurl ||
|
||||||
|
oldMemberData.color !== newMemberData.color) {
|
||||||
|
cacheMemberDatum(newMemberData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runtime.log("Meh, memberdata list broken: " + responseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger the next pulling
|
||||||
|
if (isInstantPullingRequested) {
|
||||||
|
pullMemberData();
|
||||||
|
} else {
|
||||||
|
serverPullingTimeoutId = runtime.setTimeout(pullMemberData, pullingIntervall);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates the pulling
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function pullNewMemberData() {
|
||||||
|
// cancel any running pulling timeout
|
||||||
|
if (serverPullingTimeoutId !== null) {
|
||||||
|
runtime.clearTimeout(serverPullingTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
isInstantPullingRequested = true;
|
||||||
|
isServerPullingActivated = true;
|
||||||
|
|
||||||
|
pullMemberData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivates the pulling if there are no more subscribers
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function deactivatePeriodicMemberDataPulling() {
|
||||||
|
var key;
|
||||||
|
|
||||||
|
if (!isServerPullingActivated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there is no more subscription
|
||||||
|
for(key in memberDataSubscribers) {
|
||||||
|
if (memberDataSubscribers.hasOwnProperty(key)) {
|
||||||
|
// still subscribers, cannot deactivate yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isServerPullingActivated = false;
|
||||||
|
// cancel any running pulling timeout
|
||||||
|
if (serverPullingTimeoutId !== null) {
|
||||||
|
runtime.clearTimeout(serverPullingTimeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* callback is called as soon as the memberdata is available and after that
|
||||||
|
* on every memberdata update.
|
||||||
|
* a parameter `null` passed to the callback means that the member is finally
|
||||||
|
* not known.
|
||||||
|
*
|
||||||
|
* @param {!string} memberId
|
||||||
|
* @param {!function(!string, ?Object)} subscriber
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
this.getMemberDetailsAndUpdates = function (memberId, subscriber) {
|
||||||
|
var /**@type{Object}*/
|
||||||
|
memberData = cachedMemberData[memberId],
|
||||||
|
subscribers = memberDataSubscribers[memberId] || [],
|
||||||
|
i;
|
||||||
|
memberDataSubscribers[memberId] = subscribers;
|
||||||
|
|
||||||
|
runtime.assert(subscriber !== undefined, "missing callback");
|
||||||
|
|
||||||
|
// detect double subscription
|
||||||
|
for (i=0; i<subscribers.length; i+=1) {
|
||||||
|
if (subscribers[i] === subscriber) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < subscribers.length) {
|
||||||
|
// already subscribed
|
||||||
|
runtime.log("double subscription request for "+memberId+" in PullBoxMemberModel::getMemberDetailsAndUpdates");
|
||||||
|
} else {
|
||||||
|
// subscribe
|
||||||
|
subscribers.push(subscriber);
|
||||||
|
// query data from server, if not done yet
|
||||||
|
if (subscribers.length === 1) {
|
||||||
|
// TODO: only fetch data for memberId here
|
||||||
|
pullNewMemberData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberData) {
|
||||||
|
// data available from cache
|
||||||
|
subscriber(memberId, memberData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getMemberDetailsAndUpdates subscribes a callback for updates on member details.
|
||||||
|
* this function undoes this subscription.
|
||||||
|
*
|
||||||
|
* @param {!string} memberId
|
||||||
|
* @param {!function(!string, ?Object)} subscriber
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
this.unsubscribeMemberDetailsUpdates = function (memberId, subscriber) {
|
||||||
|
var i,
|
||||||
|
subscribers = memberDataSubscribers[memberId];
|
||||||
|
|
||||||
|
runtime.assert(subscriber!==undefined, "missing subscriber parameter or null");
|
||||||
|
runtime.assert(subscribers,
|
||||||
|
"tried to unsubscribe when no one is subscribed ('" + memberId + "')");
|
||||||
|
if (subscribers) {
|
||||||
|
for (i=0; i<subscribers.length; i+=1) {
|
||||||
|
if (subscribers[i] === subscriber) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.assert((i < subscribers.length),
|
||||||
|
"tried to unsubscribe when not subscribed for memberId '" + memberId + "'");
|
||||||
|
|
||||||
|
subscribers.splice(i,1);
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
if (subscribers.length === 0) {
|
||||||
|
runtime.log("no more subscribers for: "+memberId);
|
||||||
|
delete memberDataSubscribers[memberId];
|
||||||
|
delete cachedMemberData[memberId];
|
||||||
|
deactivatePeriodicMemberDataPulling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a gracefull shutdown of the Member Model
|
||||||
|
* No more network activity is necessary.
|
||||||
|
*/
|
||||||
|
this.close = function (cb) {
|
||||||
|
isServerPullingOpen = false;
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
runtime.assert(server.networkStatus() === "ready", "network not ready");
|
||||||
|
};
|
||||||
|
});
|
@ -0,0 +1,508 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
|
||||||
|
*
|
||||||
|
* @licstart
|
||||||
|
* The JavaScript code in this page 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. The code is distributed
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
|
||||||
|
*
|
||||||
|
* As additional permission under GNU AGPL version 3 section 7, you
|
||||||
|
* may distribute non-source (e.g., minimized or compacted) forms of
|
||||||
|
* that code without the copy of the GNU GPL normally required by
|
||||||
|
* section 4, provided you include this license notice and a URL
|
||||||
|
* through which recipients can access the Corresponding Source.
|
||||||
|
*
|
||||||
|
* As a special exception to the AGPL, any HTML file which merely makes function
|
||||||
|
* calls to this code, and for that purpose includes it by reference shall be
|
||||||
|
* deemed a separate work for copyright law purposes. In addition, the copyright
|
||||||
|
* holders of this code give you permission to combine this code with free
|
||||||
|
* software libraries that are released under the GNU LGPL. You may copy and
|
||||||
|
* distribute such a system following the terms of the GNU AGPL for this code
|
||||||
|
* and the LGPL for the libraries. If you modify this code, you may extend this
|
||||||
|
* exception to your version of the code, but you are not obligated to do so.
|
||||||
|
* If you do not wish to do so, delete this exception statement from your
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* This license applies to this entire compilation.
|
||||||
|
* @licend
|
||||||
|
* @source: http://www.webodf.org/
|
||||||
|
* @source: http://gitorious.org/webodf/webodf/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*global runtime, ops*/
|
||||||
|
|
||||||
|
define("webodf/editor/server/pullbox/OperationRouter", [], function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
runtime.loadClass("ops.OperationTransformer");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var operationFactory,
|
||||||
|
singleTimeCallbackOnCompleteServerOpSpecsPlay,
|
||||||
|
/**@type{function(!ops.Operation)}*/
|
||||||
|
playbackFunction,
|
||||||
|
/**@type{?{active:!boolean}}*/
|
||||||
|
pullingTimeOutFlag = null,
|
||||||
|
/**@type{!boolean}*/
|
||||||
|
triggerPushingOpsActivated = false,
|
||||||
|
/**@type{!boolean}*/
|
||||||
|
playUnplayedServerOpSpecsTriggered = false,
|
||||||
|
/**@type{!boolean}*/
|
||||||
|
syncLock = false,
|
||||||
|
/**@type{!boolean}*/
|
||||||
|
hasUnresolvableConflict = false,
|
||||||
|
/**@type{!boolean}*/
|
||||||
|
syncingBlocked = false,
|
||||||
|
/** @type {!string} id of latest op stack state known on the server */
|
||||||
|
lastServerSeq = "",
|
||||||
|
/** @type {!Array.<!Object>} ops created since the last sync call to the server */
|
||||||
|
unsyncedClientOpspecQueue = [],
|
||||||
|
/** @type {!Array.<!Object>} ops created since the last sync call to the server */
|
||||||
|
unplayedServerOpspecQueue = [],
|
||||||
|
/** @type {!Array.<!function(!boolean):undefined>} ops created since the last sync call to the server */
|
||||||
|
hasLocalUnsyncedOpsStateSubscribers = [],
|
||||||
|
/**@type{!boolean}*/
|
||||||
|
hasLocalUnsyncedOps = false,
|
||||||
|
/**@type{!boolean} tells if any local ops have been modifying ops */
|
||||||
|
hasPushedModificationOps = false,
|
||||||
|
operationTransformer = new ops.OperationTransformer(),
|
||||||
|
/**@const*/replayTime = 500,
|
||||||
|
/**@const*/pushingIntervall = 3000,
|
||||||
|
/**@const*/pullingIntervall = 8000;
|
||||||
|
|
||||||
|
|
||||||
|
function updateHasLocalUnsyncedOpsState() {
|
||||||
|
var i,
|
||||||
|
hasLocalUnsyncedOpsNow = (unsyncedClientOpspecQueue.length > 0);
|
||||||
|
|
||||||
|
// no change?
|
||||||
|
if (hasLocalUnsyncedOps === hasLocalUnsyncedOpsNow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasLocalUnsyncedOps = hasLocalUnsyncedOpsNow;
|
||||||
|
for (i=0; i<hasLocalUnsyncedOpsStateSubscribers.length; i+=1) {
|
||||||
|
hasLocalUnsyncedOpsStateSubscribers[i](hasLocalUnsyncedOps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Array.<!Object>} opspecs
|
||||||
|
* @return {!Array.<!Object>}
|
||||||
|
*/
|
||||||
|
function compressOpSpecs(opspecs) {
|
||||||
|
var i, j, op,
|
||||||
|
result = [];
|
||||||
|
|
||||||
|
i = 0;
|
||||||
|
while (i < opspecs.length) {
|
||||||
|
// use factory to create an instance, and playback!
|
||||||
|
op = operationFactory.create(opspecs[i]);
|
||||||
|
// is known op and can do merge?
|
||||||
|
if (op !== null && op.merge) {
|
||||||
|
// go over the following and try to merge them
|
||||||
|
for (j = i+1; j < opspecs.length; j += 1) {
|
||||||
|
if (!op.merge(opspecs[j])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
runtime.log("Merged: "+opspecs[i].optype+" with "+opspecs[j].optype);
|
||||||
|
}
|
||||||
|
// add the resulting op to the results
|
||||||
|
result.push(op.spec());
|
||||||
|
// and continue with the one which could not be merged, or behind end
|
||||||
|
i = j;
|
||||||
|
} else {
|
||||||
|
// just pass on
|
||||||
|
result.push(opspecs[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runtime.log("Merged: from "+opspecs.length+" to "+result.length+" specs");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function playUnplayedServerOpSpecs() {
|
||||||
|
/**
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function doPlayUnplayedServerOpSpecs() {
|
||||||
|
var opspec, op, startTime;
|
||||||
|
|
||||||
|
playUnplayedServerOpSpecsTriggered = false;
|
||||||
|
|
||||||
|
// take start time
|
||||||
|
startTime = (new Date()).getTime();
|
||||||
|
|
||||||
|
// apply as much as possible in the given time
|
||||||
|
while (unplayedServerOpspecQueue.length > 0) {
|
||||||
|
// time over?
|
||||||
|
if ((new Date().getTime()) - 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) {
|
||||||
|
playbackFunction(op);
|
||||||
|
} else {
|
||||||
|
runtime.log("ignoring invalid incoming opspec: " + opspec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// still unplayed opspecs?
|
||||||
|
if (unplayedServerOpspecQueue.length > 0) {
|
||||||
|
// let other events be handled. then continue
|
||||||
|
playUnplayedServerOpSpecsTriggered = true;
|
||||||
|
runtime.getWindow().setTimeout(doPlayUnplayedServerOpSpecs, 1);
|
||||||
|
} else {
|
||||||
|
// This is such a sad hack. But there is no other way for now to inject
|
||||||
|
// the callback after the initial replay.
|
||||||
|
if (singleTimeCallbackOnCompleteServerOpSpecsPlay) {
|
||||||
|
singleTimeCallbackOnCompleteServerOpSpecsPlay();
|
||||||
|
singleTimeCallbackOnCompleteServerOpSpecsPlay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playUnplayedServerOpSpecsTriggered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doPlayUnplayedServerOpSpecs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array.<!Object>} opspecs
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function receiveOpSpecsFromNetwork(opspecs) {
|
||||||
|
// append to existing unplayed
|
||||||
|
unplayedServerOpspecQueue = unplayedServerOpspecQueue.concat(opspecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.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.opsB.length; i += 1) {
|
||||||
|
unplayedServerOpspecQueue.push(transformResult.opsB[i].spec());
|
||||||
|
}
|
||||||
|
|
||||||
|
// store opspecs of all transformed client opspecs
|
||||||
|
unsyncedClientOpspecQueue = [];
|
||||||
|
for (i = 0; i < transformResult.opsA.length; i += 1) {
|
||||||
|
unsyncedClientOpspecQueue.push(transformResult.opsA[i].spec());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function syncOps() {
|
||||||
|
function triggerPullingOps() {
|
||||||
|
var flag = {active: true};
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function doSyncOps() {
|
||||||
|
var syncedClientOpspecs;
|
||||||
|
|
||||||
|
if (syncLock || hasUnresolvableConflict) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: hack, remove
|
||||||
|
if (syncingBlocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncLock = true;
|
||||||
|
|
||||||
|
// take specs from queue, if any
|
||||||
|
syncedClientOpspecs = unsyncedClientOpspecQueue;
|
||||||
|
unsyncedClientOpspecQueue = [];
|
||||||
|
|
||||||
|
server.call({
|
||||||
|
command: 'sync_ops',
|
||||||
|
args: {
|
||||||
|
es_id: sessionId,
|
||||||
|
member_id: memberId,
|
||||||
|
seq_head: String(lastServerSeq),
|
||||||
|
client_ops: syncedClientOpspecs
|
||||||
|
}
|
||||||
|
}, function(responseData) {
|
||||||
|
var shouldRetryInstantly = false,
|
||||||
|
response = /** @type{{result:string, head_seq:string, ops:Array.<!Object>}} */(runtime.fromJson(responseData));
|
||||||
|
|
||||||
|
// TODO: hack, remove
|
||||||
|
if (syncingBlocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(compressOpSpecs(response.ops));
|
||||||
|
} 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(compressOpSpecs(response.ops));
|
||||||
|
}
|
||||||
|
// and note server state
|
||||||
|
lastServerSeq = response.head_seq;
|
||||||
|
}
|
||||||
|
} else if (response.result === "added") {
|
||||||
|
runtime.log("All added to server");
|
||||||
|
// 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);
|
||||||
|
// 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(compressOpSpecs(response.ops));
|
||||||
|
// and note server state
|
||||||
|
lastServerSeq = response.head_seq;
|
||||||
|
// try again instantly
|
||||||
|
if (!hasUnresolvableConflict) {
|
||||||
|
shouldRetryInstantly = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runtime.assert(false, "Unexpected result on sync-ops call: "+response.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncLock = false;
|
||||||
|
|
||||||
|
if (hasUnresolvableConflict) {
|
||||||
|
// TODO: offer option to reload session automatically?
|
||||||
|
runtime.assert(false,
|
||||||
|
"Sorry to tell:\n" +
|
||||||
|
"we hit a pair of operations in a state which yet need to be supported for transformation against each other.\n" +
|
||||||
|
"Client disconnected from session, no further editing accepted.\n\n" +
|
||||||
|
"Please reconnect manually for now.");
|
||||||
|
} else {
|
||||||
|
if (shouldRetryInstantly) {
|
||||||
|
doSyncOps();
|
||||||
|
} else {
|
||||||
|
runtime.log("Preparing next: " + (unsyncedClientOpspecQueue.length === 0));
|
||||||
|
// prepare next sync
|
||||||
|
// nothing to push right now?
|
||||||
|
if (unsyncedClientOpspecQueue.length === 0) {
|
||||||
|
triggerPullingOps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playUnplayedServerOpSpecs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
doSyncOps();
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerPushingOps() {
|
||||||
|
if (syncLock || triggerPushingOpsActivated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerPushingOpsActivated = true;
|
||||||
|
// disable current pulling timeout
|
||||||
|
if (pullingTimeOutFlag) {
|
||||||
|
pullingTimeOutFlag.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.getWindow().setTimeout(function() {
|
||||||
|
runtime.log("Pushing activated");
|
||||||
|
triggerPushingOpsActivated = false;
|
||||||
|
syncOps();
|
||||||
|
}, pushingIntervall);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestReplay = function (done_cb) {
|
||||||
|
singleTimeCallbackOnCompleteServerOpSpecsPlay = done_cb;
|
||||||
|
syncOps();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the factory to use to create operation instances from operation specs.
|
||||||
|
*
|
||||||
|
* @param {!ops.OperationFactory} f
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
this.setOperationFactory = function (f) {
|
||||||
|
operationFactory = f;
|
||||||
|
operationTransformer.setOperationFactory(f);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the method which should be called to apply operations.
|
||||||
|
*
|
||||||
|
* @param {!function(!ops.Operation)} playback_func
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
this.setPlaybackFunction = function (playback_func) {
|
||||||
|
playbackFunction = playback_func;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Brings the locally created operations into the game.
|
||||||
|
*
|
||||||
|
* @param {!ops.Operation} op
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
this.push = function (op) {
|
||||||
|
var timedOp,
|
||||||
|
opspec = op.spec();
|
||||||
|
|
||||||
|
if (hasUnresolvableConflict) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// note if any local ops modified TODO: find less fragile way, perhaps have the operationFactory check it?
|
||||||
|
hasPushedModificationOps = hasPushedModificationOps || !/^(AddCursor|RemoveCursor)$/.test(opspec.optype);
|
||||||
|
|
||||||
|
// apply locally
|
||||||
|
opspec.timestamp = (new Date()).getTime();
|
||||||
|
timedOp = operationFactory.create(opspec);
|
||||||
|
|
||||||
|
playbackFunction(timedOp);
|
||||||
|
|
||||||
|
// send to server
|
||||||
|
unsyncedClientOpspecQueue.push(opspec);
|
||||||
|
|
||||||
|
triggerPushingOps();
|
||||||
|
|
||||||
|
updateHasLocalUnsyncedOpsState();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 writeSessionStateToFile() {
|
||||||
|
function cbSuccess(fileData) {
|
||||||
|
server.writeSessionStateToFile(sessionId, memberId, lastServerSeq, fileData, cb);
|
||||||
|
};
|
||||||
|
odfContainer.createByteArray(cbSuccess, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: hack, rather add callback to syncOps for success and properly close things
|
||||||
|
syncOps();
|
||||||
|
runtime.getWindow().setTimeout(function() {
|
||||||
|
syncingBlocked = true;
|
||||||
|
if (hasPushedModificationOps) {
|
||||||
|
writeSessionStateToFile();
|
||||||
|
} else {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getHasLocalUnsyncedOpsAndUpdates = function (subscriber) {
|
||||||
|
var i;
|
||||||
|
|
||||||
|
// detect double subscription
|
||||||
|
for (i=0; i<hasLocalUnsyncedOpsStateSubscribers.length; i+=1) {
|
||||||
|
if (subscribers[i] === subscriber) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < hasLocalUnsyncedOpsStateSubscribers.length) {
|
||||||
|
// already subscribed
|
||||||
|
runtime.log("double subscription request in PullBoxMemberModel::getHasLocalUnsyncedOpsAndUpdates");
|
||||||
|
} else {
|
||||||
|
// subscribe
|
||||||
|
hasLocalUnsyncedOpsStateSubscribers.push(subscriber);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber(hasLocalUnsyncedOps);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*jslint emptyblock: true, unparam: true*/
|
||||||
|
this.unsubscribeHasLocalUnsyncedOpsUpdates = function (subscriber) {
|
||||||
|
var i;
|
||||||
|
|
||||||
|
for (i=0; i<hasLocalUnsyncedOpsStateSubscribers.length; i+=1) {
|
||||||
|
if (hasLocalUnsyncedOpsStateSubscribers[i] === subscriber) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.assert((i < hasLocalUnsyncedOpsStateSubscribers.length),
|
||||||
|
"tried to unsubscribe when not subscribed in PullBoxMemberModel::getHasLocalUnsyncedOpsAndUpdates");
|
||||||
|
|
||||||
|
hasLocalUnsyncedOpsStateSubscribers.splice(i,1);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
@ -0,0 +1,293 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
|
||||||
|
*
|
||||||
|
* @licstart
|
||||||
|
* The JavaScript code in this page 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. The code is distributed
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
|
||||||
|
*
|
||||||
|
* As additional permission under GNU AGPL version 3 section 7, you
|
||||||
|
* may distribute non-source (e.g., minimized or compacted) forms of
|
||||||
|
* that code without the copy of the GNU GPL normally required by
|
||||||
|
* section 4, provided you include this license notice and a URL
|
||||||
|
* through which recipients can access the Corresponding Source.
|
||||||
|
*
|
||||||
|
* As a special exception to the AGPL, any HTML file which merely makes function
|
||||||
|
* calls to this code, and for that purpose includes it by reference shall be
|
||||||
|
* deemed a separate work for copyright law purposes. In addition, the copyright
|
||||||
|
* holders of this code give you permission to combine this code with free
|
||||||
|
* software libraries that are released under the GNU LGPL. You may copy and
|
||||||
|
* distribute such a system following the terms of the GNU AGPL for this code
|
||||||
|
* and the LGPL for the libraries. If you modify this code, you may extend this
|
||||||
|
* exception to your version of the code, but you are not obligated to do so.
|
||||||
|
* If you do not wish to do so, delete this exception statement from your
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* This license applies to this entire compilation.
|
||||||
|
* @licend
|
||||||
|
* @source: http://www.webodf.org/
|
||||||
|
* @source: http://gitorious.org/webodf/webodf/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*global XMLHttpRequest, runtime, core, ops*/
|
||||||
|
|
||||||
|
define("webodf/editor/server/pullbox/Server", [], function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
runtime.loadClass("core.Base64");
|
||||||
|
runtime.loadClass("core.ByteArrayWriter");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @implements ops.Server
|
||||||
|
* @param {{url:string}} args
|
||||||
|
*/
|
||||||
|
return function PullBoxServer(args) {
|
||||||
|
|
||||||
|
var self = this,
|
||||||
|
token,
|
||||||
|
base64 = new core.Base64();
|
||||||
|
|
||||||
|
args = args || {};
|
||||||
|
args.url = args.url || "/WSER";
|
||||||
|
args.sessionStateToFileUrl = args.sessionStateToFileUrl || "/SS2F"
|
||||||
|
|
||||||
|
this.getGenesisUrl = function (sessionId) {
|
||||||
|
return "/session/" + sessionId + "/genesis";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Object} message
|
||||||
|
* @param {!function(!string)} cb
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function call(message, cb) {
|
||||||
|
var xhr = new XMLHttpRequest(),
|
||||||
|
byteArrayWriter = new core.ByteArrayWriter("utf8"),
|
||||||
|
messageString = JSON.stringify(message),
|
||||||
|
data;
|
||||||
|
|
||||||
|
function handleResult() {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if ((xhr.status < 200 || xhr.status >= 300) && xhr.status === 0) {
|
||||||
|
// report error
|
||||||
|
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
|
||||||
|
byteArrayWriter.appendString(messageString);
|
||||||
|
// byteArrayWriter.appendByteArray(zipData);
|
||||||
|
data = byteArrayWriter.getByteArray();
|
||||||
|
|
||||||
|
// do the request
|
||||||
|
xhr.open('POST', args.url, true);
|
||||||
|
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 (data.buffer && !xhr.sendAsBinary) {
|
||||||
|
data = data.buffer; // webkit supports sending an ArrayBuffer
|
||||||
|
} else {
|
||||||
|
// encode into a string, this works in FireFox >= 3
|
||||||
|
data = runtime.byteArrayToString(data, "binary");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (xhr.sendAsBinary) {
|
||||||
|
xhr.sendAsBinary(data);
|
||||||
|
} else {
|
||||||
|
xhr.send(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
runtime.log("Problem with calling server: " + e + " " + data);
|
||||||
|
cb(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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!string} userId
|
||||||
|
* @param {!string} sessionId
|
||||||
|
* @param {!function(!string)} successCb
|
||||||
|
* @param {function()=} failCb
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
this.joinSession = function (userId, sessionId, successCb, failCb) {
|
||||||
|
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 {
|
||||||
|
if (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 {
|
||||||
|
if (failCb) {
|
||||||
|
failCb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!string} sessionId
|
||||||
|
* @param {!string} memberId
|
||||||
|
* @param {!string} seqHead
|
||||||
|
* @param {function()=} 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);
|
||||||
|
xhr.setRequestHeader("x-webodf-es_id", sessionId);
|
||||||
|
xhr.setRequestHeader("x-webodf-member_id", memberId);
|
||||||
|
xhr.setRequestHeader("x-webodf-seq_head", 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue