@marimo-team/frontend 0.25.3-dev80 → 0.25.3-dev82

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 (106) hide show
  1. package/dist/assets/{CellStatus-cD3S0wEA.js → CellStatus-DXroYLOP.js} +1 -1
  2. package/dist/assets/{ErrorBoundary-DJhsFCsX.js → ErrorBoundary-DaQ9THH_.js} +2 -2
  3. package/dist/assets/{JsonOutput-Pm6OCP3J.js → JsonOutput-VxzHyItt.js} +1 -1
  4. package/dist/assets/{MarimoErrorOutput-DlEjLCZ-.js → MarimoErrorOutput-D-hLaDA-.js} +1 -1
  5. package/dist/assets/{RenderHTML-CfQ6i3QV.js → RenderHTML-DjOEZL25.js} +1 -1
  6. package/dist/assets/{RunButton-DvuvkgrG.js → RunButton-CuhNTo5_.js} +1 -1
  7. package/dist/assets/{add-cell-with-ai-By51zSlC.js → add-cell-with-ai-BbVntTh7.js} +1 -1
  8. package/dist/assets/{add-connection-dialog-D7EYSraq.js → add-connection-dialog-Qf63aKV6.js} +1 -1
  9. package/dist/assets/{agent-panel-aCWa3Yai.js → agent-panel-DMOfF3qF.js} +1 -1
  10. package/dist/assets/{ai-model-dropdown-bwTRyFn4.js → ai-model-dropdown-B15l0R62.js} +2 -2
  11. package/dist/assets/{app-config-button-DGhsM0xw.js → app-config-button-BIMB8qjD.js} +1 -1
  12. package/dist/assets/{blob-e6VlOmQP.js → blob-De9GR2s6.js} +1 -1
  13. package/dist/assets/{cell-editor-M8dEnDzk.js → cell-editor-Di0cSFhC.js} +32 -11
  14. package/dist/assets/{cell-link-DFqfW-Ym.js → cell-link-DR3W2HC7.js} +1 -1
  15. package/dist/assets/{cells-T9_Y4Jlv.js → cells-styV7IX8.js} +6 -6
  16. package/dist/assets/{chat-display-DXn2Yrjr.js → chat-display-BzdYdN39.js} +1 -1
  17. package/dist/assets/{chat-panel-D3iUyZId.js → chat-panel-De3YzkSF.js} +1 -1
  18. package/dist/assets/{chat-ui-YuJtv3UJ.js → chat-ui-NTNapdtP.js} +1 -1
  19. package/dist/assets/{code-visibility-CV3S4xuf.js → code-visibility-csc3tgA0.js} +1 -1
  20. package/dist/assets/{column-preview-C6uXRJry.js → column-preview-DkH-oD8f.js} +1 -1
  21. package/dist/assets/{command-palette-CGNUQdgW.js → command-palette-Dg_xstl0.js} +1 -1
  22. package/dist/assets/{common-DeeLgjWQ.js → common-D7VAS6rd.js} +1 -1
  23. package/dist/assets/{components-DyAF3_Nf.js → components-CNmvC781.js} +1 -1
  24. package/dist/assets/config-BQT0uYUo.js +1 -0
  25. package/dist/assets/{config-CjQ4WJnJ.js → config-BbRbT9Ww.js} +1 -1
  26. package/dist/assets/{datasource-gqFgG9Qd.js → datasource-C3yfeDa2.js} +1 -1
  27. package/dist/assets/{dependency-graph-panel-B3T9kSTs.js → dependency-graph-panel-CjoA6L0w.js} +1 -1
  28. package/dist/assets/{documentation-panel-BBpGbT-f.js → documentation-panel-DHWFOiG1.js} +1 -1
  29. package/dist/assets/{download-C8Gw-D0Y.js → download-BrCpRZKN.js} +1 -1
  30. package/dist/assets/{edit-page-CBftLPze.js → edit-page-BPMo_x-T.js} +4 -4
  31. package/dist/assets/{error-entries-uyqOG5tg.js → error-entries-BkEAmmnD.js} +1 -1
  32. package/dist/assets/{error-panel-Cli5zPWi.js → error-panel-BhTfPA_i.js} +1 -1
  33. package/dist/assets/feedback-button-BAcClv7c.js +1 -0
  34. package/dist/assets/{feedback-button-CvrwBGr7.js → feedback-button-D9InK87G.js} +1 -1
  35. package/dist/assets/{file-explorer-panel-BSU5ultY.js → file-explorer-panel-BSd_ZbUC.js} +1 -1
  36. package/dist/assets/{file-name-input-CDrRlIiX.js → file-name-input-RHURglL_.js} +1 -1
  37. package/dist/assets/{floating-outline-GQD9km0W.js → floating-outline-Cm49rYYe.js} +1 -1
  38. package/dist/assets/{gallery-page-B-5H1fEt.js → gallery-page-DdMdOtdv.js} +1 -1
  39. package/dist/assets/{glide-data-editor-B1kpoetH.js → glide-data-editor-eiSlEUyR.js} +1 -1
  40. package/dist/assets/{home-page-3qsPTb4v.js → home-page-DwWvBB0R.js} +1 -1
  41. package/dist/assets/{hooks-BrFYiTJS.js → hooks-CHeUZHPp.js} +1 -1
  42. package/dist/assets/{index-D0IWODMU.js → index-DckSn2w6.js} +8 -8
  43. package/dist/assets/{layout-CGC8ajpe.js → layout-_lp06T0C.js} +3 -3
  44. package/dist/assets/{logs-panel-CdUc5vtA.js → logs-panel-CAAk72Gb.js} +1 -1
  45. package/dist/assets/{markdown-renderer-pKc_bFac.js → markdown-renderer-CAyCgogC.js} +1 -1
  46. package/dist/assets/{name-cell-input-csH3Cy54.js → name-cell-input-DA8hU9XT.js} +1 -1
  47. package/dist/assets/{outline-panel-DhsHYTuo.js → outline-panel-Cc5oQ4LL.js} +1 -1
  48. package/dist/assets/{packages-panel-BtkXVz3L.js → packages-panel-DTfKez6I.js} +1 -1
  49. package/dist/assets/{panel-accordion-state-Bco6nsUI.js → panel-accordion-state-BZHex38W.js} +1 -1
  50. package/dist/assets/panels-DU-SaRHy.js +105 -0
  51. package/dist/assets/{process-output-B2-bT4cq.js → process-output-CrDc_O1D.js} +1 -1
  52. package/dist/assets/{readonly-code-display-Dsp6vWPh.js → readonly-code-display-BR-_jUj8.js} +1 -1
  53. package/dist/assets/{readonly-python-code-B81AjzXM.js → readonly-python-code-D7jcy9ds.js} +1 -1
  54. package/dist/assets/{reveal-component-Cxo3hLGA.js → reveal-component-eLftBwOe.js} +1 -1
  55. package/dist/assets/{run-page-CLP1s8uX.js → run-page-mURyATDJ.js} +1 -1
  56. package/dist/assets/{runtime-details-state-CznIe40L.js → runtime-details-state-hb7A1jgR.js} +1 -1
  57. package/dist/assets/{scratchpad-panel-yHIW63VQ.js → scratchpad-panel-B_gEnoqj.js} +1 -1
  58. package/dist/assets/{session-panel-C7-wmZhc.js → session-panel-DmDjaPsN.js} +1 -1
  59. package/dist/assets/{snippets-panel-BARpOPyS.js → snippets-panel-o9KL5jMi.js} +1 -1
  60. package/dist/assets/state-BkAqJoW7.js +3 -0
  61. package/dist/assets/{state-Bye6rCo4.js → state-Os7LSUUZ.js} +1 -1
  62. package/dist/assets/{terminal-Bhcn_y1N.js → terminal-BI85khrS.js} +1 -1
  63. package/dist/assets/{tracing-DDohxo24.js → tracing-AsNGuDsh.js} +1 -1
  64. package/dist/assets/{tracing-panel-DYhGDAo5.js → tracing-panel-CBcmea6L.js} +2 -2
  65. package/dist/assets/{tree-actions-Cb-nUK1F.js → tree-actions-Cx5EDpA6.js} +1 -1
  66. package/dist/assets/{useAddCell-Bielffe0.js → useAddCell-uJJYtgwa.js} +1 -1
  67. package/dist/assets/{useBoolean-CMgj9hnq.js → useBoolean-8wlrTeus.js} +1 -1
  68. package/dist/assets/{useCellActionButton-DAcQhse_.js → useCellActionButton-CmQ-OcPa.js} +1 -1
  69. package/dist/assets/{useDeleteCell-BV4VemCv.js → useDeleteCell-CXgOCDqY.js} +1 -1
  70. package/dist/assets/{useDependencyPanelTab-PtXIVh53.js → useDependencyPanelTab-Ch6gm7uM.js} +1 -1
  71. package/dist/assets/{useInstallPackage-C7neMPwh.js → useInstallPackage-CYQoMdMQ.js} +1 -1
  72. package/dist/assets/{useNotebookActions-DZBB98fP.js → useNotebookActions-DGShHZ_1.js} +1 -1
  73. package/dist/assets/{useRunCells-BFeZMi7f.js → useRunCells-DAfDmOMC.js} +1 -1
  74. package/dist/assets/{useSplitCell-DllkNFke.js → useSplitCell-NLULgcq7.js} +1 -1
  75. package/dist/assets/{vega-component-D7JFthDD.js → vega-component-BKJU3uaB.js} +1 -1
  76. package/dist/index.html +24 -24
  77. package/package.json +1 -1
  78. package/src/core/host/AGENTS.md +6 -5
  79. package/src/core/host/__tests__/notebook-client.test.ts +368 -350
  80. package/src/core/host/connection.ts +1 -40
  81. package/src/core/host/notebook-client.ts +129 -186
  82. package/src/core/wasm/bridge.ts +0 -8
  83. package/src/core/websocket/__tests__/connection-state.test.ts +0 -12
  84. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +133 -59
  85. package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +152 -250
  86. package/src/core/websocket/connection-state.ts +0 -10
  87. package/src/core/websocket/transports/__tests__/notebook.test.ts +502 -0
  88. package/src/core/websocket/transports/notebook.ts +213 -0
  89. package/src/core/websocket/useNotebookClient.ts +28 -17
  90. package/dist/assets/config-DzdxZb17.js +0 -1
  91. package/dist/assets/feedback-button-DknbRCN_.js +0 -1
  92. package/dist/assets/panels-D1i80TGn.js +0 -107
  93. package/dist/assets/state-BRYi026f.js +0 -3
  94. package/dist/assets/ws-BXWY541V.js +0 -22
  95. package/src/core/host/__tests__/events.test.ts +0 -49
  96. package/src/core/host/events.ts +0 -68
  97. package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
  98. package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
  99. package/src/core/websocket/transports/__tests__/sse.test.ts +0 -352
  100. package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
  101. package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
  102. package/src/core/websocket/transports/basic.ts +0 -102
  103. package/src/core/websocket/transports/create.ts +0 -31
  104. package/src/core/websocket/transports/sse.ts +0 -320
  105. package/src/core/websocket/transports/transport.ts +0 -50
  106. package/src/core/websocket/transports/ws.ts +0 -98
@@ -1,352 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
- import { SseTransport } from "../sse";
5
- import { MAX_RETRIES, TRANSPORT_EXHAUSTED_REASON } from "../ws";
6
-
7
- const encoder = new TextEncoder();
8
-
9
- function createControllableStream() {
10
- let controller!: ReadableStreamDefaultController<Uint8Array>;
11
- const stream = new ReadableStream<Uint8Array>({
12
- start(c) {
13
- controller = c;
14
- },
15
- });
16
- return {
17
- stream,
18
- push: (text: string) => controller.enqueue(encoder.encode(text)),
19
- end: () => controller.close(),
20
- };
21
- }
22
-
23
- function sseResponse(body: ReadableStream<Uint8Array> | null, status = 200) {
24
- return {
25
- ok: status >= 200 && status < 300,
26
- status,
27
- body,
28
- } as Response;
29
- }
30
-
31
- describe("SseTransport", () => {
32
- let transport: SseTransport;
33
- let fetchMock: ReturnType<typeof vi.fn>;
34
- let opens: Event[];
35
- let messages: MessageEvent[];
36
- let closes: CloseEvent[];
37
- let errors: Event[];
38
-
39
- beforeEach(() => {
40
- fetchMock = vi.fn();
41
- vi.stubGlobal("fetch", fetchMock);
42
- transport = new SseTransport(() => "http://example.invalid/sse");
43
- opens = [];
44
- messages = [];
45
- closes = [];
46
- errors = [];
47
- transport.addEventListener("open", (e) => opens.push(e));
48
- transport.addEventListener("message", (e) => messages.push(e));
49
- transport.addEventListener("close", (e) => closes.push(e));
50
- transport.addEventListener("error", (e) => errors.push(e));
51
- });
52
-
53
- afterEach(() => {
54
- transport.close();
55
- vi.unstubAllGlobals();
56
- vi.useRealTimers();
57
- });
58
-
59
- it("starts closed and connects on reconnect()", async () => {
60
- expect(transport.readyState).toBe(WebSocket.CLOSED);
61
- const { stream } = createControllableStream();
62
- fetchMock.mockResolvedValueOnce(sseResponse(stream));
63
-
64
- transport.reconnect();
65
- expect(transport.readyState).toBe(WebSocket.CONNECTING);
66
- await vi.waitFor(() => expect(opens).toHaveLength(1));
67
- expect(transport.readyState).toBe(WebSocket.OPEN);
68
- expect(fetchMock).toHaveBeenCalledWith(
69
- "http://example.invalid/sse",
70
- expect.objectContaining({
71
- headers: { Accept: "text/event-stream" },
72
- }),
73
- );
74
- });
75
-
76
- it("dispatches message events, joining multi-line data", async () => {
77
- const { stream, push } = createControllableStream();
78
- fetchMock.mockResolvedValueOnce(sseResponse(stream));
79
- transport.reconnect();
80
- await vi.waitFor(() => expect(opens).toHaveLength(1));
81
-
82
- push('data: {"op": "alert"}\n\n');
83
- await vi.waitFor(() => expect(messages).toHaveLength(1));
84
- expect(messages[0].data).toBe('{"op": "alert"}');
85
-
86
- // Multi-line data and records split across chunks
87
- push("data: line1\nda");
88
- push("ta: line2\n\n");
89
- await vi.waitFor(() => expect(messages).toHaveLength(2));
90
- expect(messages[1].data).toBe("line1\nline2");
91
- });
92
-
93
- it("preserves host event names and cursors without carrying IDs into snapshots", async () => {
94
- const { stream, push } = createControllableStream();
95
- fetchMock.mockResolvedValueOnce(sseResponse(stream));
96
- transport.reconnect();
97
- await vi.waitFor(() => expect(opens).toHaveLength(1));
98
-
99
- push("id: cursor-1\nevent: rea");
100
- push("dy\ndata: {}\n\n: keep-alive\n\nevent: reset\ndata: {}\n\n");
101
- push('event: marimo\ndata: {"marimo":\ndata: {"op":"reconnected"}}\n\n');
102
- await vi.waitFor(() => expect(messages).toHaveLength(3));
103
- expect(messages.map((message) => JSON.parse(message.data))).toEqual([
104
- { event: "ready", id: "cursor-1", data: {} },
105
- { event: "reset", data: {} },
106
- { event: "marimo", data: { marimo: { op: "reconnected" } } },
107
- ]);
108
- });
109
-
110
- it("ignores keep-alive comments", async () => {
111
- const { stream, push } = createControllableStream();
112
- fetchMock.mockResolvedValueOnce(sseResponse(stream));
113
- transport.reconnect();
114
- await vi.waitFor(() => expect(opens).toHaveLength(1));
115
-
116
- push(": keep-alive\n\n");
117
- push("data: after\n\n");
118
- await vi.waitFor(() => expect(messages).toHaveLength(1));
119
- expect(messages[0].data).toBe("after");
120
- });
121
-
122
- it("surfaces a server close event with its code and reason", async () => {
123
- const { stream, push } = createControllableStream();
124
- fetchMock.mockResolvedValueOnce(sseResponse(stream));
125
- transport.reconnect();
126
- await vi.waitFor(() => expect(opens).toHaveLength(1));
127
-
128
- transport.addEventListener("close", () => transport.close());
129
- push('event: close\ndata: {"code": 1000, "reason": "MARIMO_SHUTDOWN"}\n\n');
130
- await vi.waitFor(() => expect(closes).toHaveLength(1));
131
- expect(closes[0].code).toBe(1000);
132
- expect(closes[0].reason).toBe("MARIMO_SHUTDOWN");
133
- expect(closes[0].wasClean).toBe(true);
134
- expect(transport.readyState).toBe(WebSocket.CLOSED);
135
- expect(fetchMock).toHaveBeenCalledTimes(1);
136
- });
137
-
138
- it("emits an empty-reason close and retries when the stream ends", async () => {
139
- vi.useFakeTimers();
140
- const first = createControllableStream();
141
- fetchMock.mockResolvedValueOnce(sseResponse(first.stream));
142
- transport.reconnect();
143
- await vi.waitFor(() => expect(opens).toHaveLength(1));
144
-
145
- first.end();
146
- await vi.waitFor(() => expect(closes).toHaveLength(1));
147
- expect(closes[0].reason).toBe("");
148
- expect(closes[0].wasClean).toBe(false);
149
-
150
- // A retry is scheduled and reconnects
151
- const second = createControllableStream();
152
- fetchMock.mockResolvedValueOnce(sseResponse(second.stream));
153
- await vi.advanceTimersByTimeAsync(10_000);
154
- await vi.waitFor(() => expect(opens).toHaveLength(2));
155
- });
156
-
157
- it("does not stack its own retry when the consumer reconnects in onClose", async () => {
158
- vi.useFakeTimers();
159
- const first = createControllableStream();
160
- const second = createControllableStream();
161
- fetchMock
162
- .mockResolvedValueOnce(sseResponse(first.stream))
163
- .mockResolvedValueOnce(sseResponse(second.stream));
164
- // Reconnect synchronously from the close handler.
165
- transport.addEventListener("close", () => transport.reconnect());
166
-
167
- transport.reconnect();
168
- await vi.waitFor(() => expect(opens).toHaveLength(1));
169
-
170
- first.end();
171
- await vi.waitFor(() => expect(opens).toHaveLength(2));
172
-
173
- // The superseded run must not schedule a second connection on top of
174
- // the consumer's reconnect.
175
- await vi.advanceTimersByTimeAsync(60_000);
176
- expect(fetchMock).toHaveBeenCalledTimes(2);
177
-
178
- // The consumer-driven connection is healthy
179
- second.push("data: alive\n\n");
180
- await vi.waitFor(() => expect(messages).toHaveLength(1));
181
- expect(transport.readyState).toBe(WebSocket.OPEN);
182
- });
183
-
184
- it("exhausts the retry budget when the server closes every connection", async () => {
185
- vi.useFakeTimers();
186
- // A server that accepts, immediately sends a close event, and never
187
- // delivers a message must not be retried forever.
188
- fetchMock.mockImplementation(() => {
189
- const { stream, push } = createControllableStream();
190
- push('event: close\ndata: {"code": 3000, "reason": "MARIMO_X"}\n\n');
191
- return Promise.resolve(sseResponse(stream));
192
- });
193
-
194
- transport.reconnect();
195
- for (let i = 0; i < MAX_RETRIES; i++) {
196
- await vi.advanceTimersByTimeAsync(15_000);
197
- }
198
-
199
- expect(closes.length).toBe(MAX_RETRIES);
200
- expect(closes.at(-1)?.reason).toBe(TRANSPORT_EXHAUSTED_REASON);
201
- const fetchCallsWhenExhausted = fetchMock.mock.calls.length;
202
- await vi.advanceTimersByTimeAsync(60_000);
203
- expect(fetchMock.mock.calls.length).toBe(fetchCallsWhenExhausted);
204
- });
205
-
206
- it("a delivered message resets the retry budget", async () => {
207
- vi.useFakeTimers();
208
- const first = createControllableStream();
209
- fetchMock.mockResolvedValueOnce(sseResponse(first.stream));
210
- transport.reconnect();
211
- await vi.waitFor(() => expect(opens).toHaveLength(1));
212
- first.push("data: healthy\n\n");
213
- await vi.waitFor(() => expect(messages).toHaveLength(1));
214
-
215
- first.end();
216
- await vi.waitFor(() => expect(closes).toHaveLength(1));
217
- // Budget was reset by the message, so this close is retry #1, not
218
- // an exhaustion.
219
- expect(closes[0].reason).toBe("");
220
- });
221
-
222
- it("gives up with TRANSPORT_EXHAUSTED_REASON after MAX_RETRIES failures", async () => {
223
- vi.useFakeTimers();
224
- fetchMock.mockRejectedValue(new Error("connection refused"));
225
-
226
- transport.reconnect();
227
- for (let i = 0; i < MAX_RETRIES; i++) {
228
- await vi.advanceTimersByTimeAsync(15_000);
229
- }
230
-
231
- expect(closes.length).toBe(MAX_RETRIES);
232
- expect(closes.at(-1)?.reason).toBe(TRANSPORT_EXHAUSTED_REASON);
233
- expect(errors.length).toBe(MAX_RETRIES);
234
- const fetchCallsWhenExhausted = fetchMock.mock.calls.length;
235
-
236
- // No further retries once exhausted
237
- await vi.advanceTimersByTimeAsync(60_000);
238
- expect(fetchMock.mock.calls.length).toBe(fetchCallsWhenExhausted);
239
-
240
- // A manual reconnect() resets the budget
241
- const { stream } = createControllableStream();
242
- fetchMock.mockResolvedValueOnce(sseResponse(stream));
243
- transport.reconnect();
244
- await vi.waitFor(() => expect(opens).toHaveLength(1));
245
- });
246
-
247
- it("close() aborts the connection and cancels retries", async () => {
248
- vi.useFakeTimers();
249
- const { stream } = createControllableStream();
250
- fetchMock.mockResolvedValueOnce(sseResponse(stream));
251
- transport.reconnect();
252
- await vi.waitFor(() => expect(opens).toHaveLength(1));
253
-
254
- transport.close();
255
- expect(transport.readyState).toBe(WebSocket.CLOSED);
256
- // No close event is dispatched for a user-initiated close, and no
257
- // retry is scheduled.
258
- await vi.advanceTimersByTimeAsync(60_000);
259
- expect(closes).toHaveLength(0);
260
- expect(fetchMock).toHaveBeenCalledTimes(1);
261
- });
262
-
263
- it("treats a non-2xx response as a connection failure", async () => {
264
- vi.useFakeTimers();
265
- fetchMock.mockResolvedValueOnce(sseResponse(null, 500));
266
- transport.reconnect();
267
-
268
- await vi.waitFor(() => expect(closes).toHaveLength(1));
269
- expect(opens).toHaveLength(0);
270
- expect(errors).toHaveLength(1);
271
- expect(closes[0].reason).toBe("");
272
- });
273
-
274
- it("sends provided headers (auth) with the request", async () => {
275
- const withHeaders = new SseTransport(
276
- () => "http://example.invalid/sse",
277
- () => ({ Authorization: "Bearer token123" }),
278
- );
279
- const { stream } = createControllableStream();
280
- fetchMock.mockResolvedValueOnce(sseResponse(stream));
281
- withHeaders.reconnect();
282
- await vi.waitFor(() => expect(fetchMock).toHaveBeenCalled());
283
- withHeaders.close();
284
-
285
- expect(fetchMock).toHaveBeenCalledWith(
286
- "http://example.invalid/sse",
287
- expect.objectContaining({
288
- headers: {
289
- Authorization: "Bearer token123",
290
- Accept: "text/event-stream",
291
- },
292
- }),
293
- );
294
- });
295
-
296
- it("aborts a connection attempt that exceeds the timeout", async () => {
297
- vi.useFakeTimers();
298
- // fetch that only settles when aborted
299
- fetchMock.mockImplementation(
300
- (_url: string, init: RequestInit) =>
301
- new Promise((_, reject) => {
302
- init.signal?.addEventListener("abort", () =>
303
- reject(new Error("aborted")),
304
- );
305
- }),
306
- );
307
-
308
- transport.reconnect();
309
- await vi.advanceTimersByTimeAsync(11_000);
310
- expect(errors).toHaveLength(1);
311
- expect(closes).toHaveLength(1);
312
- expect(closes[0].reason).toBe("");
313
- });
314
-
315
- it("reconnect() mid-stream supersedes the old connection silently", async () => {
316
- const first = createControllableStream();
317
- const second = createControllableStream();
318
- fetchMock
319
- .mockResolvedValueOnce(sseResponse(first.stream))
320
- .mockResolvedValueOnce(sseResponse(second.stream));
321
-
322
- transport.reconnect();
323
- await vi.waitFor(() => expect(opens).toHaveLength(1));
324
-
325
- transport.reconnect();
326
- await vi.waitFor(() => expect(opens).toHaveLength(2));
327
-
328
- // The aborted first connection must not surface close/error events
329
- second.push("data: alive\n\n");
330
- await vi.waitFor(() => expect(messages).toHaveLength(1));
331
- expect(closes).toHaveLength(0);
332
- expect(errors).toHaveLength(0);
333
- });
334
-
335
- it("send() is a no-op", () => {
336
- expect(() => transport.send("data")).not.toThrow();
337
- });
338
-
339
- it("dedupes repeated addEventListener calls", async () => {
340
- const { stream, push } = createControllableStream();
341
- fetchMock.mockResolvedValueOnce(sseResponse(stream));
342
- const onMessage = (e: MessageEvent) => messages.push(e);
343
- transport.addEventListener("message", onMessage);
344
- transport.addEventListener("message", onMessage);
345
- transport.reconnect();
346
- await vi.waitFor(() => expect(opens).toHaveLength(1));
347
-
348
- push("data: once\n\n");
349
- // The beforeEach listener plus the deduped listener = 2 events
350
- await vi.waitFor(() => expect(messages).toHaveLength(2));
351
- });
352
- });
@@ -1,308 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import { describe, expect, it, vi } from "vitest";
4
- import { ConnectionSubscriptions } from "../transport";
5
-
6
- describe("ConnectionSubscriptions", () => {
7
- describe("addSubscription", () => {
8
- it("should add a subscription for an event", () => {
9
- const subscriptions = new ConnectionSubscriptions();
10
- const callback = vi.fn();
11
-
12
- subscriptions.addSubscription("open", callback);
13
-
14
- // Verify by notifying
15
- subscriptions.notify("open", new Event("open"));
16
- expect(callback).toHaveBeenCalledTimes(1);
17
- });
18
-
19
- it("should add multiple subscriptions for the same event", () => {
20
- const subscriptions = new ConnectionSubscriptions();
21
- const callback1 = vi.fn();
22
- const callback2 = vi.fn();
23
- const callback3 = vi.fn();
24
-
25
- subscriptions.addSubscription("message", callback1);
26
- subscriptions.addSubscription("message", callback2);
27
- subscriptions.addSubscription("message", callback3);
28
-
29
- const messageEvent = new MessageEvent("message", { data: "test" });
30
- subscriptions.notify("message", messageEvent);
31
-
32
- expect(callback1).toHaveBeenCalledWith(messageEvent);
33
- expect(callback2).toHaveBeenCalledWith(messageEvent);
34
- expect(callback3).toHaveBeenCalledWith(messageEvent);
35
- });
36
-
37
- it("should add subscriptions for different event types", () => {
38
- const subscriptions = new ConnectionSubscriptions();
39
- const openCallback = vi.fn();
40
- const closeCallback = vi.fn();
41
- const messageCallback = vi.fn();
42
-
43
- subscriptions.addSubscription("open", openCallback);
44
- subscriptions.addSubscription("close", closeCallback);
45
- subscriptions.addSubscription("message", messageCallback);
46
-
47
- subscriptions.notify("open", new Event("open"));
48
- expect(openCallback).toHaveBeenCalledTimes(1);
49
- expect(closeCallback).not.toHaveBeenCalled();
50
- expect(messageCallback).not.toHaveBeenCalled();
51
- });
52
-
53
- it("should not add duplicate subscriptions for the same callback", () => {
54
- const subscriptions = new ConnectionSubscriptions();
55
- const callback = vi.fn();
56
-
57
- subscriptions.addSubscription("close", callback);
58
- subscriptions.addSubscription("close", callback);
59
- subscriptions.addSubscription("close", callback);
60
-
61
- subscriptions.notify("close", new Event("close"));
62
-
63
- // Set only stores unique callbacks, so should be called once
64
- expect(callback).toHaveBeenCalledTimes(1);
65
- });
66
- });
67
-
68
- describe("removeSubscription", () => {
69
- it("should remove a subscription", () => {
70
- const subscriptions = new ConnectionSubscriptions();
71
- const callback = vi.fn();
72
-
73
- subscriptions.addSubscription("message", callback);
74
- subscriptions.removeSubscription("message", callback);
75
-
76
- const messageEvent = new MessageEvent("message", { data: "test" });
77
- subscriptions.notify("message", messageEvent);
78
-
79
- expect(callback).not.toHaveBeenCalled();
80
- });
81
-
82
- it("should only remove the specific callback", () => {
83
- const subscriptions = new ConnectionSubscriptions();
84
- const callback1 = vi.fn();
85
- const callback2 = vi.fn();
86
- const callback3 = vi.fn();
87
-
88
- subscriptions.addSubscription("close", callback1);
89
- subscriptions.addSubscription("close", callback2);
90
- subscriptions.addSubscription("close", callback3);
91
-
92
- subscriptions.removeSubscription("close", callback2);
93
-
94
- subscriptions.notify("close", new Event("close"));
95
-
96
- expect(callback1).toHaveBeenCalledTimes(1);
97
- expect(callback2).not.toHaveBeenCalled();
98
- expect(callback3).toHaveBeenCalledTimes(1);
99
- });
100
-
101
- it("should handle removing subscription from non-existent event type", () => {
102
- const subscriptions = new ConnectionSubscriptions();
103
- const callback = vi.fn();
104
-
105
- subscriptions.addSubscription("open", callback);
106
-
107
- expect(() =>
108
- subscriptions.removeSubscription("close", callback),
109
- ).not.toThrow();
110
-
111
- subscriptions.notify("open", new Event("open"));
112
- expect(callback).toHaveBeenCalledTimes(1);
113
- });
114
- });
115
-
116
- describe("notify", () => {
117
- it("should notify all subscribed callbacks for an event", () => {
118
- const subscriptions = new ConnectionSubscriptions();
119
- const callback1 = vi.fn();
120
- const callback2 = vi.fn();
121
-
122
- subscriptions.addSubscription("open", callback1);
123
- subscriptions.addSubscription("open", callback2);
124
-
125
- const openEvent = new Event("open");
126
- subscriptions.notify("open", openEvent);
127
-
128
- expect(callback1).toHaveBeenCalledWith(openEvent);
129
- expect(callback2).toHaveBeenCalledWith(openEvent);
130
- });
131
-
132
- it("should pass the correct data to callbacks", () => {
133
- const subscriptions = new ConnectionSubscriptions();
134
- const callback = vi.fn();
135
-
136
- subscriptions.addSubscription("message", callback);
137
-
138
- const messageEvent = new MessageEvent("message", {
139
- data: "test data",
140
- });
141
- subscriptions.notify("message", messageEvent);
142
-
143
- expect(callback).toHaveBeenCalledWith(messageEvent);
144
- expect(callback.mock.calls[0][0].data).toBe("test data");
145
- });
146
-
147
- it("should not notify callbacks for different event types", () => {
148
- const subscriptions = new ConnectionSubscriptions();
149
- const openCallback = vi.fn();
150
- const closeCallback = vi.fn();
151
-
152
- subscriptions.addSubscription("open", openCallback);
153
- subscriptions.addSubscription("close", closeCallback);
154
-
155
- subscriptions.notify("open", new Event("open"));
156
-
157
- expect(openCallback).toHaveBeenCalledTimes(1);
158
- expect(closeCallback).not.toHaveBeenCalled();
159
- });
160
-
161
- it("should handle exceptions in callbacks without affecting other callbacks", () => {
162
- const subscriptions = new ConnectionSubscriptions();
163
- const throwingCallback = vi.fn(() => {
164
- throw new Error("Callback error");
165
- });
166
- const normalCallback = vi.fn();
167
-
168
- subscriptions.addSubscription("message", throwingCallback);
169
- subscriptions.addSubscription("message", normalCallback);
170
-
171
- const messageEvent = new MessageEvent("message", { data: "test" });
172
-
173
- expect(() => subscriptions.notify("message", messageEvent)).toThrow(
174
- "Callback error",
175
- );
176
-
177
- expect(throwingCallback).toHaveBeenCalledWith(messageEvent);
178
- expect(normalCallback).not.toHaveBeenCalled();
179
- });
180
- });
181
-
182
- describe("Integration scenarios", () => {
183
- it("should handle complete subscription lifecycle", () => {
184
- const subscriptions = new ConnectionSubscriptions();
185
- const callback = vi.fn();
186
-
187
- subscriptions.addSubscription("open", callback);
188
-
189
- subscriptions.notify("open", new Event("open"));
190
- expect(callback).toHaveBeenCalledTimes(1);
191
-
192
- subscriptions.removeSubscription("open", callback);
193
-
194
- subscriptions.notify("open", new Event("open"));
195
- expect(callback).toHaveBeenCalledTimes(1);
196
- });
197
-
198
- it("should handle multiple events with multiple callbacks", () => {
199
- const subscriptions = new ConnectionSubscriptions();
200
-
201
- const openCallback1 = vi.fn();
202
- const openCallback2 = vi.fn();
203
- const closeCallback1 = vi.fn();
204
- const messageCallback = vi.fn();
205
-
206
- subscriptions.addSubscription("open", openCallback1);
207
- subscriptions.addSubscription("open", openCallback2);
208
- subscriptions.addSubscription("close", closeCallback1);
209
- subscriptions.addSubscription("message", messageCallback);
210
-
211
- subscriptions.notify("open", new Event("open"));
212
- expect(openCallback1).toHaveBeenCalledTimes(1);
213
- expect(openCallback2).toHaveBeenCalledTimes(1);
214
- expect(closeCallback1).not.toHaveBeenCalled();
215
- expect(messageCallback).not.toHaveBeenCalled();
216
-
217
- const messageEvent = new MessageEvent("message", { data: "test" });
218
- subscriptions.notify("message", messageEvent);
219
- expect(openCallback1).toHaveBeenCalledTimes(1);
220
- expect(openCallback2).toHaveBeenCalledTimes(1);
221
- expect(closeCallback1).not.toHaveBeenCalled();
222
- expect(messageCallback).toHaveBeenCalledTimes(1);
223
- expect(messageCallback).toHaveBeenCalledWith(messageEvent);
224
-
225
- subscriptions.notify("close", new Event("close"));
226
- expect(openCallback1).toHaveBeenCalledTimes(1);
227
- expect(openCallback2).toHaveBeenCalledTimes(1);
228
- expect(closeCallback1).toHaveBeenCalledTimes(1);
229
- expect(messageCallback).toHaveBeenCalledTimes(1);
230
- });
231
-
232
- it("should handle add and remove operations interleaved", () => {
233
- const subscriptions = new ConnectionSubscriptions();
234
- const callback1 = vi.fn();
235
- const callback2 = vi.fn();
236
- const callback3 = vi.fn();
237
-
238
- subscriptions.addSubscription("message", callback1);
239
- subscriptions.addSubscription("message", callback2);
240
-
241
- const msg1 = new MessageEvent("message", { data: "msg1" });
242
- subscriptions.notify("message", msg1);
243
-
244
- expect(callback1).toHaveBeenCalledWith(msg1);
245
- expect(callback2).toHaveBeenCalledWith(msg1);
246
- expect(callback3).not.toHaveBeenCalled();
247
-
248
- subscriptions.removeSubscription("message", callback1);
249
- subscriptions.addSubscription("message", callback3);
250
-
251
- const msg2 = new MessageEvent("message", { data: "msg2" });
252
- subscriptions.notify("message", msg2);
253
-
254
- expect(callback1).toHaveBeenCalledTimes(1);
255
- expect(callback2).toHaveBeenCalledWith(msg2);
256
- expect(callback3).toHaveBeenCalledWith(msg2);
257
- });
258
-
259
- it("should maintain separate subscription lists per event type", () => {
260
- const subscriptions = new ConnectionSubscriptions();
261
- const callback = vi.fn();
262
-
263
- subscriptions.addSubscription("open", callback);
264
- subscriptions.addSubscription("close", callback);
265
- subscriptions.addSubscription("message", callback);
266
-
267
- subscriptions.removeSubscription("close", callback);
268
-
269
- subscriptions.notify("open", new Event("open"));
270
- expect(callback).toHaveBeenCalledTimes(1);
271
-
272
- subscriptions.notify(
273
- "message",
274
- new MessageEvent("message", { data: "test" }),
275
- );
276
- expect(callback).toHaveBeenCalledTimes(2);
277
-
278
- subscriptions.notify("close", new Event("close"));
279
- expect(callback).toHaveBeenCalledTimes(2);
280
- });
281
- });
282
-
283
- describe("Edge cases", () => {
284
- it("should handle callback that modifies subscriptions", () => {
285
- const subscriptions = new ConnectionSubscriptions();
286
- const callback1 = vi.fn();
287
- const callback2 = vi.fn(() => {
288
- subscriptions.removeSubscription("message", callback1);
289
- });
290
-
291
- subscriptions.addSubscription("message", callback1);
292
- subscriptions.addSubscription("message", callback2);
293
-
294
- const messageEvent = new MessageEvent("message", { data: "test" });
295
- subscriptions.notify("message", messageEvent);
296
-
297
- expect(callback1).toHaveBeenCalledTimes(1);
298
- expect(callback2).toHaveBeenCalledTimes(1);
299
-
300
- callback1.mockClear();
301
- callback2.mockClear();
302
-
303
- subscriptions.notify("message", messageEvent);
304
- expect(callback1).not.toHaveBeenCalled();
305
- expect(callback2).toHaveBeenCalledTimes(1);
306
- });
307
- });
308
- });