import type { BobbinContext, XrpcRequestInit } from "./client"; import type { Nsid } from "@atcute/lexicons/syntax"; import { INVALID_HANDLE, type MiniDoc } from "./identity"; import { jsonPost } from "./_request"; import { didFromUri, rkeyFromUri } from "./uri"; // payload types, also the keys payloads land under in the data sidecar export const TYPE_COUNT = "sh.tangled.query.enrichResponse#count"; export const TYPE_VIEWER = "sh.tangled.query.enrichResponse#viewer"; export const TYPE_MINIDOC = "blue.microcosm.identity.resolveMiniDoc"; export type PayloadType = typeof TYPE_COUNT | typeof TYPE_VIEWER | typeof TYPE_MINIDOC; export type RecordPath = string & {}; export type EnvelopePath = ".repo" | ".collection" | ".rkey" | "."; export type LinkSource = `${Nsid}:${RecordPath | EnvelopePath}`; // a descriptor is also the key its payload reads back under, so the readers // take the whole descriptor, a request and its reads can't drift apart export interface LinkDescriptor { source: LinkSource; type: T; targets?: RecordPath[]; } export type CountDescriptor = LinkDescriptor; export type ViewerDescriptor = LinkDescriptor; export type DocDescriptor = LinkDescriptor; // data[ref][source][type] = payload, payload shape depends on the type export type Sidecar = Record>>; export interface Enriched { output: O; data: Sidecar; } export interface EnrichRequest { xrpc: string; params?: Record; enrich: LinkDescriptor[]; viewer?: string; } export const target = ( descriptor: LinkDescriptor, targets: RecordPath[] ): LinkDescriptor => ({ ...descriptor, targets }); export const targetAll = ( descriptors: LinkDescriptor[], targets: RecordPath[] ): LinkDescriptor[] => descriptors.map((descriptor) => target(descriptor, targets)); // bobbin rejects viewer payloads with no viewer, so a logged-out request drops // them here instead of at every call site export const enrich = ( ctx: BobbinContext, req: EnrichRequest, init?: XrpcRequestInit ): Promise> => jsonPost>( ctx, "sh.tangled.query.enrichResponse", { ...req, enrich: req.viewer ? req.enrich : req.enrich.filter((descriptor) => descriptor.type !== TYPE_VIEWER) }, init ); const payloadOf = ( data: Sidecar, ref: string | undefined, { source, type }: LinkDescriptor ): unknown => (ref !== undefined ? data[ref]?.[source]?.[type] : undefined); export const countOf = (data: Sidecar, ref: string | undefined, of: CountDescriptor): number => (payloadOf(data, ref, of) as number | undefined) ?? 0; // undefined means no viewer or inapplicable descriptor, null means no matching record export const viewerUriOf = ( data: Sidecar, ref: string | undefined, of: ViewerDescriptor ): string | null | undefined => payloadOf(data, ref, of) as string | null | undefined; export const viewerRkeyOf = ( data: Sidecar, ref: string | undefined, of: ViewerDescriptor ): string | null | undefined => { const uri = viewerUriOf(data, ref, of); return uri ? rkeyFromUri(uri) : uri; }; export const miniDocOf = ( data: Sidecar, did: string | undefined, of: DocDescriptor ): MiniDoc | undefined => payloadOf(data, did, of) as MiniDoc | undefined; export const handleOf = (data: Sidecar, did: string | undefined, of: DocDescriptor): string => miniDocOf(data, did, of)?.handle ?? INVALID_HANDLE; export const authorOf = ( data: Sidecar, uri: string, of: DocDescriptor ): { did: string; handle: string } => { const did = didFromUri(uri); return { did, handle: handleOf(data, did, of) }; };