Expose more caching flexibility for Babel's internal use.

This commit is contained in:
Logan Smyth
2017-11-14 17:19:38 -08:00
parent e44cef3473
commit 3942862a2a
4 changed files with 254 additions and 223 deletions

View File

@@ -1,12 +1,13 @@
// @flow
type CacheConfigurator = CacheConfiguratorFn & CacheConfiguratorObj;
type SimpleCacheConfigurator = SimpleCacheConfiguratorFn &
SimpleCacheConfiguratorObj;
type CacheConfiguratorFn = {
type SimpleCacheConfiguratorFn = {
(boolean): void,
<T>(handler: () => T): T,
};
type CacheConfiguratorObj = {
type SimpleCacheConfiguratorObj = {
forever: () => void,
never: () => void,
using: <T>(handler: () => T) => T,
@@ -56,165 +57,187 @@ function makeCachedFunction<ArgT, ResultT, Cache: CacheMap<ArgT, ResultT>>(
}
}
const { cache, result, deactivate } = makeCacheConfig();
const cache = new CacheConfigurator();
const value = handler(arg, cache);
if (autoPermacache && !result.configured) cache.forever();
if (autoPermacache && !cache.configured()) cache.forever();
deactivate();
cache.deactivate();
if (!result.configured) {
// eslint-disable-next-line max-len
throw new Error(
[
"Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured",
"for various types of caching, using the first param of their handler functions:",
"",
"module.exports = function(api) {",
" // The API exposes the following:",
"",
" // Cache the returned value forever and don't call this function again.",
" api.cache(true);",
"",
" // Don't cache at all. Not recommended because it will be very slow.",
" api.cache(false);",
"",
" // Cached based on the value of some function. If this function returns a value different from",
" // a previously-encountered value, the plugins will re-evaluate.",
" var env = api.cache(() => process.env.NODE_ENV);",
"",
" // If testing for a specific env, we recommend specifics to avoid instantiating a plugin for",
" // any possible NODE_ENV value that might come up during plugin execution.",
' var isProd = api.cache(() => process.env.NODE_ENV === "production");',
"",
" // .cache(fn) will perform a linear search though instances to find the matching plugin based",
" // based on previous instantiated plugins. If you want to recreate the plugin and discard the",
" // previous instance whenever something changes, you may use:",
' var isProd = api.cache.invalidate(() => process.env.NODE_ENV === "production");',
"",
" // Note, we also expose the following more-verbose versions of the above examples:",
" api.cache.forever(); // api.cache(true)",
" api.cache.never(); // api.cache(false)",
" api.cache.using(fn); // api.cache(fn)",
"",
" // Return the value that will be cached.",
" return { };",
"};",
].join("\n"),
);
}
cache.assertConfigured();
if (!result.never) {
if (result.forever) {
switch (cache.mode()) {
case "forever":
cachedValue = [[value, () => true]];
} else if (result.invalidate) {
cachedValue = [[value, result.valid]];
} else {
cachedValue = cachedValue || [];
cachedValue.push([value, result.valid]);
}
callCache.set(arg, cachedValue);
callCache.set(arg, cachedValue);
break;
case "invalidate":
cachedValue = [[value, cache.validator()]];
callCache.set(arg, cachedValue);
break;
case "valid":
if (cachedValue) {
cachedValue.push([value, cache.validator()]);
} else {
cachedValue = [[value, cache.validator()]];
callCache.set(arg, cachedValue);
}
}
return value;
};
}
function makeCacheConfig(): {
cache: CacheConfigurator,
result: *,
deactivate: () => void,
} {
const pairs = [];
class CacheConfigurator {
_active: boolean = true;
_never: boolean = false;
_forever: boolean = false;
_invalidate: boolean = false;
const result = {
configured: false,
never: false,
forever: false,
invalidate: false,
valid: () => pairs.every(([key, fn]) => key === fn()),
};
_configured: boolean = false;
let active = true;
const deactivate = () => {
active = false;
};
_pairs: Array<[mixed, () => mixed]> = [];
const cache: CacheConfigurator = Object.assign(
(function cacheFn(val) {
if (typeof val === "boolean") {
if (val) cache.forever();
else cache.never();
return;
}
simple() {
return makeSimpleConfigurator(this);
}
return cache.using(val);
}: any),
({
forever() {
if (!active) {
throw new Error(
"Cannot change caching after evaluation has completed.",
);
}
if (result.never) {
throw new Error("Caching has already been configured with .never()");
}
result.forever = true;
result.configured = true;
},
never() {
if (!active) {
throw new Error(
"Cannot change caching after evaluation has completed.",
);
}
if (result.forever) {
throw new Error(
"Caching has already been configured with .forever()",
);
}
result.never = true;
result.configured = true;
},
using<T>(handler: () => T): T {
if (!active) {
throw new Error(
"Cannot change caching after evaluation has completed.",
);
}
if (result.never || result.forever) {
throw new Error(
"Caching has already been configured with .never or .forever()",
);
}
result.configured = true;
mode() {
if (this._never) return "never";
if (this._forever) return "forever";
if (this._invalidate) return "invalidate";
return "valid";
}
const key = handler();
pairs.push([key, handler]);
return key;
},
invalidate<T>(handler: () => T): T {
if (!active) {
throw new Error(
"Cannot change caching after evaluation has completed.",
);
}
if (result.never || result.forever) {
throw new Error(
"Caching has already been configured with .never or .forever()",
);
}
result.invalidate = true;
result.configured = true;
forever() {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._never) {
throw new Error("Caching has already been configured with .never()");
}
this._forever = true;
this._configured = true;
}
const key = handler();
pairs.push([key, handler]);
return key;
},
}: CacheConfiguratorObj),
);
never() {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._forever) {
throw new Error("Caching has already been configured with .forever()");
}
this._never = true;
this._configured = true;
}
return { cache, result, deactivate };
using<T>(handler: () => T): T {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._never || this._forever) {
throw new Error(
"Caching has already been configured with .never or .forever()",
);
}
this._configured = true;
const key = handler();
this._pairs.push([key, handler]);
return key;
}
invalidate<T>(handler: () => T): T {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._never || this._forever) {
throw new Error(
"Caching has already been configured with .never or .forever()",
);
}
this._invalidate = true;
this._configured = true;
const key = handler();
this._pairs.push([key, handler]);
return key;
}
validator(): () => boolean {
const pairs = this._pairs;
return () => pairs.every(([key, fn]) => key === fn());
}
deactivate() {
this._active = false;
}
configured() {
return this._configured;
}
assertConfigured() {
if (this.configured()) return;
// eslint-disable-next-line max-len
throw new Error(
[
"Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured",
"for various types of caching, using the first param of their handler functions:",
"",
"module.exports = function(api) {",
" // The API exposes the following:",
"",
" // Cache the returned value forever and don't call this function again.",
" api.cache(true);",
"",
" // Don't cache at all. Not recommended because it will be very slow.",
" api.cache(false);",
"",
" // Cached based on the value of some function. If this function returns a value different from",
" // a previously-encountered value, the plugins will re-evaluate.",
" var env = api.cache(() => process.env.NODE_ENV);",
"",
" // If testing for a specific env, we recommend specifics to avoid instantiating a plugin for",
" // any possible NODE_ENV value that might come up during plugin execution.",
' var isProd = api.cache(() => process.env.NODE_ENV === "production");',
"",
" // .cache(fn) will perform a linear search though instances to find the matching plugin based",
" // based on previous instantiated plugins. If you want to recreate the plugin and discard the",
" // previous instance whenever something changes, you may use:",
' var isProd = api.cache.invalidate(() => process.env.NODE_ENV === "production");',
"",
" // Note, we also expose the following more-verbose versions of the above examples:",
" api.cache.forever(); // api.cache(true)",
" api.cache.never(); // api.cache(false)",
" api.cache.using(fn); // api.cache(fn)",
"",
" // Return the value that will be cached.",
" return { };",
"};",
].join("\n"),
);
}
}
function makeSimpleConfigurator(
cache: CacheConfigurator,
): SimpleCacheConfigurator {
function cacheFn(val) {
if (typeof val === "boolean") {
if (val) cache.forever();
else cache.never();
return;
}
return cache.using(val);
}
cacheFn.forever = () => cache.forever();
cacheFn.never = () => cache.never();
cacheFn.using = cb => cache.using(cb);
cacheFn.invalidate = cb => cache.invalidate(cb);
return (cacheFn: any);
}

View File

@@ -139,7 +139,7 @@ const readConfigJS = makeStrongCache((filepath, cache) => {
if (typeof options === "function") {
options = options({
cache,
cache: cache.simple(),
// Expose ".env()" so people can easily get the same env that we expose using the "env" key.
env: () => cache.using(() => getEnv()),
async: () => false,

View File

@@ -187,7 +187,7 @@ const loadDescriptor = makeWeakCache(
let item = value;
if (typeof value === "function") {
const api = Object.assign(Object.create(context), {
cache,
cache: cache.simple(),
env: () => cache.using(() => getEnv()),
async: () => false,
});

View File

@@ -19,23 +19,6 @@ describe("caching API", () => {
assert.notEqual(fn("one"), fn("two"));
});
it("should allow permacaching with cache(true)", () => {
let count = 0;
const fn = makeStrongCache((arg, cache) => {
cache(true);
return { arg, count: count++ };
});
assert.deepEqual(fn("one"), { arg: "one", count: 0 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", count: 1 });
assert.equal(fn("two"), fn("two"));
assert.notEqual(fn("one"), fn("two"));
});
it("should allow disabling caching with .never()", () => {
let count = 0;
@@ -55,25 +38,6 @@ describe("caching API", () => {
assert.notEqual(fn("one"), fn("two"));
});
it("should allow disabling caching with cache(false)", () => {
let count = 0;
const fn = makeStrongCache((arg, cache) => {
cache(false);
return { arg, count: count++ };
});
assert.deepEqual(fn("one"), { arg: "one", count: 0 });
assert.deepEqual(fn("one"), { arg: "one", count: 1 });
assert.notEqual(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", count: 4 });
assert.deepEqual(fn("two"), { arg: "two", count: 5 });
assert.notEqual(fn("two"), fn("two"));
assert.notEqual(fn("one"), fn("two"));
});
it("should allow caching based on a value with .using(fn)", () => {
let count = 0;
let other = "default";
@@ -115,47 +79,6 @@ describe("caching API", () => {
assert.equal(fn("two"), fn("two"));
});
it("should allow caching based on a value with cache(fn)", () => {
let count = 0;
let other = "default";
const fn = makeStrongCache((arg, cache) => {
const val = cache(() => other);
return { arg, val, count: count++ };
});
assert.deepEqual(fn("one"), { arg: "one", val: "default", count: 0 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", val: "default", count: 1 });
assert.equal(fn("two"), fn("two"));
other = "new";
assert.deepEqual(fn("one"), { arg: "one", val: "new", count: 2 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", val: "new", count: 3 });
assert.equal(fn("two"), fn("two"));
other = "default";
assert.deepEqual(fn("one"), { arg: "one", val: "default", count: 0 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", val: "default", count: 1 });
assert.equal(fn("two"), fn("two"));
other = "new";
assert.deepEqual(fn("one"), { arg: "one", val: "new", count: 2 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", val: "new", count: 3 });
assert.equal(fn("two"), fn("two"));
});
it("should allow invalidation based on a value with .invalidate(fn)", () => {
let count = 0;
let other = "default";
@@ -410,4 +333,89 @@ describe("caching API", () => {
/Cannot change caching after evaluation/,
);
});
describe("simple", () => {
it("should allow permacaching with cache(true)", () => {
let count = 0;
const fn = makeStrongCache((arg, cache) => {
cache = cache.simple();
cache(true);
return { arg, count: count++ };
});
assert.deepEqual(fn("one"), { arg: "one", count: 0 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", count: 1 });
assert.equal(fn("two"), fn("two"));
assert.notEqual(fn("one"), fn("two"));
});
it("should allow disabling caching with cache(false)", () => {
let count = 0;
const fn = makeStrongCache((arg, cache) => {
cache = cache.simple();
cache(false);
return { arg, count: count++ };
});
assert.deepEqual(fn("one"), { arg: "one", count: 0 });
assert.deepEqual(fn("one"), { arg: "one", count: 1 });
assert.notEqual(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", count: 4 });
assert.deepEqual(fn("two"), { arg: "two", count: 5 });
assert.notEqual(fn("two"), fn("two"));
assert.notEqual(fn("one"), fn("two"));
});
it("should allow caching based on a value with cache(fn)", () => {
let count = 0;
let other = "default";
const fn = makeStrongCache((arg, cache) => {
cache = cache.simple();
const val = cache(() => other);
return { arg, val, count: count++ };
});
assert.deepEqual(fn("one"), { arg: "one", val: "default", count: 0 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", val: "default", count: 1 });
assert.equal(fn("two"), fn("two"));
other = "new";
assert.deepEqual(fn("one"), { arg: "one", val: "new", count: 2 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", val: "new", count: 3 });
assert.equal(fn("two"), fn("two"));
other = "default";
assert.deepEqual(fn("one"), { arg: "one", val: "default", count: 0 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", val: "default", count: 1 });
assert.equal(fn("two"), fn("two"));
other = "new";
assert.deepEqual(fn("one"), { arg: "one", val: "new", count: 2 });
assert.equal(fn("one"), fn("one"));
assert.deepEqual(fn("two"), { arg: "two", val: "new", count: 3 });
assert.equal(fn("two"), fn("two"));
});
});
});