|
@@ -0,0 +1,191 @@
|
|
|
|
|
+using Astralis;
|
|
|
|
|
+using Invercargill;
|
|
|
|
|
+using Invercargill.DataStructures;
|
|
|
|
|
+using Inversion;
|
|
|
|
|
+using Spry;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * ProgressExample demonstrates the continuation feature for server-sent events (SSE).
|
|
|
|
|
+ *
|
|
|
|
|
+ * The continuation feature allows a Component to send real-time progress updates
|
|
|
|
|
+ * to the client via SSE. This is useful for:
|
|
|
|
|
+ * - Long-running task progress reporting
|
|
|
|
|
+ * - Real-time status updates
|
|
|
|
|
+ * - Live data streaming
|
|
|
|
|
+ *
|
|
|
|
|
+ * How it works:
|
|
|
|
|
+ * 1. Add `spry-continuation` attribute to an element in your markup
|
|
|
|
|
+ * (This is shorthand for: hx-ext="sse" sse-connect="(endpoint)" sse-close="_spry-close")
|
|
|
|
|
+ * 2. Use `sse-swap="eventname"` on child elements to swap content when events arrive
|
|
|
|
|
+ * 3. Override the `continuation(SseStream stream)` method in your Component
|
|
|
|
|
+ * 4. Use `stream.send_event()` to send SSE events with HTML content to swap
|
|
|
|
|
+ */
|
|
|
|
|
+class ProgressComponent : Component {
|
|
|
|
|
+
|
|
|
|
|
+ public override string markup { get {
|
|
|
|
|
+ return """
|
|
|
|
|
+ <!DOCTYPE html>
|
|
|
|
|
+ <html>
|
|
|
|
|
+ <head>
|
|
|
|
|
+ <script spry-res="htmx.js"></script>
|
|
|
|
|
+ <script spry-res="htmx-sse.js"></script>
|
|
|
|
|
+ <style>
|
|
|
|
|
+ body { font-family: system-ui, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
|
|
|
|
|
+ .progress-container {
|
|
|
|
|
+ background: #e0e0e0;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ margin: 20px 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ .progress-bar {
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ background: linear-gradient(90deg, #4CAF50, #8BC34A);
|
|
|
|
|
+ transition: width 0.3s ease;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ min-width: 40px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .status {
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ margin: 10px 0;
|
|
|
|
|
+ border-left: 4px solid #2196F3;
|
|
|
|
|
+ }
|
|
|
|
|
+ .log {
|
|
|
|
|
+ max-height: 200px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ background: #263238;
|
|
|
|
|
+ color: #4CAF50;
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-family: monospace;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .log-entry { margin: 5px 0; }
|
|
|
|
|
+ h1 { color: #333; }
|
|
|
|
|
+ .info { color: #666; font-size: 14px; }
|
|
|
|
|
+ </style>
|
|
|
|
|
+ </head>
|
|
|
|
|
+ <body>
|
|
|
|
|
+ <h1>Task Progress Demo</h1>
|
|
|
|
|
+ <p class="info">This example demonstrates Spry's continuation feature for real-time progress updates via Server-Sent Events (SSE).</p>
|
|
|
|
|
+
|
|
|
|
|
+ <div spry-continuation>
|
|
|
|
|
+ <div class="progress-container" sse-swap="progress">
|
|
|
|
|
+ <div class="progress-bar" id="progress-bar" style="width: 0%">
|
|
|
|
|
+ 0%
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="status" sse-swap="status">
|
|
|
|
|
+ <strong>Status:</strong> Initializing...
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="log" id="log">
|
|
|
|
|
+ <div sse-swap="log">Waiting for task to start...</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </body>
|
|
|
|
|
+ </html>
|
|
|
|
|
+ """;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The continuation method is called when a client connects to the SSE endpoint.
|
|
|
|
|
+ * This is where you can send real-time updates to the client.
|
|
|
|
|
+ *
|
|
|
|
|
+ * The event data should be HTML content that will be swapped into elements
|
|
|
|
|
+ * with matching sse-swap="eventname" attributes.
|
|
|
|
|
+ */
|
|
|
|
|
+ public async override void continuation(SseStream stream) throws Error {
|
|
|
|
|
+ // Simulate a long-running task with progress updates
|
|
|
|
|
+ var steps = new string[] {
|
|
|
|
|
+ "Initializing task...",
|
|
|
|
|
+ "Loading configuration...",
|
|
|
|
|
+ "Connecting to database...",
|
|
|
|
|
+ "Fetching records...",
|
|
|
|
|
+ "Processing batch 1/5...",
|
|
|
|
|
+ "Processing batch 2/5...",
|
|
|
|
|
+ "Processing batch 3/5...",
|
|
|
|
|
+ "Processing batch 4/5...",
|
|
|
|
|
+ "Processing batch 5/5...",
|
|
|
|
|
+ "Validating results...",
|
|
|
|
|
+ "Generating report...",
|
|
|
|
|
+ "Finalizing..."
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = 0; i < steps.length; i++) {
|
|
|
|
|
+ // Calculate progress percentage
|
|
|
|
|
+ int percent = (int)(((i + 1) / (double)steps.length) * 100);
|
|
|
|
|
+
|
|
|
|
|
+ // Send progress bar update - HTML that will be swapped into the progress bar
|
|
|
|
|
+ yield stream.send_event(new SseEvent.with_type("progress",
|
|
|
|
|
+ @"<div class=\"progress-bar\" id=\"progress-bar\" style=\"width: $percent%\">$percent%</div>"));
|
|
|
|
|
+
|
|
|
|
|
+ // Send status update - HTML that will be swapped into the status div
|
|
|
|
|
+ yield stream.send_event(new SseEvent.with_type("status",
|
|
|
|
|
+ @"<strong>Status:</strong> $(steps[i])"));
|
|
|
|
|
+
|
|
|
|
|
+ // Send log message - HTML that will be appended to the log
|
|
|
|
|
+ yield stream.send_event(new SseEvent.with_type("log",
|
|
|
|
|
+ @"<div class=\"log-entry\">$(steps[i])</div>"));
|
|
|
|
|
+
|
|
|
|
|
+ // Simulate work being done (500ms per step)
|
|
|
|
|
+ Timeout.add(500, () => {
|
|
|
|
|
+ continuation.callback();
|
|
|
|
|
+ return false;
|
|
|
|
|
+ });
|
|
|
|
|
+ yield;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Send final completion messages
|
|
|
|
|
+ yield stream.send_event(new SseEvent.with_type("progress",
|
|
|
|
|
+ "<div class=\"progress-bar\" id=\"progress-bar\" style=\"width: 100%\">100% ✓</div>"));
|
|
|
|
|
+ yield stream.send_event(new SseEvent.with_type("status",
|
|
|
|
|
+ "<strong>Status:</strong> Task completed successfully!"));
|
|
|
|
|
+ yield stream.send_event(new SseEvent.with_type("log",
|
|
|
|
|
+ "<div class=\"log-entry\">✓ All tasks completed!</div>"));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class HomePageEndpoint : Object, Endpoint {
|
|
|
|
|
+
|
|
|
|
|
+ private ProgressComponent progress_component = inject<ProgressComponent>();
|
|
|
|
|
+
|
|
|
|
|
+ public async Astralis.HttpResult handle_request(Astralis.HttpContext http_context, Astralis.RouteContext route_context) throws Error {
|
|
|
|
|
+ return yield progress_component.to_result();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void main(string[] args) {
|
|
|
|
|
+ int port = args.length > 1 ? int.parse(args[1]) : 8080;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ var application = new WebApplication(port);
|
|
|
|
|
+
|
|
|
|
|
+ // Register compression components
|
|
|
|
|
+ application.use_compression();
|
|
|
|
|
+
|
|
|
|
|
+ // Add Spry module (includes ContinuationProvider for SSE)
|
|
|
|
|
+ application.add_module<SpryModule>();
|
|
|
|
|
+
|
|
|
|
|
+ // Register the progress component
|
|
|
|
|
+ application.add_transient<ProgressComponent>();
|
|
|
|
|
+
|
|
|
|
|
+ // Register the home page endpoint
|
|
|
|
|
+ application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
|
|
|
|
|
+
|
|
|
|
|
+ print("Progress Example running on http://localhost:%d/\n", port);
|
|
|
|
|
+ print("Open the URL in your browser to see real-time progress updates via SSE.\n");
|
|
|
|
|
+
|
|
|
|
|
+ application.run();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ printerr("Error: %s\n", e.message);
|
|
|
|
|
+ Process.exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|