about summary refs log tree commit diff
path: root/src/state/models/cache
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models/cache')
-rw-r--r--src/state/models/cache/image-sizes.ts37
-rw-r--r--src/state/models/cache/link-metas.ts44
-rw-r--r--src/state/models/cache/my-follows.ts126
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)
+      }
+    }
+  }
+}