@marimo-team/frontend 0.25.3-dev129 → 0.25.3-dev134
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-6xDEj3Hs.js → CellStatus-DC6FFQOg.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DCcLAktu.js → ConnectedDataExplorerComponent-BV1mVpaE.js} +1 -1
- package/dist/assets/{ErrorBoundary-98Xo04DS.js → ErrorBoundary-Ce-Ujlwj.js} +2 -2
- package/dist/assets/{JsonOutput-V47aiTIK.js → JsonOutput-CfEBzaqW.js} +4 -4
- package/dist/assets/{MarimoErrorOutput-DeWZNBgL.js → MarimoErrorOutput-Bh6o9zle.js} +1 -1
- package/dist/assets/{RenderHTML-Bjvcs70-.js → RenderHTML-COGZsXQV.js} +1 -1
- package/dist/assets/{RunButton-CnvvXD9d.js → RunButton-DQ51plUv.js} +1 -1
- package/dist/assets/{add-cell-with-ai-BbuqzITi.js → add-cell-with-ai-D7gtGUsg.js} +1 -1
- package/dist/assets/{add-connection-dialog-DIC7owvZ.js → add-connection-dialog-CpMCc97S.js} +1 -1
- package/dist/assets/{agent-panel-D8hO2P1x.js → agent-panel-BSFYqJ9B.js} +2 -2
- package/dist/assets/{ai-model-dropdown-DgmiG_5j.js → ai-model-dropdown-D575Vid8.js} +3 -3
- package/dist/assets/{app-config-button-UZHv1Iim.js → app-config-button-Cfcg_5Bz.js} +1 -1
- package/dist/assets/{blob-BPknzzkY.js → blob-CszMu60N.js} +1 -1
- package/dist/assets/{cell-editor-CM-K1_wq.js → cell-editor-BLpOlLnM.js} +2 -2
- package/dist/assets/cell-link-Cj0gGaMV.js +1 -0
- package/dist/assets/{cells-BypTg035.js → cells-SvbLY10y.js} +1 -1
- package/dist/assets/{chat-display-BRLNOoTF.js → chat-display-D43_g3TX.js} +1 -1
- package/dist/assets/{chat-panel-D02Rk9we.js → chat-panel-B_HAJMwx.js} +1 -1
- package/dist/assets/{chat-ui-DFN_RwxF.js → chat-ui-BEcYCphu.js} +2 -2
- package/dist/assets/{column-preview-85ORMhfB.js → column-preview-CrzaJKk8.js} +1 -1
- package/dist/assets/{command-palette-CtyjjdsA.js → command-palette-Bm9yIVcD.js} +1 -1
- package/dist/assets/{common-J265II7i.js → common-BJyUxCzv.js} +1 -1
- package/dist/assets/{components-Bd3Dpi92.js → components-D1f_Cz4i.js} +1 -1
- package/dist/assets/{datasource-Dy4rzzSP.js → datasource-C8KSVfSD.js} +1 -1
- package/dist/assets/{dependency-graph-panel-ByGFFT4E.js → dependency-graph-panel-BhmyJbUl.js} +1 -1
- package/dist/assets/{documentation-panel-LxuJCY3M.js → documentation-panel-DcVG0Q0d.js} +1 -1
- package/dist/assets/{download-BsBJWH_j.js → download-Bx52VRE4.js} +1 -1
- package/dist/assets/{edit-page--d6X-OO9.js → edit-page-DUrwOHR-.js} +6 -6
- package/dist/assets/{error-entries-0wTSSuK9.js → error-entries-11OuHzUF.js} +1 -1
- package/dist/assets/{error-panel-ZNdbE3Fo.js → error-panel-XJy5S25F.js} +1 -1
- package/dist/assets/{feedback-button-C6asPmsh.js → feedback-button-B7jh_B4w.js} +1 -1
- package/dist/assets/feedback-button-C-eKtxXE.js +1 -0
- package/dist/assets/{file-explorer-panel-0fUP_ptL.js → file-explorer-panel-CnVxbF4A.js} +1 -1
- package/dist/assets/{file-name-input-B8lzI5iS.js → file-name-input-BJuqjQqH.js} +2 -2
- package/dist/assets/floating-outline-8yyZ-MOJ.js +1 -0
- package/dist/assets/{gallery-page-ClR8U-km.js → gallery-page-DJvGHePf.js} +1 -1
- package/dist/assets/{glide-data-editor-B6QQBQwS.js → glide-data-editor-Cyc8DHZx.js} +1 -1
- package/dist/assets/{home-page-CJnqDcT9.js → home-page-tRZJmSU6.js} +1 -1
- package/dist/assets/{hooks-CCek4i9f.js → hooks-CxGwybGw.js} +1 -1
- package/dist/assets/index-CoxbqhGH.js +30 -0
- package/dist/assets/index-u08m0hQE.css +2 -0
- package/dist/assets/{layout-66Urh4Zg.js → layout-DvXJxBiL.js} +5 -5
- package/dist/assets/{logs-panel-DFhkIcge.js → logs-panel-KtsY7TrR.js} +1 -1
- package/dist/assets/{markdown-renderer-mYYQYD2m.js → markdown-renderer-DUgV7_fm.js} +1 -1
- package/dist/assets/{name-cell-input-Crcu3i2z.js → name-cell-input-C3-Cj-Ek.js} +1 -1
- package/dist/assets/{outline-panel-BFIPK0H_.js → outline-panel-BzyY4JA6.js} +1 -1
- package/dist/assets/{packages-panel-B4KtjgkF.js → packages-panel-DxosbWko.js} +1 -1
- package/dist/assets/{panel-accordion-state-D6KtwSF8.js → panel-accordion-state-BRbTib2B.js} +1 -1
- package/dist/assets/panels-Da3071Q4.js +1 -0
- package/dist/assets/{process-output-DDvMd6Jh.js → process-output-CSwx7rvo.js} +1 -1
- package/dist/assets/{readonly-code-display-VDqYn2d0.js → readonly-code-display-B9y9423Y.js} +1 -1
- package/dist/assets/{readonly-python-code-CZUKLXJ4.js → readonly-python-code-Dgw6trlk.js} +1 -1
- package/dist/assets/{reveal-component-BMmDU1uN.js → reveal-component-B9A4wkG-.js} +1 -1
- package/dist/assets/{run-page-BmOPPh2Y.js → run-page-C1MQ5b_A.js} +1 -1
- package/dist/assets/{runtime-details-state-CipqynUD.js → runtime-details-state-D_hOQL9T.js} +1 -1
- package/dist/assets/scratchpad-panel-CBCsPmMW.js +1 -0
- package/dist/assets/secrets-panel-ConQpchl.js +1 -0
- package/dist/assets/session-panel-CiHCKPlx.js +1 -0
- package/dist/assets/snippets-panel-C0VDE_4i.js +1 -0
- package/dist/assets/{state-QfGh3m2G.js → state-DrraMmqz.js} +1 -1
- package/dist/assets/{state-BLGvzMB6.js → state-DvU2AiFx.js} +22 -22
- package/dist/assets/tracing-C5d51Nmi.js +1 -0
- package/dist/assets/{tracing-panel-CsHuzRGP.js → tracing-panel-B89R8dWE.js} +2 -2
- package/dist/assets/{tree-actions-BQw_utcj.js → tree-actions-Cph8FVzh.js} +1 -1
- package/dist/assets/{useAddCell-_b8InOZC.js → useAddCell-VPkB72pn.js} +1 -1
- package/dist/assets/{useCellActionButton-BbgMzBdk.js → useCellActionButton--zI3GFnt.js} +1 -1
- package/dist/assets/{useDeleteCell-DhoFbEkU.js → useDeleteCell-CjewKeSr.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-COTULhoH.js → useDependencyPanelTab-Bt_ka-wl.js} +1 -1
- package/dist/assets/{useNotebookActions-xnA_l3nY.js → useNotebookActions-DV2S6KaG.js} +1 -1
- package/dist/assets/{useRunCells-VBCe6CLn.js → useRunCells-BupCZpzk.js} +1 -1
- package/dist/assets/{useSplitCell-AvSpVAmq.js → useSplitCell-CaLIR0TS.js} +1 -1
- package/dist/index.html +21 -21
- package/package.json +1 -1
- package/src/__tests__/module-entry.test.ts +32 -0
- package/src/components/ai/__tests__/ai-model-dropdown.test.tsx +63 -0
- package/src/components/ai/ai-model-dropdown.tsx +11 -6
- package/src/components/chat/acp/__tests__/session-tabs.test.tsx +43 -0
- package/src/components/chat/acp/blocks.tsx +1 -0
- package/src/components/chat/acp/session-tabs.tsx +8 -3
- package/src/components/data-table/TableTopBar.tsx +2 -0
- package/src/components/data-table/__tests__/data-table.test.tsx +1 -0
- package/src/components/data-table/__tests__/keyboard-selection.test.tsx +188 -0
- package/src/components/data-table/data-table.tsx +6 -0
- package/src/components/data-table/range-focus/atoms.ts +4 -6
- package/src/components/data-table/range-focus/use-cell-range-selection.ts +65 -38
- package/src/components/data-table/range-focus/use-scroll-into-view.ts +21 -2
- package/src/components/data-table/renderers.tsx +16 -7
- package/src/components/data-table/row-viewer-panel/__tests__/row-viewer.test.tsx +24 -1
- package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
- package/src/components/datasources/datasources.tsx +1 -0
- package/src/components/editor/__tests__/package-alert.test.tsx +22 -1
- package/src/components/editor/cell/collapse.tsx +5 -3
- package/src/components/editor/chrome/panels/outline/__tests__/outline-keyboard.test.tsx +84 -0
- package/src/components/editor/chrome/panels/outline/floating-outline.tsx +39 -14
- package/src/components/editor/chrome/panels/outline/useActiveOutline.tsx +3 -2
- package/src/components/editor/chrome/panels/secrets-panel.tsx +1 -0
- package/src/components/editor/chrome/panels/snippets-panel.tsx +5 -1
- package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +4 -4
- package/src/components/editor/controls/__tests__/keyboard-shortcuts.test.tsx +61 -0
- package/src/components/editor/controls/keyboard-shortcuts.tsx +14 -5
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +4 -4
- package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +47 -0
- package/src/components/editor/file-tree/file-explorer.tsx +35 -3
- package/src/components/editor/file-tree/file-name-input.tsx +1 -0
- package/src/components/editor/header/status.tsx +28 -5
- package/src/components/editor/links/__tests__/cell-link.test.tsx +146 -0
- package/src/components/editor/links/cell-link.tsx +14 -5
- package/src/components/editor/navigation/focus-utils.ts +4 -4
- package/src/components/editor/notebook-cell.tsx +11 -2
- package/src/components/editor/output/MarimoTracebackOutput.tsx +17 -15
- package/src/components/editor/output/__tests__/traceback.test.tsx +27 -1
- package/src/components/editor/package-alert.tsx +8 -3
- package/src/components/scratchpad/scratchpad.test.tsx +95 -0
- package/src/components/scratchpad/scratchpad.tsx +14 -1
- package/src/components/tracing/tracing-keyboard.test.tsx +85 -0
- package/src/components/tracing/tracing.tsx +43 -39
- package/src/components/ui/__tests__/draggable-popover.test.tsx +24 -0
- package/src/components/ui/draggable-popover.tsx +24 -2
- package/src/core/attachments/state.ts +2 -2
- package/src/core/document/__tests__/codemirror.test.ts +3 -1
- package/src/core/document/__tests__/hooks.test.tsx +7 -7
- package/src/core/document/__tests__/model.test.ts +44 -11
- package/src/core/document/__tests__/scenario-fixture.ts +2 -1
- package/src/core/document/__tests__/test-notebook.ts +19 -9
- package/src/core/document/draft.ts +1 -1
- package/src/core/document/index.ts +3 -10
- package/src/core/document/model.ts +14 -11
- package/src/core/document/state.ts +1 -1
- package/src/core/document/sync.ts +24 -18
- package/src/core/host/AGENTS.md +3 -7
- package/src/core/host/__tests__/fixtures.ts +30 -0
- package/src/core/host/__tests__/{notebook-client.test.ts → replica.test.ts} +42 -74
- package/src/core/host/__tests__/wasm.test.ts +8 -0
- package/src/core/host/backend.ts +4 -0
- package/src/core/host/index.ts +14 -0
- package/src/core/host/{notebook-client.ts → replica.ts} +75 -86
- package/src/core/host/static.ts +2 -0
- package/src/core/host/wasm.ts +10 -1
- package/src/core/wasm/bridge.ts +1 -1
- package/src/core/websocket/__tests__/backend.test.ts +111 -22
- package/src/core/websocket/__tests__/useNotebookReplica.hook.test.tsx +141 -0
- package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +4 -4
- package/src/core/websocket/backend.ts +43 -5
- package/src/core/websocket/connection-state.ts +1 -1
- package/src/core/websocket/state.ts +3 -3
- package/src/core/websocket/transports/__tests__/notebook.test.ts +11 -21
- package/src/core/websocket/types.ts +2 -2
- package/src/core/websocket/useNotebookReplica.ts +69 -0
- package/src/core/websocket/useNotebookSession.tsx +6 -6
- package/src/plugins/impl/chat/chat-ui.tsx +2 -1
- package/src/plugins/impl/data-explorer/components/column-summary.tsx +4 -2
- package/src/plugins/impl/data-frames/__tests__/sidebar.test.tsx +160 -0
- package/src/plugins/impl/data-frames/panel.tsx +43 -16
- package/dist/assets/cell-link-CAjuFcLK.js +0 -1
- package/dist/assets/feedback-button-Bt89tGG2.js +0 -1
- package/dist/assets/floating-outline-CRpmOv32.js +0 -1
- package/dist/assets/index-B5pWkt7a.js +0 -30
- package/dist/assets/index-BnhSnwvB.css +0 -2
- package/dist/assets/panels-DiwGKPzK.js +0 -1
- package/dist/assets/scratchpad-panel-Drr_kuwb.js +0 -1
- package/dist/assets/secrets-panel-D-W1PL76.js +0 -1
- package/dist/assets/session-panel-W7JOcog_.js +0 -1
- package/dist/assets/snippets-panel-cUPEdm66.js +0 -1
- package/dist/assets/tracing-BNFynaTu.js +0 -1
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +0 -228
- package/src/core/websocket/useNotebookClient.ts +0 -89
package/src/core/host/AGENTS.md
CHANGED
|
@@ -4,13 +4,9 @@ Production code may import host modules, `@marimo-team/client`,
|
|
|
4
4
|
`jotai/vanilla`, `zod`, and `@/utils/Logger`. Enforced by
|
|
5
5
|
`marimo/host-imports`. Tests are exempt.
|
|
6
6
|
|
|
7
|
+
Code outside this directory imports it as `@/core/host`. Enforced by
|
|
8
|
+
`marimo/module-entry`.
|
|
9
|
+
|
|
7
10
|
The protocol schemas and types live in `@marimo-team/client/protocol`.
|
|
8
11
|
|
|
9
12
|
Define interfaces here and inject application behavior through adapters.
|
|
10
|
-
|
|
11
|
-
`notebook-client.ts` follows the notebook with `NotebookEvents` from
|
|
12
|
-
`@marimo-team/client`, over a `NotebookTransport` the editor supplies. The
|
|
13
|
-
editor's transports for its session routes live in
|
|
14
|
-
`core/websocket/transports/notebook.ts`. Each kind of object is a `NotebookResource`
|
|
15
|
-
in `resources/` with pure handlers keyed by event name, composed with
|
|
16
|
-
`createNotebookReducer`. The client holds one state atom and exposes derived atoms.
|
|
@@ -2,7 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
import NodeFs from "node:fs";
|
|
4
4
|
import NodeUrl from "node:url";
|
|
5
|
+
import type { NotebookTransport } from "@marimo-team/client";
|
|
5
6
|
import * as Protocol from "@marimo-team/client/protocol";
|
|
7
|
+
import { vi } from "vitest";
|
|
8
|
+
import type { NotebookBackend } from "../backend";
|
|
9
|
+
|
|
10
|
+
type Commands = ReturnType<NotebookBackend["notebook"]>;
|
|
11
|
+
|
|
12
|
+
/** A backend command, with the notebook it was sent to. */
|
|
13
|
+
export type FakeEdit = (
|
|
14
|
+
notebookId: Protocol.NotebookId,
|
|
15
|
+
...args: Parameters<Commands["edit"]>
|
|
16
|
+
) => ReturnType<Commands["edit"]>;
|
|
17
|
+
|
|
18
|
+
/** A backend over `transport` whose edits go to `edit`. */
|
|
19
|
+
export function fakeBackend({
|
|
20
|
+
transport = async () => undefined,
|
|
21
|
+
edit = vi.fn<FakeEdit>(),
|
|
22
|
+
checkHealth,
|
|
23
|
+
}: {
|
|
24
|
+
transport?: NotebookTransport;
|
|
25
|
+
edit?: FakeEdit;
|
|
26
|
+
checkHealth?: () => Promise<boolean>;
|
|
27
|
+
} = {}): NotebookBackend {
|
|
28
|
+
return {
|
|
29
|
+
transport,
|
|
30
|
+
notebook: (notebookId) => ({
|
|
31
|
+
edit: (...args) => edit(notebookId, ...args),
|
|
32
|
+
}),
|
|
33
|
+
checkHealth,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
6
36
|
|
|
7
37
|
/** Read the protocol's portable examples as transport messages. */
|
|
8
38
|
export function fixtureMessages(name: string) {
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import { type NotebookTransport, StreamStopped } from "@marimo-team/client";
|
|
5
5
|
import { createStore } from "jotai";
|
|
6
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest";
|
|
7
7
|
import { Deferred } from "@/utils/Deferred";
|
|
8
8
|
import { ConnectionState } from "../../websocket/types";
|
|
9
|
-
import type { NotebookBackend } from "../backend";
|
|
10
9
|
import type { ConnectionStatus } from "../connection";
|
|
11
|
-
import {
|
|
12
|
-
import { fixtureEvents, fixtureMessages } from "./fixtures";
|
|
10
|
+
import { createNotebookReplica } from "../replica";
|
|
11
|
+
import { fakeBackend, fixtureEvents, fixtureMessages } from "./fixtures";
|
|
13
12
|
|
|
14
13
|
/** One connection the fake transport opened. */
|
|
15
14
|
interface Attempt {
|
|
@@ -23,14 +22,7 @@ interface Attempt {
|
|
|
23
22
|
stop(status?: ConnectionStatus): void;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
/** A backend edit with the notebook it was sent to. */
|
|
28
|
-
type Edit = (
|
|
29
|
-
id: string,
|
|
30
|
-
...args: Parameters<Commands["edit"]>
|
|
31
|
-
) => ReturnType<Commands["edit"]>;
|
|
32
|
-
|
|
33
|
-
function setup(edit = vi.fn<Edit>()) {
|
|
25
|
+
function setup() {
|
|
34
26
|
const attempts: Attempt[] = [];
|
|
35
27
|
const transport: NotebookTransport = ({ cursor, signal }, handlers) =>
|
|
36
28
|
new Promise((resolve, reject) => {
|
|
@@ -48,19 +40,15 @@ function setup(edit = vi.fn<Edit>()) {
|
|
|
48
40
|
const onNotification = vi.fn();
|
|
49
41
|
const checkHealth = vi.fn(async () => true);
|
|
50
42
|
const store = createStore();
|
|
51
|
-
const backend
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const client = controller.client;
|
|
57
|
-
const connect = (signal: AbortSignal) =>
|
|
58
|
-
controller.connect({
|
|
59
|
-
backend,
|
|
60
|
-
signal,
|
|
61
|
-
checkHealth,
|
|
43
|
+
const backend = fakeBackend({ transport, checkHealth });
|
|
44
|
+
const client = createNotebookReplica(store);
|
|
45
|
+
/** Follow the backend until `signal` aborts. */
|
|
46
|
+
const connect = (signal: AbortSignal) => {
|
|
47
|
+
const disconnect = client.connect(backend, {
|
|
62
48
|
onNotification: (notification) => onNotification({ data: notification }),
|
|
63
49
|
});
|
|
50
|
+
signal.addEventListener("abort", disconnect, { once: true });
|
|
51
|
+
};
|
|
64
52
|
let next = 0;
|
|
65
53
|
/** Wait for the next connection, and open it. */
|
|
66
54
|
const opened = async () => {
|
|
@@ -71,6 +59,7 @@ function setup(edit = vi.fn<Edit>()) {
|
|
|
71
59
|
};
|
|
72
60
|
return {
|
|
73
61
|
attempts,
|
|
62
|
+
backend,
|
|
74
63
|
client,
|
|
75
64
|
connect,
|
|
76
65
|
onNotification,
|
|
@@ -109,61 +98,40 @@ const notebook = {
|
|
|
109
98
|
updated_at: null,
|
|
110
99
|
};
|
|
111
100
|
|
|
112
|
-
describe("
|
|
113
|
-
it("
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
status: 409,
|
|
117
|
-
problem: { type: "conflict", title: "conflict", status: 409 },
|
|
118
|
-
});
|
|
119
|
-
const { connect, opened, client } = setup(edit);
|
|
120
|
-
connect(lifetime().signal);
|
|
101
|
+
describe("NotebookReplica lifetime", () => {
|
|
102
|
+
it("ends the previous connection when it connects again", async () => {
|
|
103
|
+
const { attempts, backend, client, opened, store } = setup();
|
|
104
|
+
const first = client.connect(backend, { onNotification: vi.fn() });
|
|
121
105
|
const { receive } = await opened();
|
|
122
106
|
fixtureMessages("notebook-events.sse").forEach(receive);
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
receive({
|
|
133
|
-
event: "notebook",
|
|
134
|
-
data: { notebook: { ...notebook, id: "n2" } },
|
|
107
|
+
const second = client.connect(backend, { onNotification: vi.fn() });
|
|
108
|
+
onTestFinished(second);
|
|
109
|
+
expect(attempts[0].signal.aborted).toBe(true);
|
|
110
|
+
expect(store.get(client.documentAtom)).toBeNull();
|
|
111
|
+
await opened();
|
|
112
|
+
first();
|
|
113
|
+
expect(attempts[1].signal.aborted).toBe(false);
|
|
114
|
+
expect(store.get(client.connectionAtom)).toEqual({
|
|
115
|
+
state: ConnectionState.OPEN,
|
|
135
116
|
});
|
|
136
|
-
await expect(client.edit("n1", [], options)).rejects.toThrow(
|
|
137
|
-
"notebook changed",
|
|
138
|
-
);
|
|
139
|
-
expect(edit).toHaveBeenCalledTimes(2);
|
|
140
117
|
});
|
|
141
118
|
|
|
142
|
-
it("
|
|
143
|
-
const { client } = setup();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
const reason = new Error("Session ended");
|
|
160
|
-
controller.abort(reason);
|
|
161
|
-
await expect(
|
|
162
|
-
client.edit("n1", [], {
|
|
163
|
-
idempotencyKey: "edit-1",
|
|
164
|
-
signal: controller.signal,
|
|
165
|
-
}),
|
|
166
|
-
).rejects.toBe(reason);
|
|
119
|
+
it("exposes the backend it follows until that connection ends", async () => {
|
|
120
|
+
const { backend, client, opened, store } = setup();
|
|
121
|
+
expect(store.get(client.backendAtom)).toBeNull();
|
|
122
|
+
const disconnect = client.connect(backend, { onNotification: vi.fn() });
|
|
123
|
+
expect(store.get(client.backendAtom)).toBe(backend);
|
|
124
|
+
const { receive, drop } = await opened();
|
|
125
|
+
fixtureMessages("notebook-events.sse").forEach(receive);
|
|
126
|
+
receive({ event: "reset", data: {} });
|
|
127
|
+
drop();
|
|
128
|
+
expect(store.get(client.backendAtom)).toBe(backend);
|
|
129
|
+
|
|
130
|
+
const other = fakeBackend();
|
|
131
|
+
onTestFinished(client.connect(other, { onNotification: vi.fn() }));
|
|
132
|
+
expect(store.get(client.backendAtom)).toBe(other);
|
|
133
|
+
disconnect();
|
|
134
|
+
expect(store.get(client.backendAtom)).toBe(other);
|
|
167
135
|
});
|
|
168
136
|
|
|
169
137
|
it("exposes identity and capabilities only after the notebook is ready", async () => {
|
|
@@ -450,7 +418,7 @@ describe("NotebookClient lifetime", () => {
|
|
|
450
418
|
});
|
|
451
419
|
});
|
|
452
420
|
|
|
453
|
-
describe("
|
|
421
|
+
describe("NotebookReplica attachments", () => {
|
|
454
422
|
const [browser, agent] = fixtureEvents("notebook-events.sse").flatMap(
|
|
455
423
|
(event) => (event.event === "attachment" ? [event.data.attachment] : []),
|
|
456
424
|
);
|
|
@@ -99,6 +99,14 @@ describe("WASM notebook backend", () => {
|
|
|
99
99
|
).rejects.toBeInstanceOf(StreamStopped);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
it("refuses commands for any notebook but the worker's", () => {
|
|
103
|
+
const { bridge } = worker();
|
|
104
|
+
expect(() => createWasmNotebookBackend(bridge).notebook("n2")).toThrow(
|
|
105
|
+
"serves notebook n1, not n2",
|
|
106
|
+
);
|
|
107
|
+
expect(bridge.call).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
102
110
|
it("sends edits as calls with the retry key and validates replies", async () => {
|
|
103
111
|
const { bridge, reply } = worker();
|
|
104
112
|
const backend = createWasmNotebookBackend(bridge);
|
package/src/core/host/backend.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
/** A notebook's events and commands, from a host or a stand-in for one. */
|
|
4
|
+
|
|
3
5
|
import type { NotebookClient, NotebookTransport } from "@marimo-team/client";
|
|
4
6
|
import type { NotebookId } from "@marimo-team/client/protocol";
|
|
5
7
|
|
|
@@ -12,4 +14,6 @@ export interface NotebookBackend {
|
|
|
12
14
|
* Method presence is not permission. The notebook advertises its operations.
|
|
13
15
|
*/
|
|
14
16
|
notebook(id: NotebookId): Pick<NotebookClient, "edit">;
|
|
17
|
+
/** Whether the host is reachable before an explicit reconnect. Absent means always. */
|
|
18
|
+
checkHealth?(): Promise<boolean>;
|
|
15
19
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/** Notebook backends and the replica that follows them. */
|
|
4
|
+
|
|
5
|
+
export type { NotebookBackend } from "./backend";
|
|
6
|
+
export {
|
|
7
|
+
type CloseDecision,
|
|
8
|
+
ConnectionClosedReason,
|
|
9
|
+
ConnectionState,
|
|
10
|
+
type ConnectionStatus,
|
|
11
|
+
} from "./connection";
|
|
12
|
+
export { createNotebookReplica, type NotebookReplica } from "./replica";
|
|
13
|
+
export type { Document } from "./resources/document";
|
|
14
|
+
export { createWasmNotebookBackend } from "./wasm";
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
/** What a backend's stream has confirmed about a notebook, kept in atoms. */
|
|
4
|
+
|
|
3
5
|
import { Logger } from "@/utils/Logger";
|
|
4
6
|
import {
|
|
5
7
|
createNotebookReducer,
|
|
6
8
|
type NotebookEvent,
|
|
7
9
|
NotebookEvents,
|
|
8
|
-
type Result,
|
|
9
10
|
StreamStopped,
|
|
10
11
|
} from "@marimo-team/client";
|
|
11
|
-
import type {
|
|
12
|
-
Attachment,
|
|
13
|
-
Change,
|
|
14
|
-
EditResponseOutput,
|
|
15
|
-
NotebookId,
|
|
16
|
-
NotebookOutput,
|
|
17
|
-
} from "@marimo-team/client/protocol";
|
|
12
|
+
import type { Attachment, NotebookOutput } from "@marimo-team/client/protocol";
|
|
18
13
|
import { type Atom, atom, type createStore } from "jotai/vanilla";
|
|
19
14
|
import { z } from "zod";
|
|
20
15
|
import type { NotebookBackend } from "./backend";
|
|
@@ -34,9 +29,9 @@ type EditorEvent = NotebookEvent<(typeof TYPES)[number]>;
|
|
|
34
29
|
/** A native marimo notification, passed through without interpretation. */
|
|
35
30
|
const MarimoMessage = z.object({ marimo: z.unknown() });
|
|
36
31
|
|
|
37
|
-
/** A
|
|
38
|
-
export interface
|
|
39
|
-
/**
|
|
32
|
+
/** A notebook as its backend's stream reports it, kept current while connected. */
|
|
33
|
+
export interface NotebookReplica {
|
|
34
|
+
/** Connection status, independent of application session readiness. */
|
|
40
35
|
readonly connectionAtom: Atom<ConnectionStatus>;
|
|
41
36
|
/** Identity and capabilities, available while the stream is connected and ready. */
|
|
42
37
|
readonly notebookAtom: Atom<NotebookOutput | null>;
|
|
@@ -48,30 +43,32 @@ export interface NotebookClient {
|
|
|
48
43
|
* retains the last document. Partial updates are not exposed.
|
|
49
44
|
*/
|
|
50
45
|
readonly documentAtom: Atom<Document | null>;
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The backend this replica follows, or null while not connected.
|
|
48
|
+
*
|
|
49
|
+
* Its commands reach the host that confirmed `documentAtom`. A temporary
|
|
50
|
+
* disconnection keeps it. Connecting again or disconnecting replaces it.
|
|
51
|
+
*/
|
|
52
|
+
readonly backendAtom: Atom<NotebookBackend | null>;
|
|
57
53
|
/** Live attachments, empty until the stream is ready or while disconnected. */
|
|
58
54
|
readonly attachmentsAtom: Atom<readonly Attachment[]>;
|
|
59
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Follow a backend, replacing any current connection.
|
|
57
|
+
*
|
|
58
|
+
* The returned function ends this connection and clears the replica.
|
|
59
|
+
* It does nothing once another connection has replaced this one.
|
|
60
|
+
*/
|
|
61
|
+
connect(
|
|
62
|
+
backend: NotebookBackend,
|
|
63
|
+
options: {
|
|
64
|
+
/** Handle a native marimo notification. Its payload has not been validated. */
|
|
65
|
+
onNotification: (notification: unknown) => void;
|
|
66
|
+
},
|
|
67
|
+
): () => void;
|
|
68
|
+
/** Restore the current connection after its stream closed, if the host is healthy. */
|
|
60
69
|
reconnect(): Promise<void>;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
/** The backend and application callbacks needed to follow a notebook. */
|
|
64
|
-
export interface NotebookConnection {
|
|
65
|
-
/** Follows the notebook's stream and sends its commands. */
|
|
66
|
-
backend: NotebookBackend;
|
|
67
|
-
/** Abort to close the stream and clear the client's state. */
|
|
68
|
-
signal: AbortSignal;
|
|
69
|
-
/** Check whether the host is reachable before an explicit reconnect. */
|
|
70
|
-
checkHealth: () => Promise<boolean>;
|
|
71
|
-
/** Handle a native marimo notification. Its payload has not been validated. */
|
|
72
|
-
onNotification: (notification: unknown) => void;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
72
|
const disconnected = {
|
|
76
73
|
state: ConnectionState.CLOSED,
|
|
77
74
|
code: ConnectionClosedReason.KERNEL_DISCONNECTED,
|
|
@@ -103,8 +100,10 @@ function statusAfter(error: unknown): ConnectionStatus {
|
|
|
103
100
|
return disconnected;
|
|
104
101
|
}
|
|
105
102
|
|
|
106
|
-
/** Create a
|
|
107
|
-
export function
|
|
103
|
+
/** Create a replica without connecting it. */
|
|
104
|
+
export function createNotebookReplica(
|
|
105
|
+
store: ReturnType<typeof createStore>,
|
|
106
|
+
): NotebookReplica {
|
|
108
107
|
// `useNotebookSession` copies this into the editor's shared `connectionAtom`,
|
|
109
108
|
// which gates features until MO-8051 derives availability separately.
|
|
110
109
|
const connectionAtom = atom<ConnectionStatus>({
|
|
@@ -130,50 +129,40 @@ export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
|
130
129
|
? get(streamAtom).resources.attachments
|
|
131
130
|
: attachments.initial,
|
|
132
131
|
);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
connectionAtom,
|
|
137
|
-
notebookAtom,
|
|
138
|
-
documentAtom,
|
|
139
|
-
edit: async (notebookId, changes, options) => {
|
|
140
|
-
options.signal.throwIfAborted();
|
|
141
|
-
const currentId = store.get(streamAtom).notebookId;
|
|
142
|
-
if (currentId !== null && currentId !== notebookId) {
|
|
143
|
-
throw new Error("The notebook changed before the edit was sent");
|
|
144
|
-
}
|
|
145
|
-
if (backend === undefined) {
|
|
146
|
-
return {
|
|
147
|
-
ok: false,
|
|
148
|
-
status: 501,
|
|
149
|
-
problem: {
|
|
150
|
-
type: "tag:marimo.io,2026:host/unsupported-operation",
|
|
151
|
-
title: "Document editing is unavailable on this connection",
|
|
152
|
-
status: 501,
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
return backend.notebook(notebookId).edit(changes, options);
|
|
157
|
-
},
|
|
158
|
-
attachmentsAtom,
|
|
159
|
-
reconnect: async () => reconnect?.(),
|
|
160
|
-
};
|
|
132
|
+
const backendAtom = atom<NotebookBackend | null>(null);
|
|
133
|
+
/** The connection this replica follows. Connecting again ends it. */
|
|
134
|
+
let current: { end(): void; reconnect(): Promise<void> } | undefined;
|
|
161
135
|
|
|
162
136
|
function setStatus(status: ConnectionStatus) {
|
|
163
137
|
store.set(readyAtom, false);
|
|
164
138
|
store.set(connectionAtom, status);
|
|
165
139
|
}
|
|
166
140
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
141
|
+
/** Drop what the stream reported and show `status`. */
|
|
142
|
+
function clear(status: ConnectionStatus) {
|
|
143
|
+
store.set(streamAtom, reducer.initial);
|
|
144
|
+
setStatus(status);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
connectionAtom,
|
|
149
|
+
notebookAtom,
|
|
150
|
+
documentAtom,
|
|
151
|
+
backendAtom,
|
|
152
|
+
attachmentsAtom,
|
|
153
|
+
connect,
|
|
154
|
+
reconnect: async () => current?.reconnect(),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function connect(
|
|
158
|
+
connected: NotebookBackend,
|
|
159
|
+
{ onNotification }: { onNotification: (notification: unknown) => void },
|
|
160
|
+
) {
|
|
161
|
+
current?.end();
|
|
162
|
+
const lifetime = new AbortController();
|
|
163
|
+
const { signal } = lifetime;
|
|
164
|
+
const checkHealth = connected.checkHealth ?? (async () => true);
|
|
165
|
+
store.set(backendAtom, connected);
|
|
177
166
|
const { transport } = connected;
|
|
178
167
|
let events: NotebookEvents<(typeof TYPES)[number]> | undefined;
|
|
179
168
|
let checkingHealth = false;
|
|
@@ -222,8 +211,7 @@ export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
|
222
211
|
source.close();
|
|
223
212
|
// The cursor can already include an event the replica rejected.
|
|
224
213
|
resumable = false;
|
|
225
|
-
|
|
226
|
-
setStatus(malformed);
|
|
214
|
+
clear(malformed);
|
|
227
215
|
}
|
|
228
216
|
|
|
229
217
|
function apply(source: NotebookEvents<string>, event: EditorEvent) {
|
|
@@ -250,7 +238,7 @@ export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
|
250
238
|
}
|
|
251
239
|
}
|
|
252
240
|
|
|
253
|
-
reconnect = async () => {
|
|
241
|
+
const reconnect = async () => {
|
|
254
242
|
if (signal.aborted || checkingHealth || events?.status !== "closed") {
|
|
255
243
|
return;
|
|
256
244
|
}
|
|
@@ -276,22 +264,23 @@ export function createNotebookClient(store: ReturnType<typeof createStore>) {
|
|
|
276
264
|
}
|
|
277
265
|
};
|
|
278
266
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (backend === connected) {
|
|
284
|
-
backend = undefined;
|
|
267
|
+
const connection = {
|
|
268
|
+
end: () => {
|
|
269
|
+
if (signal.aborted) {
|
|
270
|
+
return;
|
|
285
271
|
}
|
|
286
|
-
|
|
287
|
-
|
|
272
|
+
lifetime.abort();
|
|
273
|
+
if (current === connection) {
|
|
274
|
+
current = undefined;
|
|
275
|
+
store.set(backendAtom, null);
|
|
276
|
+
}
|
|
277
|
+
clear({ state: ConnectionState.NOT_STARTED });
|
|
288
278
|
},
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
279
|
+
reconnect,
|
|
280
|
+
};
|
|
281
|
+
current = connection;
|
|
292
282
|
setStatus({ state: ConnectionState.CONNECTING });
|
|
293
283
|
follow();
|
|
284
|
+
return connection.end;
|
|
294
285
|
}
|
|
295
|
-
|
|
296
|
-
return { client, connect };
|
|
297
286
|
}
|
package/src/core/host/static.ts
CHANGED
package/src/core/host/wasm.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
/** A backend for the notebook running in the WASM worker. */
|
|
4
|
+
|
|
3
5
|
import {
|
|
4
6
|
NotebookClient,
|
|
5
7
|
StreamStopped,
|
|
@@ -88,6 +90,13 @@ export function createWasmNotebookBackend(
|
|
|
88
90
|
signal.removeEventListener("abort", abort);
|
|
89
91
|
}
|
|
90
92
|
},
|
|
91
|
-
notebook: () =>
|
|
93
|
+
notebook: (id) => {
|
|
94
|
+
if (id !== bridge.notebookId) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`The worker serves notebook ${bridge.notebookId}, not ${id}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return client;
|
|
100
|
+
},
|
|
92
101
|
};
|
|
93
102
|
}
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { reloadSafe } from "@/utils/reload-safe";
|
|
|
11
11
|
import { generateUUID } from "@/utils/uuid";
|
|
12
12
|
import { createModuleWorker } from "@/utils/worker";
|
|
13
13
|
import { notebookIsRunningAtom } from "../cells/cells";
|
|
14
|
-
import { createWasmNotebookBackend } from "../host
|
|
14
|
+
import { createWasmNotebookBackend } from "../host";
|
|
15
15
|
import type { ClientCommand } from "../kernel/messages";
|
|
16
16
|
import { getInitialAppMode } from "../mode";
|
|
17
17
|
import { API } from "../network/api";
|