(text: string, opts?: { timeoutMs?: number })
| 181 | } |
| 182 | |
| 183 | export async function scanWithSidecar(text: string, opts?: { timeoutMs?: number }): Promise<{ verdict: unknown }> { |
| 184 | const s = getState(); |
| 185 | if (s.brokenCircuit) { |
| 186 | throw new Error("sidecar-circuit-broken"); |
| 187 | } |
| 188 | if (Buffer.byteLength(text, "utf-8") > REQUEST_CAP_BYTES) { |
| 189 | throw new Error("payload-too-large"); |
| 190 | } |
| 191 | if (!s.child) { |
| 192 | if (!spawnSidecar()) { |
| 193 | throw new Error("sidecar-spawn-failed"); |
| 194 | } |
| 195 | } |
| 196 | const id = String(s.nextId++); |
| 197 | const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS; |
| 198 | |
| 199 | return new Promise((resolve, reject) => { |
| 200 | const timer = setTimeout(() => { |
| 201 | s.pending.delete(id); |
| 202 | recordFailure(); |
| 203 | reject(new Error("sidecar-timeout")); |
| 204 | }, timeoutMs); |
| 205 | |
| 206 | s.pending.set(id, { |
| 207 | resolve: (response: unknown) => { |
| 208 | const r = response as { verdict?: unknown }; |
| 209 | resolve({ verdict: r.verdict }); |
| 210 | }, |
| 211 | reject, |
| 212 | timer, |
| 213 | }); |
| 214 | |
| 215 | const payload = JSON.stringify({ id, op: "scan-page-content", text }) + "\n"; |
| 216 | try { |
| 217 | s.child!.stdin.write(payload); |
| 218 | } catch (err) { |
| 219 | clearTimeout(timer); |
| 220 | s.pending.delete(id); |
| 221 | recordFailure(); |
| 222 | reject(err instanceof Error ? err : new Error(String(err))); |
| 223 | } |
| 224 | }); |
| 225 | } |
| 226 | |
| 227 | /** Reset the circuit breaker. Test-only escape hatch. */ |
| 228 | export function resetSidecarForTests(): void { |
no test coverage detected