@marimo-team/frontend 0.25.3-dev10 → 0.25.3-dev12
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-nZpuDkm7.js → CellStatus-DVOHpdeJ.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-t8fH_TNc.js → ConnectedDataExplorerComponent-TprnxZeJ.js} +1 -1
- package/dist/assets/{ErrorBoundary-B4n9EnHB.js → ErrorBoundary-CM13wrd8.js} +2 -2
- package/dist/assets/{JsonOutput-BxB0NL8w.js → JsonOutput-Cx1iyibB.js} +1 -1
- package/dist/assets/{LazyAnyLanguageCodeMirror-CKcCbG2G.js → LazyAnyLanguageCodeMirror-B4fCknq1.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-BTy4MOzX.js → MarimoErrorOutput-s_5GOX8k.js} +1 -1
- package/dist/assets/{RenderHTML-9bMgbYmo.js → RenderHTML-DX6i99Op.js} +1 -1
- package/dist/assets/{RunButton-CFQUNN5c.js → RunButton-BKoh0zYG.js} +1 -1
- package/dist/assets/{add-cell-with-ai-f_MnLCwI.js → add-cell-with-ai-BpD_790A.js} +6 -6
- package/dist/assets/{add-connection-dialog-CoE_PhuD.js → add-connection-dialog-B1BXCjex.js} +2 -2
- package/dist/assets/{agent-panel-BWZz24jK.js → agent-panel-CDFUc1uu.js} +1 -1
- package/dist/assets/{ai-model-dropdown-zm-2fXzf.js → ai-model-dropdown-i1vfGeVi.js} +2 -2
- package/dist/assets/{any-language-editor-BLCAD3yx.js → any-language-editor-BdTWdMCI.js} +1 -1
- package/dist/assets/{app-config-button-BhU0ulYs.js → app-config-button-AdV-QDD7.js} +1 -1
- package/dist/assets/{blob-iOBwGaP9.js → blob-BG8yoQmx.js} +1 -1
- package/dist/assets/{cell-editor-CAj1UKxx.js → cell-editor-BKJ06H5F.js} +5 -5
- package/dist/assets/cell-link-8Auigi8P.js +1 -0
- package/dist/assets/{cells-rA_jsZCj.js → cells-C6YGYGdi.js} +56 -56
- package/dist/assets/{chat-display-BmEPmhDP.js → chat-display-DhgI2MS9.js} +1 -1
- package/dist/assets/{chat-panel-Ce2Xh7dM.js → chat-panel-Cau71Ygr.js} +1 -1
- package/dist/assets/{chat-ui-DTy98rP3.js → chat-ui-C5ql1Up-.js} +1 -1
- package/dist/assets/code-visibility-BJImdpDi.js +1 -0
- package/dist/assets/coerce-DWjFeXOY.js +1 -0
- package/dist/assets/{column-preview-DU6dT80y.js → column-preview-BZ_iNhtJ.js} +1 -1
- package/dist/assets/{command-palette-BMqFKXVr.js → command-palette-DDC3kBet.js} +1 -1
- package/dist/assets/{common-lisZ0l6a.js → common-Sl3uthjg.js} +1 -1
- package/dist/assets/{components-Dvd1GnD6.js → components-3cou6Mka.js} +1 -1
- package/dist/assets/{components-CP_UoFtT.js → components-CskjK6xZ.js} +1 -1
- package/dist/assets/{components-CXfO-F47.js → components-YBmtV0p6.js} +1 -1
- package/dist/assets/{config-VPSq73n8.js → config-CA1V-wjh.js} +1 -1
- package/dist/assets/{config-DJLAC21v.js → config-DRIpOKqh.js} +1 -1
- package/dist/assets/config-Dy28fDwn.js +1 -0
- package/dist/assets/{datasource-Dr1xNKUn.js → datasource-DfbNb83e.js} +1 -1
- package/dist/assets/{dependency-graph-panel-B0uP-_id.js → dependency-graph-panel-L5cBE5sl.js} +1 -1
- package/dist/assets/{documentation-panel-DVl_ebWy.js → documentation-panel-D7S8JIc2.js} +1 -1
- package/dist/assets/{download-Bx55LMki.js → download-C03FOElq.js} +1 -1
- package/dist/assets/{edit-page-DlfCLUaq.js → edit-page-D_Rg0gQO.js} +6 -6
- package/dist/assets/{error-banner-Cd69vvyX.js → error-banner-COZ7SfYV.js} +1 -1
- package/dist/assets/{error-entries-DOWpdGvI.js → error-entries-CLnPDv7b.js} +1 -1
- package/dist/assets/{error-panel-DpbNKSLs.js → error-panel-D2xrYLue.js} +1 -1
- package/dist/assets/{errors-DCrsa1nH.js → errors-CNsgAw6m.js} +1 -1
- package/dist/assets/{feedback-button-B99ThNJW.js → feedback-button-DzPunIqU.js} +2 -2
- package/dist/assets/feedback-button-Kla7i5RT.js +1 -0
- package/dist/assets/{file-explorer-panel-Bo9eN3rW.js → file-explorer-panel-Uk4_QIJQ.js} +1 -1
- package/dist/assets/{file-name-input-ChnBsDE_.js → file-name-input-B4fRaQkI.js} +1 -1
- package/dist/assets/{floating-outline-MYqQW4Vp.js → floating-outline-B5MaM1JS.js} +1 -1
- package/dist/assets/{gallery-page-DSL0J4md.js → gallery-page-BefvdsM7.js} +1 -1
- package/dist/assets/{glide-data-editor-B1MWlKeM.js → glide-data-editor-C3eaYlXg.js} +1 -1
- package/dist/assets/{home-page-KrhQ55Vg.js → home-page-hmvKlc1w.js} +1 -1
- package/dist/assets/{hooks-BIogTGCr.js → hooks-DzML9nez.js} +1 -1
- package/dist/assets/index-ptK1gxmo.js +30 -0
- package/dist/assets/{layout-DOpdUwym.js → layout-Ck1RoL1B.js} +5 -5
- package/dist/assets/{logs-panel-y0XnfulJ.js → logs-panel-BRytaWaO.js} +1 -1
- package/dist/assets/{markdown-renderer-Q5AhbRU3.js → markdown-renderer-DX0Xrrfy.js} +1 -1
- package/dist/assets/{mermaid-a3xYymTy.js → mermaid-C4EiVqcd.js} +1 -1
- package/dist/assets/{name-cell-input-BNOnF6-0.js → name-cell-input-DlpTvPLa.js} +1 -1
- package/dist/assets/{outline-panel-Be7stWwv.js → outline-panel-B2WIpz-1.js} +1 -1
- package/dist/assets/{packages-panel-ClxOAEGy.js → packages-panel-CI5ff9b-.js} +1 -1
- package/dist/assets/{panel-accordion-state-GeMEdCPw.js → panel-accordion-state-BqSH5ttk.js} +1 -1
- package/dist/assets/panels-BCcZ_yLK.js +107 -0
- package/dist/assets/{process-output-DNFKMODr.js → process-output-f3C3PD0Z.js} +1 -1
- package/dist/assets/{readonly-code-display-YULOXf4h.js → readonly-code-display-MnMEoYW5.js} +1 -1
- package/dist/assets/{readonly-python-code-CadgXgRu.js → readonly-python-code-ggs4hAzC.js} +1 -1
- package/dist/assets/{renderShortcut-BkVJQ_Zh.js → renderShortcut-C1JwYcqW.js} +1 -1
- package/dist/assets/{reveal-component-BVAR7ayZ.js → reveal-component-PiV1eQk9.js} +1 -1
- package/dist/assets/run-page-AfZhtEh1.js +1 -0
- package/dist/assets/{runtime-details-state-DuChzsA6.js → runtime-details-state-CoL-p0MC.js} +1 -1
- package/dist/assets/{schemas-Dv42wLFq.js → schemas-ChRqMOTL.js} +1 -1
- package/dist/assets/{scratchpad-panel-By-ybhjg.js → scratchpad-panel-D2rWg4k8.js} +1 -1
- package/dist/assets/{secrets-panel-Bctbtkrp.js → secrets-panel-D7KN4tVs.js} +1 -1
- package/dist/assets/{session-panel-BmW6PH4z.js → session-panel-CnANhcTo.js} +1 -1
- package/dist/assets/{snippets-panel-CQ1dn3lA.js → snippets-panel-ClGHtEKQ.js} +1 -1
- package/dist/assets/{state-z3dlofZB.js → state-CbjBtkdd.js} +3 -3
- package/dist/assets/{state-Bfh_2SLl.js → state-DtGB7xeJ.js} +1 -1
- package/dist/assets/{terminal-Ck_vK-34.js → terminal-wnSHYT8N.js} +1 -1
- package/dist/assets/{tracing-BARlJ_Dk.js → tracing-Dz2rYOFL.js} +1 -1
- package/dist/assets/{tracing-panel-dgnr2fIN.js → tracing-panel-DqQY9V91.js} +2 -2
- package/dist/assets/{tree-actions-DAd80MbP.js → tree-actions-DnUokmva.js} +1 -1
- package/dist/assets/{useAddCell-DGstJK0S.js → useAddCell-IN24thff.js} +1 -1
- package/dist/assets/{useBoolean-BDD6_xQQ.js → useBoolean-d0Tty94N.js} +1 -1
- package/dist/assets/{useCellActionButton-DY9RlJTb.js → useCellActionButton-DmaFZ4oF.js} +1 -1
- package/dist/assets/{useDeleteCell-DFBGlSQk.js → useDeleteCell-fQlM46Y2.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-DydxsE5d.js → useDependencyPanelTab-ImXYINm_.js} +1 -1
- package/dist/assets/{useInstallPackage-iX1eufdl.js → useInstallPackage-BW06eter.js} +1 -1
- package/dist/assets/{useNotebookActions-DfvB7RD_.js → useNotebookActions-DHt3s-ia.js} +1 -1
- package/dist/assets/{useRunCells-BFjFX3hz.js → useRunCells-DSJPXE-j.js} +1 -1
- package/dist/assets/{useSplitCell-Tqv5pPtL.js → useSplitCell-DQyMF5gd.js} +1 -1
- package/dist/assets/{useTheme-COhSHkCj.js → useTheme-n8Dj1zH7.js} +1 -1
- package/dist/assets/{vega-component-CnadTgoE.js → vega-component-IouMe13t.js} +1 -1
- package/dist/index.html +32 -32
- package/package.json +1 -1
- package/src/__tests__/mount.test.ts +20 -0
- package/src/core/edit-app.tsx +2 -2
- package/src/core/host/__tests__/events.test.ts +46 -0
- package/src/core/host/__tests__/notebook-client.test.ts +191 -0
- package/src/core/host/connection.ts +81 -0
- package/src/core/host/events.ts +60 -0
- package/src/core/host/notebook-client.ts +226 -0
- package/src/core/run-app.tsx +2 -2
- package/src/core/runtime/__tests__/runtime.test.ts +55 -11
- package/src/core/runtime/runtime.ts +14 -1
- 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 +140 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +227 -133
- package/src/core/websocket/connection-state.ts +70 -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 +80 -0
- package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +64 -231
- package/src/mount.tsx +4 -0
- package/dist/assets/cell-link-1CynTVGQ.js +0 -1
- package/dist/assets/code-visibility-B-_GvwR5.js +0 -1
- package/dist/assets/coerce-CpE6kUdX.js +0 -1
- package/dist/assets/config-DX8Y2WbJ.js +0 -1
- package/dist/assets/feedback-button-DlqZkJwz.js +0 -1
- package/dist/assets/index-B3fcrmuf.js +0 -30
- package/dist/assets/panels-DfCmzLtT.js +0 -4
- package/dist/assets/run-page-Cxu1yV8Q.js +0 -1
- package/src/core/websocket/useWebSocket.tsx +0 -88
|
@@ -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,6 +36,9 @@ 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 {
|
|
@@ -52,15 +50,19 @@ import { notebookAtom } from "@/core/cells/cells";
|
|
|
52
50
|
import { AppConfigSchema } from "@/core/config/config-schema";
|
|
53
51
|
import { ConnectionNotice } from "@/components/editor/alerts/connection-notice";
|
|
54
52
|
import { kernelStartupErrorAtom } from "@/core/errors/state";
|
|
53
|
+
import type { RunEvent } from "@/core/host/protocol";
|
|
55
54
|
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
56
55
|
import { useRuntimeManager } from "@/core/runtime/config";
|
|
57
56
|
import { initialRunCompletedAtom } from "../../kernel/state";
|
|
58
57
|
import { connectionAtom, startupProgressAtom } from "../../network/connection";
|
|
59
|
-
import
|
|
58
|
+
import { generateSessionId } from "../../kernel/session";
|
|
59
|
+
import { RuntimeManager } from "../../runtime/runtime";
|
|
60
60
|
import { WebSocketClosedReason, ConnectionState } from "../types";
|
|
61
61
|
import type { IConnectionTransport } from "../transports/transport";
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
62
|
+
import { useNotebookSession } from "../useNotebookSession";
|
|
63
|
+
import { createConnectionTransport } from "../transports/create";
|
|
64
|
+
|
|
65
|
+
const SESSION_ID = generateSessionId();
|
|
64
66
|
|
|
65
67
|
interface MockTransport {
|
|
66
68
|
readyState: 0 | 1 | 2 | 3;
|
|
@@ -84,16 +86,47 @@ function makeTransport(
|
|
|
84
86
|
};
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
function getTransportHandlers() {
|
|
90
|
+
const result = vi.mocked(createConnectionTransport).mock.results.at(-1);
|
|
91
|
+
if (!result || result.type !== "return") {
|
|
92
|
+
throw new Error("No connection transport was created");
|
|
93
|
+
}
|
|
94
|
+
const transport = result.value;
|
|
95
|
+
function emit(
|
|
96
|
+
event: "open" | "close" | "error" | "message",
|
|
97
|
+
message: Event | CloseEvent | MessageEvent,
|
|
98
|
+
) {
|
|
99
|
+
const callback = vi
|
|
100
|
+
.mocked(transport.addEventListener)
|
|
101
|
+
.mock.calls.find(([name]) => name === event)?.[1];
|
|
102
|
+
callback?.(message);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
onError: (event: Event) => emit("error", event),
|
|
106
|
+
onOpen: (event: Event) => emit("open", event),
|
|
107
|
+
onClose: (event: CloseEvent) => emit("close", event),
|
|
108
|
+
onMessage: (event: MessageEvent) => emit("message", event),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
87
112
|
function makeRuntimeManager(
|
|
88
113
|
reconcileFromHealth = vi.fn<() => Promise<boolean>>().mockResolvedValue(true),
|
|
89
114
|
) {
|
|
90
|
-
|
|
115
|
+
const runtime = new RuntimeManager(
|
|
116
|
+
{ url: "http://localhost", lazy: true },
|
|
117
|
+
true,
|
|
118
|
+
);
|
|
119
|
+
vi.spyOn(runtime, "reconcileFromHealth").mockImplementation(
|
|
91
120
|
reconcileFromHealth,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
);
|
|
122
|
+
vi.spyOn(runtime, "probeHealth").mockResolvedValue(true);
|
|
123
|
+
vi.spyOn(runtime, "getWsURL").mockReturnValue(new URL("ws://localhost/ws"));
|
|
124
|
+
// Keep the initial connection pending; these tests exercise session retries.
|
|
125
|
+
vi.spyOn(runtime, "waitForHealthy").mockReturnValue(
|
|
126
|
+
new Deferred<void>().promise,
|
|
127
|
+
);
|
|
128
|
+
vi.spyOn(runtime, "isSameOrigin", "get").mockReturnValue(false);
|
|
129
|
+
return runtime;
|
|
97
130
|
}
|
|
98
131
|
|
|
99
132
|
function renderConnectionHook(store: ReturnType<typeof createStore>) {
|
|
@@ -104,16 +137,16 @@ function renderConnectionHook(store: ReturnType<typeof createStore>) {
|
|
|
104
137
|
);
|
|
105
138
|
return renderHook(
|
|
106
139
|
() =>
|
|
107
|
-
|
|
108
|
-
sessionId:
|
|
140
|
+
useNotebookSession({
|
|
141
|
+
sessionId: SESSION_ID,
|
|
109
142
|
autoInstantiate: false,
|
|
110
|
-
setCells: ()
|
|
143
|
+
setCells: vi.fn(),
|
|
111
144
|
}),
|
|
112
145
|
{ wrapper },
|
|
113
146
|
);
|
|
114
147
|
}
|
|
115
148
|
|
|
116
|
-
describe("
|
|
149
|
+
describe("useNotebookSession.reconnect()", () => {
|
|
117
150
|
let transport: MockTransport;
|
|
118
151
|
let isHealthy: Mock<() => Promise<boolean>>;
|
|
119
152
|
let store: ReturnType<typeof createStore>;
|
|
@@ -127,12 +160,8 @@ describe("useMarimoKernelConnection.reconnect()", () => {
|
|
|
127
160
|
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
128
161
|
reason: "kernel not found",
|
|
129
162
|
});
|
|
130
|
-
vi.mocked(
|
|
131
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
132
|
-
makeRuntimeManager(isHealthy) as unknown as ReturnType<
|
|
133
|
-
typeof useRuntimeManager
|
|
134
|
-
>,
|
|
135
|
-
);
|
|
163
|
+
vi.mocked(createConnectionTransport).mockReturnValue(transport);
|
|
164
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager(isHealthy));
|
|
136
165
|
});
|
|
137
166
|
|
|
138
167
|
function renderUseHook() {
|
|
@@ -188,17 +217,29 @@ describe("useMarimoKernelConnection.reconnect()", () => {
|
|
|
188
217
|
});
|
|
189
218
|
});
|
|
190
219
|
|
|
191
|
-
describe("
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
220
|
+
describe("useNotebookSession messages", () => {
|
|
221
|
+
let store: ReturnType<typeof createStore>;
|
|
222
|
+
|
|
223
|
+
beforeEach(() => {
|
|
224
|
+
store = createStore();
|
|
225
|
+
vi.mocked(createConnectionTransport).mockClear();
|
|
226
|
+
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
195
227
|
makeTransport(WebSocket.OPEN),
|
|
196
228
|
);
|
|
197
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
198
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
199
|
-
);
|
|
229
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
200
230
|
renderConnectionHook(store);
|
|
201
|
-
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
function receive(payload: unknown) {
|
|
234
|
+
const options = getTransportHandlers();
|
|
235
|
+
act(() => {
|
|
236
|
+
options.onMessage(
|
|
237
|
+
new MessageEvent("message", { data: JSON.stringify(payload) }),
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
it("replaces attachment snapshots, including the last detach", () => {
|
|
202
243
|
const agent: Attachment = {
|
|
203
244
|
id: "a1",
|
|
204
245
|
kind: "agent",
|
|
@@ -215,46 +256,157 @@ describe("useMarimoKernelConnection messages", () => {
|
|
|
215
256
|
notebook_id: "nb-test",
|
|
216
257
|
};
|
|
217
258
|
for (const attachments of [[agent, client], [client], []]) {
|
|
218
|
-
|
|
219
|
-
options.onMessage(
|
|
220
|
-
new MessageEvent("message", {
|
|
221
|
-
data: JSON.stringify({
|
|
222
|
-
op: "attachments",
|
|
223
|
-
data: { op: "attachments", attachments },
|
|
224
|
-
}),
|
|
225
|
-
}),
|
|
226
|
-
),
|
|
227
|
-
);
|
|
259
|
+
receive({ data: { op: "attachments", attachments } });
|
|
228
260
|
expect(store.get(attachmentsAtom)).toEqual(attachments);
|
|
229
261
|
}
|
|
230
262
|
});
|
|
231
263
|
|
|
232
|
-
it("
|
|
264
|
+
it.each(["{broken", "null"])(
|
|
265
|
+
"reports a malformed legacy frame and continues applying notifications: %s",
|
|
266
|
+
(data) => {
|
|
267
|
+
const toast = vi.spyOn(Toast, "toast");
|
|
268
|
+
try {
|
|
269
|
+
receive({ data: { op: "reconnected" } });
|
|
270
|
+
act(() =>
|
|
271
|
+
getTransportHandlers().onMessage(
|
|
272
|
+
new MessageEvent("message", { data }),
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
expect(toast).toHaveBeenCalledExactlyOnceWith({
|
|
276
|
+
title: "Failed to handle message",
|
|
277
|
+
description: expect.any(String),
|
|
278
|
+
variant: "danger",
|
|
279
|
+
});
|
|
280
|
+
expect(store.get(connectionAtom)).toEqual({
|
|
281
|
+
state: ConnectionState.OPEN,
|
|
282
|
+
});
|
|
283
|
+
receive({
|
|
284
|
+
data: {
|
|
285
|
+
op: "completed-run",
|
|
286
|
+
run_id: "r1",
|
|
287
|
+
outcome: "succeeded",
|
|
288
|
+
error: null,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
expect(store.get(initialRunCompletedAtom)).toBe(true);
|
|
292
|
+
} finally {
|
|
293
|
+
toast.mockRestore();
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
it.each(["session", "host"])(
|
|
299
|
+
"records completion of the initial run from the %s stream",
|
|
300
|
+
(stream) => {
|
|
301
|
+
const notification = {
|
|
302
|
+
op: "completed-run",
|
|
303
|
+
run_id: null,
|
|
304
|
+
outcome: "succeeded",
|
|
305
|
+
error: null,
|
|
306
|
+
} satisfies NotificationPayload["data"];
|
|
307
|
+
expect(store.get(initialRunCompletedAtom)).toBe(false);
|
|
308
|
+
receive(
|
|
309
|
+
stream === "host"
|
|
310
|
+
? { id: "42", event: "marimo", data: { marimo: notification } }
|
|
311
|
+
: { op: notification.op, data: notification },
|
|
312
|
+
);
|
|
313
|
+
expect(store.get(initialRunCompletedAtom)).toBe(true);
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
it("ignores other host events before handling the next notification", () => {
|
|
318
|
+
const errors = vi.spyOn(Logger, "error").mockImplementation(vi.fn());
|
|
319
|
+
try {
|
|
320
|
+
receive({ id: "1", event: "ready", data: {} });
|
|
321
|
+
receive({
|
|
322
|
+
id: "2",
|
|
323
|
+
event: "run",
|
|
324
|
+
data: {
|
|
325
|
+
run: {
|
|
326
|
+
id: "r1",
|
|
327
|
+
notebook_id: "n1",
|
|
328
|
+
caused_by: null,
|
|
329
|
+
status: "completed",
|
|
330
|
+
outcome: "succeeded",
|
|
331
|
+
started_at: "2026-10-19T12:00:00Z",
|
|
332
|
+
completed_at: "2026-10-19T12:00:01Z",
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
} satisfies RunEvent);
|
|
336
|
+
receive({
|
|
337
|
+
id: "3",
|
|
338
|
+
event: "future-event",
|
|
339
|
+
data: { marimo: { op: "completed-run", run_id: "r1" } },
|
|
340
|
+
});
|
|
341
|
+
receive({ id: "4", event: "another-event", data: null });
|
|
342
|
+
expect(store.get(initialRunCompletedAtom)).toBe(false);
|
|
343
|
+
receive({
|
|
344
|
+
id: "5",
|
|
345
|
+
event: "marimo",
|
|
346
|
+
data: {
|
|
347
|
+
marimo: {
|
|
348
|
+
op: "completed-run",
|
|
349
|
+
run_id: "r1",
|
|
350
|
+
outcome: "succeeded",
|
|
351
|
+
error: null,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
expect(store.get(initialRunCompletedAtom)).toBe(true);
|
|
356
|
+
expect(errors).not.toHaveBeenCalled();
|
|
357
|
+
} finally {
|
|
358
|
+
errors.mockRestore();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it.each(["kernel-ready", "reconnected"])(
|
|
364
|
+
"waits for %s before marking the session connected",
|
|
365
|
+
async (op) => {
|
|
233
366
|
const store = createStore();
|
|
234
|
-
|
|
235
|
-
vi.mocked(
|
|
367
|
+
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
368
|
+
vi.mocked(createConnectionTransport).mockClear();
|
|
369
|
+
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
236
370
|
makeTransport(WebSocket.OPEN),
|
|
237
371
|
);
|
|
238
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
239
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
240
|
-
);
|
|
372
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
241
373
|
renderConnectionHook(store);
|
|
374
|
+
const options = getTransportHandlers();
|
|
375
|
+
|
|
376
|
+
await act(async () => {
|
|
377
|
+
await options.onOpen(new Event("open"));
|
|
378
|
+
});
|
|
379
|
+
expect(store.get(connectionAtom).state).toBe(ConnectionState.CONNECTING);
|
|
242
380
|
|
|
243
|
-
const options = vi.mocked(useConnectionTransport).mock.calls.at(-1)?.[0];
|
|
244
|
-
expect(store.get(initialRunCompletedAtom)).toBe(false);
|
|
245
381
|
act(() => {
|
|
246
|
-
options
|
|
382
|
+
options.onMessage(
|
|
247
383
|
new MessageEvent("message", {
|
|
248
384
|
data: JSON.stringify({
|
|
249
|
-
op
|
|
250
|
-
data: {
|
|
385
|
+
op,
|
|
386
|
+
data: {
|
|
387
|
+
op,
|
|
388
|
+
cell_ids: [],
|
|
389
|
+
codes: [],
|
|
390
|
+
names: [],
|
|
391
|
+
configs: [],
|
|
392
|
+
layout: null,
|
|
393
|
+
resumed: true,
|
|
394
|
+
ui_values: {},
|
|
395
|
+
last_executed_code: {},
|
|
396
|
+
last_execution_time: {},
|
|
397
|
+
app_config: { width: "normal" },
|
|
398
|
+
kiosk: false,
|
|
399
|
+
capabilities: { terminal: false },
|
|
400
|
+
auto_instantiated: false,
|
|
401
|
+
consumer_capabilities: { edit: true, interact: true },
|
|
402
|
+
},
|
|
251
403
|
}),
|
|
252
404
|
}),
|
|
253
405
|
);
|
|
254
406
|
});
|
|
255
|
-
expect(store.get(
|
|
256
|
-
}
|
|
257
|
-
|
|
407
|
+
expect(store.get(connectionAtom).state).toBe(ConnectionState.OPEN);
|
|
408
|
+
},
|
|
409
|
+
);
|
|
258
410
|
|
|
259
411
|
describe("attachment snapshots across browser connections", () => {
|
|
260
412
|
const agent: Attachment = {
|
|
@@ -271,12 +423,10 @@ describe("attachment snapshots across browser connections", () => {
|
|
|
271
423
|
store.set(connectionAtom, { state: ConnectionState.OPEN });
|
|
272
424
|
store.set(attachmentsAtom, [agent]);
|
|
273
425
|
const transport = makeTransport(WebSocket.OPEN);
|
|
274
|
-
vi.mocked(
|
|
275
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
276
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
277
|
-
);
|
|
426
|
+
vi.mocked(createConnectionTransport).mockReturnValue(transport);
|
|
427
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
278
428
|
renderConnectionHook(store);
|
|
279
|
-
const options =
|
|
429
|
+
const options = getTransportHandlers();
|
|
280
430
|
const receive = (data: NotificationPayload["data"]) =>
|
|
281
431
|
act(() => {
|
|
282
432
|
options.onMessage(
|
|
@@ -351,56 +501,6 @@ describe("attachment snapshots across browser connections", () => {
|
|
|
351
501
|
});
|
|
352
502
|
});
|
|
353
503
|
|
|
354
|
-
it.each(["kernel-ready", "reconnected"])(
|
|
355
|
-
"waits for %s before marking the session connected",
|
|
356
|
-
async (op) => {
|
|
357
|
-
const store = createStore();
|
|
358
|
-
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
359
|
-
vi.mocked(useConnectionTransport).mockClear();
|
|
360
|
-
vi.mocked(useConnectionTransport).mockReturnValue(
|
|
361
|
-
makeTransport(WebSocket.OPEN),
|
|
362
|
-
);
|
|
363
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
364
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
365
|
-
);
|
|
366
|
-
renderConnectionHook(store);
|
|
367
|
-
const options = vi.mocked(useConnectionTransport).mock.calls.at(-1)![0];
|
|
368
|
-
|
|
369
|
-
await act(async () => {
|
|
370
|
-
await options.onOpen(new Event("open"));
|
|
371
|
-
});
|
|
372
|
-
expect(store.get(connectionAtom).state).toBe(ConnectionState.CONNECTING);
|
|
373
|
-
|
|
374
|
-
act(() => {
|
|
375
|
-
options.onMessage(
|
|
376
|
-
new MessageEvent("message", {
|
|
377
|
-
data: JSON.stringify({
|
|
378
|
-
op,
|
|
379
|
-
data: {
|
|
380
|
-
op,
|
|
381
|
-
cell_ids: [],
|
|
382
|
-
codes: [],
|
|
383
|
-
names: [],
|
|
384
|
-
configs: [],
|
|
385
|
-
layout: null,
|
|
386
|
-
resumed: true,
|
|
387
|
-
ui_values: {},
|
|
388
|
-
last_executed_code: {},
|
|
389
|
-
last_execution_time: {},
|
|
390
|
-
app_config: { width: "normal" },
|
|
391
|
-
kiosk: false,
|
|
392
|
-
capabilities: { terminal: false },
|
|
393
|
-
auto_instantiated: false,
|
|
394
|
-
consumer_capabilities: { edit: true, interact: true },
|
|
395
|
-
},
|
|
396
|
-
}),
|
|
397
|
-
}),
|
|
398
|
-
);
|
|
399
|
-
});
|
|
400
|
-
expect(store.get(connectionAtom).state).toBe(ConnectionState.OPEN);
|
|
401
|
-
},
|
|
402
|
-
);
|
|
403
|
-
|
|
404
504
|
it.each([true, false])(
|
|
405
505
|
"keeps attachments only when kernel-ready resumes the session: %s",
|
|
406
506
|
(resumed) => {
|
|
@@ -416,14 +516,12 @@ it.each([true, false])(
|
|
|
416
516
|
},
|
|
417
517
|
];
|
|
418
518
|
store.set(attachmentsAtom, attachments);
|
|
419
|
-
vi.mocked(
|
|
519
|
+
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
420
520
|
makeTransport(WebSocket.OPEN),
|
|
421
521
|
);
|
|
422
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
423
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
424
|
-
);
|
|
522
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
425
523
|
renderConnectionHook(store);
|
|
426
|
-
const options =
|
|
524
|
+
const options = getTransportHandlers();
|
|
427
525
|
act(() =>
|
|
428
526
|
options.onMessage(
|
|
429
527
|
new MessageEvent("message", {
|
|
@@ -462,15 +560,13 @@ describe("connection notice", () => {
|
|
|
462
560
|
const store = createStore();
|
|
463
561
|
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
464
562
|
const transport = makeTransport(WebSocket.OPEN);
|
|
465
|
-
vi.mocked(
|
|
466
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
467
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
468
|
-
);
|
|
563
|
+
vi.mocked(createConnectionTransport).mockReturnValue(transport);
|
|
564
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
469
565
|
const Connection = () => {
|
|
470
|
-
const { reconnect } =
|
|
471
|
-
sessionId:
|
|
566
|
+
const { reconnect } = useNotebookSession({
|
|
567
|
+
sessionId: SESSION_ID,
|
|
472
568
|
autoInstantiate: false,
|
|
473
|
-
setCells: ()
|
|
569
|
+
setCells: vi.fn(),
|
|
474
570
|
});
|
|
475
571
|
return (
|
|
476
572
|
<ConnectionNotice
|
|
@@ -486,7 +582,7 @@ describe("connection notice", () => {
|
|
|
486
582
|
</ErrorBoundary>
|
|
487
583
|
</JotaiProvider>,
|
|
488
584
|
);
|
|
489
|
-
const options =
|
|
585
|
+
const options = getTransportHandlers();
|
|
490
586
|
const send = (data: NotificationPayload["data"]) =>
|
|
491
587
|
act(() => {
|
|
492
588
|
options.onMessage(
|
|
@@ -675,15 +771,13 @@ describe("connection notice", () => {
|
|
|
675
771
|
|
|
676
772
|
it("replaces environment state from snapshots, then continues live progress", () => {
|
|
677
773
|
const store = createStore();
|
|
678
|
-
vi.mocked(
|
|
679
|
-
vi.mocked(
|
|
774
|
+
vi.mocked(createConnectionTransport).mockClear();
|
|
775
|
+
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
680
776
|
makeTransport(WebSocket.OPEN),
|
|
681
777
|
);
|
|
682
|
-
vi.mocked(useRuntimeManager).mockReturnValue(
|
|
683
|
-
makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
|
|
684
|
-
);
|
|
778
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
685
779
|
renderConnectionHook(store);
|
|
686
|
-
const options =
|
|
780
|
+
const options = getTransportHandlers();
|
|
687
781
|
function receive(data: NotificationPayload["data"]) {
|
|
688
782
|
act(() =>
|
|
689
783
|
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
|
+
}
|
|
@@ -108,8 +108,6 @@ describe("SseTransport", () => {
|
|
|
108
108
|
transport.reconnect();
|
|
109
109
|
await vi.waitFor(() => expect(opens).toHaveLength(1));
|
|
110
110
|
|
|
111
|
-
// Stop retrying when the consumer treats the close as terminal,
|
|
112
|
-
// mirroring useMarimoKernelConnection's terminal path.
|
|
113
111
|
transport.addEventListener("close", () => transport.close());
|
|
114
112
|
push('event: close\ndata: {"code": 1000, "reason": "MARIMO_SHUTDOWN"}\n\n');
|
|
115
113
|
await vi.waitFor(() => expect(closes).toHaveLength(1));
|
|
@@ -146,8 +144,7 @@ describe("SseTransport", () => {
|
|
|
146
144
|
fetchMock
|
|
147
145
|
.mockResolvedValueOnce(sseResponse(first.stream))
|
|
148
146
|
.mockResolvedValueOnce(sseResponse(second.stream));
|
|
149
|
-
//
|
|
150
|
-
// synchronously from inside the close handler.
|
|
147
|
+
// Reconnect synchronously from the close handler.
|
|
151
148
|
transport.addEventListener("close", () => transport.reconnect());
|
|
152
149
|
|
|
153
150
|
transport.reconnect();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { createPyodideConnection } from "../../wasm/bridge";
|
|
4
|
+
import { isWasm } from "../../wasm/utils";
|
|
5
|
+
import { BasicTransport } from "./basic";
|
|
6
|
+
import { SseTransport } from "./sse";
|
|
7
|
+
import { WsTransport } from "./ws";
|
|
8
|
+
import type { IConnectionTransport } from "./transport";
|
|
9
|
+
|
|
10
|
+
export interface ConnectionTransportOptions {
|
|
11
|
+
url: () => string;
|
|
12
|
+
static: boolean;
|
|
13
|
+
transportType: "websocket" | "sse";
|
|
14
|
+
headers: () => Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createConnectionTransport(
|
|
18
|
+
options: ConnectionTransportOptions,
|
|
19
|
+
): IConnectionTransport {
|
|
20
|
+
if (options.static) {
|
|
21
|
+
return BasicTransport.empty();
|
|
22
|
+
}
|
|
23
|
+
if (isWasm()) {
|
|
24
|
+
return createPyodideConnection();
|
|
25
|
+
}
|
|
26
|
+
// Read the URL lazily to follow runtime redirects.
|
|
27
|
+
if (options.transportType === "sse") {
|
|
28
|
+
return new SseTransport(options.url, options.headers);
|
|
29
|
+
}
|
|
30
|
+
return new WsTransport(options.url);
|
|
31
|
+
}
|
|
@@ -1,30 +1,19 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
export type ConnectionEvent = "open" | "close" | "message" | "error";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
import type {
|
|
4
|
+
NotebookTransport,
|
|
5
|
+
ConnectionEvent,
|
|
6
|
+
ConnectionTransportCallback,
|
|
7
|
+
IConnectionTransportMap,
|
|
8
|
+
} from "../../host/connection";
|
|
9
|
+
export type {
|
|
10
|
+
ConnectionEvent,
|
|
11
|
+
ConnectionTransportCallback,
|
|
12
|
+
IConnectionTransportMap,
|
|
13
|
+
} from "../../host/connection";
|
|
14
14
|
|
|
15
|
-
export interface IConnectionTransport {
|
|
16
|
-
reconnect(code?: number | undefined, reason?: string | undefined): void;
|
|
17
|
-
close(): void;
|
|
15
|
+
export interface IConnectionTransport extends NotebookTransport {
|
|
18
16
|
send(data: string | ArrayBuffer | Blob | ArrayBufferView<ArrayBuffer>): void;
|
|
19
|
-
addEventListener<T extends ConnectionEvent>(
|
|
20
|
-
event: T,
|
|
21
|
-
callback: ConnectionTransportCallback<T>,
|
|
22
|
-
): void;
|
|
23
|
-
removeEventListener<T extends ConnectionEvent>(
|
|
24
|
-
event: T,
|
|
25
|
-
callback: ConnectionTransportCallback<T>,
|
|
26
|
-
): void;
|
|
27
|
-
readyState: WebSocket["readyState"];
|
|
28
17
|
}
|
|
29
18
|
|
|
30
19
|
export class ConnectionSubscriptions {
|