Stepper cookie persistence: SSR + client-side save/restore
- Parse Cookie header in OCaml HTTP server for get-cookie primitive - Stepper saves step-idx to cookie via host-set! FFI on click - Stepper restores from cookie: get-cookie on server, host-get FFI on client - Cache key includes stepper cookie value to avoid stale SSR - registerNative: also update Sx_primitives table for CALL_PRIM dispatch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1761,6 +1761,9 @@ let http_inject_shell_statics env static_dir sx_sxc =
|
|||||||
let templates_dir = project_dir ^ "/shared/sx/templates" in
|
let templates_dir = project_dir ^ "/shared/sx/templates" in
|
||||||
let client_libs = [
|
let client_libs = [
|
||||||
templates_dir ^ "/cssx.sx";
|
templates_dir ^ "/cssx.sx";
|
||||||
|
templates_dir ^ "/tw-layout.sx";
|
||||||
|
templates_dir ^ "/tw-type.sx";
|
||||||
|
templates_dir ^ "/tw.sx";
|
||||||
] in
|
] in
|
||||||
List.iter (fun path ->
|
List.iter (fun path ->
|
||||||
if Sys.file_exists path then begin
|
if Sys.file_exists path then begin
|
||||||
@@ -2593,6 +2596,21 @@ let http_mode port =
|
|||||||
_req_headers := parse_http_headers data;
|
_req_headers := parse_http_headers data;
|
||||||
_req_body := (if method_ = "POST" || method_ = "PUT" || method_ = "PATCH"
|
_req_body := (if method_ = "POST" || method_ = "PUT" || method_ = "PATCH"
|
||||||
then extract_body data else "");
|
then extract_body data else "");
|
||||||
|
(* Parse Cookie header into request_cookies for get-cookie primitive *)
|
||||||
|
Hashtbl.clear _request_cookies;
|
||||||
|
(match List.assoc_opt "cookie"
|
||||||
|
(List.map (fun (k,v) -> (String.lowercase_ascii k, v)) !_req_headers) with
|
||||||
|
| Some cookie_str ->
|
||||||
|
List.iter (fun pair ->
|
||||||
|
let trimmed = String.trim pair in
|
||||||
|
(match String.index_opt trimmed '=' with
|
||||||
|
| Some i ->
|
||||||
|
let k = String.sub trimmed 0 i in
|
||||||
|
let v = String.sub trimmed (i+1) (String.length trimmed - i - 1) in
|
||||||
|
Hashtbl.replace _request_cookies k v
|
||||||
|
| None -> ())
|
||||||
|
) (String.split_on_char ';' cookie_str)
|
||||||
|
| None -> ());
|
||||||
if path = "/" then begin
|
if path = "/" then begin
|
||||||
write_response fd (http_redirect "/sx/"); true
|
write_response fd (http_redirect "/sx/"); true
|
||||||
end else
|
end else
|
||||||
@@ -2736,7 +2754,10 @@ let http_mode port =
|
|||||||
in
|
in
|
||||||
write_response fd response; true
|
write_response fd response; true
|
||||||
end else if is_sx then begin
|
end else if is_sx then begin
|
||||||
let cache_key = if is_ajax then "ajax:" ^ path else path in
|
let has_stepper_cookie = Hashtbl.mem _request_cookies "sx-home-stepper" in
|
||||||
|
let cache_key = if is_ajax then "ajax:" ^ path
|
||||||
|
else if has_stepper_cookie then path ^ ":step=" ^ (try Hashtbl.find _request_cookies "sx-home-stepper" with Not_found -> "")
|
||||||
|
else path in
|
||||||
match Hashtbl.find_opt response_cache cache_key with
|
match Hashtbl.find_opt response_cache cache_key with
|
||||||
| Some cached -> write_response fd cached; true
|
| Some cached -> write_response fd cached; true
|
||||||
| None ->
|
| None ->
|
||||||
|
|||||||
@@ -401,6 +401,7 @@ let api_register_native name_js callback_js =
|
|||||||
js_to_value (Js.Unsafe.fun_call callback_js [| Js.Unsafe.inject (Js.array js_args) |])
|
js_to_value (Js.Unsafe.fun_call callback_js [| Js.Unsafe.inject (Js.array js_args) |])
|
||||||
in
|
in
|
||||||
let v = NativeFn (name, native_fn) in
|
let v = NativeFn (name, native_fn) in
|
||||||
|
Sx_primitives.register name native_fn;
|
||||||
ignore (env_bind global_env name v);
|
ignore (env_bind global_env name v);
|
||||||
Hashtbl.replace _vm_globals name v;
|
Hashtbl.replace _vm_globals name v;
|
||||||
Js.Unsafe.inject Js.null
|
Js.Unsafe.inject Js.null
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
~home/stepper
|
~home/stepper
|
||||||
()
|
()
|
||||||
(let
|
(let
|
||||||
((source "(div (~cssx/tw :tokens \"text-center\")\n (h1 (~cssx/tw :tokens \"text-3xl font-bold mb-2\")\n (span (~cssx/tw :tokens \"text-rose-500\") \"the \")\n (span (~cssx/tw :tokens \"text-amber-500\") \"joy \")\n (span (~cssx/tw :tokens \"text-emerald-500\") \"of \")\n (span (~cssx/tw :tokens \"text-violet-600 text-4xl\") \"sx\")))")
|
((source "(div (~tw :tokens \"text-center\")\n (h1 (~tw :tokens \"text-3xl font-bold mb-2\")\n (span (~tw :tokens \"text-rose-500\") \"the \")\n (span (~tw :tokens \"text-amber-500\") \"joy \")\n (span (~tw :tokens \"text-emerald-500\") \"of \")\n (span (~tw :tokens \"text-violet-600 text-4xl\") \"sx\")))")
|
||||||
(steps (signal (list)))
|
(steps (signal (list)))
|
||||||
(store (if (client?) (def-store "home-stepper" (fn () {:step-idx (signal 9)})) nil))
|
(store
|
||||||
|
(if (client?) (def-store "home-stepper" (fn () {:step-idx (signal 9)})) nil))
|
||||||
(step-idx (if store (get store "step-idx") (signal 9)))
|
(step-idx (if store (get store "step-idx") (signal 9)))
|
||||||
(dom-stack-sig (signal (list)))
|
(dom-stack-sig (signal (list)))
|
||||||
(code-tokens (signal (list))))
|
(code-tokens (signal (list))))
|
||||||
@@ -204,7 +205,7 @@
|
|||||||
(= (keyword-name (nth sp 1)) "tokens")
|
(= (keyword-name (nth sp 1)) "tokens")
|
||||||
(string? (nth sp 2)))
|
(string? (nth sp 2)))
|
||||||
(let
|
(let
|
||||||
((result (trampoline (~cssx/tw :tokens (nth sp 2)))))
|
((result (trampoline (~tw :tokens (nth sp 2)))))
|
||||||
(when
|
(when
|
||||||
(spread? result)
|
(spread? result)
|
||||||
(let
|
(let
|
||||||
@@ -239,7 +240,14 @@
|
|||||||
(= step-type "expr")
|
(= step-type "expr")
|
||||||
nil))
|
nil))
|
||||||
(swap! step-idx inc)
|
(swap! step-idx inc)
|
||||||
(update-code-highlight))))
|
(update-code-highlight)
|
||||||
|
(host-set!
|
||||||
|
(dom-document)
|
||||||
|
"cookie"
|
||||||
|
(str
|
||||||
|
"sx-home-stepper="
|
||||||
|
(deref step-idx)
|
||||||
|
";path=/;max-age=31536000;SameSite=Lax")))))
|
||||||
(rebuild-preview
|
(rebuild-preview
|
||||||
(fn
|
(fn
|
||||||
(target)
|
(target)
|
||||||
@@ -266,16 +274,22 @@
|
|||||||
(rebuild-preview target)
|
(rebuild-preview target)
|
||||||
(reset! step-idx target)
|
(reset! step-idx target)
|
||||||
(update-code-highlight)
|
(update-code-highlight)
|
||||||
)))))
|
(host-set!
|
||||||
|
(dom-document)
|
||||||
|
"cookie"
|
||||||
|
(str
|
||||||
|
"sx-home-stepper="
|
||||||
|
target
|
||||||
|
";path=/;max-age=31536000;SameSite=Lax")))))))
|
||||||
(let
|
(let
|
||||||
((saved (get-cookie "sx-home-stepper")))
|
((saved (if (client?) (let ((raw (host-get (dom-document) "cookie")) (prefix "sx-home-stepper=")) (when (and (string? raw) (contains? raw prefix)) (let ((start (+ (index-of raw prefix) (len prefix)))) (let ((rest (slice raw start)) (end-pos (index-of rest ";"))) (if (> end-pos -1) (slice rest 0 end-pos) rest))))) (get-cookie "sx-home-stepper"))))
|
||||||
(when
|
(when
|
||||||
saved
|
(string? saved)
|
||||||
(thaw-from-sx saved)
|
(let
|
||||||
(when
|
((n (parse-number saved)))
|
||||||
(or (< (deref step-idx) 0) (> (deref step-idx) 16))
|
(when
|
||||||
(reset! step-idx 9))))
|
(and (number? n) (>= n 0) (<= n 16))
|
||||||
|
(reset! step-idx n)))))
|
||||||
(let
|
(let
|
||||||
((parsed (sx-parse source)))
|
((parsed (sx-parse source)))
|
||||||
(when
|
(when
|
||||||
@@ -292,10 +306,10 @@
|
|||||||
(let
|
(let
|
||||||
((_eff (effect (fn () (schedule-idle (fn () (build-code-dom) (rebuild-preview (deref step-idx)) (update-code-highlight) (run-post-render-hooks)))))))
|
((_eff (effect (fn () (schedule-idle (fn () (build-code-dom) (rebuild-preview (deref step-idx)) (update-code-highlight) (run-post-render-hooks)))))))
|
||||||
(div
|
(div
|
||||||
:class "space-y-4"
|
(~tw :tokens "space-y-4 text-center")
|
||||||
(div
|
(div
|
||||||
:data-code-view true
|
:data-code-view true
|
||||||
(~cssx/tw
|
(~tw
|
||||||
:tokens "font-mono bg-stone-50 rounded p-2 overflow-x-auto leading-relaxed whitespace-pre-wrap")
|
:tokens "font-mono bg-stone-50 rounded p-2 overflow-x-auto leading-relaxed whitespace-pre-wrap")
|
||||||
:style "font-size:0.5rem"
|
:style "font-size:0.5rem"
|
||||||
(map
|
(map
|
||||||
@@ -319,12 +333,9 @@
|
|||||||
(span :class cls (get tok "text"))))
|
(span :class cls (get tok "text"))))
|
||||||
(deref code-tokens)))
|
(deref code-tokens)))
|
||||||
(div
|
(div
|
||||||
:class "flex items-center justify-center gap-2 md:gap-3"
|
(~tw :tokens "flex items-center justify-center gap-2 md:gap-3")
|
||||||
(button
|
(button
|
||||||
:on-click (fn
|
:on-click (fn (e) (do-back))
|
||||||
(e)
|
|
||||||
(do-back)
|
|
||||||
)
|
|
||||||
:class (str
|
:class (str
|
||||||
"px-2 py-1 rounded text-3xl "
|
"px-2 py-1 rounded text-3xl "
|
||||||
(if
|
(if
|
||||||
@@ -333,15 +344,12 @@
|
|||||||
"text-stone-300 cursor-not-allowed"))
|
"text-stone-300 cursor-not-allowed"))
|
||||||
"◀")
|
"◀")
|
||||||
(span
|
(span
|
||||||
:class "text-sm text-stone-500 font-mono tabular-nums"
|
(~tw :tokens "text-sm text-stone-500 font-mono tabular-nums")
|
||||||
(deref step-idx)
|
(deref step-idx)
|
||||||
" / "
|
" / "
|
||||||
(len (deref steps)))
|
(len (deref steps)))
|
||||||
(button
|
(button
|
||||||
:on-click (fn
|
:on-click (fn (e) (do-step))
|
||||||
(e)
|
|
||||||
(do-step)
|
|
||||||
)
|
|
||||||
:class (str
|
:class (str
|
||||||
"px-2 py-1 rounded text-3xl "
|
"px-2 py-1 rounded text-3xl "
|
||||||
(if
|
(if
|
||||||
|
|||||||
Reference in New Issue
Block a user