From 09a0b2a33657e00df6c171f91cffb73debb434c9 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sun, 17 Mar 2013 18:21:24 +0000 Subject: [PATCH] Use the newly refactored syncer in favour of the old tiddlyweb code --- plugins/tiddlywiki/tiddlyweb/tiddlyweb.js | 681 ------------------ .../tiddlywebadaptor.js | 0 plugins/tiddlywiki/tiddlyweb2/plugin.bundle | 7 - 3 files changed, 688 deletions(-) delete mode 100644 plugins/tiddlywiki/tiddlyweb/tiddlyweb.js rename plugins/tiddlywiki/{tiddlyweb2 => tiddlyweb}/tiddlywebadaptor.js (100%) delete mode 100644 plugins/tiddlywiki/tiddlyweb2/plugin.bundle diff --git a/plugins/tiddlywiki/tiddlyweb/tiddlyweb.js b/plugins/tiddlywiki/tiddlyweb/tiddlyweb.js deleted file mode 100644 index bbaf474bb..000000000 --- a/plugins/tiddlywiki/tiddlyweb/tiddlyweb.js +++ /dev/null @@ -1,681 +0,0 @@ -/*\ -title: $:/plugins/tiddlywiki/tiddlyweb/tiddlyweb.js -type: application/javascript -module-type: syncer - -Syncer module for TiddlyWeb-compatible web servers. It is used for working with TiddlyWeb, TiddlySpace and with TiddlyWiki5's built in web server. - -The subset of TiddlyWeb features that are required are described below. - -! TiddlyWeb format JSON tiddlers - -TiddlyWeb uses JSON to represent tiddlers as a hashmap object with the wrinkle that fields other than the standard ones are stored in a special `fields` object. For example: - -``` -{ - creator: "jermolene", - fields: { - _hash: "da39a3ee5e6b4b0d3255bfef95601890afd80709", - customField: "Some custom value" - }, - created: "20130309145404", - recipe: "spacename_private", - modified: "20130309145414", - text: "", - title: "Testing times", - modifier: "jermolene", - type: null, - tags: [], - revision: 1139558 -} -``` - -The revision field is treated as an opaque string by TiddlyWiki5, and only tested for equality. If it is passed as a number it is converted to a string before use. - -! Get Status - -`GET ///status` returns a JSON object that has the following fields: - -* `username`, a string containing the username of the currently logged-in user, or the special value `GUEST` for non-authenticated users -* optionally, `space`, an object, may be present containing a field `recipe` that contains the name o the recipe that generated this wiki - -! Get Skinny Tiddlers - -`GET ///tiddlers.json` or, if a recipe was specified in the results of the status request, `GET ///recipes//tiddlers.json`, returns a JSON array of skinny tiddler objects in TiddlyWeb format. "Skinny" means that the tiddlers lack a `text` field. - -! Get Tiddler - -`GET ///tiddlers/` or, if a recipe was specified in the results of the status request, `GET <protocol>//<host>/recipes/<recipe>/tiddlers/<title>`, returns a tiddler in TiddlyWeb format. - -! Put Tiddler - -`PUT <protocol>//<host>/tiddlers/<title>` or, if a recipe was specified in the results of the status request, `PUT <protocol>//<host>/recipes/<recipe>/tiddlers/<title>`, saves a tiddler in TiddlyWeb format. - -\*/ -(function(){ - -/*jslint node: true, browser: true */ -/*global $tw: false */ -"use strict"; - -/* -Creates a TiddlyWebSyncer object -*/ -var TiddlyWebSyncer = function(options) { - this.wiki = options.wiki; - // Hashmap of {revision:,bag:,changeCount:} - this.tiddlerInfo = {}; - var self = this; - // Record information for known tiddlers - this.wiki.forEachTiddler(function(title,tiddler) { - if(tiddler.fields["revision"]) { - self.tiddlerInfo[title] = { - revision: tiddler.fields["revision"], - bag: tiddler.fields["bag"], - changeCount: self.wiki.getChangeCount(title) - } - } - }); - // Tasks are {type: "load"/"save"/"delete", title:, queueTime:, lastModificationTime:} - this.taskQueue = {}; // Hashmap of tasks to be performed - this.taskInProgress = {}; // Hash of tasks in progress - this.taskTimerId = null; // Sync timer - // Compute the host and recipe - this.host = document.location.protocol + "//" + document.location.host + "/"; - this.recipe = undefined; // Filled in by getStatus() - // Mark us as not logged in - this.wiki.addTiddler({title: TiddlyWebSyncer.titleIsLoggedIn,text: "no"}); - // Listen out for changes to tiddlers - this.wiki.addEventListener("change",function(changes) { - self.syncToServer(changes); - }); - // Listen out for lazyLoad events - this.wiki.addEventListener("lazyLoad",function(title) { - self.handleLazyLoadEvent(title); - }); - // Listen our for login/logout events - document.addEventListener("tw-login",function(event) { - self.handleLoginEvent(event); - },false); - document.addEventListener("tw-logout",function(event) { - self.handleLogoutEvent(event); - },false); - // Get the login status - this.getStatus(function (err,isLoggedIn,json) { - if(isLoggedIn) { - // Do a sync - self.syncFromServer(); - } - }); - this.log("Initialising with host:",this.host); -}; - -TiddlyWebSyncer.titleIsLoggedIn = "$:/plugins/tiddlyweb/IsLoggedIn"; -TiddlyWebSyncer.titleUserName = "$:/plugins/tiddlyweb/UserName"; -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 -TiddlyWebSyncer.pollTimerInterval = 60 * 1000; // Interval for polling for changes on the server - -/* -Error handling -*/ -TiddlyWebSyncer.prototype.showError = function(error) { - alert("TiddlyWeb error: " + error); - console.log("TiddlyWeb error: " + error); -}; - -/* -Message logging -*/ -TiddlyWebSyncer.prototype.log = function(/* arguments */) { - var args = Array.prototype.slice.call(arguments,0); - args[0] = "TiddlyWeb: " + args[0]; - $tw.utils.log.apply(null,args); -}; - -/* -Lazily load a skinny tiddler if we can -*/ -TiddlyWebSyncer.prototype.handleLazyLoadEvent = function(title) { - // Queue up a sync task to load this tiddler - this.enqueueSyncTask({ - type: "load", - title: title - }); -}; - -/* -Get the current status of the TiddlyWeb connection -*/ -TiddlyWebSyncer.prototype.getStatus = function(callback) { - // Get status - var self = this; - this.log("Getting status"); - this.httpRequest({ - url: this.host + "status", - callback: function(err,data) { - if(err) { - return callback(err); - } - // Decode the status JSON - var json = null, - isLoggedIn = false; - try { - json = JSON.parse(data); - } catch (e) { - } - if(json) { - // Record the recipe - if(json.space) { - self.recipe = json.space.recipe; - } - // Check if we're logged in - isLoggedIn = json.username !== "GUEST"; - // Set the various status tiddlers - self.wiki.addTiddler({title: TiddlyWebSyncer.titleIsLoggedIn,text: isLoggedIn ? "yes" : "no"}); - if(isLoggedIn) { - self.wiki.addTiddler({title: TiddlyWebSyncer.titleUserName,text: json.username}); - } else { - self.wiki.deleteTiddler(TiddlyWebSyncer.titleUserName); - } - } - // Invoke the callback if present - if(callback) { - callback(null,isLoggedIn,json); - } - } - }); -}; - -/* -Dispay a password prompt and allow the user to login -*/ -TiddlyWebSyncer.prototype.handleLoginEvent = function() { - var self = this; - this.getStatus(function(isLoggedIn,json) { - 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) -*/ -TiddlyWebSyncer.prototype.login = function(username,password,callback) { - this.log("Attempting to login as",username); - var self = this, - httpRequest = this.httpRequest({ - url: this.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) { - if(callback) { - callback(err); - } - } else { - self.log("Returned from logging in with data:",data); - self.getStatus(function(err,isLoggedIn,json) { - if(callback) { - callback(null,isLoggedIn); - } - }); - } - } - }); -}; - -/* -Attempt to log out of TiddlyWeb -*/ -TiddlyWebSyncer.prototype.handleLogoutEvent = function(options) { - options = options || {}; - this.log("Attempting to logout"); - var self = this, - httpRequest = this.httpRequest({ - url: this.host + "logout", - type: "POST", - data: { - csrf_token: this.getCsrfToken(), - tiddlyweb_redirect: "/status" // workaround to marginalize automatic subsequent GET - }, - callback: function(err,data) { - if(err) { - self.showError("logout error: " + err); - } else { - self.log("Returned from logging out with data:",data); - self.getStatus(); - } - } - }); -}; - -/* -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() { - this.log("Retrieving skinny tiddler list"); - var self = this; - this.httpRequest({ - url: this.host + "recipes/" + this.recipe + "/tiddlers.json", - callback: function(err,data) { - // Check for errors - if(err) { - self.log("Error retrieving skinny tiddler list:",err); - return; - } - // Process each incoming tiddler - var json = JSON.parse(data); - for(var t=0; t<json.length; t++) { - // Get the incoming tiddler fields, and the existing tiddler - var tiddlerFields = json[t], - incomingRevision = tiddlerFields.revision.toString(), - 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); - } - } - } - // Trigger another sync - window.setTimeout(function() { - self.syncFromServer.call(self); - },TiddlyWebSyncer.pollTimerInterval); - } - }); -}; - -/* -Synchronise a set of changes to the server -*/ -TiddlyWebSyncer.prototype.syncToServer = function(changes) { - var self = this, - now = new Date(); - $tw.utils.each(changes,function(change,title,object) { - // Queue a task to sync this tiddler - self.enqueueSyncTask({ - type: change.deleted ? "delete" : "save", - title: title - }); - }); -}; - -/* -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) { - var self = this, - now = new Date(); - // 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] = { - revision: "0", - bag: "bag-not-set", - 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)) { - this.log("Re-queueing up sync task with type:",task.type,"title:",task.title); - 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"; - } - } else { - this.log("Queuing up sync task with type:",task.type,"title:",task.title); - // If it is not in the queue, insert it - this.taskQueue[task.title] = task; - } - // Process the queue - $tw.utils.nextTick(function() {self.processTaskQueue.call(self);}); -}; - -/* -Return the number of tasks in progress -*/ -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() { - var self = this; - 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) { - // 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(); - } - } - } -}; - -/* -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.log("Dispatching 'save' task:",task.title); - this.httpRequest({ - url: this.host + "recipes/" + encodeURIComponent(this.recipe) + "/tiddlers/" + encodeURIComponent(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 etagInfo = self.parseEtag(request.getResponseHeader("Etag")), - tiddlerInfo = self.tiddlerInfo[task.title]; - tiddlerInfo.changeCount = changeCount; - tiddlerInfo.bag = etagInfo.bag; - tiddlerInfo.revision = etagInfo.revision; - // Invoke the callback - callback(null); - } - }); - } else if(task.type === "load") { - // Load the tiddler - this.log("Dispatching 'load' task:",task.title); - this.httpRequest({ - url: this.host + "recipes/" + encodeURIComponent(this.recipe) + "/tiddlers/" + encodeURIComponent(task.title), - callback: function(err,data,request) { - if(err) { - return callback(err); - } - // Store the tiddler and revision number - self.storeTiddler(JSON.parse(data)); - // Invoke the callback - callback(null); - } - }); - } else if(task.type === "delete") { - // Delete the tiddler - this.log("Dispatching 'delete' task:",task.title); - var bag = this.tiddlerInfo[task.title].bag; - this.httpRequest({ - url: this.host + "bags/" + encodeURIComponent(bag) + "/tiddlers/" + encodeURIComponent(task.title), - type: "DELETE", - callback: function(err,data,request) { - if(err) { - return callback(err); - } - // Invoke the callback - callback(null); - } - }); - } -}; - -/* -Convert a TiddlyWeb JSON tiddler into a TiddlyWiki5 tiddler and save it in the store. Returns true if the tiddler was actually stored -*/ -TiddlyWebSyncer.prototype.storeTiddler = function(tiddlerFields) { - var self = this, - result = {}; - // Transfer the fields, pulling down the `fields` hashmap - $tw.utils.each(tiddlerFields,function(element,title,object) { - if(title === "fields") { - $tw.utils.each(element,function(element,subTitle,object) { - result[subTitle] = element; - }); - } else { - result[title] = tiddlerFields[title]; - } - }); - // Some unholy freaking of content types - if(result.type === "text/javascript") { - result.type = "application/javascript"; - } else if(!result.type || result.type === "None") { - result.type = "text/x-tiddlywiki"; - } - // Save the tiddler - self.wiki.addTiddler(new $tw.Tiddler(self.wiki.getTiddler(result.title),result)); - // Save the tiddler revision and changeCount details - self.tiddlerInfo[result.title] = { - revision: tiddlerFields.revision, - bag: tiddlerFields.bag, - changeCount: self.wiki.getChangeCount(result.title) - }; -}; - -/* -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 = fieldName === "tags" ? - tiddler.fields.tags : - tiddler.getFieldString(fieldName); // Tags must be passed as an array, not a string - - 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; - } - }); - } - // Convert the type "text/x-tiddlywiki" into null - if(result.type === "text/x-tiddlywiki") { - result.type = null; - } - return JSON.stringify(result,null,$tw.config.preferences.jsonSpaces); -}; - -/* -Split a TiddlyWeb Etag into its constituent parts. For example: - -``` -"system-images_public/unsyncedIcon/946151:9f11c278ccde3a3149f339f4a1db80dd4369fc04" -``` - -Note that the value includes the opening and closing double quotes. - -The parts are: - -``` -<bag>/<title>/<revision>:<hash> -``` -*/ -TiddlyWebSyncer.prototype.parseEtag = function(etag) { - var firstSlash = etag.indexOf("/"), - lastSlash = etag.lastIndexOf("/"), - colon = etag.lastIndexOf(":"); - if(firstSlash === -1 || lastSlash === -1 || colon === -1) { - return null; - } else { - return { - bag: decodeURIComponent(etag.substring(1,firstSlash)), - title: decodeURIComponent(etag.substring(firstSlash + 1,lastSlash)), - revision: etag.substring(lastSlash + 1,colon) - } - } -}; - -/* -A quick and dirty HTTP function; to be refactored later. Options are: - url: URL to retrieve - type: GET, PUT, POST etc - callback: function invoked with (err,data) -*/ -TiddlyWebSyncer.prototype.httpRequest = function(options) { - var type = options.type || "GET", - headers = options.headers || {accept: "application/json"}, - request = new XMLHttpRequest(), - data = "", - f,results; - // Massage the data hashmap into a string - if(options.data) { - if(typeof options.data === "string") { // Already a string - data = options.data; - } else { // A hashmap of strings - results = []; - $tw.utils.each(options.data,function(dataItem,dataItemTitle) { - results.push(dataItemTitle + "=" + encodeURIComponent(dataItem)); - }); - data = results.join("&"); - } - } - // Set up the state change handler - request.onreadystatechange = function() { - if(this.readyState === 4) { - if(this.status === 200 || this.status === 204) { - // Success! - options.callback(null,this.responseText,this); - return; - } - // Something went wrong - options.callback(new Error("XMLHttpRequest error: " + this.status)); - } - }; - // Make the request - request.open(type,options.url,true); - if(headers) { - $tw.utils.each(headers,function(header,headerTitle,object) { - request.setRequestHeader(headerTitle,header); - }); - } - if(data && !$tw.utils.hop(headers,"Content-type")) { - request.setRequestHeader("Content-type","application/x-www-form-urlencoded; charset=UTF-8"); - } - request.send(data); - return request; -}; - -/* -Retrieve the CSRF token from its cookie -*/ -TiddlyWebSyncer.prototype.getCsrfToken = function() { - var regex = /^(?:.*; )?csrf_token=([^(;|$)]*)(?:;|$)/, - match = regex.exec(document.cookie), - csrf = null; - if (match && (match.length === 2)) { - csrf = match[1]; - } - return csrf; -}; - -// Only export anything on the browser -if($tw.browser) { - exports.name = "tiddlywebsyncer"; - exports.syncer = TiddlyWebSyncer; -} - -})(); diff --git a/plugins/tiddlywiki/tiddlyweb2/tiddlywebadaptor.js b/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js similarity index 100% rename from plugins/tiddlywiki/tiddlyweb2/tiddlywebadaptor.js rename to plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js diff --git a/plugins/tiddlywiki/tiddlyweb2/plugin.bundle b/plugins/tiddlywiki/tiddlyweb2/plugin.bundle deleted file mode 100644 index 882ecb543..000000000 --- a/plugins/tiddlywiki/tiddlyweb2/plugin.bundle +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "$:/plugins/tiddlywiki/tiddlyweb", - "description": "TiddlyWeb and TiddlySpace components", - "author": "JeremyRuston", - "version": "0.0.0", - "coreVersion": ">=5.0.0" -}