Server-Sent Events example

I manged to get server-sent events (SSEs to its friends) working for a simple ping-pong example using this code, which is a bit of a hack, so I’m sure can be improved:

:- dynamic message/1.

:- use_module(library(http/http_server)).
:- use_module(library(http/http_files)).
:- use_module(library(http/http_unix_daemon)).

:- initialization http_daemon.

:- http_handler(root(.), http_reply_from_files(".", [indexes(["./index.html"])]), [prefix]).
:- http_handler(root(prolog), prolog_handler, []).
:- http_handler(root(sse), sse_handler, []).

message('{"reply": "ignore"}').

prolog_handler(Request) :-
  http_read_json_dict(Request, Dict),
  query_handler(Dict.request, Dict),
  reply_json_dict(json{reply: ok}).

sse_handler(_Request) :-
  message(Message),
  format("Content-Type: text/event-stream; charset=UTF-8~n~n"),
  format("Cache-Control: no-cache~n~n"),
  format("data: ~s~n~n", [Message]).

query_handler("ping", _Dict) :-
  retractall(message(_)),
  assertz(message('{"reply": "pong"}')).

On the browser JavaScript side, the code looks like this:

const sseTarget = new EventSource("sse");
let json;

function prologServer(Request) {
  return fetch("prolog",
  { method: "POST"
  , headers: { "Content-Type": "application/json"}
  , body: JSON.stringify(Request)
  });
}

sseTarget.addEventListener("message", function(event) {
  json = JSON.parse(event.data);
  if (json.reply !== "ignore") {
    document.querySelector("#message").innerText = json.reply;
  }
});

sseTarget.addEventListener("open", function(event) {
  prologServer({"request": "ping"});
});

The very hacky part is using a global variable in the sense of message(Message) which my query_handler/2 predicate resets with retractall/1 and assertz/1 to pass the reply to sse_handler after doing whatever to the input Json data.

I’ve started a new thread here following on from a post I did suggesting using websocket to glue SWI Prolog to whatever language (Angular in the original thread).

I hit various snags with websockets. Besides browsers and proxies like nginx closing the connections, I couldn’t figure out how to get nginx to resolve wss://www.example.server/ws requests. (I notice both fetch and EventSource refuse to work with URLs if they’re not just path and filenames, so maybe the same applies to websocket. But the timeout problems made me decide against bothering to experiment further).

If the response to a request is going to be fairly quick, the just using fetch solution I gave earlier is probably a neater and easier solution.

But I want something where the server may spend over a minute thinking, and also may want to initiate the conversation, so that’s where SSE comes in.

I based my code on the example on this tutorial which used php. I’m not quite sure how it handles polling from the browser every 3 seconds (the default which can be speeded up or slowed down) where no response is required. I used the hack of initially setting the response to “ignore” for the listener to ignore.