Merge branch 'worktree-iso-phase-4' into macros

This commit is contained in:
2026-03-07 18:24:38 +00:00

View File

@@ -320,14 +320,10 @@ async def execute_page_streaming(
): ):
"""Execute a page with streaming response. """Execute a page with streaming response.
Returns an async generator that yields HTML chunks: All context-dependent setup (g, request, current_app access) runs in
1. HTML shell with suspense placeholders (immediate) this regular async function — called while the request context is live.
2. Resolution <script> tags as IO completes Returns an async generator that yields pre-computed HTML chunks and
3. Closing </body></html> awaits already-created tasks (no further context access needed).
Each suspense placeholder renders a loading skeleton. As data and
header IO resolve, the server streams inline scripts that call
``__sxResolve(id, sx)`` to replace the placeholder content.
""" """
import asyncio import asyncio
from .jinja_bridge import get_component_env, _get_request_context from .jinja_bridge import get_component_env, _get_request_context
@@ -392,7 +388,7 @@ async def execute_page_streaming(
layout_name = str(page_def.layout) layout_name = str(page_def.layout)
layout = get_layout(layout_name) layout = get_layout(layout_name)
# --- Concurrent IO tasks --- # --- Launch concurrent IO tasks (inherit context via create_task) ---
async def _eval_data_and_content(): async def _eval_data_and_content():
data_env = dict(env) data_env = dict(env)
@@ -417,7 +413,7 @@ async def execute_page_streaming(
data_task = asyncio.create_task(_eval_data_and_content()) data_task = asyncio.create_task(_eval_data_and_content())
header_task = asyncio.create_task(_eval_headers()) header_task = asyncio.create_task(_eval_headers())
# --- Build initial page SX with suspense placeholders --- # --- Build initial shell (still in request context) ---
initial_page_sx = await _render_to_sx("app-body", initial_page_sx = await _render_to_sx("app-body",
header_rows=SxExpr( header_rows=SxExpr(
@@ -430,10 +426,12 @@ async def execute_page_streaming(
shell, tail = sx_page_streaming_parts(tctx, initial_page_sx) shell, tail = sx_page_streaming_parts(tctx, initial_page_sx)
# --- Yield initial shell + scripts --- # --- Return async generator that yields chunks ---
# No context access needed below — just awaiting tasks and yielding strings.
async def _stream_chunks():
yield shell + tail yield shell + tail
# --- Yield resolution chunks in completion order ---
tasks = {data_task: "data", header_task: "headers"} tasks = {data_task: "data", header_task: "headers"}
pending = set(tasks.keys()) pending = set(tasks.keys())
@@ -459,6 +457,8 @@ async def execute_page_streaming(
yield "\n</body>\n</html>" yield "\n</body>\n</html>"
return _stream_chunks()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Blueprint mounting # Blueprint mounting
@@ -507,7 +507,7 @@ def mount_pages(bp: Any, service_name: str,
def _mount_one_page(bp: Any, service_name: str, page_def: PageDef) -> None: def _mount_one_page(bp: Any, service_name: str, page_def: PageDef) -> None:
"""Mount a single PageDef as a GET route on the blueprint.""" """Mount a single PageDef as a GET route on the blueprint."""
from quart import make_response, Response, stream_with_context from quart import make_response, Response
if page_def.stream: if page_def.stream:
# Streaming response: yields HTML chunks as IO resolves # Streaming response: yields HTML chunks as IO resolves
@@ -520,15 +520,13 @@ def _mount_one_page(bp: Any, service_name: str, page_def: PageDef) -> None:
if hasattr(result, "status_code"): if hasattr(result, "status_code"):
return result return result
return await make_response(result, 200) return await make_response(result, 200)
# stream_with_context is a decorator — wrap the generator function # execute_page_streaming does all context-dependent setup as a
# so app/request context is preserved across yields # regular async function (while request context is live), then
@stream_with_context # returns an async generator that only yields strings.
async def _stream(): gen = await execute_page_streaming(
async for chunk in execute_page_streaming(
current, service_name, url_params=kwargs, current, service_name, url_params=kwargs,
): )
yield chunk return Response(gen, content_type="text/html; charset=utf-8")
return Response(_stream(), content_type="text/html; charset=utf-8")
else: else:
# Standard non-streaming response # Standard non-streaming response
async def page_view(**kwargs: Any) -> Any: async def page_view(**kwargs: Any) -> Any: