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:
parent
f18b9b32b0
commit
4c4ba553bd
27 changed files with 115 additions and 203 deletions
66
src/state/cache/post-shadow.ts
vendored
66
src/state/cache/post-shadow.ts
vendored
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
70
src/state/cache/profile-shadow.ts
vendored
70
src/state/cache/profile-shadow.ts
vendored
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
8
src/state/cache/types.ts
vendored
8
src/state/cache/types.ts
vendored
|
@ -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>
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue