Highly ambitious ATProtocol AppView service and sdks
at main 8.7 kB view raw
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}