Native RE2 partial matching for Node.js 26 and later. It exposes a small API for matching one expression or many expressions at once without backtracking.
import { RE2, RE2Set } from '@nxtedition/re2'
const expression = new RE2('foo')
expression.test(Buffer.from('before foo after')) // true
expression.testManySync([Buffer.from('foo'), Buffer.from('bar')]) // [true, false]
await expression.testManyAsync([Buffer.from('foo'), Buffer.from('bar')]) // [true, false]
const expressions = new RE2Set(['foo', 'bar'])
expressions.test(Buffer.from('bar')) // [1]
expressions.testManySync([Buffer.from('foo'), Buffer.from('bar')]) // [[0], [1]]
await expressions.testManyAsync([Buffer.from('foo'), Buffer.from('bar')]) // [[0], [1]]
const asyncExpressions = await RE2Set.compileAsync(['foo', 'bar'])
asyncExpressions.test(Buffer.from('foo')) // [0]
expression.testManyString(['foo', 'bar']) // [true, false]
expression.testManyBuffer([Buffer.from('foo')]) // [true]
expression.testManySlice([{ buffer: Buffer.from('xfooy'), byteOffset: 1, byteLength: 3 }]) // [true]
expression.testStringSync('foo') // true
expression.testBufferSync(Buffer.from('foo')) // true
expression.testSliceSync({ buffer: Buffer.from('xfooy'), byteOffset: 1, byteLength: 3 }) // trueA binary view is a Buffer, TypedArray, or DataView, or a slice object naming a byte range of one. Patterns may be strings or binary views, and so may the input to test(), testManySync(), testMany(), and testManyAsync(); the input-specific methods below narrow that to one form each. Both APIs operate on bytes; optional byteOffset and byteLength values select the input range. Negative values clamp to zero, values past the view clamp to its bounds, and fractional values are truncated. test()'s own range applies to the bytes its argument already selected, so test(slice, 1) skips the first byte of the slice.
A slice is a { buffer, byteOffset, byteLength } object whose optional byte range is clamped exactly like test(); byteOffset defaults to 0 and byteLength defaults to the bytes remaining after the truncated offset. Slices passed to the slice-specific batch methods have their properties read once, by the native addon, so invalid slice contents reject the Promise of the asynchronous variants instead of throwing. Everywhere else a slice is resolved before matching starts, which reports the same errors synchronously.
Single inputs also have input-specific entry points: testStringSync(), testBufferSync(), and testSliceSync(), with testString(), testBuffer(), and testSlice() as aliases. All six exist on RE2 and RE2Set, take one input and no options, and match all of it with the semantics of test().
Batch matching also has input-specific entry points that avoid mixed input arrays: testManySliceSync(), testManyStringSync(), and testManyBufferSync(), plus the testManySliceAsync(), testManyStringAsync(), and testManyBufferAsync() counterparts. All six exist on RE2 and RE2Set and take the same options as testManySync() and testManyAsync(). testManySlice(), testManyString(), and testManyBuffer() are synchronous compatibility aliases for the three …Sync() methods. Strings match their UTF-8 bytes, with unpaired surrogates encoded as U+FFFD, exactly like Buffer.from(input); those bytes are always copied, so unsafe has no effect on testManyStringAsync().
testManySync() blocks until matching completes. Without intra-call parallelism it runs inline; with parallelism the caller joins the OpenMP team and blocks until every chunk completes. It borrows the supplied views for the duration of the call, so their backing stores must not be mutated, resized, transferred, or detached concurrently. testManyAsync() returns a Promise and performs matching from the Node.js worker pool, optionally entering OpenMP from that worker. It snapshots all input bytes before returning by default, so later mutation, resizing, transfer, or detachment cannot affect the result. { unsafe: true } skips that copy; every backing store must then remain attached, the same size, and unmodified until the Promise settles. SharedArrayBuffer-backed views follow the same rule. The slice and Buffer variants borrow and snapshot on exactly the same terms, applied to the views they reference; only the string variants are exempt, because their bytes are always copied. testMany() remains a synchronous compatibility alias for testManySync().
All batch methods accept up to 1,048,576 inputs and preserve their outer result order. Their optional batchSize is the number of inputs per native scheduling chunk. Infinity or a value greater than or equal to the input count disables intra-call OpenMP: synchronous matching stays on the caller thread, while asynchronous matching stays on one Node.js worker. Omitting batchSize keeps automatic scheduling at about 16 chunks per requested OpenMP thread.
The published Linux x64 prebuild uses GNU OpenMP when a batch contains enough input bytes to amortize dispatch. It requests at most half of the container-affinity CPUs, including the initiating thread, and respects the OpenMP runtime thread limit. Concurrent Node.js environments using the same addon instance share one submission lock to avoid oversubscribing that limit; libgomp owns the process-wide worker lifecycle. The prebuild dynamically links libgomp.so.1; Debian and Ubuntu provide it in the libgomp1 package. Source builds, macOS, and other platforms match each batch sequentially regardless of batchSize; testManyAsync() still moves that sequential work to a Node.js worker.
Invalid RE2 syntax throws during synchronous construction and rejects RE2Set.compileAsync(). A pattern is limited to 16 MiB; a set accepts at most 100,000 patterns and 16 MiB of aggregate pattern bytes. The asynchronous API snapshots pattern bytes before returning, compiles on the Node.js worker pool, and resolves to a normal RE2Set.
Compiled patterns are immutable and matched through const methods, so equal patterns share one compiled program. new RE2() reuses a compiled pattern from a bounded process-wide cache of the 64 most recent patterns, and new RE2Set() and RE2Set.compileAsync() share one cache of the 16 most recent ordered pattern lists, keyed on the exact pattern bytes and their order. A synchronous construction can therefore be satisfied by a set an earlier compileAsync() compiled, and vice versa; concurrent compilations of the same patterns are compiled once and shared. Failed compilations are never cached, so an invalid pattern reports its error every time. Caching is transparent: instances stay independent, and the only observable effects are lower compile latency and memory use.
RE2, RE2Set, and RE2Set.compileAsync() take an optional second argument with two independent compilation options. Both default to false, so nothing changes unless you ask for it.
new RE2('(?<=foo)bar', { compat: true }).test(Buffer.from('foobar')) // true
new RE2('(', { ignoreInvalid: true }).test(Buffer.from('anything')) // false
const set = new RE2Set(['foo', '(?<=x)y', '('], { compat: true, ignoreInvalid: true })
set.test(Buffer.from('xy')) // [1]compat matches patterns RE2 cannot compile with JavaScript RegExp instead, which covers lookbehind, lookahead, backreferences, \k<name>, \uXXXX, \cA, [\b], \p{Script=…} and other property forms, [], [^], and repeat counts above RE2's limit of 1000. Constructs neither engine accepts still throw: atomic groups (?>…), possessive quantifiers a++, (?#comment), \Z, \8, \9, and a{2,1}. Patterns whose bytes are not valid UTF-8 have no RegExp source, so they throw too. Errors keep RE2's type and message; the JavaScript SyntaxError is attached as cause when there is one.
ignoreInvalid turns a pattern that cannot be compiled at all into one that never matches. In an RE2Set it preserves the caller's index space, so a pattern at index i that never matches simply never appears in results while every other index is unchanged. It only absorbs compilation failures: argument type errors and the pattern-count and pattern-byte limits still throw.
Compat patterns give up RE2's linear-time guarantee. JavaScript
RegExpbacktracks, and a runningtest()cannot be interrupted from the same thread, so a pattern like(a+)+$that RE2 answers in microseconds can hang the event loop indefinitely. Do not enablecompatfor patterns from untrusted sources.
Compat matching is subject to the following differences, because it bridges byte-oriented RE2 to UTF-16 RegExp:
- Patterns RE2 does accept keep RE2 semantics. This is a fallback for patterns that fail to compile, not a
RegExpemulation, so\p{L},[[:alpha:]],\A,\z,\Q…\E,\C, and[]]still mean what RE2 says they mean. - Compat patterns are compiled with the
uflag, matching RE2's default UTF-8 rune semantics. Under that flag.excludes\r, U+2028, and U+2029 in addition to\n, and\smatches more than RE2's[\t\n\f\r ]. - Input bytes are decoded as UTF-8. Invalid bytes, and byte ranges that split a rune, become U+FFFD, which
.,[^x], and\Smatch; RE2 matches nothing there. Only well-formed UTF-8 input is byte-exact. batchSizeis still validated but does not apply, since compat matching does not use OpenMP.{ unsafe: true }cannot avoid the copy, becauseRegExpneeds a string; input bytes are decoded before the call returns, so safe-mode snapshot semantics still hold.- The asynchronous entry points evaluate compat patterns on the main thread one turn later. Only the natively compiled patterns of a mixed
RE2Setrun on the worker pool.
Async compilations are cached by the complete ordered pattern bytes. Concurrent calls for the same pattern set share one in-flight compilation, and the native addon retains recent compiled sets in a bounded process-wide cache shared across Node.js Workers. Compiled sets use shared ownership so cache eviction and Worker teardown cannot invalidate another Worker's live set. Failed compilations are not cached. All RE2Set matching methods return matching pattern indices in the same order as the input pattern list, or [] when nothing matches.
Run npm run benchmark to compare scalar, single-threaded, automatic, and explicitly batched synchronous matching. The harness verifies every candidate, independently confirms the fastest scheduling mode, reports requested and observed OpenMP threads, and measures safe/unsafe asynchronous admission plus settlement time. It also measures cold, cached, and deduplicated compileAsync() calls. Use npm run benchmark:prebuild to force the packaged prebuild. Input count, input bytes, iterations, set size, and warmups can be configured with command-line options; run node benchmark.js --help for details.
Release tarballs include Node-API prebuilds for darwin-arm64 and glibc linux-x64. The Linux binary is built in the Node 26 Bookworm image with -march=znver3 -mtune=znver3 by default, enabling AVX2 and targeting Zen 3-compatible deployment CPUs. Set RE2_LEVEL_MARCH when running ./build.sh to select another CPU, or set RE2_LEVEL_MARCH= explicitly for a portable x86-64 artifact. The prebuild is compatible with Bookworm and newer glibc systems, and is libc-tagged so it is never selected on musl. Other platforms fall back to a portable source build when install scripts are enabled; Linux x64 source builds can opt into the same tuning with RE2_LEVEL_MARCH=znver3.
./build.sh builds and tests the Linux prebuild in Docker, validates its exact artifact manifest, and installs it transactionally. From a clean, up-to-date main checkout on an arm64 Mac, yarn release [patch|minor|major|x.y.z] builds and tests the Linux and Darwin prebuilds before changing the version, verifies the npm tarball's exact native-artifact manifest, and only then versions, publishes, and pushes the release. Both platform builds stage their candidate and restore the prior artifact if generation, validation, or installation fails.
npm install
npm testnpm test always rebuilds the native addon before running the JavaScript and TypeScript contract tests.