Shadow refactoring and improvements (#1959)

* Make shadow a type-only concept

* Prevent unnecessary init state recalc

* Use derived state instead of effects

* Batch emitter updates

* Use object first seen time instead of dataUpdatedAt

* Stop threading dataUpdatedAt through

* Use same value consistently
This commit is contained in:
dan 2023-11-21 22:42:30 +00:00 committed by GitHub
parent f18b9b32b0
commit 4c4ba553bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 115 additions and 203 deletions

View file

@ -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,
}
})
}

View file

@ -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,
}
})
}

View file

@ -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>
}