import { Timestamp } from '@bufbuild/protobuf' import { AtUriString, Cid, InferInput, InferOutput, LexParseOptions, LexValue, RecordSchema, Schema, TypedLexMap, ValidateOptions, lexParseJsonBytes, parseCidSafe, } from '@atproto/lex' import { AtUri } from '@atproto/syntax' import { Record as RecordEntry } from '../proto/bsky_pb.js' const PARSE_OPTIONS: LexParseOptions & ValidateOptions = { strict: false, } export class HydrationMap extends Map implements Merges { merge(map: HydrationMap): this { for (const [key, val] of map) { this.set(key, val) } return this } } export interface Merges { merge(map: T): this } export type RecordInfo = { record: T cid: string sortedAt: Date indexedAt: Date takedownRef: string | undefined } export const mergeMaps = >( mapA?: V, mapB?: V, ): V | undefined => { if (!mapA) return mapB if (!mapB) return mapA return mapA.merge(mapB) } export const mergeNestedMaps = >( mapA?: HydrationMap, mapB?: HydrationMap, ): HydrationMap | undefined => { if (!mapA) return mapB if (!mapB) return mapA for (const [key, map] of mapB) { const merged = mergeMaps(mapA.get(key) ?? undefined, map ?? undefined) mapA.set(key, merged ?? null) } return mapA } export const mergeManyMaps = (...maps: HydrationMap[]) => { return maps.reduce(mergeMaps, undefined as HydrationMap | undefined) } export type ItemRef = { uri: AtUriString; cid?: string } export function parseRecord( recordSchema: TSchema, recordEntry: RecordEntry, includeTakedowns: boolean, ): RecordInfo> | undefined { if (!includeTakedowns && recordEntry.takenDown) { return undefined } const cid = recordEntry.cid if (!cid) { return undefined } if (recordEntry.record.byteLength === 0) { return undefined } const record = lexParseJsonBytes(recordEntry.record, PARSE_OPTIONS) if (!record) { return undefined } // @NOTE We cannot use parse mode here. We must return the original to ensure // that the caller gets the same data as what is stored in the PDS (in case of // records). This is important because the receiver of the data should be able // to compute the right record CID. if (!recordSchema.$matches(record, PARSE_OPTIONS)) { return undefined } return { record, cid, sortedAt: parseDate(recordEntry.sortedAt) ?? new Date(0), indexedAt: parseDate(recordEntry.indexedAt) ?? new Date(0), takedownRef: safeTakedownRef(recordEntry), } } /** * Decodes binary data containing a JSON representation of a Lex value, and * validates it against the provided schema, in parse mode (i.e., allowing * coercion & defaults). * * Returns undefined if the input is empty (from dataplane's empty value * convention), or if the validation fails. */ export const parseJsonBytes = >( schema: TSchema, bytes: Uint8Array | undefined, ): InferOutput | undefined => { if (!bytes || bytes.byteLength === 0) return undefined const value = lexParseJsonBytes(bytes, PARSE_OPTIONS) const result = schema.safeParse(value, PARSE_OPTIONS) return result.success ? result.value : undefined } export const parseString = ( str: undefined | string, ): T | undefined => { return str ? (str as T) : undefined } export const parseCid = (cidStr: string | undefined): Cid | null => { if (!cidStr) return null return parseCidSafe(cidStr) } export const parseDate = ( timestamp: Timestamp | undefined, ): Date | undefined => { if (!timestamp) return undefined const date = timestamp.toDate() // Check for year 1 (0001-01-01 00:00:00 UTC) which is -62135596800000ms from epoch. // The Go dataplane gives us those values as they come from the Go zero-value for dates. if (date.getTime() === -62135596800000) return undefined return date } export const urisByCollection = ( uris: Iterable, ): Map => { const result = new Map() for (const uri of uris) { const collection = new AtUri(uri).collection const items = result.get(collection) ?? [] items.push(uri) result.set(collection, items) } return result } export const split = ( items: T[], predicate: (item: T) => boolean, ): [T[], T[]] => { const yes: T[] = [] const no: T[] = [] for (const item of items) { if (predicate(item)) { yes.push(item) } else { no.push(item) } } return [yes, no] } export const safeTakedownRef = (obj?: { takenDown: boolean takedownRef: string }): string | undefined => { if (!obj) return if (obj.takedownRef) return obj.takedownRef if (obj.takenDown) return 'BSKY-TAKEDOWN-UNKNOWN' } export const isActivitySubscriptionEnabled = ({ post, reply, }: { post: boolean reply: boolean }): boolean => post || reply