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.
Files changed (37) hide show
  1. package/changelog.md +4 -0
  2. package/docs/code-structure.md +15 -11
  3. package/eslint.config.js +5 -1
  4. package/package.json +9 -9
  5. package/src/ui/css/main.css +22 -78
  6. package/src/ui/favicon.png +0 -0
  7. package/src/ui/index.html +19 -14
  8. package/src/ui/js/components/activityLabels.js +41 -0
  9. package/src/ui/js/components/activityTimeSeries.js +58 -0
  10. package/src/ui/js/components/drawSessionLabel.js +117 -0
  11. package/src/ui/js/components/footerStatus.js +37 -0
  12. package/src/ui/js/components/headers.js +100 -0
  13. package/src/ui/js/components/sessionWedges.js +96 -0
  14. package/src/ui/js/components/statusRing.js +51 -0
  15. package/src/ui/js/data/wtrActivity.js +85 -0
  16. package/src/ui/js/data/wtrActivity.types.js +3 -0
  17. package/src/ui/js/data/wtrStatus.js +134 -0
  18. package/src/ui/js/data/wtrStatus.types.js +61 -0
  19. package/src/ui/js/{util → setup}/EventEmitter.js +5 -1
  20. package/src/ui/js/{util → setup}/WebsocketClient.js +1 -4
  21. package/src/ui/js/setup/dependencyManager.js +9 -0
  22. package/src/ui/js/setup/evalOnChange.js +18 -0
  23. package/src/ui/js/setup/eventSubscriber.js +21 -0
  24. package/src/ui/js/setup.js +141 -0
  25. package/src/ui/js/util/constants.js +5 -1
  26. package/src/ui/js/util/drawing.js +26 -76
  27. package/src/websocket-interface/httpServer.js +1 -1
  28. package/src/ui/js/components/ActivityTimeseriesGraph.js +0 -194
  29. package/src/ui/js/components/HeaderSummary.js +0 -22
  30. package/src/ui/js/components/ServerStatus.js +0 -43
  31. package/src/ui/js/components/SessionLabels.js +0 -319
  32. package/src/ui/js/components/SessionWedges.js +0 -127
  33. package/src/ui/js/components/StatusRing.js +0 -54
  34. package/src/ui/js/components/grids.js +0 -36
  35. package/src/ui/js/index.js +0 -121
  36. package/src/ui/js/main.js +0 -128
  37. 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,3 @@
1
+ /**
2
+ * @typedef {{action: "relay", relayer: SessionId, watchers: SessionId[]}} WtrActivityMessage
3
+ */
@@ -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 (!listener) {
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 })