First pass at SSE support

This commit is contained in:
Jeremy Ruston 2024-03-25 08:36:42 +00:00
parent 708e21951f
commit 7a0c43436f
3 changed files with 173 additions and 5 deletions

View File

@ -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: {

View File

@ -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();
};
}());

View File

@ -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;
});
};