readline-pager 0.2.6 → 0.3.0

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/dist/main.d.mts CHANGED
@@ -9,12 +9,10 @@ interface PagerOptions extends Partial<ReaderOptions> {
9
9
  backward?: boolean;
10
10
  useWorker?: boolean;
11
11
  }
12
- interface Pager extends AsyncIterable<string[]> {
12
+ interface Pager extends AsyncIterable<string[]>, Iterable<string[]> {
13
13
  next(): Promise<string[] | null>;
14
- close(): void;
15
- readonly lineCount: number;
16
- readonly firstLine: string | null;
17
- readonly lastLine: string | null;
14
+ nextSync(): string[] | null;
15
+ close(): Promise<void>;
18
16
  }
19
17
  //#endregion
20
18
  //#region src/main.d.ts
package/dist/main.mjs CHANGED
@@ -1,67 +1,189 @@
1
1
  import { createRequire } from "node:module";
2
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
2
3
  import { open } from "node:fs/promises";
3
4
  import { Worker } from "node:worker_threads";
4
-
5
5
  //#region \0rolldown/runtime.js
6
6
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
7
-
8
7
  //#endregion
9
8
  //#region src/queue.ts
10
- function createPageQueue() {
11
- const queue = [];
12
- let resolver = null;
9
+ function createRingBuffer(capacity) {
10
+ if (!Number.isFinite(capacity) || capacity <= 0) throw new RangeError("capacity must be a positive number");
11
+ let buf = new Array(capacity);
12
+ let head = 0;
13
+ let tail = 0;
14
+ let count = 0;
15
+ let consumerWaiter = null;
16
+ let producerWaiter = null;
17
+ function push(item) {
18
+ if (count === buf.length) {
19
+ const newCap = buf.length * 2;
20
+ const newBuf = new Array(newCap);
21
+ for (let i = 0; i < count; i++) newBuf[i] = buf[(head + i) % buf.length];
22
+ buf = newBuf;
23
+ head = 0;
24
+ tail = count;
25
+ }
26
+ buf[tail] = item;
27
+ tail++;
28
+ if (tail === buf.length) tail = 0;
29
+ count++;
30
+ if (consumerWaiter) {
31
+ const w = consumerWaiter;
32
+ consumerWaiter = null;
33
+ w();
34
+ }
35
+ }
36
+ function shiftSync() {
37
+ if (count === 0) return null;
38
+ const v = buf[head];
39
+ buf[head] = void 0;
40
+ head++;
41
+ if (head === buf.length) head = 0;
42
+ count--;
43
+ if (producerWaiter) {
44
+ const w = producerWaiter;
45
+ producerWaiter = null;
46
+ w();
47
+ }
48
+ return v;
49
+ }
50
+ async function shift(done = false) {
51
+ if (count) return shiftSync();
52
+ if (done) return null;
53
+ await new Promise((r) => {
54
+ consumerWaiter = r;
55
+ });
56
+ if (count) return shiftSync();
57
+ return null;
58
+ }
59
+ function wake() {
60
+ if (consumerWaiter) {
61
+ const w = consumerWaiter;
62
+ consumerWaiter = null;
63
+ w();
64
+ }
65
+ if (producerWaiter) {
66
+ const w = producerWaiter;
67
+ producerWaiter = null;
68
+ w();
69
+ }
70
+ }
71
+ function clear() {
72
+ for (let i = 0; i < buf.length; i++) buf[i] = void 0;
73
+ head = 0;
74
+ tail = 0;
75
+ count = 0;
76
+ wake();
77
+ }
13
78
  return {
14
- queue,
15
- push(page) {
16
- queue.push(page);
17
- resolver?.();
18
- resolver = null;
19
- },
20
- wake() {
21
- resolver?.();
22
- resolver = null;
79
+ push,
80
+ shift,
81
+ shiftSync,
82
+ wake,
83
+ clear,
84
+ get count() {
85
+ return count;
23
86
  },
24
- async shift(done) {
25
- if (queue.length) return queue.shift();
26
- if (done()) return null;
27
- await new Promise((r) => resolver = r);
28
- if (queue.length) return queue.shift();
29
- if (done()) return null;
30
- return null;
87
+ get capacity() {
88
+ return buf.length;
31
89
  }
32
90
  };
33
91
  }
34
-
35
92
  //#endregion
36
93
  //#region src/reader/backward.reader.ts
37
94
  function createBackwardReader(filepath, options) {
38
95
  const { chunkSize, pageSize, delimiter, prefetch } = options;
39
- const pageQueue = createPageQueue();
96
+ const pageQueue = createRingBuffer(Math.max(2, prefetch + 1));
97
+ const local = [];
40
98
  let fd = null;
99
+ let fdSync = null;
41
100
  let pos = 0;
42
101
  let buffer = "";
43
102
  let done = false;
44
103
  let closed = false;
45
- let emittedCount = 0;
46
- let firstLine = null;
47
- let lastLine = null;
48
- const local = [];
49
- async function init() {
50
- if (fd) return;
51
- fd = await open(filepath, "r");
52
- pos = (await fd.stat()).size;
53
- if (pos === 0) done = true;
104
+ let startsWithDelimiter = false;
105
+ fdSync = openSync(filepath, "r");
106
+ pos = statSync(filepath).size;
107
+ if (pos === 0) {
108
+ pageQueue.push([buffer]);
109
+ done = true;
110
+ pageQueue.wake();
54
111
  }
55
- async function fill() {
112
+ (async () => {
113
+ try {
114
+ fd = await open(filepath, "r");
115
+ pos = (await fd.stat()).size;
116
+ if (pos === 0) {
117
+ if (!done) {
118
+ pageQueue.push([buffer]);
119
+ done = true;
120
+ }
121
+ if (fd) {
122
+ await fd.close();
123
+ fd = null;
124
+ }
125
+ pageQueue.wake();
126
+ return;
127
+ }
128
+ while (!done && !closed) {
129
+ while (pageQueue.count < prefetch && pos > 0 && !closed) {
130
+ const readSize = Math.min(chunkSize, pos);
131
+ pos -= readSize;
132
+ const buf = Buffer.allocUnsafe(readSize);
133
+ await fd.read(buf, 0, readSize, pos);
134
+ buffer = buf.toString("utf8") + buffer;
135
+ if (pos === 0 && buffer.startsWith(delimiter)) startsWithDelimiter = true;
136
+ let idx;
137
+ while ((idx = buffer.lastIndexOf(delimiter)) !== -1) {
138
+ const line = buffer.slice(idx + delimiter.length);
139
+ buffer = buffer.slice(0, idx);
140
+ local.push(line);
141
+ while (local.length >= pageSize) {
142
+ const page = local.splice(0, pageSize);
143
+ pageQueue.push(page);
144
+ }
145
+ }
146
+ }
147
+ if (pos === 0 && !done) {
148
+ if (buffer.length > 0) local.push(buffer);
149
+ else if (startsWithDelimiter) local.push("");
150
+ buffer = "";
151
+ while (local.length > 0 && !closed) {
152
+ const page = local.slice(local.length - Math.min(pageSize, local.length));
153
+ local.length -= page.length;
154
+ pageQueue.push(page);
155
+ }
156
+ done = true;
157
+ if (fd) {
158
+ await fd.close();
159
+ fd = null;
160
+ }
161
+ pageQueue.wake();
162
+ break;
163
+ }
164
+ if (!done && !closed) await new Promise((r) => setImmediate(r));
165
+ }
166
+ } catch {
167
+ done = true;
168
+ pageQueue.wake();
169
+ try {
170
+ if (fd) {
171
+ await fd.close();
172
+ fd = null;
173
+ }
174
+ } catch {}
175
+ }
176
+ })();
177
+ function fillSync() {
56
178
  if (done || closed) return;
57
- await init();
58
- if (!fd) return;
59
- while (pageQueue.queue.length < prefetch && pos > 0) {
179
+ if (fdSync === null) return;
180
+ while (pageQueue.count < prefetch && pos > 0 && !closed) {
60
181
  const readSize = Math.min(chunkSize, pos);
61
182
  pos -= readSize;
62
183
  const buf = Buffer.allocUnsafe(readSize);
63
- await fd.read(buf, 0, readSize, pos);
64
- buffer += buf.toString("utf8");
184
+ readSync(fdSync, buf, 0, readSize, pos);
185
+ buffer = buf.toString("utf8") + buffer;
186
+ if (pos === 0 && buffer.startsWith(delimiter)) startsWithDelimiter = true;
65
187
  let idx;
66
188
  while ((idx = buffer.lastIndexOf(delimiter)) !== -1) {
67
189
  const line = buffer.slice(idx + delimiter.length);
@@ -73,90 +195,178 @@ function createBackwardReader(filepath, options) {
73
195
  }
74
196
  }
75
197
  }
76
- if (pos === 0) {
77
- local.push(buffer);
198
+ if (pos === 0 && !done) {
199
+ if (buffer.length > 0) local.push(buffer);
200
+ else if (startsWithDelimiter) local.push("");
78
201
  buffer = "";
79
202
  while (local.length > 0) {
80
- const sliceSize = Math.min(pageSize, local.length);
81
- const page = local.splice(local.length - sliceSize, sliceSize);
203
+ const page = local.slice(local.length - Math.min(pageSize, local.length));
204
+ local.length -= page.length;
82
205
  pageQueue.push(page);
83
206
  }
84
207
  done = true;
85
- if (fd) {
86
- await fd.close();
87
- fd = null;
208
+ if (fdSync !== null) {
209
+ closeSync(fdSync);
210
+ fdSync = null;
88
211
  }
212
+ pageQueue.wake();
89
213
  }
90
214
  }
91
215
  async function next() {
92
216
  if (closed) return null;
93
- await fill();
94
- const page = await pageQueue.shift(() => done);
95
- if (!page) return null;
96
- emittedCount += page.length;
97
- firstLine ??= page[0];
98
- lastLine = page[page.length - 1];
99
- return page;
217
+ return await pageQueue.shift(done);
218
+ }
219
+ function nextSync() {
220
+ if (closed) return null;
221
+ fillSync();
222
+ return pageQueue.shiftSync();
100
223
  }
101
224
  async function close() {
102
225
  closed = true;
103
226
  done = true;
104
- pageQueue.queue.length = 0;
105
- if (fd) await fd.close();
227
+ pageQueue.clear();
228
+ if (fd) {
229
+ try {
230
+ await fd.close();
231
+ } catch {}
232
+ fd = null;
233
+ }
234
+ if (fdSync !== null) {
235
+ try {
236
+ closeSync(fdSync);
237
+ } catch {}
238
+ fdSync = null;
239
+ }
106
240
  }
107
241
  return {
108
242
  next,
243
+ nextSync,
109
244
  close,
110
- get lineCount() {
111
- return emittedCount;
112
- },
113
- get firstLine() {
114
- return firstLine;
115
- },
116
- get lastLine() {
117
- return lastLine;
118
- },
119
245
  async *[Symbol.asyncIterator]() {
120
- while (true) {
121
- const p = await next();
122
- if (!p) break;
123
- yield p;
246
+ try {
247
+ while (true) {
248
+ const p = await next();
249
+ if (!p) break;
250
+ yield p;
251
+ }
252
+ } finally {
253
+ await close();
254
+ }
255
+ },
256
+ *[Symbol.iterator]() {
257
+ try {
258
+ while (true) {
259
+ const p = nextSync();
260
+ if (!p) break;
261
+ yield p;
262
+ }
263
+ } finally {
264
+ closed = true;
265
+ done = true;
266
+ pageQueue.clear();
267
+ try {
268
+ if (fdSync) closeSync(fdSync);
269
+ } catch {}
270
+ fdSync = null;
271
+ try {
272
+ if (fd?.fd) closeSync(fd.fd);
273
+ } catch {}
274
+ fd = null;
124
275
  }
125
276
  }
126
277
  };
127
278
  }
128
-
129
279
  //#endregion
130
280
  //#region src/reader/forward.reader.ts
131
281
  function createForwardReader(filepath, options) {
132
282
  const { chunkSize, pageSize, delimiter, prefetch } = options;
133
- const pageQueue = createPageQueue();
283
+ const pageQueue = createRingBuffer(Math.max(2, prefetch + 1));
284
+ const local = [];
134
285
  let fd = null;
286
+ let fdSync = null;
135
287
  let pos = 0;
136
288
  let size = 0;
137
289
  let buffer = "";
138
290
  let done = false;
139
291
  let closed = false;
140
- let emittedCount = 0;
141
- let firstLine = null;
142
- let lastLine = null;
143
- const local = [];
144
- async function init() {
145
- if (fd) return;
146
- fd = await open(filepath, "r");
147
- size = (await fd.stat()).size;
148
- if (size === 0) done = true;
292
+ let flushed = false;
293
+ fdSync = openSync(filepath, "r");
294
+ size = statSync(filepath).size;
295
+ if (size === 0) {
296
+ pageQueue.push([buffer]);
297
+ done = true;
298
+ pageQueue.wake();
149
299
  }
150
- async function fill() {
300
+ (async () => {
301
+ try {
302
+ fd = await open(filepath, "r");
303
+ size = (await fd.stat()).size;
304
+ if (size === 0) {
305
+ if (!done) {
306
+ pageQueue.push([buffer]);
307
+ done = true;
308
+ }
309
+ if (fd) {
310
+ await fd.close();
311
+ fd = null;
312
+ }
313
+ pageQueue.wake();
314
+ return;
315
+ }
316
+ while (!done && !closed) {
317
+ while (pageQueue.count < prefetch && pos < size && !closed) {
318
+ const readSize = Math.min(chunkSize, size - pos);
319
+ const buf = Buffer.allocUnsafe(readSize);
320
+ const { bytesRead } = await fd.read(buf, 0, readSize, pos);
321
+ pos += bytesRead;
322
+ buffer = buffer + buf.toString("utf8", 0, bytesRead);
323
+ let idx;
324
+ while ((idx = buffer.indexOf(delimiter)) !== -1) {
325
+ const line = buffer.slice(0, idx);
326
+ buffer = buffer.slice(idx + delimiter.length);
327
+ local.push(line);
328
+ while (local.length >= pageSize) pageQueue.push(local.splice(0, pageSize));
329
+ }
330
+ }
331
+ if (pos >= size && !flushed) {
332
+ flushed = true;
333
+ local.push(buffer.length > 0 ? buffer : "");
334
+ buffer = "";
335
+ while (local.length > 0 && !closed) {
336
+ const page = local.slice(0, pageSize);
337
+ local.length -= page.length;
338
+ pageQueue.push(page);
339
+ }
340
+ done = true;
341
+ if (fd) {
342
+ await fd.close();
343
+ fd = null;
344
+ }
345
+ pageQueue.wake();
346
+ break;
347
+ }
348
+ if (!done && !closed) await new Promise((r) => setImmediate(r));
349
+ }
350
+ } catch {
351
+ done = true;
352
+ pageQueue.wake();
353
+ try {
354
+ if (fd) {
355
+ await fd.close();
356
+ fd = null;
357
+ }
358
+ } catch {}
359
+ }
360
+ })();
361
+ function fillSync() {
151
362
  if (done || closed) return;
152
- await init();
153
- if (!fd) return;
154
- while (pageQueue.queue.length < prefetch && pos < size) {
363
+ if (fdSync === null) return;
364
+ while (pageQueue.count < prefetch && pos < size && !closed) {
155
365
  const readSize = Math.min(chunkSize, size - pos);
156
366
  const buf = Buffer.allocUnsafe(readSize);
157
- const { bytesRead } = await fd.read(buf, 0, readSize, pos);
367
+ const bytesRead = readSync(fdSync, buf, 0, readSize, pos);
158
368
  pos += bytesRead;
159
- buffer += buf.toString("utf8", 0, bytesRead);
369
+ buffer = buffer + buf.toString("utf8", 0, bytesRead);
160
370
  let idx;
161
371
  while ((idx = buffer.indexOf(delimiter)) !== -1) {
162
372
  const line = buffer.slice(0, idx);
@@ -165,73 +375,102 @@ function createForwardReader(filepath, options) {
165
375
  while (local.length >= pageSize) pageQueue.push(local.splice(0, pageSize));
166
376
  }
167
377
  }
168
- if (pos >= size) {
169
- const parts = buffer.length > 0 ? buffer.split(delimiter) : [""];
170
- for (const line of parts) local.push(line);
378
+ if (pos >= size && !flushed) {
379
+ flushed = true;
380
+ local.push(buffer.length > 0 ? buffer : "");
171
381
  buffer = "";
172
- while (local.length > 0) pageQueue.push(local.splice(0, pageSize));
382
+ while (local.length > 0) {
383
+ const page = local.slice(0, pageSize);
384
+ local.length -= page.length;
385
+ pageQueue.push(page);
386
+ }
173
387
  done = true;
174
- if (fd) {
175
- await fd.close();
176
- fd = null;
388
+ if (fdSync !== null) {
389
+ closeSync(fdSync);
390
+ fdSync = null;
177
391
  }
392
+ pageQueue.wake();
178
393
  }
179
394
  }
180
395
  async function next() {
181
396
  if (closed) return null;
182
- await fill();
183
- const page = await pageQueue.shift(() => done);
184
- if (!page) return null;
185
- emittedCount += page.length;
186
- firstLine ??= page[0];
187
- lastLine = page[page.length - 1];
188
- return page;
397
+ return await pageQueue.shift(done);
398
+ }
399
+ function nextSync() {
400
+ if (closed) return null;
401
+ fillSync();
402
+ return pageQueue.shiftSync();
189
403
  }
190
404
  async function close() {
191
405
  closed = true;
192
406
  done = true;
193
- pageQueue.queue.length = 0;
194
- if (fd) await fd.close();
407
+ pageQueue.clear();
408
+ if (fd) {
409
+ try {
410
+ await fd.close();
411
+ } catch {}
412
+ fd = null;
413
+ }
414
+ if (fdSync !== null) {
415
+ try {
416
+ closeSync(fdSync);
417
+ } catch {}
418
+ fdSync = null;
419
+ }
195
420
  }
196
421
  return {
197
422
  next,
423
+ nextSync,
198
424
  close,
199
- get lineCount() {
200
- return emittedCount;
201
- },
202
- get firstLine() {
203
- return firstLine;
204
- },
205
- get lastLine() {
206
- return lastLine;
207
- },
208
425
  async *[Symbol.asyncIterator]() {
209
- while (true) {
210
- const p = await next();
211
- if (!p) break;
212
- yield p;
426
+ try {
427
+ while (true) {
428
+ const p = await next();
429
+ if (!p) break;
430
+ yield p;
431
+ }
432
+ } finally {
433
+ await close();
434
+ }
435
+ },
436
+ *[Symbol.iterator]() {
437
+ try {
438
+ while (true) {
439
+ const p = nextSync();
440
+ if (!p) break;
441
+ yield p;
442
+ }
443
+ } finally {
444
+ closed = true;
445
+ done = true;
446
+ pageQueue.clear();
447
+ try {
448
+ if (fdSync !== null) closeSync(fdSync);
449
+ } catch {}
450
+ fdSync = null;
451
+ try {
452
+ if (fd?.fd) closeSync(fd.fd);
453
+ } catch {}
454
+ fd = null;
213
455
  }
214
456
  }
215
457
  };
216
458
  }
217
-
218
459
  //#endregion
219
460
  //#region src/reader/worker.reader.ts
220
461
  const workerFile = typeof import.meta !== "undefined" ? new URL("./worker.mjs", import.meta.url) : __require.resolve("./worker.cjs");
221
462
  function createWorkerReader(filepath, options) {
222
463
  const { chunkSize, pageSize, delimiter, prefetch } = options;
464
+ const pageQueue = createRingBuffer(Math.max(2, prefetch + 1));
465
+ let done = false;
466
+ let closed = false;
223
467
  const worker = new Worker(new URL(workerFile, import.meta.url), { workerData: {
224
468
  filepath,
225
469
  chunkSize,
226
470
  pageSize,
227
- delimiter
471
+ delimiter,
472
+ prefetch
228
473
  } });
229
- const pageQueue = createPageQueue();
230
- let done = false;
231
- let closed = false;
232
- let emittedCount = 0;
233
- let firstLine = null;
234
- let lastLine = null;
235
474
  worker.on("message", (msg) => {
236
475
  if (msg.type === "page") pageQueue.push(msg.data);
237
476
  if (msg.type === "done") {
@@ -239,49 +478,66 @@ function createWorkerReader(filepath, options) {
239
478
  pageQueue.wake();
240
479
  }
241
480
  });
481
+ worker.on("error", () => {
482
+ done = true;
483
+ pageQueue.wake();
484
+ });
485
+ worker.on("exit", () => {
486
+ done = true;
487
+ pageQueue.wake();
488
+ });
242
489
  async function next() {
243
490
  if (closed) return null;
244
- const page = await pageQueue.shift(() => done);
245
- if (!page) return null;
246
- emittedCount += page.length;
247
- firstLine ??= page[0];
248
- lastLine = page[page.length - 1];
249
- return page;
491
+ return await pageQueue.shift(done);
492
+ }
493
+ function nextSync() {
494
+ if (closed) return null;
495
+ return pageQueue.shiftSync();
250
496
  }
251
497
  async function close() {
252
498
  closed = true;
253
499
  done = true;
254
- worker.terminate();
500
+ pageQueue.clear();
501
+ await worker.terminate();
502
+ }
503
+ function tryClose() {
504
+ close().catch(() => {});
255
505
  }
256
506
  return {
257
507
  next,
508
+ nextSync,
258
509
  close,
259
- get lineCount() {
260
- return emittedCount;
261
- },
262
- get firstLine() {
263
- return firstLine;
264
- },
265
- get lastLine() {
266
- return lastLine;
267
- },
268
510
  async *[Symbol.asyncIterator]() {
269
- while (true) {
270
- const p = await next();
271
- if (!p) break;
272
- yield p;
511
+ try {
512
+ while (true) {
513
+ const p = await next();
514
+ if (!p) break;
515
+ yield p;
516
+ }
517
+ } finally {
518
+ tryClose();
519
+ }
520
+ },
521
+ *[Symbol.iterator]() {
522
+ try {
523
+ while (true) {
524
+ const p = nextSync();
525
+ if (!p) break;
526
+ yield p;
527
+ }
528
+ } finally {
529
+ tryClose();
273
530
  }
274
531
  }
275
532
  };
276
533
  }
277
-
278
534
  //#endregion
279
535
  //#region src/main.ts
280
536
  function createPager(filepath, options = {}) {
281
- const { chunkSize = 64 * 1024, pageSize = 1e3, delimiter = "\n", prefetch = 1, backward = false, useWorker = false } = options;
537
+ const { chunkSize = 64 * 1024, pageSize = 1e3, delimiter = "\n", prefetch = 8, backward = false, useWorker = false } = options;
282
538
  if (!filepath) throw new Error("filepath required");
283
- if (pageSize <= 0) throw new RangeError("pageSize must be > 0");
284
- if (prefetch <= 0) throw new RangeError("prefetch must be >= 1");
539
+ if (pageSize < 1) throw new RangeError("pageSize must be >= 1");
540
+ if (prefetch < 1) throw new RangeError("prefetch must be >= 1");
285
541
  if (backward && useWorker) throw new Error("backward not supported with useWorker");
286
542
  return useWorker ? createWorkerReader(filepath, {
287
543
  chunkSize,
@@ -300,6 +556,5 @@ function createPager(filepath, options = {}) {
300
556
  delimiter
301
557
  });
302
558
  }
303
-
304
559
  //#endregion
305
- export { createPager, createPager as default };
560
+ export { createPager, createPager as default };