websocket-text-relay 1.0.0
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/.eslintrc +29 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/docs/code-structure.md +77 -0
- package/docs/creating-text-editor-plugin.md +10 -0
- package/docs/dev-getting-started.md +8 -0
- package/index.js +87 -0
- package/package.json +45 -0
- package/src/language-server/JsonRpcInterface.js +126 -0
- package/src/language-server/JsonRpcInterface.test.js +496 -0
- package/src/language-server/LspReader.js +119 -0
- package/src/language-server/LspReader.test.js +508 -0
- package/src/language-server/LspWriter.js +44 -0
- package/src/language-server/LspWriter.test.js +179 -0
- package/src/language-server/constants.js +23 -0
- package/src/ui/css/fonts/Montserrat-Black.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Bold.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-ExtraBold.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-ExtraLight.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Light.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Medium.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Regular.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-SemiBold.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Thin.ttf +0 -0
- package/src/ui/css/fonts.css +54 -0
- package/src/ui/css/main.css +198 -0
- package/src/ui/index.html +36 -0
- package/src/ui/js/components/ActivityTimeseriesGraph.js +149 -0
- package/src/ui/js/components/HeaderSummary.js +23 -0
- package/src/ui/js/components/ServerStatus.js +31 -0
- package/src/ui/js/components/SessionLabels.js +171 -0
- package/src/ui/js/components/SessionWedges.js +107 -0
- package/src/ui/js/components/StatusRing.js +42 -0
- package/src/ui/js/components/grids.js +29 -0
- package/src/ui/js/index.js +117 -0
- package/src/ui/js/main.js +97 -0
- package/src/ui/js/util/DependencyManager.js +31 -0
- package/src/ui/js/util/EventEmitter.js +40 -0
- package/src/ui/js/util/WebsocketClient.js +76 -0
- package/src/ui/js/util/constants.js +9 -0
- package/src/ui/js/util/drawing.js +128 -0
- package/src/websocket-interface/WebsocketClient.js +56 -0
- package/src/websocket-interface/WebsocketInterface.js +71 -0
- package/src/websocket-interface/WtrSession.js +122 -0
- package/src/websocket-interface/httpServer.js +58 -0
- package/src/websocket-interface/sessionManager.js +107 -0
- package/src/websocket-interface/util.js +12 -0
- package/src/websocket-interface/websocketApi.js +116 -0
- package/src/websocket-interface/websocketServer.js +18 -0
- package/start.js +4 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest"
|
|
2
|
+
import { PassThrough, Writable } from 'node:stream'
|
|
3
|
+
import { JsonRpcInterface } from "./JsonRpcInterface.js"
|
|
4
|
+
import { TESTONLY_resetRequestId } from "./LspWriter.js"
|
|
5
|
+
import { invalidRequestErrorCode, invalidRequestErrorMessage, methodNotFoundErrorCode, methodNotFoundErrorMessage, unexpectedNotificationErrorCode,
|
|
6
|
+
unexpectedNotificationErrorMessage, unexpectedRequestErrorCode, unexpectedRequestErrorMessage } from "./constants.js"
|
|
7
|
+
|
|
8
|
+
describe("JsonRpcInterface", () => {
|
|
9
|
+
describe("onNotification", () => {
|
|
10
|
+
describe("when a we receive a notification message that has a handler", () => {
|
|
11
|
+
let inputStream, outputStream, jsonRpc
|
|
12
|
+
const incomingMessage = `Content-Length: 64\r\n\r\n{"jsonrpc":"2.0","method":"test/test-method","params":{"a":"1"}}`
|
|
13
|
+
const expectedParams = {a: "1"}
|
|
14
|
+
let actualParams = null
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
inputStream = new PassThrough()
|
|
18
|
+
outputStream = new PassThrough()
|
|
19
|
+
jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
20
|
+
|
|
21
|
+
jsonRpc.onNotification("test/test-method", (params) => {
|
|
22
|
+
actualParams= params
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
inputStream.write(incomingMessage)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("should call the handler with the correct params", () => {
|
|
29
|
+
expect(actualParams).to.deep.equal(expectedParams)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe("when attempting to register duplicate onNotification handlers ", () => {
|
|
34
|
+
let inputStream, outputStream, jsonRpc
|
|
35
|
+
let f1 = () => {}
|
|
36
|
+
let f2 = () => {}
|
|
37
|
+
|
|
38
|
+
beforeAll(() => {
|
|
39
|
+
inputStream = new PassThrough()
|
|
40
|
+
outputStream = new PassThrough()
|
|
41
|
+
jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
42
|
+
|
|
43
|
+
jsonRpc.onNotification("test/test-method", f1)
|
|
44
|
+
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("should throw an error if we try to register the same method twice (without first removing the handler)", () => {
|
|
48
|
+
expect(() => jsonRpc.onNotification("test/test-method", f2)).to.toThrowError(/duplicate method handlers/i)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("when using removeNotificationHandler to change the existing method handler", () => {
|
|
53
|
+
let inputStream, outputStream, jsonRpc
|
|
54
|
+
const incomingMessage = `Content-Length: 64\r\n\r\n{"jsonrpc":"2.0","method":"test/test-method","params":{"a":"1"}}`
|
|
55
|
+
let f1Fired = false
|
|
56
|
+
let f2Fired = false
|
|
57
|
+
let f1 = () => f1Fired = true
|
|
58
|
+
let f2 = () => f2Fired = true
|
|
59
|
+
|
|
60
|
+
beforeAll(() => {
|
|
61
|
+
inputStream = new PassThrough()
|
|
62
|
+
outputStream = new PassThrough()
|
|
63
|
+
jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
64
|
+
|
|
65
|
+
jsonRpc.onNotification("test/test-method", f1)
|
|
66
|
+
jsonRpc.removeNotificationHandler("test/test-method")
|
|
67
|
+
jsonRpc.onNotification("test/test-method", f2)
|
|
68
|
+
|
|
69
|
+
inputStream.write(incomingMessage)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("should call function f2 instead of f1", () => {
|
|
73
|
+
expect(f1Fired).to.equal(false)
|
|
74
|
+
expect(f2Fired).to.equal(true)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe("when a notification handler throws an error", () => {
|
|
79
|
+
let inputStream, outputStream, jsonRpc
|
|
80
|
+
const incomingMessage = `Content-Length: 64\r\n\r\n{"jsonrpc":"2.0","method":"test/test-method","params":{"a":"1"}}`
|
|
81
|
+
const expectedError = {code: unexpectedNotificationErrorCode, message: unexpectedNotificationErrorMessage, data: {method: "test/test-method", params: {a: "1"}, error: "Test notification error"}}
|
|
82
|
+
let actualError
|
|
83
|
+
|
|
84
|
+
beforeAll(() => {
|
|
85
|
+
inputStream = new PassThrough()
|
|
86
|
+
outputStream = new PassThrough()
|
|
87
|
+
jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
88
|
+
|
|
89
|
+
jsonRpc.onNotification("test/test-method", () => {
|
|
90
|
+
throw new Error("Test notification error")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
jsonRpc.events.on('notification-error', (error) => {
|
|
94
|
+
actualError = error
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
inputStream.write(incomingMessage)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("should fire the notification-error event with the correct error data", () => {
|
|
101
|
+
expect(actualError).to.deep.equal(expectedError)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe("when we receive a notification without a handler", () => {
|
|
106
|
+
let inputStream, outputStream, jsonRpc
|
|
107
|
+
const incomingMessage = `Content-Length: 64\r\n\r\n{"jsonrpc":"2.0","method":"test/test-method","params":{"a":"1"}}`
|
|
108
|
+
const expectedError = {code: methodNotFoundErrorCode, message: methodNotFoundErrorMessage, data: {method: "test/test-method"}}
|
|
109
|
+
let actualError
|
|
110
|
+
|
|
111
|
+
beforeAll(() => {
|
|
112
|
+
inputStream = new PassThrough()
|
|
113
|
+
outputStream = new PassThrough()
|
|
114
|
+
jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
115
|
+
|
|
116
|
+
jsonRpc.events.on('notification-error', (error) => {
|
|
117
|
+
actualError = error
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
inputStream.write(incomingMessage)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("should fire the notification-error event with the correct error data", () => {
|
|
124
|
+
expect(actualError).to.deep.equal(expectedError)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe("onRequest", () => {
|
|
130
|
+
describe("when a we receive a request message that has a handler", () => {
|
|
131
|
+
const incomingMessage = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
|
|
132
|
+
const expectedParams = {a: "1"}
|
|
133
|
+
let actualParams = null
|
|
134
|
+
const expectedOutput = `Content-Length: 54\r\n\r\n{"jsonrpc":"2.0","id":"0","result":{"someValue":"33"}}`
|
|
135
|
+
let actualOutput = ""
|
|
136
|
+
|
|
137
|
+
beforeAll(() => {
|
|
138
|
+
const inputStream = new PassThrough()
|
|
139
|
+
const outputStream = new Writable({
|
|
140
|
+
write (data, _enc, next) {
|
|
141
|
+
actualOutput += data.toString()
|
|
142
|
+
next()
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
const jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
146
|
+
|
|
147
|
+
jsonRpc.onRequest("test/test-method", (params) => {
|
|
148
|
+
actualParams= params
|
|
149
|
+
return {someValue: "33"}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
inputStream.write(incomingMessage)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("should call the handler with the correct params", () => {
|
|
156
|
+
expect(actualParams).to.deep.equal(expectedParams)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("should write the response to the output stream", () => {
|
|
160
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe("should be able to use an async function as a handler", () => {
|
|
165
|
+
const incomingMessage = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
|
|
166
|
+
const expectedParams = {a: "1"}
|
|
167
|
+
let actualParams = null
|
|
168
|
+
const expectedOutput = `Content-Length: 54\r\n\r\n{"jsonrpc":"2.0","id":"0","result":{"someValue":"34"}}`
|
|
169
|
+
let actualOutput = ""
|
|
170
|
+
|
|
171
|
+
beforeAll(() => {
|
|
172
|
+
const inputStream = new PassThrough()
|
|
173
|
+
const outputStream = new Writable({
|
|
174
|
+
write (data, _enc, next) {
|
|
175
|
+
actualOutput += data.toString()
|
|
176
|
+
next()
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
const jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
180
|
+
|
|
181
|
+
jsonRpc.onRequest("test/test-method", async (params) => {
|
|
182
|
+
actualParams= params
|
|
183
|
+
const someValue = await new Promise((resolve) => {
|
|
184
|
+
setTimeout(() => resolve("34"), 1)
|
|
185
|
+
})
|
|
186
|
+
return {someValue}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
inputStream.write(incomingMessage)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("should call the handler with the correct params", () => {
|
|
193
|
+
expect(actualParams).to.deep.equal(expectedParams)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("should not write the response until the promise resolves", () => {
|
|
197
|
+
expect(actualOutput).toEqual("")
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("should write the response after the async function resolves with the result", async () => {
|
|
201
|
+
await new Promise((resolve) => setTimeout(resolve, 1))
|
|
202
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe("when attempting to register duplicate onRequest handlers ", () => {
|
|
207
|
+
let inputStream, outputStream, jsonRpc
|
|
208
|
+
let f1 = () => {}
|
|
209
|
+
let f2 = () => {}
|
|
210
|
+
|
|
211
|
+
beforeAll(() => {
|
|
212
|
+
inputStream = new PassThrough()
|
|
213
|
+
outputStream = new PassThrough()
|
|
214
|
+
jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
215
|
+
|
|
216
|
+
jsonRpc.onRequest("test/test-method", f1)
|
|
217
|
+
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it("should throw an error if we try to register the same method twice (without first removing the handler)", () => {
|
|
221
|
+
expect(() => jsonRpc.onRequest("test/test-method", f2)).to.toThrowError(/duplicate method handlers/i)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe("when using removeRequestHandler to change the existing method handler", () => {
|
|
226
|
+
const incomingMessage = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
|
|
227
|
+
let f1Fired = false
|
|
228
|
+
let f2Fired = false
|
|
229
|
+
let f1 = () => f1Fired = true
|
|
230
|
+
let f2 = () => f2Fired = true
|
|
231
|
+
|
|
232
|
+
beforeAll(() => {
|
|
233
|
+
const inputStream = new PassThrough()
|
|
234
|
+
const outputStream = new PassThrough()
|
|
235
|
+
const jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
236
|
+
|
|
237
|
+
jsonRpc.onRequest("test/test-method", f1)
|
|
238
|
+
jsonRpc.removeRequestHandler("test/test-method")
|
|
239
|
+
jsonRpc.onRequest("test/test-method", f2)
|
|
240
|
+
|
|
241
|
+
inputStream.write(incomingMessage)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it("should call function f2 instead of f1", () => {
|
|
245
|
+
expect(f1Fired).to.equal(false)
|
|
246
|
+
expect(f2Fired).to.equal(true)
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe("when a request handler throws an error", () => {
|
|
251
|
+
const incomingMessage = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
|
|
252
|
+
const expectedError = {id: "0", error: {code: unexpectedRequestErrorCode, message: unexpectedRequestErrorMessage, data: {method: "test/test-method", params: {a: "1"}, error: "Test request error"}}}
|
|
253
|
+
let actualError
|
|
254
|
+
const expectedOutput = `Content-Length: 179\r\n\r\n{"jsonrpc":"2.0","id":"0","error":{"code":-2,"message":"${unexpectedRequestErrorMessage}","data":{"method":"test/test-method","params":{"a":"1"},"error":"Test request error"}}}`
|
|
255
|
+
let actualOutput = ""
|
|
256
|
+
|
|
257
|
+
beforeAll(() => {
|
|
258
|
+
const inputStream = new PassThrough()
|
|
259
|
+
const outputStream = new Writable({
|
|
260
|
+
write (data, _enc, next) {
|
|
261
|
+
actualOutput += data.toString()
|
|
262
|
+
next()
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
const jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
266
|
+
|
|
267
|
+
jsonRpc.onRequest("test/test-method", () => {
|
|
268
|
+
throw new Error("Test request error")
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
jsonRpc.events.on('request-error', (error) => {
|
|
272
|
+
actualError = error
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
inputStream.write(incomingMessage)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it("should fire the notification-error event with the correct error data", () => {
|
|
279
|
+
expect(actualError).to.deep.equal(expectedError)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it("should write the error to the output stream", () => {
|
|
283
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe("when we receive a request without a handler", () => {
|
|
288
|
+
const incomingMessage = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
|
|
289
|
+
const expectedError = {id: "0", error: {code: methodNotFoundErrorCode, message: methodNotFoundErrorMessage, data: {method: "test/test-method"}}}
|
|
290
|
+
let actualError
|
|
291
|
+
const expectedOutput = `Content-Length: 116\r\n\r\n{"jsonrpc":"2.0","id":"0","error":{"code":-32601,"message":"${methodNotFoundErrorMessage}","data":{"method":"test/test-method"}}}`
|
|
292
|
+
let actualOutput = ""
|
|
293
|
+
|
|
294
|
+
beforeAll(() => {
|
|
295
|
+
const inputStream = new PassThrough()
|
|
296
|
+
const outputStream = new Writable({
|
|
297
|
+
write (data, _enc, next) {
|
|
298
|
+
actualOutput += data.toString()
|
|
299
|
+
next()
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
const jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
303
|
+
|
|
304
|
+
jsonRpc.events.on('request-error', (error) => {
|
|
305
|
+
actualError = error
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
inputStream.write(incomingMessage)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it("should fire the notification-error event with the correct error data", () => {
|
|
312
|
+
expect(actualError).to.deep.equal(expectedError)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it("should write the error to the output stream", () => {
|
|
316
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
describe("sendNotification", () => {
|
|
322
|
+
const method = "test/test-method"
|
|
323
|
+
const parameters = {"a": "1"}
|
|
324
|
+
const expectedOutput = `Content-Length: 64\r\n\r\n{"jsonrpc":"2.0","method":"test/test-method","params":{"a":"1"}}`
|
|
325
|
+
let actualOutput = ""
|
|
326
|
+
|
|
327
|
+
beforeAll(() => {
|
|
328
|
+
const inputStream = new PassThrough()
|
|
329
|
+
const outputStream = new Writable({
|
|
330
|
+
write (data, _enc, next) {
|
|
331
|
+
actualOutput += data.toString()
|
|
332
|
+
next()
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
const jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
336
|
+
jsonRpc.sendNotification(method, parameters)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it("should write a notification message without an ID property on the output stream", () => {
|
|
340
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
describe("sendRequest", () => {
|
|
345
|
+
describe("with result response", () => {
|
|
346
|
+
const method = "test/test-method"
|
|
347
|
+
const parameters = {"a": "1"}
|
|
348
|
+
const expectedOutput = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
|
|
349
|
+
let actualOutput = ""
|
|
350
|
+
const clientResponse = `Content-Length: 52\r\n\r\n{"jsonrpc":"2.0","id":"0","result":{"testVal":"39"}}`
|
|
351
|
+
const expectedResult = {testVal: "39"}
|
|
352
|
+
let actualResult = null
|
|
353
|
+
let inputStream, requestPromise
|
|
354
|
+
|
|
355
|
+
beforeAll(() => {
|
|
356
|
+
TESTONLY_resetRequestId()
|
|
357
|
+
inputStream = new PassThrough()
|
|
358
|
+
const outputStream = new Writable({
|
|
359
|
+
write (data, _enc, next) {
|
|
360
|
+
actualOutput += data.toString()
|
|
361
|
+
next()
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
const jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
365
|
+
requestPromise = jsonRpc.sendRequest(method, parameters)
|
|
366
|
+
requestPromise.then((result) => {
|
|
367
|
+
actualResult = result
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it("should write the request message to the output stream", () => {
|
|
372
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it("should return an unresolved promise", () => {
|
|
376
|
+
expect(requestPromise).toBeInstanceOf(Promise)
|
|
377
|
+
expect(actualResult).toBeNull()
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it("should resolve the promise when the client responds with a result", async () => {
|
|
381
|
+
inputStream.write(clientResponse)
|
|
382
|
+
const result = await requestPromise
|
|
383
|
+
expect(result).to.deep.equal(expectedResult)
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
describe("with error response", () => {
|
|
388
|
+
const method = "test/test-method"
|
|
389
|
+
const parameters = {"a": "1"}
|
|
390
|
+
const expectedOutput = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
|
|
391
|
+
let actualOutput = ""
|
|
392
|
+
const clientResponse = `Content-Length: 83\r\n\r\n{"jsonrpc":"2.0","id":"0","error":{"code":1,"message":"test error","data":{"t":5}}}`
|
|
393
|
+
const expectedError = {"code":1,"message":"test error","data":{"t":5}}
|
|
394
|
+
let actualResult = null
|
|
395
|
+
let inputStream, requestPromise
|
|
396
|
+
|
|
397
|
+
beforeAll(() => {
|
|
398
|
+
TESTONLY_resetRequestId()
|
|
399
|
+
inputStream = new PassThrough()
|
|
400
|
+
const outputStream = new Writable({
|
|
401
|
+
write (data, _enc, next) {
|
|
402
|
+
actualOutput += data.toString()
|
|
403
|
+
next()
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
const jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
407
|
+
requestPromise = jsonRpc.sendRequest(method, parameters)
|
|
408
|
+
requestPromise.catch((result) => {
|
|
409
|
+
actualResult = result
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it("should write the request message to the output stream", () => {
|
|
414
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it("should return an unresolved promise", () => {
|
|
418
|
+
expect(requestPromise).toBeInstanceOf(Promise)
|
|
419
|
+
expect(actualResult).toBeNull()
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it("should reject the promise when the client responds with an error", async () => {
|
|
423
|
+
inputStream.write(clientResponse)
|
|
424
|
+
await requestPromise.catch((error) => {
|
|
425
|
+
expect(error).to.deep.equal(expectedError)
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
describe("when the lspInputStream encounters a parse-error", () => {
|
|
432
|
+
const error = {code: 1, message: "test error", data: {t: 5}}
|
|
433
|
+
const expectedOutput = `Content-Length: 84\r\n\r\n{"jsonrpc":"2.0","id":null,"error":{"code":1,"message":"test error","data":{"t":5}}}`
|
|
434
|
+
let actualOutput = ""
|
|
435
|
+
|
|
436
|
+
beforeAll(() => {
|
|
437
|
+
const inputStream = new PassThrough()
|
|
438
|
+
const outputStream = new Writable({
|
|
439
|
+
write (data, _enc, next) {
|
|
440
|
+
actualOutput += data.toString()
|
|
441
|
+
next()
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
const jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
445
|
+
jsonRpc.lspInputStream.emit('parse-error', error)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it("should write the error to the output stream", () => {
|
|
449
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
450
|
+
})
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
describe("_handlRequestResponse: when there is no response handler", () => {
|
|
454
|
+
let jsonRpc
|
|
455
|
+
|
|
456
|
+
beforeAll(() => {
|
|
457
|
+
const inputStream = new PassThrough()
|
|
458
|
+
const outputStream = new PassThrough()
|
|
459
|
+
jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it("should not throw an error when there is not a response handler for that id", () => {
|
|
463
|
+
jsonRpc._handleRequestResponse(33)
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
describe("_handleIncomingLspMessage: when there is no id or method", () => {
|
|
468
|
+
let jsonRpc, actualError
|
|
469
|
+
let actualOutput = ""
|
|
470
|
+
const expectedError = {code: invalidRequestErrorCode, message: invalidRequestErrorMessage}
|
|
471
|
+
const expectedOutput = `Content-Length: 105\r\n\r\n{"jsonrpc":"2.0","id":null,"error":{"code":${invalidRequestErrorCode},"message":"${invalidRequestErrorMessage}"}}`
|
|
472
|
+
|
|
473
|
+
beforeAll(() => {
|
|
474
|
+
const inputStream = new PassThrough()
|
|
475
|
+
const outputStream = new Writable({
|
|
476
|
+
write (data, _enc, next) {
|
|
477
|
+
actualOutput += data.toString()
|
|
478
|
+
next()
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
jsonRpc = new JsonRpcInterface({inputStream, outputStream})
|
|
482
|
+
jsonRpc.events.on('rpc-error', (error) => {
|
|
483
|
+
actualError = error
|
|
484
|
+
})
|
|
485
|
+
jsonRpc._handleIncomingLspMessage({id: null, message: null})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it("should emit an rpc-error event on the events emitter", () => {
|
|
489
|
+
expect(actualError).to.deep.equal(expectedError)
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it("should notify the client that it sent an invalid rpc message", () => {
|
|
493
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
494
|
+
})
|
|
495
|
+
})
|
|
496
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Transform } from 'node:stream'
|
|
2
|
+
import { headerKey, parseErrorCode, parseHeaderErrorMessage, parseJsonErrorMessage } from './constants.js'
|
|
3
|
+
|
|
4
|
+
const defaultInitialBufferSize = 10000
|
|
5
|
+
|
|
6
|
+
const jsonStartCharCode = "{".charCodeAt(0)
|
|
7
|
+
|
|
8
|
+
// as long as there are no '{' characters in the header section, this function will work. (LSP Clients should not be putting any { chars in the header anyhow)
|
|
9
|
+
const findJsonStartIndex = (data, bufferStartIndex, bufferEndIndex) => {
|
|
10
|
+
const bufferedData = data.subarray(bufferStartIndex, bufferEndIndex)
|
|
11
|
+
const index = bufferedData.indexOf(jsonStartCharCode)
|
|
12
|
+
if (index < 0) { return index }
|
|
13
|
+
return index + bufferStartIndex
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const frameStartRegex = new RegExp(`${headerKey}: ([0-9]+)`, "i")
|
|
17
|
+
|
|
18
|
+
const getJsonLength = (data, bufferStartIndex, jsonStartIndex) => {
|
|
19
|
+
const startStr = data.subarray(bufferStartIndex, jsonStartIndex).toString()
|
|
20
|
+
const match = frameStartRegex.exec(startStr)
|
|
21
|
+
if (!match) { return null }
|
|
22
|
+
return Number.parseInt(match[1])
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class LspReader extends Transform {
|
|
26
|
+
constructor (options = {}) {
|
|
27
|
+
super({readableObjectMode: true})
|
|
28
|
+
this.buffer = Buffer.alloc(options.initialBufferSize || defaultInitialBufferSize)
|
|
29
|
+
this._resetBuffer()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_transform (data, _enc, next) {
|
|
33
|
+
this._writeToBuffer(data)
|
|
34
|
+
this._handleBufferedData()
|
|
35
|
+
next()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_resetBuffer () {
|
|
39
|
+
this.bufferStartIndex = 0
|
|
40
|
+
this.bufferIndex = 0
|
|
41
|
+
this.jsonStartIndex = -1
|
|
42
|
+
this.jsonEndIndex = -1
|
|
43
|
+
this.jsonLength = null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_resetBufferWithRemainingData () {
|
|
47
|
+
const remainingData = this.buffer.subarray(this.bufferStartIndex, this.bufferIndex)
|
|
48
|
+
this._resetBuffer()
|
|
49
|
+
this._writeToBuffer(remainingData)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_advanceBufferOneMessage () {
|
|
53
|
+
this.bufferStartIndex = this.jsonEndIndex
|
|
54
|
+
this.jsonStartIndex = -1
|
|
55
|
+
this.jsonEndIndex = -1
|
|
56
|
+
this.jsonLength = null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_writeToBuffer (data) {
|
|
60
|
+
const spaceLeft = this.buffer.length - this.bufferIndex
|
|
61
|
+
if (data.length > spaceLeft) {
|
|
62
|
+
const newBufferSize = (this.bufferIndex + data.length) * 1.3 // add a padding of 30% to reduce the number of resizes
|
|
63
|
+
const newBuffer = Buffer.alloc(newBufferSize, this.buffer)
|
|
64
|
+
this.buffer = newBuffer
|
|
65
|
+
}
|
|
66
|
+
data.copy(this.buffer, this.bufferIndex)
|
|
67
|
+
this.bufferIndex += data.length
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_foundJsonLength () {
|
|
71
|
+
if (this.jsonStartIndex < 0) {
|
|
72
|
+
this.jsonStartIndex = findJsonStartIndex(this.buffer, this.bufferStartIndex, this.bufferIndex)
|
|
73
|
+
if (this.jsonStartIndex < 0) { return false }
|
|
74
|
+
|
|
75
|
+
this.jsonLength = getJsonLength(this.buffer, this.bufferStartIndex, this.jsonStartIndex)
|
|
76
|
+
|
|
77
|
+
if (this.jsonLength === null) {
|
|
78
|
+
this.emit("parse-error", {code: parseErrorCode, message: parseHeaderErrorMessage})
|
|
79
|
+
this._resetBuffer()
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.jsonEndIndex = this.jsonStartIndex + this.jsonLength
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_handleBufferedData () {
|
|
90
|
+
// loop as long as we have messages to parse in the buffer (ignore the eslint warning about the constant true value)
|
|
91
|
+
while (true) { // eslint-disable-line
|
|
92
|
+
if (!this._foundJsonLength()) { break } // we don't have a header to read yet, keep buffering
|
|
93
|
+
if (this.jsonEndIndex > this.bufferIndex) { break } // we have a header but not the complete json message, keep buffering
|
|
94
|
+
|
|
95
|
+
const messageStr = this.buffer.subarray(this.jsonStartIndex, this.jsonEndIndex).toString()
|
|
96
|
+
|
|
97
|
+
let messageObj
|
|
98
|
+
try {
|
|
99
|
+
messageObj = JSON.parse(messageStr)
|
|
100
|
+
} catch (e) {
|
|
101
|
+
this.emit("parse-error", {code: parseErrorCode, message: parseJsonErrorMessage})
|
|
102
|
+
this._resetBuffer()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.push(messageObj)
|
|
107
|
+
this._advanceBufferOneMessage()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this.bufferStartIndex === this.bufferIndex) {
|
|
111
|
+
// we have read all messages in the buffer, reset everything back to empty state
|
|
112
|
+
this._resetBuffer()
|
|
113
|
+
} else if (this.bufferStartIndex > 0) {
|
|
114
|
+
// we have a partial message at the end of this buffer, reset the buffer to contain only this data
|
|
115
|
+
this._resetBufferWithRemainingData()
|
|
116
|
+
}
|
|
117
|
+
// if the bufferStartIndex is still 0, then there is nothing to reset, just keep buffering until we have a complete message
|
|
118
|
+
}
|
|
119
|
+
}
|