2013-03-17 15:28:49 +00:00
/ * \
title : $ : / c o r e / m o d u l e s / s y n c e r . j s
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 ( ) ;
2023-11-29 09:31:19 +00:00
// this.logger.log("Initialising tiddlerInfo for " + tiddlers.length + " tiddlers");
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 ( ) ;
this . logger . log ( "Dirty status was " + dirtyStatus ) ;
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
} ;
2023-11-24 13:02:09 +00:00
this . logger . log ( "Updating tiddler info in syncer.storeTiddler for " + tiddlerFields . title + " " + JSON . stringify ( this . tiddlerInfo [ 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 ( ) ;
2023-11-28 11:44:21 +00:00
// self.logger.log("Chosen next task " + task);
2020-03-30 14:24:05 +00:00
// 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 ( )
} ;
2023-11-28 11:44:21 +00:00
// self.syncer.logger.log("Updating tiddler info in SaveTiddlerTask.run for " + self.title + " " + JSON.stringify(self.syncer.tiddlerInfo[self.title]));
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 {
2023-11-28 11:44:21 +00:00
// this.syncer.logger.log(" Not Dispatching 'save' task:",this.title,"tiddler does not exist");
2020-03-30 14:24:05 +00:00
$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
2023-11-28 11:44:21 +00:00
// self.syncer.logger.log("Deleting tiddler info in DeleteTiddlerTask.run for " + self.title);
2020-03-30 14:24:05 +00:00
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 ) {
self . syncer . logger . log ( "Retrieving updated tiddler list" ) ;
this . syncer . syncadaptor . getUpdatedTiddlers ( self , function ( err , updates ) {
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 . logger . log ( "Retrieving skinny tiddler list" ) ;
this . syncer . syncadaptor . getSkinnyTiddlers ( function ( err , tiddlers ) {
2023-11-28 11:44:21 +00:00
// self.syncer.logger.log("Retrieved skinny tiddler list");
2023-11-24 13:02:09 +00:00
// 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 ;
} ) ( ) ;