using Spry; using Inversion; /** * ComponentsContinuationsPage - Documentation for Spry continuations (SSE) * * This page covers what continuations are, the spry-continuation attribute, * and how to send real-time updates to clients. */ public class ComponentsContinuationsPage : PageComponent { public const string ROUTE = "/components/continuations"; private ComponentFactory factory = inject(); public override string markup { get { return """

Continuations

Continuations enable real-time updates from server to client using Server-Sent Events (SSE). They're perfect for progress indicators, live status updates, and any scenario where the server needs to push updates to the client over time.

What are Continuations?

A continuation is a long-running server process that can send multiple updates to the client over a single HTTP connection. Unlike regular requests that return once, continuations can push updates as they happen.

💡 Use Cases: Progress bars, build status, live logs, real-time notifications, file upload progress, long-running task monitoring.

The spry-continuation Attribute

Add spry-continuation to an element to enable SSE for its children. This attribute is shorthand for:

  • hx-ext="sse" - Enable HTMX SSE extension
  • sse-connect="(auto-endpoint)" - Connect to SSE endpoint
  • sse-close="_spry-close" - Close event name

The spry-dynamic Attribute

Mark child elements with spry-dynamic="name" to make them updatable. When you call send_dynamic("name"), that element's HTML is sent to the client and swapped in place.

The continuation() Method

Override continuation(ContinuationContext context) in your component to implement long-running processes that send updates.

ContinuationContext API

The ContinuationContext parameter provides methods for sending updates:

Method Description
send_dynamic(name) Send a dynamic section (by spry-dynamic name) as an SSE event
send_json(event_type, node) Send JSON data as an SSE event
send_string(event_type, data) Send raw string data as an SSE event
send_full_update(event_type) Send the entire component document
send_event(event) Send a custom SseEvent

Required Scripts

Include the HTMX SSE extension in your page for continuations to work:

The spry-unique Attribute

Use spry-unique on elements that need stable IDs for targeting. Spry generates unique IDs automatically.

⚠️ Restrictions: Cannot use spry-unique with an explicit id attribute, or inside spry-per-* loops.

Cancellation Handling

Override continuation_canceled() to clean up resources when the client disconnects:

Live Demo: Progress Bar

This demo shows a progress bar that updates in real-time using SSE. Click "Start Task" to see continuations in action!

Next Steps

"""; }} public override async void prepare() throws Error { // spry-continuation attribute example var continuation_attr = get_component_child("continuation-attr"); continuation_attr.language = "HTML"; continuation_attr.code = "
\n" + " \n" + "
\n" + "
\n" + "
\n" + "
\n" + "
\n" + " Status: Ready\n" + "
\n" + "
"; // spry-dynamic attribute example var dynamic_attr = get_component_child("dynamic-attr"); dynamic_attr.language = "HTML"; dynamic_attr.code = "\n" + "
\n" + "
...
\n" + "
\n\n" + "
\n" + " Status: Processing...\n" + "
\n\n" + ""; // continuation() method example var continuation_method = get_component_child("continuation-method"); continuation_method.language = "Vala"; continuation_method.code = "public async override void continuation(ContinuationContext ctx) throws Error {\n" + " for (int i = 0; i <= 100; i += 10) {\n" + " percent = i;\n" + " status = @\"Processing... $(i)%\";\n\n" + " // Send dynamic section updates to the client\n" + " yield ctx.send_dynamic(\"progress-bar\");\n" + " yield ctx.send_dynamic(\"status\");\n\n" + " // Simulate async work\n" + " Timeout.add(500, () => {\n" + " continuation.callback();\n" + " return false;\n" + " });\n" + " yield;\n" + " }\n\n" + " status = \"Complete!\";\n" + " yield ctx.send_dynamic(\"status\");\n" + "}"; // Required scripts example var scripts = get_component_child("scripts"); scripts.language = "HTML"; scripts.code = "\n" + "\n" + ""; // spry-unique attribute example var unique_attr = get_component_child("unique-attr"); unique_attr.language = "HTML"; unique_attr.code = "\n" + "
\n" + " \n" + "
\n\n" + "\n" + "Loading..."; // continuation_canceled example var canceled_method = get_component_child("canceled-method"); canceled_method.language = "Vala"; canceled_method.code = "public async override void continuation_canceled() throws Error {\n" + " // Client disconnected - clean up resources\n" + " cancel_background_task();\n" + " release_file_handles();\n" + " log_message(\"Task canceled by client\");\n" + "}"; // Set up the demo host var demo = get_component_child("progress-demo"); demo.source_file = "demo/DemoComponents/ProgressDemo.vala"; // Create and set the actual demo component var progress_demo = factory.create(); demo.set_outlet_child("demo-outlet", progress_demo); } }