ProgressExample.vala 7.1 KB

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