@marimo-team/frontend 0.25.3-dev80 → 0.25.3-dev81
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-DFNLDjfX.js} +3 -3
- 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-OOQHBTnr.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-DafDcIAI.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-C4GjmtQ-.js} +1 -1
- package/dist/assets/{index-D0IWODMU.js → index-jR_KRDMC.js} +7 -7
- package/dist/assets/{layout-CGC8ajpe.js → layout-Bq7wRzdR.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-Cnu2Flfw.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-BWu6kkxZ.js} +1 -1
- package/dist/assets/{run-page-CLP1s8uX.js → run-page-BJq9MULx.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-OHQ7Z0RY.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-CqXa3U6u.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/__tests__/notebook-client.test.ts +368 -350
- package/src/core/host/notebook-client.ts +129 -186
- 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/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
|
@@ -1,42 +1,42 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
// @vitest-environment jsdom
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { type NotebookTransport, StreamStopped } from "@marimo-team/client";
|
|
5
5
|
import { createStore } from "jotai";
|
|
6
|
-
import {
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { Deferred } from "@/utils/Deferred";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { ConnectionState } from "../../websocket/types";
|
|
9
|
+
import type { ConnectionStatus } from "../connection";
|
|
10
10
|
import { createNotebookClient } from "../notebook-client";
|
|
11
11
|
import { fixtureEvents, fixtureMessages } from "./fixtures";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
/** One connection the fake transport opened. */
|
|
14
|
+
interface Attempt {
|
|
15
|
+
readonly cursor: string | null;
|
|
16
|
+
readonly signal: AbortSignal;
|
|
17
|
+
open(): void;
|
|
18
|
+
receive(message: unknown): void;
|
|
19
|
+
/** End the connection so the stream reconnects. */
|
|
20
|
+
drop(): void;
|
|
21
|
+
/** End the stream, with the status the editor shows if the transport has one. */
|
|
22
|
+
stop(status?: ConnectionStatus): void;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function setup(
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
function setup() {
|
|
26
|
+
const attempts: Attempt[] = [];
|
|
27
|
+
const transport: NotebookTransport = ({ cursor, signal }, handlers) =>
|
|
28
|
+
new Promise((resolve, reject) => {
|
|
29
|
+
attempts.push({
|
|
30
|
+
cursor,
|
|
31
|
+
signal,
|
|
32
|
+
open: handlers.open,
|
|
33
|
+
receive: handlers.message,
|
|
34
|
+
drop: resolve,
|
|
35
|
+
stop: (status) =>
|
|
36
|
+
reject(new StreamStopped("stopped", { cause: status })),
|
|
37
|
+
});
|
|
38
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
39
|
+
});
|
|
40
40
|
const onNotification = vi.fn();
|
|
41
41
|
const checkHealth = vi.fn(async () => true);
|
|
42
42
|
const store = createStore();
|
|
@@ -44,51 +44,64 @@ function setup(url: () => string = () => "ws://localhost/ws") {
|
|
|
44
44
|
const client = controller.client;
|
|
45
45
|
const connect = (signal: AbortSignal) =>
|
|
46
46
|
controller.connect({
|
|
47
|
-
|
|
48
|
-
provider = next;
|
|
49
|
-
return transport;
|
|
50
|
-
},
|
|
51
|
-
url,
|
|
47
|
+
transport,
|
|
52
48
|
signal,
|
|
53
|
-
waitUntilReady: () => healthy.promise,
|
|
54
49
|
checkHealth,
|
|
55
50
|
onNotification: (notification) => onNotification({ data: notification }),
|
|
56
|
-
decodeJson: jsonParseWithSpecialChar,
|
|
57
|
-
classifyClose: classifyCloseEvent,
|
|
58
51
|
});
|
|
52
|
+
let next = 0;
|
|
53
|
+
/** Wait for the next connection, and open it. */
|
|
54
|
+
const opened = async () => {
|
|
55
|
+
await vi.waitFor(() => expect(attempts.length).toBeGreaterThan(next));
|
|
56
|
+
const attempt = attempts[next++];
|
|
57
|
+
attempt.open();
|
|
58
|
+
return attempt;
|
|
59
|
+
};
|
|
59
60
|
return {
|
|
60
|
-
|
|
61
|
-
healthy,
|
|
62
|
-
transport,
|
|
61
|
+
attempts,
|
|
63
62
|
client,
|
|
64
63
|
connect,
|
|
65
64
|
onNotification,
|
|
66
65
|
store,
|
|
67
66
|
checkHealth,
|
|
68
|
-
|
|
69
|
-
receive: (message: unknown) =>
|
|
70
|
-
events.dispatchEvent(
|
|
71
|
-
new MessageEvent("message", { data: JSON.stringify(message) }),
|
|
72
|
-
),
|
|
67
|
+
opened,
|
|
73
68
|
};
|
|
74
69
|
}
|
|
75
70
|
|
|
71
|
+
const lifetimes: AbortController[] = [];
|
|
72
|
+
function lifetime() {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
lifetimes.push(controller);
|
|
75
|
+
return controller;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
lifetimes.splice(0).forEach((controller) => controller.abort());
|
|
80
|
+
vi.useRealTimers();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** Wait out the stream's backoff before it reconnects. */
|
|
84
|
+
const backoff = () => vi.advanceTimersByTimeAsync(2000);
|
|
85
|
+
|
|
86
|
+
const kernelReady = {
|
|
87
|
+
event: "marimo",
|
|
88
|
+
data: { marimo: { op: "kernel-ready", resumed: false } },
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const notebook = {
|
|
92
|
+
id: "nb/with space",
|
|
93
|
+
project_id: "p1",
|
|
94
|
+
path: null,
|
|
95
|
+
name: "Notebook",
|
|
96
|
+
operations: ["notebook.edit"],
|
|
97
|
+
updated_at: null,
|
|
98
|
+
};
|
|
99
|
+
|
|
76
100
|
describe("NotebookClient lifetime", () => {
|
|
77
101
|
it("exposes identity and capabilities only after the notebook is ready", async () => {
|
|
78
|
-
const { connect,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
healthy.resolve();
|
|
82
|
-
await healthy.promise;
|
|
83
|
-
events.dispatchEvent(new Event("open"));
|
|
84
|
-
const notebook = {
|
|
85
|
-
id: "nb/with space",
|
|
86
|
-
project_id: "p1",
|
|
87
|
-
path: null,
|
|
88
|
-
name: "Notebook",
|
|
89
|
-
operations: ["notebook.edit"],
|
|
90
|
-
updated_at: null,
|
|
91
|
-
};
|
|
102
|
+
const { connect, opened, client, store } = setup();
|
|
103
|
+
connect(lifetime().signal);
|
|
104
|
+
const { receive } = await opened();
|
|
92
105
|
receive({ event: "notebook", data: { notebook } });
|
|
93
106
|
expect(store.get(client.notebookAtom)).toBeNull();
|
|
94
107
|
receive({ event: "ready", id: "1", data: {} });
|
|
@@ -101,396 +114,401 @@ describe("NotebookClient lifetime", () => {
|
|
|
101
114
|
expect(store.get(client.notebookAtom)?.operations).toEqual([]);
|
|
102
115
|
receive({ event: "notebook.removed", id: "3", data: { id: notebook.id } });
|
|
103
116
|
expect(store.get(client.notebookAtom)).toBeNull();
|
|
104
|
-
lifetime.abort();
|
|
105
117
|
});
|
|
106
118
|
|
|
107
119
|
it("rebuilds the replica after a reconnect reset selects a new notebook", async () => {
|
|
108
|
-
|
|
109
|
-
const { connect,
|
|
110
|
-
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
connect(lifetime.signal);
|
|
114
|
-
healthy.resolve();
|
|
115
|
-
await healthy.promise;
|
|
116
|
-
events.dispatchEvent(new Event("open"));
|
|
117
|
-
fixtureMessages("notebook-events.sse").forEach(receive);
|
|
120
|
+
vi.useFakeTimers();
|
|
121
|
+
const { connect, opened, client, store } = setup();
|
|
122
|
+
connect(lifetime().signal);
|
|
123
|
+
const first = await opened();
|
|
124
|
+
fixtureMessages("notebook-events.sse").forEach(first.receive);
|
|
118
125
|
expect(store.get(client.documentAtom)?.revision.notebook_id).toBe("n1");
|
|
119
126
|
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
first.drop();
|
|
128
|
+
await backoff();
|
|
122
129
|
expect(store.get(client.documentAtom)?.revision.notebook_id).toBe("n1");
|
|
123
130
|
expect(store.get(client.notebookAtom)).toBeNull();
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
const second = await opened();
|
|
132
|
+
expect(second.cursor).toBe("97");
|
|
133
|
+
second.receive({ event: "reset", data: {} });
|
|
126
134
|
expect(store.get(client.documentAtom)).toBeNull();
|
|
127
135
|
for (const event of fixtureMessages("notebook-events.sse")) {
|
|
128
|
-
receive(
|
|
129
|
-
JSON.parse(
|
|
130
|
-
JSON.stringify(event).replaceAll('"n1"', JSON.stringify(notebookId)),
|
|
131
|
-
),
|
|
136
|
+
second.receive(
|
|
137
|
+
JSON.parse(JSON.stringify(event).replaceAll('"n1"', '"n2"')),
|
|
132
138
|
);
|
|
133
139
|
}
|
|
134
|
-
expect(store.get(client.documentAtom)?.revision.notebook_id).toBe(
|
|
135
|
-
notebookId,
|
|
136
|
-
);
|
|
140
|
+
expect(store.get(client.documentAtom)?.revision.notebook_id).toBe("n2");
|
|
137
141
|
expect(store.get(client.connectionAtom).state).toBe(ConnectionState.OPEN);
|
|
138
|
-
expect(store.get(client.notebookAtom)?.id).toBe(
|
|
139
|
-
lifetime.abort();
|
|
142
|
+
expect(store.get(client.notebookAtom)?.id).toBe("n2");
|
|
140
143
|
});
|
|
141
144
|
|
|
142
|
-
it.each([
|
|
143
|
-
"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
event: "marimo",
|
|
155
|
-
data: { marimo: { op: "reconnected" } },
|
|
156
|
-
}),
|
|
157
|
-
}),
|
|
158
|
-
);
|
|
159
|
-
expect(store.get(client.connectionAtom)).toEqual({
|
|
160
|
-
state: ConnectionState.CLOSED,
|
|
161
|
-
code: "KERNEL_DISCONNECTED",
|
|
162
|
-
reason: "Failed to decode notebook message",
|
|
145
|
+
it.each([
|
|
146
|
+
["a message that is not an event", "{broken"],
|
|
147
|
+
["an event from another notebook", "identity"],
|
|
148
|
+
])("stops consuming a stream on %s", async (_name, message) => {
|
|
149
|
+
const { connect, opened, client, store, onNotification } = setup();
|
|
150
|
+
connect(lifetime().signal);
|
|
151
|
+
const attempt = await opened();
|
|
152
|
+
if (message === "identity") {
|
|
153
|
+
attempt.receive({ event: "notebook", data: { notebook } });
|
|
154
|
+
attempt.receive({
|
|
155
|
+
event: "notebook",
|
|
156
|
+
data: { notebook: { ...notebook, id: "n2" } },
|
|
163
157
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
lifetime.abort();
|
|
167
|
-
},
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
it("resumes from the last cursor until reset or a new session", async () => {
|
|
171
|
-
const session = { id: "first", token: "a" };
|
|
172
|
-
const { connect, events, healthy, urls, receive } = setup(
|
|
173
|
-
() =>
|
|
174
|
-
`ws://localhost/ws?session_id=${session.id}&access_token=${session.token}&cursor=999`,
|
|
175
|
-
);
|
|
176
|
-
const lifetime = new AbortController();
|
|
177
|
-
connect(lifetime.signal);
|
|
178
|
-
healthy.resolve();
|
|
179
|
-
await healthy.promise;
|
|
180
|
-
expect(urls[0].searchParams.get("cursor")).toBeNull();
|
|
181
|
-
|
|
182
|
-
const snapshot = fixtureMessages("notebook-events.sse");
|
|
183
|
-
events.dispatchEvent(new Event("open"));
|
|
184
|
-
for (const message of snapshot) {
|
|
185
|
-
receive(message);
|
|
158
|
+
} else {
|
|
159
|
+
attempt.receive(message);
|
|
186
160
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
expect(
|
|
204
|
-
expect(
|
|
205
|
-
lifetime.abort();
|
|
161
|
+
attempt.receive({
|
|
162
|
+
event: "marimo",
|
|
163
|
+
data: { marimo: { op: "reconnected" } },
|
|
164
|
+
});
|
|
165
|
+
await vi.waitFor(() =>
|
|
166
|
+
expect(store.get(client.connectionAtom).state).toBe(
|
|
167
|
+
ConnectionState.CLOSED,
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
expect(store.get(client.connectionAtom)).toMatchInlineSnapshot(`
|
|
171
|
+
{
|
|
172
|
+
"code": "KERNEL_DISCONNECTED",
|
|
173
|
+
"reason": "Failed to decode notebook message",
|
|
174
|
+
"state": "CLOSED",
|
|
175
|
+
}
|
|
176
|
+
`);
|
|
177
|
+
expect(attempt.signal.aborted).toBe(true);
|
|
178
|
+
expect(onNotification).not.toHaveBeenCalled();
|
|
206
179
|
});
|
|
207
180
|
|
|
208
181
|
it("exposes the confirmed document until the lifetime ends", async () => {
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await
|
|
214
|
-
events.dispatchEvent(new Event("open"));
|
|
182
|
+
vi.useFakeTimers();
|
|
183
|
+
const { connect, opened, client, store } = setup();
|
|
184
|
+
const current = lifetime();
|
|
185
|
+
connect(current.signal);
|
|
186
|
+
const attempt = await opened();
|
|
215
187
|
const snapshot = fixtureMessages("notebook-events.sse");
|
|
216
|
-
|
|
217
|
-
receive(message);
|
|
218
|
-
}
|
|
188
|
+
snapshot.slice(0, -3).forEach(attempt.receive);
|
|
219
189
|
expect(store.get(client.documentAtom)).toBeNull();
|
|
220
|
-
|
|
221
|
-
receive(message);
|
|
222
|
-
}
|
|
190
|
+
snapshot.slice(-3).forEach(attempt.receive);
|
|
223
191
|
expect(store.get(client.documentAtom)?.revision.order).toEqual([
|
|
224
192
|
"c1",
|
|
225
193
|
"c3",
|
|
226
194
|
"k7f2",
|
|
227
195
|
]);
|
|
228
|
-
|
|
196
|
+
attempt.drop();
|
|
197
|
+
await backoff();
|
|
229
198
|
expect(store.get(client.documentAtom)?.revision.order).toEqual([
|
|
230
199
|
"c1",
|
|
231
200
|
"c3",
|
|
232
201
|
"k7f2",
|
|
233
202
|
]);
|
|
234
|
-
|
|
203
|
+
current.abort();
|
|
235
204
|
expect(store.get(client.documentAtom)).toBeNull();
|
|
236
205
|
});
|
|
237
206
|
|
|
238
|
-
it(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
lifetime.abort();
|
|
255
|
-
});
|
|
207
|
+
it.each([
|
|
208
|
+
["a malformed native envelope", { event: "marimo", id: "98", data: {} }],
|
|
209
|
+
["null native event data", { event: "marimo", id: "98", data: null }],
|
|
210
|
+
["an invalid protocol record", { event: "cell", id: "98", data: {} }],
|
|
211
|
+
["an undecodable message", "{broken"],
|
|
212
|
+
["a changed notebook", { event: "notebook", id: "98", data: { notebook } }],
|
|
213
|
+
])(
|
|
214
|
+
"rebuilds from a fresh snapshot after rejecting %s",
|
|
215
|
+
async (_name, message) => {
|
|
216
|
+
const { connect, opened, client, store, onNotification } = setup();
|
|
217
|
+
connect(lifetime().signal);
|
|
218
|
+
const first = await opened();
|
|
219
|
+
const snapshot = fixtureMessages("notebook-events.sse");
|
|
220
|
+
snapshot.forEach(first.receive);
|
|
221
|
+
expect(store.get(client.documentAtom)).not.toBeNull();
|
|
222
|
+
onNotification.mockClear();
|
|
256
223
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
client,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
224
|
+
first.receive(message);
|
|
225
|
+
first.receive(kernelReady);
|
|
226
|
+
expect({
|
|
227
|
+
connection: store.get(client.connectionAtom),
|
|
228
|
+
document: store.get(client.documentAtom),
|
|
229
|
+
attachments: store.get(client.attachmentsAtom),
|
|
230
|
+
aborted: first.signal.aborted,
|
|
231
|
+
notifications: onNotification.mock.calls,
|
|
232
|
+
}).toEqual({
|
|
233
|
+
connection: {
|
|
234
|
+
state: ConnectionState.CLOSED,
|
|
235
|
+
code: "KERNEL_DISCONNECTED",
|
|
236
|
+
reason: "Failed to decode notebook message",
|
|
237
|
+
},
|
|
238
|
+
document: null,
|
|
239
|
+
attachments: [],
|
|
240
|
+
aborted: true,
|
|
241
|
+
notifications: [],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await client.reconnect();
|
|
245
|
+
const second = await opened();
|
|
246
|
+
expect(second.cursor).toBeNull();
|
|
247
|
+
snapshot.forEach(second.receive);
|
|
248
|
+
expect(store.get(client.documentAtom)?.revision.notebook_id).toBe("n1");
|
|
249
|
+
expect(store.get(client.connectionAtom)).toEqual({
|
|
250
|
+
state: ConnectionState.OPEN,
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
it.each([
|
|
256
|
+
[
|
|
257
|
+
"a final close",
|
|
258
|
+
{
|
|
259
|
+
state: ConnectionState.CLOSED,
|
|
260
|
+
code: "KERNEL_DISCONNECTED",
|
|
261
|
+
reason: "not authorized",
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
["the host giving up", undefined],
|
|
265
|
+
] as const)(
|
|
266
|
+
"shows the stop after %s and resumes on an explicit reconnect",
|
|
267
|
+
async (_name, status) => {
|
|
268
|
+
const { connect, opened, attempts, client, store, checkHealth } = setup();
|
|
269
|
+
connect(lifetime().signal);
|
|
270
|
+
const first = await opened();
|
|
271
|
+
first.receive({ event: "ready", id: "5", data: {} });
|
|
272
|
+
first.stop(status);
|
|
273
|
+
await vi.waitFor(() =>
|
|
274
|
+
expect(store.get(client.connectionAtom)).toEqual(
|
|
275
|
+
status ?? {
|
|
276
|
+
state: ConnectionState.CLOSED,
|
|
277
|
+
code: "KERNEL_DISCONNECTED",
|
|
278
|
+
reason: "kernel not found",
|
|
279
|
+
},
|
|
280
|
+
),
|
|
277
281
|
);
|
|
278
|
-
expect(
|
|
282
|
+
expect(attempts).toHaveLength(1);
|
|
279
283
|
await client.reconnect();
|
|
280
284
|
expect(checkHealth).toHaveBeenCalledOnce();
|
|
281
|
-
expect(
|
|
282
|
-
lifetime.abort();
|
|
285
|
+
expect((await opened()).cursor).toBe("5");
|
|
283
286
|
},
|
|
284
287
|
);
|
|
285
288
|
|
|
286
|
-
it("only resets the automatic retry budget after an open event", () => {
|
|
287
|
-
const { connect, events, transport } = setup();
|
|
288
|
-
const lifetime = new AbortController();
|
|
289
|
-
connect(lifetime.signal);
|
|
290
|
-
events.dispatchEvent(new CloseEvent("close"));
|
|
291
|
-
events.dispatchEvent(new Event("error"));
|
|
292
|
-
events.dispatchEvent(new CloseEvent("close"));
|
|
293
|
-
expect(transport.reconnect).toHaveBeenCalledOnce();
|
|
294
|
-
events.dispatchEvent(new Event("open"));
|
|
295
|
-
events.dispatchEvent(new CloseEvent("close"));
|
|
296
|
-
expect(transport.reconnect).toHaveBeenCalledTimes(2);
|
|
297
|
-
lifetime.abort();
|
|
298
|
-
});
|
|
299
|
-
|
|
300
289
|
it("does not revive an aborted lifetime when a manual health check finishes", async () => {
|
|
301
|
-
const { connect,
|
|
290
|
+
const { connect, opened, attempts, client, checkHealth } = setup();
|
|
302
291
|
const health = new Deferred<boolean>();
|
|
303
292
|
checkHealth.mockReturnValue(health.promise);
|
|
304
|
-
const
|
|
305
|
-
connect(
|
|
293
|
+
const current = lifetime();
|
|
294
|
+
connect(current.signal);
|
|
295
|
+
const first = await opened();
|
|
296
|
+
first.stop({
|
|
297
|
+
state: ConnectionState.CLOSED,
|
|
298
|
+
code: "KERNEL_DISCONNECTED",
|
|
299
|
+
reason: "",
|
|
300
|
+
});
|
|
301
|
+
await vi.waitFor(() => expect(checkHealth).not.toHaveBeenCalled());
|
|
306
302
|
const attempt = client.reconnect();
|
|
307
|
-
|
|
303
|
+
current.abort();
|
|
308
304
|
health.resolve(true);
|
|
309
305
|
await attempt;
|
|
310
|
-
expect(
|
|
306
|
+
expect(attempts).toHaveLength(1);
|
|
311
307
|
});
|
|
312
308
|
|
|
313
|
-
it("
|
|
314
|
-
const {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
309
|
+
it("discards an interrupted snapshot before a manual reconnect", async () => {
|
|
310
|
+
const { connect, opened, client, store } = setup();
|
|
311
|
+
connect(lifetime().signal);
|
|
312
|
+
const first = await opened();
|
|
313
|
+
const snapshot = fixtureEvents("notebook-events.sse");
|
|
314
|
+
for (const event of snapshot) {
|
|
315
|
+
if (event.event === "ready") {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
first.receive(event);
|
|
319
|
+
}
|
|
320
|
+
first.stop();
|
|
321
|
+
await vi.waitFor(() =>
|
|
322
|
+
expect(store.get(client.connectionAtom).state).toBe(
|
|
323
|
+
ConnectionState.CLOSED,
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
await client.reconnect();
|
|
328
|
+
const second = await opened();
|
|
329
|
+
expect(second.cursor).toBeNull();
|
|
330
|
+
for (const event of snapshot) {
|
|
331
|
+
if (event.event === "notebook") {
|
|
332
|
+
second.receive(event);
|
|
333
|
+
}
|
|
334
|
+
if (event.event === "document") {
|
|
335
|
+
second.receive({
|
|
336
|
+
...event,
|
|
337
|
+
data: { document: { ...event.data.document, order: [] } },
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
second.receive({ event: "ready", id: "100", data: {} });
|
|
342
|
+
expect({
|
|
343
|
+
connection: store.get(client.connectionAtom),
|
|
344
|
+
order: store.get(client.documentAtom)?.revision.order,
|
|
345
|
+
attachments: store.get(client.attachmentsAtom),
|
|
346
|
+
}).toEqual({
|
|
347
|
+
connection: { state: ConnectionState.OPEN },
|
|
348
|
+
order: [],
|
|
349
|
+
attachments: [],
|
|
350
|
+
});
|
|
323
351
|
});
|
|
324
352
|
|
|
325
|
-
it("
|
|
326
|
-
const { connect, client, store
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
data: JSON.stringify({ data: { op: "reconnected" } }),
|
|
334
|
-
}),
|
|
335
|
-
);
|
|
336
|
-
expect(transport.reconnect).not.toHaveBeenCalled();
|
|
353
|
+
it("does not connect after abort", async () => {
|
|
354
|
+
const { connect, attempts, client, store } = setup();
|
|
355
|
+
const current = lifetime();
|
|
356
|
+
connect(current.signal);
|
|
357
|
+
current.abort();
|
|
358
|
+
await client.reconnect();
|
|
359
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
360
|
+
expect(attempts).toHaveLength(0);
|
|
337
361
|
expect(store.get(client.connectionAtom)).toEqual({
|
|
338
362
|
state: ConnectionState.NOT_STARTED,
|
|
339
363
|
});
|
|
340
|
-
expect(onNotification).not.toHaveBeenCalled();
|
|
341
364
|
});
|
|
342
365
|
|
|
343
|
-
it("
|
|
344
|
-
const { connect,
|
|
345
|
-
const first =
|
|
366
|
+
it("forwards notifications for the current lifetime only", async () => {
|
|
367
|
+
const { connect, opened, onNotification } = setup();
|
|
368
|
+
const first = lifetime();
|
|
346
369
|
connect(first.signal);
|
|
370
|
+
const earlier = await opened();
|
|
347
371
|
first.abort();
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
data: JSON.stringify({
|
|
356
|
-
event: "marimo",
|
|
357
|
-
data: { marimo: { op: "reconnected" } },
|
|
358
|
-
}),
|
|
359
|
-
}),
|
|
360
|
-
);
|
|
372
|
+
connect(lifetime().signal);
|
|
373
|
+
const current = await opened();
|
|
374
|
+
earlier.receive(kernelReady);
|
|
375
|
+
current.receive({
|
|
376
|
+
event: "marimo",
|
|
377
|
+
data: { marimo: { op: "reconnected" } },
|
|
378
|
+
});
|
|
361
379
|
expect(onNotification).toHaveBeenCalledExactlyOnceWith({
|
|
362
380
|
data: { op: "reconnected" },
|
|
363
381
|
});
|
|
364
|
-
second.abort();
|
|
365
382
|
});
|
|
366
383
|
});
|
|
367
384
|
|
|
368
|
-
async function open() {
|
|
369
|
-
const url = vi.fn(() => "ws://localhost/ws?session_id=first&cursor=999");
|
|
370
|
-
const setupResult = setup(url);
|
|
371
|
-
const lifetime = new AbortController();
|
|
372
|
-
setupResult.connect(lifetime.signal);
|
|
373
|
-
setupResult.healthy.resolve();
|
|
374
|
-
await setupResult.healthy.promise;
|
|
375
|
-
setupResult.events.dispatchEvent(new Event("open"));
|
|
376
|
-
return { ...setupResult, lifetime, url };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
385
|
describe("NotebookClient attachments", () => {
|
|
380
386
|
const [browser, agent] = fixtureEvents("notebook-events.sse").flatMap(
|
|
381
387
|
(event) => (event.event === "attachment" ? [event.data.attachment] : []),
|
|
382
388
|
);
|
|
383
389
|
|
|
390
|
+
async function open() {
|
|
391
|
+
const result = setup();
|
|
392
|
+
result.connect(lifetime().signal);
|
|
393
|
+
return { ...result, attempt: await result.opened() };
|
|
394
|
+
}
|
|
395
|
+
|
|
384
396
|
it("publishes a complete snapshot and applies live changes independently of kernel readiness", async () => {
|
|
385
|
-
const { client, store,
|
|
397
|
+
const { client, store, attempt } = await open();
|
|
386
398
|
const snapshot = fixtureMessages("notebook-events.sse");
|
|
387
399
|
const readyIndex = snapshot.findIndex((event) => event.event === "ready");
|
|
388
|
-
snapshot.slice(0, readyIndex).forEach(receive);
|
|
400
|
+
snapshot.slice(0, readyIndex).forEach(attempt.receive);
|
|
389
401
|
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
390
|
-
snapshot.slice(readyIndex).forEach(receive);
|
|
402
|
+
snapshot.slice(readyIndex).forEach(attempt.receive);
|
|
391
403
|
expect(store.get(client.attachmentsAtom)).toEqual([browser, agent]);
|
|
392
404
|
const renamed = { ...agent, name: "Renamed agent" };
|
|
393
|
-
receive({
|
|
394
|
-
|
|
405
|
+
attempt.receive({
|
|
406
|
+
event: "attachment",
|
|
407
|
+
id: "98",
|
|
408
|
+
data: { attachment: renamed },
|
|
409
|
+
});
|
|
410
|
+
attempt.receive({
|
|
395
411
|
event: "attachment.removed",
|
|
396
412
|
id: "99",
|
|
397
413
|
data: { id: browser.id },
|
|
398
414
|
});
|
|
399
415
|
expect(store.get(client.attachmentsAtom)).toEqual([renamed]);
|
|
400
|
-
receive(
|
|
416
|
+
attempt.receive(kernelReady);
|
|
401
417
|
expect(store.get(client.attachmentsAtom)).toEqual([renamed]);
|
|
402
|
-
receive({
|
|
403
|
-
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
404
|
-
lifetime.abort();
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it("resumes attachments alongside an incomplete document and waits for replay ready", async () => {
|
|
408
|
-
const { client, store, receive, events, urls, lifetime } = await open();
|
|
409
|
-
expect(urls[0].searchParams.has("cursor")).toBe(false);
|
|
410
|
-
fixtureMessages("notebook-events.sse").forEach(receive);
|
|
411
|
-
const confirmed = store.get(client.documentAtom);
|
|
412
|
-
const edits = fixtureMessages("edits.sse");
|
|
413
|
-
edits.slice(0, 3).forEach(receive);
|
|
414
|
-
const renamed = { ...agent, name: "Renamed agent" };
|
|
415
|
-
receive({ event: "attachment", id: "112a", data: { attachment: renamed } });
|
|
416
|
-
expect(store.get(client.attachmentsAtom)).toEqual([browser, renamed]);
|
|
417
|
-
events.dispatchEvent(new CloseEvent("close"));
|
|
418
|
-
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
419
|
-
expect(urls.at(-1)!.searchParams.get("cursor")).toBe("112a");
|
|
420
|
-
events.dispatchEvent(new Event("open"));
|
|
421
|
-
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
422
|
-
expect(store.get(client.documentAtom)).toBe(confirmed);
|
|
423
|
-
edits.slice(3).forEach(receive);
|
|
424
|
-
receive({
|
|
418
|
+
attempt.receive({
|
|
425
419
|
event: "attachment.removed",
|
|
426
|
-
id: "
|
|
427
|
-
data: { id:
|
|
420
|
+
id: "100",
|
|
421
|
+
data: { id: agent.id },
|
|
428
422
|
});
|
|
429
423
|
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
430
|
-
receive({ event: "ready", id: "116", data: {} });
|
|
431
|
-
expect(store.get(client.attachmentsAtom)).toEqual([renamed]);
|
|
432
|
-
const revisions = fixtureEvents("edits.sse").flatMap((event) =>
|
|
433
|
-
event.event === "document" ? [event.data.document] : [],
|
|
434
|
-
);
|
|
435
|
-
expect(store.get(client.documentAtom)?.revision).toEqual(revisions.at(-1));
|
|
436
|
-
lifetime.abort();
|
|
437
424
|
});
|
|
438
425
|
|
|
439
|
-
it("
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
426
|
+
it.each(["automatic", "manual"])(
|
|
427
|
+
"preserves an incomplete live document across %s reconnect and waits for replay ready",
|
|
428
|
+
async (recovery) => {
|
|
429
|
+
vi.useFakeTimers();
|
|
430
|
+
const { client, store, attempt, opened } = await open();
|
|
431
|
+
expect(attempt.cursor).toBeNull();
|
|
432
|
+
fixtureMessages("notebook-events.sse").forEach(attempt.receive);
|
|
433
|
+
const confirmed = store.get(client.documentAtom);
|
|
434
|
+
const edits = fixtureMessages("edits.sse");
|
|
435
|
+
edits.slice(0, 3).forEach(attempt.receive);
|
|
436
|
+
const renamed = { ...agent, name: "Renamed agent" };
|
|
437
|
+
attempt.receive({
|
|
438
|
+
event: "attachment",
|
|
439
|
+
id: "112a",
|
|
440
|
+
data: { attachment: renamed },
|
|
441
|
+
});
|
|
442
|
+
expect(store.get(client.attachmentsAtom)).toEqual([browser, renamed]);
|
|
443
|
+
if (recovery === "automatic") {
|
|
444
|
+
attempt.drop();
|
|
445
|
+
await backoff();
|
|
446
|
+
} else {
|
|
447
|
+
attempt.stop();
|
|
448
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
449
|
+
await client.reconnect();
|
|
450
|
+
}
|
|
451
|
+
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
452
|
+
const replay = await opened();
|
|
453
|
+
expect(replay.cursor).toBe("112a");
|
|
454
|
+
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
455
|
+
expect(store.get(client.documentAtom)).toBe(confirmed);
|
|
456
|
+
edits.slice(3).forEach(replay.receive);
|
|
457
|
+
replay.receive({
|
|
458
|
+
event: "attachment.removed",
|
|
459
|
+
id: "115",
|
|
460
|
+
data: { id: browser.id },
|
|
461
|
+
});
|
|
462
|
+
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
463
|
+
replay.receive({ event: "ready", id: "116", data: {} });
|
|
464
|
+
expect(store.get(client.attachmentsAtom)).toEqual([renamed]);
|
|
465
|
+
const revisions = fixtureEvents("edits.sse").flatMap((event) =>
|
|
466
|
+
event.event === "document" ? [event.data.document] : [],
|
|
467
|
+
);
|
|
468
|
+
expect(store.get(client.documentAtom)?.revision).toEqual(
|
|
469
|
+
revisions.at(-1),
|
|
470
|
+
);
|
|
471
|
+
},
|
|
472
|
+
);
|
|
450
473
|
|
|
451
|
-
it.each(["
|
|
474
|
+
it.each(["drop", "stop", "removed", "abort"])(
|
|
452
475
|
"clears presence on %s and ignores messages after disposal",
|
|
453
476
|
async (reason) => {
|
|
454
|
-
|
|
455
|
-
|
|
477
|
+
vi.useFakeTimers();
|
|
478
|
+
const current = lifetime();
|
|
479
|
+
const result = setup();
|
|
480
|
+
result.connect(current.signal);
|
|
481
|
+
const { client, store } = result;
|
|
482
|
+
const attempt = await result.opened();
|
|
483
|
+
fixtureMessages("notebook-events.sse").forEach(attempt.receive);
|
|
456
484
|
expect(store.get(client.attachmentsAtom)).toEqual([browser, agent]);
|
|
457
485
|
switch (reason) {
|
|
458
|
-
case "
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
);
|
|
486
|
+
case "drop":
|
|
487
|
+
attempt.drop();
|
|
488
|
+
await backoff();
|
|
462
489
|
break;
|
|
463
|
-
case "
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
490
|
+
case "stop":
|
|
491
|
+
attempt.stop({
|
|
492
|
+
state: ConnectionState.CLOSED,
|
|
493
|
+
code: "KERNEL_DISCONNECTED",
|
|
494
|
+
reason: "not authorized",
|
|
495
|
+
});
|
|
496
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
468
497
|
break;
|
|
469
498
|
case "removed":
|
|
470
|
-
receive({
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
);
|
|
476
|
-
events.dispatchEvent(new CloseEvent("close"));
|
|
477
|
-
receive({ event: "ready", id: "98", data: {} });
|
|
499
|
+
attempt.receive({
|
|
500
|
+
event: "notebook.removed",
|
|
501
|
+
id: "98",
|
|
502
|
+
data: { id: "n1" },
|
|
503
|
+
});
|
|
478
504
|
break;
|
|
479
505
|
default:
|
|
480
|
-
|
|
506
|
+
current.abort();
|
|
481
507
|
}
|
|
482
508
|
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
483
|
-
|
|
484
|
-
fixtureMessages("notebook-events.sse").forEach(receive);
|
|
509
|
+
current.abort();
|
|
510
|
+
fixtureMessages("notebook-events.sse").forEach(attempt.receive);
|
|
485
511
|
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
486
512
|
},
|
|
487
513
|
);
|
|
488
|
-
|
|
489
|
-
it("keeps host resources empty for notification-only WASM connections", async () => {
|
|
490
|
-
const { client, store, receive, lifetime } = await open();
|
|
491
|
-
receive({ data: { op: "kernel-ready" } });
|
|
492
|
-
receive({ data: { op: "reconnected" } });
|
|
493
|
-
expect(store.get(client.attachmentsAtom)).toEqual([]);
|
|
494
|
-
lifetime.abort();
|
|
495
|
-
});
|
|
496
514
|
});
|