Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support buffer/URL/number paths #409

Merged
merged 3 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
98 changes: 66 additions & 32 deletions lib/CachedInputFileSystem.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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");
});
});