ProgressExample.vala 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. using Inversion;
  5. using Spry;
  6. /**
  7. * ProgressExample demonstrates the continuation feature for server-sent events (SSE).
  8. *
  9. * The continuation feature allows a Component to send real-time progress updates
  10. * to the client via SSE. This is useful for:
  11. * - Long-running task progress reporting
  12. * - Real-time status updates
  13. * - Live data streaming
  14. *
  15. * How it works:
  16. * 1. Add `spry-continuation` attribute to an element in your markup
  17. * (This is shorthand for: hx-ext="sse" sse-connect="(endpoint)" sse-close="_spry-close")
  18. * 2. Use `sse-swap="eventname"` on child elements to swap content when events arrive
  19. * 3. Override the `continuation(SseStream stream)` method in your Component
  20. * 4. Use `stream.send_event()` to send SSE events with HTML content to swap
  21. */
  22. class ProgressComponent : Component {
  23. public override string markup { get {
  24. return """
  25. <!DOCTYPE html>
  26. <html>
  27. <head>
  28. <script spry-res="htmx.js"></script>
  29. <script spry-res="htmx-sse.js"></script>
  30. <style>
  31. body { font-family: system-ui, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
  32. .progress-container {
  33. background: #e0e0e0;
  34. border-radius: 8px;
  35. overflow: hidden;
  36. margin: 20px 0;
  37. }
  38. .progress-bar {
  39. height: 30px;
  40. background: linear-gradient(90deg, #4CAF50, #8BC34A);
  41. transition: width 0.3s ease;
  42. display: flex;
  43. align-items: center;
  44. justify-content: center;
  45. color: white;
  46. font-weight: bold;
  47. min-width: 40px;
  48. }
  49. .status {
  50. padding: 15px;
  51. background: #f5f5f5;
  52. border-radius: 4px;
  53. margin: 10px 0;
  54. border-left: 4px solid #2196F3;
  55. }
  56. .log {
  57. max-height: 200px;
  58. overflow-y: auto;
  59. background: #263238;
  60. color: #4CAF50;
  61. padding: 15px;
  62. border-radius: 4px;
  63. font-family: monospace;
  64. font-size: 14px;
  65. }
  66. .log-entry { margin: 5px 0; }
  67. h1 { color: #333; }
  68. .info { color: #666; font-size: 14px; }
  69. </style>
  70. </head>
  71. <body>
  72. <h1>Task Progress Demo</h1>
  73. <p class="info">This example demonstrates Spry's continuation feature for real-time progress updates via Server-Sent Events (SSE).</p>
  74. <div spry-continuation>
  75. <div class="progress-container" sse-swap="progress">
  76. <div class="progress-bar" id="progress-bar" style="width: 0%">
  77. 0%
  78. </div>
  79. </div>
  80. <div class="status" sse-swap="status">
  81. <strong>Status:</strong> Initializing...
  82. </div>
  83. <div class="log" id="log">
  84. <div sse-swap="log">Waiting for task to start...</div>
  85. </div>
  86. </div>
  87. </body>
  88. </html>
  89. """;
  90. }}
  91. /**
  92. * The continuation method is called when a client connects to the SSE endpoint.
  93. * This is where you can send real-time updates to the client.
  94. *
  95. * The event data should be HTML content that will be swapped into elements
  96. * with matching sse-swap="eventname" attributes.
  97. */
  98. public async override void continuation(SseStream stream) throws Error {
  99. // Simulate a long-running task with progress updates
  100. var steps = new string[] {
  101. "Initializing task...",
  102. "Loading configuration...",
  103. "Connecting to database...",
  104. "Fetching records...",
  105. "Processing batch 1/5...",
  106. "Processing batch 2/5...",
  107. "Processing batch 3/5...",
  108. "Processing batch 4/5...",
  109. "Processing batch 5/5...",
  110. "Validating results...",
  111. "Generating report...",
  112. "Finalizing..."
  113. };
  114. for (int i = 0; i < steps.length; i++) {
  115. // Calculate progress percentage
  116. int percent = (int)(((i + 1) / (double)steps.length) * 100);
  117. // Send progress bar update - HTML that will be swapped into the progress bar
  118. yield stream.send_event(new SseEvent.with_type("progress",
  119. @"<div class=\"progress-bar\" id=\"progress-bar\" style=\"width: $percent%\">$percent%</div>"));
  120. // Send status update - HTML that will be swapped into the status div
  121. yield stream.send_event(new SseEvent.with_type("status",
  122. @"<strong>Status:</strong> $(steps[i])"));
  123. // Send log message - HTML that will be appended to the log
  124. yield stream.send_event(new SseEvent.with_type("log",
  125. @"<div class=\"log-entry\">$(steps[i])</div>"));
  126. // Simulate work being done (500ms per step)
  127. Timeout.add(500, () => {
  128. continuation.callback();
  129. return false;
  130. });
  131. yield;
  132. }
  133. // Send final completion messages
  134. yield stream.send_event(new SseEvent.with_type("progress",
  135. "<div class=\"progress-bar\" id=\"progress-bar\" style=\"width: 100%\">100% ✓</div>"));
  136. yield stream.send_event(new SseEvent.with_type("status",
  137. "<strong>Status:</strong> Task completed successfully!"));
  138. yield stream.send_event(new SseEvent.with_type("log",
  139. "<div class=\"log-entry\">✓ All tasks completed!</div>"));
  140. }
  141. }
  142. class HomePageEndpoint : Object, Endpoint {
  143. private ProgressComponent progress_component = inject<ProgressComponent>();
  144. public async Astralis.HttpResult handle_request(Astralis.HttpContext http_context, Astralis.RouteContext route_context) throws Error {
  145. return yield progress_component.to_result();
  146. }
  147. }
  148. void main(string[] args) {
  149. int port = args.length > 1 ? int.parse(args[1]) : 8080;
  150. try {
  151. var application = new WebApplication(port);
  152. // Register compression components
  153. application.use_compression();
  154. // Add Spry module (includes ContinuationProvider for SSE)
  155. application.add_module<SpryModule>();
  156. // Register the progress component
  157. application.add_transient<ProgressComponent>();
  158. // Register the home page endpoint
  159. application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
  160. print("Progress Example running on http://localhost:%d/\n", port);
  161. print("Open the URL in your browser to see real-time progress updates via SSE.\n");
  162. application.run();
  163. } catch (Error e) {
  164. printerr("Error: %s\n", e.message);
  165. Process.exit(1);
  166. }
  167. }