@marimo-team/frontend 0.25.3-dev13 → 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-DC6FFQOg.js +1 -0
- package/dist/assets/{ConnectedDataExplorerComponent-TprnxZeJ.js → ConnectedDataExplorerComponent-BV1mVpaE.js} +1 -1
- package/dist/assets/{ErrorBoundary-CM13wrd8.js → ErrorBoundary-Ce-Ujlwj.js} +2 -2
- package/dist/assets/JsonOutput-CfEBzaqW.js +11 -0
- package/dist/assets/{LazyAnyLanguageCodeMirror-B4fCknq1.js → LazyAnyLanguageCodeMirror-DoxWX_Wp.js} +2 -2
- package/dist/assets/MarimoErrorOutput-Bh6o9zle.js +10 -0
- package/dist/assets/{RenderHTML-DX6i99Op.js → RenderHTML-COGZsXQV.js} +1 -1
- package/dist/assets/RunButton-DQ51plUv.js +1 -0
- package/dist/assets/__vite-browser-external-BynxqS4w.js +1 -0
- package/dist/assets/__vite-browser-external-DTOT9NDN.js +1 -0
- package/dist/assets/{add-cell-with-ai-BpD_790A.js → add-cell-with-ai-D7gtGUsg.js} +6 -6
- package/dist/assets/{add-connection-dialog-B1BXCjex.js → add-connection-dialog-CpMCc97S.js} +1 -1
- package/dist/assets/{agent-panel-CDFUc1uu.js → agent-panel-BSFYqJ9B.js} +3 -3
- package/dist/assets/ai-model-dropdown-D575Vid8.js +13 -0
- package/dist/assets/alert-dialog-8YiYLrfr.js +1 -0
- package/dist/assets/any-language-editor-BWmz0P_C.js +1 -0
- package/dist/assets/{app-config-button-AdV-QDD7.js → app-config-button-Cfcg_5Bz.js} +1 -1
- package/dist/assets/arrays-0R2reF2y.js +1 -0
- package/dist/assets/{blob-BG8yoQmx.js → blob-CszMu60N.js} +1 -1
- package/dist/assets/{button-COI-NGrx.js → button-Cl7haVNh.js} +1 -1
- package/dist/assets/{cache-panel-DTY1dqvi.js → cache-panel-DuBsA6oO.js} +1 -1
- package/dist/assets/{capabilities-BJvf-MD4.js → capabilities-DcB-Dd0T.js} +1 -1
- package/dist/assets/cell-editor-BLpOlLnM.js +42 -0
- package/dist/assets/cell-link-Cj0gGaMV.js +1 -0
- package/dist/assets/cells-SvbLY10y.js +220 -0
- package/dist/assets/{chat-display-DhgI2MS9.js → chat-display-D43_g3TX.js} +1 -1
- package/dist/assets/chat-panel-B_HAJMwx.js +4 -0
- package/dist/assets/chat-ui-BEcYCphu.js +4 -0
- package/dist/assets/circle-B0vOU-Dh.js +1 -0
- package/dist/assets/{code-visibility-BJImdpDi.js → code-visibility-D1Wf7-vD.js} +1 -1
- package/dist/assets/column-preview-CrzaJKk8.js +1 -0
- package/dist/assets/command-palette-Bm9yIVcD.js +1 -0
- package/dist/assets/{common-Sl3uthjg.js → common-BJyUxCzv.js} +1 -1
- package/dist/assets/{components-CskjK6xZ.js → components-9p_w2C6e.js} +1 -1
- package/dist/assets/components-B6gDDccL.js +1 -0
- package/dist/assets/{components-3cou6Mka.js → components-D1f_Cz4i.js} +1 -1
- package/dist/assets/config-7IjqNuVp.js +1 -0
- package/dist/assets/config-BC32AAdw.js +1 -0
- package/dist/assets/{config-CA1V-wjh.js → config-CXnC3MRv.js} +1 -1
- package/dist/assets/{copy-D8O_CYCG.js → copy-74EjuZso.js} +1 -1
- package/dist/assets/{copy-icon-BIiRXh_3.js → copy-icon-Craaocbu.js} +1 -1
- package/dist/assets/{createReducer-DgRODTW6.js → createReducer-B04CIO-e.js} +1 -1
- package/dist/assets/{datasource-DfbNb83e.js → datasource-C8KSVfSD.js} +1 -1
- package/dist/assets/{dates-CfmdTL6B.js → dates-CaPZVGtm.js} +1 -1
- package/dist/assets/{dependency-graph-panel-L5cBE5sl.js → dependency-graph-panel-BhmyJbUl.js} +3 -3
- package/dist/assets/dist-gaK_BThd.js +1 -0
- package/dist/assets/{documentation-panel-D7S8JIc2.js → documentation-panel-DcVG0Q0d.js} +1 -1
- package/dist/assets/download-Bx52VRE4.js +9 -0
- package/dist/assets/dropdown-menu-Cwi17BoX.js +1 -0
- package/dist/assets/edit-page-DUrwOHR-.js +9 -0
- package/dist/assets/{error-banner-COZ7SfYV.js → error-banner-m3mastc-.js} +1 -1
- package/dist/assets/{error-entries-CLnPDv7b.js → error-entries-11OuHzUF.js} +1 -1
- package/dist/assets/{error-panel-D2xrYLue.js → error-panel-XJy5S25F.js} +1 -1
- package/dist/assets/{es-Cqst59WX.js → es-Rg27gAEy.js} +1 -1
- package/dist/assets/{feedback-button-DzPunIqU.js → feedback-button-B7jh_B4w.js} +1 -1
- package/dist/assets/feedback-button-C-eKtxXE.js +1 -0
- package/dist/assets/file-explorer-panel-CnVxbF4A.js +1 -0
- package/dist/assets/{file-name-input-B4fRaQkI.js → file-name-input-BJuqjQqH.js} +2 -2
- package/dist/assets/floating-outline-8yyZ-MOJ.js +1 -0
- package/dist/assets/{formats-yZ06h1Pl.js → formats-CYyvbiZA.js} +1 -1
- package/dist/assets/{formatting-BqrP-n49.js → formatting-DW2u7HXI.js} +1 -1
- package/dist/assets/fullscreen-D-ldV-19.js +41 -0
- package/dist/assets/{gallery-page-BefvdsM7.js → gallery-page-DJvGHePf.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-2n7Ig5Jm.js → ganttDiagram-EL5Y4UJY-CYqkCfFG.js} +1 -1
- package/dist/assets/{glide-data-editor-C3eaYlXg.js → glide-data-editor-Cyc8DHZx.js} +2 -2
- package/dist/assets/{glide-utils-CIrAxdjv.js → glide-utils-DsTeJ7WA.js} +2 -2
- package/dist/assets/home-page-tRZJmSU6.js +1 -0
- package/dist/assets/{hooks-DzML9nez.js → hooks-CxGwybGw.js} +1 -1
- package/dist/assets/{html-to-image-DK9Zl1lU.js → html-to-image-BW0fcUS8.js} +1 -1
- package/dist/assets/index-CoxbqhGH.js +30 -0
- package/dist/assets/index-u08m0hQE.css +2 -0
- package/dist/assets/{input-BoBxR50a.js → input-DLA5Dhn3.js} +2 -2
- package/dist/assets/kiosk-mode-Cbxffiz9.js +1 -0
- package/dist/assets/{layout-Ck1RoL1B.js → layout-DvXJxBiL.js} +4 -4
- package/dist/assets/linear-Bma8J3iM.js +1 -0
- package/dist/assets/links-UwsB2RA1.js +1 -0
- package/dist/assets/{logs-panel-BRytaWaO.js → logs-panel-KtsY7TrR.js} +1 -1
- package/dist/assets/{markdown-renderer-DX0Xrrfy.js → markdown-renderer-DUgV7_fm.js} +1 -1
- package/dist/assets/menu-items-yD7IV3V4.js +1 -0
- package/dist/assets/{mermaid-C4EiVqcd.js → mermaid-DmekSy8J.js} +2 -2
- package/dist/assets/{name-cell-input-DlpTvPLa.js → name-cell-input-C3-Cj-Ek.js} +1 -1
- package/dist/assets/numbers-DJK15_4S.js +1 -0
- package/dist/assets/{objects-snlMn4yk.js → objects-rLPlQ3X_.js} +1 -1
- package/dist/assets/{once-Haz_uxEL.js → once-6KNTRW0u.js} +1 -1
- package/dist/assets/{outline-panel-B2WIpz-1.js → outline-panel-BzyY4JA6.js} +1 -1
- package/dist/assets/{package-data-dv4FBHxR.js → package-data-CKPmBDtg.js} +1 -1
- package/dist/assets/packages-panel-DxosbWko.js +1 -0
- package/dist/assets/{panel-accordion-state-BqSH5ttk.js → panel-accordion-state-BRbTib2B.js} +1 -1
- package/dist/assets/panels-Da3071Q4.js +1 -0
- package/dist/assets/precisionRound-BxlUs8G4.js +1 -0
- package/dist/assets/process-output-CSwx7rvo.js +1 -0
- package/dist/assets/{quadrantDiagram-AXDQQJYC-yBhCDI7E.js → quadrantDiagram-AXDQQJYC-D9KH5Zge.js} +1 -1
- package/dist/assets/react-vega-BTNeboR2.js +189 -0
- package/dist/assets/{readonly-code-display-MnMEoYW5.js → readonly-code-display-B9y9423Y.js} +1 -1
- package/dist/assets/{readonly-python-code-ggs4hAzC.js → readonly-python-code-Dgw6trlk.js} +1 -1
- package/dist/assets/{renderShortcut-C1JwYcqW.js → renderShortcut-Bxv9ZLnD.js} +1 -1
- package/dist/assets/{requests-lt0JK8Jt.js → requests-BIFR-h4M.js} +1 -1
- package/dist/assets/requests-DuLJN9Tf.js +1 -0
- package/dist/assets/{reveal-component-PiV1eQk9.js → reveal-component-B9A4wkG-.js} +37 -37
- package/dist/assets/run-page-C1MQ5b_A.js +1 -0
- package/dist/assets/{runs-BLp5jezv.js → runs-C8uOPNAW.js} +1 -1
- package/dist/assets/{runtime-details-state-CoL-p0MC.js → runtime-details-state-D_hOQL9T.js} +1 -1
- package/dist/assets/{save-worker-BbUSwZRG.js → save-worker-UwdGcNK5.js} +2 -1
- package/dist/assets/scratchpad-panel-CBCsPmMW.js +1 -0
- package/dist/assets/{scroll-to-bottom-button-1FyowpWV.js → scroll-to-bottom-button-CidjyqiH.js} +1 -1
- package/dist/assets/secrets-panel-ConQpchl.js +1 -0
- package/dist/assets/select-CgWQvW1F.js +1 -0
- package/dist/assets/{session-DqoWmb7X.js → session-BHgGkjDm.js} +1 -1
- package/dist/assets/session-panel-CiHCKPlx.js +1 -0
- package/dist/assets/snippets-panel-C0VDE_4i.js +1 -0
- package/dist/assets/{spec-BfSBcO5B.js → spec-DFtwOyXT.js} +1 -1
- package/dist/assets/state-BYETGwDZ.js +1 -0
- package/dist/assets/{state-uwRvJLb8.js → state-CZu2E2Lb.js} +1 -1
- package/dist/assets/{state-C3oDoyFe.js → state-ClBFtZxn.js} +1 -1
- package/dist/assets/{state-DxQnC_1I.js → state-CrLrURYG.js} +1 -1
- package/dist/assets/{state-DtGB7xeJ.js → state-DrraMmqz.js} +1 -1
- package/dist/assets/state-DvU2AiFx.js +107 -0
- package/dist/assets/{strings-B2vv-ENv.js → strings-3lNX212T.js} +1 -1
- package/dist/assets/{swiper-component-a07domrZ.js → swiper-component-BwGv-Pu1.js} +1 -1
- package/dist/assets/switch-D2gdC_ep.js +1 -0
- package/dist/assets/tabs-BLczoY3k.js +1 -0
- package/dist/assets/{terminal-wnSHYT8N.js → terminal-DLgGyFIP.js} +1 -1
- package/dist/assets/{time-MgToa8VM.js → time-DswSXBDk.js} +1 -1
- package/dist/assets/toast-trUlkvx3.js +1 -0
- package/dist/assets/tooltip-B5lAA_jn.js +1 -0
- package/dist/assets/tooltip-BiQdpXA0.js +61 -0
- package/dist/assets/tracing-C5d51Nmi.js +1 -0
- package/dist/assets/tracing-panel-B89R8dWE.js +2 -0
- package/dist/assets/{tree-actions-DnUokmva.js → tree-actions-Cph8FVzh.js} +1 -1
- package/dist/assets/use-restore-focus-DMSzeuUr.js +1 -0
- package/dist/assets/{useAddCell-IN24thff.js → useAddCell-VPkB72pn.js} +1 -1
- package/dist/assets/useAsyncData-qvoPYHtx.js +1 -0
- package/dist/assets/useBoolean-BkeUcn1n.js +1 -0
- package/dist/assets/useCellActionButton--zI3GFnt.js +1 -0
- package/dist/assets/useDebounce-DwTQ_1Pu.js +1 -0
- package/dist/assets/{useDeepCompareMemoize-DFIVN9dn.js → useDeepCompareMemoize-B2DVK2De.js} +1 -1
- package/dist/assets/useDelayElapsed-BlsDT4TV.js +1 -0
- package/dist/assets/{useDeleteCell-fQlM46Y2.js → useDeleteCell-CjewKeSr.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-ImXYINm_.js → useDependencyPanelTab-Bt_ka-wl.js} +1 -1
- package/dist/assets/{useEvent-Dfcbgjhg.js → useEvent-BDjgaBtz.js} +1 -1
- package/dist/assets/{useEventListener-D9cftcHj.js → useEventListener-DPOTkksd.js} +1 -1
- package/dist/assets/{useIframeCapabilities-CwTkczVN.js → useIframeCapabilities-sHeZt_EA.js} +1 -1
- package/dist/assets/{useInstallPackage-BW06eter.js → useInstallPackage-BfNKN33w.js} +1 -1
- package/dist/assets/{useInterval--x3s1VpI.js → useInterval-BcdwHHB5.js} +1 -1
- package/dist/assets/useNotebookActions-DV2S6KaG.js +1 -0
- package/dist/assets/{useRunCells-DSJPXE-j.js → useRunCells-BupCZpzk.js} +1 -1
- package/dist/assets/{useSplitCell-DQyMF5gd.js → useSplitCell-CaLIR0TS.js} +1 -1
- package/dist/assets/useStateForKey-CGMJQT8x.js +1 -0
- package/dist/assets/{useTheme-n8Dj1zH7.js → useTheme-C2dnh2vM.js} +1 -1
- package/dist/assets/{utils-CaQYvazl.js → utils-DfElfBpz.js} +1 -1
- package/dist/assets/vega-component-DJf6ho22.js +1 -0
- package/dist/assets/{vega-embed-container-DJPNkHPb.js → vega-embed-container-DtArSJz1.js} +1 -1
- package/dist/assets/vega-loader.browser-Bu25TKax.js +6 -0
- package/dist/assets/worker-jm2VC7pS.js +159 -0
- package/dist/assets/{write-secret-modal-DzKAInL2.js → write-secret-modal-CBi2vNK_.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-KbGzG6-u.js → xychartDiagram-S5SC5T6Z-CeRnBS5e.js} +1 -1
- package/dist/index.html +81 -79
- package/package.json +14 -14
- package/src/__tests__/host-imports.test.ts +1 -0
- package/src/__tests__/module-entry.test.ts +32 -0
- package/src/__tests__/mount.test.ts +0 -20
- 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/app-config/ai-config.tsx +7 -2
- package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
- package/src/components/chat/acp/__tests__/session-tabs.test.tsx +43 -0
- package/src/components/chat/acp/__tests__/state.test.ts +2 -2
- package/src/components/chat/acp/blocks.tsx +3 -2
- package/src/components/chat/acp/session-tabs.tsx +8 -3
- package/src/components/chat/chat-components.tsx +2 -0
- package/src/components/chat/chat-panel.tsx +9 -8
- package/src/components/chat/chat-utils.ts +4 -8
- package/src/components/data-table/TableTopBar.tsx +2 -0
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +195 -195
- package/src/components/data-table/__tests__/column-explorer.test.tsx +27 -1
- package/src/components/data-table/__tests__/columns.test.tsx +105 -0
- package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
- package/src/components/data-table/__tests__/data-table.test.tsx +1 -0
- package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
- package/src/components/data-table/__tests__/export-actions.test.tsx +105 -0
- package/src/components/data-table/__tests__/keyboard-selection.test.tsx +188 -0
- package/src/components/data-table/__tests__/search-highlight.test.tsx +109 -0
- package/src/components/data-table/__tests__/url-detector.test.tsx +74 -0
- package/src/components/data-table/cell-selection/feature.ts +2 -2
- package/src/components/data-table/cell-selection/types.ts +2 -2
- package/src/components/data-table/charts/__tests__/chart-panel.test.tsx +74 -1
- package/src/components/data-table/charts/__tests__/spec-snapshot.test.ts +20 -20
- package/src/components/data-table/charts/__tests__/spec.test.ts +78 -52
- package/src/components/data-table/charts/__tests__/storage.test.ts +4 -1
- package/src/components/data-table/charts/chart-spec/spec.ts +68 -43
- package/src/components/data-table/charts/charts.tsx +40 -30
- package/src/components/data-table/charts/storage.ts +16 -5
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +1 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +23 -16
- package/src/components/data-table/column-summary/column-summary.tsx +2 -4
- package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
- package/src/components/data-table/columns.tsx +45 -46
- package/src/components/data-table/context-menu.tsx +36 -20
- package/src/components/data-table/data-table.tsx +13 -1
- package/src/components/data-table/date-popover.tsx +6 -4
- package/src/components/data-table/export-actions.tsx +76 -48
- 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 +4 -4
- package/src/components/data-table/search-highlight.tsx +79 -0
- package/src/components/data-table/types.ts +1 -0
- package/src/components/data-table/url-detector.tsx +49 -9
- package/src/components/datasources/__tests__/column-actions.test.tsx +97 -0
- package/src/components/datasources/__tests__/column-preview.test.tsx +63 -2
- package/src/components/datasources/__tests__/table-list.test.tsx +118 -0
- package/src/components/datasources/column-preview.tsx +8 -2
- package/src/components/datasources/datasources.tsx +48 -35
- package/src/components/debugger/debugger-code.tsx +5 -0
- package/src/components/dependency-graph/dependency-graph-tree.tsx +8 -8
- package/src/components/dependency-graph/elements.ts +24 -21
- package/src/components/dependency-graph/panels.tsx +2 -1
- package/src/components/dependency-graph/utils/__tests__/layout.test.ts +18 -11
- package/src/components/editor/SortableCell.tsx +1 -3
- package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
- package/src/components/editor/__tests__/package-alert.test.tsx +22 -1
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +24 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +18 -15
- package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
- package/src/components/editor/ai/ai-completion-editor.tsx +1 -0
- package/src/components/editor/alerts/startup-logs-alert.tsx +1 -0
- package/src/components/editor/cell/CreateCellButton.tsx +8 -10
- package/src/components/editor/cell/DeleteButton.tsx +1 -0
- package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
- package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
- package/src/components/editor/cell/code/cell-editor.tsx +1 -1
- package/src/components/editor/cell/code/language-toggle.tsx +8 -7
- package/src/components/editor/cell/collapse.tsx +5 -3
- package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
- package/src/components/editor/chrome/panels/cache-panel.tsx +1 -0
- 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/packages-panel.tsx +4 -7
- 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 +12 -18
- package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +58 -1
- package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +1 -7
- package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +12 -2
- package/src/components/editor/chrome/wrapper/storage.ts +1 -1
- package/src/components/editor/connections/components.tsx +1 -1
- package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
- package/src/components/editor/controls/__tests__/keyboard-shortcuts.test.tsx +61 -0
- package/src/components/editor/controls/command-palette.tsx +22 -10
- package/src/components/editor/controls/keyboard-shortcuts.tsx +14 -5
- package/src/components/editor/dynamic-favicon.tsx +21 -40
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +12 -23
- package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
- package/src/components/editor/file-tree/__tests__/file-explorer-navigation.test.tsx +47 -0
- package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
- package/src/components/editor/file-tree/file-explorer.tsx +45 -8
- package/src/components/editor/file-tree/file-name-input.tsx +1 -0
- package/src/components/editor/file-tree/file-viewer.tsx +4 -2
- package/src/components/editor/file-tree/use-file-search.ts +35 -21
- 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-banner.tsx +1 -0
- package/src/components/editor/notebook-cell.tsx +11 -2
- package/src/components/editor/output/MarimoTracebackOutput.tsx +19 -17
- package/src/components/editor/output/__tests__/traceback.test.tsx +27 -1
- package/src/components/editor/output/ansi-reduce.ts +33 -20
- package/src/components/editor/output/console/ConsoleOutput.tsx +19 -25
- package/src/components/editor/package-alert.tsx +14 -7
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +12 -6
- package/src/components/editor/renderers/vertical-layout/sidebar/toggle.tsx +7 -1
- package/src/components/home/components.tsx +17 -31
- package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
- package/src/components/icons/large-spinner.tsx +3 -14
- package/src/components/icons/spinner.tsx +1 -1
- package/src/components/mcp/mcp-status-indicator.tsx +1 -1
- package/src/components/pages/__tests__/home-page.test.tsx +116 -0
- package/src/components/pages/home-page.tsx +164 -91
- package/src/components/scratchpad/scratchpad.test.tsx +95 -0
- package/src/components/scratchpad/scratchpad.tsx +16 -3
- package/src/components/slides/reveal-component.tsx +4 -2
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slide-form.tsx +1 -0
- package/src/components/tracing/tracing-keyboard.test.tsx +85 -0
- package/src/components/tracing/tracing.tsx +45 -41
- package/src/components/ui/__tests__/draggable-popover.test.tsx +24 -0
- package/src/components/ui/alert-dialog.tsx +1 -1
- package/src/components/ui/draggable-popover.tsx +24 -2
- package/src/core/ai/context/providers/datasource.ts +6 -2
- package/src/core/ai/tools/base.ts +16 -7
- package/src/core/ai/tools/edit-notebook-tool.ts +47 -45
- package/src/core/ai/tools/registry.ts +13 -8
- package/src/core/ai/tools/sample-tool.ts +6 -7
- package/src/core/attachments/state.ts +5 -8
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +4 -0
- package/src/core/cells/__tests__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/cells.test.ts +3 -1
- package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +54 -0
- package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
- package/src/core/cells/__tests__/session.test.ts +117 -36
- package/src/core/cells/cell.ts +14 -3
- package/src/core/cells/cells.ts +3 -1
- package/src/core/cells/pending-delete-service.ts +2 -6
- package/src/core/cells/session.ts +1 -1
- package/src/core/cells/types.ts +10 -1
- package/src/core/codemirror/__tests__/setup.test.ts +29 -29
- package/src/core/codemirror/cells/__tests__/extensions.test.ts +15 -0
- package/src/core/codemirror/cells/extensions.ts +7 -3
- package/src/core/codemirror/keymaps/vim.ts +2 -6
- package/src/core/codemirror/language/__tests__/sql.test.ts +22 -20
- package/src/core/codemirror/language/extension.ts +8 -8
- package/src/core/codemirror/language/languages/markdown.ts +8 -6
- package/src/core/codemirror/language/languages/python.ts +16 -10
- package/src/core/codemirror/language/languages/sql/sql.ts +9 -7
- package/src/core/codemirror/language/panel/panel.tsx +1 -0
- package/src/core/codemirror/language/types.ts +7 -7
- package/src/core/codemirror/rtc/extension.ts +24 -12
- package/src/core/codemirror/rtc/loro/awareness.ts +34 -19
- package/src/core/codemirror/rtc/loro/sync.ts +1 -0
- package/src/core/codemirror/utils.ts +1 -0
- package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
- package/src/core/datasets/data-source-discovery.ts +4 -6
- package/src/core/document/__tests__/codemirror.test.ts +485 -0
- package/src/core/document/__tests__/draft.test.ts +322 -0
- package/src/core/document/__tests__/fixtures.ts +92 -0
- package/src/core/document/__tests__/hooks.test.tsx +386 -0
- package/src/core/document/__tests__/lifecycle-scenarios.test.ts +61 -0
- package/src/core/document/__tests__/model.test.ts +191 -0
- package/src/core/document/__tests__/protocol-fixtures.ts +38 -0
- package/src/core/document/__tests__/recovery-scenarios.test.ts +230 -0
- package/src/core/document/__tests__/scenario-fixture.ts +43 -0
- package/src/core/document/__tests__/scenarios.test.ts +205 -0
- package/src/core/document/__tests__/state.test.ts +438 -0
- package/src/core/document/__tests__/submission.test.ts +203 -0
- package/src/core/document/__tests__/test-host.test.ts +92 -0
- package/src/core/document/__tests__/test-host.ts +247 -0
- package/src/core/document/__tests__/test-notebook.ts +149 -0
- package/src/core/document/__tests__/user.ts +61 -0
- package/src/core/document/codemirror.ts +311 -0
- package/src/core/document/draft.ts +472 -0
- package/src/core/document/hooks.tsx +118 -0
- package/src/core/document/index.ts +15 -0
- package/src/core/document/model.ts +55 -0
- package/src/core/document/select.ts +41 -0
- package/src/core/document/state.ts +374 -0
- package/src/core/document/sync.ts +369 -0
- package/src/core/host/AGENTS.md +8 -2
- package/src/core/host/__tests__/attachments.test.ts +56 -0
- package/src/core/host/__tests__/document.test.ts +126 -0
- package/src/core/host/__tests__/fixtures.ts +71 -0
- package/src/core/host/__tests__/replica.test.ts +550 -0
- package/src/core/host/__tests__/static.test.ts +169 -0
- package/src/core/host/__tests__/wasm.test.ts +162 -0
- package/src/core/host/backend.ts +19 -0
- package/src/core/host/connection.ts +1 -40
- package/src/core/host/index.ts +14 -0
- package/src/core/host/replica.ts +286 -0
- package/src/core/host/resources/attachments.ts +22 -0
- package/src/core/host/resources/document.ts +101 -0
- package/src/core/host/static.ts +72 -0
- package/src/core/host/wasm.ts +102 -0
- package/src/core/hotkeys/actions.ts +2 -4
- package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
- package/src/core/islands/bootstrap.ts +0 -1
- package/src/core/islands/components/__tests__/IslandControls.test.tsx +7 -5
- package/src/core/kernel/__tests__/RuntimeState.test.ts +1 -1
- package/src/core/network/connection.ts +4 -2
- package/src/core/network/useConnectionNotice.ts +8 -2
- package/src/core/runtime/__tests__/createWsUrl.test.ts +77 -7
- package/src/core/runtime/__tests__/runtime.test.ts +32 -58
- package/src/core/runtime/runtime.ts +38 -42
- package/src/core/runtime/types.ts +0 -2
- package/src/core/state/__tests__/jotai.test.tsx +108 -0
- package/src/core/state/jotai.ts +1 -1
- package/src/core/static/__tests__/files.test.ts +38 -10
- package/src/core/static/files.ts +2 -0
- package/src/core/wasm/bridge.ts +38 -10
- package/src/core/wasm/store.ts +3 -3
- package/src/core/wasm/worker/bootstrap.ts +2 -1
- package/src/core/wasm/worker/getMarimoWheel.ts +1 -1
- package/src/core/wasm/worker/types.ts +11 -0
- package/src/core/wasm/worker/worker.ts +48 -0
- package/src/core/websocket/__tests__/backend.test.ts +200 -0
- package/src/core/websocket/__tests__/connection-state.test.ts +0 -12
- package/src/core/websocket/__tests__/useNotebookReplica.hook.test.tsx +141 -0
- package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +154 -428
- package/src/core/websocket/backend.ts +106 -0
- package/src/core/websocket/connection-state.ts +1 -11
- package/src/core/websocket/state.ts +7 -0
- package/src/core/websocket/transports/__tests__/notebook.test.ts +445 -0
- package/src/core/websocket/transports/notebook.ts +190 -0
- package/src/core/websocket/types.ts +2 -2
- package/src/core/websocket/useNotebookReplica.ts +69 -0
- package/src/core/websocket/useNotebookSession.tsx +10 -22
- package/src/css/globals.css +5 -0
- package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
- package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
- package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
- package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
- package/src/hooks/__tests__/useScript.test.tsx +77 -0
- package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
- package/src/hooks/useAsyncData.ts +3 -3
- package/src/hooks/useDataSourceDiscovery.ts +6 -4
- package/src/hooks/useDebounce.ts +6 -10
- package/src/hooks/useDeepCompareMemoize.ts +1 -1
- package/src/hooks/useDelayElapsed.ts +14 -10
- package/src/hooks/useElapsedTime.ts +3 -3
- package/src/hooks/useEventListener.ts +1 -2
- package/src/hooks/useInputHistory.ts +4 -7
- package/src/hooks/useInternalStateWithSync.ts +6 -6
- package/src/hooks/useResizeObserver.ts +4 -7
- package/src/hooks/useScript.ts +67 -70
- package/src/hooks/useStateForKey.ts +93 -0
- package/src/mount.tsx +0 -4
- package/src/plugins/__tests__/plugin-object-schemas.test.ts +104 -0
- package/src/plugins/core/registerReactComponent.tsx +1 -1
- package/src/plugins/impl/CodeEditorPlugin.tsx +1 -1
- package/src/plugins/impl/DataEditorPlugin.tsx +1 -1
- package/src/plugins/impl/DataTablePlugin.tsx +29 -33
- package/src/plugins/impl/DropdownPlugin.tsx +2 -6
- package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/RangeSliderPlugin.tsx +0 -1
- package/src/plugins/impl/RefreshPlugin.tsx +1 -0
- package/src/plugins/impl/SliderPlugin.tsx +6 -8
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +117 -1
- package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
- package/src/plugins/impl/anywidget/runtime.ts +21 -9
- package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
- package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
- package/src/plugins/impl/chat/chat-ui.tsx +14 -11
- package/src/plugins/impl/code/any-language-editor.tsx +2 -2
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
- package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
- package/src/plugins/impl/data-explorer/ConnectedDataExplorerComponent.tsx +1 -1
- package/src/plugins/impl/data-explorer/components/column-summary.tsx +4 -2
- package/src/plugins/impl/data-explorer/state/reducer.ts +1 -1
- package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -2
- package/src/plugins/impl/data-frames/__tests__/sidebar.test.tsx +160 -0
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +66 -66
- package/src/plugins/impl/data-frames/panel.tsx +43 -16
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
- package/src/plugins/impl/panel/PanelPlugin.tsx +16 -17
- package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
- package/src/plugins/impl/panel/utils.ts +17 -0
- package/src/plugins/impl/plotly/PlotlyPlugin.tsx +2 -5
- package/src/plugins/impl/plotly/selection.ts +25 -19
- package/src/plugins/impl/vega/VegaPlugin.tsx +2 -3
- package/src/plugins/impl/vega/make-selectable.ts +4 -7
- package/src/plugins/layout/RoutesPlugin.tsx +1 -1
- package/src/stories/__tests__/editor-story.test.tsx +33 -0
- package/src/stories/button.stories.tsx +1 -0
- package/src/stories/editor.stories.tsx +24 -17
- package/src/stories/form-wrapper.stories.tsx +2 -2
- package/src/stories/select.stories.tsx +2 -2
- package/src/stories/switchable-multi-select.stories.tsx +1 -1
- package/src/utils/__tests__/arrays.test.ts +35 -0
- package/src/utils/__tests__/batch-requests.test.ts +3 -3
- package/src/utils/__tests__/numbers.test.ts +48 -1
- package/src/utils/__tests__/objects.test.ts +21 -0
- package/src/utils/arrays.ts +10 -0
- package/src/utils/blob.ts +1 -1
- package/src/utils/numbers.ts +28 -17
- package/src/utils/objects.ts +3 -7
- package/src/utils/once.ts +19 -19
- package/src/utils/tracer.ts +9 -5
- package/src/utils/zod-utils.ts +1 -3
- package/dist/assets/CellStatus-DVOHpdeJ.js +0 -1
- package/dist/assets/JsonOutput-Cx1iyibB.js +0 -11
- package/dist/assets/MarimoErrorOutput-s_5GOX8k.js +0 -10
- package/dist/assets/RunButton-BKoh0zYG.js +0 -1
- package/dist/assets/__vite-browser-external-CYO_P3S8.js +0 -1
- package/dist/assets/__vite-browser-external-FeDVGZU8.js +0 -1
- package/dist/assets/ai-model-dropdown-i1vfGeVi.js +0 -13
- package/dist/assets/alert-dialog-BaPGzeOK.js +0 -1
- package/dist/assets/any-language-editor-BdTWdMCI.js +0 -1
- package/dist/assets/arrays-BqUx5USx.js +0 -1
- package/dist/assets/cell-editor-BKJ06H5F.js +0 -21
- package/dist/assets/cell-link-8Auigi8P.js +0 -1
- package/dist/assets/cells-C6YGYGdi.js +0 -220
- package/dist/assets/chat-panel-Cau71Ygr.js +0 -4
- package/dist/assets/chat-ui-C5ql1Up-.js +0 -4
- package/dist/assets/circle-DXf-VWZG.js +0 -1
- package/dist/assets/circle-play-CYIb0qT6.js +0 -1
- package/dist/assets/column-preview-BZ_iNhtJ.js +0 -1
- package/dist/assets/command-palette-DDC3kBet.js +0 -1
- package/dist/assets/components-YBmtV0p6.js +0 -1
- package/dist/assets/config-DRIpOKqh.js +0 -1
- package/dist/assets/config-Dy28fDwn.js +0 -1
- package/dist/assets/dist-iaThVDkA.js +0 -1
- package/dist/assets/download-C03FOElq.js +0 -9
- package/dist/assets/dropdown-menu-C1rnl-0o.js +0 -1
- package/dist/assets/edit-page-D_Rg0gQO.js +0 -9
- package/dist/assets/feedback-button-Kla7i5RT.js +0 -1
- package/dist/assets/file-explorer-panel-Uk4_QIJQ.js +0 -1
- package/dist/assets/floating-outline-B5MaM1JS.js +0 -1
- package/dist/assets/fullscreen-B9tHcfpV.js +0 -41
- package/dist/assets/home-page-hmvKlc1w.js +0 -4
- package/dist/assets/index-6b4mf8Ai.css +0 -2
- package/dist/assets/index-ptK1gxmo.js +0 -30
- package/dist/assets/kiosk-mode-BZECfg9k.js +0 -1
- package/dist/assets/linear-0eij_aAG.js +0 -1
- package/dist/assets/link-NuFVR7G9.js +0 -1
- package/dist/assets/links-DkkL9DVa.js +0 -1
- package/dist/assets/menu-items-BrwnTgyK.js +0 -1
- package/dist/assets/numbers-Mk3-CcoU.js +0 -1
- package/dist/assets/packages-panel-CI5ff9b-.js +0 -1
- package/dist/assets/panels-BCcZ_yLK.js +0 -107
- package/dist/assets/precisionRound-Bq_nmf5u.js +0 -1
- package/dist/assets/process-output-f3C3PD0Z.js +0 -1
- package/dist/assets/react-vega-C3rYGtP6.js +0 -189
- package/dist/assets/requests-CgSY1X4a.js +0 -1
- package/dist/assets/run-page-AfZhtEh1.js +0 -1
- package/dist/assets/scratchpad-panel-D2rWg4k8.js +0 -1
- package/dist/assets/secrets-panel-D7KN4tVs.js +0 -1
- package/dist/assets/select-DTBn5eTb.js +0 -1
- package/dist/assets/session-panel-CnANhcTo.js +0 -1
- package/dist/assets/snippets-panel-ClGHtEKQ.js +0 -1
- package/dist/assets/state-BAJYRhmO.js +0 -1
- package/dist/assets/state-CbjBtkdd.js +0 -3
- package/dist/assets/switch-CTaiAjrz.js +0 -1
- package/dist/assets/tabs-BuQlZFbt.js +0 -1
- package/dist/assets/toast-BGzLGnvw.js +0 -1
- package/dist/assets/tooltip-C9TuY18o.js +0 -1
- package/dist/assets/tooltip-mT3CGest.js +0 -61
- package/dist/assets/tracing-Dz2rYOFL.js +0 -1
- package/dist/assets/tracing-panel-DqQY9V91.js +0 -2
- package/dist/assets/use-restore-focus-BxEGwIbk.js +0 -1
- package/dist/assets/useAsyncData-C7aJJz0Y.js +0 -1
- package/dist/assets/useBoolean-d0Tty94N.js +0 -1
- package/dist/assets/useCellActionButton-DmaFZ4oF.js +0 -1
- package/dist/assets/useDebounce-B_ccBZTp.js +0 -1
- package/dist/assets/useNotebookActions-DHt3s-ia.js +0 -1
- package/dist/assets/vega-component-IouMe13t.js +0 -1
- package/dist/assets/vega-loader.browser-CLRkNgm7.js +0 -6
- package/dist/assets/worker-DTQ7oH2A.js +0 -158
- package/dist/assets/ws-BXWY541V.js +0 -22
- package/src/core/host/__tests__/events.test.ts +0 -46
- package/src/core/host/__tests__/notebook-client.test.ts +0 -191
- package/src/core/host/__tests__/protocol.test.ts +0 -214
- package/src/core/host/events.ts +0 -60
- package/src/core/host/notebook-client.ts +0 -226
- package/src/core/host/protocol.ts +0 -1943
- package/src/core/state/__tests__/jotai.test.ts +0 -25
- package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +0 -140
- package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
- package/src/core/websocket/transports/__tests__/sse.test.ts +0 -335
- package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
- package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
- package/src/core/websocket/transports/basic.ts +0 -102
- package/src/core/websocket/transports/create.ts +0 -31
- package/src/core/websocket/transports/sse.ts +0 -308
- package/src/core/websocket/transports/transport.ts +0 -50
- package/src/core/websocket/transports/ws.ts +0 -98
- package/src/core/websocket/useNotebookClient.ts +0 -80
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { createNotebookReducer, NotebookEvents } from "@marimo-team/client";
|
|
4
|
+
import * as Protocol from "@marimo-team/client/protocol";
|
|
5
|
+
import { afterEach, assert, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { document as documentResource } from "../resources/document";
|
|
7
|
+
import {
|
|
8
|
+
createStaticNotebookBackend,
|
|
9
|
+
type StaticNotebookSnapshot,
|
|
10
|
+
} from "../static";
|
|
11
|
+
import { fixtureEvents } from "./fixtures";
|
|
12
|
+
|
|
13
|
+
function snapshot(): StaticNotebookSnapshot {
|
|
14
|
+
const events = fixtureEvents("notebook-events.sse");
|
|
15
|
+
const notebook = events.find((event) => event.event === "notebook")?.data
|
|
16
|
+
.notebook;
|
|
17
|
+
const document = events.find((event) => event.event === "document")?.data
|
|
18
|
+
.document;
|
|
19
|
+
assert(notebook && document);
|
|
20
|
+
return {
|
|
21
|
+
notebook,
|
|
22
|
+
document,
|
|
23
|
+
cells: events.flatMap((event) =>
|
|
24
|
+
event.event === "cell" ? [event.data.cell] : [],
|
|
25
|
+
),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const streams: NotebookEvents[] = [];
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
const closed = streams.splice(0);
|
|
32
|
+
closed.forEach((stream) => stream.close());
|
|
33
|
+
await Promise.all(closed.map((stream) => stream.closed));
|
|
34
|
+
vi.useRealTimers();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function read(
|
|
38
|
+
backend: ReturnType<typeof createStaticNotebookBackend>,
|
|
39
|
+
cursor?: string,
|
|
40
|
+
) {
|
|
41
|
+
const stream = new NotebookEvents(backend.transport, { cursor });
|
|
42
|
+
streams.push(stream);
|
|
43
|
+
const events: Protocol.EventOutput[] = [];
|
|
44
|
+
await new Promise<void>((resolve, reject) => {
|
|
45
|
+
stream.subscribe((event) => {
|
|
46
|
+
events.push(Protocol.Event.parse(event));
|
|
47
|
+
if (event.event === "ready") {
|
|
48
|
+
resolve();
|
|
49
|
+
}
|
|
50
|
+
}, reject);
|
|
51
|
+
});
|
|
52
|
+
return { stream, events };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("static notebook backend", () => {
|
|
56
|
+
it("supplies a complete document through the normal event reader and reducer", async () => {
|
|
57
|
+
const source = snapshot();
|
|
58
|
+
const { stream, events } = await read(createStaticNotebookBackend(source));
|
|
59
|
+
const reducer = createNotebookReducer({ document: documentResource });
|
|
60
|
+
const state = events.reduce(reducer.reduce, reducer.initial);
|
|
61
|
+
expect(events.map((event) => event.event)).toEqual([
|
|
62
|
+
"notebook",
|
|
63
|
+
"cell",
|
|
64
|
+
"cell",
|
|
65
|
+
"cell",
|
|
66
|
+
"document",
|
|
67
|
+
"ready",
|
|
68
|
+
]);
|
|
69
|
+
expect(state.notebook).toEqual({ ...source.notebook, operations: [] });
|
|
70
|
+
expect(state.resources.document.current).toEqual({
|
|
71
|
+
revision: source.document,
|
|
72
|
+
cells: new Map(source.cells.map((cell) => [cell.id, cell])),
|
|
73
|
+
});
|
|
74
|
+
expect(stream.status).toBe("connected");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("preserves withheld source and its revision metadata", async () => {
|
|
78
|
+
const source = snapshot();
|
|
79
|
+
const cells = source.cells.map((cell) => ({ ...cell, code: null }));
|
|
80
|
+
const { events } = await read(
|
|
81
|
+
createStaticNotebookBackend({ ...source, cells }),
|
|
82
|
+
);
|
|
83
|
+
expect(
|
|
84
|
+
events
|
|
85
|
+
.filter((event) => event.event === "cell")
|
|
86
|
+
.map((event) => event.data.cell),
|
|
87
|
+
).toEqual(cells);
|
|
88
|
+
expect(
|
|
89
|
+
events.find((event) => event.event === "document")?.data.document,
|
|
90
|
+
).toEqual(source.document);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("serves an empty document as available", async () => {
|
|
94
|
+
const source = snapshot();
|
|
95
|
+
const document = { ...source.document, order: [] };
|
|
96
|
+
const { events } = await read(
|
|
97
|
+
createStaticNotebookBackend({ ...source, document, cells: [] }),
|
|
98
|
+
);
|
|
99
|
+
const reducer = createNotebookReducer({ document: documentResource });
|
|
100
|
+
expect(
|
|
101
|
+
events.reduce(reducer.reduce, reducer.initial).resources.document.current,
|
|
102
|
+
).toEqual({
|
|
103
|
+
revision: document,
|
|
104
|
+
cells: new Map(),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("keeps independent readers open without polling or reconnecting", async () => {
|
|
109
|
+
vi.useFakeTimers();
|
|
110
|
+
const backend = createStaticNotebookBackend(snapshot());
|
|
111
|
+
const first = await read(backend);
|
|
112
|
+
const second = await read(backend);
|
|
113
|
+
first.stream.close();
|
|
114
|
+
await first.stream.closed;
|
|
115
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
116
|
+
expect(second.events).toEqual(first.events);
|
|
117
|
+
expect(second.stream.status).toBe("connected");
|
|
118
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("resets a resume cursor before replaying its snapshot", async () => {
|
|
122
|
+
const { events } = await read(
|
|
123
|
+
createStaticNotebookBackend(snapshot()),
|
|
124
|
+
"previous-10",
|
|
125
|
+
);
|
|
126
|
+
expect(events[0]).toEqual({ event: "reset", data: {} });
|
|
127
|
+
expect(events.at(-1)).toEqual({ event: "ready", id: "static", data: {} });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("advertises no mutations and returns a protocol failure for direct edits", async () => {
|
|
131
|
+
const backend = createStaticNotebookBackend(snapshot());
|
|
132
|
+
expect(await backend.notebook(snapshot().notebook.id).edit([])).toEqual({
|
|
133
|
+
ok: false,
|
|
134
|
+
status: 501,
|
|
135
|
+
problem: {
|
|
136
|
+
type: "tag:marimo.io,2026:host/unsupported-operation",
|
|
137
|
+
title: "Unsupported operation",
|
|
138
|
+
status: 501,
|
|
139
|
+
detail: "Static notebooks do not support editing",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("respects cancellation before opening, during delivery, and for commands", async () => {
|
|
145
|
+
const backend = createStaticNotebookBackend(snapshot());
|
|
146
|
+
const before = new AbortController();
|
|
147
|
+
before.abort();
|
|
148
|
+
const open = vi.fn();
|
|
149
|
+
const message = vi.fn();
|
|
150
|
+
await backend.transport(
|
|
151
|
+
{ cursor: null, signal: before.signal },
|
|
152
|
+
{ open, message },
|
|
153
|
+
);
|
|
154
|
+
expect(open).not.toHaveBeenCalled();
|
|
155
|
+
expect(message).not.toHaveBeenCalled();
|
|
156
|
+
await expect(
|
|
157
|
+
backend
|
|
158
|
+
.notebook(snapshot().notebook.id)
|
|
159
|
+
.edit([], { signal: before.signal }),
|
|
160
|
+
).rejects.toBe(before.signal.reason);
|
|
161
|
+
const during = new AbortController();
|
|
162
|
+
message.mockImplementation(() => during.abort());
|
|
163
|
+
await backend.transport(
|
|
164
|
+
{ cursor: null, signal: during.signal },
|
|
165
|
+
{ open, message },
|
|
166
|
+
);
|
|
167
|
+
expect(message).toHaveBeenCalledOnce();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { NotebookEvents, StreamStopped } from "@marimo-team/client";
|
|
4
|
+
import { afterEach, assert, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { Deferred } from "@/utils/Deferred";
|
|
6
|
+
import { createWasmNotebookBackend, type WasmNotebookBridge } from "../wasm";
|
|
7
|
+
import { fixtureEvents } from "./fixtures";
|
|
8
|
+
|
|
9
|
+
const streams: NotebookEvents[] = [];
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
for (const stream of streams.splice(0)) {
|
|
12
|
+
stream.close();
|
|
13
|
+
await stream.closed.catch(() => undefined);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function worker() {
|
|
18
|
+
const frames = fixtureEvents("notebook-events.sse");
|
|
19
|
+
const document = frames.find((event) => event.event === "document")?.data
|
|
20
|
+
.document;
|
|
21
|
+
assert(document);
|
|
22
|
+
const listeners = new Set<(event: unknown) => void>();
|
|
23
|
+
const reply = { ok: true, status: 200, data: { document, cells: {} } };
|
|
24
|
+
const wire = { status: 200, body: reply.data };
|
|
25
|
+
const bridge = {
|
|
26
|
+
notebookId: "n1",
|
|
27
|
+
subscribe: vi.fn(
|
|
28
|
+
async (_cursor: string | null, emit: (event: unknown) => void) => {
|
|
29
|
+
listeners.add(emit);
|
|
30
|
+
frames.forEach(emit);
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(emit);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
call: vi.fn<WasmNotebookBridge["call"]>().mockResolvedValue(wire),
|
|
37
|
+
};
|
|
38
|
+
return { bridge, listeners, reply, wire };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function follow(bridge: WasmNotebookBridge) {
|
|
42
|
+
const stream = new NotebookEvents(
|
|
43
|
+
createWasmNotebookBackend(bridge).transport,
|
|
44
|
+
);
|
|
45
|
+
streams.push(stream);
|
|
46
|
+
const ready = new Deferred<void>();
|
|
47
|
+
stream.subscribe((event) => {
|
|
48
|
+
if (event.event === "ready") {
|
|
49
|
+
ready.resolve();
|
|
50
|
+
}
|
|
51
|
+
}, ready.reject);
|
|
52
|
+
await ready.promise;
|
|
53
|
+
return stream;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("WASM notebook backend", () => {
|
|
57
|
+
it("follows independent readers and releases each subscription on close", async () => {
|
|
58
|
+
const { bridge, listeners } = worker();
|
|
59
|
+
const first = await follow(bridge);
|
|
60
|
+
const second = await follow(bridge);
|
|
61
|
+
expect(listeners.size).toBe(2);
|
|
62
|
+
expect(second.status).toBe("connected");
|
|
63
|
+
first.close();
|
|
64
|
+
await first.closed;
|
|
65
|
+
expect(listeners.size).toBe(1);
|
|
66
|
+
second.close();
|
|
67
|
+
await second.closed;
|
|
68
|
+
expect(listeners.size).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("closes during startup and releases the late subscription", async () => {
|
|
72
|
+
const { bridge } = worker();
|
|
73
|
+
const pending = new Deferred<() => void>();
|
|
74
|
+
bridge.subscribe.mockReturnValue(pending.promise);
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const done = createWasmNotebookBackend(bridge).transport(
|
|
77
|
+
{ cursor: "previous-4", signal: controller.signal },
|
|
78
|
+
{ open: vi.fn(), message: vi.fn() },
|
|
79
|
+
);
|
|
80
|
+
expect(bridge.subscribe).toHaveBeenCalledWith(
|
|
81
|
+
"previous-4",
|
|
82
|
+
expect.any(Function),
|
|
83
|
+
);
|
|
84
|
+
controller.abort();
|
|
85
|
+
await done;
|
|
86
|
+
const close = vi.fn();
|
|
87
|
+
pending.resolve(close);
|
|
88
|
+
await vi.waitFor(() => expect(close).toHaveBeenCalledOnce());
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("stops on worker initialization failure", async () => {
|
|
92
|
+
const { bridge } = worker();
|
|
93
|
+
bridge.subscribe.mockRejectedValue(new Error("Initialization failed"));
|
|
94
|
+
await expect(
|
|
95
|
+
createWasmNotebookBackend(bridge).transport(
|
|
96
|
+
{ cursor: null, signal: new AbortController().signal },
|
|
97
|
+
{ open: vi.fn(), message: vi.fn() },
|
|
98
|
+
),
|
|
99
|
+
).rejects.toBeInstanceOf(StreamStopped);
|
|
100
|
+
});
|
|
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
|
+
|
|
110
|
+
it("sends edits as calls with the retry key and validates replies", async () => {
|
|
111
|
+
const { bridge, reply } = worker();
|
|
112
|
+
const backend = createWasmNotebookBackend(bridge);
|
|
113
|
+
expect(
|
|
114
|
+
await backend.notebook("n1").edit([], { idempotencyKey: "edit-7" }),
|
|
115
|
+
).toEqual(reply);
|
|
116
|
+
expect(bridge.call).toHaveBeenCalledWith({
|
|
117
|
+
operation: "notebook.edit",
|
|
118
|
+
params: { notebook_id: "n1" },
|
|
119
|
+
headers: { "idempotency-key": "edit-7" },
|
|
120
|
+
body: { changes: [] },
|
|
121
|
+
});
|
|
122
|
+
const problem = {
|
|
123
|
+
type: "tag:marimo.io,2026:host/forbidden",
|
|
124
|
+
title: "Forbidden",
|
|
125
|
+
status: 403,
|
|
126
|
+
};
|
|
127
|
+
bridge.call.mockResolvedValue({ status: 403, body: problem });
|
|
128
|
+
expect(await backend.notebook("n1").edit([])).toEqual({
|
|
129
|
+
ok: false,
|
|
130
|
+
status: 403,
|
|
131
|
+
problem,
|
|
132
|
+
});
|
|
133
|
+
bridge.call.mockResolvedValue({ status: 200, body: {} });
|
|
134
|
+
await expect(backend.notebook("n1").edit([])).rejects.toThrow(
|
|
135
|
+
/Invalid input/,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("cancels waiting for an accepted edit and permits retry with the same key", async () => {
|
|
140
|
+
const { bridge, reply, wire } = worker();
|
|
141
|
+
const pending = new Deferred<unknown>();
|
|
142
|
+
bridge.call.mockReturnValue(pending.promise);
|
|
143
|
+
const backend = createWasmNotebookBackend(bridge);
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const editing = backend.notebook("n1").edit([], {
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
idempotencyKey: "edit-8",
|
|
148
|
+
});
|
|
149
|
+
await vi.waitFor(() => expect(bridge.call).toHaveBeenCalledOnce());
|
|
150
|
+
controller.abort();
|
|
151
|
+
await expect(editing).rejects.toBe(controller.signal.reason);
|
|
152
|
+
pending.resolve(wire);
|
|
153
|
+
expect(
|
|
154
|
+
await backend.notebook("n1").edit([], { idempotencyKey: "edit-8" }),
|
|
155
|
+
).toEqual(reply);
|
|
156
|
+
expect(bridge.call.mock.calls[0]).toEqual(bridge.call.mock.calls[1]);
|
|
157
|
+
await expect(
|
|
158
|
+
backend.notebook("n1").edit([], { signal: controller.signal }),
|
|
159
|
+
).rejects.toBe(controller.signal.reason);
|
|
160
|
+
expect(bridge.call).toHaveBeenCalledTimes(2);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/** A notebook's events and commands, from a host or a stand-in for one. */
|
|
4
|
+
|
|
5
|
+
import type { NotebookClient, NotebookTransport } from "@marimo-team/client";
|
|
6
|
+
import type { NotebookId } from "@marimo-team/client/protocol";
|
|
7
|
+
|
|
8
|
+
/** Events and commands for a notebook, independent of React or a replica. */
|
|
9
|
+
export interface NotebookBackend {
|
|
10
|
+
readonly transport: NotebookTransport;
|
|
11
|
+
/**
|
|
12
|
+
* Commands for a notebook the transport reported.
|
|
13
|
+
*
|
|
14
|
+
* Method presence is not permission. The notebook advertises its operations.
|
|
15
|
+
*/
|
|
16
|
+
notebook(id: NotebookId): Pick<NotebookClient, "edit">;
|
|
17
|
+
/** Whether the host is reachable before an explicit reconnect. Absent means always. */
|
|
18
|
+
checkHealth?(): Promise<boolean>;
|
|
19
|
+
}
|
|
@@ -37,45 +37,6 @@ export type ConnectionStatus =
|
|
|
37
37
|
|
|
38
38
|
/** The recovery policy for a closed connection. */
|
|
39
39
|
export interface CloseDecision {
|
|
40
|
-
kind: "terminal" | "
|
|
40
|
+
kind: "terminal" | "retry";
|
|
41
41
|
status: ConnectionStatus;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
export type ConnectionEvent = "open" | "close" | "message" | "error";
|
|
45
|
-
|
|
46
|
-
export interface IConnectionTransportMap {
|
|
47
|
-
open: WebSocketEventMap["open"];
|
|
48
|
-
message: WebSocketEventMap["message"];
|
|
49
|
-
close: WebSocketEventMap["close"];
|
|
50
|
-
error: WebSocketEventMap["error"];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type ConnectionTransportCallback<T extends ConnectionEvent> = (
|
|
54
|
-
data: IConnectionTransportMap[T],
|
|
55
|
-
) => void;
|
|
56
|
-
|
|
57
|
-
/** Transport readiness, using the WebSocket readyState values. */
|
|
58
|
-
export const TransportReadyState = {
|
|
59
|
-
CONNECTING: 0,
|
|
60
|
-
OPEN: 1,
|
|
61
|
-
CLOSING: 2,
|
|
62
|
-
CLOSED: 3,
|
|
63
|
-
} as const;
|
|
64
|
-
|
|
65
|
-
export type TransportReadyState =
|
|
66
|
-
(typeof TransportReadyState)[keyof typeof TransportReadyState];
|
|
67
|
-
|
|
68
|
-
/** A reconnectable source of notebook messages. */
|
|
69
|
-
export interface NotebookTransport {
|
|
70
|
-
reconnect(code?: number | undefined, reason?: string | undefined): void;
|
|
71
|
-
close(): void;
|
|
72
|
-
addEventListener<T extends ConnectionEvent>(
|
|
73
|
-
event: T,
|
|
74
|
-
callback: ConnectionTransportCallback<T>,
|
|
75
|
-
): void;
|
|
76
|
-
removeEventListener<T extends ConnectionEvent>(
|
|
77
|
-
event: T,
|
|
78
|
-
callback: ConnectionTransportCallback<T>,
|
|
79
|
-
): void;
|
|
80
|
-
readyState: TransportReadyState;
|
|
81
|
-
}
|
|
@@ -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";
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/** What a backend's stream has confirmed about a notebook, kept in atoms. */
|
|
4
|
+
|
|
5
|
+
import { Logger } from "@/utils/Logger";
|
|
6
|
+
import {
|
|
7
|
+
createNotebookReducer,
|
|
8
|
+
type NotebookEvent,
|
|
9
|
+
NotebookEvents,
|
|
10
|
+
StreamStopped,
|
|
11
|
+
} from "@marimo-team/client";
|
|
12
|
+
import type { Attachment, NotebookOutput } from "@marimo-team/client/protocol";
|
|
13
|
+
import { type Atom, atom, type createStore } from "jotai/vanilla";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import type { NotebookBackend } from "./backend";
|
|
16
|
+
import {
|
|
17
|
+
ConnectionClosedReason,
|
|
18
|
+
ConnectionState,
|
|
19
|
+
type ConnectionStatus,
|
|
20
|
+
} from "./connection";
|
|
21
|
+
import { attachments } from "./resources/attachments";
|
|
22
|
+
import { type Document, document } from "./resources/document";
|
|
23
|
+
|
|
24
|
+
/** The kinds the editor reads, with marimo's own notifications. */
|
|
25
|
+
const TYPES = ["notebook", "cell", "document", "attachment", "marimo"] as const;
|
|
26
|
+
|
|
27
|
+
type EditorEvent = NotebookEvent<(typeof TYPES)[number]>;
|
|
28
|
+
|
|
29
|
+
/** A native marimo notification, passed through without interpretation. */
|
|
30
|
+
const MarimoMessage = z.object({ marimo: z.unknown() });
|
|
31
|
+
|
|
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. */
|
|
35
|
+
readonly connectionAtom: Atom<ConnectionStatus>;
|
|
36
|
+
/** Identity and capabilities, available while the stream is connected and ready. */
|
|
37
|
+
readonly notebookAtom: Atom<NotebookOutput | null>;
|
|
38
|
+
/**
|
|
39
|
+
* The most recently confirmed document.
|
|
40
|
+
*
|
|
41
|
+
* Null until a snapshot is complete, after reset or removal, and on
|
|
42
|
+
* connections without host document events. A temporary disconnection
|
|
43
|
+
* retains the last document. Partial updates are not exposed.
|
|
44
|
+
*/
|
|
45
|
+
readonly documentAtom: Atom<Document | null>;
|
|
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>;
|
|
53
|
+
/** Live attachments, empty until the stream is ready or while disconnected. */
|
|
54
|
+
readonly attachmentsAtom: Atom<readonly Attachment[]>;
|
|
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. */
|
|
69
|
+
reconnect(): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const disconnected = {
|
|
73
|
+
state: ConnectionState.CLOSED,
|
|
74
|
+
code: ConnectionClosedReason.KERNEL_DISCONNECTED,
|
|
75
|
+
reason: "kernel not found",
|
|
76
|
+
} satisfies ConnectionStatus;
|
|
77
|
+
|
|
78
|
+
const malformed = {
|
|
79
|
+
...disconnected,
|
|
80
|
+
reason: "Failed to decode notebook message",
|
|
81
|
+
} satisfies ConnectionStatus;
|
|
82
|
+
|
|
83
|
+
function isStatus(value: unknown): value is ConnectionStatus {
|
|
84
|
+
return typeof value === "object" && value !== null && "state" in value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The status to show after the stream stopped.
|
|
89
|
+
*
|
|
90
|
+
* A transport says which status its stop means. An event the protocol
|
|
91
|
+
* rejects is malformed. Anything else means the host was not reached.
|
|
92
|
+
*/
|
|
93
|
+
function statusAfter(error: unknown): ConnectionStatus {
|
|
94
|
+
if (error instanceof StreamStopped && isStatus(error.cause)) {
|
|
95
|
+
return error.cause;
|
|
96
|
+
}
|
|
97
|
+
if (error instanceof StreamStopped && error.cause instanceof z.ZodError) {
|
|
98
|
+
return malformed;
|
|
99
|
+
}
|
|
100
|
+
return disconnected;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Create a replica without connecting it. */
|
|
104
|
+
export function createNotebookReplica(
|
|
105
|
+
store: ReturnType<typeof createStore>,
|
|
106
|
+
): NotebookReplica {
|
|
107
|
+
// `useNotebookSession` copies this into the editor's shared `connectionAtom`,
|
|
108
|
+
// which gates features until MO-8051 derives availability separately.
|
|
109
|
+
const connectionAtom = atom<ConnectionStatus>({
|
|
110
|
+
state: ConnectionState.NOT_STARTED,
|
|
111
|
+
});
|
|
112
|
+
// One atom holds the whole stream, which resource atoms derive from.
|
|
113
|
+
const reducer = createNotebookReducer({ document, attachments });
|
|
114
|
+
const streamAtom = atom(reducer.initial);
|
|
115
|
+
// A resumed stream is live, but presence waits for this connection's ready.
|
|
116
|
+
const readyAtom = atom(false);
|
|
117
|
+
const presentAtom = atom(
|
|
118
|
+
(get) =>
|
|
119
|
+
get(readyAtom) && get(connectionAtom).state === ConnectionState.OPEN,
|
|
120
|
+
);
|
|
121
|
+
const notebookAtom = atom((get) =>
|
|
122
|
+
get(presentAtom) ? get(streamAtom).notebook : null,
|
|
123
|
+
);
|
|
124
|
+
const documentAtom = atom(
|
|
125
|
+
(get) => get(streamAtom).resources.document.current,
|
|
126
|
+
);
|
|
127
|
+
const attachmentsAtom = atom((get) =>
|
|
128
|
+
get(presentAtom)
|
|
129
|
+
? get(streamAtom).resources.attachments
|
|
130
|
+
: attachments.initial,
|
|
131
|
+
);
|
|
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;
|
|
135
|
+
|
|
136
|
+
function setStatus(status: ConnectionStatus) {
|
|
137
|
+
store.set(readyAtom, false);
|
|
138
|
+
store.set(connectionAtom, status);
|
|
139
|
+
}
|
|
140
|
+
|
|
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);
|
|
166
|
+
const { transport } = connected;
|
|
167
|
+
let events: NotebookEvents<(typeof TYPES)[number]> | undefined;
|
|
168
|
+
let checkingHealth = false;
|
|
169
|
+
// Whether the replica still holds the state the last cursor describes.
|
|
170
|
+
let resumable = true;
|
|
171
|
+
|
|
172
|
+
/** Mirror one stream into the atoms until it closes. */
|
|
173
|
+
function follow(cursor?: string) {
|
|
174
|
+
if (cursor === undefined) {
|
|
175
|
+
store.set(streamAtom, reducer.initial);
|
|
176
|
+
}
|
|
177
|
+
resumable = true;
|
|
178
|
+
const current = new NotebookEvents(transport, {
|
|
179
|
+
types: TYPES,
|
|
180
|
+
cursor,
|
|
181
|
+
signal,
|
|
182
|
+
// TODO(MO-8051): Gate editing and running on the notebook's operations
|
|
183
|
+
// and request results instead. The stream's status would then only
|
|
184
|
+
// mark the view as stale.
|
|
185
|
+
onStatus: (status) => {
|
|
186
|
+
if (status === "connecting") {
|
|
187
|
+
setStatus({ state: ConnectionState.CONNECTING });
|
|
188
|
+
} else if (status === "connected") {
|
|
189
|
+
setStatus({ state: ConnectionState.OPEN });
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
current.subscribe(
|
|
194
|
+
(event) => apply(current, event),
|
|
195
|
+
(error) => {
|
|
196
|
+
if (!signal.aborted && current === events) {
|
|
197
|
+
Logger.warn("Notebook stream stopped", error);
|
|
198
|
+
const status = statusAfter(error);
|
|
199
|
+
if (status === malformed) {
|
|
200
|
+
stopMalformed(current);
|
|
201
|
+
} else {
|
|
202
|
+
setStatus(status);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
events = current;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function stopMalformed(source: NotebookEvents<string>) {
|
|
211
|
+
source.close();
|
|
212
|
+
// The cursor can already include an event the replica rejected.
|
|
213
|
+
resumable = false;
|
|
214
|
+
clear(malformed);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function apply(source: NotebookEvents<string>, event: EditorEvent) {
|
|
218
|
+
if (event.event === "marimo") {
|
|
219
|
+
const message = MarimoMessage.safeParse(event.data);
|
|
220
|
+
if (message.success) {
|
|
221
|
+
onNotification(message.data.marimo);
|
|
222
|
+
} else {
|
|
223
|
+
Logger.error("Failed to decode notebook message", event);
|
|
224
|
+
stopMalformed(source);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
store.set(streamAtom, reducer.reduce(store.get(streamAtom), event));
|
|
230
|
+
} catch (error) {
|
|
231
|
+
// An event the replica cannot apply, such as another notebook's.
|
|
232
|
+
Logger.error("Failed to apply notebook event", error);
|
|
233
|
+
stopMalformed(source);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (event.event === "ready" || event.event === "reset") {
|
|
237
|
+
store.set(readyAtom, event.event === "ready");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const reconnect = async () => {
|
|
242
|
+
if (signal.aborted || checkingHealth || events?.status !== "closed") {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
checkingHealth = true;
|
|
246
|
+
setStatus({ state: ConnectionState.CONNECTING });
|
|
247
|
+
try {
|
|
248
|
+
const healthy = await checkHealth();
|
|
249
|
+
if (signal.aborted) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!healthy) {
|
|
253
|
+
setStatus(disconnected);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
follow(resumable ? (events.cursor ?? undefined) : undefined);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (!signal.aborted) {
|
|
259
|
+
Logger.error("Notebook health check failed", error);
|
|
260
|
+
setStatus(disconnected);
|
|
261
|
+
}
|
|
262
|
+
} finally {
|
|
263
|
+
checkingHealth = false;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const connection = {
|
|
268
|
+
end: () => {
|
|
269
|
+
if (signal.aborted) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
lifetime.abort();
|
|
273
|
+
if (current === connection) {
|
|
274
|
+
current = undefined;
|
|
275
|
+
store.set(backendAtom, null);
|
|
276
|
+
}
|
|
277
|
+
clear({ state: ConnectionState.NOT_STARTED });
|
|
278
|
+
},
|
|
279
|
+
reconnect,
|
|
280
|
+
};
|
|
281
|
+
current = connection;
|
|
282
|
+
setStatus({ state: ConnectionState.CONNECTING });
|
|
283
|
+
follow();
|
|
284
|
+
return connection.end;
|
|
285
|
+
}
|
|
286
|
+
}
|