/*\ title: $:/core/modules/syncer.js type: application/javascript module-type: global The syncer tracks changes to the store. If a syncadaptor is used then individual tiddlers are synchronised through it. If there is no syncadaptor then the entire wiki is saved via saver 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; // Make a logger this.logger = new $tw.utils.Logger("syncer" + ($tw.browser ? "-browser" : "") + ($tw.node ? "-server" : "")); // 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); } }); // Initialise our savers if($tw.browser) { this.initSavers(); } // Compile the dirty tiddler filter this.filterFn = this.wiki.compileFilter(this.wiki.getTiddlerText(this.titleSyncFilter)); // Record information for known tiddlers this.readTiddlerInfo(); // Tasks are {type: "load"/"save"/"delete", title:, queueTime:, lastModificationTime:} this.taskQueue = {}; // Hashmap of tasks yet 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 if(this.syncadaptor) { this.wiki.addEventListener("lazyLoad",function(title) { self.handleLazyLoadEvent(title); }); } // Get the login status this.getStatus(function (err,isLoggedIn) { // Do a sync from the server self.syncFromServer(); }); } /* Constants */ Syncer.prototype.titleIsLoggedIn = "$:/status/IsLoggedIn"; Syncer.prototype.titleUserName = "$:/status/UserName"; Syncer.prototype.titleSyncFilter = "$:/config/SyncFilter"; Syncer.prototype.titleAutoSave = "$:/config/AutoSave"; Syncer.prototype.titleSavedNotification = "$:/language/Notifications/Save/Done"; 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 /* Read (or re-read) the latest tiddler info from the store */ Syncer.prototype.readTiddlerInfo = function() { // Hashmap by title of {revision:,changeCount:,adaptorInfo:} this.tiddlerInfo = {}; // Record information for known tiddlers var self = this, tiddlers = this.filterFn.call(this.wiki); $tw.utils.each(tiddlers,function(title) { var tiddler = self.wiki.getTiddler(title); self.tiddlerInfo[title] = { revision: tiddler.fields["revision"], adaptorInfo: self.syncadaptor && self.syncadaptor.getTiddlerInfo(tiddler), changeCount: self.wiki.getChangeCount(title) } }); }; /* Select the appropriate saver modules and set them up */ Syncer.prototype.initSavers = function(moduleType) { moduleType = moduleType || "saver"; // Instantiate the available savers this.savers = []; var self = this; $tw.modules.forEachModuleOfType(moduleType,function(title,module) { if(module.canSave(self)) { self.savers.push(module.create(self.wiki)); } }); // Sort the savers into priority order this.savers.sort(function(a,b) { if(a.info.priority < b.info.priority) { return -1; } else { if(a.info.priority > b.info.priority) { return +1; } else { return 0; } } }); }; /* Save the wiki contents. Options are: method: "save" or "download" template: the tiddler containing the template to save downloadType: the content type for the saved file */ Syncer.prototype.saveWiki = function(options) { options = options || {}; var self = this, method = options.method || "save", template = options.template || "$:/core/save/all", downloadType = options.downloadType || "text/plain", text = this.wiki.renderTiddler(downloadType,template), callback = function(err) { if(err) { alert("Error while saving:\n\n" + err); } else { $tw.notifier.display(self.titleSavedNotification); if(options.callback) { options.callback(); } } }; // Ignore autosave if we've got a syncadaptor or autosave is disabled if(method === "autosave") { if(this.syncadaptor || this.wiki.getTiddlerText(this.titleAutoSave,"yes") !== "yes") { return false; } } // Call the highest priority saver that supports this method for(var t=this.savers.length-1; t>=0; t--) { var saver = this.savers[t]; if(saver.info.capabilities.indexOf(method) !== -1 && saver.save(text,method,callback)) { this.logger.log("Saving wiki with method",method,"through saver",saver.info.name); // Clear the task queue if we're saving (rather than downloading) if(method !== "download") { this.readTiddlerInfo(); this.taskQueue = {}; } return true; } } return false; }; /* Checks whether the wiki is dirty (ie the window shouldn't be closed) */ Syncer.prototype.isDirty = function() { return (this.numTasksInQueue() > 0) || (this.numTasksInProgress() > 0); }; /* 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 && 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.logger.alert(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 && this.syncadaptor.getSkinnyTiddlers) { this.logger.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.logger.alert("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 = Date.now(); // 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.logger.log("Dispatching 'save' task:",task.title); if(tiddler) { 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 { this.logger.log(" Not Dispatching 'save' task:",task.title,"tiddler does not exist"); return callback(null); } } else if(task.type === "load") { // Load the tiddler this.logger.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.logger.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; })();