@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.
- package/dist/assets/{cell-editor-DFNLDjfX.js → cell-editor-Di0cSFhC.js} +32 -11
- package/dist/assets/{command-palette-OOQHBTnr.js → command-palette-Dg_xstl0.js} +1 -1
- package/dist/assets/{edit-page-DafDcIAI.js → edit-page-BPMo_x-T.js} +4 -4
- package/dist/assets/{hooks-C4GjmtQ-.js → hooks-CHeUZHPp.js} +1 -1
- package/dist/assets/{index-jR_KRDMC.js → index-DckSn2w6.js} +6 -6
- package/dist/assets/{layout-Bq7wRzdR.js → layout-_lp06T0C.js} +2 -2
- package/dist/assets/panels-DU-SaRHy.js +105 -0
- package/dist/assets/{reveal-component-BWu6kkxZ.js → reveal-component-eLftBwOe.js} +1 -1
- package/dist/assets/{run-page-BJq9MULx.js → run-page-mURyATDJ.js} +1 -1
- package/dist/assets/{scratchpad-panel-OHQ7Z0RY.js → scratchpad-panel-B_gEnoqj.js} +1 -1
- package/dist/assets/{useNotebookActions-CqXa3U6u.js → useNotebookActions-DGShHZ_1.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/core/host/AGENTS.md +6 -5
- package/src/core/host/connection.ts +1 -40
- package/src/core/wasm/bridge.ts +0 -8
- package/src/core/websocket/__tests__/connection-state.test.ts +0 -12
- package/src/core/websocket/connection-state.ts +0 -10
- package/dist/assets/panels-Cnu2Flfw.js +0 -105
- package/dist/assets/ws-BXWY541V.js +0 -22
- package/src/core/host/__tests__/events.test.ts +0 -49
- package/src/core/host/events.ts +0 -68
- package/src/core/websocket/__tests__/createConnectionTransport.test.ts +0 -49
- package/src/core/websocket/transports/__tests__/basic.test.ts +0 -275
- package/src/core/websocket/transports/__tests__/sse.test.ts +0 -352
- package/src/core/websocket/transports/__tests__/transport.test.ts +0 -308
- package/src/core/websocket/transports/__tests__/ws.test.ts +0 -125
- package/src/core/websocket/transports/basic.ts +0 -102
- package/src/core/websocket/transports/create.ts +0 -31
- package/src/core/websocket/transports/sse.ts +0 -320
- package/src/core/websocket/transports/transport.ts +0 -50
- package/src/core/websocket/transports/ws.ts +0 -98
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { Logger } from "@/utils/Logger";
|
|
4
|
-
import {
|
|
5
|
-
type ConnectionEvent,
|
|
6
|
-
ConnectionSubscriptions,
|
|
7
|
-
type ConnectionTransportCallback,
|
|
8
|
-
type IConnectionTransport,
|
|
9
|
-
} from "./transport";
|
|
10
|
-
import { MAX_RETRIES, TRANSPORT_EXHAUSTED_REASON } from "./ws";
|
|
11
|
-
|
|
12
|
-
// Mirrors WsTransport/partysocket retry behavior.
|
|
13
|
-
const CONNECTION_TIMEOUT_MS = 10_000;
|
|
14
|
-
const MIN_RETRY_DELAY_MS = 1000;
|
|
15
|
-
const MAX_RETRY_DELAY_MS = 10_000;
|
|
16
|
-
const RETRY_GROWTH_FACTOR = 1.3;
|
|
17
|
-
|
|
18
|
-
// Stream ended without a server close event (transient disconnect); matches
|
|
19
|
-
// the WebSocket abnormal-closure code so close handling stays uniform.
|
|
20
|
-
const ABNORMAL_CLOSURE = 1006;
|
|
21
|
-
|
|
22
|
-
interface ServerClose {
|
|
23
|
-
code: number;
|
|
24
|
-
reason: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* A kernel-connection transport over server-sent events, used when
|
|
29
|
-
* `server.transport` is `"sse"` (experimental).
|
|
30
|
-
*
|
|
31
|
-
* The kernel connection only carries messages server to client (control
|
|
32
|
-
* requests go over HTTP POST), so an SSE stream can fully replace the
|
|
33
|
-
* WebSocket. The server mirrors WebSocket close frames with a `close` SSE
|
|
34
|
-
* event carrying `{code, reason}`; a stream that ends without one is a
|
|
35
|
-
* transient disconnect.
|
|
36
|
-
*
|
|
37
|
-
* Uses `fetch` + stream parsing rather than `EventSource`: we need to
|
|
38
|
-
* surface server close reasons, send auth headers, and match partysocket's
|
|
39
|
-
* retry semantics (per-`reconnect()` retry budget, exhaustion surfaced via
|
|
40
|
-
* `TRANSPORT_EXHAUSTED_REASON`), none of which `EventSource` supports.
|
|
41
|
-
*
|
|
42
|
-
* Like `WsTransport`, starts closed; the first `reconnect()` connects.
|
|
43
|
-
* `close()` stops retrying and does not dispatch a close event.
|
|
44
|
-
*/
|
|
45
|
-
export class SseTransport implements IConnectionTransport {
|
|
46
|
-
private subscriptions = new ConnectionSubscriptions();
|
|
47
|
-
private urlProvider: () => string;
|
|
48
|
-
private headersProvider: () => Record<string, string>;
|
|
49
|
-
private abortController: AbortController | null = null;
|
|
50
|
-
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
51
|
-
private retryCount = 0;
|
|
52
|
-
private userClosed = false;
|
|
53
|
-
private state: WebSocket["readyState"] = WebSocket.CLOSED;
|
|
54
|
-
|
|
55
|
-
public constructor(
|
|
56
|
-
urlProvider: () => string,
|
|
57
|
-
headersProvider: () => Record<string, string> = () => ({}),
|
|
58
|
-
) {
|
|
59
|
-
this.urlProvider = urlProvider;
|
|
60
|
-
this.headersProvider = headersProvider;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
public get readyState(): WebSocket["readyState"] {
|
|
64
|
-
return this.state;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
public reconnect(_code?: number, _reason?: string): void {
|
|
68
|
-
this.userClosed = false;
|
|
69
|
-
this.retryCount = 0;
|
|
70
|
-
this.clearRetryTimer();
|
|
71
|
-
this.abortController?.abort();
|
|
72
|
-
this.connect();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
public close(): void {
|
|
76
|
-
this.userClosed = true;
|
|
77
|
-
this.clearRetryTimer();
|
|
78
|
-
this.state = WebSocket.CLOSED;
|
|
79
|
-
this.abortController?.abort();
|
|
80
|
-
this.abortController = null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
public send(_data: string | ArrayBuffer | Blob | ArrayBufferView): void {
|
|
84
|
-
// The kernel connection is receive-only; all client requests go over
|
|
85
|
-
// HTTP POST endpoints.
|
|
86
|
-
Logger.warn("SseTransport does not support send(); dropping message");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
public addEventListener<T extends ConnectionEvent>(
|
|
90
|
-
event: T,
|
|
91
|
-
callback: ConnectionTransportCallback<T>,
|
|
92
|
-
): void {
|
|
93
|
-
this.subscriptions.addSubscription(
|
|
94
|
-
event,
|
|
95
|
-
callback as ConnectionTransportCallback<ConnectionEvent>,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
public removeEventListener<T extends ConnectionEvent>(
|
|
100
|
-
event: T,
|
|
101
|
-
callback: ConnectionTransportCallback<T>,
|
|
102
|
-
): void {
|
|
103
|
-
this.subscriptions.removeSubscription(
|
|
104
|
-
event,
|
|
105
|
-
callback as ConnectionTransportCallback<ConnectionEvent>,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private connect(): void {
|
|
110
|
-
this.state = WebSocket.CONNECTING;
|
|
111
|
-
void this.run();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private async run(): Promise<void> {
|
|
115
|
-
const controller = new AbortController();
|
|
116
|
-
this.abortController = controller;
|
|
117
|
-
// Long timeout — the server can become slow when many notebooks are open.
|
|
118
|
-
const connectTimeout = setTimeout(
|
|
119
|
-
() => controller.abort(),
|
|
120
|
-
CONNECTION_TIMEOUT_MS,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
let serverClose: ServerClose | null = null;
|
|
124
|
-
try {
|
|
125
|
-
const response = await fetch(this.urlProvider(), {
|
|
126
|
-
headers: {
|
|
127
|
-
...this.headersProvider(),
|
|
128
|
-
Accept: "text/event-stream",
|
|
129
|
-
},
|
|
130
|
-
signal: controller.signal,
|
|
131
|
-
});
|
|
132
|
-
clearTimeout(connectTimeout);
|
|
133
|
-
if (!response.ok || response.body === null) {
|
|
134
|
-
throw new Error(`Unexpected SSE response: ${response.status}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
this.state = WebSocket.OPEN;
|
|
138
|
-
this.subscriptions.notify("open", new Event("open"));
|
|
139
|
-
|
|
140
|
-
serverClose = await this.readEvents(response.body);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
clearTimeout(connectTimeout);
|
|
143
|
-
if (this.isSuperseded(controller)) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
Logger.warn("SSE connection failed", error);
|
|
147
|
-
this.subscriptions.notify("error", new Event("error"));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (this.isSuperseded(controller)) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
controller.abort();
|
|
154
|
-
this.state = WebSocket.CLOSED;
|
|
155
|
-
|
|
156
|
-
// The retry budget counts consecutive attempts without a healthy
|
|
157
|
-
// stream (readEvents resets it on the first message), so a server
|
|
158
|
-
// that accepts and immediately closes still exhausts the budget.
|
|
159
|
-
this.retryCount += 1;
|
|
160
|
-
const exhausted = this.retryCount >= MAX_RETRIES;
|
|
161
|
-
this.subscriptions.notify(
|
|
162
|
-
"close",
|
|
163
|
-
new CloseEvent("close", {
|
|
164
|
-
code: serverClose?.code ?? ABNORMAL_CLOSURE,
|
|
165
|
-
// On exhaustion the reason is rewritten so the consumer stops
|
|
166
|
-
// retrying, matching WsTransport.
|
|
167
|
-
reason: exhausted
|
|
168
|
-
? TRANSPORT_EXHAUSTED_REASON
|
|
169
|
-
: (serverClose?.reason ?? ""),
|
|
170
|
-
wasClean: serverClose !== null,
|
|
171
|
-
}),
|
|
172
|
-
);
|
|
173
|
-
// The consumer's close handler runs synchronously inside notify and
|
|
174
|
-
// may have called reconnect() (starting a fresh connection) or
|
|
175
|
-
// close(); re-check before scheduling our own retry so we never
|
|
176
|
-
// stack a second connection on top of theirs.
|
|
177
|
-
if (exhausted || this.isSuperseded(controller)) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
this.scheduleRetry();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Whether this run was replaced by a newer reconnect() or close(). */
|
|
184
|
-
private isSuperseded(controller: AbortController): boolean {
|
|
185
|
-
return this.userClosed || this.abortController !== controller;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private scheduleRetry(): void {
|
|
189
|
-
this.clearRetryTimer();
|
|
190
|
-
this.state = WebSocket.CONNECTING;
|
|
191
|
-
const delay = Math.min(
|
|
192
|
-
MIN_RETRY_DELAY_MS * RETRY_GROWTH_FACTOR ** this.retryCount,
|
|
193
|
-
MAX_RETRY_DELAY_MS,
|
|
194
|
-
);
|
|
195
|
-
this.retryTimer = setTimeout(() => {
|
|
196
|
-
this.retryTimer = null;
|
|
197
|
-
this.connect();
|
|
198
|
-
}, delay);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private clearRetryTimer(): void {
|
|
202
|
-
if (this.retryTimer !== null) {
|
|
203
|
-
clearTimeout(this.retryTimer);
|
|
204
|
-
this.retryTimer = null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Read and dispatch SSE events until the stream ends.
|
|
210
|
-
*
|
|
211
|
-
* Returns the server close payload if a `close` event arrives, or null
|
|
212
|
-
* if the stream ends without one.
|
|
213
|
-
*/
|
|
214
|
-
private async readEvents(
|
|
215
|
-
body: ReadableStream<Uint8Array>,
|
|
216
|
-
): Promise<ServerClose | null> {
|
|
217
|
-
const reader = body.getReader();
|
|
218
|
-
const decoder = new TextDecoder();
|
|
219
|
-
let buffer = "";
|
|
220
|
-
let eventName: string | null = null;
|
|
221
|
-
let eventId: string | null = null;
|
|
222
|
-
let dataLines: string[] = [];
|
|
223
|
-
|
|
224
|
-
const dispatch = (): ServerClose | null => {
|
|
225
|
-
if (dataLines.length === 0) {
|
|
226
|
-
eventName = null;
|
|
227
|
-
eventId = null;
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
const data = dataLines.join("\n");
|
|
231
|
-
const name = eventName;
|
|
232
|
-
const id = eventId;
|
|
233
|
-
eventName = null;
|
|
234
|
-
eventId = null;
|
|
235
|
-
dataLines = [];
|
|
236
|
-
if (name === "close") {
|
|
237
|
-
return parseServerClose(data);
|
|
238
|
-
}
|
|
239
|
-
// A message means the stream is healthy: reset the retry budget
|
|
240
|
-
// (rather than on open, so a server that accepts and immediately
|
|
241
|
-
// closes cannot retry forever).
|
|
242
|
-
this.retryCount = 0;
|
|
243
|
-
// Normalize SSE fields into the same envelope as host WebSockets.
|
|
244
|
-
// Leave the payload text intact for the client's JSON decoder.
|
|
245
|
-
const envelope =
|
|
246
|
-
name === null
|
|
247
|
-
? data
|
|
248
|
-
: `{"event":${JSON.stringify(name)},"data":${data}${id === null ? "" : `,"id":${JSON.stringify(id)}`}}`;
|
|
249
|
-
this.subscriptions.notify(
|
|
250
|
-
"message",
|
|
251
|
-
new MessageEvent("message", { data: envelope }),
|
|
252
|
-
);
|
|
253
|
-
return null;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
while (true) {
|
|
257
|
-
const { done, value } = await reader.read();
|
|
258
|
-
if (done) {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
buffer += decoder.decode(value, { stream: true });
|
|
262
|
-
|
|
263
|
-
// Consume complete lines with a cursor; a single tail slice per
|
|
264
|
-
// read keeps parsing linear even for many buffered lines.
|
|
265
|
-
let cursor = 0;
|
|
266
|
-
let newlineIndex = buffer.indexOf("\n", cursor);
|
|
267
|
-
while (newlineIndex !== -1) {
|
|
268
|
-
let line = buffer.slice(cursor, newlineIndex);
|
|
269
|
-
cursor = newlineIndex + 1;
|
|
270
|
-
newlineIndex = buffer.indexOf("\n", cursor);
|
|
271
|
-
if (line.endsWith("\r")) {
|
|
272
|
-
line = line.slice(0, -1);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (line === "") {
|
|
276
|
-
const serverClose = dispatch();
|
|
277
|
-
if (serverClose !== null) {
|
|
278
|
-
return serverClose;
|
|
279
|
-
}
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (line.startsWith(":")) {
|
|
283
|
-
// Comment (keep-alive)
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const colonIndex = line.indexOf(":");
|
|
288
|
-
const field = colonIndex === -1 ? line : line.slice(0, colonIndex);
|
|
289
|
-
let fieldValue = colonIndex === -1 ? "" : line.slice(colonIndex + 1);
|
|
290
|
-
if (fieldValue.startsWith(" ")) {
|
|
291
|
-
fieldValue = fieldValue.slice(1);
|
|
292
|
-
}
|
|
293
|
-
if (field === "data") {
|
|
294
|
-
dataLines.push(fieldValue);
|
|
295
|
-
} else if (field === "event") {
|
|
296
|
-
eventName = fieldValue;
|
|
297
|
-
} else if (field === "id") {
|
|
298
|
-
eventId = fieldValue;
|
|
299
|
-
}
|
|
300
|
-
// The host client owns the cursor; `retry` does not override our policy.
|
|
301
|
-
}
|
|
302
|
-
if (cursor > 0) {
|
|
303
|
-
buffer = buffer.slice(cursor);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function parseServerClose(data: string): ServerClose {
|
|
310
|
-
try {
|
|
311
|
-
const parsed = JSON.parse(data) as Partial<ServerClose>;
|
|
312
|
-
return {
|
|
313
|
-
code: typeof parsed.code === "number" ? parsed.code : 1000,
|
|
314
|
-
reason: typeof parsed.reason === "string" ? parsed.reason : "",
|
|
315
|
-
};
|
|
316
|
-
} catch {
|
|
317
|
-
Logger.warn("Failed to parse SSE close event", data);
|
|
318
|
-
return { code: 1000, reason: "" };
|
|
319
|
-
}
|
|
320
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
NotebookTransport,
|
|
5
|
-
ConnectionEvent,
|
|
6
|
-
ConnectionTransportCallback,
|
|
7
|
-
IConnectionTransportMap,
|
|
8
|
-
} from "../../host/connection";
|
|
9
|
-
export type {
|
|
10
|
-
ConnectionEvent,
|
|
11
|
-
ConnectionTransportCallback,
|
|
12
|
-
IConnectionTransportMap,
|
|
13
|
-
} from "../../host/connection";
|
|
14
|
-
|
|
15
|
-
export interface IConnectionTransport extends NotebookTransport {
|
|
16
|
-
send(data: string | ArrayBuffer | Blob | ArrayBufferView<ArrayBuffer>): void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class ConnectionSubscriptions {
|
|
20
|
-
private subscriptions = new Map<
|
|
21
|
-
ConnectionEvent,
|
|
22
|
-
Set<ConnectionTransportCallback<ConnectionEvent>>
|
|
23
|
-
>();
|
|
24
|
-
|
|
25
|
-
public addSubscription(
|
|
26
|
-
event: ConnectionEvent,
|
|
27
|
-
callback: ConnectionTransportCallback<ConnectionEvent>,
|
|
28
|
-
): void {
|
|
29
|
-
if (!this.subscriptions.has(event)) {
|
|
30
|
-
this.subscriptions.set(event, new Set());
|
|
31
|
-
}
|
|
32
|
-
this.subscriptions.get(event)?.add(callback);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
public removeSubscription(
|
|
36
|
-
event: ConnectionEvent,
|
|
37
|
-
callback: ConnectionTransportCallback<ConnectionEvent>,
|
|
38
|
-
): void {
|
|
39
|
-
this.subscriptions.get(event)?.delete(callback);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public notify(
|
|
43
|
-
event: ConnectionEvent,
|
|
44
|
-
data: IConnectionTransportMap[ConnectionEvent],
|
|
45
|
-
): void {
|
|
46
|
-
for (const callback of this.subscriptions.get(event) ?? []) {
|
|
47
|
-
callback(data);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import ReconnectingWebSocket from "partysocket/ws";
|
|
4
|
-
import type {
|
|
5
|
-
ConnectionEvent,
|
|
6
|
-
ConnectionTransportCallback,
|
|
7
|
-
IConnectionTransport,
|
|
8
|
-
} from "./transport";
|
|
9
|
-
|
|
10
|
-
// Per-`reconnect()` retry budget. After exhaustion, partysocket stops silently;
|
|
11
|
-
// the wrapper rewrites the close-event reason to surface the give-up.
|
|
12
|
-
export const MAX_RETRIES = 10;
|
|
13
|
-
|
|
14
|
-
export const TRANSPORT_EXHAUSTED_REASON = "MARIMO_TRANSPORT_EXHAUSTED";
|
|
15
|
-
|
|
16
|
-
export class WsTransport implements IConnectionTransport {
|
|
17
|
-
private inner: ReconnectingWebSocket;
|
|
18
|
-
private closeWrappers = new WeakMap<
|
|
19
|
-
ConnectionTransportCallback<"close">,
|
|
20
|
-
ConnectionTransportCallback<"close">
|
|
21
|
-
>();
|
|
22
|
-
|
|
23
|
-
public constructor(urlProvider: () => string) {
|
|
24
|
-
this.inner = new ReconnectingWebSocket(urlProvider, undefined, {
|
|
25
|
-
maxRetries: MAX_RETRIES,
|
|
26
|
-
debug: false,
|
|
27
|
-
startClosed: true,
|
|
28
|
-
// long timeout — the server can become slow when many notebooks are open.
|
|
29
|
-
connectionTimeout: 10_000,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
public get readyState(): WebSocket["readyState"] {
|
|
34
|
-
return this.inner.readyState as WebSocket["readyState"];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
public reconnect(code?: number, reason?: string): void {
|
|
38
|
-
this.inner.reconnect(code, reason);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public close(): void {
|
|
42
|
-
this.inner.close();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
public send(
|
|
46
|
-
data: string | ArrayBuffer | Blob | ArrayBufferView<ArrayBuffer>,
|
|
47
|
-
): void {
|
|
48
|
-
this.inner.send(data);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
public addEventListener<T extends ConnectionEvent>(
|
|
52
|
-
event: T,
|
|
53
|
-
callback: ConnectionTransportCallback<T>,
|
|
54
|
-
): void {
|
|
55
|
-
if (event === "close") {
|
|
56
|
-
const userCb = callback as ConnectionTransportCallback<"close">;
|
|
57
|
-
// Match native EventTarget dedupe: a second addEventListener with the
|
|
58
|
-
// same listener is a no-op. Without this, repeated adds leak wrappers
|
|
59
|
-
// on the inner socket and double-fire on close.
|
|
60
|
-
if (this.closeWrappers.has(userCb)) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const wrapper: ConnectionTransportCallback<"close"> = (e) => {
|
|
64
|
-
if (this.inner.retryCount >= MAX_RETRIES) {
|
|
65
|
-
userCb(
|
|
66
|
-
new CloseEvent("close", {
|
|
67
|
-
code: e.code,
|
|
68
|
-
reason: TRANSPORT_EXHAUSTED_REASON,
|
|
69
|
-
wasClean: e.wasClean,
|
|
70
|
-
}),
|
|
71
|
-
);
|
|
72
|
-
} else {
|
|
73
|
-
userCb(e);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
this.closeWrappers.set(userCb, wrapper);
|
|
77
|
-
this.inner.addEventListener("close", wrapper);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
this.inner.addEventListener(event, callback as never);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
public removeEventListener<T extends ConnectionEvent>(
|
|
84
|
-
event: T,
|
|
85
|
-
callback: ConnectionTransportCallback<T>,
|
|
86
|
-
): void {
|
|
87
|
-
if (event === "close") {
|
|
88
|
-
const userCb = callback as ConnectionTransportCallback<"close">;
|
|
89
|
-
const wrapper = this.closeWrappers.get(userCb);
|
|
90
|
-
if (wrapper) {
|
|
91
|
-
this.closeWrappers.delete(userCb);
|
|
92
|
-
this.inner.removeEventListener("close", wrapper);
|
|
93
|
-
}
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
this.inner.removeEventListener(event, callback as never);
|
|
97
|
-
}
|
|
98
|
-
}
|