core/converters/journal.converter.ts

core/converters/journal.converter.ts is a file in Bane's Lab Build Scripts. 132 lines of code and 30 definitions.

import type { Entry, Leaf } from "#types/catalog.types";
import {
    JOURNAL_SHAPE,
    MOVED_TITLE,
    journalFieldNotObject,
    malformedJournal,
} from "#configuration/strings/catalog.strings";
import type { Journal, JournalRef, Tombstone } from "#types/journal.types";
import { localAddress, movedIndex, retabbedAddress } from "#core/resolvers/catalog.resolver";
import { INDEX_KIND } from "#configuration/constants/catalog.constants";
import { RETIRED_TABS } from "#configuration/constants/tree.constants";
import { isRecord } from "#core/selectors/base.selector";

const MOVED_REF = "api:moved";
const MOVED_COLUMNS = ["ref", "json", "to"] as const;
const TITLE_SEPARATOR = "\u0000";

export const EMPTY_JOURNAL: Journal = { moved: {}, refs: {} };

const isText = function isText(value: unknown): value is string {
    return typeof value === "string";
};

const isJournalRef = function isJournalRef(value: unknown): value is JournalRef {
    return isRecord(value) && [value["fingerprint"], value["json"], value["kind"], value["title"]].every(isText);
};

const isTombstone = function isTombstone(value: unknown): value is Tombstone {
    return isRecord(value) && isText(value["json"]) && (value["to"] === null || isText(value["to"]));
};

const recordOf = function recordOf<T>(
    value: unknown,
    guard: (item: unknown) => item is T,
    key: string,
): Record<string, T> {
    if (!isRecord(value)) {
        throw new TypeError(journalFieldNotObject(key));
    }
    const bad = Object.entries(value).filter(([, item]) => !guard(item));
    if (bad.length > 0) {
        throw new TypeError(
            malformedJournal(
                key,
                bad.map(([ref]) => ref),
            ),
        );
    }
    return Object.fromEntries(Object.entries(value).filter((pair): pair is [string, T] => guard(pair[1])));
};

export const parseJournal = function parseJournal(text: string): Journal {
    const parsed: unknown = JSON.parse(text);
    if (!isRecord(parsed)) {
        throw new TypeError(JOURNAL_SHAPE);
    }
    return {
        moved: recordOf(parsed["moved"], isTombstone, "moved"),
        refs: recordOf(parsed["refs"], isJournalRef, "refs"),
    };
};

interface CurrentRefs {
    readonly byJson: ReadonlyMap<string, string>;
    readonly byTitle: ReadonlyMap<string, readonly string[]>;
}

const retabbedOf = function retabbedOf(address: string, current: CurrentRefs): string | null {
    for (const [from, to] of Object.entries(RETIRED_TABS)) {
        const json = retabbedAddress(address, from, to);
        const ref = json === null ? undefined : current.byJson.get(json);
        if (ref !== undefined) {
            return ref;
        }
    }
    return null;
};

const replacementOf = function replacementOf(gone: JournalRef, current: CurrentRefs): string | null {
    const retabbed = retabbedOf(gone.json, current);
    if (retabbed !== null) {
        return retabbed;
    }
    const candidates = current.byTitle.get(gone.kind + TITLE_SEPARATOR + gone.title) ?? [];
    return candidates.length === 1 ? (candidates[0] ?? null) : null;
};

const settledOf = function settledOf(tombstone: Tombstone, current: CurrentRefs): Tombstone {
    return tombstone.to === null ? { json: tombstone.json, to: retabbedOf(tombstone.json, current) } : tombstone;
};

export const updateJournal = function updateJournal(
    previous: Journal,
    entries: readonly Entry[],
    site: string,
): Journal {
    const refs: Record<string, JournalRef> = Object.fromEntries(
        entries.map((entry) => [
            entry.ref,
            {
                fingerprint: entry.fingerprint,
                json: localAddress(site, entry.json),
                kind: entry.kind,
                title: entry.title,
            },
        ]),
    );
    const byTitle = new Map<string, string[]>();
    const byJson = new Map<string, string>();
    for (const [ref, held] of Object.entries(refs)) {
        const key = held.kind + TITLE_SEPARATOR + held.title;
        byTitle.set(key, [...(byTitle.get(key) ?? []), ref]);
        byJson.set(held.json, ref);
    }
    const current: CurrentRefs = { byJson, byTitle };
    const kept = Object.entries(previous.moved)
        .filter(([ref]) => !(ref in refs))
        .map(([ref, tombstone]): [string, Tombstone] => [ref, settledOf(tombstone, current)]);
    const vanished = Object.entries(previous.refs)
        .filter(([ref]) => !(ref in refs))
        .map(([ref, gone]): [string, Tombstone] => [ref, { json: gone.json, to: replacementOf(gone, current) }]);
    return { moved: Object.fromEntries([...kept, ...vanished]), refs };
};

export const movedLeaf = function movedLeaf(journal: Journal, site: string): Leaf {
    const current = new Map(Object.entries(journal.refs));
    const rows = Object.entries(journal.moved)
        .toSorted(([left], [right]) => left.localeCompare(right))
        .map(([ref, tombstone]) => {
            const to = tombstone.to === null ? undefined : current.get(tombstone.to);
            return [ref, site + tombstone.json, to === undefined ? null : site + to.json];
        });
    return {
        data: { columns: MOVED_COLUMNS, count: rows.length, ref: MOVED_REF, rows, title: MOVED_TITLE },
        identity: {
            address: movedIndex(),
            href: null,
            kind: INDEX_KIND,
            ref: MOVED_REF,
            summary: null,
            title: MOVED_TITLE,
        },
        markdown: null,
    };
};