@marimo-team/islands 0.25.3-dev312 → 0.25.3-dev317
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-CCrWQ8Zi.js → ErrorBoundary-BzPZHD9j.js} +1 -1
- package/dist/{chat-ui-DkuxGEDp.js → chat-ui-DASKYW38.js} +13 -13
- package/dist/{code-visibility-DLBADpvq.js → code-visibility-nvHsJTCm.js} +31 -32
- package/dist/{common-Cc-bvilP.js → common-DmnownXk.js} +5074 -5074
- package/dist/{config-BVYA1P0C.js → config-CL7mwGLX.js} +87 -129
- package/dist/{data-grid-overlay-editor-CAEih52I.js → data-grid-overlay-editor-eXoZLGys.js} +1 -1
- package/dist/{error-entries-istW7Xbs.js → error-entries-kb4H6PAT.js} +533 -508
- package/dist/feedback-button-DDdckIvG.js +370 -0
- package/dist/{formats-CMa7L3E8.js → formats-qhBmYb0l.js} +1 -1
- package/dist/{glide-data-editor-hsf5AOwE.js → glide-data-editor-keVyefZP.js} +3 -3
- package/dist/{globals-Blgj9EWO.js → globals-UY7m7ERN.js} +1 -1
- package/dist/{html-to-image-pPbR2cDx.js → html-to-image-DR8VPB7E.js} +4018 -4021
- package/dist/main.js +606 -607
- package/dist/{number-overlay-editor-BgNsDtQP.js → number-overlay-editor-Dryq4VDA.js} +1 -1
- package/dist/{process-output-BySO1nIs.js → process-output-BDMtqL6K.js} +2 -2
- package/dist/{reveal-component-MUD9gGln.js → reveal-component-BIBrITpY.js} +4 -4
- package/dist/static-state-Dtocd9UO.js +68 -0
- package/dist/{vega-component-Bly2lDhz.js → vega-component-CeN8zClP.js} +9 -9
- package/package.json +1 -1
- package/src/__tests__/mount.test.ts +17 -58
- package/src/components/editor/actions/useRestartKernel.tsx +0 -5
- package/src/components/editor/cell/__tests__/useRunCells.test.tsx +0 -3
- package/src/components/editor/cell/useRunCells.ts +2 -2
- package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +2 -16
- package/src/components/editor/renderers/cell-array.tsx +0 -3
- package/src/core/edit-app.tsx +2 -7
- package/src/core/executions/__tests__/run.test.ts +0 -3
- package/src/core/executions/index.ts +1 -1
- package/src/core/executions/run.ts +1 -14
- package/src/core/islands/bootstrap.ts +0 -1
- package/src/core/network/__tests__/requests-network.test.ts +0 -16
- package/src/core/network/connection.ts +0 -5
- package/src/core/network/requests-toasting.tsx +1 -49
- package/src/core/network/resolve.ts +1 -5
- package/src/core/notebook/replica/__tests__/replica.test.ts +3 -3
- package/src/core/notebook/replica/connection.ts +1 -5
- package/src/core/runtime/__tests__/createWsUrl.test.ts +4 -16
- package/src/core/runtime/__tests__/runtime.test.ts +58 -168
- package/src/core/runtime/adapter.ts +1 -8
- package/src/core/runtime/config.ts +4 -59
- package/src/core/runtime/runtime.ts +1 -31
- package/src/core/runtime/types.ts +0 -5
- package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +5 -5
- package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +125 -29
- package/src/core/session/connection-utils.ts +2 -14
- package/src/core/session/useNotebookSession.tsx +19 -12
- package/src/mount.tsx +3 -9
- package/src/utils/errors.ts +0 -7
- package/dist/feedback-button-BJvIOw-c.js +0 -371
- package/dist/toDate-5qPtZ6HL.js +0 -94
- package/src/components/editor/alerts/connecting-alert.tsx +0 -27
- package/src/core/network/__tests__/requests-lazy.test.ts +0 -322
- package/src/core/network/requests-lazy.ts +0 -222
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import { atom,
|
|
3
|
-
import useEvent from "react-use-event-hook";
|
|
4
|
-
import { prettyError } from "@/utils/errors";
|
|
5
|
-
import { Logger } from "@/utils/Logger";
|
|
6
|
-
import { connectionAtom } from "../network/connection";
|
|
7
|
-
import { ConnectionClosedReason } from "../notebook/replica";
|
|
2
|
+
import { atom, useAtomValue } from "jotai";
|
|
8
3
|
import { store } from "../state/jotai";
|
|
9
|
-
import { isAppNotStarted } from "../session/connection-utils";
|
|
10
|
-
import { ConnectionState } from "../session/types";
|
|
11
4
|
import { RuntimeManager } from "./runtime";
|
|
12
5
|
import type { RuntimeConfig } from "./types";
|
|
13
6
|
|
|
@@ -19,33 +12,18 @@ function getBaseURI(): string {
|
|
|
19
12
|
}
|
|
20
13
|
|
|
21
14
|
export const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
|
|
22
|
-
lazy: true,
|
|
23
15
|
url: getBaseURI(),
|
|
24
16
|
};
|
|
25
17
|
|
|
26
18
|
export const runtimeConfigAtom = atom<RuntimeConfig>(DEFAULT_RUNTIME_CONFIG);
|
|
27
|
-
const runtimeManagerAtom = atom<RuntimeManager>(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
19
|
+
const runtimeManagerAtom = atom<RuntimeManager>(
|
|
20
|
+
(get) => new RuntimeManager(get(runtimeConfigAtom)),
|
|
21
|
+
);
|
|
31
22
|
|
|
32
23
|
export function useRuntimeManager(): RuntimeManager {
|
|
33
24
|
return useAtomValue(runtimeManagerAtom);
|
|
34
25
|
}
|
|
35
26
|
|
|
36
|
-
export function useConnectToRuntime(): () => Promise<void> {
|
|
37
|
-
const runtimeManager = useRuntimeManager();
|
|
38
|
-
const [connection, setConnection] = useAtom(connectionAtom);
|
|
39
|
-
return useEvent(async () => {
|
|
40
|
-
if (isAppNotStarted(connection.state)) {
|
|
41
|
-
setConnection({ state: ConnectionState.CONNECTING });
|
|
42
|
-
await runtimeManager.start();
|
|
43
|
-
} else {
|
|
44
|
-
Logger.log("Runtime already started or starting...");
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
27
|
/**
|
|
50
28
|
* Prefer to use useRuntimeManager instead of this function.
|
|
51
29
|
*/
|
|
@@ -53,39 +31,6 @@ export function getRuntimeManager(): RuntimeManager {
|
|
|
53
31
|
return store.get(runtimeManagerAtom);
|
|
54
32
|
}
|
|
55
33
|
|
|
56
|
-
/**
|
|
57
|
-
* Start a lazy runtime the first time something needs it.
|
|
58
|
-
*
|
|
59
|
-
* The connection can already read as connecting before anyone has asked
|
|
60
|
-
* for the runtime, because the notebook stream waits for the runtime's
|
|
61
|
-
* health and the editor mirrors that wait. So whether the runtime has
|
|
62
|
-
* been asked to start is the runtime's own business, and this only keeps
|
|
63
|
-
* the connection state honest around the attempt. A start that gives up
|
|
64
|
-
* closes the connection with the reason, which leaves the user a way to
|
|
65
|
-
* reconnect instead of a connection that looks like it is still coming up.
|
|
66
|
-
*/
|
|
67
|
-
export async function startRuntimeIfLazy(): Promise<void> {
|
|
68
|
-
const runtimeManager = getRuntimeManager();
|
|
69
|
-
if (runtimeManager.isServerless || !runtimeManager.isLazy) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (isAppNotStarted(store.get(connectionAtom).state)) {
|
|
73
|
-
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
await runtimeManager.start();
|
|
77
|
-
} catch (error) {
|
|
78
|
-
if (store.get(connectionAtom).state === ConnectionState.CONNECTING) {
|
|
79
|
-
store.set(connectionAtom, {
|
|
80
|
-
state: ConnectionState.CLOSED,
|
|
81
|
-
code: ConnectionClosedReason.KERNEL_STARTUP_ERROR,
|
|
82
|
-
reason: prettyError(error),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
throw error;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
34
|
export function asRemoteURL(path: string): URL {
|
|
90
35
|
if (path.startsWith("http")) {
|
|
91
36
|
return new URL(path);
|
|
@@ -14,13 +14,10 @@ const knownQueryParams = new Set(Object.values(KnownQueryParams));
|
|
|
14
14
|
|
|
15
15
|
export class RuntimeManager {
|
|
16
16
|
private initialHealthyCheck = new Deferred<void>();
|
|
17
|
-
private starting: Promise<void> | undefined;
|
|
18
17
|
private config: RuntimeConfig;
|
|
19
|
-
private lazy: boolean;
|
|
20
18
|
|
|
21
|
-
public constructor(config: RuntimeConfig
|
|
19
|
+
public constructor(config: RuntimeConfig) {
|
|
22
20
|
this.config = config;
|
|
23
|
-
this.lazy = lazy;
|
|
24
21
|
// Validate the URL on construction
|
|
25
22
|
try {
|
|
26
23
|
new URL(this.config.url);
|
|
@@ -30,14 +27,6 @@ export class RuntimeManager {
|
|
|
30
27
|
{ cause: error },
|
|
31
28
|
);
|
|
32
29
|
}
|
|
33
|
-
|
|
34
|
-
if (!this.lazy) {
|
|
35
|
-
this.init();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
public get isLazy(): boolean {
|
|
40
|
-
return this.lazy;
|
|
41
30
|
}
|
|
42
31
|
|
|
43
32
|
public get httpURL(): URL {
|
|
@@ -346,25 +335,6 @@ export class RuntimeManager {
|
|
|
346
335
|
return this.initialHealthyCheck.promise;
|
|
347
336
|
}
|
|
348
337
|
|
|
349
|
-
/**
|
|
350
|
-
* Start the runtime once and wait until it is healthy.
|
|
351
|
-
*
|
|
352
|
-
* A run, the connect button, and the notebook stream can each ask for
|
|
353
|
-
* the runtime, so the first call starts it and later calls join that
|
|
354
|
-
* attempt. An attempt that gave up is forgotten, so the next call tries
|
|
355
|
-
* again instead of failing on the old result.
|
|
356
|
-
*/
|
|
357
|
-
public start(): Promise<void> {
|
|
358
|
-
this.starting ??= this.init()
|
|
359
|
-
.then(() => this.waitForHealthy())
|
|
360
|
-
.catch((error: unknown) => {
|
|
361
|
-
this.starting = undefined;
|
|
362
|
-
this.initialHealthyCheck = new Deferred<void>();
|
|
363
|
-
throw error;
|
|
364
|
-
});
|
|
365
|
-
return this.starting;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
338
|
public headers(): KnownHeaders {
|
|
369
339
|
const headers: KnownHeaders = {
|
|
370
340
|
"Marimo-Session-Id": getSessionId(),
|
|
@@ -5,11 +5,6 @@ export interface RuntimeConfig {
|
|
|
5
5
|
* The URL of the runtime server.
|
|
6
6
|
*/
|
|
7
7
|
url: string;
|
|
8
|
-
/**
|
|
9
|
-
* If true, the runtime will not be loaded until the user interacts with the notebook.
|
|
10
|
-
* If false, the runtime will be loaded immediately.
|
|
11
|
-
*/
|
|
12
|
-
readonly lazy: boolean;
|
|
13
8
|
/**
|
|
14
9
|
* The server token for the runtime (Skew protection token)
|
|
15
10
|
*/
|
|
@@ -20,9 +20,9 @@ import { activeNotebookReplicaAtom } from "../active-notebook";
|
|
|
20
20
|
vi.mock("../backend", () => ({ createEditorBackend: vi.fn() }));
|
|
21
21
|
|
|
22
22
|
/** A host-framed native notification. */
|
|
23
|
-
const
|
|
23
|
+
const notification = {
|
|
24
24
|
event: "marimo",
|
|
25
|
-
data: { marimo: { op: "
|
|
25
|
+
data: { marimo: { op: "interrupted" } },
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
/** A transport whose connections the test drives. */
|
|
@@ -41,7 +41,7 @@ function makeTransport() {
|
|
|
41
41
|
transport,
|
|
42
42
|
opened,
|
|
43
43
|
/** Open the latest connection and deliver a host-framed notification. */
|
|
44
|
-
notify: () => opened.at(-1)?.receive(
|
|
44
|
+
notify: () => opened.at(-1)?.receive(notification),
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -153,7 +153,7 @@ describe("useNotebookReplica", () => {
|
|
|
153
153
|
act(() => fake.notify());
|
|
154
154
|
expect(options.onNotification).not.toHaveBeenCalled();
|
|
155
155
|
expect(onNotification).toHaveBeenCalledExactlyOnceWith({
|
|
156
|
-
data: { op: "
|
|
156
|
+
data: { op: "interrupted" },
|
|
157
157
|
});
|
|
158
158
|
const [{ url }] = vi.mocked(createEditorBackend).mock.calls[0];
|
|
159
159
|
expect(url()).toBe("ws://redirected/ws");
|
|
@@ -175,7 +175,7 @@ describe("useNotebookReplica", () => {
|
|
|
175
175
|
expect(store.get(result.current.connectionAtom)).toEqual({
|
|
176
176
|
state: ConnectionState.OPEN,
|
|
177
177
|
});
|
|
178
|
-
act(() => fake.opened.forEach((attempt) => attempt.receive(
|
|
178
|
+
act(() => fake.opened.forEach((attempt) => attempt.receive(notification)));
|
|
179
179
|
expect(options.onNotification).toHaveBeenCalledOnce();
|
|
180
180
|
unmount();
|
|
181
181
|
expect(active.signal.aborted).toBe(true);
|
|
@@ -55,6 +55,7 @@ import { classifyCloseEvent } from "../../transports/connection-state";
|
|
|
55
55
|
import { createEditorBackend } from "../backend";
|
|
56
56
|
import {
|
|
57
57
|
fakeBackend,
|
|
58
|
+
fixtureEvents,
|
|
58
59
|
fixtureMessages,
|
|
59
60
|
} from "../../notebook/replica/__tests__/fixtures";
|
|
60
61
|
import { useNotebookSession } from "../useNotebookSession";
|
|
@@ -114,6 +115,15 @@ function connection(fake: FakeTransport) {
|
|
|
114
115
|
/** Deliver a notification as the session routes frame it. */
|
|
115
116
|
send: (notification: unknown) =>
|
|
116
117
|
current().receive({ event: "marimo", data: { marimo: notification } }),
|
|
118
|
+
/** Deliver a snapshot of the notebook, ending in the stream's `ready`. */
|
|
119
|
+
ready: () => {
|
|
120
|
+
for (const message of fixtureMessages("notebook-events.sse")) {
|
|
121
|
+
current().receive(message);
|
|
122
|
+
if (message.event === "ready") {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
117
127
|
/** End the connection so the stream reconnects. */
|
|
118
128
|
drop: () => current().drop(),
|
|
119
129
|
/** Close the connection with a reason the editor treats as final. */
|
|
@@ -129,10 +139,7 @@ function connection(fake: FakeTransport) {
|
|
|
129
139
|
function makeRuntimeManager(
|
|
130
140
|
reconcileFromHealth = vi.fn<() => Promise<boolean>>().mockResolvedValue(true),
|
|
131
141
|
) {
|
|
132
|
-
const runtime = new RuntimeManager(
|
|
133
|
-
{ url: "http://localhost", lazy: true },
|
|
134
|
-
true,
|
|
135
|
-
);
|
|
142
|
+
const runtime = new RuntimeManager({ url: "http://localhost" });
|
|
136
143
|
vi.spyOn(runtime, "reconcileFromHealth").mockImplementation(
|
|
137
144
|
reconcileFromHealth,
|
|
138
145
|
);
|
|
@@ -350,11 +357,67 @@ describe("useNotebookSession messages", () => {
|
|
|
350
357
|
});
|
|
351
358
|
});
|
|
352
359
|
|
|
353
|
-
it
|
|
354
|
-
|
|
355
|
-
|
|
360
|
+
it("waits for the stream's snapshot before marking the session connected", async () => {
|
|
361
|
+
const store = createStore();
|
|
362
|
+
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
363
|
+
const fake = withTransport();
|
|
364
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
365
|
+
renderConnectionHook(store);
|
|
366
|
+
await started();
|
|
367
|
+
const conn = connection(fake);
|
|
368
|
+
|
|
369
|
+
await act(async () => conn.open());
|
|
370
|
+
expect(store.get(connectionAtom).state).toBe(ConnectionState.CONNECTING);
|
|
371
|
+
|
|
372
|
+
// A host that is not marimo never sends kernel-ready.
|
|
373
|
+
for (const message of fixtureMessages("notebook-events.sse")) {
|
|
374
|
+
act(() => conn.receive(message));
|
|
375
|
+
if (message.event === "ready") {
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
expect(store.get(connectionAtom)).toEqual({
|
|
379
|
+
state: ConnectionState.CONNECTING,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
expect(store.get(connectionAtom)).toEqual({ state: ConnectionState.OPEN });
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("waits for another ready snapshot after the stream resets", async () => {
|
|
386
|
+
const store = createStore();
|
|
387
|
+
const fake = withTransport();
|
|
388
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
389
|
+
renderConnectionHook(store);
|
|
390
|
+
await started();
|
|
391
|
+
const conn = connection(fake);
|
|
392
|
+
act(() => {
|
|
393
|
+
conn.open();
|
|
394
|
+
conn.ready();
|
|
395
|
+
});
|
|
396
|
+
expect(store.get(connectionAtom)).toEqual({ state: ConnectionState.OPEN });
|
|
397
|
+
|
|
398
|
+
act(() => conn.receive({ event: "reset", data: {} }));
|
|
399
|
+
expect(store.get(connectionAtom)).toEqual({
|
|
400
|
+
state: ConnectionState.CONNECTING,
|
|
401
|
+
phase: "reconnecting",
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
for (const message of fixtureMessages("notebook-events.sse")) {
|
|
405
|
+
act(() => conn.receive(message));
|
|
406
|
+
if (message.event === "ready") {
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
expect(store.get(connectionAtom)).toEqual({
|
|
410
|
+
state: ConnectionState.CONNECTING,
|
|
411
|
+
phase: "reconnecting",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
expect(store.get(connectionAtom)).toEqual({ state: ConnectionState.OPEN });
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("does not mark a static page connected", async () => {
|
|
418
|
+
window.__MARIMO_STATIC__ = { files: {} };
|
|
419
|
+
try {
|
|
356
420
|
const store = createStore();
|
|
357
|
-
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
358
421
|
const fake = withTransport();
|
|
359
422
|
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
360
423
|
renderConnectionHook(store);
|
|
@@ -362,24 +425,38 @@ it.each(["kernel-ready", "reconnected"])(
|
|
|
362
425
|
const conn = connection(fake);
|
|
363
426
|
|
|
364
427
|
await act(async () => conn.open());
|
|
365
|
-
|
|
428
|
+
act(() => conn.ready());
|
|
429
|
+
expect(store.get(connectionAtom).state).not.toBe(ConnectionState.OPEN);
|
|
430
|
+
} finally {
|
|
431
|
+
delete window.__MARIMO_STATIC__;
|
|
432
|
+
}
|
|
433
|
+
});
|
|
366
434
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
435
|
+
it("does not mark the session connected on kernel-ready alone", async () => {
|
|
436
|
+
const store = createStore();
|
|
437
|
+
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
438
|
+
const fake = withTransport();
|
|
439
|
+
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
440
|
+
renderConnectionHook(store);
|
|
441
|
+
await started();
|
|
442
|
+
const conn = connection(fake);
|
|
443
|
+
await act(async () => conn.open());
|
|
444
|
+
|
|
445
|
+
act(() =>
|
|
446
|
+
conn.send({
|
|
447
|
+
op: "kernel-ready",
|
|
448
|
+
layout: null,
|
|
449
|
+
resumed: true,
|
|
450
|
+
ui_values: {},
|
|
451
|
+
app_config: { width: "normal" },
|
|
452
|
+
kiosk: false,
|
|
453
|
+
capabilities: { terminal: false },
|
|
454
|
+
auto_instantiated: false,
|
|
455
|
+
consumer_capabilities: { edit: true, interact: true },
|
|
456
|
+
}),
|
|
457
|
+
);
|
|
458
|
+
expect(store.get(connectionAtom).state).toBe(ConnectionState.CONNECTING);
|
|
459
|
+
});
|
|
383
460
|
|
|
384
461
|
describe("connection notice", () => {
|
|
385
462
|
beforeEach(() => vi.useFakeTimers());
|
|
@@ -455,7 +532,7 @@ describe("connection notice", () => {
|
|
|
455
532
|
screen.getByRole("button", { name: "Starting kernel… Open Packages" }),
|
|
456
533
|
).toBeVisible();
|
|
457
534
|
expect(screen.queryByText(/Elapsed/)).not.toBeInTheDocument();
|
|
458
|
-
|
|
535
|
+
act(() => conn.ready());
|
|
459
536
|
expect(
|
|
460
537
|
screen.queryByRole("region", { name: "Notebook startup" }),
|
|
461
538
|
).not.toBeInTheDocument();
|
|
@@ -498,7 +575,7 @@ describe("connection notice", () => {
|
|
|
498
575
|
|
|
499
576
|
it("restores completed startup output without putting a connected kernel back into startup", async () => {
|
|
500
577
|
const { store, conn, send } = await renderNotice();
|
|
501
|
-
|
|
578
|
+
act(() => conn.ready());
|
|
502
579
|
send({
|
|
503
580
|
op: "startup-progress",
|
|
504
581
|
phase: "starting-kernel",
|
|
@@ -515,6 +592,25 @@ describe("connection notice", () => {
|
|
|
515
592
|
screen.queryByRole("region", { name: "Notebook startup" }),
|
|
516
593
|
).not.toBeInTheDocument();
|
|
517
594
|
|
|
595
|
+
const notebook = fixtureEvents("notebook-events.sse").find(
|
|
596
|
+
(event) => event.event === "notebook",
|
|
597
|
+
);
|
|
598
|
+
if (!notebook) {
|
|
599
|
+
throw new Error("Missing notebook fixture");
|
|
600
|
+
}
|
|
601
|
+
act(() =>
|
|
602
|
+
conn.receive({
|
|
603
|
+
...notebook,
|
|
604
|
+
data: {
|
|
605
|
+
notebook: { ...notebook.data.notebook, name: "Renamed notebook" },
|
|
606
|
+
},
|
|
607
|
+
}),
|
|
608
|
+
);
|
|
609
|
+
expect(store.get(startupProgressAtom)).toEqual({
|
|
610
|
+
phase: "starting-kernel",
|
|
611
|
+
logs: "Kernel startup output\n",
|
|
612
|
+
});
|
|
613
|
+
|
|
518
614
|
// A new connection starts from the next session's authoritative snapshot.
|
|
519
615
|
await act(async () => conn.drop());
|
|
520
616
|
await act(async () => vi.advanceTimersByTime(2000));
|
|
@@ -523,7 +619,7 @@ describe("connection notice", () => {
|
|
|
523
619
|
});
|
|
524
620
|
|
|
525
621
|
it("does not flash a notice when startup finishes within the delay", async () => {
|
|
526
|
-
const { send } = await renderNotice();
|
|
622
|
+
const { conn, send } = await renderNotice();
|
|
527
623
|
send({
|
|
528
624
|
op: "startup-progress",
|
|
529
625
|
phase: "preparing-environment",
|
|
@@ -541,7 +637,7 @@ describe("connection notice", () => {
|
|
|
541
637
|
expect(
|
|
542
638
|
screen.queryByRole("region", { name: "Notebook startup" }),
|
|
543
639
|
).not.toBeInTheDocument();
|
|
544
|
-
|
|
640
|
+
act(() => conn.ready());
|
|
545
641
|
act(() => vi.advanceTimersByTime(500));
|
|
546
642
|
expect(
|
|
547
643
|
screen.queryByRole("region", { name: "Notebook startup" }),
|
|
@@ -22,13 +22,6 @@ export function isAppConnected(state: ConnectionState): boolean {
|
|
|
22
22
|
return state === ConnectionState.OPEN;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
* Check if the app is in a closing state
|
|
27
|
-
*/
|
|
28
|
-
export function isAppClosing(state: ConnectionState): boolean {
|
|
29
|
-
return state === ConnectionState.CLOSING;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
/**
|
|
33
26
|
* Check if the app is in a not started state
|
|
34
27
|
*/
|
|
@@ -41,9 +34,7 @@ export function isAppNotStarted(state: ConnectionState): boolean {
|
|
|
41
34
|
*/
|
|
42
35
|
export function isAppInteractionDisabled(state: ConnectionState): boolean {
|
|
43
36
|
return (
|
|
44
|
-
state === ConnectionState.CLOSED ||
|
|
45
|
-
state === ConnectionState.CLOSING ||
|
|
46
|
-
state === ConnectionState.CONNECTING
|
|
37
|
+
state === ConnectionState.CLOSED || state === ConnectionState.CONNECTING
|
|
47
38
|
);
|
|
48
39
|
}
|
|
49
40
|
|
|
@@ -56,12 +47,9 @@ export function getConnectionTooltip(state: ConnectionState): string {
|
|
|
56
47
|
return "App disconnected";
|
|
57
48
|
case ConnectionState.CONNECTING:
|
|
58
49
|
return "Connecting to a runtime ...";
|
|
59
|
-
case ConnectionState.CLOSING:
|
|
60
|
-
return "App disconnecting...";
|
|
61
50
|
case ConnectionState.OPEN:
|
|
62
|
-
return "";
|
|
63
51
|
case ConnectionState.NOT_STARTED:
|
|
64
|
-
return "
|
|
52
|
+
return "";
|
|
65
53
|
default:
|
|
66
54
|
return "";
|
|
67
55
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { invalidatePackageData } from "@/core/packages/package-data";
|
|
4
|
-
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
|
|
4
|
+
import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
|
|
5
5
|
import { useEffect, useLayoutEffect, useMemo } from "react";
|
|
6
6
|
import { useErrorBoundary } from "react-error-boundary";
|
|
7
7
|
import { toast } from "@/components/ui/use-toast";
|
|
@@ -142,8 +142,6 @@ export function useNotebookSession(opts: {
|
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
144
|
case "kernel-ready": {
|
|
145
|
-
setKernelStartupError(null);
|
|
146
|
-
setConnection({ state: ConnectionState.OPEN });
|
|
147
145
|
setInitialRunCompleted(
|
|
148
146
|
Boolean(msg.data.resumed || msg.data.auto_instantiated),
|
|
149
147
|
);
|
|
@@ -359,11 +357,6 @@ export function useNotebookSession(opts: {
|
|
|
359
357
|
DownloadStorage.resolve(msg.data.request_id as RequestId, msg.data);
|
|
360
358
|
return;
|
|
361
359
|
|
|
362
|
-
case "reconnected":
|
|
363
|
-
setKernelStartupError(null);
|
|
364
|
-
setConnection({ state: ConnectionState.OPEN });
|
|
365
|
-
return;
|
|
366
|
-
|
|
367
360
|
case "focus-cell":
|
|
368
361
|
focusAndScrollCellOutputIntoView(msg.data.cell_id);
|
|
369
362
|
return;
|
|
@@ -454,14 +447,25 @@ export function useNotebookSession(opts: {
|
|
|
454
447
|
|
|
455
448
|
// Subscribe before passive connection setup: static/WASM transports can emit synchronously.
|
|
456
449
|
useLayoutEffect(() => {
|
|
450
|
+
const readyAtom = atom((get) => get(replica.notebookAtom) !== null);
|
|
457
451
|
const updateConnection = () => {
|
|
458
|
-
|
|
452
|
+
let status = store.get(replica.connectionAtom);
|
|
459
453
|
if (status.state === ConnectionState.NOT_STARTED) {
|
|
460
454
|
return;
|
|
461
455
|
}
|
|
462
456
|
if (status.state === ConnectionState.OPEN) {
|
|
463
457
|
updateStartupProgress(null);
|
|
464
|
-
|
|
458
|
+
// The stream's snapshot, not the socket, establishes readiness. A
|
|
459
|
+
// static page has a snapshot but no host to be connected to.
|
|
460
|
+
if (isStaticNotebook()) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (store.get(readyAtom)) {
|
|
464
|
+
setKernelStartupError(null);
|
|
465
|
+
setConnection({ state: ConnectionState.OPEN });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
status = { state: ConnectionState.CONNECTING };
|
|
465
469
|
}
|
|
466
470
|
if (status.state === ConnectionState.CONNECTING) {
|
|
467
471
|
setKernelStartupError(null);
|
|
@@ -486,9 +490,12 @@ export function useNotebookSession(opts: {
|
|
|
486
490
|
return status;
|
|
487
491
|
});
|
|
488
492
|
};
|
|
489
|
-
const
|
|
493
|
+
const unsubscribes = [
|
|
494
|
+
store.sub(replica.connectionAtom, updateConnection),
|
|
495
|
+
store.sub(readyAtom, updateConnection),
|
|
496
|
+
];
|
|
490
497
|
updateConnection();
|
|
491
|
-
return unsubscribe;
|
|
498
|
+
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
492
499
|
}, [
|
|
493
500
|
replica,
|
|
494
501
|
store,
|
package/src/mount.tsx
CHANGED
|
@@ -40,11 +40,11 @@ import {
|
|
|
40
40
|
viewStateAtom,
|
|
41
41
|
} from "./core/mode";
|
|
42
42
|
import { cleanupAuthQueryParams } from "./core/network/auth";
|
|
43
|
-
import { connectionAtom } from "./core/network/connection";
|
|
44
43
|
import { requestClientAtom } from "./core/network/requests";
|
|
45
44
|
import { resolveRequestClient } from "./core/network/resolve";
|
|
46
45
|
import {
|
|
47
46
|
DEFAULT_RUNTIME_CONFIG,
|
|
47
|
+
getRuntimeManager,
|
|
48
48
|
runtimeConfigAtom,
|
|
49
49
|
} from "./core/runtime/config";
|
|
50
50
|
import {
|
|
@@ -63,7 +63,6 @@ import { maybeRegisterVSCodeBindings } from "./core/vscode/vscode-bindings";
|
|
|
63
63
|
import type { FileStore } from "./core/wasm/store";
|
|
64
64
|
import { notebookFileStore } from "./core/wasm/store";
|
|
65
65
|
import { embeddedSnapshotAtom } from "./core/session/embedded";
|
|
66
|
-
import { ConnectionState } from "./core/session/types";
|
|
67
66
|
import {
|
|
68
67
|
handleWidgetMessage,
|
|
69
68
|
WIDGET_REGISTRY,
|
|
@@ -282,8 +281,6 @@ const mountOptionsSchema = z.object({
|
|
|
282
281
|
.array(
|
|
283
282
|
z.looseObject({
|
|
284
283
|
url: z.string(),
|
|
285
|
-
// Lazy by default, but can be overridden by the runtime config
|
|
286
|
-
lazy: z.boolean().default(true),
|
|
287
284
|
authToken: z.string().nullish(),
|
|
288
285
|
}),
|
|
289
286
|
)
|
|
@@ -359,17 +356,14 @@ function initStore(options: unknown) {
|
|
|
359
356
|
...firstRuntimeConfig,
|
|
360
357
|
serverToken: parsedOptions.data.serverToken,
|
|
361
358
|
});
|
|
362
|
-
// If the remote runtime is not lazy, start it in CONNECTING
|
|
363
|
-
if (!firstRuntimeConfig.lazy && !isStaticNotebook()) {
|
|
364
|
-
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
365
|
-
}
|
|
366
359
|
} else {
|
|
367
360
|
store.set(runtimeConfigAtom, {
|
|
368
361
|
...DEFAULT_RUNTIME_CONFIG,
|
|
369
|
-
lazy: false,
|
|
370
362
|
serverToken: parsedOptions.data.serverToken,
|
|
371
363
|
});
|
|
372
364
|
}
|
|
365
|
+
// The page's runtime is known from here on, so look for it now.
|
|
366
|
+
void getRuntimeManager().init();
|
|
373
367
|
|
|
374
368
|
// The notebook the page was served with shows until the host answers.
|
|
375
369
|
const { snapshot, cellRuntime } = readExport({
|
package/src/utils/errors.ts
CHANGED
|
@@ -94,10 +94,3 @@ export class FunctionNotFoundError extends Error {
|
|
|
94
94
|
this.name = "FunctionNotFoundError";
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
|
|
98
|
-
export class NoKernelConnectedError extends Error {
|
|
99
|
-
public constructor(message = "Not yet connected to a kernel.") {
|
|
100
|
-
super(message);
|
|
101
|
-
this.name = "NoKernelConnectedError";
|
|
102
|
-
}
|
|
103
|
-
}
|