perf: streamline view data loading
This commit is contained in:
@@ -153,7 +153,113 @@ export function isFabDrag(deltaX: number, deltaY: number, threshold = 8) {
|
||||
}
|
||||
|
||||
let habitGridCache: { week: string; habits: unknown[] } | null = null
|
||||
let countdownCache: { items: unknown[]; archived: unknown[] } | null = null
|
||||
let countdownCache: { items: unknown[]; archived: unknown[]; writtenAt: number } | null = null
|
||||
let countdownInFlight: Promise<{ items: unknown[]; archived: unknown[] }> | null = null
|
||||
let countdownCacheGeneration = 0
|
||||
const requestGenerations = new Map<string, number>()
|
||||
|
||||
export function beginLatestRequest(key: string) {
|
||||
const generation = (requestGenerations.get(key) ?? 0) + 1
|
||||
requestGenerations.set(key, generation)
|
||||
return generation
|
||||
}
|
||||
|
||||
export function isLatestRequest(key: string, generation: number) {
|
||||
return requestGenerations.get(key) === generation
|
||||
}
|
||||
|
||||
export type RequestContext = { key: string; generation: number }
|
||||
|
||||
export function captureRequestContext(key: string): RequestContext {
|
||||
return { key, generation: requestGenerations.get(key) ?? 0 }
|
||||
}
|
||||
|
||||
export function commitIfRequestContextCurrent(context: RequestContext, commit: () => void) {
|
||||
if (!isLatestRequest(context.key, context.generation)) return false
|
||||
commit()
|
||||
return true
|
||||
}
|
||||
|
||||
export async function runLatestRequest<T>(
|
||||
key: string,
|
||||
request: () => Promise<T>,
|
||||
callbacks: {
|
||||
success: (value: T) => void
|
||||
error: (reason: unknown) => void
|
||||
finally: () => void
|
||||
},
|
||||
) {
|
||||
const generation = beginLatestRequest(key)
|
||||
try {
|
||||
const value = await request()
|
||||
if (isLatestRequest(key, generation)) callbacks.success(value)
|
||||
} catch (reason) {
|
||||
if (isLatestRequest(key, generation)) callbacks.error(reason)
|
||||
} finally {
|
||||
if (isLatestRequest(key, generation)) callbacks.finally()
|
||||
}
|
||||
}
|
||||
|
||||
export type MutationReconciler<TContext> = {
|
||||
run<T>(
|
||||
mutation: () => Promise<T>,
|
||||
onSuccess: (value: T) => void,
|
||||
onError?: (reason: unknown) => void,
|
||||
onCurrentSuccess?: (value: T) => void,
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
export function createMutationReconciler<TContext>(
|
||||
currentContext: () => TContext,
|
||||
sameContext: (left: TContext, right: TContext) => boolean,
|
||||
refresh: () => Promise<unknown>,
|
||||
): MutationReconciler<TContext> {
|
||||
let dirty = 0
|
||||
let reconciled = 0
|
||||
let refreshLoop: Promise<void> | null = null
|
||||
|
||||
const reconcile = (context: TContext) => {
|
||||
if (!sameContext(context, currentContext())) return Promise.resolve()
|
||||
dirty += 1
|
||||
if (!refreshLoop) {
|
||||
refreshLoop = (async () => {
|
||||
while (reconciled < dirty && sameContext(context, currentContext())) {
|
||||
const target = dirty
|
||||
await refresh()
|
||||
if (!sameContext(context, currentContext())) break
|
||||
reconciled = target
|
||||
}
|
||||
})().finally(() => { refreshLoop = null })
|
||||
}
|
||||
return refreshLoop
|
||||
}
|
||||
|
||||
return {
|
||||
async run(mutation, onSuccess, onError, onCurrentSuccess) {
|
||||
const context = currentContext()
|
||||
let value: Awaited<ReturnType<typeof mutation>>
|
||||
try {
|
||||
value = await mutation()
|
||||
} catch (reason) {
|
||||
onError?.(reason)
|
||||
return
|
||||
}
|
||||
onSuccess(value)
|
||||
if (sameContext(context, currentContext())) onCurrentSuccess?.(value)
|
||||
await reconcile(context)
|
||||
if (sameContext(context, currentContext()) && reconciled < dirty) await reconcile(context)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function startPrimaryWithBackground<T>(
|
||||
primary: Array<() => Promise<T>>,
|
||||
background: () => Promise<unknown>,
|
||||
) {
|
||||
const pending = primary.map((start) => start())
|
||||
void background().catch(() => undefined)
|
||||
return Promise.all(pending)
|
||||
}
|
||||
|
||||
export function readHabitGridCache<T>(week: string): T[] | null {
|
||||
return habitGridCache?.week === week ? habitGridCache.habits as T[] : null
|
||||
@@ -164,11 +270,47 @@ export function writeHabitGridCache<T>(week: string, habits: T[]) {
|
||||
}
|
||||
|
||||
export function readCountdownCache<T>() {
|
||||
return countdownCache as { items: T[]; archived: T[] } | null
|
||||
if (!countdownCache) return null
|
||||
return { items: countdownCache.items as T[], archived: countdownCache.archived as T[] }
|
||||
}
|
||||
|
||||
export function writeCountdownCache<T>(items: T[], archived: T[]) {
|
||||
countdownCache = { items, archived }
|
||||
export function writeCountdownCache<T>(items: T[], archived: T[], now = Date.now()) {
|
||||
countdownCache = { items, archived, writtenAt: now }
|
||||
}
|
||||
|
||||
export function invalidateCountdownCache() {
|
||||
countdownCacheGeneration += 1
|
||||
countdownCache = null
|
||||
countdownInFlight = null
|
||||
}
|
||||
|
||||
export function getCountdownCacheGeneration() {
|
||||
return countdownCacheGeneration
|
||||
}
|
||||
|
||||
export function isCountdownCacheGenerationCurrent(generation: number) {
|
||||
return generation === countdownCacheGeneration
|
||||
}
|
||||
|
||||
export function loadCountdownCache<T>(
|
||||
fetcher: () => Promise<{ items: T[]; archived: T[] }>,
|
||||
options: { now?: number; maxAge?: number; force?: boolean } = {},
|
||||
) {
|
||||
const now = options.now ?? Date.now()
|
||||
const maxAge = options.maxAge ?? 30_000
|
||||
if (!options.force && countdownCache && now - countdownCache.writtenAt < maxAge) {
|
||||
return Promise.resolve(readCountdownCache<T>()!)
|
||||
}
|
||||
if (countdownInFlight) return countdownInFlight as Promise<{ items: T[]; archived: T[] }>
|
||||
const generation = countdownCacheGeneration
|
||||
const pending = fetcher().then((data) => {
|
||||
if (generation === countdownCacheGeneration) writeCountdownCache(data.items, data.archived, now)
|
||||
return data
|
||||
}).finally(() => {
|
||||
if (countdownInFlight === pending) countdownInFlight = null
|
||||
})
|
||||
countdownInFlight = pending as Promise<{ items: unknown[]; archived: unknown[] }>
|
||||
return pending
|
||||
}
|
||||
|
||||
export type HabitFormValues = {
|
||||
|
||||
Reference in New Issue
Block a user