import type { BuildEvent, BuildRequest, BuildStatus, Sha256Digest, } from "../shared/build-protocol"; export const BUILD_RECORD_API_VERSION = "kuber.astrxl.dev/v1" as const; export interface BuildRecord { apiVersion: typeof BUILD_RECORD_API_VERSION; kind: "BuildRecord"; metadata: { name: string; resourceVersion: string; creationTimestamp: string; labels: Record; }; spec: { request: BuildRequest; imageKey: string; jobName: string; workspaceSubPath: string; }; status: BuildStatus & { cancelled?: boolean; jobCreated?: boolean; logBytes: number; logOffset: number; nextSequence: number; events: BuildEvent[]; }; } export interface UploadRecord { apiVersion: typeof BUILD_RECORD_API_VERSION; kind: "BuildUpload"; metadata: { name: string; resourceVersion: string; creationTimestamp: string; }; spec: { digest: Sha256Digest; size: number }; status: { offset: number; data: Uint8Array }; } export type CreateBuildResult = { record: BuildRecord; created: boolean }; /** Implementations must make createBuild and replace operations atomic. */ export interface BuildStore { createBuild(record: BuildRecord): Promise; getBuild(id: string): Promise; listBuilds(): Promise; replaceBuild( record: BuildRecord, expectedResourceVersion: string, ): Promise; getUpload(digest: Sha256Digest): Promise; createUpload(record: UploadRecord): Promise; replaceUpload( record: UploadRecord, expectedResourceVersion: string, ): Promise; deleteUpload(digest: Sha256Digest): Promise; } export class BuildStoreConflictError extends Error { readonly code = "BUILD_STORE_CONFLICT"; } function clone(value: T): T { return structuredClone(value); } function terminal(record: BuildRecord): boolean { return ( record.status.state === "succeeded" || record.status.state === "failed" ); } function sameSpec(left: BuildRecord, right: BuildRecord): boolean { return JSON.stringify(left.spec) === JSON.stringify(right.spec); } export class MemoryBuildStore implements BuildStore { private readonly builds = new Map(); private readonly uploads = new Map(); private readonly active = new Map(); async createBuild(record: BuildRecord): Promise { const existing = this.builds.get(record.metadata.name); if (existing) { if (!sameSpec(existing, record)) { throw new BuildStoreConflictError( "Build ID was already used for a different request", ); } return { record: clone(existing), created: false }; } const activeId = this.active.get(record.spec.imageKey); if (activeId) { const active = this.builds.get(activeId); if (active && !terminal(active)) { throw new BuildStoreConflictError( `Build '${activeId}' is already active for ${record.spec.imageKey}`, ); } } this.builds.set(record.metadata.name, clone(record)); this.active.set(record.spec.imageKey, record.metadata.name); return { record: clone(record), created: true }; } async getBuild(id: string) { const value = this.builds.get(id); return value && clone(value); } async listBuilds() { return [...this.builds.values()] .sort((a, b) => a.metadata.creationTimestamp.localeCompare( b.metadata.creationTimestamp, ), ) .map(clone); } async replaceBuild(record: BuildRecord, expectedResourceVersion: string) { const current = this.builds.get(record.metadata.name); if ( !current || current.metadata.resourceVersion !== expectedResourceVersion ) { throw new BuildStoreConflictError( "Build record was concurrently modified", ); } if (!sameSpec(current, record)) { throw new BuildStoreConflictError("Build specification is immutable"); } if ( current.status.state === "succeeded" && (record.status.state !== "succeeded" || record.status.digest !== current.status.digest) ) { throw new BuildStoreConflictError( "A successful image digest is immutable", ); } this.builds.set(record.metadata.name, clone(record)); if ( terminal(record) && this.active.get(record.spec.imageKey) === record.metadata.name ) { this.active.delete(record.spec.imageKey); } } async getUpload(digest: Sha256Digest) { const value = this.uploads.get(digest); return value && clone(value); } async createUpload(record: UploadRecord) { const current = this.uploads.get(record.spec.digest); if (current) return clone(current); this.uploads.set(record.spec.digest, clone(record)); return clone(record); } async replaceUpload(record: UploadRecord, expectedResourceVersion: string) { const current = this.uploads.get(record.spec.digest); if ( !current || current.metadata.resourceVersion !== expectedResourceVersion ) { throw new BuildStoreConflictError( "Upload record was concurrently modified", ); } this.uploads.set(record.spec.digest, clone(record)); } async deleteUpload(digest: Sha256Digest) { this.uploads.delete(digest); } }