Phase 5: async IO rendering — components call IO primitives client-side
Wire async rendering into client-side routing: pages whose component trees reference IO primitives (highlight, current-user, etc.) now render client-side via Promise-aware asyncRenderToDom. IO calls proxy through /sx/io/<name> endpoint, which falls back to page helpers. - Add has-io flag to page registry entries (helpers.py) - Remove IO purity filter — include IO-dependent components in bundles - Extend try-client-route with 4 paths: pure, data, IO, data+IO - Convert tryAsyncEvalContent to callback style, add platform mapping - IO proxy falls back to page helpers (highlight works via proxy) - Demo page: /isomorphism/async-io with inline highlight calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -331,6 +331,9 @@ def auto_mount_pages(app: Any, service_name: str) -> None:
|
||||
if has_data_pages:
|
||||
auto_mount_page_data(app, service_name)
|
||||
|
||||
# Mount IO proxy endpoint for Phase 5: client-side IO primitives
|
||||
mount_io_endpoint(app, service_name)
|
||||
|
||||
|
||||
def mount_pages(bp: Any, service_name: str,
|
||||
names: set[str] | list[str] | None = None) -> None:
|
||||
@@ -535,3 +538,73 @@ def auto_mount_page_data(app: Any, service_name: str) -> None:
|
||||
methods=["GET"],
|
||||
)
|
||||
logger.info("Mounted page data endpoint for %s at /sx/data/<page_name>", service_name)
|
||||
|
||||
|
||||
def mount_io_endpoint(app: Any, service_name: str) -> None:
|
||||
"""Mount /sx/io/<name> endpoint for client-side IO primitive calls.
|
||||
|
||||
The client can call any allowed IO primitive or page helper via GET/POST.
|
||||
Result is returned as SX wire format (text/sx).
|
||||
|
||||
Falls back to page helpers when the name isn't a global IO primitive,
|
||||
so service-specific functions like ``highlight`` work via the proxy.
|
||||
"""
|
||||
import asyncio as _asyncio
|
||||
from quart import make_response, request, abort as quart_abort
|
||||
from .primitives_io import IO_PRIMITIVES, execute_io
|
||||
from .jinja_bridge import _get_request_context
|
||||
from .parser import serialize
|
||||
|
||||
# Allowlist of IO primitives + page helpers the client may call
|
||||
_ALLOWED_IO = {
|
||||
"highlight", "current-user", "request-arg", "request-path",
|
||||
"htmx-request?", "app-url", "asset-url", "config",
|
||||
}
|
||||
|
||||
async def io_proxy(name: str) -> Any:
|
||||
if name not in _ALLOWED_IO:
|
||||
quart_abort(403)
|
||||
|
||||
# Parse args from query string or JSON body
|
||||
args: list = []
|
||||
kwargs: dict = {}
|
||||
if request.method == "GET":
|
||||
for k, v in request.args.items():
|
||||
if k.startswith("_arg"):
|
||||
args.append(v)
|
||||
else:
|
||||
kwargs[k] = v
|
||||
else:
|
||||
data = await request.get_json(silent=True) or {}
|
||||
args = data.get("args", [])
|
||||
kwargs = data.get("kwargs", {})
|
||||
|
||||
# Try global IO primitives first
|
||||
if name in IO_PRIMITIVES:
|
||||
ctx = _get_request_context()
|
||||
result = await execute_io(name, args, kwargs, ctx)
|
||||
else:
|
||||
# Fall back to page helpers (service-specific functions)
|
||||
helpers = get_page_helpers(service_name)
|
||||
helper_fn = helpers.get(name)
|
||||
if helper_fn is None:
|
||||
quart_abort(404)
|
||||
result = helper_fn(*args, **kwargs) if kwargs else helper_fn(*args)
|
||||
if _asyncio.iscoroutine(result):
|
||||
result = await result
|
||||
|
||||
result_sx = serialize(result) if result is not None else "nil"
|
||||
resp = await make_response(result_sx, 200)
|
||||
resp.content_type = "text/sx; charset=utf-8"
|
||||
return resp
|
||||
|
||||
io_proxy.__name__ = "sx_io_proxy"
|
||||
io_proxy.__qualname__ = "sx_io_proxy"
|
||||
|
||||
app.add_url_rule(
|
||||
"/sx/io/<name>",
|
||||
endpoint="sx_io_proxy",
|
||||
view_func=io_proxy,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
logger.info("Mounted IO proxy endpoint for %s at /sx/io/<name>", service_name)
|
||||
|
||||
Reference in New Issue
Block a user