Peer

tcmts.cli.sync.Peer · #dvzrh46e · class

The serving side of the protocol, over a local codebase.

import type { Codebase, LockEntry } from "#codebase/tcmts/cli/codebase";
import {
  closeContribution, commentOn, mergeContribution, openContribution, viewContribution,
} from "#codebase/tcmts/cli/contributions";
import type { ContributionView } from "#codebase/tcmts/cli/contributions";
import { branchConflicts, takeConflicts } from "#codebase/tcmts/cli/merge";
import { addServerUser, ConflictError, removeServerUser, serverUsers, validNames } from "#codebase/tcmts/cli/sync";
import type {
  BranchState, NamespaceNode, NewContribution, Output, SharedLint, SharedResult,
} from "#codebase/tcmts/cli/sync";
import { shortHash } from "#codebase/tcmts/core/hash";
import type { DefRecord } from "#codebase/tcmts/core/types";
import { runStreaming } from "#codebase/tcmts/loader";
import type { Causal, Contribution, Release } from "#codebase/tcmts/store";

export class Peer {
  readonly cb: Codebase;
  readonly allowRun: boolean;

  constructor(cb: Codebase, allowRun = true) {
    this.cb = cb;
    this.allowRun = allowRun;
  }

  missing(components: string[]): string[] {
    return components.filter((c) => this.cb.store.componentMembers(c).length === 0);
  }

  get(hashes: string[]): DefRecord[] {
    const comps = new Set<string>();
    for (const h of hashes) {
      const rec = this.cb.store.getDef(h);
      if (rec) comps.add(rec.componentHash);
    }
    // each as first stored, and the other texts of what was asked for (comments, layout, doc) as records of their own:
    // a receiver checks each is the same code and keeps it beside it
    return [
      ...[...comps].flatMap((c) =>
        this.cb.store.componentMembers(c).map((m) => this.cb.store.defWithText(m.hash, undefined)!),
      ),
      ...hashes.flatMap((h) =>
        this.cb.store.textVersions(h).map((t) => this.cb.store.defWithText(h, t)!),
      ),
    ];
  }

  put(records: DefRecord[]): Promise<{ stored: number; checkers: number }> {
    return this.cb.receiveInParallel(records);
  }

  packages(): LockEntry[] {
    return this.cb.store.lockedPackages();
  }

  putPackages(entries: LockEntry[]) {
    this.cb.adoptPackages(entries);
  }

  branch(name: string): BranchState | null {
    if (!this.cb.store.hasBranch(name)) return null;
    return {
      head: this.cb.store.head(name),
      names: this.cb.store.names(name, true),
      conflicts: branchConflicts(this.cb, name),
    };
  }

  /** (`conflicts`: the sides of its conflicts, kept for it in place of what was) */
  setBranch(
    name: string,
    head: string,
    expect: string | null,
    command: string,
    conflicts?: unknown,
  ) {
    const current = this.cb.store.hasBranch(name) ? this.cb.store.head(name) : null;
    if (current !== expect)
      throw new ConflictError(`branch '${name}' changed since you last synced`);
    const c = this.cb.store.causal(head);
    if (!c) throw new Error(`history entry ${shortHash(head)} was not sent`);
    validNames(this.cb, this.cb.store.namespaceRows(c.namespace));
    this.cb.store.setHead(name, head, command);
    if (conflicts !== undefined) takeConflicts(this.cb, name, conflicts, "replace");
    this.cb.refreshShim();
    // (what its terms' types are, for those that came without: the server's own branch's are as its shim is written)
    if (name !== this.cb.store.branch().name)
      this.cb.fillShapesQuietly(this.cb.store.names(name).map((n) => n.hash));
  }

  ancestors(head: string): string[] {
    return this.cb.store.ancestors(head);
  }
  missingCausals(hashes: string[]): string[] {
    return this.cb.store.missingCausals(hashes);
  }
  getCausals(hashes: string[]): Causal[] {
    return this.cb.store.getCausals(hashes);
  }
  putCausals(causals: Causal[]) {
    this.cb.store.putCausals(causals);
  }
  missingNamespaces(hashes: string[]): string[] {
    return this.cb.store.missingNamespaces(hashes);
  }
  getNamespaces(hashes: string[]): NamespaceNode[] {
    return this.cb.store.getNamespaces(hashes);
  }
  putNamespaces(nodes: NamespaceNode[]) {
    this.cb.store.putNamespaces(nodes);
  }
  releases(): Release[] {
    return this.cb.store.releases();
  }
  putReleases(releases: Release[]) {
    this.cb.store.transaction(() =>
      releases.forEach((r) =>
        this.cb.store.putRelease({
          package: String(r.package),
          version: String(r.version),
          namespace: String(r.namespace),
          causal: String(r.causal),
          notes: String(r.notes),
          author: String(r.author),
          ts: Number(r.ts),
        }),
      ),
    );
  }
  contributions(all: boolean): Contribution[] {
    return this.cb.store.contributions(all ? undefined : "open");
  }
  contribution(id: number): ContributionView {
    return viewContribution(this.cb, id);
  }
  openContribution(c: NewContribution) {
    return openContribution(this.cb, c);
  }
  comment(id: number, c: { author: string; body: string; about?: string | null }) {
    commentOn(this.cb, id, c);
  }
  mergeContribution(id: number, author: string) {
    return mergeContribution(this.cb, id, author);
  }
  closeContribution(id: number, author: string, reopen = false) {
    closeContribution(this.cb, id, author, reopen);
  }
  /** the branch a contribution proposes */
  sourceOf(id: number): string | undefined {
    return this.cb.store.contributions().find((c) => c.id === id)?.source;
  }
  /** the contributors it knows (their tokens' digests left out) */
  users(): { handle: string; added: number }[] {
    return serverUsers(this.cb).map(({ handle, added }) => ({ handle, added }));
  }
  addUser(handle: string): string {
    return addServerUser(this.cb, handle);
  }
  removeUser(handle: string): boolean {
    return removeServerUser(this.cb, handle);
  }

  resultKeys(hashes: string[]): [string, string][] {
    return hashes.flatMap((h) =>
      this.cb.store.resultKeysOf(h).map((k): [string, string] => [h, k]),
    );
  }
  results(pairs: [string, string][]): SharedResult[] {
    return pairs.flatMap(([h, key]) => {
      const r = this.cb.store.testResult(String(h), String(key));
      return r ? [{ ...r, key: String(key) }] : [];
    });
  }
  lintKeys(hashes: string[]): [string, string][] {
    return hashes.flatMap((h) => this.cb.store.lintKeysOf(h).map((k): [string, string] => [h, k]));
  }
  lints(pairs: [string, string][]): SharedLint[] {
    return pairs.flatMap(([h, key]) => {
      const issues = this.cb.store.lintResult(String(h), String(key));
      return issues === undefined ? [] : [{ hash: String(h), key: String(key), issues }];
    });
  }
  /** lint findings of the definitions it has, where it has none of its own; only a list of findings is kept */
  putLints(lints: SharedLint[]) {
    this.cb.store.transaction(() => {
      for (const l of lints) {
        const hash = String(l.hash);
        const key = String(l.key);
        if (!this.cb.store.hasDef(hash) || this.cb.store.lintResult(hash, key) !== undefined)
          continue;
        try {
          if (!Array.isArray(JSON.parse(String(l.issues)))) continue;
        } catch {
          continue;
        }
        this.cb.store.putLintResult(hash, key, String(l.issues));
      }
    });
  }
  /** results of the definitions it has, where it has none of its own (a result ran here stays) */
  putResults(results: SharedResult[]) {
    this.cb.store.transaction(() => {
      for (const r of results) {
        const hash = String(r.hash);
        const key = String(r.key);
        if (!this.cb.store.hasDef(hash) || this.cb.store.testResult(hash, key)) continue;
        this.cb.store.putTestResult(
          {
            hash,
            ok: !!r.ok,
            message: String(r.message ?? ""),
            output: String(r.output ?? ""),
            ms: Number(r.ms) || 0,
          },
          key,
        );
      }
    });
  }

  run(hash: string, args: string[], out: Output): Promise<number> {
    if (!this.allowRun) throw new Error("this server does not run code (started with --no-run)");
    const rec = this.cb.store.getDef(hash);
    if (!rec) throw new Error(`no definition ${shortHash(hash)}; send it first`);
    if (rec.kind === "type") throw new Error(`${shortHash(hash)} is a type`);
    return runStreaming(hash, args, this.cb.source(), this.cb.lay, out);
  }
}

uses

used by