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
<div spry-continuation>
<!-- Children can receive SSE updates -->
<div spry-dynamic="progress-bar">
<div class="progress-bar" style-width-expr='format("%i%%", this.percent)'>
</div>
</div>
<div spry-dynamic="status">
<strong>Status:</strong> <span spry-unique content-expr="this.status">Ready</span>
</div>
</div>
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.
<!-- spry-dynamic marks elements for SSE swapping -->
<div class="progress-container" spry-dynamic="progress-bar">
<div class="progress-bar">...</div>
</div>
<div class="status" spry-dynamic="status">
<strong>Status:</strong> <span>Processing...</span>
</div>
<!-- Automatically gets: sse-swap="_spry-dynamic-{name}" hx-swap="outerHTML" -->
The continuation() Method
Override continuation(ContinuationContext context) in your component
to implement long-running processes that send updates.
public async override void continuation(ContinuationContext ctx) throws Error {
for (int i = 0; i <= 100; i += 10) {
percent = i;
status = @"Processing... $(i)%";
// Send dynamic section updates to the client
yield ctx.send_dynamic("progress-bar");
yield ctx.send_dynamic("status");
// Simulate async work
Timeout.add(500, () => {
continuation.callback();
return false;
});
yield;
}
status = "Complete!";
yield ctx.send_dynamic("status");
}
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:
<!-- In your page template or component -->
<script spry-res="htmx.js"></script>
<script spry-res="htmx-sse.js"></script>
The spry-unique Attribute
Use spry-unique on elements that need stable IDs for targeting.
Spry generates unique IDs automatically.
<!-- spry-unique generates a stable unique ID -->
<div class="progress-bar" spry-unique>
<!-- Gets ID like: _spry-unique-0-abc123 -->
</div>
<!-- Use with content-expr for dynamic content -->
<span spry-unique content-expr="this.status">Loading...</span>
⚠️ 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:
public async override void continuation_canceled() throws Error {
// Client disconnected - clean up resources
cancel_background_task();
release_file_handles();
log_message("Task canceled by client");
}
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!