Compare commits
2
Commits
5af879bc2f
...
7ad14449ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad14449ce | ||
|
|
ed14019052 |
No files matched your search
+13
-5
@@ -38,15 +38,17 @@ const busy = ref(false)
|
|||||||
const connected = ref(false)
|
const connected = ref(false)
|
||||||
const socketError = ref('')
|
const socketError = ref('')
|
||||||
const socketFailures = ref(0)
|
const socketFailures = ref(0)
|
||||||
// Some mobile browsers (notably iOS Safari with an auth-gated page) abort the
|
const pollingFallback = ref(false)
|
||||||
// wss:// upgrade even when the server accepts it. After a few failures we stop
|
// Some mobile browsers (notably iOS Safari with an auth-gated page) abort or
|
||||||
// retrying, rely on the REST poll, and present it as a normal live state rather
|
// silently hang the wss:// upgrade even when the server accepts it. We fall
|
||||||
// than a degraded "reconnecting".
|
// back to the REST poll after a few failures, or after a short grace period if
|
||||||
const usePolling = computed(() => socketFailures.value >= 3)
|
// the socket never opens at all, and present it as a normal live state.
|
||||||
|
const usePolling = computed(() => pollingFallback.value || socketFailures.value >= 3)
|
||||||
|
|
||||||
let socket
|
let socket
|
||||||
let pollTimer
|
let pollTimer
|
||||||
let statusTimer
|
let statusTimer
|
||||||
|
let graceTimer
|
||||||
|
|
||||||
async function refreshAll() {
|
async function refreshAll() {
|
||||||
const [p, o, e, q] = await Promise.all([
|
const [p, o, e, q] = await Promise.all([
|
||||||
@@ -151,6 +153,11 @@ onMounted(async () => {
|
|||||||
else socketError.value = `socket closed (code ${code})`
|
else socketError.value = `socket closed (code ${code})`
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
// If the socket hasn't opened shortly after load, commit to polling so the
|
||||||
|
// badge never hangs on "connecting".
|
||||||
|
graceTimer = setTimeout(() => {
|
||||||
|
if (!connected.value) pollingFallback.value = true
|
||||||
|
}, 6000)
|
||||||
// Poll fast enough that the UI feels live even when it is the only channel.
|
// Poll fast enough that the UI feels live even when it is the only channel.
|
||||||
pollTimer = setInterval(refreshAll, 3000)
|
pollTimer = setInterval(refreshAll, 3000)
|
||||||
statusTimer = setInterval(() => api.getStatus().then((s) => (status.value = s)), 3000)
|
statusTimer = setInterval(() => api.getStatus().then((s) => (status.value = s)), 3000)
|
||||||
@@ -160,6 +167,7 @@ onUnmounted(() => {
|
|||||||
socket?.close()
|
socket?.close()
|
||||||
clearInterval(pollTimer)
|
clearInterval(pollTimer)
|
||||||
clearInterval(statusTimer)
|
clearInterval(statusTimer)
|
||||||
|
clearTimeout(graceTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+30
-6
@@ -45,23 +45,47 @@ export function openSocket({ onMessage, onOpen, onClose, onError }) {
|
|||||||
|
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
if (closed) return
|
if (closed) return
|
||||||
|
let opened = false
|
||||||
|
let reported = false
|
||||||
socket = new WebSocket(url)
|
socket = new WebSocket(url)
|
||||||
|
|
||||||
|
// Report at most one failure per attempt (Safari often fires error *and*
|
||||||
|
// close for the same bad handshake).
|
||||||
|
const reportFailure = (code, detail) => {
|
||||||
|
if (reported) return
|
||||||
|
reported = true
|
||||||
|
onError?.(code, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some mobile browsers leave a blocked upgrade hanging without firing
|
||||||
|
// onclose/onerror. Treat a slow handshake as a failure so the caller can
|
||||||
|
// fall back to polling instead of waiting forever.
|
||||||
|
const openTimeout = setTimeout(() => {
|
||||||
|
if (!opened) {
|
||||||
|
reportFailure(0, 'handshake timed out')
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
|
opened = true
|
||||||
|
clearTimeout(openTimeout)
|
||||||
attempt = 0
|
attempt = 0
|
||||||
onOpen?.()
|
onOpen?.()
|
||||||
}
|
}
|
||||||
socket.onmessage = (event) => onMessage?.(JSON.parse(event.data))
|
socket.onmessage = (event) => onMessage?.(JSON.parse(event.data))
|
||||||
socket.onclose = (event) => {
|
socket.onclose = (event) => {
|
||||||
|
clearTimeout(openTimeout)
|
||||||
onClose?.()
|
onClose?.()
|
||||||
// 1008 = policy violation (token rejected); 1006 = abnormal close, which
|
if (!opened) {
|
||||||
// is what an auth/proxy rejection looks like to the browser.
|
// 1008 = policy violation (token rejected); 1006 = abnormal close.
|
||||||
onError?.(event.code, event.reason)
|
reportFailure(event.code, event.reason)
|
||||||
|
}
|
||||||
schedule()
|
schedule()
|
||||||
}
|
}
|
||||||
socket.onerror = (event) => {
|
socket.onerror = (event) => {
|
||||||
// The browser hides details here; surface what it does give us.
|
clearTimeout(openTimeout)
|
||||||
const detail = event?.message || 'connection error'
|
if (!opened) reportFailure(0, event?.message || 'connection error')
|
||||||
onError?.(0, detail)
|
|
||||||
socket.close()
|
socket.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user