mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-11-23 18:17:20 +00:00
062822d45c
We don’t need to load tiddlers because they will all have been loaded during bootup, or been created through the file system adaptor.
485 lines
14 KiB
JavaScript
485 lines
14 KiB
JavaScript
/*\
|
|
title: $:/core/modules/syncer.js
|
|
type: application/javascript
|
|
module-type: global
|
|
|
|
The syncer transfers content to and from data sources using syncadaptor modules.
|
|
|
|
\*/
|
|
(function(){
|
|
|
|
/*jslint node: true, browser: true */
|
|
/*global $tw: false */
|
|
"use strict";
|
|
|
|
/*
|
|
Instantiate the syncer with the following options:
|
|
wiki: wiki to be synced
|
|
*/
|
|
function Syncer(options) {
|
|
var self = this;
|
|
this.wiki = options.wiki;
|
|
// Find a working syncadaptor
|
|
this.syncadaptor = undefined;
|
|
$tw.modules.forEachModuleOfType("syncadaptor",function(title,module) {
|
|
if(!self.syncadaptor && module.adaptorClass) {
|
|
self.syncadaptor = new module.adaptorClass(self);
|
|
}
|
|
});
|
|
// Only do anything if we've got a syncadaptor
|
|
if(this.syncadaptor) {
|
|
this.init();
|
|
}
|
|
}
|
|
|
|
/*
|
|
Error handling
|
|
*/
|
|
Syncer.prototype.showError = function(error) {
|
|
alert("Syncer error: " + error);
|
|
$tw.utils.log("Syncer error: " + error);
|
|
};
|
|
|
|
/*
|
|
Message logging
|
|
*/
|
|
Syncer.prototype.log = function(/* arguments */) {
|
|
var args = Array.prototype.slice.call(arguments,0);
|
|
args[0] = "Syncer: " + args[0];
|
|
// Temporarily disable logging to help the wood vs. trees situation; we need better filtering of log messages
|
|
//$tw.utils.log.apply(null,args);
|
|
};
|
|
|
|
/*
|
|
Constants
|
|
*/
|
|
Syncer.prototype.titleIsLoggedIn = "$:/status/IsLoggedIn";
|
|
Syncer.prototype.titleUserName = "$:/status/UserName";
|
|
Syncer.prototype.taskTimerInterval = 1 * 1000; // Interval for sync timer
|
|
Syncer.prototype.throttleInterval = 1 * 1000; // Defer saving tiddlers if they've changed in the last 1s...
|
|
Syncer.prototype.fallbackInterval = 10 * 1000; // Unless the task is older than 10s
|
|
Syncer.prototype.pollTimerInterval = 60 * 1000; // Interval for polling for changes from the adaptor
|
|
|
|
/*
|
|
Initialise the syncer
|
|
*/
|
|
Syncer.prototype.init = function() {
|
|
var self = this;
|
|
// Hashmap by title of {revision:,changeCount:,adaptorInfo:}
|
|
this.tiddlerInfo = {};
|
|
// Record information for known tiddlers
|
|
this.wiki.forEachTiddler(function(title,tiddler) {
|
|
if(tiddler.fields["revision"]) {
|
|
self.tiddlerInfo[title] = {
|
|
revision: tiddler.fields["revision"],
|
|
adaptorInfo: self.syncadaptor.getTiddlerInfo(tiddler),
|
|
changeCount: self.wiki.getChangeCount(title)
|
|
}
|
|
}
|
|
});
|
|
// Tasks are {type: "load"/"save"/"delete", title:, queueTime:, lastModificationTime:}
|
|
this.taskQueue = {}; // Hashmap of tasks to be performed
|
|
this.taskInProgress = {}; // Hash of tasks in progress
|
|
this.taskTimerId = null; // Timer for task dispatch
|
|
this.pollTimerId = null; // Timer for polling server
|
|
// Listen out for changes to tiddlers
|
|
this.wiki.addEventListener("change",function(changes) {
|
|
self.syncToServer(changes);
|
|
});
|
|
// Listen out for lazyLoad events
|
|
this.wiki.addEventListener("lazyLoad",function(title) {
|
|
self.handleLazyLoadEvent(title);
|
|
});
|
|
// Listen out for login/logout/refresh events in the browser
|
|
if($tw.browser) {
|
|
document.addEventListener("tw-login",function(event) {
|
|
self.handleLoginEvent(event);
|
|
},false);
|
|
document.addEventListener("tw-logout",function(event) {
|
|
self.handleLogoutEvent(event);
|
|
},false);
|
|
document.addEventListener("tw-server-refresh",function(event) {
|
|
self.handleRefreshEvent(event);
|
|
},false);
|
|
}
|
|
// Get the login status
|
|
this.getStatus(function (err,isLoggedIn) {
|
|
if(isLoggedIn) {
|
|
// Do a sync from the server
|
|
self.syncFromServer();
|
|
}
|
|
});
|
|
};
|
|
|
|
/*
|
|
Save an incoming tiddler in the store, and updates the associated tiddlerInfo
|
|
*/
|
|
Syncer.prototype.storeTiddler = function(tiddlerFields) {
|
|
// Save the tiddler
|
|
var tiddler = new $tw.Tiddler(this.wiki.getTiddler(tiddlerFields.title),tiddlerFields);
|
|
this.wiki.addTiddler(tiddler);
|
|
// Save the tiddler revision and changeCount details
|
|
this.tiddlerInfo[tiddlerFields.title] = {
|
|
revision: tiddlerFields.revision,
|
|
adaptorInfo: this.syncadaptor.getTiddlerInfo(tiddler),
|
|
changeCount: this.wiki.getChangeCount(tiddlerFields.title)
|
|
};
|
|
};
|
|
|
|
Syncer.prototype.getStatus = function(callback) {
|
|
var self = this;
|
|
// Check if the adaptor supports getStatus()
|
|
if(this.syncadaptor.getStatus) {
|
|
// Mark us as not logged in
|
|
this.wiki.addTiddler({title: this.titleIsLoggedIn,text: "no"});
|
|
// Get login status
|
|
this.syncadaptor.getStatus(function(err,isLoggedIn,username) {
|
|
if(err) {
|
|
self.showError(err);
|
|
return;
|
|
}
|
|
// Set the various status tiddlers
|
|
self.wiki.addTiddler({title: self.titleIsLoggedIn,text: isLoggedIn ? "yes" : "no"});
|
|
if(isLoggedIn) {
|
|
self.wiki.addTiddler({title: self.titleUserName,text: username});
|
|
} else {
|
|
self.wiki.deleteTiddler(self.titleUserName);
|
|
}
|
|
// Invoke the callback
|
|
if(callback) {
|
|
callback(err,isLoggedIn,username);
|
|
}
|
|
});
|
|
} else {
|
|
callback(null,true,"UNAUTHENTICATED");
|
|
}
|
|
};
|
|
|
|
/*
|
|
Synchronise from the server by reading the skinny tiddler list and queuing up loads for any tiddlers that we don't already have up to date
|
|
*/
|
|
Syncer.prototype.syncFromServer = function() {
|
|
if(this.syncadaptor.getSkinnyTiddlers) {
|
|
this.log("Retrieving skinny tiddler list");
|
|
var self = this;
|
|
if(this.pollTimerId) {
|
|
clearTimeout(this.pollTimerId);
|
|
this.pollTimerId = null;
|
|
}
|
|
this.syncadaptor.getSkinnyTiddlers(function(err,tiddlers) {
|
|
// Trigger another sync
|
|
self.pollTimerId = setTimeout(function() {
|
|
self.pollTimerId = null;
|
|
self.syncFromServer.call(self);
|
|
},self.pollTimerInterval);
|
|
// Check for errors
|
|
if(err) {
|
|
self.log("Error retrieving skinny tiddler list:",err);
|
|
return;
|
|
}
|
|
// Process each incoming tiddler
|
|
for(var t=0; t<tiddlers.length; t++) {
|
|
// Get the incoming tiddler fields, and the existing tiddler
|
|
var tiddlerFields = tiddlers[t],
|
|
incomingRevision = tiddlerFields.revision + "",
|
|
tiddler = self.wiki.getTiddler(tiddlerFields.title),
|
|
tiddlerInfo = self.tiddlerInfo[tiddlerFields.title],
|
|
currRevision = tiddlerInfo ? tiddlerInfo.revision : null;
|
|
// Ignore the incoming tiddler if it's the same as the revision we've already got
|
|
if(currRevision !== incomingRevision) {
|
|
// Do a full load if we've already got a fat version of the tiddler
|
|
if(tiddler && tiddler.fields.text !== undefined) {
|
|
// Do a full load of this tiddler
|
|
self.enqueueSyncTask({
|
|
type: "load",
|
|
title: tiddlerFields.title
|
|
});
|
|
} else {
|
|
// Load the skinny version of the tiddler
|
|
self.storeTiddler(tiddlerFields);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
/*
|
|
Synchronise a set of changes to the server
|
|
*/
|
|
Syncer.prototype.syncToServer = function(changes) {
|
|
var self = this,
|
|
now = new Date();
|
|
$tw.utils.each(changes,function(change,title,object) {
|
|
// Ignore the change if it is a shadow tiddler
|
|
if((change.deleted && $tw.utils.hop(self.tiddlerInfo,title)) || (!change.deleted && self.wiki.tiddlerExists(title))) {
|
|
// Queue a task to sync this tiddler
|
|
self.enqueueSyncTask({
|
|
type: change.deleted ? "delete" : "save",
|
|
title: title
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
/*
|
|
Lazily load a skinny tiddler if we can
|
|
*/
|
|
Syncer.prototype.handleLazyLoadEvent = function(title) {
|
|
// Queue up a sync task to load this tiddler
|
|
this.enqueueSyncTask({
|
|
type: "load",
|
|
title: title
|
|
});
|
|
};
|
|
|
|
/*
|
|
Dispay a password prompt and allow the user to login
|
|
*/
|
|
Syncer.prototype.handleLoginEvent = function() {
|
|
var self = this;
|
|
this.getStatus(function(err,isLoggedIn,username) {
|
|
if(!isLoggedIn) {
|
|
$tw.passwordPrompt.createPrompt({
|
|
serviceName: "Login to TiddlySpace",
|
|
callback: function(data) {
|
|
self.login(data.username,data.password,function(err,isLoggedIn) {
|
|
self.syncFromServer();
|
|
});
|
|
return true; // Get rid of the password prompt
|
|
}
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
/*
|
|
Attempt to login to TiddlyWeb.
|
|
username: username
|
|
password: password
|
|
callback: invoked with arguments (err,isLoggedIn)
|
|
*/
|
|
Syncer.prototype.login = function(username,password,callback) {
|
|
this.log("Attempting to login as",username);
|
|
var self = this;
|
|
if(this.syncadaptor.login) {
|
|
this.syncadaptor.login(username,password,function(err) {
|
|
if(err) {
|
|
return callback(err);
|
|
}
|
|
self.getStatus(function(err,isLoggedIn,username) {
|
|
if(callback) {
|
|
callback(null,isLoggedIn);
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
callback(null,true);
|
|
}
|
|
};
|
|
|
|
/*
|
|
Attempt to log out of TiddlyWeb
|
|
*/
|
|
Syncer.prototype.handleLogoutEvent = function() {
|
|
this.log("Attempting to logout");
|
|
var self = this;
|
|
if(this.syncadaptor.logout) {
|
|
this.syncadaptor.logout(function(err) {
|
|
if(err) {
|
|
self.showError(err);
|
|
} else {
|
|
self.getStatus();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
/*
|
|
Immediately refresh from the server
|
|
*/
|
|
Syncer.prototype.handleRefreshEvent = function() {
|
|
this.syncFromServer();
|
|
};
|
|
|
|
/*
|
|
Queue up a sync task. If there is already a pending task for the tiddler, just update the last modification time
|
|
*/
|
|
Syncer.prototype.enqueueSyncTask = function(task) {
|
|
var self = this,
|
|
now = new Date();
|
|
// Set the timestamps on this task
|
|
task.queueTime = now;
|
|
task.lastModificationTime = now;
|
|
// Fill in some tiddlerInfo if the tiddler is one we haven't seen before
|
|
if(!$tw.utils.hop(this.tiddlerInfo,task.title)) {
|
|
this.tiddlerInfo[task.title] = {
|
|
revision: null,
|
|
adaptorInfo: {},
|
|
changeCount: -1
|
|
}
|
|
}
|
|
// Bail if this is a save and the tiddler is already at the changeCount that the server has
|
|
if(task.type === "save" && this.wiki.getChangeCount(task.title) <= this.tiddlerInfo[task.title].changeCount) {
|
|
return;
|
|
}
|
|
// Check if this tiddler is already in the queue
|
|
if($tw.utils.hop(this.taskQueue,task.title)) {
|
|
this.log("Re-queueing up sync task with type:",task.type,"title:",task.title);
|
|
var existingTask = this.taskQueue[task.title];
|
|
// If so, just update the last modification time
|
|
existingTask.lastModificationTime = task.lastModificationTime;
|
|
// If the new task is a save then we upgrade the existing task to a save. Thus a pending load is turned into a save if the tiddler changes locally in the meantime. But a pending save is not modified to become a load
|
|
if(task.type === "save" || task.type === "delete") {
|
|
existingTask.type = task.type;
|
|
}
|
|
} else {
|
|
this.log("Queuing up sync task with type:",task.type,"title:",task.title);
|
|
// If it is not in the queue, insert it
|
|
this.taskQueue[task.title] = task;
|
|
}
|
|
// Process the queue
|
|
$tw.utils.nextTick(function() {self.processTaskQueue.call(self);});
|
|
};
|
|
|
|
/*
|
|
Return the number of tasks in progress
|
|
*/
|
|
Syncer.prototype.numTasksInProgress = function() {
|
|
return $tw.utils.count(this.taskInProgress);
|
|
};
|
|
|
|
/*
|
|
Return the number of tasks in the queue
|
|
*/
|
|
Syncer.prototype.numTasksInQueue = function() {
|
|
return $tw.utils.count(this.taskQueue);
|
|
};
|
|
|
|
/*
|
|
Trigger a timeout if one isn't already outstanding
|
|
*/
|
|
Syncer.prototype.triggerTimeout = function() {
|
|
var self = this;
|
|
if(!this.taskTimerId) {
|
|
this.taskTimerId = setTimeout(function() {
|
|
self.taskTimerId = null;
|
|
self.processTaskQueue.call(self);
|
|
},self.taskTimerInterval);
|
|
}
|
|
};
|
|
|
|
/*
|
|
Process the task queue, performing the next task if appropriate
|
|
*/
|
|
Syncer.prototype.processTaskQueue = function() {
|
|
var self = this;
|
|
// Only process a task if we're not already performing a task. If we are already performing a task then we'll dispatch the next one when it completes
|
|
if(this.numTasksInProgress() === 0) {
|
|
// Choose the next task to perform
|
|
var task = this.chooseNextTask();
|
|
// Perform the task if we had one
|
|
if(task) {
|
|
// Remove the task from the queue and add it to the in progress list
|
|
delete this.taskQueue[task.title];
|
|
this.taskInProgress[task.title] = task;
|
|
// Dispatch the task
|
|
this.dispatchTask(task,function(err) {
|
|
if(err) {
|
|
console.log("Sync error while processing '" + task.title + "':\n" + err);
|
|
}
|
|
// Mark that this task is no longer in progress
|
|
delete self.taskInProgress[task.title];
|
|
// Process the next task
|
|
self.processTaskQueue.call(self);
|
|
});
|
|
} else {
|
|
// Make sure we've set a time if there wasn't a task to perform, but we've still got tasks in the queue
|
|
if(this.numTasksInQueue() > 0) {
|
|
this.triggerTimeout();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/*
|
|
Choose the next applicable task
|
|
*/
|
|
Syncer.prototype.chooseNextTask = function() {
|
|
var self = this,
|
|
candidateTask = null,
|
|
now = new Date();
|
|
// Select the best candidate task
|
|
$tw.utils.each(this.taskQueue,function(task,title) {
|
|
// Exclude the task if there's one of the same name in progress
|
|
if($tw.utils.hop(self.taskInProgress,title)) {
|
|
return;
|
|
}
|
|
// Exclude the task if it is a save and the tiddler has been modified recently, but not hit the fallback time
|
|
if(task.type === "save" && (now - task.lastModificationTime) < self.throttleInterval &&
|
|
(now - task.queueTime) < self.fallbackInterval) {
|
|
return;
|
|
}
|
|
// Exclude the task if it is newer than the current best candidate
|
|
if(candidateTask && candidateTask.queueTime < task.queueTime) {
|
|
return;
|
|
}
|
|
// Now this is our best candidate
|
|
candidateTask = task;
|
|
});
|
|
return candidateTask;
|
|
};
|
|
|
|
/*
|
|
Dispatch a task and invoke the callback
|
|
*/
|
|
Syncer.prototype.dispatchTask = function(task,callback) {
|
|
var self = this;
|
|
if(task.type === "save") {
|
|
var changeCount = this.wiki.getChangeCount(task.title),
|
|
tiddler = this.wiki.getTiddler(task.title);
|
|
this.log("Dispatching 'save' task:",task.title);
|
|
this.syncadaptor.saveTiddler(tiddler,function(err,adaptorInfo,revision) {
|
|
if(err) {
|
|
return callback(err);
|
|
}
|
|
// Adjust the info stored about this tiddler
|
|
self.tiddlerInfo[task.title] = {
|
|
changeCount: changeCount,
|
|
adaptorInfo: adaptorInfo,
|
|
revision: revision
|
|
};
|
|
// Invoke the callback
|
|
callback(null);
|
|
});
|
|
} else if(task.type === "load") {
|
|
// Load the tiddler
|
|
this.log("Dispatching 'load' task:",task.title);
|
|
this.syncadaptor.loadTiddler(task.title,function(err,tiddlerFields) {
|
|
if(err) {
|
|
return callback(err);
|
|
}
|
|
// Store the tiddler
|
|
if(tiddlerFields) {
|
|
self.storeTiddler(tiddlerFields);
|
|
}
|
|
// Invoke the callback
|
|
callback(null);
|
|
});
|
|
} else if(task.type === "delete") {
|
|
// Delete the tiddler
|
|
this.log("Dispatching 'delete' task:",task.title);
|
|
this.syncadaptor.deleteTiddler(task.title,function(err) {
|
|
if(err) {
|
|
return callback(err);
|
|
}
|
|
// Invoke the callback
|
|
callback(null);
|
|
});
|
|
}
|
|
};
|
|
|
|
exports.Syncer = Syncer;
|
|
|
|
})();
|