Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/Caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The main option for configuring the Metro cache is [`cacheStores`](./Configurati
Metro provides a number of built-in cache store implementations for use with the [`cacheStores`](./Configuration.md#cachestores) config option:

* **`FileStore({root: string})`** will store cache entries as files under the directory specified by `root`.
* **`AutoCleanFileStore()`** is a `FileStore` that periodically cleans up old entries. It accepts the same options as `FileStore` plus the following:
* **`AutoCleanFileStore()`** <div class="label deprecated">Deprecated</div> is a `FileStore` that periodically cleans up old entries. It accepts the same options as `FileStore` plus the following:
* **`options.intervalMs: number`** is the time in milliseconds between cleanup attempts. Defaults to 10 minutes.
* **`options.cleanupThresholdMs: number`** is the minimum time in milliseconds since the last modification of an entry before it can be deleted. Defaults to 3 days.
* **`HttpStore(options)`** is a bare-bones remote cache client that reads (`GET`) and writes (`PUT`) compressed cache artifacts over HTTP or HTTPS.
Expand Down
2 changes: 1 addition & 1 deletion packages/metro-cache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"metro-core": "0.83.1"
},
"devDependencies": {
"metro-memory-fs": "*"
"memfs": "^4.38.2"
},
"license": "MIT",
"engines": {
Expand Down
20 changes: 9 additions & 11 deletions packages/metro-cache/src/Cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @oncall react_native
*/

import type {CacheStore} from 'metro-cache';
import type {CacheStore} from './types';

import {Logger} from 'metro-core';

Expand All @@ -21,17 +21,15 @@ import {Logger} from 'metro-core';
* All get/set operations are logged via Metro's logger.
*/
export default class Cache<T> {
_stores: $ReadOnlyArray<CacheStore<T>>;

_hits: WeakMap<Buffer, CacheStore<T>>;
+#stores: $ReadOnlyArray<CacheStore<T>>;
+#hits: WeakMap<Buffer, CacheStore<T>> = new WeakMap();

constructor(stores: $ReadOnlyArray<CacheStore<T>>) {
this._hits = new WeakMap();
this._stores = stores;
this.#stores = stores;
}

async get(key: Buffer): Promise<?T> {
const stores = this._stores;
const stores = this.#stores;
const length = stores.length;

for (let i = 0; i < length; i++) {
Expand Down Expand Up @@ -74,7 +72,7 @@ export default class Cache<T> {
);

if (value != null) {
this._hits.set(key, store);
this.#hits.set(key, store);

return value;
}
Expand All @@ -85,8 +83,8 @@ export default class Cache<T> {
}

async set(key: Buffer, value: T): Promise<void> {
const stores = this._stores;
const stop = this._hits.get(key);
const stores = this.#stores;
const stop = this.#hits.get(key);
const length = stores.length;
const promises = [];
const writeErrors = [];
Expand Down Expand Up @@ -133,6 +131,6 @@ export default class Cache<T> {
// writing to the cache is a no-op and reading from the cache will always
// return null.
get isDisabled(): boolean {
return this._stores.length === 0;
return this.#stores.length === 0;
}
}
11 changes: 10 additions & 1 deletion packages/metro-cache/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export {
stableHash,
};

export interface MetroCache {
+AutoCleanFileStore: typeof AutoCleanFileStore;
+Cache: typeof Cache;
+FileStore: typeof FileStore;
+HttpGetStore: typeof HttpGetStore;
+HttpStore: typeof HttpStore;
+stableHash: typeof stableHash;
}

/**
* Backwards-compatibility with CommonJS consumers using interopRequireDefault.
* Do not add to this list.
Expand All @@ -42,4 +51,4 @@ export default {
HttpGetStore,
HttpStore,
stableHash,
};
} as MetroCache;
103 changes: 43 additions & 60 deletions packages/metro-cache/src/stores/AutoCleanFileStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @flow
* @oncall react_native
*/

import type {Options} from './FileStore';
Expand All @@ -14,90 +15,72 @@ import FileStore from './FileStore';
import fs from 'fs';
import path from 'path';

type CleanOptions = {
type CleanOptions = $ReadOnly<{
...Options,
intervalMs?: number,
cleanupThresholdMs?: number,
...
};

type FileList = {
path: string,
stats: fs.Stats,
...
};

// List all files in a directory in Node.js recursively in a synchronous fashion
const walkSync = function (
dir: string,
filelist: Array<FileList>,
): Array<FileList> {
const files = fs.readdirSync(dir);
filelist = filelist || [];
files.forEach(function (file) {
const fullPath = path.join(dir, file);
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
filelist = walkSync(fullPath + path.sep, filelist);
} else {
filelist.push({path: fullPath, stats});
}
});
return filelist;
};

function get<T>(property: ?T, defaultValue: T): T {
if (property == null) {
return defaultValue;
}

return property;
}
}>;

/**
* A FileStore that cleans itself up in a given interval
* A FileStore that, at a given interval, stats the content of the cache root
* and deletes any file last modified a set threshold in the past.
*
* @deprecated This is not efficiently implemented and may cause significant
* redundant I/O when caches are large. Prefer your own cleanup scripts, or a
* custom Metro cache that uses watches, hooks get/set, and/or implements LRU.
*/
export default class AutoCleanFileStore<T> extends FileStore<T> {
_intervalMs: number;
_cleanupThresholdMs: number;
_root: string;
+#intervalMs: number;
+#cleanupThresholdMs: number;
+#root: string;

constructor(opts: CleanOptions) {
super({root: opts.root});

this._intervalMs = get(opts.intervalMs, 10 * 60 * 1000); // 10 minutes
this._cleanupThresholdMs = get(
opts.cleanupThresholdMs,
3 * 24 * 60 * 60 * 1000, // 3 days
);
this.#root = opts.root;
this.#intervalMs = opts.intervalMs ?? 10 * 60 * 1000; // 10 minutes
this.#cleanupThresholdMs =
opts.cleanupThresholdMs ?? 3 * 24 * 60 * 60 * 1000; // 3 days

this._scheduleCleanup();
this.#scheduleCleanup();
}

_scheduleCleanup() {
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
setTimeout(this._doCleanup.bind(this), this._intervalMs);
#scheduleCleanup() {
setTimeout(() => this.#doCleanup(), this.#intervalMs);
}

_doCleanup() {
const files = walkSync(this._root, []);
#doCleanup() {
const dirents = fs.readdirSync(this.#root, {
recursive: true,
withFileTypes: true,
});

let warned = false;
files.forEach(file => {
if (file.stats.mtimeMs < Date.now() - this._cleanupThresholdMs) {
const minModifiedTime = Date.now() - this.#cleanupThresholdMs;
dirents
.filter(dirent => dirent.isFile())
.forEach(dirent => {
const absolutePath = path.join(
// $FlowFixMe[prop-missing] - dirent.parentPath added in Node 20.12
dirent.parentPath,
dirent.name.toString(),
);
try {
fs.unlinkSync(file.path);
if (fs.statSync(absolutePath).mtimeMs < minModifiedTime) {
fs.unlinkSync(absolutePath);
}
} catch (e) {
if (!warned) {
console.warn(
'Problem cleaning up cache for ' + file.path + ': ' + e.message,
'Problem cleaning up cache for ' +
absolutePath +
': ' +
e.message,
);
warned = true;
}
}
}
});

this._scheduleCleanup();
});
this.#scheduleCleanup();
}
}
31 changes: 16 additions & 15 deletions packages/metro-cache/src/stores/FileStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
* @format
* @oncall react_native
*/

import fs from 'fs';
Expand All @@ -14,20 +15,20 @@ import path from 'path';
const NULL_BYTE = 0x00;
const NULL_BYTE_BUFFER = Buffer.from([NULL_BYTE]);

export type Options = {
export type Options = $ReadOnly<{
root: string,
};
}>;

export default class FileStore<T> {
_root: string;
+#root: string;

constructor(options: Options) {
this._root = options.root;
this.#root = options.root;
}

async get(key: Buffer): Promise<?T> {
try {
const data = await fs.promises.readFile(this._getFilePath(key));
const data = await fs.promises.readFile(this.#getFilePath(key));

if (data[0] === NULL_BYTE) {
return (data.slice(1): any);
Expand All @@ -44,20 +45,20 @@ export default class FileStore<T> {
}

async set(key: Buffer, value: T): Promise<void> {
const filePath = this._getFilePath(key);
const filePath = this.#getFilePath(key);
try {
await this._set(filePath, value);
await this.#set(filePath, value);
} catch (err) {
if (err.code === 'ENOENT') {
fs.mkdirSync(path.dirname(filePath), {recursive: true});
await this._set(filePath, value);
await this.#set(filePath, value);
} else {
throw err;
}
}
}

async _set(filePath: string, value: T): Promise<void> {
async #set(filePath: string, value: T): Promise<void> {
let content;
if (value instanceof Buffer) {
content = Buffer.concat([NULL_BYTE_BUFFER, value]);
Expand All @@ -68,20 +69,20 @@ export default class FileStore<T> {
}

clear() {
this._removeDirs();
this.#removeDirs();
}

_getFilePath(key: Buffer): string {
#getFilePath(key: Buffer): string {
return path.join(
this._root,
this.#root,
key.slice(0, 1).toString('hex'),
key.slice(1).toString('hex'),
);
}

_removeDirs() {
#removeDirs() {
for (let i = 0; i < 256; i++) {
fs.rmSync(path.join(this._root, ('0' + i.toString(16)).slice(-2)), {
fs.rmSync(path.join(this.#root, ('0' + i.toString(16)).slice(-2)), {
force: true,
recursive: true,
});
Expand Down
2 changes: 1 addition & 1 deletion packages/metro-cache/src/stores/HttpError.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
* @format
*/

export default class HttpError extends Error {
Expand Down
Loading
Loading