| 1 | module sse |
| 2 | |
| 3 | import veb |
| 4 | import net |
| 5 | import strings |
| 6 | |
| 7 | // This module implements the server side of `Server Sent Events`. |
| 8 | // See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format |
| 9 | // as well as https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events |
| 10 | // for detailed description of the protocol, and a simple web browser client example. |
| 11 | // |
| 12 | // > Event stream format |
| 13 | // > The event stream is a simple stream of text data which must be encoded using UTF-8. |
| 14 | // > Messages in the event stream are separated by a pair of newline characters. |
| 15 | // > A colon as the first character of a line is in essence a comment, and is ignored. |
| 16 | // > Note: The comment line can be used to prevent connections from timing out; |
| 17 | // > a server can send a comment periodically to keep the connection alive. |
| 18 | // > |
| 19 | // > Each message consists of one or more lines of text listing the fields for that message. |
| 20 | // > Each field is represented by the field name, followed by a colon, followed by the text |
| 21 | // > data for that field's value. |
| 22 | |
| 23 | @[params] |
| 24 | pub struct SSEMessage { |
| 25 | pub mut: |
| 26 | id string |
| 27 | event string |
| 28 | data string |
| 29 | retry int |
| 30 | } |
| 31 | |
| 32 | @[heap] |
| 33 | pub struct SSEConnection { |
| 34 | pub mut: |
| 35 | conn &net.TcpConn @[required] |
| 36 | } |
| 37 | |
| 38 | // start an SSE connection |
| 39 | pub fn start_connection(mut ctx veb.Context) &SSEConnection { |
| 40 | if ctx.conn == unsafe { nil } { |
| 41 | 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.') |
| 42 | return &SSEConnection{ |
| 43 | conn: unsafe { nil } |
| 44 | } |
| 45 | } |
| 46 | // Build and send HTTP response headers directly. |
| 47 | // SSE responses must NOT include Content-Length since data is streamed. |
| 48 | mut sb := strings.new_builder(256) |
| 49 | sb.write_string('HTTP/1.1 200 OK\r\n') |
| 50 | sb.write_string('Content-Type: text/event-stream\r\n') |
| 51 | sb.write_string('Connection: keep-alive\r\n') |
| 52 | sb.write_string('Cache-Control: no-cache\r\n') |
| 53 | sb.write_string('Server: veb\r\n') |
| 54 | sb.write_string('\r\n') |
| 55 | ctx.conn.write(sb) or {} |
| 56 | |
| 57 | return &SSEConnection{ |
| 58 | conn: ctx.conn |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // send_message sends a single message to the http client that listens for SSE. |
| 63 | // It does not close the connection, so you can use it many times in a loop. |
| 64 | pub fn (mut sse SSEConnection) send_message(message SSEMessage) ! { |
| 65 | if sse.conn == unsafe { nil } { |
| 66 | return error('SSE connection is not available (no TCP connection)') |
| 67 | } |
| 68 | mut sb := strings.new_builder(512) |
| 69 | if message.id != '' { |
| 70 | sb.write_string('id: ${message.id}\n') |
| 71 | } |
| 72 | if message.event != '' { |
| 73 | sb.write_string('event: ${message.event}\n') |
| 74 | } |
| 75 | if message.data != '' { |
| 76 | sb.write_string('data: ${message.data}\n') |
| 77 | } |
| 78 | if message.retry != 0 { |
| 79 | sb.write_string('retry: ${message.retry}\n') |
| 80 | } |
| 81 | sb.write_string('\n') |
| 82 | sse.conn.write(sb)! |
| 83 | } |
| 84 | |
| 85 | // send a 'close' event and close the tcp connection. |
| 86 | pub fn (mut sse SSEConnection) close() { |
| 87 | if sse.conn == unsafe { nil } { |
| 88 | return |
| 89 | } |
| 90 | sse.send_message(event: 'close', data: 'Closing the connection', retry: -1) or {} |
| 91 | sse.conn.close() or {} |
| 92 | } |
| 93 | |