{"version":3,"file":"searxng.mjs","names":[],"sources":["../../src/providers/searxng.ts"],"sourcesContent":["import type { SearchResult, SearchRequestOptions, ProviderConfig } from \"../core/types.ts\";\nimport { Provider, type ProviderSearchPage } from \"../core/provider.ts\";\nimport { InvalidSearchContinuationError, normalizeError } from \"../core/errors.ts\";\n\ninterface SearXNGResult {\n  readonly title: string;\n  readonly url: string;\n  readonly content: string;\n  readonly engine: string;\n  readonly engines: readonly string[];\n  readonly score: number;\n  readonly category: string;\n  readonly publishedDate?: string;\n  readonly img_src?: string;\n  readonly thumbnail?: string;\n}\n\ninterface SearXNGSearchResponse {\n  readonly results: readonly SearXNGResult[];\n  readonly number_of_results?: number;\n  readonly query: string;\n}\n\nconst SEARXNG_PROBE_TIMEOUT_MS = 2000;\n\nexport class SearXNGProvider extends Provider {\n  static readonly providerName = \"searxng\";\n  static readonly defaultBaseURL = \"http://localhost:8080\";\n\n  constructor(config: Readonly<ProviderConfig>) {\n    super(config, SearXNGProvider);\n  }\n\n  /**\n   * Quick reachability probe for self-hosted instances. Returns false instead\n   * of throwing so {@link searchAll} can skip an unreachable instance silently.\n   * Treats any HTTP response (even 4xx) as reachable — the host is up.\n   * Uses a <=2s timeout so a dead endpoint does not stall fan-out.\n   * @param signal - Caller cancellation shared with provider discovery.\n   * @returns {Promise<boolean>} Whether the endpoint responds.\n   */\n  async isAvailable(signal?: Readonly<AbortSignal>): Promise<boolean> {\n    const controller = new AbortController();\n    const timer = setTimeout(\n      () => controller.abort(new DOMException(\"SearXNG probe timed out\", \"TimeoutError\")),\n      SEARXNG_PROBE_TIMEOUT_MS,\n    );\n    const probeSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;\n    try {\n      const response = await fetch(this.baseURL, {\n        method: \"GET\",\n        signal: probeSignal,\n      });\n      // Any HTTP status means the host responded; treat as reachable.\n      return typeof response.status === \"number\";\n    } catch {\n      signal?.throwIfAborted();\n      return false;\n    } finally {\n      clearTimeout(timer);\n    }\n  }\n\n  async search(query: string, options?: SearchRequestOptions): Promise<SearchResult[]> {\n    return (await this.searchPage(query, options)).results;\n  }\n\n  async searchPage(\n    query: string,\n    options?: SearchRequestOptions,\n    continuation?: string,\n  ): Promise<ProviderSearchPage> {\n    try {\n      const page = searxngPage(continuation);\n      const params = new URLSearchParams({\n        q: query,\n        format: \"json\",\n        pageno: String(page),\n      });\n\n      if (options?.category) {\n        params.append(\"categories\", options.category);\n      }\n\n      const url = `${this.baseURL}/search?${params.toString()}`;\n      const response = await this.client.getJSON<SearXNGSearchResponse>(\n        url,\n        undefined,\n        options?.signal,\n      );\n      const results = response.results.map(mapResult);\n\n      return {\n        results: options?.maxResults ? results.slice(0, options.maxResults) : results,\n        ...(response.results.length > 0\n          ? { continuation: String(page + 1), continuationStatus: \"unknown\" as const }\n          : {}),\n      };\n    } catch (error) {\n      throw normalizeError(error, \"searxng\");\n    }\n  }\n}\n\nfunction searxngPage(continuation?: string): number {\n  if (continuation === undefined) return 1;\n  if (!/^[1-9]\\d*$/u.test(continuation)) throw new InvalidSearchContinuationError();\n  const page = Number(continuation);\n  if (!Number.isSafeInteger(page)) throw new InvalidSearchContinuationError();\n  return page;\n}\n\nfunction mapResult(result: SearXNGResult): SearchResult {\n  return {\n    url: result.url,\n    title: result.title,\n    snippet: result.content,\n    score: result.score,\n    publishedDate: result.publishedDate,\n    image: result.img_src ?? result.thumbnail,\n    metadata: {\n      engine: result.engine,\n      engines: result.engines,\n      category: result.category,\n    },\n  };\n}\n"],"mappings":";;;AAuBA,MAAM,2BAA2B;AAEjC,IAAa,kBAAb,MAAa,wBAAwB,SAAS;CAC5C,OAAgB,eAAe;CAC/B,OAAgB,iBAAiB;CAEjC,YAAY,QAAkC;EAC5C,MAAM,QAAQ,eAAe;CAC/B;;;;;;;;;CAUA,MAAM,YAAY,QAAkD;EAClE,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,QAAQ,iBACN,WAAW,MAAM,IAAI,aAAa,2BAA2B,cAAc,CAAC,GAClF,wBACF;EACA,MAAM,cAAc,SAAS,YAAY,IAAI,CAAC,QAAQ,WAAW,MAAM,CAAC,IAAI,WAAW;EACvF,IAAI;GAMF,OAAO,QAAO,MALS,MAAM,KAAK,SAAS;IACzC,QAAQ;IACR,QAAQ;GACV,CAAC,EAAA,CAEsB,WAAW;EACpC,QAAQ;GACN,QAAQ,eAAe;GACvB,OAAO;EACT,UAAU;GACR,aAAa,KAAK;EACpB;CACF;CAEA,MAAM,OAAO,OAAe,SAAyD;EACnF,QAAQ,MAAM,KAAK,WAAW,OAAO,OAAO,EAAA,CAAG;CACjD;CAEA,MAAM,WACJ,OACA,SACA,cAC6B;EAC7B,IAAI;GACF,MAAM,OAAO,YAAY,YAAY;GACrC,MAAM,SAAS,IAAI,gBAAgB;IACjC,GAAG;IACH,QAAQ;IACR,QAAQ,OAAO,IAAI;GACrB,CAAC;GAED,IAAI,SAAS,UACX,OAAO,OAAO,cAAc,QAAQ,QAAQ;GAG9C,MAAM,MAAM,GAAG,KAAK,QAAQ,UAAU,OAAO,SAAS;GACtD,MAAM,WAAW,MAAM,KAAK,OAAO,QACjC,KACA,KAAA,GACA,SAAS,MACX;GACA,MAAM,UAAU,SAAS,QAAQ,IAAI,SAAS;GAE9C,OAAO;IACL,SAAS,SAAS,aAAa,QAAQ,MAAM,GAAG,QAAQ,UAAU,IAAI;IACtE,GAAI,SAAS,QAAQ,SAAS,IAC1B;KAAE,cAAc,OAAO,OAAO,CAAC;KAAG,oBAAoB;IAAmB,IACzE,CAAC;GACP;EACF,SAAS,OAAO;GACd,MAAM,eAAe,OAAO,SAAS;EACvC;CACF;AACF;AAEA,SAAS,YAAY,cAA+B;CAClD,IAAI,iBAAiB,KAAA,GAAW,OAAO;CACvC,IAAI,CAAC,cAAc,KAAK,YAAY,GAAG,MAAM,IAAI,+BAA+B;CAChF,MAAM,OAAO,OAAO,YAAY;CAChC,IAAI,CAAC,OAAO,cAAc,IAAI,GAAG,MAAM,IAAI,+BAA+B;CAC1E,OAAO;AACT;AAEA,SAAS,UAAU,QAAqC;CACtD,OAAO;EACL,KAAK,OAAO;EACZ,OAAO,OAAO;EACd,SAAS,OAAO;EAChB,OAAO,OAAO;EACd,eAAe,OAAO;EACtB,OAAO,OAAO,WAAW,OAAO;EAChC,UAAU;GACR,QAAQ,OAAO;GACf,SAAS,OAAO;GAChB,UAAU,OAAO;EACnB;CACF;AACF"}