websocket.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { buttonResults } from '../resources/vue/stores/buttonResults.js'
  2. import { rateLimits } from '../resources/vue/stores/rateLimits.js'
  3. import { connectionState } from '../resources/vue/stores/connectionState.js'
  4. const RECONNECT_DELAYS_MS = [200, 1000, 2000, 4000, 8000, 16000, 32000]
  5. const BANNER_DELAY_MS = 2000
  6. let reconnectAttempt = 0
  7. let reconnectTimer = null
  8. let listenersInitialized = false
  9. let eventStreamGeneration = 0
  10. let eventStreamAbortController = null
  11. function shouldConnectEventStream () {
  12. return window.initResponse && !window.initResponse.loginRequired
  13. }
  14. export function stopEventStream () {
  15. eventStreamGeneration++
  16. if (eventStreamAbortController != null) {
  17. eventStreamAbortController.abort()
  18. eventStreamAbortController = null
  19. }
  20. if (reconnectTimer != null) {
  21. clearTimeout(reconnectTimer)
  22. reconnectTimer = null
  23. }
  24. reconnectAttempt = 0
  25. connectionState.connected = false
  26. connectionState.reconnecting = false
  27. connectionState.scheduledReconnectDelayMs = 0
  28. connectionState.nextReconnectAt = null
  29. connectionState.showDisconnectedBanner = false
  30. window.websocketAvailable = false
  31. }
  32. export function connectEventStreamIfNeeded () {
  33. if (!shouldConnectEventStream()) {
  34. stopEventStream()
  35. return
  36. }
  37. if (connectionState.connected || reconnectTimer != null) {
  38. return
  39. }
  40. reconnectWebsocket()
  41. }
  42. export function initWebsocket () {
  43. if (!listenersInitialized) {
  44. window.addEventListener('EventOutputChunk', onOutputChunk)
  45. window.addEventListener('EventExecutionStarted', onExecutionChanged)
  46. window.addEventListener('EventExecutionFinished', onExecutionChanged)
  47. window.addEventListener('pagehide', stopEventStream)
  48. listenersInitialized = true
  49. }
  50. connectEventStreamIfNeeded()
  51. }
  52. window.websocketAvailable = false
  53. export function requestReconnectNow () {
  54. if (!shouldConnectEventStream()) {
  55. return
  56. }
  57. if (connectionState.connected) {
  58. return
  59. }
  60. if (reconnectTimer != null) {
  61. clearTimeout(reconnectTimer)
  62. reconnectTimer = null
  63. }
  64. reconnectAttempt = 0
  65. scheduleReconnect(RECONNECT_DELAYS_MS[0])
  66. }
  67. function scheduleReconnect (delayMs) {
  68. if (reconnectTimer != null) {
  69. clearTimeout(reconnectTimer)
  70. reconnectTimer = null
  71. }
  72. connectionState.scheduledReconnectDelayMs = delayMs
  73. connectionState.nextReconnectAt = delayMs > 0 ? Date.now() + delayMs : null
  74. updateBannerVisibility()
  75. reconnectTimer = setTimeout(() => {
  76. reconnectTimer = null
  77. reconnectWebsocket()
  78. }, delayMs)
  79. }
  80. function updateBannerVisibility () {
  81. if (connectionState.connected) {
  82. connectionState.showDisconnectedBanner = false
  83. return
  84. }
  85. connectionState.showDisconnectedBanner = connectionState.scheduledReconnectDelayMs >= BANNER_DELAY_MS
  86. }
  87. async function reconnectWebsocket () {
  88. if (!shouldConnectEventStream()) {
  89. return
  90. }
  91. if (connectionState.connected) {
  92. return
  93. }
  94. const streamGeneration = ++eventStreamGeneration
  95. if (eventStreamAbortController != null) {
  96. eventStreamAbortController.abort()
  97. }
  98. eventStreamAbortController = new AbortController()
  99. connectionState.reconnecting = true
  100. connectionState.connected = false
  101. if (connectionState.disconnectedAt == null) {
  102. connectionState.disconnectedAt = Date.now()
  103. }
  104. connectionState.nextReconnectAt = null
  105. connectionState.scheduledReconnectDelayMs = 0
  106. try {
  107. window.websocketAvailable = true
  108. const stream = window.client.eventStream({}, { signal: eventStreamAbortController.signal })
  109. connectionState.connected = true
  110. connectionState.reconnecting = false
  111. connectionState.disconnectedAt = null
  112. connectionState.nextReconnectAt = null
  113. connectionState.scheduledReconnectDelayMs = 0
  114. connectionState.showDisconnectedBanner = false
  115. for await (const e of stream) {
  116. if (streamGeneration !== eventStreamGeneration) {
  117. return
  118. }
  119. if (reconnectAttempt !== 0) {
  120. reconnectAttempt = 0
  121. }
  122. handleEvent(e)
  123. }
  124. } catch (err) {
  125. if (streamGeneration !== eventStreamGeneration) {
  126. return
  127. }
  128. console.error('Websocket connection failed: ', err)
  129. }
  130. if (streamGeneration !== eventStreamGeneration) {
  131. return
  132. }
  133. window.websocketAvailable = false
  134. connectionState.connected = false
  135. connectionState.reconnecting = false
  136. connectionState.disconnectedAt = connectionState.disconnectedAt ?? Date.now()
  137. const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)]
  138. reconnectAttempt++
  139. console.log('Reconnecting websocket in ' + delay + 'ms...')
  140. if (!shouldConnectEventStream()) {
  141. return
  142. }
  143. scheduleReconnect(delay)
  144. }
  145. async function refreshInitAfterConfigChange () {
  146. if (!window.client) {
  147. return
  148. }
  149. try {
  150. window.initResponse = await window.client.init({})
  151. if (typeof window.updateHeaderFromInit === 'function') {
  152. window.updateHeaderFromInit()
  153. }
  154. } catch (err) {
  155. console.error('Failed to refresh config from server after EventConfigChanged:', err)
  156. }
  157. }
  158. async function handleConfigChangedEvent (j) {
  159. await refreshInitAfterConfigChange()
  160. window.dispatchEvent(j)
  161. }
  162. const eventCaseToTypeName = {
  163. entityChanged: 'EventEntityChanged',
  164. configChanged: 'EventConfigChanged',
  165. executionFinished: 'EventExecutionFinished',
  166. executionStarted: 'EventExecutionStarted',
  167. outputChunk: 'EventOutputChunk',
  168. heartbeat: 'EventHeartbeat'
  169. }
  170. function handleEvent (msg) {
  171. const eventCase = msg?.event?.case
  172. const eventValue = msg?.event?.value
  173. const typeName = eventCaseToTypeName[eventCase]
  174. if (!typeName || !eventValue) {
  175. console.warn('Skipping websocket event with no payload:', msg)
  176. return
  177. }
  178. const j = new Event(typeName)
  179. j.payload = eventValue
  180. switch (typeName) {
  181. case 'EventConfigChanged':
  182. handleConfigChangedEvent(j).catch((err) => {
  183. console.error('EventConfigChanged handler failed:', err)
  184. })
  185. break
  186. case 'EventHeartbeat':
  187. break
  188. case 'EventOutputChunk':
  189. case 'EventEntityChanged':
  190. window.dispatchEvent(j)
  191. break
  192. case 'EventExecutionFinished':
  193. case 'EventExecutionStarted':
  194. window.dispatchEvent(j)
  195. break
  196. default:
  197. console.warn('Unhandled websocket message type from server: ', typeName)
  198. window.showBigError('ws-unhandled-message', 'handling websocket message', 'Unhandled websocket message type from server: ' + typeName, true)
  199. }
  200. }
  201. function onOutputChunk (evt) {
  202. const chunk = evt.payload
  203. if (window.terminal) {
  204. if (chunk.executionTrackingId === window.terminal.executionTrackingId) {
  205. window.terminal.write(chunk.output)
  206. }
  207. }
  208. }
  209. export function applyExecutionLogEntry (logEntry) {
  210. if (!logEntry?.executionTrackingId) {
  211. return
  212. }
  213. buttonResults[logEntry.executionTrackingId] = logEntry
  214. if (logEntry.datetimeRateLimitExpires && logEntry.bindingId) {
  215. const date = new Date(logEntry.datetimeRateLimitExpires.replace(' ', 'T') + 'Z')
  216. rateLimits[logEntry.bindingId] = date.getTime() / 1000
  217. } else if (logEntry.bindingId) {
  218. rateLimits[logEntry.bindingId] = 0
  219. }
  220. }
  221. function onExecutionChanged (evt) {
  222. applyExecutionLogEntry(evt.payload.logEntry)
  223. }