@marimo-team/frontend 0.23.16-dev35 → 0.23.16-dev36
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/{CellStatus-CmojVYzm.js → CellStatus-Dua-Q5qp.js} +1 -1
- package/dist/assets/{JsonOutput-BwwRpByA.js → JsonOutput-BjTsPSAI.js} +12 -12
- package/dist/assets/{MarimoErrorOutput-BWte8vjk.js → MarimoErrorOutput-BSyqD6Hg.js} +8 -8
- package/dist/assets/{RenderHTML-DRltV71H.js → RenderHTML-DG0Fm01X.js} +1 -1
- package/dist/assets/{RunButton-DIegd2mN.js → RunButton-DFs1N-c9.js} +1 -1
- package/dist/assets/{add-cell-with-ai-BBFD-M9V.js → add-cell-with-ai-C6Kxfu_D.js} +17 -17
- package/dist/assets/{add-connection-dialog-CECJ8507.js → add-connection-dialog-DAqIrB2O.js} +1 -1
- package/dist/assets/{agent-panel-YaS0WXP1.js → agent-panel-GtkUm7qc.js} +5 -5
- package/dist/assets/ai-model-dropdown-UEHQ_Tx0.js +5 -0
- package/dist/assets/{app-config-button-BW-w01aF.js → app-config-button-BmBlROBo.js} +1 -1
- package/dist/assets/{cell-editor-EV-1IccQ.js → cell-editor-DKzBZjFa.js} +10 -10
- package/dist/assets/{cell-link-C5gpAn3R.js → cell-link-CRHwRCw2.js} +1 -1
- package/dist/assets/cells-C-9Qa_6_.js +235 -0
- package/dist/assets/{chat-display-8BbC5B-D.js → chat-display-CqB9H1kF.js} +1 -1
- package/dist/assets/{chat-panel-CMOZp5xW.js → chat-panel-C3wpuHtk.js} +2 -2
- package/dist/assets/{chat-ui-DWhJ2Ozy.js → chat-ui-DF2nZlBy.js} +1 -1
- package/dist/assets/{column-preview-DeXESYpM.js → column-preview-Bp4QHPE2.js} +1 -1
- package/dist/assets/{command-palette-Dx6AkiPd.js → command-palette-DaJdKaQz.js} +1 -1
- package/dist/assets/{common-XVELED_O.js → common-DeheUlBO.js} +1 -1
- package/dist/assets/{components-BLHpmhXx.js → components-Dk-7Tn_8.js} +1 -1
- package/dist/assets/{datasource-CTqa8bf-.js → datasource-D-oZZRFN.js} +2 -2
- package/dist/assets/{dependency-graph-panel-C4Nzcddp.js → dependency-graph-panel-JK4qjNID.js} +3 -3
- package/dist/assets/{documentation-panel-Bym4G7IS.js → documentation-panel-C_TxLeX8.js} +1 -1
- package/dist/assets/{download-HKgqJX4l.js → download-QhwPYWYu.js} +2 -2
- package/dist/assets/{edit-page-Jtp6Qqjl.js → edit-page-C8yy6X_7.js} +6 -6
- package/dist/assets/{error-entries-CxyvaxEM.js → error-entries-B8hPG61B.js} +2 -2
- package/dist/assets/{error-panel-CfSy7uUr.js → error-panel-BOBOm9wN.js} +1 -1
- package/dist/assets/{file-explorer-panel-Dd5Xn2TK.js → file-explorer-panel-CQOFpuIG.js} +4 -4
- package/dist/assets/{file-icons-CSjJ3JC6.js → file-icons-DoBeqg86.js} +1 -1
- package/dist/assets/{file-name-input-CrKtrXMl.js → file-name-input-B8d-QPzY.js} +1 -1
- package/dist/assets/{floating-outline-ByQ8vvif.js → floating-outline-ylNOBTvA.js} +1 -1
- package/dist/assets/{focus-CVONl-Dy.js → focus-CJzSyxLL.js} +1 -1
- package/dist/assets/{form-Ca3yd_Od.js → form-C2_p_FeE.js} +2 -2
- package/dist/assets/{home-page-FpAawj9s.js → home-page-D_6eKuGc.js} +2 -2
- package/dist/assets/{html-to-image-C-4ysbGe.js → html-to-image-DfyxinVN.js} +2 -2
- package/dist/assets/{index-CiJmYQBT.js → index-D-wPCBTa.js} +7 -7
- package/dist/assets/{index-xs-zSSrX.css → index-D6Dp_PFj.css} +1 -1
- package/dist/assets/{layout-Dtql_o_T.js → layout-BWV4OadK.js} +5 -5
- package/dist/assets/logs-panel-DpMLUEOB.js +1 -0
- package/dist/assets/{markdown-renderer-dABDqZDw.js → markdown-renderer-DtzxgcfB.js} +2 -2
- package/dist/assets/{name-cell-input-Y4knmEIU.js → name-cell-input-SbknxDvi.js} +1 -1
- package/dist/assets/{outline-panel-CQ_KWLip.js → outline-panel-kV7ARoXE.js} +1 -1
- package/dist/assets/{packages-panel-Ijt7d51P.js → packages-panel-B3biBPQ1.js} +1 -1
- package/dist/assets/{panels-DfU1Oikj.js → panels-BsrSW0bC.js} +2 -2
- package/dist/assets/process-output-DCAOrKEU.js +1 -0
- package/dist/assets/{radio-group-22eWJ9fv.js → radio-group-BWAhJtjV.js} +1 -1
- package/dist/assets/readonly-code-display-D_rgEfAm.js +1 -0
- package/dist/assets/{readonly-python-code-n_TrLmsR.js → readonly-python-code-B5ABqdqK.js} +1 -1
- package/dist/assets/{reveal-component-CCLTV1q6.js → reveal-component-CrcdxGl_.js} +2 -2
- package/dist/assets/{run-page-DbezK7Qs.js → run-page-DFHpKr66.js} +1 -1
- package/dist/assets/{scratchpad-panel-C2wAEv5n.js → scratchpad-panel-CfqnZK3C.js} +1 -1
- package/dist/assets/{session-panel-BZGNcZmp.js → session-panel-DWS61Z1x.js} +1 -1
- package/dist/assets/{skeleton-BzlKcq21.js → skeleton-PJnQ2_IA.js} +1 -1
- package/dist/assets/{snippets-panel-DNaNtkR0.js → snippets-panel-D1eovB0O.js} +1 -1
- package/dist/assets/{state-jCdh8Dcx.js → state-BjouLTuw.js} +1 -1
- package/dist/assets/{state-DFPAfLw7.js → state-Bu-aMpIc.js} +3 -3
- package/dist/assets/{textarea-DuEtSOCl.js → textarea-DhPaQ0wx.js} +1 -1
- package/dist/assets/{tracing-ttUd3J7U.js → tracing-B2ux60rQ.js} +1 -1
- package/dist/assets/{tracing-panel-rVAa3En0.js → tracing-panel-CF8H5wRv.js} +2 -2
- package/dist/assets/{tree-actions-UCr1tMHw.js → tree-actions-CPSDN4eE.js} +1 -1
- package/dist/assets/useCellActionButton-k2OXllYI.js +1 -0
- package/dist/assets/{useDeleteCell-PrgejW8A.js → useDeleteCell-C3nmnuxK.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-5jc-_-hy.js → useDependencyPanelTab-D14Lna3z.js} +1 -1
- package/dist/assets/{useNotebookActions-4vw_NiRs.js → useNotebookActions-CHM0Ardg.js} +3 -3
- package/dist/assets/useRunCells-C4qD32Ix.js +1 -0
- package/dist/assets/useSplitCell-BEebdldD.js +1 -0
- package/dist/index.html +23 -23
- package/package.json +2 -3
- package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
- package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
- package/src/core/cells/__tests__/cells.test.ts +12 -2
- package/src/core/cells/__tests__/logs.test.ts +5 -5
- package/src/core/cells/cells.ts +13 -0
- package/src/core/cells/document-changes.ts +1 -0
- package/src/core/cells/logs.ts +14 -4
- package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
- package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
- package/src/core/codemirror/copilot/language-server.ts +182 -70
- package/src/core/codemirror/copilot/transport.ts +15 -207
- package/src/core/codemirror/copilot/types.ts +9 -2
- package/src/core/codemirror/language/languages/python.ts +23 -11
- package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
- package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
- package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
- package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
- package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
- package/src/core/codemirror/lsp/log-messages.ts +37 -0
- package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
- package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
- package/src/core/codemirror/lsp/transports.ts +8 -1
- package/src/core/codemirror/lsp/types.ts +68 -8
- package/src/core/codemirror/lsp/utils.ts +14 -0
- package/src/core/lsp/__tests__/transport.test.ts +509 -337
- package/src/core/lsp/transport.ts +304 -132
- package/src/css/app/codemirror.css +16 -2
- package/src/stories/log-viewer.stories.tsx +12 -6
- package/dist/assets/ai-model-dropdown-BpVBUZnz.js +0 -5
- package/dist/assets/cells-6yd8nOAu.js +0 -231
- package/dist/assets/logs-panel-5u1wUBn1.js +0 -1
- package/dist/assets/process-output-Dd11YI04.js +0 -1
- package/dist/assets/readonly-code-display-BjK-TEri.js +0 -1
- package/dist/assets/useCellActionButton-CJknR0XY.js +0 -1
- package/dist/assets/useRunCells-DHsd7HGL.js +0 -1
- package/dist/assets/useSplitCell-MZpER0Bh.js +0 -1
|
@@ -1,439 +1,611 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { WebSocketTransport } from "@open-rpc/client-js";
|
|
5
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { JSONRPCMessage } from "@marimo-team/codemirror-languageserver";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
4
|
import { Mocks } from "@/__mocks__/common";
|
|
5
|
+
import { isRecord } from "@/utils/records";
|
|
7
6
|
import { ReconnectingWebSocketTransport } from "../transport";
|
|
8
7
|
|
|
9
|
-
// Mock the Logger
|
|
10
8
|
vi.mock("@/utils/Logger", () => ({
|
|
11
9
|
Logger: Mocks.logger(),
|
|
12
10
|
}));
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
class FakeWebSocket {
|
|
13
|
+
static readonly CONNECTING = 0;
|
|
14
|
+
static readonly OPEN = 1;
|
|
15
|
+
static readonly CLOSING = 2;
|
|
16
|
+
static readonly CLOSED = 3;
|
|
17
|
+
static readonly instances: FakeWebSocket[] = [];
|
|
18
|
+
|
|
19
|
+
readonly sent: string[] = [];
|
|
20
|
+
readonly url: string;
|
|
21
|
+
readonly protocols: string | string[] | undefined;
|
|
22
|
+
readyState = FakeWebSocket.CONNECTING;
|
|
23
|
+
private readonly listeners = new Map<
|
|
24
|
+
string,
|
|
25
|
+
Set<(event: { data?: unknown }) => void>
|
|
26
|
+
>();
|
|
27
|
+
|
|
28
|
+
constructor(url: string, protocols?: string | string[]) {
|
|
29
|
+
this.url = url;
|
|
30
|
+
this.protocols = protocols;
|
|
31
|
+
FakeWebSocket.instances.push(this);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
addEventListener(
|
|
35
|
+
type: string,
|
|
36
|
+
listener: (event: { data?: unknown }) => void,
|
|
37
|
+
) {
|
|
38
|
+
const listeners = this.listeners.get(type) ?? new Set();
|
|
39
|
+
listeners.add(listener);
|
|
40
|
+
this.listeners.set(type, listeners);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
send(frame: string) {
|
|
44
|
+
this.sent.push(frame);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
close() {
|
|
48
|
+
this.readyState = FakeWebSocket.CLOSED;
|
|
49
|
+
this.emit("close", {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
open() {
|
|
53
|
+
this.readyState = FakeWebSocket.OPEN;
|
|
54
|
+
this.emit("open", {});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fail() {
|
|
58
|
+
this.emit("error", {});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
disconnect() {
|
|
62
|
+
this.readyState = FakeWebSocket.CLOSED;
|
|
63
|
+
this.emit("close", {});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
receive(message: JSONRPCMessage) {
|
|
67
|
+
this.emit("message", { data: JSON.stringify(message) });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
messages(): JSONRPCMessage[] {
|
|
71
|
+
return this.sent.map((frame) => {
|
|
72
|
+
const message: unknown = JSON.parse(frame);
|
|
73
|
+
if (!isJSONRPCMessage(message)) {
|
|
74
|
+
throw new Error("Fake WebSocket received an invalid JSON-RPC message");
|
|
75
|
+
}
|
|
76
|
+
return message;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private emit(type: string, event: { data?: unknown }) {
|
|
81
|
+
for (const listener of this.listeners.get(type) ?? []) {
|
|
82
|
+
listener(event);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isJSONRPCMessage(value: unknown): value is JSONRPCMessage {
|
|
88
|
+
if (!isRecord(value) || value.jsonrpc !== "2.0") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (typeof value.method === "string") {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return (
|
|
95
|
+
(typeof value.id === "string" || typeof value.id === "number") &&
|
|
96
|
+
("result" in value || "error" in value)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function getSocket(index: number): Promise<FakeWebSocket> {
|
|
101
|
+
await vi.waitFor(() => {
|
|
102
|
+
expect(FakeWebSocket.instances.length).toBeGreaterThan(index);
|
|
103
|
+
});
|
|
104
|
+
return FakeWebSocket.instances[index];
|
|
105
|
+
}
|
|
25
106
|
|
|
26
107
|
describe("ReconnectingWebSocketTransport", () => {
|
|
27
108
|
const mockWsUrl = "ws://localhost:8080/lsp";
|
|
28
|
-
let mockConnection: any;
|
|
29
109
|
|
|
30
110
|
beforeEach(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Create a mock WebSocket connection with readyState
|
|
34
|
-
mockConnection = {
|
|
35
|
-
readyState: WebSocket.OPEN,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// Mock the WebSocketTransport constructor to set the connection
|
|
39
|
-
(WebSocketTransport as any).mockImplementation(function (this: any) {
|
|
40
|
-
this.connection = mockConnection;
|
|
41
|
-
this.connect = vi.fn().mockResolvedValue(undefined);
|
|
42
|
-
this.close = vi.fn();
|
|
43
|
-
this.sendData = vi.fn().mockResolvedValue({ result: "success" });
|
|
44
|
-
this.subscribe = vi.fn();
|
|
45
|
-
this.unsubscribe = vi.fn();
|
|
46
|
-
});
|
|
111
|
+
FakeWebSocket.instances.length = 0;
|
|
112
|
+
vi.stubGlobal("WebSocket", FakeWebSocket);
|
|
47
113
|
});
|
|
48
114
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const transport = new ReconnectingWebSocketTransport({ getWsUrl });
|
|
52
|
-
|
|
53
|
-
expect(transport).toBeDefined();
|
|
54
|
-
expect(getWsUrl).not.toHaveBeenCalled(); // URL function not called until connect
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
vi.unstubAllGlobals();
|
|
55
117
|
});
|
|
56
118
|
|
|
57
|
-
it("
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("should wait for connection before connecting", async () => {
|
|
119
|
+
it("waits for prerequisites and shares concurrent connection attempts", async () => {
|
|
120
|
+
let releaseConnection: (() => void) | undefined;
|
|
121
|
+
const waitForConnection = vi.fn(
|
|
122
|
+
() =>
|
|
123
|
+
new Promise<void>((resolve) => {
|
|
124
|
+
releaseConnection = resolve;
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
68
127
|
const getWsUrl = vi.fn(() => mockWsUrl);
|
|
69
|
-
const waitForConnection = vi.fn().mockResolvedValue(undefined);
|
|
70
128
|
const transport = new ReconnectingWebSocketTransport({
|
|
71
129
|
getWsUrl,
|
|
72
130
|
waitForConnection,
|
|
73
131
|
});
|
|
74
132
|
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
const firstConnection = transport.connect();
|
|
134
|
+
const secondConnection = transport.connect();
|
|
77
135
|
expect(waitForConnection).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(FakeWebSocket.instances).toHaveLength(0);
|
|
137
|
+
|
|
138
|
+
releaseConnection?.();
|
|
139
|
+
const socket = await getSocket(0);
|
|
140
|
+
socket.open();
|
|
141
|
+
await Promise.all([firstConnection, secondConnection]);
|
|
142
|
+
|
|
143
|
+
expect(socket.url).toBe(mockWsUrl);
|
|
78
144
|
expect(getWsUrl).toHaveBeenCalledTimes(1);
|
|
79
145
|
});
|
|
80
146
|
|
|
81
|
-
it("
|
|
82
|
-
const getWsUrl = vi.fn(() => mockWsUrl);
|
|
83
|
-
const waitForConnection = vi
|
|
84
|
-
.fn()
|
|
85
|
-
.mockImplementation(
|
|
86
|
-
() => new Promise((resolve) => setTimeout(resolve, 100)),
|
|
87
|
-
);
|
|
147
|
+
it("forwards messages in both directions and supports unsubscribe", async () => {
|
|
88
148
|
const transport = new ReconnectingWebSocketTransport({
|
|
89
|
-
getWsUrl,
|
|
90
|
-
waitForConnection,
|
|
149
|
+
getWsUrl: () => mockWsUrl,
|
|
91
150
|
});
|
|
151
|
+
const inbound = vi.fn();
|
|
152
|
+
const unsubscribe = transport.onMessage(inbound);
|
|
153
|
+
const connection = transport.connect();
|
|
154
|
+
const socket = await getSocket(0);
|
|
155
|
+
socket.open();
|
|
156
|
+
await connection;
|
|
157
|
+
|
|
158
|
+
const outbound: JSONRPCMessage = {
|
|
159
|
+
jsonrpc: "2.0",
|
|
160
|
+
method: "initialized",
|
|
161
|
+
params: {},
|
|
162
|
+
};
|
|
163
|
+
transport.send(outbound);
|
|
164
|
+
expect(socket.messages()).toEqual([outbound]);
|
|
92
165
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expect(WebSocketTransport).toHaveBeenCalledTimes(1);
|
|
101
|
-
expect(waitForConnection).toHaveBeenCalledTimes(1);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("should send data successfully when connected", async () => {
|
|
105
|
-
const getWsUrl = vi.fn(() => mockWsUrl);
|
|
106
|
-
const transport = new ReconnectingWebSocketTransport({ getWsUrl });
|
|
107
|
-
|
|
108
|
-
await transport.connect();
|
|
109
|
-
|
|
110
|
-
const data: any = { method: "test", params: [] };
|
|
111
|
-
const result = await transport.sendData(data, 5000);
|
|
166
|
+
const notification: JSONRPCMessage = {
|
|
167
|
+
jsonrpc: "2.0",
|
|
168
|
+
method: "textDocument/publishDiagnostics",
|
|
169
|
+
params: { uri: "file:///test.py", diagnostics: [] },
|
|
170
|
+
};
|
|
171
|
+
socket.receive(notification);
|
|
172
|
+
expect(inbound).toHaveBeenCalledWith(notification);
|
|
112
173
|
|
|
113
|
-
|
|
174
|
+
unsubscribe();
|
|
175
|
+
socket.receive(notification);
|
|
176
|
+
expect(inbound).toHaveBeenCalledTimes(1);
|
|
114
177
|
});
|
|
115
178
|
|
|
116
|
-
it("
|
|
117
|
-
const
|
|
118
|
-
|
|
179
|
+
it("restores LSP state before replaying queued requests", async () => {
|
|
180
|
+
const onReconnect = vi.fn(async () => {
|
|
181
|
+
transport.send({
|
|
182
|
+
jsonrpc: "2.0",
|
|
183
|
+
method: "initialized",
|
|
184
|
+
params: {},
|
|
185
|
+
});
|
|
186
|
+
transport.send({
|
|
187
|
+
jsonrpc: "2.0",
|
|
188
|
+
method: "textDocument/didOpen",
|
|
189
|
+
params: { textDocument: { uri: "file:///test.py" } },
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
const transport = new ReconnectingWebSocketTransport({
|
|
193
|
+
getWsUrl: () => mockWsUrl,
|
|
194
|
+
onReconnect,
|
|
195
|
+
});
|
|
119
196
|
|
|
120
|
-
|
|
121
|
-
await
|
|
122
|
-
|
|
197
|
+
const initialConnection = transport.connect();
|
|
198
|
+
const firstSocket = await getSocket(0);
|
|
199
|
+
firstSocket.open();
|
|
200
|
+
await initialConnection;
|
|
201
|
+
firstSocket.disconnect();
|
|
123
202
|
|
|
124
|
-
|
|
125
|
-
|
|
203
|
+
transport.send({
|
|
204
|
+
jsonrpc: "2.0",
|
|
205
|
+
method: "textDocument/didChange",
|
|
206
|
+
params: { textDocument: { uri: "file:///test.py", version: 2 } },
|
|
207
|
+
});
|
|
208
|
+
const completionRequest: JSONRPCMessage = {
|
|
209
|
+
jsonrpc: "2.0",
|
|
210
|
+
id: 1,
|
|
211
|
+
method: "textDocument/completion",
|
|
212
|
+
params: { textDocument: { uri: "file:///test.py" } },
|
|
213
|
+
};
|
|
214
|
+
transport.send(completionRequest);
|
|
126
215
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await
|
|
216
|
+
const secondSocket = await getSocket(1);
|
|
217
|
+
secondSocket.open();
|
|
218
|
+
await vi.waitFor(() => {
|
|
219
|
+
expect(onReconnect).toHaveBeenCalledTimes(1);
|
|
220
|
+
expect(secondSocket.messages()).toHaveLength(3);
|
|
221
|
+
});
|
|
130
222
|
|
|
131
|
-
|
|
132
|
-
|
|
223
|
+
expect(secondSocket.messages()).toEqual([
|
|
224
|
+
{
|
|
225
|
+
jsonrpc: "2.0",
|
|
226
|
+
method: "initialized",
|
|
227
|
+
params: {},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
jsonrpc: "2.0",
|
|
231
|
+
method: "textDocument/didOpen",
|
|
232
|
+
params: { textDocument: { uri: "file:///test.py" } },
|
|
233
|
+
},
|
|
234
|
+
completionRequest,
|
|
235
|
+
]);
|
|
133
236
|
});
|
|
134
237
|
|
|
135
|
-
it("
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
238
|
+
it("holds live requests until asynchronous state restoration finishes", async () => {
|
|
239
|
+
let finishRestoration: (() => void) | undefined;
|
|
240
|
+
const restorationGate = new Promise<void>((resolve) => {
|
|
241
|
+
finishRestoration = resolve;
|
|
242
|
+
});
|
|
243
|
+
const onReconnect = vi.fn(async () => {
|
|
244
|
+
transport.send({
|
|
245
|
+
jsonrpc: "2.0",
|
|
246
|
+
id: 10,
|
|
247
|
+
method: "initialize",
|
|
248
|
+
params: {},
|
|
249
|
+
});
|
|
250
|
+
await restorationGate;
|
|
251
|
+
transport.send({
|
|
252
|
+
jsonrpc: "2.0",
|
|
253
|
+
method: "initialized",
|
|
254
|
+
params: {},
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
const transport = new ReconnectingWebSocketTransport({
|
|
258
|
+
getWsUrl: () => mockWsUrl,
|
|
259
|
+
onReconnect,
|
|
260
|
+
});
|
|
153
261
|
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
262
|
+
const initialConnection = transport.connect();
|
|
263
|
+
const firstSocket = await getSocket(0);
|
|
264
|
+
firstSocket.open();
|
|
265
|
+
await initialConnection;
|
|
266
|
+
firstSocket.disconnect();
|
|
267
|
+
|
|
268
|
+
const secondSocket = await getSocket(1);
|
|
269
|
+
secondSocket.open();
|
|
270
|
+
await vi.waitFor(() => {
|
|
271
|
+
expect(secondSocket.messages()).toEqual([
|
|
272
|
+
{
|
|
273
|
+
jsonrpc: "2.0",
|
|
274
|
+
id: 10,
|
|
275
|
+
method: "initialize",
|
|
276
|
+
params: {},
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
});
|
|
157
280
|
|
|
158
|
-
|
|
159
|
-
|
|
281
|
+
const hoverRequest: JSONRPCMessage = {
|
|
282
|
+
jsonrpc: "2.0",
|
|
283
|
+
id: 11,
|
|
284
|
+
method: "textDocument/hover",
|
|
285
|
+
params: { textDocument: { uri: "file:///test.py" } },
|
|
286
|
+
};
|
|
287
|
+
transport.send(hoverRequest);
|
|
288
|
+
expect(secondSocket.messages()).toHaveLength(1);
|
|
160
289
|
|
|
161
|
-
|
|
162
|
-
await
|
|
290
|
+
finishRestoration?.();
|
|
291
|
+
await vi.waitFor(() => {
|
|
292
|
+
expect(secondSocket.messages()).toHaveLength(3);
|
|
293
|
+
});
|
|
294
|
+
expect(secondSocket.messages()).toEqual([
|
|
295
|
+
{
|
|
296
|
+
jsonrpc: "2.0",
|
|
297
|
+
id: 10,
|
|
298
|
+
method: "initialize",
|
|
299
|
+
params: {},
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
jsonrpc: "2.0",
|
|
303
|
+
method: "initialized",
|
|
304
|
+
params: {},
|
|
305
|
+
},
|
|
306
|
+
hoverRequest,
|
|
307
|
+
]);
|
|
163
308
|
});
|
|
164
309
|
|
|
165
|
-
it("
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
310
|
+
it("replays document changes made while state restoration is in flight", async () => {
|
|
311
|
+
let finishRestoration: (() => void) | undefined;
|
|
312
|
+
const restorationGate = new Promise<void>((resolve) => {
|
|
313
|
+
finishRestoration = resolve;
|
|
314
|
+
});
|
|
315
|
+
const onReconnect = vi.fn(async () => {
|
|
316
|
+
transport.send({
|
|
317
|
+
jsonrpc: "2.0",
|
|
318
|
+
method: "textDocument/didOpen",
|
|
319
|
+
params: { textDocument: { uri: "file:///test.py", version: 3 } },
|
|
320
|
+
});
|
|
321
|
+
await restorationGate;
|
|
322
|
+
});
|
|
323
|
+
const transport = new ReconnectingWebSocketTransport({
|
|
324
|
+
getWsUrl: () => mockWsUrl,
|
|
325
|
+
onReconnect,
|
|
326
|
+
});
|
|
180
327
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
328
|
+
const initialConnection = transport.connect();
|
|
329
|
+
const firstSocket = await getSocket(0);
|
|
330
|
+
firstSocket.open();
|
|
331
|
+
await initialConnection;
|
|
332
|
+
firstSocket.disconnect();
|
|
184
333
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
334
|
+
const staleChange: JSONRPCMessage = {
|
|
335
|
+
jsonrpc: "2.0",
|
|
336
|
+
method: "textDocument/didChange",
|
|
337
|
+
params: { textDocument: { uri: "file:///test.py", version: 2 } },
|
|
338
|
+
};
|
|
339
|
+
transport.send(staleChange);
|
|
188
340
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.close = vi.fn();
|
|
194
|
-
this.sendData = vi.fn();
|
|
341
|
+
const secondSocket = await getSocket(1);
|
|
342
|
+
secondSocket.open();
|
|
343
|
+
await vi.waitFor(() => {
|
|
344
|
+
expect(onReconnect).toHaveBeenCalledTimes(1);
|
|
195
345
|
});
|
|
196
346
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
347
|
+
// The user keeps typing while the resync is still awaiting.
|
|
348
|
+
const liveChange: JSONRPCMessage = {
|
|
349
|
+
jsonrpc: "2.0",
|
|
350
|
+
method: "textDocument/didChange",
|
|
351
|
+
params: { textDocument: { uri: "file:///test.py", version: 4 } },
|
|
352
|
+
};
|
|
353
|
+
transport.send(liveChange);
|
|
200
354
|
|
|
201
|
-
|
|
202
|
-
|
|
355
|
+
finishRestoration?.();
|
|
356
|
+
await vi.waitFor(() => {
|
|
357
|
+
expect(secondSocket.messages()).toHaveLength(2);
|
|
358
|
+
});
|
|
359
|
+
expect(secondSocket.messages()).toEqual([
|
|
360
|
+
{
|
|
361
|
+
jsonrpc: "2.0",
|
|
362
|
+
method: "textDocument/didOpen",
|
|
363
|
+
params: { textDocument: { uri: "file:///test.py", version: 3 } },
|
|
364
|
+
},
|
|
365
|
+
liveChange,
|
|
366
|
+
]);
|
|
367
|
+
expect(secondSocket.messages()).not.toContainEqual(staleChange);
|
|
203
368
|
});
|
|
204
369
|
|
|
205
|
-
it("
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
370
|
+
it("retries failed state restoration without losing queued requests", async () => {
|
|
371
|
+
const onReconnect = vi
|
|
372
|
+
.fn<() => Promise<void>>()
|
|
373
|
+
.mockRejectedValueOnce(new Error("State restoration failed"))
|
|
374
|
+
.mockResolvedValue(undefined);
|
|
210
375
|
const transport = new ReconnectingWebSocketTransport({
|
|
211
|
-
getWsUrl,
|
|
212
|
-
|
|
376
|
+
getWsUrl: () => mockWsUrl,
|
|
377
|
+
onReconnect,
|
|
378
|
+
retries: 2,
|
|
379
|
+
retryDelayMs: 0,
|
|
213
380
|
});
|
|
214
381
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
382
|
+
const initialConnection = transport.connect();
|
|
383
|
+
const firstSocket = await getSocket(0);
|
|
384
|
+
firstSocket.open();
|
|
385
|
+
await initialConnection;
|
|
386
|
+
firstSocket.disconnect();
|
|
387
|
+
|
|
388
|
+
const queuedRequest: JSONRPCMessage = {
|
|
389
|
+
jsonrpc: "2.0",
|
|
390
|
+
id: 12,
|
|
391
|
+
method: "textDocument/hover",
|
|
392
|
+
params: { textDocument: { uri: "file:///test.py" } },
|
|
393
|
+
};
|
|
394
|
+
transport.send(queuedRequest);
|
|
228
395
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
await
|
|
396
|
+
const failedRestorationSocket = await getSocket(1);
|
|
397
|
+
failedRestorationSocket.open();
|
|
398
|
+
const recoveredSocket = await getSocket(2);
|
|
399
|
+
recoveredSocket.open();
|
|
232
400
|
|
|
233
|
-
|
|
401
|
+
await vi.waitFor(() => {
|
|
402
|
+
expect(onReconnect).toHaveBeenCalledTimes(2);
|
|
403
|
+
expect(recoveredSocket.messages()).toEqual([queuedRequest]);
|
|
404
|
+
});
|
|
405
|
+
expect(failedRestorationSocket.messages()).toEqual([]);
|
|
234
406
|
});
|
|
235
407
|
|
|
236
|
-
it("
|
|
237
|
-
|
|
238
|
-
|
|
408
|
+
it("abandons a restoration whose socket died instead of waiting for it", async () => {
|
|
409
|
+
// A restoration awaiting a response on a dead socket only settles when the
|
|
410
|
+
// LSP request times out, so recovery must not be chained to it.
|
|
411
|
+
const strandedRestoration = new Promise<void>(() => {
|
|
412
|
+
// never settles
|
|
413
|
+
});
|
|
414
|
+
const onReconnect = vi
|
|
415
|
+
.fn<() => Promise<void>>()
|
|
416
|
+
.mockImplementationOnce(() => strandedRestoration)
|
|
417
|
+
.mockResolvedValue(undefined);
|
|
239
418
|
const transport = new ReconnectingWebSocketTransport({
|
|
240
|
-
getWsUrl,
|
|
419
|
+
getWsUrl: () => mockWsUrl,
|
|
241
420
|
onReconnect,
|
|
242
421
|
});
|
|
243
422
|
|
|
244
|
-
|
|
245
|
-
await
|
|
246
|
-
|
|
423
|
+
const initialConnection = transport.connect();
|
|
424
|
+
const firstSocket = await getSocket(0);
|
|
425
|
+
firstSocket.open();
|
|
426
|
+
await initialConnection;
|
|
427
|
+
firstSocket.disconnect();
|
|
247
428
|
|
|
248
|
-
|
|
249
|
-
|
|
429
|
+
const secondSocket = await getSocket(1);
|
|
430
|
+
secondSocket.open();
|
|
431
|
+
await vi.waitFor(() => {
|
|
432
|
+
expect(onReconnect).toHaveBeenCalledTimes(1);
|
|
433
|
+
});
|
|
250
434
|
|
|
251
|
-
|
|
252
|
-
const data: any = { method: "test", params: [] };
|
|
253
|
-
await transport.sendData(data, 5000);
|
|
435
|
+
secondSocket.disconnect();
|
|
254
436
|
|
|
255
|
-
|
|
437
|
+
const thirdSocket = await getSocket(2);
|
|
438
|
+
thirdSocket.open();
|
|
439
|
+
await vi.waitFor(() => {
|
|
440
|
+
expect(onReconnect).toHaveBeenCalledTimes(2);
|
|
441
|
+
});
|
|
442
|
+
expect(thirdSocket.readyState).toBe(FakeWebSocket.OPEN);
|
|
256
443
|
});
|
|
257
444
|
|
|
258
|
-
it("
|
|
259
|
-
const
|
|
260
|
-
|
|
445
|
+
it("keeps the queued backlog when a restoration is abandoned", async () => {
|
|
446
|
+
const strandedRestoration = new Promise<void>(() => {
|
|
447
|
+
// never settles
|
|
448
|
+
});
|
|
449
|
+
const onReconnect = vi
|
|
450
|
+
.fn<() => Promise<void>>()
|
|
451
|
+
.mockImplementationOnce(() => strandedRestoration)
|
|
452
|
+
.mockResolvedValue(undefined);
|
|
261
453
|
const transport = new ReconnectingWebSocketTransport({
|
|
262
|
-
getWsUrl,
|
|
454
|
+
getWsUrl: () => mockWsUrl,
|
|
263
455
|
onReconnect,
|
|
264
456
|
});
|
|
265
457
|
|
|
266
|
-
|
|
458
|
+
const initialConnection = transport.connect();
|
|
459
|
+
const firstSocket = await getSocket(0);
|
|
460
|
+
firstSocket.open();
|
|
461
|
+
await initialConnection;
|
|
462
|
+
firstSocket.disconnect();
|
|
463
|
+
|
|
464
|
+
const queuedRequest: JSONRPCMessage = {
|
|
465
|
+
jsonrpc: "2.0",
|
|
466
|
+
id: 21,
|
|
467
|
+
method: "textDocument/hover",
|
|
468
|
+
params: { textDocument: { uri: "file:///test.py" } },
|
|
469
|
+
};
|
|
470
|
+
transport.send(queuedRequest);
|
|
267
471
|
|
|
268
|
-
|
|
472
|
+
const secondSocket = await getSocket(1);
|
|
473
|
+
secondSocket.open();
|
|
474
|
+
await vi.waitFor(() => {
|
|
475
|
+
expect(onReconnect).toHaveBeenCalledTimes(1);
|
|
476
|
+
});
|
|
477
|
+
// The doomed attempt detached the backlog; abandoning it must hand the
|
|
478
|
+
// messages back rather than strand them.
|
|
479
|
+
secondSocket.disconnect();
|
|
480
|
+
|
|
481
|
+
const thirdSocket = await getSocket(2);
|
|
482
|
+
thirdSocket.open();
|
|
483
|
+
await vi.waitFor(() => {
|
|
484
|
+
expect(thirdSocket.messages()).toEqual([queuedRequest]);
|
|
485
|
+
});
|
|
269
486
|
});
|
|
270
487
|
|
|
271
|
-
it("
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
488
|
+
it("clears the in-flight backlog when closed during restoration", async () => {
|
|
489
|
+
const strandedRestoration = new Promise<void>(() => {
|
|
490
|
+
// never settles
|
|
491
|
+
});
|
|
492
|
+
const onReconnect = vi.fn<() => Promise<void>>(() => strandedRestoration);
|
|
275
493
|
const transport = new ReconnectingWebSocketTransport({
|
|
276
|
-
getWsUrl,
|
|
494
|
+
getWsUrl: () => mockWsUrl,
|
|
277
495
|
onReconnect,
|
|
278
496
|
});
|
|
279
497
|
|
|
280
|
-
|
|
281
|
-
await
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// Reconnect - should propagate the error from onReconnect
|
|
287
|
-
const data: any = { method: "test", params: [] };
|
|
288
|
-
await expect(transport.sendData(data, 5000)).rejects.toThrow(
|
|
289
|
-
"Reconnect callback failed",
|
|
290
|
-
);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe("subscribe", () => {
|
|
294
|
-
it("should track subscriptions", () => {
|
|
295
|
-
const getWsUrl = vi.fn(() => mockWsUrl);
|
|
296
|
-
const transport = new ReconnectingWebSocketTransport({ getWsUrl });
|
|
297
|
-
|
|
298
|
-
const handler = vi.fn();
|
|
299
|
-
transport.subscribe("notification", handler);
|
|
498
|
+
const initialConnection = transport.connect();
|
|
499
|
+
const firstSocket = await getSocket(0);
|
|
500
|
+
firstSocket.open();
|
|
501
|
+
await initialConnection;
|
|
502
|
+
firstSocket.disconnect();
|
|
300
503
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
});
|
|
504
|
+
transport.send({
|
|
505
|
+
jsonrpc: "2.0",
|
|
506
|
+
method: "textDocument/didChange",
|
|
507
|
+
params: {},
|
|
306
508
|
});
|
|
307
509
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
await transport.connect();
|
|
313
|
-
|
|
314
|
-
const handler = vi.fn();
|
|
315
|
-
transport.subscribe("notification", handler);
|
|
316
|
-
|
|
317
|
-
const delegate = (transport as any).delegate;
|
|
318
|
-
expect(delegate.subscribe).toHaveBeenCalledWith("notification", handler);
|
|
510
|
+
const secondSocket = await getSocket(1);
|
|
511
|
+
secondSocket.open();
|
|
512
|
+
await vi.waitFor(() => {
|
|
513
|
+
expect(onReconnect).toHaveBeenCalledTimes(1);
|
|
319
514
|
});
|
|
515
|
+
// Restoration is now stranded and holding the backlog it detached.
|
|
516
|
+
expect(
|
|
517
|
+
(transport as unknown as { inFlightBacklog?: unknown[] }).inFlightBacklog,
|
|
518
|
+
).toHaveLength(1);
|
|
320
519
|
|
|
321
|
-
|
|
322
|
-
const getWsUrl = vi.fn(() => mockWsUrl);
|
|
323
|
-
const transport = new ReconnectingWebSocketTransport({ getWsUrl });
|
|
324
|
-
|
|
325
|
-
const handler1 = vi.fn();
|
|
326
|
-
const handler2 = vi.fn();
|
|
327
|
-
transport.subscribe("notification", handler1);
|
|
328
|
-
transport.subscribe("response", handler2);
|
|
520
|
+
transport.close();
|
|
329
521
|
|
|
330
|
-
|
|
522
|
+
expect(
|
|
523
|
+
(transport as unknown as { inFlightBacklog?: unknown[] }).inFlightBacklog,
|
|
524
|
+
).toBeUndefined();
|
|
525
|
+
});
|
|
331
526
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
527
|
+
it("keeps inbound subscriptions active after reconnection", async () => {
|
|
528
|
+
const transport = new ReconnectingWebSocketTransport({
|
|
529
|
+
getWsUrl: () => mockWsUrl,
|
|
335
530
|
});
|
|
531
|
+
const inbound = vi.fn();
|
|
532
|
+
transport.onMessage(inbound);
|
|
533
|
+
const initialConnection = transport.connect();
|
|
534
|
+
const firstSocket = await getSocket(0);
|
|
535
|
+
firstSocket.open();
|
|
536
|
+
await initialConnection;
|
|
537
|
+
|
|
538
|
+
firstSocket.disconnect();
|
|
539
|
+
const secondSocket = await getSocket(1);
|
|
540
|
+
secondSocket.open();
|
|
541
|
+
const notification: JSONRPCMessage = {
|
|
542
|
+
jsonrpc: "2.0",
|
|
543
|
+
method: "window/logMessage",
|
|
544
|
+
params: { type: 3, message: "reconnected" },
|
|
545
|
+
};
|
|
546
|
+
secondSocket.receive(notification);
|
|
336
547
|
|
|
337
|
-
|
|
338
|
-
const getWsUrl = vi.fn(() => mockWsUrl);
|
|
339
|
-
const transport = new ReconnectingWebSocketTransport({ getWsUrl });
|
|
340
|
-
|
|
341
|
-
// Add subscription before connection
|
|
342
|
-
const handler = vi.fn();
|
|
343
|
-
transport.subscribe("notification", handler);
|
|
344
|
-
|
|
345
|
-
// First connection
|
|
346
|
-
await transport.connect();
|
|
347
|
-
const firstDelegate = (transport as any).delegate;
|
|
348
|
-
expect(firstDelegate.subscribe).toHaveBeenCalledWith(
|
|
349
|
-
"notification",
|
|
350
|
-
handler,
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
// Clear mock calls
|
|
354
|
-
firstDelegate.subscribe.mockClear();
|
|
355
|
-
|
|
356
|
-
// Simulate connection loss
|
|
357
|
-
mockConnection.readyState = WebSocket.CLOSED;
|
|
358
|
-
|
|
359
|
-
// Reconnect by sending data
|
|
360
|
-
const data: any = { method: "test", params: [] };
|
|
361
|
-
await transport.sendData(data, 5000);
|
|
362
|
-
|
|
363
|
-
// New delegate should have been created
|
|
364
|
-
const secondDelegate = (transport as any).delegate;
|
|
365
|
-
expect(secondDelegate).not.toBe(firstDelegate);
|
|
366
|
-
|
|
367
|
-
// Subscription should be re-registered on new delegate
|
|
368
|
-
expect(secondDelegate.subscribe).toHaveBeenCalledWith(
|
|
369
|
-
"notification",
|
|
370
|
-
handler,
|
|
371
|
-
);
|
|
372
|
-
});
|
|
548
|
+
expect(inbound).toHaveBeenCalledWith(notification);
|
|
373
549
|
});
|
|
374
550
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
expect((transport as any).pendingSubscriptions).toHaveLength(1);
|
|
383
|
-
|
|
384
|
-
transport.unsubscribe("notification", handler);
|
|
385
|
-
expect((transport as any).pendingSubscriptions).toHaveLength(0);
|
|
551
|
+
it("retries failed connection attempts", async () => {
|
|
552
|
+
const onConnectionFailure = vi.fn();
|
|
553
|
+
const transport = new ReconnectingWebSocketTransport({
|
|
554
|
+
getWsUrl: () => mockWsUrl,
|
|
555
|
+
retries: 2,
|
|
556
|
+
retryDelayMs: 0,
|
|
557
|
+
onConnectionFailure,
|
|
386
558
|
});
|
|
387
559
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const handler = vi.fn();
|
|
395
|
-
transport.subscribe("notification", handler);
|
|
560
|
+
const connection = transport.connect();
|
|
561
|
+
const firstSocket = await getSocket(0);
|
|
562
|
+
firstSocket.fail();
|
|
563
|
+
const secondSocket = await getSocket(1);
|
|
564
|
+
secondSocket.open();
|
|
565
|
+
await connection;
|
|
396
566
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
transport.unsubscribe("notification", handler);
|
|
567
|
+
expect(secondSocket.readyState).toBe(FakeWebSocket.OPEN);
|
|
568
|
+
expect(onConnectionFailure).not.toHaveBeenCalled();
|
|
569
|
+
});
|
|
401
570
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
)
|
|
571
|
+
it("reports the final connection failure", async () => {
|
|
572
|
+
const onConnectionFailure = vi.fn();
|
|
573
|
+
const transport = new ReconnectingWebSocketTransport({
|
|
574
|
+
getWsUrl: () => mockWsUrl,
|
|
575
|
+
retries: 1,
|
|
576
|
+
onConnectionFailure,
|
|
406
577
|
});
|
|
407
578
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const handler1 = vi.fn();
|
|
413
|
-
const handler2 = vi.fn();
|
|
414
|
-
transport.subscribe("notification", handler1);
|
|
415
|
-
transport.subscribe("response", handler2);
|
|
416
|
-
|
|
417
|
-
await transport.connect();
|
|
418
|
-
|
|
419
|
-
// Unsubscribe handler1
|
|
420
|
-
transport.unsubscribe("notification", handler1);
|
|
579
|
+
const connection = transport.connect();
|
|
580
|
+
const socket = await getSocket(0);
|
|
581
|
+
socket.fail();
|
|
421
582
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
583
|
+
await expect(connection).rejects.toThrow(
|
|
584
|
+
"WebSocket connection to ws://localhost:8080/lsp failed",
|
|
585
|
+
);
|
|
586
|
+
expect(onConnectionFailure).toHaveBeenCalledWith(
|
|
587
|
+
expect.objectContaining({ message: expect.stringContaining("failed") }),
|
|
588
|
+
);
|
|
589
|
+
});
|
|
428
590
|
|
|
429
|
-
|
|
591
|
+
it("closes permanently and ignores later sends", async () => {
|
|
592
|
+
const transport = new ReconnectingWebSocketTransport({
|
|
593
|
+
getWsUrl: () => mockWsUrl,
|
|
594
|
+
});
|
|
595
|
+
const connection = transport.connect();
|
|
596
|
+
const socket = await getSocket(0);
|
|
597
|
+
socket.open();
|
|
598
|
+
await connection;
|
|
430
599
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
expect(newDelegate.subscribe).toHaveBeenCalledWith("response", handler2);
|
|
600
|
+
transport.close();
|
|
601
|
+
transport.send({
|
|
602
|
+
jsonrpc: "2.0",
|
|
603
|
+
method: "initialized",
|
|
604
|
+
params: {},
|
|
437
605
|
});
|
|
606
|
+
|
|
607
|
+
expect(socket.messages()).toEqual([]);
|
|
608
|
+
await expect(transport.connect()).rejects.toThrow("Transport is closed");
|
|
609
|
+
expect(FakeWebSocket.instances).toHaveLength(1);
|
|
438
610
|
});
|
|
439
611
|
});
|