diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 9cab15d3..ec706aa3 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -18,123 +18,6 @@ open Sx_types -(* ====================================================================== *) -(* Font measurement via otfm — reads OpenType/TrueType font tables *) -(* ====================================================================== *) - -(* Font cache: font-family → (units_per_em, glyph_id_map, advance_width_map) *) -let _font_cache : (string, (int * (int, int) Hashtbl.t * (int, int) Hashtbl.t)) Hashtbl.t = Hashtbl.create 4 - -(* Map font-family names to file paths *) -let _font_base = ref "shared/static/fonts" - -let font_path_for family = - let base = !_font_base in - let try_paths paths = - List.find_opt Sys.file_exists paths in - match String.lowercase_ascii family with - | "serif" | "times" | "times new roman" -> - try_paths [base ^ "/DejaVuSerif.ttf"; - "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf"] - | "sans-serif" | "sans" | "arial" | "helvetica" -> - try_paths [base ^ "/DejaVuSans.ttf"; - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"] - | "monospace" | "mono" | "courier" -> - try_paths [base ^ "/DejaVuSansMono-Bold.ttf"; - "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"] - | path when Sys.file_exists path -> Some path - | _ -> try_paths [base ^ "/DejaVuSerif.ttf"; - "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf"] - -(* Load a font file and extract cmap + hmtx tables *) -let load_font family = - match Hashtbl.find_opt _font_cache family with - | Some cached -> Some cached - | None -> - match font_path_for family with - | None -> None - | Some path -> - try - let ic = open_in_bin path in - let n = in_channel_length ic in - let buf = Bytes.create n in - really_input ic buf 0 n; - close_in ic; - let s = Bytes.to_string buf in - let d = Otfm.decoder (`String s) in - (* Get units_per_em from head table *) - let upm = match Otfm.head d with - | Error _ -> 2048 - | Ok h -> h.Otfm.head_units_per_em in - (* Build char→glyph_id map from cmap. - cmap folds: (acc -> map_kind -> (u0,u1) -> glyph_id -> acc) *) - let cmap_tbl = Hashtbl.create 256 in - let _ = Otfm.cmap d (fun acc kind (u0, u1) gid -> - (match kind with - | `Glyph -> - (* Single mapping: u0 -> gid (u0 = u1) *) - Hashtbl.replace cmap_tbl u0 gid - | `Glyph_range -> - (* Range: u0..u1 -> gid..(gid + u1 - u0) *) - for i = 0 to u1 - u0 do - Hashtbl.replace cmap_tbl (u0 + i) (gid + i) - done); - acc) () in - (* Build glyph_id→advance_width map from hmtx. - hmtx folds: (acc -> glyph_id -> adv_width -> lsb -> acc) *) - let hmtx_tbl = Hashtbl.create 256 in - let _ = Otfm.hmtx d (fun acc gid adv _lsb -> - Hashtbl.replace hmtx_tbl gid adv; acc) () in - let result = (upm, cmap_tbl, hmtx_tbl) in - Printf.eprintf "[font] loaded %s: %d glyphs, %d metrics, upm=%d\n%!" - (Filename.basename path) (Hashtbl.length cmap_tbl) (Hashtbl.length hmtx_tbl) upm; - Hashtbl.replace _font_cache family result; - Some result - with e -> - Printf.eprintf "[font] error loading %s: %s\n%!" path (Printexc.to_string e); - None - -(* Measure text width using font tables *) -let measure_text_otfm family size text = - match load_font family with - | None -> - (* Fallback to monospace approximation *) - let w = size *. 0.6 *. (float_of_int (String.length text)) in - (w, size, size *. 0.8, size *. 0.2) - | Some (upm, cmap_tbl, hmtx_tbl) -> - let scale = size /. (float_of_int upm) in - let width = ref 0.0 in - (* Iterate over UTF-8 codepoints *) - let i = ref 0 in - let len = String.length text in - while !i < len do - let byte = Char.code text.[!i] in - let cp, advance = - if byte < 0x80 then (byte, 1) - else if byte < 0xC0 then (byte, 1) (* continuation — skip *) - else if byte < 0xE0 then - ((byte land 0x1F) lsl 6 lor (Char.code text.[min (!i+1) (len-1)] land 0x3F), 2) - else if byte < 0xF0 then - ((byte land 0x0F) lsl 12 - lor (Char.code text.[min (!i+1) (len-1)] land 0x3F) lsl 6 - lor (Char.code text.[min (!i+2) (len-1)] land 0x3F), 3) - else - ((byte land 0x07) lsl 18 - lor (Char.code text.[min (!i+1) (len-1)] land 0x3F) lsl 12 - lor (Char.code text.[min (!i+2) (len-1)] land 0x3F) lsl 6 - lor (Char.code text.[min (!i+3) (len-1)] land 0x3F), 4) - in - let gid = match Hashtbl.find_opt cmap_tbl cp with - | Some g -> g | None -> 0 in - let adv = match Hashtbl.find_opt hmtx_tbl gid with - | Some a -> a | None -> upm / 2 in - width := !width +. (float_of_int adv) *. scale; - i := !i + advance - done; - let ascent = size *. 0.8 in (* approximate — could read OS/2 table *) - let descent = size *. 0.2 in - (!width, size, ascent, descent) - (* ====================================================================== *) (* Output helpers *) @@ -188,56 +71,6 @@ let rec serialize_value = function "(make-spread {" ^ String.concat " " items ^ "})" | _ -> "nil" -(** Collect all ~-prefixed symbol references from an AST value. - Walks the tree recursively, returns a deduplicated list of symbol names - like ["~card"; "~layout/base"]. Used for dependency analysis. *) -let collect_tilde_refs body = - let seen = Hashtbl.create 16 in - let rec walk = function - | Symbol s when String.length s > 0 && s.[0] = '~' -> - if not (Hashtbl.mem seen s) then Hashtbl.replace seen s () - | List items | ListRef { contents = items } -> - List.iter walk items - | Dict d -> - Hashtbl.iter (fun _k v -> walk v) d - | Spread pairs -> - List.iter (fun (_k, v) -> walk v) pairs - | _ -> () - in - walk body; - Hashtbl.fold (fun k () acc -> k :: acc) seen [] - -(** Serialize a value to SX text, replacing ~-prefixed symbol references - with their content hashes from the index. Symbols not in the index - are emitted verbatim (unknown ref or non-component symbol). *) -let rec serialize_value_hashed (index : (string, string) Hashtbl.t) = function - | Nil -> "nil" - | Bool true -> "true" - | Bool false -> "false" - | Number n -> - if Float.is_integer n then string_of_int (int_of_float n) - else Printf.sprintf "%g" n - | String s -> "\"" ^ escape_sx_string s ^ "\"" - | Symbol s when String.length s > 0 && s.[0] = '~' -> - (match Hashtbl.find_opt index s with - | Some h -> "@h:" ^ h - | None -> s) - | Symbol s -> s - | Keyword k -> ":" ^ k - | List items | ListRef { contents = items } -> - "(" ^ String.concat " " (List.map (serialize_value_hashed index) items) ^ ")" - | Dict d -> - let pairs = Hashtbl.fold (fun k v acc -> - (Printf.sprintf ":%s %s" k (serialize_value_hashed index v)) :: acc) d [] in - "{" ^ String.concat " " (List.sort String.compare pairs) ^ "}" - | RawHTML s -> "\"" ^ escape_sx_string s ^ "\"" - | SxExpr s -> s - | Spread pairs -> - let items = List.map (fun (k, v) -> - Printf.sprintf ":%s %s" k (serialize_value_hashed index v)) pairs in - "(make-spread {" ^ String.concat " " items ^ "})" - | _ -> "nil" - (** Request epoch — monotonically increasing, set by (epoch N) from Python. All responses are tagged with the current epoch so Python can discard stale messages from previous requests. Makes pipe desync impossible. *) @@ -316,11 +149,6 @@ let _scope_stacks = Sx_primitives._scope_stacks (* Populated from __app-config dict after SX files load. *) let _app_config : (string, value) Hashtbl.t option ref = ref None let _defpage_paths : string list ref = ref [] -(* Streaming pages: path → page name, for pages with :stream true *) -let _streaming_pages : (string, string) Hashtbl.t = Hashtbl.create 8 -(* Mutex to serialize streaming renders — OCaml threads share the runtime - lock, and concurrent CEK evaluations corrupt shared state. *) -let _stream_mutex = Mutex.create () let get_app_config key default = match !_app_config with @@ -487,12 +315,12 @@ let resolve_library_path lib_spec = The file should contain a define-library form that registers itself. *) let _import_env : env option ref = ref None -let rec load_library_file path = - (* Use eval_expr_io for IO-aware loading (handles nested imports) *) +let load_library_file path = + (* Use eval_expr which has the cek_run import patch — handles nested imports *) let env = match !_import_env with Some e -> e | None -> Sx_types.make_env () in let exprs = Sx_parser.parse_file path in List.iter (fun expr -> - try ignore (eval_expr_io expr (Env env)) + try ignore (Sx_ref.eval_expr expr (Env env)) with Eval_error msg -> Printf.eprintf "[load-library] %s: %s\n%!" (Filename.basename path) msg ) exprs @@ -500,7 +328,7 @@ let rec load_library_file path = (** IO-aware CEK run — handles suspension by dispatching IO requests. Import requests are handled locally (load .sx file). Other IO requests are sent to the Python bridge. *) -and cek_run_with_io state = +let cek_run_with_io state = let s = ref state in let is_terminal s = match Sx_ref.cek_terminal_p s with Bool true -> true | _ -> false in let is_suspended s = match Sx_runtime.get_val s (String "phase") with String "io-suspended" -> true | _ -> false in @@ -527,24 +355,6 @@ and cek_run_with_io state = (Sx_runtime.value_to_str lib_spec)); Nil end - | "text-measure" -> - let args = let a = Sx_runtime.get_val request (String "args") in - (match a with List l -> l | _ -> [a]) in - let font = match args with String f :: _ -> f | _ -> "serif" in - let size = match args with - | [_font; Number sz; _text] -> sz - | [_font; Number sz] -> sz - | _ -> 16.0 in - let text = match args with - | [_font; _sz; String t] -> t - | _ -> "" in - let (w, h, asc, desc) = measure_text_otfm font size text in - let d = Hashtbl.create 4 in - Hashtbl.replace d "width" (Number w); - Hashtbl.replace d "height" (Number h); - Hashtbl.replace d "ascent" (Number asc); - Hashtbl.replace d "descent" (Number desc); - Dict d | _ -> let args = let a = Sx_runtime.get_val request (String "args") in (match a with List l -> l | _ -> [a]) in @@ -558,7 +368,7 @@ and cek_run_with_io state = loop () (** IO-aware eval_expr — like eval_expr but handles IO suspension. *) -and eval_expr_io expr env = +let _eval_expr_io expr env = let state = Sx_ref.make_cek_state expr env (List []) in cek_run_with_io state @@ -1133,16 +943,7 @@ let register_jit_hook env = (try Some (Sx_vm.call_closure cl args cl.vm_env_ref) with | Sx_vm.VmSuspended (request, saved_vm) -> - (* Try inline IO resolution; fall back to suspend marker *) - (match !Sx_types._cek_io_resolver with - | Some resolver -> - let rec resolve_loop req vm = - let result = resolver req (Nil) in - (try Some (Sx_vm.resume_vm vm result) - with Sx_vm.VmSuspended (req2, vm2) -> resolve_loop req2 vm2) - in - resolve_loop request saved_vm - | None -> Some (make_vm_suspend_marker request saved_vm)) + Some (make_vm_suspend_marker request saved_vm) | e -> let fn_name = match l.l_name with Some n -> n | None -> "?" in if not (Hashtbl.mem _jit_warned fn_name) then begin @@ -1167,15 +968,7 @@ let register_jit_hook env = (try Some (Sx_vm.call_closure cl args cl.vm_env_ref) with | Sx_vm.VmSuspended (request, saved_vm) -> - (match !Sx_types._cek_io_resolver with - | Some resolver -> - let rec resolve_loop req vm = - let result = resolver req (Nil) in - (try Some (Sx_vm.resume_vm vm result) - with Sx_vm.VmSuspended (req2, vm2) -> resolve_loop req2 vm2) - in - resolve_loop request saved_vm - | None -> Some (make_vm_suspend_marker request saved_vm)) + Some (make_vm_suspend_marker request saved_vm) | e -> Printf.eprintf "[jit] %s first-call fallback to CEK: %s\n%!" fn_name (Printexc.to_string e); Hashtbl.replace _jit_warned fn_name true; @@ -1201,41 +994,6 @@ let rebind_host_extensions env = | _ -> raise (Eval_error "register-special-form!: expected (name handler)"))); ignore (env_bind env "*custom-special-forms*" Sx_ref.custom_special_forms) -(* Path-based naming for unnamed definitions *) -(* ====================================================================== *) - -let def_keywords = ["defcomp"; "defisland"; "defmacro"; "define"; - "defhandler"; "defstyle"; "deftype"; "defeffect"; - "defrelation"; "deftest"; "defpage"] - -(* Inject path-derived name into unnamed definitions. - (defcomp (params) body) -> (defcomp ~path/name (params) body) - Only applied when base_dir is provided (service components). *) -let inject_path_name expr path base_dir = - match expr with - | List (Symbol kw :: rest) when List.mem kw def_keywords -> - (match rest with - | Symbol _ :: _ -> expr (* Already named *) - | _ -> - (* Unnamed — derive name from file path relative to base_dir *) - let rel = if String.length path > String.length base_dir + 1 - then String.sub path (String.length base_dir + 1) - (String.length path - String.length base_dir - 1) - else Filename.basename path in - let stem = if Filename.check_suffix rel ".sx" - then String.sub rel 0 (String.length rel - 3) - else rel in - (* index files are known by their directory *) - let name = if Filename.basename stem = "index" - then let d = Filename.dirname stem in - if d = "." then "index" else d - else stem in - (* Components/islands get ~ prefix *) - let prefixed = if kw = "defcomp" || kw = "defisland" - then "~" ^ name else name in - List (Symbol kw :: Symbol prefixed :: rest)) - | _ -> expr - (* Command dispatch *) (* ====================================================================== *) @@ -1244,20 +1002,14 @@ let rec dispatch env cmd = | List [Symbol "ping"] -> send_ok_string "ocaml-cek" - | List [Symbol "load"; String path] - | List [Symbol "load"; String path; String _] -> - let base_dir = match cmd with - | List [_; _; String b] -> b | _ -> "" in + | List [Symbol "load"; String path] -> (try let exprs = Sx_parser.parse_file path in let prev_file = if Sx_types.env_has env "*current-file*" then Some (Sx_types.env_get env "*current-file*") else None in ignore (Sx_types.env_bind env "*current-file*" (String path)); let count = ref 0 in List.iter (fun expr -> - let expr' = if base_dir <> "" then inject_path_name expr path base_dir else expr in - (try ignore (eval_expr_io expr' (Env env)) - with Eval_error msg -> - Printf.eprintf "[load] %s: %s\n%!" (Filename.basename path) msg); + ignore (Sx_ref.eval_expr expr (Env env)); incr count ) exprs; (* Rebind host extension points after .sx load — evaluator.sx @@ -1988,32 +1740,6 @@ let http_response ?(status=200) ?(content_type="text/html; charset=utf-8") body let http_redirect url = Printf.sprintf "HTTP/1.1 301 Moved Permanently\r\nLocation: %s\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n" url -(* Chunked transfer encoding helpers for streaming responses *) -let http_chunked_header ?(status=200) ?(content_type="text/html; charset=utf-8") () = - let status_text = match status with - | 200 -> "OK" | 404 -> "Not Found" | 500 -> "Internal Server Error" | _ -> "Unknown" in - Printf.sprintf "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\nX-Accel-Buffering: no\r\nCache-Control: no-cache, no-transform\r\n\r\n" - status status_text content_type - -let write_chunk fd data = - if String.length data > 0 then begin - let chunk = Printf.sprintf "%x\r\n%s\r\n" (String.length data) data in - let bytes = Bytes.of_string chunk in - let total = Bytes.length bytes in - let written = ref 0 in - try - while !written < total do - let n = Unix.write fd bytes !written (total - !written) in - written := !written + n - done; - true - with Unix.Unix_error _ -> false - end else true - -let end_chunked fd = - (try ignore (Unix.write_substring fd "0\r\n\r\n" 0 5) with Unix.Unix_error _ -> ()); - (try Unix.close fd with _ -> ()) - let parse_http_request data = match String.index_opt data '\r' with | None -> (match String.index_opt data '\n' with @@ -2060,48 +1786,6 @@ let parse_http_headers data = ) (match lines with _ :: rest -> rest | [] -> []); !headers -(* IO-aware eval for rendering — handles perform (text-measure, sleep, import). - Used by aser and SSR so components can call measure-text via perform. *) -let eval_with_io_render expr env = - let state = ref (Sx_ref.cek_step_loop (Sx_ref.make_cek_state expr (Env env) Nil)) in - while sx_truthy (Sx_ref.cek_suspended_p !state) do - let request = Sx_ref.cek_io_request !state in - let op = match request with - | Dict d -> (match Hashtbl.find_opt d "op" with Some (String s) -> s | Some (Symbol s) -> s | _ -> "") - | _ -> "" in - let args = match request with - | Dict d -> (match Hashtbl.find_opt d "args" with Some v -> v | None -> Nil) - | _ -> Nil in - let result = match op with - | "text-measure" -> - let font = match args with - | List (String f :: _) -> f | _ -> "serif" in - let size = match args with - | List [_font; Number sz; _text] -> sz - | List [_font; Number sz] -> sz - | _ -> 16.0 in - let text = match args with - | List [_font; _sz; String t] -> t - | _ -> "" in - let (w, h, asc, desc) = measure_text_otfm font size text in - let d = Hashtbl.create 4 in - Hashtbl.replace d "width" (Number w); - Hashtbl.replace d "height" (Number h); - Hashtbl.replace d "ascent" (Number asc); - Hashtbl.replace d "descent" (Number desc); - Dict d - | "io-sleep" | "sleep" -> - let ms = match args with - | List (Number n :: _) -> n | Number n -> n | _ -> 0.0 in - Unix.sleepf (ms /. 1000.0); Nil - | "import" -> Nil - | _ -> Nil - in - state := Sx_ref.cek_step_loop (Sx_ref.cek_resume !state result) - done; - if sx_truthy (Sx_ref.cek_terminal_p !state) then Sx_ref.cek_value !state - else Nil - (** Render a page. Routing + AJAX detection in SX (request-handler.sx), render pipeline (aser → SSR → shell) in OCaml for reliable env access. *) let http_render_page env path headers = @@ -2153,7 +1837,7 @@ let http_render_page env path headers = (* AJAX: return SX wire format (aser output) with text/sx content type *) let body_result = let call = List [Symbol "aser"; List [Symbol "quote"; wrapped]; Env env] in - eval_with_io_render call env in + Sx_ref.eval_expr call (Env env) in let body_str = match body_result with | String s | SxExpr s -> s | _ -> serialize_value body_result in let t1 = Unix.gettimeofday () in @@ -2167,7 +1851,7 @@ let http_render_page env path headers = let t1 = Unix.gettimeofday () in let body_result = let call = List [Symbol "aser"; List [Symbol "quote"; full_ast]; Env env] in - eval_with_io_render call env in + Sx_ref.eval_expr call (Env env) in let body_str = match body_result with | String s | SxExpr s -> s | _ -> serialize_value body_result in let t2 = Unix.gettimeofday () in @@ -2177,7 +1861,7 @@ let http_render_page env path headers = if env_has env "render-to-html" then let render_call = List [Symbol "render-to-html"; List [Symbol "quote"; body_expr]; Env env] in - (match eval_with_io_render render_call env with + (match Sx_ref.eval_expr render_call (Env env) with | String s | RawHTML s -> s | v -> Sx_runtime.value_to_str v) else Sx_render.sx_render_to_html env body_expr env with e -> Printf.eprintf "[http-ssr] failed for %s: %s\n%!" path (Printexc.to_string e); "" in @@ -2189,7 +1873,6 @@ let http_render_page env path headers = Keyword "body-html"; String body_html; Keyword "component-defs"; get_shell "component-defs"; Keyword "component-hash"; get_shell "component-hash"; - Keyword "component-manifest"; get_shell "component-manifest"; Keyword "pages-sx"; get_shell "pages-sx"; Keyword "sx-css"; get_shell "sx-css"; Keyword "asset-url"; get_shell "asset-url"; @@ -2217,297 +1900,6 @@ let http_render_page env path headers = end end -(* JSON-encode a string for use in __sxResolve script tags *) -let json_encode_string s = - let buf = Buffer.create (String.length s + 16) in - Buffer.add_char buf '"'; - String.iter (fun c -> match c with - | '"' -> Buffer.add_string buf "\\\"" - | '\\' -> Buffer.add_string buf "\\\\" - | '\n' -> Buffer.add_string buf "\\n" - | '\r' -> Buffer.add_string buf "\\r" - | '\t' -> Buffer.add_string buf "\\t" - | c when Char.code c < 0x20 -> - Buffer.add_string buf (Printf.sprintf "\\u%04x" (Char.code c)) - | c -> Buffer.add_char buf c - ) s; - Buffer.add_char buf '"'; - Buffer.contents buf - -(* Bootstrap script that queues resolves arriving before sx.js loads. - Must match _SX_STREAMING_BOOTSTRAP in shared/sx/helpers.py *) -let _sx_streaming_bootstrap = - "" - -(* Build a resolve script tag. Must match _SX_STREAMING_RESOLVE in helpers.py *) -let sx_streaming_resolve_script id sx_source = - Printf.sprintf "" - (json_encode_string id) (json_encode_string sx_source) - -(* ====================================================================== *) -(* IO-resolving evaluator for streaming render *) -(* ====================================================================== *) - -(* Evaluate an expression with IO suspension handling. - Steps the CEK machine; when it suspends on an IO request, resolves - the request locally and resumes. Supports: io-sleep, import, helper. *) -let eval_with_io expr env = - let state = ref (Sx_ref.cek_step_loop (Sx_ref.make_cek_state expr (Env env) Nil)) in - while sx_truthy (Sx_ref.cek_suspended_p !state) do - let request = Sx_ref.cek_io_request !state in - let op = match request with - | Dict d -> (match Hashtbl.find_opt d "op" with Some (String s) -> s | Some (Symbol s) -> s | _ -> "") - | _ -> "" in - let args = match request with - | Dict d -> (match Hashtbl.find_opt d "args" with Some v -> v | None -> Nil) - | _ -> Nil in - let result = match op with - | "io-sleep" | "sleep" -> - let ms = match args with - | List (Number n :: _) -> n - | Number n -> n - | _ -> 0.0 in - Unix.sleepf (ms /. 1000.0); - Nil - | "import" -> - (* Library import — delegate to existing import hook *) - (try - let lib_name = match request with - | Dict d -> (match Hashtbl.find_opt d "library" with Some v -> v | _ -> args) - | _ -> args in - ignore lib_name; (* import handled by _import_hook if registered *) - Nil - with _ -> Nil) - | "text-measure" -> - let font = match args with - | List (String f :: _) -> f | _ -> "serif" in - let size = match args with - | List [_font; Number sz; _text] -> sz - | List [_font; Number sz] -> sz - | _ -> 16.0 in - let text = match args with - | List [_font; _sz; String t] -> t - | _ -> "" in - let (w, h, asc, desc) = measure_text_otfm font size text in - let d = Hashtbl.create 4 in - Hashtbl.replace d "width" (Number w); - Hashtbl.replace d "height" (Number h); - Hashtbl.replace d "ascent" (Number asc); - Hashtbl.replace d "descent" (Number desc); - Dict d - | _ -> - Printf.eprintf "[io] unhandled IO op: %s\n%!" op; - Nil - in - state := Sx_ref.cek_step_loop (Sx_ref.cek_resume !state result) - done; - if sx_truthy (Sx_ref.cek_terminal_p !state) then Sx_ref.cek_value !state - else Nil - -(* ====================================================================== *) -(* Streaming page render — shell-first with chunked transfer encoding *) -(* ====================================================================== *) - -let http_render_page_streaming env path _headers fd page_name = - (* No send timeout for streaming — the alive check in write_chunk handles - broken pipe. Streaming clients may be slow to receive large shell chunks - while busy parsing/downloading other resources. *) - (try Unix.setsockopt_float fd Unix.SO_SNDTIMEO 30.0 with _ -> ()); - let t0 = Unix.gettimeofday () in - let page_def = try - match env_get env ("page:" ^ page_name) with Dict d -> d | _ -> raise Not_found - with _ -> - Printf.eprintf "[sx-stream] page def not found: page:%s\n%!" page_name; - let err = http_response ~status:500 "