Node.js · line reader
Stream, seek, or splice by line number without loading the file as a string. An optional .lseek sidecar makes later seeks an 8-byte lookup plus a short walk. It is not readline, not split, and not ripgrep.
npm i line-seeker
Honest split
stronger here
buildIndex once. Later getLine reads 8 bytes from the sidecar, preads that offset, then walks at most stride lines. refreshIndex only scans bytes appended since.
RAM ≈ chunk sizeweaker here
readline or n-readlines for a single sequential walk. readFile + split for a few KB. ripgrep for search. Disk edits copy the whole file, not just the line.
use those insteadOptional index
buildIndex reads the file once and writes .lseek (~8 bytes per 64 lines). After that, line 500000 is an 8-byte lookup, a pread, then at most stride lines. Skip the index for a one-shot or a small file. find is still a linear scan.
// Index is optional. First build is a full read.
import { createSeeker, readLine, setLine } from "line-seeker";
await setLine("./notes.txt", 3, "updated");
const line = await readLine("./notes.txt", 3);
const mem = createSeeker({ text: "a\\nb\\nc\\n" });
await mem.replaceLine(2, "B");
const log = createSeeker("./huge.log");
await log.buildIndex({ stride: 64 });
await log.getLine(500_000);getLine(50) stops at line 50. Without an index, a late line still costs a prefix scan. The sequential cursor makes 50 then 51 cheap.
A .lseek sidecar stores every Nth offset. First build is a full read. Skip it unless you will seek again.
Path, Buffer, or { text }. In-memory holds the buffer you passed in. Stdin is forward-only: no index, no edits.
replaceLine splices in RAM for { text }. On a file it copies through a temp path. A one-line change on 5 GB rewrites 5 GB.
Node.js 18 · MIT