forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1import {
2 AtProtoClient,
3 NetworkSlicesActorDefsProfileViewBasic,
4 NetworkSlicesActorProfile,
5 NetworkSlicesSlice,
6 NetworkSlicesSliceDefsSliceView,
7 NetworkSlicesSliceDefsSparklinePoint,
8 NetworkSlicesSliceStatsOutput,
9} from "../client.ts";
10import { recordBlobToCdnUrl, RecordResponse } from "@slices/client";
11
12async function fetchSparklinesForSlices(
13 client: AtProtoClient,
14 sliceUris: string[]
15): Promise<Record<string, NetworkSlicesSliceDefsSparklinePoint[]>> {
16 if (sliceUris.length === 0) return {};
17
18 try {
19 const sparklinesResponse = await client.network.slices.slice.getSparklines({
20 slices: sliceUris,
21 duration: "24h",
22 });
23
24 // Convert array of sparklineEntry objects to a map
25 const sparklinesMap: Record<string, NetworkSlicesSliceDefsSparklinePoint[]> = {};
26 for (const entry of sparklinesResponse.sparklines) {
27 sparklinesMap[entry.sliceUri] = entry.points;
28 }
29 return sparklinesMap;
30 } catch (error) {
31 console.warn("Failed to fetch batch sparkline data:", error);
32 }
33 return {};
34}
35
36async function fetchStatsForSlice(
37 client: AtProtoClient,
38 sliceUri: string
39): Promise<NetworkSlicesSliceStatsOutput | null> {
40 try {
41 const statsResponse = await client.network.slices.slice.stats({
42 slice: sliceUri,
43 });
44 return statsResponse;
45 } catch (error) {
46 console.warn("Failed to fetch stats for slice:", sliceUri, error);
47 }
48 return null;
49}
50
51async function fetchStatsForSlices(
52 client: AtProtoClient,
53 sliceUris: string[]
54): Promise<Record<string, NetworkSlicesSliceStatsOutput | null>> {
55 const statsMap: Record<string, NetworkSlicesSliceStatsOutput | null> = {};
56
57 // Fetch stats for each slice individually (no batch stats endpoint yet)
58 await Promise.all(
59 sliceUris.map(async (uri) => {
60 statsMap[uri] = await fetchStatsForSlice(client, uri);
61 })
62 );
63
64 return statsMap;
65}
66
67export async function getSlice(
68 client: AtProtoClient,
69 uri: string
70): Promise<NetworkSlicesSliceDefsSliceView | null> {
71 try {
72 const sliceRecord = await client.network.slices.slice.getRecord({ uri });
73 if (!sliceRecord) return null;
74
75 const creatorProfile = await getSliceActor(client, sliceRecord.did);
76 if (!creatorProfile) return null;
77
78 const [sparklinesMap, stats, requestCount, inviteCount] = await Promise.all([
79 fetchSparklinesForSlices(client, [uri]),
80 fetchStatsForSlice(client, uri),
81 client.network.slices.waitlist.request.countRecords({
82 where: { slice: { eq: uri } },
83 }).then(r => r.count).catch(() => 0),
84 client.network.slices.waitlist.invite.countRecords({
85 where: { slice: { eq: uri } },
86 }).then(r => r.count).catch(() => 0),
87 ]);
88
89 const sparklineData = sparklinesMap[uri];
90
91 return sliceToView(sliceRecord, creatorProfile, sparklineData, stats, requestCount, inviteCount);
92 } catch (error) {
93 console.error("Failed to get slice:", error);
94 return null;
95 }
96}
97
98export async function getSliceActor(
99 client: AtProtoClient,
100 did: string
101): Promise<NetworkSlicesActorDefsProfileViewBasic | null> {
102 try {
103 const profileUri = `at://${did}/network.slices.actor.profile/self`;
104 const profileRecord = await client.network.slices.actor.profile.getRecord({
105 uri: profileUri,
106 });
107 if (!profileRecord) return null;
108
109 // Get the actual handle using getActors
110 const actorsResponse = await client.getActors({
111 where: { did: { eq: did } },
112 limit: 1,
113 });
114
115 const handle =
116 actorsResponse.actors.length > 0
117 ? actorsResponse.actors[0].handle ?? did
118 : did; // fallback to DID if no handle found
119
120 return actorToView(profileRecord, handle);
121 } catch (error) {
122 console.error("Failed to get slice actor:", error);
123 return null;
124 }
125}
126
127export function sliceToView(
128 sliceRecord: RecordResponse<NetworkSlicesSlice>,
129 creator: NetworkSlicesActorDefsProfileViewBasic,
130 sparkline?: NetworkSlicesSliceDefsSparklinePoint[],
131 stats?: NetworkSlicesSliceStatsOutput | null,
132 waitlistRequestCount?: number,
133 waitlistInviteCount?: number
134): NetworkSlicesSliceDefsSliceView {
135 return {
136 uri: sliceRecord.uri,
137 cid: sliceRecord.cid,
138 name: sliceRecord.value.name,
139 domain: sliceRecord.value.domain,
140 creator,
141 createdAt: sliceRecord.value.createdAt,
142 sparkline,
143 indexedRecordCount: stats?.totalRecords || 0,
144 indexedActorCount: stats?.totalActors || 0,
145 indexedCollectionCount: stats?.collectionStats.length || 0,
146 waitlistRequestCount: waitlistRequestCount || 0,
147 waitlistInviteCount: waitlistInviteCount || 0,
148 };
149}
150
151export function actorToView(
152 profileRecord: RecordResponse<NetworkSlicesActorProfile>,
153 handle: string
154): NetworkSlicesActorDefsProfileViewBasic {
155 return {
156 did: profileRecord.did,
157 handle,
158 displayName: profileRecord.value.displayName,
159 description: profileRecord.value.description,
160 avatar: profileRecord.value.avatar
161 ? recordBlobToCdnUrl(profileRecord, profileRecord.value.avatar, "avatar")
162 : undefined,
163 };
164}
165
166export async function getSlicesForActor(
167 client: AtProtoClient,
168 did: string
169): Promise<NetworkSlicesSliceDefsSliceView[]> {
170 try {
171 const slicesResponse = await client.network.slices.slice.getRecords({
172 where: {
173 did: { eq: did },
174 },
175 sortBy: [{ field: "createdAt", direction: "desc" }],
176 });
177
178 // Collect slice URIs for batch sparkline and stats fetch
179 const sliceUris = slicesResponse.records.map((record) => record.uri);
180
181 // Fetch sparklines and stats for all slices at once
182 const [sparklinesMap, statsMap] = await Promise.all([
183 fetchSparklinesForSlices(client, sliceUris),
184 fetchStatsForSlices(client, sliceUris),
185 ]);
186
187 const sliceViews: NetworkSlicesSliceDefsSliceView[] = [];
188 for (const sliceRecord of slicesResponse.records) {
189 const creator = await getSliceActor(client, sliceRecord.did);
190 if (creator) {
191 const sparklineData = sparklinesMap[sliceRecord.uri];
192 const statsData = statsMap[sliceRecord.uri];
193 sliceViews.push(
194 sliceToView(sliceRecord, creator, sparklineData, statsData)
195 );
196 }
197 }
198
199 return sliceViews;
200 } catch (error) {
201 console.error("Failed to get slices for actor:", error);
202 return [];
203 }
204}
205
206export async function searchSlices(
207 client: AtProtoClient,
208 query: string,
209 limit = 20
210): Promise<NetworkSlicesSliceDefsSliceView[]> {
211 try {
212 const slicesResponse = await client.network.slices.slice.getRecords({
213 where: {
214 name: { contains: query },
215 },
216 limit,
217 });
218
219 // Collect slice URIs for batch sparkline and stats fetch
220 const sliceUris = slicesResponse.records.map((record) => record.uri);
221
222 // Fetch sparklines and stats for all slices at once
223 const [sparklinesMap, statsMap] = await Promise.all([
224 fetchSparklinesForSlices(client, sliceUris),
225 fetchStatsForSlices(client, sliceUris),
226 ]);
227
228 const sliceViews: NetworkSlicesSliceDefsSliceView[] = [];
229 for (const sliceRecord of slicesResponse.records) {
230 const creator = await getSliceActor(client, sliceRecord.did);
231 if (creator) {
232 const sparklineData = sparklinesMap[sliceRecord.uri];
233 const statsData = statsMap[sliceRecord.uri];
234 sliceViews.push(
235 sliceToView(sliceRecord, creator, sparklineData, statsData)
236 );
237 }
238 }
239
240 return sliceViews;
241 } catch (error) {
242 console.error("Failed to search slices:", error);
243 return [];
244 }
245}
246
247export async function getTimeline(
248 client: AtProtoClient,
249 limit = 50
250): Promise<NetworkSlicesSliceDefsSliceView[]> {
251 try {
252 const slicesResponse = await client.network.slices.slice.getRecords({
253 limit,
254 sortBy: [{ field: "createdAt", direction: "desc" }],
255 });
256
257 // Collect slice URIs for batch sparkline and stats fetch
258 const sliceUris = slicesResponse.records.map((record) => record.uri);
259
260 // Fetch sparklines and stats for all slices at once
261 const [sparklinesMap, statsMap] = await Promise.all([
262 fetchSparklinesForSlices(client, sliceUris),
263 fetchStatsForSlices(client, sliceUris),
264 ]);
265
266 const sliceViews: NetworkSlicesSliceDefsSliceView[] = [];
267 for (const sliceRecord of slicesResponse.records) {
268 const creator = await getSliceActor(client, sliceRecord.did);
269 if (creator) {
270 const sparklineData = sparklinesMap[sliceRecord.uri];
271 const statsData = statsMap[sliceRecord.uri];
272 sliceViews.push(
273 sliceToView(sliceRecord, creator, sparklineData, statsData)
274 );
275 }
276 }
277
278 return sliceViews;
279 } catch (error) {
280 console.error("Failed to get timeline:", error);
281 return [];
282 }
283}