Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+65
View File
@@ -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
+92
View File
@@ -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 {}
}
+75
View File
@@ -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'
}