@marimo-team/frontend 0.25.3-dev80 → 0.25.3-dev82
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-cD3S0wEA.js → CellStatus-DXroYLOP.js} +1 -1
- package/dist/assets/{ErrorBoundary-DJhsFCsX.js → ErrorBoundary-DaQ9THH_.js} +2 -2
- package/dist/assets/{JsonOutput-Pm6OCP3J.js → JsonOutput-VxzHyItt.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-DlEjLCZ-.js → MarimoErrorOutput-D-hLaDA-.js} +1 -1
- package/dist/assets/{RenderHTML-CfQ6i3QV.js → RenderHTML-DjOEZL25.js} +1 -1
- package/dist/assets/{RunButton-DvuvkgrG.js → RunButton-CuhNTo5_.js} +1 -1
- package/dist/assets/{add-cell-with-ai-By51zSlC.js → add-cell-with-ai-BbVntTh7.js} +1 -1
- package/dist/assets/{add-connection-dialog-D7EYSraq.js → add-connection-dialog-Qf63aKV6.js} +1 -1
- package/dist/assets/{agent-panel-aCWa3Yai.js → agent-panel-DMOfF3qF.js} +1 -1
- package/dist/assets/{ai-model-dropdown-bwTRyFn4.js → ai-model-dropdown-B15l0R62.js} +2 -2
- package/dist/assets/{app-config-button-DGhsM0xw.js → app-config-button-BIMB8qjD.js} +1 -1
- package/dist/assets/{blob-e6VlOmQP.js → blob-De9GR2s6.js} +1 -1
- package/dist/assets/{cell-editor-M8dEnDzk.js → cell-editor-Di0cSFhC.js} +32 -11
- package/dist/assets/{cell-link-DFqfW-Ym.js → cell-link-DR3W2HC7.js} +1 -1
- package/dist/assets/{cells-T9_Y4Jlv.js → cells-styV7IX8.js} +6 -6
- package/dist/assets/{chat-display-DXn2Yrjr.js → chat-display-BzdYdN39.js} +1 -1
- package/dist/assets/{chat-panel-D3iUyZId.js → chat-panel-De3YzkSF.js} +1 -1
- package/dist/assets/{chat-ui-YuJtv3UJ.js → chat-ui-NTNapdtP.js} +1 -1
- package/dist/assets/{code-visibility-CV3S4xuf.js → code-visibility-csc3tgA0.js} +1 -1
- package/dist/assets/{column-preview-C6uXRJry.js → column-preview-DkH-oD8f.js} +1 -1
- package/dist/assets/{command-palette-CGNUQdgW.js → command-palette-Dg_xstl0.js} +1 -1
- package/dist/assets/{common-DeeLgjWQ.js → common-D7VAS6rd.js} +1 -1
- package/dist/assets/{components-DyAF3_Nf.js → components-CNmvC781.js} +1 -1
- package/dist/assets/config-BQT0uYUo.js +1 -0
- package/dist/assets/{config-CjQ4WJnJ.js → config-BbRbT9Ww.js} +1 -1
- package/dist/assets/{datasource-gqFgG9Qd.js → datasource-C3yfeDa2.js} +1 -1
- package/dist/assets/{dependency-graph-panel-B3T9kSTs.js → dependency-graph-panel-CjoA6L0w.js} +1 -1
- package/dist/assets/{documentation-panel-BBpGbT-f.js → documentation-panel-DHWFOiG1.js} +1 -1
- package/dist/assets/{download-C8Gw-D0Y.js → download-BrCpRZKN.js} +1 -1
- package/dist/assets/{edit-page-CBftLPze.js → edit-page-BPMo_x-T.js} +4 -4
- package/dist/assets/{error-entries-uyqOG5tg.js → error-entries-BkEAmmnD.js} +1 -1
- package/dist/assets/{error-panel-Cli5zPWi.js → error-panel-BhTfPA_i.js} +1 -1
- package/dist/assets/feedback-button-BAcClv7c.js +1 -0
- package/dist/assets/{feedback-button-CvrwBGr7.js → feedback-button-D9InK87G.js} +1 -1
- package/dist/assets/{file-explorer-panel-BSU5ultY.js → file-explorer-panel-BSd_ZbUC.js} +1 -1
- package/dist/assets/{file-name-input-CDrRlIiX.js → file-name-input-RHURglL_.js} +1 -1
- package/dist/assets/{floating-outline-GQD9km0W.js → floating-outline-Cm49rYYe.js} +1 -1
- package/dist/assets/{gallery-page-B-5H1fEt.js → gallery-page-DdMdOtdv.js} +1 -1
- package/dist/assets/{glide-data-editor-B1kpoetH.js → glide-data-editor-eiSlEUyR.js} +1 -1
- package/dist/assets/{home-page-3qsPTb4v.js → home-page-DwWvBB0R.js} +1 -1
- package/dist/assets/{hooks-BrFYiTJS.js → hooks-CHeUZHPp.js} +1 -1
- package/dist/assets/{index-D0IWODMU.js → index-DckSn2w6.js} +8 -8
- package/dist/assets/{layout-CGC8ajpe.js → layout-_lp06T0C.js} +3 -3
- package/dist/assets/{logs-panel-CdUc5vtA.js → logs-panel-CAAk72Gb.js} +1 -1
- package/dist/assets/{markdown-renderer-pKc_bFac.js → markdown-renderer-CAyCgogC.js} +1 -1
- package/dist/assets/{name-cell-input-csH3Cy54.js → name-cell-input-DA8hU9XT.js} +1 -1
- package/dist/assets/{outline-panel-DhsHYTuo.js → outline-panel-Cc5oQ4LL.js} +1 -1
- package/dist/assets/{packages-panel-BtkXVz3L.js → packages-panel-DTfKez6I.js} +1 -1
- package/dist/assets/{panel-accordion-state-Bco6nsUI.js → panel-accordion-state-BZHex38W.js} +1 -1
- package/dist/assets/panels-DU-SaRHy.js +105 -0
- package/dist/assets/{process-output-B2-bT4cq.js → process-output-CrDc_O1D.js} +1 -1
- package/dist/assets/{readonly-code-display-Dsp6vWPh.js → readonly-code-display-BR-_jUj8.js} +1 -1
- package/dist/assets/{readonly-python-code-B81AjzXM.js → readonly-python-code-D7jcy9ds.js} +1 -1
- package/dist/assets/{reveal-component-Cxo3hLGA.js → reveal-component-eLftBwOe.js} +1 -1
- package/dist/assets/{run-page-CLP1s8uX.js → run-page-mURyATDJ.js} +1 -1
- package/dist/assets/{runtime-details-state-CznIe40L.js → runtime-details-state-hb7A1jgR.js} +1 -1
- package/dist/assets/{scratchpad-panel-yHIW63VQ.js → scratchpad-panel-B_gEnoqj.js} +1 -1
- package/dist/assets/{session-panel-C7-wmZhc.js → session-panel-DmDjaPsN.js} +1 -1
- package/dist/assets/{snippets-panel-BARpOPyS.js → snippets-panel-o9KL5jMi.js} +1 -1
- package/dist/assets/state-BkAqJoW7.js +3 -0
- package/dist/assets/{state-Bye6rCo4.js → state-Os7LSUUZ.js} +1 -1
- package/dist/assets/{terminal-Bhcn_y1N.js → terminal-BI85khrS.js} +1 -1
- package/dist/assets/{tracing-DDohxo24.js → tracing-AsNGuDsh.js} +1 -1
- package/dist/assets/{tracing-panel-DYhGDAo5.js → tracing-panel-CBcmea6L.js} +2 -2
- package/dist/assets/{tree-actions-Cb-nUK1F.js → tree-actions-Cx5EDpA6.js} +1 -1
- package/dist/assets/{useAddCell-Bielffe0.js → useAddCell-uJJYtgwa.js} +1 -1
- package/dist/assets/{useBoolean-CMgj9hnq.js → useBoolean-8wlrTeus.js} +1 -1
- package/dist/assets/{useCellActionButton-DAcQhse_.js → useCellActionButton-CmQ-OcPa.js} +1 -1
- package/dist/assets/{useDeleteCell-BV4VemCv.js → useDeleteCell-CXgOCDqY.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-PtXIVh53.js → useDependencyPanelTab-Ch6gm7uM.js} +1 -1
- package/dist/assets/{useInstallPackage-C7neMPwh.js → useInstallPackage-CYQoMdMQ.js} +1 -1
- package/dist/assets/{useNotebookActions-DZBB98fP.js → useNotebookActions-DGShHZ_1.js} +1 -1
- package/dist/assets/{useRunCells-BFeZMi7f.js → useRunCells-DAfDmOMC.js} +1 -1
- package/dist/assets/{useSplitCell-DllkNFke.js → useSplitCell-NLULgcq7.js} +1 -1
- package/dist/assets/{vega-component-D7JFthDD.js → vega-component-BKJU3uaB.js} +1 -1
- package/dist/index.html +24 -24
- package/package.json +1 -1
- package/src/core/host/AGENTS.md +6 -5
- package/src/core/host/__tests__/notebook-client.test.ts +368 -350
- package/src/core/host/connection.ts +1 -40
- package/src/core/host/notebook-client.ts +129 -186
- package/src/core/wasm/bridge.ts +0 -8
- package/src/core/websocket/__tests__/connection-state.test.ts +0 -12
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +133 -59
- package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +152 -250
- package/src/core/websocket/connection-state.ts +0 -10
- package/src/core/websocket/transports/__tests__/notebook.test.ts +502 -0
- package/src/core/websocket/transports/notebook.ts +213 -0
- package/src/core/websocket/useNotebookClient.ts +28 -17
- package/dist/assets/config-DzdxZb17.js +0 -1
- package/dist/assets/feedback-button-DknbRCN_.js +0 -1
- package/dist/assets/panels-D1i80TGn.js +0 -107
- package/dist/assets/state-BRYi026f.js +0 -3
- package/dist/assets/ws-BXWY541V.js +0 -22
- package/src/core/host/__tests__/events.test.ts +0 -49
- package/src/core/host/events.ts +0 -68
- package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
- package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
- package/src/core/websocket/transports/__tests__/sse.test.ts +0 -352
- package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
- package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
- package/src/core/websocket/transports/basic.ts +0 -102
- package/src/core/websocket/transports/create.ts +0 -31
- package/src/core/websocket/transports/sse.ts +0 -320
- package/src/core/websocket/transports/transport.ts +0 -50
- package/src/core/websocket/transports/ws.ts +0 -98
|
@@ -37,45 +37,6 @@ export type ConnectionStatus =
|
|
|
37
37
|
|
|
38
38
|
/** The recovery policy for a closed connection. */
|
|
39
39
|
export interface CloseDecision {
|
|
40
|
-
kind: "terminal" | "
|
|
40
|
+
kind: "terminal" | "retry";
|
|
41
41
|
status: ConnectionStatus;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
export type ConnectionEvent = "open" | "close" | "message" | "error";
|
|
45
|
-
|
|
46
|
-
export interface IConnectionTransportMap {
|
|
47
|
-
open: WebSocketEventMap["open"];
|
|
48
|
-
message: WebSocketEventMap["message"];
|
|
49
|
-
close: WebSocketEventMap["close"];
|
|
50
|
-
error: WebSocketEventMap["error"];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type ConnectionTransportCallback<T extends ConnectionEvent> = (
|
|
54
|
-
data: IConnectionTransportMap[T],
|
|
55
|
-
) => void;
|
|
56
|
-
|
|
57
|
-
/** Transport readiness, using the WebSocket readyState values. */
|
|
58
|
-
export const TransportReadyState = {
|
|
59
|
-
CONNECTING: 0,
|
|
60
|
-
OPEN: 1,
|
|
61
|
-
CLOSING: 2,
|
|
62
|
-
CLOSED: 3,
|
|
63
|
-
} as const;
|
|
64
|
-
|
|
65
|
-
export type TransportReadyState =
|
|
66
|
-
(typeof TransportReadyState)[keyof typeof TransportReadyState];
|
|
67
|
-
|
|
68
|
-
/** A reconnectable source of notebook messages. */
|
|
69
|
-
export interface NotebookTransport {
|
|
70
|
-
reconnect(code?: number | undefined, reason?: string | undefined): void;
|
|
71
|
-
close(): void;
|
|
72
|
-
addEventListener<T extends ConnectionEvent>(
|
|
73
|
-
event: T,
|
|
74
|
-
callback: ConnectionTransportCallback<T>,
|
|
75
|
-
): void;
|
|
76
|
-
removeEventListener<T extends ConnectionEvent>(
|
|
77
|
-
event: T,
|
|
78
|
-
callback: ConnectionTransportCallback<T>,
|
|
79
|
-
): void;
|
|
80
|
-
readyState: TransportReadyState;
|
|
81
|
-
}
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { Logger } from "@/utils/Logger";
|
|
4
|
+
import {
|
|
5
|
+
createNotebookReducer,
|
|
6
|
+
type NotebookEvent,
|
|
7
|
+
NotebookEvents,
|
|
8
|
+
type NotebookTransport,
|
|
9
|
+
StreamStopped,
|
|
10
|
+
} from "@marimo-team/client";
|
|
4
11
|
import type { Attachment, NotebookOutput } from "@marimo-team/client/protocol";
|
|
5
|
-
import { createNotebookReducer } from "@marimo-team/client";
|
|
6
12
|
import { type Atom, atom, type createStore } from "jotai/vanilla";
|
|
13
|
+
import { z } from "zod";
|
|
7
14
|
import {
|
|
8
|
-
type ConnectionStatus,
|
|
9
|
-
ConnectionState,
|
|
10
15
|
ConnectionClosedReason,
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
TransportReadyState,
|
|
16
|
+
ConnectionState,
|
|
17
|
+
type ConnectionStatus,
|
|
14
18
|
} from "./connection";
|
|
15
|
-
import { type NotebookEvent, parseNotebookEvent } from "./events";
|
|
16
19
|
import { attachments } from "./resources/attachments";
|
|
17
20
|
import { type Document, document } from "./resources/document";
|
|
18
21
|
|
|
19
|
-
/**
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
/** The kinds the editor reads, with marimo's own notifications. */
|
|
23
|
+
const TYPES = ["notebook", "cell", "document", "attachment", "marimo"] as const;
|
|
24
|
+
|
|
25
|
+
type EditorEvent = NotebookEvent<(typeof TYPES)[number]>;
|
|
26
|
+
|
|
27
|
+
/** A native marimo notification, passed through without interpretation. */
|
|
28
|
+
const MarimoMessage = z.object({ marimo: z.unknown() });
|
|
25
29
|
|
|
26
30
|
/** A connection to a notebook on a marimo host. */
|
|
27
31
|
export interface NotebookClient {
|
|
@@ -43,28 +47,53 @@ export interface NotebookClient {
|
|
|
43
47
|
reconnect(): Promise<void>;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
/** The transport and application callbacks needed to
|
|
50
|
+
/** The transport and application callbacks needed to follow a notebook. */
|
|
47
51
|
export interface NotebookConnection {
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
url: () => string;
|
|
52
|
-
/** Abort to close the transport and clear the client's state. */
|
|
52
|
+
/** Opens one connection to the notebook's stream. */
|
|
53
|
+
transport: NotebookTransport;
|
|
54
|
+
/** Abort to close the stream and clear the client's state. */
|
|
53
55
|
signal: AbortSignal;
|
|
54
|
-
/** Wait until the host can accept the initial connection. */
|
|
55
|
-
waitUntilReady: () => Promise<void>;
|
|
56
56
|
/** Check whether the host is reachable before an explicit reconnect. */
|
|
57
57
|
checkHealth: () => Promise<boolean>;
|
|
58
|
-
/** Decode wire text without interpreting its event type. */
|
|
59
|
-
decodeJson: (message: string) => unknown;
|
|
60
58
|
/** Handle a native marimo notification. Its payload has not been validated. */
|
|
61
59
|
onNotification: (notification: unknown) => void;
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const disconnected = {
|
|
63
|
+
state: ConnectionState.CLOSED,
|
|
64
|
+
code: ConnectionClosedReason.KERNEL_DISCONNECTED,
|
|
65
|
+
reason: "kernel not found",
|
|
66
|
+
} satisfies ConnectionStatus;
|
|
67
|
+
|
|
68
|
+
const malformed = {
|
|
69
|
+
...disconnected,
|
|
70
|
+
reason: "Failed to decode notebook message",
|
|
71
|
+
} satisfies ConnectionStatus;
|
|
72
|
+
|
|
73
|
+
function isStatus(value: unknown): value is ConnectionStatus {
|
|
74
|
+
return typeof value === "object" && value !== null && "state" in value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The status to show after the stream stopped.
|
|
79
|
+
*
|
|
80
|
+
* A transport says which status its stop means. An event the protocol
|
|
81
|
+
* rejects is malformed. Anything else means the host was not reached.
|
|
82
|
+
*/
|
|
83
|
+
function statusAfter(error: unknown): ConnectionStatus {
|
|
84
|
+
if (error instanceof StreamStopped && isStatus(error.cause)) {
|
|
85
|
+
return error.cause;
|
|
86
|
+
}
|
|
87
|
+
if (error instanceof StreamStopped && error.cause instanceof z.ZodError) {
|
|
88
|
+
return malformed;
|
|
89
|
+
}
|
|
90
|
+
return disconnected;
|
|
64
91
|
}
|
|
65
92
|
|
|
66
93
|
/** Create a client without opening a connection. */
|
|
67
94
|
export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
95
|
+
// `useNotebookSession` copies this into the editor's shared `connectionAtom`,
|
|
96
|
+
// which gates features until MO-8051 derives availability separately.
|
|
68
97
|
const connectionAtom = atom<ConnectionStatus>({
|
|
69
98
|
state: ConnectionState.NOT_STARTED,
|
|
70
99
|
});
|
|
@@ -73,16 +102,18 @@ export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
|
73
102
|
const streamAtom = atom(reducer.initial);
|
|
74
103
|
// A resumed stream is live, but presence waits for this connection's ready.
|
|
75
104
|
const readyAtom = atom(false);
|
|
105
|
+
const presentAtom = atom(
|
|
106
|
+
(get) =>
|
|
107
|
+
get(readyAtom) && get(connectionAtom).state === ConnectionState.OPEN,
|
|
108
|
+
);
|
|
76
109
|
const notebookAtom = atom((get) =>
|
|
77
|
-
get(
|
|
78
|
-
? get(streamAtom).notebook
|
|
79
|
-
: null,
|
|
110
|
+
get(presentAtom) ? get(streamAtom).notebook : null,
|
|
80
111
|
);
|
|
81
112
|
const documentAtom = atom(
|
|
82
113
|
(get) => get(streamAtom).resources.document.current,
|
|
83
114
|
);
|
|
84
115
|
const attachmentsAtom = atom((get) =>
|
|
85
|
-
get(
|
|
116
|
+
get(presentAtom)
|
|
86
117
|
? get(streamAtom).resources.attachments
|
|
87
118
|
: attachments.initial,
|
|
88
119
|
);
|
|
@@ -95,156 +126,97 @@ export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
|
95
126
|
reconnect: async () => reconnect?.(),
|
|
96
127
|
};
|
|
97
128
|
|
|
129
|
+
function setStatus(status: ConnectionStatus) {
|
|
130
|
+
store.set(readyAtom, false);
|
|
131
|
+
store.set(connectionAtom, status);
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
function connect({
|
|
99
|
-
|
|
100
|
-
url,
|
|
135
|
+
transport,
|
|
101
136
|
signal,
|
|
102
|
-
waitUntilReady,
|
|
103
137
|
checkHealth,
|
|
104
138
|
onNotification,
|
|
105
|
-
decodeJson,
|
|
106
|
-
classifyClose,
|
|
107
139
|
}: NotebookConnection) {
|
|
108
140
|
if (signal.aborted) {
|
|
109
141
|
return;
|
|
110
142
|
}
|
|
111
|
-
let
|
|
112
|
-
let terminal = false;
|
|
143
|
+
let events: NotebookEvents<(typeof TYPES)[number]> | undefined;
|
|
113
144
|
let checkingHealth = false;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let cursor: string | null = null;
|
|
117
|
-
const transport = createTransport(() => {
|
|
118
|
-
const next = new URL(url());
|
|
119
|
-
next.searchParams.delete("cursor");
|
|
120
|
-
if (signal.aborted) {
|
|
121
|
-
return next.toString();
|
|
122
|
-
}
|
|
123
|
-
// A cursor is useful with the state that produced it, which a change
|
|
124
|
-
// of session or notebook invalidates. Authentication may change freely.
|
|
125
|
-
const identity = new URL(next);
|
|
126
|
-
identity.searchParams.delete("access_token");
|
|
127
|
-
identity.searchParams.sort();
|
|
128
|
-
const key = identity.toString();
|
|
129
|
-
const current = store.get(streamAtom);
|
|
130
|
-
// A live stream keeps its state and resumes from the cursor. An
|
|
131
|
-
// incomplete snapshot starts over.
|
|
132
|
-
const state =
|
|
133
|
-
key !== streamKey || current.phase === "snapshot"
|
|
134
|
-
? reducer.initial
|
|
135
|
-
: current;
|
|
136
|
-
if (state.phase === "snapshot") {
|
|
137
|
-
cursor = null;
|
|
138
|
-
}
|
|
139
|
-
streamKey = key;
|
|
140
|
-
store.set(readyAtom, false);
|
|
141
|
-
store.set(streamAtom, state);
|
|
142
|
-
if (cursor !== null) {
|
|
143
|
-
next.searchParams.set("cursor", cursor);
|
|
144
|
-
}
|
|
145
|
-
return next.toString();
|
|
146
|
-
});
|
|
147
|
-
const setStatus = (status: ConnectionStatus) => {
|
|
148
|
-
store.set(readyAtom, false);
|
|
149
|
-
store.set(connectionAtom, status);
|
|
150
|
-
};
|
|
151
|
-
const disconnected = {
|
|
152
|
-
state: ConnectionState.CLOSED,
|
|
153
|
-
code: ConnectionClosedReason.KERNEL_DISCONNECTED,
|
|
154
|
-
reason: "kernel not found",
|
|
155
|
-
} satisfies ConnectionStatus;
|
|
145
|
+
// Whether the replica still holds the state the last cursor describes.
|
|
146
|
+
let resumable = true;
|
|
156
147
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function onOpen() {
|
|
165
|
-
if (signal.aborted || terminal) {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
shouldRetry = true;
|
|
169
|
-
setStatus({ state: ConnectionState.OPEN });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function onClose(event: CloseEvent) {
|
|
173
|
-
if (signal.aborted || terminal) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
Logger.warn("Notebook connection closed", event.code, event.reason);
|
|
177
|
-
const decision = classifyClose(event);
|
|
178
|
-
setStatus(decision.status);
|
|
179
|
-
if (decision.kind === "terminal") {
|
|
180
|
-
terminal = true;
|
|
181
|
-
transport.close();
|
|
182
|
-
} else if (decision.kind === "retry") {
|
|
183
|
-
retry(event.code, event.reason);
|
|
184
|
-
} else {
|
|
185
|
-
terminal = true;
|
|
148
|
+
/** Mirror one stream into the atoms until it closes. */
|
|
149
|
+
function follow(cursor?: string) {
|
|
150
|
+
if (cursor === undefined) {
|
|
151
|
+
store.set(streamAtom, reducer.initial);
|
|
186
152
|
}
|
|
153
|
+
resumable = true;
|
|
154
|
+
const current = new NotebookEvents(transport, {
|
|
155
|
+
types: TYPES,
|
|
156
|
+
cursor,
|
|
157
|
+
signal,
|
|
158
|
+
// TODO(MO-8051): Gate editing and running on the notebook's operations
|
|
159
|
+
// and request results instead. The stream's status would then only
|
|
160
|
+
// mark the view as stale.
|
|
161
|
+
onStatus: (status) => {
|
|
162
|
+
if (status === "connecting") {
|
|
163
|
+
setStatus({ state: ConnectionState.CONNECTING });
|
|
164
|
+
} else if (status === "connected") {
|
|
165
|
+
setStatus({ state: ConnectionState.OPEN });
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
current.subscribe(
|
|
170
|
+
(event) => apply(current, event),
|
|
171
|
+
(error) => {
|
|
172
|
+
if (!signal.aborted && current === events) {
|
|
173
|
+
Logger.warn("Notebook stream stopped", error);
|
|
174
|
+
const status = statusAfter(error);
|
|
175
|
+
if (status === malformed) {
|
|
176
|
+
stopMalformed(current);
|
|
177
|
+
} else {
|
|
178
|
+
setStatus(status);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
events = current;
|
|
187
184
|
}
|
|
188
185
|
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
setStatus(
|
|
195
|
-
retry();
|
|
186
|
+
function stopMalformed(source: NotebookEvents<string>) {
|
|
187
|
+
source.close();
|
|
188
|
+
// The cursor can already include an event the replica rejected.
|
|
189
|
+
resumable = false;
|
|
190
|
+
store.set(streamAtom, reducer.initial);
|
|
191
|
+
setStatus(malformed);
|
|
196
192
|
}
|
|
197
193
|
|
|
198
|
-
function
|
|
199
|
-
if (
|
|
194
|
+
function apply(source: NotebookEvents<string>, event: EditorEvent) {
|
|
195
|
+
if (event.event === "marimo") {
|
|
196
|
+
const message = MarimoMessage.safeParse(event.data);
|
|
197
|
+
if (message.success) {
|
|
198
|
+
onNotification(message.data.marimo);
|
|
199
|
+
} else {
|
|
200
|
+
Logger.error("Failed to decode notebook message", event);
|
|
201
|
+
stopMalformed(source);
|
|
202
|
+
}
|
|
200
203
|
return;
|
|
201
204
|
}
|
|
202
|
-
let event;
|
|
203
205
|
try {
|
|
204
|
-
|
|
205
|
-
const state = store.get(streamAtom);
|
|
206
|
-
if (
|
|
207
|
-
state.phase === "live" &&
|
|
208
|
-
event.id === undefined &&
|
|
209
|
-
!UNCURSORED.has(event.event)
|
|
210
|
-
) {
|
|
211
|
-
throw new Error("Live notebook event is missing its cursor");
|
|
212
|
-
}
|
|
213
|
-
if (event.event !== "marimo" && event.event !== "unknown") {
|
|
214
|
-
store.set(streamAtom, reducer.reduce(state, event));
|
|
215
|
-
}
|
|
216
|
-
if (event.event === "reset") {
|
|
217
|
-
cursor = null;
|
|
218
|
-
} else if (event.event === "ready" || state.phase === "live") {
|
|
219
|
-
cursor = event.id ?? cursor;
|
|
220
|
-
}
|
|
221
|
-
if (event.event === "ready" || event.event === "reset") {
|
|
222
|
-
store.set(readyAtom, event.event === "ready");
|
|
223
|
-
}
|
|
206
|
+
store.set(streamAtom, reducer.reduce(store.get(streamAtom), event));
|
|
224
207
|
} catch (error) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
terminal = true;
|
|
229
|
-
setStatus({
|
|
230
|
-
...disconnected,
|
|
231
|
-
reason: "Failed to decode notebook message",
|
|
232
|
-
});
|
|
233
|
-
transport.close();
|
|
208
|
+
// An event the replica cannot apply, such as another notebook's.
|
|
209
|
+
Logger.error("Failed to apply notebook event", error);
|
|
210
|
+
stopMalformed(source);
|
|
234
211
|
return;
|
|
235
212
|
}
|
|
236
|
-
if (event.event === "
|
|
237
|
-
|
|
213
|
+
if (event.event === "ready" || event.event === "reset") {
|
|
214
|
+
store.set(readyAtom, event.event === "ready");
|
|
238
215
|
}
|
|
239
216
|
}
|
|
240
217
|
|
|
241
218
|
reconnect = async () => {
|
|
242
|
-
if (
|
|
243
|
-
signal.aborted ||
|
|
244
|
-
checkingHealth ||
|
|
245
|
-
transport.readyState === TransportReadyState.OPEN ||
|
|
246
|
-
transport.readyState === TransportReadyState.CONNECTING
|
|
247
|
-
) {
|
|
219
|
+
if (signal.aborted || checkingHealth || events?.status !== "closed") {
|
|
248
220
|
return;
|
|
249
221
|
}
|
|
250
222
|
checkingHealth = true;
|
|
@@ -255,17 +227,13 @@ export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
|
255
227
|
return;
|
|
256
228
|
}
|
|
257
229
|
if (!healthy) {
|
|
258
|
-
shouldRetry = false;
|
|
259
230
|
setStatus(disconnected);
|
|
260
231
|
return;
|
|
261
232
|
}
|
|
262
|
-
|
|
263
|
-
shouldRetry = true;
|
|
264
|
-
transport.reconnect();
|
|
233
|
+
follow(resumable ? (events.cursor ?? undefined) : undefined);
|
|
265
234
|
} catch (error) {
|
|
266
235
|
if (!signal.aborted) {
|
|
267
236
|
Logger.error("Notebook health check failed", error);
|
|
268
|
-
shouldRetry = false;
|
|
269
237
|
setStatus(disconnected);
|
|
270
238
|
}
|
|
271
239
|
} finally {
|
|
@@ -276,11 +244,6 @@ export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
|
276
244
|
signal.addEventListener(
|
|
277
245
|
"abort",
|
|
278
246
|
() => {
|
|
279
|
-
transport.removeEventListener("open", onOpen);
|
|
280
|
-
transport.removeEventListener("close", onClose);
|
|
281
|
-
transport.removeEventListener("error", onError);
|
|
282
|
-
transport.removeEventListener("message", onMessage);
|
|
283
|
-
transport.close();
|
|
284
247
|
reconnect = undefined;
|
|
285
248
|
store.set(streamAtom, reducer.initial);
|
|
286
249
|
setStatus({ state: ConnectionState.NOT_STARTED });
|
|
@@ -288,28 +251,8 @@ export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
|
288
251
|
{ once: true },
|
|
289
252
|
);
|
|
290
253
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
transport.addEventListener("error", onError);
|
|
294
|
-
transport.addEventListener("message", onMessage);
|
|
295
|
-
|
|
296
|
-
if (transport.readyState === TransportReadyState.CLOSED) {
|
|
297
|
-
setStatus({ state: ConnectionState.CONNECTING });
|
|
298
|
-
void waitUntilReady()
|
|
299
|
-
.then(() => {
|
|
300
|
-
if (!signal.aborted) {
|
|
301
|
-
transport.reconnect();
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
.catch((error) => {
|
|
305
|
-
if (!signal.aborted) {
|
|
306
|
-
Logger.error("Healthy connection never made", error);
|
|
307
|
-
terminal = true;
|
|
308
|
-
setStatus(disconnected);
|
|
309
|
-
transport.close();
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
}
|
|
254
|
+
setStatus({ state: ConnectionState.CONNECTING });
|
|
255
|
+
follow();
|
|
313
256
|
}
|
|
314
257
|
|
|
315
258
|
return { client, connect };
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -38,8 +38,6 @@ import type {
|
|
|
38
38
|
} from "../network/types";
|
|
39
39
|
import { filenameAtom } from "../saving/file-state";
|
|
40
40
|
import { store } from "../state/jotai";
|
|
41
|
-
import { BasicTransport } from "../websocket/transports/basic";
|
|
42
|
-
import type { IConnectionTransport } from "../websocket/transports/transport";
|
|
43
41
|
import { PyodideRouter } from "./router";
|
|
44
42
|
import { getWorkerRPC } from "./rpc";
|
|
45
43
|
import { getWasmRuntimeConfig } from "./runtime-config";
|
|
@@ -719,12 +717,6 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
719
717
|
}
|
|
720
718
|
}
|
|
721
719
|
|
|
722
|
-
export function createPyodideConnection(): IConnectionTransport {
|
|
723
|
-
return BasicTransport.withProducerCallback((callback) => {
|
|
724
|
-
PyodideBridge.INSTANCE.attachMessageConsumer(callback);
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
|
|
728
720
|
// Compose the worker name. The optional "::controller" suffix tells
|
|
729
721
|
// getController.ts that the host page provides a custom /wasm/controller.js
|
|
730
722
|
// and that the dynamic import should be attempted. Hosts opt in by setting
|
|
@@ -64,16 +64,4 @@ describe("classifyCloseEvent", () => {
|
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
|
-
|
|
68
|
-
describe("transport exhaustion", () => {
|
|
69
|
-
it("MARIMO_TRANSPORT_EXHAUSTED → gave-up with KERNEL_DISCONNECTED", () => {
|
|
70
|
-
const decision = classify("MARIMO_TRANSPORT_EXHAUSTED");
|
|
71
|
-
expect(decision.kind).toBe("gave-up");
|
|
72
|
-
expect(decision.status).toEqual({
|
|
73
|
-
state: ConnectionState.CLOSED,
|
|
74
|
-
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
75
|
-
reason: "kernel not found",
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
67
|
});
|