|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"use strict"; |
|
|
|
|
|
|
|
|
const { nextTick } = require("process"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const dirname = (path) => { |
|
|
let idx = path.length - 1; |
|
|
while (idx >= 0) { |
|
|
const char = path.charCodeAt(idx); |
|
|
|
|
|
if (char === 47 || char === 92) break; |
|
|
idx--; |
|
|
} |
|
|
if (idx < 0) return ""; |
|
|
return path.slice(0, idx); |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const runCallbacks = (callbacks, err, result) => { |
|
|
if (callbacks.length === 1) { |
|
|
callbacks[0](err, result); |
|
|
callbacks.length = 0; |
|
|
return; |
|
|
} |
|
|
let error; |
|
|
for (const callback of callbacks) { |
|
|
try { |
|
|
callback(err, result); |
|
|
} catch (err) { |
|
|
if (!error) error = err; |
|
|
} |
|
|
} |
|
|
callbacks.length = 0; |
|
|
if (error) throw error; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OperationMergerBackend { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(provider, syncProvider, providerContext) { |
|
|
this._provider = provider; |
|
|
this._syncProvider = syncProvider; |
|
|
this._providerContext = providerContext; |
|
|
this._activeAsyncOperations = new Map(); |
|
|
|
|
|
this.provide = this._provider |
|
|
? |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(path, options, callback) => { |
|
|
if (typeof options === "function") { |
|
|
callback = |
|
|
|
|
|
(options); |
|
|
options = undefined; |
|
|
} |
|
|
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; |
|
|
} |
|
|
if (options) { |
|
|
return (this._provider).call( |
|
|
this._providerContext, |
|
|
path, |
|
|
options, |
|
|
callback, |
|
|
); |
|
|
} |
|
|
let callbacks = this._activeAsyncOperations.get(path); |
|
|
if (callbacks) { |
|
|
callbacks.push(callback); |
|
|
return; |
|
|
} |
|
|
this._activeAsyncOperations.set(path, (callbacks = [callback])); |
|
|
|
|
|
(provider)( |
|
|
path, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(err, result) => { |
|
|
this._activeAsyncOperations.delete(path); |
|
|
runCallbacks(callbacks, err, result); |
|
|
}, |
|
|
); |
|
|
} |
|
|
: null; |
|
|
this.provideSync = this._syncProvider |
|
|
? |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(path, options) => |
|
|
(this._syncProvider).call( |
|
|
this._providerContext, |
|
|
path, |
|
|
options, |
|
|
) |
|
|
: null; |
|
|
} |
|
|
|
|
|
purge() {} |
|
|
|
|
|
purgeParent() {} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const STORAGE_MODE_IDLE = 0; |
|
|
const STORAGE_MODE_SYNC = 1; |
|
|
const STORAGE_MODE_ASYNC = 2; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CacheBackend { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(duration, provider, syncProvider, providerContext) { |
|
|
this._duration = duration; |
|
|
this._provider = provider; |
|
|
this._syncProvider = syncProvider; |
|
|
this._providerContext = providerContext; |
|
|
|
|
|
this._activeAsyncOperations = new Map(); |
|
|
|
|
|
this._data = new Map(); |
|
|
|
|
|
this._levels = []; |
|
|
for (let i = 0; i < 10; i++) this._levels.push(new Set()); |
|
|
for (let i = 5000; i < duration; i += 500) this._levels.push(new Set()); |
|
|
this._currentLevel = 0; |
|
|
this._tickInterval = Math.floor(duration / this._levels.length); |
|
|
|
|
|
this._mode = STORAGE_MODE_IDLE; |
|
|
|
|
|
|
|
|
this._timeout = undefined; |
|
|
|
|
|
this._nextDecay = undefined; |
|
|
|
|
|
|
|
|
|
|
|
this.provide = provider ? this.provide.bind(this) : null; |
|
|
|
|
|
|
|
|
this.provideSync = syncProvider ? this.provideSync.bind(this) : null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
provide(path, options, callback) { |
|
|
if (typeof options === "function") { |
|
|
callback = options; |
|
|
options = undefined; |
|
|
} |
|
|
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 (this._provider).call( |
|
|
this._providerContext, |
|
|
path, |
|
|
options, |
|
|
callback, |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
if (this._mode === STORAGE_MODE_SYNC) { |
|
|
this._enterAsyncMode(); |
|
|
} |
|
|
|
|
|
|
|
|
const cacheEntry = this._data.get(strPath); |
|
|
if (cacheEntry !== undefined) { |
|
|
if (cacheEntry.err) return nextTick(callback, cacheEntry.err); |
|
|
return nextTick(callback, null, cacheEntry.result); |
|
|
} |
|
|
|
|
|
|
|
|
let callbacks = this._activeAsyncOperations.get(strPath); |
|
|
if (callbacks !== undefined) { |
|
|
callbacks.push(callback); |
|
|
return; |
|
|
} |
|
|
this._activeAsyncOperations.set(strPath, (callbacks = [callback])); |
|
|
|
|
|
|
|
|
|
|
|
(this._provider).call( |
|
|
this._providerContext, |
|
|
path, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(err, result) => { |
|
|
this._activeAsyncOperations.delete(strPath); |
|
|
this._storeResult(strPath, err, result); |
|
|
|
|
|
|
|
|
this._enterAsyncMode(); |
|
|
|
|
|
runCallbacks( |
|
|
(callbacks), |
|
|
err, |
|
|
result, |
|
|
); |
|
|
}, |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
provideSync(path, options) { |
|
|
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 (this._syncProvider).call( |
|
|
this._providerContext, |
|
|
path, |
|
|
options, |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
if (this._mode === STORAGE_MODE_SYNC) { |
|
|
this._runDecays(); |
|
|
} |
|
|
|
|
|
|
|
|
const cacheEntry = this._data.get(strPath); |
|
|
if (cacheEntry !== undefined) { |
|
|
if (cacheEntry.err) throw cacheEntry.err; |
|
|
return cacheEntry.result; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const callbacks = this._activeAsyncOperations.get(strPath); |
|
|
this._activeAsyncOperations.delete(strPath); |
|
|
|
|
|
|
|
|
|
|
|
let result; |
|
|
try { |
|
|
result = (this._syncProvider).call( |
|
|
this._providerContext, |
|
|
path, |
|
|
); |
|
|
} catch (err) { |
|
|
this._storeResult(strPath, (err), undefined); |
|
|
this._enterSyncModeWhenIdle(); |
|
|
if (callbacks) { |
|
|
runCallbacks(callbacks, (err), undefined); |
|
|
} |
|
|
throw err; |
|
|
} |
|
|
this._storeResult(strPath, null, result); |
|
|
this._enterSyncModeWhenIdle(); |
|
|
if (callbacks) { |
|
|
runCallbacks(callbacks, null, result); |
|
|
} |
|
|
return result; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
purge(what) { |
|
|
if (!what) { |
|
|
if (this._mode !== STORAGE_MODE_IDLE) { |
|
|
this._data.clear(); |
|
|
for (const level of this._levels) { |
|
|
level.clear(); |
|
|
} |
|
|
this._enterIdleMode(); |
|
|
} |
|
|
} else if ( |
|
|
typeof what === "string" || |
|
|
Buffer.isBuffer(what) || |
|
|
what instanceof URL || |
|
|
typeof what === "number" |
|
|
) { |
|
|
const strWhat = typeof what !== "string" ? what.toString() : what; |
|
|
for (const [key, data] of this._data) { |
|
|
if (key.startsWith(strWhat)) { |
|
|
this._data.delete(key); |
|
|
data.level.delete(key); |
|
|
} |
|
|
} |
|
|
if (this._data.size === 0) { |
|
|
this._enterIdleMode(); |
|
|
} |
|
|
} else { |
|
|
for (const [key, data] of this._data) { |
|
|
for (const item of what) { |
|
|
const strItem = typeof item !== "string" ? item.toString() : item; |
|
|
if (key.startsWith(strItem)) { |
|
|
this._data.delete(key); |
|
|
data.level.delete(key); |
|
|
break; |
|
|
} |
|
|
} |
|
|
} |
|
|
if (this._data.size === 0) { |
|
|
this._enterIdleMode(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
purgeParent(what) { |
|
|
if (!what) { |
|
|
this.purge(); |
|
|
} 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) { |
|
|
const strItem = typeof item !== "string" ? item.toString() : item; |
|
|
set.add(dirname(strItem)); |
|
|
} |
|
|
this.purge(set); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_storeResult(path, err, result) { |
|
|
if (this._data.has(path)) return; |
|
|
const level = this._levels[this._currentLevel]; |
|
|
this._data.set(path, { err, result, level }); |
|
|
level.add(path); |
|
|
} |
|
|
|
|
|
_decayLevel() { |
|
|
const nextLevel = (this._currentLevel + 1) % this._levels.length; |
|
|
const decay = this._levels[nextLevel]; |
|
|
this._currentLevel = nextLevel; |
|
|
for (const item of decay) { |
|
|
this._data.delete(item); |
|
|
} |
|
|
decay.clear(); |
|
|
if (this._data.size === 0) { |
|
|
this._enterIdleMode(); |
|
|
} else { |
|
|
|
|
|
(this._nextDecay) += this._tickInterval; |
|
|
} |
|
|
} |
|
|
|
|
|
_runDecays() { |
|
|
while ( |
|
|
(this._nextDecay) <= Date.now() && |
|
|
this._mode !== STORAGE_MODE_IDLE |
|
|
) { |
|
|
this._decayLevel(); |
|
|
} |
|
|
} |
|
|
|
|
|
_enterAsyncMode() { |
|
|
let timeout = 0; |
|
|
switch (this._mode) { |
|
|
case STORAGE_MODE_ASYNC: |
|
|
return; |
|
|
case STORAGE_MODE_IDLE: |
|
|
this._nextDecay = Date.now() + this._tickInterval; |
|
|
timeout = this._tickInterval; |
|
|
break; |
|
|
case STORAGE_MODE_SYNC: |
|
|
this._runDecays(); |
|
|
|
|
|
if ( |
|
|
|
|
|
(this._mode) === STORAGE_MODE_IDLE |
|
|
) { |
|
|
return; |
|
|
} |
|
|
timeout = Math.max( |
|
|
0, |
|
|
(this._nextDecay) - Date.now(), |
|
|
); |
|
|
break; |
|
|
} |
|
|
this._mode = STORAGE_MODE_ASYNC; |
|
|
const ref = setTimeout(() => { |
|
|
this._mode = STORAGE_MODE_SYNC; |
|
|
this._runDecays(); |
|
|
}, timeout); |
|
|
if (ref.unref) ref.unref(); |
|
|
this._timeout = ref; |
|
|
} |
|
|
|
|
|
_enterSyncModeWhenIdle() { |
|
|
if (this._mode === STORAGE_MODE_IDLE) { |
|
|
this._mode = STORAGE_MODE_SYNC; |
|
|
this._nextDecay = Date.now() + this._tickInterval; |
|
|
} |
|
|
} |
|
|
|
|
|
_enterIdleMode() { |
|
|
this._mode = STORAGE_MODE_IDLE; |
|
|
this._nextDecay = undefined; |
|
|
if (this._timeout) clearTimeout(this._timeout); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const createBackend = (duration, provider, syncProvider, providerContext) => { |
|
|
if (duration > 0) { |
|
|
return new CacheBackend(duration, provider, syncProvider, providerContext); |
|
|
} |
|
|
return new OperationMergerBackend(provider, syncProvider, providerContext); |
|
|
}; |
|
|
|
|
|
module.exports = class CachedInputFileSystem { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(fileSystem, duration) { |
|
|
this.fileSystem = fileSystem; |
|
|
|
|
|
this._lstatBackend = createBackend( |
|
|
duration, |
|
|
this.fileSystem.lstat, |
|
|
this.fileSystem.lstatSync, |
|
|
this.fileSystem, |
|
|
); |
|
|
const lstat = this._lstatBackend.provide; |
|
|
this.lstat = (lstat); |
|
|
const lstatSync = this._lstatBackend.provideSync; |
|
|
this.lstatSync = (lstatSync); |
|
|
|
|
|
this._statBackend = createBackend( |
|
|
duration, |
|
|
this.fileSystem.stat, |
|
|
this.fileSystem.statSync, |
|
|
this.fileSystem, |
|
|
); |
|
|
const stat = this._statBackend.provide; |
|
|
this.stat = (stat); |
|
|
const statSync = this._statBackend.provideSync; |
|
|
this.statSync = (statSync); |
|
|
|
|
|
this._readdirBackend = createBackend( |
|
|
duration, |
|
|
this.fileSystem.readdir, |
|
|
this.fileSystem.readdirSync, |
|
|
this.fileSystem, |
|
|
); |
|
|
const readdir = this._readdirBackend.provide; |
|
|
this.readdir = (readdir); |
|
|
const readdirSync = this._readdirBackend.provideSync; |
|
|
this.readdirSync = ( |
|
|
readdirSync |
|
|
); |
|
|
|
|
|
this._readFileBackend = createBackend( |
|
|
duration, |
|
|
this.fileSystem.readFile, |
|
|
this.fileSystem.readFileSync, |
|
|
this.fileSystem, |
|
|
); |
|
|
const readFile = this._readFileBackend.provide; |
|
|
this.readFile = (readFile); |
|
|
const readFileSync = this._readFileBackend.provideSync; |
|
|
this.readFileSync = ( |
|
|
readFileSync |
|
|
); |
|
|
|
|
|
this._readJsonBackend = createBackend( |
|
|
duration, |
|
|
|
|
|
this.fileSystem.readJson || |
|
|
(this.readFile && |
|
|
( |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(path, callback) => { |
|
|
this.readFile(path, (err, buffer) => { |
|
|
if (err) return callback(err); |
|
|
if (!buffer || buffer.length === 0) |
|
|
{return callback(new Error("No file content"));} |
|
|
let data; |
|
|
try { |
|
|
data = JSON.parse(buffer.toString("utf8")); |
|
|
} catch (err_) { |
|
|
return callback( (err_)); |
|
|
} |
|
|
callback(null, data); |
|
|
}); |
|
|
}) |
|
|
), |
|
|
|
|
|
this.fileSystem.readJsonSync || |
|
|
(this.readFileSync && |
|
|
( |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(path) => { |
|
|
const buffer = this.readFileSync(path); |
|
|
const data = JSON.parse(buffer.toString("utf8")); |
|
|
return data; |
|
|
} |
|
|
)), |
|
|
this.fileSystem, |
|
|
); |
|
|
const readJson = this._readJsonBackend.provide; |
|
|
this.readJson = (readJson); |
|
|
const readJsonSync = this._readJsonBackend.provideSync; |
|
|
this.readJsonSync = ( |
|
|
readJsonSync |
|
|
); |
|
|
|
|
|
this._readlinkBackend = createBackend( |
|
|
duration, |
|
|
this.fileSystem.readlink, |
|
|
this.fileSystem.readlinkSync, |
|
|
this.fileSystem, |
|
|
); |
|
|
const readlink = this._readlinkBackend.provide; |
|
|
this.readlink = (readlink); |
|
|
const readlinkSync = this._readlinkBackend.provideSync; |
|
|
this.readlinkSync = ( |
|
|
readlinkSync |
|
|
); |
|
|
|
|
|
this._realpathBackend = createBackend( |
|
|
duration, |
|
|
this.fileSystem.realpath, |
|
|
this.fileSystem.realpathSync, |
|
|
this.fileSystem, |
|
|
); |
|
|
const realpath = this._realpathBackend.provide; |
|
|
this.realpath = (realpath); |
|
|
const realpathSync = this._realpathBackend.provideSync; |
|
|
this.realpathSync = ( |
|
|
realpathSync |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
purge(what) { |
|
|
this._statBackend.purge(what); |
|
|
this._lstatBackend.purge(what); |
|
|
this._readdirBackend.purgeParent(what); |
|
|
this._readFileBackend.purge(what); |
|
|
this._readlinkBackend.purge(what); |
|
|
this._readJsonBackend.purge(what); |
|
|
this._realpathBackend.purge(what); |
|
|
} |
|
|
}; |
|
|
|