examples: add examples/vweb/server_sent_events; implement vweb.sse
parent
a73c20916d
commit
f4b757e47d
|
@ -0,0 +1,19 @@
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
color: #eee;
|
||||||
|
background-color: #333;
|
||||||
|
background-image: url("v-logo.svg");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 10em;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 11em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #6699CC;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.logo {
|
||||||
|
float: left;
|
||||||
|
width: 10em;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 500 500" width="500px" height="500px"><defs><clipPath id="_clipPath_8TWIgR1z3pxinjWBiigzcEIrVJKv9Gq4"><rect width="500" height="500"/></clipPath></defs><g clip-path="url(#_clipPath_8TWIgR1z3pxinjWBiigzcEIrVJKv9Gq4)"><path d=" M 318.422 453.543 L 463.705 49.541 C 466.168 42.689 462.285 37.693 455.037 38.392 L 340.786 49.398 C 333.539 50.097 325.71 56.246 323.316 63.121 L 188.843 449.216 C 186.447 456.091 190.414 461.673 197.695 461.673 L 308.901 461.673 C 312.541 461.673 316.497 458.893 317.729 455.466 L 318.422 453.543 Z " fill="rgb(83,107,138)"/><defs><filter id="Hmac7mZraFWHw0G84Yxj4QuzeTFp0E7Y" x="-200%" y="-200%" width="400%" height="400%" filterUnits="objectBoundingBox" color-interpolation-filters="sRGB"><feGaussianBlur xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" stdDeviation="6.440413594258542"/><feOffset xmlns="http://www.w3.org/2000/svg" dx="0" dy="0" result="pf_100_offsetBlur"/><feFlood xmlns="http://www.w3.org/2000/svg" flood-color="#000000" flood-opacity="0.65"/><feComposite xmlns="http://www.w3.org/2000/svg" in2="pf_100_offsetBlur" operator="in" result="pf_100_dropShadow"/><feBlend xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" in2="pf_100_dropShadow" mode="normal"/></filter></defs><g filter="url(#Hmac7mZraFWHw0G84Yxj4QuzeTFp0E7Y)"><path d=" M 301.848 455.466 L 241.359 280.725 L 250 275.324 L 311.57 453.543 L 301.848 455.466 Z " fill="rgb(235,235,235)"/></g><path d=" M 44.963 38.392 L 159.214 49.398 C 166.461 50.097 174.298 56.243 176.704 63.115 L 314.022 455.448 C 315.224 458.885 313.245 461.673 309.604 461.673 L 197.695 461.673 C 190.414 461.673 182.502 456.111 180.038 449.259 L 36.295 49.541 C 33.832 42.689 37.715 37.693 44.963 38.392 Z " fill="rgb(93,135,191)"/></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,38 @@
|
||||||
|
<html>
|
||||||
|
<header>
|
||||||
|
<title>@title</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
@css 'assets/site.css'
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<h1>@title</h1>
|
||||||
|
<button>Close the connection</button>
|
||||||
|
<ul></ul>
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
var button = document.querySelector('button');
|
||||||
|
var eventList = document.querySelector('ul');
|
||||||
|
const evtSource = new EventSource('/sse');
|
||||||
|
evtSource.onerror = function() { console.log("EventSource failed."); };
|
||||||
|
console.log(evtSource.withCredentials);
|
||||||
|
console.log(evtSource.readyState);
|
||||||
|
console.log(evtSource.url);
|
||||||
|
evtSource.onopen = function() {
|
||||||
|
console.log("Connection to server opened.");
|
||||||
|
};
|
||||||
|
evtSource.onmessage = function(e) {
|
||||||
|
var newElement = document.createElement("li");
|
||||||
|
newElement.textContent = "message: " + e.data;
|
||||||
|
eventList.appendChild(newElement);
|
||||||
|
};
|
||||||
|
evtSource.addEventListener("ping", function(e) {
|
||||||
|
console.log(e)
|
||||||
|
var newElement = document.createElement("li");
|
||||||
|
var obj = JSON.parse(e.data);
|
||||||
|
newElement.innerHTML = "ping at " + obj.time + ' server data: ' + e.data;
|
||||||
|
eventList.appendChild(newElement);
|
||||||
|
}, false);
|
||||||
|
button.onclick = function() { console.log('Connection closed'); evtSource.close(); };
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,38 @@
|
||||||
|
module main
|
||||||
|
|
||||||
|
import rand
|
||||||
|
import time
|
||||||
|
import vweb
|
||||||
|
import vweb.sse
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
vweb.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
vweb.run<App>(8081)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut app App) init_once() {
|
||||||
|
app.serve_static('/favicon.ico', 'favicon.ico', 'img/x-icon')
|
||||||
|
app.handle_static('.')
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn (mut app App) index() vweb.Result {
|
||||||
|
title := 'SSE Example'
|
||||||
|
return $vweb.html()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut app App) sse() vweb.Result {
|
||||||
|
mut session := sse.new_connection(app.conn)
|
||||||
|
// NB: you can setup session.write_timeout and session.headers here
|
||||||
|
session.start() or { return app.server_error(501) }
|
||||||
|
session.send_message(data: 'ok') or { return app.server_error(501) }
|
||||||
|
for {
|
||||||
|
data := '{"time": "$time.now().str()", "random_id": "$rand.ulid()"}'
|
||||||
|
session.send_message(event: 'ping', data: data) or { return app.server_error(501) }
|
||||||
|
println('> sent event: $data')
|
||||||
|
time.sleep_ms(1000)
|
||||||
|
}
|
||||||
|
return app.server_error(501)
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
module sse
|
||||||
|
|
||||||
|
import net
|
||||||
|
import time
|
||||||
|
import strings
|
||||||
|
|
||||||
|
// This module implements the server side of `Server Sent Events`.
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
|
||||||
|
// as well as https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
|
||||||
|
// for detailed description of the protocol, and a simple web browser client example.
|
||||||
|
//
|
||||||
|
// > Event stream format
|
||||||
|
// > The event stream is a simple stream of text data which must be encoded using UTF-8.
|
||||||
|
// > Messages in the event stream are separated by a pair of newline characters.
|
||||||
|
// > A colon as the first character of a line is in essence a comment, and is ignored.
|
||||||
|
// > Note: The comment line can be used to prevent connections from timing out;
|
||||||
|
// > a server can send a comment periodically to keep the connection alive.
|
||||||
|
// >
|
||||||
|
// > Each message consists of one or more lines of text listing the fields for that message.
|
||||||
|
// > Each field is represented by the field name, followed by a colon, followed by the text
|
||||||
|
// > data for that field's value.
|
||||||
|
|
||||||
|
[ref_only]
|
||||||
|
pub struct SSEConnection {
|
||||||
|
pub mut:
|
||||||
|
headers map[string]string
|
||||||
|
conn &net.TcpConn
|
||||||
|
write_timeout time.Duration = 600 * time.second
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SSEMessage {
|
||||||
|
id string
|
||||||
|
event string
|
||||||
|
data string
|
||||||
|
retry int
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_connection(conn &net.TcpConn) SSEConnection {
|
||||||
|
return SSEConnection{
|
||||||
|
conn: conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sse_start is used to send the start of a Server Side Event response.
|
||||||
|
pub fn (mut sse SSEConnection) start() ? {
|
||||||
|
sse.conn.set_write_timeout(sse.write_timeout)
|
||||||
|
mut start_sb := strings.new_builder(512)
|
||||||
|
start_sb.write('HTTP/1.1 200')
|
||||||
|
start_sb.write('\r\nConnection: keep-alive')
|
||||||
|
start_sb.write('\r\nCache-Control: no-cache')
|
||||||
|
start_sb.write('\r\nContent-Type: text/event-stream')
|
||||||
|
for k, v in sse.headers {
|
||||||
|
start_sb.write('\r\n$k: $v')
|
||||||
|
}
|
||||||
|
start_sb.write('\r\n')
|
||||||
|
sse.conn.write(start_sb.buf) or { return error('could not start sse response') }
|
||||||
|
}
|
||||||
|
|
||||||
|
// send_message sends a single message to the http client that listens for SSE.
|
||||||
|
// It does not close the connection, so you can use it many times in a loop.
|
||||||
|
pub fn (mut sse SSEConnection) send_message(message SSEMessage) ? {
|
||||||
|
mut sb := strings.new_builder(512)
|
||||||
|
if message.id != '' {
|
||||||
|
sb.write('id: $message.id\n')
|
||||||
|
}
|
||||||
|
if message.event != '' {
|
||||||
|
sb.write('event: $message.event\n')
|
||||||
|
}
|
||||||
|
if message.data != '' {
|
||||||
|
sb.write('data: $message.data\n')
|
||||||
|
}
|
||||||
|
if message.retry != 0 {
|
||||||
|
sb.write('retry: $message.retry\n')
|
||||||
|
}
|
||||||
|
sb.write('\n')
|
||||||
|
sse.conn.write(sb.buf) ?
|
||||||
|
}
|
|
@ -158,6 +158,17 @@ pub fn (mut ctx Context) ok(s string) Result {
|
||||||
return Result{}
|
return Result{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (mut ctx Context) server_error(ecode int) Result {
|
||||||
|
$if debug {
|
||||||
|
eprintln('> ctx.server_error ecode: $ecode')
|
||||||
|
}
|
||||||
|
if ctx.done {
|
||||||
|
return Result{}
|
||||||
|
}
|
||||||
|
send_string(mut ctx.conn, vweb.http_500) or { }
|
||||||
|
return Result{}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn (mut ctx Context) redirect(url string) Result {
|
pub fn (mut ctx Context) redirect(url string) Result {
|
||||||
if ctx.done {
|
if ctx.done {
|
||||||
return Result{}
|
return Result{}
|
||||||
|
@ -592,6 +603,7 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T) {
|
||||||
// call action method
|
// call action method
|
||||||
if method.args.len == vars.len {
|
if method.args.len == vars.len {
|
||||||
app.$method(vars)
|
app.$method(vars)
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the vweb route `$method.attrs` ($vars.len)')
|
eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the vweb route `$method.attrs` ($vars.len)')
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue