@marimo-team/islands 0.25.3-dev192 → 0.25.3-dev196
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/{ConnectedDataExplorerComponent-BBbOElk3.js → ConnectedDataExplorerComponent-BRuwLOIx.js} +9 -9
- package/dist/{ErrorBoundary-Dw6uRKVk.js → ErrorBoundary-C3Xv3yzV.js} +2 -2
- package/dist/{abnfDiagram-VCTEODGH-DhMT6Uhd.js → abnfDiagram-VCTEODGH-LlJTOOU_.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-BVbEiJLq.js → architectureDiagram-5GKGNRK7-9PQtGLLZ.js} +1 -1
- package/dist/{arrays-R6JsImiT.js → arrays-DA6KkLfq.js} +6 -12
- package/dist/assets/__vite-browser-external-BCJsx8QS.js +1 -0
- package/dist/assets/{worker-C6e9ERyd.js → worker-DodjWT_Z.js} +2 -2
- package/dist/{capabilities-CI4xUPsp.js → capabilities-CvWU-ey_.js} +1 -1
- package/dist/{chat-ui-BguxTQEL.js → chat-ui-Bl2euMBD.js} +93 -93
- package/dist/{chunk-IMKFNOWR-By5hSbJJ.js → chunk-IMKFNOWR-DcXv-BD5.js} +1 -1
- package/dist/{chunk-SHT3W25Y-DFrEYG54.js → chunk-SHT3W25Y-VwnELN23.js} +1 -1
- package/dist/{chunk-SVP7TREG-DFXGc9ew.js → chunk-SVP7TREG-DgQpI5Zi.js} +1 -1
- package/dist/{chunk-TICWLB2K-DZdNIpoZ.js → chunk-TICWLB2K-CTg6VTJT.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-Cgj_eEQA.js → classDiagram-ZZMXUADV-UzmcFrnW.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-DLAUrSqj.js → classDiagram-v2-VYDZK3BY-DhjiI6IQ.js} +2 -2
- package/dist/{code-visibility-v_7ijLtx.js → code-visibility-BCcqmP58.js} +38 -42
- package/dist/{common-BUwWHpnK.js → common-DrNB2bY2.js} +849 -849
- package/dist/{cynefinDiagram-5FMLGOSQ-Cikf9AKa.js → cynefinDiagram-5FMLGOSQ-O4ShIbJX.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-C5KEi5_l.js → dagre-GXQ25YYZ-dtHhhcPu.js} +1 -1
- package/dist/{data-grid-overlay-editor-4IZymnXZ.js → data-grid-overlay-editor-TGXV_Hf4.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BaoBTQjR.js → diagram-S7CK7UJ4-ppPYVZ6W.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DDufBsUA.js → diagram-UQ7AKVKN-CMZDf--k.js} +1 -1
- package/dist/{diagram-VX7I27RA-CY6RALJ8.js → diagram-VX7I27RA-CFvJdx-C.js} +1 -1
- package/dist/{diagram-Z3DM3KII-Bc-LHsD6.js → diagram-Z3DM3KII-Dt6_uFbv.js} +1 -1
- package/dist/{dropdown-menu-drDjxJyV.js → dropdown-menu-CeaOsjWZ.js} +2 -2
- package/dist/{ebnfDiagram-PWID7BFC-Cz0J-tcC.js → ebnfDiagram-PWID7BFC-DpRFrKK-.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-DTXTDgFu.js → erDiagram-RLTQ6QDP-CqAU4b0u.js} +1 -1
- package/dist/{error-entries-CZ1NQ6Kv.js → error-entries-CYGwr5-O.js} +5596 -6012
- package/dist/feedback-button-PTiPn2GP.js +374 -0
- package/dist/{flowDiagram-HODETNUW-RbwWf2zc.js → flowDiagram-HODETNUW-i8-Gw8HO.js} +2 -2
- package/dist/{formats-DbXnijtH.js → formats-BtoHHy42.js} +1 -1
- package/dist/{glide-data-editor-DU8_EDG5.js → glide-data-editor-BSyexjkB.js} +5 -5
- package/dist/{globals-BeBDOk22.js → globals-B7gkJAxm.js} +2 -2
- package/dist/{html-to-image-D_4_hQ8U.js → html-to-image-Cy5UDpQJ.js} +56 -56
- package/dist/{infoDiagram-27XIBGKW-DlmzZjmI.js → infoDiagram-27XIBGKW-DpfOKdLL.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-EN3TYP_q.js → ishikawaDiagram-5VMMS53U-Dg7mrMG7.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-CK_-Ru7Q.js → kanban-definition-UXKFOSKX-QB6e3Jrj.js} +1 -1
- package/dist/main.js +5875 -5876
- package/dist/{mermaid-DU9ZRmkG.js → mermaid-CeWG4Egr.js} +48 -48
- package/dist/{mindmap-definition-YA3MSWOX-C-hqgUvF.js → mindmap-definition-YA3MSWOX-BTz3uTGe.js} +1 -1
- package/dist/{number-overlay-editor-BoIkyh6H.js → number-overlay-editor-Bb91yQ46.js} +1 -1
- package/dist/{once-eG8Vmvjd.js → once-DWGO2mQ2.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-BiMejYPB.js → pegDiagram-XKGWAZYB-3ZIEa3mB.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-B1zCZ1eq.js → pieDiagram-E7YTZNPT-BJAlk046.js} +1 -1
- package/dist/{process-output-DxSCHBWp.js → process-output-q9MTAIMX.js} +2 -2
- package/dist/{railroadDiagram-O6MQD6OU-BLcfaASl.js → railroadDiagram-O6MQD6OU-Dn3bsSRa.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-DtBOf52-.js → requirementDiagram-BXWQKSXE-wcOmQQCL.js} +1 -1
- package/dist/{reveal-component-5M3a2rpj.js → reveal-component-vTdjiyDv.js} +17 -17
- package/dist/{sequenceDiagram-WJ2MYXX4-DIwF19_n.js → sequenceDiagram-WJ2MYXX4-DSh_qxoF.js} +1 -1
- package/dist/{spec-DZDnnlgT.js → spec-C7Fos-If.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-sWk1AdYL.js → stateDiagram-D77RDMKH-DH_H8NMH.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-CiNo-OI7.js → stateDiagram-v2-MP3YSRHH-D_JM7MEK.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-Bj3hiOY5.js → swimlanes-42K2YHIH-CfoTJO-9.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-BzEo2r_N.js → swimlanesDiagram-VR7AAH4N-Z_QhvK0G.js} +2 -2
- package/dist/{swiper-component-BkCm-kLg.js → swiper-component-D2geL1Ku.js} +1 -1
- package/dist/{timeline-definition-24CTP7MA-CrPQk42H.js → timeline-definition-24CTP7MA-BFtdScaL.js} +1 -1
- package/dist/{toDate-Cu2pIZcC.js → toDate-CQAFSwos.js} +24 -24
- package/dist/{useAsyncData-DEhLdRIu.js → useAsyncData-BeyZyjso.js} +1 -1
- package/dist/{useDeepCompareMemoize-RjZGVhBV.js → useDeepCompareMemoize-DOQgae0W.js} +1 -1
- package/dist/{useIframeCapabilities-CSdWn6NP.js → useIframeCapabilities-DNvaj9wM.js} +1 -1
- package/dist/{useLifecycle-DtcB2s1z.js → useLifecycle-n2lnCFS8.js} +1 -1
- package/dist/{useTheme-CoEtr4mK.js → useTheme-CY777LXN.js} +303 -99
- package/dist/{vega-component-BhjTrP9-.js → vega-component-0nQzA-I8.js} +6 -6
- package/dist/{vennDiagram-4TSXK5OY-BaYMoIpj.js → vennDiagram-4TSXK5OY-BXY8IGCA.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-DjHeuj8p.js → wardleyDiagram-VM6X3IG4-D0tFe6eC.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-Bc_hcuXS.js → xychartDiagram-S5SC5T6Z-Dx8wFM-y.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/notebook.ts +0 -1
- package/src/__mocks__/partial.ts +26 -0
- package/src/__tests__/main.test.tsx +24 -1
- package/src/components/app-config/app-config-form.tsx +5 -9
- package/src/components/editor/__tests__/data-attributes.test.tsx +0 -1
- package/src/components/editor/__tests__/output-persistence.test.tsx +0 -1
- package/src/components/editor/actions/useCellActionButton.tsx +6 -5
- package/src/components/editor/actions/useConfigActions.tsx +3 -4
- package/src/components/editor/actions/useNotebookActions.tsx +5 -4
- package/src/components/editor/cell/code/cell-editor.tsx +13 -6
- package/src/components/editor/cell/collapse.tsx +6 -1
- package/src/components/editor/chrome/components/feedback-button.tsx +1 -7
- package/src/components/editor/controls/Controls.tsx +1 -1
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +8 -6
- package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -15
- package/src/components/editor/navigation/__tests__/selection.test.ts +44 -21
- package/src/components/editor/navigation/focus-utils.ts +19 -0
- package/src/components/editor/navigation/multi-cell-action-toolbar.tsx +6 -13
- package/src/components/editor/navigation/navigation.ts +52 -43
- package/src/components/editor/navigation/selection.ts +30 -29
- package/src/components/editor/notebook-cell.tsx +10 -18
- package/src/components/editor/renderers/cell-array.tsx +6 -5
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/components/pages/edit-page.tsx +0 -13
- package/src/components/scratchpad/scratchpad.tsx +0 -1
- package/src/components/slides/reveal-component.tsx +1 -1
- package/src/components/slides/slide-cell-view.tsx +0 -1
- package/src/components/sort/SortableCellsProvider.tsx +6 -5
- package/src/core/MarimoApp.tsx +1 -1
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -37
- package/src/core/cells/__tests__/actions.test.ts +190 -80
- package/src/core/cells/__tests__/arrange.test.ts +263 -0
- package/src/core/cells/__tests__/cells.test.ts +11 -1101
- package/src/core/cells/__tests__/document.test.ts +3 -32
- package/src/core/cells/__tests__/editor.test.ts +57 -0
- package/src/core/cells/__tests__/harness.ts +82 -0
- package/src/core/cells/__tests__/layout.test.ts +189 -0
- package/src/core/cells/__tests__/notebook-outline.test.ts +7 -1
- package/src/core/cells/__tests__/session.test.ts +329 -174
- package/src/core/cells/__tests__/utils.test.ts +0 -1
- package/src/core/cells/actions.ts +247 -82
- package/src/core/cells/arrange.ts +341 -0
- package/src/core/cells/cells.ts +27 -516
- package/src/core/cells/document.ts +6 -5
- package/src/core/cells/edit.ts +30 -0
- package/src/core/cells/editor.ts +82 -0
- package/src/core/cells/layout.ts +152 -0
- package/src/core/cells/session.ts +75 -52
- package/src/core/cells/types.ts +0 -5
- package/src/core/cells/utils.ts +6 -4
- package/src/core/config/__tests__/app-config.test.ts +64 -0
- package/src/core/config/__tests__/config-schema.test.ts +1 -1
- package/src/core/config/config-schema.ts +6 -2
- package/src/core/config/config.ts +26 -12
- package/src/core/islands/bootstrap.ts +0 -1
- package/src/core/kernel/handlers.ts +1 -13
- package/src/core/meta/__tests__/code-visibility.test.tsx +78 -70
- package/src/core/meta/code-visibility.ts +18 -9
- package/src/core/notebook/__tests__/operations.test.ts +51 -0
- package/src/core/notebook/document/__tests__/codemirror.test.ts +33 -7
- package/src/core/notebook/index.ts +2 -1
- package/src/core/notebook/operations.ts +30 -0
- package/src/core/saving/__tests__/state.test.ts +111 -0
- package/src/core/saving/save-component.tsx +19 -33
- package/src/core/saving/state.ts +38 -39
- package/src/core/saving/useAutoSave.ts +8 -35
- package/src/core/session/__tests__/backend.test.ts +1 -0
- package/src/core/session/__tests__/useNotebookReplica.hook.test.tsx +1 -0
- package/src/core/session/active-notebook.ts +11 -0
- package/src/core/session/backend.ts +17 -19
- package/src/core/session/embedded.ts +7 -0
- package/src/core/session/useNotebookReplica.ts +3 -1
- package/src/core/session/useNotebookSession.tsx +7 -8
- package/src/core/transports/__tests__/static.test.ts +139 -8
- package/src/core/transports/static.ts +98 -16
- package/src/core/wasm/__tests__/bridge.test.ts +1 -0
- package/src/mount.tsx +19 -10
- package/src/stories/cell.stories.tsx +0 -2
- package/src/stories/layout/vertical/one-column.stories.tsx +0 -2
- package/src/utils/__tests__/id-tree.test.ts +20 -1548
- package/src/utils/id-tree.tsx +12 -537
- package/dist/assets/__vite-browser-external-C8gQho07.js +0 -1
- package/dist/feedback-button-MpeoIn1D.js +0 -379
- package/src/components/editor/navigation/__tests__/state.test.ts +0 -81
- package/src/components/editor/navigation/state.ts +0 -65
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { useAtom, useAtomValue,
|
|
3
|
+
import { useAtom, useAtomValue, useStore } from "jotai";
|
|
4
4
|
import { SaveIcon } from "lucide-react";
|
|
5
5
|
import { useState } from "react";
|
|
6
6
|
import { FilenameInput } from "@/components/editor/header/filename-input";
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { Label } from "../../components/ui/label";
|
|
21
21
|
import { useEvent } from "../../hooks/useEvent";
|
|
22
22
|
import { Logger } from "../../utils/Logger";
|
|
23
|
-
import { getCellConfigs, getNotebook
|
|
23
|
+
import { getCellConfigs, getNotebook } from "../cells/cells";
|
|
24
24
|
import { flushDocument, withDocumentSave } from "../cells/document";
|
|
25
25
|
import { notebookCells } from "../cells/utils";
|
|
26
26
|
import { formatAll } from "../codemirror/format";
|
|
@@ -28,20 +28,16 @@ import { autoSaveConfigAtom } from "../config/config";
|
|
|
28
28
|
import { useAutoExport } from "../export/hooks";
|
|
29
29
|
import { getSerializedLayout } from "../layout/layout";
|
|
30
30
|
import { layoutStateAtom } from "../layout/state";
|
|
31
|
-
import { kioskModeAtom } from "../mode";
|
|
32
31
|
import { connectionAtom } from "../network/connection";
|
|
32
|
+
import { canEditAtom } from "@/core/notebook";
|
|
33
33
|
import { useRequestClient } from "../network/requests";
|
|
34
34
|
import { ConnectionState } from "../session/types";
|
|
35
35
|
import { isWasm } from "../wasm/utils";
|
|
36
36
|
import { filenameAtom } from "./file-state";
|
|
37
37
|
import { useFilename, useUpdateFilename } from "./filename";
|
|
38
|
-
import {
|
|
38
|
+
import { documentVersionAtom, needsSaveAtom, savedLayoutAtom } from "./state";
|
|
39
39
|
import { useAutoSave } from "./useAutoSave";
|
|
40
40
|
|
|
41
|
-
interface SaveNotebookProps {
|
|
42
|
-
kioskMode: boolean;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
let notebookSaveQueue: Promise<void> = Promise.resolve();
|
|
46
42
|
|
|
47
43
|
export function enqueueNotebookSave<T>(save: () => Promise<T>): Promise<T> {
|
|
@@ -53,12 +49,12 @@ export function enqueueNotebookSave<T>(save: () => Promise<T>): Promise<T> {
|
|
|
53
49
|
return result;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
export const SaveComponent = (
|
|
52
|
+
export const SaveComponent = () => {
|
|
57
53
|
const filename = useFilename();
|
|
58
54
|
const needsSave = useAtomValue(needsSaveAtom);
|
|
59
55
|
const closed = useAtomValue(connectionAtom).state === ConnectionState.CLOSED;
|
|
60
56
|
const { saveOrNameNotebook, saveIfNotebookIsPersistent } = useSaveNotebook();
|
|
61
|
-
useAutoSaveNotebook({ onSave: saveIfNotebookIsPersistent
|
|
57
|
+
useAutoSaveNotebook({ onSave: saveIfNotebookIsPersistent });
|
|
62
58
|
|
|
63
59
|
useAutoExport();
|
|
64
60
|
|
|
@@ -105,7 +101,6 @@ export const SaveComponent = ({ kioskMode }: SaveNotebookProps) => {
|
|
|
105
101
|
export function useSaveNotebook() {
|
|
106
102
|
const { sendSave } = useRequestClient();
|
|
107
103
|
const { openModal, closeModal, openAlert } = useImperativeModal();
|
|
108
|
-
const setLastSavedNotebook = useSetAtom(lastSavedNotebookAtom);
|
|
109
104
|
const updateFilename = useUpdateFilename();
|
|
110
105
|
const store = useStore();
|
|
111
106
|
|
|
@@ -114,11 +109,6 @@ export function useSaveNotebook() {
|
|
|
114
109
|
enqueueNotebookSave(async () => {
|
|
115
110
|
const connection = store.get(connectionAtom);
|
|
116
111
|
const autoSaveConfig = store.get(autoSaveConfigAtom);
|
|
117
|
-
const kioskMode = store.get(kioskModeAtom);
|
|
118
|
-
|
|
119
|
-
if (kioskMode) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
112
|
|
|
123
113
|
// Don't save if we are not connected to a kernel
|
|
124
114
|
if (connection.state !== ConnectionState.OPEN) {
|
|
@@ -126,6 +116,11 @@ export function useSaveNotebook() {
|
|
|
126
116
|
return;
|
|
127
117
|
}
|
|
128
118
|
|
|
119
|
+
// The host may not let this consumer edit, or may not have said yet.
|
|
120
|
+
if (!store.get(canEditAtom)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
129
124
|
Logger.log("saving to ", filename);
|
|
130
125
|
|
|
131
126
|
if (userInitiated && autoSaveConfig.format_on_save) {
|
|
@@ -157,14 +152,14 @@ export function useSaveNotebook() {
|
|
|
157
152
|
layout: getSerializedLayout(),
|
|
158
153
|
persist: true,
|
|
159
154
|
});
|
|
160
|
-
return
|
|
155
|
+
return layout;
|
|
161
156
|
};
|
|
162
157
|
// TODO(MO-7955): Remove this conditional once all frontend saves use
|
|
163
158
|
// the host protocol. Host saves cannot recover edits from an editor snapshot.
|
|
164
|
-
const
|
|
159
|
+
const savedLayout = isWasm()
|
|
165
160
|
? await flushDocument().then(save)
|
|
166
161
|
: await withDocumentSave(save);
|
|
167
|
-
|
|
162
|
+
store.set(savedLayoutAtom, savedLayout);
|
|
168
163
|
}),
|
|
169
164
|
);
|
|
170
165
|
|
|
@@ -224,29 +219,20 @@ export function isNamedPersistentFile(
|
|
|
224
219
|
);
|
|
225
220
|
}
|
|
226
221
|
|
|
227
|
-
export function useAutoSaveNotebook(opts: {
|
|
228
|
-
onSave: () => void;
|
|
229
|
-
kioskMode: boolean;
|
|
230
|
-
}) {
|
|
222
|
+
export function useAutoSaveNotebook(opts: { onSave: () => void }) {
|
|
231
223
|
const autoSaveConfig = useAtomValue(autoSaveConfigAtom);
|
|
232
|
-
const
|
|
224
|
+
const canEdit = useAtomValue(canEditAtom);
|
|
233
225
|
const [connection] = useAtom(connectionAtom);
|
|
234
226
|
const needsSave = useAtomValue(needsSaveAtom);
|
|
235
|
-
|
|
236
|
-
const cells = notebookCells(notebook);
|
|
237
|
-
const codes = cells.map((cell) => cell.code);
|
|
238
|
-
const cellNames = cells.map((cell) => cell.name);
|
|
239
|
-
const configs = getCellConfigs(notebook);
|
|
227
|
+
const version = useAtomValue(documentVersionAtom);
|
|
240
228
|
|
|
241
229
|
useAutoSave({
|
|
242
230
|
onSave: opts.onSave,
|
|
243
231
|
needsSave: needsSave,
|
|
244
|
-
|
|
245
|
-
cellConfigs: configs,
|
|
246
|
-
cellNames: cellNames,
|
|
232
|
+
version,
|
|
247
233
|
connStatus: connection,
|
|
248
234
|
config: autoSaveConfig,
|
|
249
|
-
|
|
235
|
+
canEdit,
|
|
250
236
|
});
|
|
251
237
|
}
|
|
252
238
|
|
package/src/core/saving/state.ts
CHANGED
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
/** Whether the notebook has changes the host has not written to its store. */
|
|
4
|
+
|
|
3
5
|
import { dequal as isEqual } from "dequal";
|
|
4
6
|
import { atom } from "jotai";
|
|
5
|
-
import { arrayShallowEquals } from "@/utils/arrays";
|
|
6
|
-
import { type NotebookState, notebookAtom } from "../cells/cells";
|
|
7
7
|
import { type LayoutState, layoutStateAtom } from "../layout/state";
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
configs: CellConfig[];
|
|
13
|
-
names: string[];
|
|
14
|
-
layout: LayoutState;
|
|
15
|
-
}
|
|
8
|
+
import {
|
|
9
|
+
activeDocumentModelAtom,
|
|
10
|
+
confirmedDocumentAtom,
|
|
11
|
+
} from "../session/active-notebook";
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
/** The document version, which counts confirmed changes. Null without a document. */
|
|
14
|
+
export const documentVersionAtom = atom(
|
|
15
|
+
(get) => get(confirmedDocumentAtom)?.revision.version ?? null,
|
|
19
16
|
);
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
18
|
+
/**
|
|
19
|
+
* The layout as the file last had it. The host document does not carry
|
|
20
|
+
* the layout file, so this client compares it itself until it does.
|
|
21
|
+
*/
|
|
22
|
+
export const savedLayoutAtom = atom<LayoutState | undefined>(undefined);
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}) {
|
|
37
|
-
if (!lastSavedNotebook) {
|
|
24
|
+
/**
|
|
25
|
+
* Whether saving would change the file: the host's document differs from
|
|
26
|
+
* what it last wrote, this client has edits the host has not confirmed,
|
|
27
|
+
* or the layout changed. A document never written reports as unsaved.
|
|
28
|
+
*/
|
|
29
|
+
export const needsSaveAtom = atom((get) => {
|
|
30
|
+
const document = get(confirmedDocumentAtom);
|
|
31
|
+
if (document === null) {
|
|
38
32
|
return false;
|
|
39
33
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
34
|
+
if (document.revision.etag !== document.revision.saved_etag) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const model = get(activeDocumentModelAtom);
|
|
38
|
+
if (model !== null) {
|
|
39
|
+
const { status } = get(model.syncStatusAtom);
|
|
40
|
+
if (status !== "idle" && status !== "unavailable") {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const saved = get(savedLayoutAtom);
|
|
45
|
+
const layout = get(layoutStateAtom);
|
|
45
46
|
return (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
!isEqual(layout.selectedLayout, lastSavedNotebook.layout.selectedLayout) ||
|
|
50
|
-
!isEqual(layout.layoutData, lastSavedNotebook.layout.layoutData)
|
|
47
|
+
saved !== undefined &&
|
|
48
|
+
(layout.selectedLayout !== saved.selectedLayout ||
|
|
49
|
+
!isEqual(layout.layoutData, saved.layoutData))
|
|
51
50
|
);
|
|
52
|
-
}
|
|
51
|
+
});
|
|
@@ -1,40 +1,23 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import type { UserConfig } from "../config/config-schema";
|
|
4
|
-
import type { CellConfig } from "../network/types";
|
|
5
4
|
import { type ConnectionStatus, ConnectionState } from "../session/types";
|
|
6
5
|
|
|
6
|
+
/** Save after a delay while the notebook needs it, restarting on every confirmed change. */
|
|
7
7
|
export function useAutoSave(opts: {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
cellNames: string[];
|
|
8
|
+
/** The document version, which counts confirmed changes. */
|
|
9
|
+
version: number | null;
|
|
11
10
|
config: UserConfig["save"];
|
|
12
11
|
connStatus: ConnectionStatus;
|
|
13
12
|
needsSave: boolean;
|
|
14
|
-
|
|
13
|
+
canEdit: boolean;
|
|
15
14
|
onSave: () => void;
|
|
16
15
|
}) {
|
|
17
|
-
const {
|
|
18
|
-
codes,
|
|
19
|
-
cellConfigs,
|
|
20
|
-
config,
|
|
21
|
-
connStatus,
|
|
22
|
-
cellNames,
|
|
23
|
-
needsSave,
|
|
24
|
-
kioskMode,
|
|
25
|
-
onSave,
|
|
26
|
-
} = opts;
|
|
16
|
+
const { version, config, connStatus, needsSave, canEdit, onSave } = opts;
|
|
27
17
|
const autosaveTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
|
28
18
|
|
|
29
|
-
const codesString = codes.join(":");
|
|
30
|
-
const cellConfigsString = cellConfigs
|
|
31
|
-
.map((config) => JSON.stringify(config))
|
|
32
|
-
.join(":");
|
|
33
|
-
const cellNamesString = cellNames.join(":");
|
|
34
|
-
|
|
35
19
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
if (kioskMode) {
|
|
20
|
+
if (!canEdit) {
|
|
38
21
|
return;
|
|
39
22
|
}
|
|
40
23
|
|
|
@@ -53,16 +36,6 @@ export function useAutoSave(opts: {
|
|
|
53
36
|
clearTimeout(autosaveTimeoutId.current);
|
|
54
37
|
}
|
|
55
38
|
};
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
}, [
|
|
59
|
-
codesString,
|
|
60
|
-
cellConfigsString,
|
|
61
|
-
cellNamesString,
|
|
62
|
-
config,
|
|
63
|
-
connStatus.state,
|
|
64
|
-
onSave,
|
|
65
|
-
kioskMode,
|
|
66
|
-
needsSave,
|
|
67
|
-
]);
|
|
39
|
+
// `version` restarts the delay on every confirmed change.
|
|
40
|
+
}, [version, config, connStatus.state, onSave, canEdit, needsSave]);
|
|
68
41
|
}
|
|
@@ -9,3 +9,14 @@ export const activeNotebookReplicaAtom = atom<NotebookReplica | null>(null);
|
|
|
9
9
|
|
|
10
10
|
/** The document model owned by the mounted notebook session. */
|
|
11
11
|
export const activeDocumentModelAtom = atom<DocumentModel | null>(null);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The document the host last confirmed, or null while no notebook is
|
|
15
|
+
* connected. Facts the host owns read this one, such as permission, the
|
|
16
|
+
* app config, saved state and whether source is available. What the user
|
|
17
|
+
* is editing reads the model's document, which adds this browser's drafts.
|
|
18
|
+
*/
|
|
19
|
+
export const confirmedDocumentAtom = atom((get) => {
|
|
20
|
+
const replica = get(activeNotebookReplicaAtom);
|
|
21
|
+
return replica === null ? null : get(replica.documentAtom);
|
|
22
|
+
});
|
|
@@ -8,11 +8,18 @@ import type { NotebookBackend } from "../notebook/replica";
|
|
|
8
8
|
import { PyodideBridge } from "../wasm/bridge";
|
|
9
9
|
import { isWasm } from "../wasm/utils";
|
|
10
10
|
import { sessionSse, websocket, withHeaders } from "../transports/server";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createStaticNotebookBackend,
|
|
13
|
+
emptySnapshot,
|
|
14
|
+
type StaticNotebookSnapshot,
|
|
15
|
+
startWithSnapshot,
|
|
16
|
+
} from "../transports/static";
|
|
12
17
|
|
|
13
18
|
/** How the editor follows its notebook. */
|
|
14
19
|
export interface EditorBackendOptions {
|
|
15
20
|
static: boolean;
|
|
21
|
+
/** The notebook the page was served with, shown until the host answers. */
|
|
22
|
+
snapshot: StaticNotebookSnapshot | null;
|
|
16
23
|
transportType: "websocket" | "sse";
|
|
17
24
|
/** The session route, read before each connection. */
|
|
18
25
|
url: () => string;
|
|
@@ -86,30 +93,21 @@ function wasm(): NotebookBackend {
|
|
|
86
93
|
};
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
/** Commands for a notebook the editor cannot change through its backend. */
|
|
90
|
-
function uneditable(): ReturnType<NotebookBackend["notebook"]> {
|
|
91
|
-
const unsupported: NotebookClient["save"] = async () => ({
|
|
92
|
-
ok: false,
|
|
93
|
-
status: 501,
|
|
94
|
-
problem: {
|
|
95
|
-
type: "tag:marimo.io,2026:host/unsupported-operation",
|
|
96
|
-
title: "Document mutations are unavailable on this connection",
|
|
97
|
-
status: 501,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
return { edit: () => unsupported(), save: unsupported };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
96
|
/** Choose the backend for the editor's notebook. */
|
|
104
97
|
export function createEditorBackend(
|
|
105
98
|
options: EditorBackendOptions,
|
|
106
99
|
): NotebookBackend {
|
|
107
100
|
if (options.static) {
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return wasm();
|
|
101
|
+
return createStaticNotebookBackend(
|
|
102
|
+
options.snapshot ?? emptySnapshot("static"),
|
|
103
|
+
);
|
|
112
104
|
}
|
|
105
|
+
const live = isWasm() ? wasm() : server(options);
|
|
106
|
+
return options.snapshot ? startWithSnapshot(options.snapshot, live) : live;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Follow the notebook on the server, over SSE or a WebSocket. */
|
|
110
|
+
function server(options: EditorBackendOptions): NotebookBackend {
|
|
113
111
|
const url = afterStartup(options);
|
|
114
112
|
return {
|
|
115
113
|
transport:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { atom } from "jotai";
|
|
4
|
+
import type { StaticNotebookSnapshot } from "../transports/static";
|
|
5
|
+
|
|
6
|
+
/** The notebook the page was served with, read at mount. Null when none was. */
|
|
7
|
+
export const embeddedSnapshotAtom = atom<StaticNotebookSnapshot | null>(null);
|
|
@@ -22,7 +22,7 @@ type Options = EditorBackendOptions & {
|
|
|
22
22
|
|
|
23
23
|
export function useNotebookReplica(options: Options): NotebookReplica {
|
|
24
24
|
const store = useStore();
|
|
25
|
-
const { static: isStatic, transportType } = options;
|
|
25
|
+
const { static: isStatic, snapshot, transportType } = options;
|
|
26
26
|
const replica = useMemo(() => createNotebookReplica(store), [store]);
|
|
27
27
|
const url = useEvent(options.url);
|
|
28
28
|
const headers = useEvent(options.headers);
|
|
@@ -35,6 +35,7 @@ export function useNotebookReplica(options: Options): NotebookReplica {
|
|
|
35
35
|
() =>
|
|
36
36
|
createEditorBackend({
|
|
37
37
|
static: isStatic,
|
|
38
|
+
snapshot,
|
|
38
39
|
transportType,
|
|
39
40
|
url,
|
|
40
41
|
headers,
|
|
@@ -44,6 +45,7 @@ export function useNotebookReplica(options: Options): NotebookReplica {
|
|
|
44
45
|
}),
|
|
45
46
|
[
|
|
46
47
|
isStatic,
|
|
48
|
+
snapshot,
|
|
47
49
|
transportType,
|
|
48
50
|
url,
|
|
49
51
|
headers,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
} from "../codemirror/cells/debugger-state";
|
|
36
36
|
import { notebookCells } from "../cells/utils";
|
|
37
37
|
import { capabilitiesAtom } from "../config/capabilities";
|
|
38
|
-
import { connectionTransportTypeAtom
|
|
38
|
+
import { connectionTransportTypeAtom } from "../config/config";
|
|
39
39
|
import { useDataSourceActions } from "../datasets/data-source-connections";
|
|
40
40
|
import { invalidateDataSourceDiscovery } from "../datasets/data-source-discovery";
|
|
41
41
|
import type { ConnectionName } from "../datasets/engines";
|
|
@@ -62,9 +62,10 @@ import {
|
|
|
62
62
|
stableSessionIdAtom,
|
|
63
63
|
} from "../kernel/session";
|
|
64
64
|
import { initialRunCompletedAtom, kernelStateAtom } from "../kernel/state";
|
|
65
|
-
import {
|
|
65
|
+
import { useLayoutActions } from "../layout/state";
|
|
66
66
|
import { kioskModeAtom } from "../mode";
|
|
67
67
|
import { connectionAtom, startupProgressAtom } from "../network/connection";
|
|
68
|
+
import { savedLayoutAtom } from "../saving/state";
|
|
68
69
|
import type { RequestId } from "../network/DeferredRequestRegistry";
|
|
69
70
|
import { useRuntimeManager } from "../runtime/config";
|
|
70
71
|
import { SECRETS_REGISTRY } from "../secrets/request-registry";
|
|
@@ -77,16 +78,15 @@ import { useStorageActions } from "../storage/state";
|
|
|
77
78
|
import { useVariablesActions } from "../variables/state";
|
|
78
79
|
import type { VariableName } from "../variables/types";
|
|
79
80
|
import { activeDocumentModelAtom } from "./active-notebook";
|
|
81
|
+
import { embeddedSnapshotAtom } from "./embedded";
|
|
80
82
|
import { WebSocketClosedReason, ConnectionState } from "./types";
|
|
81
83
|
|
|
82
84
|
/** Coordinate session startup and apply notifications to the editor. */
|
|
83
85
|
export function useNotebookSession(opts: {
|
|
84
86
|
sessionId: SessionId;
|
|
85
87
|
autoInstantiate: boolean;
|
|
86
|
-
/** Called with the layout the kernel reported, after its cells are ready. */
|
|
87
|
-
onKernelReady?: (layout: LayoutState) => void;
|
|
88
88
|
}) {
|
|
89
|
-
const { autoInstantiate, sessionId
|
|
89
|
+
const { autoInstantiate, sessionId } = opts;
|
|
90
90
|
const { showBoundary } = useErrorBoundary();
|
|
91
91
|
const store = useStore();
|
|
92
92
|
|
|
@@ -99,7 +99,6 @@ export function useNotebookSession(opts: {
|
|
|
99
99
|
const { addCellNotification } = useRunsActions();
|
|
100
100
|
const setKernelState = useSetAtom(kernelStateAtom);
|
|
101
101
|
const setInitialRunCompleted = useSetAtom(initialRunCompletedAtom);
|
|
102
|
-
const setAppConfig = useSetAppConfig();
|
|
103
102
|
const { setVariables, setMetadata } = useVariablesActions();
|
|
104
103
|
const { addColumnPreview } = useDatasetsActions();
|
|
105
104
|
const { addDatasets, filterDatasetsFromVariables } = useDatasetsActions();
|
|
@@ -156,12 +155,11 @@ export function useNotebookSession(opts: {
|
|
|
156
155
|
setLastRun,
|
|
157
156
|
resetCellRuntimes,
|
|
158
157
|
setLayoutData,
|
|
159
|
-
setAppConfig,
|
|
160
158
|
setCapabilities,
|
|
161
159
|
setKernelState,
|
|
162
160
|
onError: showBoundary,
|
|
163
161
|
});
|
|
164
|
-
|
|
162
|
+
store.set(savedLayoutAtom, layout);
|
|
165
163
|
if (!msg.data.resumed) {
|
|
166
164
|
// A freshly started kernel may expose a different environment
|
|
167
165
|
// (e.g. new env vars); re-run discovery instead of reusing stale
|
|
@@ -399,6 +397,7 @@ export function useNotebookSession(opts: {
|
|
|
399
397
|
};
|
|
400
398
|
const replica = useNotebookReplica({
|
|
401
399
|
static: isStaticNotebook(),
|
|
400
|
+
snapshot: useAtomValue(embeddedSnapshotAtom),
|
|
402
401
|
transportType,
|
|
403
402
|
url: () =>
|
|
404
403
|
transportType === "sse"
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
import { createNotebookReducer, NotebookEvents } from "@marimo-team/client";
|
|
4
4
|
import * as Protocol from "@marimo-team/client/protocol";
|
|
5
5
|
import { afterEach, assert, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { NotebookBackend } from "../../notebook/replica";
|
|
6
7
|
import { document as documentResource } from "../../notebook/replica/resources/document";
|
|
7
8
|
import {
|
|
8
9
|
createStaticNotebookBackend,
|
|
10
|
+
emptySnapshot,
|
|
9
11
|
type StaticNotebookSnapshot,
|
|
12
|
+
startWithSnapshot,
|
|
10
13
|
} from "../static";
|
|
11
14
|
import { fixtureEvents } from "../../notebook/replica/__tests__/fixtures";
|
|
12
15
|
|
|
@@ -34,24 +37,48 @@ afterEach(async () => {
|
|
|
34
37
|
vi.useRealTimers();
|
|
35
38
|
});
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
cursor?: string,
|
|
40
|
-
) {
|
|
41
|
-
const stream = new NotebookEvents(backend.transport, { cursor });
|
|
42
|
-
streams.push(stream);
|
|
40
|
+
/** Collect a stream's events up to and including its next `ready`. */
|
|
41
|
+
function untilReady(stream: NotebookEvents): Promise<Protocol.EventOutput[]> {
|
|
43
42
|
const events: Protocol.EventOutput[] = [];
|
|
44
|
-
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
45
44
|
stream.subscribe((event) => {
|
|
46
45
|
events.push(Protocol.Event.parse(event));
|
|
47
46
|
if (event.event === "ready") {
|
|
48
|
-
resolve();
|
|
47
|
+
resolve(events);
|
|
49
48
|
}
|
|
50
49
|
}, reject);
|
|
51
50
|
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Open a backend's stream and read until it is ready. */
|
|
54
|
+
async function read(
|
|
55
|
+
backend: ReturnType<typeof createStaticNotebookBackend>,
|
|
56
|
+
cursor?: string,
|
|
57
|
+
) {
|
|
58
|
+
const stream = new NotebookEvents(backend.transport, { cursor });
|
|
59
|
+
streams.push(stream);
|
|
60
|
+
const events = await untilReady(stream);
|
|
52
61
|
return { stream, events };
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
/** A live backend whose stream sends nothing until `release` is called. */
|
|
65
|
+
function silentUntilReleased(backend: NotebookBackend) {
|
|
66
|
+
let release = () => {};
|
|
67
|
+
const released = new Promise<void>((resolve) => {
|
|
68
|
+
release = resolve;
|
|
69
|
+
});
|
|
70
|
+
const attempts: Array<string | null> = [];
|
|
71
|
+
const silent: NotebookBackend = {
|
|
72
|
+
...backend,
|
|
73
|
+
transport: async (attempt, handlers) => {
|
|
74
|
+
attempts.push(attempt.cursor);
|
|
75
|
+
await released;
|
|
76
|
+
return backend.transport(attempt, handlers);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return { backend: silent, release: () => release(), attempts };
|
|
80
|
+
}
|
|
81
|
+
|
|
55
82
|
describe("static notebook backend", () => {
|
|
56
83
|
it("supplies a complete document through the normal event reader and reducer", async () => {
|
|
57
84
|
const source = snapshot();
|
|
@@ -173,3 +200,107 @@ describe("static notebook backend", () => {
|
|
|
173
200
|
expect(message).toHaveBeenCalledOnce();
|
|
174
201
|
});
|
|
175
202
|
});
|
|
203
|
+
|
|
204
|
+
describe("starting from an embedded snapshot", () => {
|
|
205
|
+
it("shows the snapshot until the host answers, then resets into its stream", async () => {
|
|
206
|
+
const embedded = snapshot();
|
|
207
|
+
const live = silentUntilReleased(
|
|
208
|
+
createStaticNotebookBackend(emptySnapshot("live")),
|
|
209
|
+
);
|
|
210
|
+
const reducer = createNotebookReducer({ document: documentResource });
|
|
211
|
+
|
|
212
|
+
// The snapshot is published on its own, with the host still silent.
|
|
213
|
+
const { stream, events } = await read(
|
|
214
|
+
startWithSnapshot(embedded, live.backend),
|
|
215
|
+
);
|
|
216
|
+
expect(events.map((event) => event.event)).toEqual([
|
|
217
|
+
"notebook",
|
|
218
|
+
"cell",
|
|
219
|
+
"cell",
|
|
220
|
+
"cell",
|
|
221
|
+
"document",
|
|
222
|
+
"ready",
|
|
223
|
+
]);
|
|
224
|
+
const shown = events.reduce(reducer.reduce, reducer.initial);
|
|
225
|
+
expect(shown.notebook?.id).toBe(embedded.notebook.id);
|
|
226
|
+
expect(shown.resources.document.current?.cells.size).toBe(3);
|
|
227
|
+
expect(live.attempts).toEqual([null]);
|
|
228
|
+
|
|
229
|
+
// The host's first frame discards it, and the host's snapshot follows.
|
|
230
|
+
const host = untilReady(stream);
|
|
231
|
+
live.release();
|
|
232
|
+
const rest = await host;
|
|
233
|
+
expect(rest.map((event) => event.event)).toEqual([
|
|
234
|
+
"reset",
|
|
235
|
+
"notebook",
|
|
236
|
+
"document",
|
|
237
|
+
"ready",
|
|
238
|
+
]);
|
|
239
|
+
const state = rest.reduce(reducer.reduce, shown);
|
|
240
|
+
expect(state.notebook?.id).toBe("live");
|
|
241
|
+
expect(state.resources.document.current?.cells.size).toBe(0);
|
|
242
|
+
expect(stream.status).toBe("connected");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("does not open the host when cancelled while showing the snapshot", async () => {
|
|
246
|
+
const attempts: Array<string | null> = [];
|
|
247
|
+
const live: NotebookBackend = {
|
|
248
|
+
...createStaticNotebookBackend(emptySnapshot("live")),
|
|
249
|
+
transport: async (attempt) => {
|
|
250
|
+
attempts.push(attempt.cursor);
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
const controller = new AbortController();
|
|
254
|
+
const message = vi.fn(() => controller.abort());
|
|
255
|
+
|
|
256
|
+
await startWithSnapshot(snapshot(), live).transport(
|
|
257
|
+
{ cursor: null, signal: controller.signal },
|
|
258
|
+
{ open: vi.fn(), message },
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
expect(message).toHaveBeenCalledOnce();
|
|
262
|
+
expect(attempts).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("starts the host connection fresh when resuming from the snapshot's cursor", async () => {
|
|
266
|
+
const embedded = snapshot();
|
|
267
|
+
const live = silentUntilReleased(
|
|
268
|
+
createStaticNotebookBackend(emptySnapshot("live")),
|
|
269
|
+
);
|
|
270
|
+
live.release();
|
|
271
|
+
const { events } = await read(
|
|
272
|
+
startWithSnapshot(embedded, live.backend),
|
|
273
|
+
`${embedded.notebook.id}:snapshot`,
|
|
274
|
+
);
|
|
275
|
+
expect(live.attempts).toEqual([null]);
|
|
276
|
+
expect(events.map((event) => event.event)).toEqual([
|
|
277
|
+
"reset",
|
|
278
|
+
"notebook",
|
|
279
|
+
"document",
|
|
280
|
+
"ready",
|
|
281
|
+
]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("skips the snapshot when a connection resumes", async () => {
|
|
285
|
+
const backend = startWithSnapshot(
|
|
286
|
+
snapshot(),
|
|
287
|
+
createStaticNotebookBackend(emptySnapshot("live")),
|
|
288
|
+
);
|
|
289
|
+
const { events } = await read(backend, "static");
|
|
290
|
+
expect(events.map((event) => event.event)).toEqual([
|
|
291
|
+
"reset",
|
|
292
|
+
"notebook",
|
|
293
|
+
"document",
|
|
294
|
+
"ready",
|
|
295
|
+
]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("keeps the live backend's commands", async () => {
|
|
299
|
+
const backend = startWithSnapshot(
|
|
300
|
+
snapshot(),
|
|
301
|
+
createStaticNotebookBackend(emptySnapshot("live")),
|
|
302
|
+
);
|
|
303
|
+
const result = await backend.notebook("live").save();
|
|
304
|
+
expect(result).toMatchObject({ ok: false, status: 501 });
|
|
305
|
+
});
|
|
306
|
+
});
|