react-native
Version:
A framework for building native apps using React
255 lines (219 loc) • 7.11 kB
JavaScript
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
;
const crypto = require('crypto');
const denodeify = require('denodeify');
const fs = require('graceful-fs');
const isAbsolutePath = require('absolute-path');
const path = require('path');
const tmpDir = require('os').tmpdir();
function getObjectValues<T>(object: {[key: string]: T}): Array<T> {
return Object.keys(object).map(key => object[key]);
}
function debounce(fn, delay) {
var timeout;
return () => {
clearTimeout(timeout);
timeout = setTimeout(fn, delay);
};
}
type Record = {
data: {[field: string]: Promise<mixed>},
metadata: {[field: string]: Promise<mixed>},
};
class Cache {
_cacheFilePath: string;
_data: {[filename: string]: Record};
_persistEventually: () => void;
_persisting: ?Promise<boolean> | void;
constructor({
resetCache,
cacheKey,
cacheDirectory = tmpDir,
}: {
resetCache: boolean,
cacheKey: string,
cacheDirectory?: string,
}) {
this._cacheFilePath = Cache.getCacheFilePath(cacheDirectory, cacheKey);
if (!resetCache) {
this._data = this._loadCacheSync(this._cacheFilePath);
} else {
this._data = Object.create(null);
}
this._persistEventually = debounce(this._persistCache.bind(this), 2000);
}
static getCacheFilePath(tmpdir, ...args) {
const hash = crypto.createHash('md5');
args.forEach(arg => hash.update(arg));
return path.join(tmpdir, hash.digest('hex'));
}
get<T>(
filepath: string,
field: string,
loaderCb: (filepath: string) => Promise<T>,
): Promise<T> {
if (!isAbsolutePath(filepath)) {
throw new Error('Use absolute paths');
}
return this.has(filepath, field)
/* $FlowFixMe: this class is unsound as a whole because it uses
* untyped storage where in fact each "field" has a particular type.
* We cannot express this using Flow. */
? (this._data[filepath].data[field]: Promise<T>)
: this.set(filepath, field, loaderCb(filepath));
}
invalidate(filepath: string, field: ?string) {
if (this.has(filepath, field)) {
if (field == null) {
delete this._data[filepath];
} else {
delete this._data[filepath].data[field];
}
}
}
end() {
return this._persistCache();
}
has(filepath: string, field: ?string) {
return Object.prototype.hasOwnProperty.call(this._data, filepath) &&
(field == null || Object.prototype.hasOwnProperty.call(this._data[filepath].data, field));
}
set<T>(
filepath: string,
field: string,
loaderPromise: Promise<T>,
): Promise<T> {
let record = this._data[filepath];
if (!record) {
// $FlowFixMe: temporarily invalid record.
record = (Object.create(null): Record);
this._data[filepath] = record;
this._data[filepath].data = Object.create(null);
this._data[filepath].metadata = Object.create(null);
}
const cachedPromise = record.data[field] = loaderPromise
.then(data => Promise.all([
data,
denodeify(fs.stat)(filepath),
]))
.then(([data, stat]) => {
this._persistEventually();
// Evict all existing field data from the cache if we're putting new
// more up to date data
var mtime = stat.mtime.getTime();
if (record.metadata.mtime !== mtime) {
record.data = Object.create(null);
}
record.metadata.mtime = mtime;
return data;
});
// don't cache rejected promises
cachedPromise.catch(error => delete record.data[field]);
return cachedPromise;
}
_persistCache() {
if (this._persisting != null) {
return this._persisting;
}
const data = this._data;
const cacheFilepath = this._cacheFilePath;
const allPromises = getObjectValues(data)
.map(record => {
const fieldNames = Object.keys(record.data);
const fieldValues = getObjectValues(record.data);
return Promise
.all(fieldValues)
.then(ref => {
// $FlowFixMe: temporarily invalid record.
const ret = (Object.create(null): Record);
ret.metadata = record.metadata;
ret.data = Object.create(null);
/* $FlowFixMe(>=0.36.0 site=react_native_fb,react_native_oss) Flow
* error detected during the deploy of Flow v0.36.0. To see the
* error, remove this comment and run Flow */
fieldNames.forEach((field, index) =>
ret.data[field] = ref[index]
);
return ret;
});
}
);
this._persisting = Promise.all(allPromises)
.then(values => {
const json = Object.create(null);
Object.keys(data).forEach((key, i) => {
// make sure the key wasn't added nor removed after we started
// persisting the cache
const value = values[i];
if (!value) {
return;
}
json[key] = Object.create(null);
json[key].metadata = data[key].metadata;
json[key].data = value.data;
});
return denodeify(fs.writeFile)(cacheFilepath, JSON.stringify(json));
})
.catch(e => console.error(
'[node-haste] Encountered an error while persisting cache:\n%s',
e.stack.split('\n').map(line => '> ' + line).join('\n')
))
.then(() => {
this._persisting = null;
return true;
});
return this._persisting;
}
_loadCacheSync(cachePath) {
var ret = Object.create(null);
var cacheOnDisk = loadCacheSync(cachePath);
// Filter outdated cache and convert to promises.
Object.keys(cacheOnDisk).forEach(key => {
if (!fs.existsSync(key)) {
return;
}
var record = cacheOnDisk[key];
var stat = fs.statSync(key);
if (stat.mtime.getTime() === record.metadata.mtime) {
ret[key] = Object.create(null);
ret[key].metadata = Object.create(null);
ret[key].data = Object.create(null);
// $FlowFixMe: we should maybe avoid Object.create().
ret[key].metadata.mtime = record.metadata.mtime;
Object.keys(record.data).forEach(field => {
ret[key].data[field] = Promise.resolve(record.data[field]);
});
}
});
return ret;
}
}
function loadCacheSync(cachePath) {
if (!fs.existsSync(cachePath)) {
return Object.create(null);
}
try {
return JSON.parse(fs.readFileSync(cachePath, 'utf8'));
} catch (e) {
if (e instanceof SyntaxError) {
console.warn('Unable to parse cache file. Will clear and continue.');
try {
fs.unlinkSync(cachePath);
} catch (err) {
// Someone else might've deleted it.
}
return Object.create(null);
}
throw e;
}
}
module.exports = Cache;