Files
meteor/tools/fs/optimistic.js

241 lines
6.0 KiB
JavaScript

import assert from "assert";
import { wrap } from "optimism";
import { Profile } from "../tool-env/profile.js";
import { watch } from "./safe-watcher.js";
import { sha1 } from "./watch.js";
import {
pathSep,
pathIsAbsolute,
statOrNull,
lstat,
readFile,
readdir,
} from "./files.js";
// When in doubt, the optimistic caching system can be completely disabled
// by setting this environment variable.
const ENABLED = ! process.env.METEOR_DISABLE_OPTIMISTIC_CACHING;
function makeOptimistic(name, fn) {
const wrapper = wrap(ENABLED ? function (...args) {
maybeDependOnNodeModules(args[0]);
return fn.apply(this, args);
} : fn, {
makeCacheKey(...args) {
if (! ENABLED) {
// Cache nothing when the optimistic caching system is disabled.
return;
}
const path = args[0];
if (! pathIsAbsolute(path)) {
return;
}
var parts = [];
for (var i = 0; i < args.length; ++i) {
var arg = args[i];
if (typeof arg !== "string") {
// If any of the arguments is not a string, then we won't cache the
// result of the corresponding file.* method invocation.
return;
}
parts.push(arg);
}
return parts.join("\0");
},
subscribe(...args) {
const path = args[0];
if (! shouldWatch(path)) {
return;
}
assert.ok(pathIsAbsolute(path));
let watcher = watch(path, () => {
wrapper.dirty(...args);
});
return () => {
if (watcher) {
watcher.close();
watcher = null;
}
};
}
});
return Profile("optimistic " + name, wrapper);
}
export const shouldWatch = wrap(path => {
const parts = path.split(pathSep);
const nmi = parts.indexOf("node_modules");
if (nmi < 0) {
// Watch everything not in a node_modules directory.
return true;
}
if (nmi < parts.length - 1) {
const nmi2 = parts.indexOf("node_modules", nmi + 1);
if (nmi2 > nmi) {
// If this path is nested inside more than one node_modules
// directory, then it isn't part of a linked npm package, so we
// should not watch it.
return false;
}
const packageDirParts = parts.slice(0, nmi + 2);
if (parts[nmi + 1].startsWith("@")) {
// For linked @scoped npm packages, the symlink is nested inside the
// @scoped directory (which is a child of node_modules).
packageDirParts.push(parts[nmi + 2]);
}
const packageDir = packageDirParts.join(pathSep);
if (optimisticIsSymbolicLink(packageDir)) {
// If this path is in a linked npm package, then it might be under
// active development, so we should watch it.
return true;
}
}
// Starting a watcher for every single file contained within a
// node_modules directory would be prohibitively expensive, so
// instead we rely on dependOnNodeModules to tell us when files in
// node_modules directories might have changed.
return false;
});
function maybeDependOnNodeModules(path) {
if (typeof path !== "string") {
return;
}
const parts = path.split(pathSep);
while (true) {
const index = parts.lastIndexOf("node_modules");
if (index < 0) {
return;
}
parts.length = index + 1;
dependOnNodeModules(parts.join(pathSep));
assert.strictEqual(parts.pop(), "node_modules");
}
}
let npmDepCount = 0;
// Called by any optimistic function that receives a */node_modules/* path
// as its first argument, so that we can later bulk-invalidate the results
// of those calls if the contents of the node_modules directory change.
// Note that this strategy will not detect changes within subdirectories
// of this node_modules directory, but that's ok because the use case we
// care about is adding or removing npm packages.
const dependOnNodeModules = wrap(nodeModulesDir => {
assert(pathIsAbsolute(nodeModulesDir));
assert(nodeModulesDir.endsWith(pathSep + "node_modules"));
// Always return something different to prevent optimism from
// second-guessing the dirtiness of this function.
return ++npmDepCount;
}, {
subscribe(nodeModulesDir) {
let watcher = watch(
nodeModulesDir,
() => dependOnNodeModules.dirty(nodeModulesDir),
);
return function () {
if (watcher) {
watcher.close();
watcher = null;
}
};
}
});
// Invalidate all optimistic results derived from paths involving the
// given node_modules directory.
export function dirtyNodeModulesDirectory(nodeModulesDir) {
dependOnNodeModules.dirty(nodeModulesDir);
}
export const optimisticStatOrNull = makeOptimistic("statOrNull", statOrNull);
export const optimisticLStat = makeOptimistic("lstat", lstat);
export const optimisticLStatOrNull = makeOptimistic("lstatOrNull", path => {
try {
return optimisticLStat(path);
} catch (e) {
if (e.code !== "ENOENT") throw e;
return null;
}
});
export const optimisticReadFile = makeOptimistic("readFile", readFile);
export const optimisticReaddir = makeOptimistic("readdir", readdir);
export const optimisticHashOrNull = makeOptimistic("hashOrNull", (...args) => {
try {
return sha1(optimisticReadFile(...args));
} catch (e) {
if (e.code !== "EISDIR" &&
e.code !== "ENOENT") {
throw e;
}
}
return null;
});
export const optimisticReadJsonOrNull =
makeOptimistic("readJsonOrNull", (path, options) => {
try {
return JSON.parse(optimisticReadFile(path, options));
} catch (e) {
if (e.code === "ENOENT") {
return null;
}
if (e instanceof SyntaxError &&
options && options.allowSyntaxError) {
return null;
}
throw e;
}
});
const optimisticIsSymbolicLink = wrap(path => {
try {
return lstat(path).isSymbolicLink();
} catch (e) {
if (e.code !== "ENOENT") throw e;
return false;
}
}, {
subscribe(path) {
let watcher = watch(path, () => {
optimisticIsSymbolicLink.dirty(path);
});
return function () {
if (watcher) {
watcher.close();
watcher = null;
}
};
}
});