{"record":{"id":"6ba72a957ed931aa","repo":"koala73/worldmonitor","slug":"search-manager-destroyed","errorCode":null,"errorMessage":"Search manager destroyed","messagePattern":"Search manager destroyed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/app/webmcp-search-controller.ts","lineNumber":96,"sourceCode":"      this.subscribeAfterInitial(this.bindings.subscribeEntitlement, invalidate),\n      this.bindings.subscribeRuntimeConfig(invalidate),\n      this.bindings.subscribeWidgetAccess(invalidate),\n    ];\n  }\n\n  public destroy(): void {\n    for (const unsubscribe of this.unsubscribers) unsubscribe();\n    this.unsubscribers = [];\n    this.resultCache.clear();\n  }\n\n  public async search(\n    query: string,\n    scope: DashboardSearchScope,\n    limit: number,\n  ): Promise<DashboardSearchResponse> {\n    await this.bindings.waitForIndexReady();\n    if (this.bindings.isDestroyed()) throw new Error('Search manager destroyed');\n    this.bindings.refreshIndex();\n    const modal = this.bindings.getModal();\n    if (!modal) throw new Error('Search index is not initialised');\n\n    let searchResult = modal.search(query, scope as SearchScope);\n    if (\n      searchResult.flightCallsign\n      && searchResult.orderedMatches.length === 0\n      && this.bindings.hasPremiumAccess()\n    ) {\n      try {\n        await this.bindings.fetchLiveFlight(searchResult.flightCallsign);\n        if (this.bindings.isDestroyed()) throw new Error('Search manager destroyed');\n        this.bindings.refreshIndex();\n        searchResult = modal.search(query, scope as SearchScope);\n      } catch (error) {\n        if (this.bindings.isDestroyed()) throw error;\n        // Live enrichment is optional. A failed lookup is an empty result.","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/src/app/webmcp-search-controller.ts#L78-L114","documentation":"Thrown by WebMcpSearchController.search() when, after awaiting bindings.waitForIndexReady(), bindings.isDestroyed() is true. destroy() already ran on the controller (unsubscribers cleared, result cache emptied), so an in-flight or subsequent search refuses to proceed: its index, modal, and subscriptions may be gone.","triggerScenarios":"search() is invoked and awaits index readiness; during that await the app (and thus the controller) is destroyed — teardown of the WebMCP search feature racing the first (or any) search call. A search issued after destroy() hits the same check immediately.","commonSituations":"Tab unload or HMR teardown during an agent's search_dashboard call; tests that destroy the controller while a search promise is pending; sequential tool calls where the second arrives after cleanup began.","solutions":["Catch this error and treat it as terminal for the controller — do not retry search on a destroyed instance; re-create the controller with the app","Add an AbortSignal to search() and destroy() so teardown cancels in-flight searches explicitly instead of relying on the post-await check","Queue or reject new searches at the entry point when bindings.isDestroyed() is already true, before awaiting index readiness"],"exampleFix":"// before\nconst response = await controller.search(query, scope, limit); // unhandled throw after destroy\n\n// after\nif (controller.isDestroyed()) return { ok: false, status: 'denied', reason: 'search_manager_destroyed' };\ntry {\n  const response = await controller.search(query, scope, limit);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Search manager destroyed') return deniedResult();\n  throw e;\n}","handlingStrategy":"type-guard","validationCode":"if (controller.isDestroyed()) { // check before entry AND after every await\n  return { ok: false, status: 'denied', reason: 'search_manager_destroyed' };\n}","typeGuard":"function isSearchManagerUsable(controller: { isDestroyed(): boolean }): boolean {\n  return !controller.isDestroyed();\n}","tryCatchPattern":"try { return await controller.search(q, scope, limit); } catch (e) { if (e instanceof Error && e.message === 'Search manager destroyed') return DESTROYED_RESULT; throw e; }","preventionTips":["Add an AbortSignal to search()/destroy() so teardown cancels in-flight work instead of surfacing as this error","Reject new searches at the entry point once destroyed","Re-create the controller together with the app, never independently"],"tags":["webmcp","agent-tools","search","lifecycle","race-condition"],"backgroundTag":"stale-app-instance","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}