Fix ListRef handling in streaming data — list from SX is ListRef in OCaml
The streaming render matched `List items` but SX's `(list ...)` produces `ListRef` (mutable list) in the OCaml runtime. Data items were rejected with "returned list, expected dict or list" — 0 resolve chunks sent. Fixed both streaming render and AJAX paths to handle ListRef. Added sandbox test for streaming-demo-data return type validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2099,16 +2099,17 @@ let http_render_page_streaming env path _headers fd page_name =
|
|||||||
let t3_data = Unix.gettimeofday () in
|
let t3_data = Unix.gettimeofday () in
|
||||||
|
|
||||||
(* Determine single-stream vs multi-stream *)
|
(* Determine single-stream vs multi-stream *)
|
||||||
let data_items = match data_result with
|
let data_items =
|
||||||
|
let extract_items items = List.map (fun item ->
|
||||||
|
let stream_id = match item with
|
||||||
|
| Dict d -> (match Hashtbl.find_opt d "stream-id" with
|
||||||
|
| Some (String s) -> s | _ -> "stream-content")
|
||||||
|
| _ -> "stream-content" in
|
||||||
|
(item, stream_id)) items in
|
||||||
|
match data_result with
|
||||||
| Dict _ -> [(data_result, "stream-content")]
|
| Dict _ -> [(data_result, "stream-content")]
|
||||||
| List items ->
|
| List items -> extract_items items
|
||||||
List.map (fun item ->
|
| ListRef { contents = items } -> extract_items items
|
||||||
let stream_id = match item with
|
|
||||||
| Dict d -> (match Hashtbl.find_opt d "stream-id" with
|
|
||||||
| Some (String s) -> s | _ -> "stream-content")
|
|
||||||
| _ -> "stream-content" in
|
|
||||||
(item, stream_id)
|
|
||||||
) items
|
|
||||||
| _ ->
|
| _ ->
|
||||||
Printf.eprintf "[sx-stream] :data returned %s, expected dict or list\n%!"
|
Printf.eprintf "[sx-stream] :data returned %s, expected dict or list\n%!"
|
||||||
(Sx_runtime.type_of data_result |> Sx_runtime.value_to_str);
|
(Sx_runtime.type_of data_result |> Sx_runtime.value_to_str);
|
||||||
@@ -3366,12 +3367,14 @@ let http_mode port =
|
|||||||
(* If we have data+content, resolve all slots and embed as OOB swaps *)
|
(* If we have data+content, resolve all slots and embed as OOB swaps *)
|
||||||
let resolve_oob = if data_ast <> Nil && content_ast <> Nil then begin
|
let resolve_oob = if data_ast <> Nil && content_ast <> Nil then begin
|
||||||
let data_result = try Sx_ref.eval_expr data_ast (Env env) with _ -> Nil in
|
let data_result = try Sx_ref.eval_expr data_ast (Env env) with _ -> Nil in
|
||||||
|
let extract_sid items = List.map (fun item ->
|
||||||
|
let sid = match item with Dict d ->
|
||||||
|
(match Hashtbl.find_opt d "stream-id" with Some (String s) -> s | _ -> "stream-content")
|
||||||
|
| _ -> "stream-content" in (item, sid)) items in
|
||||||
let data_items = match data_result with
|
let data_items = match data_result with
|
||||||
| Dict _ -> [(data_result, "stream-content")]
|
| Dict _ -> [(data_result, "stream-content")]
|
||||||
| List items -> List.map (fun item ->
|
| List items -> extract_sid items
|
||||||
let sid = match item with Dict d ->
|
| ListRef { contents = items } -> extract_sid items
|
||||||
(match Hashtbl.find_opt d "stream-id" with Some (String s) -> s | _ -> "stream-content")
|
|
||||||
| _ -> "stream-content" in (item, sid)) items
|
|
||||||
| _ -> [] in
|
| _ -> [] in
|
||||||
let buf = Buffer.create 1024 in
|
let buf = Buffer.create 1024 in
|
||||||
List.iter (fun (item, stream_id) ->
|
List.iter (fun (item, stream_id) ->
|
||||||
|
|||||||
@@ -263,6 +263,34 @@ test.describe('Streaming sandbox', () => {
|
|||||||
await expect(page.locator('[data-suspense="stream-slow"]')).toContainText('Slow resolved!');
|
await expect(page.locator('[data-suspense="stream-slow"]')).toContainText('Slow resolved!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('streaming-demo-data returns iterable list of dicts with stream-ids', async ({ page }) => {
|
||||||
|
const errors = await bootSandbox(page);
|
||||||
|
expect(errors).toEqual([]);
|
||||||
|
|
||||||
|
// Verify the data function returns a list that can be iterated
|
||||||
|
// (catches ListRef vs List type mismatch)
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const K = window.SxKernel;
|
||||||
|
try {
|
||||||
|
const type = K.eval('(type-of (streaming-demo-data))');
|
||||||
|
const len = K.eval('(len (streaming-demo-data))');
|
||||||
|
const ids = K.eval('(join "," (map (fn (item) (get item "stream-id")) (streaming-demo-data)))');
|
||||||
|
const delays = K.eval('(join "," (map (fn (item) (str (get item "delay"))) (streaming-demo-data)))');
|
||||||
|
return { type, len, ids, delays };
|
||||||
|
} catch(e) {
|
||||||
|
return { error: e.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.error || '').toBe('');
|
||||||
|
expect(result.type).toBe('list');
|
||||||
|
expect(result.len).toBe(3);
|
||||||
|
expect(result.ids).toContain('stream-fast');
|
||||||
|
expect(result.ids).toContain('stream-medium');
|
||||||
|
expect(result.ids).toContain('stream-slow');
|
||||||
|
expect(result.delays).toContain('1000');
|
||||||
|
});
|
||||||
|
|
||||||
test('streaming shell renders with outer layout gutters', async ({ page }) => {
|
test('streaming shell renders with outer layout gutters', async ({ page }) => {
|
||||||
const errors = await bootSandbox(page);
|
const errors = await bootSandbox(page);
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user