mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-01-05 23:10:28 +00:00
First pass at SSE support
This commit is contained in:
parent
708e21951f
commit
7a0c43436f
@ -108,6 +108,38 @@ Get details of changed tiddlers from the server
|
||||
*/
|
||||
MultiWikiClientAdaptor.prototype.getUpdatedTiddlers = function(syncer,callback) {
|
||||
var self = this;
|
||||
const eventSource = new EventSource("/recipes/" + this.recipe + "/events?last_known_tiddler_id=" + this.last_known_tiddler_id);
|
||||
eventSource.onerror = function(event) {
|
||||
console.log("SSE connection error",event);
|
||||
}
|
||||
eventSource.onopen = function(event) {
|
||||
console.log("SSE connection opened",event);
|
||||
}
|
||||
eventSource.onmessage = function(event) {
|
||||
console.log("SSE Event",event);
|
||||
};
|
||||
eventSource.addEventListener("change", function(event) {
|
||||
const data = $tw.utils.parseJSONSafe(event.data);
|
||||
if(data) {
|
||||
console.log("SSE data",data)
|
||||
if(data.is_deleted) {
|
||||
self.removeTiddlerInfo(data.title);
|
||||
delete syncer.tiddlerInfo[data.title];
|
||||
syncer.logger.log("Deleting tiddler missing from server:",data.title);
|
||||
syncer.wiki.deleteTiddler(data.title);
|
||||
syncer.processTaskQueue();
|
||||
} else {
|
||||
syncer.titlesToBeLoaded[data.title] = true;
|
||||
syncer.processTaskQueue();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return callback(null,{
|
||||
modifications: [],
|
||||
deletions: []
|
||||
});
|
||||
|
||||
$tw.utils.httpRequest({
|
||||
url: this.host + "recipes/" + this.recipe + "/tiddlers.json",
|
||||
data: {
|
||||
|
@ -0,0 +1,88 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-recipe-events.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
GET /recipes/:recipe_name/events
|
||||
|
||||
headers:
|
||||
|
||||
Last-Event-ID:
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
const SSE_HEARTBEAT_INTERVAL_MS = 10 * 1000;
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/recipes\/([^\/]+)\/events$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
// Get the parameters
|
||||
const recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]);
|
||||
let last_known_tiddler_id = 0;
|
||||
if(request.headers["Last-Event-ID"]) {
|
||||
last_known_tiddler_id = $tw.utils.parseNumber(request.headers["Last-Event-ID"]);
|
||||
} else if(state.queryParameters.last_known_tiddler_id) {
|
||||
last_known_tiddler_id = $tw.utils.parseNumber(state.queryParameters.last_known_tiddler_id);
|
||||
}
|
||||
if(recipe_name) {
|
||||
// Start streaming the response
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive"
|
||||
});
|
||||
// Setup the heartbeat timer
|
||||
var heartbeatTimer = setInterval(function() {
|
||||
response.write(':keep-alive\n\n');
|
||||
},SSE_HEARTBEAT_INTERVAL_MS);
|
||||
// Method to get changed tiddler events and send to the client
|
||||
function sendUpdates() {
|
||||
// Get the tiddlers in the recipe since the last known tiddler_id
|
||||
var recipeTiddlers = $tw.mws.store.getRecipeTiddlers(recipe_name,{
|
||||
include_deleted: true,
|
||||
last_known_tiddler_id: last_known_tiddler_id
|
||||
});
|
||||
// Send to the client
|
||||
if(recipeTiddlers) {
|
||||
for(let index = recipeTiddlers.length-1; index>=0; index--) {
|
||||
const tiddlerInfo = recipeTiddlers[index];
|
||||
if(tiddlerInfo.tiddler_id > last_known_tiddler_id) {
|
||||
last_known_tiddler_id = tiddlerInfo.tiddler_id;
|
||||
}
|
||||
response.write(`event: change\n`)
|
||||
let data = tiddlerInfo;
|
||||
if(!tiddlerInfo.is_deleted) {
|
||||
const tiddler = $tw.mws.store.getRecipeTiddler(tiddlerInfo.title,recipe_name);
|
||||
if(tiddler) {
|
||||
data = $tw.utils.extend({},data,{tiddler: tiddler.tiddler})
|
||||
}
|
||||
}
|
||||
response.write(`data: ${JSON.stringify(data)}\n`);
|
||||
response.write(`id: ${tiddlerInfo.tiddler_id}\n`)
|
||||
response.write(`\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Send current and future changes
|
||||
sendUpdates();
|
||||
$tw.mws.store.addEventListener("change",sendUpdates);
|
||||
// Clean up when the connection closes
|
||||
response.on("close",function () {
|
||||
clearInterval(heartbeatTimer);
|
||||
$tw.mws.store.removeEventListener("change",sendUpdates);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Fail if something went wrong
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -27,6 +27,8 @@ function SqlTiddlerStore(options) {
|
||||
options = options || {};
|
||||
this.attachmentStore = options.attachmentStore;
|
||||
this.adminWiki = options.adminWiki || $tw.wiki;
|
||||
this.eventListeners = {}; // Hashmap by type of array of event listener functions
|
||||
this.eventOutstanding = {}; // Hashmap by type of boolean true of outstanding events
|
||||
// Create the database
|
||||
this.databasePath = options.databasePath || ":memory:";
|
||||
var SqlTiddlerDatabase = require("$:/plugins/tiddlywiki/multiwikiserver/store/sql-tiddler-database.js").SqlTiddlerDatabase;
|
||||
@ -37,6 +39,39 @@ function SqlTiddlerStore(options) {
|
||||
this.sqlTiddlerDatabase.createTables();
|
||||
}
|
||||
|
||||
SqlTiddlerStore.prototype.addEventListener = function(type,listener) {
|
||||
this.eventListeners[type] = this.eventListeners[type] || [];
|
||||
this.eventListeners[type].push(listener);
|
||||
};
|
||||
|
||||
SqlTiddlerStore.prototype.removeEventListener = function(type,listener) {
|
||||
const listeners = this.eventListeners[type];
|
||||
if(listeners) {
|
||||
var p = listeners.indexOf(listener);
|
||||
if(p !== -1) {
|
||||
listeners.splice(p,1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SqlTiddlerStore.prototype.dispatchEvent = function(type /*, args */) {
|
||||
const self = this;
|
||||
if(!this.eventOutstanding[type]) {
|
||||
$tw.utils.nextTick(function() {
|
||||
self.eventOutstanding[type] = false;
|
||||
const args = Array.prototype.slice.call(arguments,1),
|
||||
listeners = self.eventListeners[type];
|
||||
if(listeners) {
|
||||
for(var p=0; p<listeners.length; p++) {
|
||||
var listener = listeners[p];
|
||||
listener.apply(listener,args);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.eventOutstanding[type] = true;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Returns null if a bag/recipe name is valid, or a string error message if not
|
||||
*/
|
||||
@ -142,6 +177,7 @@ SqlTiddlerStore.prototype.saveTiddlersFromPath = function(tiddler_files_path,bag
|
||||
}
|
||||
}
|
||||
});
|
||||
self.dispatchEvent("change");
|
||||
};
|
||||
|
||||
SqlTiddlerStore.prototype.listBags = function() {
|
||||
@ -156,6 +192,7 @@ SqlTiddlerStore.prototype.createBag = function(bag_name,description) {
|
||||
return {message: validationBagName};
|
||||
}
|
||||
self.sqlTiddlerDatabase.createBag(bag_name,description);
|
||||
self.dispatchEvent("change");
|
||||
return null;
|
||||
});
|
||||
};
|
||||
@ -184,6 +221,7 @@ SqlTiddlerStore.prototype.createRecipe = function(recipe_name,bag_names,descript
|
||||
var self = this;
|
||||
return this.sqlTiddlerDatabase.transaction(function() {
|
||||
self.sqlTiddlerDatabase.createRecipe(recipe_name,bag_names,description);
|
||||
self.dispatchEvent("change");
|
||||
return null;
|
||||
});
|
||||
};
|
||||
@ -193,7 +231,9 @@ Returns {tiddler_id:}
|
||||
*/
|
||||
SqlTiddlerStore.prototype.saveBagTiddler = function(incomingTiddlerFields,bag_name) {
|
||||
const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields);
|
||||
return this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields,bag_name,attachment_blob);
|
||||
const result = this.sqlTiddlerDatabase.saveBagTiddler(tiddlerFields,bag_name,attachment_blob);
|
||||
this.dispatchEvent("change");
|
||||
return result;
|
||||
};
|
||||
|
||||
/*
|
||||
@ -209,7 +249,9 @@ Returns {tiddler_id:}
|
||||
SqlTiddlerStore.prototype.saveBagTiddlerWithAttachment = function(incomingTiddlerFields,bag_name,options) {
|
||||
const attachment_blob = this.attachmentStore.adoptAttachment(options.filepath,options.type,options.hash);
|
||||
if(attachment_blob) {
|
||||
return this.sqlTiddlerDatabase.saveBagTiddler(incomingTiddlerFields,bag_name,attachment_blob);
|
||||
const result = this.sqlTiddlerDatabase.saveBagTiddler(incomingTiddlerFields,bag_name,attachment_blob);
|
||||
this.dispatchEvent("change");
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -220,11 +262,15 @@ Returns {tiddler_id:,bag_name:}
|
||||
*/
|
||||
SqlTiddlerStore.prototype.saveRecipeTiddler = function(incomingTiddlerFields,recipe_name) {
|
||||
const {tiddlerFields, attachment_blob} = this.processIncomingTiddler(incomingTiddlerFields);
|
||||
return this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields,recipe_name,attachment_blob);
|
||||
const result = this.sqlTiddlerDatabase.saveRecipeTiddler(tiddlerFields,recipe_name,attachment_blob);
|
||||
this.dispatchEvent("change");
|
||||
return result;
|
||||
};
|
||||
|
||||
SqlTiddlerStore.prototype.deleteTiddler = function(title,bag_name) {
|
||||
return this.sqlTiddlerDatabase.deleteTiddler(title,bag_name);
|
||||
const result = this.sqlTiddlerDatabase.deleteTiddler(title,bag_name);
|
||||
this.dispatchEvent("change");
|
||||
return result;
|
||||
};
|
||||
|
||||
/*
|
||||
@ -332,7 +378,9 @@ SqlTiddlerStore.prototype.getRecipeLastTiddlerId = function(recipe_name) {
|
||||
SqlTiddlerStore.prototype.deleteAllTiddlersInBag = function(bag_name) {
|
||||
var self = this;
|
||||
return this.sqlTiddlerDatabase.transaction(function() {
|
||||
return self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bag_name);
|
||||
const result = self.sqlTiddlerDatabase.deleteAllTiddlersInBag(bag_name);
|
||||
self.dispatchEvent("change");
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user