You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
477 lines
13 KiB
477 lines
13 KiB
/* |
|
MIT License http://www.opensource.org/licenses/mit-license.php |
|
Author Tobias Koppers @sokra |
|
*/ |
|
|
|
"use strict"; |
|
|
|
const nextTick = require("process").nextTick; |
|
|
|
/** @typedef {import("./Resolver").FileSystem} FileSystem */ |
|
/** @typedef {import("./Resolver").SyncFileSystem} SyncFileSystem */ |
|
|
|
const dirname = path => { |
|
let idx = path.length - 1; |
|
while (idx >= 0) { |
|
const c = path.charCodeAt(idx); |
|
// slash or backslash |
|
if (c === 47 || c === 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 (e) { |
|
if (!error) error = e; |
|
} |
|
} |
|
callbacks.length = 0; |
|
if (error) throw error; |
|
}; |
|
|
|
class OperationMergerBackend { |
|
/** |
|
* @param {any} provider async method |
|
* @param {any} syncProvider sync method |
|
* @param {any} providerContext call context for the provider methods |
|
*/ |
|
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 (options) { |
|
return this._provider.call( |
|
this._providerContext, |
|
path, |
|
options, |
|
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); |
|
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) => { |
|
return this._syncProvider.call(this._providerContext, path, options); |
|
} |
|
: null; |
|
} |
|
|
|
purge() {} |
|
purgeParent() {} |
|
} |
|
|
|
/* |
|
|
|
IDLE: |
|
insert data: goto SYNC |
|
|
|
SYNC: |
|
before provide: run ticks |
|
event loop tick: goto ASYNC_ACTIVE |
|
|
|
ASYNC: |
|
timeout: run tick, goto ASYNC_PASSIVE |
|
|
|
ASYNC_PASSIVE: |
|
before provide: run ticks |
|
|
|
IDLE --[insert data]--> SYNC --[event loop tick]--> ASYNC_ACTIVE --[interval tick]-> ASYNC_PASSIVE |
|
^ | |
|
+---------[insert data]-------+ |
|
*/ |
|
|
|
const STORAGE_MODE_IDLE = 0; |
|
const STORAGE_MODE_SYNC = 1; |
|
const STORAGE_MODE_ASYNC = 2; |
|
|
|
class CacheBackend { |
|
/** |
|
* @param {number} duration max cache duration of items |
|
* @param {any} provider async method |
|
* @param {any} syncProvider sync method |
|
* @param {any} providerContext call context for the provider methods |
|
*/ |
|
constructor(duration, provider, syncProvider, providerContext) { |
|
this._duration = duration; |
|
this._provider = provider; |
|
this._syncProvider = syncProvider; |
|
this._providerContext = providerContext; |
|
/** @type {Map<string, (function(Error, any): void)[]>} */ |
|
this._activeAsyncOperations = new Map(); |
|
/** @type {Map<string, { err: Error, result: any, level: Set<string> }>} */ |
|
this._data = new Map(); |
|
/** @type {Set<string>[]} */ |
|
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); |
|
/** @type {STORAGE_MODE_IDLE | STORAGE_MODE_SYNC | STORAGE_MODE_ASYNC} */ |
|
this._mode = STORAGE_MODE_IDLE; |
|
|
|
/** @type {NodeJS.Timeout | undefined} */ |
|
this._timeout = undefined; |
|
/** @type {number | 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") { |
|
callback(new TypeError("path must be a string")); |
|
return; |
|
} |
|
if (options) { |
|
return this._provider.call( |
|
this._providerContext, |
|
path, |
|
options, |
|
callback |
|
); |
|
} |
|
|
|
// When in sync mode we can move to async mode |
|
if (this._mode === STORAGE_MODE_SYNC) { |
|
this._enterAsyncMode(); |
|
} |
|
|
|
// Check in cache |
|
let cacheEntry = this._data.get(path); |
|
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); |
|
if (callbacks !== undefined) { |
|
callbacks.push(callback); |
|
return; |
|
} |
|
this._activeAsyncOperations.set(path, (callbacks = [callback])); |
|
|
|
// Run the operation |
|
this._provider.call(this._providerContext, path, (err, result) => { |
|
this._activeAsyncOperations.delete(path); |
|
this._storeResult(path, err, result); |
|
|
|
// Enter async mode if not yet done |
|
this._enterAsyncMode(); |
|
|
|
runCallbacks(callbacks, err, result); |
|
}); |
|
} |
|
|
|
provideSync(path, options) { |
|
if (typeof path !== "string") { |
|
throw new TypeError("path must be a string"); |
|
} |
|
if (options) { |
|
return this._syncProvider.call(this._providerContext, path, options); |
|
} |
|
|
|
// In sync mode we may have to decay some cache items |
|
if (this._mode === STORAGE_MODE_SYNC) { |
|
this._runDecays(); |
|
} |
|
|
|
// Check in cache |
|
let cacheEntry = this._data.get(path); |
|
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); |
|
|
|
// Run the operation |
|
// When in idle mode, we will enter sync mode |
|
let result; |
|
try { |
|
result = this._syncProvider.call(this._providerContext, path); |
|
} catch (err) { |
|
this._storeResult(path, err, undefined); |
|
this._enterSyncModeWhenIdle(); |
|
if (callbacks) runCallbacks(callbacks, err, undefined); |
|
throw err; |
|
} |
|
this._storeResult(path, undefined, result); |
|
this._enterSyncModeWhenIdle(); |
|
if (callbacks) runCallbacks(callbacks, undefined, 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") { |
|
for (let [key, data] of this._data) { |
|
if (key.startsWith(what)) { |
|
this._data.delete(key); |
|
data.level.delete(key); |
|
} |
|
} |
|
if (this._data.size === 0) { |
|
this._enterIdleMode(); |
|
} |
|
} else { |
|
for (let [key, data] of this._data) { |
|
for (const item of what) { |
|
if (key.startsWith(item)) { |
|
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") { |
|
this.purge(dirname(what)); |
|
} else { |
|
const set = new Set(); |
|
for (const item of what) { |
|
set.add(dirname(item)); |
|
} |
|
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 (let item of decay) { |
|
this._data.delete(item); |
|
} |
|
decay.clear(); |
|
if (this._data.size === 0) { |
|
this._enterIdleMode(); |
|
} else { |
|
// @ts-ignore _nextDecay is always a number in sync mode |
|
this._nextDecay += this._tickInterval; |
|
} |
|
} |
|
|
|
_runDecays() { |
|
while ( |
|
/** @type {number} */ (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(); |
|
// @ts-ignore _runDecays may change the mode |
|
if (this._mode === STORAGE_MODE_IDLE) return; |
|
timeout = Math.max( |
|
0, |
|
/** @type {number} */ (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 = /** @type {FileSystem["lstat"]} */ (lstat); |
|
const lstatSync = this._lstatBackend.provideSync; |
|
this.lstatSync = /** @type {SyncFileSystem["lstatSync"]} */ (lstatSync); |
|
|
|
this._statBackend = createBackend( |
|
duration, |
|
this.fileSystem.stat, |
|
this.fileSystem.statSync, |
|
this.fileSystem |
|
); |
|
const stat = this._statBackend.provide; |
|
this.stat = /** @type {FileSystem["stat"]} */ (stat); |
|
const statSync = this._statBackend.provideSync; |
|
this.statSync = /** @type {SyncFileSystem["statSync"]} */ (statSync); |
|
|
|
this._readdirBackend = createBackend( |
|
duration, |
|
this.fileSystem.readdir, |
|
this.fileSystem.readdirSync, |
|
this.fileSystem |
|
); |
|
const readdir = this._readdirBackend.provide; |
|
this.readdir = /** @type {FileSystem["readdir"]} */ (readdir); |
|
const readdirSync = this._readdirBackend.provideSync; |
|
this.readdirSync = /** @type {SyncFileSystem["readdirSync"]} */ (readdirSync); |
|
|
|
this._readFileBackend = createBackend( |
|
duration, |
|
this.fileSystem.readFile, |
|
this.fileSystem.readFileSync, |
|
this.fileSystem |
|
); |
|
const readFile = this._readFileBackend.provide; |
|
this.readFile = /** @type {FileSystem["readFile"]} */ (readFile); |
|
const readFileSync = this._readFileBackend.provideSync; |
|
this.readFileSync = /** @type {SyncFileSystem["readFileSync"]} */ (readFileSync); |
|
|
|
this._readJsonBackend = createBackend( |
|
duration, |
|
this.fileSystem.readJson || |
|
(this.readFile && |
|
((path, callback) => { |
|
// @ts-ignore |
|
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("utf-8")); |
|
} catch (e) { |
|
return callback(e); |
|
} |
|
callback(null, data); |
|
}); |
|
})), |
|
this.fileSystem.readJsonSync || |
|
(this.readFileSync && |
|
(path => { |
|
const buffer = this.readFileSync(path); |
|
const data = JSON.parse(buffer.toString("utf-8")); |
|
return data; |
|
})), |
|
this.fileSystem |
|
); |
|
const readJson = this._readJsonBackend.provide; |
|
this.readJson = /** @type {FileSystem["readJson"]} */ (readJson); |
|
const readJsonSync = this._readJsonBackend.provideSync; |
|
this.readJsonSync = /** @type {SyncFileSystem["readJsonSync"]} */ (readJsonSync); |
|
|
|
this._readlinkBackend = createBackend( |
|
duration, |
|
this.fileSystem.readlink, |
|
this.fileSystem.readlinkSync, |
|
this.fileSystem |
|
); |
|
const readlink = this._readlinkBackend.provide; |
|
this.readlink = /** @type {FileSystem["readlink"]} */ (readlink); |
|
const readlinkSync = this._readlinkBackend.provideSync; |
|
this.readlinkSync = /** @type {SyncFileSystem["readlinkSync"]} */ (readlinkSync); |
|
} |
|
|
|
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); |
|
} |
|
};
|
|
|