diff --git a/.gitignore b/.gitignore index cf44158b6..3d910f86d 100644 --- a/.gitignore +++ b/.gitignore @@ -108,7 +108,7 @@ dist dist/ site/ *.bat -*.cache +*.cache* *.sh !test/*.sh diff --git a/src/igir.ts b/src/igir.ts index 88b0a9210..859393e75 100644 --- a/src/igir.ts +++ b/src/igir.ts @@ -230,13 +230,17 @@ export default class Igir { } private async getCachePath(): Promise { - const defaultFileName = `${Package.NAME}.cache`; + const defaultFileName = process.versions.bun + // As of v1.1.26, Bun uses a different serializer than V8, making cache files incompatible + // @see https://bun.sh/docs/runtime/nodejs-apis + ? `${Package.NAME}.bun.cache` + : `${Package.NAME}.cache`; - // Try to use the provided path + // First, try to use the provided path let cachePath = this.options.getCachePath(); if (cachePath !== undefined && await FsPoly.isDirectory(cachePath)) { cachePath = path.join(cachePath, defaultFileName); - this.logger.warn(`A directory was provided for cache path instead of a file, using '${cachePath}' instead`); + this.logger.warn(`A directory was provided for the cache path instead of a file, using '${cachePath}' instead`); } if (cachePath !== undefined) { if (await FsPoly.isWritable(cachePath)) { @@ -245,19 +249,33 @@ export default class Igir { this.logger.warn('Provided cache path isn\'t writable, using the default path'); } - // Otherwise, use a default path - return [ + const cachePathCandidates = [ path.join(path.resolve(Package.DIRECTORY), defaultFileName), path.join(os.homedir(), defaultFileName), path.join(process.cwd(), defaultFileName), ] .filter((filePath) => filePath && !filePath.startsWith(os.tmpdir())) - .find(async (filePath) => { - if (await FsPoly.exists(filePath)) { - return true; - } - return FsPoly.isWritable(filePath); - }); + .reduce(ArrayPoly.reduceUnique(), []); + + // Next, try to use an already existing path + const exists = await Promise.all( + cachePathCandidates.map(async (pathCandidate) => FsPoly.exists(pathCandidate)), + ); + const existsCachePath = cachePathCandidates.find((_, idx) => exists[idx]); + if (existsCachePath !== undefined) { + return existsCachePath; + } + + // Next, try to find a writable path + const writable = await Promise.all( + cachePathCandidates.map(async (pathCandidate) => FsPoly.isWritable(pathCandidate)), + ); + const writableCachePath = cachePathCandidates.find((_, idx) => writable[idx]); + if (writableCachePath !== undefined) { + return writableCachePath; + } + + return undefined; } private async processDATScanner(fileFactory: FileFactory): Promise { diff --git a/src/polyfill/fsPoly.ts b/src/polyfill/fsPoly.ts index 0439456d5..5718c60c5 100644 --- a/src/polyfill/fsPoly.ts +++ b/src/polyfill/fsPoly.ts @@ -226,7 +226,7 @@ export default class FsPoly { return false; } finally { if (!exists) { - await this.rm(filePath); + await this.rm(filePath, { force: true }); } } } diff --git a/src/types/cache.ts b/src/types/cache.ts index 3fa95c269..56cf61f6f 100644 --- a/src/types/cache.ts +++ b/src/types/cache.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import util from 'node:util'; +import * as v8 from 'node:v8'; import * as zlib from 'node:zlib'; import { E_CANCELED, Mutex } from 'async-mutex'; @@ -9,10 +9,6 @@ import KeyedMutex from '../keyedMutex.js'; import FsPoly from '../polyfill/fsPoly.js'; import Timer from '../timer.js'; -interface CacheData { - data: string, -} - export interface CacheProps { filePath?: string, fileFlushMillis?: number, @@ -109,8 +105,11 @@ export default class Cache { } private setUnsafe(key: string, val: V): void { + const oldVal = this.keyValues.get(key); this.keyValues.set(key, val); - this.saveWithTimeout(); + if (val !== oldVal) { + this.saveWithTimeout(); + } } /** @@ -145,12 +144,13 @@ export default class Cache { } try { - const cacheData = JSON.parse( - await fs.promises.readFile(this.filePath, { encoding: Cache.BUFFER_ENCODING }), - ) as CacheData; - const compressed = Buffer.from(cacheData.data, Cache.BUFFER_ENCODING); - const decompressed = await util.promisify(zlib.inflate)(compressed); - const keyValuesObject = JSON.parse(decompressed.toString(Cache.BUFFER_ENCODING)); + const compressed = await fs.promises.readFile(this.filePath); + if (compressed.length === 0) { + return this; + } + // NOTE(cemmer): util.promisify(zlib.inflate) seems to have issues not throwing correctly + const decompressed = zlib.inflateSync(compressed); + const keyValuesObject = v8.deserialize(decompressed); const keyValuesEntries = Object.entries(keyValuesObject) as [string, V][]; this.keyValues = new Map(keyValuesEntries); } catch { /* ignored */ } @@ -187,11 +187,9 @@ export default class Cache { } const keyValuesObject = Object.fromEntries(this.keyValues); - const decompressed = JSON.stringify(keyValuesObject); - const compressed = await util.promisify(zlib.deflate)(decompressed); - const cacheData = { - data: compressed.toString(Cache.BUFFER_ENCODING), - } satisfies CacheData; + const decompressed = v8.serialize(keyValuesObject); + // NOTE(cemmer): util.promisify(zlib.deflate) seems to have issues not throwing correctly + const compressed = zlib.deflateSync(decompressed); // Ensure the directory exists const dirPath = path.dirname(this.filePath); @@ -203,7 +201,7 @@ export default class Cache { const tempFile = await FsPoly.mktemp(this.filePath); await FsPoly.writeFile( tempFile, - JSON.stringify(cacheData), + compressed, { encoding: Cache.BUFFER_ENCODING }, );