@loom-io/core
Version:
A file system wrapper for Node.js and Bun
223 lines (222 loc) • 8.05 kB
JavaScript
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";
}
}