280 lines
7.7 KiB
JavaScript
Raw Permalink Normal View History

2020-02-25 19:37:10 -08:00
const path = require('path');
const Promise = require('bluebird');
const fs = require('fs-extra');
const { memoize: memoizeSync } = require('lodash');
const memoizeAsync = require('memoizepromise');
const { resolve, readFile } = require('./resolve');
2020-02-28 10:31:13 -08:00
const { hasOwn, isFunction } = require('./lib/util');
2020-02-25 19:37:10 -08:00
const revHash = require('rev-hash');
const revPath = require('rev-path');
const log = require('fancy-log');
2020-02-25 19:37:10 -08:00
const CACHE = 'if-cache';
const MANIFEST = 'if-cache.json';
const REV_MANIFEST = 'rev-manifest.json';
module.exports = exports = class Manifest {
constructor ({ /* time = true, */ inputRev = true, prod = false, writeCount = 100, writeInterval = 10000 }) {
this.compareBy = { inputRev };
2020-02-25 19:37:10 -08:00
this.manifest = {};
this.rev = memoizeSync(revHash);
this.stat = memoizeAsync((f) =>
fs.stat(resolve(f))
.catch(() => null)
.then((stats) => (stats && Math.floor(stats.mtimeMs / 1000)))
2020-02-25 19:37:10 -08:00
);
this.revFile = memoizeAsync((f) =>
readFile(f)
.then(revHash)
.catch(() => null)
2020-02-25 19:37:10 -08:00
);
this.isProd = prod;
this.writeCounter = 0;
this.lastWriteTime = 0;
this.writeCountThreshold = writeCount;
this.writeTimeThreshold = writeInterval;
this.revManifest = {};
}
async load () {
const [ manifest ] = await Promise.all([
fs.readJson(resolve(MANIFEST)).catch(() => ({})),
fs.ensureDir(resolve(CACHE)),
]);
this.manifest = manifest;
}
hash ({ action, input, output, ...task }) {
if (!isFunction(action)) {
2021-08-25 08:53:42 -07:00
console.error({ action, input, output }); // eslint-disable-line
throw new Error('Task action is not a task action (function).');
}
2020-02-25 19:37:10 -08:00
const name = action.name;
const hash = [
name,
this.rev(input),
this.rev(output),
];
// if this is an image operation, include the format and width in the hash
if (name === 'image') hash.splice(1, 0, task.width, task.format);
2020-02-25 19:37:10 -08:00
return hash.filter(Boolean).join('.');
2020-02-25 19:37:10 -08:00
}
has (task) {
const hash = this.hash(task);
return hasOwn(this.manifest, hash);
}
async get (task) {
if (task === undefined || task === null) {
log.error(task);
throw new Error('Task action is undefined or null.');
}
if (task.input === undefined || task.input === null) {
log.error(task);
throw new Error('Task action is missing input. (tip: remove `twitter-cache.json` and run `gulp` again)');
}
2020-02-25 19:37:10 -08:00
const hash = this.hash(task);
const { input, output, cache: altCachePath } = task;
2020-02-25 19:37:10 -08:00
const ext = path.extname(task.output);
const local = !task.input.includes('://');
var cached = path.join(CACHE, hash + ext);
2020-02-25 19:37:10 -08:00
const result = {
iTime: 0,
iRev: null,
oRev: null,
...this.manifest[hash],
hash,
action: task.action.name,
input,
output,
duplicate: altCachePath,
2020-02-25 19:37:10 -08:00
mode: 'new',
};
var acTime;
if (altCachePath) {
acTime = await this.stat(altCachePath);
}
2020-02-25 19:37:10 -08:00
const [ iTime, oTime, cTime, iRev ] = await Promise.all([
local && this.stat(input),
this.stat(output),
this.stat(cached),
local && this.compareBy.inputRev && this.revFile(input),
]);
if (task.nocache) {
result.iRev = iRev;
result.mode = 'silent';
return result;
}
2020-02-25 19:37:10 -08:00
if (local && !iTime) throw new Error('Input file does not exist: ' + input);
if (!local && !cTime) {
// This is a remote file and we don't have a cached copy, run it
return result;
} else if (local && !result.iTime) {
// we've never seen this file before, build new
return result;
}
2020-02-25 19:37:10 -08:00
result.outputExists = !!oTime;
if (oTime) {
// output exists, we can move on
result.mode = 'skip';
return result;
}
if (local && this.compareBy.time && iTime > result.iTime) {
result.inputDiffers = true;
result.iRev = iRev;
result.mode = 'update';
result.why = 'input-time';
return result;
}
if (local && this.compareBy.inputRev && iRev !== result.iRev) {
2020-02-25 19:37:10 -08:00
// either we aren't checking time, or the time has changed
// check if the contents changed
result.inputDiffers = true;
result.iRev = iRev;
result.mode = 'update';
result.why = 'input-rev';
2020-02-25 19:37:10 -08:00
return result;
}
if (!cTime || cTime < iTime) {
// output does not exist in the cache or the cached file predates input, we need to remake.
result.inputDiffers = true;
result.oRev = null;
result.mode = 'rebuild';
result.why = 'cache-missing';
2020-02-25 19:37:10 -08:00
return result;
}
result.mode = 'cached';
if (acTime && acTime > cTime) {
result.cache = await readFile(altCachePath);
} else {
result.cache = await readFile(cached);
if (altCachePath && !acTime) {
await fs.ensureDir(resolve(path.dirname(altCachePath)));
await fs.writeFile(resolve(altCachePath), result.cache);
}
}
2020-02-25 19:37:10 -08:00
return result;
}
async touch (task, lastSeen = new Date()) {
if (task.nocache || !task.action.name) return null;
2020-02-25 19:37:10 -08:00
const hash = this.hash(task);
const { input, output, cache: altCachePath } = task;
2020-02-25 19:37:10 -08:00
const local = !task.input.includes('://');
const [ iTime, iRev ] = await Promise.all([
local && this.stat(input),
local && this.compareBy.inputRev && this.revFile(input),
]);
const record = {
...this.manifest[hash],
action: task.action.name,
hash,
input,
iTime,
iRev,
output,
oTime: Math.floor(lastSeen / 1000),
lastSeen,
duplicate: altCachePath,
2020-02-25 19:37:10 -08:00
};
if (record.revPath) this.revManifest[output] = record.revPath;
this.manifest[hash] = record;
await this.writeManifest();
return { ...record };
}
async set (task, result, lastSeen = new Date()) {
const hash = this.hash(task);
const { input, output, cache: altCachePath } = task;
const nocache = task.nocache || task.action.name === 'copy';
2020-02-25 19:37:10 -08:00
const ext = path.extname(task.output);
const local = !task.input.includes('://');
const cached = path.join(CACHE, hash + ext);
const oRev = revHash(result);
if (result && altCachePath) {
await fs.ensureDir(resolve(path.dirname(altCachePath)));
}
2020-02-25 19:37:10 -08:00
const [ iTime, iRev ] = await Promise.all([
local && this.stat(input),
local && this.compareBy.inputRev && this.revFile(input),
result && !nocache && fs.writeFile(resolve(cached), result),
result && altCachePath && fs.writeFile(resolve(altCachePath), result),
2020-02-25 19:37:10 -08:00
]);
const record = {
action: task.action.name,
hash,
input,
iTime,
iRev,
output,
oTime: Math.floor(lastSeen / 1000),
oRev,
lastSeen,
duplicate: altCachePath,
2020-02-25 19:37:10 -08:00
revPath: revPath(output, oRev),
};
this.revManifest[output] = record.revPath;
if (!nocache) {
this.manifest[hash] = record;
await this.writeManifest();
}
2020-02-25 19:37:10 -08:00
return { ...record };
}
async writeManifest (force) {
if (!force && this.isProd) return; // disable interim writes during prod builds.
if (!force && ++this.writeCounter % this.writeCountThreshold) return;
const now = Date.now();
if (!force && now - this.lastWriteTime < this.writeTimeThreshold) return;
this.lastWriteTime = now;
await fs.writeFile(resolve(MANIFEST), JSON.stringify(this.manifest, null, 2));
}
async save () {
const revManifest = false && this.isProd && await fs.readJson(resolve(REV_MANIFEST))
2020-02-25 19:37:10 -08:00
.catch(() => ({}))
.then((old) => ({ ...old, ...this.revManifest }));
await Promise.all([
revManifest && fs.writeFile(resolve(REV_MANIFEST), JSON.stringify(revManifest, null, 2)),
this.writeManifest(true),
]);
2020-04-07 10:31:52 -07:00
return { revManifest: revManifest || {}, manifest: this.manifest };
2020-02-25 19:37:10 -08:00
}
};