Right now web socket library (treeform/ws) not working with httpBeast on jester yet https://github.com/treeform/ws/issues/11#event-2831261507. So I want to use SSE (server side event) instead.
After googling and do it myself several time I still cannot make it work yet. Anyone could here guide me what's the correct way to use it with jester? (I'll do pull request to jester to add this example after it work)
Here's code that working ok but using PHP https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
Here's my code so far (that's not work)
server
import jester
import os, random
routes:
get "/":
resp "Home"
get "/demo_sse":
request.headers.add "Content-Type", "text/event-stream"
request.headers.add "Cache-Control", "no-cache"
request.sendHeaders()
for i in 1..20:
request.send($rand(100))
sleep(100)
client code in js
var source = new EventSource("/demo_sse");
source.onmessage = function (event) {
document.getElementById("result").innerHTML += event.data + "<br>";
};
Any example available?
Right now web socket library (treeform/ws) not working with httpBeast on jester yet https://github.com/treeform/ws/issues/11#event-2831261507.
It is working with niv's websocket library, why not try that out?
SSE (server side event) is really old way to do things, WebSockets are way better.
On vacation now will look into why httpBeast does not work work withy my websocket library when I get back. Its probably some thing really simple.
Thank you for help looking on the problem!!
I think the problem is from this line https://github.com/treeform/ws/blob/master/src/ws/jester_extra.nim#L5 which you expect to detect defined(useHttpBeast) but it's fail to detect
SSE (server side event) is really old way to do things, WebSockets are way better.
Hmm, it is an old thing, but as far as I know it has some advantages over WebSockets:
Also, I saw some blog post with benchmarks, claiming that WebSockets are just a slightly better in performance and latency than plain HTTP with keep-alive.
Working example.
The only problem, the request.close() seems to not actually close the socket. The socket will be closed only when the nim process is killed. Is there a way to forcefully close the socket?
import jester, httpcore, asyncdispatch, strformat
routes:
get "/demo_sse":
enableRawMode()
request.sendHeaders(HttpCode(200), @[
("Content-Type", "text/event-stream"),
("Cache-Control", "no-cache"),
("Connection", "keep-alive"),
("Access-Control-Allow-Origin", "*")
])
# res.write("retry: 10000\n\n");
for i in 1..5:
request.send("data: " & $i & "\n\n")
await sleep_async(1000)
resp "data: some-last-message\n\n"
request.close() # <== problem here, the socket is not closed
var source = new EventSource("http://localhost:5000/demo_sse")
source.onmessage = function (event) {
console.log(event)
}
source.onerror = function (event) {
console.log("closed")
}
source.onopen = function (event) {
console.log("opened")
}
In @alexeypetrushin example with the asynchttpserver lib, I replaced the "for" loop code with a "while". So I can test if the client is closed. But when I close the browser, the server continues to send the response to the client. Does the isclosed function detect when the connection to the browser is closed?
import httpcore, asyncdispatch, strformat, asynchttpserver, asyncnet
proc cb(req: Request): Future[void] {.async.} =
let headers = newHttpHeaders(@[
("Content-Type", "text/event-stream"),
("Cache-Control", "no-cache"),
("Connection", "keep-alive"),
# ("Connection", "close"),
("Access-Control-Allow-Origin", "*"),
("Content-Length", "") # To prevent AsyncHttpServer from adding content-length
])
await req.respond(Http200, "200", headers)
# for i in 1..5:
var i = 0
while not req.client.isclosed():
echo "I'm alive ", $i
let msg = "data: " & $i & "\n\n"
await req.respond(Http200, msg)
await sleep_async(1000)
inc(i)
# req.client.close() # <= closing, otherwise it's going to be kept alive and
# EvenSource in Browser won't reconnect automatically
var server = new_async_http_server()
async_check server.serve(Port(5000), cb, "localhost")
echo "started"
run_forever()
But the goal is really that, to keep working until the user closes the browser.
Yes, it will be like that, the browser would auto-reconnect every 5 min (there going to be small delay for that reconnection, but for many apps it's acceptable).
Hmm, one option is to charge a good money for processing such heavy queries. So even if some queries are lost (and left unpaid), you still have a good cash anyway ;)
P.S.
I don't know sockets/networking deep enough to answer how it should be done, but yes it sounds like there should be such kind of event in case when client dropped.
Unfortunately detecting client disconnects isn't particularly easy with SSE, given that it's a one-way communication channel (unlike WebSockets which are two-way).
Even just talking about disconnects, there are several different types of disconnect that can happen such as:
All of these will behave in slightly different ways, and detecting them isn't particularly easy in some cases depending on keep-alives and such. You can probably catch an exception from the underlying asyncdispatch.send() call which actually transmits data to the client to catch most cases where the client disconnects though.
I found a way that works to detect when the client closes the browser and thus close the connection (based on @alexeypetrushin idea of sending a ping every n sec to the server and Using server-sent events Mozilla's documentation).
import httpcore, asyncdispatch, strformat, asynchttpserver, asyncnet
import times
from oids import genOid, `$`
from md5 import toMD5, `$`
from strutils import split
import tables
const
pingTime = 10 # 10 seconds
timeout = 30 # 30 seconds
var testClients = newTable[string, float]()
proc ping(req: Request): Future[void] {.async,gcsafe.} =
let pair = req.url.query.split('=')
echo "Pair: ", $pair
if (pair.len == 2) and (pair[0] == "key") and (pair[1] in testClients):
testClients.del(pair[1])
await req.respond(Http200, "")
proc ssedemo(req: Request): Future[void] {.async,gcsafe.} =
let headers = newHttpHeaders(@[
("Content-Type", "text/event-stream"),
("Cache-Control", "no-cache"),
("Connection", "keep-alive"),
("Access-Control-Allow-Origin", "*"),
("Content-Length", "")
])
await req.respond(Http200, "200", headers)
var savedTime = epochTime()
var key = ""
var i = 0
while not req.client.isclosed():
if (key != "") and (key in testClients):
if (epochTime() - testClients[key]) > timeout:
testClients.del(key)
break
else:
key = ""
echo "I'm alive ", $i
var msg = "event: test\ndata: " & $i & "\n\n"
let elapsed = epochTime() - savedTime
if key == "" and (elapsed >= pingTime):
key = $toMD5($genOid())
savedTime = epochTime()
testClients[key] = savedTime
msg.add("event: ping\ndata: " & key & "\n\n")
await req.respond(Http200, msg)
await sleep_async(1000)
inc(i)
echo "client disconnected"
req.client.close()
proc home(req: Request): Future[void] {.async.} =
const homePage = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Test SSE</title>
</head>
<body>
<div id="testdemo"></div>
<div id="testping"></div>
<script type="text/javascript">
//<![CDATA[
if(typeof(EventSource) !== "undefined") {
// Yes! Server-sent events support!
const evtSource = new EventSource("ssedemo");
evtSource.onopen = function (event) {
console.log("EventSource opened");
}
// evtSource.onmessage = function(event) {
// console.log("New Message...");
// console.log("Data: " + event.data);
//
// document.getElementById('demotest').innerHTML = "message: " + event.data;
// }
evtSource.addEventListener("test", function(event) {
console.log("New Test");
console.log("Data: " + event.data);
document.getElementById('testdemo').innerHTML = "message: " + event.data;
});
evtSource.addEventListener("ping", function(event) {
console.log("New Ping");
console.log("Ping Data: " + event.data);
document.getElementById('testping').innerHTML = "ping data: " + event.data;
const xhttp = new XMLHttpRequest();
xhttp.open("GET", "ping?key=" + event.data, true);
xhttp.send();
});
evtSource.onerror = function(err) {
console.error("EventSource failed:", err);
};
} else {
// Sorry! No server-sent events support..
console.log("Sorry! No server-sent events support..");
}
//]]>
</script>
</body>
</html>"""
await req.respond(Http200, homePage)
proc cb(req: Request): Future[void] {.async.} =
if req.reqMethod == HttpGet:
if req.url.path == "/":
await req.home()
elif req.url.path == "/ssedemo":
await req.ssedemo()
elif req.url.path == "/ping":
await req.ping()
else:
await req.respond(Http200, "Unknown")
else:
await req.respond(Http200, "Unknown")
var server = new_async_http_server()
async_check server.serve(Port(5000), cb, "localhost")
echo "started"
run_forever()
SSE could be a good and simple option to add reactivity to web site without too much complexities.
It is an alternative with strengths and weaknesses! It all depends on the end use and the result required.