| |
| |
| |
| |
| |
| "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); |
| } |
| }; |
|
|