* Introduce an image sizes cache to improve feed layouts (close #213) * Clear out resolved promises from the image cache
This commit is contained in:
parent
c1d454b7cf
commit
858d4c8c88
7 changed files with 92 additions and 30 deletions
37
src/state/models/cache/image-sizes.ts
vendored
Normal file
37
src/state/models/cache/image-sizes.ts
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
44
src/state/models/cache/link-metas.ts
vendored
Normal file
44
src/state/models/cache/link-metas.ts
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
126
src/state/models/cache/my-follows.ts
vendored
Normal file
126
src/state/models/cache/my-follows.ts
vendored
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue