-
Notifications
You must be signed in to change notification settings - Fork 9.5k
/
Copy pathcomputed-artifact.js
83 lines (69 loc) · 3.44 KB
/
computed-artifact.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/**
* @license
* Copyright 2018 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import log from 'lighthouse-logger';
import {ArbitraryEqualityMap} from '../lib/arbitrary-equality-map.js';
import {isUnderTest} from '../lib/lh-env.js';
/**
* Decorate computableArtifact with a caching `request()` method which will
* automatically call `computableArtifact.compute_()` under the hood.
* @template {{name: string, compute_(dependencies: unknown, context: LH.Artifacts.ComputedContext): Promise<unknown>}} C
* @template {Array<keyof LH.Util.FirstParamType<C['compute_']>>} K
* @param {C} computableArtifact
* @param {(K & ([keyof LH.Util.FirstParamType<C['compute_']>] extends [K[number]] ? unknown : never)) | null} keys
* List of properties of `dependencies` used by `compute_`; other properties are filtered out.
* Use `null` to allow all properties. Ensures that only required properties are used for caching result.
* For optional properties of `dependencies`, undefined cannot be used and if found is treated as an error.
* This is to guard against developer mistakes. For optional properties, make it nullable instead.
*/
function makeComputedArtifact(computableArtifact, keys) {
// tsc (3.1) has more difficulty with template inter-references in jsdoc, so
// give types to params and return value the long way, essentially recreating
// polymorphic-this behavior for C.
/**
* Return an automatically cached result from the computed artifact.
* @param {LH.Util.FirstParamType<C['compute_']>} dependencies
* @param {LH.Artifacts.ComputedContext} context
* @return {ReturnType<C['compute_']>}
*/
const request = (dependencies, context) => {
const computedName = computableArtifact.name;
// Guard against missing properties. Optional properties must be passed as null - if missing or
// undefined, throw an error.
for (const key of keys || []) {
if (dependencies && typeof dependencies === 'object' && dependencies[key] === undefined) {
// eslint-disable-next-line max-len
const err = new Error(`missing required key "${String(key)}" for computed artifact ${computableArtifact.name}`);
if (isUnderTest) {
throw err;
} else {
// For now, simply log in production.
log.error(`lh:computed:${computedName}`, err);
}
}
}
const pickedDependencies = keys ?
Object.fromEntries(keys.map(key => [key, dependencies[key]])) :
dependencies;
// NOTE: break immutability solely for this caching-controller function.
const computedCache = /** @type {Map<string, ArbitraryEqualityMap>} */ (context.computedCache);
const cache = computedCache.get(computedName) || new ArbitraryEqualityMap();
computedCache.set(computedName, cache);
/** @type {ReturnType<C['compute_']>|undefined} */
const computed = cache.get(pickedDependencies);
if (computed) {
return computed;
}
const status = {msg: `Computing artifact: ${computedName}`, id: `lh:computed:${computedName}`};
log.time(status, 'verbose');
const artifactPromise = /** @type {ReturnType<C['compute_']>} */
(computableArtifact.compute_(pickedDependencies, context));
cache.set(pickedDependencies, artifactPromise);
artifactPromise.then(() => log.timeEnd(status)).catch(() => log.timeEnd(status));
return artifactPromise;
};
return Object.assign(computableArtifact, {request});
}
export {makeComputedArtifact};