readline-pager 0.2.2 → 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,16 +1,21 @@
1
1
  # 📄 readline-pager
2
2
 
3
+ <p align="center"><img src="logo.webp" alt="logo" width="349"></p>
4
+
3
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`)
10
+ - ✅ Async iterator (`for await...of`) + manual `next()` API
9
11
  - ✅ Forward & backward reading (EOF → BOF)
10
12
  - ✅ Optional worker thread mode (forward only)
11
13
  - ✅ Up to ~3× faster than Node.js `readline`
12
14
  - ✅ ~97% test coverage & fully typed (TypeScript)
13
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.
18
+
14
19
  ---
15
20
 
16
21
  ## 📦 Installation
@@ -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,24 +40,25 @@ for await (const page of pager) {
35
40
 
36
41
  ---
37
42
 
38
- ## ⚡ Manual iteration (recommended for maximum 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
 
@@ -60,19 +66,21 @@ while ((page = await pager.next()) !== null) {
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 number of pages buffered internally. Higher values increase throughput but use 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` — offload parsing to a worker thread (forward mode only).
83
+ - `useWorker` — offload parsing to a worker thread (forward only).
76
84
 
77
85
  ---
78
86
 
@@ -87,23 +95,15 @@ Returns the next page or `null` when finished. Empty lines are preserved.
87
95
  - A completely empty file (`0` bytes) produces `[""]` on the first read.
88
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.
89
97
 
90
- ✅ Key points:
91
-
92
- - A 0-byte file → `[""]`
93
- - Consecutive `\n\n` → `["", ""]`
94
- - Node.js `readline` may skip initial empty line(s) and emit nothing for empty files.
95
-
96
98
  ### `pager.close(): void`
97
99
 
98
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 per 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
 
@@ -115,49 +115,26 @@ Run the included benchmark:
115
115
  # default run
116
116
  node test/_benchmark.ts
117
117
 
118
- # 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 Linux machine (SSD + fast CPU) using generated files.
123
-
124
- ### Summary (averages)
125
-
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** |
134
-
135
- **Key takeaways**
136
-
137
- - `readline-pager` is consistently **~2.3×–2.8× faster** than Node.js `readline`.
138
- - Relative performance gains increase with file size.
139
- - Sustained throughput exceeds **1.2 GB/s** on large files (machine-dependent).
140
-
141
- ---
142
-
143
- ## 🧠 Iteration Performance Notes
144
-
145
- - **Fastest**: manual
146
- `while ((page = await pager.next()) !== null) { ... }`
147
- Avoids async-iterator protocol overhead and microtask churn.
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.
148
123
 
149
- - **More ergonomic**:
150
- `for await (const page of pager) { ... }`
151
- Cleaner, but slightly slower in hot paths.
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** |
152
129
 
153
- **Recommendation:** use the explicit `next()` loop for benchmarks and performance-critical workloads; use `for await...of` for clarity elsewhere.
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.
154
131
 
155
132
  ---
156
133
 
157
134
  ## 🛠 Development & Contributing
158
135
 
159
- - Minimum supported Node.js: **18.12+** (LTS).
160
- - Development/test environment: **Node v25.6.1**, TypeScript `~5.9.x`.
136
+ - Minimum supported Node.js: **v18.12** (lts/hydrogen).
137
+ - Development/test environment: **Node v25.6**, **TypeScript v5.9**.
161
138
 
162
139
  Run tests:
163
140
 
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.2",
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
- });