# core/coordinators/surface.coordinator.ts

> 118 lines of code and 31 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: application
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-coordinators-surface-coordinator-ts
Source text: https://banes-lab.com/source/build-scripts/core/coordinators/surface.coordinator.ts.txt

Listed in [core/coordinators](https://banes-lab.com/api/source/build-scripts/core/coordinators.md), after [core/coordinators/server.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/server.coordinator.ts.md) and before [core/coordinators/validation.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/validation.coordinator.ts.md).

## Definitions

- `figureNames` (lexical_declaration, line 70)
- `read` (lexical_declaration, line 98)
- `raiseBoard` (lexical_declaration, line 96)
- `reusable` (lexical_declaration, line 74)
- `scriptsOf` (lexical_declaration, line 82)
- `isSlotText` (lexical_declaration, line 38)
- `record` (lexical_declaration, line 105)
- `recordSurfaces` (lexical_declaration, line 120, exported)
- `UTF8` (lexical_declaration, line 26)
- `LINE_BREAK` (lexical_declaration, line 28)
- `HASH` (lexical_declaration, line 30)
- `HEX` (lexical_declaration, line 32)
- `TOOLS` (lexical_declaration, line 34)
- `SlotText` (type_alias_declaration, line 36)
- `filesUnder` (lexical_declaration, line 42)
- `base` (lexical_declaration, line 43)
- `inputsHash` (lexical_declaration, line 52)
- `hash` (lexical_declaration, line 53)
- `build` (lexical_declaration, line 60)
- `sources` (lexical_declaration, line 61)
- `path` (lexical_declaration, line 76)
- `parsed` (lexical_declaration, line 83)
- `scripts` (lexical_declaration, line 84)
- `script` (lexical_declaration, line 87)
- `loaded` (lexical_declaration, line 97)
- `host` (lexical_declaration, line 106)
- `install` (lexical_declaration, line 107)
- `member` (lexical_declaration, line 121, exported)
- `inputs` (lexical_declaration, line 122, exported)
- `recordings` (lexical_declaration, line 126, exported)
- `files` (lexical_declaration, line 127, exported)

## Contained in

- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)

## Uses

- [configuration/strings/surface.strings.ts](https://banes-lab.com/source/build-scripts/configuration/strings/surface.strings.ts.md)
- [core/persistence/asset.persistence.ts](https://banes-lab.com/source/build-scripts/core/persistence/asset.persistence.ts.md)
- [core/persistence/coordination.persistence.ts](https://banes-lab.com/source/build-scripts/core/persistence/coordination.persistence.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-configuration-strings.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)
- [core/persistence](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-persistence.md)

## Source

```typescript
import type { FigureFrame, ScenarioTool, SurfaceRecord } from "#types/surface.types";
import {
    HASHED_FOLDERS,
    INSTALL_FOLDER,
    RECORDER_FOLDERS,
    RECORDER_SUBJECT,
    SCENARIO,
    SCRIPT_RUNNER,
    TEMPORARY_PREFIX,
} from "#configuration/constants/surface.constants";
import { basename, join, relative } from "node:path";
import { copyFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { scriptMissing, slotsUnreadable } from "#configuration/strings/surface.strings";
import { ScenarioPlayer } from "#core/adapters/surface.adapter";
import type { SurfaceFigureName } from "@banes-lab/web/types/surface.types.ts";
import { absolutePath } from "@ssot/paths";
import { createHash } from "node:crypto";
import { isRecord } from "#core/selectors/base.selector";
import { pathToFileURL } from "node:url";
import { recordingsOf } from "#core/converters/surface.converter";
import { replaceFolderFiles } from "#core/persistence/asset.persistence";
import { supplyCoordination } from "#core/persistence/coordination.persistence";
import { surfaceLocation } from "@banes-lab/web/core/assets/surface.asset.ts";
import { tmpdir } from "node:os";

const UTF8 = "utf8";

const LINE_BREAK = "\n";

const HASH = "sha256";

const HEX = "hex";

const TOOLS: readonly ScenarioTool[] = ["await", "govern"];

type SlotText = (section: string, name: string) => string;

const isSlotText = function isSlotText(value: unknown): value is SlotText {
    return typeof value === "function";
};

const filesUnder = function filesUnder(root: string, folder: string): string[] {
    const base = join(root, folder);
    if (!existsSync(base)) {
        return [];
    }
    return readdirSync(base, { encoding: UTF8, recursive: true, withFileTypes: true })
        .filter((entry) => entry.isFile())
        .map((entry) => join(entry.parentPath, entry.name));
};

const inputsHash = function inputsHash(member: string): string {
    const hash = createHash(HASH).update(JSON.stringify(SCENARIO));
    const files = HASHED_FOLDERS.flatMap((folder) => filesUnder(member, folder)).toSorted((left, right) =>
        left.localeCompare(right),
    );
    for (const file of [join(member, "package.json"), ...files]) {
        hash.update(relative(member, file)).update(readFileSync(file));
    }
    const build = absolutePath("app.build");
    const sources = RECORDER_FOLDERS.flatMap((folder) => filesUnder(build, folder))
        .filter((file) => basename(file).startsWith(RECORDER_SUBJECT))
        .toSorted((left, right) => left.localeCompare(right));
    for (const source of sources) {
        hash.update(relative(build, source)).update(readFileSync(source));
    }
    return hash.digest(HEX);
};

const figureNames = function figureNames(): SurfaceFigureName[] {
    return [...new Set(SCENARIO.steps.flatMap((step) => step.figures))];
};

const reusable = function reusable(inputs: string): boolean {
    return figureNames().every((figure) => {
        const path = join(absolutePath("app.surfaces"), basename(surfaceLocation(figure)));
        const parsed: unknown = existsSync(path) ? JSON.parse(readFileSync(path, UTF8)) : null;
        return isRecord(parsed) && parsed["inputs"] === inputs;
    });
};

const scriptsOf = function scriptsOf(install: string): ReadonlyMap<ScenarioTool, string> {
    const parsed: unknown = JSON.parse(readFileSync(join(install, "package.json"), UTF8));
    const scripts = isRecord(parsed) && isRecord(parsed["scripts"]) ? parsed["scripts"] : {};
    return new Map(
        TOOLS.map((tool) => {
            const script = scripts[tool];
            if (typeof script !== "string" || !script.startsWith(SCRIPT_RUNNER)) {
                throw new Error(scriptMissing(tool));
            }
            return [tool, script.slice(SCRIPT_RUNNER.length)];
        }),
    );
};

const raiseBoard = async function raiseBoard(install: string): Promise<void> {
    const loaded: unknown = await import(pathToFileURL(join(install, "config", "surface.config.ts")).href);
    const read = isRecord(loaded) ? loaded["slotText"] : null;
    if (!isSlotText(read)) {
        throw new Error(slotsUnreadable());
    }
    copyFileSync(join(install, read("surface", "board_template")), join(install, read("surface", "board")));
};

const record = async function record(member: string): Promise<readonly FigureFrame[]> {
    const host = mkdtempSync(join(tmpdir(), TEMPORARY_PREFIX));
    const install = join(host, INSTALL_FOLDER);
    try {
        await supplyCoordination(member, install);
        await raiseBoard(install);
        for (const sample of SCENARIO.samples) {
            writeFileSync(join(host, sample.name), sample.text + LINE_BREAK, UTF8);
        }
        return await new ScenarioPlayer(install, scriptsOf(install)).play();
    } finally {
        rmSync(host, { force: true, maxRetries: 5, recursive: true });
    }
};

export const recordSurfaces = async function recordSurfaces(): Promise<SurfaceRecord> {
    const member = absolutePath("app.coordination");
    const inputs = inputsHash(member);
    if (reusable(inputs)) {
        return { figures: figureNames().length, reused: true };
    }
    const recordings = recordingsOf(await record(member), inputs);
    const files = [...recordings].map(([figure, recording]): [string, string] => [
        basename(surfaceLocation(figure)),
        `${JSON.stringify(recording, null, 4)}\n`,
    ]);
    replaceFolderFiles(absolutePath("app.surfaces"), new Map(files));
    return { figures: recordings.size, reused: false };
};
```
