fix(skill): cancel opening watchers on dispose
This commit is contained in:
@@ -260,6 +260,7 @@ interface WatchHandle {
|
||||
class SkillWatchManager {
|
||||
private readonly roots = new Map<string, RootWatchState>()
|
||||
private readonly projects = new Map<string, Set<string>>()
|
||||
private readonly lifecycle = new AbortController()
|
||||
private closing = false
|
||||
private invalidationQueued = false
|
||||
|
||||
@@ -313,6 +314,7 @@ class SkillWatchManager {
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.closing = true
|
||||
this.lifecycle.abort(new Error('skill-local watcher disposed'))
|
||||
const states = [...this.roots.values()]
|
||||
this.roots.clear()
|
||||
this.projects.clear()
|
||||
@@ -348,6 +350,7 @@ class SkillWatchManager {
|
||||
}
|
||||
|
||||
private ensureWatcher(state: RootWatchState): Promise<void> {
|
||||
/* v8 ignore next -- A scheduled rewatch can reach this guard only when teardown wins its await. */
|
||||
if (this.closing || !this.config.enabled) return Promise.resolve()
|
||||
if (state.opening !== undefined) return state.opening
|
||||
const opening = this.ensureCurrentWatcher(state)
|
||||
@@ -395,8 +398,11 @@ class SkillWatchManager {
|
||||
state.watcher = watcher
|
||||
state.unhealthy = false
|
||||
} catch (error) {
|
||||
state.unhealthy = true
|
||||
this.ctx.logger.warn(`skill-local: failed to watch ${state.root.path}: ${errorMessage(error)}`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- teardown can race awaited watcher startup
|
||||
if (!this.closing) {
|
||||
state.unhealthy = true
|
||||
this.ctx.logger.warn(`skill-local: failed to watch ${state.root.path}: ${errorMessage(error)}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -474,6 +480,9 @@ class SkillWatchManager {
|
||||
}
|
||||
let ready = false
|
||||
const readiness = Promise.withResolvers<undefined>()
|
||||
const signal = this.lifecycle.signal
|
||||
const onAbort = (): void => { readiness.reject(signal.reason) }
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
const onError = (error: unknown): void => {
|
||||
if (!ready) {
|
||||
readiness.reject(error)
|
||||
@@ -494,6 +503,8 @@ class SkillWatchManager {
|
||||
} catch (error) {
|
||||
await this.closeWatcher(handle)
|
||||
throw error
|
||||
} finally {
|
||||
signal.removeEventListener('abort', onAbort)
|
||||
}
|
||||
return handle
|
||||
}
|
||||
@@ -539,6 +550,7 @@ class SkillWatchManager {
|
||||
this.invalidationQueued = true
|
||||
queueMicrotask(() => {
|
||||
this.invalidationQueued = false
|
||||
/* v8 ignore next -- Effect teardown can win this queued microtask before provider disposal emits. */
|
||||
if (this.closing) return
|
||||
this.invalidate()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user