Server-sent events
Server-Sent Events (SSE) is a server push technology enabling a client to receive automatic updates from a server via an HTTP connection, and describes how servers can initiate data transmission towards clients once an initial client connection has been established.
A simple approach for implementing SSE on OpenACS with discussed in a forum thread.
The example below shows, how to use SSE to associate a background job with the client. This can be used e.g. when executing longer a running job in the background and to keep the client up incrementally to date what is currently happening. This can be seen as an alternative to streaming HTML.
Page with associated background activity
ad_page_contract {
Sample page for emulating streaming HTML via SSE (server side
events) for a background job. In this example, the same page is
used as the event sink (where the events are displayed) and as an
event source (when called with a session_id).
@author Gustaf Neumann
} {
{session_id:integer ""}
}
set title "Sample HTML streaming job page (SSE)"
set context $title
if {$session_id ne ""} {
#
# We are called by the event handler from JavaScript. This block
# could be a different, generic script, but we keep it here
# together for reducing number of files.
#
#
# Set up SSE: return headers and register reporting channel for
# the session_id.
#
set channel [ns_connchan detach]
ns_connchan write $channel [append _ \
"HTTP/1.1 200 OK\r\n" \
"Cache-Control: no-cache\r\n" \
"X-Accel-Buffering': no
\r\n" \
"Content-type: text/event-stream\r\n" \
"\r\n"]
sse::channel $session_id $channel
ad_script_abort
} else {
#
# We are called as an ADP page.
#
set session_id [ns_conn id]
#
# Register the SSE event handler in JavaScript.
#
template::head::add_script -script [subst {
if(typeof(EventSource) !== "undefined") {
var sse = new EventSource("[ns_conn url]?session_id=$session_id");
sse.onmessage = function(event) {
if ('__CLOSE_SSE__' == event.data) {
sse.close(); // stop retry
} else {
document.getElementById("result").innerHTML += event.data;
}
};
} else {
document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
}
}]
#
# Run some job in the background and report updates via SSE to the
# current page. The session_id is used to associate the background
# job with the client.
#
sse::job $session_id {
foreach i {1 2 3 4} {
set HTML "<li>finish $i: ...([person::name -person_id [ad_conn user_id]])</li>"
sse::message -localize $session_id $HTML
ns_sleep 1s
}
sse::close $session_id
}
ad_return_template
}
Corresponding ADP page:
@title;literal@ @context;literal@ <h1>Getting updates from job associated with session_id @session_id@</h1> <ul> </ul>
Library support
Save this under e.g. acs-tcl/tcl/sse-procs.tcl
############################################################################# # Simple API for SSE messages via a SESSION_ID, e.g. via a job running # in the background. # # Gustaf Neumann fecit ############################################################################# namespace eval sse { # # sse::job # ad_proc ::sse::job { {-lang} session_id script} { Execute some (long running) script in the background. The background script is associated via the session_id with the client and can send messages to it via sse::message. When done, the job should issue sse::close. @param lang language used for internationalization. If not set, taken from the calling environment @param session_id associated session_id, used for reporting back @param script script to be executed in the background } { if {![info exists lang]} { set lang [uplevel {ad_conn locale}] } nsv_set sse_dict $session_id \ [list \ lang $lang \ ad_conn [list array set ::ad_conn [array get ::ad_conn]] \ xo::cc [expr {[info commands ::xo::cc] ne "" ? [::xo::cc serialize] : ""} ] \ ] ns_job queue -detached sse-jobs \ [list ::apply [list session_id "sse::init_job $session_id\n$script"] $session_id] } # # sse::message # ad_proc ::sse::message { {-localize:boolean} session_id msg} { Send a message to the client. In case the channel was not jet registered, buffer the message in an nsv dict. @param localize Perform localization of the provided text @param session_id associated session_id, used for associating output session @param msg The message to be sent back } { if {$localize_p} { set msg [lang::util::localize $msg [nsv_dict get sse_dict $session_id lang]] } if {[nsv_dict get -varname channel sse_dict $session_id channel]} { # # The channel is already set up. # if {[nsv_dict get -varname messages sse_dict $session_id messages]} { # # The "nsv_dict unset" poses a potential race condition, # ... but not for the job application scenario. # nsv_dict unset sse_dict $session_id messages #ns_log notice "--- sse $session_id get buffered messages '$messages'" } else { set messages "" } lappend messages $msg foreach message $messages { ns_log notice "--- sse $session_id message '$message'" # # SSE needs handling of new-lines in the data field; here we # send two messages in this case. In other cases, maybe coding # newline as literally \n and decoding it on the client might # be appropriate. # ns_connchan write $channel \ [string cat "data:" [join [split $message \n] "\ndata:"] "\n\n"] if {$message eq "__CLOSE_SSE__"} { ns_connchan close $channel nsv_unset sse_dict $session_id } } } else { # # Buffer the message. # #ns_log notice "--- sse $session_id must buffer message '$msg'" nsv_dict lappend sse_dict $session_id messages $msg } } # # sse::channel: register a channel for the session id. This is # typically called by the event handler, which is called from # JavaScript. # proc ::sse::channel {session_id channel} { nsv_dict set sse_dict $session_id channel $channel } # # sse::init_job: setup a context quite similar to a connection thread # proc ::sse::init_job {session_id} { set sse_dict [nsv_get sse_dict $session_id] eval [dict get $sse_dict ad_conn] eval [dict get $sse_dict xo::cc] } # # sse::close: provide an API for closing the session # proc ::sse::close {session_id} { ::sse::message $session_id __CLOSE_SSE__ } if {"sse-jobs" ni [ns_job queues]} { ns_job create sse-jobs 4 nsv_set sse_dict "" "" #ns_job configure -jobsperthread 10000 } } # # Local variables: # mode: tcl # tcl-indent-level: 2 # indent-tabs-mode: nil # End:
Im case you are running NGINX and you are experiencing problems with SSE, check out this entry on stackoverflow.