forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 type AppBskyEmbedRecord,
3 type AppBskyFeedDefs,
4 AppBskyFeedPost,
5 AtUri,
6 type BskyAgent,
7} from '@atproto/api'
8import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
9import {useQuery} from '@tanstack/react-query'
10
11import {retry} from '#/lib/async/retry'
12import {STALE} from '#/state/queries'
13import {useAgent} from '#/state/session'
14import * as bsky from '#/types/bsky'
15import {type EmbedType} from '#/types/bsky/post'
16
17const RQKEY_ROOT = 'direct-fetch-record'
18export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
19
20export async function directFetchRecordAndProfile(
21 agent: BskyAgent,
22 uri: string,
23) {
24 const urip = new AtUri(uri)
25
26 if (!urip.host.startsWith('did:')) {
27 const res = await agent.resolveHandle({
28 handle: urip.host,
29 })
30 urip.host = res.data.did as `did:${string}:${string}`
31 }
32
33 try {
34 const [profile, record] = await Promise.all([
35 (async () => (await agent.getProfile({actor: urip.host})).data)(),
36 (async () =>
37 (
38 await retry(
39 2,
40 e => {
41 if (e.message.includes(`Could not locate record:`)) {
42 return false
43 }
44 return true
45 },
46 () =>
47 agent.api.com.atproto.repo.getRecord({
48 repo: urip.host,
49 collection: 'app.bsky.feed.post',
50 rkey: urip.rkey,
51 }),
52 )
53 ).data.value)(),
54 ])
55
56 return {profile, record}
57 } catch (e) {
58 console.error(e)
59 return undefined
60 }
61}
62
63export async function directFetchEmbedRecord(
64 agent: BskyAgent,
65 uri: string,
66): Promise<EmbedType<'post'> | undefined> {
67 const res = await directFetchRecordAndProfile(agent, uri)
68 if (res === undefined) return undefined
69 const {profile, record} = res
70
71 if (record && bsky.validate(record, AppBskyFeedPost.validateRecord)) {
72 return {
73 type: 'post',
74 view: {
75 $type: 'app.bsky.embed.record#viewRecord',
76 uri,
77 author: profile as ProfileViewBasic,
78 cid: 'directfetch',
79 value: record,
80 indexedAt: new Date().toISOString(),
81 } satisfies AppBskyEmbedRecord.ViewRecord,
82 }
83 } else {
84 return undefined
85 }
86}
87
88export function useDirectFetchEmbedRecord({
89 uri,
90 enabled,
91}: {
92 uri: string
93 enabled?: boolean
94}) {
95 const agent = useAgent()
96 return useQuery<EmbedType<'post'> | undefined>({
97 staleTime: STALE.HOURS.ONE,
98 queryKey: RQKEY(uri || ''),
99 async queryFn() {
100 return directFetchEmbedRecord(agent, uri)
101 },
102 enabled: enabled && !!uri,
103 })
104}
105
106export async function directFetchPostRecord(
107 agent: BskyAgent,
108 uri: string,
109): Promise<AppBskyFeedDefs.PostView | undefined> {
110 const res = await directFetchRecordAndProfile(agent, uri)
111 if (res === undefined) return undefined
112 const {profile, record} = res
113
114 if (record && bsky.validate(record, AppBskyFeedPost.validateRecord)) {
115 return {
116 $type: 'app.bsky.feed.defs#postView',
117 uri,
118 author: profile as ProfileViewBasic,
119 cid: 'directfetch',
120 record,
121 indexedAt: new Date().toISOString(),
122 } satisfies AppBskyFeedDefs.PostView
123 } else {
124 return undefined
125 }
126}
127
128// based on https://stackoverflow.com/a/46432113
129export class LRU<K, V> {
130 max: number
131 private cache: Map<K, Promise<V>>
132 constructor(max = 1_024) {
133 this.max = max
134 this.cache = new Map()
135 }
136
137 get(key: K) {
138 let item = this.cache.get(key)
139 if (item !== undefined) {
140 // refresh key
141 this.cache.delete(key)
142 this.cache.set(key, item)
143 }
144 return item
145 }
146
147 set(key: K, val: Promise<V>) {
148 // refresh key
149 if (this.cache.has(key)) this.cache.delete(key)
150 // evict oldest
151 else if (this.cache.size >= this.max)
152 this.cache.delete(this.nonemptyFirst())
153 this.cache.set(key, val)
154 }
155
156 delete(key: K) {
157 return this.cache.delete(key)
158 }
159
160 private nonemptyFirst() {
161 return this.cache.keys().next().value!
162 }
163
164 async getOrInsertWith(key: K, fn: () => Promise<V>): Promise<V> {
165 const val = this.get(key)
166 if (val !== undefined) return val
167
168 const promise = fn()
169 this.set(key, promise)
170 return promise
171 }
172
173 // try to insert, but remove from cache on error and bubble
174 async getOrTryInsertWith(key: K, fn: () => Promise<V>): Promise<V> {
175 const val = this.get(key)
176 if (val !== undefined) return val
177
178 const promise = fn()
179 this.set(key, promise)
180 try {
181 return await promise
182 } catch (e) {
183 this.delete(key)
184 throw e
185 }
186 }
187}