runtime/entrypoints/catalog.entrypoint.ts

runtime/entrypoints/catalog.entrypoint.ts is a file in Bane's Lab Server. 872 lines of code and 217 definitions.

import fs from "node:fs";

interface NjsRequest {
    readonly args: Readonly<Record<string, string | undefined>>;
    readonly headersOut: Record<string, string>;
    readonly uri: string;
    readonly variables: { readonly document_root: string; readonly host?: string };
    return: (status: number, body: string) => void;
}

type Row = Record<string, unknown>;

interface Table {
    readonly columns: readonly string[];
    readonly rows: readonly (readonly unknown[])[];
}

interface IdRow extends Row {
    readonly json: string;
    readonly kind: string;
    readonly ref: string;
    readonly title: string;
}

interface MoveRow {
    readonly json: string;
    readonly to: string | null;
}

interface Address {
    readonly json: string;
}

interface IdsShard extends Address {
    readonly group: string;
}

interface IdsHead {
    readonly kinds: readonly string[];
    readonly shards: readonly IdsShard[];
}

interface FuzzyRules {
    readonly stemMinimum: number;
    readonly suffixes: readonly string[];
    readonly suggestions: number;
}

interface NameMatch {
    readonly key: string;
    readonly match: string;
}

interface QueryLeaf {
    readonly endpoint: string;
    readonly fuzzy: FuzzyRules;
    readonly index: string;
    readonly limits: { readonly defaultLimit: number; readonly depth: number; readonly limit: number };
    readonly messages: Readonly<Record<string, string>>;
    readonly relations: readonly string[];
    readonly site: string;
}

interface SlugRules {
    readonly characters: string;
    readonly separator: string;
}

interface WordRules {
    readonly americanWords: Readonly<Record<string, string>>;
    readonly fuzzyMinimum: number;
    readonly izeStems: readonly string[];
    readonly izeSuffixes: Readonly<Record<string, string>>;
    readonly pluralIes: readonly [string, string];
    readonly pluralKeptAfter: readonly string[];
    readonly pluralMinimum: number;
    readonly pluralSuffix: string;
    readonly spellingPrefixes: readonly string[];
    readonly wordCharacters: string;
}

interface SearchLeaf {
    readonly kinds: Readonly<Record<string, Address>>;
    readonly rules: WordRules;
}

type ShardMap = Readonly<Record<string, string | undefined>>;

interface SearchIndex extends Table {
    readonly parts?: readonly string[];
    readonly shards: ShardMap;
}

interface ResolveLeaf {
    readonly shards: ShardMap;
    readonly slugRules: SlugRules;
}

interface PostingShard {
    readonly postings: Readonly<Record<string, readonly (readonly [number, number])[]>>;
}

interface SlugShard {
    readonly slugs: Readonly<Record<string, readonly string[] | undefined>>;
}

interface EntryLeaf {
    readonly entries: readonly { readonly ref: string }[];
    readonly parts?: readonly Address[];
}

interface RecordLeaf {
    readonly relations?: readonly {
        readonly links: readonly { readonly json: string; readonly label: string; readonly ref: string | null }[];
        readonly relation: string;
    }[];
}

interface State {
    readonly groups: Map<string, ReadonlyMap<string, IdRow>>;
    readonly ids: IdsHead;
    readonly mtime: number;
    readonly query: QueryLeaf;
    readonly root: string;
    readonly shards: Record<string, unknown>;
    readonly site: string;
}

interface Ranked {
    readonly position: number | null;
    readonly row: Row;
    readonly score: number;
    readonly title: string;
}

interface Refusal {
    readonly code: string;
    readonly values: Readonly<Record<string, unknown>>;
}

const EXTENSION = ".json";
const QUERY_PATH = "/json/api/query";
const IDS_PATH = "/json/api/ids";
const MOVED_PATH = "/json/api/moved";
const SEARCH_PATH = "/json/api/search";
const RESOLVE_PATH = "/json/api/resolve";
const FACETS_PATH = "/json/api/facets/";
const RECORDS_PATH = "/json/api/records/";
const REF_SEPARATOR = ":";
const OPERATIONS: ReadonlySet<string> = new Set(["ref", "name", "q", "collection", "id", "walk"]);
const PAGING: ReadonlySet<string> = new Set(["limit", "offset"]);
const OPERATION_OPTIONS: Readonly<Record<string, ReadonlySet<string> | undefined>> = {
    id: new Set(["kind"]),
    name: new Set(["kind"]),
    q: new Set(["kind"]),
    walk: new Set(["depth", "relation"]),
};
const KIND_OPTION = "kind";
const WILDCARD = "*";
const MISSING = "ENOENT";
const SPACE = " ";
const DIGIT_CHARACTERS = "0123456789";
const PERMANENT_REDIRECT = 308;
const MARKDOWN_EXTENSION = ".md";
const PAGE_EXTENSION = ".html";
const HOME_TWIN = "/index.md";
const HOME_ROUTE = "/";
const HOME_PAGE = "/index.html";

type Guard<T> = (value: unknown) => value is T;

const states: Record<string, State | undefined> = {};

const isObject = function isObject(value: unknown): value is Row {
    return typeof value === "object" && value !== null;
};

const isText = function isText(value: Row, key: string): boolean {
    return typeof value[key] === "string";
};

const isTable = function isTable(value: unknown): value is Table {
    return isObject(value) && Array.isArray(value["columns"]) && Array.isArray(value["rows"]);
};

const hasTexts = function hasTexts(value: Row, keys: readonly string[]): boolean {
    return keys.every((key) => isText(value, key));
};

const hasObjects = function hasObjects(value: Row, keys: readonly string[]): boolean {
    return keys.every((key) => isObject(value[key]));
};

const isQueryLeaf = function isQueryLeaf(value: unknown): value is QueryLeaf {
    return (
        isObject(value) &&
        hasTexts(value, ["site"]) &&
        hasObjects(value, ["fuzzy", "limits", "messages"]) &&
        Array.isArray(value["relations"])
    );
};

const isIdsHead = function isIdsHead(value: unknown): value is IdsHead {
    return isObject(value) && Array.isArray(value["shards"]) && Array.isArray(value["kinds"]);
};

const isSearchLeaf = function isSearchLeaf(value: unknown): value is SearchLeaf {
    return isObject(value) && isObject(value["kinds"]) && isObject(value["rules"]);
};

const isResolveLeaf = function isResolveLeaf(value: unknown): value is ResolveLeaf {
    return isObject(value) && isObject(value["slugRules"]) && isObject(value["shards"]);
};

const isSearchIndex = function isSearchIndex(value: unknown): value is SearchIndex {
    return isTable(value) && isObject(Reflect.get(value, "shards"));
};

const isPostingShard = function isPostingShard(value: unknown): value is PostingShard {
    return isObject(value) && isObject(value["postings"]);
};

const isSlugShard = function isSlugShard(value: unknown): value is SlugShard {
    return isObject(value) && isObject(value["slugs"]);
};

const isEntryLeaf = function isEntryLeaf(value: unknown): value is EntryLeaf {
    return isObject(value) && Array.isArray(value["entries"]);
};

const isRecordLeaf = function isRecordLeaf(value: unknown): value is RecordLeaf {
    return isObject(value);
};

const isIdRow = function isIdRow(value: unknown): value is IdRow {
    return isObject(value) && hasTexts(value, ["json", "kind", "ref", "title"]);
};

const isMoveRow = function isMoveRow(value: unknown): value is MoveRow {
    return isObject(value) && isText(value, "json") && (value["to"] === null || isText(value, "to"));
};

const hasRef = function hasRef(value: Row): value is Row & { readonly ref: string } {
    return isText(value, "ref");
};

const read = function read<T>(root: string, path: string, guard: Guard<T>): T {
    const parsed: unknown = JSON.parse(fs.readFileSync(root + path + EXTENSION, "utf8"));
    if (!guard(parsed)) {
        throw new TypeError(root + path + EXTENSION);
    }
    return parsed;
};

const isMissing = function isMissing(error: unknown): boolean {
    return typeof error === "object" && error !== null && "code" in error && error.code === MISSING;
};

const readIfPresent = function readIfPresent<T>(root: string, path: string, guard: Guard<T>): T | null {
    try {
        return read(root, path, guard);
    } catch (error) {
        if (isMissing(error)) {
            return null;
        }
        throw error;
    }
};

const localOf = function localOf(site: string, url: string): string {
    return url.startsWith(site) ? url.slice(site.length) : url;
};

const rowsOf = function rowsOf(leaf: Table): Row[] {
    return leaf.rows.map((row) => {
        const record: Row = {};
        leaf.columns.forEach((column, at) => {
            record[column] = row[at];
        });
        return record;
    });
};

const loadState = function loadState(root: string, mtime: number): State {
    const query = read(root, QUERY_PATH, isQueryLeaf);
    return {
        groups: new Map(),
        ids: read(root, IDS_PATH, isIdsHead),
        mtime,
        query,
        root,
        shards: {},
        site: query.site,
    };
};

const stateOf = function stateOf(root: string): State {
    const held = states[root];
    const mtime = fs.statSync(root + IDS_PATH + EXTENSION).mtimeMs;
    if (held?.mtime === mtime) {
        return held;
    }
    const loaded = loadState(root, mtime);
    states[root] = loaded;
    return loaded;
};

const cached = function cached<T>(state: State, path: string, guard: Guard<T>): T | null {
    if (!(path in state.shards)) {
        state.shards[path] = readIfPresent(state.root, path, guard);
    }
    const held = state.shards[path];
    return guard(held) ? held : null;
};

const groupRows = function groupRows(state: State, group: string): ReadonlyMap<string, IdRow> {
    const held = state.groups.get(group);
    if (held !== undefined) {
        return held;
    }
    const rows = new Map<string, IdRow>();
    state.ids.shards
        .filter((shard) => shard.group === group)
        .forEach((shard) => {
            rowsOf(read(state.root, localOf(state.site, shard.json), isTable)).forEach((row) => {
                if (isIdRow(row)) {
                    rows.set(row.ref, row);
                }
            });
        });
    state.groups.set(group, rows);
    return rows;
};

const rowOf = function rowOf(state: State, ref: string): IdRow | null {
    const at = ref.indexOf(REF_SEPARATOR);
    return at === -1 ? null : (groupRows(state, ref.slice(0, at)).get(ref) ?? null);
};

const allRows = function allRows(state: State): IdRow[] {
    const groups = [...new Set(state.ids.shards.map((shard) => shard.group))];
    return groups.flatMap((group) => [...groupRows(state, group).values()]);
};

const knownRows = function knownRows(state: State, refs: readonly string[]): IdRow[] {
    return refs.flatMap((ref) => {
        const row = rowOf(state, ref);
        return row === null ? [] : [row];
    });
};

const searchLeafOf = function searchLeafOf(state: State): SearchLeaf {
    const leaf = cached(state, SEARCH_PATH, isSearchLeaf);
    if (leaf === null) {
        throw new TypeError(state.root + SEARCH_PATH + EXTENSION);
    }
    return leaf;
};

const resolveLeafOf = function resolveLeafOf(state: State): ResolveLeaf {
    const leaf = cached(state, RESOLVE_PATH, isResolveLeaf);
    if (leaf === null) {
        throw new TypeError(state.root + RESOLVE_PATH + EXTENSION);
    }
    return leaf;
};

const slugRulesOf = function slugRulesOf(state: State): SlugRules {
    return resolveLeafOf(state).slugRules;
};

const shardsUnder = function shardsUnder(shards: ShardMap, start: string): string[] {
    return Object.keys(shards)
        .filter((prefix) => prefix.startsWith(start) || start.startsWith(prefix))
        .flatMap((prefix) => {
            const address = shards[prefix];
            return address === undefined ? [] : [address];
        });
};

const indexRows = function indexRows(state: State, index: SearchIndex): Row[] {
    if (index.parts === undefined) {
        return rowsOf(index);
    }
    return index.parts.flatMap((part) => rowsOf(read(state.root, localOf(state.site, part), isTable)));
};

const entriesOf = function entriesOf(state: State, leaf: EntryLeaf): readonly { readonly ref: string }[] {
    if (leaf.parts === undefined) {
        return leaf.entries;
    }
    return leaf.parts.flatMap((part) => read(state.root, localOf(state.site, part.json), isEntryLeaf).entries);
};

const filled = function filled(template: string, values: Readonly<Record<string, unknown>>): string {
    return Object.keys(values).reduce((text, key) => text.split(`{${key}}`).join(String(values[key])), template);
};

const slugOf = function slugOf(text: string, rules: SlugRules): string {
    let out = "";
    let joined = false;
    for (const char of text.toLowerCase()) {
        if (rules.characters.includes(char)) {
            out += char;
            joined = false;
            continue;
        }
        if (out.length > 0 && !joined) {
            out += rules.separator;
            joined = true;
        }
    }
    return joined ? out.slice(0, -rules.separator.length) : out;
};

const isPrefixedStem = function isPrefixedStem(stem: string, rules: WordRules): boolean {
    return (
        rules.izeStems.includes(stem) ||
        rules.spellingPrefixes.some(
            (prefix) => stem.startsWith(prefix) && rules.izeStems.includes(stem.slice(prefix.length)),
        )
    );
};

const listedOf = function listedOf(word: string, rules: WordRules): string | null {
    const listed = Object.keys(rules.americanWords);
    if (listed.includes(word)) {
        return rules.americanWords[word] ?? null;
    }
    const prefix = rules.spellingPrefixes.find(
        (candidate) => word.startsWith(candidate) && listed.includes(word.slice(candidate.length)),
    );
    return prefix === undefined ? null : prefix + (rules.americanWords[word.slice(prefix.length)] ?? "");
};

const spelledOf = function spelledOf(word: string, rules: WordRules): string {
    const listed = listedOf(word, rules);
    if (listed !== null) {
        return listed;
    }
    const suffixes = [...Object.keys(rules.izeSuffixes)].sort((left, right) => right.length - left.length);
    for (const british of suffixes) {
        const stem = word.slice(0, -british.length);
        if (word.endsWith(british) && isPrefixedStem(stem, rules)) {
            return stem + (rules.izeSuffixes[british] ?? british);
        }
    }
    return word;
};

const singularOf = function singularOf(word: string, rules: WordRules): string {
    const suffix = rules.pluralSuffix;
    if (word.length < rules.pluralMinimum || !word.endsWith(suffix)) {
        return word;
    }
    if (word.endsWith(rules.pluralIes[0])) {
        return word.slice(0, -rules.pluralIes[0].length) + rules.pluralIes[1];
    }
    const before = word.charAt(word.length - suffix.length - 1);
    return rules.pluralKeptAfter.includes(before) ? word : word.slice(0, -suffix.length);
};

const normalizeWord = function normalizeWord(word: string, rules: WordRules): string {
    const spelled = spelledOf(word.toLowerCase(), rules);
    return spelledOf(singularOf(spelled, rules), rules);
};

const wordsOf = function wordsOf(text: string, rules: WordRules): string[] {
    const words: string[] = [];
    let current = "";
    for (const char of text.toLowerCase() + SPACE) {
        if (rules.wordCharacters.includes(char)) {
            current += char;
            continue;
        }
        if (current.length > 0) {
            words.push(normalizeWord(current, rules));
            current = "";
        }
    }
    return words;
};

const isSwap = function isSwap(left: string, right: string, at: number): boolean {
    return (
        left.length === right.length &&
        left.charAt(at) === right.charAt(at + 1) &&
        left.charAt(at + 1) === right.charAt(at) &&
        left.slice(at + 2) === right.slice(at + 2)
    );
};

const oneEditApart = function oneEditApart(left: string, right: string): boolean {
    if (Math.abs(left.length - right.length) > 1 || left === right) {
        return false;
    }
    let at = 0;
    while (at < left.length && at < right.length && left.charAt(at) === right.charAt(at)) {
        at += 1;
    }
    return (
        left.slice(at + 1) === right.slice(at + 1) ||
        left.slice(at + 1) === right.slice(at) ||
        left.slice(at) === right.slice(at + 1) ||
        isSwap(left, right, at)
    );
};

const matchedKeys = function matchedKeys(
    postings: PostingShard["postings"],
    word: string,
    minimum: number,
): readonly string[] {
    if (word in postings) {
        return [word];
    }
    const keys = Object.keys(postings);
    const prefixed = keys.filter((key) => key.startsWith(word));
    if (prefixed.length > 0 || word.length < minimum) {
        return prefixed;
    }
    return keys.filter((key) => oneEditApart(key, word));
};

const termScores = function termScores(
    state: State,
    rules: WordRules,
    index: SearchIndex,
    word: string,
): Map<number, number> {
    const scores = new Map<number, number>();
    const postings: Record<string, readonly (readonly [number, number])[]> = {};
    shardsUnder(index.shards, word.charAt(0)).forEach((address) => {
        const shard = cached(state, localOf(state.site, address), isPostingShard);
        Object.assign(postings, shard === null ? {} : shard.postings);
    });
    matchedKeys(postings, word, rules.fuzzyMinimum).forEach((key) => {
        (postings[key] ?? []).forEach((posting) => {
            scores.set(posting[0], (scores.get(posting[0]) ?? 0) + posting[1]);
        });
    });
    return scores;
};

const searchKind = function searchKind(
    state: State,
    leaf: SearchLeaf,
    kind: string,
    words: readonly string[],
): Ranked[] {
    const address = leaf.kinds[kind];
    const index = address === undefined ? null : cached(state, localOf(state.site, address.json), isSearchIndex);
    if (index === null) {
        return [];
    }
    const rows = indexRows(state, index);
    const perWord = words.map((word) => termScores(state, leaf.rules, index, word));
    const results: Ranked[] = [];
    (perWord[0] ?? new Map<number, number>()).forEach((_score, at) => {
        const entry = rows[at];
        const listed = entry !== undefined && hasRef(entry) ? rowOf(state, entry.ref) : null;
        if (listed === null || entry === undefined || !perWord.every((scores) => scores.has(at))) {
            return;
        }
        const score = perWord.reduce((acc, scores) => acc + (scores.get(at) ?? 0), 0);
        const position = typeof entry["position"] === "number" ? entry["position"] : null;
        results.push({ position, row: { ...listed, score }, score, title: listed.title });
    });
    return results;
};

const textOrder = function textOrder(left: string, right: string): number {
    const length = Math.min(left.length, right.length);
    for (let at = 0; at < length; at += 1) {
        const difference = (left.codePointAt(at) ?? 0) - (right.codePointAt(at) ?? 0);
        if (difference !== 0) {
            return difference;
        }
    }
    return left.length - right.length;
};

const rankOf = function rankOf(left: Ranked, right: Ranked): number {
    if (left.score !== right.score) {
        return right.score - left.score;
    }
    const leftAt = left.position ?? Number.MAX_VALUE;
    const rightAt = right.position ?? Number.MAX_VALUE;
    return leftAt === rightAt ? textOrder(left.title, right.title) : leftAt - rightAt;
};

const byWords = function byWords(state: State, text: string): readonly Row[] {
    const leaf = searchLeafOf(state);
    const words = wordsOf(text, leaf.rules);
    if (words.length === 0) {
        return [];
    }
    return Object.keys(leaf.kinds)
        .flatMap((kind) => searchKind(state, leaf, kind, words))
        .sort(rankOf)
        .map((ranked) => ranked.row);
};

const caseSplit = function caseSplit(text: string): string {
    let out = "";
    let previousLower = false;
    for (const char of text) {
        const upper = char !== char.toLowerCase();
        if (upper && previousLower) {
            out += SPACE;
        }
        out += char;
        previousLower = !upper && !DIGIT_CHARACTERS.includes(char);
    }
    return out;
};

const nameKeyOf = function nameKeyOf(text: string, slugRules: SlugRules, wordRules: WordRules): string {
    return slugOf(caseSplit(text), slugRules)
        .split(slugRules.separator)
        .filter((word) => word.length > 0)
        .map((word) => normalizeWord(word, wordRules))
        .join(slugRules.separator);
};

const slugsOf = function slugsOf(state: State, letter: string): SlugShard["slugs"] {
    if (letter.length === 0) {
        return {};
    }
    const slugs: Record<string, readonly string[] | undefined> = {};
    shardsUnder(resolveLeafOf(state).shards, letter).forEach((address) => {
        const shard = cached(state, localOf(state.site, address), isSlugShard);
        Object.assign(slugs, shard === null ? {} : shard.slugs);
    });
    return slugs;
};

const nearKeys = function nearKeys(state: State, key: string): string[] {
    const letters = [key.charAt(0), key.charAt(1)].filter((letter, at, all) => all.indexOf(letter) === at);
    return letters.flatMap((letter) => Object.keys(slugsOf(state, letter)));
};

const stemOf = function stemOf(key: string, fuzzy: FuzzyRules): string {
    const fitting = fuzzy.suffixes.filter(
        (candidate) => key.endsWith(candidate) && key.length - candidate.length >= fuzzy.stemMinimum,
    );
    const [suffix] = [...fitting].sort((left, right) => right.length - left.length);
    return suffix === undefined ? key : key.slice(0, -suffix.length);
};

const fuzzyKeys = function fuzzyKeys(state: State, key: string, minimum: number): string[] {
    const near = nearKeys(state, key);
    const edits = key.length < minimum ? [] : near.filter((candidate) => oneEditApart(candidate, key));
    if (edits.length > 0) {
        return edits;
    }
    const stem = stemOf(key, state.query.fuzzy);
    return stem.length < state.query.fuzzy.stemMinimum
        ? []
        : near.filter((candidate) => candidate !== key && stemOf(candidate, state.query.fuzzy) === stem);
};

const nameMatches = function nameMatches(state: State, text: string): NameMatch[] {
    const slugRules = slugRulesOf(state);
    const wordRules = searchLeafOf(state).rules;
    const key = nameKeyOf(text, slugRules, wordRules);
    if (key.length === 0) {
        return [];
    }
    if (slugsOf(state, key.charAt(0))[key] !== undefined) {
        return [{ key, match: slugOf(text, slugRules) === key ? "exact" : "normalized" }];
    }
    return fuzzyKeys(state, key, wordRules.fuzzyMinimum).map((found) => ({ key: found, match: "fuzzy" }));
};

const byName = function byName(state: State, text: string): Row[] {
    const rows = nameMatches(state, text).flatMap((found) =>
        knownRows(state, slugsOf(state, found.key.charAt(0))[found.key] ?? []).map((row) => ({
            ...row,
            match: found.match,
        })),
    );
    return rows.filter((row, at) => rows.findIndex((other) => other.ref === row.ref) === at);
};

const sharedPrefix = function sharedPrefix(left: string, right: string): number {
    let at = 0;
    while (at < left.length && at < right.length && left.charAt(at) === right.charAt(at)) {
        at += 1;
    }
    return at;
};

const nameSuggestions = function nameSuggestions(state: State, text: string): string[] {
    const key = nameKeyOf(text, slugRulesOf(state), searchLeafOf(state).rules);
    return Object.keys(slugsOf(state, key.charAt(0)))
        .sort((left, right) => sharedPrefix(right, key) - sharedPrefix(left, key) || textOrder(left, right))
        .slice(0, state.query.fuzzy.suggestions);
};

const byId = function byId(state: State, pattern: string): IdRow[] {
    const prefix = pattern.endsWith(WILDCARD) ? pattern.slice(0, -WILDCARD.length) : null;
    if (prefix === null) {
        return knownRows(state, [pattern]);
    }
    return allRows(state).filter((row) => row.ref.startsWith(prefix));
};

const byFacets = function byFacets(
    state: State,
    collection: string,
    fields: readonly (readonly [string, string])[],
): IdRow[] {
    const rules = slugRulesOf(state);
    const lists =
        fields.length === 0
            ? [cached(state, RECORDS_PATH + slugOf(collection, rules), isEntryLeaf)]
            : fields.map((field) =>
                  cached(state, FACETS_PATH + [collection, field[0], slugOf(field[1], rules)].join("/"), isEntryLeaf),
              );
    const present = lists.flatMap((leaf) => (leaf === null ? [] : [leaf]));
    if (present.length !== lists.length) {
        return [];
    }
    const sets = present.map((leaf) => entriesOf(state, leaf).map((entry) => entry.ref));
    const [first = []] = sets;
    return knownRows(
        state,
        first.filter((ref) => sets.every((set) => set.includes(ref))),
    );
};

const byWalk = function byWalk(state: State, start: string, relation: string | undefined, depth: number): Row[] {
    const edges: Row[] = [];
    const seen = new Set([start]);
    let frontier = [start];
    for (let hop = 1; hop <= depth && frontier.length > 0; hop += 1) {
        const next: string[] = [];
        frontier.forEach((ref) => {
            const row = rowOf(state, ref);
            const leaf = row === null ? null : readIfPresent(state.root, localOf(state.site, row.json), isRecordLeaf);
            (leaf?.relations ?? []).forEach((group) => {
                if (relation !== undefined && group.relation !== relation) {
                    return;
                }
                group.links.forEach((target) => {
                    const listed = target.ref === null ? null : rowOf(state, target.ref);
                    if (listed === null) {
                        return;
                    }
                    edges.push({ ...listed, from: ref, hop, relation: group.relation });
                    if (!seen.has(listed.ref)) {
                        seen.add(listed.ref);
                        next.push(listed.ref);
                    }
                });
            });
        });
        frontier = next;
    }
    return edges;
};

const numberOf = function numberOf(value: string | undefined, fallback: number): number | null {
    if (value === undefined) {
        return fallback;
    }
    const parsed = Number(value);
    return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
};

const respond = function respond(r: NjsRequest, status: number, body: object): void {
    r.headersOut["Content-Type"] = "application/json";
    r.headersOut["Access-Control-Allow-Origin"] = "*";
    r.return(status, `${JSON.stringify(body)}\n`);
};

const messageOf = function messageOf(state: State, code: string): string {
    return state.query.messages[code] ?? "";
};

const refuse = function refuse(r: NjsRequest, state: State, refusal: Refusal): void {
    const values = { ids: state.site + IDS_PATH, index: state.query.index, ...refusal.values };
    respond(r, 400, {
        code: refusal.code,
        error: filled(messageOf(state, refusal.code), values),
        index: state.query.index,
    });
};

const argOf = function argOf(args: NjsRequest["args"], key: string): string {
    return args[key] ?? "";
};

type Check = (state: State, args: NjsRequest["args"], operation: string) => Refusal | null;

const unknownParameter: Check = (_state, args, operation) => {
    if (operation === "collection") {
        return null;
    }
    const options = OPERATION_OPTIONS[operation] ?? new Set<string>();
    const unknown = Object.keys(args).find((key) => !OPERATIONS.has(key) && !PAGING.has(key) && !options.has(key));
    return unknown === undefined ? null : { code: "unknownParameter", values: { name: unknown } };
};

const emptyValue: Check = (_state, args, operation) =>
    argOf(args, operation).length === 0 ? { code: "emptyValue", values: { name: operation } } : null;

const badLimit: Check = (state, args) => {
    const { limits } = state.query;
    const limit = numberOf(args["limit"], limits.defaultLimit);
    return limit === null || limit > limits.limit
        ? { code: "badNumber", values: { max: limits.limit, name: "limit" } }
        : null;
};

const badOffset: Check = (state, args) =>
    numberOf(args["offset"], 0) === null
        ? { code: "badNumber", values: { max: state.query.limits.limit, name: "offset" } }
        : null;

const badDepth: Check = (state, args) => {
    const depth = numberOf(args["depth"], 1);
    const inRange = depth !== null && depth >= 1 && depth <= state.query.limits.depth;
    return inRange ? null : { code: "badDepth", values: { max: state.query.limits.depth } };
};

const unknownRelation: Check = (state, args) => {
    const { relation } = args;
    return relation === undefined || state.query.relations.includes(relation)
        ? null
        : { code: "unknownRelation", values: { name: relation } };
};

const unknownKind: Check = (state, args, operation) => {
    const kind = operation === "collection" ? undefined : args[KIND_OPTION];
    return kind === undefined || state.ids.kinds.includes(kind)
        ? null
        : { code: "unknownKind", values: { name: kind } };
};

const CHECKS: readonly Check[] = [
    unknownParameter,
    emptyValue,
    badLimit,
    badOffset,
    badDepth,
    unknownRelation,
    unknownKind,
];

const refusalOf = function refusalOf(
    state: State,
    args: NjsRequest["args"],
    operations: readonly string[],
): Refusal | null {
    const [operation] = operations;
    if (operation === undefined) {
        return { code: "noOperation", values: {} };
    }
    if (operations.length > 1) {
        return { code: "manyOperations", values: {} };
    }
    for (const check of CHECKS) {
        const refusal = check(state, args, operation);
        if (refusal !== null) {
            return refusal;
        }
    }
    return null;
};

const run = function run(state: State, args: NjsRequest["args"], operation: string): readonly Row[] {
    if (operation === "ref") {
        return knownRows(state, [argOf(args, "ref")]);
    }
    if (operation === "name") {
        return byName(state, argOf(args, "name"));
    }
    if (operation === "q") {
        return byWords(state, argOf(args, "q"));
    }
    if (operation === "id") {
        return byId(state, argOf(args, "id"));
    }
    if (operation === "walk") {
        return byWalk(state, argOf(args, "walk"), args["relation"], numberOf(args["depth"], 1) ?? 1);
    }
    const fields = Object.keys(args)
        .filter((key) => !OPERATIONS.has(key) && !PAGING.has(key))
        .map((key): readonly [string, string] => [key, argOf(args, key)]);
    return byFacets(state, argOf(args, "collection"), fields);
};

const q = function q(r: NjsRequest): void {
    const state = stateOf(r.variables.document_root);
    const { args } = r;
    const operations = Object.keys(args).filter((key) => OPERATIONS.has(key));
    const refusal = refusalOf(state, args, operations);
    const [operation] = operations;
    if (refusal !== null || operation === undefined) {
        refuse(r, state, refusal ?? { code: "noOperation", values: {} });
        return;
    }
    const kind = operation === "collection" ? undefined : args[KIND_OPTION];
    const results = run(state, args, operation).filter((row) => kind === undefined || row["kind"] === kind);
    if (results.length === 0) {
        const suggestions = operation === "name" ? { suggestions: nameSuggestions(state, argOf(args, "name")) } : {};
        respond(r, 404, {
            code: "notFound",
            error: messageOf(state, "notFound"),
            index: state.query.index,
            ...suggestions,
        });
        return;
    }
    const limit = numberOf(args["limit"], state.query.limits.defaultLimit) ?? state.query.limits.defaultLimit;
    const offset = numberOf(args["offset"], 0) ?? 0;
    respond(r, 200, {
        count: results.length,
        limit,
        offset,
        operation,
        results: results.slice(offset, offset + limit),
    });
};

const movesOf = function movesOf(state: State): Map<string, string | null> {
    const moves = new Map<string, string | null>();
    const moved = cached(state, MOVED_PATH, isTable);
    rowsOf(moved ?? { columns: [], rows: [] }).forEach((row) => {
        if (isMoveRow(row)) {
            moves.set(localOf(state.site, row.json), row.to);
        }
    });
    return moves;
};

const missing = function missing(r: NjsRequest): void {
    const state = stateOf(r.variables.document_root);
    const index = `${state.site}/json/api`;
    const moves = movesOf(state);
    if (!moves.has(r.uri)) {
        respond(r, 404, { code: "missing", error: messageOf(state, "missing"), index });
        return;
    }
    const to = moves.get(r.uri) ?? null;
    if (to === null) {
        respond(r, 410, { code: "gone", error: messageOf(state, "gone"), index });
        return;
    }
    r.headersOut["Access-Control-Allow-Origin"] = "*";
    r.return(PERMANENT_REDIRECT, to);
};

const staticQuery = function staticQuery(r: NjsRequest): void {
    const state = stateOf(r.variables.document_root);
    respond(r, 400, { code: "staticQuery", error: messageOf(state, "staticQuery"), query: state.query.endpoint });
};

const pageExists = function pageExists(root: string, page: string): boolean {
    try {
        fs.statSync(root + page);
        return true;
    } catch (error) {
        if (isMissing(error)) {
            return false;
        }
        throw error;
    }
};

const markdownCanonical = function markdownCanonical(r: NjsRequest): string {
    if (!r.uri.endsWith(MARKDOWN_EXTENSION)) {
        return "";
    }
    const home = r.uri === HOME_TWIN;
    const route = home ? HOME_ROUTE : r.uri.slice(0, -MARKDOWN_EXTENSION.length);
    const page = home ? HOME_PAGE : route + PAGE_EXTENSION;
    if (!pageExists(r.variables.document_root, page)) {
        return "";
    }
    return `<https://${r.variables.host ?? ""}${route}>; rel="canonical"`;
};

export default { markdownCanonical, missing, nameKeyOf, normalizeWord, q, staticQuery };