readline-pager 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,24 @@
1
- # readline-pager
1
+ # 📄 readline-pager
2
2
 
3
- Memory-efficient, paginated file reader for Node.js with async iteration, prefetching, backward reading and optional worker support.
3
+ <p align="center"><img src="logo.webp" alt="logo" width="349"></p>
4
+
5
+ Memory-efficient, paginated file reader for Node.js with async iteration, prefetching, backward reading, and optional worker support.
4
6
 
5
7
  `readline-pager` reads large text files page-by-page without loading the entire file into memory.
6
8
 
7
9
  - ✅ Zero dependencies
8
- - ✅ Async iterator support (`for await...of`)
9
- - ✅ Forward & backward reading (read from EOF → BOF)
10
+ - ✅ Async iterator (`for await...of`) + manual `next()` API
11
+ - ✅ Forward & backward reading (EOF → BOF)
10
12
  - ✅ Optional worker thread mode (forward only)
11
- - ✅ Up to ~3× faster than vanilla Node.js `readline`
12
- - ✅ ~97% test coverage & Fully typed (TypeScript)
13
+ - ✅ Up to ~3× faster than Node.js `readline`
14
+ - ✅ ~97% test coverage & fully typed (TypeScript)
15
+
16
+ > **Important:**
17
+ > Performance is heavily dependent on the `chunkSize` option; ensure you fine-tune it for your specific I/O hardware. A setting of **64 KB** is typically a good starting point. Increasing it might gradually improve read speeds, usually reaching an optimal peak depending on your hardware's capabilities.
13
18
 
14
19
  ---
15
20
 
16
- ## Installation
21
+ ## 📦 Installation
17
22
 
18
23
  ```bash
19
24
  npm install readline-pager
@@ -21,7 +26,7 @@ npm install readline-pager
21
26
 
22
27
  ---
23
28
 
24
- ## Quick Start
29
+ ## 🚀 Quick start
25
30
 
26
31
  ```ts
27
32
  import { createPager } from "readline-pager";
@@ -35,138 +40,113 @@ for await (const page of pager) {
35
40
 
36
41
  ---
37
42
 
38
- ## Manual iteration (recommended for max throughput)
43
+ **Recommended for highest throughput:**
39
44
 
40
45
  ```ts
41
46
  const pager = createPager("./bigfile.txt");
42
47
 
48
+ while (true) {
49
+ const page = await pager.next();
50
+ if (!page) break;
51
+ }
52
+
53
+ // or
43
54
  let page;
44
55
  while ((page = await pager.next()) !== null) {
45
- // page: string[]
46
56
  // process page
47
57
  }
48
58
  ```
49
59
 
50
- `pager.next()` returns:
51
-
52
- - `Promise<string[]>` — next page
53
- - `Promise<null>` — end of file
54
-
55
- > Use `while + next()` when raw throughput matters (see Iteration Performance Notes).
60
+ - `while + next()` is the fastest iteration method (avoids extra async-iterator overhead).
61
+ - `for await of` is more ergonomic and convenient.
56
62
 
57
63
  ---
58
64
 
59
- ## Options
65
+ ## ⚙️ Options
60
66
 
61
67
  ```ts
62
68
  createPager(filepath, {
69
+ chunkSize?: number, // default: 64 * 1024 (64 KiB)
63
70
  pageSize?: number, // default: 1_000
64
- delimiter?: string // default: "\n"
71
+ delimiter?: string, // default: "\n"
65
72
  prefetch?: number, // default: 1
66
73
  backward?: boolean, // default: false
67
74
  useWorker?: boolean, // default: false (forward only)
68
75
  });
69
76
  ```
70
77
 
78
+ - `chunkSize`: number of bytes read per I/O operation. **Tune this** — default is `64 * 1024`.
71
79
  - `pageSize` — number of lines per page.
72
80
  - `delimiter` — line separator.
73
- - `prefetch` — max pages buffered internally. Higher increases throughput but uses more memory.
81
+ - `prefetch` — max number of pages buffered internally. Not required for typical use; tuning has little effect once the engine is optimized.
74
82
  - `backward` — read file from end → start (not supported with `useWorker`).
75
- - `useWorker` — move parsing to a worker thread (forward only).
83
+ - `useWorker` — offload parsing to a worker thread (forward only).
76
84
 
77
85
  ---
78
86
 
79
- ## API
87
+ ## 📚 API
80
88
 
81
89
  ### `pager.next(): Promise<string[] | null>`
82
90
 
83
91
  Returns the next page or `null` when finished. Empty lines are preserved.
84
92
 
85
- **Note:** Unlike Node.js `readline`, which skips empty files or empty lines at the start, `readline-pager` always returns all lines.
93
+ **Note:** Unlike Node.js `readline`, which may skip empty files or leading empty lines, `readline-pager` always returns all lines.
86
94
 
87
95
  - A completely empty file (`0` bytes) produces `[""]` on the first read.
88
- - A file with multiple empty lines returns each line as an empty string (e.g., `["", ""]` for two empty lines). Node.js `readline` would emit fewer or no `line` events in these cases.
89
-
90
- ✅ Key points:
91
-
92
- - A 0-byte file → `[""]`
93
- - Consecutive `\n\n` → `["", ""]`
94
- - Node.js `readline` skips first empty line(s) and might emit nothing for empty files.
96
+ - A file with multiple empty lines returns each line as an empty string (e.g., `["", ""]` for two empty lines). Node.js `readline` may emit fewer or no `line` events in these cases.
95
97
 
96
98
  ### `pager.close(): void`
97
99
 
98
- Stops reading and releases resources immediately. Safe to call any time.
100
+ Stops reading and releases resources immediately. Safe to call at any time.
99
101
 
100
- ### Properties
102
+ ### Read-only properties
101
103
 
102
- ```ts
103
- pager.lineCount; // total lines emitted so far
104
- pager.firstLine; // first line emitted (available after first read)
105
- pager.lastLine; // last line emitted (updated each page)
106
- ```
104
+ - `pager.lineCount` — lines emitted so far
105
+ - `pager.firstLine` first emitted line (available after first read)
106
+ - `pager.lastLine` last emitted line (updated per page)
107
107
 
108
108
  ---
109
109
 
110
- ## Benchmark
110
+ ## 📊 Benchmark
111
111
 
112
112
  Run the included benchmark:
113
113
 
114
114
  ```bash
115
115
  # default run
116
- node test/benchmark.ts
116
+ node test/_benchmark.ts
117
117
 
118
- # or customize
119
- node test/benchmark.ts --lines=20000 --page-size=500 --prefetch=4 --backward
118
+ # or customize with args
119
+ node test/_benchmark.ts --lines=20000 --page-size=500 --backward
120
120
  ```
121
121
 
122
- Benchmarks were executed on a high-end consumer Linux machine (SSD + fast CPU) using generated files.
123
-
124
- ### Summary (averages)
122
+ > Test setup: generated text files with uuid, run on a fast NVMe machine with default options; values are averages from multiple runs. Results are machine-dependent.
125
123
 
126
- | Lines | File Size (MB) | Implementation | Avg Time (ms) | Avg Throughput (MB/s) | Speedup vs `readline` |
127
- | ----------- | -------------- | -------------- | ------------- | --------------------: | --------------------: |
128
- | 1,000,000 | 35.29 MB | readline | 100.21 | 352.31 | |
129
- | 1,000,000 | 35.29 MB | readline-pager | 43.31 | 815.71 | **2.32× faster** |
130
- | 10,000,000 | 352.86 MB | readline | 802.61 | 439.80 | |
131
- | 10,000,000 | 352.86 MB | readline-pager | 292.33 | 1207.77 | **2.75× faster** |
132
- | 100,000,000 | 3528.59 MB | readline | 7777.52 | 453.75 | — |
133
- | 100,000,000 | 3528.59 MB | readline-pager | 2742.99 | 1286.50 | **2.83× faster** |
124
+ | Lines | File MB | Node `readline` (MB/s) | Bun streaming (MB/s) | `readline-pager` (Node) (MB/s) |
125
+ | -----: | -------: | ---------------------: | -------------------: | -----------------------------: |
126
+ | 10M | 352.86 | ~423 | ~296 | **~1,327** |
127
+ | 100M | 3528.59 | ~441 | ~298 | **~1,378** |
128
+ | 1,000M | 35285.95 | ~426 | ~294 | **~1,168** |
134
129
 
135
- **Key takeaways**
136
-
137
- - `readline-pager` is consistently **~2.3×–2.8× faster** than Node.js `readline`.
138
- - Relative gain increases with file size.
139
- - Sustained throughput exceeds **1.2 GB/s** on large files (machine-dependent).
130
+ **Takeaway:** `readline-pager` delivers multi-GB/s memory-to-memory throughput on large files on typical NVMe hardware; results vary with `chunkSize`, runtime (Node vs Bun), and CPU/OS.
140
131
 
141
132
  ---
142
133
 
143
- ## Iteration performance notes
144
-
145
- - **Fastest**: manual `while ((page = await pager.next()) !== null) { ... }`
146
- Avoids async-iterator protocol overhead and microtask churn.
147
-
148
- - **More ergonomic**: `for await (const page of pager) { ... }`
149
- Cleaner but slightly slower in hot paths.
150
-
151
- **Recommendation:** use the explicit `next()` loop for benchmarks and performance-critical code; use `for await...of` for clarity in less performance-sensitive code.
152
-
153
- ---
134
+ ## 🛠 Development & Contributing
154
135
 
155
- ## Development & Contributing
136
+ - Minimum supported Node.js: **v18.12** (lts/hydrogen).
137
+ - Development/test environment: **Node v25.6**, **TypeScript v5.9**.
156
138
 
157
- - Minimum supported Node.js: **18.12+** (LTS).
158
- - Development/test environment used by the author: **Node v25.6.1**, TypeScript `~5.9.x`.
159
- - To run tests & coverage:
139
+ Run tests:
160
140
 
161
141
  ```bash
162
142
  npm ci
163
143
  npm test
164
144
  ```
165
145
 
166
- If you want to contribute, open an issue or PR. A `CONTRIBUTING.md` is welcome for larger workflow notes.
146
+ Contributions are welcome — feel free to open an issue or PR.
167
147
 
168
148
  ---
169
149
 
170
- ## License
150
+ ## 📜 License
171
151
 
172
152
  MIT — © Morteza Jamshidi
package/dist/main.cjs CHANGED
@@ -1,5 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_constants = require('./constants-Cyb_UQAC.cjs');
3
2
  let node_fs_promises = require("node:fs/promises");
4
3
  let node_worker_threads = require("node:worker_threads");
5
4
 
@@ -32,7 +31,7 @@ function createPageQueue() {
32
31
  //#endregion
33
32
  //#region src/reader/backward.reader.ts
34
33
  function createBackwardReader(filepath, options) {
35
- const { pageSize, delimiter, prefetch } = options;
34
+ const { chunkSize, pageSize, delimiter, prefetch } = options;
36
35
  const pageQueue = createPageQueue();
37
36
  let fd = null;
38
37
  let pos = 0;
@@ -54,11 +53,11 @@ function createBackwardReader(filepath, options) {
54
53
  await init();
55
54
  if (!fd) return;
56
55
  while (pageQueue.queue.length < prefetch && pos > 0) {
57
- const readSize = Math.min(require_constants.CHUNK_SIZE, pos);
56
+ const readSize = Math.min(chunkSize, pos);
58
57
  pos -= readSize;
59
58
  const buf = Buffer.allocUnsafe(readSize);
60
59
  await fd.read(buf, 0, readSize, pos);
61
- buffer += buf.toString(require_constants.ENCODING);
60
+ buffer += buf.toString("utf8");
62
61
  let idx;
63
62
  while ((idx = buffer.lastIndexOf(delimiter)) !== -1) {
64
63
  const line = buffer.slice(idx + delimiter.length);
@@ -126,7 +125,7 @@ function createBackwardReader(filepath, options) {
126
125
  //#endregion
127
126
  //#region src/reader/forward.reader.ts
128
127
  function createForwardReader(filepath, options) {
129
- const { pageSize, delimiter, prefetch } = options;
128
+ const { chunkSize, pageSize, delimiter, prefetch } = options;
130
129
  const pageQueue = createPageQueue();
131
130
  let fd = null;
132
131
  let pos = 0;
@@ -149,11 +148,11 @@ function createForwardReader(filepath, options) {
149
148
  await init();
150
149
  if (!fd) return;
151
150
  while (pageQueue.queue.length < prefetch && pos < size) {
152
- const readSize = Math.min(require_constants.CHUNK_SIZE, size - pos);
151
+ const readSize = Math.min(chunkSize, size - pos);
153
152
  const buf = Buffer.allocUnsafe(readSize);
154
153
  const { bytesRead } = await fd.read(buf, 0, readSize, pos);
155
154
  pos += bytesRead;
156
- buffer += buf.toString(require_constants.ENCODING, 0, bytesRead);
155
+ buffer += buf.toString("utf8", 0, bytesRead);
157
156
  let idx;
158
157
  while ((idx = buffer.indexOf(delimiter)) !== -1) {
159
158
  const line = buffer.slice(0, idx);
@@ -216,9 +215,10 @@ function createForwardReader(filepath, options) {
216
215
  //#region src/reader/worker.reader.ts
217
216
  const workerFile = typeof {} !== "undefined" ? new URL("./worker.mjs", require("url").pathToFileURL(__filename).href) : require.resolve("./worker.cjs");
218
217
  function createWorkerReader(filepath, options) {
219
- const { pageSize, delimiter, prefetch } = options;
218
+ const { chunkSize, pageSize, delimiter, prefetch } = options;
220
219
  const worker = new node_worker_threads.Worker(new URL(workerFile, require("url").pathToFileURL(__filename).href), { workerData: {
221
220
  filepath,
221
+ chunkSize,
222
222
  pageSize,
223
223
  delimiter
224
224
  } });
@@ -274,20 +274,23 @@ function createWorkerReader(filepath, options) {
274
274
  //#endregion
275
275
  //#region src/main.ts
276
276
  function createPager(filepath, options = {}) {
277
- const { pageSize = 1e3, delimiter = "\n", prefetch = 1, backward = false, useWorker = false } = options;
277
+ const { chunkSize = 64 * 1024, pageSize = 1e3, delimiter = "\n", prefetch = 1, backward = false, useWorker = false } = options;
278
278
  if (!filepath) throw new Error("filepath required");
279
279
  if (pageSize <= 0) throw new RangeError("pageSize must be > 0");
280
280
  if (prefetch <= 0) throw new RangeError("prefetch must be >= 1");
281
281
  if (backward && useWorker) throw new Error("backward not supported with useWorker");
282
282
  return useWorker ? createWorkerReader(filepath, {
283
+ chunkSize,
283
284
  pageSize,
284
285
  prefetch,
285
286
  delimiter
286
287
  }) : backward ? createBackwardReader(filepath, {
288
+ chunkSize,
287
289
  pageSize,
288
290
  prefetch,
289
291
  delimiter
290
292
  }) : createForwardReader(filepath, {
293
+ chunkSize,
291
294
  pageSize,
292
295
  prefetch,
293
296
  delimiter
package/dist/main.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  //#region src/types.d.ts
2
2
  interface ReaderOptions {
3
+ chunkSize: number;
3
4
  pageSize: number;
4
5
  delimiter: string;
5
6
  prefetch: number;
package/dist/main.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  //#region src/types.d.ts
2
2
  interface ReaderOptions {
3
+ chunkSize: number;
3
4
  pageSize: number;
4
5
  delimiter: string;
5
6
  prefetch: number;
package/dist/main.mjs CHANGED
@@ -1,4 +1,3 @@
1
- import { n as ENCODING, t as CHUNK_SIZE } from "./constants-BNKQoOqH.mjs";
2
1
  import { createRequire } from "node:module";
3
2
  import { open } from "node:fs/promises";
4
3
  import { Worker } from "node:worker_threads";
@@ -36,7 +35,7 @@ function createPageQueue() {
36
35
  //#endregion
37
36
  //#region src/reader/backward.reader.ts
38
37
  function createBackwardReader(filepath, options) {
39
- const { pageSize, delimiter, prefetch } = options;
38
+ const { chunkSize, pageSize, delimiter, prefetch } = options;
40
39
  const pageQueue = createPageQueue();
41
40
  let fd = null;
42
41
  let pos = 0;
@@ -58,11 +57,11 @@ function createBackwardReader(filepath, options) {
58
57
  await init();
59
58
  if (!fd) return;
60
59
  while (pageQueue.queue.length < prefetch && pos > 0) {
61
- const readSize = Math.min(CHUNK_SIZE, pos);
60
+ const readSize = Math.min(chunkSize, pos);
62
61
  pos -= readSize;
63
62
  const buf = Buffer.allocUnsafe(readSize);
64
63
  await fd.read(buf, 0, readSize, pos);
65
- buffer += buf.toString(ENCODING);
64
+ buffer += buf.toString("utf8");
66
65
  let idx;
67
66
  while ((idx = buffer.lastIndexOf(delimiter)) !== -1) {
68
67
  const line = buffer.slice(idx + delimiter.length);
@@ -130,7 +129,7 @@ function createBackwardReader(filepath, options) {
130
129
  //#endregion
131
130
  //#region src/reader/forward.reader.ts
132
131
  function createForwardReader(filepath, options) {
133
- const { pageSize, delimiter, prefetch } = options;
132
+ const { chunkSize, pageSize, delimiter, prefetch } = options;
134
133
  const pageQueue = createPageQueue();
135
134
  let fd = null;
136
135
  let pos = 0;
@@ -153,11 +152,11 @@ function createForwardReader(filepath, options) {
153
152
  await init();
154
153
  if (!fd) return;
155
154
  while (pageQueue.queue.length < prefetch && pos < size) {
156
- const readSize = Math.min(CHUNK_SIZE, size - pos);
155
+ const readSize = Math.min(chunkSize, size - pos);
157
156
  const buf = Buffer.allocUnsafe(readSize);
158
157
  const { bytesRead } = await fd.read(buf, 0, readSize, pos);
159
158
  pos += bytesRead;
160
- buffer += buf.toString(ENCODING, 0, bytesRead);
159
+ buffer += buf.toString("utf8", 0, bytesRead);
161
160
  let idx;
162
161
  while ((idx = buffer.indexOf(delimiter)) !== -1) {
163
162
  const line = buffer.slice(0, idx);
@@ -220,9 +219,10 @@ function createForwardReader(filepath, options) {
220
219
  //#region src/reader/worker.reader.ts
221
220
  const workerFile = typeof import.meta !== "undefined" ? new URL("./worker.mjs", import.meta.url) : __require.resolve("./worker.cjs");
222
221
  function createWorkerReader(filepath, options) {
223
- const { pageSize, delimiter, prefetch } = options;
222
+ const { chunkSize, pageSize, delimiter, prefetch } = options;
224
223
  const worker = new Worker(new URL(workerFile, import.meta.url), { workerData: {
225
224
  filepath,
225
+ chunkSize,
226
226
  pageSize,
227
227
  delimiter
228
228
  } });
@@ -278,20 +278,23 @@ function createWorkerReader(filepath, options) {
278
278
  //#endregion
279
279
  //#region src/main.ts
280
280
  function createPager(filepath, options = {}) {
281
- const { pageSize = 1e3, delimiter = "\n", prefetch = 1, backward = false, useWorker = false } = options;
281
+ const { chunkSize = 64 * 1024, pageSize = 1e3, delimiter = "\n", prefetch = 1, backward = false, useWorker = false } = options;
282
282
  if (!filepath) throw new Error("filepath required");
283
283
  if (pageSize <= 0) throw new RangeError("pageSize must be > 0");
284
284
  if (prefetch <= 0) throw new RangeError("prefetch must be >= 1");
285
285
  if (backward && useWorker) throw new Error("backward not supported with useWorker");
286
286
  return useWorker ? createWorkerReader(filepath, {
287
+ chunkSize,
287
288
  pageSize,
288
289
  prefetch,
289
290
  delimiter
290
291
  }) : backward ? createBackwardReader(filepath, {
292
+ chunkSize,
291
293
  pageSize,
292
294
  prefetch,
293
295
  delimiter
294
296
  }) : createForwardReader(filepath, {
297
+ chunkSize,
295
298
  pageSize,
296
299
  prefetch,
297
300
  delimiter
package/dist/worker.cjs CHANGED
@@ -1,9 +1,8 @@
1
- const require_constants = require('./constants-Cyb_UQAC.cjs');
2
1
  let node_fs_promises = require("node:fs/promises");
3
2
  let node_worker_threads = require("node:worker_threads");
4
3
 
5
4
  //#region src/worker.ts
6
- const { filepath, pageSize, delimiter } = node_worker_threads.workerData;
5
+ const { filepath, chunkSize, pageSize, delimiter } = node_worker_threads.workerData;
7
6
  (async () => {
8
7
  const fd = await (0, node_fs_promises.open)(filepath, "r");
9
8
  const { size } = await fd.stat();
@@ -11,7 +10,7 @@ const { filepath, pageSize, delimiter } = node_worker_threads.workerData;
11
10
  let buffer = "";
12
11
  const local = [];
13
12
  while (pos < size) {
14
- const readSize = Math.min(require_constants.CHUNK_SIZE, size - pos);
13
+ const readSize = Math.min(chunkSize, size - pos);
15
14
  const buf = Buffer.allocUnsafe(readSize);
16
15
  const { bytesRead } = await fd.read(buf, 0, readSize, pos);
17
16
  pos += bytesRead;
package/dist/worker.mjs CHANGED
@@ -1,9 +1,8 @@
1
- import { t as CHUNK_SIZE } from "./constants-BNKQoOqH.mjs";
2
1
  import { open } from "node:fs/promises";
3
2
  import { parentPort, workerData } from "node:worker_threads";
4
3
 
5
4
  //#region src/worker.ts
6
- const { filepath, pageSize, delimiter } = workerData;
5
+ const { filepath, chunkSize, pageSize, delimiter } = workerData;
7
6
  (async () => {
8
7
  const fd = await open(filepath, "r");
9
8
  const { size } = await fd.stat();
@@ -11,7 +10,7 @@ const { filepath, pageSize, delimiter } = workerData;
11
10
  let buffer = "";
12
11
  const local = [];
13
12
  while (pos < size) {
14
- const readSize = Math.min(CHUNK_SIZE, size - pos);
13
+ const readSize = Math.min(chunkSize, size - pos);
15
14
  const buf = Buffer.allocUnsafe(readSize);
16
15
  const { bytesRead } = await fd.read(buf, 0, readSize, pos);
17
16
  pos += bytesRead;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "readline-pager",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "scripts": {
5
5
  "build": "tsdown",
6
6
  "pretest": "npm run build",
@@ -8,6 +8,7 @@
8
8
  "prepublishOnly": "npm run build && npm run test"
9
9
  },
10
10
  "devDependencies": {
11
+ "@types/bun": "~1.3.9",
11
12
  "@types/node": "~25.3.0",
12
13
  "prettier": "~3.8.1",
13
14
  "prettier-plugin-organize-imports": "~4.3.0",
@@ -1,6 +0,0 @@
1
- //#region src/constants.ts
2
- const CHUNK_SIZE = 64 * 1024;
3
- const ENCODING = "utf8";
4
-
5
- //#endregion
6
- export { ENCODING as n, CHUNK_SIZE as t };
@@ -1,18 +0,0 @@
1
-
2
- //#region src/constants.ts
3
- const CHUNK_SIZE = 64 * 1024;
4
- const ENCODING = "utf8";
5
-
6
- //#endregion
7
- Object.defineProperty(exports, 'CHUNK_SIZE', {
8
- enumerable: true,
9
- get: function () {
10
- return CHUNK_SIZE;
11
- }
12
- });
13
- Object.defineProperty(exports, 'ENCODING', {
14
- enumerable: true,
15
- get: function () {
16
- return ENCODING;
17
- }
18
- });