@marimo-team/islands 0.25.3-dev5 → 0.25.3-dev51

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.
Files changed (220) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-Da5OoEUL.js} +14 -14
  2. package/dist/{ErrorBoundary-BiZWOrv4.js → ErrorBoundary-CbxCC2kk.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-Bz3mrprX.js} +2 -2
  4. package/dist/any-language-editor-k2B8YI3Y.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-Cs5ujb9K.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
  8. package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
  9. package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-C1Gjgokv.js} +1103 -1103
  10. package/dist/{check-EDwSwP89.js → check-P35dc2dH.js} +1 -1
  11. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-CcTSaxZt.js} +1 -1
  12. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-DcJcjOaO.js} +1 -1
  13. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BVtDTjaF.js} +1 -1
  14. package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-BkDFkh8q.js} +1 -1
  15. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  16. package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-CfAt7HT5.js} +2 -2
  17. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CGEjNYa2.js} +2 -2
  18. package/dist/{code-visibility-CW5_8ZOI.js → code-visibility-DoZ9lxzx.js} +6 -6
  19. package/dist/{common-mzWw4FdI.js → common-D1PacyXO.js} +3112 -3015
  20. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-CPoxUG7G.js} +3 -3
  21. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-BW6NmfJi.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-CWg3CTuF.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-CJmehSdc.js → data-grid-overlay-editor-B50UpkXd.js} +5 -5
  24. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  25. package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BPxtoS-z.js} +1 -1
  26. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DA-vJdFL.js} +1 -1
  27. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-D6bkR84S.js} +1 -1
  28. package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-m60g4cAT.js} +1 -1
  29. package/dist/{dialog-CEBFZNyc.js → dialog-C1PcpCqX.js} +3 -3
  30. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  31. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-MG5hiCIa.js} +76 -76
  32. package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-CK99sho2.js} +2 -2
  33. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-Bryq0tdE.js} +1 -1
  34. package/dist/{error-banner-D-WCIiMt.js → error-banner-Sl2FlSJ4.js} +2 -2
  35. package/dist/{error-entries-DSvFWT_M.js → error-entries-CMNtA8NK.js} +959 -959
  36. package/dist/{feedback-button-BcT1FBud.js → feedback-button-DqPqwryB.js} +29 -29
  37. package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-D-3aRpfS.js} +2 -2
  38. package/dist/{formats-CeOmU1MU.js → formats-6pLWWrY2.js} +3 -3
  39. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  40. package/dist/{glide-data-editor-XckatD0j.js → glide-data-editor-BxMhUd3K.js} +370 -368
  41. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
  42. package/dist/{globals-jRrLCkrP.js → globals-DTJa_doJ.js} +3 -3
  43. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  44. package/dist/{html-to-image-B8s5SgGm.js → html-to-image-By72ALyI.js} +29 -29
  45. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-s1C1kgfU.js} +1 -1
  46. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-CCsRgolY.js} +1 -1
  47. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-BrmxDlcc.js} +1 -1
  48. package/dist/{label-BDKF3Afc.js → label-BRnUC0NX.js} +2 -2
  49. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  50. package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
  51. package/dist/main.js +2358 -2221
  52. package/dist/{mermaid-CHo49Yv-.js → mermaid-DKI0869I.js} +38 -38
  53. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  54. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-641xJysF.js} +1 -1
  55. package/dist/{number-overlay-editor-pZx8dUDV.js → number-overlay-editor-Z_H6x5b-.js} +3 -3
  56. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  57. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  58. package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-keVUAUeS.js} +2 -2
  59. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-DHktXPO_.js} +1 -1
  60. package/dist/precisionRound-DhAzeRcm.js +49 -0
  61. package/dist/{process-output-CKRXyszb.js → process-output-D-OMW5sV.js} +4 -4
  62. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  63. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-BVEF4OV_.js} +2 -2
  64. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  65. package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CWo-UAKV.js} +1 -1
  66. package/dist/{reveal-component-79ez8-D5.js → reveal-component-tAB5K3QE.js} +10 -10
  67. package/dist/{select-BButFi-d.js → select-B0LRDH0g.js} +4 -4
  68. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-CHQvLnq4.js} +1 -1
  69. package/dist/{spec-BwarBpUJ.js → spec-C-LRKGtT.js} +4 -4
  70. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-BOJZgbUJ.js} +2 -2
  71. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-C53XXdYH.js} +2 -2
  72. package/dist/style.css +1 -1
  73. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-D2o1gOtw.js} +1 -1
  74. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js} +2 -2
  75. package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
  76. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  77. package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-rv7QUa46.js} +1 -1
  78. package/dist/{toDate-vIXljnsr.js → toDate-CjPuEPou.js} +21 -20
  79. package/dist/{toast-B9UMEi8u.js → toast-CFWpihJU.js} +4 -4
  80. package/dist/{tooltip-BO1REbqs.js → tooltip-CKO1pHJo.js} +2 -2
  81. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-DBCITLeF.js} +1 -1
  82. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ChgNtdmE.js} +15 -14
  83. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-c_yJtkAe.js} +2 -2
  84. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Bet7kSsI.js} +1 -1
  85. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
  86. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-VVmz_24o.js} +3 -3
  87. package/dist/{useTheme-CRnjkku0.js → useTheme-DympCsQg.js} +5 -5
  88. package/dist/{vega-component-DTG6zxaY.js → vega-component-DnfQeoyt.js} +147 -147
  89. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  90. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  91. package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-D36Zk7t-.js} +1 -1
  92. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-BLBolvyQ.js} +1 -1
  93. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CfH57l0v.js} +2 -2
  94. package/package.json +4 -4
  95. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  96. package/src/__tests__/host-imports.test.ts +1 -0
  97. package/src/__tests__/mount.test.ts +8 -8
  98. package/src/components/app-config/ai-config.tsx +5 -1
  99. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  100. package/src/components/chat/chat-panel.tsx +3 -7
  101. package/src/components/chat/chat-utils.ts +4 -8
  102. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  103. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  104. package/src/components/data-table/__tests__/export-actions.test.tsx +417 -6
  105. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  106. package/src/components/data-table/columns.tsx +2 -1
  107. package/src/components/data-table/context-menu.tsx +36 -20
  108. package/src/components/data-table/date-popover.tsx +6 -4
  109. package/src/components/data-table/export-actions.tsx +263 -161
  110. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  111. package/src/components/data-table/schemas.ts +14 -36
  112. package/src/components/editor/Output.tsx +5 -1
  113. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  114. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  115. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
  116. package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
  117. package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
  118. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  119. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  120. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  121. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  122. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  123. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +277 -78
  124. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +196 -41
  125. package/src/components/editor/connections/components.tsx +1 -1
  126. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  127. package/src/components/editor/controls/command-palette.tsx +22 -10
  128. package/src/components/editor/dynamic-favicon.tsx +21 -40
  129. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +139 -36
  130. package/src/components/editor/errors/send-error-report-button.tsx +10 -7
  131. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  132. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -22
  133. package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
  134. package/src/components/pages/home-page.tsx +4 -4
  135. package/src/components/slides/slide-cell-view.tsx +1 -1
  136. package/src/components/tracing/tracing.tsx +2 -2
  137. package/src/core/attachments/__tests__/toast.test.tsx +58 -0
  138. package/src/core/attachments/state.ts +5 -8
  139. package/src/core/attachments/toast.ts +15 -0
  140. package/src/core/cells/__tests__/error-report.test.ts +53 -3
  141. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  142. package/src/core/cells/__tests__/session.test.ts +18 -3
  143. package/src/core/cells/cells.ts +2 -1
  144. package/src/core/cells/error-report.ts +14 -6
  145. package/src/core/cells/pending-delete-service.ts +2 -6
  146. package/src/core/cells/session.ts +19 -1
  147. package/src/core/config/pair.ts +5 -3
  148. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  149. package/src/core/datasets/data-source-discovery.ts +4 -6
  150. package/src/core/edit-app.tsx +2 -2
  151. package/src/core/host/AGENTS.md +11 -2
  152. package/src/core/host/__tests__/attachments.test.ts +56 -0
  153. package/src/core/host/__tests__/document.test.ts +126 -0
  154. package/src/core/host/__tests__/events.test.ts +49 -0
  155. package/src/core/host/__tests__/fixtures.ts +41 -0
  156. package/src/core/host/__tests__/notebook-client.test.ts +496 -0
  157. package/src/core/host/connection.ts +81 -0
  158. package/src/core/host/events.ts +68 -0
  159. package/src/core/host/notebook-client.ts +316 -0
  160. package/src/core/host/resources/attachments.ts +19 -0
  161. package/src/core/host/resources/document.ts +101 -0
  162. package/src/core/hotkeys/actions.ts +2 -4
  163. package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
  164. package/src/core/islands/bootstrap.ts +0 -1
  165. package/src/core/network/connection.ts +4 -2
  166. package/src/core/run-app.tsx +2 -2
  167. package/src/core/runtime/__tests__/runtime.test.ts +5 -12
  168. package/src/core/runtime/runtime.ts +5 -5
  169. package/src/core/static/files.ts +2 -0
  170. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  171. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  172. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
  173. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
  174. package/src/core/websocket/connection-state.ts +70 -0
  175. package/src/core/websocket/state.ts +7 -0
  176. package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
  177. package/src/core/websocket/transports/create.ts +31 -0
  178. package/src/core/websocket/transports/sse.ts +14 -2
  179. package/src/core/websocket/transports/transport.ts +12 -23
  180. package/src/core/websocket/types.ts +9 -39
  181. package/src/core/websocket/useNotebookClient.ts +78 -0
  182. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
  183. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  184. package/src/hooks/useAsyncData.ts +3 -3
  185. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  186. package/src/hooks/useElapsedTime.ts +3 -3
  187. package/src/hooks/useInputHistory.ts +4 -7
  188. package/src/hooks/useResizeObserver.ts +4 -7
  189. package/src/mount.tsx +3 -3
  190. package/src/plugins/core/registerReactComponent.tsx +1 -1
  191. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  192. package/src/plugins/impl/FormPlugin.tsx +1 -1
  193. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
  194. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  195. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  196. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  197. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  198. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  199. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  200. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  201. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  202. package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
  203. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  204. package/src/plugins/impl/panel/utils.ts +17 -0
  205. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  206. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  207. package/src/stories/editor.stories.tsx +24 -17
  208. package/src/utils/__tests__/arrays.test.ts +35 -0
  209. package/src/utils/__tests__/numbers.test.ts +48 -1
  210. package/src/utils/__tests__/objects.test.ts +21 -0
  211. package/src/utils/arrays.ts +10 -0
  212. package/src/utils/numbers.ts +28 -17
  213. package/src/utils/objects.ts +3 -7
  214. package/src/utils/once.ts +19 -19
  215. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  216. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  217. package/dist/precisionRound-DMIVQrt2.js +0 -97
  218. package/src/core/host/__tests__/protocol.test.ts +0 -214
  219. package/src/core/host/protocol.ts +0 -1943
  220. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -0,0 +1,49 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, expectTypeOf, it } from "vitest";
4
+ import { z } from "zod";
5
+ import { type NotebookEvent, parseNotebookEvent } from "../events";
6
+
7
+ describe("notebook messages", () => {
8
+ it("normalizes session and host envelopes for native marimo messages", () => {
9
+ const notification = {
10
+ op: "variables",
11
+ variables: [{ name: "x", value: Number.NaN }],
12
+ };
13
+ expectTypeOf<
14
+ Extract<NotebookEvent, { event: "marimo" }>["data"]["marimo"]
15
+ >().toBeUnknown();
16
+ const host = { event: "marimo", data: { marimo: notification } };
17
+ expect(parseNotebookEvent({ data: notification })).toEqual(host);
18
+ expect(parseNotebookEvent({ ...host, id: "42" })).toEqual({
19
+ ...host,
20
+ id: "42",
21
+ });
22
+ });
23
+
24
+ it("retains unknown event cursors without interpreting their payloads", () => {
25
+ expect(
26
+ parseNotebookEvent({ event: "future", id: "42", data: null }),
27
+ ).toEqual({ event: "unknown", name: "future", id: "42" });
28
+ });
29
+
30
+ it("decodes known events outside the client's subscription filters", () => {
31
+ const event = {
32
+ event: "run.removed",
33
+ id: "42",
34
+ data: { id: "r1", time: "2026-10-19T12:00:00Z" },
35
+ };
36
+ expect(parseNotebookEvent(event)).toEqual(event);
37
+ });
38
+
39
+ it.each([
40
+ { event: "cell", id: "42", data: { cell: { id: "c1" } } },
41
+ { event: "run.removed", id: "42", data: {} },
42
+ { event: "ready", data: {} },
43
+ { event: "marimo", data: {} },
44
+ {},
45
+ null,
46
+ ])("rejects malformed known messages: %j", (message) => {
47
+ expect(() => parseNotebookEvent(message)).toThrow(z.ZodError);
48
+ });
49
+ });
@@ -0,0 +1,41 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import NodeFs from "node:fs";
4
+ import NodeUrl from "node:url";
5
+ import * as Protocol from "@marimo-team/client/protocol";
6
+
7
+ /** Read the protocol's portable examples as transport messages. */
8
+ export function fixtureMessages(name: string) {
9
+ const path = new NodeUrl.URL(
10
+ `../../../../../protocols/host/fixtures/${name}`,
11
+ import.meta.url,
12
+ );
13
+ return NodeFs.readFileSync(path, "utf8")
14
+ .split(/\r?\n\r?\n/)
15
+ .flatMap((frame) => {
16
+ const event = /^event: (.+)$/m.exec(frame)?.[1];
17
+ if (!event) {
18
+ return [];
19
+ }
20
+ const data = frame
21
+ .split(/\r?\n/)
22
+ .filter((line) => line.startsWith("data:"))
23
+ .map((line) => line.slice(5).trimStart())
24
+ .join("\n");
25
+ const payload: unknown = JSON.parse(data);
26
+ return [
27
+ {
28
+ event,
29
+ id: /^id: (.+)$/m.exec(frame)?.[1],
30
+ data: payload,
31
+ },
32
+ ];
33
+ });
34
+ }
35
+
36
+ /** Read the protocol's portable examples as decoded protocol events. */
37
+ export function fixtureEvents(name: string): Protocol.EventOutput[] {
38
+ return fixtureMessages(name).map(({ id, ...message }) =>
39
+ Protocol.Event.parse(id === undefined ? message : { ...message, id }),
40
+ );
41
+ }
@@ -0,0 +1,496 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { createStore } from "jotai";
6
+ import { ConnectionState } from "../../websocket/types";
7
+ import { Deferred } from "@/utils/Deferred";
8
+ import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
9
+ import { classifyCloseEvent } from "../../websocket/connection-state";
10
+ import { createNotebookClient } from "../notebook-client";
11
+ import { fixtureEvents, fixtureMessages } from "./fixtures";
12
+
13
+ function notebookMessage(id: string) {
14
+ const notebook = {
15
+ id,
16
+ project_id: "p1",
17
+ path: null,
18
+ name: id,
19
+ operations: [],
20
+ updated_at: null,
21
+ };
22
+ return JSON.stringify({ event: "notebook", data: { notebook } });
23
+ }
24
+
25
+ function setup(url: () => string = () => "ws://localhost/ws") {
26
+ const events = new EventTarget();
27
+ const healthy = new Deferred<void>();
28
+ let provider = url;
29
+ const urls: URL[] = [];
30
+ const transport = {
31
+ readyState: WebSocket.CLOSED,
32
+ reconnect: vi.fn(() => {
33
+ urls.push(new URL(provider()));
34
+ }),
35
+ close: vi.fn(() => events.dispatchEvent(new CloseEvent("close"))),
36
+ send: vi.fn(),
37
+ addEventListener: events.addEventListener.bind(events),
38
+ removeEventListener: events.removeEventListener.bind(events),
39
+ };
40
+ const onNotification = vi.fn();
41
+ const checkHealth = vi.fn(async () => true);
42
+ const store = createStore();
43
+ const controller = createNotebookClient(store);
44
+ const client = controller.client;
45
+ const connect = (signal: AbortSignal) =>
46
+ controller.connect({
47
+ createTransport: (next) => {
48
+ provider = next;
49
+ return transport;
50
+ },
51
+ url,
52
+ signal,
53
+ waitUntilReady: () => healthy.promise,
54
+ checkHealth,
55
+ onNotification: (notification) => onNotification({ data: notification }),
56
+ decodeJson: jsonParseWithSpecialChar,
57
+ classifyClose: classifyCloseEvent,
58
+ });
59
+ return {
60
+ events,
61
+ healthy,
62
+ transport,
63
+ client,
64
+ connect,
65
+ onNotification,
66
+ store,
67
+ checkHealth,
68
+ urls,
69
+ receive: (message: unknown) =>
70
+ events.dispatchEvent(
71
+ new MessageEvent("message", { data: JSON.stringify(message) }),
72
+ ),
73
+ };
74
+ }
75
+
76
+ describe("NotebookClient lifetime", () => {
77
+ it("exposes identity and capabilities only after the notebook is ready", async () => {
78
+ const { connect, healthy, events, receive, client, store } = setup();
79
+ const lifetime = new AbortController();
80
+ connect(lifetime.signal);
81
+ healthy.resolve();
82
+ await healthy.promise;
83
+ events.dispatchEvent(new Event("open"));
84
+ const notebook = {
85
+ id: "nb/with space",
86
+ project_id: "p1",
87
+ path: null,
88
+ name: "Notebook",
89
+ operations: ["notebook.edit"],
90
+ updated_at: null,
91
+ };
92
+ receive({ event: "notebook", data: { notebook } });
93
+ expect(store.get(client.notebookAtom)).toBeNull();
94
+ receive({ event: "ready", id: "1", data: {} });
95
+ expect(store.get(client.notebookAtom)).toEqual(notebook);
96
+ receive({
97
+ event: "notebook",
98
+ id: "2",
99
+ data: { notebook: { ...notebook, operations: [] } },
100
+ });
101
+ expect(store.get(client.notebookAtom)?.operations).toEqual([]);
102
+ receive({ event: "notebook.removed", id: "3", data: { id: notebook.id } });
103
+ expect(store.get(client.notebookAtom)).toBeNull();
104
+ lifetime.abort();
105
+ });
106
+
107
+ it("rebuilds the replica after a reconnect reset selects a new notebook", async () => {
108
+ const notebookId = "n2";
109
+ const { connect, events, healthy, urls, receive, client, store } = setup(
110
+ () => "ws://localhost/ws?session_id=visitor",
111
+ );
112
+ const lifetime = new AbortController();
113
+ connect(lifetime.signal);
114
+ healthy.resolve();
115
+ await healthy.promise;
116
+ events.dispatchEvent(new Event("open"));
117
+ fixtureMessages("notebook-events.sse").forEach(receive);
118
+ expect(store.get(client.documentAtom)?.revision.notebook_id).toBe("n1");
119
+
120
+ events.dispatchEvent(new CloseEvent("close"));
121
+ expect(urls.at(-1)?.searchParams.get("cursor")).toBe("97");
122
+ expect(store.get(client.documentAtom)?.revision.notebook_id).toBe("n1");
123
+ expect(store.get(client.notebookAtom)).toBeNull();
124
+ events.dispatchEvent(new Event("open"));
125
+ receive({ event: "reset", data: {} });
126
+ expect(store.get(client.documentAtom)).toBeNull();
127
+ for (const event of fixtureMessages("notebook-events.sse")) {
128
+ receive(
129
+ JSON.parse(
130
+ JSON.stringify(event).replaceAll('"n1"', JSON.stringify(notebookId)),
131
+ ),
132
+ );
133
+ }
134
+ expect(store.get(client.documentAtom)?.revision.notebook_id).toBe(
135
+ notebookId,
136
+ );
137
+ expect(store.get(client.connectionAtom).state).toBe(ConnectionState.OPEN);
138
+ expect(store.get(client.notebookAtom)?.id).toBe(notebookId);
139
+ lifetime.abort();
140
+ });
141
+
142
+ it.each(["{broken", JSON.stringify({ event: "ready", data: {} })])(
143
+ "stops consuming a malformed host stream: %s",
144
+ (data) => {
145
+ const { connect, events, client, store, transport, onNotification } =
146
+ setup();
147
+ const lifetime = new AbortController();
148
+ connect(lifetime.signal);
149
+ events.dispatchEvent(new Event("open"));
150
+ events.dispatchEvent(new MessageEvent("message", { data }));
151
+ events.dispatchEvent(
152
+ new MessageEvent("message", {
153
+ data: JSON.stringify({
154
+ event: "marimo",
155
+ data: { marimo: { op: "reconnected" } },
156
+ }),
157
+ }),
158
+ );
159
+ expect(store.get(client.connectionAtom)).toEqual({
160
+ state: ConnectionState.CLOSED,
161
+ code: "KERNEL_DISCONNECTED",
162
+ reason: "Failed to decode notebook message",
163
+ });
164
+ expect(transport.close).toHaveBeenCalledOnce();
165
+ expect(onNotification).not.toHaveBeenCalled();
166
+ lifetime.abort();
167
+ },
168
+ );
169
+
170
+ it("resumes from the last cursor until reset or a new session", async () => {
171
+ const session = { id: "first", token: "a" };
172
+ const { connect, events, healthy, urls, receive } = setup(
173
+ () =>
174
+ `ws://localhost/ws?session_id=${session.id}&access_token=${session.token}&cursor=999`,
175
+ );
176
+ const lifetime = new AbortController();
177
+ connect(lifetime.signal);
178
+ healthy.resolve();
179
+ await healthy.promise;
180
+ expect(urls[0].searchParams.get("cursor")).toBeNull();
181
+
182
+ const snapshot = fixtureMessages("notebook-events.sse");
183
+ events.dispatchEvent(new Event("open"));
184
+ for (const message of snapshot) {
185
+ receive(message);
186
+ }
187
+ session.token = "b";
188
+ events.dispatchEvent(new CloseEvent("close"));
189
+ expect(urls[1].searchParams.get("cursor")).toBe("97");
190
+ expect(urls[1].searchParams.get("access_token")).toBe("b");
191
+
192
+ events.dispatchEvent(new Event("open"));
193
+ receive({ event: "reset", data: {} });
194
+ events.dispatchEvent(new CloseEvent("close"));
195
+ expect(urls[2].searchParams.get("cursor")).toBeNull();
196
+
197
+ events.dispatchEvent(new Event("open"));
198
+ for (const message of snapshot) {
199
+ receive(message);
200
+ }
201
+ session.id = "second";
202
+ events.dispatchEvent(new CloseEvent("close"));
203
+ expect(urls[3].searchParams.get("cursor")).toBeNull();
204
+ expect(urls[3].searchParams.get("session_id")).toBe("second");
205
+ lifetime.abort();
206
+ });
207
+
208
+ it("exposes the confirmed document until the lifetime ends", async () => {
209
+ const { connect, events, client, store, healthy, receive } = setup();
210
+ const lifetime = new AbortController();
211
+ connect(lifetime.signal);
212
+ healthy.resolve();
213
+ await healthy.promise;
214
+ events.dispatchEvent(new Event("open"));
215
+ const snapshot = fixtureMessages("notebook-events.sse");
216
+ for (const message of snapshot.slice(0, -3)) {
217
+ receive(message);
218
+ }
219
+ expect(store.get(client.documentAtom)).toBeNull();
220
+ for (const message of snapshot.slice(-3)) {
221
+ receive(message);
222
+ }
223
+ expect(store.get(client.documentAtom)?.revision.order).toEqual([
224
+ "c1",
225
+ "c3",
226
+ "k7f2",
227
+ ]);
228
+ events.dispatchEvent(new CloseEvent("close"));
229
+ expect(store.get(client.documentAtom)?.revision.order).toEqual([
230
+ "c1",
231
+ "c3",
232
+ "k7f2",
233
+ ]);
234
+ lifetime.abort();
235
+ expect(store.get(client.documentAtom)).toBeNull();
236
+ });
237
+
238
+ it("stops consuming a stream whose notebook changes", () => {
239
+ const { connect, events, client, store, transport } = setup();
240
+ const lifetime = new AbortController();
241
+ connect(lifetime.signal);
242
+ events.dispatchEvent(new Event("open"));
243
+ for (const id of ["n1", "n2"]) {
244
+ events.dispatchEvent(
245
+ new MessageEvent("message", { data: notebookMessage(id) }),
246
+ );
247
+ }
248
+ expect(store.get(client.connectionAtom)).toEqual({
249
+ state: ConnectionState.CLOSED,
250
+ code: "KERNEL_DISCONNECTED",
251
+ reason: "Failed to decode notebook message",
252
+ });
253
+ expect(transport.close).toHaveBeenCalledOnce();
254
+ lifetime.abort();
255
+ });
256
+
257
+ it.each(["MARIMO_UNAUTHORIZED", "MARIMO_TRANSPORT_EXHAUSTED"])(
258
+ "stops automatic recovery after %s but permits an explicit reconnect",
259
+ async (reason) => {
260
+ const {
261
+ connect,
262
+ client,
263
+ store,
264
+ events,
265
+ transport,
266
+ healthy,
267
+ checkHealth,
268
+ } = setup();
269
+ const lifetime = new AbortController();
270
+ connect(lifetime.signal);
271
+ healthy.resolve();
272
+ await healthy.promise;
273
+ events.dispatchEvent(new CloseEvent("close", { reason }));
274
+ events.dispatchEvent(new Event("error"));
275
+ expect(store.get(client.connectionAtom).state).toBe(
276
+ ConnectionState.CLOSED,
277
+ );
278
+ expect(transport.reconnect).toHaveBeenCalledOnce();
279
+ await client.reconnect();
280
+ expect(checkHealth).toHaveBeenCalledOnce();
281
+ expect(transport.reconnect).toHaveBeenCalledTimes(2);
282
+ lifetime.abort();
283
+ },
284
+ );
285
+
286
+ it("only resets the automatic retry budget after an open event", () => {
287
+ const { connect, events, transport } = setup();
288
+ const lifetime = new AbortController();
289
+ connect(lifetime.signal);
290
+ events.dispatchEvent(new CloseEvent("close"));
291
+ events.dispatchEvent(new Event("error"));
292
+ events.dispatchEvent(new CloseEvent("close"));
293
+ expect(transport.reconnect).toHaveBeenCalledOnce();
294
+ events.dispatchEvent(new Event("open"));
295
+ events.dispatchEvent(new CloseEvent("close"));
296
+ expect(transport.reconnect).toHaveBeenCalledTimes(2);
297
+ lifetime.abort();
298
+ });
299
+
300
+ it("does not revive an aborted lifetime when a manual health check finishes", async () => {
301
+ const { connect, client, transport, checkHealth } = setup();
302
+ const health = new Deferred<boolean>();
303
+ checkHealth.mockReturnValue(health.promise);
304
+ const lifetime = new AbortController();
305
+ connect(lifetime.signal);
306
+ const attempt = client.reconnect();
307
+ lifetime.abort();
308
+ health.resolve(true);
309
+ await attempt;
310
+ expect(transport.reconnect).not.toHaveBeenCalled();
311
+ });
312
+
313
+ it("does not connect after abort, even when the health check finishes", async () => {
314
+ const { client, connect, healthy, transport } = setup();
315
+ const controller = new AbortController();
316
+ connect(controller.signal);
317
+ controller.abort();
318
+ healthy.resolve();
319
+ await healthy.promise;
320
+ client.reconnect();
321
+ expect(transport.reconnect).not.toHaveBeenCalled();
322
+ expect(transport.close).toHaveBeenCalledOnce();
323
+ });
324
+
325
+ it("detaches listeners before closing so cleanup cannot trigger a session retry", () => {
326
+ const { connect, client, store, events, transport, onNotification } =
327
+ setup();
328
+ const controller = new AbortController();
329
+ connect(controller.signal);
330
+ controller.abort();
331
+ events.dispatchEvent(
332
+ new MessageEvent("message", {
333
+ data: JSON.stringify({ data: { op: "reconnected" } }),
334
+ }),
335
+ );
336
+ expect(transport.reconnect).not.toHaveBeenCalled();
337
+ expect(store.get(client.connectionAtom)).toEqual({
338
+ state: ConnectionState.NOT_STARTED,
339
+ });
340
+ expect(onNotification).not.toHaveBeenCalled();
341
+ });
342
+
343
+ it("can reconnect after effect cleanup without reviving the earlier client attempt", async () => {
344
+ const { connect, healthy, transport, events, onNotification } = setup();
345
+ const first = new AbortController();
346
+ connect(first.signal);
347
+ first.abort();
348
+ const second = new AbortController();
349
+ connect(second.signal);
350
+ healthy.resolve();
351
+ await healthy.promise;
352
+ expect(transport.reconnect).toHaveBeenCalledOnce();
353
+ events.dispatchEvent(
354
+ new MessageEvent("message", {
355
+ data: JSON.stringify({
356
+ event: "marimo",
357
+ data: { marimo: { op: "reconnected" } },
358
+ }),
359
+ }),
360
+ );
361
+ expect(onNotification).toHaveBeenCalledExactlyOnceWith({
362
+ data: { op: "reconnected" },
363
+ });
364
+ second.abort();
365
+ });
366
+ });
367
+
368
+ async function open() {
369
+ const url = vi.fn(() => "ws://localhost/ws?session_id=first&cursor=999");
370
+ const setupResult = setup(url);
371
+ const lifetime = new AbortController();
372
+ setupResult.connect(lifetime.signal);
373
+ setupResult.healthy.resolve();
374
+ await setupResult.healthy.promise;
375
+ setupResult.events.dispatchEvent(new Event("open"));
376
+ return { ...setupResult, lifetime, url };
377
+ }
378
+
379
+ describe("NotebookClient attachments", () => {
380
+ const [browser, agent] = fixtureEvents("notebook-events.sse").flatMap(
381
+ (event) => (event.event === "attachment" ? [event.data.attachment] : []),
382
+ );
383
+
384
+ it("publishes a complete snapshot and applies live changes independently of kernel readiness", async () => {
385
+ const { client, store, receive, lifetime } = await open();
386
+ const snapshot = fixtureMessages("notebook-events.sse");
387
+ const readyIndex = snapshot.findIndex((event) => event.event === "ready");
388
+ snapshot.slice(0, readyIndex).forEach(receive);
389
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
390
+ snapshot.slice(readyIndex).forEach(receive);
391
+ expect(store.get(client.attachmentsAtom)).toEqual([browser, agent]);
392
+ const renamed = { ...agent, name: "Renamed agent" };
393
+ receive({ event: "attachment", id: "98", data: { attachment: renamed } });
394
+ receive({
395
+ event: "attachment.removed",
396
+ id: "99",
397
+ data: { id: browser.id },
398
+ });
399
+ expect(store.get(client.attachmentsAtom)).toEqual([renamed]);
400
+ receive({ data: { op: "kernel-ready", resumed: false } });
401
+ expect(store.get(client.attachmentsAtom)).toEqual([renamed]);
402
+ receive({ event: "attachment.removed", id: "100", data: { id: agent.id } });
403
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
404
+ lifetime.abort();
405
+ });
406
+
407
+ it("resumes attachments alongside an incomplete document and waits for replay ready", async () => {
408
+ const { client, store, receive, events, urls, lifetime } = await open();
409
+ expect(urls[0].searchParams.has("cursor")).toBe(false);
410
+ fixtureMessages("notebook-events.sse").forEach(receive);
411
+ const confirmed = store.get(client.documentAtom);
412
+ const edits = fixtureMessages("edits.sse");
413
+ edits.slice(0, 3).forEach(receive);
414
+ const renamed = { ...agent, name: "Renamed agent" };
415
+ receive({ event: "attachment", id: "112a", data: { attachment: renamed } });
416
+ expect(store.get(client.attachmentsAtom)).toEqual([browser, renamed]);
417
+ events.dispatchEvent(new CloseEvent("close"));
418
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
419
+ expect(urls.at(-1)!.searchParams.get("cursor")).toBe("112a");
420
+ events.dispatchEvent(new Event("open"));
421
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
422
+ expect(store.get(client.documentAtom)).toBe(confirmed);
423
+ edits.slice(3).forEach(receive);
424
+ receive({
425
+ event: "attachment.removed",
426
+ id: "115",
427
+ data: { id: browser.id },
428
+ });
429
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
430
+ receive({ event: "ready", id: "116", data: {} });
431
+ expect(store.get(client.attachmentsAtom)).toEqual([renamed]);
432
+ const revisions = fixtureEvents("edits.sse").flatMap((event) =>
433
+ event.event === "document" ? [event.data.document] : [],
434
+ );
435
+ expect(store.get(client.documentAtom)?.revision).toEqual(revisions.at(-1));
436
+ lifetime.abort();
437
+ });
438
+
439
+ it("replaces stale attachments with an empty snapshot after reset", async () => {
440
+ const { client, store, receive, lifetime } = await open();
441
+ fixtureMessages("notebook-events.sse").forEach(receive);
442
+ receive({ event: "reset", data: {} });
443
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
444
+ fixtureMessages("notebook-events.sse")
445
+ .filter((event) => event.event !== "attachment")
446
+ .forEach(receive);
447
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
448
+ lifetime.abort();
449
+ });
450
+
451
+ it.each(["close", "error", "malformed", "removed", "abort", "identity"])(
452
+ "clears presence on %s and ignores messages after disposal",
453
+ async (reason) => {
454
+ const { client, store, receive, events, url, lifetime } = await open();
455
+ fixtureMessages("notebook-events.sse").forEach(receive);
456
+ expect(store.get(client.attachmentsAtom)).toEqual([browser, agent]);
457
+ switch (reason) {
458
+ case "close":
459
+ events.dispatchEvent(
460
+ new CloseEvent("close", { reason: "MARIMO_UNAUTHORIZED" }),
461
+ );
462
+ break;
463
+ case "error":
464
+ events.dispatchEvent(new Event("error"));
465
+ break;
466
+ case "malformed":
467
+ receive({ event: "attachment", id: "98", data: {} });
468
+ break;
469
+ case "removed":
470
+ receive({ event: "notebook.removed", id: "98", data: { id: "n1" } });
471
+ break;
472
+ case "identity":
473
+ url.mockReturnValue(
474
+ "ws://localhost/api/marimo/v1/notebooks/n2/events/ws",
475
+ );
476
+ events.dispatchEvent(new CloseEvent("close"));
477
+ receive({ event: "ready", id: "98", data: {} });
478
+ break;
479
+ default:
480
+ lifetime.abort();
481
+ }
482
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
483
+ lifetime.abort();
484
+ fixtureMessages("notebook-events.sse").forEach(receive);
485
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
486
+ },
487
+ );
488
+
489
+ it("keeps host resources empty for notification-only WASM connections", async () => {
490
+ const { client, store, receive, lifetime } = await open();
491
+ receive({ data: { op: "kernel-ready" } });
492
+ receive({ data: { op: "reconnected" } });
493
+ expect(store.get(client.attachmentsAtom)).toEqual([]);
494
+ lifetime.abort();
495
+ });
496
+ });
@@ -0,0 +1,81 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ export const ConnectionState = {
4
+ NOT_STARTED: "NOT_STARTED",
5
+ CONNECTING: "CONNECTING",
6
+ OPEN: "OPEN",
7
+ CLOSING: "CLOSING",
8
+ CLOSED: "CLOSED",
9
+ } as const;
10
+
11
+ export type ConnectionState =
12
+ (typeof ConnectionState)[keyof typeof ConnectionState];
13
+
14
+ export const ConnectionClosedReason = {
15
+ KERNEL_DISCONNECTED: "KERNEL_DISCONNECTED",
16
+ KERNEL_STARTUP_ERROR: "KERNEL_STARTUP_ERROR",
17
+ } as const;
18
+
19
+ export type ConnectionClosedReason =
20
+ (typeof ConnectionClosedReason)[keyof typeof ConnectionClosedReason];
21
+
22
+ export type ConnectionStatus =
23
+ | {
24
+ state: typeof ConnectionState.CLOSED;
25
+ code: ConnectionClosedReason;
26
+ reason: string;
27
+ }
28
+ | {
29
+ state: typeof ConnectionState.CONNECTING;
30
+ }
31
+ | {
32
+ state:
33
+ | typeof ConnectionState.OPEN
34
+ | typeof ConnectionState.CLOSING
35
+ | typeof ConnectionState.NOT_STARTED;
36
+ };
37
+
38
+ /** The recovery policy for a closed connection. */
39
+ export interface CloseDecision {
40
+ kind: "terminal" | "gave-up" | "retry";
41
+ status: ConnectionStatus;
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
+ }