UNPKG

@loom-io/core

Version:

A file system wrapper for Node.js and Bun

223 lines (222 loc) 8.05 kB
import { LineResult, SearchResult } from "../helper/result.js"; import { TextItemList } from "../helper/textItemList.js"; export class Editor { ref; file; chunkSize = 1024; lineInfo; newLineCharacter = Buffer.from("\n"); //0x0a = \n currentLine = 0; EOF = false; static async from(adapter, file) { const handler = await adapter.openFile(file.path); return new Editor(file, handler); } constructor(ref, file) { this.ref = ref; this.file = file; // TODO: watch file for changes //fs.watch(ref.path); } /** * Get the raw file handle */ get raw() { return this.file; } async getSizeInBytes() { return await this.ref.getSizeInBytes(); } /** * Close the file */ async close() { await this.file.close(); } /** * Symbol to close automatically with using */ async [Symbol.asyncDispose]() { await this.close(); } [Symbol.dispose]() { throw new Error("Method not implemented."); } /** * Alias for searchFirst * * @param value - value to search * @returns */ async search(value) { return await this.searchFirst(value); } /** * Search for a string in the file by chunking the file into pieces to avoid memory overflow. * set start to 'EOF' to search from the end of the file. * * @param value - value to search * @param start - start position in the file */ async searchFirst(value, start = 0) { const searchValue = Buffer.from(value); const fileSize = await this.getSizeInBytes(); const item = await this.loopForward(searchValue, start, fileSize); if (item === undefined) { return undefined; } return new SearchResult(item, searchValue, this); } /** * Search for a string in the file by chunking the file into pieces to avoid memory overflow. * set start to 'EOF' to search from the end of the file. * * @param value - value to search * @param start - last value included in the search */ async searchLast(value, start) { const searchValue = Buffer.from(value); const item = await this.loopReverse(searchValue, 0, start || (await this.getSizeInBytes())); if (item === undefined) { return undefined; } return new SearchResult(item, searchValue, this); } calcChunkSize(valueLength) { return this.chunkSize > valueLength ? this.chunkSize : valueLength * 3; } async loopForward(value, first, last) { let position = first; const valueLength = value.byteLength; const chunkSize = this.calcChunkSize(valueLength); let item = undefined; const length = chunkSize + valueLength; do { const chunk = await this.read(position, length); const matches = this.searchInChunk(value, chunk); item = this.convertChunkMatchesToItems(matches, valueLength, position); position += chunkSize - Math.floor(valueLength / 2); } while (item === undefined && position < last); return item?.getFirstItem(); } async loopReverse(value, first = 0, last) { let position = last; const valueLength = value.byteLength; const chunkSize = this.calcChunkSize(valueLength); let item; do { const param = this.loopReverseCalcNextChunk(position, chunkSize, valueLength, first); ({ position } = param); const chunk = (await this.file.read(param)).buffer; const matches = this.searchInChunk(value, chunk); item = this.convertChunkMatchesToItems(matches, valueLength, position, true); } while (item === undefined && position > first); return item?.getLastItem(); } searchInChunk(value, chunk) { const results = []; let i = 0; while ((i = chunk.indexOf(value, i)) !== -1) { results.push(i); i += value.byteLength; } return results; } /** * Generate the next chunk position and length for fs.read function * * @param current - Start position of the last chunk * @param chunkSize - chunk size of the last chunk * @param valueLength - length of the value to search * @param min - minimum positive position in file * @returns */ loopReverseCalcNextChunk(current, chunkSize, valueLength, min) { let nextPosition = current - (chunkSize + Math.floor(valueLength / 2)); let length = chunkSize + valueLength; if (nextPosition < min) { nextPosition = min; length = current - min + Math.floor(valueLength / 2); } return { position: nextPosition, length }; } convertChunkMatchesToItems(matches, valueLength, chunkPosition, isReverseRead = false) { return matches.reduce((item, match) => { const start = chunkPosition + match; const end = start + valueLength; const newItem = new TextItemList({ start, end, readReverse: isReverseRead, }); item?.add(newItem); return item ?? newItem; }, undefined); } /** * Read a chunk of the file * * @param start - start position in the file * @param length - length of the data to read * @returns - return a buffer with the data read from the file */ async read(start, length) { const buffer = Buffer.alloc(length); const data = await this.file.read(buffer, { position: start }); return data.buffer; } async handleFileWithOnlyOneLine(separator) { const fileSize = await this.getSizeInBytes(); const item = new TextItemList({ start: 0, end: fileSize + separator.length, }); return new LineResult(item.getFirstItem(), separator, this); } /** * Analyze the file junkvisely till the first line is found * and return a LineResult object to read the line or step to the next one. * * @param separator - line separator to use, default is new line character LF * @returns - return a LineResult object witch allow you to read line by line forward (next) and backward (prev) */ async firstLine(separator = this.newLineCharacter) { const bSeparator = Buffer.from(separator); const fileSize = await this.getSizeInBytes(); const item = await this.loopForward(bSeparator, 0, fileSize); if (item === undefined) { return await this.handleFileWithOnlyOneLine(bSeparator); } const first = item.getFirstItem(); TextItemList.patch(first, { ...first.content, start: 0, }); return new LineResult(first, bSeparator, this); } /** * Analyze the file junkvisely from the end till the last line is found * and return a LineResult object to read the line or step to the previous one. * * @param separator - line separator to use, default is new line character LF * @returns - return a LineResult object witch allow you to read line by line forward (next) and backward (prev) */ async lastLine(separator = this.newLineCharacter) { const bSeparator = Buffer.from(separator); const fileSize = await this.getSizeInBytes(); const item = await this.loopReverse(bSeparator, 0, fileSize); if (item === undefined) { return await this.handleFileWithOnlyOneLine(bSeparator); } const last = item.getLastItem(); TextItemList.patch(last, { ...last.content, start: last.content.end, end: fileSize + bSeparator.length, }); return new LineResult(last, bSeparator, this); } get [Symbol.toStringTag]() { return "LoomEditor"; } }