import { existsSync } from "node:fs"; import { join } from "node:path"; import type { TaxonomyHydrationInput, TaxonomyHydrationResult, } from "@/lib/server/taxonomy/types"; import { withRetry } from "@/lib/server/utils/retry"; type SpawnedSidecar = { stdin: { write: (chunk: Uint8Array) => void; end: () => void }; stdout: ReadableStream; stderr: ReadableStream; exited: Promise; kill: () => void; }; type SidecarDeps = { existsSync: typeof existsSync; spawn: typeof Bun.spawn; setTimeout: typeof globalThis.setTimeout; clearTimeout: typeof globalThis.clearTimeout; }; function candidateBinaryPaths() { return [ process.env.FISCAL_XBRL_BIN?.trim(), join(process.cwd(), "bin", "fiscal-xbrl"), join(process.cwd(), "rust", "target", "release", "fiscal-xbrl"), join(process.cwd(), "rust", "target", "debug", "fiscal-xbrl"), ].filter( (value): value is string => typeof value === "string" && value.length > 0, ); } export function resolveFiscalXbrlBinary() { return resolveFiscalXbrlBinaryWithDeps({ existsSync, }); } function resolveFiscalXbrlBinaryWithDeps( deps: Pick, ) { const resolved = candidateBinaryPaths().find((path) => deps.existsSync(path)); if (!resolved) { throw new Error( "Rust XBRL sidecar binary is required but was not found. Set FISCAL_XBRL_BIN or build `fiscal-xbrl` under rust/target.", ); } return resolved; } export async function hydrateFilingTaxonomySnapshotFromSidecar( input: TaxonomyHydrationInput, ): Promise { return hydrateFilingTaxonomySnapshotFromSidecarWithDeps(input, { existsSync, spawn: Bun.spawn, setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout, }); } async function hydrateFilingTaxonomySnapshotFromSidecarWithDeps( input: TaxonomyHydrationInput, deps: SidecarDeps, ): Promise { return withRetry(() => hydrateFromSidecarImpl(input, deps)); } async function hydrateFromSidecarImpl( input: TaxonomyHydrationInput, deps: SidecarDeps = { existsSync, spawn: Bun.spawn, setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout, }, ): Promise { const binary = resolveFiscalXbrlBinaryWithDeps(deps); const timeoutMs = Math.max( Number(process.env.XBRL_ENGINE_TIMEOUT_MS ?? 45_000), 1_000, ); const command = [binary, "hydrate-filing"]; const requestBody = JSON.stringify({ filingId: input.filingId, ticker: input.ticker, cik: input.cik, accessionNumber: input.accessionNumber, filingDate: input.filingDate, filingType: input.filingType, filingUrl: input.filingUrl, primaryDocument: input.primaryDocument, cacheDir: process.env.FISCAL_XBRL_CACHE_DIR ?? join(process.cwd(), ".cache", "xbrl"), }); const child = deps.spawn(command, { stdin: "pipe", stdout: "pipe", stderr: "pipe", env: { ...process.env, }, }) as SpawnedSidecar; child.stdin.write(new TextEncoder().encode(requestBody)); child.stdin.end(); const timeout = deps.setTimeout(() => { child.kill(); }, timeoutMs); try { const [stdout, stderr, exitCode] = await Promise.all([ new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited, ]); if (stderr.trim().length > 0) { console.warn(`[fiscal-xbrl] ${stderr.trim()}`); } if (exitCode !== 0) { throw new Error( `Rust XBRL sidecar failed with exit code ${exitCode}: ${stderr.trim() || stdout.trim() || "no error output"}`, ); } return JSON.parse(stdout) as TaxonomyHydrationResult; } finally { deps.clearTimeout(timeout); } } export const __parserClientInternals = { candidateBinaryPaths, hydrateFilingTaxonomySnapshotFromSidecarWithDeps, hydrateFromSidecarImpl, resolveFiscalXbrlBinary: resolveFiscalXbrlBinaryWithDeps, };