@marimo-team/frontend 0.23.16-dev4 → 0.23.16-dev41
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-BalYv0pK.js → CellStatus-C6Cj3oJc.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DI7mV-Gw.js → ConnectedDataExplorerComponent-CMaZapRd.js} +1 -1
- package/dist/assets/{ErrorBoundary-C7_5jjWi.js → ErrorBoundary-DzTD8LVq.js} +1 -1
- package/dist/assets/{ImperativeModal-BIavOiUv.js → ImperativeModal-fY-ZfT0b.js} +1 -1
- package/dist/assets/{JsonOutput-CkCD-FgM.js → JsonOutput-CyzvjIjx.js} +12 -12
- package/dist/assets/{LazyAnyLanguageCodeMirror-sVm_hz8A.js → LazyAnyLanguageCodeMirror-B2JvSXP-.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DO2k2fA5.js → MarimoErrorOutput-CIuzXFFo.js} +4 -4
- package/dist/assets/{RSPContexts-CgC81f0I.js → RSPContexts-WiCgGm__.js} +1 -1
- package/dist/assets/RenderHTML-Cd-PP5ZR.js +1 -0
- package/dist/assets/{RunButton-BKRupqr7.js → RunButton-CnYWedW4.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CEZtaZCK.js → add-cell-with-ai-ByOBQT99.js} +8 -8
- package/dist/assets/add-connection-dialog-C97ylgk0.js +170 -0
- package/dist/assets/{agent-panel-CHB6t7Zy.js → agent-panel-Dxo9WUKV.js} +5 -5
- package/dist/assets/ai-model-dropdown-Bvqy98cI.js +5 -0
- package/dist/assets/{alert-dialog-BXegNC0I.js → alert-dialog-Bb8Lz8ko.js} +1 -1
- package/dist/assets/{any-language-editor-DCpZRrpx.js → any-language-editor--tRHs8C_.js} +1 -1
- package/dist/assets/{app-config-button-NrM9ywXA.js → app-config-button-v1lG63C2.js} +1 -1
- package/dist/assets/{button-eIihcbUw.js → button-DDGiD1NW.js} +1 -1
- package/dist/assets/{cache-panel-wllSTlbM.js → cache-panel-DcQUeUpg.js} +1 -1
- package/dist/assets/{capabilities-B09j1Eor.js → capabilities-DoEg0XwL.js} +1 -1
- package/dist/assets/{cell-editor-B5JVq7_q.js → cell-editor-BKrHbyX_.js} +16 -16
- package/dist/assets/cell-link-w5IbCQV4.js +1 -0
- package/dist/assets/cells-DzyDERZy.js +235 -0
- package/dist/assets/{chat-display-BG08ltQ3.js → chat-display-Ccgk4vbE.js} +1 -1
- package/dist/assets/{chat-panel-P2CzCtXJ.js → chat-panel--53TIc3t.js} +4 -4
- package/dist/assets/{chat-ui-fAJZAoTD.js → chat-ui-B5rBxgsw.js} +4 -4
- package/dist/assets/{column-preview-Dne1dIt0.js → column-preview-Cb8R9kfl.js} +1 -1
- package/dist/assets/{command-CAJLPEBq.js → command-Dqxie4gq.js} +1 -1
- package/dist/assets/{command-palette-iOCqUIs1.js → command-palette-B1S9PjZi.js} +1 -1
- package/dist/assets/{common-CD6Mj8n6.js → common-CJ3baa6D.js} +1 -1
- package/dist/assets/components-DpUIWGaj.js +1 -0
- package/dist/assets/config-B-Q7J_6d.js +1 -0
- package/dist/assets/{config-COUOIPEq.js → config-BJVx3eBW.js} +1 -1
- package/dist/assets/{copy-BtTOqPTS.js → copy-Bxb1_Gtw.js} +1 -1
- package/dist/assets/{copy-icon-C2pG0ZNK.js → copy-icon-neDQUo4Q.js} +1 -1
- package/dist/assets/{createReducer-C7kZ8zEe.js → createReducer-DGx5Af-C.js} +1 -1
- package/dist/assets/{datasource-ySAiC9gr.js → datasource-D_VcQMEn.js} +2 -2
- package/dist/assets/{dates-D-kIKDYA.js → dates-C2LWzsCZ.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BF23EwcD.js → dependency-graph-panel-DY5lvPDx.js} +3 -3
- package/dist/assets/{dialog-D1DEsRZ2.js → dialog-BCVepWLJ.js} +1 -1
- package/dist/assets/{dist-ByVeMWkr.js → dist-BLN8PNJV.js} +1 -1
- package/dist/assets/dist-CiVShZKM.js +2 -0
- package/dist/assets/{documentation-panel-e9Fi9R7e.js → documentation-panel-CGhwxsXK.js} +1 -1
- package/dist/assets/download-DpS57jyt.js +9 -0
- package/dist/assets/{dropdown-menu-BFZLnWeO.js → dropdown-menu-dHmZR8bI.js} +1 -1
- package/dist/assets/{edit-page-7X3HYey7.js → edit-page-DA765yQV.js} +6 -6
- package/dist/assets/{error-banner-B6eIlxzg.js → error-banner-BSxEOIn2.js} +1 -1
- package/dist/assets/error-entries-CTYMwfsh.js +8 -0
- package/dist/assets/{error-panel-DdsUL6b7.js → error-panel-DGAkG5kW.js} +1 -1
- package/dist/assets/{es-1D83cgiu.js → es-gYgfF8y5.js} +1 -1
- package/dist/assets/{esm-BL7jQmzn.js → esm-B30N1GS0.js} +1 -1
- package/dist/assets/{field-BeZbVzXA.js → field-BsxD35VE.js} +1 -1
- package/dist/assets/{file-explorer-panel-Ntq1-VbF.js → file-explorer-panel-BKUo-MDh.js} +3 -3
- package/dist/assets/{file-icons-Cb54mIL8.js → file-icons-DfWENcb8.js} +1 -1
- package/dist/assets/{file-name-input-BWFcK1ey.js → file-name-input-CNVZtm5K.js} +1 -1
- package/dist/assets/floating-outline-LitP0wCD.js +1 -0
- package/dist/assets/focus-CNKbm-Xz.js +1 -0
- package/dist/assets/{form-B6BkSCJb.js → form-nHzLTU1m.js} +2 -2
- package/dist/assets/{formats-ClEFGPy-.js → formats-DzmLpc9A.js} +1 -1
- package/dist/assets/{formatting-B_41gSF5.js → formatting-CDf6w1Kt.js} +1 -1
- package/dist/assets/{fullscreen-dUjPYOmm.js → fullscreen-CTDqNQr6.js} +1 -1
- package/dist/assets/{gallery-page-CWQQAYpa.js → gallery-page-CJ3ms1Hn.js} +1 -1
- package/dist/assets/{glide-data-editor-lMARFWHS.js → glide-data-editor-j2igs9Y_.js} +1 -1
- package/dist/assets/home-page-DM9Z_IJ7.js +4 -0
- package/dist/assets/{html-to-image-CwmX28T9.js → html-to-image-JaxFFZcr.js} +2 -2
- package/dist/assets/index-C7_REKh1.js +38 -0
- package/dist/assets/index-D6Dp_PFj.css +2 -0
- package/dist/assets/{input-CqrUjynb.js → input-JxFwu-3G.js} +1 -1
- package/dist/assets/kiosk-mode-BumOFEtY.js +1 -0
- package/dist/assets/{label-BElwWzf0.js → label-CqjrpwRC.js} +1 -1
- package/dist/assets/{layout-CUlZijwG.js → layout-B29hqiiK.js} +5 -5
- package/dist/assets/{links-CToBjIIj.js → links-CZr5bjvo.js} +1 -1
- package/dist/assets/logs-panel-CQRPYS8j.js +1 -0
- package/dist/assets/{markdown-renderer-QvXG0CRk.js → markdown-renderer-sEayY9-m.js} +2 -2
- package/dist/assets/{menu-items-DbSLBfXR.js → menu-items-CCi6aOPm.js} +1 -1
- package/dist/assets/{mermaid-Bh0flEIb.js → mermaid-CYN0wOyG.js} +1 -1
- package/dist/assets/{name-cell-input-C6cukQ9k.js → name-cell-input-B-aj1KH3.js} +1 -1
- package/dist/assets/node-sql-parser-Si4oZ2P-.js +68 -0
- package/dist/assets/{numbers-CeeaFKqu.js → numbers-rmawRqQP.js} +1 -1
- package/dist/assets/{outline-panel-BPlg9_CE.js → outline-panel-BOryk60D.js} +1 -1
- package/dist/assets/{packages-panel-BzQF60rG.js → packages-panel-DoYDLty3.js} +1 -1
- package/dist/assets/pair-with-agent-modal-BCRSACei.js +4 -0
- package/dist/assets/panels-Bb4fIHgH.js +4 -0
- package/dist/assets/{popover-DrHQ3KEt.js → popover-Bcz1Zikb.js} +1 -1
- package/dist/assets/process-output-d-LEMtGI.js +1 -0
- package/dist/assets/{radio-group-CLBjGkt5.js → radio-group-DfioQthW.js} +1 -1
- package/dist/assets/{readonly-code-display-D63oRtBP.js → readonly-code-display-C9fu8tvK.js} +1 -1
- package/dist/assets/{readonly-python-code-CW_G5e4E.js → readonly-python-code-DHxOnQGj.js} +1 -1
- package/dist/assets/{renderShortcut-BZLr8D7H.js → renderShortcut-Bl8_FVn8.js} +1 -1
- package/dist/assets/{reveal-component-BLMTmd8T.js → reveal-component-5jf0W625.js} +60 -60
- package/dist/assets/{run-page-B6pgDYEJ.js → run-page-BMKx9ld_.js} +1 -1
- package/dist/assets/{runs-D51Xwrsk.js → runs-DBTXqJHS.js} +1 -1
- package/dist/assets/scratchpad-panel-C8z8vzBX.js +1 -0
- package/dist/assets/{scroll-to-bottom-button-Cgr10kaJ.js → scroll-to-bottom-button-C8yfT4wq.js} +1 -1
- package/dist/assets/{secrets-panel-Cc5KDBk6.js → secrets-panel-6J6wcuVR.js} +1 -1
- package/dist/assets/{select-B5TFNjmU.js → select-BP75_5jJ.js} +1 -1
- package/dist/assets/{session-CEFnN9Cq.js → session-BmukRjSi.js} +1 -1
- package/dist/assets/{session-panel-Y0Oon0sD.js → session-panel-BVVk-02h.js} +1 -1
- package/dist/assets/{share-CYpcX4N4.js → share-852X_cqy.js} +1 -1
- package/dist/assets/{skeleton-D3QPxqWC.js → skeleton-Bubr9pIj.js} +1 -1
- package/dist/assets/slides-DkEgzTJB.css +1 -0
- package/dist/assets/{snippets-panel-CXpGflMO.js → snippets-panel-U2-iok-c.js} +1 -1
- package/dist/assets/{spec-CzqcmYQk.js → spec-C7opZZRp.js} +1 -1
- package/dist/assets/{state-BvdK8eiw.js → state-D10U2MwA.js} +1 -1
- package/dist/assets/{state-WBgc7MiA.js → state-DDag0B_B.js} +1 -1
- package/dist/assets/state-DtziTd1T.js +3 -0
- package/dist/assets/{state-DBzEQtWz.js → state-DxQhUsG9.js} +1 -1
- package/dist/assets/{strings-Djnbwmfl.js → strings--BknHX9C.js} +1 -1
- package/dist/assets/{swiper-component-DInvCxhI.js → swiper-component-g8EXE911.js} +1 -1
- package/dist/assets/switch-OVajJBPR.js +1 -0
- package/dist/assets/{terminal-D3IQBBYo.js → terminal-217O8hlZ.js} +1 -1
- package/dist/assets/{textarea-CpFfEUet.js → textarea-Bmzmbbpo.js} +1 -1
- package/dist/assets/{tooltip-CFX4fYHU.js → tooltip-4bZlU7jl.js} +1 -1
- package/dist/assets/{tooltip-HLb6RxVp.js → tooltip-B7U4xkIN.js} +1 -1
- package/dist/assets/{tracing-CKn3gWHw.js → tracing-B5XFvMmV.js} +1 -1
- package/dist/assets/{tracing-panel-BU-o_LCd.js → tracing-panel-DnR2bYDe.js} +2 -2
- package/dist/assets/{tree-actions-CsC2XmGo.js → tree-actions-Dx9CDaxV.js} +1 -1
- package/dist/assets/{useBoolean-D89u30n-.js → useBoolean-CFj86iac.js} +1 -1
- package/dist/assets/useCellActionButton-DypR3yDt.js +1 -0
- package/dist/assets/{useDeleteCell-EPH4kcG7.js → useDeleteCell-vMXAr3rY.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CZnK7N2L.js → useDependencyPanelTab-CwdwKf7A.js} +1 -1
- package/dist/assets/useEventListener-C0195JsF.js +1 -0
- package/dist/assets/{useHotkey-DIuRH8Wu.js → useHotkey-DLZvrRZT.js} +1 -1
- package/dist/assets/{useIframeCapabilities-Ccb4fvKc.js → useIframeCapabilities-j6fR_dBA.js} +1 -1
- package/dist/assets/{useInstallPackage-BRbUAUFn.js → useInstallPackage-WiYBwhUO.js} +1 -1
- package/dist/assets/{useInterval-CC0vKpWm.js → useInterval-CWBTUy5W.js} +1 -1
- package/dist/assets/useNotebookActions-jWIcmpv0.js +12 -0
- package/dist/assets/{useRunCells-CSSF_mJ0.js → useRunCells-C2dvU2jA.js} +1 -1
- package/dist/assets/{useSplitCell-9Qz93yPZ.js → useSplitCell-C1tubdNi.js} +1 -1
- package/dist/assets/{useTheme-KXqAyDPC.js → useTheme-Bxv9B31g.js} +1 -1
- package/dist/assets/{utils-Z80SV4_d.js → utils-BWBBpCkT.js} +1 -1
- package/dist/assets/{vega-component-Cie3ag_T.js → vega-component-Db8KjZJG.js} +1 -1
- package/dist/assets/{write-secret-modal-DEEn7sZV.js → write-secret-modal-3-v-dHVj.js} +1 -1
- package/dist/index.html +68 -68
- package/package.json +3 -4
- package/src/__mocks__/requests.ts +45 -4
- package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
- package/src/components/data-table/column-summary/utils.ts +26 -7
- package/src/components/data-table/types.ts +1 -1
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
- package/src/components/editor/actions/__tests__/pdf-export.test.ts +0 -4
- package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
- package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
- package/src/components/editor/actions/pdf-export.ts +1 -4
- package/src/components/editor/actions/useNotebookActions.tsx +7 -13
- package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
- package/src/components/editor/cell/code/cell-editor.tsx +13 -7
- package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
- package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +50 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +87 -8
- package/src/components/editor/connections/storage/as-code.ts +40 -21
- package/src/components/editor/connections/storage/schemas.ts +83 -42
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
- package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
- package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
- package/src/components/slides/reveal-component.tsx +25 -19
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slide-scroll-hint.tsx +161 -0
- package/src/components/slides/slides.css +4 -2
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/core/cells/__tests__/cells.test.ts +12 -2
- package/src/core/cells/__tests__/logs.test.ts +5 -5
- package/src/core/cells/cells.ts +17 -4
- package/src/core/cells/document-changes.ts +1 -0
- package/src/core/cells/logs.ts +14 -4
- package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
- package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
- package/src/core/codemirror/copilot/language-server.ts +182 -70
- package/src/core/codemirror/copilot/transport.ts +15 -207
- package/src/core/codemirror/copilot/types.ts +9 -2
- package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
- package/src/core/codemirror/find-replace/navigate.ts +2 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
- package/src/core/codemirror/go-to-definition/commands.ts +3 -0
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
- package/src/core/codemirror/language/languages/python.ts +23 -11
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
- package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
- package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
- package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
- package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
- package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
- package/src/core/codemirror/lsp/log-messages.ts +37 -0
- package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
- package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
- package/src/core/codemirror/lsp/transports.ts +8 -1
- package/src/core/codemirror/lsp/types.ts +68 -8
- package/src/core/codemirror/lsp/utils.ts +14 -0
- package/src/core/codemirror/utils.ts +28 -0
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bridge.ts +16 -0
- package/src/core/lsp/__tests__/transport.test.ts +509 -337
- package/src/core/lsp/transport.ts +304 -132
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +32 -1
- package/src/core/network/__tests__/requests-network.test.ts +21 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +14 -8
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +22 -6
- package/src/core/rtc/state.ts +9 -0
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/bridge.ts +4 -2
- package/src/core/wasm/worker/types.ts +5 -2
- package/src/css/app/codemirror.css +16 -2
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/src/stories/log-viewer.stories.tsx +12 -6
- package/src/utils/__tests__/download.test.tsx +24 -1
- package/src/utils/download.ts +11 -6
- package/dist/assets/RenderHTML-BCvU9dYz.js +0 -1
- package/dist/assets/add-connection-dialog-B-jCRNTN.js +0 -170
- package/dist/assets/ai-model-dropdown-DXgLphFa.js +0 -5
- package/dist/assets/cell-link-DqBkyAFe.js +0 -1
- package/dist/assets/cells-Cf7bHHq3.js +0 -229
- package/dist/assets/components-BH6gDVv6.js +0 -1
- package/dist/assets/config-BbOTTCYY.js +0 -1
- package/dist/assets/dist-BCMBu0rO.js +0 -2
- package/dist/assets/download-BomYt3nl.js +0 -9
- package/dist/assets/error-entries-Birw-8VX.js +0 -8
- package/dist/assets/floating-outline-wz-WSRgP.js +0 -1
- package/dist/assets/focus-DiEyfiAG.js +0 -1
- package/dist/assets/home-page-DJ4jJsZj.js +0 -4
- package/dist/assets/index-A71LAt6h.css +0 -2
- package/dist/assets/index-CMZzy-dF.js +0 -38
- package/dist/assets/kiosk-mode-ABm3oYgq.js +0 -1
- package/dist/assets/logs-panel-iFV4ZJ9b.js +0 -1
- package/dist/assets/node-sql-parser-BWgUjcfV.js +0 -68
- package/dist/assets/pair-with-agent-modal-CKOYwOYw.js +0 -4
- package/dist/assets/panels-Dj45fW-v.js +0 -4
- package/dist/assets/process-output-BEhIGfkO.js +0 -1
- package/dist/assets/scratchpad-panel-B0mEqBNk.js +0 -1
- package/dist/assets/slides-MCbMDtlY.css +0 -1
- package/dist/assets/state-BZQc7xxL.js +0 -3
- package/dist/assets/switch-CvweaCXq.js +0 -1
- package/dist/assets/useCellActionButton-CRfIEVB5.js +0 -1
- package/dist/assets/useEventListener-B_w3A283.js +0 -1
- package/dist/assets/useNotebookActions-DTXAoukG.js +0 -12
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type JSONRPCMessage,
|
|
5
|
+
type Transport,
|
|
6
|
+
WebSocketTransport,
|
|
7
|
+
} from "@marimo-team/codemirror-languageserver";
|
|
5
8
|
import { Logger } from "@/utils/Logger";
|
|
6
9
|
|
|
10
|
+
const MAX_PENDING_MESSAGES = 1000;
|
|
11
|
+
const RESYNCED_NOTIFICATION_METHODS = new Set([
|
|
12
|
+
"textDocument/didOpen",
|
|
13
|
+
"textDocument/didChange",
|
|
14
|
+
"textDocument/didClose",
|
|
15
|
+
]);
|
|
16
|
+
const RESTORATION_METHODS = new Set([
|
|
17
|
+
"initialize",
|
|
18
|
+
"initialized",
|
|
19
|
+
"workspace/didChangeConfiguration",
|
|
20
|
+
"textDocument/didOpen",
|
|
21
|
+
]);
|
|
22
|
+
|
|
7
23
|
export interface ReconnectingWebSocketTransportOptions {
|
|
8
24
|
/**
|
|
9
25
|
* Function that returns the WebSocket URL to connect to.
|
|
@@ -21,183 +37,339 @@ export interface ReconnectingWebSocketTransportOptions {
|
|
|
21
37
|
* This allows the LSP client to re-synchronize state (e.g., re-send document open notifications).
|
|
22
38
|
*/
|
|
23
39
|
onReconnect?: () => Promise<void>;
|
|
24
|
-
}
|
|
25
40
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Number of connection attempts in each retry cycle.
|
|
43
|
+
* @default 1
|
|
44
|
+
*/
|
|
45
|
+
retries?: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Delay between connection attempts.
|
|
49
|
+
* @default 1000
|
|
50
|
+
*/
|
|
51
|
+
retryDelayMs?: number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Called when every connection attempt in a retry cycle fails.
|
|
55
|
+
*/
|
|
56
|
+
onConnectionFailure?: (error: Error) => void;
|
|
29
57
|
}
|
|
30
58
|
|
|
31
59
|
/**
|
|
32
|
-
* A WebSocket transport that automatically reconnects when the connection is
|
|
33
|
-
*
|
|
60
|
+
* A WebSocket transport that automatically reconnects when the connection is
|
|
61
|
+
* lost. Messages produced while reconnecting are replayed only after the LSP
|
|
62
|
+
* client has re-initialized and restored its document state.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately does not implement the optional `Transport.onClose`: JSONRPCClient
|
|
65
|
+
* latches the first close error and never clears it, so forwarding drops would
|
|
66
|
+
* fail every later request instead of surviving a reconnect.
|
|
34
67
|
*/
|
|
35
|
-
export class ReconnectingWebSocketTransport
|
|
68
|
+
export class ReconnectingWebSocketTransport implements Transport {
|
|
36
69
|
private delegate: WebSocketTransport | undefined;
|
|
37
|
-
private readonly options: ReconnectingWebSocketTransportOptions;
|
|
38
70
|
private connectionPromise: Promise<void> | undefined;
|
|
71
|
+
private isDelegateConnected = false;
|
|
72
|
+
/**
|
|
73
|
+
* Identifies the current connection attempt. Bumping it abandons whatever is
|
|
74
|
+
* in flight: an attempt whose socket has already died may stay parked on an
|
|
75
|
+
* LSP request timeout, and it must neither delay recovery nor mutate state
|
|
76
|
+
* belonging to the attempt that replaced it.
|
|
77
|
+
*/
|
|
78
|
+
private connectionGeneration = 0;
|
|
79
|
+
/** Generation currently restoring LSP state, if any. */
|
|
80
|
+
private restoringGeneration: number | undefined;
|
|
39
81
|
private isClosed = false;
|
|
40
82
|
private hasConnectedBefore = false;
|
|
41
|
-
private
|
|
83
|
+
private readonly messageHandlers = new Set<
|
|
84
|
+
(message: JSONRPCMessage) => void
|
|
85
|
+
>();
|
|
86
|
+
private pendingMessages: JSONRPCMessage[] = [];
|
|
87
|
+
/**
|
|
88
|
+
* Backlog detached by the in-flight attempt. Held here rather than in a local
|
|
89
|
+
* so that abandoning that attempt can hand the messages straight back to the
|
|
90
|
+
* queue instead of stranding them until it eventually settles.
|
|
91
|
+
*/
|
|
92
|
+
private inFlightBacklog: JSONRPCMessage[] | undefined;
|
|
93
|
+
private hasLoggedQueueOverflow = false;
|
|
94
|
+
private disposeDelegateMessage: (() => void) | undefined;
|
|
95
|
+
private disposeDelegateClose: (() => void) | undefined;
|
|
96
|
+
private readonly options: Required<
|
|
97
|
+
Pick<ReconnectingWebSocketTransportOptions, "retries" | "retryDelayMs">
|
|
98
|
+
> &
|
|
99
|
+
Omit<ReconnectingWebSocketTransportOptions, "retries" | "retryDelayMs">;
|
|
100
|
+
|
|
101
|
+
public onReconnect: (() => Promise<void>) | undefined;
|
|
42
102
|
|
|
43
103
|
constructor(options: ReconnectingWebSocketTransportOptions) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
104
|
+
this.options = {
|
|
105
|
+
...options,
|
|
106
|
+
retries: options.retries ?? 1,
|
|
107
|
+
retryDelayMs: options.retryDelayMs ?? 1000,
|
|
108
|
+
};
|
|
109
|
+
this.onReconnect = options.onReconnect;
|
|
47
110
|
}
|
|
48
111
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
private createDelegate(): WebSocketTransport {
|
|
53
|
-
// Close the old delegate if it exists
|
|
54
|
-
if (this.delegate) {
|
|
55
|
-
try {
|
|
56
|
-
this.delegate.close();
|
|
57
|
-
} catch (error) {
|
|
58
|
-
Logger.warn("Error closing old WebSocket delegate", error);
|
|
59
|
-
}
|
|
112
|
+
async connect(): Promise<void> {
|
|
113
|
+
if (this.isClosed) {
|
|
114
|
+
throw new Error("Transport is closed");
|
|
60
115
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
for (const { event, handler } of this.pendingSubscriptions) {
|
|
67
|
-
this.delegate.subscribe(event, handler);
|
|
116
|
+
if (this.isDelegateConnected) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (this.connectionPromise) {
|
|
120
|
+
return this.connectionPromise;
|
|
68
121
|
}
|
|
69
122
|
|
|
70
|
-
|
|
123
|
+
const generation = ++this.connectionGeneration;
|
|
124
|
+
const promise = this.connectWithRetries(generation).finally(() => {
|
|
125
|
+
// An abandoned attempt must not clear its successor's promise.
|
|
126
|
+
if (this.connectionPromise === promise) {
|
|
127
|
+
this.connectionPromise = undefined;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
this.connectionPromise = promise;
|
|
131
|
+
return promise;
|
|
71
132
|
}
|
|
72
133
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
134
|
+
/** Whether `generation` has been superseded or the transport was closed. */
|
|
135
|
+
private isStale(generation: number): boolean {
|
|
136
|
+
return this.isClosed || generation !== this.connectionGeneration;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
send(message: JSONRPCMessage): void {
|
|
140
|
+
if (this.isClosed) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
this.delegate &&
|
|
145
|
+
this.isDelegateConnected &&
|
|
146
|
+
(this.restoringGeneration === undefined ||
|
|
147
|
+
this.canSendWhileRestoring(message))
|
|
148
|
+
) {
|
|
149
|
+
this.delegate.send(message);
|
|
150
|
+
return;
|
|
79
151
|
}
|
|
80
152
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!
|
|
84
|
-
|
|
153
|
+
if (this.pendingMessages.length < MAX_PENDING_MESSAGES) {
|
|
154
|
+
this.pendingMessages.push(message);
|
|
155
|
+
} else if (!this.hasLoggedQueueOverflow) {
|
|
156
|
+
// Log once per episode; the queue fills one keystroke at a time.
|
|
157
|
+
this.hasLoggedQueueOverflow = true;
|
|
158
|
+
Logger.error(
|
|
159
|
+
`LSP reconnect queue is full (${MAX_PENDING_MESSAGES}); dropping messages until it drains`,
|
|
160
|
+
);
|
|
85
161
|
}
|
|
162
|
+
this.reconnect();
|
|
163
|
+
}
|
|
86
164
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
165
|
+
onMessage(handler: (message: JSONRPCMessage) => void): () => void {
|
|
166
|
+
this.messageHandlers.add(handler);
|
|
167
|
+
return () => {
|
|
168
|
+
this.messageHandlers.delete(handler);
|
|
169
|
+
};
|
|
91
170
|
}
|
|
92
171
|
|
|
93
|
-
|
|
94
|
-
// Don't reconnect if explicitly closed
|
|
172
|
+
close(): void {
|
|
95
173
|
if (this.isClosed) {
|
|
96
|
-
|
|
174
|
+
return;
|
|
97
175
|
}
|
|
176
|
+
this.isClosed = true;
|
|
177
|
+
this.isDelegateConnected = false;
|
|
178
|
+
this.restoringGeneration = undefined;
|
|
179
|
+
// Abandon anything in flight so it cannot touch state after teardown.
|
|
180
|
+
this.connectionGeneration++;
|
|
181
|
+
this.connectionPromise = undefined;
|
|
182
|
+
this.pendingMessages = [];
|
|
183
|
+
this.inFlightBacklog = undefined;
|
|
184
|
+
this.messageHandlers.clear();
|
|
185
|
+
this.disposeDelegate();
|
|
186
|
+
this.delegate?.close();
|
|
187
|
+
this.delegate = undefined;
|
|
188
|
+
}
|
|
98
189
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return this.connectionPromise;
|
|
102
|
-
}
|
|
190
|
+
private async connectWithRetries(generation: number): Promise<void> {
|
|
191
|
+
let lastError: Error | undefined;
|
|
103
192
|
|
|
104
|
-
|
|
193
|
+
for (let attempt = 1; attempt <= this.options.retries; attempt++) {
|
|
105
194
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
delegate = this.createDelegate();
|
|
195
|
+
await this.connectDelegate(generation);
|
|
196
|
+
return;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
199
|
+
if (this.isStale(generation)) {
|
|
200
|
+
// Superseded while we were failing; the live attempt owns the
|
|
201
|
+
// delegate now, so tear down nothing and retry nothing.
|
|
202
|
+
throw lastError;
|
|
115
203
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Logger.log("Calling onReconnect callback to re-synchronize state");
|
|
127
|
-
await this.options.onReconnect();
|
|
204
|
+
this.invalidateDelegate();
|
|
205
|
+
Logger.warn(
|
|
206
|
+
`WebSocket connection attempt ${attempt}/${this.options.retries} failed`,
|
|
207
|
+
lastError,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (attempt < this.options.retries) {
|
|
211
|
+
await new Promise((resolve) =>
|
|
212
|
+
setTimeout(resolve, this.options.retryDelayMs),
|
|
213
|
+
);
|
|
128
214
|
}
|
|
129
|
-
} catch (error) {
|
|
130
|
-
Logger.error("WebSocket transport connection failed", error);
|
|
131
|
-
// Clear the delegate on failure so we create a new one on retry
|
|
132
|
-
this.delegate = undefined;
|
|
133
|
-
throw error;
|
|
134
|
-
} finally {
|
|
135
|
-
this.connectionPromise = undefined;
|
|
136
215
|
}
|
|
137
|
-
}
|
|
216
|
+
}
|
|
138
217
|
|
|
139
|
-
|
|
218
|
+
const error = lastError ?? new Error("WebSocket connection failed");
|
|
219
|
+
this.options.onConnectionFailure?.(error);
|
|
220
|
+
throw error;
|
|
140
221
|
}
|
|
141
222
|
|
|
142
|
-
|
|
143
|
-
this.
|
|
144
|
-
this.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
override subscribe(...args: Parameters<Transport["subscribe"]>): void {
|
|
150
|
-
// Register handler on parent Transport
|
|
151
|
-
super.subscribe(...args);
|
|
223
|
+
private async connectDelegate(generation: number): Promise<void> {
|
|
224
|
+
await this.options.waitForConnection?.();
|
|
225
|
+
if (this.isStale(generation)) {
|
|
226
|
+
throw new Error("Connection attempt abandoned");
|
|
227
|
+
}
|
|
152
228
|
|
|
153
|
-
const
|
|
229
|
+
const delegate = this.createDelegate();
|
|
230
|
+
await delegate.connect();
|
|
231
|
+
if (this.isStale(generation) || this.delegate !== delegate) {
|
|
232
|
+
delegate.close();
|
|
233
|
+
throw new Error("Transport closed while connecting");
|
|
234
|
+
}
|
|
154
235
|
|
|
155
|
-
|
|
156
|
-
|
|
236
|
+
this.isDelegateConnected = true;
|
|
237
|
+
const isReconnection = this.hasConnectedBefore;
|
|
238
|
+
this.hasConnectedBefore = true;
|
|
239
|
+
|
|
240
|
+
// Detach before restoring: notifications queued while the socket was down
|
|
241
|
+
// are superseded by what `onReconnect` re-sends, but ones produced *during*
|
|
242
|
+
// restoration are not — they carry edits the restore snapshot never saw,
|
|
243
|
+
// and the notebook snapshotter will not re-emit them.
|
|
244
|
+
const queuedWhileDisconnected = this.detachPendingMessages();
|
|
245
|
+
this.inFlightBacklog = queuedWhileDisconnected;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
if (isReconnection) {
|
|
249
|
+
this.restoringGeneration = generation;
|
|
250
|
+
try {
|
|
251
|
+
await this.onReconnect?.();
|
|
252
|
+
} finally {
|
|
253
|
+
// Only clear the gate if a newer attempt hasn't taken it over.
|
|
254
|
+
if (this.restoringGeneration === generation) {
|
|
255
|
+
this.restoringGeneration = undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
157
259
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
260
|
+
if (
|
|
261
|
+
this.isStale(generation) ||
|
|
262
|
+
this.delegate !== delegate ||
|
|
263
|
+
!this.isDelegateConnected
|
|
264
|
+
) {
|
|
265
|
+
throw new Error("WebSocket closed while restoring LSP state");
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
// Nothing superseded the backlog, so restore it ahead of anything queued
|
|
269
|
+
// since. If this attempt was abandoned, whoever abandoned it already
|
|
270
|
+
// reclaimed the backlog and owns the queue now.
|
|
271
|
+
if (!this.isStale(generation)) {
|
|
272
|
+
this.reclaimInFlightBacklog();
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
161
275
|
}
|
|
162
|
-
}
|
|
163
276
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
277
|
+
const replayable =
|
|
278
|
+
isReconnection && this.onReconnect
|
|
279
|
+
? queuedWhileDisconnected.filter(
|
|
280
|
+
(message) =>
|
|
281
|
+
!(
|
|
282
|
+
"method" in message &&
|
|
283
|
+
RESYNCED_NOTIFICATION_METHODS.has(message.method)
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
: queuedWhileDisconnected;
|
|
287
|
+
|
|
288
|
+
this.inFlightBacklog = undefined;
|
|
289
|
+
for (const message of [...replayable, ...this.detachPendingMessages()]) {
|
|
290
|
+
delegate.send(message);
|
|
291
|
+
}
|
|
169
292
|
|
|
170
|
-
|
|
293
|
+
Logger.log("WebSocket transport connected successfully");
|
|
294
|
+
}
|
|
171
295
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
296
|
+
private detachPendingMessages(): JSONRPCMessage[] {
|
|
297
|
+
const messages = this.pendingMessages;
|
|
298
|
+
this.pendingMessages = [];
|
|
299
|
+
this.hasLoggedQueueOverflow = false;
|
|
300
|
+
return messages;
|
|
301
|
+
}
|
|
176
302
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
303
|
+
/** Return the in-flight attempt's backlog to the front of the queue. */
|
|
304
|
+
private reclaimInFlightBacklog(): void {
|
|
305
|
+
if (!this.inFlightBacklog) {
|
|
306
|
+
return;
|
|
180
307
|
}
|
|
308
|
+
this.pendingMessages = [
|
|
309
|
+
...this.inFlightBacklog,
|
|
310
|
+
...this.pendingMessages,
|
|
311
|
+
].slice(0, MAX_PENDING_MESSAGES);
|
|
312
|
+
this.inFlightBacklog = undefined;
|
|
313
|
+
}
|
|
181
314
|
|
|
182
|
-
|
|
315
|
+
private canSendWhileRestoring(message: JSONRPCMessage): boolean {
|
|
316
|
+
// Responses to server-initiated requests must not be held or the
|
|
317
|
+
// initialization handshake could deadlock.
|
|
318
|
+
return !("method" in message) || RESTORATION_METHODS.has(message.method);
|
|
183
319
|
}
|
|
184
320
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
321
|
+
private createDelegate(): WebSocketTransport {
|
|
322
|
+
this.invalidateDelegate();
|
|
323
|
+
|
|
324
|
+
const delegate = new WebSocketTransport(this.options.getWsUrl());
|
|
325
|
+
this.delegate = delegate;
|
|
326
|
+
this.disposeDelegateMessage = delegate.onMessage((message) => {
|
|
327
|
+
for (const handler of this.messageHandlers) {
|
|
328
|
+
try {
|
|
329
|
+
handler(message);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
Logger.error("LSP message handler failed", error);
|
|
332
|
+
}
|
|
197
333
|
}
|
|
198
|
-
}
|
|
334
|
+
});
|
|
335
|
+
this.disposeDelegateClose = delegate.onClose((error) => {
|
|
336
|
+
if (this.delegate !== delegate || this.isClosed) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
Logger.warn("WebSocket transport connection closed", error);
|
|
340
|
+
this.isDelegateConnected = false;
|
|
341
|
+
this.disposeDelegate();
|
|
342
|
+
this.delegate = undefined;
|
|
343
|
+
// Abandon any attempt still in flight: it is bound to the socket that
|
|
344
|
+
// just died and may sit parked on an LSP request timeout. Take its
|
|
345
|
+
// backlog and drop the promise so the next connect() starts immediately
|
|
346
|
+
// rather than joining a doomed one.
|
|
347
|
+
this.restoringGeneration = undefined;
|
|
348
|
+
this.reclaimInFlightBacklog();
|
|
349
|
+
this.connectionPromise = undefined;
|
|
350
|
+
queueMicrotask(() => this.reconnect());
|
|
351
|
+
});
|
|
352
|
+
return delegate;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private reconnect(): void {
|
|
356
|
+
void this.connect().catch((error) => {
|
|
357
|
+
Logger.error("WebSocket transport reconnection failed", error);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private invalidateDelegate(): void {
|
|
362
|
+
this.isDelegateConnected = false;
|
|
363
|
+
this.restoringGeneration = undefined;
|
|
364
|
+
this.disposeDelegate();
|
|
365
|
+
this.delegate?.close();
|
|
366
|
+
this.delegate = undefined;
|
|
367
|
+
}
|
|
199
368
|
|
|
200
|
-
|
|
201
|
-
|
|
369
|
+
private disposeDelegate(): void {
|
|
370
|
+
this.disposeDelegateMessage?.();
|
|
371
|
+
this.disposeDelegateMessage = undefined;
|
|
372
|
+
this.disposeDelegateClose?.();
|
|
373
|
+
this.disposeDelegateClose = undefined;
|
|
202
374
|
}
|
|
203
375
|
}
|
package/src/core/mode.ts
CHANGED
|
@@ -78,6 +78,16 @@ export const viewStateAtom = atom<ViewState>({
|
|
|
78
78
|
|
|
79
79
|
export const initialModeAtom = atom<AppMode | undefined>(undefined);
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Whether the current page hosts a notebook, and so connects to a kernel.
|
|
83
|
+
* False for non-notebook pages (home, gallery), which are served without a
|
|
84
|
+
* session.
|
|
85
|
+
*/
|
|
86
|
+
export function isNotebookPage(): boolean {
|
|
87
|
+
const mode = store.get(initialModeAtom);
|
|
88
|
+
return mode !== "home" && mode !== "gallery";
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export const kioskModeAtom = atom<boolean>(false);
|
|
82
92
|
|
|
83
93
|
/**
|
|
@@ -94,4 +94,46 @@ describe("API", () => {
|
|
|
94
94
|
}),
|
|
95
95
|
).rejects.toBeInstanceOf(HTTPError);
|
|
96
96
|
});
|
|
97
|
+
|
|
98
|
+
it("preserves export metadata from response headers", async () => {
|
|
99
|
+
const response = new Response("markdown", {
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Disposition":
|
|
102
|
+
"attachment; filename*=UTF-8''r%C3%A9sum%C3%A9.qmd",
|
|
103
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await expect(
|
|
108
|
+
API.handleExportResponse({ data: "markdown", response }),
|
|
109
|
+
).resolves.toEqual({
|
|
110
|
+
contents: "markdown",
|
|
111
|
+
filename: "résumé.qmd",
|
|
112
|
+
mediaType: "text/plain; charset=utf-8",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects export responses without a filename", async () => {
|
|
117
|
+
await expect(
|
|
118
|
+
API.handleExportResponse({
|
|
119
|
+
data: "markdown",
|
|
120
|
+
response: new Response(null, {
|
|
121
|
+
headers: { "Content-Type": "text/plain" },
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
).rejects.toThrow("Export response is missing a filename");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects export responses without a media type", async () => {
|
|
128
|
+
await expect(
|
|
129
|
+
API.handleExportResponse({
|
|
130
|
+
data: "markdown",
|
|
131
|
+
response: new Response(null, {
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Disposition": "attachment; filename*=UTF-8''notebook.md",
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
}),
|
|
137
|
+
).rejects.toThrow("Export response is missing a media type");
|
|
138
|
+
});
|
|
97
139
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { initialModeAtom } from "@/core/mode";
|
|
5
|
+
import { store } from "@/core/state/jotai";
|
|
6
|
+
import { WebSocketState } from "../../websocket/types";
|
|
7
|
+
import { connectionAtom, waitForConnectionOpenIfNotebook } from "../connection";
|
|
8
|
+
|
|
9
|
+
const NEVER = Symbol("never");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves to NEVER if the promise does not settle within a macrotask.
|
|
13
|
+
*/
|
|
14
|
+
function settledOrNever(promise: Promise<unknown>) {
|
|
15
|
+
return Promise.race([
|
|
16
|
+
promise,
|
|
17
|
+
new Promise((resolve) => setTimeout(() => resolve(NEVER), 0)),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("waitForConnectionOpenIfNotebook", () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
store.set(connectionAtom, { state: WebSocketState.NOT_STARTED });
|
|
24
|
+
store.set(initialModeAtom, undefined);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each(["home", "gallery"] as const)(
|
|
28
|
+
"resolves without a connection in %s mode",
|
|
29
|
+
async (mode) => {
|
|
30
|
+
store.set(initialModeAtom, mode);
|
|
31
|
+
await expect(
|
|
32
|
+
settledOrNever(waitForConnectionOpenIfNotebook()),
|
|
33
|
+
).resolves.not.toBe(NEVER);
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
it("waits for the connection to open in edit mode", async () => {
|
|
38
|
+
store.set(initialModeAtom, "edit");
|
|
39
|
+
const promise = waitForConnectionOpenIfNotebook();
|
|
40
|
+
await expect(settledOrNever(promise)).resolves.toBe(NEVER);
|
|
41
|
+
|
|
42
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
43
|
+
await expect(settledOrNever(promise)).resolves.not.toBe(NEVER);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -4,7 +4,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
4
4
|
import { cellId, requestId, uiElementId } from "@/__tests__/branded";
|
|
5
5
|
import type { RuntimeManager } from "../../runtime/runtime";
|
|
6
6
|
import { createLazyRequests } from "../requests-lazy";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
EditRequests,
|
|
9
|
+
ExportAvailabilityResponse,
|
|
10
|
+
RunRequests,
|
|
11
|
+
} from "../types";
|
|
8
12
|
|
|
9
13
|
// Mock the connection module
|
|
10
14
|
vi.mock("../connection", async () => {
|
|
@@ -72,6 +76,33 @@ describe("createLazyRequests", () => {
|
|
|
72
76
|
expect(mockDelegate.getEnvironmentInfo).toHaveBeenCalledOnce();
|
|
73
77
|
});
|
|
74
78
|
|
|
79
|
+
it("gets export availability without starting a kernel", async () => {
|
|
80
|
+
const availability: ExportAvailabilityResponse = {
|
|
81
|
+
source: "server",
|
|
82
|
+
formats: [
|
|
83
|
+
{
|
|
84
|
+
format: "ipynb",
|
|
85
|
+
dependenciesAvailable: false,
|
|
86
|
+
missingPackages: ["nbformat"],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
mockDelegate.getExportAvailability = vi
|
|
91
|
+
.fn()
|
|
92
|
+
.mockResolvedValue(availability) as EditRequests["getExportAvailability"];
|
|
93
|
+
|
|
94
|
+
const lazyRequests = createLazyRequests(
|
|
95
|
+
mockDelegate,
|
|
96
|
+
mockGetRuntimeManager,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await expect(lazyRequests.getExportAvailability()).resolves.toEqual(
|
|
100
|
+
availability,
|
|
101
|
+
);
|
|
102
|
+
expect(mockInit).not.toHaveBeenCalled();
|
|
103
|
+
expect(mockDelegate.getExportAvailability).toHaveBeenCalledOnce();
|
|
104
|
+
});
|
|
105
|
+
|
|
75
106
|
it("should call init once before first request", async () => {
|
|
76
107
|
const lazyRequests = createLazyRequests(
|
|
77
108
|
mockDelegate,
|