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,31 @@
1
+ const { exportDeps, drawText } = window.__WTR__
2
+
3
+ const valueTextClass = "server_status_value"
4
+ const offlineTextClass = "server_status_offline"
5
+
6
+ class ServerStatus {
7
+ constructor ({parentNode}) {
8
+ this.parentNode = parentNode
9
+ this.draw()
10
+ }
11
+
12
+ draw () {
13
+ this.parentNode.innerHTML = ""
14
+ drawText({x: 0, y: .85, text: "WS Server PID", className: "server_status_label", parentNode: this.parentNode})
15
+ this.valueElement = drawText({x: 0, y: .748, text: "138324", parentNode: this.parentNode})
16
+ this.offlineElement = drawText({x: 0, y: .748, text: "OFFLINE", className:offlineTextClass, parentNode: this.parentNode})
17
+ }
18
+
19
+ update (pid) {
20
+ if (pid == null) {
21
+ this.valueElement.classList.remove(valueTextClass)
22
+ this.offlineElement.classList.add(offlineTextClass)
23
+ } else {
24
+ this.valueElement.innerHTML = pid
25
+ this.valueElement.classList.add(valueTextClass)
26
+ this.offlineElement.classList.remove(offlineTextClass)
27
+ }
28
+ }
29
+ }
30
+
31
+ exportDeps({ServerStatus})
@@ -0,0 +1,171 @@
1
+ const { exportDeps, polarToCartesian, coordsToPathData, drawLinearPath, drawCircle, drawText, drawSvgElement, drawToolTip } = window.__WTR__
2
+
3
+ // a value with a colored circle and tooltip
4
+ const drawSummaryValue = ({x, y, label, circleClass, parentNode}) => {
5
+ const tooltipWrapperGroup = drawSvgElement("g", undefined, "tooltip_wrapper_group", parentNode)
6
+ drawCircle({cx: x, cy: y - 0.0032, r: summaryCircleRadius, className: circleClass, parentNode: tooltipWrapperGroup})
7
+ drawToolTip({x: x, y: y - 0.0032, text: label, parentNode: tooltipWrapperGroup})
8
+ return drawText({x: x + summaryCircleRadius * 2, y: y, dominantBaseline: "middle", text: "0", className: "summary_text_value", parentNode: tooltipWrapperGroup})
9
+ }
10
+
11
+ // similar to summary value, but with an update function that automatically handles moving the circle on the left as the text size changes
12
+ const drawRightAlignedSummaryValue = ({x, y, label, circleClass, parentNode}) => {
13
+ const tooltipWrapperGroup = drawSvgElement("g", undefined, "tooltip_wrapper_group", parentNode)
14
+ drawToolTip({x: x, y: y - 0.0032, text: label, parentNode: tooltipWrapperGroup})
15
+ const textElement = drawText({x: x + summaryCircleRadius * 2, y: y, dominantBaseline: "middle", text: "0", textAnchor: "end", className: "summary_text_value", parentNode: tooltipWrapperGroup})
16
+ const xDiff = textElement.getBBox().width
17
+ const labelCircle = drawCircle({cx: x - xDiff, cy: y - 0.0032, r: summaryCircleRadius, className: circleClass, parentNode: tooltipWrapperGroup})
18
+ const update = (value) => {
19
+ textElement.innerHTML = value
20
+ const xDiff = textElement.getBBox().width
21
+ labelCircle.setAttribute("cx", x - xDiff)
22
+ }
23
+ return {textElement, update}
24
+ }
25
+
26
+ const underlinePadding = 0.01
27
+ const summaryCircleRadius = 0.0140
28
+ const summaryLeftPadding = 0.03
29
+ const summaryValueSpacing = 0.065
30
+ const editorSummaryPadding = 0.025
31
+
32
+ const addPadding = ({x, y, height, width}, horizontalPadding, verticalPadding) => {
33
+ if (verticalPadding == null) { verticalPadding = horizontalPadding }
34
+ x -= horizontalPadding
35
+ width += horizontalPadding * 2
36
+ y -= verticalPadding
37
+ height += verticalPadding * 2
38
+ return {x, y, height, width}
39
+ }
40
+
41
+ const labelLineDistance = 0.07
42
+
43
+ class ClientLabel {
44
+ constructor ({wedgeCenterAngle, wedgeCenterRadius, parentNode}) {
45
+ this.parentNode = parentNode
46
+ this.wedgeCenterAngle = wedgeCenterAngle
47
+ this.wedgeCenterRadius = wedgeCenterRadius
48
+ this.wedgeCenter = polarToCartesian(this.wedgeCenterAngle, this.wedgeCenterRadius)
49
+ this.draw()
50
+ }
51
+
52
+ draw () {
53
+ const textStart = polarToCartesian(this.wedgeCenterAngle, this.wedgeCenterRadius + labelLineDistance)
54
+
55
+ this.topNameElement = drawText({x: textStart[0], y: textStart[1] - 0.06 - underlinePadding / 2, text: "", className: ["wedge_identifier", "small_text"], parentNode: this.parentNode})
56
+ this.nameTextElement = drawText({x: textStart[0], y: textStart[1] - 0.017 - underlinePadding / 2, text: ".", className: "wedge_identifier", parentNode: this.parentNode})
57
+
58
+ drawCircle({cx: this.wedgeCenter[0], cy: this.wedgeCenter[1], r: 0.01, className: "test_circle", parentNode: this.parentNode})
59
+
60
+ const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
61
+ const underlineCoords = [[this.wedgeCenter[0], this.wedgeCenter[1]], [textBbox.x, textBbox.y + textBbox.height], [textBbox.x + textBbox.width, textBbox.y + textBbox.height]]
62
+ this.underlinePath = drawLinearPath({coords: underlineCoords, className: "test_line", parentNode: this.parentNode})
63
+
64
+ const summaryMidY = textBbox.y + textBbox.height + underlinePadding + summaryCircleRadius / 2 + 0.02
65
+ const summaryStartX = textBbox.x + summaryLeftPadding
66
+ this.watchedCountElement = drawSummaryValue({x: summaryStartX, y: summaryMidY, label: "Watched Files", circleClass: "summary_watched_circle", parentNode: this.parentNode})
67
+ const watchedCountBbox = this.watchedCountElement.getBBox()
68
+
69
+ const xDiff = summaryValueSpacing + watchedCountBbox.width
70
+ this.activeCountTranslateWrapper = drawSvgElement("g", {transform: `translate(${xDiff}, 0)`}, undefined, this.parentNode)
71
+ this.activeCountElement = drawSummaryValue({x: summaryStartX, y: summaryMidY, label: "Active Files", circleClass: "summary_active_circle", parentNode: this.activeCountTranslateWrapper})
72
+ }
73
+
74
+ update (client) {
75
+ let {name} = client
76
+ if (!name || name.length === 0) {
77
+ name = "."
78
+ }
79
+
80
+ if (name.length > 14) {
81
+ const halfIndex = Math.floor(name.length / 2)
82
+ const nameFirstHalf = name.substring(0, halfIndex)
83
+ const nameSecondHalf = name.substring(halfIndex, name.length)
84
+ this.topNameElement.innerHTML = nameFirstHalf
85
+ this.nameTextElement.innerHTML = nameSecondHalf
86
+ this.nameTextElement.classList.add("small_text")
87
+ } else {
88
+ this.topNameElement.innerHTML = ""
89
+ this.nameTextElement.classList.remove("small_text")
90
+ this.nameTextElement.innerHTML = name
91
+ }
92
+
93
+ const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
94
+ const underlineCoords = [[this.wedgeCenter[0], this.wedgeCenter[1]], [textBbox.x, textBbox.y + textBbox.height], [textBbox.x + textBbox.width, textBbox.y + textBbox.height]]
95
+ const newUnderlinePathData = coordsToPathData(underlineCoords)
96
+ this.underlinePath.setAttribute("d", newUnderlinePathData)
97
+
98
+ this.watchedCountElement.innerHTML = client.watchCount
99
+ this.activeCountElement.innerHTML = client.activeWatchCount
100
+
101
+ const xDiff = summaryValueSpacing + this.watchedCountElement.getBBox().width
102
+ this.activeCountTranslateWrapper.setAttribute("transform", `translate(${xDiff}, 0)`)
103
+ }
104
+ }
105
+
106
+ class EditorLabel {
107
+ constructor ({wedgeCenterAngle, wedgeCenterRadius, parentNode}) {
108
+ this.parentNode = parentNode
109
+ this.wedgeCenterAngle = wedgeCenterAngle
110
+ this.wedgeCenterRadius = wedgeCenterRadius
111
+ this.wedgeCenter = polarToCartesian(this.wedgeCenterAngle, this.wedgeCenterRadius)
112
+ this.draw()
113
+ }
114
+
115
+ draw () {
116
+ const textStart = polarToCartesian(this.wedgeCenterAngle, this.wedgeCenterRadius + labelLineDistance)
117
+
118
+ this.topNameElement = drawText({x: textStart[0], y: textStart[1] - 0.06 - underlinePadding / 2, text: "", textAnchor: "end", className: ["wedge_identifier", "small_text"], parentNode: this.parentNode})
119
+ this.nameTextElement = drawText({x: textStart[0], y: textStart[1] - 0.017 - underlinePadding / 2, text: ".", textAnchor: "end", className: "wedge_identifier", parentNode: this.parentNode})
120
+
121
+ drawCircle({cx: this.wedgeCenter[0], cy: this.wedgeCenter[1], r: 0.01, className: "test_circle", parentNode: this.parentNode})
122
+
123
+ const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
124
+ const underlineCoords = [[this.wedgeCenter[0], this.wedgeCenter[1]], [textBbox.x, textBbox.y + textBbox.height], [textBbox.x + textBbox.width, textBbox.y + textBbox.height]]
125
+ this.underlinePath = drawLinearPath({coords: underlineCoords, className: "test_line", parentNode: this.parentNode})
126
+
127
+ const summaryStartX = textBbox.x + textBbox.width - summaryLeftPadding
128
+ const summaryMidY = textBbox.y + textBbox.height + underlinePadding * 3 + summaryCircleRadius / 2
129
+
130
+ this.activeCountSummaryValue = drawRightAlignedSummaryValue({x: summaryStartX, y: summaryMidY, label: "Active Files", circleClass: "summary_active_circle", parentNode: this.parentNode})
131
+ const xDiff = this.activeCountSummaryValue.textElement.getBBox().width + summaryCircleRadius + editorSummaryPadding * 2
132
+ this.openFilesTransformGroup = drawSvgElement('g', {transform: `translate(${-xDiff})`}, undefined, this.parentNode)
133
+ this.openCountSummaryValue = drawRightAlignedSummaryValue({x: summaryStartX, y: summaryMidY, label: "Open Files", circleClass: "summary_watched_circle", parentNode: this.openFilesTransformGroup})
134
+ }
135
+
136
+ update (editor) {
137
+ let {name} = editor
138
+ if (!name || name.length === 0) {
139
+ name = "."
140
+ }
141
+
142
+ if (editor.isServer) {
143
+ name = "* " + name
144
+ }
145
+
146
+ if (name.length > 14) {
147
+ const halfIndex = Math.floor(name.length / 2)
148
+ const nameFirstHalf = name.substring(0, halfIndex)
149
+ const nameSecondHalf = name.substring(halfIndex, name.length)
150
+ this.topNameElement.innerHTML = nameFirstHalf
151
+ this.nameTextElement.innerHTML = nameSecondHalf
152
+ this.nameTextElement.classList.add("small_text")
153
+ } else {
154
+ this.topNameElement.innerHTML = ""
155
+ this.nameTextElement.classList.remove("small_text")
156
+ this.nameTextElement.innerHTML = name
157
+ }
158
+
159
+ const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
160
+ const underlineCoords = [[this.wedgeCenter[0], this.wedgeCenter[1]], [textBbox.x + textBbox.width, textBbox.y + textBbox.height], [textBbox.x, textBbox.y + textBbox.height]]
161
+ const newUnderlinePathData = coordsToPathData(underlineCoords)
162
+ this.underlinePath.setAttribute("d", newUnderlinePathData)
163
+
164
+ this.activeCountSummaryValue.update(editor.activeOpenCount)
165
+ this.openCountSummaryValue.update(editor.openCount)
166
+ const xDiff = this.activeCountSummaryValue.textElement.getBBox().width + summaryCircleRadius + editorSummaryPadding * 2
167
+ this.openFilesTransformGroup.setAttribute("transform", `translate(${-xDiff})`)
168
+ }
169
+ }
170
+
171
+ exportDeps({ClientLabel, EditorLabel})
@@ -0,0 +1,107 @@
1
+ const { exportDeps, drawSvgElement, drawWedge } = window.__WTR__
2
+
3
+ const numWedges = 5
4
+ const wedgeSpacing = 0.01
5
+ const wedgeWidth = 0.08
6
+
7
+ const flashAnimationKeyframes = [{fill: "#fff"}, {fill: "var(--wedge-active-color)"}]
8
+ const flashAnimationProps = {duration: 250, easing: "ease-out", iterations: 1}
9
+
10
+ const createSessionSummary = (sessions) => {
11
+ const summary = {
12
+ name: `${sessions.length - numWedges + 1} others...`,
13
+ watchCount: 0,
14
+ activeWatchCount: 0,
15
+ openCount: 0,
16
+ activeOpenCount: 0
17
+
18
+ }
19
+
20
+ for (let i = numWedges - 1; i < sessions.length; i++) {
21
+ const session = sessions[i]
22
+ summary.watchCount += session.watchCount
23
+ summary.activeWatchCount += session.activeWatchCount
24
+ summary.openCount += session.openCount
25
+ summary.activeOpenCount += session.activeOpenCount
26
+ }
27
+
28
+ return summary
29
+ }
30
+
31
+ class SessionWedges {
32
+ constructor ({outerRingRadius, outerArcSize, direction = 1, Label, parentNode}) {
33
+ this.outerRingRadius = outerRingRadius
34
+ this.outerArcSize = outerArcSize
35
+ this.parentNode = parentNode
36
+ this.direction = direction
37
+ this.Label = Label
38
+ this.draw()
39
+ }
40
+
41
+ draw () {
42
+ this.parentNode.innerHTML = ""
43
+ this.wedgeNodes = []
44
+ const totalStartAngle = 0.25 + this.direction * this.outerArcSize / 2
45
+ const totalAngleDelta = 0.5 - this.outerArcSize - wedgeSpacing
46
+ const wedgeAngleDelta = (totalAngleDelta / numWedges) - wedgeSpacing
47
+ const innerRadius = this.outerRingRadius - wedgeWidth / 2
48
+ for (let i = 0; i < numWedges; i++) {
49
+ const group = drawSvgElement("g", undefined, "single_wedge_group", this.parentNode)
50
+
51
+ let startAngle = totalStartAngle + this.direction * (i + 1) * wedgeSpacing + this.direction * i * wedgeAngleDelta
52
+ if (this.direction === -1) {
53
+ startAngle -= wedgeAngleDelta
54
+ }
55
+
56
+ const wedge = drawWedge({startAngle, angleDelta: wedgeAngleDelta, innerRadius, radiusDelta: wedgeWidth, className: "wedge_node", parentNode: group})
57
+
58
+ const wedgeCenterAngle = startAngle + wedgeAngleDelta / 2
59
+ const label = new this.Label({wedgeCenterAngle, wedgeCenterRadius: this.outerRingRadius, parentNode: group})
60
+
61
+ this.wedgeNodes.push({group, label, wedge})
62
+ }
63
+ this.drawCalled = true
64
+ }
65
+
66
+ update (sessions) {
67
+ this.sessionsData = sessions
68
+ for (let i = 0; i < numWedges; i++) {
69
+ const {group, label} = this.wedgeNodes[i]
70
+ group.classList.remove('active', 'online')
71
+ let session = sessions[i]
72
+ if (!session) {
73
+ continue
74
+ }
75
+
76
+ if (i === numWedges - 1 && sessions.length > numWedges) {
77
+ session = createSessionSummary(sessions)
78
+ }
79
+
80
+ if (session.activeWatchCount > 0 || session.activeOpenCount > 0) {
81
+ group.classList.add('active')
82
+ } else {
83
+ group.classList.add('online')
84
+ }
85
+
86
+ label.update(session)
87
+ }
88
+ }
89
+
90
+ triggerActivity (sessionIds) {
91
+ const sessions = this.sessionsData
92
+ if (!sessions) { return }
93
+ for (let i = 0; i < numWedges; i++) {
94
+ const {wedge} = this.wedgeNodes[i]
95
+ const session = sessions[i]
96
+ if (!session) {
97
+ continue
98
+ }
99
+
100
+ if ((typeof sessionIds === 'number' && session.id === sessionIds) || sessionIds.has && sessionIds.has(session.id)) {
101
+ wedge.animate(flashAnimationKeyframes, flashAnimationProps)
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ exportDeps({SessionWedges})
@@ -0,0 +1,42 @@
1
+ const { exportDeps, drawCircle, drawWedge } = window.__WTR__
2
+
3
+ class StatusRing {
4
+ constructor ({innerRingRadius, outerRingRadius, outerArcSize, parentNode}) {
5
+ this.innerRingRadius = innerRingRadius
6
+ this.outerRingRadius = outerRingRadius
7
+ this.outerArcSize = outerArcSize
8
+ this.parentNode = parentNode
9
+ this.draw()
10
+ }
11
+
12
+ draw () {
13
+ this.parentNode.innerHTML = ""
14
+ this.parentNode.classList.remove(...this.parentNode.classList)
15
+ this.currentClassName = 'offline'
16
+ this.parentNode.classList.add(this.currentClassName)
17
+
18
+ drawCircle({ cx: 0, cy: 0, r: this.innerRingRadius, parentNode: this.parentNode })
19
+ drawWedge({startAngle: 0.25 - this.outerArcSize / 2, angleDelta: this.outerArcSize, innerRadius: this.outerRingRadius, radiusDelta: 0, parentNode: this.parentNode})
20
+ drawWedge({startAngle: 0.75 - this.outerArcSize / 2, angleDelta: this.outerArcSize, innerRadius: this.outerRingRadius, radiusDelta: 0, parentNode: this.parentNode})
21
+ }
22
+
23
+ update (data) {
24
+ const hasActiveClient = data.clients.some(client => client.activeWatchCount > 0)
25
+ let newClassName
26
+ if (hasActiveClient) {
27
+ newClassName = "active"
28
+ } else if (data.clients.length > 0) {
29
+ newClassName = "online"
30
+ } else {
31
+ newClassName = "offline"
32
+ }
33
+
34
+ if (newClassName != this.currentClassName) {
35
+ this.parentNode.classList.remove(this.currentClassName)
36
+ this.parentNode.classList.add(newClassName)
37
+ this.currentClassName = newClassName
38
+ }
39
+ }
40
+ }
41
+
42
+ exportDeps({StatusRing})
@@ -0,0 +1,29 @@
1
+ const { exportDeps, drawLine, drawCircle, drawPolarLine } = window.__WTR__
2
+
3
+ const gridCellWidth = 0.08
4
+ const maxGrid = 1
5
+
6
+ const drawGrid = (parentNode) => {
7
+ for (let i = 0; i < maxGrid; i += gridCellWidth) {
8
+ const className = i === 0 ? "grid_axis" : "grid_line"
9
+ drawLine({x1: -1, y1: i, x2: 1, y2: i, className, parentNode})
10
+ drawLine({x1: -1, y1: -i, x2: 1, y2: -i, className, parentNode})
11
+ drawLine({x1: i, y1: -1, x2: i, y2: 1, className, parentNode})
12
+ drawLine({x1: -i, y1: -1, x2: -i, y2: 1, className, parentNode})
13
+ }
14
+ }
15
+
16
+ const radiusDiff = 0.04
17
+ const angleDiff = 0.025
18
+
19
+ const drawPolarGrid = (parentNode) => {
20
+ for (let r = 0; r <= 1.0001; r += radiusDiff) {
21
+ drawCircle({cx: 0, cy: 0, r, className: "grid_line", parentNode})
22
+ }
23
+
24
+ for (let angle = 0; angle < 1; angle += angleDiff) {
25
+ drawPolarLine({startAngle: 0, startRadius: 0, endAngle: angle, endRadius: 1, className: "grid_line", parentNode})
26
+ }
27
+ }
28
+
29
+ exportDeps({drawGrid, drawPolarGrid})
@@ -0,0 +1,117 @@
1
+ import { WebsocketClient } from "./util/WebsocketClient.js"
2
+ import "./util/DependencyManager.js"
3
+
4
+ const { cleanupEventHandlers, statusDataEmitter } = window.__WTR__
5
+
6
+ const PORT = 38378
7
+
8
+ const FILE_PREFIX = "websocket-text-relay/src/ui/"
9
+ const CSS_FILE = "css/main.css"
10
+ const cssEndsWith = FILE_PREFIX + CSS_FILE
11
+
12
+ const MAIN_JS_FILE = "js/main.js"
13
+ const mainJsEndsWith = FILE_PREFIX + MAIN_JS_FILE
14
+
15
+ const jsFiles = [
16
+ "js/util/constants.js",
17
+ "js/util/drawing.js",
18
+ "js/components/grids.js",
19
+ "js/components/HeaderSummary.js",
20
+ "js/components/StatusRing.js",
21
+ "js/components/SessionWedges.js",
22
+ "js/components/SessionLabels.js",
23
+ "js/components/ActivityTimeseriesGraph.js",
24
+ "js/components/ServerStatus.js",
25
+ MAIN_JS_FILE
26
+ ]
27
+
28
+ const svgRoot = document.getElementById('svg_root')
29
+ const cssElement = document.getElementById('main_style')
30
+
31
+ const ws = new WebsocketClient({port: PORT})
32
+
33
+ const handleCss = (contents) => {
34
+ cssElement.innerText = contents
35
+ }
36
+
37
+ let lastMainContents = null
38
+ let initFinished = false
39
+ const handleJs = (contents) => {
40
+ try {
41
+ eval(contents)
42
+ } catch (e) {
43
+ window._lastEvalError = e
44
+ console.log(e)
45
+ }
46
+ }
47
+
48
+ ws.emitter.on('message', (message) => {
49
+ console.log("got emitter message", message)
50
+ if (message.endsWith === cssEndsWith) {
51
+ handleCss(message.contents)
52
+ return
53
+ }
54
+ if (message.method === "watch-file" && message.endsWith.endsWith('.js')) {
55
+ if (!initFinished) { return }
56
+ cleanupEventHandlers()
57
+ handleJs(message.contents)
58
+ if (message.endsWith === mainJsEndsWith) {
59
+ lastMainContents = message.contents
60
+ } else if (lastMainContents != null) {
61
+ handleJs(lastMainContents)
62
+ }
63
+ return
64
+ }
65
+ if (message.method === "watch-wtr-status") {
66
+ statusDataEmitter.emit('data', message.data)
67
+ return
68
+ }
69
+ if (message.method === "watch-wtr-activity") {
70
+ statusDataEmitter.emit('activity', message.data)
71
+ return
72
+ }
73
+ })
74
+
75
+ ws.emitter.on('socket-open', () => statusDataEmitter.emit('socket-open'))
76
+ ws.emitter.on('socket-close', () => statusDataEmitter.emit('socket-close'))
77
+
78
+ const initFiles = async () => {
79
+ await fetch(CSS_FILE).then(r => r.text()).then(handleCss)
80
+ const jsFetches = jsFiles.map(async (fileName) => {
81
+ return fetch(fileName).then(r => r.text())
82
+ })
83
+ const jsResults = await Promise.all(jsFetches)
84
+ lastMainContents = jsResults.at(-1)
85
+ requestAnimationFrame(() => { // make sure css is applied first
86
+ jsResults.forEach((contents) => {
87
+ handleJs(contents)
88
+ })
89
+ initFinished = true
90
+ })
91
+ }
92
+
93
+ const subscribeWatchers = () => {
94
+ ws.sendMessage({method: "watch-log-messages"})
95
+ ws.sendMessage({method: "watch-wtr-status"})
96
+ ws.sendMessage({method: "watch-wtr-activity"})
97
+ ws.sendMessage({ method: "init", name: "beta-status-ui" })
98
+ ws.sendMessage({ method: "watch-file", endsWith: cssEndsWith })
99
+ jsFiles.forEach((jsFile) => {
100
+ const jsEndsWith = FILE_PREFIX + jsFile
101
+ ws.sendMessage({ method: "watch-file", endsWith: jsEndsWith })
102
+ })
103
+
104
+ }
105
+
106
+ await initFiles()
107
+ subscribeWatchers()
108
+
109
+ const updateSvgDimensions = () => {
110
+ svgRoot.setAttribute("height", window.innerHeight - 4)
111
+ svgRoot.setAttribute("width", window.innerWidth)
112
+ }
113
+
114
+ updateSvgDimensions()
115
+ window.addEventListener("resize", () => {
116
+ updateSvgDimensions()
117
+ })
@@ -0,0 +1,97 @@
1
+ /* eslint no-unused-vars: 0 */
2
+ const { drawGrid, drawPolarGrid, HeaderSummary, StatusRing, onEvent, statusDataEmitter, SessionWedges, ClientLabel,
3
+ ActivityTimeseriesGraph, EditorLabel, ServerStatus, constants } = window.__WTR__
4
+ const { outerRingRadius, innerRingRadius, outerArcSize } = constants
5
+
6
+ const gridGroup = document.getElementById("grid_group")
7
+ gridGroup.innerHTML = ""
8
+ // drawPolarGrid(gridGroup)
9
+ // drawGrid(gridGroup)
10
+
11
+ const headerSummaryNode = document.getElementById('header_summary_group')
12
+ const headerSummary = new HeaderSummary({parentNode: headerSummaryNode})
13
+
14
+ const statusRingNode = document.getElementById('status_ring_group')
15
+ const statusRing = new StatusRing({innerRingRadius, outerRingRadius, outerArcSize, parentNode: statusRingNode})
16
+
17
+ const clientWedgesNode = document.getElementById('client_wedges_group')
18
+ const clientWedges = new SessionWedges({outerRingRadius, outerArcSize, direction: -1, Label: ClientLabel, parentNode: clientWedgesNode})
19
+
20
+ const editorWedgesNode = document.getElementById('editor_wedges_group')
21
+ const editorWedges = new SessionWedges({outerRingRadius, outerArcSize, Label: EditorLabel, parentNode: editorWedgesNode})
22
+
23
+ const activityGraphNode = document.getElementById('activity_timeseries_graph')
24
+ const activityGraph = new ActivityTimeseriesGraph({innerRingRadius, parentNode: activityGraphNode})
25
+
26
+ const serverStatusNode = document.getElementById('server_status_group')
27
+ const serverStatus = new ServerStatus({parentNode: serverStatusNode})
28
+
29
+ const statusDataTranform = (rawData) => {
30
+
31
+ const editors = []
32
+ const clients = []
33
+ const allSessions = new Map()
34
+
35
+ rawData.sessions.forEach((session) => {
36
+ allSessions.set(session.id, session)
37
+ session.activeWatchCount = 0
38
+ session.activeOpenCount = 0
39
+ const isEditor = session.editorPid != null || session.lsPid != null
40
+ if (isEditor) {
41
+ editors.push(session)
42
+ } else {
43
+ clients.push(session)
44
+ }
45
+ })
46
+
47
+ for (const session of allSessions.values()) {
48
+ const openFileLinks = Object.values(session.openFileLinks)
49
+ session.activeOpenCount = openFileLinks.length
50
+ openFileLinks.forEach((links) => {
51
+ links.forEach(({clientId}) => {
52
+ const clientSession = allSessions.get(clientId)
53
+ if (!clientSession) { return }
54
+ clientSession.activeWatchCount++
55
+ })
56
+ })
57
+ }
58
+
59
+ return { editors, clients }
60
+ }
61
+
62
+ const getServerPid = (editors) => {
63
+ for (const editor of editors) {
64
+ if (editor.isServer) { return editor.lsPid }
65
+ }
66
+ return null
67
+ }
68
+
69
+ const handleStatusData = (rawData) => {
70
+ window.__WTR__.lastStatusData = rawData
71
+ const data = statusDataTranform(rawData)
72
+ console.log("status data updated", data)
73
+ headerSummary.update(data)
74
+ statusRing.update(data)
75
+ clientWedges.update(data.clients)
76
+ editorWedges.update(data.editors)
77
+ serverStatus.update(getServerPid(data.editors))
78
+ }
79
+
80
+ onEvent(statusDataEmitter, 'data', handleStatusData)
81
+
82
+ if (window.__WTR__.lastStatusData) {
83
+ handleStatusData(window.__WTR__.lastStatusData)
84
+ }
85
+
86
+ const handleActivity = (data) => {
87
+ editorWedges.triggerActivity(data.relayer)
88
+ clientWedges.triggerActivity(new Set(data.watchers))
89
+ activityGraph.triggerActivity()
90
+ }
91
+
92
+ onEvent(statusDataEmitter, 'activity', handleActivity)
93
+
94
+ onEvent(statusDataEmitter, 'socket-close', () => {
95
+ console.log("socket closed!")
96
+ serverStatus.update(null)
97
+ })
@@ -0,0 +1,31 @@
1
+ import { EventEmitter } from './EventEmitter.js'
2
+
3
+ const dependencies = {}
4
+ window.__WTR__ = dependencies
5
+
6
+ const exportDeps = (exportObj) => Object.assign(dependencies, exportObj)
7
+
8
+ const statusDataEmitter = new EventEmitter()
9
+
10
+ const cleanupFuncs = []
11
+
12
+ const onEvent = (emitter, event, func) => {
13
+ if (typeof emitter.on === 'function') {
14
+ emitter.on(event, func)
15
+ cleanupFuncs.push(() => emitter.removeListener(event, func))
16
+ } else if (typeof emitter.addEventListener === 'function') {
17
+ emitter.addEventListener(event, func)
18
+ cleanupFuncs.push(() => emitter.removeEventListener(event, func))
19
+ } else {
20
+ throw new Error("target is not an event emitter")
21
+ }
22
+ }
23
+
24
+ const cleanupEventHandlers = () => {
25
+ cleanupFuncs.forEach(f => f())
26
+ cleanupFuncs.length = 0
27
+ }
28
+
29
+ exportDeps({exportDeps, statusDataEmitter, onEvent, cleanupEventHandlers})
30
+
31
+ export default dependencies
@@ -0,0 +1,40 @@
1
+ export class EventEmitter {
2
+ constructor () {
3
+ this.events = new Map()
4
+ }
5
+
6
+ on (event, listener) {
7
+ let eventSubscribers = this.events.get(event)
8
+ if (!eventSubscribers) {
9
+ eventSubscribers = new Set()
10
+ this.events.set(event, eventSubscribers)
11
+ }
12
+ eventSubscribers.add(listener)
13
+ return () => this.removeListener(event, listener)
14
+ }
15
+
16
+ removeListener (event, listener) {
17
+ const eventSubscribers = this.events.get(event)
18
+ if (!eventSubscribers) { return }
19
+ if (!listener) {
20
+ this.events.delete(event)
21
+ return
22
+ }
23
+ eventSubscribers.delete(listener)
24
+ }
25
+
26
+ emit (event, ...args) {
27
+ const eventSubscribers = this.events.get(event)
28
+ if (!eventSubscribers) { return }
29
+ for (const listener of eventSubscribers) {
30
+ listener.apply(this, args)
31
+ }
32
+ }
33
+
34
+ once (event, listener) {
35
+ const remove = this.on(event, (...args) => {
36
+ remove()
37
+ listener.apply(this, args)
38
+ })
39
+ }
40
+ }