[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { DataPlane } from "../data-plane/index.ts";
2import * as so from "../lex/so.ts";
3import { HydrationMap, parseRecord, RecordInfo } from "./util.ts";
4
5export type BlockRecord = so.sprk.graph.block.Main;
6export type FollowRecord = so.sprk.graph.follow.Main;
7
8export type Follow = RecordInfo<FollowRecord>;
9export type Follows = HydrationMap<Follow>;
10
11export type FollowInfo = {
12 uri: string;
13 actorDid: string;
14 subjectDid: string;
15};
16
17export type Block = RecordInfo<BlockRecord>;
18
19export type RelationshipPair = [didA: string, didB: string];
20
21const dedupePairs = (pairs: RelationshipPair[]): RelationshipPair[] => {
22 const deduped = pairs.reduce((acc, pair) => {
23 return acc.set(Blocks.key(...pair), pair);
24 }, new Map<string, RelationshipPair>());
25 return [...deduped.values()];
26};
27
28export class Blocks {
29 _blocks: Map<string, BlockEntry> = new Map(); // did:a,did:b -> block
30 constructor() {}
31
32 static key(didA: string, didB: string): string {
33 return [didA, didB].sort().join(",");
34 }
35
36 set(didA: string, didB: string, block: BlockEntry): Blocks {
37 const key = Blocks.key(didA, didB);
38 this._blocks.set(key, block);
39 return this;
40 }
41
42 get(didA: string, didB: string): BlockEntry | null {
43 if (didA === didB) return null; // ignore self-blocks
44 const key = Blocks.key(didA, didB);
45 return this._blocks.get(key) ?? null;
46 }
47
48 merge(blocks: Blocks): Blocks {
49 blocks._blocks.forEach((block, key) => {
50 this._blocks.set(key, block);
51 });
52 return this;
53 }
54}
55
56// No "blocking" vs. "blocked" directionality: only suitable for bidirectional block checks
57export type BlockEntry = {
58 blockUri: string | undefined;
59};
60
61export class GraphHydrator {
62 constructor(public dataplane: DataPlane) {}
63
64 async getBidirectionalBlocks(pairs: RelationshipPair[]): Promise<Blocks> {
65 if (!pairs.length) return new Blocks();
66 const deduped = dedupePairs(pairs).map(([a, b]) => ({ a, b }));
67 const res = await this.dataplane.relationships.getBlockExistence(deduped);
68 const blocks = new Blocks();
69 for (let i = 0; i < deduped.length; i++) {
70 const pair = deduped[i];
71 const block = res.blocks[i];
72 blocks.set(pair.a, pair.b, {
73 blockUri: block.blockedBy || block.blocking || undefined,
74 });
75 }
76 return blocks;
77 }
78
79 async getFollows(uris: string[], includeTakedowns = false): Promise<Follows> {
80 if (!uris.length) return new HydrationMap<Follow>();
81 const res = await this.dataplane.records.getFollowRecords(uris);
82 return uris.reduce((acc, uri, i) => {
83 const record = parseRecord<FollowRecord>(
84 so.sprk.graph.follow.main,
85 res.records[i],
86 includeTakedowns,
87 );
88 return acc.set(uri, record ?? null);
89 }, new HydrationMap<Follow>());
90 }
91
92 async getBlocks(
93 uris: string[],
94 includeTakedowns = false,
95 ): Promise<HydrationMap<Block>> {
96 if (!uris.length) return new HydrationMap<Block>();
97 const res = await this.dataplane.records.getBlockRecords(uris);
98 return uris.reduce((acc, uri, i) => {
99 const record = parseRecord<BlockRecord>(
100 so.sprk.graph.block.main,
101 res.records[i],
102 includeTakedowns,
103 );
104 return acc.set(uri, record ?? null);
105 }, new HydrationMap<Block>());
106 }
107
108 async getActorFollows(input: {
109 did: string;
110 cursor?: string;
111 limit?: number;
112 }): Promise<{ follows: FollowInfo[]; cursor: string | undefined }> {
113 const { did, cursor, limit } = input;
114 const res = await this.dataplane.follows.getFollows(
115 did,
116 limit,
117 cursor,
118 );
119 return { follows: res.follows, cursor: res.cursor };
120 }
121
122 async getActorFollowers(input: {
123 did: string;
124 cursor?: string;
125 limit?: number;
126 }): Promise<{ followers: FollowInfo[]; cursor: string | undefined }> {
127 const { did, cursor, limit } = input;
128 const res = await this.dataplane.follows.getFollowers(
129 did,
130 limit,
131 cursor,
132 );
133 return { followers: res.followers, cursor: res.cursor };
134 }
135}