Skip to content

Commit

Permalink
fix: support Buffer/URL/number paths in cached filessystem
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Mar 7, 2024
2 parents 2f51fb0 + eb55873 commit 834875a
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 34 deletions.
98 changes: 66 additions & 32 deletions lib/CachedInputFileSystem.js
Expand Up @@ -72,16 +72,28 @@ class OperationMergerBackend {

this.provide = this._provider
? /**
* @param {string} path path
* @param {any} options options
* @param {function} callback callback
* @param {PathLike | PathOrFileDescriptor} path path
* @param {object | FileSystemCallback<any> | undefined} options options
* @param {FileSystemCallback<any>=} callback callback
* @returns {any} result
*/
(path, options, callback) => {
if (typeof options === "function") {
callback = options;
callback = /** @type {FileSystemCallback<any>} */ (options);
options = undefined;
}
if (
typeof path !== "string" &&
!Buffer.isBuffer(path) &&
!(path instanceof URL) &&
typeof path !== "number"
) {
/** @type {Function} */
(callback)(
new TypeError("path must be a string, Buffer, URL or number")
);
return;
}
if (options) {
return /** @type {Function} */ (this._provider).call(
this._providerContext,
Expand All @@ -90,10 +102,6 @@ class OperationMergerBackend {
callback
);
}
if (typeof path !== "string") {
callback(new TypeError("path must be a string"));
return;
}
let callbacks = this._activeAsyncOperations.get(path);
if (callbacks) {
callbacks.push(callback);
Expand All @@ -116,8 +124,8 @@ class OperationMergerBackend {
: null;
this.provideSync = this._syncProvider
? /**
* @param {string} path path
* @param {any} options options
* @param {PathLike | PathOrFileDescriptor} path path
* @param {object=} options options
* @returns {any} result
*/
(path, options) => {
Expand Down Expand Up @@ -213,10 +221,16 @@ class CacheBackend {
callback = options;
options = undefined;
}
if (typeof path !== "string") {
callback(new TypeError("path must be a string"));
if (
typeof path !== "string" &&
!Buffer.isBuffer(path) &&
!(path instanceof URL) &&
typeof path !== "number"
) {
callback(new TypeError("path must be a string, Buffer, URL or number"));
return;
}
const strPath = typeof path !== "string" ? path.toString() : path;
if (options) {
return /** @type {Function} */ (this._provider).call(
this._providerContext,
Expand All @@ -232,19 +246,19 @@ class CacheBackend {
}

// Check in cache
let cacheEntry = this._data.get(path);
let cacheEntry = this._data.get(strPath);
if (cacheEntry !== undefined) {
if (cacheEntry.err) return nextTick(callback, cacheEntry.err);
return nextTick(callback, null, cacheEntry.result);
}

// Check if there is already the same operation running
let callbacks = this._activeAsyncOperations.get(path);
let callbacks = this._activeAsyncOperations.get(strPath);
if (callbacks !== undefined) {
callbacks.push(callback);
return;
}
this._activeAsyncOperations.set(path, (callbacks = [callback]));
this._activeAsyncOperations.set(strPath, (callbacks = [callback]));

// Run the operation
/** @type {Function} */
Expand All @@ -256,8 +270,8 @@ class CacheBackend {
* @param {any} [result] result
*/
(err, result) => {
this._activeAsyncOperations.delete(path);
this._storeResult(path, err, result);
this._activeAsyncOperations.delete(strPath);
this._storeResult(strPath, err, result);

// Enter async mode if not yet done
this._enterAsyncMode();
Expand All @@ -277,9 +291,15 @@ class CacheBackend {
* @returns {any} result
*/
provideSync(path, options) {
if (typeof path !== "string") {
if (
typeof path !== "string" &&
!Buffer.isBuffer(path) &&
!(path instanceof URL) &&
typeof path !== "number"
) {
throw new TypeError("path must be a string");
}
const strPath = typeof path !== "string" ? path.toString() : path;
if (options) {
return /** @type {Function} */ (this._syncProvider).call(
this._providerContext,
Expand All @@ -294,16 +314,16 @@ class CacheBackend {
}

// Check in cache
let cacheEntry = this._data.get(path);
let cacheEntry = this._data.get(strPath);
if (cacheEntry !== undefined) {
if (cacheEntry.err) throw cacheEntry.err;
return cacheEntry.result;
}

// Get all active async operations
// This sync operation will also complete them
const callbacks = this._activeAsyncOperations.get(path);
this._activeAsyncOperations.delete(path);
const callbacks = this._activeAsyncOperations.get(strPath);
this._activeAsyncOperations.delete(strPath);

// Run the operation
// When in idle mode, we will enter sync mode
Expand All @@ -314,14 +334,14 @@ class CacheBackend {
path
);
} catch (err) {
this._storeResult(path, /** @type {Error} */ (err), undefined);
this._storeResult(strPath, /** @type {Error} */ (err), undefined);
this._enterSyncModeWhenIdle();
if (callbacks) {
runCallbacks(callbacks, /** @type {Error} */ (err), undefined);
}
throw err;
}
this._storeResult(path, null, result);
this._storeResult(strPath, null, result);
this._enterSyncModeWhenIdle();
if (callbacks) {
runCallbacks(callbacks, null, result);
Expand All @@ -330,7 +350,7 @@ class CacheBackend {
}

/**
* @param {string | string[] | Set<string>} [what] what to purge
* @param {string | Buffer | URL | number | (string | URL | Buffer | number)[] | Set<string | URL | Buffer | number>} [what] what to purge
*/
purge(what) {
if (!what) {
Expand All @@ -341,9 +361,15 @@ class CacheBackend {
}
this._enterIdleMode();
}
} else if (typeof what === "string") {
} else if (
typeof what === "string" ||
Buffer.isBuffer(what) ||
what instanceof URL ||
typeof what === "number"
) {
const strWhat = typeof what !== "string" ? what.toString() : what;
for (let [key, data] of this._data) {
if (key.startsWith(what)) {
if (key.startsWith(strWhat)) {
this._data.delete(key);
data.level.delete(key);
}
Expand All @@ -354,7 +380,8 @@ class CacheBackend {
} else {
for (let [key, data] of this._data) {
for (const item of what) {
if (key.startsWith(item)) {
const strItem = typeof item !== "string" ? item.toString() : item;
if (key.startsWith(strItem)) {
this._data.delete(key);
data.level.delete(key);
break;
Expand All @@ -368,17 +395,24 @@ class CacheBackend {
}

/**
* @param {string|string[]|Set<string>} [what] what to purge
* @param {string | Buffer | URL | number | (string | URL | Buffer | number)[] | Set<string | URL | Buffer | number>} [what] what to purge
*/
purgeParent(what) {
if (!what) {
this.purge();
} else if (typeof what === "string") {
this.purge(dirname(what));
} else if (
typeof what === "string" ||
Buffer.isBuffer(what) ||
what instanceof URL ||
typeof what === "number"
) {
const strWhat = typeof what !== "string" ? what.toString() : what;
this.purge(dirname(strWhat));
} else {
const set = new Set();
for (const item of what) {
set.add(dirname(item));
const strItem = typeof item !== "string" ? item.toString() : item;
set.add(dirname(strItem));
}
this.purge(set);
}
Expand Down Expand Up @@ -616,7 +650,7 @@ module.exports = class CachedInputFileSystem {
}

/**
* @param {string|string[]|Set<string>} [what] what to purge
* @param {string | Buffer | URL | number | (string | URL | Buffer | number)[] | Set<string | URL | Buffer | number>} [what] what to purge
*/
purge(what) {
this._statBackend.purge(what);
Expand Down
132 changes: 131 additions & 1 deletion test/CachedInputFileSystem.test.js
@@ -1,4 +1,6 @@
const { CachedInputFileSystem } = require("../");
const path = require("path");
const url = require("url");

describe("CachedInputFileSystem OperationMergerBackend ('stat' and 'statSync')", () => {
let fs;
Expand Down Expand Up @@ -432,7 +434,19 @@ describe("CachedInputFileSystem CacheBackend", () => {
fs.purge(["/test/path"]);
fs.readdir("/test/path", (err, r) => {
expect(r[0]).toEqual("2");
done();
fs.purge([url.pathToFileURL("/test/path")]);
fs.readdir("/test/path", (err, r) => {
expect(r[0]).toEqual("2");
fs.purge(Buffer.from("/test/path"));
fs.readdir("/test/path", (err, r) => {
expect(r[0]).toEqual("3");
fs.purge([Buffer.from("/test/path")]);
fs.readdir("/test/path", (err, r) => {
expect(r[0]).toEqual("4");
done();
});
});
});
});
});
});
Expand All @@ -453,3 +467,119 @@ describe("CachedInputFileSystem CacheBackend", () => {
next();
});
});

describe("CachedInputFileSystem CacheBackend and Node.JS filesystem", () => {
let fs;

beforeEach(() => {
fs = new CachedInputFileSystem(require("fs"), 1);
});

const file = path.resolve(__dirname, "./fixtures/abc.txt");

it("should work with string async", function (done) {
fs.readFile(file, (err, r) => {
if (err) {
done(err);
return;
}
expect(r.toString()).toEqual("abc");
done();
});
});

it("should work with string sync", function () {
const r = fs.readFileSync(file);
expect(r.toString()).toEqual("abc");
});

it("should work with Buffer async", function (done) {
fs.readFile(Buffer.from(file), (err, r) => {
if (err) {
done(err);
return;
}
expect(r.toString()).toEqual("abc");
done();
});
});

it("should work with Buffer sync", function () {
const r = fs.readFileSync(Buffer.from(file));
expect(r.toString()).toEqual("abc");
});

it("should work with URL async", function (done) {
fs.readFile(url.pathToFileURL(file), (err, r) => {
if (err) {
done(err);
return;
}
expect(r.toString()).toEqual("abc");
done();
});
});

it("should work with URL sync", function () {
const r = fs.readFileSync(url.pathToFileURL(file));
expect(r.toString()).toEqual("abc");
});
});

describe("CachedInputFileSystem OperationMergerBackend and Node.JS filesystem", () => {
let fs;

beforeEach(() => {
fs = new CachedInputFileSystem(require("fs"), 0);
});

const file = path.resolve(__dirname, "./fixtures/abc.txt");

it("should work with string async", function (done) {
fs.readFile(file, (err, r) => {
if (err) {
done(err);
return;
}
expect(r.toString()).toEqual("abc");
done();
});
});

it("should work with string sync", function () {
const r = fs.readFileSync(file);
expect(r.toString()).toEqual("abc");
});

it("should work with Buffer async", function (done) {
fs.readFile(Buffer.from(file), (err, r) => {
if (err) {
done(err);
return;
}
expect(r.toString()).toEqual("abc");
done();
});
});

it("should work with Buffer sync", function () {
const r = fs.readFileSync(Buffer.from(file));
expect(r.toString()).toEqual("abc");
});

it("should work with URL async", function (done) {
fs.readFile(url.pathToFileURL(file), (err, r) => {
if (err) {
done(err);
return;
}
expect(r.toString()).toEqual("abc");
done();
});
});

it("should work with URL sync", function () {
const r = fs.readFileSync(url.pathToFileURL(file));
expect(r.toString()).toEqual("abc");
});
});

0 comments on commit 834875a

Please sign in to comment.