@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
|
@@ -52,6 +52,7 @@ export const Tracing: React.FC = () => {
|
|
|
52
52
|
|
|
53
53
|
const { theme } = useTheme();
|
|
54
54
|
const [chartPosition, setChartPosition] = useState<ChartPosition>("above");
|
|
55
|
+
const chartPositionId = React.useId();
|
|
55
56
|
const panelSection = usePanelSection();
|
|
56
57
|
|
|
57
58
|
const toggleChartPosition = () => {
|
|
@@ -76,11 +77,12 @@ export const Tracing: React.FC = () => {
|
|
|
76
77
|
<div className="py-1 px-2 overflow-y-scroll h-full">
|
|
77
78
|
<div className="flex flex-row justify-start gap-3">
|
|
78
79
|
<div className="flex flex-row gap-1 items-center">
|
|
79
|
-
<label htmlFor=
|
|
80
|
+
<label htmlFor={chartPositionId} className="text-xs">
|
|
80
81
|
Inline chart
|
|
81
82
|
</label>
|
|
82
83
|
<input
|
|
83
84
|
type="checkbox"
|
|
85
|
+
id={chartPositionId}
|
|
84
86
|
name="chartPosition"
|
|
85
87
|
data-testid="chartPosition"
|
|
86
88
|
onClick={toggleChartPosition}
|
|
@@ -163,28 +165,31 @@ const TraceBlock: React.FC<{
|
|
|
163
165
|
const chevron = <Icon height={16} className="inline" />;
|
|
164
166
|
|
|
165
167
|
const traceTitle = (
|
|
166
|
-
<
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
aria-expanded={isExpanded}
|
|
171
|
+
className="text-sm cursor-pointer"
|
|
172
|
+
onClick={onToggleExpanded}
|
|
173
|
+
>
|
|
167
174
|
Run - {formatLogTimestamp(run.runStartTime)}
|
|
168
175
|
{chevron}
|
|
169
|
-
</
|
|
176
|
+
</button>
|
|
170
177
|
);
|
|
171
178
|
|
|
172
|
-
if (!isExpanded) {
|
|
173
|
-
return (
|
|
174
|
-
<div key={run.runId} className="flex flex-col">
|
|
175
|
-
<pre className="font-mono font-semibold">{traceTitle}</pre>
|
|
176
|
-
</div>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
179
|
return (
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
<div
|
|
181
|
+
className={cn(
|
|
182
|
+
"grid grid-cols-1",
|
|
183
|
+
isExpanded &&
|
|
184
|
+
chartPosition === "sideBySide" &&
|
|
185
|
+
"grid-cols-[auto_minmax(0,1fr)] grid-rows-[auto_1fr]",
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
<pre className="font-mono font-semibold">{traceTitle}</pre>
|
|
189
|
+
{isExpanded && (
|
|
190
|
+
<TraceBlockBody run={run} chartPosition={chartPosition} theme={theme} />
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
188
193
|
);
|
|
189
194
|
};
|
|
190
195
|
|
|
@@ -192,8 +197,7 @@ const TraceBlockBody: React.FC<{
|
|
|
192
197
|
run: Run;
|
|
193
198
|
chartPosition: ChartPosition;
|
|
194
199
|
theme: ResolvedTheme;
|
|
195
|
-
|
|
196
|
-
}> = ({ run, chartPosition, theme, title }) => {
|
|
200
|
+
}> = ({ run, chartPosition, theme }) => {
|
|
197
201
|
const [hoveredCellId, setHoveredCellId] = useState<CellId | null>();
|
|
198
202
|
const vegaRef = useRef<HTMLDivElement>(null);
|
|
199
203
|
const { ref, width = 300 } = useResizeObserver<HTMLDivElement>();
|
|
@@ -264,16 +268,22 @@ const TraceBlockBody: React.FC<{
|
|
|
264
268
|
}, [embed]);
|
|
265
269
|
|
|
266
270
|
const traceRows = (
|
|
267
|
-
<
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
<div className="font-mono font-semibold">
|
|
272
|
+
<TraceRows
|
|
273
|
+
run={run}
|
|
274
|
+
hoveredCellId={hoveredCellId}
|
|
275
|
+
hiddenInputElementId={hiddenInputElementId}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
272
278
|
);
|
|
273
279
|
|
|
274
280
|
const chartElement = (
|
|
275
281
|
<div
|
|
276
|
-
className={
|
|
282
|
+
className={
|
|
283
|
+
chartPosition === "sideBySide"
|
|
284
|
+
? "-mt-0.5 col-start-2 row-start-1 row-span-2 min-w-0"
|
|
285
|
+
: "min-w-0"
|
|
286
|
+
}
|
|
277
287
|
ref={ref}
|
|
278
288
|
>
|
|
279
289
|
<Suspense>
|
|
@@ -284,24 +294,18 @@ const TraceBlockBody: React.FC<{
|
|
|
284
294
|
|
|
285
295
|
if (chartPosition === "above") {
|
|
286
296
|
return (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
{traceRows}
|
|
292
|
-
</pre>
|
|
293
|
-
</div>
|
|
297
|
+
<>
|
|
298
|
+
{chartElement}
|
|
299
|
+
{traceRows}
|
|
300
|
+
</>
|
|
294
301
|
);
|
|
295
302
|
}
|
|
296
303
|
|
|
297
304
|
return (
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
{title}
|
|
301
|
-
{traceRows}
|
|
302
|
-
</pre>
|
|
305
|
+
<>
|
|
306
|
+
{traceRows}
|
|
303
307
|
{chartElement}
|
|
304
|
-
|
|
308
|
+
</>
|
|
305
309
|
);
|
|
306
310
|
};
|
|
307
311
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import { expect, it } from "vitest";
|
|
5
|
+
import { DraggablePopover } from "../draggable-popover";
|
|
6
|
+
|
|
7
|
+
it("moves from the keyboard handle without intercepting child input arrows", () => {
|
|
8
|
+
render(
|
|
9
|
+
<DraggablePopover open={true}>
|
|
10
|
+
<input aria-label="Filter" />
|
|
11
|
+
</DraggablePopover>,
|
|
12
|
+
);
|
|
13
|
+
const handle = screen.getByRole("button", {
|
|
14
|
+
name: "Move popup with arrow keys",
|
|
15
|
+
});
|
|
16
|
+
const popup = screen.getByRole("dialog");
|
|
17
|
+
fireEvent.keyDown(handle, { key: "ArrowRight" });
|
|
18
|
+
fireEvent.keyDown(handle, { key: "ArrowDown" });
|
|
19
|
+
expect(popup).toHaveStyle({ left: "10px", top: "10px" });
|
|
20
|
+
expect(
|
|
21
|
+
fireEvent.keyDown(screen.getByRole("textbox"), { key: "ArrowLeft" }),
|
|
22
|
+
).toBe(true);
|
|
23
|
+
expect(popup).toHaveStyle({ left: "10px", top: "10px" });
|
|
24
|
+
});
|
|
@@ -53,14 +53,36 @@ export const DraggablePopover = ({
|
|
|
53
53
|
top: position.y,
|
|
54
54
|
}}
|
|
55
55
|
>
|
|
56
|
-
<
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
aria-label="Move popup with arrow keys"
|
|
57
59
|
onMouseDown={handleMouseDown}
|
|
60
|
+
onKeyDown={(e) => {
|
|
61
|
+
const directions: Record<
|
|
62
|
+
string,
|
|
63
|
+
{ x: number; y: number } | undefined
|
|
64
|
+
> = {
|
|
65
|
+
ArrowLeft: { x: -10, y: 0 },
|
|
66
|
+
ArrowRight: { x: 10, y: 0 },
|
|
67
|
+
ArrowUp: { x: 0, y: -10 },
|
|
68
|
+
ArrowDown: { x: 0, y: 10 },
|
|
69
|
+
};
|
|
70
|
+
const offset = directions[e.key];
|
|
71
|
+
if (offset) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
setPosition((previous) => ({
|
|
75
|
+
x: previous.x + offset.x,
|
|
76
|
+
y: previous.y + offset.y,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
}}
|
|
58
80
|
className={`flex items-center justify-center absolute top-0 left-1/2 -translate-x-1/2 ${
|
|
59
81
|
isDragging ? "cursor-grabbing" : "cursor-grab"
|
|
60
82
|
}`}
|
|
61
83
|
>
|
|
62
84
|
<GripHorizontalIcon className="h-5 w-5 mt-1 text-muted-foreground/40" />
|
|
63
|
-
</
|
|
85
|
+
</button>
|
|
64
86
|
{children}
|
|
65
87
|
</PopoverContent>
|
|
66
88
|
</Popover>
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { atom } from "jotai";
|
|
4
4
|
import type { Attachment } from "@marimo-team/client/protocol";
|
|
5
|
-
import {
|
|
5
|
+
import { notebookReplicaAtom } from "@/core/websocket/state";
|
|
6
6
|
|
|
7
7
|
export type { Attachment } from "@marimo-team/client/protocol";
|
|
8
8
|
|
|
9
9
|
export const agentAttachmentsAtom = atom((get) => {
|
|
10
|
-
const notebook = get(
|
|
10
|
+
const notebook = get(notebookReplicaAtom);
|
|
11
11
|
return (notebook ? get(notebook.attachmentsAtom) : []).filter(
|
|
12
12
|
(attachment) => attachment.kind === "agent",
|
|
13
13
|
);
|
|
@@ -24,6 +24,7 @@ import { SCHEMA_CACHE } from "../../codemirror/language/languages/sql/completion
|
|
|
24
24
|
import type { Document } from "../../host/resources/document";
|
|
25
25
|
import { cellCodeBinding } from "../codemirror";
|
|
26
26
|
import { createDocumentModel } from "../model";
|
|
27
|
+
import { fakeBackend } from "../../host/__tests__/fixtures";
|
|
27
28
|
import { cell, document, withCells } from "./fixtures";
|
|
28
29
|
|
|
29
30
|
const views: EditorView[] = [];
|
|
@@ -87,7 +88,8 @@ function setup({
|
|
|
87
88
|
lifetimes.push(controller);
|
|
88
89
|
const model = createDocumentModel({
|
|
89
90
|
store,
|
|
90
|
-
|
|
91
|
+
documentAtom: source,
|
|
92
|
+
backendAtom: atom(fakeBackend({ edit: () => new Promise(() => {}) })),
|
|
91
93
|
signal: controller.signal,
|
|
92
94
|
});
|
|
93
95
|
const editor = (text = doc) => {
|
|
@@ -15,11 +15,10 @@ import {
|
|
|
15
15
|
useCellOrder,
|
|
16
16
|
useDocumentEdit,
|
|
17
17
|
useDocumentSync,
|
|
18
|
-
type DocumentHost,
|
|
19
18
|
} from "..";
|
|
20
19
|
import { cell, CONFIG, document, withCells } from "./fixtures";
|
|
20
|
+
import { type FakeEdit, fakeBackend } from "../../host/__tests__/fixtures";
|
|
21
21
|
|
|
22
|
-
type HostEdit = DocumentHost["edit"];
|
|
23
22
|
const lifetimes: AbortController[] = [];
|
|
24
23
|
|
|
25
24
|
afterEach(() => {
|
|
@@ -28,7 +27,7 @@ afterEach(() => {
|
|
|
28
27
|
|
|
29
28
|
function setup(
|
|
30
29
|
initial: Document | null = document(),
|
|
31
|
-
edit:
|
|
30
|
+
edit: FakeEdit = () => new Promise(() => {}),
|
|
32
31
|
) {
|
|
33
32
|
const store = createStore();
|
|
34
33
|
const source = atom(initial);
|
|
@@ -38,7 +37,8 @@ function setup(
|
|
|
38
37
|
lifetimes.push(controller);
|
|
39
38
|
const model = createDocumentModel({
|
|
40
39
|
store,
|
|
41
|
-
|
|
40
|
+
documentAtom: source,
|
|
41
|
+
backendAtom: atom(fakeBackend({ edit })),
|
|
42
42
|
signal: controller.signal,
|
|
43
43
|
});
|
|
44
44
|
const wrapper = ({ children }: PropsWithChildren) => (
|
|
@@ -53,14 +53,14 @@ describe("document hooks", () => {
|
|
|
53
53
|
it("flushes edits through the model and aborts work when the session ends", async () => {
|
|
54
54
|
const accepted = withCells(document(), { c1: cell("c1", "local") });
|
|
55
55
|
const edit = vi
|
|
56
|
-
.fn<
|
|
56
|
+
.fn<FakeEdit>()
|
|
57
57
|
.mockResolvedValueOnce({
|
|
58
58
|
ok: true,
|
|
59
59
|
status: 200,
|
|
60
60
|
data: { document: accepted.revision, cells: {} },
|
|
61
61
|
})
|
|
62
62
|
.mockImplementation(
|
|
63
|
-
() => new Deferred<Awaited<ReturnType<
|
|
63
|
+
() => new Deferred<Awaited<ReturnType<FakeEdit>>>().promise,
|
|
64
64
|
);
|
|
65
65
|
const { store, source, wrapper, controller } = setup(document(), edit);
|
|
66
66
|
const { result, unmount } = renderHook(
|
|
@@ -98,7 +98,7 @@ describe("document hooks", () => {
|
|
|
98
98
|
await rejected;
|
|
99
99
|
});
|
|
100
100
|
expect(edit).toHaveBeenCalledTimes(2);
|
|
101
|
-
expect(edit.mock.calls[1][2]
|
|
101
|
+
expect(edit.mock.calls[1][2]?.signal?.aborted).toBe(true);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
it("publishes compound edits atomically without changing host records", () => {
|
|
@@ -5,8 +5,10 @@ import type * as Protocol from "@marimo-team/client/protocol";
|
|
|
5
5
|
import { atom, createStore } from "jotai/vanilla";
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { Deferred } from "@/utils/Deferred";
|
|
8
|
+
import type { NotebookBackend } from "../../host/backend";
|
|
8
9
|
import type { Document } from "../../host/resources/document";
|
|
9
|
-
import { createDocumentModel
|
|
10
|
+
import { createDocumentModel } from "../model";
|
|
11
|
+
import { type FakeEdit, fakeBackend } from "../../host/__tests__/fixtures";
|
|
10
12
|
import { cell, document, withCells } from "./fixtures";
|
|
11
13
|
|
|
12
14
|
function response(confirmed: Document): Result<Protocol.EditResponseOutput> {
|
|
@@ -30,11 +32,11 @@ async function expectAbort(promise: Promise<unknown>, reason: Error) {
|
|
|
30
32
|
interface SentEdit {
|
|
31
33
|
notebookId: Protocol.NotebookId;
|
|
32
34
|
changes: readonly Protocol.Change[];
|
|
33
|
-
idempotencyKey
|
|
34
|
-
signal
|
|
35
|
+
idempotencyKey?: string;
|
|
36
|
+
signal?: AbortSignal;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
function setup() {
|
|
39
|
+
function setup({ connected = true } = {}) {
|
|
38
40
|
const store = createStore();
|
|
39
41
|
const source = atom<Document | null>(document());
|
|
40
42
|
const unmounted = vi.fn();
|
|
@@ -45,21 +47,24 @@ function setup() {
|
|
|
45
47
|
replies.push(reply);
|
|
46
48
|
return reply.promise;
|
|
47
49
|
});
|
|
48
|
-
const
|
|
49
|
-
documentAtom: source,
|
|
50
|
+
const backend = fakeBackend({
|
|
50
51
|
edit: (notebookId, changes, options) =>
|
|
51
52
|
sent({ notebookId, changes, ...options }),
|
|
52
|
-
};
|
|
53
|
+
});
|
|
54
|
+
const backendAtom = atom<NotebookBackend | null>(connected ? backend : null);
|
|
53
55
|
const controller = new AbortController();
|
|
54
56
|
lifetimes.push(controller);
|
|
55
57
|
const model = createDocumentModel({
|
|
56
58
|
store,
|
|
57
|
-
|
|
59
|
+
documentAtom: source,
|
|
60
|
+
backendAtom,
|
|
58
61
|
signal: controller.signal,
|
|
59
62
|
});
|
|
60
63
|
return {
|
|
61
64
|
store,
|
|
62
65
|
source,
|
|
66
|
+
backend,
|
|
67
|
+
backendAtom,
|
|
63
68
|
model,
|
|
64
69
|
sent,
|
|
65
70
|
replies,
|
|
@@ -88,11 +93,12 @@ describe("document synchronization", () => {
|
|
|
88
93
|
const controller = new AbortController();
|
|
89
94
|
const reason = new Error("Session ended");
|
|
90
95
|
controller.abort(reason);
|
|
91
|
-
const edit = vi.fn<
|
|
96
|
+
const edit = vi.fn<FakeEdit>();
|
|
92
97
|
expect(() =>
|
|
93
98
|
createDocumentModel({
|
|
94
99
|
store,
|
|
95
|
-
|
|
100
|
+
documentAtom: source,
|
|
101
|
+
backendAtom: atom(fakeBackend({ edit })),
|
|
96
102
|
signal: controller.signal,
|
|
97
103
|
}),
|
|
98
104
|
).toThrow(reason);
|
|
@@ -152,7 +158,34 @@ describe("document synchronization", () => {
|
|
|
152
158
|
await vi.advanceTimersByTimeAsync(0);
|
|
153
159
|
store.set(source, document("n2"));
|
|
154
160
|
await rejected;
|
|
155
|
-
expect(sent.mock.calls[0][0].signal
|
|
161
|
+
expect(sent.mock.calls[0][0].signal?.aborted).toBe(true);
|
|
156
162
|
expect(status()).toMatchObject({ status: "error", retryable: false });
|
|
157
163
|
});
|
|
164
|
+
|
|
165
|
+
it("aborts an in-flight edit when the backend changes and never sends it to the new one", async () => {
|
|
166
|
+
const { store, backendAtom, sent, edit, model } = setup();
|
|
167
|
+
edit("local");
|
|
168
|
+
const rejected = expectFailure(model.flush(), "notebook changed");
|
|
169
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
170
|
+
const other = vi.fn<FakeEdit>();
|
|
171
|
+
store.set(backendAtom, fakeBackend({ edit: other }));
|
|
172
|
+
await rejected;
|
|
173
|
+
expect(sent).toHaveBeenCalledOnce();
|
|
174
|
+
expect(sent.mock.calls[0][0].signal?.aborted).toBe(true);
|
|
175
|
+
expect(other).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("keeps edits as drafts until a backend is connected", async () => {
|
|
179
|
+
const { store, backend, backendAtom, sent, edit, status, value } = setup({
|
|
180
|
+
connected: false,
|
|
181
|
+
});
|
|
182
|
+
edit("local");
|
|
183
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
184
|
+
expect(status()).toEqual({ status: "unavailable" });
|
|
185
|
+
expect(value()?.code).toBe("local");
|
|
186
|
+
|
|
187
|
+
store.set(backendAtom, backend);
|
|
188
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
189
|
+
expect(sent).toHaveBeenCalledOnce();
|
|
190
|
+
});
|
|
158
191
|
});
|
|
@@ -13,7 +13,8 @@ function createSession(initial: Document, controller: AbortController) {
|
|
|
13
13
|
const { notebook, control } = createTestNotebook(store, initial);
|
|
14
14
|
const model = createDocumentModel({
|
|
15
15
|
store,
|
|
16
|
-
notebook,
|
|
16
|
+
documentAtom: notebook.documentAtom,
|
|
17
|
+
backendAtom: notebook.backendAtom,
|
|
17
18
|
signal: controller.signal,
|
|
18
19
|
});
|
|
19
20
|
return {
|
|
@@ -6,8 +6,8 @@ import { atom, type createStore } from "jotai/vanilla";
|
|
|
6
6
|
import { vi } from "vitest";
|
|
7
7
|
import { Deferred } from "@/utils/Deferred";
|
|
8
8
|
import { invariant } from "@/utils/invariant";
|
|
9
|
+
import { type FakeEdit, fakeBackend } from "../../host/__tests__/fixtures";
|
|
9
10
|
import type { Document } from "../../host/resources/document";
|
|
10
|
-
import type { DocumentHost } from "../model";
|
|
11
11
|
import { createTestHost, type EditRequest } from "./test-host";
|
|
12
12
|
|
|
13
13
|
export interface PendingRequest extends EditRequest {
|
|
@@ -33,19 +33,25 @@ export function createTestNotebook(
|
|
|
33
33
|
documents.push(update);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
const edit = vi.fn<
|
|
37
|
-
options
|
|
36
|
+
const edit = vi.fn<FakeEdit>((notebookId, changes, options) => {
|
|
37
|
+
const signal = options?.signal;
|
|
38
|
+
const idempotencyKey = options?.idempotencyKey;
|
|
39
|
+
invariant(
|
|
40
|
+
signal !== undefined && idempotencyKey !== undefined,
|
|
41
|
+
"The test host answers edits that carry a key and a signal",
|
|
42
|
+
);
|
|
43
|
+
signal.throwIfAborted();
|
|
38
44
|
const reply = new Deferred<Result<Protocol.EditResponseOutput>>();
|
|
39
45
|
let outcome: Result<Protocol.EditResponseOutput> | undefined;
|
|
40
|
-
const abort = () => reply.reject(
|
|
41
|
-
const finish = () =>
|
|
42
|
-
|
|
46
|
+
const abort = () => reply.reject(signal.reason);
|
|
47
|
+
const finish = () => signal.removeEventListener("abort", abort);
|
|
48
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
43
49
|
void reply.promise.then(finish, finish);
|
|
44
50
|
const request: PendingRequest = {
|
|
45
51
|
notebookId,
|
|
46
52
|
changes: structuredClone(changes),
|
|
47
|
-
idempotencyKey
|
|
48
|
-
signal
|
|
53
|
+
idempotencyKey,
|
|
54
|
+
signal,
|
|
49
55
|
process() {
|
|
50
56
|
invariant(outcome === undefined, "Request was already processed");
|
|
51
57
|
// Cancellation of the client's wait does not roll back a server commit.
|
|
@@ -74,7 +80,11 @@ export function createTestNotebook(
|
|
|
74
80
|
}
|
|
75
81
|
return reply.promise;
|
|
76
82
|
});
|
|
77
|
-
const notebook = {
|
|
83
|
+
const notebook = {
|
|
84
|
+
documentAtom: source,
|
|
85
|
+
backendAtom: atom(fakeBackend({ edit })),
|
|
86
|
+
edit,
|
|
87
|
+
};
|
|
78
88
|
return {
|
|
79
89
|
notebook,
|
|
80
90
|
control: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import type { Document } from "../host
|
|
3
|
+
import type { Document } from "../host";
|
|
4
4
|
import * as Protocol from "@marimo-team/client/protocol";
|
|
5
5
|
|
|
6
6
|
/** The fields a client edits. Null code means the host withheld source. */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
/** The editable document, its drafts, and its synchronization with the host. */
|
|
4
|
+
|
|
3
5
|
export {
|
|
4
6
|
DocumentProvider,
|
|
5
7
|
useCellCode,
|
|
@@ -10,13 +12,4 @@ export {
|
|
|
10
12
|
useDocumentEdit,
|
|
11
13
|
useDocumentSync,
|
|
12
14
|
} from "./hooks";
|
|
13
|
-
export { createDocumentModel } from "./model";
|
|
14
|
-
export type {
|
|
15
|
-
DocumentEdit,
|
|
16
|
-
DocumentEditor,
|
|
17
|
-
DocumentHost,
|
|
18
|
-
DocumentModel,
|
|
19
|
-
DocumentSyncStatus,
|
|
20
|
-
EditableCell,
|
|
21
|
-
EditableDocument,
|
|
22
|
-
} from "./model";
|
|
15
|
+
export { createDocumentModel, type DocumentModel } from "./model";
|
|
@@ -2,16 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import type * as Protocol from "@marimo-team/client/protocol";
|
|
4
4
|
import type { Atom, createStore } from "jotai/vanilla";
|
|
5
|
+
import type { Document, NotebookBackend } from "../host";
|
|
5
6
|
import { createDocumentState, type DocumentState } from "./state";
|
|
6
|
-
import {
|
|
7
|
-
createDocumentSync,
|
|
8
|
-
type DocumentHost,
|
|
9
|
-
type DocumentSyncStatus,
|
|
10
|
-
} from "./sync";
|
|
7
|
+
import { createDocumentSync, type DocumentSyncStatus } from "./sync";
|
|
11
8
|
|
|
12
9
|
export type { DocumentEdit, DocumentEditor } from "./state";
|
|
13
10
|
export type { EditableCell, EditableDocument } from "./draft";
|
|
14
|
-
export type {
|
|
11
|
+
export type { DocumentSyncStatus } from "./sync";
|
|
15
12
|
|
|
16
13
|
/** The editable document and its synchronization with the host. */
|
|
17
14
|
export interface DocumentModel extends Pick<
|
|
@@ -28,20 +25,26 @@ export interface DocumentModel extends Pick<
|
|
|
28
25
|
/** Create once per editing session; abort to stop editing and synchronization together. */
|
|
29
26
|
export function createDocumentModel({
|
|
30
27
|
store,
|
|
31
|
-
|
|
28
|
+
documentAtom,
|
|
29
|
+
backendAtom,
|
|
32
30
|
signal,
|
|
33
31
|
}: {
|
|
34
32
|
store: ReturnType<typeof createStore>;
|
|
35
|
-
|
|
33
|
+
/** The document the host last confirmed. */
|
|
34
|
+
documentAtom: Atom<Document | null>;
|
|
35
|
+
/** The backend that confirmed it, whose commands reach its host. */
|
|
36
|
+
backendAtom: Atom<NotebookBackend | null>;
|
|
36
37
|
signal: AbortSignal;
|
|
37
38
|
}): DocumentModel {
|
|
38
39
|
signal.throwIfAborted();
|
|
39
|
-
const state = createDocumentState({
|
|
40
|
+
const state = createDocumentState({ store, source: documentAtom, signal });
|
|
41
|
+
const sync = createDocumentSync({
|
|
40
42
|
store,
|
|
41
|
-
|
|
43
|
+
documentAtom,
|
|
44
|
+
backendAtom,
|
|
45
|
+
state,
|
|
42
46
|
signal,
|
|
43
47
|
});
|
|
44
|
-
const sync = createDocumentSync({ store, notebook, state, signal });
|
|
45
48
|
return {
|
|
46
49
|
documentAtom: state.documentAtom,
|
|
47
50
|
editAtom: state.editAtom,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type createStore,
|
|
7
7
|
type WritableAtom,
|
|
8
8
|
} from "jotai/vanilla";
|
|
9
|
-
import type { Document } from "../host
|
|
9
|
+
import type { Document } from "../host";
|
|
10
10
|
import * as Protocol from "@marimo-team/client/protocol";
|
|
11
11
|
import {
|
|
12
12
|
acknowledge,
|
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { idempotencyKey, type Result } from "@marimo-team/client";
|
|
4
4
|
import type * as Protocol from "@marimo-team/client/protocol";
|
|
5
|
-
import { atom, type createStore } from "jotai/vanilla";
|
|
6
|
-
import type {
|
|
5
|
+
import { type Atom, atom, type createStore } from "jotai/vanilla";
|
|
6
|
+
import type { Document, NotebookBackend } from "../host";
|
|
7
7
|
import type { DocumentState, DocumentSubmission } from "./state";
|
|
8
8
|
|
|
9
|
-
/** The notebook state and operation needed by document editing. */
|
|
10
|
-
export type DocumentHost = Pick<NotebookClient, "documentAtom" | "edit">;
|
|
11
|
-
|
|
12
9
|
export type DocumentSyncStatus =
|
|
13
10
|
| {
|
|
14
11
|
readonly status:
|
|
@@ -27,6 +24,8 @@ export type DocumentSyncStatus =
|
|
|
27
24
|
|
|
28
25
|
interface Submission {
|
|
29
26
|
readonly submission: DocumentSubmission;
|
|
27
|
+
/** The backend that confirmed the submission's base. */
|
|
28
|
+
readonly backend: NotebookBackend;
|
|
30
29
|
readonly idempotencyKey: string;
|
|
31
30
|
readonly startedAt: number;
|
|
32
31
|
readonly controller: AbortController;
|
|
@@ -74,20 +73,22 @@ function delay(ms: number, signal: AbortSignal): Promise<void> {
|
|
|
74
73
|
/** Own submission scheduling; follow the existing source without opening a stream. */
|
|
75
74
|
export function createDocumentSync({
|
|
76
75
|
store,
|
|
77
|
-
|
|
76
|
+
documentAtom: source,
|
|
77
|
+
backendAtom,
|
|
78
78
|
state,
|
|
79
79
|
signal,
|
|
80
80
|
}: {
|
|
81
81
|
store: ReturnType<typeof createStore>;
|
|
82
|
-
|
|
82
|
+
documentAtom: Atom<Document | null>;
|
|
83
|
+
backendAtom: Atom<NotebookBackend | null>;
|
|
83
84
|
state: DocumentState;
|
|
84
85
|
signal: AbortSignal;
|
|
85
86
|
}) {
|
|
86
87
|
signal.throwIfAborted();
|
|
87
|
-
const source = notebook.documentAtom;
|
|
88
88
|
const statusAtom = atom<DocumentSyncStatus>({ status: "idle" });
|
|
89
89
|
const observed = atom((get) => ({
|
|
90
90
|
document: get(source),
|
|
91
|
+
backend: get(backendAtom),
|
|
91
92
|
pending: get(state.pendingAtom),
|
|
92
93
|
changes: get(state.changesAtom),
|
|
93
94
|
}));
|
|
@@ -117,8 +118,9 @@ export function createDocumentSync({
|
|
|
117
118
|
const current = store.get(source);
|
|
118
119
|
if (
|
|
119
120
|
active &&
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
(store.get(backendAtom) !== active.backend ||
|
|
122
|
+
(current &&
|
|
123
|
+
current.revision.notebook_id !== active.submission.notebookId))
|
|
122
124
|
) {
|
|
123
125
|
active.controller.abort(
|
|
124
126
|
new Error("The notebook changed during synchronization"),
|
|
@@ -129,7 +131,7 @@ export function createDocumentSync({
|
|
|
129
131
|
}
|
|
130
132
|
clearTimer();
|
|
131
133
|
const changes = store.get(state.changesAtom);
|
|
132
|
-
if (changes === null) {
|
|
134
|
+
if (changes === null || store.get(backendAtom) === null) {
|
|
133
135
|
store.set(statusAtom, { status: "unavailable" });
|
|
134
136
|
} else if (changes.length === 0) {
|
|
135
137
|
// Release unsent reversals instead of keeping an invisible override.
|
|
@@ -170,14 +172,12 @@ export function createDocumentSync({
|
|
|
170
172
|
signal.throwIfAborted();
|
|
171
173
|
request.controller.signal.throwIfAborted();
|
|
172
174
|
result = await abortable(
|
|
173
|
-
|
|
174
|
-
request.submission.notebookId
|
|
175
|
-
request.submission.changes,
|
|
176
|
-
{
|
|
175
|
+
request.backend
|
|
176
|
+
.notebook(request.submission.notebookId)
|
|
177
|
+
.edit(request.submission.changes, {
|
|
177
178
|
idempotencyKey: request.idempotencyKey,
|
|
178
179
|
signal: controller.signal,
|
|
179
|
-
},
|
|
180
|
-
),
|
|
180
|
+
}),
|
|
181
181
|
controller.signal,
|
|
182
182
|
);
|
|
183
183
|
} catch (error) {
|
|
@@ -248,7 +248,12 @@ export function createDocumentSync({
|
|
|
248
248
|
throw new Error("The notebook changed during synchronization");
|
|
249
249
|
}
|
|
250
250
|
if (active === null) {
|
|
251
|
-
|
|
251
|
+
const backend = store.get(backendAtom);
|
|
252
|
+
if (
|
|
253
|
+
current === null ||
|
|
254
|
+
backend === null ||
|
|
255
|
+
store.get(state.changesAtom) === null
|
|
256
|
+
) {
|
|
252
257
|
throw unavailable();
|
|
253
258
|
}
|
|
254
259
|
const submission = store.set(state.submitAtom);
|
|
@@ -257,6 +262,7 @@ export function createDocumentSync({
|
|
|
257
262
|
}
|
|
258
263
|
active = {
|
|
259
264
|
submission,
|
|
265
|
+
backend,
|
|
260
266
|
idempotencyKey: idempotencyKey(),
|
|
261
267
|
startedAt: Date.now(),
|
|
262
268
|
controller: new AbortController(),
|