fix: cache NativeFile with LRU, closes #797 (#816)

This commit is contained in:
Huang Xin
2025-04-05 17:27:07 +08:00
committed by GitHub
parent ce88f0229a
commit dcecda2984
2 changed files with 89 additions and 10 deletions
@@ -35,6 +35,7 @@ import {
import { getOSPlatform, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
import { downloadFile, uploadFile, deleteFile, createProgressHandler } from '@/libs/storage';
import { ClosableFile } from '@/utils/file';
import { ProgressHandler } from '@/utils/transfer';
import { TxtToEpubConverter } from '@/utils/txt';
import { BOOK_FILE_NOT_FOUND_ERROR } from './errors';
@@ -244,6 +245,10 @@ export abstract class BaseAppService implements AppService {
}
}
book.coverImageUrl = await this.generateCoverImageUrl(book);
const f = file as ClosableFile;
if (f && f.close) {
await f.close();
}
return book;
} catch (error) {
@@ -282,6 +287,10 @@ export abstract class BaseAppService implements AppService {
const file = await this.fs.openFile(lfp, 'Books', cfp);
const localFullpath = `${this.localBooksDir}/${lfp}`;
await uploadFile(file, localFullpath, handleProgress, hash);
const f = file as ClosableFile;
if (f && f.close) {
await f.close();
}
}
async uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void> {
@@ -421,6 +430,10 @@ export abstract class BaseAppService implements AppService {
}
const { file } = (await this.loadBookContent(book, settings)) as BookContent;
const bookDoc = (await new DocumentLoader(file).open()).book as BookDoc;
const f = file as ClosableFile;
if (f && f.close) {
await f.close();
}
return bookDoc.metadata;
}
+76 -10
View File
@@ -50,7 +50,12 @@ class DeferredBlob extends Blob {
}
}
export class NativeFile extends File {
export interface ClosableFile extends File {
open(): Promise<this>;
close(): Promise<void>;
}
export class NativeFile extends File implements ClosableFile {
#handle: FileHandle | null = null;
#fp: string;
#name: string;
@@ -59,6 +64,11 @@ export class NativeFile extends File {
#size: number = -1;
#type: string = '';
static MAX_CACHE_CHUNK_SIZE = 1024 * 1024;
static MAX_CACHE_ITEMS_SIZE = 20;
#cache: Map<number, ArrayBuffer> = new Map();
#order: number[] = [];
constructor(fp: string, name?: string, baseDir: BaseDirectory | null = null, type = '') {
super([], name || fp, { type });
this.#fp = fp;
@@ -74,6 +84,15 @@ export class NativeFile extends File {
return this;
}
async close() {
if (this.#handle) {
await this.#handle.close();
this.#handle = null;
}
this.#cache.clear();
this.#order = [];
}
override get name() {
return this.#name;
}
@@ -94,10 +113,6 @@ export class NativeFile extends File {
return this.#handle?.stat();
}
async close(): Promise<void> {
return this.#handle?.close();
}
async seek(offset: number, whence: SeekMode): Promise<number> {
if (!this.#handle) {
throw new Error('File handle is not open');
@@ -112,10 +127,58 @@ export class NativeFile extends File {
}
start = Math.max(0, start);
end = Math.max(start, Math.min(this.size, end));
await this.#handle.seek(start, SeekMode.Start);
const buffer = new Uint8Array(end - start);
const size = end - start;
if (size > NativeFile.MAX_CACHE_CHUNK_SIZE) {
await this.#handle.seek(start, SeekMode.Start);
const buffer = new Uint8Array(size);
await this.#handle.read(buffer);
return buffer.buffer;
}
const cachedChunkStart = Array.from(this.#cache.keys()).find((chunkStart) => {
const buffer = this.#cache.get(chunkStart)!;
return start >= chunkStart && end <= chunkStart + buffer.byteLength;
});
if (cachedChunkStart !== undefined) {
this.#updateAccessOrder(cachedChunkStart);
const buffer = this.#cache.get(cachedChunkStart)!;
const offset = start - cachedChunkStart;
return buffer.slice(offset, offset + size);
}
const chunkStart = Math.max(0, start - 1024);
const chunkEnd = Math.min(this.size, chunkStart + NativeFile.MAX_CACHE_CHUNK_SIZE);
const chunkSize = chunkEnd - chunkStart;
await this.#handle.seek(chunkStart, SeekMode.Start);
const buffer = new Uint8Array(chunkSize);
await this.#handle.read(buffer);
return buffer.buffer;
this.#cache.set(chunkStart, buffer.buffer);
this.#updateAccessOrder(chunkStart);
this.#ensureCacheSize();
const offset = start - chunkStart;
return buffer.buffer.slice(offset, offset + size);
}
#updateAccessOrder(chunkStart: number) {
const index = this.#order.indexOf(chunkStart);
if (index > -1) {
this.#order.splice(index, 1);
}
this.#order.unshift(chunkStart);
}
#ensureCacheSize() {
while (this.#cache.size > NativeFile.MAX_CACHE_ITEMS_SIZE) {
const oldestKey = this.#order.pop();
if (oldestKey !== undefined) {
this.#cache.delete(oldestKey);
}
}
}
override slice(start = 0, end = this.size, contentType = this.type): Blob {
@@ -172,7 +235,7 @@ export class NativeFile extends File {
}
}
export class RemoteFile extends File {
export class RemoteFile extends File implements ClosableFile {
url: string;
#name: string;
#lastModified: number;
@@ -238,7 +301,10 @@ export class RemoteFile extends File {
}
}
async close(): Promise<void> {}
async close(): Promise<void> {
this.#cache.clear();
this.#order = [];
}
async fetchRangePart(start: number, end: number) {
start = Math.max(0, start);