-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Server Sent Events #4609
Add Server Sent Events #4609
Changes from 6 commits
68af95d
3d59bd2
8da6630
c013a90
85c5588
63b5710
65a5dae
2632661
e8d62fe
06009b0
b861b58
5fb2c5c
eed4c18
2264946
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/*\ | ||
title: $:/core/modules/server/server-sent-events.js | ||
type: application/javascript | ||
module-type: library | ||
\*/ | ||
(function(){ | ||
|
||
/*jslint node: true, browser: true */ | ||
/*global $tw: false */ | ||
"use strict"; | ||
|
||
/** | ||
* @param {string} prefix | ||
* Usually the plugin path, such as `plugins/tiddlywiki/tiddlyweb`. | ||
* The route will match `/events/${prefix}` exactly. | ||
* @param {( | ||
* request: import("http").IncomingMessage, | ||
* state, | ||
* emit: (event: string, data: string) => void, | ||
* end: () => void | ||
* ) => void} handler | ||
* A function that will be called each time a request | ||
* comes in with the request and state from the | ||
* route and an emit function to call. | ||
*/ | ||
var ServerSentEvents = /** @class */ (function () { | ||
|
||
function ServerSentEvents(prefix, handler) { | ||
this.handler = handler; | ||
this.prefix = prefix; | ||
} | ||
ServerSentEvents.prototype.getExports = function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The core leaves a blank line between methods |
||
return { | ||
bodyFormat: "stream", | ||
method: "GET", | ||
path: new RegExp("^/events/" + this.prefix + "$"), | ||
handler: this.handleEventRequest.bind(this) | ||
}; | ||
}; | ||
/** | ||
* | ||
* @param {import("http").IncomingMessage} request | ||
* @param {import("http").ServerResponse} response | ||
* @param {*} state | ||
*/ | ||
ServerSentEvents.prototype.handleEventRequest = function (request, response, state) { | ||
if (request.headers.accept && request.headers.accept.startsWith('text/event-stream')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The core doesn't use a space between |
||
response.writeHead(200, { | ||
'Content-Type': 'text/event-stream', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The core prefers double quotes for string constants, only using single quotes for strings that need to contain double quotes. |
||
'Cache-Control': 'no-cache', | ||
'Connection': 'keep-alive' | ||
}); | ||
this.handler(request, state, this.emit.bind(this, response), this.end.bind(this, response)); | ||
} | ||
else { | ||
response.writeHead(406, "Not Acceptable", {}); | ||
response.end(); | ||
} | ||
}; | ||
ServerSentEvents.prototype.emit = function (response, event, data) { | ||
if (typeof event !== "string" || event.indexOf("\n") !== -1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The core always wraps the body of an |
||
throw new Error("type must be a single-line string"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would normally capitalise an error message |
||
if (typeof data !== "string" || data.indexOf("\n") !== -1) | ||
throw new Error("data must be a single-line string"); | ||
response.write("event: " + event + "\ndata: " + data + "\n\n", "utf8"); | ||
}; | ||
ServerSentEvents.prototype.end = function (response) { | ||
response.end(); | ||
}; | ||
return ServerSentEvents; | ||
}()); | ||
|
||
exports.ServerSentEvents = ServerSentEvents; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The construction used to export the class isn't consistent with the rest of the core, which doesn't use the inner IIFE that returns "ServerSentEvents". It should follow the same approach as, say, the core widget classes. |
||
|
||
})(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/*\ | ||
title: $:/plugins/tiddlywiki/tiddlyweb/sse-client.js | ||
type: application/javascript | ||
module-type: startup | ||
|
||
GET /recipes/default/tiddlers/:title | ||
|
||
\*/ | ||
(function(){ | ||
|
||
/*jslint node: true, browser: true */ | ||
/*global $tw: false */ | ||
"use strict"; | ||
|
||
exports.name = "/events/plugins/tiddlywiki/tiddlyweb"; | ||
exports.after = ["startup"]; | ||
exports.synchronous = true; | ||
exports.platforms = ["browser"]; | ||
exports.startup = function () { | ||
//make sure we're actually being used | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comments should be capitalised, with a space after the |
||
if($tw.syncadaptor.name !== "tiddlyweb") return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we instead install the event handler when we startup the syncadaptor? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sure we could but I don't really know where. I did it here because that is right after the syncer and sync adapter are initialized. But you could just as easily do it there. |
||
//get the mount point in case a path prefix is used | ||
var host = $tw.syncadaptor.getHost(); | ||
//make sure it ends with a slash (it usually does) | ||
if(!host.endsWith("/")) host += "/"; | ||
Arlen22 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
//setup the event listener | ||
var events = new EventSource(host + "events/plugins/tiddlywiki/tiddlyweb"); | ||
events.addEventListener("change", function () { | ||
$tw.syncer.syncFromServer(); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be a good idea to add some debouncing here to reduce the amount of requests going to the server over a small period of time? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another idea - you might want to add an event listener for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think debouncing is going to be necessary to avoid obvious DDOS vectors. Alternatively, perhaps we should disable SSE by default. |
||
} | ||
|
||
})(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/*\ | ||
title: $:/plugins/tiddlywiki/tiddlyweb/sse-server.js | ||
type: application/javascript | ||
module-type: route | ||
|
||
GET /events/plugins/tiddlywiki/tiddlyweb | ||
|
||
\*/ | ||
(function(){ | ||
|
||
/*jslint node: true, browser: true */ | ||
/*global $tw: false */ | ||
"use strict"; | ||
|
||
/** @type {Record<any, { | ||
* request: import("http").IncomingMessage, | ||
* state: {wiki,pathPrefix}, | ||
* emit: (event: string, data: string) => void, | ||
* end: () => void | ||
* }[]>} */ | ||
var wikis = {}; | ||
/** | ||
* Setups up the array for this wiki and adds the change listener | ||
* | ||
* @param {$tw.Wiki} wiki The wiki object to listen to changes on | ||
*/ | ||
function setupWiki(wiki) { | ||
// add a new array for this wiki (object references work as keys) | ||
wikis[wiki] = []; | ||
Arlen22 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// add the change listener for this wiki | ||
wiki.addEventListener("change", function (changes) { | ||
wikis[wiki].forEach(function (item) { | ||
item.emit("change", JSON.stringify(changes)); | ||
}); | ||
}); | ||
} | ||
/** | ||
* | ||
* @param {import("http").IncomingMessage} request | ||
* @param {{wiki,pathPrefix}} state | ||
* @param {(event: string, data: string) => void} emit | ||
* @param {() => void} end | ||
*/ | ||
function handleConnection(request, state, emit, end) { | ||
// setup this particular wiki if we haven't seen it before | ||
if (!wikis[state.wiki]) setupWiki(state.wiki); | ||
// add the connection to the list of connections for this wiki | ||
var item = { request: request, state: state, emit: emit, end: end }; | ||
wikis[state.wiki].push(item); | ||
// remove the connection when it closes | ||
request.on("close",function(){ | ||
wikis[state.wiki].splice(wikis[state.wiki].indexOf(item),1); | ||
}); | ||
} | ||
// import the ServerSentEvents class | ||
var ServerSentEvents = require("$:/core/modules/server/server-sent-events.js").ServerSentEvents; | ||
// instantiate the class | ||
var events = new ServerSentEvents("plugins/tiddlywiki/tiddlyweb", handleConnection); | ||
// export the route definition for this server sent events instance | ||
module.exports = events.getExports(); | ||
|
||
})(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The core doesn't use JSDoc, just plain text for code comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I use it to give VS Code the proper intellisense. TiddlyWiki is really hard to work with otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, for us familiar with type script, interface with typing is very nice.