@marimo-team/frontend 0.25.3-dev81 → 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 (32) hide show
  1. package/dist/assets/{cell-editor-DFNLDjfX.js → cell-editor-Di0cSFhC.js} +32 -11
  2. package/dist/assets/{command-palette-OOQHBTnr.js → command-palette-Dg_xstl0.js} +1 -1
  3. package/dist/assets/{edit-page-DafDcIAI.js → edit-page-BPMo_x-T.js} +4 -4
  4. package/dist/assets/{hooks-C4GjmtQ-.js → hooks-CHeUZHPp.js} +1 -1
  5. package/dist/assets/{index-jR_KRDMC.js → index-DckSn2w6.js} +6 -6
  6. package/dist/assets/{layout-Bq7wRzdR.js → layout-_lp06T0C.js} +2 -2
  7. package/dist/assets/panels-DU-SaRHy.js +105 -0
  8. package/dist/assets/{reveal-component-BWu6kkxZ.js → reveal-component-eLftBwOe.js} +1 -1
  9. package/dist/assets/{run-page-BJq9MULx.js → run-page-mURyATDJ.js} +1 -1
  10. package/dist/assets/{scratchpad-panel-OHQ7Z0RY.js → scratchpad-panel-B_gEnoqj.js} +1 -1
  11. package/dist/assets/{useNotebookActions-CqXa3U6u.js → useNotebookActions-DGShHZ_1.js} +1 -1
  12. package/dist/index.html +1 -1
  13. package/package.json +1 -1
  14. package/src/core/host/AGENTS.md +6 -5
  15. package/src/core/host/connection.ts +1 -40
  16. package/src/core/wasm/bridge.ts +0 -8
  17. package/src/core/websocket/__tests__/connection-state.test.ts +0 -12
  18. package/src/core/websocket/connection-state.ts +0 -10
  19. package/dist/assets/panels-Cnu2Flfw.js +0 -105
  20. package/dist/assets/ws-BXWY541V.js +0 -22
  21. package/src/core/host/__tests__/events.test.ts +0 -49
  22. package/src/core/host/events.ts +0 -68
  23. package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
  24. package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
  25. package/src/core/websocket/transports/__tests__/sse.test.ts +0 -352
  26. package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
  27. package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
  28. package/src/core/websocket/transports/basic.ts +0 -102
  29. package/src/core/websocket/transports/create.ts +0 -31
  30. package/src/core/websocket/transports/sse.ts +0 -320
  31. package/src/core/websocket/transports/transport.ts +0 -50
  32. package/src/core/websocket/transports/ws.ts +0 -98
@@ -1,275 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import { describe, expect, it, vi } from "vitest";
4
- import { BasicTransport } from "../basic";
5
-
6
- describe("BasicTransport", () => {
7
- describe("close", () => {
8
- it("should trigger close event", () => {
9
- const transport = BasicTransport.empty();
10
- const closeCallback = vi.fn();
11
-
12
- transport.addEventListener("close", closeCallback);
13
- transport.close();
14
-
15
- expect(closeCallback).toHaveBeenCalledTimes(1);
16
- expect(closeCallback).toHaveBeenCalledWith(expect.any(Event));
17
- });
18
- });
19
-
20
- describe("reconnect", () => {
21
- it("should trigger close and open events", async () => {
22
- const transport = BasicTransport.empty();
23
- const openCallback = vi.fn();
24
- const closeCallback = vi.fn();
25
-
26
- transport.addEventListener("open", openCallback);
27
- transport.addEventListener("close", closeCallback);
28
-
29
- // Clear the initial open callback
30
- openCallback.mockClear();
31
-
32
- transport.reconnect();
33
-
34
- // Wait for connect to complete
35
- await new Promise((resolve) => setTimeout(resolve, 10));
36
-
37
- expect(closeCallback).toHaveBeenCalledTimes(1);
38
- expect(openCallback).toHaveBeenCalledTimes(1);
39
- });
40
- });
41
-
42
- describe("send", () => {
43
- it("should trigger message event with string data", async () => {
44
- const transport = BasicTransport.empty();
45
- const messageCallback = vi.fn();
46
-
47
- transport.addEventListener("message", messageCallback);
48
-
49
- transport.send("test message");
50
-
51
- expect(messageCallback).toHaveBeenCalledTimes(1);
52
- expect(messageCallback).toHaveBeenCalledWith(
53
- expect.objectContaining({
54
- data: "test message",
55
- }),
56
- );
57
- });
58
-
59
- it("should trigger message event with ArrayBuffer data", async () => {
60
- const transport = BasicTransport.empty();
61
- const messageCallback = vi.fn();
62
-
63
- transport.addEventListener("message", messageCallback);
64
-
65
- const buffer = new ArrayBuffer(8);
66
- transport.send(buffer);
67
-
68
- expect(messageCallback).toHaveBeenCalledTimes(1);
69
- expect(messageCallback).toHaveBeenCalledWith(
70
- expect.objectContaining({
71
- data: buffer,
72
- }),
73
- );
74
- });
75
- });
76
-
77
- describe("addEventListener", () => {
78
- it("should add open event listener and call it immediately", () => {
79
- const transport = BasicTransport.empty();
80
- const callback = vi.fn();
81
-
82
- transport.addEventListener("open", callback);
83
-
84
- expect(callback).toHaveBeenCalledTimes(1);
85
- expect(callback).toHaveBeenCalledWith(expect.any(Event));
86
- });
87
-
88
- it("should add message event listener and start producer", () => {
89
- const producer = vi.fn();
90
- const transport = BasicTransport.withProducerCallback(producer);
91
- const callback = vi.fn();
92
-
93
- transport.addEventListener("message", callback);
94
-
95
- // Producer should be called once
96
- expect(producer).toHaveBeenCalledTimes(1);
97
- expect(producer).toHaveBeenCalledWith(expect.any(Function));
98
- });
99
- });
100
-
101
- describe("removeEventListener", () => {
102
- it("should remove event listener", async () => {
103
- const transport = BasicTransport.empty();
104
- const callback = vi.fn();
105
-
106
- transport.addEventListener("message", callback);
107
- transport.removeEventListener("message", callback);
108
-
109
- transport.send("test");
110
-
111
- expect(callback).not.toHaveBeenCalled();
112
- });
113
-
114
- it("should only remove the specific listener", () => {
115
- const transport = BasicTransport.empty();
116
- const callback1 = vi.fn();
117
- const callback2 = vi.fn();
118
-
119
- transport.addEventListener("close", callback1);
120
- transport.addEventListener("close", callback2);
121
-
122
- transport.removeEventListener("close", callback1);
123
-
124
- transport.close();
125
-
126
- expect(callback1).not.toHaveBeenCalled();
127
- expect(callback2).toHaveBeenCalledTimes(1);
128
- });
129
- });
130
-
131
- describe("Producer functionality", () => {
132
- it("should pass producer callback that triggers message events", () => {
133
- let producerCallback: ((message: MessageEvent) => void) | undefined;
134
- const producer = vi.fn((callback) => {
135
- producerCallback = callback;
136
- });
137
- const transport = BasicTransport.withProducerCallback(producer);
138
- const messageCallback = vi.fn();
139
-
140
- transport.addEventListener("message", messageCallback);
141
-
142
- expect(producerCallback).toBeDefined();
143
-
144
- const testMessage = new MessageEvent("message", { data: "test" });
145
- producerCallback?.(testMessage);
146
-
147
- expect(messageCallback).toHaveBeenCalledWith(testMessage);
148
- });
149
-
150
- it("should handle multiple messages from producer", () => {
151
- let producerCallback: ((message: MessageEvent) => void) | undefined;
152
- const producer = vi.fn((callback) => {
153
- producerCallback = callback;
154
- });
155
- const transport = BasicTransport.withProducerCallback(producer);
156
- const messageCallback = vi.fn();
157
-
158
- transport.addEventListener("message", messageCallback);
159
-
160
- producerCallback?.(new MessageEvent("message", { data: "msg1" }));
161
- producerCallback?.(new MessageEvent("message", { data: "msg2" }));
162
- producerCallback?.(new MessageEvent("message", { data: "msg3" }));
163
-
164
- expect(messageCallback).toHaveBeenCalledTimes(3);
165
- expect(messageCallback).toHaveBeenNthCalledWith(
166
- 1,
167
- expect.objectContaining({ data: "msg1" }),
168
- );
169
- expect(messageCallback).toHaveBeenNthCalledWith(
170
- 2,
171
- expect.objectContaining({ data: "msg2" }),
172
- );
173
- expect(messageCallback).toHaveBeenNthCalledWith(
174
- 3,
175
- expect.objectContaining({ data: "msg3" }),
176
- );
177
- });
178
-
179
- it("should not start producer if no message listener is added", () => {
180
- const producer = vi.fn();
181
- const transport = BasicTransport.withProducerCallback(producer);
182
-
183
- transport.addEventListener("open", vi.fn());
184
- transport.addEventListener("close", vi.fn());
185
-
186
- expect(producer).not.toHaveBeenCalled();
187
- });
188
- });
189
-
190
- describe("Integration scenarios", () => {
191
- it("should handle complete connection lifecycle", async () => {
192
- const transport = BasicTransport.empty();
193
- const openCallback = vi.fn();
194
- const closeCallback = vi.fn();
195
-
196
- transport.addEventListener("open", openCallback);
197
- transport.addEventListener("close", closeCallback);
198
-
199
- expect(openCallback).toHaveBeenCalledTimes(1);
200
-
201
- transport.reconnect();
202
- await new Promise((resolve) => setTimeout(resolve, 10));
203
- expect(openCallback).toHaveBeenCalledTimes(2);
204
- expect(closeCallback).toHaveBeenCalledTimes(1);
205
-
206
- transport.close();
207
- expect(closeCallback).toHaveBeenCalledTimes(2);
208
- });
209
-
210
- it("should handle send and receive flow", async () => {
211
- let producerCallback: ((message: MessageEvent) => void) | undefined;
212
- const producer = vi.fn((callback) => {
213
- producerCallback = callback;
214
- });
215
- const transport = BasicTransport.withProducerCallback(producer);
216
- const messageCallback = vi.fn();
217
-
218
- transport.addEventListener("message", messageCallback);
219
-
220
- transport.send("outgoing");
221
- expect(messageCallback).toHaveBeenCalledWith(
222
- expect.objectContaining({ data: "outgoing" }),
223
- );
224
-
225
- messageCallback.mockClear();
226
-
227
- producerCallback?.(new MessageEvent("message", { data: "incoming" }));
228
- expect(messageCallback).toHaveBeenCalledWith(
229
- expect.objectContaining({ data: "incoming" }),
230
- );
231
- });
232
-
233
- it("should maintain separate event queues for different event types", async () => {
234
- const transport = BasicTransport.empty();
235
- const openCallback = vi.fn();
236
- const closeCallback = vi.fn();
237
- const messageCallback = vi.fn();
238
-
239
- transport.addEventListener("open", openCallback);
240
- transport.addEventListener("close", closeCallback);
241
- transport.addEventListener("message", messageCallback);
242
-
243
- openCallback.mockClear();
244
-
245
- transport.reconnect();
246
- await new Promise((resolve) => setTimeout(resolve, 10));
247
- transport.send("test");
248
- transport.close();
249
-
250
- expect(openCallback).toHaveBeenCalledTimes(1);
251
- expect(messageCallback).toHaveBeenCalledTimes(1);
252
- expect(closeCallback).toHaveBeenCalledTimes(2);
253
- });
254
-
255
- it("should handle rapid reconnections", async () => {
256
- const transport = BasicTransport.empty();
257
- const openCallback = vi.fn();
258
- const closeCallback = vi.fn();
259
-
260
- transport.addEventListener("open", openCallback);
261
- transport.addEventListener("close", closeCallback);
262
-
263
- openCallback.mockClear();
264
-
265
- transport.reconnect();
266
- transport.reconnect();
267
- transport.reconnect();
268
-
269
- await new Promise((resolve) => setTimeout(resolve, 20));
270
-
271
- expect(closeCallback).toHaveBeenCalledTimes(3);
272
- expect(openCallback).toHaveBeenCalledTimes(3);
273
- });
274
- });
275
- });
@@ -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
- });