/*\ 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]; $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 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 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; })();