1 | /*
|
---|
2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
---|
3 | Author Tobias Koppers @sokra
|
---|
4 | */
|
---|
5 |
|
---|
6 | "use strict";
|
---|
7 |
|
---|
8 | const nextTick = require("process").nextTick;
|
---|
9 |
|
---|
10 | /** @typedef {import("./Resolver").FileSystem} FileSystem */
|
---|
11 | /** @typedef {import("./Resolver").SyncFileSystem} SyncFileSystem */
|
---|
12 |
|
---|
13 | const dirname = path => {
|
---|
14 | let idx = path.length - 1;
|
---|
15 | while (idx >= 0) {
|
---|
16 | const c = path.charCodeAt(idx);
|
---|
17 | // slash or backslash
|
---|
18 | if (c === 47 || c === 92) break;
|
---|
19 | idx--;
|
---|
20 | }
|
---|
21 | if (idx < 0) return "";
|
---|
22 | return path.slice(0, idx);
|
---|
23 | };
|
---|
24 |
|
---|
25 | const runCallbacks = (callbacks, err, result) => {
|
---|
26 | if (callbacks.length === 1) {
|
---|
27 | callbacks[0](err, result);
|
---|
28 | callbacks.length = 0;
|
---|
29 | return;
|
---|
30 | }
|
---|
31 | let error;
|
---|
32 | for (const callback of callbacks) {
|
---|
33 | try {
|
---|
34 | callback(err, result);
|
---|
35 | } catch (e) {
|
---|
36 | if (!error) error = e;
|
---|
37 | }
|
---|
38 | }
|
---|
39 | callbacks.length = 0;
|
---|
40 | if (error) throw error;
|
---|
41 | };
|
---|
42 |
|
---|
43 | class OperationMergerBackend {
|
---|
44 | /**
|
---|
45 | * @param {any} provider async method
|
---|
46 | * @param {any} syncProvider sync method
|
---|
47 | * @param {any} providerContext call context for the provider methods
|
---|
48 | */
|
---|
49 | constructor(provider, syncProvider, providerContext) {
|
---|
50 | this._provider = provider;
|
---|
51 | this._syncProvider = syncProvider;
|
---|
52 | this._providerContext = providerContext;
|
---|
53 | this._activeAsyncOperations = new Map();
|
---|
54 |
|
---|
55 | this.provide = this._provider
|
---|
56 | ? (path, options, callback) => {
|
---|
57 | if (typeof options === "function") {
|
---|
58 | callback = options;
|
---|
59 | options = undefined;
|
---|
60 | }
|
---|
61 | if (options) {
|
---|
62 | return this._provider.call(
|
---|
63 | this._providerContext,
|
---|
64 | path,
|
---|
65 | options,
|
---|
66 | callback
|
---|
67 | );
|
---|
68 | }
|
---|
69 | if (typeof path !== "string") {
|
---|
70 | callback(new TypeError("path must be a string"));
|
---|
71 | return;
|
---|
72 | }
|
---|
73 | let callbacks = this._activeAsyncOperations.get(path);
|
---|
74 | if (callbacks) {
|
---|
75 | callbacks.push(callback);
|
---|
76 | return;
|
---|
77 | }
|
---|
78 | this._activeAsyncOperations.set(path, (callbacks = [callback]));
|
---|
79 | provider(path, (err, result) => {
|
---|
80 | this._activeAsyncOperations.delete(path);
|
---|
81 | runCallbacks(callbacks, err, result);
|
---|
82 | });
|
---|
83 | }
|
---|
84 | : null;
|
---|
85 | this.provideSync = this._syncProvider
|
---|
86 | ? (path, options) => {
|
---|
87 | return this._syncProvider.call(this._providerContext, path, options);
|
---|
88 | }
|
---|
89 | : null;
|
---|
90 | }
|
---|
91 |
|
---|
92 | purge() {}
|
---|
93 | purgeParent() {}
|
---|
94 | }
|
---|
95 |
|
---|
96 | /*
|
---|
97 |
|
---|
98 | IDLE:
|
---|
99 | insert data: goto SYNC
|
---|
100 |
|
---|
101 | SYNC:
|
---|
102 | before provide: run ticks
|
---|
103 | event loop tick: goto ASYNC_ACTIVE
|
---|
104 |
|
---|
105 | ASYNC:
|
---|
106 | timeout: run tick, goto ASYNC_PASSIVE
|
---|
107 |
|
---|
108 | ASYNC_PASSIVE:
|
---|
109 | before provide: run ticks
|
---|
110 |
|
---|
111 | IDLE --[insert data]--> SYNC --[event loop tick]--> ASYNC_ACTIVE --[interval tick]-> ASYNC_PASSIVE
|
---|
112 | ^ |
|
---|
113 | +---------[insert data]-------+
|
---|
114 | */
|
---|
115 |
|
---|
116 | const STORAGE_MODE_IDLE = 0;
|
---|
117 | const STORAGE_MODE_SYNC = 1;
|
---|
118 | const STORAGE_MODE_ASYNC = 2;
|
---|
119 |
|
---|
120 | class CacheBackend {
|
---|
121 | /**
|
---|
122 | * @param {number} duration max cache duration of items
|
---|
123 | * @param {any} provider async method
|
---|
124 | * @param {any} syncProvider sync method
|
---|
125 | * @param {any} providerContext call context for the provider methods
|
---|
126 | */
|
---|
127 | constructor(duration, provider, syncProvider, providerContext) {
|
---|
128 | this._duration = duration;
|
---|
129 | this._provider = provider;
|
---|
130 | this._syncProvider = syncProvider;
|
---|
131 | this._providerContext = providerContext;
|
---|
132 | /** @type {Map<string, (function(Error, any): void)[]>} */
|
---|
133 | this._activeAsyncOperations = new Map();
|
---|
134 | /** @type {Map<string, { err: Error, result: any, level: Set<string> }>} */
|
---|
135 | this._data = new Map();
|
---|
136 | /** @type {Set<string>[]} */
|
---|
137 | this._levels = [];
|
---|
138 | for (let i = 0; i < 10; i++) this._levels.push(new Set());
|
---|
139 | for (let i = 5000; i < duration; i += 500) this._levels.push(new Set());
|
---|
140 | this._currentLevel = 0;
|
---|
141 | this._tickInterval = Math.floor(duration / this._levels.length);
|
---|
142 | /** @type {STORAGE_MODE_IDLE | STORAGE_MODE_SYNC | STORAGE_MODE_ASYNC} */
|
---|
143 | this._mode = STORAGE_MODE_IDLE;
|
---|
144 |
|
---|
145 | /** @type {NodeJS.Timeout | undefined} */
|
---|
146 | this._timeout = undefined;
|
---|
147 | /** @type {number | undefined} */
|
---|
148 | this._nextDecay = undefined;
|
---|
149 |
|
---|
150 | this.provide = provider ? this.provide.bind(this) : null;
|
---|
151 | this.provideSync = syncProvider ? this.provideSync.bind(this) : null;
|
---|
152 | }
|
---|
153 |
|
---|
154 | provide(path, options, callback) {
|
---|
155 | if (typeof options === "function") {
|
---|
156 | callback = options;
|
---|
157 | options = undefined;
|
---|
158 | }
|
---|
159 | if (typeof path !== "string") {
|
---|
160 | callback(new TypeError("path must be a string"));
|
---|
161 | return;
|
---|
162 | }
|
---|
163 | if (options) {
|
---|
164 | return this._provider.call(
|
---|
165 | this._providerContext,
|
---|
166 | path,
|
---|
167 | options,
|
---|
168 | callback
|
---|
169 | );
|
---|
170 | }
|
---|
171 |
|
---|
172 | // When in sync mode we can move to async mode
|
---|
173 | if (this._mode === STORAGE_MODE_SYNC) {
|
---|
174 | this._enterAsyncMode();
|
---|
175 | }
|
---|
176 |
|
---|
177 | // Check in cache
|
---|
178 | let cacheEntry = this._data.get(path);
|
---|
179 | if (cacheEntry !== undefined) {
|
---|
180 | if (cacheEntry.err) return nextTick(callback, cacheEntry.err);
|
---|
181 | return nextTick(callback, null, cacheEntry.result);
|
---|
182 | }
|
---|
183 |
|
---|
184 | // Check if there is already the same operation running
|
---|
185 | let callbacks = this._activeAsyncOperations.get(path);
|
---|
186 | if (callbacks !== undefined) {
|
---|
187 | callbacks.push(callback);
|
---|
188 | return;
|
---|
189 | }
|
---|
190 | this._activeAsyncOperations.set(path, (callbacks = [callback]));
|
---|
191 |
|
---|
192 | // Run the operation
|
---|
193 | this._provider.call(this._providerContext, path, (err, result) => {
|
---|
194 | this._activeAsyncOperations.delete(path);
|
---|
195 | this._storeResult(path, err, result);
|
---|
196 |
|
---|
197 | // Enter async mode if not yet done
|
---|
198 | this._enterAsyncMode();
|
---|
199 |
|
---|
200 | runCallbacks(callbacks, err, result);
|
---|
201 | });
|
---|
202 | }
|
---|
203 |
|
---|
204 | provideSync(path, options) {
|
---|
205 | if (typeof path !== "string") {
|
---|
206 | throw new TypeError("path must be a string");
|
---|
207 | }
|
---|
208 | if (options) {
|
---|
209 | return this._syncProvider.call(this._providerContext, path, options);
|
---|
210 | }
|
---|
211 |
|
---|
212 | // In sync mode we may have to decay some cache items
|
---|
213 | if (this._mode === STORAGE_MODE_SYNC) {
|
---|
214 | this._runDecays();
|
---|
215 | }
|
---|
216 |
|
---|
217 | // Check in cache
|
---|
218 | let cacheEntry = this._data.get(path);
|
---|
219 | if (cacheEntry !== undefined) {
|
---|
220 | if (cacheEntry.err) throw cacheEntry.err;
|
---|
221 | return cacheEntry.result;
|
---|
222 | }
|
---|
223 |
|
---|
224 | // Get all active async operations
|
---|
225 | // This sync operation will also complete them
|
---|
226 | const callbacks = this._activeAsyncOperations.get(path);
|
---|
227 | this._activeAsyncOperations.delete(path);
|
---|
228 |
|
---|
229 | // Run the operation
|
---|
230 | // When in idle mode, we will enter sync mode
|
---|
231 | let result;
|
---|
232 | try {
|
---|
233 | result = this._syncProvider.call(this._providerContext, path);
|
---|
234 | } catch (err) {
|
---|
235 | this._storeResult(path, err, undefined);
|
---|
236 | this._enterSyncModeWhenIdle();
|
---|
237 | if (callbacks) runCallbacks(callbacks, err, undefined);
|
---|
238 | throw err;
|
---|
239 | }
|
---|
240 | this._storeResult(path, undefined, result);
|
---|
241 | this._enterSyncModeWhenIdle();
|
---|
242 | if (callbacks) runCallbacks(callbacks, undefined, result);
|
---|
243 | return result;
|
---|
244 | }
|
---|
245 |
|
---|
246 | purge(what) {
|
---|
247 | if (!what) {
|
---|
248 | if (this._mode !== STORAGE_MODE_IDLE) {
|
---|
249 | this._data.clear();
|
---|
250 | for (const level of this._levels) {
|
---|
251 | level.clear();
|
---|
252 | }
|
---|
253 | this._enterIdleMode();
|
---|
254 | }
|
---|
255 | } else if (typeof what === "string") {
|
---|
256 | for (let [key, data] of this._data) {
|
---|
257 | if (key.startsWith(what)) {
|
---|
258 | this._data.delete(key);
|
---|
259 | data.level.delete(key);
|
---|
260 | }
|
---|
261 | }
|
---|
262 | if (this._data.size === 0) {
|
---|
263 | this._enterIdleMode();
|
---|
264 | }
|
---|
265 | } else {
|
---|
266 | for (let [key, data] of this._data) {
|
---|
267 | for (const item of what) {
|
---|
268 | if (key.startsWith(item)) {
|
---|
269 | this._data.delete(key);
|
---|
270 | data.level.delete(key);
|
---|
271 | break;
|
---|
272 | }
|
---|
273 | }
|
---|
274 | }
|
---|
275 | if (this._data.size === 0) {
|
---|
276 | this._enterIdleMode();
|
---|
277 | }
|
---|
278 | }
|
---|
279 | }
|
---|
280 |
|
---|
281 | purgeParent(what) {
|
---|
282 | if (!what) {
|
---|
283 | this.purge();
|
---|
284 | } else if (typeof what === "string") {
|
---|
285 | this.purge(dirname(what));
|
---|
286 | } else {
|
---|
287 | const set = new Set();
|
---|
288 | for (const item of what) {
|
---|
289 | set.add(dirname(item));
|
---|
290 | }
|
---|
291 | this.purge(set);
|
---|
292 | }
|
---|
293 | }
|
---|
294 |
|
---|
295 | _storeResult(path, err, result) {
|
---|
296 | if (this._data.has(path)) return;
|
---|
297 | const level = this._levels[this._currentLevel];
|
---|
298 | this._data.set(path, { err, result, level });
|
---|
299 | level.add(path);
|
---|
300 | }
|
---|
301 |
|
---|
302 | _decayLevel() {
|
---|
303 | const nextLevel = (this._currentLevel + 1) % this._levels.length;
|
---|
304 | const decay = this._levels[nextLevel];
|
---|
305 | this._currentLevel = nextLevel;
|
---|
306 | for (let item of decay) {
|
---|
307 | this._data.delete(item);
|
---|
308 | }
|
---|
309 | decay.clear();
|
---|
310 | if (this._data.size === 0) {
|
---|
311 | this._enterIdleMode();
|
---|
312 | } else {
|
---|
313 | // @ts-ignore _nextDecay is always a number in sync mode
|
---|
314 | this._nextDecay += this._tickInterval;
|
---|
315 | }
|
---|
316 | }
|
---|
317 |
|
---|
318 | _runDecays() {
|
---|
319 | while (
|
---|
320 | /** @type {number} */ (this._nextDecay) <= Date.now() &&
|
---|
321 | this._mode !== STORAGE_MODE_IDLE
|
---|
322 | ) {
|
---|
323 | this._decayLevel();
|
---|
324 | }
|
---|
325 | }
|
---|
326 |
|
---|
327 | _enterAsyncMode() {
|
---|
328 | let timeout = 0;
|
---|
329 | switch (this._mode) {
|
---|
330 | case STORAGE_MODE_ASYNC:
|
---|
331 | return;
|
---|
332 | case STORAGE_MODE_IDLE:
|
---|
333 | this._nextDecay = Date.now() + this._tickInterval;
|
---|
334 | timeout = this._tickInterval;
|
---|
335 | break;
|
---|
336 | case STORAGE_MODE_SYNC:
|
---|
337 | this._runDecays();
|
---|
338 | // @ts-ignore _runDecays may change the mode
|
---|
339 | if (this._mode === STORAGE_MODE_IDLE) return;
|
---|
340 | timeout = Math.max(
|
---|
341 | 0,
|
---|
342 | /** @type {number} */ (this._nextDecay) - Date.now()
|
---|
343 | );
|
---|
344 | break;
|
---|
345 | }
|
---|
346 | this._mode = STORAGE_MODE_ASYNC;
|
---|
347 | const ref = setTimeout(() => {
|
---|
348 | this._mode = STORAGE_MODE_SYNC;
|
---|
349 | this._runDecays();
|
---|
350 | }, timeout);
|
---|
351 | if (ref.unref) ref.unref();
|
---|
352 | this._timeout = ref;
|
---|
353 | }
|
---|
354 |
|
---|
355 | _enterSyncModeWhenIdle() {
|
---|
356 | if (this._mode === STORAGE_MODE_IDLE) {
|
---|
357 | this._mode = STORAGE_MODE_SYNC;
|
---|
358 | this._nextDecay = Date.now() + this._tickInterval;
|
---|
359 | }
|
---|
360 | }
|
---|
361 |
|
---|
362 | _enterIdleMode() {
|
---|
363 | this._mode = STORAGE_MODE_IDLE;
|
---|
364 | this._nextDecay = undefined;
|
---|
365 | if (this._timeout) clearTimeout(this._timeout);
|
---|
366 | }
|
---|
367 | }
|
---|
368 |
|
---|
369 | const createBackend = (duration, provider, syncProvider, providerContext) => {
|
---|
370 | if (duration > 0) {
|
---|
371 | return new CacheBackend(duration, provider, syncProvider, providerContext);
|
---|
372 | }
|
---|
373 | return new OperationMergerBackend(provider, syncProvider, providerContext);
|
---|
374 | };
|
---|
375 |
|
---|
376 | module.exports = class CachedInputFileSystem {
|
---|
377 | constructor(fileSystem, duration) {
|
---|
378 | this.fileSystem = fileSystem;
|
---|
379 |
|
---|
380 | this._lstatBackend = createBackend(
|
---|
381 | duration,
|
---|
382 | this.fileSystem.lstat,
|
---|
383 | this.fileSystem.lstatSync,
|
---|
384 | this.fileSystem
|
---|
385 | );
|
---|
386 | const lstat = this._lstatBackend.provide;
|
---|
387 | this.lstat = /** @type {FileSystem["lstat"]} */ (lstat);
|
---|
388 | const lstatSync = this._lstatBackend.provideSync;
|
---|
389 | this.lstatSync = /** @type {SyncFileSystem["lstatSync"]} */ (lstatSync);
|
---|
390 |
|
---|
391 | this._statBackend = createBackend(
|
---|
392 | duration,
|
---|
393 | this.fileSystem.stat,
|
---|
394 | this.fileSystem.statSync,
|
---|
395 | this.fileSystem
|
---|
396 | );
|
---|
397 | const stat = this._statBackend.provide;
|
---|
398 | this.stat = /** @type {FileSystem["stat"]} */ (stat);
|
---|
399 | const statSync = this._statBackend.provideSync;
|
---|
400 | this.statSync = /** @type {SyncFileSystem["statSync"]} */ (statSync);
|
---|
401 |
|
---|
402 | this._readdirBackend = createBackend(
|
---|
403 | duration,
|
---|
404 | this.fileSystem.readdir,
|
---|
405 | this.fileSystem.readdirSync,
|
---|
406 | this.fileSystem
|
---|
407 | );
|
---|
408 | const readdir = this._readdirBackend.provide;
|
---|
409 | this.readdir = /** @type {FileSystem["readdir"]} */ (readdir);
|
---|
410 | const readdirSync = this._readdirBackend.provideSync;
|
---|
411 | this.readdirSync = /** @type {SyncFileSystem["readdirSync"]} */ (readdirSync);
|
---|
412 |
|
---|
413 | this._readFileBackend = createBackend(
|
---|
414 | duration,
|
---|
415 | this.fileSystem.readFile,
|
---|
416 | this.fileSystem.readFileSync,
|
---|
417 | this.fileSystem
|
---|
418 | );
|
---|
419 | const readFile = this._readFileBackend.provide;
|
---|
420 | this.readFile = /** @type {FileSystem["readFile"]} */ (readFile);
|
---|
421 | const readFileSync = this._readFileBackend.provideSync;
|
---|
422 | this.readFileSync = /** @type {SyncFileSystem["readFileSync"]} */ (readFileSync);
|
---|
423 |
|
---|
424 | this._readJsonBackend = createBackend(
|
---|
425 | duration,
|
---|
426 | this.fileSystem.readJson ||
|
---|
427 | (this.readFile &&
|
---|
428 | ((path, callback) => {
|
---|
429 | // @ts-ignore
|
---|
430 | this.readFile(path, (err, buffer) => {
|
---|
431 | if (err) return callback(err);
|
---|
432 | if (!buffer || buffer.length === 0)
|
---|
433 | return callback(new Error("No file content"));
|
---|
434 | let data;
|
---|
435 | try {
|
---|
436 | data = JSON.parse(buffer.toString("utf-8"));
|
---|
437 | } catch (e) {
|
---|
438 | return callback(e);
|
---|
439 | }
|
---|
440 | callback(null, data);
|
---|
441 | });
|
---|
442 | })),
|
---|
443 | this.fileSystem.readJsonSync ||
|
---|
444 | (this.readFileSync &&
|
---|
445 | (path => {
|
---|
446 | const buffer = this.readFileSync(path);
|
---|
447 | const data = JSON.parse(buffer.toString("utf-8"));
|
---|
448 | return data;
|
---|
449 | })),
|
---|
450 | this.fileSystem
|
---|
451 | );
|
---|
452 | const readJson = this._readJsonBackend.provide;
|
---|
453 | this.readJson = /** @type {FileSystem["readJson"]} */ (readJson);
|
---|
454 | const readJsonSync = this._readJsonBackend.provideSync;
|
---|
455 | this.readJsonSync = /** @type {SyncFileSystem["readJsonSync"]} */ (readJsonSync);
|
---|
456 |
|
---|
457 | this._readlinkBackend = createBackend(
|
---|
458 | duration,
|
---|
459 | this.fileSystem.readlink,
|
---|
460 | this.fileSystem.readlinkSync,
|
---|
461 | this.fileSystem
|
---|
462 | );
|
---|
463 | const readlink = this._readlinkBackend.provide;
|
---|
464 | this.readlink = /** @type {FileSystem["readlink"]} */ (readlink);
|
---|
465 | const readlinkSync = this._readlinkBackend.provideSync;
|
---|
466 | this.readlinkSync = /** @type {SyncFileSystem["readlinkSync"]} */ (readlinkSync);
|
---|
467 | }
|
---|
468 |
|
---|
469 | purge(what) {
|
---|
470 | this._statBackend.purge(what);
|
---|
471 | this._lstatBackend.purge(what);
|
---|
472 | this._readdirBackend.purgeParent(what);
|
---|
473 | this._readFileBackend.purge(what);
|
---|
474 | this._readlinkBackend.purge(what);
|
---|
475 | this._readJsonBackend.purge(what);
|
---|
476 | }
|
---|
477 | };
|
---|