@marimo-team/frontend 0.25.3-dev125 → 0.25.3-dev129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{CellStatus-_Aw9_foI.js → CellStatus-6xDEj3Hs.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-BRgqbAsc.js → ConnectedDataExplorerComponent-DCcLAktu.js} +1 -1
- package/dist/assets/{ErrorBoundary-DxSzjvbx.js → ErrorBoundary-98Xo04DS.js} +2 -2
- package/dist/assets/{JsonOutput-B7pGj79O.js → JsonOutput-V47aiTIK.js} +5 -5
- package/dist/assets/{LazyAnyLanguageCodeMirror-BPYE76jR.js → LazyAnyLanguageCodeMirror-DoxWX_Wp.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-Bzo6Bkp5.js → MarimoErrorOutput-DeWZNBgL.js} +1 -1
- package/dist/assets/{RenderHTML-D6AC0IhC.js → RenderHTML-Bjvcs70-.js} +1 -1
- package/dist/assets/{RunButton-BjYsPiKa.js → RunButton-CnvvXD9d.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CFrR1iW4.js → add-cell-with-ai-BbuqzITi.js} +1 -1
- package/dist/assets/{add-connection-dialog-CyNAB_W2.js → add-connection-dialog-DIC7owvZ.js} +1 -1
- package/dist/assets/{agent-panel-VOrccb5q.js → agent-panel-D8hO2P1x.js} +1 -1
- package/dist/assets/{ai-model-dropdown-Bfxv_zvu.js → ai-model-dropdown-DgmiG_5j.js} +1 -1
- package/dist/assets/alert-dialog-8YiYLrfr.js +1 -0
- package/dist/assets/{any-language-editor-DUMe1UCk.js → any-language-editor-BWmz0P_C.js} +1 -1
- package/dist/assets/{app-config-button-BRcjv8A_.js → app-config-button-UZHv1Iim.js} +1 -1
- package/dist/assets/{blob-DTYOX-m5.js → blob-BPknzzkY.js} +1 -1
- package/dist/assets/{button-D8gVKwe-.js → button-Cl7haVNh.js} +1 -1
- package/dist/assets/{cache-panel-BV-c7ed0.js → cache-panel-DuBsA6oO.js} +1 -1
- package/dist/assets/{capabilities-Pq1YNPlK.js → capabilities-DcB-Dd0T.js} +1 -1
- package/dist/assets/{cell-editor-eKgYK8Bd.js → cell-editor-CM-K1_wq.js} +1 -1
- package/dist/assets/{cell-link-D3rkqgbU.js → cell-link-CAjuFcLK.js} +1 -1
- package/dist/assets/cells-BypTg035.js +220 -0
- package/dist/assets/{chat-display-BrT4VJJK.js → chat-display-BRLNOoTF.js} +1 -1
- package/dist/assets/chat-panel-D02Rk9we.js +4 -0
- package/dist/assets/{chat-ui-SAdJxg-k.js → chat-ui-DFN_RwxF.js} +1 -1
- package/dist/assets/circle-B0vOU-Dh.js +1 -0
- package/dist/assets/{code-visibility-DYDagVy7.js → code-visibility-D1Wf7-vD.js} +1 -1
- package/dist/assets/{column-preview-yI7-INkZ.js → column-preview-85ORMhfB.js} +1 -1
- package/dist/assets/{command-palette-DF-B_YpW.js → command-palette-CtyjjdsA.js} +1 -1
- package/dist/assets/{common-DgXKwblm.js → common-J265II7i.js} +1 -1
- package/dist/assets/{components-DCo7mAWO.js → components-9p_w2C6e.js} +1 -1
- package/dist/assets/{components-CBE8cOLK.js → components-B6gDDccL.js} +1 -1
- package/dist/assets/{components-CuMhY4Sf.js → components-Bd3Dpi92.js} +1 -1
- package/dist/assets/{config-CD1IuZFJ.js → config-7IjqNuVp.js} +1 -1
- package/dist/assets/config-BC32AAdw.js +1 -0
- package/dist/assets/{config-DpY86_Yq.js → config-CXnC3MRv.js} +1 -1
- package/dist/assets/{copy-KlhazlhB.js → copy-74EjuZso.js} +1 -1
- package/dist/assets/{copy-icon-8-a5vovc.js → copy-icon-Craaocbu.js} +1 -1
- package/dist/assets/{createReducer-CaI_Oa0v.js → createReducer-B04CIO-e.js} +1 -1
- package/dist/assets/{datasource-D9JMcEAP.js → datasource-Dy4rzzSP.js} +1 -1
- package/dist/assets/{dates-CQMwFko2.js → dates-CaPZVGtm.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BVJWPCvw.js → dependency-graph-panel-ByGFFT4E.js} +1 -1
- package/dist/assets/dist-gaK_BThd.js +1 -0
- package/dist/assets/{documentation-panel--wI4XHso.js → documentation-panel-LxuJCY3M.js} +1 -1
- package/dist/assets/download-BsBJWH_j.js +9 -0
- package/dist/assets/dropdown-menu-Cwi17BoX.js +1 -0
- package/dist/assets/{edit-page-Uj9YSRMX.js → edit-page--d6X-OO9.js} +4 -4
- package/dist/assets/{error-banner-B5kzVf_0.js → error-banner-m3mastc-.js} +1 -1
- package/dist/assets/{error-entries-D00nWhWE.js → error-entries-0wTSSuK9.js} +1 -1
- package/dist/assets/{error-panel-CcN_1PWf.js → error-panel-ZNdbE3Fo.js} +1 -1
- package/dist/assets/{es-N9SHpmG7.js → es-Rg27gAEy.js} +1 -1
- package/dist/assets/feedback-button-Bt89tGG2.js +1 -0
- package/dist/assets/{feedback-button-ByGgqUsX.js → feedback-button-C6asPmsh.js} +1 -1
- package/dist/assets/{file-explorer-panel-eTEqtMPW.js → file-explorer-panel-0fUP_ptL.js} +1 -1
- package/dist/assets/{file-name-input-D5SfTNWh.js → file-name-input-B8lzI5iS.js} +1 -1
- package/dist/assets/{floating-outline-B-k3P_AO.js → floating-outline-CRpmOv32.js} +1 -1
- package/dist/assets/{formats-zylfwejq.js → formats-CYyvbiZA.js} +1 -1
- package/dist/assets/{formatting-uom1es4b.js → formatting-DW2u7HXI.js} +1 -1
- package/dist/assets/fullscreen-D-ldV-19.js +41 -0
- package/dist/assets/{gallery-page-BzbMhDHi.js → gallery-page-ClR8U-km.js} +1 -1
- package/dist/assets/{glide-data-editor-BMqhUrHI.js → glide-data-editor-B6QQBQwS.js} +1 -1
- package/dist/assets/{glide-utils-HFqhvWHI.js → glide-utils-DsTeJ7WA.js} +1 -1
- package/dist/assets/{home-page-DJcYWiga.js → home-page-CJnqDcT9.js} +1 -1
- package/dist/assets/{hooks-CGLb_CCz.js → hooks-CCek4i9f.js} +1 -1
- package/dist/assets/{html-to-image-D_0rd3_B.js → html-to-image-BW0fcUS8.js} +1 -1
- package/dist/assets/{index-bSIORW11.js → index-B5pWkt7a.js} +18 -18
- package/dist/assets/{input-DmHMEZGh.js → input-DLA5Dhn3.js} +2 -2
- package/dist/assets/{kiosk-mode-DFS6HopF.js → kiosk-mode-Cbxffiz9.js} +1 -1
- package/dist/assets/{layout-C2XnLAfO.js → layout-66Urh4Zg.js} +3 -3
- package/dist/assets/links-UwsB2RA1.js +1 -0
- package/dist/assets/{logs-panel-B3T1GFBk.js → logs-panel-DFhkIcge.js} +1 -1
- package/dist/assets/{markdown-renderer-BhiTvxtL.js → markdown-renderer-mYYQYD2m.js} +1 -1
- package/dist/assets/menu-items-yD7IV3V4.js +1 -0
- package/dist/assets/{mermaid-BKXlWk4F.js → mermaid-DmekSy8J.js} +1 -1
- package/dist/assets/{name-cell-input-CgAObR6N.js → name-cell-input-Crcu3i2z.js} +1 -1
- package/dist/assets/{numbers-CsZVw3Dc.js → numbers-DJK15_4S.js} +1 -1
- package/dist/assets/{outline-panel-BhBb7Vqw.js → outline-panel-BFIPK0H_.js} +1 -1
- package/dist/assets/{packages-panel-Bt0xNpFl.js → packages-panel-B4KtjgkF.js} +1 -1
- package/dist/assets/{panel-accordion-state-ZsUdpAYn.js → panel-accordion-state-D6KtwSF8.js} +1 -1
- package/dist/assets/panels-DiwGKPzK.js +1 -0
- package/dist/assets/process-output-DDvMd6Jh.js +1 -0
- package/dist/assets/{readonly-code-display-TvOC7ESE.js → readonly-code-display-VDqYn2d0.js} +1 -1
- package/dist/assets/{readonly-python-code-Cqkfdeyo.js → readonly-python-code-CZUKLXJ4.js} +1 -1
- package/dist/assets/{renderShortcut-rUwGqKgC.js → renderShortcut-Bxv9ZLnD.js} +1 -1
- package/dist/assets/{reveal-component-CwE6U9oJ.js → reveal-component-BMmDU1uN.js} +1 -1
- package/dist/assets/{run-page-CtonWTtJ.js → run-page-BmOPPh2Y.js} +1 -1
- package/dist/assets/{runs-C49hleBy.js → runs-C8uOPNAW.js} +1 -1
- package/dist/assets/{runtime-details-state-CKqJSfSR.js → runtime-details-state-CipqynUD.js} +1 -1
- package/dist/assets/{scratchpad-panel-CwVBedfZ.js → scratchpad-panel-Drr_kuwb.js} +1 -1
- package/dist/assets/{scroll-to-bottom-button-CIkON2WJ.js → scroll-to-bottom-button-CidjyqiH.js} +1 -1
- package/dist/assets/{secrets-panel-bcfEKxRx.js → secrets-panel-D-W1PL76.js} +1 -1
- package/dist/assets/select-CgWQvW1F.js +1 -0
- package/dist/assets/{session-CMLIzZKw.js → session-BHgGkjDm.js} +1 -1
- package/dist/assets/{session-panel-CdHdKUCF.js → session-panel-W7JOcog_.js} +1 -1
- package/dist/assets/{snippets-panel-DYLWOLYw.js → snippets-panel-cUPEdm66.js} +1 -1
- package/dist/assets/{spec-DlH4Mp7d.js → spec-DFtwOyXT.js} +1 -1
- package/dist/assets/{state-DgECPVFs.js → state-BLGvzMB6.js} +3 -3
- package/dist/assets/{state-B_MCKzBL.js → state-BYETGwDZ.js} +1 -1
- package/dist/assets/{state-3mcyTS9q.js → state-CZu2E2Lb.js} +1 -1
- package/dist/assets/{state-C5WAeuTu.js → state-ClBFtZxn.js} +1 -1
- package/dist/assets/{state-Cv2JW3l9.js → state-CrLrURYG.js} +1 -1
- package/dist/assets/{state-DZN9vZ6z.js → state-QfGh3m2G.js} +1 -1
- package/dist/assets/{strings-C25fhMR_.js → strings-3lNX212T.js} +1 -1
- package/dist/assets/{swiper-component-CRGRy5_O.js → swiper-component-BwGv-Pu1.js} +1 -1
- package/dist/assets/switch-D2gdC_ep.js +1 -0
- package/dist/assets/tabs-BLczoY3k.js +1 -0
- package/dist/assets/{terminal-Ca-kubKb.js → terminal-DLgGyFIP.js} +1 -1
- package/dist/assets/toast-trUlkvx3.js +1 -0
- package/dist/assets/tooltip-B5lAA_jn.js +1 -0
- package/dist/assets/{tooltip-Brya4Ghn.js → tooltip-BiQdpXA0.js} +1 -1
- package/dist/assets/{tracing-B0J2ctTm.js → tracing-BNFynaTu.js} +1 -1
- package/dist/assets/{tracing-panel-C1OCpoeE.js → tracing-panel-CsHuzRGP.js} +2 -2
- package/dist/assets/{tree-actions-sjBxIglc.js → tree-actions-BQw_utcj.js} +1 -1
- package/dist/assets/use-restore-focus-DMSzeuUr.js +1 -0
- package/dist/assets/{useAddCell-CaXnFGki.js → useAddCell-_b8InOZC.js} +1 -1
- package/dist/assets/{useBoolean-Dd2nWXFJ.js → useBoolean-BkeUcn1n.js} +1 -1
- package/dist/assets/{useCellActionButton-CJ90CMEj.js → useCellActionButton-BbgMzBdk.js} +1 -1
- package/dist/assets/{useDeleteCell-D9N85822.js → useDeleteCell-DhoFbEkU.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CzAE0cm-.js → useDependencyPanelTab-COTULhoH.js} +1 -1
- package/dist/assets/{useEventListener-CjDeLqFC.js → useEventListener-DPOTkksd.js} +1 -1
- package/dist/assets/{useIframeCapabilities-Czr9e4uG.js → useIframeCapabilities-sHeZt_EA.js} +1 -1
- package/dist/assets/{useInstallPackage-wcDq9hYT.js → useInstallPackage-BfNKN33w.js} +1 -1
- package/dist/assets/{useInterval-DJdQmXnq.js → useInterval-BcdwHHB5.js} +1 -1
- package/dist/assets/{useNotebookActions-CAMEQ38y.js → useNotebookActions-xnA_l3nY.js} +1 -1
- package/dist/assets/{useRunCells-DGakx8hj.js → useRunCells-VBCe6CLn.js} +1 -1
- package/dist/assets/{useSplitCell-AK5BKPGM.js → useSplitCell-AvSpVAmq.js} +1 -1
- package/dist/assets/{useTheme-BAA27lh7.js → useTheme-C2dnh2vM.js} +1 -1
- package/dist/assets/{vega-component-BeyxAJad.js → vega-component-DJf6ho22.js} +1 -1
- package/dist/assets/{write-secret-modal-BEx-EUlh.js → write-secret-modal-CBi2vNK_.js} +1 -1
- package/dist/index.html +62 -62
- package/package.json +3 -3
- package/src/core/host/__tests__/notebook-client.test.ts +18 -6
- package/src/core/host/__tests__/static.test.ts +4 -2
- package/src/core/host/__tests__/wasm.test.ts +12 -10
- package/src/core/host/backend.ts +8 -3
- package/src/core/host/notebook-client.ts +15 -15
- package/src/core/host/static.ts +2 -2
- package/src/core/host/wasm.ts +4 -3
- package/src/core/runtime/__tests__/runtime.test.ts +17 -0
- package/src/core/runtime/runtime.ts +13 -0
- package/src/core/websocket/__tests__/backend.test.ts +111 -0
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +32 -27
- package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +7 -4
- package/src/core/websocket/backend.ts +68 -0
- package/src/core/websocket/transports/__tests__/notebook.test.ts +9 -56
- package/src/core/websocket/transports/notebook.ts +15 -38
- package/src/core/websocket/useNotebookClient.ts +6 -6
- package/src/core/websocket/useNotebookSession.tsx +3 -2
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +66 -66
- package/dist/assets/alert-dialog-C-sWmKva.js +0 -1
- package/dist/assets/cells-2HETnlvE.js +0 -220
- package/dist/assets/chat-panel-qpPNEwxV.js +0 -4
- package/dist/assets/circle-B6D_6D8w.js +0 -1
- package/dist/assets/config-DMz7oN0r.js +0 -1
- package/dist/assets/dist-jlhMTKo_.js +0 -1
- package/dist/assets/download-Dod42wfD.js +0 -9
- package/dist/assets/dropdown-menu-DS6umAnH.js +0 -1
- package/dist/assets/feedback-button-tbWwJ_t6.js +0 -1
- package/dist/assets/fullscreen-BWTs8pYZ.js +0 -41
- package/dist/assets/links-DX2w97N9.js +0 -1
- package/dist/assets/menu-items-BgUyNUpu.js +0 -1
- package/dist/assets/panels-DJghvIKo.js +0 -1
- package/dist/assets/process-output-CeOMwE17.js +0 -1
- package/dist/assets/select-3CVGlYrC.js +0 -1
- package/dist/assets/switch-DYjBfpNJ.js +0 -1
- package/dist/assets/tabs-CY8E4r9U.js +0 -1
- package/dist/assets/toast-Gzxi-le_.js +0 -1
- package/dist/assets/tooltip-CkGm8X7m.js +0 -1
- package/dist/assets/use-restore-focus-DdS7Tl0F.js +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { NotebookEvents } from "@marimo-team/client";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { createEditorBackend } from "../backend";
|
|
6
|
+
|
|
7
|
+
const wasm = vi.hoisted(() => ({ enabled: false, consumer: vi.fn() }));
|
|
8
|
+
vi.mock("../../wasm/utils", () => ({ isWasm: () => wasm.enabled }));
|
|
9
|
+
vi.mock("../../wasm/bridge", () => ({
|
|
10
|
+
PyodideBridge: { INSTANCE: { attachMessageConsumer: wasm.consumer } },
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
wasm.enabled = false;
|
|
15
|
+
wasm.consumer.mockReset();
|
|
16
|
+
vi.unstubAllGlobals();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("createEditorBackend", () => {
|
|
20
|
+
const options = {
|
|
21
|
+
static: false,
|
|
22
|
+
transportType: "websocket" as const,
|
|
23
|
+
url: () => "ws://localhost/ws",
|
|
24
|
+
headers: () => ({}),
|
|
25
|
+
notebookUrl: (id: string) =>
|
|
26
|
+
new URL(`http://localhost/api/marimo/v1/notebooks/${id}`),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it.each([
|
|
30
|
+
["a static notebook", { ...options, static: true }, "idle"],
|
|
31
|
+
["WASM", options, "wasm"],
|
|
32
|
+
["SSE", { ...options, transportType: "sse" as const }, "fetch"],
|
|
33
|
+
["the default", options, "socket"],
|
|
34
|
+
] as const)("follows %s", async (_name, chosen, expected) => {
|
|
35
|
+
wasm.enabled = expected === "wasm";
|
|
36
|
+
const fetch = vi.fn(() => new Promise<Response>(() => undefined));
|
|
37
|
+
const socket = vi.fn();
|
|
38
|
+
vi.stubGlobal("fetch", fetch);
|
|
39
|
+
vi.stubGlobal(
|
|
40
|
+
"WebSocket",
|
|
41
|
+
class extends EventTarget {
|
|
42
|
+
public constructor(url: URL) {
|
|
43
|
+
super();
|
|
44
|
+
socket(url);
|
|
45
|
+
}
|
|
46
|
+
public close() {}
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
const events = new NotebookEvents(createEditorBackend(chosen).transport);
|
|
50
|
+
await vi.waitFor(() =>
|
|
51
|
+
expect(
|
|
52
|
+
{
|
|
53
|
+
idle:
|
|
54
|
+
events.status === "connected" && !wasm.consumer.mock.calls.length,
|
|
55
|
+
wasm: wasm.consumer.mock.calls.length === 1,
|
|
56
|
+
fetch: fetch.mock.calls.length === 1,
|
|
57
|
+
socket: socket.mock.calls.length === 1,
|
|
58
|
+
}[expected],
|
|
59
|
+
).toBe(true),
|
|
60
|
+
);
|
|
61
|
+
events.close();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("edits a reported notebook on its host with the session's headers", async () => {
|
|
65
|
+
const fetch = vi.fn(
|
|
66
|
+
async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
67
|
+
new Response(
|
|
68
|
+
JSON.stringify({ type: "conflict", title: "Conflict", status: 409 }),
|
|
69
|
+
{
|
|
70
|
+
status: 409,
|
|
71
|
+
headers: { "Content-Type": "application/problem+json" },
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
vi.stubGlobal("fetch", fetch);
|
|
76
|
+
const backend = createEditorBackend({
|
|
77
|
+
...options,
|
|
78
|
+
headers: () => ({
|
|
79
|
+
Authorization: "Bearer editor-token",
|
|
80
|
+
"Marimo-Server-Token": "server-token",
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
const result = await backend
|
|
84
|
+
.notebook("n1")
|
|
85
|
+
.edit([], { idempotencyKey: "edit-1" });
|
|
86
|
+
expect(result).toMatchObject({ ok: false, status: 409 });
|
|
87
|
+
const [[input, init]] = fetch.mock.calls;
|
|
88
|
+
expect(String(input)).toBe(
|
|
89
|
+
"http://localhost/api/marimo/v1/notebooks/n1/edits",
|
|
90
|
+
);
|
|
91
|
+
const headers = new Headers(init?.headers);
|
|
92
|
+
expect(init?.method).toBe("POST");
|
|
93
|
+
expect(headers.get("Authorization")).toBe("Bearer editor-token");
|
|
94
|
+
expect(headers.get("Marimo-Server-Token")).toBe("server-token");
|
|
95
|
+
expect(headers.get("Idempotency-Key")).toBe("edit-1");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it.each([
|
|
99
|
+
["a static notebook", { ...options, static: true }, false],
|
|
100
|
+
["WASM", options, true],
|
|
101
|
+
] as const)("does not edit %s", async (_name, chosen, isWasm) => {
|
|
102
|
+
wasm.enabled = isWasm;
|
|
103
|
+
const fetch = vi.fn();
|
|
104
|
+
vi.stubGlobal("fetch", fetch);
|
|
105
|
+
const result = await createEditorBackend(chosen)
|
|
106
|
+
.notebook("n1")
|
|
107
|
+
.edit([], { idempotencyKey: "edit-1" });
|
|
108
|
+
expect(result).toMatchObject({ ok: false, status: 501 });
|
|
109
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -8,13 +8,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
8
8
|
import type { NotebookTransport } from "@marimo-team/client";
|
|
9
9
|
import { ConnectionState } from "../types";
|
|
10
10
|
import { Deferred } from "@/utils/Deferred";
|
|
11
|
-
import {
|
|
11
|
+
import type { NotebookBackend } from "../../host/backend";
|
|
12
|
+
import { createEditorBackend } from "../backend";
|
|
12
13
|
import { useNotebookClient } from "../useNotebookClient";
|
|
13
14
|
import { fixtureMessages } from "../../host/__tests__/fixtures";
|
|
14
15
|
import { agentAttachmentsAtom } from "../../attachments/state";
|
|
15
16
|
import { notebookClientAtom } from "../state";
|
|
16
17
|
|
|
17
|
-
vi.mock("../
|
|
18
|
+
vi.mock("../backend", () => ({ createEditorBackend: vi.fn() }));
|
|
19
|
+
|
|
20
|
+
/** A backend that follows `transport` and cannot edit. */
|
|
21
|
+
function backend(transport: NotebookTransport): NotebookBackend {
|
|
22
|
+
return { transport, notebook: () => ({ edit: vi.fn() }) };
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
/** A transport whose connections the test drives. */
|
|
20
26
|
function makeTransport() {
|
|
@@ -53,6 +59,7 @@ function setup(
|
|
|
53
59
|
transportType: "websocket" as const,
|
|
54
60
|
url: () => "ws://localhost/ws",
|
|
55
61
|
headers: () => ({}),
|
|
62
|
+
notebookUrl: (id: string) => new URL(`http://localhost/notebooks/${id}`),
|
|
56
63
|
waitUntilReady: async () => undefined,
|
|
57
64
|
checkHealth: async () => true,
|
|
58
65
|
onNotification: vi.fn(),
|
|
@@ -70,7 +77,7 @@ function setup(
|
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
beforeEach(() => {
|
|
73
|
-
vi.mocked(
|
|
80
|
+
vi.mocked(createEditorBackend).mockReset();
|
|
74
81
|
});
|
|
75
82
|
|
|
76
83
|
describe("useNotebookClient", () => {
|
|
@@ -78,16 +85,15 @@ describe("useNotebookClient", () => {
|
|
|
78
85
|
"rechecks health after failed startup (healthy: %s)",
|
|
79
86
|
async (healthy) => {
|
|
80
87
|
const fake = makeTransport();
|
|
81
|
-
vi.mocked(
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
},
|
|
88
|
+
vi.mocked(createEditorBackend).mockImplementation(({ url }) =>
|
|
89
|
+
backend(async (attempt, handlers) => {
|
|
90
|
+
if (typeof url === "function") {
|
|
91
|
+
await url();
|
|
92
|
+
}
|
|
93
|
+
if (!attempt.signal.aborted) {
|
|
94
|
+
return fake.transport(attempt, handlers);
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
91
97
|
);
|
|
92
98
|
const waitUntilReady = vi
|
|
93
99
|
.fn()
|
|
@@ -125,14 +131,13 @@ describe("useNotebookClient", () => {
|
|
|
125
131
|
it("waits for initial readiness without reviving an unmounted connection", async () => {
|
|
126
132
|
const ready = new Deferred<void>();
|
|
127
133
|
const resolvedUrl = vi.fn();
|
|
128
|
-
vi.mocked(
|
|
129
|
-
(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
},
|
|
134
|
+
vi.mocked(createEditorBackend).mockImplementation(({ url }) =>
|
|
135
|
+
backend(async () => {
|
|
136
|
+
if (typeof url === "function") {
|
|
137
|
+
await url();
|
|
138
|
+
resolvedUrl();
|
|
139
|
+
}
|
|
140
|
+
}),
|
|
136
141
|
);
|
|
137
142
|
const { result, store, unmount } = setup(false, {
|
|
138
143
|
waitUntilReady: () => ready.promise,
|
|
@@ -152,7 +157,7 @@ describe("useNotebookClient", () => {
|
|
|
152
157
|
|
|
153
158
|
it("exposes host presence to the UI without waiting for kernel-ready", async () => {
|
|
154
159
|
const fake = makeTransport();
|
|
155
|
-
vi.mocked(
|
|
160
|
+
vi.mocked(createEditorBackend).mockReturnValue(backend(fake.transport));
|
|
156
161
|
const { result, store, unmount } = setup();
|
|
157
162
|
await act(async () => Promise.resolve());
|
|
158
163
|
expect(store.get(notebookClientAtom)).toBe(result.current);
|
|
@@ -177,7 +182,7 @@ describe("useNotebookClient", () => {
|
|
|
177
182
|
|
|
178
183
|
it("uses current callbacks and URL providers without reconnecting on a rerender", async () => {
|
|
179
184
|
const fake = makeTransport();
|
|
180
|
-
vi.mocked(
|
|
185
|
+
vi.mocked(createEditorBackend).mockReturnValue(backend(fake.transport));
|
|
181
186
|
const { rerender, options, unmount } = setup();
|
|
182
187
|
await act(async () => Promise.resolve());
|
|
183
188
|
const onNotification = vi.fn();
|
|
@@ -187,8 +192,8 @@ describe("useNotebookClient", () => {
|
|
|
187
192
|
expect(onNotification).toHaveBeenCalledExactlyOnceWith({
|
|
188
193
|
data: { op: "reconnected" },
|
|
189
194
|
});
|
|
190
|
-
expect(
|
|
191
|
-
const [{ url }] = vi.mocked(
|
|
195
|
+
expect(createEditorBackend).toHaveBeenCalledOnce();
|
|
196
|
+
const [{ url }] = vi.mocked(createEditorBackend).mock.calls[0];
|
|
192
197
|
expect(String(await (url as () => Promise<string>)())).toBe(
|
|
193
198
|
"ws://redirected/ws",
|
|
194
199
|
);
|
|
@@ -197,10 +202,10 @@ describe("useNotebookClient", () => {
|
|
|
197
202
|
|
|
198
203
|
it("closes every stream under Strict Mode and only follows the active lifetime", async () => {
|
|
199
204
|
const fakes: ReturnType<typeof makeTransport>[] = [];
|
|
200
|
-
vi.mocked(
|
|
205
|
+
vi.mocked(createEditorBackend).mockImplementation(() => {
|
|
201
206
|
const next = makeTransport();
|
|
202
207
|
fakes.push(next);
|
|
203
|
-
return next.transport;
|
|
208
|
+
return backend(next.transport);
|
|
204
209
|
});
|
|
205
210
|
const { result, store, options, unmount } = setup(true);
|
|
206
211
|
await act(async () => Promise.resolve());
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
vi,
|
|
22
22
|
} from "vitest";
|
|
23
23
|
|
|
24
|
-
vi.mock("../
|
|
25
|
-
|
|
24
|
+
vi.mock("../backend", () => ({
|
|
25
|
+
createEditorBackend: vi.fn(),
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
28
|
vi.mock("@/core/runtime/config", async () => {
|
|
@@ -51,7 +51,7 @@ import { generateSessionId } from "../../kernel/session";
|
|
|
51
51
|
import { RuntimeManager } from "../../runtime/runtime";
|
|
52
52
|
import { WebSocketClosedReason, ConnectionState } from "../types";
|
|
53
53
|
import { classifyCloseEvent } from "../connection-state";
|
|
54
|
-
import {
|
|
54
|
+
import { createEditorBackend } from "../backend";
|
|
55
55
|
import { useNotebookSession } from "../useNotebookSession";
|
|
56
56
|
import { type NotebookTransport, StreamStopped } from "@marimo-team/client";
|
|
57
57
|
|
|
@@ -85,7 +85,10 @@ type FakeTransport = ReturnType<typeof makeTransport>;
|
|
|
85
85
|
|
|
86
86
|
/** Use a fake transport for the next session the hook follows. */
|
|
87
87
|
function withTransport(fake: FakeTransport = makeTransport()) {
|
|
88
|
-
vi.mocked(
|
|
88
|
+
vi.mocked(createEditorBackend).mockReturnValue({
|
|
89
|
+
transport: fake.transport,
|
|
90
|
+
notebook: () => ({ edit: vi.fn() }),
|
|
91
|
+
});
|
|
89
92
|
return fake;
|
|
90
93
|
}
|
|
91
94
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { NotebookClient, type StreamUrl } from "@marimo-team/client";
|
|
4
|
+
import type { NotebookId } from "@marimo-team/client/protocol";
|
|
5
|
+
import type { NotebookBackend } from "../host/backend";
|
|
6
|
+
import { PyodideBridge } from "../wasm/bridge";
|
|
7
|
+
import { isWasm } from "../wasm/utils";
|
|
8
|
+
import {
|
|
9
|
+
idle,
|
|
10
|
+
pyodide,
|
|
11
|
+
sessionSse,
|
|
12
|
+
websocket,
|
|
13
|
+
withHeaders,
|
|
14
|
+
} from "./transports/notebook";
|
|
15
|
+
|
|
16
|
+
/** How the editor follows its notebook. */
|
|
17
|
+
export interface EditorBackendOptions {
|
|
18
|
+
static: boolean;
|
|
19
|
+
transportType: "websocket" | "sse";
|
|
20
|
+
/** The session route, read before each connection. */
|
|
21
|
+
url: StreamUrl;
|
|
22
|
+
/** The runtime's headers, read before each request. */
|
|
23
|
+
headers: () => Record<string, string>;
|
|
24
|
+
/** The host URL of a notebook the stream reported. */
|
|
25
|
+
notebookUrl: (notebookId: NotebookId) => URL;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Commands for a notebook the editor cannot change through its backend. */
|
|
29
|
+
function uneditable(): ReturnType<NotebookBackend["notebook"]> {
|
|
30
|
+
return {
|
|
31
|
+
edit: async () => ({
|
|
32
|
+
ok: false,
|
|
33
|
+
status: 501,
|
|
34
|
+
problem: {
|
|
35
|
+
type: "tag:marimo.io,2026:host/unsupported-operation",
|
|
36
|
+
title: "Document editing is unavailable on this connection",
|
|
37
|
+
status: 501,
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Choose the backend for the editor's notebook. */
|
|
44
|
+
export function createEditorBackend(
|
|
45
|
+
options: EditorBackendOptions,
|
|
46
|
+
): NotebookBackend {
|
|
47
|
+
if (options.static) {
|
|
48
|
+
return { transport: idle, notebook: uneditable };
|
|
49
|
+
}
|
|
50
|
+
if (isWasm()) {
|
|
51
|
+
return {
|
|
52
|
+
transport: pyodide((consumer) =>
|
|
53
|
+
PyodideBridge.INSTANCE.attachMessageConsumer(consumer),
|
|
54
|
+
),
|
|
55
|
+
notebook: uneditable,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
transport:
|
|
60
|
+
options.transportType === "sse"
|
|
61
|
+
? sessionSse(options.url, options.headers)
|
|
62
|
+
: websocket(options.url),
|
|
63
|
+
notebook: (id) =>
|
|
64
|
+
new NotebookClient(options.notebookUrl(id), {
|
|
65
|
+
fetch: withHeaders(options.headers),
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -11,18 +11,7 @@ import { createNotebookClient } from "../../../host/notebook-client";
|
|
|
11
11
|
import { fixtureMessages } from "../../../host/__tests__/fixtures";
|
|
12
12
|
import { ConnectionState } from "../../../host/connection";
|
|
13
13
|
import { Deferred } from "@/utils/Deferred";
|
|
14
|
-
import {
|
|
15
|
-
createNotebookTransport,
|
|
16
|
-
pyodide,
|
|
17
|
-
sessionSse,
|
|
18
|
-
websocket,
|
|
19
|
-
} from "../notebook";
|
|
20
|
-
|
|
21
|
-
const wasm = vi.hoisted(() => ({ enabled: false, consumer: vi.fn() }));
|
|
22
|
-
vi.mock("../../../wasm/utils", () => ({ isWasm: () => wasm.enabled }));
|
|
23
|
-
vi.mock("../../../wasm/bridge", () => ({
|
|
24
|
-
PyodideBridge: { INSTANCE: { attachMessageConsumer: wasm.consumer } },
|
|
25
|
-
}));
|
|
14
|
+
import { pyodide, sessionSse, websocket } from "../notebook";
|
|
26
15
|
|
|
27
16
|
const encoder = new TextEncoder();
|
|
28
17
|
|
|
@@ -83,14 +72,14 @@ function network(transportType: "websocket" | "sse", url: StreamUrl) {
|
|
|
83
72
|
);
|
|
84
73
|
const count = () =>
|
|
85
74
|
transportType === "websocket" ? FakeSocket.opened.length : requests.length;
|
|
86
|
-
const transport =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
75
|
+
const transport =
|
|
76
|
+
transportType === "websocket"
|
|
77
|
+
? websocket(url)
|
|
78
|
+
: sessionSse(url, () => ({}));
|
|
79
|
+
const backend = { transport, notebook: () => ({ edit: vi.fn() }) };
|
|
92
80
|
let index = 0;
|
|
93
81
|
return {
|
|
82
|
+
backend,
|
|
94
83
|
transport,
|
|
95
84
|
count,
|
|
96
85
|
opened: async () => {
|
|
@@ -126,8 +115,6 @@ function network(transportType: "websocket" | "sse", url: StreamUrl) {
|
|
|
126
115
|
|
|
127
116
|
afterEach(() => {
|
|
128
117
|
FakeSocket.opened.length = 0;
|
|
129
|
-
wasm.enabled = false;
|
|
130
|
-
wasm.consumer.mockReset();
|
|
131
118
|
vi.unstubAllGlobals();
|
|
132
119
|
vi.useRealTimers();
|
|
133
120
|
});
|
|
@@ -313,7 +300,7 @@ describe.each(["websocket", "sse"] as const)(
|
|
|
313
300
|
const lifetime = new AbortController();
|
|
314
301
|
onTestFinished(() => lifetime.abort());
|
|
315
302
|
connect({
|
|
316
|
-
|
|
303
|
+
backend: connection.backend,
|
|
317
304
|
signal: lifetime.signal,
|
|
318
305
|
checkHealth: async () => true,
|
|
319
306
|
onNotification: vi.fn(),
|
|
@@ -380,7 +367,7 @@ describe.each(["websocket", "sse"] as const)(
|
|
|
380
367
|
const lifetime = new AbortController();
|
|
381
368
|
onTestFinished(() => lifetime.abort());
|
|
382
369
|
connect({
|
|
383
|
-
|
|
370
|
+
backend: connection.backend,
|
|
384
371
|
signal: lifetime.signal,
|
|
385
372
|
checkHealth: async () => true,
|
|
386
373
|
onNotification: vi.fn(),
|
|
@@ -466,37 +453,3 @@ describe("pyodide", () => {
|
|
|
466
453
|
events.close();
|
|
467
454
|
});
|
|
468
455
|
});
|
|
469
|
-
|
|
470
|
-
describe("createNotebookTransport", () => {
|
|
471
|
-
const options = {
|
|
472
|
-
static: false,
|
|
473
|
-
transportType: "websocket" as const,
|
|
474
|
-
url: () => "ws://localhost/ws",
|
|
475
|
-
headers: () => ({}),
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
it.each([
|
|
479
|
-
["a static notebook", { ...options, static: true }, "idle"],
|
|
480
|
-
["WASM", options, "wasm"],
|
|
481
|
-
["SSE", { ...options, transportType: "sse" as const }, "fetch"],
|
|
482
|
-
["the default", options, "socket"],
|
|
483
|
-
] as const)("follows %s", async (_name, chosen, expected) => {
|
|
484
|
-
wasm.enabled = expected === "wasm";
|
|
485
|
-
const fetch = vi.fn(() => new Promise<Response>(() => undefined));
|
|
486
|
-
vi.stubGlobal("fetch", fetch);
|
|
487
|
-
vi.stubGlobal("WebSocket", FakeSocket);
|
|
488
|
-
const events = new NotebookEvents(createNotebookTransport(chosen));
|
|
489
|
-
await vi.waitFor(() =>
|
|
490
|
-
expect(
|
|
491
|
-
{
|
|
492
|
-
idle:
|
|
493
|
-
events.status === "connected" && !wasm.consumer.mock.calls.length,
|
|
494
|
-
wasm: wasm.consumer.mock.calls.length === 1,
|
|
495
|
-
fetch: fetch.mock.calls.length === 1,
|
|
496
|
-
socket: FakeSocket.opened.length === 1,
|
|
497
|
-
}[expected],
|
|
498
|
-
).toBe(true),
|
|
499
|
-
);
|
|
500
|
-
events.close();
|
|
501
|
-
});
|
|
502
|
-
});
|
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
type StreamUrl,
|
|
8
8
|
} from "@marimo-team/client";
|
|
9
9
|
import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
|
|
10
|
-
import { PyodideBridge } from "../../wasm/bridge";
|
|
11
|
-
import { isWasm } from "../../wasm/utils";
|
|
12
10
|
import { classifyCloseEvent } from "../connection-state";
|
|
13
11
|
|
|
14
12
|
/** The server can be slow to accept when many notebooks are open. */
|
|
@@ -40,6 +38,19 @@ async function resolve(url: StreamUrl): Promise<URL | undefined> {
|
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
40
|
|
|
41
|
+
/** Send each request with the runtime's headers, read before each request. */
|
|
42
|
+
export function withHeaders(
|
|
43
|
+
headers: () => Record<string, string>,
|
|
44
|
+
): typeof globalThis.fetch {
|
|
45
|
+
return (input, init) => {
|
|
46
|
+
const sent = new Headers(init?.headers);
|
|
47
|
+
for (const [name, value] of Object.entries(headers())) {
|
|
48
|
+
sent.set(name, value);
|
|
49
|
+
}
|
|
50
|
+
return globalThis.fetch(input, { ...init, headers: sent });
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
/** A resume cursor and replica belong to the session that produced them. */
|
|
44
55
|
function withSessionIdentity(
|
|
45
56
|
url: StreamUrl,
|
|
@@ -131,13 +142,7 @@ export function sessionSse(
|
|
|
131
142
|
return withSessionIdentity(url, (target) =>
|
|
132
143
|
sse(target, {
|
|
133
144
|
parseData: jsonParseWithSpecialChar,
|
|
134
|
-
fetch: (
|
|
135
|
-
const sent = new Headers(init?.headers);
|
|
136
|
-
for (const [name, value] of Object.entries(headers())) {
|
|
137
|
-
sent.set(name, value);
|
|
138
|
-
}
|
|
139
|
-
return globalThis.fetch(input, { ...init, headers: sent });
|
|
140
|
-
},
|
|
145
|
+
fetch: withHeaders(headers),
|
|
141
146
|
stop: (frame) =>
|
|
142
147
|
frame.event === "close"
|
|
143
148
|
? stopOn(frame.data as { code?: number; reason?: string })
|
|
@@ -147,7 +152,7 @@ export function sessionSse(
|
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
/** A connection that opens and delivers nothing, for static notebooks. */
|
|
150
|
-
const idle: NotebookTransport = ({ signal }, { open }) =>
|
|
155
|
+
export const idle: NotebookTransport = ({ signal }, { open }) =>
|
|
151
156
|
new Promise((settle) => {
|
|
152
157
|
open();
|
|
153
158
|
signal.addEventListener("abort", () => settle(), { once: true });
|
|
@@ -183,31 +188,3 @@ export function pyodide(
|
|
|
183
188
|
signal.addEventListener("abort", () => settle(), { once: true });
|
|
184
189
|
});
|
|
185
190
|
}
|
|
186
|
-
|
|
187
|
-
/** How the editor follows its notebook. */
|
|
188
|
-
export interface NotebookTransportOptions {
|
|
189
|
-
static: boolean;
|
|
190
|
-
transportType: "websocket" | "sse";
|
|
191
|
-
/** The session route, read before each connection. */
|
|
192
|
-
url: StreamUrl;
|
|
193
|
-
/** The runtime's headers, read before each request. */
|
|
194
|
-
headers: () => Record<string, string>;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Choose the transport for the editor's notebook. */
|
|
198
|
-
export function createNotebookTransport(
|
|
199
|
-
options: NotebookTransportOptions,
|
|
200
|
-
): NotebookTransport {
|
|
201
|
-
if (options.static) {
|
|
202
|
-
return idle;
|
|
203
|
-
}
|
|
204
|
-
if (isWasm()) {
|
|
205
|
-
return pyodide((consumer) =>
|
|
206
|
-
PyodideBridge.INSTANCE.attachMessageConsumer(consumer),
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
if (options.transportType === "sse") {
|
|
210
|
-
return sessionSse(options.url, options.headers);
|
|
211
|
-
}
|
|
212
|
-
return websocket(options.url);
|
|
213
|
-
}
|
|
@@ -13,12 +13,9 @@ import {
|
|
|
13
13
|
type NotebookClient,
|
|
14
14
|
} from "../host/notebook-client";
|
|
15
15
|
import { notebookClientAtom } from "./state";
|
|
16
|
-
import {
|
|
17
|
-
createNotebookTransport,
|
|
18
|
-
type NotebookTransportOptions,
|
|
19
|
-
} from "./transports/notebook";
|
|
16
|
+
import { createEditorBackend, type EditorBackendOptions } from "./backend";
|
|
20
17
|
|
|
21
|
-
type Options = Omit<
|
|
18
|
+
type Options = Omit<EditorBackendOptions, "url"> & {
|
|
22
19
|
/** The session route, read before each connection. */
|
|
23
20
|
url: () => string;
|
|
24
21
|
/** Wait until the host can accept a connection. */
|
|
@@ -33,6 +30,7 @@ export function useNotebookClient(options: Options): NotebookClient {
|
|
|
33
30
|
const controller = useMemo(() => createNotebookClient(store), [store]);
|
|
34
31
|
const url = useEvent(options.url);
|
|
35
32
|
const headers = useEvent(options.headers);
|
|
33
|
+
const notebookUrl = useEvent(options.notebookUrl);
|
|
36
34
|
const waitUntilReady = useEvent(options.waitUntilReady);
|
|
37
35
|
const checkHealth = useEvent(options.checkHealth);
|
|
38
36
|
const onNotification = useEvent(options.onNotification);
|
|
@@ -42,7 +40,7 @@ export function useNotebookClient(options: Options): NotebookClient {
|
|
|
42
40
|
let needsInitialReadiness = true;
|
|
43
41
|
store.set(notebookClientAtom, controller.client);
|
|
44
42
|
controller.connect({
|
|
45
|
-
|
|
43
|
+
backend: createEditorBackend({
|
|
46
44
|
static: isStatic,
|
|
47
45
|
transportType,
|
|
48
46
|
url: async () => {
|
|
@@ -60,6 +58,7 @@ export function useNotebookClient(options: Options): NotebookClient {
|
|
|
60
58
|
return url();
|
|
61
59
|
},
|
|
62
60
|
headers,
|
|
61
|
+
notebookUrl,
|
|
63
62
|
}),
|
|
64
63
|
signal: lifetime.signal,
|
|
65
64
|
checkHealth,
|
|
@@ -80,6 +79,7 @@ export function useNotebookClient(options: Options): NotebookClient {
|
|
|
80
79
|
transportType,
|
|
81
80
|
url,
|
|
82
81
|
headers,
|
|
82
|
+
notebookUrl,
|
|
83
83
|
waitUntilReady,
|
|
84
84
|
checkHealth,
|
|
85
85
|
onNotification,
|
|
@@ -75,7 +75,6 @@ import {
|
|
|
75
75
|
import { useStorageActions } from "../storage/state";
|
|
76
76
|
import { useVariablesActions } from "../variables/state";
|
|
77
77
|
import type { VariableName } from "../variables/types";
|
|
78
|
-
import { isWasm } from "../wasm/utils";
|
|
79
78
|
import { WebSocketClosedReason, ConnectionState } from "./types";
|
|
80
79
|
|
|
81
80
|
const SUPPORTS_LAZY_KERNELS = true;
|
|
@@ -418,8 +417,10 @@ export function useNotebookSession(opts: {
|
|
|
418
417
|
: runtimeManager.getWsURL(sessionId).toString(),
|
|
419
418
|
// SSE uses headers for auth. WebSockets carry the token in the URL.
|
|
420
419
|
headers: () => runtimeManager.headers(),
|
|
420
|
+
notebookUrl: (notebookId) => runtimeManager.getNotebookURL(notebookId),
|
|
421
|
+
// Only the host's transports read the URL, so static and WASM never wait.
|
|
421
422
|
waitUntilReady: async () => {
|
|
422
|
-
if (!
|
|
423
|
+
if (!runtimeManager.isSameOrigin) {
|
|
423
424
|
await runtimeManager.waitForHealthy();
|
|
424
425
|
}
|
|
425
426
|
},
|