feat: add durable addressed registry publication
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Addressed registry publication is deliberately data-only: callers provide identities and the
|
||||
* executor persists every transition before invoking a side effect. */
|
||||
export interface RegistryInstallationIdentityV1 { readonly installationId: string; readonly digest: string; }
|
||||
export interface RegistryRepositoryIdentityV1 { readonly remote: string; readonly branch: string; readonly head: string; readonly digest: string; }
|
||||
export interface RegistryRemoteIdentityV1 { readonly remote: string; readonly head: string; readonly digest: string; }
|
||||
export interface RegistryRecoveryIdentityV1 { readonly runId: string; readonly requestDigest: string; readonly digest: string; }
|
||||
interface RegistryAddressedRequestFieldsV1 { readonly installation: RegistryInstallationIdentityV1; readonly repository: RegistryRepositoryIdentityV1; readonly remote: RegistryRemoteIdentityV1; readonly workspaceIds: readonly string[]; readonly requestDigest: string; }
|
||||
export interface RegistryBootstrapAddressedRequestV1 extends RegistryAddressedRequestFieldsV1 { readonly kind: "bootstrap"; }
|
||||
export interface RegistryPublishAddressedRequestV1 extends RegistryAddressedRequestFieldsV1 { readonly kind: "publish"; readonly target: string; }
|
||||
export type RegistryAddressedRequestV1 = RegistryBootstrapAddressedRequestV1 | RegistryPublishAddressedRequestV1;
|
||||
export interface RegistryAddressedSnapshotV1 { readonly schemaVersion: 1; readonly commit: string | null; readonly baseCommit: string | null; readonly baseDigest: string | null; readonly workspaceIds: readonly string[]; readonly changedWorkspaceIds: readonly string[]; readonly installation: RegistryInstallationIdentityV1; readonly repository: RegistryRepositoryIdentityV1; readonly remote: RegistryRemoteIdentityV1; readonly requestDigest: string; readonly recovery?: RegistryRecoveryIdentityV1; }
|
||||
export interface RegistryAddressedPublicationStateV1 extends RegistryAddressedSnapshotV1 { readonly runId: string; readonly phase: "request_claimed" | "target_advertised" | "target_fetched" | "planned" | "terminal"; readonly result?: unknown; readonly updatedAt: string; }
|
||||
export interface RegistryAddressedPublicationResultV1 { readonly runId: string; readonly snapshot: RegistryAddressedSnapshotV1; readonly result?: unknown; }
|
||||
export type RegistryEnsureBootstrapAddressedResultV1 =
|
||||
| { readonly kind: "already_active"; readonly snapshot: RegistryAddressedSnapshotV1 }
|
||||
| { readonly kind: "bootstrap_terminal"; readonly result: RegistryAddressedPublicationResultV1; readonly snapshot: RegistryAddressedSnapshotV1 };
|
||||
|
||||
/** Explicit request variants used by callers that already own a durable run. */
|
||||
export interface RegistryAddressedBootstrapCreateRequestV1 extends RegistryBootstrapAddressedRequestV1 { readonly mode: "create"; }
|
||||
export interface RegistryAddressedBootstrapResumeRequestV1 extends RegistryBootstrapAddressedRequestV1 { readonly mode: "resume"; readonly runId: string; }
|
||||
export interface RegistryAddressedPublishCreateRequestV1 extends RegistryPublishAddressedRequestV1 { readonly mode: "create"; }
|
||||
export interface RegistryAddressedPublishResumeRequestV1 extends RegistryPublishAddressedRequestV1 { readonly mode: "resume"; readonly runId: string; }
|
||||
export type RegistryAddressedCreateRequestV1 = RegistryAddressedBootstrapCreateRequestV1 | RegistryAddressedPublishCreateRequestV1;
|
||||
export type RegistryAddressedResumeRequestV1 = RegistryAddressedBootstrapResumeRequestV1 | RegistryAddressedPublishResumeRequestV1;
|
||||
export type RegistryAddressedCreateResultV1 = RegistryAddressedPublicationResultV1;
|
||||
export type RegistryAddressedResumeResultV1 = RegistryAddressedPublicationResultV1;
|
||||
export const REGISTRY_SCAN_LIMITS_V1 = Object.freeze({ entries: 128, bytes: 1024 * 1024, fileBytes: 128 * 1024 });
|
||||
export const registryDigest = (value: unknown): string => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
||||
export function canonicalBootstrapRequestDigest(request: { readonly kind: "bootstrap" | "publish"; readonly installation: RegistryInstallationIdentityV1; readonly repository: RegistryRepositoryIdentityV1; readonly remote: RegistryRemoteIdentityV1; readonly workspaceIds: readonly string[] }): string { return registryDigest({ kind: request.kind, installation: request.installation, repository: request.repository, remote: request.remote, workspaceIds: [...request.workspaceIds].sort() }); }
|
||||
export function addressedRunId(): string { return randomBytes(16).toString("hex"); }
|
||||
function safeRunId(v: string): void { if (!/^[0-9a-f]{32}$/.test(v)) throw new Error("preprocessing_conflict"); }
|
||||
export class RegistryAddressedPublicationStore {
|
||||
constructor(readonly root: string) {}
|
||||
private path(runId: string): string { safeRunId(runId); return join(this.root, "addressed-publication-jobs", `${runId}.json`); }
|
||||
async claim(request: RegistryAddressedRequestV1, runId = addressedRunId()): Promise<RegistryAddressedPublicationStateV1> {
|
||||
safeRunId(runId); await mkdir(join(this.root, "addressed-publication-jobs"), { recursive: true, mode: 0o700 });
|
||||
const now = new Date().toISOString(); const snapshot = this.snapshot(request); const state: RegistryAddressedPublicationStateV1 = { ...snapshot, runId, phase: "request_claimed", updatedAt: now };
|
||||
try { await writeFile(this.path(runId), `${JSON.stringify(state)}\n`, { flag: "wx", mode: 0o600 }); } catch { throw new Error("preprocessing_conflict"); }
|
||||
return state;
|
||||
}
|
||||
async read(runId: string): Promise<RegistryAddressedPublicationStateV1> { try { return JSON.parse(await readFile(this.path(runId), "utf8")) as RegistryAddressedPublicationStateV1; } catch { throw new Error("preprocessing_conflict"); } }
|
||||
async transition(runId: string, phase: RegistryAddressedPublicationStateV1["phase"], patch: Partial<RegistryAddressedPublicationStateV1> = {}): Promise<RegistryAddressedPublicationStateV1> {
|
||||
const old = await this.read(runId); const order = ["request_claimed", "target_advertised", "target_fetched", "planned", "terminal"] as const; if (order.indexOf(phase) < order.indexOf(old.phase)) throw new Error("preprocessing_conflict");
|
||||
const next = { ...old, ...patch, phase, updatedAt: new Date().toISOString() }; await writeFile(this.path(runId), `${JSON.stringify(next)}\n`, { mode: 0o600 }); return next;
|
||||
}
|
||||
snapshotFor(state: RegistryAddressedPublicationStateV1): RegistryAddressedSnapshotV1 { const { runId: _r, phase: _p, updatedAt: _u, result: _x, ...snapshot } = state; return snapshot; }
|
||||
private snapshot(request: RegistryAddressedRequestV1): RegistryAddressedSnapshotV1 { return { schemaVersion: 1, commit: null, baseCommit: null, baseDigest: null, workspaceIds: [...request.workspaceIds].sort(), changedWorkspaceIds: [...request.workspaceIds].sort(), installation: request.installation, repository: request.repository, remote: request.remote, requestDigest: request.requestDigest }; }
|
||||
}
|
||||
Reference in New Issue
Block a user