mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-10 19:36:04 +08:00
feat(live-atc): local speech bridge discovery composable
This commit is contained in:
22
app/composables/useLocalSpeechBridge.test.ts
Normal file
22
app/composables/useLocalSpeechBridge.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
45
app/composables/useLocalSpeechBridge.ts
Normal file
45
app/composables/useLocalSpeechBridge.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user