Suspense-style streaming — shell first, deferred blocks via OOB.
Renders a page shell immediately with skeleton/fallback content for blocks whose data is still loading, then streams in the real content as each async source resolves.
Two delivery strategies (auto-selected by the negotiation layer):
- htmx navigations: deferred blocks arrive as
hx-swap-oobelements that htmx processes automatically. - Initial page loads:
<template>+ inline<script>pairs swap content into place without any framework dependency.
Pipeline::
Suspense("dashboard.html",
header=site_header(), # sync — available in the shell
stats=load_stats(), # awaitable — deferred
feed=load_feed(), # awaitable — deferred
)
1. Separate sync vs. awaitable context values
2. Render shell with sync context + ``DEFERRED`` sentinel for awaitable
keys + the ``__chirp_defer_pending__`` frozenset (``CHIRP_DEFER_PENDING_KEY``)
3. Yield shell as first chunk (instant first paint)
4. Resolve awaitables concurrently (anyio task group)
5. Determine blocks to re-render:
a. If ``defer_blocks`` is set, use that list directly
b. Otherwise, discover via ``block_metadata().depends_on``
and prune ancestor blocks (strict ``depends_on`` superset)
6. Render each block with full context
7. Yield OOB swap chunks (htmx or <template>+<script>)
templating.suspense
| Name | Type | Default | Description |
|---|---|---|---|
type
|
|
— | |
qualified_name
|
|
— | |
element_type
|
|
— | |
description
|
|
— | |
source_file
|
|
— | |
line_number
|
|
— | |
is_autodoc
|
|
— | |
autodoc_element
|
|
— | |
_autodoc_template
|
|
— | |
_autodoc_url_path
|
|
— | |
_autodoc_page_type
|
|
— | |
title
|
|
— | |
doc_content_hash
|
|
— |
Symbols on this page
One edge in a Suspense defer execution DAG.
feedslinks a deferred context key to a leaf block discovered via
depends_on. coupleslinks…
Internal Suspense defer plan — discovery + ancestor pruning.
Not a public return type. Used by runtimerender_suspenseand by
AppCompiler's freeze-time Suspense…
Sentinel value for Suspense deferred context keys.
Used instead ofNoneso that templates can distinguish "not yet loaded"
from "loaded but empty/falsy". The …
Wrap rendered block HTML as an htmx OOB swap element.
htmx scans the response body for elements withhx-swap-ooband swaps them into the…
Wrap rendered block HTML as a<template> + <script>pair.
Used for initial page loads where htmx OOB is not available. The inline script…
Render error fallback HTML for a failed deferred block.
Resolution order:
- Per-route
Suspense(error_block=...)rendered from the globalerror_template(caller should pass this as suspense_error_block…
Map each deferred context key to the template blocks that depend on it.
Uses kida'sblock_metadata()static analysis to find blocks
whosedepends_onset…
Like_find_deferred_blocks, also returning pruned ancestor names.
Drop blocks whose depends_on is a strict superset of another block's.
Parent blocks in the AST always accumulate the full dependency set of their…
Build feeds + couples edges from a key→leaf-blocks mapping.
Build an explicit defer execution DAG for template_name.
Extends Suspense block discovery + ancestor-superset pruning into a
frozen plan withfeeds(key→block) and …
Return True if the shell should be wrapped in the layout chain.
Close coroutine awaitables that validation rejected before scheduling.
Run a discrete, CPU-bound kida render off the event loop (issue #145).
Each Suspense render (the shelltemplate.render(...)and every deferred
template.render_block(...)) is…
Render aSuspensereturn value as an async chunk stream.
DeferEdge
class
One edge in a Suspense defer execution DAG.
feedslinks a deferred context key to a leaf block discovered via
depends_on. coupleslinks two deferred keys that share a leaf block
(they are not independent for concurrent checkout / contract purposes).
DeferExecutionPlan
class
Internal Suspense defer plan — discovery + ancestor pruning.
Not a public return type. Used by runtimerender_suspenseand by
AppCompiler's freeze-time Suspense DAG (#948).
_Deferred
class
Sentinel value for Suspense deferred context keys.
Used instead ofNoneso that templates can distinguish "not yet loaded"
from "loaded but empty/falsy". Thedeferredkida test
({% if x is deferred %}) checks identity against this singleton.
__bool__ raises TypeError so that bare {% if x %}fails loudly
instead of silently treating a pending value as falsy.
format_oob_htmx
function
def format_oob_htmx(block_html: str, target_id: str, swap: str = 'true', *, wrap: bool = True) -> str
Wrap rendered block HTML as an htmx OOB swap element.
htmx scans the response body for elements withhx-swap-oob
and swaps them into the page byid.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
block_html
|
str
|
— | |
target_id
|
str
|
— | |
swap
|
str
|
'true'
|
|
wrap
|
bool
|
True
|
format_oob_script
function
def format_oob_script(block_html: str, target_id: str, *, nonce: str = '') -> str
Wrap rendered block HTML as a<template> + <script>pair.
Used for initial page loads where htmx OOB is not available. The inline script moves template content into the target element.
If the block's first child element has the sameidas the target,
replaceWithis used (outerHTML-style) to avoid double-nesting.
OtherwiseinnerHTMLreplacement is used.
When nonce is non-empty, the emitted<script>carries a
nonce="..."attribute so it survives a nonce-based CSP that no longer
ships'unsafe-inline'. Suspense streams capture the live request nonce
(seecsp_nonce()) at this call site.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
block_html
|
str
|
— | |
target_id
|
str
|
— | |
nonce
|
str
|
''
|
_render_error_html
function
async
async def _render_error_html(env: Environment, *, block_name: str, deferred_key: str, error: BaseException | None, error_template: str | None, error_block: str, suspense_error_block: str | None) -> str
Render error fallback HTML for a failed deferred block.
Resolution order:
- Per-route
Suspense(error_block=...)rendered from the globalerror_template(caller should pass this as suspense_error_block) - Global
error_template+error_blockfrom AppConfig - Hardcoded default HTML
The fallbackrender_blockis CPU-bound, so it runs off the event loop
(see_render_off_loop()) — the error path must not reintroduce the
inline-on-loop blocking the main render path avoids.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
env
|
Environment
|
— | |
block_name
|
str
|
— | |
deferred_key
|
str
|
— | |
error
|
BaseException | None
|
— | |
error_template
|
str | None
|
— | |
error_block
|
str
|
— | |
suspense_error_block
|
str | None
|
— |
_find_deferred_blocks
function
def _find_deferred_blocks(env: Environment, template_name: str, deferred_keys: set[str]) -> dict[str, list[str]]
Map each deferred context key to the template blocks that depend on it.
Uses kida'sblock_metadata()static analysis to find blocks
whosedepends_onset intersects with the deferred keys.
Parent blocks whosedepends_onis a strict superset of another
matched block are pruned — they would re-render the entire section
for an OOB target that likely doesn't exist in the DOM.
Returns{context_key: [block_name, ...]}— a key may affect
multiple blocks, and a block may appear under multiple keys
(de-duplicated during rendering).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
env
|
Environment
|
— | |
template_name
|
str
|
— | |
deferred_keys
|
set[str]
|
— |
_find_deferred_blocks_with_pruning
function
def _find_deferred_blocks_with_pruning(env: Environment, template_name: str, deferred_keys: set[str]) -> tuple[dict[str, list[str]], tuple[str, ...]]
Like_find_deferred_blocks, also returning pruned ancestor names.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
env
|
Environment
|
— | |
template_name
|
str
|
— | |
deferred_keys
|
set[str]
|
— |
_prune_ancestor_blocks
function
def _prune_ancestor_blocks(blocks: list[str], deps_by_block: dict[str, frozenset[str]]) -> list[str]
Drop blocks whose depends_on is a strict superset of another block's.
Parent blocks in the AST always accumulate the full dependency set
of their children. When both a parent (page_content) and a leaf
(stats_panel) match a deferred key, the parent's depends_on
is a strict superset of the leaf's. Re-rendering the parent as an
OOB chunk is expensive and the target id rarely exists in the DOM.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
blocks
|
list[str]
|
— | |
deps_by_block
|
dict[str, frozenset[str]]
|
— |
_edges_from_key_blocks
function
def _edges_from_key_blocks(key_to_blocks: Mapping[str, list[str] | tuple[str, ...]]) -> tuple[DeferEdge, ...]
Build feeds + couples edges from a key→leaf-blocks mapping.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
key_to_blocks
|
Mapping[str, list[str] | tuple[str, ...]]
|
— |
plan_defer_execution
function
def plan_defer_execution(env: Environment, template_name: str, deferred_keys: set[str] | frozenset[str], *, defer_blocks: tuple[str, ...] | None = None) -> DeferExecutionPlan
Build an explicit defer execution DAG for template_name.
Extends Suspense block discovery + ancestor-superset pruning into a
frozen plan withfeeds (key→block) and couples(shared-block)
edges. Explicitdefer_blocksbypasses discovery (same as runtime).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
env
|
Environment
|
— | |
template_name
|
str
|
— | |
deferred_keys
|
set[str] | frozenset[str]
|
— | |
defer_blocks
|
tuple[str, ...] | None
|
None
|
_should_wrap_in_layouts
function
def _should_wrap_in_layouts(layout_chain: Any, request: Any) -> bool
Return True if the shell should be wrapped in the layout chain.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
layout_chain
|
Any
|
— | |
request
|
Any
|
— |
_close_unstarted_awaitables
function
def _close_unstarted_awaitables(awaitables: dict[str, Awaitable[Any]]) -> None
Close coroutine awaitables that validation rejected before scheduling.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
awaitables
|
dict[str, Awaitable[Any]]
|
— |
_render_off_loop
function
async
async def _render_off_loop(fn: Callable[[], T]) -> T
Run a discrete, CPU-bound kida render off the event loop (issue #145).
Each Suspense render (the shelltemplate.render(...)and every deferred
template.render_block(...)) is a complete synchronous call that returns
a full string — unlikeStream(one lazy progressive generator that cannot
userun_sync). So the correct tool is anyio.to_thread.run_syncper
render call: it moves the blocking work to a worker thread so concurrent
loop tasks make progress, while Suspense's progressiveness (shell first, then
one OOB chunk per resolved awaitable) is preserved byrender_suspense
yielding between calls. A single complete render on one worker thread also
satisfies kida's single-thread renderer contract.
The loop's contextvars are copied onto the worker (mirroring
render_stream_async) so get_request()and the live CSP nonce (#181)
are visible inside template globals/filters during the render. anyio>=4 also
copies the current context, but the explicit copy is the established,
unambiguous pattern.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
fn
|
Callable[[], T]
|
— |
render_suspense
function
async
async def render_suspense(env: Environment, suspense: Suspense, *, is_htmx: bool = False, layout_chain: Any = None, layout_context: dict[str, Any] | None = None, request: Any = None, oob_registry: OOBRegistry | None = None, fragment_target_registry: FragmentTargetRegistry | None = None, error_template: str | None = None, error_block: str = 'fallback') -> AsyncIterator[str]
Render aSuspensereturn value as an async chunk stream.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
env
|
Environment
|
— | Kida template environment. |
suspense
|
Suspense
|
— | The ``Suspense`` return value from a route handler. |
is_htmx
|
bool
|
False
|
If ``True``, use ``hx-swap-oob`` formatting. If ``False``, use ``<template>`` + ``<script>`` pairs. |
layout_chain
|
Any
|
None
|
Optional layout chain to wrap the shell in. |
layout_context
|
dict[str, Any] | None
|
None
|
Context for layout templates (when layout_chain used). |
request
|
Any
|
None
|
Request for fragment detection (when layout_chain used). |
oob_registry
|
OOBRegistry | None
|
None
|
Optional OOB registry for swap/wrap resolution. |
fragment_target_registry
|
FragmentTargetRegistry | None
|
None
|
Optional fragment target registry for replace-style boosted navigation that must skip outer layouts. |
error_template
|
str | None
|
None
|
|
error_block
|
str
|
'fallback'
|
View source · /home/runner/work/chirp/chirp/site/../src/chirp/templating/suspense.py:1