websocket-text-relay 1.1.4 → 1.1.6
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/changelog.md +4 -0
- package/docs/code-structure.md +15 -11
- package/eslint.config.js +5 -1
- package/package.json +9 -9
- package/src/ui/css/main.css +22 -78
- package/src/ui/favicon.png +0 -0
- package/src/ui/index.html +19 -14
- package/src/ui/js/components/activityLabels.js +41 -0
- package/src/ui/js/components/activityTimeSeries.js +58 -0
- package/src/ui/js/components/drawSessionLabel.js +117 -0
- package/src/ui/js/components/footerStatus.js +37 -0
- package/src/ui/js/components/headers.js +100 -0
- package/src/ui/js/components/sessionWedges.js +96 -0
- package/src/ui/js/components/statusRing.js +51 -0
- package/src/ui/js/data/wtrActivity.js +85 -0
- package/src/ui/js/data/wtrActivity.types.js +3 -0
- package/src/ui/js/data/wtrStatus.js +134 -0
- package/src/ui/js/data/wtrStatus.types.js +61 -0
- package/src/ui/js/{util → setup}/EventEmitter.js +5 -1
- package/src/ui/js/{util → setup}/WebsocketClient.js +1 -4
- package/src/ui/js/setup/dependencyManager.js +9 -0
- package/src/ui/js/setup/evalOnChange.js +18 -0
- package/src/ui/js/setup/eventSubscriber.js +21 -0
- package/src/ui/js/setup.js +141 -0
- package/src/ui/js/util/constants.js +5 -1
- package/src/ui/js/util/drawing.js +26 -76
- package/src/websocket-interface/httpServer.js +1 -1
- package/src/ui/js/components/ActivityTimeseriesGraph.js +0 -194
- package/src/ui/js/components/HeaderSummary.js +0 -22
- package/src/ui/js/components/ServerStatus.js +0 -43
- package/src/ui/js/components/SessionLabels.js +0 -319
- package/src/ui/js/components/SessionWedges.js +0 -127
- package/src/ui/js/components/StatusRing.js +0 -54
- package/src/ui/js/components/grids.js +0 -36
- package/src/ui/js/index.js +0 -121
- package/src/ui/js/main.js +0 -128
- package/src/ui/js/util/DependencyManager.js +0 -31
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const {
|
|
2
|
+
drawSvgElement,
|
|
3
|
+
drawWedge,
|
|
4
|
+
drawSessionLabel,
|
|
5
|
+
wtrStatusEmitter,
|
|
6
|
+
wtrActivityEmitter,
|
|
7
|
+
onEvent,
|
|
8
|
+
constants,
|
|
9
|
+
} = __WTR__
|
|
10
|
+
const { outerRingRadius, outerArcSize, maxSessionWedges } = constants
|
|
11
|
+
|
|
12
|
+
const editorsParentGroup = document.getElementById("editor_wedges_group")
|
|
13
|
+
editorsParentGroup.innerHTML = ""
|
|
14
|
+
|
|
15
|
+
const clientsParentGroup = document.getElementById("client_wedges_group")
|
|
16
|
+
clientsParentGroup.innerHTML = ""
|
|
17
|
+
|
|
18
|
+
const wedgeSpacing = 0.01
|
|
19
|
+
const wedgeWidth = 0.08
|
|
20
|
+
|
|
21
|
+
const startAngleOffset = outerArcSize / 2
|
|
22
|
+
const totalAngleDelta = 0.5 - outerArcSize - wedgeSpacing
|
|
23
|
+
const wedgeAngleDelta = totalAngleDelta / maxSessionWedges - wedgeSpacing
|
|
24
|
+
const innerWedgeRadius = outerRingRadius - wedgeWidth / 2
|
|
25
|
+
|
|
26
|
+
/** @type {Map<SessionId, HTMLElement>} */
|
|
27
|
+
let wedgeMap = new Map()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param sessions {EditorStatus[] | ClientStatus[] }
|
|
31
|
+
* @param parent {HTMLElement}
|
|
32
|
+
* @param direction {1 | -1}
|
|
33
|
+
*/
|
|
34
|
+
const drawWedges = (sessions, parent, direction = 1) => {
|
|
35
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
36
|
+
if (i >= maxSessionWedges) {
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
const session = sessions[i]
|
|
40
|
+
|
|
41
|
+
let startAngle = 0.25 + direction * (startAngleOffset + (i + 1) * wedgeSpacing + i * wedgeAngleDelta)
|
|
42
|
+
if (direction === -1) {
|
|
43
|
+
startAngle -= wedgeAngleDelta
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isActive = session.activeWatchCount || session.activeOpenCount
|
|
47
|
+
|
|
48
|
+
const classNames = ["wedge_node"]
|
|
49
|
+
if (isActive) {
|
|
50
|
+
classNames.push("active")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const wedgeLabelGroup = drawSvgElement({ tag: "g", parent })
|
|
54
|
+
|
|
55
|
+
const wedge = drawWedge({
|
|
56
|
+
startAngle,
|
|
57
|
+
angleDelta: wedgeAngleDelta,
|
|
58
|
+
innerRadius: innerWedgeRadius,
|
|
59
|
+
radiusDelta: wedgeWidth,
|
|
60
|
+
className: classNames,
|
|
61
|
+
parent: wedgeLabelGroup,
|
|
62
|
+
})
|
|
63
|
+
wedgeMap.set(session.id, wedge)
|
|
64
|
+
|
|
65
|
+
drawSessionLabel({
|
|
66
|
+
wedgeCenterAngle: startAngle + wedgeAngleDelta / 2,
|
|
67
|
+
wedgeCenterRadius: outerRingRadius,
|
|
68
|
+
direction,
|
|
69
|
+
session,
|
|
70
|
+
parent: wedgeLabelGroup,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onEvent(wtrStatusEmitter, "data", (/** @type {WtrStatus} */ { editors, clients }) => {
|
|
76
|
+
wedgeMap = new Map()
|
|
77
|
+
editorsParentGroup.innerHTML = ""
|
|
78
|
+
clientsParentGroup.innerHTML = ""
|
|
79
|
+
drawWedges(editors, editorsParentGroup, 1)
|
|
80
|
+
drawWedges(clients, clientsParentGroup, -1)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const flashAnimationKeyframes = [{ fill: "#fff" }, { fill: "var(--wedge-active-color)" }]
|
|
84
|
+
const flashAnimationProps = { duration: 250, easing: "ease-out", iterations: 1 }
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param sessionId {SessionId}
|
|
88
|
+
*/
|
|
89
|
+
const triggerActivityAnimation = (sessionId) => {
|
|
90
|
+
wedgeMap.get(sessionId)?.animate(flashAnimationKeyframes, flashAnimationProps)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onEvent(wtrActivityEmitter, "data", (/** @type {WtrActivityMessage} */ { relayer, watchers }) => {
|
|
94
|
+
triggerActivityAnimation(relayer)
|
|
95
|
+
watchers.forEach(triggerActivityAnimation)
|
|
96
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { drawSvgElement, drawWedge, constants, wtrStatusEmitter, onEvent } = __WTR__
|
|
2
|
+
const { innerRingRadius, outerRingRadius, outerArcSize } = constants
|
|
3
|
+
|
|
4
|
+
const parentGroup = document.getElementById("status_ring_group")
|
|
5
|
+
parentGroup.innerHTML = ""
|
|
6
|
+
|
|
7
|
+
let currentClass = "offline"
|
|
8
|
+
|
|
9
|
+
const wrapper = drawSvgElement({
|
|
10
|
+
tag: "g",
|
|
11
|
+
className: ["status_ring_wrapper", currentClass],
|
|
12
|
+
parent: parentGroup,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
drawSvgElement({
|
|
16
|
+
tag: "circle",
|
|
17
|
+
attributes: { r: innerRingRadius },
|
|
18
|
+
parent: wrapper,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
drawWedge({
|
|
22
|
+
startAngle: 0.25 - outerArcSize / 2,
|
|
23
|
+
angleDelta: outerArcSize,
|
|
24
|
+
innerRadius: outerRingRadius,
|
|
25
|
+
radiusDelta: 0,
|
|
26
|
+
parent: wrapper,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
drawWedge({
|
|
30
|
+
startAngle: 0.75 - outerArcSize / 2,
|
|
31
|
+
angleDelta: outerArcSize,
|
|
32
|
+
innerRadius: outerRingRadius,
|
|
33
|
+
radiusDelta: 0,
|
|
34
|
+
parent: wrapper,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
onEvent(wtrStatusEmitter, "data", (/** @type {WtrStatus} */ data) => {
|
|
38
|
+
let nextClass
|
|
39
|
+
if (data.isOnline) {
|
|
40
|
+
const hasActiveClient = data.clients.some((client) => client.activeWatchCount > 0)
|
|
41
|
+
nextClass = hasActiveClient ? "active" : "online"
|
|
42
|
+
} else {
|
|
43
|
+
nextClass = "offline"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (nextClass !== currentClass) {
|
|
47
|
+
wrapper.classList.remove(currentClass)
|
|
48
|
+
wrapper.classList.add(nextClass)
|
|
49
|
+
currentClass = nextClass
|
|
50
|
+
}
|
|
51
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { exportDeps } from "../setup/dependencyManager.js"
|
|
2
|
+
import { EventEmitter } from "../setup/EventEmitter.js"
|
|
3
|
+
import "../util/constants.js"
|
|
4
|
+
|
|
5
|
+
const { dataWindowSize } = __WTR__.constants
|
|
6
|
+
const dataWindowInterval = 1000
|
|
7
|
+
|
|
8
|
+
let currentMaxValue = 0
|
|
9
|
+
const dataWindow = initDataWindow()
|
|
10
|
+
let currentDataWindowMessage = { ...processDataWindow(dataWindow), prevMaxValue: 0, currentValue: 0 }
|
|
11
|
+
|
|
12
|
+
const wtrActivityEmitter = new EventEmitter()
|
|
13
|
+
|
|
14
|
+
const wtrActivityDataWindowEmitter = new EventEmitter()
|
|
15
|
+
wtrActivityDataWindowEmitter.on = (event, handler) => {
|
|
16
|
+
if (event === "data") {
|
|
17
|
+
handler(currentDataWindowMessage)
|
|
18
|
+
}
|
|
19
|
+
EventEmitter.prototype.on.call(wtrActivityDataWindowEmitter, event, handler)
|
|
20
|
+
}
|
|
21
|
+
wtrActivityDataWindowEmitter.addEventListener = wtrActivityDataWindowEmitter.on
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {WtrActivityMessage} activityMessage
|
|
25
|
+
* @returns {void}
|
|
26
|
+
*/
|
|
27
|
+
export const emitActivity = (activityMessage) => {
|
|
28
|
+
wtrActivityEmitter.emit("data", activityMessage)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let currentActivityCount = 0
|
|
32
|
+
|
|
33
|
+
const onTick = async () => {
|
|
34
|
+
scheduleNextTick()
|
|
35
|
+
const prevEndTime = dataWindow.at(-1).time
|
|
36
|
+
const newTime = prevEndTime + dataWindowInterval
|
|
37
|
+
dataWindow.shift()
|
|
38
|
+
dataWindow.push({ time: newTime, value: currentActivityCount })
|
|
39
|
+
const { path, maxValue } = processDataWindow(dataWindow)
|
|
40
|
+
currentDataWindowMessage = {
|
|
41
|
+
path,
|
|
42
|
+
maxValue,
|
|
43
|
+
prevMaxValue: currentMaxValue,
|
|
44
|
+
currentValue: currentActivityCount,
|
|
45
|
+
}
|
|
46
|
+
currentMaxValue = maxValue
|
|
47
|
+
currentActivityCount = 0
|
|
48
|
+
wtrActivityDataWindowEmitter.emit("data", currentDataWindowMessage)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function initDataWindow() {
|
|
52
|
+
const now = new Date()
|
|
53
|
+
now.setMilliseconds(0)
|
|
54
|
+
const endTime = now.valueOf()
|
|
55
|
+
return Array.from({ length: dataWindowSize }, (_, i) => ({
|
|
56
|
+
time: endTime - (dataWindowSize - 1 - i) * dataWindowInterval,
|
|
57
|
+
value: 0,
|
|
58
|
+
}))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function processDataWindow(dataWindow) {
|
|
62
|
+
let maxValue = 0
|
|
63
|
+
const coords = dataWindow.map(({ value }, i) => {
|
|
64
|
+
if (value > maxValue) {
|
|
65
|
+
maxValue = value
|
|
66
|
+
}
|
|
67
|
+
return `${i},${value}`
|
|
68
|
+
})
|
|
69
|
+
const path = "M " + coords.join(" L ")
|
|
70
|
+
return { path, maxValue }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const scheduleNextTick = () => {
|
|
74
|
+
const nowMillis = new Date().getMilliseconds()
|
|
75
|
+
const millisUntilNextSecond = 1000 - nowMillis
|
|
76
|
+
setTimeout(onTick, millisUntilNextSecond)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
scheduleNextTick()
|
|
80
|
+
|
|
81
|
+
wtrActivityEmitter.on("data", () => {
|
|
82
|
+
currentActivityCount++
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
exportDeps({ wtrActivityEmitter, wtrActivityDataWindowEmitter })
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { exportDeps } from "../setup/dependencyManager.js"
|
|
2
|
+
import { EventEmitter } from "../setup/EventEmitter.js"
|
|
3
|
+
import "../util/constants.js"
|
|
4
|
+
|
|
5
|
+
// how long to wait after disconnect before clearing client side data
|
|
6
|
+
const CLEAR_TIMEOUT_LENGTH_MS = 3000
|
|
7
|
+
const { maxSessionWedges } = __WTR__.constants
|
|
8
|
+
|
|
9
|
+
/** @type {WtrStatus} */
|
|
10
|
+
const currentStatus = {
|
|
11
|
+
isOnline: false,
|
|
12
|
+
editors: [],
|
|
13
|
+
clients: [],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const wtrStatusEmitter = new EventEmitter()
|
|
17
|
+
wtrStatusEmitter.on = function (event, handler) {
|
|
18
|
+
EventEmitter.prototype.on.call(wtrStatusEmitter, event, handler)
|
|
19
|
+
// automatically push the data to the event handler when it initially subscribes
|
|
20
|
+
if (event === "data") {
|
|
21
|
+
handler(currentStatus)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
wtrStatusEmitter.addEventListener = wtrStatusEmitter.on
|
|
25
|
+
|
|
26
|
+
let clearDataTimeout
|
|
27
|
+
export const setIsOnline = (isOnline) => {
|
|
28
|
+
if (isOnline === currentStatus.isOnline) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
currentStatus.isOnline = isOnline
|
|
32
|
+
|
|
33
|
+
if (!isOnline) {
|
|
34
|
+
currentStatus.editors = currentStatus.editors.filter((s) => !s.isServer)
|
|
35
|
+
clearDataTimeout = setTimeout(() => {
|
|
36
|
+
setSessions([])
|
|
37
|
+
}, CLEAR_TIMEOUT_LENGTH_MS)
|
|
38
|
+
} else {
|
|
39
|
+
clearTimeout(clearDataTimeout)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
wtrStatusEmitter.emit("data", currentStatus)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {SessionStatus[]} sessions
|
|
47
|
+
* @returns {void}
|
|
48
|
+
*/
|
|
49
|
+
export const setSessions = (sessions) => {
|
|
50
|
+
const { editors, clients } = sessionDataTranform(sessions)
|
|
51
|
+
currentStatus.editors = editors
|
|
52
|
+
currentStatus.clients = clients
|
|
53
|
+
wtrStatusEmitter.emit("data", currentStatus)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {EditorStatus[] | ClientStatus[]} sessions
|
|
58
|
+
* @returns {EditorStatus[] | ClientStatus[]}
|
|
59
|
+
*/
|
|
60
|
+
const summarizeSessions = (sessions) => {
|
|
61
|
+
if (sessions.length <= maxSessionWedges) {
|
|
62
|
+
return sessions
|
|
63
|
+
}
|
|
64
|
+
const numOthers = sessions.length - maxSessionWedges + 1
|
|
65
|
+
const truncatedList = []
|
|
66
|
+
/** @type {EditorStatus & ClientStatus} */
|
|
67
|
+
const otherSessions = {
|
|
68
|
+
name: `${numOthers} others...`,
|
|
69
|
+
openCount: 0,
|
|
70
|
+
activeOpenCount: 0,
|
|
71
|
+
watchCount: 0,
|
|
72
|
+
activeWatchCount: 0,
|
|
73
|
+
isServer: false,
|
|
74
|
+
lsPid: null,
|
|
75
|
+
editorPid: null,
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < maxSessionWedges - 1; i++) {
|
|
78
|
+
truncatedList.push(sessions[i])
|
|
79
|
+
}
|
|
80
|
+
truncatedList.push(otherSessions)
|
|
81
|
+
|
|
82
|
+
for (let i = maxSessionWedges - 1; i < sessions.length; i++) {
|
|
83
|
+
const session = sessions[i]
|
|
84
|
+
otherSessions.openCount += session.openCount
|
|
85
|
+
otherSessions.activeOpenCount += session.activeOpenCount
|
|
86
|
+
otherSessions.watchCount += session.watchCount
|
|
87
|
+
otherSessions.activeWatchCount += session.activeWatchCount
|
|
88
|
+
otherSessions.lsPid = otherSessions.lsPid || session.lsPid
|
|
89
|
+
otherSessions.editorPid = otherSessions.editorPid || session.editorPid
|
|
90
|
+
otherSessions.isServer = otherSessions.isServer || session.isServer
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return truncatedList
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {SessionStatus[]} sessions
|
|
98
|
+
* @returns {editors: EditorStatus[], clients: ClientStatus[]}
|
|
99
|
+
*/
|
|
100
|
+
const sessionDataTranform = (sessions) => {
|
|
101
|
+
const editors = []
|
|
102
|
+
const clients = []
|
|
103
|
+
const allSessions = new Map()
|
|
104
|
+
|
|
105
|
+
sessions.forEach((session) => {
|
|
106
|
+
allSessions.set(session.id, session)
|
|
107
|
+
session.activeWatchCount = 0
|
|
108
|
+
session.activeOpenCount = 0
|
|
109
|
+
const isEditor = session.editorPid != null || session.lsPid != null
|
|
110
|
+
if (isEditor) {
|
|
111
|
+
editors.push(session)
|
|
112
|
+
} else {
|
|
113
|
+
clients.push(session)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
for (const session of allSessions.values()) {
|
|
118
|
+
const openFileLinks = Object.values(session.openFileLinks)
|
|
119
|
+
session.activeOpenCount = openFileLinks.length
|
|
120
|
+
openFileLinks.forEach((links) => {
|
|
121
|
+
links.forEach(({ clientId }) => {
|
|
122
|
+
const clientSession = allSessions.get(clientId)
|
|
123
|
+
if (!clientSession) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
clientSession.activeWatchCount++
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { editors: summarizeSessions(editors), clients: summarizeSessions(clients) }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
exportDeps({ wtrStatusEmitter })
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file path suffix used to match against open file locations.
|
|
3
|
+
* @typedef {string} EndsWith
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {number} SessionId
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{ clientId: SessionId, endsWith: EndsWith }} FileLink
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map of absolute file paths → arrays of metadata links.
|
|
16
|
+
* @typedef {Record<string, FileLink[]>} OpenFileLinks
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} SessionStatus
|
|
21
|
+
* Represents one session, which may be an editor or a client.
|
|
22
|
+
*
|
|
23
|
+
* @property {string} name
|
|
24
|
+
* @property {number} id
|
|
25
|
+
* @property {number} [editorPid] Optional — only present for editor sessions
|
|
26
|
+
* @property {number} [lsPid] Optional — language server PID only present for editor sessions
|
|
27
|
+
* @property {boolean} isServer
|
|
28
|
+
* @property {number} watchCount
|
|
29
|
+
* @property {number} openCount
|
|
30
|
+
* @property {OpenFileLinks} openFileLinks
|
|
31
|
+
* @property {number} activeWatchCount
|
|
32
|
+
* @property {number} activeOpenCount
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Editor sessions have all SessionStatus fields,
|
|
37
|
+
* but editorPid and lsPid become required,
|
|
38
|
+
* and activeWatchCount and activeOpenCount are defined
|
|
39
|
+
*
|
|
40
|
+
* @typedef {SessionStatus & {
|
|
41
|
+
* editorPid: number,
|
|
42
|
+
* lsPid: number,
|
|
43
|
+
* activeWatchCount: number,
|
|
44
|
+
* activeOpenCount: number
|
|
45
|
+
* }} EditorStatus
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Client sessions have all SessionStatus fields,
|
|
50
|
+
* but editorPid and lsPid are omitted,
|
|
51
|
+
* and activeWatchCount and activeOpenCount are defined
|
|
52
|
+
*
|
|
53
|
+
* @typedef {Omit<SessionStatus, "editorPid" | "lsPid"> & {
|
|
54
|
+
* activeWatchCount: number,
|
|
55
|
+
* activeOpenCount: number
|
|
56
|
+
* }} ClientStatus
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {{ isOnline: boolean, editors: EditorStatus[], clients: ClientStatus[] }} WtrStatus
|
|
61
|
+
*/
|
|
@@ -18,7 +18,7 @@ export class EventEmitter {
|
|
|
18
18
|
if (!eventSubscribers) {
|
|
19
19
|
return
|
|
20
20
|
}
|
|
21
|
-
if (
|
|
21
|
+
if (typeof listener !== "function") {
|
|
22
22
|
this.events.delete(event)
|
|
23
23
|
return
|
|
24
24
|
}
|
|
@@ -42,3 +42,7 @@ export class EventEmitter {
|
|
|
42
42
|
})
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
// make compatible with functions that handle DOM event targets
|
|
47
|
+
EventEmitter.prototype.addEventListener = EventEmitter.prototype.on
|
|
48
|
+
EventEmitter.prototype.removeEventListener = EventEmitter.prototype.removeListener
|
|
@@ -8,7 +8,6 @@ export class WebsocketClient {
|
|
|
8
8
|
this.host = host
|
|
9
9
|
this.protocol = protocol
|
|
10
10
|
this.emitter = new EventEmitter()
|
|
11
|
-
this.sessionMessages = []
|
|
12
11
|
this.socket = null
|
|
13
12
|
this.socketOpen = false
|
|
14
13
|
|
|
@@ -36,7 +35,6 @@ export class WebsocketClient {
|
|
|
36
35
|
console.log("websocket client connected")
|
|
37
36
|
this.socketOpen = true
|
|
38
37
|
this.emitter.emit("socket-open")
|
|
39
|
-
this.sessionMessages.forEach((msgStr) => this.socket.send(msgStr))
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
onSocketClose() {
|
|
@@ -72,11 +70,10 @@ export class WebsocketClient {
|
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
sendMessage(message) {
|
|
75
|
-
const messageStr = JSON.stringify(message)
|
|
76
|
-
this.sessionMessages.push(messageStr)
|
|
77
73
|
if (!this.socketOpen) {
|
|
78
74
|
return
|
|
79
75
|
}
|
|
76
|
+
const messageStr = JSON.stringify(message)
|
|
80
77
|
this.socket.send(messageStr)
|
|
81
78
|
}
|
|
82
79
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const dependencies = {}
|
|
2
|
+
// we export dependencies used in live js files to the __WTR__ object on the global scope
|
|
3
|
+
window.__WTR__ = dependencies
|
|
4
|
+
|
|
5
|
+
// export function to be used in other setup files
|
|
6
|
+
export const exportDeps = (exportObj) => Object.assign(dependencies, exportObj)
|
|
7
|
+
|
|
8
|
+
// export function to be used in live util files
|
|
9
|
+
exportDeps({ exportDeps })
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { exportDeps } from "./dependencyManager.js" // make sure the global __WTR__ object is initilized with the exportDeps function
|
|
2
|
+
|
|
3
|
+
let currentEvalOnChangeFiles = []
|
|
4
|
+
|
|
5
|
+
// functions used by setup
|
|
6
|
+
export const getEvalOnChangeFiles = () => currentEvalOnChangeFiles
|
|
7
|
+
|
|
8
|
+
export const clearEvalOnChangeFiles = () => {
|
|
9
|
+
currentEvalOnChangeFiles = []
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// function used by live js files
|
|
13
|
+
// define which functions need to be run every time that file changes
|
|
14
|
+
const evalOnChange = (files) => {
|
|
15
|
+
currentEvalOnChangeFiles = files
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
exportDeps({ evalOnChange })
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const allCleanupFunctions = new Map()
|
|
2
|
+
|
|
3
|
+
export const eventSubscriber = (key) => {
|
|
4
|
+
const previousCleanupFunctions = allCleanupFunctions.get(key)
|
|
5
|
+
previousCleanupFunctions?.forEach((f) => f())
|
|
6
|
+
|
|
7
|
+
const cleanupFunctions = []
|
|
8
|
+
allCleanupFunctions.set(key, cleanupFunctions)
|
|
9
|
+
|
|
10
|
+
return function onEvent(emitter, event, handler) {
|
|
11
|
+
if (typeof emitter.on === "function") {
|
|
12
|
+
cleanupFunctions.push(() => emitter.removeListener(event, handler))
|
|
13
|
+
emitter.on(event, handler)
|
|
14
|
+
} else if (typeof emitter.addEventListener === "function") {
|
|
15
|
+
cleanupFunctions.push(() => emitter.removeEventListener(event, handler))
|
|
16
|
+
emitter.addEventListener(event, handler)
|
|
17
|
+
} else {
|
|
18
|
+
throw new Error("target is not an event emitter")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { WebsocketClient } from "./setup/WebsocketClient.js"
|
|
2
|
+
import { getEvalOnChangeFiles, clearEvalOnChangeFiles } from "./setup/evalOnChange.js" // initialize dependencies on the global __WTR__ object
|
|
3
|
+
import { setIsOnline, setSessions } from "./data/wtrStatus.js"
|
|
4
|
+
import { eventSubscriber } from "./setup/eventSubscriber.js" // make sure the eventSubscriber function is available on the __WTR__ object
|
|
5
|
+
import { emitActivity } from "./data/wtrActivity.js"
|
|
6
|
+
|
|
7
|
+
const FILE_PREFIX = "websocket-text-relay/src/ui/"
|
|
8
|
+
const WS_PORT = 38378
|
|
9
|
+
const { hostname, protocol } = window.location
|
|
10
|
+
const wsProtocol = protocol === "http:" ? "ws" : "wss"
|
|
11
|
+
const CSS_FILE = "css/main.css"
|
|
12
|
+
const cssEndsWith = FILE_PREFIX + CSS_FILE
|
|
13
|
+
|
|
14
|
+
const createJsEndsWith = (jsFile) => FILE_PREFIX + jsFile
|
|
15
|
+
|
|
16
|
+
const jsFiles = [
|
|
17
|
+
"js/util/constants.js",
|
|
18
|
+
"js/util/drawing.js",
|
|
19
|
+
"js/components/headers.js",
|
|
20
|
+
"js/components/footerStatus.js",
|
|
21
|
+
"js/components/statusRing.js",
|
|
22
|
+
"js/components/drawSessionLabel.js",
|
|
23
|
+
"js/components/sessionWedges.js",
|
|
24
|
+
"js/components/activityTimeSeries.js",
|
|
25
|
+
"js/components/activityLabels.js",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const ws = new WebsocketClient({ port: WS_PORT, host: hostname, protocol: wsProtocol })
|
|
29
|
+
|
|
30
|
+
const cssElement = document.getElementById("main_style")
|
|
31
|
+
|
|
32
|
+
const handleCss = (contents) => {
|
|
33
|
+
cssElement.innerText = contents
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const handleJs = (contents, jsEndsWith) => {
|
|
37
|
+
// register and clean up event handlers on a per file basis
|
|
38
|
+
window.__WTR__.onEvent = eventSubscriber(jsEndsWith)
|
|
39
|
+
try {
|
|
40
|
+
eval(contents)
|
|
41
|
+
} catch (e) {
|
|
42
|
+
window._lastEvalError = e
|
|
43
|
+
console.log(e)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fetchAllFiles = async (fileNames) => {
|
|
48
|
+
const fetches = fileNames.map(async (fileName) => {
|
|
49
|
+
return fetch(fileName).then((r) => r.text())
|
|
50
|
+
})
|
|
51
|
+
return Promise.all(fetches)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const initFiles = async () => {
|
|
55
|
+
await fetch(CSS_FILE)
|
|
56
|
+
.then((r) => r.text())
|
|
57
|
+
.then(handleCss)
|
|
58
|
+
|
|
59
|
+
const jsResults = await fetchAllFiles(jsFiles)
|
|
60
|
+
await new Promise((resolve) => {
|
|
61
|
+
requestAnimationFrame(() => {
|
|
62
|
+
// wait for one frame to make sure css is applied first
|
|
63
|
+
jsResults.forEach((contents, i) => {
|
|
64
|
+
handleJs(contents, createJsEndsWith(jsFiles[i]))
|
|
65
|
+
})
|
|
66
|
+
resolve()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
await initFiles()
|
|
71
|
+
|
|
72
|
+
const subscribeWatchers = () => {
|
|
73
|
+
ws.sendMessage({ method: "watch-log-messages" })
|
|
74
|
+
ws.sendMessage({ method: "watch-wtr-status" })
|
|
75
|
+
ws.sendMessage({ method: "watch-wtr-activity" })
|
|
76
|
+
ws.sendMessage({ method: "init", name: "WTR Status" })
|
|
77
|
+
ws.sendMessage({ method: "watch-file", endsWith: cssEndsWith })
|
|
78
|
+
jsFiles.forEach((jsFile) => {
|
|
79
|
+
const jsEndsWith = FILE_PREFIX + jsFile
|
|
80
|
+
ws.sendMessage({ method: "watch-file", endsWith: jsEndsWith })
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (ws.socketOpen) {
|
|
85
|
+
setIsOnline(true)
|
|
86
|
+
subscribeWatchers()
|
|
87
|
+
}
|
|
88
|
+
ws.emitter.on("socket-open", () => {
|
|
89
|
+
setIsOnline(true)
|
|
90
|
+
subscribeWatchers()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
ws.emitter.on("socket-close", () => {
|
|
94
|
+
setIsOnline(false)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
let evalInProgress = false
|
|
98
|
+
let bufferedMessage = null
|
|
99
|
+
const handleJsMessage = async (message) => {
|
|
100
|
+
if (evalInProgress) {
|
|
101
|
+
bufferedMessage = message
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
evalInProgress = true
|
|
105
|
+
clearEvalOnChangeFiles()
|
|
106
|
+
handleJs(message.contents, message.endsWith)
|
|
107
|
+
const postEvalFiles = getEvalOnChangeFiles()
|
|
108
|
+
const jsResults = await fetchAllFiles(postEvalFiles)
|
|
109
|
+
jsResults.forEach((contents, i) => {
|
|
110
|
+
handleJs(contents, createJsEndsWith(postEvalFiles, i))
|
|
111
|
+
})
|
|
112
|
+
evalInProgress = false
|
|
113
|
+
if (bufferedMessage) {
|
|
114
|
+
const outstandingMessage = bufferedMessage
|
|
115
|
+
bufferedMessage = null
|
|
116
|
+
handleJsMessage(outstandingMessage)
|
|
117
|
+
}
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ws.emitter.on("message", async (message) => {
|
|
122
|
+
if (message.method === "watch-file" && message.endsWith === cssEndsWith) {
|
|
123
|
+
handleCss(message.contents)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (message.method === "watch-file" && message.endsWith.endsWith(".js")) {
|
|
128
|
+
await handleJsMessage(message)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (message.method === "watch-wtr-status") {
|
|
133
|
+
setSessions(message.data?.sessions ?? [])
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (message.method === "watch-wtr-activity") {
|
|
138
|
+
emitActivity(message.data)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
})
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
const { exportDeps } = window.__WTR__
|
|
1
|
+
const { exportDeps, evalOnChange } = window.__WTR__
|
|
2
|
+
|
|
3
|
+
evalOnChange(["js/components/statusRing.js", "js/components/sessionWedges.js"])
|
|
2
4
|
|
|
3
5
|
const constants = {
|
|
4
6
|
innerRingRadius: 0.33,
|
|
5
7
|
outerRingRadius: 0.6,
|
|
6
8
|
outerArcSize: 0.175,
|
|
9
|
+
dataWindowSize: 16,
|
|
10
|
+
maxSessionWedges: 5,
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
exportDeps({ constants })
|