koala73/worldmonitor · error · Error

Search manager destroyed

Error message

Search manager destroyed

What it means

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.

Source

Thrown at src/app/webmcp-search-controller.ts:96

      this.subscribeAfterInitial(this.bindings.subscribeEntitlement, invalidate),
      this.bindings.subscribeRuntimeConfig(invalidate),
      this.bindings.subscribeWidgetAccess(invalidate),
    ];
  }

  public destroy(): void {
    for (const unsubscribe of this.unsubscribers) unsubscribe();
    this.unsubscribers = [];
    this.resultCache.clear();
  }

  public async search(
    query: string,
    scope: DashboardSearchScope,
    limit: number,
  ): Promise<DashboardSearchResponse> {
    await this.bindings.waitForIndexReady();
    if (this.bindings.isDestroyed()) throw new Error('Search manager destroyed');
    this.bindings.refreshIndex();
    const modal = this.bindings.getModal();
    if (!modal) throw new Error('Search index is not initialised');

    let searchResult = modal.search(query, scope as SearchScope);
    if (
      searchResult.flightCallsign
      && searchResult.orderedMatches.length === 0
      && this.bindings.hasPremiumAccess()
    ) {
      try {
        await this.bindings.fetchLiveFlight(searchResult.flightCallsign);
        if (this.bindings.isDestroyed()) throw new Error('Search manager destroyed');
        this.bindings.refreshIndex();
        searchResult = modal.search(query, scope as SearchScope);
      } catch (error) {
        if (this.bindings.isDestroyed()) throw error;
        // Live enrichment is optional. A failed lookup is an empty result.

View on GitHub (pinned to eeab0a219f)

Solutions

  1. 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
  2. Add an AbortSignal to search() and destroy() so teardown cancels in-flight searches explicitly instead of relying on the post-await check
  3. Queue or reject new searches at the entry point when bindings.isDestroyed() is already true, before awaiting index readiness

Example fix

// before
const response = await controller.search(query, scope, limit); // unhandled throw after destroy

// after
if (controller.isDestroyed()) return { ok: false, status: 'denied', reason: 'search_manager_destroyed' };
try {
  const response = await controller.search(query, scope, limit);
} catch (e) {
  if (e instanceof Error && e.message === 'Search manager destroyed') return deniedResult();
  throw e;
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (controller.isDestroyed()) { // check before entry AND after every await
  return { ok: false, status: 'denied', reason: 'search_manager_destroyed' };
}

Type guard

function isSearchManagerUsable(controller: { isDestroyed(): boolean }): boolean {
  return !controller.isDestroyed();
}

Try / catch

try { return await controller.search(q, scope, limit); } catch (e) { if (e instanceof Error && e.message === 'Search manager destroyed') return DESTROYED_RESULT; throw e; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/6ba72a957ed931aa. Report an issue: GitHub.