import {Stats} from 'fs'; import {stat} from 'fs/promises'; import {glob} from 'glob'; import path from 'path'; interface JobContext { name: string; inputs: string[]; outputs: string[]; } class JobBuilder { #inputs: string[] = []; #outputs: string[] = []; #callback: (ctx: JobContext) => Promise; #name: string; constructor(name: string, callback: (ctx: JobContext) => Promise) { this.#name = name; this.#callback = callback; } inputs(inputs: string[]): JobBuilder { this.#inputs = inputs.flatMap(value => { value = path.resolve(__dirname, '..', '..', value); const paths = glob.sync(value); return paths.length ? paths : [value]; }); return this; } outputs(outputs: string[]): JobBuilder { if (!this.#name) { this.#name = outputs[0]!; } this.#outputs = outputs.map(value => { return path.resolve(__dirname, '..', '..', value); }); return this; } async build(): Promise { console.log(`Running job ${this.#name}...`); let shouldRun = true; const inputStats = await Promise.all( this.#inputs.map(input => { return stat(input); }) ); let outputStats: Stats[]; try { outputStats = await Promise.all( this.#outputs.map(output => { return stat(output); }) ); if ( outputStats.reduce(reduceMinTime, Infinity) > inputStats.reduce(reduceMaxTime, 0) ) { shouldRun = false; } } catch {} if (shouldRun) { this.#run(); } } #run(): Promise { return this.#callback({ name: this.#name, inputs: this.#inputs, outputs: this.#outputs, }); } } export const job = ( name: string, callback: (ctx: JobContext) => Promise ): JobBuilder => { return new JobBuilder(name, callback); }; const reduceMaxTime = (time: number, stat: Stats) => { return time < stat.mtimeMs ? stat.mtimeMs : time; }; const reduceMinTime = (time: number, stat: Stats) => { return time > stat.mtimeMs ? stat.mtimeMs : time; };