Cache sx component definitions in localStorage across page loads

Server computes SHA-256 hash of all component source at startup.
Client signals its cached hash via cookie (sx-comp-hash). On full
page load: cookie match → server sends empty script tag with just
the hash; mismatch → sends full source. Client loads from
localStorage on hit, parses inline + caches on miss.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 00:57:53 +00:00
parent 4ede0368dc
commit 5436dfe76c
3 changed files with 122 additions and 13 deletions

View File

@@ -21,6 +21,7 @@ Setup::
from __future__ import annotations
import glob
import hashlib
import os
from typing import Any
@@ -37,12 +38,42 @@ from .html import render as html_render, _render_component
# definition files or calling register_components().
_COMPONENT_ENV: dict[str, Any] = {}
# SHA-256 hash (12 hex chars) of all component definitions — used for
# client-side localStorage caching.
_COMPONENT_HASH: str = ""
def get_component_env() -> dict[str, Any]:
"""Return the shared component environment."""
return _COMPONENT_ENV
def get_component_hash() -> str:
"""Return the current component definitions hash."""
return _COMPONENT_HASH
def _compute_component_hash() -> None:
"""Recompute _COMPONENT_HASH from all registered Component definitions."""
global _COMPONENT_HASH
from .parser import serialize
parts = []
for key in sorted(_COMPONENT_ENV):
val = _COMPONENT_ENV[key]
if isinstance(val, Component):
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body)
parts.append(f"(defcomp ~{val.name} {params_sx} {body_sx})")
if parts:
digest = hashlib.sha256("\n".join(parts).encode()).hexdigest()[:12]
_COMPONENT_HASH = digest
else:
_COMPONENT_HASH = ""
def load_sx_dir(directory: str) -> None:
"""Load all .sx files from a directory and register components."""
for filepath in sorted(
@@ -132,6 +163,8 @@ def register_components(sx_source: str) -> None:
all_classes = scan_classes_from_sx(sx_source)
val.css_classes = set(all_classes)
_compute_component_hash()
# ---------------------------------------------------------------------------
# sx() — render s-expression from Jinja template