SseExample.vala 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. /// ClockEndpoint is a singleton SSE endpoint that broadcasts the current time
  5. /// to all connected clients every second.
  6. ///
  7. /// It demonstrates:
  8. /// - Singleton pattern for SSE endpoints (shared state across connections)
  9. /// - Implementing retry_interval property
  10. /// - Using new_connection for initial welcome message
  11. /// - Public method to broadcast events
  12. public class ClockEndpoint : SseEndpoint {
  13. private int connection_counter = 0;
  14. private Mutex counter_mutex = Mutex();
  15. /// Retry interval: clients should wait 3 seconds before reconnecting
  16. public override uint retry_interval { get { return 3000; } }
  17. /// Called when a new client connects - send welcome message
  18. public override async void new_connection(SseStream stream) {
  19. // Assign a unique connection ID
  20. int connection_id;
  21. counter_mutex.lock();
  22. connection_id = ++connection_counter;
  23. counter_mutex.unlock();
  24. print(@"SSE client connected (connection #$connection_id, total: $(get_open_streams().length))\n");
  25. // Send welcome message
  26. try {
  27. yield stream.send_event(new SseEvent.with_type("connected", @"You are connection #$connection_id"));
  28. } catch (Error e) {
  29. print(@"Failed to send welcome: $(e.message)\n");
  30. }
  31. // Listen for disconnection
  32. stream.disconnected.connect(() => {
  33. print(@"SSE client disconnected (connection #$connection_id)\n");
  34. });
  35. }
  36. /// Public method to broadcast the current time to all connected clients.
  37. /// This can be called from anywhere (e.g., from another endpoint or service).
  38. public async void broadcast_time() {
  39. var now = new DateTime.now_local();
  40. var time_str = now.format("%H:%M:%S");
  41. var date_str = now.format("%Y-%m-%d");
  42. // Create event with current time
  43. var time_event = new SseEvent.with_type("time", time_str);
  44. // Also send a JSON-formatted message
  45. var json_data = @"{\"time\":\"$time_str\",\"date\":\"$date_str\"}";
  46. var json_event = new SseEvent.with_type("datetime", json_data);
  47. // Broadcast to all connected clients
  48. yield broadcast_event(time_event);
  49. yield broadcast_event(json_event);
  50. }
  51. /// Start the broadcast loop. Called externally after construction.
  52. public void start_broadcast_loop() {
  53. broadcast_loop_iteration.begin();
  54. }
  55. private async void broadcast_loop_iteration() {
  56. // Broadcast current time
  57. yield broadcast_time();
  58. // Schedule next iteration in 1 second
  59. Timeout.add(1000, () => {
  60. broadcast_loop_iteration.begin();
  61. return false; // Don't repeat, we'll reschedule manually
  62. });
  63. }
  64. }
  65. /// IndexEndpoint serves a simple HTML page that connects to the SSE endpoints
  66. class IndexEndpoint : Object, Endpoint {
  67. public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws Error {
  68. var html = """
  69. <!DOCTYPE html>
  70. <html>
  71. <head>
  72. <title>Astralis SSE Example</title>
  73. <style>
  74. body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
  75. .section { margin: 20px 0; padding: 20px; border: 1px solid #ccc; border-radius: 8px; }
  76. h1 { color: #333; }
  77. h2 { color: #666; margin-top: 0; }
  78. #clock { font-size: 3em; font-family: monospace; text-align: center; }
  79. .status { padding: 5px 10px; border-radius: 4px; margin: 10px 0; }
  80. .status.connected { background: #d4edda; color: #155724; }
  81. .status.disconnected { background: #f8d7da; color: #721c24; }
  82. #events { background: #f5f5f5; padding: 10px; height: 200px; overflow-y: scroll; font-family: monospace; font-size: 0.9em; }
  83. .event { margin: 2px 0; padding: 2px 5px; border-bottom: 1px solid #ddd; }
  84. .event-time { color: #999; }
  85. .event-type { color: #0066cc; font-weight: bold; }
  86. </style>
  87. </head>
  88. <body>
  89. <h1>Astralis SSE Example</h1>
  90. <div class="section">
  91. <h2>Live Clock (Broadcast)</h2>
  92. <div id="clock-status" class="status disconnected">Disconnected</div>
  93. <div id="clock">--:--:--</div>
  94. </div>
  95. <div class="section">
  96. <h2>Event Log</h2>
  97. <div id="events"></div>
  98. </div>
  99. <script>
  100. function logEvent(type, data) {
  101. const eventsDiv = document.getElementById('events');
  102. const time = new Date().toLocaleTimeString();
  103. const eventDiv = document.createElement('div');
  104. eventDiv.className = 'event';
  105. eventDiv.innerHTML = '<span class="event-time">' + time + '</span> <span class="event-type">[' + type + ']</span> ' + data;
  106. eventsDiv.insertBefore(eventDiv, eventsDiv.firstChild);
  107. }
  108. // Clock SSE connection
  109. const clockSource = new EventSource('/clock-stream');
  110. const clockStatus = document.getElementById('clock-status');
  111. clockSource.onopen = function() {
  112. clockStatus.textContent = 'Connected';
  113. clockStatus.className = 'status connected';
  114. };
  115. clockSource.onerror = function() {
  116. clockStatus.textContent = 'Disconnected (reconnecting...)';
  117. clockStatus.className = 'status disconnected';
  118. };
  119. clockSource.addEventListener('time', function(e) {
  120. document.getElementById('clock').textContent = e.data;
  121. });
  122. clockSource.addEventListener('datetime', function(e) {
  123. logEvent('datetime', e.data);
  124. });
  125. clockSource.addEventListener('connected', function(e) {
  126. logEvent('connected', e.data);
  127. });
  128. </script>
  129. </body>
  130. </html>
  131. """;
  132. return new HttpStringResult(html)
  133. .set_header("Content-Type", "text/html");
  134. }
  135. }
  136. void main() {
  137. var application = new WebApplication(8080);
  138. // Register SSE endpoint as a singleton with an explicit factory
  139. // This ensures the constructor logic runs
  140. application.add_singleton_endpoint<ClockEndpoint>(
  141. new EndpointRoute("/clock-stream"),
  142. () => {
  143. var endpoint = new ClockEndpoint();
  144. endpoint.start_broadcast_loop();
  145. return endpoint;
  146. }
  147. );
  148. // Register the index page
  149. application.add_endpoint<IndexEndpoint>(new EndpointRoute("/"));
  150. print("SSE Example server running on http://localhost:8080\n");
  151. print("Open http://localhost:8080 in your browser to see SSE in action\n");
  152. application.run();
  153. }