core/adapters/surface.adapter.ts

core/adapters/surface.adapter.ts is a file in Bane's Lab Build Scripts. 154 lines of code and 42 definitions.

import type { FigureFrame, Region, ScenarioStep, ScenarioTool, ToolStep, WaitStep } from "#types/surface.types";
import { SAMPLE_INVARIANT, stepFailed, venueMissing, waitUnresolved } from "#configuration/strings/surface.strings";
import {
    SCENARIO,
    STEP_TIMEOUT_MS,
    VENUE_PLACEHOLDER,
    WAIT_LIMIT_MS,
    WAIT_POLL_MS,
    WAIT_SETTLE_MS,
} from "#configuration/constants/surface.constants";
import { appendFileSync, readFileSync, readdirSync } from "node:fs";
import { appendLine, changedRegion, commandLine, markedLines, venueView } from "#core/converters/surface.converter";
import { spawn, spawnSync } from "node:child_process";
import type { SurfaceFigureName } from "@banes-lab/web/types/surface.types.ts";
import { join } from "node:path";

const UTF8 = "utf8";

const LINE_BREAK = "\n";

interface Shown {
    readonly command: string;
    readonly output: string;
    readonly before: readonly string[];
    readonly highlight: Region | null;
}

const pause = async function pause(ms: number): Promise<void> {
    await new Promise<void>((done) => {
        setTimeout(done, ms);
    });
};

const settled = async function settled(
    read: () => string,
    since: string,
    quietFor: number,
    left: number,
): Promise<string> {
    await pause(WAIT_POLL_MS);
    const now = read();
    const quiet = now.length > 0 && now === since ? quietFor + WAIT_POLL_MS : 0;
    return quiet >= WAIT_SETTLE_MS || left <= 0 ? now : settled(read, now, quiet, left - WAIT_POLL_MS);
};

const exitWithin = async function exitWithin(closed: Promise<number>, limit: number): Promise<number | null> {
    const expired = pause(limit).then((): number | null => null);
    return Promise.race<number | null>([closed, expired]);
};

export class ScenarioPlayer {
    private readonly install: string;
    private readonly scripts: ReadonlyMap<ScenarioTool, string>;
    private readonly frames: FigureFrame[] = [];
    private venue: string | null = null;
    private lastRegion: Region | null = null;

    public constructor(install: string, scripts: ReadonlyMap<ScenarioTool, string>) {
        this.install = install;
        this.scripts = scripts;
    }

    public async play(): Promise<readonly FigureFrame[]> {
        await this.playFrom(0);
        return this.frames;
    }

    private async playFrom(index: number): Promise<void> {
        const step = SCENARIO.steps.at(index);
        if (step === undefined) {
            return;
        }
        const taken = await this.take(step, SCENARIO.steps.at(index + 1));
        await this.playFrom(index + taken);
    }

    private async take(step: ScenarioStep, next: ScenarioStep | undefined): Promise<number> {
        if (step.kind === "wait") {
            await this.runWait(step, next);
            return 2;
        }
        if (step.kind === "write") {
            this.runWrite(step.text, step.figures);
        } else {
            this.runTool(step);
        }
        return 1;
    }

    private resolved(args: readonly string[]): string[] {
        return args.map((argument) => (argument === VENUE_PLACEHOLDER ? (this.venue ?? argument) : argument));
    }

    private venueLines(): string[] {
        return this.venue === null
            ? []
            : venueView(readFileSync(join(this.install, this.venue), UTF8).split(LINE_BREAK));
    }

    private discoverVenue(): void {
        this.venue ??= readdirSync(this.install).find((name) => name.startsWith(`${SAMPLE_INVARIANT}.`)) ?? null;
    }

    private show(figures: readonly SurfaceFigureName[], shown: Shown): void {
        const lines = markedLines(this.venueLines(), shown.highlight);
        const frame = { command: shown.command, lines, output: shown.output, surface: this.venue ?? "" };
        this.frames.push(...figures.map((figure) => ({ before: shown.before, figure, frame })));
    }

    private runTool(step: ToolStep): void {
        const args = this.resolved(step.args);
        const command = commandLine(step.tool, args);
        const before = this.venueLines();
        const result = spawnSync(process.execPath, [this.scripts.get(step.tool) ?? "", ...args], {
            cwd: this.install,
            encoding: UTF8,
            timeout: STEP_TIMEOUT_MS,
        });
        const output = `${result.stdout}${result.stderr}`;
        const code = result.status ?? -1;
        if (step.exit !== null && code !== step.exit) {
            throw new Error(stepFailed(command, step.exit, code, output));
        }
        this.discoverVenue();
        const own = changedRegion(before, this.venueLines());
        const delivered = step.delivers ? this.lastRegion : null;
        this.lastRegion = own ?? this.lastRegion;
        this.show(step.figures, { before, command, highlight: own ?? delivered, output });
    }

    private async runWait(step: WaitStep, writer: ScenarioStep | undefined): Promise<void> {
        const args = this.resolved(step.args);
        const command = commandLine("await", args);
        const child = spawn(process.execPath, [this.scripts.get("await") ?? "", ...args], { cwd: this.install });
        let output = "";
        const collect = (chunk: string): void => {
            output += chunk;
        };
        child.stdout.setEncoding(UTF8).on("data", collect);
        child.stderr.setEncoding(UTF8).on("data", collect);
        const closed = new Promise<number>((done) => {
            child.on("close", (code) => {
                done(code ?? -1);
            });
        });

        const parked = await settled(() => output, "", 0, WAIT_LIMIT_MS);
        this.show(step.figures, { before: this.venueLines(), command, highlight: null, output: parked });
        if (writer?.kind === "tool") {
            this.runTool(writer);
        }

        const code = await exitWithin(closed, WAIT_LIMIT_MS);
        if (code === null) {
            child.kill();
            throw new Error(waitUnresolved(command));
        }
        if (code !== step.exit) {
            throw new Error(stepFailed(command, step.exit, code, output));
        }
        this.show(step.figures, { before: this.venueLines(), command, highlight: this.lastRegion, output });
    }

    private runWrite(text: string, figures: readonly SurfaceFigureName[]): void {
        if (this.venue === null) {
            throw new Error(venueMissing());
        }
        const before = this.venueLines();
        appendFileSync(join(this.install, this.venue), LINE_BREAK + text + LINE_BREAK, UTF8);
        const own = changedRegion(before, this.venueLines());
        this.lastRegion = own ?? this.lastRegion;
        this.show(figures, { before, command: appendLine(text, this.venue), highlight: own, output: "" });
    }
}