147 lines
4.0 KiB
TypeScript
147 lines
4.0 KiB
TypeScript
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<Uint8Array>;
|
|
stderr: ReadableStream<Uint8Array>;
|
|
exited: Promise<number>;
|
|
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,
|
|
);
|
|
}
|
|
|
|
function resolveFiscalXbrlBinary() {
|
|
return resolveFiscalXbrlBinaryWithDeps({
|
|
existsSync,
|
|
});
|
|
}
|
|
|
|
function resolveFiscalXbrlBinaryWithDeps(
|
|
deps: Pick<SidecarDeps, "existsSync">,
|
|
) {
|
|
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<TaxonomyHydrationResult> {
|
|
return hydrateFilingTaxonomySnapshotFromSidecarWithDeps(input, {
|
|
existsSync,
|
|
spawn: Bun.spawn,
|
|
setTimeout: globalThis.setTimeout,
|
|
clearTimeout: globalThis.clearTimeout,
|
|
});
|
|
}
|
|
|
|
async function hydrateFilingTaxonomySnapshotFromSidecarWithDeps(
|
|
input: TaxonomyHydrationInput,
|
|
deps: SidecarDeps,
|
|
): Promise<TaxonomyHydrationResult> {
|
|
return withRetry(() => hydrateFromSidecarImpl(input, deps));
|
|
}
|
|
|
|
async function hydrateFromSidecarImpl(
|
|
input: TaxonomyHydrationInput,
|
|
deps: SidecarDeps = {
|
|
existsSync,
|
|
spawn: Bun.spawn,
|
|
setTimeout: globalThis.setTimeout,
|
|
clearTimeout: globalThis.clearTimeout,
|
|
},
|
|
): Promise<TaxonomyHydrationResult> {
|
|
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,
|
|
issuerOverlay: input.issuerOverlay ?? null,
|
|
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,
|
|
};
|