Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,65 @@
|
||||
# Server Sent Events
|
||||
|
||||
This module implements the server side of `Server Sent Events`, SSE.
|
||||
See [mozilla SSE][mozilla_sse]
|
||||
as well as [whatwg][whatwg html spec]
|
||||
for detailed description of the protocol, and a simple web browser client example.
|
||||
|
||||
## Usage
|
||||
|
||||
With SSE we want to keep the connection open, so we are able to
|
||||
keep sending events to the client. But if we hold the connection open indefinitely
|
||||
veb isn't able to process any other requests.
|
||||
|
||||
We can let veb know that it can continue processing other requests and that we will
|
||||
handle the connection ourself by calling `ctx.takeover_conn()` and returning an empty result
|
||||
with `veb.no_result()`. veb will not close the connection and we can handle
|
||||
the connection in a separate thread.
|
||||
|
||||
**Example:**
|
||||
|
||||
```v ignore
|
||||
import veb.sse
|
||||
|
||||
// endpoint handler for SSE connections
|
||||
fn (app &App) sse(mut ctx Context) veb.Result {
|
||||
// let veb know that the connection should not be closed
|
||||
ctx.takeover_conn()
|
||||
// handle the connection in a new thread
|
||||
spawn handle_sse_conn(mut ctx)
|
||||
// we will send a custom response ourself, so we can safely return an empty result
|
||||
return veb.no_result()
|
||||
}
|
||||
|
||||
fn handle_sse_conn(mut ctx Context) {
|
||||
// pass veb.Context
|
||||
mut sse_conn := sse.start_connection(mut ctx.Context)
|
||||
|
||||
// send a message every second 3 times
|
||||
for _ in 0.. 3 {
|
||||
time.sleep(time.second)
|
||||
sse_conn.send_message(data: 'ping') or { break }
|
||||
}
|
||||
// close the SSE connection
|
||||
sse_conn.close()
|
||||
}
|
||||
```
|
||||
|
||||
Javascript code:
|
||||
|
||||
```js
|
||||
const eventSource = new EventSource('/sse');
|
||||
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
console.log('received message:', event.data);
|
||||
});
|
||||
|
||||
eventSource.addEventListener('close', () => {
|
||||
console.log('closing the connection');
|
||||
// prevent browser from reconnecting
|
||||
eventSource.close();
|
||||
});
|
||||
```
|
||||
|
||||
[mozilla_sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
|
||||
[whatwg html spec]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
|
||||
@@ -0,0 +1,92 @@
|
||||
module sse
|
||||
|
||||
import veb
|
||||
import net
|
||||
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.
|
||||
|
||||
@[params]
|
||||
pub struct SSEMessage {
|
||||
pub mut:
|
||||
id string
|
||||
event string
|
||||
data string
|
||||
retry int
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct SSEConnection {
|
||||
pub mut:
|
||||
conn &net.TcpConn @[required]
|
||||
}
|
||||
|
||||
// start an SSE connection
|
||||
pub fn start_connection(mut ctx veb.Context) &SSEConnection {
|
||||
if ctx.conn == unsafe { nil } {
|
||||
eprintln('[veb.sse] WARNING: SSE requires a direct TCP connection (ctx.conn) which is not available. Use `ctx.takeover_conn()` before starting an SSE connection.')
|
||||
return &SSEConnection{
|
||||
conn: unsafe { nil }
|
||||
}
|
||||
}
|
||||
// Build and send HTTP response headers directly.
|
||||
// SSE responses must NOT include Content-Length since data is streamed.
|
||||
mut sb := strings.new_builder(256)
|
||||
sb.write_string('HTTP/1.1 200 OK\r\n')
|
||||
sb.write_string('Content-Type: text/event-stream\r\n')
|
||||
sb.write_string('Connection: keep-alive\r\n')
|
||||
sb.write_string('Cache-Control: no-cache\r\n')
|
||||
sb.write_string('Server: veb\r\n')
|
||||
sb.write_string('\r\n')
|
||||
ctx.conn.write(sb) or {}
|
||||
|
||||
return &SSEConnection{
|
||||
conn: ctx.conn
|
||||
}
|
||||
}
|
||||
|
||||
// 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) ! {
|
||||
if sse.conn == unsafe { nil } {
|
||||
return error('SSE connection is not available (no TCP connection)')
|
||||
}
|
||||
mut sb := strings.new_builder(512)
|
||||
if message.id != '' {
|
||||
sb.write_string('id: ${message.id}\n')
|
||||
}
|
||||
if message.event != '' {
|
||||
sb.write_string('event: ${message.event}\n')
|
||||
}
|
||||
if message.data != '' {
|
||||
sb.write_string('data: ${message.data}\n')
|
||||
}
|
||||
if message.retry != 0 {
|
||||
sb.write_string('retry: ${message.retry}\n')
|
||||
}
|
||||
sb.write_string('\n')
|
||||
sse.conn.write(sb)!
|
||||
}
|
||||
|
||||
// send a 'close' event and close the tcp connection.
|
||||
pub fn (mut sse SSEConnection) close() {
|
||||
if sse.conn == unsafe { nil } {
|
||||
return
|
||||
}
|
||||
sse.send_message(event: 'close', data: 'Closing the connection', retry: -1) or {}
|
||||
sse.conn.close() or {}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// vtest retry: 3
|
||||
// vtest build: !windows // fasthttp.Server.run is not implemented on windows yet
|
||||
import veb
|
||||
import veb.sse
|
||||
import time
|
||||
import net.http
|
||||
|
||||
const port = 13008
|
||||
const localserver = 'http://127.0.0.1:${port}'
|
||||
const exit_after = time.second * 10
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
mut:
|
||||
started chan bool
|
||||
}
|
||||
|
||||
pub fn (mut app App) before_accept_loop() {
|
||||
app.started <- true
|
||||
}
|
||||
|
||||
fn (app &App) sse(mut ctx Context) veb.Result {
|
||||
ctx.takeover_conn()
|
||||
spawn handle_sse_conn(mut ctx)
|
||||
return veb.no_result()
|
||||
}
|
||||
|
||||
fn handle_sse_conn(mut ctx Context) {
|
||||
mut sse_conn := sse.start_connection(mut ctx.Context)
|
||||
|
||||
for _ in 0 .. 3 {
|
||||
time.sleep(time.second)
|
||||
sse_conn.send_message(data: 'ping') or { break }
|
||||
}
|
||||
sse_conn.close()
|
||||
}
|
||||
|
||||
fn testsuite_begin() {
|
||||
mut app := &App{}
|
||||
spawn fn () {
|
||||
time.sleep(exit_after)
|
||||
assert true == false, 'timeout reached!'
|
||||
exit(1)
|
||||
}()
|
||||
|
||||
spawn veb.run_at[App, Context](mut app, port: port, family: .ip)
|
||||
// app startup time
|
||||
_ := <-app.started
|
||||
}
|
||||
|
||||
fn test_sse() ! {
|
||||
mut x := http.get('${localserver}/sse')!
|
||||
|
||||
connection := x.header.get(.connection) or {
|
||||
assert true == false, 'Header Connection should be set!'
|
||||
panic('missing header')
|
||||
}
|
||||
cache_control := x.header.get(.cache_control) or {
|
||||
assert true == false, 'Header Cache-Control should be set!'
|
||||
panic('missing header')
|
||||
}
|
||||
content_type := x.header.get(.content_type) or {
|
||||
assert true == false, 'Header Content-Type should be set!'
|
||||
panic('missing header')
|
||||
}
|
||||
assert connection == 'keep-alive'
|
||||
assert cache_control == 'no-cache'
|
||||
assert content_type == 'text/event-stream'
|
||||
|
||||
eprintln(x.body)
|
||||
assert x.body == 'data: ping\n\ndata: ping\n\ndata: ping\n\nevent: close\ndata: Closing the connection\nretry: -1\n\n'
|
||||
}
|
||||
Reference in new issue
Block a user