ProgressExample.vala 7.5 KB

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