about summary refs log tree commit diff
path: root/src/state/cache
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/cache')
-rw-r--r--src/state/cache/post-shadow.ts66
-rw-r--r--src/state/cache/profile-shadow.ts70
-rw-r--r--src/state/cache/types.ts8
3 files changed, 82 insertions, 62 deletions
diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts
index d20f6ebaa..b21bb7129 100644
--- a/src/state/cache/post-shadow.ts
+++ b/src/state/cache/post-shadow.ts
@@ -1,7 +1,8 @@
-import {useEffect, useState, useMemo, useCallback, useRef} from 'react'
+import {useEffect, useState, useMemo, useCallback} from 'react'
 import EventEmitter from 'eventemitter3'
 import {AppBskyFeedDefs} from '@atproto/api'
-import {Shadow} from './types'
+import {batchedUpdates} from '#/lib/batchedUpdates'
+import {Shadow, castAsShadow} from './types'
 export type {Shadow} from './types'
 
 const emitter = new EventEmitter()
@@ -21,15 +22,36 @@ interface CacheEntry {
   value: PostShadow
 }
 
+const firstSeenMap = new WeakMap<AppBskyFeedDefs.PostView, number>()
+function getFirstSeenTS(post: AppBskyFeedDefs.PostView): number {
+  let timeStamp = firstSeenMap.get(post)
+  if (timeStamp !== undefined) {
+    return timeStamp
+  }
+  timeStamp = Date.now()
+  firstSeenMap.set(post, timeStamp)
+  return timeStamp
+}
+
 export function usePostShadow(
   post: AppBskyFeedDefs.PostView,
-  ifAfterTS: number,
 ): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE {
-  const [state, setState] = useState<CacheEntry>({
-    ts: Date.now(),
+  const postSeenTS = getFirstSeenTS(post)
+  const [state, setState] = useState<CacheEntry>(() => ({
+    ts: postSeenTS,
     value: fromPost(post),
-  })
-  const firstRun = useRef(true)
+  }))
+
+  const [prevPost, setPrevPost] = useState(post)
+  if (post !== prevPost) {
+    // if we got a new prop, assume it's fresher
+    // than whatever shadow state we accumulated
+    setPrevPost(post)
+    setState({
+      ts: postSeenTS,
+      value: fromPost(post),
+    })
+  }
 
   const onUpdate = useCallback(
     (value: Partial<PostShadow>) => {
@@ -46,30 +68,17 @@ export function usePostShadow(
     }
   }, [post.uri, onUpdate])
 
-  // react to post updates
-  useEffect(() => {
-    // dont fire on first run to avoid needless re-renders
-    if (!firstRun.current) {
-      setState({ts: Date.now(), value: fromPost(post)})
-    }
-    firstRun.current = false
-  }, [post])
-
   return useMemo(() => {
-    return state.ts > ifAfterTS
+    return state.ts > postSeenTS
       ? mergeShadow(post, state.value)
-      : {...post, isShadowed: true}
-  }, [post, state, ifAfterTS])
+      : castAsShadow(post)
+  }, [post, state, postSeenTS])
 }
 
 export function updatePostShadow(uri: string, value: Partial<PostShadow>) {
-  emitter.emit(uri, value)
-}
-
-export function isPostShadowed(
-  v: AppBskyFeedDefs.PostView | Shadow<AppBskyFeedDefs.PostView>,
-): v is Shadow<AppBskyFeedDefs.PostView> {
-  return 'isShadowed' in v && !!v.isShadowed
+  batchedUpdates(() => {
+    emitter.emit(uri, value)
+  })
 }
 
 function fromPost(post: AppBskyFeedDefs.PostView): PostShadow {
@@ -89,7 +98,7 @@ function mergeShadow(
   if (shadow.isDeleted) {
     return POST_TOMBSTONE
   }
-  return {
+  return castAsShadow({
     ...post,
     likeCount: shadow.likeCount,
     repostCount: shadow.repostCount,
@@ -98,6 +107,5 @@ function mergeShadow(
       like: shadow.likeUri,
       repost: shadow.repostUri,
     },
-    isShadowed: true,
-  }
+  })
 }
diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts
index 5323effaf..6ebd39132 100644
--- a/src/state/cache/profile-shadow.ts
+++ b/src/state/cache/profile-shadow.ts
@@ -1,7 +1,8 @@
-import {useEffect, useState, useMemo, useCallback, useRef} from 'react'
+import {useEffect, useState, useMemo, useCallback} from 'react'
 import EventEmitter from 'eventemitter3'
 import {AppBskyActorDefs} from '@atproto/api'
-import {Shadow} from './types'
+import {batchedUpdates} from '#/lib/batchedUpdates'
+import {Shadow, castAsShadow} from './types'
 export type {Shadow} from './types'
 
 const emitter = new EventEmitter()
@@ -22,15 +23,34 @@ type ProfileView =
   | AppBskyActorDefs.ProfileViewBasic
   | AppBskyActorDefs.ProfileViewDetailed
 
-export function useProfileShadow(
-  profile: ProfileView,
-  ifAfterTS: number,
-): Shadow<ProfileView> {
-  const [state, setState] = useState<CacheEntry>({
-    ts: Date.now(),
+const firstSeenMap = new WeakMap<ProfileView, number>()
+function getFirstSeenTS(profile: ProfileView): number {
+  let timeStamp = firstSeenMap.get(profile)
+  if (timeStamp !== undefined) {
+    return timeStamp
+  }
+  timeStamp = Date.now()
+  firstSeenMap.set(profile, timeStamp)
+  return timeStamp
+}
+
+export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> {
+  const profileSeenTS = getFirstSeenTS(profile)
+  const [state, setState] = useState<CacheEntry>(() => ({
+    ts: profileSeenTS,
     value: fromProfile(profile),
-  })
-  const firstRun = useRef(true)
+  }))
+
+  const [prevProfile, setPrevProfile] = useState(profile)
+  if (profile !== prevProfile) {
+    // if we got a new prop, assume it's fresher
+    // than whatever shadow state we accumulated
+    setPrevProfile(profile)
+    setState({
+      ts: profileSeenTS,
+      value: fromProfile(profile),
+    })
+  }
 
   const onUpdate = useCallback(
     (value: Partial<ProfileShadow>) => {
@@ -47,33 +67,20 @@ export function useProfileShadow(
     }
   }, [profile.did, onUpdate])
 
-  // react to profile updates
-  useEffect(() => {
-    // dont fire on first run to avoid needless re-renders
-    if (!firstRun.current) {
-      setState({ts: Date.now(), value: fromProfile(profile)})
-    }
-    firstRun.current = false
-  }, [profile])
-
   return useMemo(() => {
-    return state.ts > ifAfterTS
+    return state.ts > profileSeenTS
       ? mergeShadow(profile, state.value)
-      : {...profile, isShadowed: true}
-  }, [profile, state, ifAfterTS])
+      : castAsShadow(profile)
+  }, [profile, state, profileSeenTS])
 }
 
 export function updateProfileShadow(
   uri: string,
   value: Partial<ProfileShadow>,
 ) {
-  emitter.emit(uri, value)
-}
-
-export function isProfileShadowed<T extends ProfileView>(
-  v: T | Shadow<T>,
-): v is Shadow<T> {
-  return 'isShadowed' in v && !!v.isShadowed
+  batchedUpdates(() => {
+    emitter.emit(uri, value)
+  })
 }
 
 function fromProfile(profile: ProfileView): ProfileShadow {
@@ -88,7 +95,7 @@ function mergeShadow(
   profile: ProfileView,
   shadow: ProfileShadow,
 ): Shadow<ProfileView> {
-  return {
+  return castAsShadow({
     ...profile,
     viewer: {
       ...(profile.viewer || {}),
@@ -96,6 +103,5 @@ function mergeShadow(
       muted: shadow.muted,
       blocking: shadow.blockingUri,
     },
-    isShadowed: true,
-  }
+  })
 }
diff --git a/src/state/cache/types.ts b/src/state/cache/types.ts
index 8bfcc867c..055f4167e 100644
--- a/src/state/cache/types.ts
+++ b/src/state/cache/types.ts
@@ -1 +1,7 @@
-export type Shadow<T> = T & {isShadowed: true}
+// This isn't a real property, but it prevents T being compatible with Shadow<T>.
+declare const shadowTag: unique symbol
+export type Shadow<T> = T & {[shadowTag]: true}
+
+export function castAsShadow<T>(value: T): Shadow<T> {
+  return value as any as Shadow<T>
+}