@marimo-team/islands 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.
Files changed (42) hide show
  1. package/dist/{ErrorBoundary-BsqSMK9Y.js → ErrorBoundary-CUd8VnYI.js} +1 -1
  2. package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-DaHynk8g.js} +4 -4
  3. package/dist/{code-visibility-CXQvUXHT.js → code-visibility-P0Ornave.js} +3 -3
  4. package/dist/{common-CuD-CAC-.js → common-BtO98PuE.js} +5 -5
  5. package/dist/{data-grid-overlay-editor-DgeyYmC1.js → data-grid-overlay-editor-B801aMeG.js} +1 -1
  6. package/dist/{error-entries-DSvFWT_M.js → error-entries-Bv6bZPF3.js} +1 -1
  7. package/dist/{feedback-button-DZMj9PF3.js → feedback-button-Bpu-arCP.js} +3 -3
  8. package/dist/{formats-CeOmU1MU.js → formats-27CW_UP4.js} +1 -1
  9. package/dist/{glide-data-editor-g_L8FLAC.js → glide-data-editor-XZB3u4tH.js} +3 -3
  10. package/dist/{globals-CYGlmBXb.js → globals-BIQc0DK0.js} +1 -1
  11. package/dist/{html-to-image-B8s5SgGm.js → html-to-image-Bf8QMGi4.js} +2 -2
  12. package/dist/main.js +13 -13
  13. package/dist/{number-overlay-editor-9VRv-pdj.js → number-overlay-editor-CEO_7tD7.js} +1 -1
  14. package/dist/{process-output-CKRXyszb.js → process-output-BZg56CCb.js} +2 -2
  15. package/dist/{reveal-component-C87mv9Mn.js → reveal-component-LPCUNcgp.js} +4 -4
  16. package/dist/{toDate-vIXljnsr.js → toDate-UtiwmnH2.js} +17 -16
  17. package/dist/{vega-component-DTG6zxaY.js → vega-component-BeF9dfyO.js} +2 -2
  18. package/package.json +1 -1
  19. package/src/__tests__/mount.test.ts +20 -0
  20. package/src/core/edit-app.tsx +2 -2
  21. package/src/core/host/__tests__/events.test.ts +46 -0
  22. package/src/core/host/__tests__/notebook-client.test.ts +191 -0
  23. package/src/core/host/connection.ts +81 -0
  24. package/src/core/host/events.ts +60 -0
  25. package/src/core/host/notebook-client.ts +226 -0
  26. package/src/core/run-app.tsx +2 -2
  27. package/src/core/runtime/__tests__/runtime.test.ts +55 -11
  28. package/src/core/runtime/runtime.ts +14 -1
  29. package/src/core/runtime/types.ts +2 -0
  30. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  31. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  32. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +140 -0
  33. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +227 -133
  34. package/src/core/websocket/connection-state.ts +70 -0
  35. package/src/core/websocket/transports/__tests__/sse.test.ts +1 -4
  36. package/src/core/websocket/transports/create.ts +31 -0
  37. package/src/core/websocket/transports/transport.ts +12 -23
  38. package/src/core/websocket/types.ts +9 -39
  39. package/src/core/websocket/useNotebookClient.ts +80 -0
  40. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +64 -231
  41. package/src/mount.tsx +4 -0
  42. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -9,6 +9,8 @@ import { isStaticNotebook } from "../static/static-state";
9
9
  import { isWasm } from "../wasm/utils";
10
10
  import type { RuntimeConfig } from "./types";
11
11
 
12
+ const HOST_EVENT_TYPES = ["marimo"];
13
+
12
14
  export class RuntimeManager {
13
15
  private initialHealthyCheck = new Deferred<void>();
14
16
  private config: RuntimeConfig;
@@ -154,7 +156,18 @@ export class RuntimeManager {
154
156
  * The WebSocket URL of the runtime.
155
157
  */
156
158
  public getWsURL(sessionId: SessionId): URL {
157
- return this.formatWsURL("/ws", this.getSessionSearchParams(sessionId));
159
+ const searchParams = this.getSessionSearchParams(sessionId);
160
+ // TODO(MO-8039): Rediscover hosts and resolve notebooks on reconnect.
161
+ const notebookId = this.config.notebookId;
162
+ const path = notebookId
163
+ ? `/api/marimo/v1/notebooks/${encodeURIComponent(notebookId)}/events/ws`
164
+ : "/ws";
165
+ const url = this.formatWsURL(path, searchParams);
166
+ if (notebookId) {
167
+ // TODO(MO-8034): Separate host options from notebook query params.
168
+ url.searchParams.set("types", HOST_EVENT_TYPES.join(","));
169
+ }
170
+ return url;
158
171
  }
159
172
 
160
173
  /**
@@ -1,6 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  export interface RuntimeConfig {
4
+ /** The host notebook for WebSocket events. SSE uses the legacy session stream. */
5
+ readonly notebookId?: string;
4
6
  /**
5
7
  * The URL of the runtime server.
6
8
  */
@@ -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 "../useMarimoKernelConnection";
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.kind).toBe("retry");
17
- expect(decision.status).toEqual({ state: ConnectionState.CONNECTING });
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, closes transport", (reason) => {
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
- closeTransport: true,
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 + closeTransport", () => {
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
- closeTransport: true,
60
+ status: {
61
+ state: ConnectionState.CLOSED,
62
+ code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
63
+ },
66
64
  });
67
65
  });
68
66
  });
@@ -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 "../useWebSocket";
7
+ import { createConnectionTransport } from "../transports/create";
8
8
 
9
9
  vi.mock("../../wasm/utils", () => ({
10
10
  isWasm: () => false,
@@ -0,0 +1,140 @@
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
+
13
+ vi.mock("../transports/create", () => ({ createConnectionTransport: vi.fn() }));
14
+
15
+ function makeTransport() {
16
+ const events = new EventTarget();
17
+ const transport: IConnectionTransport = {
18
+ readyState: WebSocket.CLOSED,
19
+ reconnect: vi.fn(() => {
20
+ transport.readyState = WebSocket.OPEN;
21
+ events.dispatchEvent(new Event("open"));
22
+ }),
23
+ close: vi.fn(() => {
24
+ transport.readyState = WebSocket.CLOSED;
25
+ events.dispatchEvent(new CloseEvent("close"));
26
+ }),
27
+ send: vi.fn(),
28
+ addEventListener: events.addEventListener.bind(events),
29
+ removeEventListener: events.removeEventListener.bind(events),
30
+ };
31
+ return {
32
+ transport,
33
+ notify: () =>
34
+ events.dispatchEvent(
35
+ new MessageEvent("message", {
36
+ data: JSON.stringify({
37
+ event: "marimo",
38
+ data: { marimo: { op: "reconnected" } },
39
+ }),
40
+ }),
41
+ ),
42
+ };
43
+ }
44
+
45
+ function setup(strict = false) {
46
+ const store = createStore();
47
+ const options = {
48
+ sessionId: "first",
49
+ static: false,
50
+ transportType: "websocket" as const,
51
+ messageFormat: "host" as const,
52
+ url: () => "ws://localhost/ws",
53
+ headers: () => ({}),
54
+ waitUntilReady: async () => undefined,
55
+ checkHealth: async () => true,
56
+ onNotification: vi.fn(),
57
+ onMessageError: vi.fn(),
58
+ };
59
+ const wrapper = ({ children }: PropsWithChildren) => (
60
+ <Provider store={store}>{children}</Provider>
61
+ );
62
+ const hook = renderHook((props) => useNotebookClient(props), {
63
+ initialProps: options,
64
+ reactStrictMode: strict,
65
+ wrapper,
66
+ });
67
+ return { ...hook, options, store };
68
+ }
69
+
70
+ beforeEach(() => {
71
+ vi.mocked(createConnectionTransport).mockReset();
72
+ });
73
+
74
+ describe("useNotebookClient", () => {
75
+ it("uses current callbacks and URL providers without reconnecting on a rerender", async () => {
76
+ const first = makeTransport();
77
+ vi.mocked(createConnectionTransport).mockReturnValue(first.transport);
78
+ const { rerender, options, unmount } = setup();
79
+ await act(async () => Promise.resolve());
80
+ const onNotification = vi.fn();
81
+ rerender({ ...options, onNotification, url: () => "ws://redirected/ws" });
82
+ act(() => first.notify());
83
+ expect(options.onNotification).not.toHaveBeenCalled();
84
+ expect(onNotification).toHaveBeenCalledExactlyOnceWith({
85
+ data: { op: "reconnected" },
86
+ });
87
+ expect(createConnectionTransport).toHaveBeenCalledOnce();
88
+ expect(vi.mocked(createConnectionTransport).mock.calls[0][0].url()).toBe(
89
+ "ws://redirected/ws",
90
+ );
91
+ unmount();
92
+ });
93
+
94
+ it("replaces the transport when session identity changes and ignores old messages", async () => {
95
+ const first = makeTransport();
96
+ const second = makeTransport();
97
+ vi.mocked(createConnectionTransport)
98
+ .mockReturnValueOnce(first.transport)
99
+ .mockReturnValueOnce(second.transport);
100
+ const { rerender, options, unmount } = setup();
101
+ await act(async () => Promise.resolve());
102
+ rerender({ ...options, sessionId: "second" });
103
+ await act(async () => Promise.resolve());
104
+ expect(first.transport.close).toHaveBeenCalledOnce();
105
+ act(() => {
106
+ first.notify();
107
+ second.notify();
108
+ });
109
+ expect(options.onNotification).toHaveBeenCalledOnce();
110
+ unmount();
111
+ });
112
+
113
+ it("cleans up every transport under Strict Mode and only connects the active lifetime", async () => {
114
+ const transports: ReturnType<typeof makeTransport>[] = [];
115
+ vi.mocked(createConnectionTransport).mockImplementation(() => {
116
+ const next = makeTransport();
117
+ transports.push(next);
118
+ return next.transport;
119
+ });
120
+ const { result, store, options, unmount } = setup(true);
121
+ await act(async () => Promise.resolve());
122
+ expect(transports).toHaveLength(2);
123
+ expect(transports[0].transport.reconnect).not.toHaveBeenCalled();
124
+ expect(transports[0].transport.close).toHaveBeenCalledOnce();
125
+ expect(transports[1].transport.reconnect).toHaveBeenCalledOnce();
126
+ expect(store.get(result.current.connectionAtom)).toEqual({
127
+ state: ConnectionState.OPEN,
128
+ });
129
+ act(() => {
130
+ for (const transport of transports) {
131
+ transport.notify();
132
+ }
133
+ });
134
+ expect(options.onNotification).toHaveBeenCalledOnce();
135
+ unmount();
136
+ expect(transports[1].transport.close).toHaveBeenCalledOnce();
137
+ await result.current.reconnect();
138
+ expect(transports[1].transport.reconnect).toHaveBeenCalledOnce();
139
+ });
140
+ });