ProgressExample.vala 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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" spry-dynamic="progress-bar">
  79. <div class="progress-bar" spry-unique
  80. content-expr='format("%i%%", this.percent)' style-width-expr='format("%i%%", this.percent)'>
  81. 0%
  82. </div>
  83. </div>
  84. <div class="status" spry-dynamic="status">
  85. <strong>Status:</strong> <span spry-unique content-expr="this.status">Initializing...</span>
  86. </div>
  87. <div class="log" sid="log" spry-dynamic="log">
  88. <div spry-per-task="this.completed_tasks" content-expr="task"></div>
  89. </div>
  90. </div>
  91. </body>
  92. </html>
  93. """;
  94. }}
  95. /**
  96. * The continuation method is called when a client connects to the SSE endpoint.
  97. * This is where you can send real-time updates to the client.
  98. *
  99. * The event data should be HTML content that will be swapped into elements
  100. * with matching sse-swap="eventname" attributes.
  101. */
  102. public async override void continuation(ContinuationContext continuation_context) throws Error {
  103. // Simulate a long-running task with progress updates
  104. var steps = new string[] {
  105. "Initializing task...",
  106. "Loading configuration...",
  107. "Connecting to database...",
  108. "Fetching records...",
  109. "Processing batch 1/5...",
  110. "Processing batch 2/5...",
  111. "Processing batch 3/5...",
  112. "Processing batch 4/5...",
  113. "Processing batch 5/5...",
  114. "Validating results...",
  115. "Generating report...",
  116. "Finalizing..."
  117. };
  118. for (int i = 0; i < steps.length; i++) {
  119. // Update the template
  120. percent = (int)(((i + 1) / (double)steps.length) * 100);
  121. status = steps[i];
  122. completed_tasks.add_start(steps[i]);
  123. // Send progress bar update - HTML that will be swapped into the progress bar
  124. yield continuation_context.send_dynamic("progress-bar");
  125. // Send status update - HTML that will be swapped into the status div
  126. yield continuation_context.send_dynamic("status");
  127. // Send log message - HTML that will be appended to the log
  128. yield continuation_context.send_dynamic("log");
  129. // Simulate work being done (500ms per step)
  130. Timeout.add(500, () => {
  131. continuation.callback();
  132. return false;
  133. });
  134. yield;
  135. }
  136. // Send final completion messages
  137. percent = 100;
  138. status = "Task completed successfully!";
  139. yield continuation_context.send_dynamic("progress-bar");
  140. yield continuation_context.send_dynamic("status");
  141. }
  142. }
  143. class HomePageEndpoint : Object, Endpoint {
  144. private ProgressComponent progress_component = inject<ProgressComponent>();
  145. public async Astralis.HttpResult handle_request(Astralis.HttpContext http_context, Astralis.RouteContext route_context) throws Error {
  146. return yield progress_component.to_result();
  147. }
  148. }
  149. void main(string[] args) {
  150. int port = args.length > 1 ? int.parse(args[1]) : 8080;
  151. try {
  152. var application = new WebApplication(port);
  153. // Register compression components
  154. application.use_compression();
  155. // Add Spry module (includes ContinuationProvider for SSE)
  156. application.add_module<SpryModule>();
  157. // Register the progress component
  158. application.add_transient<ProgressComponent>();
  159. // Register the home page endpoint
  160. application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
  161. print("Progress Example running on http://localhost:%d/\n", port);
  162. print("Open the URL in your browser to see real-time progress updates via SSE.\n");
  163. application.run();
  164. } catch (Error e) {
  165. printerr("Error: %s\n", e.message);
  166. Process.exit(1);
  167. }
  168. }