Isomorphic cookie support + stepper cookie persistence

get-cookie / set-cookie primitives on both server and client:
  - JS: reads/writes document.cookie
  - OCaml: get-cookie reads from _request_cookies hashtable,
    set-cookie is no-op (server sets cookies via HTTP headers)
  - Python bridge: _inject_request_cookies_locked() sends
    (set-request-cookies {:name "val"}) to kernel before page render

Stepper island (home-stepper.sx):
  - Persistence switched from localStorage to cookie (sx-home-stepper)
  - freeze-scope/thaw-from-sx mechanism preserved, just different storage
  - Server reads cookie → thaw restores step-idx → SSR renders correct step
  - Code highlighting: removed imperative code-spans/build-code-dom/
    update-code-highlight; replaced with live DOM query that survives morphs
  - Removed code-view lake wrapper (now plain reactive DOM)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 04:13:53 +00:00
parent e021184935
commit bf305deae1
5 changed files with 120 additions and 96 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-24T02:56:45Z";
var SX_VERSION = "2026-03-24T04:10:36Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -555,6 +555,18 @@
lst[idx] = val;
return NIL;
};
// Cookie access — isomorphic state persistence
PRIMITIVES["set-cookie"] = function(name, value, days) {
var d = days || 365;
var expires = new Date(Date.now() + d * 864e5).toUTCString();
document.cookie = name + "=" + encodeURIComponent(value) + ";expires=" + expires + ";path=/;SameSite=Lax";
return NIL;
};
PRIMITIVES["get-cookie"] = function(name) {
var m = document.cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]*)"));
return m ? decodeURIComponent(m[1]) : NIL;
};
PRIMITIVES["env-parent"] = function(env) {
if (env && Object.getPrototypeOf(env) !== Object.prototype &&
Object.getPrototypeOf(env) !== null)

View File

@@ -205,6 +205,27 @@ class OcamlBridge:
self._shell_statics_injected = True
_logger.info("Injected shell statics into OCaml kernel")
async def _inject_request_cookies_locked(self) -> None:
"""Send current request cookies to kernel for get-cookie primitive."""
try:
from quart import request
cookies = request.cookies
except Exception:
return # no request context (CLI mode, tests)
if not cookies:
return
# Build SX dict: {:name1 "val1" :name2 "val2"}
pairs = []
for k, v in cookies.items():
pairs.append(f':{k} "{_escape(str(v))}"')
if pairs:
cmd = f'(set-request-cookies {{{" ".join(pairs)}}})'
try:
await self._send(cmd)
await self._read_until_ok(ctx=None)
except OcamlBridgeError as e:
_logger.debug("Cookie inject failed: %s", e)
async def sx_page_full(
self,
page_source: str,
@@ -221,6 +242,8 @@ class OcamlBridge:
async with self._lock:
await self._inject_helpers_locked()
await self._inject_shell_statics_locked()
# Send request cookies so get-cookie works during SSR
await self._inject_request_cookies_locked()
# Large/complex blobs use placeholders — OCaml renders the shell
# with short tokens; Python splices in the real values post-render.
# This avoids piping large strings or strings with special chars