/** * Reading and updating the dev.pdsjs.oci.repo record value. */ import { OCI_REPO_COLLECTION } from './lexicon.js'; /** * @typedef {Object} BlobRef * @property {'blob'} $type * @property {{$link: string}} ref * @property {string} mimeType * @property {number} size */ /** * @typedef {Object} TagEntry * @property {string} tag * @property {string} digest */ /** * @typedef {Object} ManifestEntry * @property {string} digest * @property {string} mediaType * @property {BlobRef} blob */ /** * @typedef {Object} LayerEntry * @property {string} digest * @property {BlobRef} blob */ /** * @typedef {Object} OciRepoRecord * @property {string} name * @property {TagEntry[]} tags * @property {ManifestEntry[]} manifests * @property {LayerEntry[]} layers * @property {string} createdAt * @property {string} [updatedAt] */ /** * @param {string} cid * @param {string} mimeType * @param {number} size * @returns {BlobRef} */ export function blobRef(cid, mimeType, size) { return { $type: 'blob', ref: { $link: cid }, mimeType, size }; } /** * A blob reference read back over XRPC carries `$link`; one decoded from a * CAR carries raw CID bytes. Only the `$link` shape is expected here, since * every read goes through getRecord, but a missing ref still parses to null * rather than throwing. * @param {unknown} value * @returns {BlobRef|null} */ function asBlobRef(value) { if (!value || typeof value !== 'object') return null; const ref = /** @type {{ref?: {$link?: unknown}, mimeType?: unknown, size?: unknown}} */ ( value ); if (typeof ref.ref?.$link !== 'string') return null; return { $type: 'blob', ref: { $link: ref.ref.$link }, mimeType: typeof ref.mimeType === 'string' ? ref.mimeType : 'application/octet-stream', size: typeof ref.size === 'number' ? ref.size : 0, }; } /** * @param {unknown} value - record value from getRecord * @returns {OciRepoRecord|null} */ export function parseRepoRecord(value) { if (!value || typeof value !== 'object') return null; const record = /** @type {Record} */ (value); if (typeof record.name !== 'string') return null; /** @type {TagEntry[]} */ const tags = []; for (const entry of Array.isArray(record.tags) ? record.tags : []) { if ( entry && typeof entry.tag === 'string' && typeof entry.digest === 'string' ) { tags.push({ tag: entry.tag, digest: entry.digest }); } } /** @type {ManifestEntry[]} */ const manifests = []; for (const entry of Array.isArray(record.manifests) ? record.manifests : []) { const blob = asBlobRef(entry?.blob); if (entry && typeof entry.digest === 'string' && blob) { manifests.push({ digest: entry.digest, mediaType: typeof entry.mediaType === 'string' ? entry.mediaType : 'application/octet-stream', blob, }); } } /** @type {LayerEntry[]} */ const layers = []; for (const entry of Array.isArray(record.layers) ? record.layers : []) { const blob = asBlobRef(entry?.blob); if (entry && typeof entry.digest === 'string' && blob) { layers.push({ digest: entry.digest, blob }); } } return { name: record.name, tags, manifests, layers, createdAt: typeof record.createdAt === 'string' ? record.createdAt : new Date(0).toISOString(), updatedAt: typeof record.updatedAt === 'string' ? record.updatedAt : undefined, }; } /** * The record value to store, with `$type` set and empty updatedAt dropped. * @param {OciRepoRecord} record * @returns {Record} */ export function serializeRepoRecord(record) { return { $type: OCI_REPO_COLLECTION, name: record.name, tags: record.tags, manifests: record.manifests, layers: record.layers, createdAt: record.createdAt, ...(record.updatedAt ? { updatedAt: record.updatedAt } : {}), }; } /** * Every blob CID the record references, for the link table. * @param {OciRepoRecord} record * @returns {string[]} */ export function recordBlobCids(record) { const cids = new Set(); for (const entry of record.manifests) cids.add(entry.blob.ref.$link); for (const entry of record.layers) cids.add(entry.blob.ref.$link); return [...cids]; } /** * Drop the named manifest and every tag naming it, then drop layers no * remaining manifest references. Layer pruning is by digest presence in the * remaining manifests' stored reference lists, which the record does not * hold; the caller passes the still-referenced digests it computed from the * remaining manifest bodies. * @param {OciRepoRecord} record * @param {string} digest * @param {Set} stillReferenced - layer digests the remaining * manifests reference * @returns {OciRepoRecord} */ export function removeManifest(record, digest, stillReferenced) { return { ...record, tags: record.tags.filter((t) => t.digest !== digest), manifests: record.manifests.filter((m) => m.digest !== digest), layers: record.layers.filter((l) => stillReferenced.has(l.digest)), }; }