# core/steps/probe.step.ts

> 197 lines of code and 57 definitions.

Tree: Bane's Lab Deploy
Language: typescript
Layer: application
Canonical: https://banes-lab.com/anatomy/deploy#file-deploy-core-steps-probe-step-ts
Source text: https://banes-lab.com/source/deploy/core/steps/probe.step.ts.txt

Listed in [core/steps](https://banes-lab.com/api/source/deploy/core/steps.md), after [core/steps/nginx.step.ts](https://banes-lab.com/source/deploy/core/steps/nginx.step.ts.md) and before [core/steps/recovery.step.ts](https://banes-lab.com/source/deploy/core/steps/recovery.step.ts.md).

## Definitions

- `expectation` (lexical_declaration, line 74)
- `probeSite` (lexical_declaration, line 170, exported)
- `mismatch` (lexical_declaration, line 66)
- `answer` (lexical_declaration, line 62)
- `behaviors` (lexical_declaration, line 83)
- `queryProbes` (lexical_declaration, line 117)
- `behaviorProblem` (lexical_declaration, line 163)
- `JSON_TYPE` (lexical_declaration, line 37)
- `MARKDOWN_TYPE` (lexical_declaration, line 38)
- `TEXT_TYPE` (lexical_declaration, line 39)
- `ANY_TYPE` (lexical_declaration, line 40)
- `GET` (lexical_declaration, line 41)
- `HEAD` (lexical_declaration, line 42)
- `OPTIONS` (lexical_declaration, line 43)
- `OK` (lexical_declaration, line 44)
- `NO_CONTENT` (lexical_declaration, line 45)
- `NOT_FOUND` (lexical_declaration, line 46)
- `BAD_REQUEST` (lexical_declaration, line 47)
- `QUERY_PROBE` (lexical_declaration, line 48)
- `STATIC_QUERY_PROBE` (lexical_declaration, line 49)
- `MISSING_SEGMENT` (lexical_declaration, line 50)
- `MARKDOWN_SUFFIX` (lexical_declaration, line 51)
- `TEXT_SUFFIX` (lexical_declaration, line 52)
- `JSON_SUFFIX` (lexical_declaration, line 53)
- `SPACE` (lexical_declaration, line 54)
- `STOP` (lexical_declaration, line 55)
- `QUERY_MARK` (lexical_declaration, line 56)
- `REF_SEPARATOR` (lexical_declaration, line 57)
- `WILDCARD` (lexical_declaration, line 58)
- `LETTERS` (lexical_declaration, line 59)
- `ENCODING` (lexical_declaration, line 60)
- `root` (lexical_declaration, line 84)
- `route` (lexical_declaration, line 85)
- `groupOf` (lexical_declaration, line 102)
- `firstWord` (lexical_declaration, line 106)
- `word` (lexical_declaration, line 107)
- `record` (lexical_declaration, line 121)
- `group` (lexical_declaration, line 125)
- `parsed` (lexical_declaration, line 139)
- `isStale` (lexical_declaration, line 143)
- `rowProblems` (lexical_declaration, line 150)
- `json` (lexical_declaration, line 151)
- `markdown` (lexical_declaration, line 152)
- `found` (lexical_declaration, line 153)
- `log` (lexical_declaration, line 177, exported)
- `offset` (lexical_declaration, line 178, exported)
- `rootAddress` (lexical_declaration, line 179, exported)
- `idsAddress` (lexical_declaration, line 180, exported)
- `[root, ids]` (lexical_declaration, line 181, exported)
- `shards` (lexical_declaration, line 182, exported)
- `rows` (lexical_declaration, line 183, exported)
- `row` (lexical_declaration, line 186, exported)
- `queries` (lexical_declaration, line 190, exported)
- `heads` (lexical_declaration, line 191, exported)
- `rowFindings` (lexical_declaration, line 199, exported)
- `logged` (lexical_declaration, line 200, exported)
- `problems` (lexical_declaration, line 201, exported)

## Contained in

- [core/steps](https://banes-lab.com/anatomy/deploy/folder-deploy-core-steps.md)

## Used by

- [runtime/entrypoints/deployment.entrypoint.ts](https://banes-lab.com/source/deploy/runtime/entrypoints/deployment.entrypoint.ts.md)
- [runtime/entrypoints/nginx.entrypoint.ts](https://banes-lab.com/source/deploy/runtime/entrypoints/nginx.entrypoint.ts.md)

## Linked from

- [core/steps](https://banes-lab.com/anatomy/deploy/folder-deploy-core-steps.md)
- [runtime/entrypoints](https://banes-lab.com/anatomy/deploy/folder-deploy-runtime-entrypoints.md)

## Source

```typescript
import type {
    CatalogRow,
    Journal,
    ProbeExpectation,
    ProbeFetcher,
    ProbeResponse,
    Shell,
} from "#types/deployment.types";
import { LINE_BREAK, PATH_SEPARATOR } from "#configuration/constants/deployment.constants";
import {
    PROBE_ANSWERED,
    PROBE_FAILED,
    PROBE_INSTEAD,
    PROBE_LOGGED,
    PROBE_NO_RECORD,
    PROBE_PASSED,
    PROBE_STALE,
    PROBING_SITE,
} from "#configuration/strings/deployment.strings";
import {
    QUERY_ENDPOINT,
    RECORDS_SEGMENT,
    SOURCE_SEGMENT,
} from "@banes-lab/build-scripts/configuration/constants/catalog.constants.ts";
import { idsIndex, siteIndex } from "@banes-lab/build-scripts/core/resolvers/catalog.resolver.ts";
import { logSize, loggedSince } from "#core/adapters/server.adapter";
import { rootRows, shardAddresses, shardRow } from "#core/converters/catalog.converter";
import { Buffer } from "node:buffer";
import { COLLECTION_TITLES } from "@banes-lab/web/strings/catalog.strings";
import { JSON_ROUTE } from "@banes-lab/build-scripts/configuration/constants/site.constants.ts";
import { absolutePath } from "@ssot/paths";
import { digestOf } from "@govlab/content-fingerprint";
import { errorLogOf } from "#core/converters/nginx.converter";
import { fetchProbe } from "#core/adapters/site.adapter";
import { readFileSync } from "node:fs";

const JSON_TYPE = "application/json";
const MARKDOWN_TYPE = "text/markdown";
const TEXT_TYPE = "text/plain";
const ANY_TYPE = "";
const GET = "GET";
const HEAD = "HEAD";
const OPTIONS = "OPTIONS";
const OK = 200;
const NO_CONTENT = 204;
const NOT_FOUND = 404;
const BAD_REQUEST = 400;
const QUERY_PROBE = "?ref=api:route";
const STATIC_QUERY_PROBE = "?probe=1";
const MISSING_SEGMENT = "no-such-address";
const MARKDOWN_SUFFIX = ".md";
const TEXT_SUFFIX = ".txt";
const JSON_SUFFIX = ".json";
const SPACE = " ";
const STOP = ".";
const QUERY_MARK = "?";
const REF_SEPARATOR = ":";
const WILDCARD = "*";
const LETTERS = "abcdefghijklmnopqrstuvwxyz";
const ENCODING = "utf8";

const answer = function answer(status: number, type: string): string {
    return (String(status) + SPACE + type).trim();
};

const mismatch = function mismatch(expected: ProbeExpectation, response: ProbeResponse): string | null {
    if (response.status === expected.status && response.type.startsWith(expected.type)) {
        return null;
    }
    const found = answer(response.status, response.type);
    return expected.address + PROBE_ANSWERED + found + PROBE_INSTEAD + answer(expected.status, expected.type) + STOP;
};

const expectation = function expectation(
    address: string,
    method: string,
    status: number,
    type: string,
): ProbeExpectation {
    return { address, method, status, type };
};

const behaviors = function behaviors(site: string): readonly ProbeExpectation[] {
    const root = siteIndex();
    const route = (root.markdown ?? MARKDOWN_SUFFIX).slice(0, -MARKDOWN_SUFFIX.length);
    return [
        expectation(site + route, GET, OK, MARKDOWN_TYPE),
        expectation(site + root.json + PATH_SEPARATOR, GET, OK, JSON_TYPE),
        expectation(site + root.json, OPTIONS, NO_CONTENT, ANY_TYPE),
        expectation(site + root.json + PATH_SEPARATOR + MISSING_SEGMENT, GET, NOT_FOUND, JSON_TYPE),
        expectation(site + PATH_SEPARATOR + MISSING_SEGMENT + MARKDOWN_SUFFIX, GET, NOT_FOUND, MARKDOWN_TYPE),
        expectation(site + PATH_SEPARATOR + MISSING_SEGMENT + TEXT_SUFFIX, GET, NOT_FOUND, TEXT_TYPE),
        expectation(site + PATH_SEPARATOR + MISSING_SEGMENT + JSON_SUFFIX, GET, NOT_FOUND, JSON_TYPE),
        expectation(site + JSON_ROUTE + RECORDS_SEGMENT, GET, OK, JSON_TYPE),
        expectation(site + JSON_ROUTE + SOURCE_SEGMENT, GET, OK, JSON_TYPE),
        expectation(site + QUERY_ENDPOINT + QUERY_PROBE, GET, OK, JSON_TYPE),
        expectation(site + QUERY_ENDPOINT, GET, BAD_REQUEST, JSON_TYPE),
        expectation(site + root.json + STATIC_QUERY_PROBE, GET, BAD_REQUEST, JSON_TYPE),
    ];
};

const groupOf = function groupOf(ref: string): string {
    return ref.slice(0, ref.indexOf(REF_SEPARATOR));
};

const firstWord = function firstWord(title: string): string {
    let word = "";
    for (const character of title.toLowerCase()) {
        if (!LETTERS.includes(character)) {
            break;
        }
        word += character;
    }
    return word;
};

const queryProbes = function queryProbes(
    site: string,
    rows: readonly CatalogRow[],
): readonly ProbeExpectation[] | null {
    const record = rows.find((row) => row.ref !== null && COLLECTION_TITLES.has(groupOf(row.ref)));
    if (record?.ref === null || record?.ref === undefined || record.title === null) {
        return null;
    }
    const group = groupOf(record.ref);
    const queries = [
        { ref: record.ref },
        { name: record.title },
        { q: firstWord(record.title) },
        { id: group + REF_SEPARATOR + WILDCARD },
        { walk: record.ref },
        { collection: group },
    ];
    return queries.map((query) =>
        expectation(site + QUERY_ENDPOINT + QUERY_MARK + new URLSearchParams(query).toString(), GET, OK, JSON_TYPE),
    );
};

const parsed = function parsed(response: ProbeResponse): unknown {
    return response.status === OK ? JSON.parse(response.body) : null;
};

const isStale = function isStale(row: CatalogRow, response: ProbeResponse): boolean {
    if (response.status !== OK) {
        return false;
    }
    return Buffer.byteLength(response.body) !== row.bytes || digestOf(response.body) !== row.fingerprint;
};

const rowProblems = async function rowProblems(row: CatalogRow, fetcher: ProbeFetcher): Promise<readonly string[]> {
    const json = await fetcher(row.json, GET);
    const markdown = row.markdown === null ? null : await fetcher(row.markdown, HEAD);
    const found = [
        mismatch(expectation(row.json, GET, OK, JSON_TYPE), json),
        isStale(row, json) ? row.json + PROBE_STALE : null,
        markdown === null || row.markdown === null
            ? null
            : mismatch(expectation(row.markdown, HEAD, OK, MARKDOWN_TYPE), markdown),
    ];
    return found.filter((problem): problem is string => problem !== null);
};

const behaviorProblem = async function behaviorProblem(
    expected: ProbeExpectation,
    fetcher: ProbeFetcher,
): Promise<string | null> {
    return mismatch(expected, await fetcher(expected.address, expected.method));
};

export const probeSite = async function probeSite(
    journal: Journal,
    site: string,
    shell: Shell,
    fetcher: ProbeFetcher = fetchProbe,
): Promise<void> {
    journal.log(PROBING_SITE);
    const log = errorLogOf(readFileSync(absolutePath("app.nginxSite"), ENCODING));
    const offset = await logSize(shell, log);
    const rootAddress = site + siteIndex().json;
    const idsAddress = site + idsIndex().json;
    const [root, ids] = await Promise.all([fetcher(rootAddress, GET), fetcher(idsAddress, GET)]);
    const shards = await Promise.all(shardAddresses(parsed(ids)).map(async (address) => fetcher(address, GET)));
    const rows = [
        ...rootRows(parsed(root)),
        ...shards.flatMap((shard) => {
            const row = shardRow(parsed(shard));
            return row === null ? [] : [row];
        }),
    ];
    const queries = queryProbes(site, rows);
    const heads = [
        mismatch(expectation(rootAddress, GET, OK, JSON_TYPE), root),
        mismatch(expectation(idsAddress, GET, OK, JSON_TYPE), ids),
        queries === null ? PROBE_NO_RECORD : null,
        ...(await Promise.all(
            [...behaviors(site), ...(queries ?? [])].map(async (expected) => behaviorProblem(expected, fetcher)),
        )),
    ];
    const rowFindings = (await Promise.all(rows.map(async (row) => rowProblems(row, fetcher)))).flat();
    const logged = await loggedSince(shell, log, offset);
    const problems = [
        ...heads.filter((problem): problem is string => problem !== null),
        ...rowFindings,
        ...(logged.length === 0 ? [] : [PROBE_LOGGED + logged]),
    ];
    if (problems.length > 0) {
        throw new Error(PROBE_FAILED + LINE_BREAK + problems.join(LINE_BREAK));
    }
    journal.mark(PROBE_PASSED);
};
```
