@marimo-team/islands 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/{ErrorBoundary-CF0lzT8W.js → ErrorBoundary-Dh-VaAao.js} +1 -1
- package/dist/{code-visibility-cv4J7KDX.js → code-visibility-BwTJAT1U.js} +1 -1
- package/dist/{common-C2fKBgIk.js → common-BTpcnvZO.js} +1 -1
- package/dist/{data-grid-overlay-editor-wIDXNxbi.js → data-grid-overlay-editor-C95q5QIf.js} +1 -1
- package/dist/{feedback-button-BrHm1nRa.js → feedback-button-sOLkDWYm.js} +2 -2
- package/dist/{glide-data-editor-CPm2l4v_.js → glide-data-editor-D6GkY0Z2.js} +3 -3
- package/dist/{globals-Crq94BHx.js → globals-rH_9s1xB.js} +1 -1
- package/dist/main.js +6 -6
- package/dist/{number-overlay-editor-C7EDk8Hn.js → number-overlay-editor-BjJttHms.js} +1 -1
- package/dist/{reveal-component-DZ5yZnHD.js → reveal-component-Ckn4ABk3.js} +2 -2
- 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/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
|
@@ -5,52 +5,48 @@ import { act, renderHook } from "@testing-library/react";
|
|
|
5
5
|
import { createStore, Provider } from "jotai";
|
|
6
6
|
import type { PropsWithChildren } from "react";
|
|
7
7
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import type { NotebookTransport } from "@marimo-team/client";
|
|
8
9
|
import { ConnectionState } from "../types";
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
10
|
+
import { Deferred } from "@/utils/Deferred";
|
|
11
|
+
import { createNotebookTransport } from "../transports/notebook";
|
|
11
12
|
import { useNotebookClient } from "../useNotebookClient";
|
|
12
13
|
import { fixtureMessages } from "../../host/__tests__/fixtures";
|
|
13
14
|
import { agentAttachmentsAtom } from "../../attachments/state";
|
|
14
15
|
import { notebookClientAtom } from "../state";
|
|
15
16
|
|
|
16
|
-
vi.mock("../transports/
|
|
17
|
+
vi.mock("../transports/notebook", () => ({ createNotebookTransport: vi.fn() }));
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}),
|
|
31
|
-
send: vi.fn(),
|
|
32
|
-
addEventListener: events.addEventListener.bind(events),
|
|
33
|
-
removeEventListener: events.removeEventListener.bind(events),
|
|
34
|
-
};
|
|
19
|
+
/** A transport whose connections the test drives. */
|
|
20
|
+
function makeTransport() {
|
|
21
|
+
const opened: {
|
|
22
|
+
signal: AbortSignal;
|
|
23
|
+
open(): void;
|
|
24
|
+
receive(message: unknown): void;
|
|
25
|
+
}[] = [];
|
|
26
|
+
const transport: NotebookTransport = ({ signal }, handlers) =>
|
|
27
|
+
new Promise((resolve) => {
|
|
28
|
+
opened.push({ signal, open: handlers.open, receive: handlers.message });
|
|
29
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
30
|
+
});
|
|
35
31
|
return {
|
|
36
32
|
transport,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
new MessageEvent("message", { data: JSON.stringify(message) }),
|
|
40
|
-
),
|
|
33
|
+
opened,
|
|
34
|
+
/** Open the latest connection and deliver a host-framed notification. */
|
|
41
35
|
notify: () =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
data: { marimo: { op: "reconnected" } },
|
|
47
|
-
}),
|
|
48
|
-
}),
|
|
49
|
-
),
|
|
36
|
+
opened.at(-1)?.receive({
|
|
37
|
+
event: "marimo",
|
|
38
|
+
data: { marimo: { op: "reconnected" } },
|
|
39
|
+
}),
|
|
50
40
|
};
|
|
51
41
|
}
|
|
52
42
|
|
|
53
|
-
function setup(
|
|
43
|
+
function setup(
|
|
44
|
+
strict = false,
|
|
45
|
+
readiness: {
|
|
46
|
+
waitUntilReady?: () => Promise<void>;
|
|
47
|
+
checkHealth?: () => Promise<boolean>;
|
|
48
|
+
} = {},
|
|
49
|
+
) {
|
|
54
50
|
const store = createStore();
|
|
55
51
|
const options = {
|
|
56
52
|
static: false,
|
|
@@ -60,6 +56,7 @@ function setup(strict = false) {
|
|
|
60
56
|
waitUntilReady: async () => undefined,
|
|
61
57
|
checkHealth: async () => true,
|
|
62
58
|
onNotification: vi.fn(),
|
|
59
|
+
...readiness,
|
|
63
60
|
};
|
|
64
61
|
const wrapper = ({ children }: PropsWithChildren) => (
|
|
65
62
|
<Provider store={store}>{children}</Provider>
|
|
@@ -73,19 +70,97 @@ function setup(strict = false) {
|
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
beforeEach(() => {
|
|
76
|
-
vi.mocked(
|
|
73
|
+
vi.mocked(createNotebookTransport).mockReset();
|
|
77
74
|
});
|
|
78
75
|
|
|
79
76
|
describe("useNotebookClient", () => {
|
|
77
|
+
it.each([true, false])(
|
|
78
|
+
"rechecks health after failed startup (healthy: %s)",
|
|
79
|
+
async (healthy) => {
|
|
80
|
+
const fake = makeTransport();
|
|
81
|
+
vi.mocked(createNotebookTransport).mockImplementation(
|
|
82
|
+
({ url }) =>
|
|
83
|
+
async (attempt, handlers) => {
|
|
84
|
+
if (typeof url === "function") {
|
|
85
|
+
await url();
|
|
86
|
+
}
|
|
87
|
+
if (!attempt.signal.aborted) {
|
|
88
|
+
return fake.transport(attempt, handlers);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
const waitUntilReady = vi
|
|
93
|
+
.fn()
|
|
94
|
+
.mockRejectedValue(new Error("Startup gave up"));
|
|
95
|
+
const checkHealth = vi.fn().mockResolvedValue(healthy);
|
|
96
|
+
const { result, store, unmount } = setup(false, {
|
|
97
|
+
waitUntilReady,
|
|
98
|
+
checkHealth,
|
|
99
|
+
});
|
|
100
|
+
await act(async () => Promise.resolve());
|
|
101
|
+
expect(store.get(result.current.connectionAtom).state).toBe(
|
|
102
|
+
ConnectionState.CLOSED,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await act(async () => result.current.reconnect());
|
|
106
|
+
expect({
|
|
107
|
+
healthChecks: checkHealth.mock.calls.length,
|
|
108
|
+
initialWaits: waitUntilReady.mock.calls.length,
|
|
109
|
+
connections: fake.opened.length,
|
|
110
|
+
state: store.get(result.current.connectionAtom).state,
|
|
111
|
+
}).toEqual({
|
|
112
|
+
healthChecks: 1,
|
|
113
|
+
initialWaits: 1,
|
|
114
|
+
connections: healthy ? 1 : 0,
|
|
115
|
+
state: healthy ? ConnectionState.CONNECTING : ConnectionState.CLOSED,
|
|
116
|
+
});
|
|
117
|
+
act(() => fake.opened.at(-1)?.open());
|
|
118
|
+
expect(store.get(result.current.connectionAtom).state).toBe(
|
|
119
|
+
healthy ? ConnectionState.OPEN : ConnectionState.CLOSED,
|
|
120
|
+
);
|
|
121
|
+
unmount();
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
it("waits for initial readiness without reviving an unmounted connection", async () => {
|
|
126
|
+
const ready = new Deferred<void>();
|
|
127
|
+
const resolvedUrl = vi.fn();
|
|
128
|
+
vi.mocked(createNotebookTransport).mockImplementation(
|
|
129
|
+
({ url }) =>
|
|
130
|
+
async () => {
|
|
131
|
+
if (typeof url === "function") {
|
|
132
|
+
await url();
|
|
133
|
+
resolvedUrl();
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
const { result, store, unmount } = setup(false, {
|
|
138
|
+
waitUntilReady: () => ready.promise,
|
|
139
|
+
});
|
|
140
|
+
await act(async () => Promise.resolve());
|
|
141
|
+
expect(resolvedUrl).not.toHaveBeenCalled();
|
|
142
|
+
expect(store.get(result.current.connectionAtom)).toEqual({
|
|
143
|
+
state: ConnectionState.CONNECTING,
|
|
144
|
+
});
|
|
145
|
+
unmount();
|
|
146
|
+
await act(async () => ready.resolve());
|
|
147
|
+
expect(store.get(result.current.connectionAtom)).toEqual({
|
|
148
|
+
state: ConnectionState.NOT_STARTED,
|
|
149
|
+
});
|
|
150
|
+
expect(store.get(notebookClientAtom)).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
80
153
|
it("exposes host presence to the UI without waiting for kernel-ready", async () => {
|
|
81
|
-
const
|
|
82
|
-
vi.mocked(
|
|
154
|
+
const fake = makeTransport();
|
|
155
|
+
vi.mocked(createNotebookTransport).mockReturnValue(fake.transport);
|
|
83
156
|
const { result, store, unmount } = setup();
|
|
84
157
|
await act(async () => Promise.resolve());
|
|
85
158
|
expect(store.get(notebookClientAtom)).toBe(result.current);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
159
|
+
const [connection] = fake.opened;
|
|
160
|
+
act(() => {
|
|
161
|
+
connection.open();
|
|
162
|
+
fixtureMessages("notebook-events.sse").forEach(connection.receive);
|
|
163
|
+
});
|
|
89
164
|
expect(store.get(agentAttachmentsAtom)).toEqual([
|
|
90
165
|
{
|
|
91
166
|
id: "a2",
|
|
@@ -101,49 +176,48 @@ describe("useNotebookClient", () => {
|
|
|
101
176
|
});
|
|
102
177
|
|
|
103
178
|
it("uses current callbacks and URL providers without reconnecting on a rerender", async () => {
|
|
104
|
-
const
|
|
105
|
-
vi.mocked(
|
|
179
|
+
const fake = makeTransport();
|
|
180
|
+
vi.mocked(createNotebookTransport).mockReturnValue(fake.transport);
|
|
106
181
|
const { rerender, options, unmount } = setup();
|
|
107
182
|
await act(async () => Promise.resolve());
|
|
108
183
|
const onNotification = vi.fn();
|
|
109
184
|
rerender({ ...options, onNotification, url: () => "ws://redirected/ws" });
|
|
110
|
-
act(() =>
|
|
185
|
+
act(() => fake.notify());
|
|
111
186
|
expect(options.onNotification).not.toHaveBeenCalled();
|
|
112
187
|
expect(onNotification).toHaveBeenCalledExactlyOnceWith({
|
|
113
188
|
data: { op: "reconnected" },
|
|
114
189
|
});
|
|
115
|
-
expect(
|
|
116
|
-
|
|
190
|
+
expect(createNotebookTransport).toHaveBeenCalledOnce();
|
|
191
|
+
const [{ url }] = vi.mocked(createNotebookTransport).mock.calls[0];
|
|
192
|
+
expect(String(await (url as () => Promise<string>)())).toBe(
|
|
117
193
|
"ws://redirected/ws",
|
|
118
194
|
);
|
|
119
195
|
unmount();
|
|
120
196
|
});
|
|
121
197
|
|
|
122
|
-
it("
|
|
123
|
-
const
|
|
124
|
-
vi.mocked(
|
|
198
|
+
it("closes every stream under Strict Mode and only follows the active lifetime", async () => {
|
|
199
|
+
const fakes: ReturnType<typeof makeTransport>[] = [];
|
|
200
|
+
vi.mocked(createNotebookTransport).mockImplementation(() => {
|
|
125
201
|
const next = makeTransport();
|
|
126
|
-
|
|
202
|
+
fakes.push(next);
|
|
127
203
|
return next.transport;
|
|
128
204
|
});
|
|
129
205
|
const { result, store, options, unmount } = setup(true);
|
|
130
206
|
await act(async () => Promise.resolve());
|
|
131
|
-
expect(
|
|
132
|
-
expect(
|
|
133
|
-
|
|
134
|
-
|
|
207
|
+
expect(fakes).toHaveLength(2);
|
|
208
|
+
expect(fakes[0].opened.every((attempt) => attempt.signal.aborted)).toBe(
|
|
209
|
+
true,
|
|
210
|
+
);
|
|
211
|
+
const [active] = fakes[1].opened;
|
|
212
|
+
act(() => active.open());
|
|
135
213
|
expect(store.get(result.current.connectionAtom)).toEqual({
|
|
136
214
|
state: ConnectionState.OPEN,
|
|
137
215
|
});
|
|
138
|
-
act(() =>
|
|
139
|
-
for (const transport of transports) {
|
|
140
|
-
transport.notify();
|
|
141
|
-
}
|
|
142
|
-
});
|
|
216
|
+
act(() => fakes.forEach((fake) => fake.notify()));
|
|
143
217
|
expect(options.onNotification).toHaveBeenCalledOnce();
|
|
144
218
|
unmount();
|
|
145
|
-
expect(
|
|
219
|
+
expect(active.signal.aborted).toBe(true);
|
|
146
220
|
await result.current.reconnect();
|
|
147
|
-
expect(
|
|
221
|
+
expect(fakes[1].opened).toHaveLength(1);
|
|
148
222
|
});
|
|
149
223
|
});
|