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
2014-02-06 21:36:30 +00:00
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 .
2013-03-17 15:28:49 +00:00
\ * /
( function ( ) {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict" ;
/ *
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 ;
2014-08-14 10:12:25 +00:00
this . syncadaptor = options . syncadaptor
2014-01-26 18:53:31 +00:00
// Make a logger
2014-02-14 07:53:41 +00:00
this . logger = new $tw . utils . Logger ( "syncer" + ( $tw . browser ? "-browser" : "" ) + ( $tw . node ? "-server" : "" ) ) ;
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 ( ) ;
// 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 ) ;
} ) ;
2014-08-13 19:07:08 +00:00
// Browser event handlers
2014-08-13 18:29:00 +00:00
if ( $tw . browser ) {
2014-08-13 19:07:08 +00:00
// Set up our beforeunload handler
2014-08-13 18:29:00 +00:00
window . addEventListener ( "beforeunload" , function ( event ) {
var confirmationMessage = undefined ;
if ( self . isDirty ( ) ) {
confirmationMessage = $tw . language . getString ( "UnsavedChangesWarning" ) ;
event . returnValue = confirmationMessage ; // Gecko
}
return confirmationMessage ;
} ) ;
2014-08-13 19:07:08 +00:00
// Listen out for login/logout/refresh events in the browser
2014-08-28 20:43:44 +00:00
$tw . rootWidget . addEventListener ( "tm-login" , function ( ) {
2014-08-14 07:54:31 +00:00
self . handleLoginEvent ( ) ;
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
} ) ;
2014-08-13 18:29:00 +00:00
}
2014-02-06 21:36:30 +00:00
// Listen out for lazyLoad events
2014-08-14 10:43:07 +00:00
this . wiki . addEventListener ( "lazyLoad" , function ( title ) {
self . handleLazyLoadEvent ( title ) ;
} ) ;
2014-02-06 21:36:30 +00:00
// Get the login status
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
}
/ *
Constants
* /
Syncer . prototype . titleIsLoggedIn = "$:/status/IsLoggedIn" ;
Syncer . prototype . titleUserName = "$:/status/UserName" ;
2014-02-06 21:36:30 +00:00
Syncer . prototype . titleSyncFilter = "$:/config/SyncFilter" ;
2014-02-16 09:46:43 +00:00
Syncer . prototype . titleSavedNotification = "$:/language/Notifications/Save/Done" ;
2013-03-17 15:28:49 +00:00
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
2014-07-30 15:46:13 +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:}
this . tiddlerInfo = { } ;
// Record information for known tiddlers
2014-02-14 07:53:41 +00:00
var self = this ,
tiddlers = this . filterFn . call ( this . wiki ) ;
$tw . utils . each ( tiddlers , function ( title ) {
var tiddler = self . wiki . getTiddler ( title ) ;
2013-12-11 22:02:34 +00:00
self . tiddlerInfo [ title ] = {
revision : tiddler . fields [ "revision" ] ,
2014-02-06 21:36:30 +00:00
adaptorInfo : self . syncadaptor && self . syncadaptor . getTiddlerInfo ( tiddler ) ,
2013-12-11 22:02:34 +00:00
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 ( ) {
return ( this . numTasksInQueue ( ) > 0 ) || ( this . numTasksInProgress ( ) > 0 ) ;
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 ( ) {
if ( $tw . browser ) {
2014-08-28 17:21:08 +00:00
$tw . utils . toggleClass ( document . body , "tc-dirty" , this . isDirty ( ) ) ;
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
* /
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 ;
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
this . syncadaptor . getStatus ( function ( err , isLoggedIn , username ) {
if ( err ) {
2014-02-14 07:53:41 +00:00
self . logger . alert ( err ) ;
2013-03-24 12:21:01 +00:00
return ;
}
// Set the various status tiddlers
self . wiki . addTiddler ( { title : self . titleIsLoggedIn , text : isLoggedIn ? "yes" : "no" } ) ;
if ( isLoggedIn ) {
2014-02-06 21:36:30 +00:00
self . wiki . addTiddler ( { title : self . titleUserName , text : username || "" } ) ;
2013-03-24 12:21:01 +00:00
} else {
self . wiki . deleteTiddler ( self . titleUserName ) ;
}
// 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 ( ) {
2014-02-06 21:36:30 +00:00
if ( this . syncadaptor && this . syncadaptor . getSkinnyTiddlers ) {
2014-02-14 07:53:41 +00:00
this . logger . log ( "Retrieving skinny tiddler list" ) ;
2013-03-24 12:21:01 +00:00
var self = this ;
if ( this . pollTimerId ) {
clearTimeout ( this . pollTimerId ) ;
this . pollTimerId = null ;
2013-03-17 15:28:49 +00:00
}
2013-03-24 12:21:01 +00:00
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 ) {
2014-02-14 07:53:41 +00:00
self . logger . alert ( "Error retrieving skinny tiddler list:" , err ) ;
2013-03-24 12:21:01 +00:00
return ;
}
// 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 ] ,
2013-11-08 21:34:47 +00:00
incomingRevision = tiddlerFields . revision + "" ,
2013-03-24 12:21:01 +00:00
tiddler = self . wiki . getTiddler ( tiddlerFields . title ) ,
tiddlerInfo = self . tiddlerInfo [ tiddlerFields . title ] ,
currRevision = tiddlerInfo ? tiddlerInfo . revision : null ;
// Ignore the incoming tiddler if it's the same as the revision we've already got
if ( currRevision !== incomingRevision ) {
// Do a full load if we've already got a fat version of the tiddler
if ( tiddler && tiddler . fields . text !== undefined ) {
// Do a full load of this tiddler
self . enqueueSyncTask ( {
type : "load" ,
title : tiddlerFields . title
} ) ;
} else {
// Load the skinny version of the tiddler
self . storeTiddler ( tiddlerFields ) ;
}
2013-03-17 15:28:49 +00:00
}
}
2013-03-24 12:21:01 +00:00
} ) ;
}
2013-03-17 15:28:49 +00:00
} ;
/ *
Synchronise a set of changes to the server
* /
Syncer . prototype . syncToServer = function ( changes ) {
var self = this ,
2014-04-14 08:02:52 +00:00
now = Date . now ( ) ,
2014-04-03 19:49:16 +00:00
filteredChanges = this . filterFn . call ( this . wiki , function ( callback ) {
$tw . utils . each ( changes , function ( change , title ) {
var tiddler = self . wiki . getTiddler ( title ) ;
callback ( tiddler , title ) ;
} ) ;
} ) ;
2013-03-17 15:28:49 +00:00
$tw . utils . each ( changes , function ( change , title , object ) {
2014-02-14 07:53:41 +00:00
// Process the change if it is a deletion of a tiddler we're already syncing, or is on the filtered change list
if ( ( change . deleted && $tw . utils . hop ( self . tiddlerInfo , title ) ) || filteredChanges . indexOf ( title ) !== - 1 ) {
2013-04-30 21:55:06 +00:00
// Queue a task to sync this tiddler
self . enqueueSyncTask ( {
type : change . deleted ? "delete" : "save" ,
title : title
} ) ;
}
2013-03-17 15:28:49 +00:00
} ) ;
} ;
/ *
Lazily load a skinny tiddler if we can
* /
Syncer . prototype . handleLazyLoadEvent = function ( title ) {
// Queue up a sync task to load this tiddler
this . enqueueSyncTask ( {
type : "load" ,
title : title
} ) ;
} ;
/ *
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 ) {
2013-03-17 15:28:49 +00:00
if ( ! isLoggedIn ) {
$tw . passwordPrompt . createPrompt ( {
serviceName : "Login to TiddlySpace" ,
callback : function ( data ) {
self . login ( data . username , data . password , function ( err , isLoggedIn ) {
self . syncFromServer ( ) ;
} ) ;
return true ; // Get rid of the password prompt
}
} ) ;
}
} ) ;
} ;
/ *
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 ) {
callback ( null , isLoggedIn ) ;
}
} ) ;
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 ) {
2014-02-14 07:53:41 +00:00
self . logger . alert ( 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 ( ) ;
} ;
/ *
Queue up a sync task . If there is already a pending task for the tiddler , just update the last modification time
* /
Syncer . prototype . enqueueSyncTask = function ( task ) {
var self = this ,
2014-04-14 08:02:52 +00:00
now = Date . now ( ) ;
2013-03-17 15:28:49 +00:00
// Set the timestamps on this task
task . queueTime = now ;
task . lastModificationTime = now ;
// Fill in some tiddlerInfo if the tiddler is one we haven't seen before
if ( ! $tw . utils . hop ( this . tiddlerInfo , task . title ) ) {
this . tiddlerInfo [ task . title ] = {
2013-03-18 10:13:36 +00:00
revision : null ,
2013-03-17 15:28:49 +00:00
adaptorInfo : { } ,
changeCount : - 1
}
}
// Bail if this is a save and the tiddler is already at the changeCount that the server has
if ( task . type === "save" && this . wiki . getChangeCount ( task . title ) <= this . tiddlerInfo [ task . title ] . changeCount ) {
return ;
}
// Check if this tiddler is already in the queue
if ( $tw . utils . hop ( this . taskQueue , task . title ) ) {
2014-02-14 07:53:41 +00:00
// this.logger.log("Re-queueing up sync task with type:",task.type,"title:",task.title);
2013-03-17 15:28:49 +00:00
var existingTask = this . taskQueue [ task . title ] ;
// If so, just update the last modification time
existingTask . lastModificationTime = task . lastModificationTime ;
// If the new task is a save then we upgrade the existing task to a save. Thus a pending load is turned into a save if the tiddler changes locally in the meantime. But a pending save is not modified to become a load
if ( task . type === "save" || task . type === "delete" ) {
existingTask . type = task . type ;
}
} else {
2014-02-14 07:53:41 +00:00
// this.logger.log("Queuing up sync task with type:",task.type,"title:",task.title);
2013-03-17 15:28:49 +00:00
// If it is not in the queue, insert it
this . taskQueue [ task . title ] = task ;
2014-07-30 15:46:13 +00:00
this . updateDirtyStatus ( ) ;
2013-03-17 15:28:49 +00:00
}
// Process the queue
2014-08-14 10:43:07 +00:00
$tw . utils . nextTick ( function ( ) { self . processTaskQueue . call ( self ) ; } ) ;
2013-03-17 15:28:49 +00:00
} ;
/ *
Return the number of tasks in progress
* /
Syncer . prototype . numTasksInProgress = function ( ) {
return $tw . utils . count ( this . taskInProgress ) ;
} ;
/ *
Return the number of tasks in the queue
* /
Syncer . prototype . numTasksInQueue = function ( ) {
return $tw . utils . count ( this . taskQueue ) ;
} ;
/ *
Trigger a timeout if one isn ' t already outstanding
* /
Syncer . prototype . triggerTimeout = function ( ) {
var self = this ;
if ( ! this . taskTimerId ) {
2013-03-24 12:21:01 +00:00
this . taskTimerId = setTimeout ( function ( ) {
2013-03-17 15:28:49 +00:00
self . taskTimerId = null ;
self . processTaskQueue . call ( self ) ;
} , self . taskTimerInterval ) ;
}
} ;
/ *
Process the task queue , performing the next task if appropriate
* /
Syncer . prototype . processTaskQueue = function ( ) {
var self = this ;
// Only process a task if 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 . numTasksInProgress ( ) === 0 ) {
// Choose the next task to perform
var task = this . chooseNextTask ( ) ;
// Perform the task if we had one
if ( task ) {
// Remove the task from the queue and add it to the in progress list
delete this . taskQueue [ task . title ] ;
this . taskInProgress [ task . title ] = task ;
2014-07-30 15:46:13 +00:00
this . updateDirtyStatus ( ) ;
2013-03-17 15:28:49 +00:00
// Dispatch the task
this . dispatchTask ( task , function ( err ) {
2013-12-03 09:35:02 +00:00
if ( err ) {
2014-02-14 07:53:41 +00:00
self . logger . alert ( "Sync error while processing '" + task . title + "':\n" + err ) ;
2013-12-03 09:35:02 +00:00
}
2013-03-17 15:28:49 +00:00
// Mark that this task is no longer in progress
delete self . taskInProgress [ task . title ] ;
2014-07-30 15:46:13 +00:00
self . updateDirtyStatus ( ) ;
2013-03-17 15:28:49 +00:00
// Process the next task
self . processTaskQueue . call ( self ) ;
} ) ;
} else {
// Make sure we've set a time if there wasn't a task to perform, but we've still got tasks in the queue
if ( this . numTasksInQueue ( ) > 0 ) {
this . triggerTimeout ( ) ;
}
}
}
} ;
/ *
Choose the next applicable task
* /
Syncer . prototype . chooseNextTask = function ( ) {
var self = this ,
candidateTask = null ,
2014-04-14 08:02:52 +00:00
now = Date . now ( ) ;
2013-03-17 15:28:49 +00:00
// 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 ) {
2014-08-13 19:07:08 +00:00
return ;
2013-03-17 15:28:49 +00:00
}
// 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 ) ;
2014-02-14 07:53:41 +00:00
this . logger . log ( "Dispatching 'save' task:" , task . title ) ;
2013-12-12 15:16:44 +00:00
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 ) ;
} ) ;
2014-03-19 14:50:54 +00:00
} else {
this . logger . log ( " Not Dispatching 'save' task:" , task . title , "tiddler does not exist" ) ;
return callback ( null ) ;
2013-12-12 15:16:44 +00:00
}
2013-03-17 15:28:49 +00:00
} else if ( task . type === "load" ) {
// Load the tiddler
2014-02-14 07:53:41 +00:00
this . logger . log ( "Dispatching 'load' task:" , task . title ) ;
2013-03-17 15:28:49 +00:00
this . syncadaptor . loadTiddler ( task . title , function ( err , tiddlerFields ) {
if ( err ) {
return callback ( err ) ;
}
// Store the tiddler
2013-12-11 11:45:15 +00:00
if ( tiddlerFields ) {
self . storeTiddler ( tiddlerFields ) ;
}
2013-03-17 15:28:49 +00:00
// Invoke the callback
callback ( null ) ;
} ) ;
} else if ( task . type === "delete" ) {
// Delete the tiddler
2014-02-14 07:53:41 +00:00
this . logger . log ( "Dispatching 'delete' task:" , task . title ) ;
2013-03-17 15:28:49 +00:00
this . syncadaptor . deleteTiddler ( task . title , function ( err ) {
if ( err ) {
return callback ( err ) ;
}
// Invoke the callback
callback ( null ) ;
2014-08-14 10:12:25 +00:00
} , {
tiddlerInfo : self . tiddlerInfo [ task . title ]
2013-03-17 15:28:49 +00:00
} ) ;
}
} ;
exports . Syncer = Syncer ;
} ) ( ) ;