chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* 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<string, unknown>} */ (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<string, unknown>}
*/
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<string>} 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)),
};
}