diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 76b9a793..ca2608a8 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -2099,16 +2099,17 @@ let http_render_page_streaming env path _headers fd page_name = let t3_data = Unix.gettimeofday () in (* 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")] - | List 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 + | List items -> extract_items items + | ListRef { contents = items } -> extract_items items | _ -> Printf.eprintf "[sx-stream] :data returned %s, expected dict or list\n%!" (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 *) 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 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 | Dict _ -> [(data_result, "stream-content")] - | List 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 + | List items -> extract_sid items + | ListRef { contents = items } -> extract_sid items | _ -> [] in let buf = Buffer.create 1024 in List.iter (fun (item, stream_id) -> diff --git a/tests/playwright/streaming.spec.js b/tests/playwright/streaming.spec.js index 6dbe2777..e84fde61 100644 --- a/tests/playwright/streaming.spec.js +++ b/tests/playwright/streaming.spec.js @@ -263,6 +263,34 @@ test.describe('Streaming sandbox', () => { 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 }) => { const errors = await bootSandbox(page); expect(errors).toEqual([]);