Peer
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
- tcmts.cli.codebase.Codebase
- tcmts.cli.codebase.LockEntry
- tcmts.cli.contributions.ContributionView
- tcmts.cli.contributions.closeContribution
- tcmts.cli.contributions.commentOn
- tcmts.cli.contributions.mergeContribution
- tcmts.cli.contributions.openContribution
- tcmts.cli.contributions.viewContribution
- tcmts.cli.merge.branchConflicts
- tcmts.cli.merge.takeConflicts
- tcmts.cli.sync.BranchState
- tcmts.cli.sync.ConflictError
- tcmts.cli.sync.NamespaceNode
- tcmts.cli.sync.NewContribution
- tcmts.cli.sync.Output
- tcmts.cli.sync.SharedLint
- tcmts.cli.sync.SharedResult
- tcmts.cli.sync.addServerUser
- tcmts.cli.sync.removeServerUser
- tcmts.cli.sync.serverUsers
- tcmts.cli.sync.validNames
- tcmts.core.hash.shortHash
- tcmts.core.types.DefRecord
- tcmts.loader.runStreaming
- tcmts.store.Causal
- tcmts.store.Contribution
- tcmts.store.Release