mirror of
https://github.com/OliveTin/OliveTin
synced 2026-09-23 02:05:42 +00:00
291 lines
7.7 KiB
JavaScript
291 lines
7.7 KiB
JavaScript
import { buttonResults } from '../resources/vue/stores/buttonResults.js'
|
|
import { rateLimits } from '../resources/vue/stores/rateLimits.js'
|
|
import { connectionState } from '../resources/vue/stores/connectionState.js'
|
|
import {
|
|
applyExecutionFinishedBindingState,
|
|
applyExecutionStartedBindingState
|
|
} from '../resources/vue/stores/bindingExecutionState.js'
|
|
import { cloneLogEntry } from '../resources/vue/utils/executionLogEvents.js'
|
|
|
|
const RECONNECT_DELAYS_MS = [200, 1000, 2000, 4000, 8000, 16000, 32000]
|
|
const BANNER_DELAY_MS = 2000
|
|
|
|
let reconnectAttempt = 0
|
|
let reconnectTimer = null
|
|
let listenersInitialized = false
|
|
let eventStreamGeneration = 0
|
|
let eventStreamAbortController = null
|
|
|
|
function shouldConnectEventStream () {
|
|
return window.initResponse && !window.initResponse.loginRequired
|
|
}
|
|
|
|
export function stopEventStream () {
|
|
eventStreamGeneration++
|
|
if (eventStreamAbortController != null) {
|
|
eventStreamAbortController.abort()
|
|
eventStreamAbortController = null
|
|
}
|
|
|
|
if (reconnectTimer != null) {
|
|
clearTimeout(reconnectTimer)
|
|
reconnectTimer = null
|
|
}
|
|
|
|
reconnectAttempt = 0
|
|
connectionState.connected = false
|
|
connectionState.reconnecting = false
|
|
connectionState.scheduledReconnectDelayMs = 0
|
|
connectionState.nextReconnectAt = null
|
|
connectionState.showDisconnectedBanner = false
|
|
window.websocketAvailable = false
|
|
}
|
|
|
|
export function connectEventStreamIfNeeded () {
|
|
if (!shouldConnectEventStream()) {
|
|
stopEventStream()
|
|
return
|
|
}
|
|
|
|
if (connectionState.connected || reconnectTimer != null) {
|
|
return
|
|
}
|
|
|
|
reconnectWebsocket()
|
|
}
|
|
|
|
export function initWebsocket () {
|
|
if (!listenersInitialized) {
|
|
window.addEventListener('EventOutputChunk', onOutputChunk)
|
|
window.addEventListener('EventExecutionStarted', onExecutionStarted)
|
|
window.addEventListener('EventExecutionFinished', onExecutionFinished)
|
|
window.addEventListener('pagehide', stopEventStream)
|
|
listenersInitialized = true
|
|
}
|
|
|
|
connectEventStreamIfNeeded()
|
|
}
|
|
|
|
window.websocketAvailable = false
|
|
|
|
export function requestReconnectNow () {
|
|
if (!shouldConnectEventStream()) {
|
|
return
|
|
}
|
|
|
|
if (connectionState.connected) {
|
|
return
|
|
}
|
|
|
|
if (reconnectTimer != null) {
|
|
clearTimeout(reconnectTimer)
|
|
reconnectTimer = null
|
|
}
|
|
|
|
reconnectAttempt = 0
|
|
scheduleReconnect(RECONNECT_DELAYS_MS[0])
|
|
}
|
|
|
|
function scheduleReconnect (delayMs) {
|
|
if (reconnectTimer != null) {
|
|
clearTimeout(reconnectTimer)
|
|
reconnectTimer = null
|
|
}
|
|
|
|
connectionState.scheduledReconnectDelayMs = delayMs
|
|
connectionState.nextReconnectAt = delayMs > 0 ? Date.now() + delayMs : null
|
|
updateBannerVisibility()
|
|
reconnectTimer = setTimeout(() => {
|
|
reconnectTimer = null
|
|
reconnectWebsocket()
|
|
}, delayMs)
|
|
}
|
|
|
|
function updateBannerVisibility () {
|
|
if (connectionState.connected) {
|
|
connectionState.showDisconnectedBanner = false
|
|
return
|
|
}
|
|
|
|
connectionState.showDisconnectedBanner = connectionState.scheduledReconnectDelayMs >= BANNER_DELAY_MS
|
|
}
|
|
|
|
async function reconnectWebsocket () {
|
|
if (!shouldConnectEventStream()) {
|
|
return
|
|
}
|
|
|
|
if (connectionState.connected) {
|
|
return
|
|
}
|
|
|
|
const streamGeneration = ++eventStreamGeneration
|
|
if (eventStreamAbortController != null) {
|
|
eventStreamAbortController.abort()
|
|
}
|
|
eventStreamAbortController = new AbortController()
|
|
|
|
connectionState.reconnecting = true
|
|
connectionState.connected = false
|
|
if (connectionState.disconnectedAt == null) {
|
|
connectionState.disconnectedAt = Date.now()
|
|
}
|
|
connectionState.nextReconnectAt = null
|
|
connectionState.scheduledReconnectDelayMs = 0
|
|
|
|
try {
|
|
window.websocketAvailable = true
|
|
const stream = window.client.eventStream({}, { signal: eventStreamAbortController.signal })
|
|
connectionState.connected = true
|
|
connectionState.reconnecting = false
|
|
connectionState.disconnectedAt = null
|
|
connectionState.nextReconnectAt = null
|
|
connectionState.scheduledReconnectDelayMs = 0
|
|
connectionState.showDisconnectedBanner = false
|
|
for await (const e of stream) {
|
|
if (streamGeneration !== eventStreamGeneration) {
|
|
return
|
|
}
|
|
if (reconnectAttempt !== 0) {
|
|
reconnectAttempt = 0
|
|
}
|
|
handleEvent(e)
|
|
}
|
|
} catch (err) {
|
|
if (streamGeneration !== eventStreamGeneration) {
|
|
return
|
|
}
|
|
console.error('Websocket connection failed: ', err)
|
|
}
|
|
|
|
if (streamGeneration !== eventStreamGeneration) {
|
|
return
|
|
}
|
|
|
|
window.websocketAvailable = false
|
|
connectionState.connected = false
|
|
connectionState.reconnecting = false
|
|
connectionState.disconnectedAt = connectionState.disconnectedAt ?? Date.now()
|
|
|
|
const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)]
|
|
reconnectAttempt++
|
|
console.log('Reconnecting websocket in ' + delay + 'ms...')
|
|
|
|
if (!shouldConnectEventStream()) {
|
|
return
|
|
}
|
|
|
|
scheduleReconnect(delay)
|
|
}
|
|
|
|
async function refreshInitAfterConfigChange () {
|
|
if (!window.client) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
window.initResponse = await window.client.init({})
|
|
|
|
if (typeof window.updateHeaderFromInit === 'function') {
|
|
window.updateHeaderFromInit()
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to refresh config from server after EventConfigChanged:', err)
|
|
}
|
|
}
|
|
|
|
async function handleConfigChangedEvent (j) {
|
|
await refreshInitAfterConfigChange()
|
|
window.dispatchEvent(j)
|
|
}
|
|
|
|
async function handleEntityChangedEvent (j) {
|
|
await refreshInitAfterConfigChange()
|
|
window.dispatchEvent(j)
|
|
}
|
|
|
|
const eventCaseToTypeName = {
|
|
entityChanged: 'EventEntityChanged',
|
|
configChanged: 'EventConfigChanged',
|
|
executionFinished: 'EventExecutionFinished',
|
|
executionStarted: 'EventExecutionStarted',
|
|
outputChunk: 'EventOutputChunk',
|
|
heartbeat: 'EventHeartbeat'
|
|
}
|
|
|
|
function handleEvent (msg) {
|
|
const eventCase = msg?.event?.case
|
|
const eventValue = msg?.event?.value
|
|
const typeName = eventCaseToTypeName[eventCase]
|
|
|
|
if (!typeName || !eventValue) {
|
|
console.warn('Skipping websocket event with no payload:', msg)
|
|
return
|
|
}
|
|
|
|
const j = new Event(typeName)
|
|
j.payload = eventValue
|
|
|
|
switch (typeName) {
|
|
case 'EventConfigChanged':
|
|
handleConfigChangedEvent(j).catch((err) => {
|
|
console.error('EventConfigChanged handler failed:', err)
|
|
})
|
|
break
|
|
case 'EventHeartbeat':
|
|
break
|
|
case 'EventOutputChunk':
|
|
window.dispatchEvent(j)
|
|
break
|
|
case 'EventEntityChanged':
|
|
handleEntityChangedEvent(j).catch((err) => {
|
|
console.error('EventEntityChanged handler failed:', err)
|
|
})
|
|
break
|
|
case 'EventExecutionFinished':
|
|
case 'EventExecutionStarted':
|
|
window.dispatchEvent(j)
|
|
break
|
|
default:
|
|
console.warn('Unhandled websocket message type from server: ', typeName)
|
|
|
|
window.showBigError('ws-unhandled-message', 'handling websocket message', 'Unhandled websocket message type from server: ' + typeName, true)
|
|
}
|
|
}
|
|
|
|
function onOutputChunk (evt) {
|
|
const chunk = evt.payload
|
|
|
|
if (window.terminal) {
|
|
if (chunk.executionTrackingId === window.terminal.executionTrackingId) {
|
|
window.terminal.write(chunk.output)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function applyExecutionLogEntry (logEntry) {
|
|
const entry = cloneLogEntry(logEntry)
|
|
if (!entry?.executionTrackingId) {
|
|
return
|
|
}
|
|
|
|
buttonResults[entry.executionTrackingId] = entry
|
|
|
|
if (entry.datetimeRateLimitExpires && entry.bindingId) {
|
|
const date = new Date(entry.datetimeRateLimitExpires.replace(' ', 'T') + 'Z')
|
|
rateLimits[entry.bindingId] = date.getTime() / 1000
|
|
} else if (entry.bindingId) {
|
|
rateLimits[entry.bindingId] = 0
|
|
}
|
|
}
|
|
|
|
function onExecutionStarted (evt) {
|
|
applyExecutionLogEntry(evt.payload.logEntry)
|
|
applyExecutionStartedBindingState(evt.payload.logEntry)
|
|
}
|
|
|
|
function onExecutionFinished (evt) {
|
|
applyExecutionLogEntry(evt.payload.logEntry)
|
|
applyExecutionFinishedBindingState(evt.payload.logEntry)
|
|
}
|