Unblock React Compiler for 6 components with try/finally that cannot hoist
The earlier pass moved `finally` bodies below the try/catch, which is only valid when a `catch` completes normally. These are the leftovers. Where the catch does complete normally but a nested callback happened to contain a `return`, the same hoist applies after all - InterestsSettings and DeleteAccountDialog were false positives in the first pass's scan. Where there is no `catch`, the cleanup has to survive the throw path, so the try/finally moves into `withCleanup` at module scope instead. Same semantics, outside the compiled function. Skipped components: 125 -> 119. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import {useCallback, useState} from 'react'
|
||||
import type * as AgeRange from 'expo-age-range'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {withCleanup} from '#/lib/async/withCleanup'
|
||||
import {useSession} from '#/state/session'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import * as Toast from '#/components/Toast'
|
||||
@@ -94,11 +95,14 @@ export function useAgeAssuranceVerificationFlow({
|
||||
// Show a loading state while the OS age prompt is up.
|
||||
setIsVerifying(true)
|
||||
let signals: AgeRange.AgeRangeResponse | undefined
|
||||
try {
|
||||
signals = await getDeviceSignals()
|
||||
} finally {
|
||||
setIsVerifying(false)
|
||||
}
|
||||
await withCleanup(
|
||||
async () => {
|
||||
signals = await getDeviceSignals()
|
||||
},
|
||||
() => {
|
||||
setIsVerifying(false)
|
||||
},
|
||||
)
|
||||
if (signals) {
|
||||
const {assuredAge} = getAgeAssuranceDataFromDeviceSignals(
|
||||
region,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {useState} from 'react'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {withCleanup} from '#/lib/async/withCleanup'
|
||||
import {atoms as a, type TextStyleProp, useTheme} from '#/alf'
|
||||
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
||||
@@ -20,14 +21,17 @@ export function ResendEmailText({
|
||||
|
||||
const handleOnPress = async () => {
|
||||
setStatus('sending')
|
||||
try {
|
||||
await wait(1000, onPress())
|
||||
setStatus('success')
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setStatus(null)
|
||||
}, 1000)
|
||||
}
|
||||
await withCleanup(
|
||||
async () => {
|
||||
await wait(1000, onPress())
|
||||
setStatus('success')
|
||||
},
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
setStatus(null)
|
||||
}, 1000)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Runs `fn`, then `cleanup`, whether or not `fn` throws - i.e. exactly what a
|
||||
* `try`/`finally` does.
|
||||
*
|
||||
* It exists because React Compiler cannot lower a `finally` block, so any
|
||||
* component or hook containing one is skipped entirely. Where the `try` has a
|
||||
* `catch` that completes normally the cleanup can simply move below the
|
||||
* `try`/`catch`, and that is the preferred fix. This is for the cases where it
|
||||
* cannot: a `try`/`finally` with no `catch`, where the cleanup has to survive
|
||||
* the throw path too. Keeping the `try`/`finally` here, out of the compiled
|
||||
* function, preserves the semantics exactly.
|
||||
*
|
||||
* Note that `return` inside `fn` returns from `fn`, not from the caller. Only
|
||||
* use this where the `try` is the whole body of its function.
|
||||
*/
|
||||
export async function withCleanup<T>(
|
||||
fn: () => Promise<T>,
|
||||
cleanup: () => void,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
|
||||
import {withCleanup} from '#/lib/async/withCleanup'
|
||||
|
||||
type Task<TServerState> = {
|
||||
isOn: boolean
|
||||
resolve: (serverState: TServerState) => void
|
||||
@@ -45,31 +47,34 @@ export function useToggleMutationQueue<TServerState>({
|
||||
// To avoid relying on the rendered state, capture it once at the start.
|
||||
// From that point on, and until the queue is drained, we'll use the real server state.
|
||||
let confirmedState: TServerState = initialState
|
||||
try {
|
||||
while (queue.queuedTask) {
|
||||
const prevTask = queue.activeTask
|
||||
const nextTask = queue.queuedTask
|
||||
queue.activeTask = nextTask
|
||||
await withCleanup(
|
||||
async () => {
|
||||
while (queue.queuedTask) {
|
||||
const prevTask = queue.activeTask
|
||||
const nextTask = queue.queuedTask
|
||||
queue.activeTask = nextTask
|
||||
queue.queuedTask = null
|
||||
if (prevTask?.isOn === nextTask.isOn) {
|
||||
// Skip multiple requests to update to the same value in a row.
|
||||
prevTask.reject(new (AbortError as any)())
|
||||
continue
|
||||
}
|
||||
try {
|
||||
// The state received from the server feeds into the next task.
|
||||
// This lets us queue deletions of not-yet-created resources.
|
||||
confirmedState = await runMutation(confirmedState, nextTask.isOn)
|
||||
nextTask.resolve(confirmedState)
|
||||
} catch (e) {
|
||||
nextTask.reject(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
() => {
|
||||
onSuccess(confirmedState)
|
||||
queue.activeTask = null
|
||||
queue.queuedTask = null
|
||||
if (prevTask?.isOn === nextTask.isOn) {
|
||||
// Skip multiple requests to update to the same value in a row.
|
||||
prevTask.reject(new (AbortError as any)())
|
||||
continue
|
||||
}
|
||||
try {
|
||||
// The state received from the server feeds into the next task.
|
||||
// This lets us queue deletions of not-yet-created resources.
|
||||
confirmedState = await runMutation(confirmedState, nextTask.isOn)
|
||||
nextTask.resolve(confirmedState)
|
||||
} catch (e) {
|
||||
nextTask.reject(e)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
onSuccess(confirmedState)
|
||||
queue.activeTask = null
|
||||
queue.queuedTask = null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function queueToggle(isOn: boolean): Promise<TServerState> {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useNavigation,
|
||||
} from '@react-navigation/native'
|
||||
|
||||
import {withCleanup} from '#/lib/async/withCleanup'
|
||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||
import {
|
||||
@@ -104,11 +105,14 @@ function BookmarksInner() {
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} finally {
|
||||
setIsPTRing(false)
|
||||
}
|
||||
await withCleanup(
|
||||
async () => {
|
||||
await refetch()
|
||||
},
|
||||
() => {
|
||||
setIsPTRing(false)
|
||||
},
|
||||
)
|
||||
}, [refetch, setIsPTRing])
|
||||
|
||||
const onEndReached = useCallback(async () => {
|
||||
|
||||
@@ -154,9 +154,8 @@ function Inner({
|
||||
type: 'error',
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
setIsSaving(false)
|
||||
}, 1500)
|
||||
}, [_, pdsClient, setIsSaving, qc, preselectedInterests])
|
||||
|
||||
|
||||
@@ -106,17 +106,16 @@ function DeleteAccountDialogInner({
|
||||
logger.error(raw || e, {
|
||||
message: 'Failed to send account deletion verification email',
|
||||
})
|
||||
} finally {
|
||||
setEmailState(EmailState.DEFAULT)
|
||||
}
|
||||
setEmailState(EmailState.DEFAULT)
|
||||
}, [client, cleanError, emailState, setEmailState])
|
||||
|
||||
const confirmDeletion = useCallback(async () => {
|
||||
if (!currentAccount?.did) {
|
||||
throw new Error('Invalid did')
|
||||
}
|
||||
try {
|
||||
setError('')
|
||||
if (!currentAccount?.did) {
|
||||
throw new Error('Invalid did')
|
||||
}
|
||||
const token = confirmCode.replace(WHITESPACE_RE, '')
|
||||
/*
|
||||
* Inform chat service of intent to delete account. A non-2xx response
|
||||
|
||||
@@ -15,6 +15,7 @@ import {type ISODatetimeString} from '@atproto/syntax'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {EventEmitter} from 'eventemitter3'
|
||||
|
||||
import {withCleanup} from '#/lib/async/withCleanup'
|
||||
import BroadcastChannel from '#/lib/broadcast'
|
||||
import {resetBadgeCount} from '#/lib/notifications/notifications'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
@@ -137,70 +138,75 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
invalidate,
|
||||
isPoll,
|
||||
}: {invalidate?: boolean; isPoll?: boolean} = {}) {
|
||||
try {
|
||||
if (!hasSession) return
|
||||
if (AppState.currentState !== 'active') {
|
||||
return
|
||||
}
|
||||
|
||||
// reduce polling if unread count is set
|
||||
if (isPoll && cacheRef.current?.unreadCount !== 0) {
|
||||
// if hit 30+ then don't poll, otherwise reduce polling by 50%
|
||||
if (cacheRef.current?.unreadCount >= 30 || Math.random() >= 0.5) {
|
||||
await withCleanup(
|
||||
async () => {
|
||||
if (!hasSession) return
|
||||
if (AppState.currentState !== 'active') {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isFetchingRef.current) {
|
||||
return
|
||||
}
|
||||
// Do not move this without ensuring it gets a symmetrical reset in the finally block.
|
||||
isFetchingRef.current = true
|
||||
// reduce polling if unread count is set
|
||||
if (isPoll && cacheRef.current?.unreadCount !== 0) {
|
||||
// if hit 30+ then don't poll, otherwise reduce polling by 50%
|
||||
if (cacheRef.current?.unreadCount >= 30 || Math.random() >= 0.5) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// count
|
||||
const {page, indexedAt: lastIndexed} = await fetchPage({
|
||||
client,
|
||||
cursor: undefined,
|
||||
limit: 40,
|
||||
queryClient,
|
||||
moderationOpts,
|
||||
reasons: [],
|
||||
if (isFetchingRef.current) {
|
||||
return
|
||||
}
|
||||
// Do not move this without ensuring it gets a symmetrical reset in the finally block.
|
||||
isFetchingRef.current = true
|
||||
|
||||
// only fetch subjects when the page is going to be used
|
||||
// in the notifications query, otherwise skip it
|
||||
fetchAdditionalData: !!invalidate,
|
||||
})
|
||||
const unreadCount = countUnread(page)
|
||||
const unreadCountStr =
|
||||
unreadCount >= 30
|
||||
? '30+'
|
||||
: unreadCount === 0
|
||||
? ''
|
||||
: String(unreadCount)
|
||||
// count
|
||||
const {page, indexedAt: lastIndexed} = await fetchPage({
|
||||
client,
|
||||
cursor: undefined,
|
||||
limit: 40,
|
||||
queryClient,
|
||||
moderationOpts,
|
||||
reasons: [],
|
||||
|
||||
// track last sync
|
||||
const now = new Date()
|
||||
const lastIndexedDate = lastIndexed
|
||||
? new Date(lastIndexed)
|
||||
: undefined
|
||||
cacheRef.current = {
|
||||
usableInFeed: !!invalidate, // will be used immediately
|
||||
data: page,
|
||||
syncedAt:
|
||||
!lastIndexedDate || now > lastIndexedDate ? now : lastIndexedDate,
|
||||
unreadCount,
|
||||
}
|
||||
// only fetch subjects when the page is going to be used
|
||||
// in the notifications query, otherwise skip it
|
||||
fetchAdditionalData: !!invalidate,
|
||||
})
|
||||
const unreadCount = countUnread(page)
|
||||
const unreadCountStr =
|
||||
unreadCount >= 30
|
||||
? '30+'
|
||||
: unreadCount === 0
|
||||
? ''
|
||||
: String(unreadCount)
|
||||
|
||||
// update & broadcast
|
||||
setNumUnread(unreadCountStr)
|
||||
if (invalidate) {
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
}
|
||||
broadcast.postMessage({event: unreadCountStr})
|
||||
} finally {
|
||||
isFetchingRef.current = false
|
||||
}
|
||||
// track last sync
|
||||
const now = new Date()
|
||||
const lastIndexedDate = lastIndexed
|
||||
? new Date(lastIndexed)
|
||||
: undefined
|
||||
cacheRef.current = {
|
||||
usableInFeed: !!invalidate, // will be used immediately
|
||||
data: page,
|
||||
syncedAt:
|
||||
!lastIndexedDate || now > lastIndexedDate
|
||||
? now
|
||||
: lastIndexedDate,
|
||||
unreadCount,
|
||||
}
|
||||
|
||||
// update & broadcast
|
||||
setNumUnread(unreadCountStr)
|
||||
if (invalidate) {
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all'))
|
||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||
}
|
||||
broadcast.postMessage({event: unreadCountStr})
|
||||
},
|
||||
() => {
|
||||
isFetchingRef.current = false
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
getCachedUnreadPage() {
|
||||
|
||||
Reference in New Issue
Block a user