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,179 @@
1
+ import { Writable } from 'node:stream'
2
+ import { describe, it, expect, beforeAll } from "vitest"
3
+ import { TESTONLY_resetRequestId, createHeader, writeNotification, writeRequest, writeResponse, writeToOutput } from "./LspWriter.js"
4
+
5
+ describe("LspWriter", () => {
6
+ describe("createHeader", () => {
7
+ describe("Simple Case", () => {
8
+ const messageObj = {}
9
+ const expectedHeader = "Content-Length: 2\r\n\r\n"
10
+
11
+ it("Should create header with correct length", () => {
12
+ const actual = createHeader(JSON.stringify(messageObj))
13
+ expect(actual).toEqual(expectedHeader)
14
+ })
15
+ })
16
+
17
+ describe("With some content", () => {
18
+ const messageObj = {"a": "1"}
19
+ const expectedHeader = "Content-Length: 9\r\n\r\n"
20
+
21
+ it("Should create header with correct length", () => {
22
+ const actual = createHeader(JSON.stringify(messageObj))
23
+ expect(actual).toEqual(expectedHeader)
24
+ })
25
+ })
26
+
27
+ describe("With multi byte characters", () => {
28
+ const messageObj = {"a": "🌊"}
29
+ const expectedHeader = "Content-Length: 12\r\n\r\n"
30
+
31
+ it("Should create header with correct byte length, not string length", () => {
32
+ const actual = createHeader(JSON.stringify(messageObj))
33
+ expect(actual).toEqual(expectedHeader)
34
+ })
35
+ })
36
+ })
37
+
38
+ describe("writeToOutput", () => {
39
+ describe("simple object", () => {
40
+ const messageObj = {"a": "1"}
41
+ const expectedOutput = `Content-Length: 9\r\n\r\n{"a":"1"}`
42
+ let actualOutput = ""
43
+
44
+ beforeAll(() => {
45
+ const outputStream = new Writable({
46
+ write (data, _enc, next) {
47
+ actualOutput += data.toString()
48
+ next()
49
+ }
50
+ })
51
+ writeToOutput(outputStream, messageObj)
52
+ })
53
+
54
+ it("should stringify the json and add a Content-Length header, then write the head and json to the output stream", () => {
55
+ expect(actualOutput).toEqual(expectedOutput)
56
+ })
57
+ })
58
+ })
59
+
60
+ describe("writeResponse", () => {
61
+ describe("successful response", () => {
62
+ const result = {"a": "1"}
63
+ const error = null
64
+ const expectedOutput = `Content-Length: 45\r\n\r\n{"jsonrpc":"2.0","id":"0","result":{"a":"1"}}`
65
+ let actualOutput = ""
66
+
67
+ beforeAll(() => {
68
+ const outputStream = new Writable({
69
+ write (data, _enc, next) {
70
+ actualOutput += data.toString()
71
+ next()
72
+ }
73
+ })
74
+ writeResponse(outputStream, "0", error, result)
75
+ })
76
+
77
+ it("should create a response object with a result property (and no error) and write it to the output stream", () => {
78
+ expect(actualOutput).toEqual(expectedOutput)
79
+ })
80
+ })
81
+
82
+ describe("error response", () => {
83
+ const result = null
84
+ const error = {code: 1, message: "test error", data: {t: 5}}
85
+ const expectedOutput = `Content-Length: 83\r\n\r\n{"jsonrpc":"2.0","id":"0","error":{"code":1,"message":"test error","data":{"t":5}}}`
86
+ let actualOutput = ""
87
+
88
+ beforeAll(() => {
89
+ const outputStream = new Writable({
90
+ write (data, _enc, next) {
91
+ actualOutput += data.toString()
92
+ next()
93
+ }
94
+ })
95
+ writeResponse(outputStream, "0", error, result)
96
+ })
97
+
98
+ it("should create a response object with an error property (and no result) and write it to the output stream", () => {
99
+ expect(actualOutput).toEqual(expectedOutput)
100
+ })
101
+ })
102
+ })
103
+
104
+ describe("writeNotification", () => {
105
+ describe("simple parameters", () => {
106
+ const method = "test/test-method"
107
+ const parameters = {"a": "1"}
108
+ const expectedOutput = `Content-Length: 64\r\n\r\n{"jsonrpc":"2.0","method":"test/test-method","params":{"a":"1"}}`
109
+ let actualOutput = ""
110
+
111
+ beforeAll(() => {
112
+ const outputStream = new Writable({
113
+ write (data, _enc, next) {
114
+ actualOutput += data.toString()
115
+ next()
116
+ }
117
+ })
118
+ writeNotification(outputStream, method, parameters)
119
+ })
120
+
121
+ it("should write a notification message without an ID property on the output stream", () => {
122
+ expect(actualOutput).toEqual(expectedOutput)
123
+ })
124
+ })
125
+ })
126
+
127
+ describe("writeReqest", () => {
128
+ describe("single request", () => {
129
+ const method = "test/test-method"
130
+ const parameters = {"a": "1"}
131
+ const expectedOutput = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
132
+ let actualOutput = ""
133
+
134
+ beforeAll(() => {
135
+ TESTONLY_resetRequestId()
136
+ const outputStream = new Writable({
137
+ write (data, _enc, next) {
138
+ actualOutput += data.toString()
139
+ next()
140
+ }
141
+ })
142
+ writeRequest(outputStream, method, parameters)
143
+ })
144
+
145
+ it("should write a request message with an ID property on the output stream", () => {
146
+ expect(actualOutput).toEqual(expectedOutput)
147
+ })
148
+ })
149
+
150
+ describe("multiple requests", () => {
151
+ const method = "test/test-method"
152
+ const params1 = {"a": "1"}
153
+ const params2 = {"b": "2"}
154
+ const params3 = {"c": "3"}
155
+ const expectedOutput1 = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
156
+ const expectedOutput2 = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"1","method":"test/test-method","params":{"b":"2"}}`
157
+ const expectedOutput3 = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"2","method":"test/test-method","params":{"c":"3"}}`
158
+ const expectedOutput = expectedOutput1 + expectedOutput2 + expectedOutput3
159
+ let actualOutput = ""
160
+
161
+ beforeAll(() => {
162
+ TESTONLY_resetRequestId()
163
+ const outputStream = new Writable({
164
+ write (data, _enc, next) {
165
+ actualOutput += data.toString()
166
+ next()
167
+ }
168
+ })
169
+ writeRequest(outputStream, method, params1)
170
+ writeRequest(outputStream, method, params2)
171
+ writeRequest(outputStream, method, params3)
172
+ })
173
+
174
+ it("should write multiple request messages with increasing IDs", () => {
175
+ expect(actualOutput).toEqual(expectedOutput)
176
+ })
177
+ })
178
+ })
179
+ })
@@ -0,0 +1,23 @@
1
+ export const headerKey = "Content-Length"
2
+
3
+ export const parseErrorCode = -32700
4
+
5
+ export const parseHeaderErrorMessage = "Could not read Content-Length from header"
6
+
7
+ export const parseJsonErrorMessage = "Could not parse JSON message"
8
+
9
+ export const methodNotFoundErrorCode = -32601
10
+
11
+ export const methodNotFoundErrorMessage = "Method not found"
12
+
13
+ export const invalidRequestErrorCode = -32600
14
+
15
+ export const invalidRequestErrorMessage = "Invalid Message. Missing id and/or method"
16
+
17
+ export const unexpectedNotificationErrorCode = -1
18
+
19
+ export const unexpectedNotificationErrorMessage = "Unexpected error in notification handler"
20
+
21
+ export const unexpectedRequestErrorCode = -2
22
+
23
+ export const unexpectedRequestErrorMessage = "Unexpected error in request handler"
@@ -0,0 +1,54 @@
1
+ @font-face {
2
+ font-family: "Montserrat";
3
+ src: url("/css/fonts/Montserrat-Thin.ttf") format("truetype");
4
+ font-weight: 100;
5
+ }
6
+
7
+ @font-face {
8
+ font-family: "Montserrat";
9
+ src: url("/css/fonts/Montserrat-ExtraLight.ttf") format("truetype");
10
+ font-weight: 200;
11
+ }
12
+
13
+ @font-face {
14
+ font-family: "Montserrat";
15
+ src: url("/css/fonts/Montserrat-Light.ttf") format("truetype");
16
+ font-weight: 300;
17
+ }
18
+
19
+ @font-face {
20
+ font-family: "Montserrat";
21
+ src: url("/css/fonts/Montserrat-Regular.ttf") format("truetype");
22
+ font-weight: 400;
23
+ }
24
+
25
+ @font-face {
26
+ font-family: "Montserrat";
27
+ src: url("/css/fonts/Montserrat-Medium.ttf") format("truetype");
28
+ font-weight: 500;
29
+ }
30
+
31
+ @font-face {
32
+ font-family: "Montserrat";
33
+ src: url("/css/fonts/Montserrat-SemiBold.ttf") format("truetype");
34
+ font-weight: 600;
35
+ }
36
+
37
+ @font-face {
38
+ font-family: "Montserrat";
39
+ src: url("/css/fonts/Montserrat-Bold.ttf") format("truetype");
40
+ font-weight: 700;
41
+ }
42
+
43
+ @font-face {
44
+ font-family: "Montserrat";
45
+ src: url("/css/fonts/Montserrat-ExtraBold.ttf") format("truetype");
46
+ font-weight: 800;
47
+ }
48
+
49
+ @font-face {
50
+ font-family: "Montserrat";
51
+ src: url("/css/fonts/Montserrat-Black.ttf") format("truetype");
52
+ font-weight: 900;
53
+ }
54
+
@@ -0,0 +1,198 @@
1
+ .base_colors {
2
+ --color-navy-nightfall: #0d151b;
3
+ --color-tangerine-sunset: #fb4b37;
4
+ --color-golden-sunset: #fbcc3b;
5
+ --color-amber-dawn: #ffd986;
6
+ --color-azure-afternoon: #629bc2;
7
+ --color-ivory-daybreak: #f0ebeb;
8
+ --color-light-sand-dune: #d6d2c2;
9
+ --color-evening-shadow: #514a45;
10
+ }
11
+
12
+ .color_palette {
13
+ --bg2: var(--color-navy-nightfall);
14
+ --accent-active: var(var(--color-golden-sunset))
15
+ }
16
+
17
+ .color_theme {
18
+ --main-background-color: var(--bg2);
19
+ --header-text-color: var(--color-ivory-daybreak);
20
+ --summary-value-text-color: hsl(from var(--header-text-color) h s l / 0.9);
21
+ --offline-color: oklch(from var(--color-golden-sunset) calc(l * .5) calc(c * .25) h);
22
+ --online-color: oklch(from var(--color-golden-sunset) calc(l * .80) calc(c * .25) h);
23
+ --active-color: var(--color-golden-sunset);
24
+ --wedge-online-color: oklch(from var(--color-azure-afternoon) calc(l * .5) calc(c * .5) h);
25
+ --wedge-active-color: var(--color-azure-afternoon);
26
+ --timeseries-line-color: var(--color-tangerine-sunset);
27
+ --timeseries-label-color: #7f8186;
28
+ --timeseries-value-color: oklch(from var(--timeseries-label-color) calc(l * 1.2) c h);
29
+
30
+ }
31
+
32
+ body {
33
+ background: var(--main-background-color);
34
+ }
35
+
36
+ #svg_root {
37
+ fill: none;
38
+ stroke: none;
39
+ stroke-width: 0.01;
40
+ font-family: Montserrat, sans-serif;
41
+ font-size: 0.1pt;
42
+ }
43
+
44
+ #grid_group {
45
+ stroke: #fff;
46
+ stroke-width: 0.001;
47
+ }
48
+
49
+ .grid_axis {
50
+ stroke-width: 0.003;
51
+ stroke: hsl(76, 90%, 80%);
52
+ }
53
+
54
+ #header_summary_group {
55
+ fill: var(--header-text-color);
56
+ font-weight: 300;
57
+ }
58
+
59
+ .right_header {
60
+ text-anchor: end;;
61
+ }
62
+
63
+ .header_number {
64
+ font-weight: 100;
65
+ }
66
+
67
+ #status_ring_group {
68
+ stroke: var(--offline-color);
69
+ }
70
+
71
+ #status_ring_group.online {
72
+ stroke: var(--online-color);
73
+ }
74
+
75
+ #status_ring_group.active {
76
+ stroke: var(--active-color);
77
+ }
78
+
79
+ .single_wedge_group {
80
+ display: none;
81
+ }
82
+
83
+ .single_wedge_group.online, .single_wedge_group.active {
84
+ display: inherit;
85
+ }
86
+
87
+ .online .wedge_node {
88
+ fill: var(--wedge-online-color);
89
+ }
90
+
91
+ .active .wedge_node {
92
+ fill: var(--wedge-active-color);
93
+ }
94
+
95
+ .wedge_identifier {
96
+ fill: var(--header-text-color);
97
+ font-size: 0.04pt;
98
+ }
99
+
100
+ .wedge_identifier.small_text {
101
+ font-size: 0.03pt;
102
+ }
103
+
104
+ .summary_text_value {
105
+ fill: var(--summary-value-text-color);
106
+ font-size: 0.03pt;
107
+ font-weight: 200;
108
+ }
109
+
110
+ .summary_watched_circle {
111
+ fill: var(--wedge-active-color);
112
+ }
113
+
114
+ .summary_active_circle {
115
+ fill: var(--active-color);
116
+ }
117
+
118
+ .timeseries_bg {
119
+ /* fill: #ffffff1f; */
120
+ fill: none;
121
+ }
122
+
123
+ .timeseries_path {
124
+ stroke: var(--timeseries-line-color);
125
+ clip-path: url(#inner_circle_clip);
126
+ }
127
+
128
+ .timeseries_big_label {
129
+ /* fill: var(--timeseries-label-color); */
130
+ font-size: 0.04pt;
131
+ font-weight: 400;
132
+ text-anchor: end;
133
+ }
134
+
135
+ .timeseries_small_label {
136
+ /* fill: var(--timeseries-label-color); */
137
+ font-size: 0.03pt;
138
+ font-weight: 400;
139
+ text-anchor: end;
140
+ }
141
+
142
+ .timeseries_value {
143
+ fill: var(--timeseries-value-color);
144
+ font-size: 0.05pt;
145
+ font-weight: 300;
146
+ text-anchor: middle;
147
+ }
148
+
149
+ .tooltip_text {
150
+ fill: #fff;
151
+ font-size: 0.025pt;
152
+ font-weight: 200;
153
+ text-anchor: middle;
154
+ }
155
+
156
+ .tooltip_outline {
157
+ stroke: #fff;
158
+ stroke-width: 0.002;
159
+ fill: #000;
160
+ }
161
+
162
+ .tooltip_display_group {
163
+ display: none;
164
+ }
165
+
166
+ .tooltip_wrapper_group:hover .tooltip_display_group {
167
+ display: initial;
168
+ }
169
+
170
+ .server_status_label {
171
+ fill: var(--timeseries-label-color);
172
+ font-weight: 300;
173
+ font-size: 0.06pt;
174
+ text-anchor: middle;
175
+ }
176
+
177
+ .server_status_value {
178
+ fill: var(--timeseries-value-color);
179
+ font-weight: 100;
180
+ font-size: 0.06pt;
181
+ text-anchor: middle;
182
+ }
183
+
184
+ .server_status_offline {
185
+ fill: var(--timeseries-line-color);
186
+ font-weight: 400;
187
+ font-size: 0.06pt;
188
+ text-anchor: middle;
189
+ }
190
+
191
+ .test_circle {
192
+ fill: #fff;
193
+ }
194
+
195
+ .test_line {
196
+ stroke: #fff;
197
+ stroke-width: 0.005;
198
+ }
@@ -0,0 +1,36 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+
7
+ <title>WTR Status</title>
8
+
9
+ <link rel="stylesheet" href="css/fonts.css">
10
+ <style id="main_style"></style>
11
+ <script type="module" src="js/index.js" defer></script>
12
+
13
+ <style>
14
+ body {
15
+ margin: 0;
16
+ overscroll-behavior: none;
17
+ display: grid;
18
+ align-items: center;
19
+ justify-content: center;
20
+ padding-top: 2px;
21
+ }
22
+ </style>
23
+ </head>
24
+
25
+ <body class="base_colors color_palette color_theme">
26
+ <svg id="svg_root" viewBox="-1 -1 2 2" xmlns="http://www.w3.org/2000/svg">
27
+ <g id="grid_group"></g>
28
+ <g id="header_summary_group"></g>
29
+ <g id="status_ring_group"></g>
30
+ <g id="client_wedges_group" class="wedge_group"></g>
31
+ <g id="editor_wedges_group" class="wedge_group"></g>
32
+ <g id="activity_timeseries_graph"></g>
33
+ <g id="server_status_group"></g>
34
+ </svg>
35
+ </body>
36
+ </html>
@@ -0,0 +1,149 @@
1
+ const { exportDeps, drawSvgElement, drawCircle, drawLinearPath, coordsToPathData, drawText, drawToolTip } = window.__WTR__
2
+
3
+ const drawValueWithTooltip = ({x, y, label, direction, parentNode}) => {
4
+ const tooltipWrapperGroup = drawSvgElement("g", undefined, "tooltip_wrapper_group", parentNode)
5
+ drawToolTip({x: x, y: y - 0.0032, text: label, direction, parentNode: tooltipWrapperGroup})
6
+ return drawText({x: x, y: y, dominantBaseline: "middle", text: "0", className: "timeseries_value", parentNode: tooltipWrapperGroup})
7
+ }
8
+
9
+ const dataWindowSize = 16
10
+ const dataWindowInterval = 1000
11
+ const graphHeight = 0.25
12
+
13
+ const innerCircleClipPathId = "inner_circle_clip"
14
+
15
+ const createLinearScale = (domainMin, domainMax, rangeMin, rangeMax) => {
16
+ const domainSize = domainMax - domainMin
17
+ if (domainSize === 0) { return () => rangeMin }
18
+ const rangeSize = rangeMax - rangeMin
19
+ const ratio = rangeSize / domainSize
20
+
21
+ return (domainValue) => (domainValue - domainMin) * ratio + rangeMin
22
+ }
23
+
24
+ const createRandomDataWindow = () => {
25
+ const series = []
26
+ const endTime = new Date().setMilliseconds(0).valueOf()
27
+ const startTime = endTime - dataWindowInterval * dataWindowSize
28
+ let maxValue = 0
29
+
30
+ for (let time = startTime; time < endTime; time += dataWindowInterval) {
31
+ // const value = Math.floor(Math.random() * 101)
32
+ const value = 0
33
+ if (value > maxValue) { maxValue = value }
34
+ series.push({time, value})
35
+ }
36
+
37
+ return series
38
+ }
39
+
40
+ const getSeriesWindowInfo = (series) => {
41
+ const startTime = series.at(1).time
42
+ const endTime = series.at(-1).time
43
+ let maxValue = 0
44
+ series.forEach(({value}) => {
45
+ if (value > maxValue) { maxValue = value }
46
+ })
47
+
48
+ return {startTime, endTime, maxValue}
49
+ }
50
+
51
+ class ActivityTimeseriesGraph {
52
+ constructor ({innerRingRadius, parentNode}) {
53
+ this.innerRingRadius = innerRingRadius
54
+ this.parentNode = parentNode
55
+ this.parentNode.innerHTML = ""
56
+ this.dataWindow = window.activityDataWindow || createRandomDataWindow()
57
+ window.activityDataWindow = this.dataWindow
58
+ if (!window.currentActivityCount) { window.currentActivityCount = 0 }
59
+ this.draw()
60
+ }
61
+
62
+ draw () {
63
+ const minX = -this.innerRingRadius
64
+ const maxX = this.innerRingRadius
65
+ const width = maxX - minX
66
+ const height = graphHeight
67
+ const minY = -height / 2
68
+ const maxY = minY + height
69
+
70
+ const clipPath = drawSvgElement("clipPath", {id: innerCircleClipPathId}, undefined, this.parentNode)
71
+ drawCircle({cx: 0, cy: 0, r: this.innerRingRadius - 0.005, parentNode: clipPath})
72
+ drawSvgElement("rect", {"clip-path": `url(#${innerCircleClipPathId})`, x: minX, y: minY, height, width}, "timeseries_bg", this.parentNode)
73
+
74
+ const series = this.dataWindow
75
+ const {startTime, endTime, maxValue} = getSeriesWindowInfo(series)
76
+
77
+ const maxValueElement = drawValueWithTooltip({x: minX + width / 2, y: minY - 0.08, label: "Max updates in a 1 second window", direction: "above", parentNode: this.parentNode})
78
+ const currentValueElement = drawValueWithTooltip({x: minX + width / 2, y: maxY + 0.095, label: "Updates in last full second", parentNode: this.parentNode})
79
+
80
+ let valueScale = createLinearScale(0, maxValue, maxY, minY) // in svg, y increases as it goes down, so we need to flip max and min in the range
81
+ let timeScale = createLinearScale(startTime, endTime, minX, maxX)
82
+
83
+ const pathCoords = series.map(({time, value}) => {
84
+ return [timeScale(time), valueScale(value)]
85
+ })
86
+
87
+ const graphPath = drawLinearPath({coords: pathCoords, className: "timeseries_path", parentNode: this.parentNode})
88
+
89
+ if (window.activityTimeout) { clearTimeout(window.activityTimeout) }
90
+ const onTickUpdate = () => {
91
+ scheduleNextTick()
92
+
93
+ console.log("data tick", Date.now())
94
+ const series = this.dataWindow
95
+ const prevEndTime = series.at(-1).time
96
+ const newTime = prevEndTime + dataWindowInterval
97
+ series.shift()
98
+ series.push({time: newTime, value: window.currentActivityCount})
99
+ currentValueElement.innerHTML = window.currentActivityCount
100
+ window.currentActivityCount = 0
101
+
102
+ const pathCoords = series.map(({time, value}) => {
103
+ return [timeScale(time), valueScale(value)]
104
+ })
105
+
106
+ const pathData = coordsToPathData(pathCoords)
107
+ graphPath.style.transition = ""
108
+ graphPath.setAttribute("d", pathData)
109
+
110
+ requestAnimationFrame(() => {
111
+ requestAnimationFrame(() => {
112
+ const {startTime, endTime, maxValue} = getSeriesWindowInfo(series)
113
+ maxValueElement.innerHTML = maxValue
114
+
115
+ valueScale = createLinearScale(0, maxValue, maxY, minY) // in svg, y increases as it goes down, so we need to flip max and min in the range
116
+ timeScale = createLinearScale(startTime, endTime, minX, maxX)
117
+
118
+ const pathCoords = series.map(({time, value}) => {
119
+ return [timeScale(time), valueScale(value)]
120
+ })
121
+
122
+ const pathData = coordsToPathData(pathCoords)
123
+ graphPath.style.transition = "all 1s linear"
124
+ graphPath.setAttribute("d", pathData)
125
+ })
126
+ })
127
+
128
+ }
129
+
130
+ const scheduleNextTick = () => {
131
+ const nowMillis = new Date().getMilliseconds()
132
+ const millisUntilNextSecond = 1000 - nowMillis
133
+ window.activityTimeout = setTimeout(onTickUpdate, millisUntilNextSecond)
134
+ }
135
+
136
+ scheduleNextTick()
137
+ }
138
+
139
+ triggerActivity () {
140
+ window.currentActivityCount++
141
+ console.log("time series graph activity")
142
+ }
143
+ }
144
+
145
+ exportDeps({ActivityTimeseriesGraph})
146
+
147
+ /*
148
+ *
149
+ */
@@ -0,0 +1,23 @@
1
+ const { exportDeps, drawText } = window.__WTR__
2
+
3
+ class HeaderSummary {
4
+ constructor ({parentNode}) {
5
+ this.parentNode = parentNode
6
+ this.draw()
7
+ }
8
+
9
+ draw () {
10
+ this.parentNode.innerHTML = ""
11
+ drawText({x: -.86, y: -.73, text: "editors", parentNode: this.parentNode})
12
+ // this.editorCountNode = drawText({x: -.96, y: -.62, text: "0", className: "header_number", parentNode: this.parentNode})
13
+ drawText({x: .86, y: -.73, text: "clients", className: "right_header", parentNode: this.parentNode})
14
+ // this.clientCountNode = drawText({x: .96, y: -.62, text: "0", className: ["right_header", "header_number"], parentNode: this.parentNode})
15
+ }
16
+
17
+ update (/* data */) {
18
+ // this.editorCountNode.innerHTML = data.editors.length
19
+ // this.clientCountNode.innerHTML = data.clients.length
20
+ }
21
+ }
22
+
23
+ exportDeps({HeaderSummary})