[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { l, lexParse } from "@atp/lex";
2import { AtUri } from "@atp/syntax";
3import { Record } from "../data-plane/routes/records.ts";
4
5export class HydrationMap<T> extends Map<string, T | null> implements Merges {
6 merge(map: HydrationMap<T>): this {
7 map.forEach((val, key) => {
8 this.set(key, val);
9 });
10 return this;
11 }
12}
13
14export interface Merges {
15 merge<T extends this>(map: T): this;
16}
17
18type UnknownRecord = { $type: string; [x: string]: unknown };
19
20export type RecordInfo<T extends UnknownRecord> = {
21 record: T;
22 cid: string;
23 sortedAt: Date;
24 indexedAt: Date;
25 takedownRef: string | undefined;
26};
27
28export const mergeMaps = <V, M extends HydrationMap<V>>(
29 mapA?: M,
30 mapB?: M,
31): M | undefined => {
32 if (!mapA) return mapB;
33 if (!mapB) return mapA;
34 return mapA.merge(mapB);
35};
36
37export const mergeNestedMaps = <V, M extends HydrationMap<HydrationMap<V>>>(
38 mapA?: M,
39 mapB?: M,
40): M | undefined => {
41 if (!mapA) return mapB;
42 if (!mapB) return mapA;
43
44 for (const [key, map] of mapB) {
45 const merged = mergeMaps(mapA.get(key) ?? undefined, map ?? undefined);
46 mapA.set(key, merged ?? null);
47 }
48
49 return mapA;
50};
51
52export const mergeManyMaps = <T>(...maps: HydrationMap<T>[]) => {
53 return maps.reduce(mergeMaps, undefined as HydrationMap<T> | undefined);
54};
55
56export type ItemRef = { uri: string; cid?: string };
57
58export const parseRecord = <T extends UnknownRecord>(
59 recordSchema: l.RecordSchema,
60 entry: Record,
61 includeTakedowns: boolean,
62): RecordInfo<T> | undefined => {
63 if (!includeTakedowns && entry.takenDown) {
64 return undefined;
65 }
66 let record: unknown;
67 try {
68 record = lexParse(entry.record, { strict: false });
69 } catch {
70 return;
71 }
72 const cid = entry.cid;
73 const sortedAt = new Date(entry.sortedAt ?? 0);
74 const indexedAt = new Date(entry.indexedAt ?? 0);
75 if (!record || !cid) return;
76 if (!recordSchema.$matches(record)) {
77 return;
78 }
79 return {
80 record: record as T,
81 cid,
82 sortedAt,
83 indexedAt,
84 takedownRef: safeTakedownRef(entry),
85 };
86};
87
88export const parseString = (str: string | undefined): string | undefined => {
89 return str && str.length > 0 ? str : undefined;
90};
91
92export const urisByCollection = (uris: string[]): Map<string, string[]> => {
93 const result = new Map<string, string[]>();
94 for (const uri of uris) {
95 const collection = new AtUri(uri).collection;
96 const items = result.get(collection) ?? [];
97 items.push(uri);
98 result.set(collection, items);
99 }
100 return result;
101};
102
103export const split = <T>(
104 items: T[],
105 predicate: (item: T) => boolean,
106): [T[], T[]] => {
107 const yes: T[] = [];
108 const no: T[] = [];
109 for (const item of items) {
110 if (predicate(item)) {
111 yes.push(item);
112 } else {
113 no.push(item);
114 }
115 }
116 return [yes, no];
117};
118
119export const safeTakedownRef = (obj?: {
120 takenDown: boolean;
121 takedownRef?: string | undefined;
122}): string | undefined => {
123 if (!obj) return;
124 if (obj.takedownRef) return obj.takedownRef;
125 if (obj.takenDown) return "SPRK-TAKEDOWN-UNKNOWN";
126};