feat(live-atc): local speech bridge discovery composable

This commit is contained in:
itsrubberduck
2026-07-22 20:27:32 +02:00
parent 990e6059eb
commit 0a51fe68ab
5 changed files with 756 additions and 1 deletions

View File

@@ -0,0 +1,22 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { probeLocalSpeech } from './useLocalSpeechBridge'
describe('probeLocalSpeech', () => {
beforeEach(() => vi.restoreAllMocks())
it('returns the base url of the first ready port', async () => {
vi.stubGlobal('fetch', vi.fn(async (url: string) => {
if (url.includes('8765')) return { ok: true, json: async () => ({ ok: true, ready: false }) } as any
if (url.includes('8766')) return { ok: true, json: async () => ({ ok: true, ready: true }) } as any
throw new Error('refused')
}))
const base = await probeLocalSpeech([8765, 8766, 8767])
expect(base).toBe('http://127.0.0.1:8766')
})
it('returns null when nothing is ready', async () => {
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('refused') }))
const base = await probeLocalSpeech([8765, 8766])
expect(base).toBeNull()
})
})

View File

@@ -0,0 +1,45 @@
import { ref } from 'vue'
const PORTS = [8765, 8766, 8767, 8768, 8769, 8770]
const HEALTH_TIMEOUT_MS = 1200
const RECHECK_MS = 15_000
export async function probeLocalSpeech(ports: number[] = PORTS): Promise<string | null> {
for (const port of ports) {
const base = `http://127.0.0.1:${port}`
try {
const ctrl = new AbortController()
const timeout = setTimeout(() => ctrl.abort(), HEALTH_TIMEOUT_MS)
const response = await fetch(`${base}/health`, { signal: ctrl.signal })
clearTimeout(timeout)
if (!response.ok) continue
const body = await response.json()
if (body?.ok && body?.ready) return base
} catch {
// Port closed/refused: probe the next one.
}
}
return null
}
const localBase = ref<string | null>(null)
let started = false
export function useLocalSpeechBridge() {
const refresh = async () => {
localBase.value = await probeLocalSpeech()
}
if (typeof window !== 'undefined' && !started) {
started = true
void refresh()
setInterval(() => void refresh(), RECHECK_MS)
}
return {
localBase,
/** Absolute endpoint URL, or null while no ready local Bridge is found. */
localUrl: (path: string) => (localBase.value ? localBase.value + path : null),
refresh,
}
}