templating.suspense

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

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…

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 ashx-swap-oob elements 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

class DeferEdge

One edge in a Suspense defer execution DAG.

feedslinks a deferred context key to a leaf block discovered via depends_on. coupleslinks…

Jump to symbol
class DeferExecutionPlan

Internal Suspense defer plan — discovery + ancestor pruning.

Not a public return type. Used by runtimerender_suspenseand by AppCompiler's freeze-time Suspense…

Jump to symbol
class _Deferred

Sentinel value for Suspense deferred context keys.

Used instead ofNoneso that templates can distinguish "not yet loaded" from "loaded but empty/falsy". The …

Jump to symbol
function format_oob_htmx

Wrap rendered block HTML as an htmx OOB swap element.

htmx scans the response body for elements withhx-swap-ooband swaps them into the…

Jump to symbol
function format_oob_script

Wrap rendered block HTML as a<template> + <script>pair.

Used for initial page loads where htmx OOB is not available. The inline script…

Jump to symbol
async function _render_error_html

Render error fallback HTML for a failed deferred block.

Resolution order:

  1. Per-routeSuspense(error_block=...)rendered from the global error_template(caller should pass this as suspense_error_block…
Jump to symbol
function _find_deferred_blocks

Map each deferred context key to the template blocks that depend on it.

Uses kida'sblock_metadata()static analysis to find blocks whosedepends_onset…

Jump to symbol
function _find_deferred_blocks_with_pruning

Like_find_deferred_blocks, also returning pruned ancestor names.

Jump to symbol
function _prune_ancestor_blocks

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…

Jump to symbol
function _edges_from_key_blocks

Build feeds + couples edges from a key→leaf-blocks mapping.

Jump to symbol
function plan_defer_execution

Build an explicit defer execution DAG for template_name.

Extends Suspense block discovery + ancestor-superset pruning into a frozen plan withfeeds(key→block) and …

Jump to symbol
function _should_wrap_in_layouts

Return True if the shell should be wrapped in the layout chain.

Jump to symbol
function _close_unstarted_awaitables

Close coroutine awaitables that validation rejected before scheduling.

Jump to symbol
async function _render_off_loop

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…

Jump to symbol
async function render_suspense

Render aSuspensereturn value as an async chunk stream.

Jump to symbol
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:

  1. Per-routeSuspense(error_block=...)rendered from the global error_template(caller should pass this as suspense_error_block)
  2. Globalerror_template + error_blockfrom AppConfig
  3. 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