mirror of https://github.com/xwiki-labs/cryptpad
224 lines
7.7 KiB
JavaScript
224 lines
7.7 KiB
JavaScript
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
define([
|
|
'/common/common-util.js',
|
|
], function (Util) {
|
|
var module = {};
|
|
|
|
module.create = function (
|
|
Common,
|
|
saveHandler,
|
|
unsavedChangesHandler) {
|
|
|
|
var exp = {};
|
|
var metadataMgr = Common.getMetadataMgr();
|
|
var privateData = metadataMgr.getPrivateData();
|
|
|
|
var config = privateData.integrationConfig;
|
|
if (!config.autosave) { return; }
|
|
if (typeof(saveHandler) !== "function") {
|
|
throw new Error("Incorrect save handler");
|
|
}
|
|
if (typeof(unsavedChangesHandler) !== "function") {
|
|
throw new Error("Incorrect unsaves changes handler");
|
|
}
|
|
|
|
var debug = console.warn;
|
|
//debug = function () {};
|
|
var execCommand = function () {}; // placeholder
|
|
|
|
var state = {
|
|
changed: false,
|
|
last: +new Date(),
|
|
lastTmp: undefined,
|
|
other: false, // save lock
|
|
me: false // save lock
|
|
};
|
|
|
|
var saveTo;
|
|
var onSettleTo;
|
|
var alreadySaved = false;
|
|
|
|
var BASE_TIMER = config.autosave * 1000;
|
|
var autosaveTimer = BASE_TIMER;
|
|
var SETTLE_TO = 3000;
|
|
var SAVE_TO = Math.min((BASE_TIMER / 2), 10000);
|
|
|
|
const setStateChanged = function(newValue) {
|
|
if (state.changed === newValue) {
|
|
return;
|
|
}
|
|
state.changed = newValue;
|
|
unsavedChangesHandler(state.changed, function() {});
|
|
};
|
|
var requestSave = function () {}; // placeholder
|
|
var saved = function () {}; // placeholder;
|
|
var save = function (id) {
|
|
id = id || Util.uid();
|
|
requestSave(id, function (allowed) {
|
|
if (!allowed) { return; }
|
|
|
|
var onError = function (err) {
|
|
state.me = false;
|
|
// Increase my timer up to 1.5 times the normal timer
|
|
if (autosaveTimer < (BASE_TIMER*1.5)) {
|
|
autosaveTimer += 1000;
|
|
}
|
|
if (!err) { return; } // "err" is undefined in case of timeout
|
|
// If err while saving, warn others
|
|
execCommand('SEND', {
|
|
msg: 'IFAILED',
|
|
uid: id
|
|
}, function (err) {
|
|
if (err) { console.error(err); }
|
|
});
|
|
};
|
|
var myTo = setTimeout(onError, SAVE_TO);
|
|
|
|
saveHandler(function (err) {
|
|
clearTimeout(myTo);
|
|
if (err) { return void onError(err); }
|
|
saved();
|
|
});
|
|
});
|
|
};
|
|
|
|
var onMessage = function (data) {
|
|
if (!data || !data.msg) { return; }
|
|
debug('Integration onMessage', data);
|
|
if (data.msg === "ISAVE") {
|
|
if (state.me) { return; } // I have the lock: abort
|
|
if (state.other && state.other !== data.uid) { return; } // someone else has the lock
|
|
// If state.other === data.uid ==> save failed and someone else tries again
|
|
if (!state.changed) { return; }
|
|
// If !state.other: nobody has the lock, give them
|
|
state.other = data.uid;
|
|
state.lastTmp = +new Date();
|
|
// Add timeout to make sure the save is done
|
|
clearTimeout(saveTo);
|
|
clearTimeout(onSettleTo);
|
|
|
|
setStateChanged(false);
|
|
saveTo = setTimeout(function () {
|
|
// They weren't able to save in time, try ourselves
|
|
var id = state.other;
|
|
state.other = false;
|
|
save(id);
|
|
}, SAVE_TO);
|
|
return;
|
|
}
|
|
if (data.msg === "ISAVED") {
|
|
// Save confirmed: update current state
|
|
state.last = state.lastTmp || +new Date();
|
|
state.other = false;
|
|
state.me = false;
|
|
alreadySaved = true;
|
|
// And clear timeout
|
|
clearTimeout(saveTo);
|
|
clearTimeout(onSettleTo);
|
|
// Make sure pending changes will be save at next interval
|
|
if (state.changed) { exp.changed(); }
|
|
}
|
|
if (data.msg === "IFAILED") {
|
|
if (state.me) { return; } // I already have the lock
|
|
if (state.other !== data.uid) { return; } // Someone else took the lock
|
|
state.other = false;
|
|
if (!state.changed) { return; }
|
|
clearTimeout(saveTo);
|
|
clearTimeout(onSettleTo);
|
|
save();
|
|
}
|
|
};
|
|
|
|
var onEvent = function (obj) {
|
|
var cmd = obj.ev;
|
|
var data = obj.data;
|
|
if (cmd === 'MESSAGE') {
|
|
onMessage(data);
|
|
return;
|
|
}
|
|
};
|
|
|
|
var module = Common.makeUniversal('integration', {
|
|
onEvent: onEvent
|
|
});
|
|
execCommand = module.execCommand;
|
|
|
|
// Request a save lock.
|
|
// Callback with "true" if allowed to save or "false" if someone else
|
|
// is already saving
|
|
requestSave = function (id, cb) {
|
|
if (state.other || state.me) { return void cb(false); } // save in progress
|
|
debug('Integration send ISAVE');
|
|
alreadySaved = false; // someone may have saved while we were waiting for our callback
|
|
execCommand('SEND', {
|
|
msg: 'ISAVE',
|
|
uid: id
|
|
}, function (err) {
|
|
if (err) { console.error(err); return void cb(false); }
|
|
debug('Integration cb ISAVE', !(state.other || state.me || alreadySaved));
|
|
if (state.other || state.me || alreadySaved) {
|
|
// someone else requested before me
|
|
return void cb(false);
|
|
}
|
|
state.me = true;
|
|
state.lastTmp = +new Date();
|
|
setStateChanged(false);
|
|
cb(true);
|
|
});
|
|
};
|
|
saved = function () {
|
|
state.last = state.lastTmp || +new Date();
|
|
state.other = false;
|
|
state.me = false;
|
|
debug('Integration send ISAVED');
|
|
execCommand('SEND', {
|
|
msg: 'ISAVED'
|
|
}, function (err) {
|
|
debug('Integration cb ISAVED');
|
|
if (err) { console.error(err); }
|
|
// Make sure pending changes will be save at next interval
|
|
if (state.changed) { exp.changed(); }
|
|
});
|
|
};
|
|
|
|
var changedTo;
|
|
|
|
// Wait for SETTLE_TO ms without changes to start the saving process
|
|
var addOnSettleTo = function () {
|
|
clearTimeout(onSettleTo);
|
|
onSettleTo = setTimeout(save, SETTLE_TO);
|
|
};
|
|
|
|
exp.changed = function () {
|
|
setStateChanged(true);
|
|
|
|
var timeSinceLastSave = +new Date() - state.last; // in ms
|
|
var to = autosaveTimer - timeSinceLastSave; // negative if we can save
|
|
if (to > 0) { // try again in "to"
|
|
if (!changedTo) { changedTo = setTimeout(exp.changed, to+1); }
|
|
return;
|
|
}
|
|
|
|
// Clear existing timeouts
|
|
clearTimeout(changedTo);
|
|
changedTo = undefined;
|
|
clearTimeout(onSettleTo);
|
|
|
|
// If someone is saving, nothing to do
|
|
if (state.other || state.me) { return; }
|
|
|
|
// We need a save: refresh TO
|
|
addOnSettleTo();
|
|
};
|
|
|
|
return exp;
|
|
};
|
|
|
|
return module;
|
|
});
|
|
|
|
|