2012-11-16 17:01:04 +00:00
/ * \
title : $ : / p l u g i n s / t i d d l y w i k i / t i d d l y w e b / t i d d l y w e b . j s
type : application / javascript
2012-11-17 20:18:36 +00:00
module - type : syncer
2012-11-16 17:01:04 +00:00
2012-11-19 09:04:35 +00:00
Main TiddlyWeb syncer module
2012-11-16 17:01:04 +00:00
\ * /
( function ( ) {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict" ;
/ *
2012-11-17 20:18:36 +00:00
Creates a TiddlyWebSyncer object
2012-11-16 17:01:04 +00:00
* /
2012-11-17 20:18:36 +00:00
var TiddlyWebSyncer = function ( options ) {
2012-11-18 10:24:20 +00:00
this . wiki = options . wiki ;
2012-11-17 21:15:19 +00:00
this . connection = undefined ;
2012-11-19 09:04:35 +00:00
this . tiddlerInfo = { } ; // Hashmap of {revision:,changeCount:}
// Tasks are {type: "load"/"save", title:, queueTime:, lastModificationTime:}
this . taskQueue = { } ; // Hashmap of tasks to be performed
2012-11-19 12:56:54 +00:00
this . taskInProgress = { } ; // Hash of tasks in progress
this . taskTimerId = null ; // Sync timer
2012-11-17 12:32:51 +00:00
} ;
2012-11-17 20:18:36 +00:00
TiddlyWebSyncer . titleIsLoggedIn = "$:/plugins/tiddlyweb/IsLoggedIn" ;
TiddlyWebSyncer . titleUserName = "$:/plugins/tiddlyweb/UserName" ;
2012-11-19 12:56:54 +00:00
TiddlyWebSyncer . taskTimerInterval = 1 * 1000 ; // Interval for sync timer
TiddlyWebSyncer . throttleInterval = 1 * 1000 ; // Defer saving tiddlers if they've changed in the last 1s...
TiddlyWebSyncer . fallbackInterval = 10 * 1000 ; // Unless the task is older than 10s
2012-11-17 20:18:36 +00:00
2012-11-17 12:32:51 +00:00
/ *
Error handling
* /
2012-11-17 20:18:36 +00:00
TiddlyWebSyncer . prototype . showError = function ( error ) {
2012-11-17 12:32:51 +00:00
alert ( "TiddlyWeb error: " + error ) ;
console . log ( "TiddlyWeb error: " + error ) ;
} ;
2012-11-17 21:15:19 +00:00
TiddlyWebSyncer . prototype . addConnection = function ( connection ) {
2012-11-18 21:07:14 +00:00
var self = this ;
2012-11-17 21:15:19 +00:00
// Check if we've already got a connection
if ( this . connection ) {
2012-11-19 09:04:35 +00:00
return new Error ( "TiddlyWebSyncer can only handle a single connection" ) ;
2012-11-17 21:15:19 +00:00
}
// Check the connection has its constituent parts
if ( ! connection . host || ! connection . recipe ) {
2012-11-19 09:04:35 +00:00
return new Error ( "Missing connection data" ) ;
2012-11-17 21:15:19 +00:00
}
// Mark us as not logged in
2012-11-18 21:07:14 +00:00
this . wiki . addTiddler ( { title : TiddlyWebSyncer . titleIsLoggedIn , text : "no" } ) ;
2012-11-17 21:15:19 +00:00
// Save and return the connection object
this . connection = connection ;
2012-11-18 21:07:14 +00:00
// Listen out for changes to tiddlers
this . wiki . addEventListener ( "" , function ( changes ) {
self . syncToServer ( changes ) ;
} ) ;
2012-11-17 21:15:19 +00:00
// Get the login status
2012-11-18 10:24:20 +00:00
this . getStatus ( function ( err , isLoggedIn , json ) {
if ( isLoggedIn ) {
self . syncFromServer ( ) ;
}
} ) ;
2012-11-17 21:15:19 +00:00
return "" ; // We only support a single connection
} ;
2012-11-17 20:18:36 +00:00
/ *
Handle syncer messages
* /
TiddlyWebSyncer . prototype . handleEvent = function ( event ) {
switch ( event . type ) {
case "tw-login" :
this . promptLogin ( ) ;
break ;
case "tw-logout" :
this . logout ( ) ;
break ;
}
} ;
2012-11-17 12:32:51 +00:00
/ *
Invoke any tiddlyweb - startup modules
* /
2012-11-17 20:18:36 +00:00
TiddlyWebSyncer . prototype . invokeTiddlyWebStartupModules = function ( loggedIn ) {
2012-11-17 12:32:51 +00:00
$tw . modules . forEachModuleOfType ( "tiddlyweb-startup" , function ( title , module ) {
module . startup ( loggedIn ) ;
2012-11-16 17:01:04 +00:00
} ) ;
2012-11-17 20:18:36 +00:00
2012-11-16 17:01:04 +00:00
} ;
2012-11-17 20:18:36 +00:00
TiddlyWebSyncer . prototype . getCsrfToken = function ( ) {
2012-11-17 12:32:51 +00:00
var regex = /^(?:.*; )?csrf_token=([^(;|$)]*)(?:;|$)/ ,
match = regex . exec ( document . cookie ) ,
csrf = null ;
if ( match && ( match . length === 2 ) ) {
csrf = match [ 1 ] ;
}
return csrf ;
2012-11-17 20:18:36 +00:00
2012-11-17 12:32:51 +00:00
} ;
2012-11-17 20:18:36 +00:00
TiddlyWebSyncer . prototype . getStatus = function ( callback ) {
2012-11-16 17:01:04 +00:00
// Get status
2012-11-18 21:07:14 +00:00
var self = this ;
2012-11-17 20:18:36 +00:00
this . httpRequest ( {
2012-11-17 21:15:19 +00:00
url : this . connection . host + "status" ,
2012-11-16 17:01:04 +00:00
callback : function ( err , data ) {
2012-11-18 10:24:20 +00:00
if ( err ) {
return callback ( err ) ;
}
2012-11-17 12:32:51 +00:00
// Decode the status JSON
2012-11-16 17:01:04 +00:00
var json = null ;
try {
json = JSON . parse ( data ) ;
} catch ( e ) {
}
if ( json ) {
2012-11-17 12:32:51 +00:00
// Check if we're logged in
2012-11-16 17:01:04 +00:00
var isLoggedIn = json . username !== "GUEST" ;
2012-11-17 12:32:51 +00:00
// Set the various status tiddlers
2012-11-18 21:07:14 +00:00
self . wiki . addTiddler ( { title : TiddlyWebSyncer . titleIsLoggedIn , text : isLoggedIn ? "yes" : "no" } ) ;
2012-11-16 17:01:04 +00:00
if ( isLoggedIn ) {
2012-11-18 21:07:14 +00:00
self . wiki . addTiddler ( { title : TiddlyWebSyncer . titleUserName , text : json . username } ) ;
2012-11-16 17:01:04 +00:00
} else {
2012-11-18 21:07:14 +00:00
self . wiki . deleteTiddler ( TiddlyWebSyncer . titleUserName ) ;
2012-11-16 17:01:04 +00:00
}
}
2012-11-17 12:32:51 +00:00
// Invoke the callback if present
2012-11-16 17:01:04 +00:00
if ( callback ) {
2012-11-18 10:24:20 +00:00
callback ( null , isLoggedIn , json ) ;
2012-11-16 17:01:04 +00:00
}
}
} ) ;
} ;
/ *
2012-11-17 12:32:51 +00:00
Dispay a password prompt and allow the user to login
2012-11-16 17:01:04 +00:00
* /
2012-11-17 20:18:36 +00:00
TiddlyWebSyncer . prototype . promptLogin = function ( ) {
var self = this ;
this . getStatus ( function ( isLoggedIn , json ) {
2012-11-17 12:32:51 +00:00
if ( ! isLoggedIn ) {
$tw . passwordPrompt . createPrompt ( {
serviceName : "Login to TiddlySpace" ,
callback : function ( data ) {
2012-11-18 10:24:20 +00:00
self . login ( data . username , data . password , function ( err , isLoggedIn ) {
self . syncFromServer ( ) ;
} ) ;
2012-11-17 12:32:51 +00:00
return true ; // Get rid of the password prompt
}
} ) ;
}
} ) ;
2012-11-16 17:01:04 +00:00
} ;
/ *
2012-11-17 12:32:51 +00:00
Attempt to login to TiddlyWeb .
username : username
password : password
callback : invoked with arguments ( err , isLoggedIn )
2012-11-16 17:01:04 +00:00
* /
2012-11-17 20:18:36 +00:00
TiddlyWebSyncer . prototype . login = function ( username , password , callback ) {
2012-11-19 12:56:54 +00:00
var self = this ,
httpRequest = this . httpRequest ( {
url : this . connection . host + "challenge/tiddlywebplugins.tiddlyspace.cookie_form" ,
type : "POST" ,
data : {
user : username ,
password : password ,
tiddlyweb _redirect : "/status" // workaround to marginalize automatic subsequent GET
} ,
callback : function ( err , data ) {
if ( err ) {
2012-11-17 12:32:51 +00:00
if ( callback ) {
2012-11-19 12:56:54 +00:00
callback ( err ) ;
2012-11-17 12:32:51 +00:00
}
2012-11-19 12:56:54 +00:00
} else {
self . getStatus ( function ( err , isLoggedIn , json ) {
if ( callback ) {
callback ( null , isLoggedIn ) ;
}
} ) ;
}
2012-11-17 12:32:51 +00:00
}
2012-11-19 12:56:54 +00:00
} ) ;
2012-11-16 17:01:04 +00:00
} ;
/ *
2012-11-17 12:32:51 +00:00
Attempt to log out of TiddlyWeb
2012-11-16 17:01:04 +00:00
* /
2012-11-17 20:18:36 +00:00
TiddlyWebSyncer . prototype . logout = function ( options ) {
2012-11-16 17:01:04 +00:00
options = options || { } ;
2012-11-17 20:18:36 +00:00
var self = this ;
var httpRequest = this . httpRequest ( {
2012-11-17 21:15:19 +00:00
url : this . connection . host + "logout" ,
2012-11-16 17:01:04 +00:00
type : "POST" ,
data : {
2012-11-17 20:18:36 +00:00
csrf _token : this . getCsrfToken ( ) ,
2012-11-16 17:01:04 +00:00
tiddlyweb _redirect : "/status" // workaround to marginalize automatic subsequent GET
} ,
callback : function ( err , data ) {
if ( err ) {
2012-11-17 20:31:30 +00:00
self . showError ( "logout error: " + err ) ;
2012-11-16 17:01:04 +00:00
} else {
2012-11-17 20:31:30 +00:00
self . getStatus ( ) ;
2012-11-16 17:01:04 +00:00
}
}
} ) ;
} ;
2012-11-18 13:14:28 +00:00
/ *
2012-11-19 09:04:35 +00:00
Convert a TiddlyWeb JSON tiddler into a TiddlyWiki5 tiddler and save it in the store
2012-11-18 13:14:28 +00:00
* /
2012-11-19 09:04:35 +00:00
TiddlyWebSyncer . prototype . storeTiddler = function ( tiddlerFields , revision ) {
2012-11-18 13:14:28 +00:00
var result = { } ;
2012-11-18 14:57:54 +00:00
// Transfer the fields, pulling down the `fields` hashmap
2012-11-19 09:04:35 +00:00
$tw . utils . each ( tiddlerFields , function ( element , title , object ) {
switch ( title ) {
2012-11-18 13:14:28 +00:00
case "fields" :
2012-11-19 09:04:35 +00:00
$tw . utils . each ( element , function ( element , subTitle , object ) {
result [ subTitle ] = element ;
} ) ;
2012-11-18 13:14:28 +00:00
break ;
default :
2012-11-19 09:04:35 +00:00
result [ title ] = tiddlerFields [ title ] ;
2012-11-18 13:14:28 +00:00
break ;
}
2012-11-19 09:04:35 +00:00
} ) ;
2012-11-18 14:57:54 +00:00
// Some unholy freaking of content types
if ( result . type === "text/javascript" ) {
result . type = "application/javascript" ;
} else if ( ! result . type || result . type === "None" ) {
2012-11-18 15:22:13 +00:00
result . type = "text/vnd.tiddlywiki2" ;
2012-11-18 14:57:54 +00:00
}
2012-11-19 09:04:35 +00:00
// Save the tiddler
this . wiki . addTiddler ( new $tw . Tiddler ( result ) ) ;
// Save the tiddler revision and changeCount details
this . tiddlerInfo [ result . title ] = {
revision : revision ,
changeCount : this . wiki . getChangeCount ( result . title )
} ;
2012-11-18 13:14:28 +00:00
} ;
2012-11-18 10:24:20 +00:00
/ *
Synchronise from the server by reading the tiddler list from the recipe and queuing up GETs for any tiddlers that we don ' t already have
* /
TiddlyWebSyncer . prototype . syncFromServer = function ( ) {
var self = this ;
this . httpRequest ( {
url : this . connection . host + "recipes/" + this . connection . recipe + "/tiddlers.json" ,
callback : function ( err , data ) {
if ( err ) {
console . log ( "error in syncFromServer" , err ) ;
return ;
}
var json = JSON . parse ( data ) ;
for ( var t = 0 ; t < json . length ; t ++ ) {
2012-11-19 09:04:35 +00:00
self . storeTiddler ( json [ t ] , json [ t ] . revision ) ;
2012-11-18 10:24:20 +00:00
}
}
} ) ;
} ;
2012-11-18 21:07:14 +00:00
/ *
Synchronise a set of changes to the server
* /
TiddlyWebSyncer . prototype . syncToServer = function ( changes ) {
2012-11-19 09:04:35 +00:00
var self = this ,
now = new Date ( ) ;
$tw . utils . each ( changes , function ( element , title , object ) {
// Queue a task to sync this tiddler
self . enqueueSyncTask ( {
type : "save" ,
2012-11-19 12:56:54 +00:00
title : title
2012-11-19 09:04:35 +00:00
} ) ;
} ) ;
} ;
/ *
Queue up a sync task . If there is already a pending task for the tiddler , just update the last modification time
* /
TiddlyWebSyncer . prototype . enqueueSyncTask = function ( task ) {
2012-11-19 12:56:54 +00:00
var self = this ,
now = new Date ( ) ;
// Set the timestamps on this task
task . queueTime = now ;
task . lastModificationTime = now ;
2012-11-19 09:04:35 +00:00
// Bail if it's not a tiddler we know about
if ( ! $tw . utils . hop ( this . tiddlerInfo , task . title ) ) {
return ;
}
2012-11-19 12:56:54 +00:00
// 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 ) {
2012-11-19 09:04:35 +00:00
return ;
}
// Check if this tiddler is already in the queue
if ( $tw . utils . hop ( this . taskQueue , task . title ) ) {
2012-11-19 12:56:54 +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 GET is turned into a PUT if the tiddler changes locally in the meantime. But a pending save is not modified to become a GET
if ( task . type === "save" ) {
existingTask . type = "save" ;
}
2012-11-19 09:04:35 +00:00
} else {
// If it is not in the queue, insert it
this . taskQueue [ task . title ] = task ;
}
2012-11-19 12:56:54 +00:00
// Process the queue
$tw . utils . nextTick ( function ( ) { self . processTaskQueue . call ( self ) ; } ) ;
2012-11-18 21:07:14 +00:00
} ;
2012-11-16 17:01:04 +00:00
/ *
2012-11-19 12:56:54 +00:00
Return the number of tasks in progress
2012-11-18 13:14:28 +00:00
* /
2012-11-19 12:56:54 +00:00
TiddlyWebSyncer . prototype . numTasksInProgress = function ( ) {
return $tw . utils . count ( this . taskInProgress ) ;
} ;
/ *
Return the number of tasks in the queue
* /
TiddlyWebSyncer . prototype . numTasksInQueue = function ( ) {
return $tw . utils . count ( this . taskQueue ) ;
} ;
/ *
Trigger a timeout if one isn ' t already outstanding
* /
TiddlyWebSyncer . prototype . triggerTimeout = function ( ) {
2012-11-18 13:14:28 +00:00
var self = this ;
2012-11-19 12:56:54 +00:00
if ( ! this . taskTimerId ) {
this . taskTimerId = window . setTimeout ( function ( ) {
self . taskTimerId = null ;
self . processTaskQueue . call ( self ) ;
} , TiddlyWebSyncer . taskTimerInterval ) ;
}
} ;
/ *
Process the task queue , performing the next task if appropriate
* /
TiddlyWebSyncer . 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 ;
// Dispatch the task
this . dispatchTask ( task , function ( err ) {
console . log ( "Done task" , task . title , "error" , err ) ;
// Mark that this task is no longer in progress
delete self . taskInProgress [ task . title ] ;
// 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 ( ) ;
2012-11-18 13:14:28 +00:00
}
}
2012-11-19 12:56:54 +00:00
}
} ;
/ *
Choose the next applicable task
* /
TiddlyWebSyncer . prototype . chooseNextTask = function ( ) {
var self = this ,
candidateTask = null ,
now = new Date ( ) ;
// 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 ) < TiddlyWebSyncer . throttleInterval &&
( now - task . queueTime ) < TiddlyWebSyncer . 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
* /
TiddlyWebSyncer . prototype . dispatchTask = function ( task , callback ) {
var self = this ;
if ( task . type === "save" ) {
var changeCount = this . wiki . getChangeCount ( task . title ) ;
this . httpRequest ( {
url : this . connection . host + "recipes/" + this . connection . recipe + "/tiddlers/" + task . title ,
type : "PUT" ,
headers : {
"Content-type" : "application/json"
} ,
data : this . convertTiddlerToTiddlyWebFormat ( task . title ) ,
callback : function ( err , data , request ) {
if ( err ) {
return callback ( err ) ;
}
// Save the details of the new revision of the tiddler
var tiddlerInfo = self . tiddlerInfo [ task . title ] ;
tiddlerInfo . changeCount = changeCount ;
tiddlerInfo . revision = self . getRevisionFromEtag ( request ) ;
// Invoke the callback
callback ( null ) ;
}
} ) ;
} else if ( task . type === "load" ) {
// Load the tiddler
this . httpRequest ( {
url : this . connection . host + "recipes/" + this . connection . recipe + "/tiddlers/" + task . title ,
callback : function ( err , data , request ) {
if ( err ) {
return callback ( err ) ;
}
// Store the tiddler and revision number
self . storeTiddler ( JSON . parse ( data ) , self . getRevisionFromEtag ( request ) ) ;
// Invoke the callback
callback ( null ) ;
}
} ) ;
}
} ;
/ *
Convert a tiddler to a field set suitable for PUTting to TiddlyWeb
* /
TiddlyWebSyncer . prototype . convertTiddlerToTiddlyWebFormat = function ( title ) {
var result = { } ,
tiddler = this . wiki . getTiddler ( title ) ,
knownFields = [
"bag" , "created" , "creator" , "modified" , "modifier" , "permissions" , "recipe" , "revision" , "tags" , "text" , "title" , "type" , "uri"
] ;
if ( tiddler ) {
$tw . utils . each ( tiddler . fields , function ( fieldValue , fieldName ) {
var fieldString = tiddler . getFieldString ( fieldName ) ;
if ( knownFields . indexOf ( fieldName ) !== - 1 ) {
// If it's a known field, just copy it across
result [ fieldName ] = fieldString ;
} else {
// If it's unknown, put it in the "fields" field
result . fields = result . fields || { } ;
result . fields [ fieldName ] = fieldString ;
}
} ) ;
}
return JSON . stringify ( result ) ;
} ;
/ *
Extract the revision from the Etag header of a request
* /
TiddlyWebSyncer . prototype . getRevisionFromEtag = function ( request ) {
var etag = request . getResponseHeader ( "Etag" ) ;
if ( etag ) {
return etag . split ( "/" ) [ 2 ] . split ( ":" ) [ 0 ] ; // etags are like "system-images_public/unsyncedIcon/946151:9f11c278ccde3a3149f339f4a1db80dd4369fc04"
} else {
return 0 ;
}
} ;
/ *
Lazily load a skinny tiddler if we can
* /
TiddlyWebSyncer . prototype . lazyLoad = function ( connection , title , tiddler ) {
// Queue up a sync task to load this tiddler
this . enqueueSyncTask ( {
type : "load" ,
title : title
2012-11-18 13:14:28 +00:00
} ) ;
} ;
/ *
A quick and dirty HTTP function ; to be refactored later . Options are :
2012-11-16 17:01:04 +00:00
url : URL to retrieve
type : GET , PUT , POST etc
callback : function invoked with ( err , data )
* /
2012-11-17 20:18:36 +00:00
TiddlyWebSyncer . prototype . httpRequest = function ( options ) {
2012-11-16 17:01:04 +00:00
var type = options . type || "GET" ,
2012-11-18 13:14:28 +00:00
headers = options . headers || { accept : "application/json" } ,
2012-11-19 09:04:35 +00:00
request = new XMLHttpRequest ( ) ,
2012-11-16 17:01:04 +00:00
data = "" ,
f , results ;
2012-11-18 13:14:28 +00:00
// Massage the data hashmap into a string
2012-11-16 17:01:04 +00:00
if ( options . data ) {
if ( typeof options . data === "string" ) { // Already a string
data = options . data ;
} else { // A hashmap of strings
results = [ ] ;
2012-11-19 09:04:35 +00:00
$tw . utils . each ( options . data , function ( element , title , object ) {
results . push ( title + "=" + encodeURIComponent ( element ) ) ;
} ) ;
2012-11-16 17:01:04 +00:00
data = results . join ( "&" )
}
}
2012-11-18 13:14:28 +00:00
// Set up the state change handler
2012-11-19 09:04:35 +00:00
request . onreadystatechange = function ( ) {
2012-11-16 17:01:04 +00:00
if ( this . readyState === 4 ) {
if ( this . status === 200 ) {
// success!
2012-11-19 09:04:35 +00:00
options . callback ( null , this . responseText , this ) ;
2012-11-16 17:01:04 +00:00
return ;
}
// something went wrong
options . callback ( new Error ( "XMLHttpRequest error: " + this . status ) ) ;
}
} ;
2012-11-18 13:14:28 +00:00
// Make the request
2012-11-19 09:04:35 +00:00
request . open ( type , options . url , true ) ;
2012-11-18 13:14:28 +00:00
if ( headers ) {
2012-11-19 09:04:35 +00:00
$tw . utils . each ( headers , function ( element , title , object ) {
request . setRequestHeader ( title , element ) ;
} ) ;
2012-11-18 13:14:28 +00:00
}
2012-11-19 12:56:54 +00:00
if ( data && ! $tw . utils . hop ( headers , "Content-type" ) ) {
2012-11-19 09:04:35 +00:00
request . setRequestHeader ( "Content-type" , "application/x-www-form-urlencoded; charset=UTF-8" ) ;
2012-11-16 17:01:04 +00:00
}
2012-11-19 09:04:35 +00:00
request . send ( data ) ;
return request ;
2012-11-16 17:01:04 +00:00
} ;
2012-11-17 20:18:36 +00:00
// Only export anything on the browser
if ( $tw . browser ) {
exports . name = "tiddlywebsyncer" ;
exports . syncer = TiddlyWebSyncer ;
}
2012-11-16 17:01:04 +00:00
} ) ( ) ;