2013-03-17 15:28:49 +00:00
|
|
|
/*\
|
|
|
|
title: $:/core/modules/syncer.js
|
|
|
|
type: application/javascript
|
|
|
|
module-type: global
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
The syncer tracks changes to the store and synchronises them to a remote data store represented as a "sync adaptor"
|
2013-03-17 15:28:49 +00:00
|
|
|
|
|
|
|
\*/
|
|
|
|
(function(){
|
|
|
|
|
|
|
|
/*jslint node: true, browser: true */
|
|
|
|
/*global $tw: false */
|
|
|
|
"use strict";
|
|
|
|
|
2017-02-04 17:25:30 +00:00
|
|
|
/*
|
|
|
|
Defaults
|
|
|
|
*/
|
|
|
|
Syncer.prototype.titleIsLoggedIn = "$:/status/IsLoggedIn";
|
2018-07-18 15:54:43 +00:00
|
|
|
Syncer.prototype.titleIsAnonymous = "$:/status/IsAnonymous";
|
|
|
|
Syncer.prototype.titleIsReadOnly = "$:/status/IsReadOnly";
|
2017-02-04 17:25:30 +00:00
|
|
|
Syncer.prototype.titleUserName = "$:/status/UserName";
|
|
|
|
Syncer.prototype.titleSyncFilter = "$:/config/SyncFilter";
|
2018-10-19 15:32:23 +00:00
|
|
|
Syncer.prototype.titleSyncPollingInterval = "$:/config/SyncPollingInterval";
|
2019-10-25 09:02:57 +00:00
|
|
|
Syncer.prototype.titleSyncDisableLazyLoading = "$:/config/SyncDisableLazyLoading";
|
2017-02-04 17:25:30 +00:00
|
|
|
Syncer.prototype.titleSavedNotification = "$:/language/Notifications/Save/Done";
|
2020-03-30 14:24:05 +00:00
|
|
|
Syncer.prototype.titleSyncThrottleInterval = "$:/config/SyncThrottleInterval";
|
2023-11-24 13:02:09 +00:00
|
|
|
Syncer.prototype.taskTimerInterval = 0.25 * 1000; // Interval for sync timer
|
2017-02-04 17:25:30 +00:00
|
|
|
Syncer.prototype.throttleInterval = 1 * 1000; // Defer saving tiddlers if they've changed in the last 1s...
|
2020-03-30 14:24:05 +00:00
|
|
|
Syncer.prototype.errorRetryInterval = 5 * 1000; // Interval to retry after an error
|
2017-02-04 17:25:30 +00:00
|
|
|
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
|
|
|
|
|
2013-03-17 15:28:49 +00:00
|
|
|
/*
|
|
|
|
Instantiate the syncer with the following options:
|
2014-08-14 10:12:25 +00:00
|
|
|
syncadaptor: reference to syncadaptor to be used
|
2013-03-17 15:28:49 +00:00
|
|
|
wiki: wiki to be synced
|
|
|
|
*/
|
|
|
|
function Syncer(options) {
|
|
|
|
var self = this;
|
|
|
|
this.wiki = options.wiki;
|
2020-03-30 14:24:05 +00:00
|
|
|
// Save parameters
|
2014-08-30 19:44:26 +00:00
|
|
|
this.syncadaptor = options.syncadaptor;
|
2017-09-20 15:28:11 +00:00
|
|
|
this.disableUI = !!options.disableUI;
|
2017-02-04 17:25:30 +00:00
|
|
|
this.titleIsLoggedIn = options.titleIsLoggedIn || this.titleIsLoggedIn;
|
|
|
|
this.titleUserName = options.titleUserName || this.titleUserName;
|
|
|
|
this.titleSyncFilter = options.titleSyncFilter || this.titleSyncFilter;
|
|
|
|
this.titleSavedNotification = options.titleSavedNotification || this.titleSavedNotification;
|
|
|
|
this.taskTimerInterval = options.taskTimerInterval || this.taskTimerInterval;
|
2020-03-30 14:24:05 +00:00
|
|
|
this.throttleInterval = options.throttleInterval || parseInt(this.wiki.getTiddlerText(this.titleSyncThrottleInterval,""),10) || this.throttleInterval;
|
|
|
|
this.errorRetryInterval = options.errorRetryInterval || this.errorRetryInterval;
|
2017-02-04 17:25:30 +00:00
|
|
|
this.fallbackInterval = options.fallbackInterval || this.fallbackInterval;
|
2018-10-19 15:32:23 +00:00
|
|
|
this.pollTimerInterval = options.pollTimerInterval || parseInt(this.wiki.getTiddlerText(this.titleSyncPollingInterval,""),10) || this.pollTimerInterval;
|
2017-09-26 16:10:57 +00:00
|
|
|
this.logging = "logging" in options ? options.logging : true;
|
2014-01-26 18:53:31 +00:00
|
|
|
// Make a logger
|
2017-09-26 16:10:57 +00:00
|
|
|
this.logger = new $tw.utils.Logger("syncer" + ($tw.browser ? "-browser" : "") + ($tw.node ? "-server" : "") + (this.syncadaptor.name ? ("-" + this.syncadaptor.name) : ""),{
|
2020-03-30 14:24:05 +00:00
|
|
|
colour: "cyan",
|
|
|
|
enable: this.logging,
|
|
|
|
saveHistory: true
|
|
|
|
});
|
2020-03-31 09:47:17 +00:00
|
|
|
// Make another logger for connection errors
|
|
|
|
this.loggerConnection = new $tw.utils.Logger("syncer" + ($tw.browser ? "-browser" : "") + ($tw.node ? "-server" : "") + (this.syncadaptor.name ? ("-" + this.syncadaptor.name) : "") + "-connection",{
|
|
|
|
colour: "cyan",
|
|
|
|
enable: this.logging
|
|
|
|
});
|
|
|
|
// Ask the syncadaptor to use the main logger
|
2020-03-30 14:24:05 +00:00
|
|
|
if(this.syncadaptor.setLoggerSaveBuffer) {
|
|
|
|
this.syncadaptor.setLoggerSaveBuffer(this.logger);
|
|
|
|
}
|
2014-02-06 21:36:30 +00:00
|
|
|
// Compile the dirty tiddler filter
|
|
|
|
this.filterFn = this.wiki.compileFilter(this.wiki.getTiddlerText(this.titleSyncFilter));
|
|
|
|
// Record information for known tiddlers
|
|
|
|
this.readTiddlerInfo();
|
2020-03-30 14:24:05 +00:00
|
|
|
this.titlesToBeLoaded = {}; // Hashmap of titles of tiddlers that need loading from the server
|
|
|
|
this.titlesHaveBeenLazyLoaded = {}; // Hashmap of titles of tiddlers that have already been lazily loaded from the server
|
|
|
|
// Timers
|
2014-02-06 21:36:30 +00:00
|
|
|
this.taskTimerId = null; // Timer for task dispatch
|
2020-03-30 14:24:05 +00:00
|
|
|
// Number of outstanding requests
|
|
|
|
this.numTasksInProgress = 0;
|
2023-11-24 13:02:09 +00:00
|
|
|
// True when we want to force an immediate sync from the server
|
|
|
|
this.forceSyncFromServer = false;
|
|
|
|
this.timestampLastSyncFromServer = new Date();
|
2014-02-06 21:36:30 +00:00
|
|
|
// Listen out for changes to tiddlers
|
|
|
|
this.wiki.addEventListener("change",function(changes) {
|
2020-03-30 14:24:05 +00:00
|
|
|
// Filter the changes to just include ones that are being synced
|
|
|
|
var filteredChanges = self.getSyncedTiddlers(function(callback) {
|
|
|
|
$tw.utils.each(changes,function(change,title) {
|
|
|
|
var tiddler = self.wiki.tiddlerExists(title) && self.wiki.getTiddler(title);
|
|
|
|
callback(tiddler,title);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
if(filteredChanges.length > 0) {
|
|
|
|
self.processTaskQueue();
|
|
|
|
} else {
|
2021-07-14 16:16:57 +00:00
|
|
|
// Look for deletions of tiddlers we're already syncing
|
2020-03-30 14:24:05 +00:00
|
|
|
var outstandingDeletion = false
|
|
|
|
$tw.utils.each(changes,function(change,title,object) {
|
|
|
|
if(change.deleted && $tw.utils.hop(self.tiddlerInfo,title)) {
|
|
|
|
outstandingDeletion = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if(outstandingDeletion) {
|
|
|
|
self.processTaskQueue();
|
|
|
|
}
|
|
|
|
}
|
2014-02-06 21:36:30 +00:00
|
|
|
});
|
2014-08-13 19:07:08 +00:00
|
|
|
// Browser event handlers
|
2017-09-20 15:28:11 +00:00
|
|
|
if($tw.browser && !this.disableUI) {
|
2014-08-13 19:07:08 +00:00
|
|
|
// Set up our beforeunload handler
|
2015-05-03 15:23:35 +00:00
|
|
|
$tw.addUnloadTask(function(event) {
|
2014-08-30 19:44:26 +00:00
|
|
|
var confirmationMessage;
|
2014-08-13 18:29:00 +00:00
|
|
|
if(self.isDirty()) {
|
|
|
|
confirmationMessage = $tw.language.getString("UnsavedChangesWarning");
|
|
|
|
event.returnValue = confirmationMessage; // Gecko
|
|
|
|
}
|
|
|
|
return confirmationMessage;
|
2015-05-03 15:23:35 +00:00
|
|
|
});
|
2014-08-13 19:07:08 +00:00
|
|
|
// Listen out for login/logout/refresh events in the browser
|
2020-10-25 16:33:44 +00:00
|
|
|
$tw.rootWidget.addEventListener("tm-login",function(event) {
|
|
|
|
var username = event && event.paramObject && event.paramObject.username,
|
|
|
|
password = event && event.paramObject && event.paramObject.password;
|
|
|
|
if(username && password) {
|
|
|
|
// Login with username and password
|
|
|
|
self.login(username,password,function() {});
|
|
|
|
} else {
|
|
|
|
// No username and password, so we display a prompt
|
2021-07-14 16:16:57 +00:00
|
|
|
self.handleLoginEvent();
|
2020-10-25 16:33:44 +00:00
|
|
|
}
|
2014-08-13 19:07:08 +00:00
|
|
|
});
|
2014-08-28 20:43:44 +00:00
|
|
|
$tw.rootWidget.addEventListener("tm-logout",function() {
|
2014-08-14 07:54:31 +00:00
|
|
|
self.handleLogoutEvent();
|
2014-08-13 19:07:08 +00:00
|
|
|
});
|
2014-08-28 20:43:44 +00:00
|
|
|
$tw.rootWidget.addEventListener("tm-server-refresh",function() {
|
2014-08-14 07:54:31 +00:00
|
|
|
self.handleRefreshEvent();
|
2014-08-13 19:07:08 +00:00
|
|
|
});
|
2020-03-30 14:24:05 +00:00
|
|
|
$tw.rootWidget.addEventListener("tm-copy-syncer-logs-to-clipboard",function() {
|
|
|
|
$tw.utils.copyToClipboard($tw.utils.getSystemInfo() + "\n\nLog:\n" + self.logger.getBuffer());
|
|
|
|
});
|
2014-08-13 18:29:00 +00:00
|
|
|
}
|
2014-02-06 21:36:30 +00:00
|
|
|
// Listen out for lazyLoad events
|
2020-05-06 10:27:50 +00:00
|
|
|
if(!this.disableUI && this.wiki.getTiddlerText(this.titleSyncDisableLazyLoading) !== "yes") {
|
2017-09-20 15:28:11 +00:00
|
|
|
this.wiki.addEventListener("lazyLoad",function(title) {
|
|
|
|
self.handleLazyLoadEvent(title);
|
2021-07-14 16:16:57 +00:00
|
|
|
});
|
2017-09-20 15:28:11 +00:00
|
|
|
}
|
2014-02-06 21:36:30 +00:00
|
|
|
// Get the login status
|
2014-09-01 19:39:08 +00:00
|
|
|
this.getStatus(function(err,isLoggedIn) {
|
2014-02-11 19:10:40 +00:00
|
|
|
// Do a sync from the server
|
|
|
|
self.syncFromServer();
|
2014-02-06 21:36:30 +00:00
|
|
|
});
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
/*
|
|
|
|
Show a generic network error alert
|
|
|
|
*/
|
2020-03-31 09:47:17 +00:00
|
|
|
Syncer.prototype.displayError = function(msg,err) {
|
|
|
|
if(err === ($tw.language.getString("Error/XMLHttpRequest") + ": 0")) {
|
|
|
|
this.loggerConnection.alert($tw.language.getString("Error/NetworkErrorAlert"));
|
|
|
|
this.logger.log(msg + ":",err);
|
|
|
|
} else {
|
|
|
|
this.logger.alert(msg + ":",err);
|
|
|
|
}
|
2020-03-30 14:24:05 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Return an array of the tiddler titles that are subjected to syncing
|
|
|
|
*/
|
|
|
|
Syncer.prototype.getSyncedTiddlers = function(source) {
|
|
|
|
return this.filterFn.call(this.wiki,source);
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Return an array of the tiddler titles that are subjected to syncing
|
|
|
|
*/
|
|
|
|
Syncer.prototype.getTiddlerRevision = function(title) {
|
|
|
|
if(this.syncadaptor && this.syncadaptor.getTiddlerRevision) {
|
|
|
|
return this.syncadaptor.getTiddlerRevision(title);
|
|
|
|
} else {
|
2021-07-14 16:16:57 +00:00
|
|
|
return this.wiki.getTiddler(title).fields.revision;
|
|
|
|
}
|
2020-03-30 14:24:05 +00:00
|
|
|
};
|
|
|
|
|
2013-03-17 15:28:49 +00:00
|
|
|
/*
|
2014-02-06 21:36:30 +00:00
|
|
|
Read (or re-read) the latest tiddler info from the store
|
2013-03-17 15:28:49 +00:00
|
|
|
*/
|
2014-02-06 21:36:30 +00:00
|
|
|
Syncer.prototype.readTiddlerInfo = function() {
|
2013-03-17 15:28:49 +00:00
|
|
|
// Hashmap by title of {revision:,changeCount:,adaptorInfo:}
|
2020-03-30 14:24:05 +00:00
|
|
|
// "revision" is the revision of the tiddler last seen on the server, and "changecount" is the corresponding local changecount
|
2013-03-17 15:28:49 +00:00
|
|
|
this.tiddlerInfo = {};
|
|
|
|
// Record information for known tiddlers
|
2014-02-14 07:53:41 +00:00
|
|
|
var self = this,
|
2020-03-30 14:24:05 +00:00
|
|
|
tiddlers = this.getSyncedTiddlers();
|
2014-02-14 07:53:41 +00:00
|
|
|
$tw.utils.each(tiddlers,function(title) {
|
2020-04-20 10:35:11 +00:00
|
|
|
var tiddler = self.wiki.getTiddler(title);
|
|
|
|
if(tiddler) {
|
|
|
|
self.tiddlerInfo[title] = {
|
|
|
|
revision: self.getTiddlerRevision(title),
|
|
|
|
adaptorInfo: self.syncadaptor && self.syncadaptor.getTiddlerInfo(tiddler),
|
|
|
|
changeCount: self.wiki.getChangeCount(title)
|
|
|
|
};
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
});
|
2014-02-06 21:36:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Checks whether the wiki is dirty (ie the window shouldn't be closed)
|
|
|
|
*/
|
|
|
|
Syncer.prototype.isDirty = function() {
|
2023-11-24 13:02:09 +00:00
|
|
|
var self = this;
|
|
|
|
function checkIsDirty() {
|
|
|
|
// Check tiddlers that are in the store and included in the filter function
|
|
|
|
var titles = self.getSyncedTiddlers();
|
|
|
|
for(var index=0; index<titles.length; index++) {
|
|
|
|
var title = titles[index],
|
|
|
|
tiddlerInfo = self.tiddlerInfo[title];
|
|
|
|
if(self.wiki.tiddlerExists(title)) {
|
|
|
|
if(tiddlerInfo) {
|
|
|
|
// If the tiddler is known on the server and has been modified locally then it needs to be saved to the server
|
|
|
|
if(self.wiki.getChangeCount(title) > tiddlerInfo.changeCount) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If the tiddler isn't known on the server then it needs to be saved to the server
|
2020-03-30 14:24:05 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-11-24 13:02:09 +00:00
|
|
|
// Check tiddlers that are known from the server but not currently in the store
|
|
|
|
titles = Object.keys(self.tiddlerInfo);
|
|
|
|
for(index=0; index<titles.length; index++) {
|
|
|
|
if(!self.wiki.tiddlerExists(titles[index])) {
|
|
|
|
// There must be a pending delete
|
|
|
|
return true;
|
|
|
|
}
|
2020-03-30 14:24:05 +00:00
|
|
|
}
|
2023-11-24 13:02:09 +00:00
|
|
|
return false;
|
2020-03-30 14:24:05 +00:00
|
|
|
}
|
2023-11-24 13:02:09 +00:00
|
|
|
var dirtyStatus = checkIsDirty();
|
|
|
|
return dirtyStatus;
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
2014-07-30 15:46:13 +00:00
|
|
|
/*
|
2014-08-28 17:21:08 +00:00
|
|
|
Update the document body with the class "tc-dirty" if the wiki has unsaved/unsynced changes
|
2014-07-30 15:46:13 +00:00
|
|
|
*/
|
|
|
|
Syncer.prototype.updateDirtyStatus = function() {
|
2017-09-20 15:28:11 +00:00
|
|
|
if($tw.browser && !this.disableUI) {
|
2020-03-30 14:24:05 +00:00
|
|
|
var dirty = this.isDirty();
|
|
|
|
$tw.utils.toggleClass(document.body,"tc-dirty",dirty);
|
|
|
|
if(!dirty) {
|
2020-03-31 09:47:17 +00:00
|
|
|
this.loggerConnection.clearAlerts();
|
2020-03-30 14:24:05 +00:00
|
|
|
}
|
2014-07-30 15:46:13 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-03-17 15:28:49 +00:00
|
|
|
/*
|
|
|
|
Save an incoming tiddler in the store, and updates the associated tiddlerInfo
|
|
|
|
*/
|
2020-03-30 14:24:05 +00:00
|
|
|
Syncer.prototype.storeTiddler = function(tiddlerFields) {
|
2013-03-17 15:28:49 +00:00
|
|
|
// Save the tiddler
|
2018-10-14 14:35:26 +00:00
|
|
|
var tiddler = new $tw.Tiddler(tiddlerFields);
|
2013-03-17 15:28:49 +00:00
|
|
|
this.wiki.addTiddler(tiddler);
|
|
|
|
// Save the tiddler revision and changeCount details
|
|
|
|
this.tiddlerInfo[tiddlerFields.title] = {
|
2020-03-30 14:24:05 +00:00
|
|
|
revision: this.getTiddlerRevision(tiddlerFields.title),
|
2013-03-17 15:28:49 +00:00
|
|
|
adaptorInfo: this.syncadaptor.getTiddlerInfo(tiddler),
|
2020-03-30 14:24:05 +00:00
|
|
|
changeCount: this.wiki.getChangeCount(tiddlerFields.title)
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
Syncer.prototype.getStatus = function(callback) {
|
|
|
|
var self = this;
|
2013-03-24 12:21:01 +00:00
|
|
|
// Check if the adaptor supports getStatus()
|
2014-02-06 21:36:30 +00:00
|
|
|
if(this.syncadaptor && this.syncadaptor.getStatus) {
|
2013-03-24 12:21:01 +00:00
|
|
|
// Mark us as not logged in
|
|
|
|
this.wiki.addTiddler({title: this.titleIsLoggedIn,text: "no"});
|
|
|
|
// Get login status
|
2021-07-14 16:16:57 +00:00
|
|
|
this.syncadaptor.getStatus(function(err,isLoggedIn,username,isReadOnly,isAnonymous) {
|
2013-03-24 12:21:01 +00:00
|
|
|
if(err) {
|
2021-07-30 15:21:02 +00:00
|
|
|
self.displayError("Get Status Error",err);
|
2020-03-30 14:24:05 +00:00
|
|
|
} else {
|
|
|
|
// Set the various status tiddlers
|
|
|
|
self.wiki.addTiddler({title: self.titleIsReadOnly,text: isReadOnly ? "yes" : "no"});
|
|
|
|
self.wiki.addTiddler({title: self.titleIsAnonymous,text: isAnonymous ? "yes" : "no"});
|
|
|
|
self.wiki.addTiddler({title: self.titleIsLoggedIn,text: isLoggedIn ? "yes" : "no"});
|
|
|
|
if(isLoggedIn) {
|
|
|
|
self.wiki.addTiddler({title: self.titleUserName,text: username || ""});
|
|
|
|
}
|
2013-03-24 12:21:01 +00:00
|
|
|
}
|
|
|
|
// Invoke the callback
|
|
|
|
if(callback) {
|
|
|
|
callback(err,isLoggedIn,username);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
callback(null,true,"UNAUTHENTICATED");
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
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() {
|
2023-11-29 09:31:19 +00:00
|
|
|
if(this.canSyncFromServer()) {
|
|
|
|
this.forceSyncFromServer = true;
|
|
|
|
this.processTaskQueue();
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
2023-11-29 09:31:19 +00:00
|
|
|
Syncer.prototype.canSyncFromServer = function() {
|
|
|
|
return !!this.syncadaptor.getUpdatedTiddlers || !!this.syncadaptor.getSkinnyTiddlers;
|
|
|
|
}
|
|
|
|
|
2013-03-17 15:28:49 +00:00
|
|
|
/*
|
2020-03-30 14:24:05 +00:00
|
|
|
Force load a tiddler from the server
|
2013-03-17 15:28:49 +00:00
|
|
|
*/
|
2020-03-30 14:24:05 +00:00
|
|
|
Syncer.prototype.enqueueLoadTiddler = function(title) {
|
|
|
|
this.titlesToBeLoaded[title] = true;
|
|
|
|
this.processTaskQueue();
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Lazily load a skinny tiddler if we can
|
|
|
|
*/
|
|
|
|
Syncer.prototype.handleLazyLoadEvent = function(title) {
|
2020-03-30 14:24:05 +00:00
|
|
|
// Ignore if the syncadaptor doesn't handle it
|
|
|
|
if(!this.syncadaptor.supportsLazyLoading) {
|
|
|
|
return;
|
|
|
|
}
|
2016-04-18 13:50:13 +00:00
|
|
|
// Don't lazy load the same tiddler twice
|
2020-03-30 14:24:05 +00:00
|
|
|
if(!this.titlesHaveBeenLazyLoaded[title]) {
|
2018-05-03 17:27:17 +00:00
|
|
|
// Don't lazy load if the tiddler isn't included in the sync filter
|
2020-03-30 14:24:05 +00:00
|
|
|
if(this.getSyncedTiddlers().indexOf(title) !== -1) {
|
|
|
|
// Mark the tiddler as needing loading, and having already been lazily loaded
|
|
|
|
this.titlesToBeLoaded[title] = true;
|
|
|
|
this.titlesHaveBeenLazyLoaded[title] = true;
|
2023-01-03 14:28:48 +00:00
|
|
|
this.processTaskQueue();
|
2018-05-03 17:27:17 +00:00
|
|
|
}
|
2016-04-18 13:50:13 +00:00
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Dispay a password prompt and allow the user to login
|
|
|
|
*/
|
|
|
|
Syncer.prototype.handleLoginEvent = function() {
|
|
|
|
var self = this;
|
2013-03-17 19:37:31 +00:00
|
|
|
this.getStatus(function(err,isLoggedIn,username) {
|
2020-03-30 14:24:05 +00:00
|
|
|
if(!err && !isLoggedIn) {
|
2020-10-25 16:33:44 +00:00
|
|
|
if(self.syncadaptor && self.syncadaptor.displayLoginPrompt) {
|
|
|
|
self.syncadaptor.displayLoginPrompt(self);
|
|
|
|
} else {
|
|
|
|
self.displayLoginPrompt();
|
2020-10-14 11:41:51 +00:00
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-10-25 16:33:44 +00:00
|
|
|
/*
|
|
|
|
Dispay a password prompt
|
|
|
|
*/
|
|
|
|
Syncer.prototype.displayLoginPrompt = function() {
|
|
|
|
var self = this;
|
|
|
|
var promptInfo = $tw.passwordPrompt.createPrompt({
|
|
|
|
serviceName: $tw.language.getString("LoginToTiddlySpace"),
|
|
|
|
callback: function(data) {
|
|
|
|
self.login(data.username,data.password,function(err,isLoggedIn) {
|
|
|
|
self.syncFromServer();
|
|
|
|
});
|
|
|
|
return true; // Get rid of the password prompt
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2013-03-17 15:28:49 +00:00
|
|
|
/*
|
|
|
|
Attempt to login to TiddlyWeb.
|
|
|
|
username: username
|
|
|
|
password: password
|
|
|
|
callback: invoked with arguments (err,isLoggedIn)
|
|
|
|
*/
|
|
|
|
Syncer.prototype.login = function(username,password,callback) {
|
2014-02-14 07:53:41 +00:00
|
|
|
this.logger.log("Attempting to login as",username);
|
2013-03-17 15:28:49 +00:00
|
|
|
var self = this;
|
2013-03-24 12:21:01 +00:00
|
|
|
if(this.syncadaptor.login) {
|
|
|
|
this.syncadaptor.login(username,password,function(err) {
|
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
2013-03-24 12:21:01 +00:00
|
|
|
self.getStatus(function(err,isLoggedIn,username) {
|
|
|
|
if(callback) {
|
2020-03-30 14:24:05 +00:00
|
|
|
callback(err,isLoggedIn);
|
2013-03-24 12:21:01 +00:00
|
|
|
}
|
|
|
|
});
|
2013-03-17 15:28:49 +00:00
|
|
|
});
|
2013-03-24 12:21:01 +00:00
|
|
|
} else {
|
|
|
|
callback(null,true);
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Attempt to log out of TiddlyWeb
|
|
|
|
*/
|
|
|
|
Syncer.prototype.handleLogoutEvent = function() {
|
2014-02-14 07:53:41 +00:00
|
|
|
this.logger.log("Attempting to logout");
|
2013-03-17 15:28:49 +00:00
|
|
|
var self = this;
|
2013-03-24 12:21:01 +00:00
|
|
|
if(this.syncadaptor.logout) {
|
|
|
|
this.syncadaptor.logout(function(err) {
|
|
|
|
if(err) {
|
2021-08-05 13:50:22 +00:00
|
|
|
self.displayError("Logout Error",err);
|
2013-03-24 12:21:01 +00:00
|
|
|
} else {
|
|
|
|
self.getStatus();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Immediately refresh from the server
|
|
|
|
*/
|
|
|
|
Syncer.prototype.handleRefreshEvent = function() {
|
|
|
|
this.syncFromServer();
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
2020-03-30 14:24:05 +00:00
|
|
|
Process the next task
|
2013-03-17 15:28:49 +00:00
|
|
|
*/
|
2020-03-30 14:24:05 +00:00
|
|
|
Syncer.prototype.processTaskQueue = function() {
|
|
|
|
var self = this;
|
|
|
|
// Only process a task if the sync adaptor is fully initialised and 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.syncadaptor.isReady || this.syncadaptor.isReady()) && this.numTasksInProgress === 0) {
|
|
|
|
// Choose the next task to perform
|
|
|
|
var task = this.chooseNextTask();
|
|
|
|
// Perform the task if we had one
|
|
|
|
if(typeof task === "object" && task !== null) {
|
|
|
|
this.numTasksInProgress += 1;
|
|
|
|
task.run(function(err) {
|
|
|
|
self.numTasksInProgress -= 1;
|
|
|
|
if(err) {
|
2020-03-31 09:47:17 +00:00
|
|
|
self.displayError("Sync error while processing " + task.type + " of '" + task.title + "'",err);
|
2020-03-30 14:24:05 +00:00
|
|
|
self.updateDirtyStatus();
|
|
|
|
self.triggerTimeout(self.errorRetryInterval);
|
|
|
|
} else {
|
|
|
|
self.updateDirtyStatus();
|
|
|
|
// Process the next task
|
2023-11-24 13:02:09 +00:00
|
|
|
self.processTaskQueue.call(self);
|
2020-03-30 14:24:05 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// No task is ready so update the status
|
|
|
|
this.updateDirtyStatus();
|
|
|
|
// And trigger a timeout if there is a pending task
|
|
|
|
if(task === true) {
|
2023-11-24 13:02:09 +00:00
|
|
|
this.triggerTimeout(this.taskTimerInterval);
|
2023-11-29 09:31:19 +00:00
|
|
|
} else if(this.canSyncFromServer()) {
|
2023-11-24 13:02:09 +00:00
|
|
|
this.triggerTimeout(this.pollTimerInterval);
|
2020-03-30 14:24:05 +00:00
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
} else {
|
2023-11-24 13:02:09 +00:00
|
|
|
this.updateDirtyStatus();
|
|
|
|
this.triggerTimeout(this.taskTimerInterval);
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
Syncer.prototype.triggerTimeout = function(interval) {
|
2013-03-17 15:28:49 +00:00
|
|
|
var self = this;
|
2023-11-24 13:02:09 +00:00
|
|
|
if(this.taskTimerId) {
|
|
|
|
clearTimeout(this.taskTimerId);
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
2023-11-24 13:02:09 +00:00
|
|
|
this.taskTimerId = setTimeout(function() {
|
|
|
|
self.taskTimerId = null;
|
|
|
|
self.processTaskQueue.call(self);
|
|
|
|
},interval || self.taskTimerInterval);
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
2023-11-24 13:02:09 +00:00
|
|
|
Choose the next sync task. We prioritise saves to the server, then getting updates from the server, then deletes to the server, then loads from the server
|
2020-03-30 14:24:05 +00:00
|
|
|
|
2023-11-24 13:02:09 +00:00
|
|
|
Returns either:
|
|
|
|
* a task object
|
|
|
|
* the boolean true if there are pending sync tasks that aren't yet due
|
|
|
|
* null if there's no pending sync tasks (just the next poll)
|
2013-03-17 15:28:49 +00:00
|
|
|
*/
|
2020-03-30 14:24:05 +00:00
|
|
|
Syncer.prototype.chooseNextTask = function() {
|
2023-11-24 13:02:09 +00:00
|
|
|
var now = new Date(),
|
|
|
|
thresholdLastSaved = now - this.throttleInterval,
|
2020-03-30 14:24:05 +00:00
|
|
|
havePending = null;
|
|
|
|
// First we look for tiddlers that have been modified locally and need saving back to the server
|
|
|
|
var titles = this.getSyncedTiddlers();
|
|
|
|
for(var index=0; index<titles.length; index++) {
|
|
|
|
var title = titles[index],
|
|
|
|
tiddler = this.wiki.tiddlerExists(title) && this.wiki.getTiddler(title),
|
|
|
|
tiddlerInfo = this.tiddlerInfo[title];
|
|
|
|
if(tiddler) {
|
|
|
|
// If the tiddler is not known on the server, or has been modified locally no more recently than the threshold then it needs to be saved to the server
|
2020-05-06 10:27:50 +00:00
|
|
|
var hasChanged = !tiddlerInfo || this.wiki.getChangeCount(title) > tiddlerInfo.changeCount,
|
2020-03-30 14:24:05 +00:00
|
|
|
isReadyToSave = !tiddlerInfo || !tiddlerInfo.timestampLastSaved || tiddlerInfo.timestampLastSaved < thresholdLastSaved;
|
|
|
|
if(hasChanged) {
|
|
|
|
if(isReadyToSave) {
|
2023-11-24 13:02:09 +00:00
|
|
|
return new SaveTiddlerTask(this,title);
|
2020-03-30 14:24:05 +00:00
|
|
|
} else {
|
|
|
|
havePending = true;
|
2013-12-03 09:35:02 +00:00
|
|
|
}
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-11-24 13:02:09 +00:00
|
|
|
// Second we check for an outstanding sync from server
|
|
|
|
if(this.forceSyncFromServer || (this.timestampLastSyncFromServer && (now.valueOf() >= (this.timestampLastSyncFromServer.valueOf() + this.pollTimerInterval)))) {
|
|
|
|
return new SyncFromServerTask(this);
|
|
|
|
}
|
|
|
|
// Third, we check tiddlers that are known from the server but not currently in the store, and so need deleting on the server
|
2020-03-30 14:24:05 +00:00
|
|
|
titles = Object.keys(this.tiddlerInfo);
|
|
|
|
for(index=0; index<titles.length; index++) {
|
|
|
|
title = titles[index];
|
|
|
|
tiddlerInfo = this.tiddlerInfo[title];
|
|
|
|
tiddler = this.wiki.tiddlerExists(title) && this.wiki.getTiddler(title);
|
|
|
|
if(!tiddler) {
|
|
|
|
return new DeleteTiddlerTask(this,title);
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
2020-03-30 14:24:05 +00:00
|
|
|
}
|
2023-11-24 13:02:09 +00:00
|
|
|
// Finally, check for tiddlers that need loading
|
2020-03-30 14:24:05 +00:00
|
|
|
title = Object.keys(this.titlesToBeLoaded)[0];
|
|
|
|
if(title) {
|
|
|
|
delete this.titlesToBeLoaded[title];
|
|
|
|
return new LoadTiddlerTask(this,title);
|
|
|
|
}
|
2023-11-24 13:02:09 +00:00
|
|
|
// No tasks are ready now, but might be in the future
|
2020-03-30 14:24:05 +00:00
|
|
|
return havePending;
|
2013-03-17 15:28:49 +00:00
|
|
|
};
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
function SaveTiddlerTask(syncer,title) {
|
|
|
|
this.syncer = syncer;
|
|
|
|
this.title = title;
|
|
|
|
this.type = "save";
|
|
|
|
}
|
|
|
|
|
2023-11-24 13:02:09 +00:00
|
|
|
SaveTiddlerTask.prototype.toString = function() {
|
|
|
|
return "SAVE " + this.title;
|
|
|
|
}
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
SaveTiddlerTask.prototype.run = function(callback) {
|
|
|
|
var self = this,
|
|
|
|
changeCount = this.syncer.wiki.getChangeCount(this.title),
|
|
|
|
tiddler = this.syncer.wiki.tiddlerExists(this.title) && this.syncer.wiki.getTiddler(this.title);
|
|
|
|
this.syncer.logger.log("Dispatching 'save' task:",this.title);
|
|
|
|
if(tiddler) {
|
2021-07-05 18:26:20 +00:00
|
|
|
this.syncer.syncadaptor.saveTiddler(tiddler,function(err,adaptorInfo,revision) {
|
2020-03-30 14:24:05 +00:00
|
|
|
// If there's an error, exit without changing any internal state
|
2013-03-17 15:28:49 +00:00
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2020-03-30 14:24:05 +00:00
|
|
|
// Adjust the info stored about this tiddler
|
|
|
|
self.syncer.tiddlerInfo[self.title] = {
|
|
|
|
changeCount: changeCount,
|
|
|
|
adaptorInfo: adaptorInfo,
|
|
|
|
revision: revision,
|
|
|
|
timestampLastSaved: new Date()
|
|
|
|
};
|
2013-03-17 15:28:49 +00:00
|
|
|
// Invoke the callback
|
|
|
|
callback(null);
|
2021-07-05 18:26:20 +00:00
|
|
|
},{
|
|
|
|
tiddlerInfo: self.syncer.tiddlerInfo[self.title]
|
2013-03-17 15:28:49 +00:00
|
|
|
});
|
2020-03-30 14:24:05 +00:00
|
|
|
} else {
|
|
|
|
$tw.utils.nextTick(callback(null));
|
2013-03-17 15:28:49 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
function DeleteTiddlerTask(syncer,title) {
|
|
|
|
this.syncer = syncer;
|
|
|
|
this.title = title;
|
|
|
|
this.type = "delete";
|
|
|
|
}
|
|
|
|
|
2023-11-24 13:02:09 +00:00
|
|
|
DeleteTiddlerTask.prototype.toString = function() {
|
|
|
|
return "DELETE " + this.title;
|
|
|
|
}
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
DeleteTiddlerTask.prototype.run = function(callback) {
|
|
|
|
var self = this;
|
|
|
|
this.syncer.logger.log("Dispatching 'delete' task:",this.title);
|
2021-07-05 18:26:20 +00:00
|
|
|
this.syncer.syncadaptor.deleteTiddler(this.title,function(err) {
|
2020-03-30 14:24:05 +00:00
|
|
|
// If there's an error, exit without changing any internal state
|
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
// Remove the info stored about this tiddler
|
|
|
|
delete self.syncer.tiddlerInfo[self.title];
|
|
|
|
// Invoke the callback
|
|
|
|
callback(null);
|
2021-07-05 18:26:20 +00:00
|
|
|
},{
|
|
|
|
tiddlerInfo: self.syncer.tiddlerInfo[this.title]
|
2020-03-30 14:24:05 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
function LoadTiddlerTask(syncer,title) {
|
|
|
|
this.syncer = syncer;
|
|
|
|
this.title = title;
|
|
|
|
this.type = "load";
|
|
|
|
}
|
|
|
|
|
2023-11-24 13:02:09 +00:00
|
|
|
LoadTiddlerTask.prototype.toString = function() {
|
|
|
|
return "LOAD " + this.title;
|
|
|
|
}
|
|
|
|
|
2020-03-30 14:24:05 +00:00
|
|
|
LoadTiddlerTask.prototype.run = function(callback) {
|
|
|
|
var self = this;
|
|
|
|
this.syncer.logger.log("Dispatching 'load' task:",this.title);
|
|
|
|
this.syncer.syncadaptor.loadTiddler(this.title,function(err,tiddlerFields) {
|
|
|
|
// If there's an error, exit without changing any internal state
|
|
|
|
if(err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
// Update the info stored about this tiddler
|
|
|
|
if(tiddlerFields) {
|
|
|
|
self.syncer.storeTiddler(tiddlerFields);
|
|
|
|
}
|
|
|
|
// Invoke the callback
|
|
|
|
callback(null);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-11-24 13:02:09 +00:00
|
|
|
function SyncFromServerTask(syncer) {
|
|
|
|
this.syncer = syncer;
|
|
|
|
this.type = "syncfromserver";
|
|
|
|
}
|
|
|
|
|
|
|
|
SyncFromServerTask.prototype.toString = function() {
|
|
|
|
return "SYNCFROMSERVER";
|
|
|
|
}
|
|
|
|
|
|
|
|
SyncFromServerTask.prototype.run = function(callback) {
|
|
|
|
var self = this;
|
|
|
|
var syncSystemFromServer = (self.syncer.wiki.getTiddlerText("$:/config/SyncSystemTiddlersFromServer") === "yes" ? true : false);
|
|
|
|
var successCallback = function() {
|
|
|
|
self.syncer.forceSyncFromServer = false;
|
|
|
|
self.syncer.timestampLastSyncFromServer = new Date();
|
|
|
|
callback(null);
|
|
|
|
};
|
|
|
|
if(this.syncer.syncadaptor.getUpdatedTiddlers) {
|
2023-12-12 15:48:09 +00:00
|
|
|
this.syncer.syncadaptor.getUpdatedTiddlers(self.syncer,function(err,updates) {
|
2023-11-24 13:02:09 +00:00
|
|
|
if(err) {
|
|
|
|
self.syncer.displayError($tw.language.getString("Error/RetrievingSkinny"),err);
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
if(updates) {
|
|
|
|
$tw.utils.each(updates.modifications,function(title) {
|
|
|
|
self.syncer.titlesToBeLoaded[title] = true;
|
|
|
|
});
|
|
|
|
$tw.utils.each(updates.deletions,function(title) {
|
|
|
|
if(syncSystemFromServer || !self.syncer.wiki.isSystemTiddler(title)) {
|
|
|
|
delete self.syncer.tiddlerInfo[title];
|
|
|
|
self.syncer.logger.log("Deleting tiddler missing from server:",title);
|
|
|
|
self.syncer.wiki.deleteTiddler(title);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return successCallback();
|
|
|
|
});
|
|
|
|
} else if(this.syncer.syncadaptor.getSkinnyTiddlers) {
|
|
|
|
this.syncer.syncadaptor.getSkinnyTiddlers(function(err,tiddlers) {
|
|
|
|
// Check for errors
|
|
|
|
if(err) {
|
|
|
|
self.syncer.displayError($tw.language.getString("Error/RetrievingSkinny"),err);
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
// Keep track of which tiddlers we already know about have been reported this time
|
|
|
|
var previousTitles = Object.keys(self.syncer.tiddlerInfo);
|
|
|
|
// 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.syncer.wiki.tiddlerExists(tiddlerFields.title) && self.syncer.wiki.getTiddler(tiddlerFields.title),
|
|
|
|
tiddlerInfo = self.syncer.tiddlerInfo[tiddlerFields.title],
|
|
|
|
currRevision = tiddlerInfo ? tiddlerInfo.revision : null,
|
|
|
|
indexInPreviousTitles = previousTitles.indexOf(tiddlerFields.title);
|
|
|
|
if(indexInPreviousTitles !== -1) {
|
|
|
|
previousTitles.splice(indexInPreviousTitles,1);
|
|
|
|
}
|
|
|
|
// Ignore the incoming tiddler if it's the same as the revision we've already got
|
|
|
|
if(currRevision !== incomingRevision) {
|
|
|
|
// Only load the skinny version if we don't already have a fat version of the tiddler
|
|
|
|
if(!tiddler || tiddler.fields.text === undefined) {
|
|
|
|
self.syncer.storeTiddler(tiddlerFields);
|
|
|
|
}
|
|
|
|
// Do a full load of this tiddler
|
|
|
|
self.syncer.titlesToBeLoaded[tiddlerFields.title] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Delete any tiddlers that were previously reported but missing this time
|
|
|
|
$tw.utils.each(previousTitles,function(title) {
|
|
|
|
if(syncSystemFromServer || !self.syncer.wiki.isSystemTiddler(title)) {
|
|
|
|
delete self.syncer.tiddlerInfo[title];
|
|
|
|
self.syncer.logger.log("Deleting tiddler missing from server:",title);
|
|
|
|
self.syncer.wiki.deleteTiddler(title);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
self.syncer.forceSyncFromServer = false;
|
|
|
|
self.syncer.timestampLastSyncFromServer = new Date();
|
|
|
|
return successCallback();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return successCallback();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-03-17 15:28:49 +00:00
|
|
|
exports.Syncer = Syncer;
|
|
|
|
|
|
|
|
})();
|