1
Fork 0
defiler/src/Defiler.ts

404 lines
13 KiB
TypeScript
Raw Normal View History

import { AsyncLocalStorage } from 'async_hooks';
2018-10-11 14:45:37 +00:00
import * as fs from 'fs';
2018-06-12 22:15:53 +00:00
import { resolve } from 'path';
import File from './File';
import Watcher, { WatcherEvent } from './Watcher';
export default class Defiler {
// set of original paths for all physical files
paths = new Set<string>();
// original paths -> original file data for all physical files ({ path, stats, bytes, enc })
2019-04-02 21:24:37 +00:00
private _orig_data = new Map<string, FileData>();
2018-06-12 22:15:53 +00:00
// original paths -> transformed files for all physical and virtual files
files = new Map<string, File>();
// Before, During, or After exec has been called
private _status = Status.Before;
// AsyncLocalStorage instance for tracking call stack contexts and dependencies
private _context = new AsyncLocalStorage<Name>();
2018-06-12 22:15:53 +00:00
// Watcher instances
private _watchers: WatcherData[];
2018-06-12 22:15:53 +00:00
// the transform to run on all files
private _transform: Transform;
// registered generators
private _generators: Generator[];
2018-06-12 22:15:53 +00:00
// (base, path) => path resolver function, used in defiler.get and defiler.add from transform
private _resolver: Resolver;
// handler to call when errors occur
private _onerror: OnError;
// original paths of all files currently undergoing transformation and symbols of all generators currently running
private _active = new Set<Name>();
// original paths -> { promise, resolve, paths } objects for when awaited files become available
2019-04-02 21:24:37 +00:00
private _when_found = new Map<string | Filter, WhenFound>();
2018-06-12 22:15:53 +00:00
// array of [dependent, dependency] pairs, specifying changes to which files should trigger re-processing which other files
private _deps: [Name, string | Filter][] = [];
2018-06-12 22:15:53 +00:00
// queue of pending Watcher events to handle
private _queue: [WatcherData, WatcherEvent][] = [];
2018-06-12 22:15:53 +00:00
// whether some Watcher event is currently already in the process of being handled
2019-04-02 21:24:37 +00:00
private _is_processing = false;
2018-06-12 22:15:53 +00:00
// end the current wave
2019-04-02 21:24:37 +00:00
private _end_wave: () => void = null;
2018-06-12 22:15:53 +00:00
constructor(...args: any[]) {
2018-10-11 14:45:37 +00:00
const { transform, generators = [], resolver, onerror } = <DefilerData>args.pop();
2018-06-12 22:15:53 +00:00
if (typeof transform !== 'function') {
throw new TypeError('defiler: transform must be a function');
}
2020-10-25 21:02:15 +00:00
if (!Array.isArray(generators) || generators.some((generator) => typeof generator !== 'function')) {
2018-06-12 22:15:53 +00:00
throw new TypeError('defiler: generators must be an array of functions');
}
if (resolver && typeof resolver !== 'function') {
throw new TypeError('defiler: resolver must be a function');
}
if (onerror && typeof onerror !== 'function') {
throw new TypeError('defiler: onerror must be a function');
}
2018-10-11 14:45:37 +00:00
this._watchers = args.map(({ dir, filter, read = true, enc = 'utf8', pre, watch = true, debounce = 10 }) => {
if (typeof dir !== 'string') {
throw new TypeError('defiler: dir must be a string');
}
if (filter && typeof filter !== 'function') {
throw new TypeError('defiler: filter must be a function');
}
if (typeof read !== 'boolean' && typeof read !== 'function') {
throw new TypeError('defiler: read must be a boolean or a function');
}
if (!Buffer.isEncoding(enc) && typeof enc !== 'function') {
throw new TypeError('defiler: enc must be a supported encoding or a function');
}
if (pre && typeof pre !== 'function') {
throw new TypeError('defiler: pre must be a function');
}
if (typeof watch !== 'boolean') {
throw new TypeError('defiler: watch must be a boolean');
}
if (typeof debounce !== 'number') {
throw new TypeError('defiler: debounce must be a number');
}
return <WatcherData>new Watcher({ dir, filter, read, enc, pre, watch, debounce });
});
2018-06-12 22:15:53 +00:00
this._transform = transform;
this._generators = generators;
2018-06-12 22:15:53 +00:00
this._resolver = resolver;
this._onerror = onerror;
}
// execute everything, and return a promise that resolves when the first wave of processing is complete
async exec(): Promise<void> {
if (this._status !== Status.Before) {
throw new Error('defiler.exec: cannot call more than once');
}
this._status = Status.During;
2019-04-02 21:24:37 +00:00
this._is_processing = true;
const done = this._start_wave();
2018-06-12 22:15:53 +00:00
// init the Watcher instances
2018-10-11 14:45:37 +00:00
const files: [WatcherData, string, { path: string; stats: fs.Stats }][] = [];
2018-06-12 22:15:53 +00:00
await Promise.all(
2020-10-25 21:02:15 +00:00
this._watchers.map(async (watcher) => {
watcher.dir = resolve(watcher.dir);
2020-10-25 21:02:15 +00:00
watcher.on('', (event) => this._enqueue(watcher, event));
2018-06-12 22:15:53 +00:00
// note that all files are pending transformation
await Promise.all(
2022-08-28 10:02:29 +00:00
(
await watcher.init()
).map(async (file) => {
2018-06-12 22:15:53 +00:00
const { path } = file;
if (watcher.pre) {
await watcher.pre(file);
}
this.paths.add(file.path);
this._active.add(file.path);
files.push([watcher, path, file]);
}),
);
}),
);
for (const generator of this._generators) {
this._active.add(generator);
2018-06-12 22:15:53 +00:00
}
// process each physical file
for (const [watcher, path, file] of files) {
2019-04-02 21:24:37 +00:00
this._process_physical_file(watcher, path, file);
2018-06-12 22:15:53 +00:00
}
// process each generator
for (const generator of this._generators) {
2019-04-02 21:24:37 +00:00
this._process_generator(generator);
2018-06-12 22:15:53 +00:00
}
// wait and finish up
await done;
this._status = Status.After;
2019-04-02 21:24:37 +00:00
this._is_processing = false;
2020-10-25 21:02:15 +00:00
if (this._watchers.some((watcher) => watcher.watch)) {
2019-06-08 12:31:02 +00:00
this._enqueue();
}
2018-06-12 22:15:53 +00:00
}
// wait for a file to be available and retrieve it, marking dependencies as appropriate
async get(path: string): Promise<File>;
async get(paths: string[]): Promise<File[]>;
async get(filter: Filter): Promise<File[]>;
async get(_: any): Promise<any> {
2018-06-24 14:59:59 +00:00
if (typeof _ === 'string') {
_ = this.resolve(_);
}
if (Array.isArray(_)) {
2020-10-25 21:02:15 +00:00
return Promise.all(_.map((path) => this.get(path)));
2018-06-12 22:15:53 +00:00
}
if (typeof _ !== 'string' && typeof _ !== 'function') {
2018-10-11 14:45:37 +00:00
throw new TypeError('defiler.get: argument must be a string, an array, or a function');
2018-06-12 22:15:53 +00:00
}
const current = this._context.getStore();
2018-06-12 22:15:53 +00:00
if (current) {
this._deps.push([current, _]);
}
2018-10-11 14:45:37 +00:00
if (this._status === Status.During && current && (typeof _ === 'function' || !this.files.has(_))) {
2019-04-02 21:24:37 +00:00
if (this._when_found.has(_)) {
const { promise, paths } = this._when_found.get(_);
2018-06-12 22:15:53 +00:00
paths.push(current);
this._check_wave();
2018-06-12 22:15:53 +00:00
await promise;
} else {
let resolve;
2020-10-25 21:02:15 +00:00
const promise = new Promise<void>((res) => (resolve = res));
2019-04-02 21:24:37 +00:00
this._when_found.set(_, { promise, resolve, paths: [current] });
this._check_wave();
2018-06-12 22:15:53 +00:00
await promise;
}
}
2018-10-11 14:45:37 +00:00
return typeof _ === 'function' ? this.get([...this.files.keys()].filter(_).sort()) : this.files.get(_);
2018-06-12 22:15:53 +00:00
}
// add a new virtual file
add(file: FileData): Promise<void> {
2018-06-12 22:15:53 +00:00
if (this._status === Status.Before) {
throw new Error('defiler.add: cannot call before calling exec');
}
if (typeof file !== 'object') {
throw new TypeError('defiler.add: file must be an object');
}
file.path = this.resolve(file.path);
2019-04-02 21:24:37 +00:00
this._orig_data.set(file.path, file);
return this._process_file(file, 'add');
2018-06-12 22:15:53 +00:00
}
// resolve a given path from the file currently being transformed
resolve(path: string): string {
2020-10-26 06:42:40 +00:00
if (this._resolver) {
const current = this._context.getStore();
if (typeof current === 'string') {
return this._resolver(current, path);
}
}
return path;
2018-06-12 22:15:53 +00:00
}
// private methods
// return a Promise that we will resolve at the end of this wave, and save its resolver
2019-04-02 21:24:37 +00:00
private _start_wave(): Promise<void> {
2020-10-25 21:02:15 +00:00
return new Promise((res) => (this._end_wave = res));
2018-06-12 22:15:53 +00:00
}
// add a Watcher event to the queue, and handle queued events
2018-10-11 14:45:37 +00:00
private async _enqueue(watcher?: WatcherData, event?: WatcherEvent): Promise<void> {
2018-06-12 22:15:53 +00:00
if (event) {
this._queue.push([watcher, event]);
}
2019-04-02 21:24:37 +00:00
if (this._is_processing) {
2018-06-12 22:15:53 +00:00
return;
}
2019-04-02 21:24:37 +00:00
this._is_processing = true;
2018-06-12 22:15:53 +00:00
while (this._queue.length) {
2019-04-02 21:24:37 +00:00
const done = this._start_wave();
2018-06-12 22:15:53 +00:00
const [watcher, { event, path, stats }] = this._queue.shift();
const file = { path, stats };
if (watcher.pre) {
await watcher.pre(file);
}
if (event === '+') {
2019-04-02 21:24:37 +00:00
this._process_physical_file(watcher, path, file);
2018-06-12 22:15:53 +00:00
} else if (event === '-') {
const { path } = file;
2019-04-02 21:24:37 +00:00
const old_file = this.files.get(path);
2018-06-12 22:15:53 +00:00
this.paths.delete(path);
2019-04-02 21:24:37 +00:00
this._orig_data.delete(path);
2018-06-12 22:15:53 +00:00
this.files.delete(path);
2019-04-02 21:24:37 +00:00
await this._call_transform(old_file, 'delete');
this._process_dependents(path);
2018-06-12 22:15:53 +00:00
}
await done;
}
2019-04-02 21:24:37 +00:00
this._is_processing = false;
2018-06-12 22:15:53 +00:00
}
// create a file object for a physical file and process it
2019-04-02 21:24:37 +00:00
private async _process_physical_file({ dir, read, enc }: WatcherData, path: string, file: FileData): Promise<void> {
2018-06-12 22:15:53 +00:00
if (typeof read === 'function') {
read = await read({ path, stats: file.stats });
}
if (read) {
2020-10-26 00:54:51 +00:00
file.bytes = await fs.promises.readFile(dir + '/' + path);
2018-06-12 22:15:53 +00:00
}
if (typeof enc === 'function') {
enc = await enc({ path, stats: file.stats, bytes: file.bytes });
}
file.enc = enc;
this.paths.add(file.path);
2019-04-02 21:24:37 +00:00
this._orig_data.set(file.path, file);
await this._process_file(file, 'read');
2018-06-12 22:15:53 +00:00
}
// transform a file, store it, and process dependents
2019-04-02 21:24:37 +00:00
private async _process_file(data: FileData, event: string): Promise<void> {
2018-06-12 22:15:53 +00:00
const file: File = Object.assign(new File(), data);
const { path } = file;
this._active.add(path);
2019-04-02 21:24:37 +00:00
await this._call_transform(file, event);
2018-06-12 22:15:53 +00:00
this.files.set(path, file);
if (this._status === Status.During) {
2019-04-02 21:24:37 +00:00
this._mark_found(path);
2018-06-12 22:15:53 +00:00
} else {
2019-04-02 21:24:37 +00:00
this._process_dependents(path);
2018-06-12 22:15:53 +00:00
}
this._active.delete(path);
2019-04-02 21:24:37 +00:00
this._check_wave();
2018-06-12 22:15:53 +00:00
}
// call the transform on a file with the given event string, and handle errors
2019-04-02 21:24:37 +00:00
private async _call_transform(file: File, event: string): Promise<void> {
2018-06-12 22:15:53 +00:00
try {
await this._context.run(file.path, () => this._transform({ file, event }));
2018-06-12 22:15:53 +00:00
} catch (error) {
if (this._onerror) {
this._onerror({ file, event, error });
}
}
}
// run the generator given by the symbol
2019-04-02 21:24:37 +00:00
private async _process_generator(generator: Generator): Promise<void> {
this._active.add(generator);
2018-06-12 22:15:53 +00:00
try {
await this._context.run(generator, generator);
2018-06-12 22:15:53 +00:00
} catch (error) {
if (this._onerror) {
this._onerror({ generator, error });
}
}
this._active.delete(generator);
2019-04-02 21:24:37 +00:00
this._check_wave();
2018-06-12 22:15:53 +00:00
}
// re-process all files that depend on a particular path
2019-04-02 21:24:37 +00:00
private _process_dependents(path: string): void {
2018-06-12 22:15:53 +00:00
const dependents = new Set<Name>();
for (const [dependent, dependency] of this._deps) {
2018-10-11 14:45:37 +00:00
if (typeof dependency === 'string' ? dependency === path : dependency(path)) {
2018-06-12 22:15:53 +00:00
dependents.add(dependent);
}
}
this._deps = this._deps.filter(([dependent]) => !dependents.has(dependent));
for (const dependent of dependents) {
if (typeof dependent === 'function') {
2019-04-02 21:24:37 +00:00
this._process_generator(dependent);
} else if (this._orig_data.has(dependent)) {
this._process_file(this._orig_data.get(dependent), 'retransform');
2018-06-12 22:15:53 +00:00
}
}
2019-04-02 21:24:37 +00:00
this._check_wave();
2018-06-12 22:15:53 +00:00
}
// check whether this wave is complete, and, if not, whether we need to break a deadlock
2019-04-02 21:24:37 +00:00
private _check_wave(): void {
2018-06-12 22:15:53 +00:00
if (!this._active.size) {
2019-04-02 21:24:37 +00:00
this._end_wave();
} else if (this._status === Status.During) {
2019-04-02 21:24:37 +00:00
const filter_waiting = new Set<Name>();
const all_waiting = new Set<Name>();
for (const [path, { paths }] of this._when_found) {
if (typeof path === 'function' || this._active.has(path)) {
2020-10-25 21:02:15 +00:00
paths.forEach((path) => filter_waiting.add(path));
}
2020-10-25 21:02:15 +00:00
paths.forEach((path) => all_waiting.add(path));
}
2020-10-25 21:02:15 +00:00
if ([...this._active].every((path) => filter_waiting.has(path))) {
// all pending files are currently waiting for a filter or another pending file
// break deadlock: assume all filters have found all they're going to find
2019-04-02 21:24:37 +00:00
for (const path of this._when_found.keys()) {
if (typeof path === 'function') {
2019-04-02 21:24:37 +00:00
this._mark_found(path);
}
}
2020-10-25 21:02:15 +00:00
} else if ([...this._active].every((path) => all_waiting.has(path))) {
// all pending files are currently waiting for one or more other files to exist
// break deadlock: assume all files that have not appeared yet will never do so
2019-04-02 21:24:37 +00:00
for (const path of this._when_found.keys()) {
if (typeof path === 'string' && !this._active.has(path)) {
2019-04-02 21:24:37 +00:00
this._mark_found(path);
}
2018-06-12 22:15:53 +00:00
}
}
}
}
// mark a given awaited file as being found
2019-04-02 21:24:37 +00:00
private _mark_found(path: string | Filter): void {
if (this._when_found.has(path)) {
this._when_found.get(path).resolve();
this._when_found.delete(path);
2018-06-12 22:15:53 +00:00
}
}
}
interface DefilerData {
transform: Transform;
generators?: Generator[];
resolver?: Resolver;
onerror?: OnError;
}
interface FileData {
path: string;
2019-04-02 21:24:37 +00:00
[prop: string]: any;
2018-06-12 22:15:53 +00:00
}
interface Filter {
(path: string): boolean;
}
2018-06-12 22:15:53 +00:00
interface Generator {
(): Promise<void>;
}
type Name = string | Generator;
2018-06-12 22:15:53 +00:00
interface OnError {
2019-04-02 21:24:37 +00:00
(arg: { file?: any; event?: string; generator?: Generator; error: Error }): void;
2018-06-12 22:15:53 +00:00
}
interface Resolver {
(base: string, path: string): string;
}
const enum Status {
Before,
During,
After,
}
interface Transform {
2019-04-02 21:24:37 +00:00
(arg: { file: File; event: string }): Promise<void>;
2018-06-12 22:15:53 +00:00
}
interface WatcherData extends Watcher {
2018-10-11 14:45:37 +00:00
read: boolean | ((arg: { path: string; stats: fs.Stats }) => Promise<boolean>);
enc: string | ((arg: { path: string; stats: fs.Stats; bytes: Buffer }) => Promise<string>);
2018-06-12 22:15:53 +00:00
pre: (data: FileData) => Promise<void>;
}
interface WhenFound {
promise: Promise<void>;
resolve: () => void;
paths: Name[];
}