@marimo-team/islands 0.25.3-dev81 → 0.25.3-dev85

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 (38) hide show
  1. package/dist/{ErrorBoundary-CRhA6ihv.js → ErrorBoundary-2IYfhnL4.js} +1 -1
  2. package/dist/{chat-ui-C0_lCzql.js → chat-ui-C9S_7vEJ.js} +4 -4
  3. package/dist/{code-visibility-Bh47dZxU.js → code-visibility-DHJtE2fT.js} +3 -3
  4. package/dist/{common-C0RwQCH0.js → common-COjg9Wjo.js} +5 -5
  5. package/dist/{data-grid-overlay-editor-BVMxTI0f.js → data-grid-overlay-editor-C9wy2To6.js} +1 -1
  6. package/dist/{error-entries-CMNtA8NK.js → error-entries-Cmiq1RT-.js} +1 -1
  7. package/dist/{feedback-button-B_XNkruv.js → feedback-button-wob0Aeib.js} +3 -3
  8. package/dist/{formats-6pLWWrY2.js → formats-ZmooA-MD.js} +1 -1
  9. package/dist/{glide-data-editor-Bk8CGhkL.js → glide-data-editor-bvazCP1C.js} +3 -3
  10. package/dist/{globals-CFPR5UsC.js → globals-dzNj_tMG.js} +1 -1
  11. package/dist/{html-to-image-6xcoTKhC.js → html-to-image-Dc3mTXAs.js} +2 -2
  12. package/dist/main.js +13 -13
  13. package/dist/{number-overlay-editor-DJUv51cZ.js → number-overlay-editor-k3nwtjJY.js} +1 -1
  14. package/dist/{process-output-BJRJFobf.js → process-output-C47Xs8_M.js} +2 -2
  15. package/dist/{reveal-component-BbakmqEi.js → reveal-component-BaedRCK0.js} +4 -4
  16. package/dist/{toDate-CjPuEPou.js → toDate-C6BRw8i5.js} +10 -13
  17. package/dist/{vega-component-DnfQeoyt.js → vega-component-B8IjMdad.js} +2 -2
  18. package/package.json +1 -1
  19. package/src/core/host/AGENTS.md +6 -5
  20. package/src/core/host/connection.ts +1 -40
  21. package/src/core/runtime/__tests__/createWsUrl.test.ts +77 -7
  22. package/src/core/runtime/__tests__/runtime.test.ts +13 -5
  23. package/src/core/runtime/runtime.ts +23 -27
  24. package/src/core/wasm/bridge.ts +0 -8
  25. package/src/core/websocket/__tests__/connection-state.test.ts +0 -12
  26. package/src/core/websocket/connection-state.ts +0 -10
  27. package/src/core/host/__tests__/events.test.ts +0 -49
  28. package/src/core/host/events.ts +0 -68
  29. package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
  30. package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
  31. package/src/core/websocket/transports/__tests__/sse.test.ts +0 -352
  32. package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
  33. package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
  34. package/src/core/websocket/transports/basic.ts +0 -102
  35. package/src/core/websocket/transports/create.ts +0 -31
  36. package/src/core/websocket/transports/sse.ts +0 -320
  37. package/src/core/websocket/transports/transport.ts +0 -50
  38. package/src/core/websocket/transports/ws.ts +0 -98
@@ -128,33 +128,32 @@ export class RuntimeManager {
128
128
  return asWsUrl(this.appendCrossOriginAuth(url).toString());
129
129
  }
130
130
 
131
- /**
132
- * Search params for a session connection: the base URL's params, merged
133
- * with the current page's, plus the session id.
134
- */
135
- private getSessionSearchParams(sessionId: SessionId): URLSearchParams {
136
- const baseUrl = new URL(this.config.url);
137
- const searchParams = new URLSearchParams(baseUrl.search);
138
-
139
- // Merge in current page's query parameters
140
- const currentParams = new URLSearchParams(window.location.search);
141
- currentParams.forEach((value, key) => {
142
- // Don't override base URL params
143
- if (!searchParams.has(key)) {
144
- searchParams.set(key, value);
145
- }
146
- });
147
-
148
- searchParams.set(KnownQueryParams.sessionId, sessionId);
149
- return searchParams;
131
+ /** Keep notebook page parameters separate from connection options. */
132
+ private getSessionURL(path: string, sessionId: SessionId): URL {
133
+ const url = this.formatHttpURL({ path });
134
+ const notebookParams = new URLSearchParams(this.httpURL.search);
135
+ const pageParams = new URLSearchParams(window.location.search);
136
+ for (const key of pageParams.keys()) {
137
+ notebookParams.delete(key);
138
+ }
139
+ for (const [key, value] of pageParams) {
140
+ notebookParams.append(key, value);
141
+ }
142
+ // These parameters are internal and are not exposed by mo.query_params().
143
+ notebookParams.delete(KnownQueryParams.accessToken);
144
+ notebookParams.delete("refresh_token");
145
+ notebookParams.delete(KnownQueryParams.sessionId);
146
+ url.searchParams.set("query_params", notebookParams.toString());
147
+ url.searchParams.set(KnownQueryParams.sessionId, sessionId);
148
+ return url;
150
149
  }
151
150
 
152
151
  /**
153
152
  * The WebSocket URL of the runtime.
154
153
  */
155
154
  public getWsURL(sessionId: SessionId): URL {
156
- const searchParams = this.getSessionSearchParams(sessionId);
157
- return this.formatWsURL("/ws", searchParams);
155
+ const url = this.getSessionURL("/ws", sessionId);
156
+ return asWsUrl(this.appendCrossOriginAuth(url).toString());
158
157
  }
159
158
 
160
159
  /**
@@ -164,11 +163,7 @@ export class RuntimeManager {
164
163
  * (see `headers()`) instead of the URL.
165
164
  */
166
165
  public getSseURL(sessionId: SessionId): URL {
167
- const url = this.formatHttpURL({
168
- path: "/sse",
169
- searchParams: this.getSessionSearchParams(sessionId),
170
- restrictToKnownQueryParams: false,
171
- });
166
+ const url = this.getSessionURL("/sse", sessionId);
172
167
  // The page URL can still carry access_token (merged in from the
173
168
  // current page's params, before auth cleanup strips it); never
174
169
  // forward it — a token in the URL leaks into proxy/server logs.
@@ -180,7 +175,8 @@ export class RuntimeManager {
180
175
  * The WebSocket Sync URL of the runtime, for real-time updates.
181
176
  */
182
177
  public getWsSyncURL(sessionId: SessionId): URL {
183
- return this.formatWsURL("/ws_sync", this.getSessionSearchParams(sessionId));
178
+ const url = this.getSessionURL("/ws_sync", sessionId);
179
+ return asWsUrl(this.appendCrossOriginAuth(url).toString());
184
180
  }
185
181
 
186
182
  /**
@@ -38,8 +38,6 @@ import type {
38
38
  } from "../network/types";
39
39
  import { filenameAtom } from "../saving/file-state";
40
40
  import { store } from "../state/jotai";
41
- import { BasicTransport } from "../websocket/transports/basic";
42
- import type { IConnectionTransport } from "../websocket/transports/transport";
43
41
  import { PyodideRouter } from "./router";
44
42
  import { getWorkerRPC } from "./rpc";
45
43
  import { getWasmRuntimeConfig } from "./runtime-config";
@@ -719,12 +717,6 @@ export class PyodideBridge implements RunRequests, EditRequests {
719
717
  }
720
718
  }
721
719
 
722
- export function createPyodideConnection(): IConnectionTransport {
723
- return BasicTransport.withProducerCallback((callback) => {
724
- PyodideBridge.INSTANCE.attachMessageConsumer(callback);
725
- });
726
- }
727
-
728
720
  // Compose the worker name. The optional "::controller" suffix tells
729
721
  // getController.ts that the host page provides a custom /wasm/controller.js
730
722
  // and that the dynamic import should be attempted. Hosts opt in by setting
@@ -64,16 +64,4 @@ describe("classifyCloseEvent", () => {
64
64
  });
65
65
  });
66
66
  });
67
-
68
- describe("transport exhaustion", () => {
69
- it("MARIMO_TRANSPORT_EXHAUSTED → gave-up with KERNEL_DISCONNECTED", () => {
70
- const decision = classify("MARIMO_TRANSPORT_EXHAUSTED");
71
- expect(decision.kind).toBe("gave-up");
72
- expect(decision.status).toEqual({
73
- state: ConnectionState.CLOSED,
74
- code: WebSocketClosedReason.KERNEL_DISCONNECTED,
75
- reason: "kernel not found",
76
- });
77
- });
78
- });
79
67
  });
@@ -2,22 +2,12 @@
2
2
 
3
3
  import type { CloseDecision } from "../host/connection";
4
4
  import { Logger } from "@/utils/Logger";
5
- import { TRANSPORT_EXHAUSTED_REASON } from "./transports/ws";
6
5
  import { ConnectionState, WebSocketClosedReason } from "./types";
7
6
 
8
7
  // Keep server reasons in sync with ws_endpoint.py and ws/*.py.
9
8
  // SSE carries the same reasons in close events. Unclassified reasons retry.
10
9
  export function classifyCloseEvent(event: { reason?: string }): CloseDecision {
11
10
  switch (event.reason) {
12
- case TRANSPORT_EXHAUSTED_REASON:
13
- return {
14
- kind: "gave-up",
15
- status: {
16
- state: ConnectionState.CLOSED,
17
- code: WebSocketClosedReason.KERNEL_DISCONNECTED,
18
- reason: "kernel not found",
19
- },
20
- };
21
11
  case "MARIMO_NO_FILE_KEY":
22
12
  case "MARIMO_NO_SESSION_ID":
23
13
  case "MARIMO_NO_SESSION":
@@ -1,49 +0,0 @@
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
- });
@@ -1,68 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import { z } from "zod";
4
- import * as Protocol from "@marimo-team/client/protocol";
5
-
6
- /**
7
- * Notebook stream events, including native marimo notifications.
8
- *
9
- * Known events follow the generated protocol. Unknown events retain their name
10
- * and cursor so clients can skip them without losing their place in the stream.
11
- */
12
-
13
- const envelope = z.object({
14
- event: z.string().min(1),
15
- data: z.unknown(),
16
- id: z.string().min(1).optional(),
17
- });
18
-
19
- const knownNames = new Set<string>(
20
- Protocol.Event.options.flatMap((schema) => schema.shape.event.options),
21
- );
22
-
23
- /**
24
- * A native marimo message carried by a host stream.
25
- *
26
- * The protocol leaves extension events to their producer. The payload is
27
- * passed through for the session handler to interpret.
28
- */
29
- const MarimoEvent = z.object({
30
- event: z.literal("marimo"),
31
- id: z.string().min(1).optional(),
32
- data: z.object({ marimo: z.unknown() }),
33
- });
34
-
35
- /** A decoded event, or the name and cursor of an unrecognized event. */
36
- export type NotebookEvent =
37
- | Protocol.EventOutput
38
- | z.output<typeof MarimoEvent>
39
- | {
40
- readonly event: "unknown";
41
- readonly name: string;
42
- readonly id?: string;
43
- };
44
-
45
- const sessionEnvelope = z.object({ data: z.unknown() });
46
-
47
- /**
48
- * Decode a host event or an in-process WASM `{ data: notification }` message.
49
- *
50
- * Throws if the envelope or a known event is malformed. Native notification
51
- * payloads are passed through for the session handler to interpret.
52
- */
53
- export function parseNotebookEvent(value: unknown): NotebookEvent {
54
- if (typeof value === "object" && value !== null && !("event" in value)) {
55
- return {
56
- event: "marimo",
57
- data: { marimo: sessionEnvelope.parse(value).data },
58
- };
59
- }
60
- const parsed = envelope.parse(value);
61
- if (parsed.event === "marimo") {
62
- return MarimoEvent.parse(parsed);
63
- }
64
- if (knownNames.has(parsed.event)) {
65
- return Protocol.Event.parse(parsed);
66
- }
67
- return { event: "unknown", name: parsed.event, id: parsed.id };
68
- }
@@ -1,49 +0,0 @@
1
- /* Copyright 2026 Marimo. All rights reserved. */
2
-
3
- import { afterEach, describe, expect, it, vi } from "vitest";
4
- import { BasicTransport } from "../transports/basic";
5
- import { SseTransport } from "../transports/sse";
6
- import { WsTransport } from "../transports/ws";
7
- import { createConnectionTransport } from "../transports/create";
8
-
9
- vi.mock("../../wasm/utils", () => ({
10
- isWasm: () => false,
11
- }));
12
-
13
- describe("createConnectionTransport", () => {
14
- afterEach(() => {
15
- vi.restoreAllMocks();
16
- });
17
-
18
- const options = {
19
- url: () => "http://localhost/ws",
20
- headers: () => ({}),
21
- };
22
-
23
- it("uses BasicTransport for static notebooks", () => {
24
- const transport = createConnectionTransport({
25
- ...options,
26
- static: true,
27
- transportType: "websocket",
28
- });
29
- expect(transport).toBeInstanceOf(BasicTransport);
30
- });
31
-
32
- it("uses WsTransport by default", () => {
33
- const transport = createConnectionTransport({
34
- ...options,
35
- static: false,
36
- transportType: "websocket",
37
- });
38
- expect(transport).toBeInstanceOf(WsTransport);
39
- });
40
-
41
- it("uses SseTransport when the transport type is sse", () => {
42
- const transport = createConnectionTransport({
43
- ...options,
44
- static: false,
45
- transportType: "sse",
46
- });
47
- expect(transport).toBeInstanceOf(SseTransport);
48
- });
49
- });
@@ -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
- });