import type { BobbinContext, XrpcRequestInit } from "#lib/api/client.ts";
import type { Did, Handle, Nsid } from "@atcute/lexicons/syntax";
import { INVALID_HANDLE, type MiniDoc } from "#lib/api/identity.ts";
import { jsonPost } from "#lib/api/_request.ts";
import { authorDidOf, rkeyFromUri } from "#lib/api/uri.ts";
import type * as OrgTangledRepoDefs from "#lib/api/lexicons/types/org/tangled/repo/defs.ts";
import type * as OrgTangledQueryEnrichResponse from "#lib/api/lexicons/types/org/tangled/query/enrichResponse.ts";
// payload types, also the keys payloads land under in the data sidecar
export const TYPE_COUNT = "org.tangled.query.enrichResponse#count";
export const TYPE_VIEWER = "org.tangled.query.enrichResponse#viewer";
export const TYPE_MINIDOC = "blue.microcosm.identity.resolveMiniDoc";
export const TYPE_REPO_VIEW = "org.tangled.repo.defs#repoViewBasic";
export const TYPE_LABELS = "org.tangled.query.enrichResponse#labels";
export const TYPE_REACTIONS = "org.tangled.query.enrichResponse#reactions";
export type PayloadType =
| typeof TYPE_COUNT
| typeof TYPE_VIEWER
| typeof TYPE_MINIDOC
| typeof TYPE_REPO_VIEW
| typeof TYPE_LABELS
| typeof TYPE_REACTIONS;
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<T extends PayloadType = PayloadType> {
source: LinkSource;
type: T;
targets?: RecordPath[];
}
export type CountDescriptor = LinkDescriptor<typeof TYPE_COUNT>;
export type ViewerDescriptor = LinkDescriptor<typeof TYPE_VIEWER>;
export type DocDescriptor = LinkDescriptor<typeof TYPE_MINIDOC>;
export type RepoViewDescriptor = LinkDescriptor<typeof TYPE_REPO_VIEW>;
export type LabelsDescriptor = LinkDescriptor<typeof TYPE_LABELS>;
export type ReactionsDescriptor = LinkDescriptor<typeof TYPE_REACTIONS>;
// data[ref][source][type] = payload, payload shape depends on the type
export type Sidecar = Record<string, Record<string, Record<string, unknown>>>;
export interface Enriched<O> {
output: O;
data: Sidecar;
}
export interface EnrichRequest {
xrpc: string;
params?: Record<string, unknown>;
enrich: LinkDescriptor[];
viewer?: string;
}
export const target = <T extends PayloadType>(
descriptor: LinkDescriptor<T>,
targets: RecordPath[]
): LinkDescriptor<T> => ({ ...descriptor, targets });
export const targetAll = <T extends PayloadType>(
descriptors: LinkDescriptor<T>[],
targets: RecordPath[]
): LinkDescriptor<T>[] => 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 = <O>(
ctx: BobbinContext,
req: EnrichRequest,
init?: XrpcRequestInit
): Promise<Enriched<O>> =>
jsonPost<Enriched<O>>(
ctx,
"org.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 repoViewOf = (
data: Sidecar,
ref: string | undefined,
of: RepoViewDescriptor
): OrgTangledRepoDefs.RepoViewBasic | undefined =>
payloadOf(data, ref, of) as OrgTangledRepoDefs.RepoViewBasic | undefined;
export const handleOf = (data: Sidecar, did: string | undefined, of: DocDescriptor): Handle =>
miniDocOf(data, did, of)?.handle ?? INVALID_HANDLE;
// bobbin only sends the writer's profile when a target asks for author or value.x-tngl-editor
// a ticket's author is who opened it, x-tngl-editor only names who wrote the latest version
export const authorOf = (
data: Sidecar,
record: { uri: string; value?: unknown; author?: Did },
of: DocDescriptor
): { did: Did; handle: Handle } => {
const did = record.author ?? authorDidOf(record);
return { did, handle: handleOf(data, did, of) };
};
export type Label = OrgTangledQueryEnrichResponse.Labels["labels"][number];
export const labelsOf = (data: Sidecar, ref: string | undefined, of: LabelsDescriptor): Label[] =>
(payloadOf(data, ref, of) as OrgTangledQueryEnrichResponse.Labels | undefined)?.labels ?? [];
export type ReactionSummary = OrgTangledQueryEnrichResponse.ReactionSummary;
export const reactionsOf = (
data: Sidecar,
ref: string | undefined,
of: ReactionsDescriptor
): ReactionSummary[] =>
(payloadOf(data, ref, of) as OrgTangledQueryEnrichResponse.Reactions | undefined)?.reactions ??
[];