@marimo-team/islands 0.25.3-dev4 → 0.25.3-dev42
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/{ConnectedDataExplorerComponent-CNSxdKyJ.js → ConnectedDataExplorerComponent-COS3f8io.js} +14 -14
- package/dist/{ErrorBoundary-CuU5CuVr.js → ErrorBoundary-Bb_hjI-F.js} +9 -9
- package/dist/{abnfDiagram-VCTEODGH-DQDjNXt3.js → abnfDiagram-VCTEODGH-DQ4bKxYa.js} +2 -2
- package/dist/any-language-editor-DQItQSua.js +75 -0
- package/dist/{architectureDiagram-5GKGNRK7-Nmls7Wxn.js → architectureDiagram-5GKGNRK7-BA7-SI7Q.js} +1 -1
- package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
- package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
- package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
- package/dist/{chat-ui-BDpymeoe.js → chat-ui-DlUp75HS.js} +1152 -1153
- package/dist/{check-EDwSwP89.js → check-C209OH-W.js} +1 -1
- package/dist/{chunk-IMKFNOWR-CoIFEo-1.js → chunk-IMKFNOWR-DPY3kqOV.js} +1 -1
- package/dist/{chunk-SHT3W25Y-lHiGx3ZC.js → chunk-SHT3W25Y-C14Ikodf.js} +1 -1
- package/dist/{chunk-SVP7TREG-BvsHIrY_.js → chunk-SVP7TREG-BKu0HO4I.js} +1 -1
- package/dist/{chunk-TICWLB2K-BC8YIm_n.js → chunk-TICWLB2K-iJDDWGtE.js} +1 -1
- package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
- package/dist/{classDiagram-ZZMXUADV-Bt98lkkv.js → classDiagram-ZZMXUADV-HuJXQI0F.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-D9bfbcC3.js → classDiagram-v2-VYDZK3BY-CpmmpTbC.js} +2 -2
- package/dist/{code-visibility-Lu0dkrFO.js → code-visibility-BseAT0Di.js} +17 -17
- package/dist/{common-DiWGWDIM.js → common--apjoMvk.js} +6683 -6454
- package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-L-mFk59e.js} +3 -3
- package/dist/{cynefinDiagram-5FMLGOSQ-CgWra7wt.js → cynefinDiagram-5FMLGOSQ-uh0puAW2.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-CFZ8-35c.js → dagre-GXQ25YYZ-4LBeAAMz.js} +1 -1
- package/dist/{data-grid-overlay-editor-ga7ytgbj.js → data-grid-overlay-editor-C2eZGIzb.js} +5 -5
- package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-lCNQrW7d.js → diagram-S7CK7UJ4-BXPmZ2sX.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-CQPQ6pP_.js → diagram-UQ7AKVKN--Xz9UNpC.js} +1 -1
- package/dist/{diagram-VX7I27RA-Cf3b0llz.js → diagram-VX7I27RA-rt8VIRNT.js} +1 -1
- package/dist/{diagram-Z3DM3KII-Bt4QLrpn.js → diagram-Z3DM3KII-Cs1GqfDD.js} +1 -1
- package/dist/{dialog-CEBFZNyc.js → dialog-CDbPtWUD.js} +25 -25
- package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
- package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-ugNoZmKx.js} +828 -828
- package/dist/{ebnfDiagram-PWID7BFC-D3NAyPFY.js → ebnfDiagram-PWID7BFC-CgTsgbmZ.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-Tlu09_Nm.js → erDiagram-RLTQ6QDP-CFoADGzT.js} +1 -1
- package/dist/{error-banner-Da600cwz.js → error-banner-DMIrHA-R.js} +27 -22
- package/dist/{cells-C8iCYvLU.js → error-entries-D3ouOLDe.js} +1378 -1322
- package/dist/{feedback-button-DQA-pzxi.js → feedback-button-Cy7x7fiM.js} +122 -123
- package/dist/{flowDiagram-HODETNUW-CAYbpJmV.js → flowDiagram-HODETNUW-BM2lvKuH.js} +2 -2
- package/dist/{formats-BGh2thRc.js → formats-C24TvExI.js} +3 -3
- package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
- package/dist/{glide-data-editor-Bv75MH6g.js → glide-data-editor-CYonCTY5.js} +370 -368
- package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Q2ZsuA3h.js} +16 -16
- package/dist/{globals-EDQGFIvi.js → globals-W7yeDcvx.js} +3 -3
- package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
- package/dist/{html-to-image-CZY1pIZH.js → html-to-image-VZvb968z.js} +69 -69
- package/dist/{infoDiagram-27XIBGKW-XrDxhr0x.js → infoDiagram-27XIBGKW-Crwjiywp.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-CD1muzov.js → ishikawaDiagram-5VMMS53U-D4US2ShV.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-Dg9W142_.js → kanban-definition-UXKFOSKX-Bk097CUX.js} +1 -1
- package/dist/{label-BDKF3Afc.js → label-CuEjBqYP.js} +2 -2
- package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
- package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
- package/dist/main.js +2696 -2646
- package/dist/{mermaid-CiAXjLpA.js → mermaid-6EkeBlhr.js} +38 -38
- package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
- package/dist/{mindmap-definition-YA3MSWOX-DgCidgYJ.js → mindmap-definition-YA3MSWOX-DmORzfXQ.js} +1 -1
- package/dist/{number-overlay-editor-BCP3MxhC.js → number-overlay-editor-_5SNWHjp.js} +3 -3
- package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
- package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-coOYYKB6.js → pegDiagram-XKGWAZYB-DT5TL02S.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-BnXTc3bj.js → pieDiagram-E7YTZNPT-B1NwGcuH.js} +1 -1
- package/dist/precisionRound-DhAzeRcm.js +49 -0
- package/dist/{process-output-e4jj9Ijt.js → process-output-CCPgTShO.js} +5 -5
- package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
- package/dist/{railroadDiagram-O6MQD6OU-B28-u83n.js → railroadDiagram-O6MQD6OU-BxoycdEu.js} +2 -2
- package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
- package/dist/{requirementDiagram-BXWQKSXE-CdYCTMu0.js → requirementDiagram-BXWQKSXE-DHq_eRrm.js} +1 -1
- package/dist/{reveal-component-DO5BJB9l.js → reveal-component-DV1u0Sl1.js} +10 -10
- package/dist/{select-BButFi-d.js → select-D2iqeJYk.js} +200 -200
- package/dist/{sequenceDiagram-WJ2MYXX4-CM3aRAe_.js → sequenceDiagram-WJ2MYXX4-Ba11BAOu.js} +1 -1
- package/dist/{spec-BwarBpUJ.js → spec-BCCcvWPS.js} +4 -4
- package/dist/{stateDiagram-D77RDMKH-DOZUMVj8.js → stateDiagram-D77RDMKH-OHQbvkbP.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-CISYxma6.js → stateDiagram-v2-MP3YSRHH-BlIquvFJ.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-BkSppxp8.js → swimlanes-42K2YHIH-DSlitS4s.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-c8G5pGxC.js → swimlanesDiagram-VR7AAH4N-ClwuzFNO.js} +2 -2
- package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
- package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
- package/dist/{timeline-definition-24CTP7MA-CaMQayqK.js → timeline-definition-24CTP7MA-hAzc5VkT.js} +1 -1
- package/dist/toDate-C0jaysq2.js +1184 -0
- package/dist/{toast-B9UMEi8u.js → toast-DxVfYlCp.js} +222 -222
- package/dist/{tooltip-BO1REbqs.js → tooltip-BCntm-jf.js} +52 -52
- package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-BhXhsNLV.js} +2 -2
- package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-BbuzBAUp.js} +15 -14
- package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-6AHbIR-J.js} +2 -2
- package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-da5A3c-Y.js} +1 -1
- package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
- package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-C3nT1kwI.js} +3 -3
- package/dist/{useTheme-CRnjkku0.js → useTheme-BC8Lof0B.js} +87 -87
- package/dist/{vega-component-DG29TAB7.js → vega-component-CF9IzE2r.js} +147 -147
- package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
- package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
- package/dist/{vennDiagram-4TSXK5OY-CF8pfaK3.js → vennDiagram-4TSXK5OY-CINKaJgr.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-dGPu39d-.js → wardleyDiagram-VM6X3IG4-KG2tlXg_.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-CP6-9q56.js → xychartDiagram-S5SC5T6Z-XIH5cwgI.js} +2 -2
- package/package.json +4 -4
- package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
- package/src/__tests__/host-imports.test.ts +1 -0
- package/src/__tests__/mount.test.ts +28 -8
- package/src/components/app-config/ai-config.tsx +5 -1
- package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
- package/src/components/data-table/__tests__/export-actions.test.tsx +329 -6
- package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
- package/src/components/data-table/columns.tsx +2 -1
- package/src/components/data-table/date-popover.tsx +6 -4
- package/src/components/data-table/export-actions.tsx +192 -116
- package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
- package/src/components/data-table/schemas.ts +14 -36
- package/src/components/editor/Output.tsx +9 -2
- package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
- package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
- package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
- package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
- package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
- package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +366 -0
- package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
- package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +237 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
- package/src/components/editor/connections/components.tsx +1 -1
- package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
- package/src/components/editor/controls/command-palette.tsx +22 -10
- package/src/components/editor/dynamic-favicon.tsx +21 -40
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +537 -0
- package/src/components/editor/errors/send-error-report-button.tsx +69 -0
- package/src/components/editor/output/MarimoErrorOutput.tsx +21 -5
- package/src/components/editor/output/MarimoTracebackOutput.tsx +36 -21
- package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
- package/src/components/editor/output/ansi-reduce.ts +6 -0
- package/src/components/pages/home-page.tsx +4 -4
- package/src/components/tracing/tracing.tsx +2 -2
- package/src/core/attachments/__tests__/toast.test.tsx +58 -0
- package/src/core/attachments/state.ts +18 -0
- package/src/core/attachments/toast.ts +15 -0
- package/src/core/cells/__tests__/cells.test.ts +52 -0
- package/src/core/cells/__tests__/error-report.test.ts +139 -0
- package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
- package/src/core/cells/__tests__/session.test.ts +50 -1
- package/src/core/cells/cells.ts +2 -1
- package/src/core/cells/error-report.ts +63 -0
- package/src/core/cells/pending-delete-service.ts +2 -6
- package/src/core/cells/session.ts +28 -1
- package/src/core/config/pair.ts +5 -3
- package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
- package/src/core/datasets/data-source-discovery.ts +4 -6
- package/src/core/edit-app.tsx +2 -2
- package/src/core/host/AGENTS.md +11 -2
- package/src/core/host/__tests__/attachments.test.ts +56 -0
- package/src/core/host/__tests__/document.test.ts +126 -0
- package/src/core/host/__tests__/events.test.ts +49 -0
- package/src/core/host/__tests__/fixtures.ts +41 -0
- package/src/core/host/__tests__/notebook-client.test.ts +433 -0
- package/src/core/host/connection.ts +81 -0
- package/src/core/host/events.ts +77 -0
- package/src/core/host/notebook-client.ts +323 -0
- package/src/core/host/resources/attachments.ts +19 -0
- package/src/core/host/resources/document.ts +101 -0
- package/src/core/hotkeys/actions.ts +2 -4
- package/src/core/network/connection.ts +4 -2
- package/src/core/run-app.tsx +2 -2
- package/src/core/runtime/__tests__/runtime.test.ts +55 -11
- package/src/core/runtime/runtime.ts +18 -5
- package/src/core/runtime/types.ts +2 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
- package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +170 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +181 -65
- package/src/core/websocket/connection-state.ts +70 -0
- package/src/core/websocket/state.ts +7 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +1 -4
- package/src/core/websocket/transports/create.ts +31 -0
- package/src/core/websocket/transports/transport.ts +12 -23
- package/src/core/websocket/types.ts +9 -39
- package/src/core/websocket/useNotebookClient.ts +88 -0
- package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +59 -225
- package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
- package/src/hooks/useAsyncData.ts +3 -3
- package/src/hooks/useDataSourceDiscovery.ts +6 -4
- package/src/hooks/useElapsedTime.ts +3 -3
- package/src/mount.tsx +7 -3
- package/src/plugins/core/registerReactComponent.tsx +1 -1
- package/src/plugins/impl/DropdownPlugin.tsx +2 -6
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
- package/src/plugins/impl/code/any-language-editor.tsx +2 -2
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
- package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
- package/src/plugins/impl/vega/make-selectable.ts +3 -6
- package/src/utils/__tests__/arrays.test.ts +35 -0
- package/src/utils/__tests__/numbers.test.ts +48 -1
- package/src/utils/__tests__/objects.test.ts +21 -0
- package/src/utils/arrays.ts +10 -0
- package/src/utils/numbers.ts +28 -17
- package/src/utils/objects.ts +3 -7
- package/src/utils/once.ts +19 -19
- package/dist/any-language-editor-B9REGlD9.js +0 -76
- package/dist/error-entries-CzoYkIQZ.js +0 -56
- package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
- package/dist/precisionRound-DMIVQrt2.js +0 -97
- package/dist/toDate-BNJf01u1.js +0 -703
- package/src/core/host/__tests__/protocol.test.ts +0 -214
- package/src/core/host/protocol.ts +0 -1943
- package/src/core/websocket/useWebSocket.tsx +0 -88
package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts}
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { Logger } from "@/utils/Logger";
|
|
5
5
|
import { WebSocketClosedReason, ConnectionState } from "../types";
|
|
6
|
-
import { classifyCloseEvent } from "../
|
|
6
|
+
import { classifyCloseEvent } from "../connection-state";
|
|
7
7
|
|
|
8
8
|
function classify(reason: string | undefined) {
|
|
9
9
|
return classifyCloseEvent({ reason });
|
|
@@ -13,8 +13,10 @@ describe("classifyCloseEvent", () => {
|
|
|
13
13
|
describe("transient closes (default branch)", () => {
|
|
14
14
|
it("retries on empty/undefined reason", () => {
|
|
15
15
|
const decision = classify(undefined);
|
|
16
|
-
expect(decision
|
|
17
|
-
|
|
16
|
+
expect(decision).toEqual({
|
|
17
|
+
kind: "retry",
|
|
18
|
+
status: { state: ConnectionState.CONNECTING },
|
|
19
|
+
});
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
it("treats unknown reason strings as transient and logs a warning", () => {
|
|
@@ -40,29 +42,25 @@ describe("classifyCloseEvent", () => {
|
|
|
40
42
|
// terminal or a rejected client would reconnect forever.
|
|
41
43
|
"MARIMO_UNAUTHORIZED",
|
|
42
44
|
"MARIMO_KIOSK_NOT_ALLOWED",
|
|
43
|
-
])("%s → terminal with KERNEL_DISCONNECTED
|
|
45
|
+
])("%s → terminal with KERNEL_DISCONNECTED", (reason) => {
|
|
44
46
|
const decision = classify(reason);
|
|
45
|
-
expect(decision.kind).toBe("terminal");
|
|
46
|
-
expect(decision.status).toMatchObject({
|
|
47
|
-
state: ConnectionState.CLOSED,
|
|
48
|
-
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
49
|
-
});
|
|
50
47
|
expect(decision).toMatchObject({
|
|
51
48
|
kind: "terminal",
|
|
52
|
-
|
|
49
|
+
status: {
|
|
50
|
+
state: ConnectionState.CLOSED,
|
|
51
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
52
|
+
},
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it("MARIMO_KERNEL_STARTUP_ERROR → terminal
|
|
56
|
+
it("MARIMO_KERNEL_STARTUP_ERROR → terminal", () => {
|
|
57
57
|
const decision = classify("MARIMO_KERNEL_STARTUP_ERROR");
|
|
58
|
-
expect(decision.kind).toBe("terminal");
|
|
59
|
-
expect(decision.status).toMatchObject({
|
|
60
|
-
state: ConnectionState.CLOSED,
|
|
61
|
-
code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
|
|
62
|
-
});
|
|
63
58
|
expect(decision).toMatchObject({
|
|
64
59
|
kind: "terminal",
|
|
65
|
-
|
|
60
|
+
status: {
|
|
61
|
+
state: ConnectionState.CLOSED,
|
|
62
|
+
code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
|
|
63
|
+
},
|
|
66
64
|
});
|
|
67
65
|
});
|
|
68
66
|
});
|
package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts}
RENAMED
|
@@ -4,7 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
4
4
|
import { BasicTransport } from "../transports/basic";
|
|
5
5
|
import { SseTransport } from "../transports/sse";
|
|
6
6
|
import { WsTransport } from "../transports/ws";
|
|
7
|
-
import { createConnectionTransport } from "../
|
|
7
|
+
import { createConnectionTransport } from "../transports/create";
|
|
8
8
|
|
|
9
9
|
vi.mock("../../wasm/utils", () => ({
|
|
10
10
|
isWasm: () => false,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
|
|
4
|
+
import { act, renderHook } from "@testing-library/react";
|
|
5
|
+
import { createStore, Provider } from "jotai";
|
|
6
|
+
import type { PropsWithChildren } from "react";
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { ConnectionState } from "../types";
|
|
9
|
+
import { createConnectionTransport } from "../transports/create";
|
|
10
|
+
import type { IConnectionTransport } from "../transports/transport";
|
|
11
|
+
import { useNotebookClient } from "../useNotebookClient";
|
|
12
|
+
import { fixtureMessages } from "../../host/__tests__/fixtures";
|
|
13
|
+
import { agentAttachmentsAtom } from "../../attachments/state";
|
|
14
|
+
import { notebookClientAtom } from "../state";
|
|
15
|
+
|
|
16
|
+
vi.mock("../transports/create", () => ({ createConnectionTransport: vi.fn() }));
|
|
17
|
+
|
|
18
|
+
function makeTransport() {
|
|
19
|
+
const events = new EventTarget();
|
|
20
|
+
const transport: IConnectionTransport = {
|
|
21
|
+
readyState: WebSocket.CLOSED,
|
|
22
|
+
reconnect: vi.fn(() => {
|
|
23
|
+
transport.readyState = WebSocket.OPEN;
|
|
24
|
+
events.dispatchEvent(new Event("open"));
|
|
25
|
+
}),
|
|
26
|
+
close: vi.fn(() => {
|
|
27
|
+
transport.readyState = WebSocket.CLOSED;
|
|
28
|
+
events.dispatchEvent(new CloseEvent("close"));
|
|
29
|
+
}),
|
|
30
|
+
send: vi.fn(),
|
|
31
|
+
addEventListener: events.addEventListener.bind(events),
|
|
32
|
+
removeEventListener: events.removeEventListener.bind(events),
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
transport,
|
|
36
|
+
receive: (message: unknown) =>
|
|
37
|
+
events.dispatchEvent(
|
|
38
|
+
new MessageEvent("message", { data: JSON.stringify(message) }),
|
|
39
|
+
),
|
|
40
|
+
notify: () =>
|
|
41
|
+
events.dispatchEvent(
|
|
42
|
+
new MessageEvent("message", {
|
|
43
|
+
data: JSON.stringify({
|
|
44
|
+
event: "marimo",
|
|
45
|
+
data: { marimo: { op: "reconnected" } },
|
|
46
|
+
}),
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setup(strict = false) {
|
|
53
|
+
const store = createStore();
|
|
54
|
+
const options = {
|
|
55
|
+
sessionId: "first",
|
|
56
|
+
static: false,
|
|
57
|
+
transportType: "websocket" as const,
|
|
58
|
+
messageFormat: "host" as const,
|
|
59
|
+
url: () => "ws://localhost/ws",
|
|
60
|
+
headers: () => ({}),
|
|
61
|
+
waitUntilReady: async () => undefined,
|
|
62
|
+
checkHealth: async () => true,
|
|
63
|
+
onNotification: vi.fn(),
|
|
64
|
+
onMessageError: vi.fn(),
|
|
65
|
+
};
|
|
66
|
+
const wrapper = ({ children }: PropsWithChildren) => (
|
|
67
|
+
<Provider store={store}>{children}</Provider>
|
|
68
|
+
);
|
|
69
|
+
const hook = renderHook((props) => useNotebookClient(props), {
|
|
70
|
+
initialProps: options,
|
|
71
|
+
reactStrictMode: strict,
|
|
72
|
+
wrapper,
|
|
73
|
+
});
|
|
74
|
+
return { ...hook, options, store };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.mocked(createConnectionTransport).mockReset();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("useNotebookClient", () => {
|
|
82
|
+
it("exposes host presence to the UI without waiting for kernel-ready", async () => {
|
|
83
|
+
const transport = makeTransport();
|
|
84
|
+
vi.mocked(createConnectionTransport).mockReturnValue(transport.transport);
|
|
85
|
+
const { result, store, unmount } = setup();
|
|
86
|
+
await act(async () => Promise.resolve());
|
|
87
|
+
expect(store.get(notebookClientAtom)).toBe(result.current);
|
|
88
|
+
act(() =>
|
|
89
|
+
fixtureMessages("notebook-events.sse").forEach(transport.receive),
|
|
90
|
+
);
|
|
91
|
+
expect(store.get(agentAttachmentsAtom)).toEqual([
|
|
92
|
+
{
|
|
93
|
+
id: "a2",
|
|
94
|
+
notebook_id: "n1",
|
|
95
|
+
kind: "agent",
|
|
96
|
+
name: "Notebook CLI",
|
|
97
|
+
since: "2026-10-01T09:20:03Z",
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
unmount();
|
|
101
|
+
expect(store.get(notebookClientAtom)).toBeNull();
|
|
102
|
+
expect(store.get(agentAttachmentsAtom)).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("uses current callbacks and URL providers without reconnecting on a rerender", async () => {
|
|
106
|
+
const first = makeTransport();
|
|
107
|
+
vi.mocked(createConnectionTransport).mockReturnValue(first.transport);
|
|
108
|
+
const { rerender, options, unmount } = setup();
|
|
109
|
+
await act(async () => Promise.resolve());
|
|
110
|
+
const onNotification = vi.fn();
|
|
111
|
+
rerender({ ...options, onNotification, url: () => "ws://redirected/ws" });
|
|
112
|
+
act(() => first.notify());
|
|
113
|
+
expect(options.onNotification).not.toHaveBeenCalled();
|
|
114
|
+
expect(onNotification).toHaveBeenCalledExactlyOnceWith({
|
|
115
|
+
data: { op: "reconnected" },
|
|
116
|
+
});
|
|
117
|
+
expect(createConnectionTransport).toHaveBeenCalledOnce();
|
|
118
|
+
expect(vi.mocked(createConnectionTransport).mock.calls[0][0].url()).toBe(
|
|
119
|
+
"ws://redirected/ws",
|
|
120
|
+
);
|
|
121
|
+
unmount();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("replaces the transport when session identity changes and ignores old messages", async () => {
|
|
125
|
+
const first = makeTransport();
|
|
126
|
+
const second = makeTransport();
|
|
127
|
+
vi.mocked(createConnectionTransport)
|
|
128
|
+
.mockReturnValueOnce(first.transport)
|
|
129
|
+
.mockReturnValueOnce(second.transport);
|
|
130
|
+
const { rerender, options, unmount } = setup();
|
|
131
|
+
await act(async () => Promise.resolve());
|
|
132
|
+
rerender({ ...options, sessionId: "second" });
|
|
133
|
+
await act(async () => Promise.resolve());
|
|
134
|
+
expect(first.transport.close).toHaveBeenCalledOnce();
|
|
135
|
+
act(() => {
|
|
136
|
+
first.notify();
|
|
137
|
+
second.notify();
|
|
138
|
+
});
|
|
139
|
+
expect(options.onNotification).toHaveBeenCalledOnce();
|
|
140
|
+
unmount();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("cleans up every transport under Strict Mode and only connects the active lifetime", async () => {
|
|
144
|
+
const transports: ReturnType<typeof makeTransport>[] = [];
|
|
145
|
+
vi.mocked(createConnectionTransport).mockImplementation(() => {
|
|
146
|
+
const next = makeTransport();
|
|
147
|
+
transports.push(next);
|
|
148
|
+
return next.transport;
|
|
149
|
+
});
|
|
150
|
+
const { result, store, options, unmount } = setup(true);
|
|
151
|
+
await act(async () => Promise.resolve());
|
|
152
|
+
expect(transports).toHaveLength(2);
|
|
153
|
+
expect(transports[0].transport.reconnect).not.toHaveBeenCalled();
|
|
154
|
+
expect(transports[0].transport.close).toHaveBeenCalledOnce();
|
|
155
|
+
expect(transports[1].transport.reconnect).toHaveBeenCalledOnce();
|
|
156
|
+
expect(store.get(result.current.connectionAtom)).toEqual({
|
|
157
|
+
state: ConnectionState.OPEN,
|
|
158
|
+
});
|
|
159
|
+
act(() => {
|
|
160
|
+
for (const transport of transports) {
|
|
161
|
+
transport.notify();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
expect(options.onNotification).toHaveBeenCalledOnce();
|
|
165
|
+
unmount();
|
|
166
|
+
expect(transports[1].transport.close).toHaveBeenCalledOnce();
|
|
167
|
+
await result.current.reconnect();
|
|
168
|
+
expect(transports[1].transport.reconnect).toHaveBeenCalledOnce();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -21,14 +21,9 @@ import {
|
|
|
21
21
|
vi,
|
|
22
22
|
} from "vitest";
|
|
23
23
|
|
|
24
|
-
vi.mock("
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
...actual,
|
|
29
|
-
useConnectionTransport: vi.fn(),
|
|
30
|
-
};
|
|
31
|
-
});
|
|
24
|
+
vi.mock("../transports/create", () => ({
|
|
25
|
+
createConnectionTransport: vi.fn(),
|
|
26
|
+
}));
|
|
32
27
|
|
|
33
28
|
vi.mock("@/core/runtime/config", async () => {
|
|
34
29
|
const actual = await vi.importActual<typeof import("@/core/runtime/config")>(
|
|
@@ -41,21 +36,28 @@ vi.mock("@/core/runtime/config", async () => {
|
|
|
41
36
|
});
|
|
42
37
|
|
|
43
38
|
import { MockNotebook } from "@/__mocks__/notebook";
|
|
39
|
+
import * as Toast from "@/components/ui/use-toast";
|
|
40
|
+
import { Logger } from "@/utils/Logger";
|
|
41
|
+
import { Deferred } from "@/utils/Deferred";
|
|
44
42
|
import { cellId } from "@/__tests__/branded";
|
|
45
43
|
import { alertAtom, getPackageAlert } from "@/core/alerts/state";
|
|
46
44
|
import { notebookAtom } from "@/core/cells/cells";
|
|
47
45
|
import { AppConfigSchema } from "@/core/config/config-schema";
|
|
48
46
|
import { ConnectionNotice } from "@/components/editor/alerts/connection-notice";
|
|
49
47
|
import { kernelStartupErrorAtom } from "@/core/errors/state";
|
|
48
|
+
import type { RunEvent } from "@marimo-team/client/protocol";
|
|
50
49
|
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
51
50
|
import { useRuntimeManager } from "@/core/runtime/config";
|
|
52
51
|
import { initialRunCompletedAtom } from "../../kernel/state";
|
|
53
52
|
import { connectionAtom, startupProgressAtom } from "../../network/connection";
|
|
54
|
-
import
|
|
53
|
+
import { generateSessionId } from "../../kernel/session";
|
|
54
|
+
import { RuntimeManager } from "../../runtime/runtime";
|
|
55
55
|
import { WebSocketClosedReason, ConnectionState } from "../types";
|
|
56
56
|
import type { IConnectionTransport } from "../transports/transport";
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
57
|
+
import { useNotebookSession } from "../useNotebookSession";
|
|
58
|
+
import { createConnectionTransport } from "../transports/create";
|
|
59
|
+
|
|
60
|
+
const SESSION_ID = generateSessionId();
|
|
59
61
|
|
|
60
62
|
interface MockTransport {
|
|
61
63
|
readyState: 0 | 1 | 2 | 3;
|
|
@@ -79,16 +81,47 @@ function makeTransport(
|
|
|
79
81
|
};
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
function getTransportHandlers() {
|
|
85
|
+
const result = vi.mocked(createConnectionTransport).mock.results.at(-1);
|
|
86
|
+
if (!result || result.type !== "return") {
|
|
87
|
+
throw new Error("No connection transport was created");
|
|
88
|
+
}
|
|
89
|
+
const transport = result.value;
|
|
90
|
+
function emit(
|
|
91
|
+
event: "open" | "close" | "error" | "message",
|
|
92
|
+
message: Event | CloseEvent | MessageEvent,
|
|
93
|
+
) {
|
|
94
|
+
const callback = vi
|
|
95
|
+
.mocked(transport.addEventListener)
|
|
96
|
+
.mock.calls.find(([name]) => name === event)?.[1];
|
|
97
|
+
callback?.(message);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
onError: (event: Event) => emit("error", event),
|
|
101
|
+
onOpen: (event: Event) => emit("open", event),
|
|
102
|
+
onClose: (event: CloseEvent) => emit("close", event),
|
|
103
|
+
onMessage: (event: MessageEvent) => emit("message", event),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
82
107
|
function makeRuntimeManager(
|
|
83
108
|
reconcileFromHealth = vi.fn<() => Promise<boolean>>().mockResolvedValue(true),
|
|
84
109
|
) {
|
|
85
|
-
|
|
110
|
+
const runtime = new RuntimeManager(
|
|
111
|
+
{ url: "http://localhost", lazy: true },
|
|
112
|
+
true,
|
|
113
|
+
);
|
|
114
|
+
vi.spyOn(runtime, "reconcileFromHealth").mockImplementation(
|
|
86
115
|
reconcileFromHealth,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
116
|
+
);
|
|
117
|
+
vi.spyOn(runtime, "probeHealth").mockResolvedValue(true);
|
|
118
|
+
vi.spyOn(runtime, "getWsURL").mockReturnValue(new URL("ws://localhost/ws"));
|
|
119
|
+
// Keep the initial connection pending; these tests exercise session retries.
|
|
120
|
+
vi.spyOn(runtime, "waitForHealthy").mockReturnValue(
|
|
121
|
+
new Deferred<void>().promise,
|
|
122
|
+
);
|
|
123
|
+
vi.spyOn(runtime, "isSameOrigin", "get").mockReturnValue(false);
|
|
124
|
+
return runtime;
|
|
92
125
|
}
|
|
93
126
|
|
|
94
127
|
function renderConnectionHook(store: ReturnType<typeof createStore>) {
|
|
@@ -99,16 +132,16 @@ function renderConnectionHook(store: ReturnType<typeof createStore>) {
|
|
|
99
132
|
);
|
|
100
133
|
return renderHook(
|
|
101
134
|
() =>
|
|
102
|
-
|
|
103
|
-
sessionId:
|
|
135
|
+
useNotebookSession({
|
|
136
|
+
sessionId: SESSION_ID,
|
|
104
137
|
autoInstantiate: false,
|
|
105
|
-
setCells: ()
|
|
138
|
+
setCells: vi.fn(),
|
|
106
139
|
}),
|
|
107
140
|
{ wrapper },
|
|
108
141
|
);
|
|
109
142
|
}
|
|
110
143
|
|
|
111
|
-
describe("
|
|
144
|
+
describe("useNotebookSession.reconnect()", () => {
|
|
112
145
|
let transport: MockTransport;
|
|
113
146
|
let isHealthy: Mock<() => Promise<boolean>>;
|
|
114
147
|
let store: ReturnType<typeof createStore>;
|
|
@@ -122,12 +155,8 @@ describe("useMarimoKernelConnection.reconnect()", () => {
|
|
|
122
155
|
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
123
156
|
reason: "kernel not found",
|
|
124
157
|
});
|
|
125
|
-
vi.mocked(
|
|
126
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
127
|
-
makeRuntimeManager(isHealthy) as unknown as ReturnType<
|
|
128
|
-
typeof useRuntimeManager
|
|
129
|
-
>,
|
|
130
|
-
);
|
|
158
|
+
vi.mocked(createConnectionTransport).mockReturnValue(transport);
|
|
159
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager(isHealthy));
|
|
131
160
|
});
|
|
132
161
|
|
|
133
162
|
function renderUseHook() {
|
|
@@ -183,31 +212,124 @@ describe("useMarimoKernelConnection.reconnect()", () => {
|
|
|
183
212
|
});
|
|
184
213
|
});
|
|
185
214
|
|
|
186
|
-
describe("
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
215
|
+
describe("useNotebookSession messages", () => {
|
|
216
|
+
let store: ReturnType<typeof createStore>;
|
|
217
|
+
|
|
218
|
+
beforeEach(() => {
|
|
219
|
+
store = createStore();
|
|
220
|
+
vi.mocked(createConnectionTransport).mockClear();
|
|
221
|
+
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
191
222
|
makeTransport(WebSocket.OPEN),
|
|
192
223
|
);
|
|
193
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
194
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
195
|
-
);
|
|
224
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
196
225
|
renderConnectionHook(store);
|
|
226
|
+
});
|
|
197
227
|
|
|
198
|
-
|
|
199
|
-
|
|
228
|
+
function receive(payload: unknown) {
|
|
229
|
+
const options = getTransportHandlers();
|
|
200
230
|
act(() => {
|
|
201
|
-
options
|
|
202
|
-
new MessageEvent("message", {
|
|
203
|
-
data: JSON.stringify({
|
|
204
|
-
op: "completed-run",
|
|
205
|
-
data: { op: "completed-run", run_id: null },
|
|
206
|
-
}),
|
|
207
|
-
}),
|
|
231
|
+
options.onMessage(
|
|
232
|
+
new MessageEvent("message", { data: JSON.stringify(payload) }),
|
|
208
233
|
);
|
|
209
234
|
});
|
|
210
|
-
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
it.each(["{broken", "null"])(
|
|
238
|
+
"reports a malformed legacy frame and continues applying notifications: %s",
|
|
239
|
+
(data) => {
|
|
240
|
+
const toast = vi.spyOn(Toast, "toast");
|
|
241
|
+
try {
|
|
242
|
+
receive({ data: { op: "reconnected" } });
|
|
243
|
+
act(() =>
|
|
244
|
+
getTransportHandlers().onMessage(
|
|
245
|
+
new MessageEvent("message", { data }),
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
expect(toast).toHaveBeenCalledExactlyOnceWith({
|
|
249
|
+
title: "Failed to handle message",
|
|
250
|
+
description: expect.any(String),
|
|
251
|
+
variant: "danger",
|
|
252
|
+
});
|
|
253
|
+
expect(store.get(connectionAtom)).toEqual({
|
|
254
|
+
state: ConnectionState.OPEN,
|
|
255
|
+
});
|
|
256
|
+
receive({
|
|
257
|
+
data: {
|
|
258
|
+
op: "completed-run",
|
|
259
|
+
run_id: "r1",
|
|
260
|
+
outcome: "succeeded",
|
|
261
|
+
error: null,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
expect(store.get(initialRunCompletedAtom)).toBe(true);
|
|
265
|
+
} finally {
|
|
266
|
+
toast.mockRestore();
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
it.each(["session", "host"])(
|
|
272
|
+
"records completion of the initial run from the %s stream",
|
|
273
|
+
(stream) => {
|
|
274
|
+
const notification = {
|
|
275
|
+
op: "completed-run",
|
|
276
|
+
run_id: null,
|
|
277
|
+
outcome: "succeeded",
|
|
278
|
+
error: null,
|
|
279
|
+
} satisfies NotificationPayload["data"];
|
|
280
|
+
expect(store.get(initialRunCompletedAtom)).toBe(false);
|
|
281
|
+
receive(
|
|
282
|
+
stream === "host"
|
|
283
|
+
? { id: "42", event: "marimo", data: { marimo: notification } }
|
|
284
|
+
: { op: notification.op, data: notification },
|
|
285
|
+
);
|
|
286
|
+
expect(store.get(initialRunCompletedAtom)).toBe(true);
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
it("ignores other host events before handling the next notification", () => {
|
|
291
|
+
const errors = vi.spyOn(Logger, "error").mockImplementation(vi.fn());
|
|
292
|
+
try {
|
|
293
|
+
receive({ id: "1", event: "ready", data: {} });
|
|
294
|
+
receive({
|
|
295
|
+
id: "2",
|
|
296
|
+
event: "run",
|
|
297
|
+
data: {
|
|
298
|
+
run: {
|
|
299
|
+
id: "r1",
|
|
300
|
+
notebook_id: "n1",
|
|
301
|
+
caused_by: null,
|
|
302
|
+
status: "completed",
|
|
303
|
+
outcome: "succeeded",
|
|
304
|
+
started_at: "2026-10-19T12:00:00Z",
|
|
305
|
+
completed_at: "2026-10-19T12:00:01Z",
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
} satisfies RunEvent);
|
|
309
|
+
receive({
|
|
310
|
+
id: "3",
|
|
311
|
+
event: "future-event",
|
|
312
|
+
data: { marimo: { op: "completed-run", run_id: "r1" } },
|
|
313
|
+
});
|
|
314
|
+
receive({ id: "4", event: "another-event", data: null });
|
|
315
|
+
expect(store.get(initialRunCompletedAtom)).toBe(false);
|
|
316
|
+
receive({
|
|
317
|
+
id: "5",
|
|
318
|
+
event: "marimo",
|
|
319
|
+
data: {
|
|
320
|
+
marimo: {
|
|
321
|
+
op: "completed-run",
|
|
322
|
+
run_id: "r1",
|
|
323
|
+
outcome: "succeeded",
|
|
324
|
+
error: null,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
expect(store.get(initialRunCompletedAtom)).toBe(true);
|
|
329
|
+
expect(errors).not.toHaveBeenCalled();
|
|
330
|
+
} finally {
|
|
331
|
+
errors.mockRestore();
|
|
332
|
+
}
|
|
211
333
|
});
|
|
212
334
|
});
|
|
213
335
|
|
|
@@ -216,15 +338,13 @@ it.each(["kernel-ready", "reconnected"])(
|
|
|
216
338
|
async (op) => {
|
|
217
339
|
const store = createStore();
|
|
218
340
|
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
219
|
-
vi.mocked(
|
|
220
|
-
vi.mocked(
|
|
341
|
+
vi.mocked(createConnectionTransport).mockClear();
|
|
342
|
+
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
221
343
|
makeTransport(WebSocket.OPEN),
|
|
222
344
|
);
|
|
223
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
224
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
225
|
-
);
|
|
345
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
226
346
|
renderConnectionHook(store);
|
|
227
|
-
const options =
|
|
347
|
+
const options = getTransportHandlers();
|
|
228
348
|
|
|
229
349
|
await act(async () => {
|
|
230
350
|
await options.onOpen(new Event("open"));
|
|
@@ -269,15 +389,13 @@ describe("connection notice", () => {
|
|
|
269
389
|
const store = createStore();
|
|
270
390
|
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
271
391
|
const transport = makeTransport(WebSocket.OPEN);
|
|
272
|
-
vi.mocked(
|
|
273
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
274
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
275
|
-
);
|
|
392
|
+
vi.mocked(createConnectionTransport).mockReturnValue(transport);
|
|
393
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
276
394
|
const Connection = () => {
|
|
277
|
-
const { reconnect } =
|
|
278
|
-
sessionId:
|
|
395
|
+
const { reconnect } = useNotebookSession({
|
|
396
|
+
sessionId: SESSION_ID,
|
|
279
397
|
autoInstantiate: false,
|
|
280
|
-
setCells: ()
|
|
398
|
+
setCells: vi.fn(),
|
|
281
399
|
});
|
|
282
400
|
return (
|
|
283
401
|
<ConnectionNotice
|
|
@@ -293,7 +411,7 @@ describe("connection notice", () => {
|
|
|
293
411
|
</ErrorBoundary>
|
|
294
412
|
</JotaiProvider>,
|
|
295
413
|
);
|
|
296
|
-
const options =
|
|
414
|
+
const options = getTransportHandlers();
|
|
297
415
|
const send = (data: NotificationPayload["data"]) =>
|
|
298
416
|
act(() => {
|
|
299
417
|
options.onMessage(
|
|
@@ -482,15 +600,13 @@ describe("connection notice", () => {
|
|
|
482
600
|
|
|
483
601
|
it("replaces environment state from snapshots, then continues live progress", () => {
|
|
484
602
|
const store = createStore();
|
|
485
|
-
vi.mocked(
|
|
486
|
-
vi.mocked(
|
|
603
|
+
vi.mocked(createConnectionTransport).mockClear();
|
|
604
|
+
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
487
605
|
makeTransport(WebSocket.OPEN),
|
|
488
606
|
);
|
|
489
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
490
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
491
|
-
);
|
|
607
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
492
608
|
renderConnectionHook(store);
|
|
493
|
-
const options =
|
|
609
|
+
const options = getTransportHandlers();
|
|
494
610
|
function receive(data: NotificationPayload["data"]) {
|
|
495
611
|
act(() =>
|
|
496
612
|
options?.onMessage(
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { CloseDecision } from "../host/connection";
|
|
4
|
+
import { Logger } from "@/utils/Logger";
|
|
5
|
+
import { TRANSPORT_EXHAUSTED_REASON } from "./transports/ws";
|
|
6
|
+
import { ConnectionState, WebSocketClosedReason } from "./types";
|
|
7
|
+
|
|
8
|
+
// Keep server reasons in sync with ws_endpoint.py and ws/*.py.
|
|
9
|
+
// SSE carries the same reasons in close events. Unclassified reasons retry.
|
|
10
|
+
export function classifyCloseEvent(event: { reason?: string }): CloseDecision {
|
|
11
|
+
switch (event.reason) {
|
|
12
|
+
case TRANSPORT_EXHAUSTED_REASON:
|
|
13
|
+
return {
|
|
14
|
+
kind: "gave-up",
|
|
15
|
+
status: {
|
|
16
|
+
state: ConnectionState.CLOSED,
|
|
17
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
18
|
+
reason: "kernel not found",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
case "MARIMO_NO_FILE_KEY":
|
|
22
|
+
case "MARIMO_NO_SESSION_ID":
|
|
23
|
+
case "MARIMO_NO_SESSION":
|
|
24
|
+
case "MARIMO_SHUTDOWN":
|
|
25
|
+
return {
|
|
26
|
+
kind: "terminal",
|
|
27
|
+
status: {
|
|
28
|
+
state: ConnectionState.CLOSED,
|
|
29
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
30
|
+
reason: "kernel not found",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
case "MARIMO_UNAUTHORIZED":
|
|
34
|
+
return {
|
|
35
|
+
kind: "terminal",
|
|
36
|
+
status: {
|
|
37
|
+
state: ConnectionState.CLOSED,
|
|
38
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
39
|
+
reason: "not authorized",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
case "MARIMO_KIOSK_NOT_ALLOWED":
|
|
43
|
+
return {
|
|
44
|
+
kind: "terminal",
|
|
45
|
+
status: {
|
|
46
|
+
state: ConnectionState.CLOSED,
|
|
47
|
+
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
48
|
+
reason: "kiosk mode is not available for this session",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
case "MARIMO_KERNEL_STARTUP_ERROR":
|
|
52
|
+
return {
|
|
53
|
+
kind: "terminal",
|
|
54
|
+
status: {
|
|
55
|
+
state: ConnectionState.CLOSED,
|
|
56
|
+
code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
|
|
57
|
+
reason: "Failed to start kernel sandbox",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
default:
|
|
61
|
+
if (event.reason) {
|
|
62
|
+
Logger.warn("Unrecognized connection close reason", event.reason);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
kind: "retry",
|
|
68
|
+
status: { state: ConnectionState.CONNECTING },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { atom } from "jotai";
|
|
4
|
+
import type { NotebookClient } from "../host/notebook-client";
|
|
5
|
+
|
|
6
|
+
/** The client owned by the mounted notebook session. */
|
|
7
|
+
export const notebookClientAtom = atom<NotebookClient | null>(null);
|