/*\ 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: syncadaptor: reference to syncadaptor to be used wiki: wiki to be synced */ function Syncer(options) { var self = this; this.wiki = options.wiki; this.syncadaptor = options.syncadaptor; // Make a logger this.logger = new $tw.utils.Logger("syncer" + ($tw.browser ? "-browser" : "") + ($tw.node ? "-server" : "")); // 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); }); // Browser event handlers if($tw.browser) { // Set up our beforeunload handler $tw.addUnloadTask(function(event) { var confirmationMessage; if(self.isDirty()) { confirmationMessage = $tw.language.getString("UnsavedChangesWarning"); event.returnValue = confirmationMessage; // Gecko } return confirmationMessage; }); // Listen out for login/logout/refresh events in the browser $tw.rootWidget.addEventListener("tm-login",function() { self.handleLoginEvent(); }); $tw.rootWidget.addEventListener("tm-logout",function() { self.handleLogoutEvent(); }); $tw.rootWidget.addEventListener("tm-server-refresh",function() { self.handleRefreshEvent(); }); } // Listen out for lazyLoad events 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.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), hasBeenLazyLoaded: false }; }); }; /* Create an tiddlerInfo structure if it doesn't already exist */ Syncer.prototype.createTiddlerInfo = function(title) { if(!$tw.utils.hop(this.tiddlerInfo,title)) { this.tiddlerInfo[title] = { revision: null, adaptorInfo: {}, changeCount: -1, hasBeenLazyLoaded: 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); }; /* Update the document body with the class "tc-dirty" if the wiki has unsaved/unsynced changes */ Syncer.prototype.updateDirtyStatus = function() { if($tw.browser) { $tw.utils.toggleClass(document.body,"tc-dirty",this.isDirty()); } }; /* Save an incoming tiddler in the store, and updates the associated tiddlerInfo */ Syncer.prototype.storeTiddler = function(tiddlerFields,hasBeenLazyLoaded) { // 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), hasBeenLazyLoaded: hasBeenLazyLoaded !== undefined ? hasBeenLazyLoaded : true }; }; 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 the next sync self.pollTimerId = setTimeout(function() { self.pollTimerId = null; self.syncFromServer.call(self); },self.pollTimerInterval); // Check for errors if(err) { self.logger.alert($tw.language.getString("Error/RetrievingSkinny") + ":",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); },{ tiddlerInfo: self.tiddlerInfo[task.title] }); } 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,true); } // 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); } delete self.tiddlerInfo[task.title]; // Invoke the callback callback(null); },{ tiddlerInfo: self.tiddlerInfo[task.title] }); } }; exports.Syncer = Syncer; })();