import type { MarketClient } from './client.js' import { assetNameSchema, MAX_ASSET_REFS_QUERY_LENGTH, semverSchema } from './schemas.js' import type { AssetRefEntry } from './v1/index.js' export interface AssetRef { name: string version?: string } /** The one call the refs read needs — lets tests fake a single function. */ export interface RefsReader { search: MarketClient['asset']['search'] } export interface ResolveAssetOptions { includeUnapproved?: boolean key?: string } /** The server accepts up to 100 refs and 4,000 characters per read. */ const MAX_REFS_PER_CALL = 100 /** * The collection read in refs mode: full entries (metadata, version history, * files with data-plane URLs) for exactly the given refs — order-preserved, * null per miss. Version omitted resolves to the latest visible version. */ export async function lookupAssets( asset: RefsReader, refs: AssetRef[], opts: { includeUnapproved?: boolean; key?: string } = {}, ): Promise<(AssetRefEntry | null)[]> { const chunks = chunkRefs(refs) const results = await Promise.all( chunks.map(async (refsQuery) => { const result = await asset.search({ refs: refsQuery, includeUnapproved: opts.includeUnapproved ?? false, ...(opts.key ? { key: opts.key } : {}), }) if (!('assets' in result)) throw new Error('refs read unexpectedly returned a search result') return result.assets }), ) return results.flat() } /** Resolve a bare asset name or exact name@version without accepting semver ranges. */ export async function resolveAsset( client: RefsReader, ref: string, opts: ResolveAssetOptions = {}, ): Promise { const separator = ref.indexOf('@') const name = assetNameSchema.parse(separator === -1 ? ref : ref.slice(0, separator)) const requestedVersion = separator === -1 ? undefined : semverSchema.parse(ref.slice(separator + 1)) const [asset] = await lookupAssets( client, [{ name, ...(requestedVersion ? { version: requestedVersion } : {}) }], { includeUnapproved: opts.includeUnapproved ?? false, ...(opts.key ? { key: opts.key } : {}), }, ) if (!asset) throw new Error(`Asset "${ref}" not found`) if (requestedVersion && asset.version !== requestedVersion) { throw new Error(`Market API did not return requested version "${ref}"`) } return asset } function chunkRefs(refs: AssetRef[]): string[] { const chunks: string[] = [] let chunk: string[] = [] let length = 0 for (const ref of refs) { const token = ref.version ? `${ref.name}@${ref.version}` : ref.name if (token.length > MAX_ASSET_REFS_QUERY_LENGTH) { throw new Error(`Asset ref exceeds ${MAX_ASSET_REFS_QUERY_LENGTH} characters: ${ref.name}`) } const separatorLength = chunk.length > 0 ? 1 : 0 const nextLength = length + separatorLength + token.length if (chunk.length === MAX_REFS_PER_CALL || nextLength > MAX_ASSET_REFS_QUERY_LENGTH) { chunks.push(chunk.join(',')) chunk = [] length = 0 } const nextSeparatorLength = chunk.length > 0 ? 1 : 0 chunk.push(token) length += nextSeparatorLength + token.length } if (chunk.length > 0) chunks.push(chunk.join(',')) return chunks }