@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.
Files changed (106) hide show
  1. package/dist/assets/{CellStatus-cD3S0wEA.js → CellStatus-DXroYLOP.js} +1 -1
  2. package/dist/assets/{ErrorBoundary-DJhsFCsX.js → ErrorBoundary-DaQ9THH_.js} +2 -2
  3. package/dist/assets/{JsonOutput-Pm6OCP3J.js → JsonOutput-VxzHyItt.js} +1 -1
  4. package/dist/assets/{MarimoErrorOutput-DlEjLCZ-.js → MarimoErrorOutput-D-hLaDA-.js} +1 -1
  5. package/dist/assets/{RenderHTML-CfQ6i3QV.js → RenderHTML-DjOEZL25.js} +1 -1
  6. package/dist/assets/{RunButton-DvuvkgrG.js → RunButton-CuhNTo5_.js} +1 -1
  7. package/dist/assets/{add-cell-with-ai-By51zSlC.js → add-cell-with-ai-BbVntTh7.js} +1 -1
  8. package/dist/assets/{add-connection-dialog-D7EYSraq.js → add-connection-dialog-Qf63aKV6.js} +1 -1
  9. package/dist/assets/{agent-panel-aCWa3Yai.js → agent-panel-DMOfF3qF.js} +1 -1
  10. package/dist/assets/{ai-model-dropdown-bwTRyFn4.js → ai-model-dropdown-B15l0R62.js} +2 -2
  11. package/dist/assets/{app-config-button-DGhsM0xw.js → app-config-button-BIMB8qjD.js} +1 -1
  12. package/dist/assets/{blob-e6VlOmQP.js → blob-De9GR2s6.js} +1 -1
  13. package/dist/assets/{cell-editor-M8dEnDzk.js → cell-editor-Di0cSFhC.js} +32 -11
  14. package/dist/assets/{cell-link-DFqfW-Ym.js → cell-link-DR3W2HC7.js} +1 -1
  15. package/dist/assets/{cells-T9_Y4Jlv.js → cells-styV7IX8.js} +6 -6
  16. package/dist/assets/{chat-display-DXn2Yrjr.js → chat-display-BzdYdN39.js} +1 -1
  17. package/dist/assets/{chat-panel-D3iUyZId.js → chat-panel-De3YzkSF.js} +1 -1
  18. package/dist/assets/{chat-ui-YuJtv3UJ.js → chat-ui-NTNapdtP.js} +1 -1
  19. package/dist/assets/{code-visibility-CV3S4xuf.js → code-visibility-csc3tgA0.js} +1 -1
  20. package/dist/assets/{column-preview-C6uXRJry.js → column-preview-DkH-oD8f.js} +1 -1
  21. package/dist/assets/{command-palette-CGNUQdgW.js → command-palette-Dg_xstl0.js} +1 -1
  22. package/dist/assets/{common-DeeLgjWQ.js → common-D7VAS6rd.js} +1 -1
  23. package/dist/assets/{components-DyAF3_Nf.js → components-CNmvC781.js} +1 -1
  24. package/dist/assets/config-BQT0uYUo.js +1 -0
  25. package/dist/assets/{config-CjQ4WJnJ.js → config-BbRbT9Ww.js} +1 -1
  26. package/dist/assets/{datasource-gqFgG9Qd.js → datasource-C3yfeDa2.js} +1 -1
  27. package/dist/assets/{dependency-graph-panel-B3T9kSTs.js → dependency-graph-panel-CjoA6L0w.js} +1 -1
  28. package/dist/assets/{documentation-panel-BBpGbT-f.js → documentation-panel-DHWFOiG1.js} +1 -1
  29. package/dist/assets/{download-C8Gw-D0Y.js → download-BrCpRZKN.js} +1 -1
  30. package/dist/assets/{edit-page-CBftLPze.js → edit-page-BPMo_x-T.js} +4 -4
  31. package/dist/assets/{error-entries-uyqOG5tg.js → error-entries-BkEAmmnD.js} +1 -1
  32. package/dist/assets/{error-panel-Cli5zPWi.js → error-panel-BhTfPA_i.js} +1 -1
  33. package/dist/assets/feedback-button-BAcClv7c.js +1 -0
  34. package/dist/assets/{feedback-button-CvrwBGr7.js → feedback-button-D9InK87G.js} +1 -1
  35. package/dist/assets/{file-explorer-panel-BSU5ultY.js → file-explorer-panel-BSd_ZbUC.js} +1 -1
  36. package/dist/assets/{file-name-input-CDrRlIiX.js → file-name-input-RHURglL_.js} +1 -1
  37. package/dist/assets/{floating-outline-GQD9km0W.js → floating-outline-Cm49rYYe.js} +1 -1
  38. package/dist/assets/{gallery-page-B-5H1fEt.js → gallery-page-DdMdOtdv.js} +1 -1
  39. package/dist/assets/{glide-data-editor-B1kpoetH.js → glide-data-editor-eiSlEUyR.js} +1 -1
  40. package/dist/assets/{home-page-3qsPTb4v.js → home-page-DwWvBB0R.js} +1 -1
  41. package/dist/assets/{hooks-BrFYiTJS.js → hooks-CHeUZHPp.js} +1 -1
  42. package/dist/assets/{index-D0IWODMU.js → index-DckSn2w6.js} +8 -8
  43. package/dist/assets/{layout-CGC8ajpe.js → layout-_lp06T0C.js} +3 -3
  44. package/dist/assets/{logs-panel-CdUc5vtA.js → logs-panel-CAAk72Gb.js} +1 -1
  45. package/dist/assets/{markdown-renderer-pKc_bFac.js → markdown-renderer-CAyCgogC.js} +1 -1
  46. package/dist/assets/{name-cell-input-csH3Cy54.js → name-cell-input-DA8hU9XT.js} +1 -1
  47. package/dist/assets/{outline-panel-DhsHYTuo.js → outline-panel-Cc5oQ4LL.js} +1 -1
  48. package/dist/assets/{packages-panel-BtkXVz3L.js → packages-panel-DTfKez6I.js} +1 -1
  49. package/dist/assets/{panel-accordion-state-Bco6nsUI.js → panel-accordion-state-BZHex38W.js} +1 -1
  50. package/dist/assets/panels-DU-SaRHy.js +105 -0
  51. package/dist/assets/{process-output-B2-bT4cq.js → process-output-CrDc_O1D.js} +1 -1
  52. package/dist/assets/{readonly-code-display-Dsp6vWPh.js → readonly-code-display-BR-_jUj8.js} +1 -1
  53. package/dist/assets/{readonly-python-code-B81AjzXM.js → readonly-python-code-D7jcy9ds.js} +1 -1
  54. package/dist/assets/{reveal-component-Cxo3hLGA.js → reveal-component-eLftBwOe.js} +1 -1
  55. package/dist/assets/{run-page-CLP1s8uX.js → run-page-mURyATDJ.js} +1 -1
  56. package/dist/assets/{runtime-details-state-CznIe40L.js → runtime-details-state-hb7A1jgR.js} +1 -1
  57. package/dist/assets/{scratchpad-panel-yHIW63VQ.js → scratchpad-panel-B_gEnoqj.js} +1 -1
  58. package/dist/assets/{session-panel-C7-wmZhc.js → session-panel-DmDjaPsN.js} +1 -1
  59. package/dist/assets/{snippets-panel-BARpOPyS.js → snippets-panel-o9KL5jMi.js} +1 -1
  60. package/dist/assets/state-BkAqJoW7.js +3 -0
  61. package/dist/assets/{state-Bye6rCo4.js → state-Os7LSUUZ.js} +1 -1
  62. package/dist/assets/{terminal-Bhcn_y1N.js → terminal-BI85khrS.js} +1 -1
  63. package/dist/assets/{tracing-DDohxo24.js → tracing-AsNGuDsh.js} +1 -1
  64. package/dist/assets/{tracing-panel-DYhGDAo5.js → tracing-panel-CBcmea6L.js} +2 -2
  65. package/dist/assets/{tree-actions-Cb-nUK1F.js → tree-actions-Cx5EDpA6.js} +1 -1
  66. package/dist/assets/{useAddCell-Bielffe0.js → useAddCell-uJJYtgwa.js} +1 -1
  67. package/dist/assets/{useBoolean-CMgj9hnq.js → useBoolean-8wlrTeus.js} +1 -1
  68. package/dist/assets/{useCellActionButton-DAcQhse_.js → useCellActionButton-CmQ-OcPa.js} +1 -1
  69. package/dist/assets/{useDeleteCell-BV4VemCv.js → useDeleteCell-CXgOCDqY.js} +1 -1
  70. package/dist/assets/{useDependencyPanelTab-PtXIVh53.js → useDependencyPanelTab-Ch6gm7uM.js} +1 -1
  71. package/dist/assets/{useInstallPackage-C7neMPwh.js → useInstallPackage-CYQoMdMQ.js} +1 -1
  72. package/dist/assets/{useNotebookActions-DZBB98fP.js → useNotebookActions-DGShHZ_1.js} +1 -1
  73. package/dist/assets/{useRunCells-BFeZMi7f.js → useRunCells-DAfDmOMC.js} +1 -1
  74. package/dist/assets/{useSplitCell-DllkNFke.js → useSplitCell-NLULgcq7.js} +1 -1
  75. package/dist/assets/{vega-component-D7JFthDD.js → vega-component-BKJU3uaB.js} +1 -1
  76. package/dist/index.html +24 -24
  77. package/package.json +1 -1
  78. package/src/core/host/AGENTS.md +6 -5
  79. package/src/core/host/__tests__/notebook-client.test.ts +368 -350
  80. package/src/core/host/connection.ts +1 -40
  81. package/src/core/host/notebook-client.ts +129 -186
  82. package/src/core/wasm/bridge.ts +0 -8
  83. package/src/core/websocket/__tests__/connection-state.test.ts +0 -12
  84. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +133 -59
  85. package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +152 -250
  86. package/src/core/websocket/connection-state.ts +0 -10
  87. package/src/core/websocket/transports/__tests__/notebook.test.ts +502 -0
  88. package/src/core/websocket/transports/notebook.ts +213 -0
  89. package/src/core/websocket/useNotebookClient.ts +28 -17
  90. package/dist/assets/config-DzdxZb17.js +0 -1
  91. package/dist/assets/feedback-button-DknbRCN_.js +0 -1
  92. package/dist/assets/panels-D1i80TGn.js +0 -107
  93. package/dist/assets/state-BRYi026f.js +0 -3
  94. package/dist/assets/ws-BXWY541V.js +0 -22
  95. package/src/core/host/__tests__/events.test.ts +0 -49
  96. package/src/core/host/events.ts +0 -68
  97. package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
  98. package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
  99. package/src/core/websocket/transports/__tests__/sse.test.ts +0 -352
  100. package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
  101. package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
  102. package/src/core/websocket/transports/basic.ts +0 -102
  103. package/src/core/websocket/transports/create.ts +0 -31
  104. package/src/core/websocket/transports/sse.ts +0 -320
  105. package/src/core/websocket/transports/transport.ts +0 -50
  106. 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" | "gave-up" | "retry";
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
- type CloseDecision,
12
- type NotebookTransport,
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
- /** Events that may arrive without a cursor after `ready`. */
20
- const UNCURSORED = new Set<NotebookEvent["event"]>([
21
- "reset",
22
- "marimo",
23
- "unknown",
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 connect a notebook. */
50
+ /** The transport and application callbacks needed to follow a notebook. */
47
51
  export interface NotebookConnection {
48
- /** Create a transport that reads the URL again on each connection attempt. */
49
- createTransport: (url: () => string) => NotebookTransport;
50
- /** The current stream URL, including authentication and subscription options. */
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
- /** Choose the connection status and retry policy after the transport closes. */
63
- classifyClose: (event: CloseEvent) => CloseDecision;
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(readyAtom) && get(connectionAtom).state === ConnectionState.OPEN
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(readyAtom) && get(connectionAtom).state === ConnectionState.OPEN
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
- createTransport,
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 shouldRetry = true;
112
- let terminal = false;
143
+ let events: NotebookEvents<(typeof TYPES)[number]> | undefined;
113
144
  let checkingHealth = false;
114
- let streamKey: string | undefined;
115
- // The id of the last live event. Null before `ready` and after `reset`.
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
- function retry(code?: number, reason?: string) {
158
- if (!signal.aborted && !terminal && shouldRetry) {
159
- shouldRetry = false;
160
- transport.reconnect(code, reason);
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 onError(event: Event) {
190
- if (signal.aborted || terminal) {
191
- return;
192
- }
193
- Logger.warn("Notebook connection error", event);
194
- setStatus(disconnected);
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 onMessage(message: MessageEvent<string>) {
199
- if (signal.aborted || terminal) {
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
- event = parseNotebookEvent(decodeJson(message.data));
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
- Logger.error("Failed to decode notebook message", error);
226
- store.set(streamAtom, reducer.initial);
227
- cursor = null;
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 === "marimo") {
237
- onNotification(event.data.marimo);
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
- terminal = false;
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
- transport.addEventListener("open", onOpen);
292
- transport.addEventListener("close", onClose);
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 };
@@ -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
  });