()
| 177 | * which /reload-plugins awaits. |
| 178 | */ |
| 179 | export async function pruneRemovedPluginHooks(): Promise<void> { |
| 180 | // Early return when nothing to prune — avoids seeding the loadAllPluginsCacheOnly |
| 181 | // memoize in test/preload.ts beforeEach (which clears registeredHooks). |
| 182 | if (!getRegisteredHooks()) return |
| 183 | const { enabled } = await loadAllPluginsCacheOnly() |
| 184 | const enabledRoots = new Set(enabled.map(p => p.path)) |
| 185 | |
| 186 | // Re-read after the await: a concurrent loadPluginHooks() (hot-reload) |
| 187 | // could have swapped STATE.registeredHooks during the gap. Holding the |
| 188 | // pre-await reference would compute survivors from stale data. |
| 189 | const current = getRegisteredHooks() |
| 190 | if (!current) return |
| 191 | |
| 192 | // Collect plugin hooks whose pluginRoot is still enabled, then swap via |
| 193 | // the existing clear+register pair (same atomic-pair pattern as |
| 194 | // loadPluginHooks above). Callback hooks are preserved by |
| 195 | // clearRegisteredPluginHooks; we only need to re-register survivors. |
| 196 | const survivors: Partial<Record<HookEvent, PluginHookMatcher[]>> = {} |
| 197 | for (const [event, matchers] of Object.entries(current)) { |
| 198 | const kept = matchers.filter( |
| 199 | (m): m is PluginHookMatcher => |
| 200 | 'pluginRoot' in m && enabledRoots.has(m.pluginRoot), |
| 201 | ) |
| 202 | if (kept.length > 0) survivors[event as HookEvent] = kept |
| 203 | } |
| 204 | |
| 205 | clearRegisteredPluginHooks() |
| 206 | registerHookCallbacks(survivors) |
| 207 | } |
| 208 | |
| 209 | /** |
| 210 | * Reset hot reload subscription state. Only for testing. |
no test coverage detected