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,76 @@
|
|
|
1
|
+
import { EventEmitter } from './EventEmitter.js'
|
|
2
|
+
|
|
3
|
+
const RECONNECT_DELAY_SECONDS = 1
|
|
4
|
+
|
|
5
|
+
export class WebsocketClient {
|
|
6
|
+
constructor ({port, host = "localhost", protocol = "ws"}) {
|
|
7
|
+
this.port = port
|
|
8
|
+
this.host = host
|
|
9
|
+
this.protocol = protocol
|
|
10
|
+
this.emitter = new EventEmitter()
|
|
11
|
+
this.sessionMessages = []
|
|
12
|
+
this.socket = null
|
|
13
|
+
this.socketOpen = false
|
|
14
|
+
|
|
15
|
+
this._onSocketOpen = this.onSocketOpen.bind(this)
|
|
16
|
+
this._onSocketMessage = this.onSocketMessage.bind(this)
|
|
17
|
+
this._onSocketError = this.onSocketError.bind(this)
|
|
18
|
+
this._onSocketClose = this.onSocketClose.bind(this)
|
|
19
|
+
|
|
20
|
+
this.startClient()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getUrl () {
|
|
24
|
+
return `${this.protocol}://${this.host}:${this.port}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
startClient () {
|
|
28
|
+
this.socket = new WebSocket(this.getUrl())
|
|
29
|
+
this.socket.addEventListener('open', this._onSocketOpen)
|
|
30
|
+
this.socket.addEventListener('message', this._onSocketMessage)
|
|
31
|
+
this.socket.addEventListener('error', this._onSocketError)
|
|
32
|
+
this.socket.addEventListener('close', this._onSocketClose)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onSocketOpen () {
|
|
36
|
+
console.log("websocket client connected")
|
|
37
|
+
this.socketOpen = true
|
|
38
|
+
this.emitter.emit("socket-open")
|
|
39
|
+
this.sessionMessages.forEach(msgStr => this.socket.send(msgStr))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onSocketClose () {
|
|
43
|
+
console.log(`websocket text relay connection closed. Retrying connection in ${RECONNECT_DELAY_SECONDS} second${RECONNECT_DELAY_SECONDS === 1 ? "" : "s"}...`)
|
|
44
|
+
if (this.socket == null) { return }
|
|
45
|
+
this.emitter.emit("socket-close")
|
|
46
|
+
this.socketOpen = false
|
|
47
|
+
this.socket.removeEventListener('open', this._onSocketOpen)
|
|
48
|
+
this.socket.removeEventListener('message', this._onSocketMessage)
|
|
49
|
+
this.socket.removeEventListener('error', this._onSocketError)
|
|
50
|
+
this.socket.removeEventListener('close', this._onSocketClose)
|
|
51
|
+
this.socket = null
|
|
52
|
+
setTimeout(() => this.startClient(), RECONNECT_DELAY_SECONDS * 1000)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onSocketMessage (event) {
|
|
56
|
+
let message
|
|
57
|
+
try {
|
|
58
|
+
message = JSON.parse(event.data)
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error("Error parsing websocket message JSON", e)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
this.emitter.emit('message', message)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onSocketError (error) {
|
|
67
|
+
console.error("websocket text relay connection error: ", error)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sendMessage (message) {
|
|
71
|
+
const messageStr = JSON.stringify(message)
|
|
72
|
+
this.sessionMessages.push(messageStr)
|
|
73
|
+
if (!this.socketOpen) { return }
|
|
74
|
+
this.socket.send(messageStr)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const { exportDeps } = window.__WTR__
|
|
2
|
+
|
|
3
|
+
const TWO_PI = 2 * Math.PI
|
|
4
|
+
const MAX_ANGLE_DELTA = .99999
|
|
5
|
+
|
|
6
|
+
const drawSvgElement = (tagName, attributes = {}, className, parentNode) => {
|
|
7
|
+
const element = document.createElementNS("http://www.w3.org/2000/svg", tagName)
|
|
8
|
+
|
|
9
|
+
if (className && className.length > 0) {
|
|
10
|
+
if (Array.isArray(className)) {
|
|
11
|
+
element.classList.add(...className)
|
|
12
|
+
} else {
|
|
13
|
+
element.classList.add(className)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Object.entries(attributes).forEach(([name, val]) => {
|
|
18
|
+
if (val != null) {
|
|
19
|
+
element.setAttribute(name, val)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
if (parentNode) {
|
|
24
|
+
parentNode.append(element)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return element
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const drawText = ({x, y, text, dominantBaseline, textAnchor, className, parentNode}) => {
|
|
31
|
+
const textElement = drawSvgElement("text", {x, y, "dominant-baseline": dominantBaseline, "text-anchor": textAnchor}, className, parentNode)
|
|
32
|
+
textElement.innerHTML = text
|
|
33
|
+
return textElement
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const drawLine = ({x1, y1, x2, y2, className, parentNode}) => {
|
|
37
|
+
return drawSvgElement("line", {x1, y1, x2, y2}, className, parentNode)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const drawCircle = ({cx, cy, r, className, parentNode}) => {
|
|
41
|
+
return drawSvgElement("circle", {cx, cy, r}, className, parentNode)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const coordsToPathData = (coords) => "M " + coords.map(coord => coord.join(',')).join(" L ")
|
|
45
|
+
|
|
46
|
+
const drawLinearPath = ({coords, className, parentNode}) => {
|
|
47
|
+
const d = coordsToPathData(coords)
|
|
48
|
+
return drawSvgElement("path", {d}, className, parentNode)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const drawPolarLine = ({startAngle, startRadius, endAngle, endRadius, className, parentNode}) => {
|
|
52
|
+
const startAngleRadians = (startAngle % 1) * TWO_PI
|
|
53
|
+
const endAngleRadians = (endAngle % 1) * TWO_PI
|
|
54
|
+
const x1 = Math.cos(startAngleRadians) * startRadius
|
|
55
|
+
const y1 = -Math.sin(startAngleRadians) * startRadius
|
|
56
|
+
const x2 = Math.cos(endAngleRadians) * endRadius
|
|
57
|
+
const y2 = -Math.sin(endAngleRadians) * endRadius
|
|
58
|
+
|
|
59
|
+
return drawSvgElement("line", {x1, y1, x2, y2}, className, parentNode)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const drawPolarCircle = ({angle, radius, r, className, parentNode}) => {
|
|
63
|
+
const angleRadians = (angle % 1) * TWO_PI
|
|
64
|
+
const cx = Math.cos(angleRadians) * radius
|
|
65
|
+
const cy = -Math.sin(angleRadians) * radius
|
|
66
|
+
|
|
67
|
+
return drawSvgElement("circle", {cx, cy, r}, className, parentNode)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const polarToCartesian = (angle, radius) => {
|
|
71
|
+
const angleRadians = (angle % 1) * TWO_PI
|
|
72
|
+
const x = Math.cos(angleRadians) * radius
|
|
73
|
+
const y = -Math.sin(angleRadians) * radius
|
|
74
|
+
return [x, y]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const drawWedge = ({startAngle, angleDelta, innerRadius, radiusDelta, className, parentNode}) => {
|
|
78
|
+
if (angleDelta < 0) { angleDelta = 0 }
|
|
79
|
+
if (angleDelta > MAX_ANGLE_DELTA) {angleDelta = MAX_ANGLE_DELTA }
|
|
80
|
+
|
|
81
|
+
const startAngleRadians = (startAngle % 1) * TWO_PI
|
|
82
|
+
const endAngleRadians = ((startAngle + angleDelta) % 1) * TWO_PI
|
|
83
|
+
const outerRadius = innerRadius + radiusDelta
|
|
84
|
+
const largeArcFlag = (angleDelta % 1) > .5 ? "1" : "0"
|
|
85
|
+
|
|
86
|
+
const startX1 = Math.cos(startAngleRadians) * innerRadius
|
|
87
|
+
const startY1 = -Math.sin(startAngleRadians) * innerRadius
|
|
88
|
+
const startX2 = Math.cos(startAngleRadians) * outerRadius
|
|
89
|
+
const startY2 = -Math.sin(startAngleRadians) * outerRadius
|
|
90
|
+
const endX1 = Math.cos(endAngleRadians) * innerRadius
|
|
91
|
+
const endY1 = -Math.sin(endAngleRadians) * innerRadius
|
|
92
|
+
const endX2 = Math.cos(endAngleRadians) * outerRadius
|
|
93
|
+
const endY2 = -Math.sin(endAngleRadians) * outerRadius
|
|
94
|
+
|
|
95
|
+
const d = `
|
|
96
|
+
M ${startX1} ${startY1},
|
|
97
|
+
A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${endX1} ${endY1},
|
|
98
|
+
L ${endX2} ${endY2},
|
|
99
|
+
A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${startX2} ${startY2},
|
|
100
|
+
Z
|
|
101
|
+
`
|
|
102
|
+
|
|
103
|
+
return drawSvgElement("path", {d}, className, parentNode)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const triangleHeight = 0.06
|
|
107
|
+
const verticalPadding = 0.01
|
|
108
|
+
const horizontalPadding = 0.04
|
|
109
|
+
|
|
110
|
+
const drawToolTip = ({x, y, text, direction = "below", parentNode}) => {
|
|
111
|
+
const directionMultiplier = direction === "above" ? -1 : 1
|
|
112
|
+
const tooltipDisplayGroup = drawSvgElement("g", undefined, "tooltip_display_group", parentNode)
|
|
113
|
+
const bgPlaceholder = drawSvgElement("g", undefined, undefined, tooltipDisplayGroup)
|
|
114
|
+
const textY = y + (triangleHeight + verticalPadding) * directionMultiplier
|
|
115
|
+
const textElement = drawText({x, y: textY, text, className: "tooltip_text", parentNode: tooltipDisplayGroup})
|
|
116
|
+
const textBbox = textElement.getBBox()
|
|
117
|
+
const rectAttributes = {
|
|
118
|
+
x: textBbox.x - horizontalPadding,
|
|
119
|
+
y: textBbox.y - verticalPadding,
|
|
120
|
+
width: textBbox.width + horizontalPadding * 2,
|
|
121
|
+
height: textBbox.height + verticalPadding * 2,
|
|
122
|
+
rx: 0.015
|
|
123
|
+
}
|
|
124
|
+
drawSvgElement("rect", rectAttributes, "tooltip_outline", bgPlaceholder)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
exportDeps({drawSvgElement, drawLine, drawCircle, drawLinearPath, drawPolarLine, drawPolarCircle, drawWedge,
|
|
128
|
+
drawText, polarToCartesian, coordsToPathData, drawToolTip})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import WebSocket from 'ws'
|
|
2
|
+
|
|
3
|
+
export class WebsocketClient {
|
|
4
|
+
constructor ({port, wsInterfaceEmitter, host = "localhost", protocol = "ws"}) {
|
|
5
|
+
this.port = port
|
|
6
|
+
this.host = host
|
|
7
|
+
this.protocol = protocol
|
|
8
|
+
this.wsInterfaceEmitter = wsInterfaceEmitter
|
|
9
|
+
this.socket = null
|
|
10
|
+
this.socketOpen = false
|
|
11
|
+
|
|
12
|
+
this._onSocketOpen = this.onSocketOpen.bind(this)
|
|
13
|
+
this._onSocketMessage = this.onSocketMessage.bind(this)
|
|
14
|
+
this._onSocketClose = this.onSocketClose.bind(this)
|
|
15
|
+
|
|
16
|
+
this.startClient()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getUrl () {
|
|
20
|
+
return `${this.protocol}://${this.host}:${this.port}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
startClient () {
|
|
24
|
+
this.socket = new WebSocket(this.getUrl())
|
|
25
|
+
this.socket.addEventListener('open', this._onSocketOpen)
|
|
26
|
+
this.socket.addEventListener('message', this._onSocketMessage)
|
|
27
|
+
this.socket.addEventListener('close', this._onSocketClose)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onSocketOpen () {
|
|
31
|
+
this.socketOpen = true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onSocketClose () {
|
|
35
|
+
this.socketOpen = false
|
|
36
|
+
this.socket.removeEventListener('open', this._onSocketOpen)
|
|
37
|
+
this.socket.removeEventListener('message', this._onSocketMessage)
|
|
38
|
+
this.socket.removeEventListener('close', this._onSocketClose)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onSocketMessage (event) {
|
|
42
|
+
let message
|
|
43
|
+
try {
|
|
44
|
+
message = JSON.parse(event.data)
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error("Error parsing websocket message JSON", e)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
this.wsInterfaceEmitter.emit('message', message)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
sendMessage (message) {
|
|
53
|
+
const messageStr = JSON.stringify(message)
|
|
54
|
+
this.socket.send(messageStr)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { WebsocketClient } from "./WebsocketClient.js"
|
|
2
|
+
import { WtrSession } from "./WtrSession.js"
|
|
3
|
+
import { apiMethods } from "./websocketApi.js"
|
|
4
|
+
import { createWebsocketServer } from "./websocketServer.js"
|
|
5
|
+
import {EventEmitter} from 'node:events'
|
|
6
|
+
|
|
7
|
+
const watchActiveFilesMessage = { method: "watch-editor-active-files" }
|
|
8
|
+
|
|
9
|
+
export class WebsocketInterface {
|
|
10
|
+
constructor ({port}) {
|
|
11
|
+
this.port = port
|
|
12
|
+
this.emitter = new EventEmitter() // emits "message" events from the server
|
|
13
|
+
this.initMessage = null
|
|
14
|
+
this.openFileListMessage = null
|
|
15
|
+
this.serverSession = null
|
|
16
|
+
this.wsClient = null
|
|
17
|
+
|
|
18
|
+
this._onSocketClose = this._onSocketClose.bind(this)
|
|
19
|
+
this._sendQueuedMessages = this._sendQueuedMessages.bind(this)
|
|
20
|
+
|
|
21
|
+
this._startInterface()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
sendInitMessage (initMessage) {
|
|
25
|
+
this.initMessage = initMessage
|
|
26
|
+
this._sendMessageToServer(initMessage)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
sendOpenFileList (files) {
|
|
30
|
+
this.openFileListMessage = {method: "update-open-files", files}
|
|
31
|
+
this._sendMessageToServer(this.openFileListMessage)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
sendText ({file, contents}) {
|
|
35
|
+
const sendTextMessage = {method: "relay-text", file, contents}
|
|
36
|
+
this._sendMessageToServer(sendTextMessage)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async _startInterface () {
|
|
40
|
+
try {
|
|
41
|
+
await createWebsocketServer(this.port)
|
|
42
|
+
this.serverSession = new WtrSession({apiMethods, wsInterfaceEmitter: this.emitter})
|
|
43
|
+
this._sendQueuedMessages()
|
|
44
|
+
} catch (e) {
|
|
45
|
+
this.wsClient = new WebsocketClient({port: this.port, wsInterfaceEmitter: this.emitter})
|
|
46
|
+
this.wsClient.socket.on('close', this._onSocketClose)
|
|
47
|
+
this.wsClient.socket.on('open', this._sendQueuedMessages)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_onSocketClose () {
|
|
52
|
+
this.wsClient.socket.removeEventListener("close", this._onSocketClose)
|
|
53
|
+
this.wsClient.socket.removeEventListener("open", this._sendQueuedMessages)
|
|
54
|
+
this.wsClient = null
|
|
55
|
+
this._startInterface()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_sendQueuedMessages () {
|
|
59
|
+
this._sendMessageToServer(watchActiveFilesMessage)
|
|
60
|
+
if (this.initMessage) { this._sendMessageToServer(this.initMessage) }
|
|
61
|
+
if (this.openFileListMessage) { this._sendMessageToServer(this.openFileListMessage) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_sendMessageToServer (message) {
|
|
65
|
+
if (this.serverSession) {
|
|
66
|
+
this.serverSession._handleApiMessage(message)
|
|
67
|
+
} else if (this.wsClient && this.wsClient.socketOpen){
|
|
68
|
+
this.wsClient.sendMessage(message)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import { getNextId } from "./util.js"
|
|
3
|
+
import { startSessionStatus, endSessionStatus, statusEvents, removeWatchedFileLinks } from './sessionManager.js'
|
|
4
|
+
|
|
5
|
+
export class WtrSession {
|
|
6
|
+
constructor ({apiMethods, wsConnection, wsInterfaceEmitter}) {
|
|
7
|
+
this.apiMethods = apiMethods
|
|
8
|
+
this.wsConnection = wsConnection
|
|
9
|
+
this.wsInterfaceEmitter = wsInterfaceEmitter
|
|
10
|
+
this.id = getNextId()
|
|
11
|
+
this.emitter = new EventEmitter()
|
|
12
|
+
this.watchedFiles = new Set()
|
|
13
|
+
this.openFiles = new Set()
|
|
14
|
+
this.activeOpenFiles = new Map()
|
|
15
|
+
|
|
16
|
+
this.watchActiveFiles = false
|
|
17
|
+
this.watchLogMessages = false
|
|
18
|
+
this.watchWtrActivity = false
|
|
19
|
+
this.watchWtrStatus = false
|
|
20
|
+
|
|
21
|
+
this._subscribeToEvents()
|
|
22
|
+
startSessionStatus(this)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
sendMessageToClient (message) {
|
|
26
|
+
if (this.wsConnection) {
|
|
27
|
+
this.wsConnection.send(JSON.stringify(message))
|
|
28
|
+
} else {
|
|
29
|
+
this.wsInterfaceEmitter.emit('message', message)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
isServer () {
|
|
34
|
+
return this.wsInterfaceEmitter != null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_subscribeToEvents = () => {
|
|
38
|
+
this.emitter.on('log', this._onLog.bind(this))
|
|
39
|
+
this.emitter.on('editor-active-files-update', this._onEditorActiveFilesUpdate.bind(this))
|
|
40
|
+
|
|
41
|
+
this._onActivityUpdate = this._onActivityUpdate.bind(this)
|
|
42
|
+
statusEvents.on('activity-update', this._onActivityUpdate)
|
|
43
|
+
this._onStatusUpdate = this._onStatusUpdate.bind(this)
|
|
44
|
+
statusEvents.on('status-update', this._onStatusUpdate)
|
|
45
|
+
|
|
46
|
+
if (this.wsConnection) {
|
|
47
|
+
this._onWsMessage = this._onWsMessage.bind(this)
|
|
48
|
+
this._onWsClose = this._onWsClose.bind(this)
|
|
49
|
+
this.wsConnection.on('message', this._onWsMessage)
|
|
50
|
+
this.wsConnection.on('close', this._onWsClose)
|
|
51
|
+
this.wsConnection.on('error', this._onWsClose)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_onWsClose () {
|
|
56
|
+
if (this.wsConnection) {
|
|
57
|
+
this.wsConnection.removeListener('message', this._onWsMessage)
|
|
58
|
+
this.wsConnection.removeListener('close', this._onWsClose)
|
|
59
|
+
this.wsConnection.removeListener('error', this._onWsClose)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
statusEvents.removeListener('activity-update', this._onEditorActiveFilesUpdate)
|
|
63
|
+
statusEvents.removeListener('status-update', this._onStatusUpdate)
|
|
64
|
+
|
|
65
|
+
endSessionStatus(this)
|
|
66
|
+
for (const endsWith of this.watchedFiles) {
|
|
67
|
+
removeWatchedFileLinks(this, endsWith)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.emitter.removeAllListeners()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_onLog (data) {
|
|
74
|
+
if (!this.watchLogMessages) { return }
|
|
75
|
+
this.sendMessageToClient({ method: "watch-log-messages", data })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_onEditorActiveFilesUpdate (files) {
|
|
79
|
+
if (!this.watchActiveFiles) { return }
|
|
80
|
+
this.sendMessageToClient({ method: "watch-editor-active-files", files })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_onActivityUpdate (data) {
|
|
84
|
+
if (!this.watchWtrActivity) { return }
|
|
85
|
+
this.sendMessageToClient({ method: "watch-wtr-activity", data })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_onStatusUpdate (data) {
|
|
89
|
+
if (!this.watchWtrStatus) { return }
|
|
90
|
+
this.sendMessageToClient({ method: "watch-wtr-status", data })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_onWsMessage (dataBuf) {
|
|
94
|
+
const str = dataBuf.toString()
|
|
95
|
+
let message
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
message = JSON.parse(str)
|
|
99
|
+
} catch (e) {
|
|
100
|
+
this.emitter.emit('log', { level: "error", text: `Could not parse JSON message: ${str}` })
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this._handleApiMessage(message)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_handleApiMessage (message) {
|
|
108
|
+
const method = message && message.method
|
|
109
|
+
const methodHandler = this.apiMethods.get(method)
|
|
110
|
+
if (!methodHandler) {
|
|
111
|
+
this.emitter.emit('log', { level: "error", text: `unknown ws api method: ${method}` })
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
methodHandler(this, message)
|
|
117
|
+
} catch (e) {
|
|
118
|
+
this.emitter.emit('log', { level: "error", text: `Error while handling method: ${method} : ${e.stacktrace}` })
|
|
119
|
+
console.error("Error while handling ws api method", method, e)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createServer } from 'node:http'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import * as url from 'node:url'
|
|
5
|
+
const parentDir = url.fileURLToPath(new URL('..', import.meta.url))
|
|
6
|
+
|
|
7
|
+
const uiDirName = "ui"
|
|
8
|
+
const uiDirPath = path.join(parentDir, uiDirName)
|
|
9
|
+
|
|
10
|
+
const allowedFileTypes = new Map([
|
|
11
|
+
["html", "text/html"],
|
|
12
|
+
["css", "text/css"],
|
|
13
|
+
["js", "application/javascript"],
|
|
14
|
+
["json", "application/json"],
|
|
15
|
+
["png", "image/png"],
|
|
16
|
+
["ttf", "font/ttf"],
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
const getFilePath = (url) => {
|
|
20
|
+
url = url === "/" ? "./index.html" : "." + url
|
|
21
|
+
return path.join(uiDirPath, url)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getFileType = (fileUrl) => {
|
|
25
|
+
const lastDotIndex = fileUrl.lastIndexOf(".")
|
|
26
|
+
if (lastDotIndex < 0 || lastDotIndex >= fileUrl.length) { return "" }
|
|
27
|
+
return fileUrl.substring(lastDotIndex + 1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const requestHandler = (req, res) => {
|
|
31
|
+
const filePath = getFilePath(req.url)
|
|
32
|
+
const fileType = getFileType(filePath)
|
|
33
|
+
const contentType = allowedFileTypes.get(fileType)
|
|
34
|
+
if (!contentType) {
|
|
35
|
+
res.writeHead(404)
|
|
36
|
+
res.end("NOT FOUND!")
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
res.setHeader("Content-Type", contentType)
|
|
40
|
+
res.writeHead(200)
|
|
41
|
+
const fileStream = fs.createReadStream(filePath)
|
|
42
|
+
fileStream.pipe(res)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const createHttpServer = (port) => {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
|
|
48
|
+
const server = createServer(requestHandler)
|
|
49
|
+
|
|
50
|
+
server.on("error", (e) => {
|
|
51
|
+
reject(e)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
server.listen(port, () => {
|
|
55
|
+
resolve(server)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import { debounce } from './util.js'
|
|
3
|
+
|
|
4
|
+
export const statusEvents = new EventEmitter()
|
|
5
|
+
|
|
6
|
+
export const wtrSessions = new Set()
|
|
7
|
+
|
|
8
|
+
const createUpdateObject = () => {
|
|
9
|
+
const sessions = [...wtrSessions].map((wtrSession) => {
|
|
10
|
+
|
|
11
|
+
const openFileLinks = {}
|
|
12
|
+
for (const [editorFile, fileLinks] of wtrSession.activeOpenFiles) {
|
|
13
|
+
if (!openFileLinks[editorFile]) {
|
|
14
|
+
openFileLinks[editorFile] = []
|
|
15
|
+
}
|
|
16
|
+
for (const {clientSession, endsWith} of fileLinks.values()) {
|
|
17
|
+
openFileLinks[editorFile].push({clientId: clientSession.id, endsWith})
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
name: wtrSession.name || "Uninitialized",
|
|
23
|
+
id: wtrSession.id,
|
|
24
|
+
editorPid: wtrSession.editorPid,
|
|
25
|
+
lsPid: wtrSession.lsPid,
|
|
26
|
+
isServer: wtrSession.isServer(),
|
|
27
|
+
watchCount: wtrSession.watchedFiles.size,
|
|
28
|
+
openCount: wtrSession.openFiles.size,
|
|
29
|
+
openFileLinks
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return {sessions}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sendUpdate = () => {
|
|
37
|
+
const updateObject = createUpdateObject()
|
|
38
|
+
statusEvents.emit('status-update', updateObject)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const triggerStatusUpdate = debounce(100, sendUpdate)
|
|
42
|
+
|
|
43
|
+
export const startSessionStatus = (wtrSession) => {
|
|
44
|
+
wtrSessions.add(wtrSession)
|
|
45
|
+
triggerStatusUpdate()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const endSessionStatus = (wtrSession) => {
|
|
49
|
+
wtrSessions.delete(wtrSession)
|
|
50
|
+
triggerStatusUpdate()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const createLinkKey = (clientSession, endsWith) => `${clientSession.id}:${endsWith}`
|
|
54
|
+
|
|
55
|
+
const createWatchLink = (wtrSession, editorFile, clientSession, endsWith) => {
|
|
56
|
+
let fileLinks = wtrSession.activeOpenFiles.get(editorFile)
|
|
57
|
+
if (!fileLinks) {
|
|
58
|
+
fileLinks = new Map()
|
|
59
|
+
wtrSession.activeOpenFiles.set(editorFile, fileLinks)
|
|
60
|
+
}
|
|
61
|
+
const linkKey = createLinkKey(clientSession, endsWith)
|
|
62
|
+
fileLinks.set(linkKey, { clientSession, endsWith })
|
|
63
|
+
triggerStatusUpdate()
|
|
64
|
+
wtrSession.emitter.emit('editor-active-files-update', [...wtrSession.activeOpenFiles.keys()])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const addOpenedFileLinks = (wtrSession, editorFile) => {
|
|
68
|
+
for (const clientSession of wtrSessions) {
|
|
69
|
+
if (clientSession === wtrSession) { continue }
|
|
70
|
+
for (const endsWith of clientSession.watchedFiles) {
|
|
71
|
+
if (editorFile.endsWith(endsWith)) {
|
|
72
|
+
createWatchLink(wtrSession, editorFile, clientSession, endsWith)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const removeOpenedFileLinks = (wtrSession, editorFile) => {
|
|
79
|
+
wtrSession.activeOpenFiles.delete(editorFile)
|
|
80
|
+
wtrSession.emitter.emit('editor-active-files-update', [...wtrSession.activeOpenFiles.keys()])
|
|
81
|
+
triggerStatusUpdate()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const addWatchedFileLinks = (clientSession, endsWith) => {
|
|
85
|
+
for (const wtrSession of wtrSessions) {
|
|
86
|
+
if (wtrSession === clientSession) { continue }
|
|
87
|
+
for (const editorFile of wtrSession.openFiles) {
|
|
88
|
+
if (editorFile.endsWith(endsWith)) {
|
|
89
|
+
createWatchLink(wtrSession, editorFile, clientSession, endsWith)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const removeWatchedFileLinks = (clientSession, endsWith) => {
|
|
96
|
+
const linkKey = createLinkKey(clientSession, endsWith)
|
|
97
|
+
for (const wtrSession of wtrSessions) {
|
|
98
|
+
for (const [editorFile, fileLinks] of wtrSession.activeOpenFiles) {
|
|
99
|
+
fileLinks.delete(linkKey)
|
|
100
|
+
if (fileLinks.size === 0) {
|
|
101
|
+
wtrSession.activeOpenFiles.delete(editorFile)
|
|
102
|
+
wtrSession.emitter.emit('editor-active-files-update', [...wtrSession.activeOpenFiles.keys()])
|
|
103
|
+
triggerStatusUpdate()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// this version of the function does not handle function arguments because it was not needed for this app
|
|
2
|
+
export const debounce = (timeout, func) => {
|
|
3
|
+
let timeoutHandle = null
|
|
4
|
+
return () => {
|
|
5
|
+
clearTimeout(timeoutHandle)
|
|
6
|
+
timeoutHandle = setTimeout(func, timeout)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// first ID returned is 0 and increases every time function is called
|
|
11
|
+
let currentId = 0
|
|
12
|
+
export const getNextId = () => currentId++
|