core/persistence/export.persistence.ts

core/persistence/export.persistence.ts is a file in Bane's Lab Social Share. 78 lines of code and 16 definitions.

import { EXPORT_RUNNING, SUBJECT_SEPARATOR } from "#configuration/strings/card.strings";
import { dirname, join } from "node:path";
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
import type { Disposer } from "@banes-lab/web/types/base.types.ts";
import { LOCK_FILE } from "#configuration/constants/card.constants";
import type { ShareEntry } from "#types/image.types";
import { absolutePath } from "@ssot/paths";
import { writeVerbatim } from "@govlab/canonical-write";

const TEXT = "utf8";
const MISSING_PROCESS = "ESRCH";
const SLASH = "/";

const isAlive = function isAlive(pid: number): boolean {
    try {
        process.kill(pid, 0);
        return true;
    } catch (error) {
        if (error instanceof Error && Reflect.get(error, "code") === MISSING_PROCESS) {
            return false;
        }
        throw error;
    }
};

const holderOf = function holderOf(location: string): number | null {
    if (!existsSync(location)) {
        return null;
    }
    const pid = Number(readFileSync(location, TEXT).trim());
    return Number.isInteger(pid) && isAlive(pid) ? pid : null;
};

export const holdCaptureLock = function holdCaptureLock(location = absolutePath("builds.root", LOCK_FILE)): Disposer {
    const holder = holderOf(location);
    if (holder === process.pid) {
        return () => {};
    }
    if (holder !== null) {
        throw new Error(EXPORT_RUNNING + SUBJECT_SEPARATOR + String(holder));
    }
    mkdirSync(dirname(location), { recursive: true });
    writeVerbatim(location, String(process.pid));
    return () => {
        rmSync(location, { force: true });
    };
};

const servedNames = function servedNames(entry: ShareEntry): readonly string[] {
    return [entry.source, ...entry.stills].map((address) => address.slice(address.lastIndexOf(SLASH) + 1));
};

export const unpublishedShares = function unpublishedShares(
    entries: readonly ShareEntry[],
    served: string,
): readonly string[] {
    return entries.flatMap(servedNames).filter((file) => !existsSync(join(served, file)));
};

export const strayShares = function strayShares(entries: readonly ShareEntry[], served: string): readonly string[] {
    if (!existsSync(served)) {
        return [];
    }
    const named = new Set(entries.flatMap(servedNames));
    return readdirSync(served).filter((file) => !named.has(file));
};

export const publishShares = function publishShares(
    entries: readonly ShareEntry[],
    renders: string,
    served: string,
): readonly string[] {
    mkdirSync(served, { recursive: true });
    for (const file of strayShares(entries, served)) {
        rmSync(join(served, file));
    }
    const published = entries.flatMap(servedNames).filter((file) => {
        const target = join(served, file);
        const source = join(renders, file);
        return !existsSync(target) || !readFileSync(target).equals(readFileSync(source));
    });
    for (const file of published) {
        writeVerbatim(join(served, file), readFileSync(join(renders, file)));
    }
    return published;
};