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.
Files changed (50) hide show
  1. package/.eslintrc +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +40 -0
  4. package/docs/code-structure.md +77 -0
  5. package/docs/creating-text-editor-plugin.md +10 -0
  6. package/docs/dev-getting-started.md +8 -0
  7. package/index.js +87 -0
  8. package/package.json +45 -0
  9. package/src/language-server/JsonRpcInterface.js +126 -0
  10. package/src/language-server/JsonRpcInterface.test.js +496 -0
  11. package/src/language-server/LspReader.js +119 -0
  12. package/src/language-server/LspReader.test.js +508 -0
  13. package/src/language-server/LspWriter.js +44 -0
  14. package/src/language-server/LspWriter.test.js +179 -0
  15. package/src/language-server/constants.js +23 -0
  16. package/src/ui/css/fonts/Montserrat-Black.ttf +0 -0
  17. package/src/ui/css/fonts/Montserrat-Bold.ttf +0 -0
  18. package/src/ui/css/fonts/Montserrat-ExtraBold.ttf +0 -0
  19. package/src/ui/css/fonts/Montserrat-ExtraLight.ttf +0 -0
  20. package/src/ui/css/fonts/Montserrat-Light.ttf +0 -0
  21. package/src/ui/css/fonts/Montserrat-Medium.ttf +0 -0
  22. package/src/ui/css/fonts/Montserrat-Regular.ttf +0 -0
  23. package/src/ui/css/fonts/Montserrat-SemiBold.ttf +0 -0
  24. package/src/ui/css/fonts/Montserrat-Thin.ttf +0 -0
  25. package/src/ui/css/fonts.css +54 -0
  26. package/src/ui/css/main.css +198 -0
  27. package/src/ui/index.html +36 -0
  28. package/src/ui/js/components/ActivityTimeseriesGraph.js +149 -0
  29. package/src/ui/js/components/HeaderSummary.js +23 -0
  30. package/src/ui/js/components/ServerStatus.js +31 -0
  31. package/src/ui/js/components/SessionLabels.js +171 -0
  32. package/src/ui/js/components/SessionWedges.js +107 -0
  33. package/src/ui/js/components/StatusRing.js +42 -0
  34. package/src/ui/js/components/grids.js +29 -0
  35. package/src/ui/js/index.js +117 -0
  36. package/src/ui/js/main.js +97 -0
  37. package/src/ui/js/util/DependencyManager.js +31 -0
  38. package/src/ui/js/util/EventEmitter.js +40 -0
  39. package/src/ui/js/util/WebsocketClient.js +76 -0
  40. package/src/ui/js/util/constants.js +9 -0
  41. package/src/ui/js/util/drawing.js +128 -0
  42. package/src/websocket-interface/WebsocketClient.js +56 -0
  43. package/src/websocket-interface/WebsocketInterface.js +71 -0
  44. package/src/websocket-interface/WtrSession.js +122 -0
  45. package/src/websocket-interface/httpServer.js +58 -0
  46. package/src/websocket-interface/sessionManager.js +107 -0
  47. package/src/websocket-interface/util.js +12 -0
  48. package/src/websocket-interface/websocketApi.js +116 -0
  49. package/src/websocket-interface/websocketServer.js +18 -0
  50. 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,9 @@
1
+ const { exportDeps } = window.__WTR__
2
+
3
+ const constants = {
4
+ innerRingRadius: .33,
5
+ outerRingRadius: 0.6,
6
+ outerArcSize: 0.175,
7
+ }
8
+
9
+ exportDeps({constants})
@@ -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++