( path: string, )
| 720 | * @returns An async generator that yields lines in reverse order |
| 721 | */ |
| 722 | export async function* readLinesReverse( |
| 723 | path: string, |
| 724 | ): AsyncGenerator<string, void, undefined> { |
| 725 | const CHUNK_SIZE = 1024 * 4 |
| 726 | const fileHandle = await open(path, 'r') |
| 727 | try { |
| 728 | const stats = await fileHandle.stat() |
| 729 | let position = stats.size |
| 730 | // Carry raw bytes (not a decoded string) across chunk boundaries so that |
| 731 | // multi-byte UTF-8 sequences split by the 4KB boundary are not corrupted. |
| 732 | // Decoding per-chunk would turn a split sequence into U+FFFD on both sides, |
| 733 | // which for history.jsonl means JSON.parse throws and the entry is dropped. |
| 734 | let remainder = Buffer.alloc(0) |
| 735 | const buffer = Buffer.alloc(CHUNK_SIZE) |
| 736 | |
| 737 | while (position > 0) { |
| 738 | const currentChunkSize = Math.min(CHUNK_SIZE, position) |
| 739 | position -= currentChunkSize |
| 740 | |
| 741 | await fileHandle.read(buffer, 0, currentChunkSize, position) |
| 742 | const combined = Buffer.concat([ |
| 743 | buffer.subarray(0, currentChunkSize), |
| 744 | remainder, |
| 745 | ]) |
| 746 | |
| 747 | const firstNewline = combined.indexOf(0x0a) |
| 748 | if (firstNewline === -1) { |
| 749 | remainder = combined |
| 750 | continue |
| 751 | } |
| 752 | |
| 753 | remainder = Buffer.from(combined.subarray(0, firstNewline)) |
| 754 | const lines = combined.toString('utf8', firstNewline + 1).split('\n') |
| 755 | |
| 756 | for (let i = lines.length - 1; i >= 0; i--) { |
| 757 | const line = lines[i]! |
| 758 | if (line) { |
| 759 | yield line |
| 760 | } |
| 761 | } |
| 762 | } |
| 763 | |
| 764 | if (remainder.length > 0) { |
| 765 | yield remainder.toString('utf8') |
| 766 | } |
| 767 | } finally { |
| 768 | await fileHandle.close() |
| 769 | } |
| 770 | } |
| 771 |
no test coverage detected