/** * @license * Copyright 2026 Steven Roussey * SPDX-License-Identifier: Apache-2.0 */ import type { RegexRunnerFactory } from "./BoundedRegexRunner"; /** * Matches lines against a regex under an interruptible wall-clock budget. * * `RegExp.prototype.test` is synchronous and uninterruptible: a catastrophic * pattern against one hostile line blocks the event loop indefinitely, and no * abort signal can reach it. V8's script-execution timeout DOES reach inside * Irregexp backtracking, so matching runs inside a `vm` context where a * `timeout` terminates it. Measured: `/(a|a)*$/` against a 40-character input * terminated at ~210 ms under a 200 ms budget. * * Matching is BATCHED because the `vm` hop is what costs. Over 100 000 lines * with `/foo/`: a bare `regex.test` per line runs 6 ms, one `vm` call per line * 25 051 ms (unusable), one `vm` call per 512-line batch 129 ms. * * The residual: the loop is still blocked for up to one batch budget, which is * bounded and finite where the unguarded call was neither. Bun honours the * timeout more coarsely — a 300 ms budget terminated at ~1025 ms — so treat the * budget as an order of magnitude, not a deadline. * * The `RegExp` is created in the calling realm and passed through the context; * the context is reusable after a timeout, but this returns a fresh matcher per * call site anyway. */ export declare function createBoundedRegexMatcher(regex: RegExp, timeoutMs: number): (texts: readonly string[]) => boolean[]; /** * Collects global `exec` matches for each line under the same interruptible * budget as {@link createBoundedRegexMatcher}. * * `test` is not a substitute for this: a pattern whose left alternative * matches quickly (`ok|(\w|\d)*$` against a line that starts with `ok`) * returns from `test` inside budget, then a later `/g` `exec` continues into * the catastrophic right alternative and wedges the event loop. `onlyMatching` * is that second pass. The regex must carry the `g` flag; `lastIndex` is reset * per line. */ export declare function createBoundedRegexExtractor(regex: RegExp, timeoutMs: number): (texts: readonly string[]) => string[][]; /** One batch of substituted lines, positionally aligned with the input. */ export interface BoundedReplaceResult { readonly texts: readonly string[]; readonly counts: readonly number[]; } /** * Substitutes over a batch of lines under the same interruptible budget as * {@link createBoundedRegexMatcher}, and for the same reason: `String.replace` * backtracks exactly like `test` does. Measured on `/(\w|\d)*$/` against * `"1".repeat(n) + "!"`: 25 ms at n=20, 392 ms at n=24, 1538 ms at n=26. * * Batched for the same cost reason as well. Over 100 000 lines with `/foo/g`: * a bare `replace` per line runs 39 ms, one `vm` call per line ~13 650 ms, one * `vm` call per 512-line batch 147 ms. * * `expand` is called from inside the context, once per replaced match, and its * return value is what `String.replace` splices in. Keeping the substitution * itself in the engine is what lets `budget` cap replacements — which needs a * callback, so `$1` / `$&` expansion cannot be left to `replace` — without this * helper reimplementing where a match starts and ends. It also means no * per-match record outlives the call: peak memory is the output, not a * description of every match in the batch. * * `budget` is the number of replacements the whole run may still make; it is * threaded across the batch, so a line that exhausts it leaves the rest of the * batch copied verbatim. */ export declare function createBoundedRegexReplacer(regex: RegExp, expand: (args: unknown[]) => string, timeoutMs: number): (texts: readonly string[], budget: number) => BoundedReplaceResult; /** * Builds a {@link RegexRunnerFactory} that runs each match under an * interruptible wall-clock budget, for the same reason and with the same * residual as {@link createBoundedRegexMatcher}. */ export declare function createBoundedRegexExecutor(timeoutMs: number): RegexRunnerFactory;