CachedInputFileSystem.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. // eslint-disable-next-line n/prefer-global/process
  7. const { nextTick } = require("process");
  8. /** @typedef {import("./Resolver").FileSystem} FileSystem */
  9. /** @typedef {import("./Resolver").PathLike} PathLike */
  10. /** @typedef {import("./Resolver").PathOrFileDescriptor} PathOrFileDescriptor */
  11. /** @typedef {import("./Resolver").SyncFileSystem} SyncFileSystem */
  12. /** @typedef {FileSystem & SyncFileSystem} BaseFileSystem */
  13. /**
  14. * @template T
  15. * @typedef {import("./Resolver").FileSystemCallback<T>} FileSystemCallback<T>
  16. */
  17. /**
  18. * @param {string} path path
  19. * @returns {string} dirname
  20. */
  21. const dirname = (path) => {
  22. let idx = path.length - 1;
  23. while (idx >= 0) {
  24. const char = path.charCodeAt(idx);
  25. // slash or backslash
  26. if (char === 47 || char === 92) break;
  27. idx--;
  28. }
  29. if (idx < 0) return "";
  30. return path.slice(0, idx);
  31. };
  32. /**
  33. * @template T
  34. * @param {FileSystemCallback<T>[]} callbacks callbacks
  35. * @param {Error | null} err error
  36. * @param {T} result result
  37. */
  38. const runCallbacks = (callbacks, err, result) => {
  39. if (callbacks.length === 1) {
  40. callbacks[0](err, result);
  41. callbacks.length = 0;
  42. return;
  43. }
  44. let error;
  45. for (const callback of callbacks) {
  46. try {
  47. callback(err, result);
  48. } catch (err) {
  49. if (!error) error = err;
  50. }
  51. }
  52. callbacks.length = 0;
  53. if (error) throw error;
  54. };
  55. // eslint-disable-next-line jsdoc/reject-function-type
  56. /** @typedef {Function} EXPECTED_FUNCTION */
  57. // eslint-disable-next-line jsdoc/reject-any-type
  58. /** @typedef {any} EXPECTED_ANY */
  59. /**
  60. * The first pending cache hit is held in these slots (a scheduled tick is
  61. * pending iff `firstCallback` is set); later hits from the same synchronous
  62. * burst spill into `dispatchQueue` as flat [callback, err, result] triples
  63. * and are drained by the same tick. This keeps the common single-hit case
  64. * as cheap as a plain `nextTick` while bursts share one tick.
  65. * @type {FileSystemCallback<EXPECTED_ANY> | undefined}
  66. */
  67. let firstCallback;
  68. /** @type {Error | null} */
  69. let firstErr = null;
  70. /** @type {EXPECTED_ANY} */
  71. let firstResult;
  72. /** @type {EXPECTED_ANY[]} */
  73. let dispatchQueue = [];
  74. let dispatchQueueLength = 0;
  75. /** @type {EXPECTED_ANY[]} */
  76. let spareQueue = [];
  77. // upper bound on the spill-array capacity kept alive between bursts
  78. const MAX_RETAINED_QUEUE_LENGTH = 1024;
  79. const runDispatch = () => {
  80. const callback = /** @type {FileSystemCallback<EXPECTED_ANY>} */ (
  81. firstCallback
  82. );
  83. const err = firstErr;
  84. const result = firstResult;
  85. // clear before calling: hits made from inside a callback start a new tick
  86. firstCallback = undefined;
  87. firstErr = null;
  88. firstResult = undefined;
  89. if (dispatchQueueLength === 0) {
  90. // single hit: no queue bookkeeping, a throw affects nobody else
  91. callback(err, result);
  92. return;
  93. }
  94. const queue = dispatchQueue;
  95. const length = dispatchQueueLength;
  96. // ping-pong the two arrays so draining never allocates
  97. dispatchQueue = spareQueue;
  98. dispatchQueueLength = 0;
  99. let i = -3;
  100. try {
  101. callback(err, result);
  102. for (i = 0; i < length; i += 3) {
  103. queue[i](queue[i + 1], queue[i + 2]);
  104. }
  105. } finally {
  106. // a throwing callback must not starve the rest: re-schedule the
  107. // remainder ahead of anything scheduled during this drain (matching
  108. // the old per-tick FIFO order) and rethrow
  109. i += 3;
  110. if (i < length) {
  111. if (firstCallback === undefined) {
  112. // no reentrant hit took the slot, so `dispatchQueue` is empty
  113. firstCallback = queue[i];
  114. firstErr = queue[i + 1];
  115. firstResult = queue[i + 2];
  116. nextTick(runDispatch);
  117. for (let j = i + 3; j < length; j++) {
  118. dispatchQueue[dispatchQueueLength++] = queue[j];
  119. }
  120. } else {
  121. // a reentrant hit claimed the slot (and scheduled the tick);
  122. // displace it behind the remainder to keep FIFO order
  123. const rest = queue.slice(i, length);
  124. rest.push(firstCallback, firstErr, firstResult);
  125. for (let j = 0; j < dispatchQueueLength; j++) {
  126. rest.push(dispatchQueue[j]);
  127. }
  128. [firstCallback, firstErr, firstResult] = rest;
  129. dispatchQueue = rest.slice(3);
  130. dispatchQueueLength = dispatchQueue.length;
  131. }
  132. }
  133. // release references but keep the backing store, so the next burst
  134. // does not have to re-grow the array from scratch
  135. for (let j = 0; j < length; j++) queue[j] = undefined;
  136. // bound the permanently retained capacity after a rare giant burst
  137. if (queue.length > MAX_RETAINED_QUEUE_LENGTH) {
  138. queue.length = MAX_RETAINED_QUEUE_LENGTH;
  139. }
  140. spareQueue = queue;
  141. }
  142. };
  143. /**
  144. * Cache hits stay asynchronous, but all hits from the same synchronous
  145. * execution share a single `nextTick` instead of scheduling one each.
  146. * @param {FileSystemCallback<EXPECTED_ANY>} callback callback
  147. * @param {Error | null} err error
  148. * @param {EXPECTED_ANY} result result
  149. */
  150. const scheduleDispatch = (callback, err, result) => {
  151. if (firstCallback === undefined) {
  152. firstCallback = callback;
  153. firstErr = err;
  154. firstResult = result;
  155. nextTick(runDispatch);
  156. } else {
  157. const queue = dispatchQueue;
  158. queue[dispatchQueueLength] = callback;
  159. queue[dispatchQueueLength + 1] = err;
  160. queue[dispatchQueueLength + 2] = result;
  161. dispatchQueueLength += 3;
  162. }
  163. };
  164. class OperationMergerBackend {
  165. /**
  166. * @param {EXPECTED_FUNCTION | undefined} provider async method in filesystem
  167. * @param {EXPECTED_FUNCTION | undefined} syncProvider sync method in filesystem
  168. * @param {BaseFileSystem} providerContext call context for the provider methods
  169. */
  170. constructor(provider, syncProvider, providerContext) {
  171. this._provider = provider;
  172. this._syncProvider = syncProvider;
  173. this._providerContext = providerContext;
  174. this._activeAsyncOperations = new Map();
  175. this.provide = this._provider
  176. ? // Comment to align jsdoc
  177. /**
  178. * @param {PathLike | PathOrFileDescriptor} path path
  179. * @param {object | FileSystemCallback<EXPECTED_ANY> | undefined} options options
  180. * @param {FileSystemCallback<EXPECTED_ANY>=} callback callback
  181. * @returns {EXPECTED_ANY} result
  182. */
  183. (path, options, callback) => {
  184. if (typeof options === "function") {
  185. callback =
  186. /** @type {FileSystemCallback<EXPECTED_ANY>} */
  187. (options);
  188. options = undefined;
  189. }
  190. if (
  191. typeof path !== "string" &&
  192. !Buffer.isBuffer(path) &&
  193. !(path instanceof URL) &&
  194. typeof path !== "number"
  195. ) {
  196. /** @type {EXPECTED_FUNCTION} */
  197. (callback)(
  198. new TypeError("path must be a string, Buffer, URL or number"),
  199. );
  200. return;
  201. }
  202. if (options) {
  203. return /** @type {EXPECTED_FUNCTION} */ (this._provider).call(
  204. this._providerContext,
  205. path,
  206. options,
  207. callback,
  208. );
  209. }
  210. let callbacks = this._activeAsyncOperations.get(path);
  211. if (callbacks) {
  212. callbacks.push(callback);
  213. return;
  214. }
  215. this._activeAsyncOperations.set(path, (callbacks = [callback]));
  216. /** @type {EXPECTED_FUNCTION} */
  217. (provider)(
  218. path,
  219. /**
  220. * @param {Error} err error
  221. * @param {EXPECTED_ANY} result result
  222. */
  223. (err, result) => {
  224. this._activeAsyncOperations.delete(path);
  225. runCallbacks(callbacks, err, result);
  226. },
  227. );
  228. }
  229. : null;
  230. this.provideSync = this._syncProvider
  231. ? // Comment to align jsdoc
  232. /**
  233. * @param {PathLike | PathOrFileDescriptor} path path
  234. * @param {object=} options options
  235. * @returns {EXPECTED_ANY} result
  236. */
  237. (path, options) =>
  238. /** @type {EXPECTED_FUNCTION} */ (this._syncProvider).call(
  239. this._providerContext,
  240. path,
  241. options,
  242. )
  243. : null;
  244. }
  245. purge() {}
  246. purgeParent() {}
  247. }
  248. /*
  249. IDLE:
  250. insert data: goto SYNC
  251. SYNC:
  252. before provide: run ticks
  253. event loop tick: goto ASYNC_ACTIVE
  254. ASYNC:
  255. timeout: run tick, goto ASYNC_PASSIVE
  256. ASYNC_PASSIVE:
  257. before provide: run ticks
  258. IDLE --[insert data]--> SYNC --[event loop tick]--> ASYNC_ACTIVE --[interval tick]-> ASYNC_PASSIVE
  259. ^ |
  260. +---------[insert data]-------+
  261. */
  262. const STORAGE_MODE_IDLE = 0;
  263. const STORAGE_MODE_SYNC = 1;
  264. const STORAGE_MODE_ASYNC = 2;
  265. /**
  266. * @callback Provide
  267. * @param {PathLike | PathOrFileDescriptor} path path
  268. * @param {EXPECTED_ANY} options options
  269. * @param {FileSystemCallback<EXPECTED_ANY>} callback callback
  270. * @returns {void}
  271. */
  272. class CacheBackend {
  273. /**
  274. * @param {number} duration max cache duration of items
  275. * @param {EXPECTED_FUNCTION | undefined} provider async method
  276. * @param {EXPECTED_FUNCTION | undefined} syncProvider sync method
  277. * @param {BaseFileSystem} providerContext call context for the provider methods
  278. */
  279. constructor(duration, provider, syncProvider, providerContext) {
  280. this._duration = duration;
  281. this._provider = provider;
  282. this._syncProvider = syncProvider;
  283. this._providerContext = providerContext;
  284. /** @type {Map<string, FileSystemCallback<EXPECTED_ANY>[]>} */
  285. this._activeAsyncOperations = new Map();
  286. /** @type {Map<string, { err: Error | null, result?: EXPECTED_ANY, level: Set<string> }>} */
  287. this._data = new Map();
  288. /** @type {Set<string>[]} */
  289. this._levels = [];
  290. for (let i = 0; i < 10; i++) this._levels.push(new Set());
  291. if (duration !== Infinity) {
  292. for (let i = 5000; i < duration; i += 500) {
  293. this._levels.push(new Set());
  294. }
  295. }
  296. this._currentLevel = 0;
  297. this._tickInterval = Math.floor(duration / this._levels.length);
  298. /** @type {STORAGE_MODE_IDLE | STORAGE_MODE_SYNC | STORAGE_MODE_ASYNC} */
  299. this._mode = STORAGE_MODE_IDLE;
  300. /** @type {NodeJS.Timeout | undefined} */
  301. this._timeout = undefined;
  302. /** @type {number | undefined} */
  303. this._nextDecay = undefined;
  304. // eslint-disable-next-line no-warning-comments
  305. // @ts-ignore
  306. this.provide = provider ? this.provide.bind(this) : null;
  307. // eslint-disable-next-line no-warning-comments
  308. // @ts-ignore
  309. this.provideSync = syncProvider ? this.provideSync.bind(this) : null;
  310. }
  311. /**
  312. * @param {PathLike | PathOrFileDescriptor} path path
  313. * @param {EXPECTED_ANY} options options
  314. * @param {FileSystemCallback<EXPECTED_ANY>} callback callback
  315. * @returns {void}
  316. */
  317. provide(path, options, callback) {
  318. if (typeof options === "function") {
  319. callback = options;
  320. options = undefined;
  321. }
  322. if (
  323. typeof path !== "string" &&
  324. !Buffer.isBuffer(path) &&
  325. !(path instanceof URL) &&
  326. typeof path !== "number"
  327. ) {
  328. callback(new TypeError("path must be a string, Buffer, URL or number"));
  329. return;
  330. }
  331. const strPath = typeof path !== "string" ? path.toString() : path;
  332. if (options) {
  333. return /** @type {EXPECTED_FUNCTION} */ (this._provider).call(
  334. this._providerContext,
  335. path,
  336. options,
  337. callback,
  338. );
  339. }
  340. // When in sync mode we can move to async mode
  341. if (this._mode === STORAGE_MODE_SYNC) {
  342. this._enterAsyncMode();
  343. }
  344. // Check in cache
  345. const cacheEntry = this._data.get(strPath);
  346. if (cacheEntry !== undefined) {
  347. if (cacheEntry.err) {
  348. return scheduleDispatch(callback, cacheEntry.err, undefined);
  349. }
  350. return scheduleDispatch(callback, null, cacheEntry.result);
  351. }
  352. // Check if there is already the same operation running
  353. let callbacks = this._activeAsyncOperations.get(strPath);
  354. if (callbacks !== undefined) {
  355. callbacks.push(callback);
  356. return;
  357. }
  358. this._activeAsyncOperations.set(strPath, (callbacks = [callback]));
  359. // Run the operation
  360. /** @type {EXPECTED_FUNCTION} */
  361. (this._provider).call(
  362. this._providerContext,
  363. path,
  364. /**
  365. * @param {Error | null} err error
  366. * @param {EXPECTED_ANY=} result result
  367. */
  368. (err, result) => {
  369. this._activeAsyncOperations.delete(strPath);
  370. this._storeResult(strPath, err, result);
  371. // Enter async mode if not yet done
  372. this._enterAsyncMode();
  373. runCallbacks(
  374. /** @type {FileSystemCallback<EXPECTED_ANY>[]} */ (callbacks),
  375. err,
  376. result,
  377. );
  378. },
  379. );
  380. }
  381. /**
  382. * @param {PathLike | PathOrFileDescriptor} path path
  383. * @param {EXPECTED_ANY} options options
  384. * @returns {EXPECTED_ANY} result
  385. */
  386. provideSync(path, options) {
  387. if (
  388. typeof path !== "string" &&
  389. !Buffer.isBuffer(path) &&
  390. !(path instanceof URL) &&
  391. typeof path !== "number"
  392. ) {
  393. throw new TypeError("path must be a string");
  394. }
  395. const strPath = typeof path !== "string" ? path.toString() : path;
  396. if (options) {
  397. return /** @type {EXPECTED_FUNCTION} */ (this._syncProvider).call(
  398. this._providerContext,
  399. path,
  400. options,
  401. );
  402. }
  403. // In sync mode we may have to decay some cache items
  404. if (this._mode === STORAGE_MODE_SYNC) {
  405. this._runDecays();
  406. }
  407. // Check in cache
  408. const cacheEntry = this._data.get(strPath);
  409. if (cacheEntry !== undefined) {
  410. if (cacheEntry.err) throw cacheEntry.err;
  411. return cacheEntry.result;
  412. }
  413. // Get all active async operations
  414. // This sync operation will also complete them
  415. const callbacks = this._activeAsyncOperations.get(strPath);
  416. this._activeAsyncOperations.delete(strPath);
  417. // Run the operation
  418. // When in idle mode, we will enter sync mode
  419. let result;
  420. try {
  421. result = /** @type {EXPECTED_FUNCTION} */ (this._syncProvider).call(
  422. this._providerContext,
  423. path,
  424. );
  425. } catch (err) {
  426. this._storeResult(strPath, /** @type {Error} */ (err), undefined);
  427. this._enterSyncModeWhenIdle();
  428. if (callbacks) {
  429. runCallbacks(callbacks, /** @type {Error} */ (err), undefined);
  430. }
  431. throw err;
  432. }
  433. this._storeResult(strPath, null, result);
  434. this._enterSyncModeWhenIdle();
  435. if (callbacks) {
  436. runCallbacks(callbacks, null, result);
  437. }
  438. return result;
  439. }
  440. /**
  441. * @param {(string | Buffer | URL | number | (string | URL | Buffer | number)[] | Set<string | URL | Buffer | number>)=} what what to purge
  442. * @param {{ exact?: boolean }=} options options; `exact: true` removes only entries whose key matches `what` exactly instead of any entry whose key starts with `what`
  443. */
  444. purge(what, options) {
  445. if (what === undefined || what === null) {
  446. if (this._mode !== STORAGE_MODE_IDLE) {
  447. this._data.clear();
  448. for (const level of this._levels) {
  449. level.clear();
  450. }
  451. this._enterIdleMode();
  452. }
  453. return;
  454. }
  455. const exact =
  456. options !== undefined && options !== null && options.exact === true;
  457. if (exact) {
  458. if (
  459. typeof what === "string" ||
  460. Buffer.isBuffer(what) ||
  461. what instanceof URL ||
  462. typeof what === "number"
  463. ) {
  464. const strWhat = typeof what !== "string" ? what.toString() : what;
  465. const data = this._data.get(strWhat);
  466. if (data !== undefined) {
  467. this._data.delete(strWhat);
  468. data.level.delete(strWhat);
  469. }
  470. } else {
  471. for (const item of what) {
  472. const strItem = typeof item !== "string" ? item.toString() : item;
  473. const data = this._data.get(strItem);
  474. if (data !== undefined) {
  475. this._data.delete(strItem);
  476. data.level.delete(strItem);
  477. }
  478. }
  479. }
  480. if (this._data.size === 0) {
  481. this._enterIdleMode();
  482. }
  483. return;
  484. }
  485. if (
  486. typeof what === "string" ||
  487. Buffer.isBuffer(what) ||
  488. what instanceof URL ||
  489. typeof what === "number"
  490. ) {
  491. const strWhat = typeof what !== "string" ? what.toString() : what;
  492. if (strWhat === "") {
  493. // empty string is a prefix of every key — short-circuit the O(n) scan
  494. if (this._mode !== STORAGE_MODE_IDLE) {
  495. this._data.clear();
  496. for (const level of this._levels) {
  497. level.clear();
  498. }
  499. this._enterIdleMode();
  500. }
  501. return;
  502. }
  503. for (const [key, data] of this._data) {
  504. if (key.startsWith(strWhat)) {
  505. this._data.delete(key);
  506. data.level.delete(key);
  507. }
  508. }
  509. if (this._data.size === 0) {
  510. this._enterIdleMode();
  511. }
  512. } else {
  513. for (const [key, data] of this._data) {
  514. for (const item of what) {
  515. const strItem = typeof item !== "string" ? item.toString() : item;
  516. if (key.startsWith(strItem)) {
  517. this._data.delete(key);
  518. data.level.delete(key);
  519. break;
  520. }
  521. }
  522. }
  523. if (this._data.size === 0) {
  524. this._enterIdleMode();
  525. }
  526. }
  527. }
  528. /**
  529. * @param {(string | Buffer | URL | number | (string | URL | Buffer | number)[] | Set<string | URL | Buffer | number>)=} what what to purge
  530. */
  531. purgeParent(what) {
  532. if (what === undefined || what === null) {
  533. this.purge();
  534. } else if (
  535. typeof what === "string" ||
  536. Buffer.isBuffer(what) ||
  537. what instanceof URL ||
  538. typeof what === "number"
  539. ) {
  540. const strWhat = typeof what !== "string" ? what.toString() : what;
  541. this.purge(dirname(strWhat));
  542. } else {
  543. const set = new Set();
  544. for (const item of what) {
  545. const strItem = typeof item !== "string" ? item.toString() : item;
  546. set.add(dirname(strItem));
  547. }
  548. this.purge(set);
  549. }
  550. }
  551. /**
  552. * @param {string} path path
  553. * @param {Error | null} err error
  554. * @param {EXPECTED_ANY} result result
  555. */
  556. _storeResult(path, err, result) {
  557. if (this._data.has(path)) return;
  558. const level = this._levels[this._currentLevel];
  559. this._data.set(path, { err, result, level });
  560. level.add(path);
  561. }
  562. _decayLevel() {
  563. const nextLevel = (this._currentLevel + 1) % this._levels.length;
  564. const decay = this._levels[nextLevel];
  565. this._currentLevel = nextLevel;
  566. for (const item of decay) {
  567. this._data.delete(item);
  568. }
  569. decay.clear();
  570. if (this._data.size === 0) {
  571. this._enterIdleMode();
  572. } else {
  573. /** @type {number} */
  574. (this._nextDecay) += this._tickInterval;
  575. }
  576. }
  577. _runDecays() {
  578. while (
  579. /** @type {number} */ (this._nextDecay) <= Date.now() &&
  580. this._mode !== STORAGE_MODE_IDLE
  581. ) {
  582. this._decayLevel();
  583. }
  584. }
  585. _enterAsyncMode() {
  586. let timeout = 0;
  587. switch (this._mode) {
  588. case STORAGE_MODE_ASYNC:
  589. return;
  590. case STORAGE_MODE_IDLE:
  591. this._nextDecay = Date.now() + this._tickInterval;
  592. timeout = this._tickInterval;
  593. break;
  594. case STORAGE_MODE_SYNC:
  595. this._runDecays();
  596. // _runDecays may change the mode
  597. if (
  598. /** @type {STORAGE_MODE_IDLE | STORAGE_MODE_SYNC | STORAGE_MODE_ASYNC} */
  599. (this._mode) === STORAGE_MODE_IDLE
  600. ) {
  601. return;
  602. }
  603. timeout = Math.max(
  604. 0,
  605. /** @type {number} */ (this._nextDecay) - Date.now(),
  606. );
  607. break;
  608. }
  609. this._mode = STORAGE_MODE_ASYNC;
  610. // When duration is Infinity, cache entries never expire, so there
  611. // is no need to schedule a decay timer.
  612. if (this._duration === Infinity) {
  613. return;
  614. }
  615. const ref = setTimeout(() => {
  616. this._mode = STORAGE_MODE_SYNC;
  617. this._runDecays();
  618. }, timeout);
  619. if (ref.unref) ref.unref();
  620. this._timeout = ref;
  621. }
  622. _enterSyncModeWhenIdle() {
  623. if (this._mode === STORAGE_MODE_IDLE) {
  624. this._mode = STORAGE_MODE_SYNC;
  625. this._nextDecay = Date.now() + this._tickInterval;
  626. }
  627. }
  628. _enterIdleMode() {
  629. this._mode = STORAGE_MODE_IDLE;
  630. this._nextDecay = undefined;
  631. if (this._timeout) clearTimeout(this._timeout);
  632. }
  633. }
  634. /**
  635. * @template {EXPECTED_FUNCTION} Provider
  636. * @template {EXPECTED_FUNCTION} AsyncProvider
  637. * @template FileSystem
  638. * @param {number} duration duration in ms files are cached
  639. * @param {Provider | undefined} provider provider
  640. * @param {AsyncProvider | undefined} syncProvider sync provider
  641. * @param {BaseFileSystem} providerContext provider context
  642. * @returns {OperationMergerBackend | CacheBackend} backend
  643. */
  644. const createBackend = (duration, provider, syncProvider, providerContext) => {
  645. if (duration > 0) {
  646. return new CacheBackend(duration, provider, syncProvider, providerContext);
  647. }
  648. return new OperationMergerBackend(provider, syncProvider, providerContext);
  649. };
  650. module.exports = class CachedInputFileSystem {
  651. /**
  652. * @param {BaseFileSystem} fileSystem file system
  653. * @param {number} duration duration in ms files are cached
  654. */
  655. constructor(fileSystem, duration) {
  656. this.fileSystem = fileSystem;
  657. this._lstatBackend = createBackend(
  658. duration,
  659. this.fileSystem.lstat,
  660. this.fileSystem.lstatSync,
  661. this.fileSystem,
  662. );
  663. const lstat = this._lstatBackend.provide;
  664. this.lstat = /** @type {FileSystem["lstat"]} */ (lstat);
  665. const lstatSync = this._lstatBackend.provideSync;
  666. this.lstatSync = /** @type {SyncFileSystem["lstatSync"]} */ (lstatSync);
  667. this._statBackend = createBackend(
  668. duration,
  669. this.fileSystem.stat,
  670. this.fileSystem.statSync,
  671. this.fileSystem,
  672. );
  673. const stat = this._statBackend.provide;
  674. this.stat = /** @type {FileSystem["stat"]} */ (stat);
  675. const statSync = this._statBackend.provideSync;
  676. this.statSync = /** @type {SyncFileSystem["statSync"]} */ (statSync);
  677. this._readdirBackend = createBackend(
  678. duration,
  679. this.fileSystem.readdir,
  680. this.fileSystem.readdirSync,
  681. this.fileSystem,
  682. );
  683. const readdir = this._readdirBackend.provide;
  684. this.readdir = /** @type {FileSystem["readdir"]} */ (readdir);
  685. const readdirSync = this._readdirBackend.provideSync;
  686. this.readdirSync = /** @type {SyncFileSystem["readdirSync"]} */ (
  687. readdirSync
  688. );
  689. this._readFileBackend = createBackend(
  690. duration,
  691. this.fileSystem.readFile,
  692. this.fileSystem.readFileSync,
  693. this.fileSystem,
  694. );
  695. const readFile = this._readFileBackend.provide;
  696. this.readFile = /** @type {FileSystem["readFile"]} */ (readFile);
  697. const readFileSync = this._readFileBackend.provideSync;
  698. this.readFileSync = /** @type {SyncFileSystem["readFileSync"]} */ (
  699. readFileSync
  700. );
  701. this._readJsonBackend = createBackend(
  702. duration,
  703. // prettier-ignore
  704. this.fileSystem.readJson ||
  705. (this.readFile &&
  706. (
  707. /**
  708. * @param {string} path path
  709. * @param {FileSystemCallback<EXPECTED_ANY>} callback callback
  710. */
  711. (path, callback) => {
  712. this.readFile(path, (err, buffer) => {
  713. if (err) return callback(err);
  714. if (!buffer || buffer.length === 0)
  715. {return callback(new Error("No file content"));}
  716. let data;
  717. try {
  718. data = JSON.parse(buffer.toString("utf8"));
  719. } catch (err_) {
  720. return callback(/** @type {Error} */ (err_));
  721. }
  722. callback(null, data);
  723. });
  724. })
  725. ),
  726. // prettier-ignore
  727. this.fileSystem.readJsonSync ||
  728. (this.readFileSync &&
  729. (
  730. /**
  731. * @param {string} path path
  732. * @returns {EXPECTED_ANY} result
  733. */
  734. (path) => {
  735. const buffer = this.readFileSync(path);
  736. const data = JSON.parse(buffer.toString("utf8"));
  737. return data;
  738. }
  739. )),
  740. this.fileSystem,
  741. );
  742. const readJson = this._readJsonBackend.provide;
  743. this.readJson = /** @type {FileSystem["readJson"]} */ (readJson);
  744. const readJsonSync = this._readJsonBackend.provideSync;
  745. this.readJsonSync = /** @type {SyncFileSystem["readJsonSync"]} */ (
  746. readJsonSync
  747. );
  748. this._readlinkBackend = createBackend(
  749. duration,
  750. this.fileSystem.readlink,
  751. this.fileSystem.readlinkSync,
  752. this.fileSystem,
  753. );
  754. const readlink = this._readlinkBackend.provide;
  755. this.readlink = /** @type {FileSystem["readlink"]} */ (readlink);
  756. const readlinkSync = this._readlinkBackend.provideSync;
  757. this.readlinkSync = /** @type {SyncFileSystem["readlinkSync"]} */ (
  758. readlinkSync
  759. );
  760. this._realpathBackend = createBackend(
  761. duration,
  762. this.fileSystem.realpath,
  763. this.fileSystem.realpathSync,
  764. this.fileSystem,
  765. );
  766. const realpath = this._realpathBackend.provide;
  767. this.realpath = /** @type {FileSystem["realpath"]} */ (realpath);
  768. const realpathSync = this._realpathBackend.provideSync;
  769. this.realpathSync = /** @type {SyncFileSystem["realpathSync"]} */ (
  770. realpathSync
  771. );
  772. }
  773. /**
  774. * @param {(string | Buffer | URL | number | (string | URL | Buffer | number)[] | Set<string | URL | Buffer | number>)=} what what to purge
  775. * @param {{ exact?: boolean }=} options options; `exact: true` removes only cache entries whose key matches `what` exactly instead of any entry whose key starts with `what`
  776. */
  777. purge(what, options) {
  778. this._statBackend.purge(what, options);
  779. this._lstatBackend.purge(what, options);
  780. if (options !== undefined && options !== null && options.exact === true) {
  781. this._readdirBackend.purge(what, options);
  782. } else {
  783. this._readdirBackend.purgeParent(what);
  784. }
  785. this._readFileBackend.purge(what, options);
  786. this._readlinkBackend.purge(what, options);
  787. this._readJsonBackend.purge(what, options);
  788. this._realpathBackend.purge(what, options);
  789. }
  790. };