Merge branch 'worktree-iso-phase-4' into macros
This commit is contained in:
@@ -320,14 +320,10 @@ async def execute_page_streaming(
|
||||
):
|
||||
"""Execute a page with streaming response.
|
||||
|
||||
Returns an async generator that yields HTML chunks:
|
||||
1. HTML shell with suspense placeholders (immediate)
|
||||
2. Resolution <script> tags as IO completes
|
||||
3. Closing </body></html>
|
||||
|
||||
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.
|
||||
All context-dependent setup (g, request, current_app access) runs in
|
||||
this regular async function — called while the request context is live.
|
||||
Returns an async generator that yields pre-computed HTML chunks and
|
||||
awaits already-created tasks (no further context access needed).
|
||||
"""
|
||||
import asyncio
|
||||
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 = get_layout(layout_name)
|
||||
|
||||
# --- Concurrent IO tasks ---
|
||||
# --- Launch concurrent IO tasks (inherit context via create_task) ---
|
||||
|
||||
async def _eval_data_and_content():
|
||||
data_env = dict(env)
|
||||
@@ -417,7 +413,7 @@ async def execute_page_streaming(
|
||||
data_task = asyncio.create_task(_eval_data_and_content())
|
||||
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",
|
||||
header_rows=SxExpr(
|
||||
@@ -430,34 +426,38 @@ async def execute_page_streaming(
|
||||
|
||||
shell, tail = sx_page_streaming_parts(tctx, initial_page_sx)
|
||||
|
||||
# --- Yield initial shell + scripts ---
|
||||
yield shell + tail
|
||||
# --- Return async generator that yields chunks ---
|
||||
# No context access needed below — just awaiting tasks and yielding strings.
|
||||
|
||||
# --- Yield resolution chunks in completion order ---
|
||||
tasks = {data_task: "data", header_task: "headers"}
|
||||
pending = set(tasks.keys())
|
||||
async def _stream_chunks():
|
||||
yield shell + tail
|
||||
|
||||
while pending:
|
||||
done, pending = await asyncio.wait(
|
||||
pending, return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
for task in done:
|
||||
label = tasks[task]
|
||||
try:
|
||||
result = task.result()
|
||||
except Exception as e:
|
||||
logger.error("Streaming %s task failed: %s", label, e)
|
||||
continue
|
||||
tasks = {data_task: "data", header_task: "headers"}
|
||||
pending = set(tasks.keys())
|
||||
|
||||
if label == "data":
|
||||
content_sx, filter_sx, aside_sx, menu_sx = result
|
||||
yield sx_streaming_resolve_script("stream-content", content_sx)
|
||||
elif label == "headers":
|
||||
header_rows, header_menu = result
|
||||
if header_rows:
|
||||
yield sx_streaming_resolve_script("stream-headers", header_rows)
|
||||
while pending:
|
||||
done, pending = await asyncio.wait(
|
||||
pending, return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
for task in done:
|
||||
label = tasks[task]
|
||||
try:
|
||||
result = task.result()
|
||||
except Exception as e:
|
||||
logger.error("Streaming %s task failed: %s", label, e)
|
||||
continue
|
||||
|
||||
yield "\n</body>\n</html>"
|
||||
if label == "data":
|
||||
content_sx, filter_sx, aside_sx, menu_sx = result
|
||||
yield sx_streaming_resolve_script("stream-content", content_sx)
|
||||
elif label == "headers":
|
||||
header_rows, header_menu = result
|
||||
if header_rows:
|
||||
yield sx_streaming_resolve_script("stream-headers", header_rows)
|
||||
|
||||
yield "\n</body>\n</html>"
|
||||
|
||||
return _stream_chunks()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -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:
|
||||
"""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:
|
||||
# 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"):
|
||||
return result
|
||||
return await make_response(result, 200)
|
||||
# stream_with_context is a decorator — wrap the generator function
|
||||
# so app/request context is preserved across yields
|
||||
@stream_with_context
|
||||
async def _stream():
|
||||
async for chunk in execute_page_streaming(
|
||||
current, service_name, url_params=kwargs,
|
||||
):
|
||||
yield chunk
|
||||
return Response(_stream(), content_type="text/html; charset=utf-8")
|
||||
# execute_page_streaming does all context-dependent setup as a
|
||||
# regular async function (while request context is live), then
|
||||
# returns an async generator that only yields strings.
|
||||
gen = await execute_page_streaming(
|
||||
current, service_name, url_params=kwargs,
|
||||
)
|
||||
return Response(gen, content_type="text/html; charset=utf-8")
|
||||
else:
|
||||
# Standard non-streaming response
|
||||
async def page_view(**kwargs: Any) -> Any:
|
||||
|
||||
Reference in New Issue
Block a user