diff options
Diffstat (limited to 'src/state/models/cache')
-rw-r--r-- | src/state/models/cache/image-sizes.ts | 37 | ||||
-rw-r--r-- | src/state/models/cache/link-metas.ts | 44 | ||||
-rw-r--r-- | src/state/models/cache/my-follows.ts | 126 |
3 files changed, 207 insertions, 0 deletions
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts new file mode 100644 index 000000000..ff0486278 --- /dev/null +++ b/src/state/models/cache/image-sizes.ts @@ -0,0 +1,37 @@ +import {Image} from 'react-native' +import {Dim} from 'lib/media/manip' + +export class ImageSizesCache { + sizes: Map<string, Dim> = new Map() + private activeRequests: Map<string, Promise<Dim>> = new Map() + + constructor() {} + + get(uri: string): Dim | undefined { + return this.sizes.get(uri) + } + + async fetch(uri: string): Promise<Dim> { + const dim = this.sizes.get(uri) + if (dim) { + return dim + } + const prom = + this.activeRequests.get(uri) || + new Promise<Dim>(resolve => { + Image.getSize( + uri, + (width: number, height: number) => resolve({width, height}), + (err: any) => { + console.error('Failed to fetch image dimensions for', uri, err) + resolve({width: 0, height: 0}) + }, + ) + }) + this.activeRequests.set(uri, prom) + const res = await prom + this.activeRequests.delete(uri) + this.sizes.set(uri, res) + return res + } +} diff --git a/src/state/models/cache/link-metas.ts b/src/state/models/cache/link-metas.ts new file mode 100644 index 000000000..607968c80 --- /dev/null +++ b/src/state/models/cache/link-metas.ts @@ -0,0 +1,44 @@ +import {makeAutoObservable} from 'mobx' +import {LRUMap} from 'lru_map' +import {RootStoreModel} from '../root-store' +import {LinkMeta, getLinkMeta} from 'lib/link-meta/link-meta' + +type CacheValue = Promise<LinkMeta> | LinkMeta +export class LinkMetasCache { + cache: LRUMap<string, CacheValue> = new LRUMap(100) + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + cache: false, + }, + {autoBind: true}, + ) + } + + // public api + // = + + async getLinkMeta(url: string) { + const cached = this.cache.get(url) + if (cached) { + try { + return await cached + } catch (e) { + // ignore, we'll try again + } + } + try { + const promise = getLinkMeta(this.rootStore, url) + this.cache.set(url, promise) + const res = await promise + this.cache.set(url, res) + return res + } catch (e) { + this.cache.delete(url) + throw e + } + } +} diff --git a/src/state/models/cache/my-follows.ts b/src/state/models/cache/my-follows.ts new file mode 100644 index 000000000..725b7841e --- /dev/null +++ b/src/state/models/cache/my-follows.ts @@ -0,0 +1,126 @@ +import {makeAutoObservable, runInAction} from 'mobx' +import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api' +import {RootStoreModel} from '../root-store' +import {bundleAsync} from 'lib/async/bundle' + +const CACHE_TTL = 1000 * 60 * 60 // hourly +type FollowsListResponse = Awaited<ReturnType<FollowRecord['list']>> +type FollowsListResponseRecord = FollowsListResponse['records'][0] +type Profile = + | AppBskyActorProfile.ViewBasic + | AppBskyActorProfile.View + | AppBskyActorRef.WithInfo + +/** + * This model is used to maintain a synced local cache of the user's + * follows. It should be periodically refreshed and updated any time + * the user makes a change to their follows. + */ +export class MyFollowsCache { + // data + followDidToRecordMap: Record<string, string> = {} + lastSync = 0 + myDid?: string + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + // public api + // = + + clear() { + this.followDidToRecordMap = {} + this.lastSync = 0 + this.myDid = undefined + } + + fetchIfNeeded = bundleAsync(async () => { + if ( + this.myDid !== this.rootStore.me.did || + Object.keys(this.followDidToRecordMap).length === 0 || + Date.now() - this.lastSync > CACHE_TTL + ) { + return await this.fetch() + } + }) + + fetch = bundleAsync(async () => { + this.rootStore.log.debug('MyFollowsModel:fetch running full fetch') + let before + let records: FollowsListResponseRecord[] = [] + do { + const res: FollowsListResponse = + await this.rootStore.api.app.bsky.graph.follow.list({ + user: this.rootStore.me.did, + before, + }) + records = records.concat(res.records) + before = res.cursor + } while (typeof before !== 'undefined') + runInAction(() => { + this.followDidToRecordMap = {} + for (const record of records) { + this.followDidToRecordMap[record.value.subject.did] = record.uri + } + this.lastSync = Date.now() + this.myDid = this.rootStore.me.did + }) + }) + + isFollowing(did: string) { + return !!this.followDidToRecordMap[did] + } + + get numFollows() { + return Object.keys(this.followDidToRecordMap).length + } + + get isEmpty() { + return Object.keys(this.followDidToRecordMap).length === 0 + } + + getFollowUri(did: string): string { + const v = this.followDidToRecordMap[did] + if (!v) { + throw new Error('Not a followed user') + } + return v + } + + addFollow(did: string, recordUri: string) { + this.followDidToRecordMap[did] = recordUri + } + + removeFollow(did: string) { + delete this.followDidToRecordMap[did] + } + + /** + * Use this to incrementally update the cache as views provide information + */ + hydrate(did: string, recordUri: string | undefined) { + if (recordUri) { + this.followDidToRecordMap[did] = recordUri + } else { + delete this.followDidToRecordMap[did] + } + } + + /** + * Use this to incrementally update the cache as views provide information + */ + hydrateProfiles(profiles: Profile[]) { + for (const profile of profiles) { + if (profile.viewer) { + this.hydrate(profile.did, profile.viewer.following) + } + } + } +} |