fix: time out stt worker startup (#3848)
This commit is contained in:
parent
f5d00526e8
commit
c4e931e806
|
|
@ -98,7 +98,7 @@ vi.mock('./model-catalog', () => ({
|
|||
})
|
||||
}))
|
||||
|
||||
import { IDLE_WORKER_TEARDOWN_MS, SttService } from './stt-service'
|
||||
import { IDLE_WORKER_TEARDOWN_MS, START_DICTATION_TIMEOUT_MS, SttService } from './stt-service'
|
||||
|
||||
describe('SttService', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -205,6 +205,36 @@ describe('SttService', () => {
|
|||
)
|
||||
})
|
||||
|
||||
it('times out startup when the worker never reports ready', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const service = new SttService({
|
||||
getModelState: vi.fn().mockResolvedValue({ id: 'model-a', status: 'ready' }),
|
||||
getModelDir: vi.fn().mockReturnValue('/tmp/model-a')
|
||||
} as never)
|
||||
|
||||
MockWorker.emitReadyOnInit = false
|
||||
const startPromise = service.startDictation('model-a', vi.fn(), undefined, 'desktop').then(
|
||||
() => 'resolved',
|
||||
(error) => (error instanceof Error ? error.message : String(error))
|
||||
)
|
||||
await Promise.resolve()
|
||||
const worker = getLastWorker()
|
||||
expect(worker).toBeDefined()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(START_DICTATION_TIMEOUT_MS)
|
||||
const outcome = await Promise.race([startPromise, Promise.resolve('pending')])
|
||||
|
||||
expect(outcome).toBe('Speech worker timed out while starting.')
|
||||
expect(worker!.terminated).toBe(true)
|
||||
expect(worker!.listenerCount('message')).toBe(0)
|
||||
expect(worker!.listenerCount('error')).toBe(0)
|
||||
expect(worker!.listenerCount('exit')).toBe(0)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not treat internal warm-worker replacement as startup cancellation', async () => {
|
||||
const service = new SttService({
|
||||
getModelState: vi.fn().mockResolvedValue({ id: 'model-a', status: 'ready' }),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { app } from 'electron'
|
|||
import { getCatalogModel } from './model-catalog'
|
||||
import type { ModelManager } from './model-manager'
|
||||
|
||||
export const START_DICTATION_TIMEOUT_MS = 60_000
|
||||
const STOP_DICTATION_TIMEOUT_MS = 60_000
|
||||
export const IDLE_WORKER_TEARDOWN_MS = 60 * 60 * 1000
|
||||
|
||||
|
|
@ -112,11 +113,24 @@ export class SttService {
|
|||
|
||||
const readyPromise = new Promise<void>((resolve, reject) => {
|
||||
let settled = false
|
||||
let startupTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
const cleanup = () => {
|
||||
if (startupTimeout) {
|
||||
clearTimeout(startupTimeout)
|
||||
startupTimeout = null
|
||||
}
|
||||
worker.off('message', onReadyOrError)
|
||||
worker.off('error', onStartupError)
|
||||
worker.off('exit', onStartupExit)
|
||||
}
|
||||
const failStartup = (error: Error): void => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
cleanup()
|
||||
reject(error)
|
||||
}
|
||||
const onReadyOrError = (msg: { type: string; text?: string; error?: string }) => {
|
||||
if (settled) {
|
||||
return
|
||||
|
|
@ -126,30 +140,24 @@ export class SttService {
|
|||
cleanup()
|
||||
resolve()
|
||||
} else if (msg.type === 'error') {
|
||||
settled = true
|
||||
cleanup()
|
||||
reject(new Error(msg.error ?? 'Speech worker failed to initialize'))
|
||||
failStartup(new Error(msg.error ?? 'Speech worker failed to initialize'))
|
||||
}
|
||||
}
|
||||
const onStartupError = (err: Error) => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
cleanup()
|
||||
reject(err)
|
||||
failStartup(err)
|
||||
}
|
||||
const onStartupExit = (code: number) => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
cleanup()
|
||||
reject(new Error(`Speech worker exited before ready: ${code}`))
|
||||
failStartup(new Error(`Speech worker exited before ready: ${code}`))
|
||||
}
|
||||
worker.on('message', onReadyOrError)
|
||||
worker.on('error', onStartupError)
|
||||
worker.on('exit', onStartupExit)
|
||||
// Why: a native STT worker can wedge while loading model bindings without
|
||||
// emitting ready/error/exit; startup must leave the UI's Starting state.
|
||||
startupTimeout = setTimeout(() => {
|
||||
failStartup(new Error('Speech worker timed out while starting.'))
|
||||
}, START_DICTATION_TIMEOUT_MS)
|
||||
startupTimeout.unref?.()
|
||||
})
|
||||
|
||||
worker.on('message', (msg: SttEvent) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue