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 type * as AgeRange from 'expo-age-range'
|
||||||
import {useLingui} from '@lingui/react/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {withCleanup} from '#/lib/async/withCleanup'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {type DialogControlProps} from '#/components/Dialog'
|
import {type DialogControlProps} from '#/components/Dialog'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
@@ -94,11 +95,14 @@ export function useAgeAssuranceVerificationFlow({
|
|||||||
// Show a loading state while the OS age prompt is up.
|
// Show a loading state while the OS age prompt is up.
|
||||||
setIsVerifying(true)
|
setIsVerifying(true)
|
||||||
let signals: AgeRange.AgeRangeResponse | undefined
|
let signals: AgeRange.AgeRangeResponse | undefined
|
||||||
try {
|
await withCleanup(
|
||||||
|
async () => {
|
||||||
signals = await getDeviceSignals()
|
signals = await getDeviceSignals()
|
||||||
} finally {
|
},
|
||||||
|
() => {
|
||||||
setIsVerifying(false)
|
setIsVerifying(false)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
if (signals) {
|
if (signals) {
|
||||||
const {assuredAge} = getAgeAssuranceDataFromDeviceSignals(
|
const {assuredAge} = getAgeAssuranceDataFromDeviceSignals(
|
||||||
region,
|
region,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {useState} from 'react'
|
|||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {wait} from '#/lib/async/wait'
|
import {wait} from '#/lib/async/wait'
|
||||||
|
import {withCleanup} from '#/lib/async/withCleanup'
|
||||||
import {atoms as a, type TextStyleProp, useTheme} from '#/alf'
|
import {atoms as a, type TextStyleProp, useTheme} from '#/alf'
|
||||||
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
||||||
@@ -20,14 +21,17 @@ export function ResendEmailText({
|
|||||||
|
|
||||||
const handleOnPress = async () => {
|
const handleOnPress = async () => {
|
||||||
setStatus('sending')
|
setStatus('sending')
|
||||||
try {
|
await withCleanup(
|
||||||
|
async () => {
|
||||||
await wait(1000, onPress())
|
await wait(1000, onPress())
|
||||||
setStatus('success')
|
setStatus('success')
|
||||||
} finally {
|
},
|
||||||
|
() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setStatus(null)
|
setStatus(null)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 {useCallback, useEffect, useRef, useState} from 'react'
|
||||||
|
|
||||||
|
import {withCleanup} from '#/lib/async/withCleanup'
|
||||||
|
|
||||||
type Task<TServerState> = {
|
type Task<TServerState> = {
|
||||||
isOn: boolean
|
isOn: boolean
|
||||||
resolve: (serverState: TServerState) => void
|
resolve: (serverState: TServerState) => void
|
||||||
@@ -45,7 +47,8 @@ export function useToggleMutationQueue<TServerState>({
|
|||||||
// To avoid relying on the rendered state, capture it once at the start.
|
// 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.
|
// From that point on, and until the queue is drained, we'll use the real server state.
|
||||||
let confirmedState: TServerState = initialState
|
let confirmedState: TServerState = initialState
|
||||||
try {
|
await withCleanup(
|
||||||
|
async () => {
|
||||||
while (queue.queuedTask) {
|
while (queue.queuedTask) {
|
||||||
const prevTask = queue.activeTask
|
const prevTask = queue.activeTask
|
||||||
const nextTask = queue.queuedTask
|
const nextTask = queue.queuedTask
|
||||||
@@ -65,11 +68,13 @@ export function useToggleMutationQueue<TServerState>({
|
|||||||
nextTask.reject(e)
|
nextTask.reject(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
},
|
||||||
|
() => {
|
||||||
onSuccess(confirmedState)
|
onSuccess(confirmedState)
|
||||||
queue.activeTask = null
|
queue.activeTask = null
|
||||||
queue.queuedTask = null
|
queue.queuedTask = null
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueToggle(isOn: boolean): Promise<TServerState> {
|
function queueToggle(isOn: boolean): Promise<TServerState> {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useNavigation,
|
useNavigation,
|
||||||
} from '@react-navigation/native'
|
} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {withCleanup} from '#/lib/async/withCleanup'
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||||
import {
|
import {
|
||||||
@@ -104,11 +105,14 @@ function BookmarksInner() {
|
|||||||
|
|
||||||
const onRefresh = useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setIsPTRing(true)
|
setIsPTRing(true)
|
||||||
try {
|
await withCleanup(
|
||||||
|
async () => {
|
||||||
await refetch()
|
await refetch()
|
||||||
} finally {
|
},
|
||||||
|
() => {
|
||||||
setIsPTRing(false)
|
setIsPTRing(false)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}, [refetch, setIsPTRing])
|
}, [refetch, setIsPTRing])
|
||||||
|
|
||||||
const onEndReached = useCallback(async () => {
|
const onEndReached = useCallback(async () => {
|
||||||
|
|||||||
@@ -154,9 +154,8 @@ function Inner({
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} finally {
|
|
||||||
setIsSaving(false)
|
|
||||||
}
|
}
|
||||||
|
setIsSaving(false)
|
||||||
}, 1500)
|
}, 1500)
|
||||||
}, [_, pdsClient, setIsSaving, qc, preselectedInterests])
|
}, [_, pdsClient, setIsSaving, qc, preselectedInterests])
|
||||||
|
|
||||||
|
|||||||
@@ -106,17 +106,16 @@ function DeleteAccountDialogInner({
|
|||||||
logger.error(raw || e, {
|
logger.error(raw || e, {
|
||||||
message: 'Failed to send account deletion verification email',
|
message: 'Failed to send account deletion verification email',
|
||||||
})
|
})
|
||||||
} finally {
|
|
||||||
setEmailState(EmailState.DEFAULT)
|
|
||||||
}
|
}
|
||||||
|
setEmailState(EmailState.DEFAULT)
|
||||||
}, [client, cleanError, emailState, setEmailState])
|
}, [client, cleanError, emailState, setEmailState])
|
||||||
|
|
||||||
const confirmDeletion = useCallback(async () => {
|
const confirmDeletion = useCallback(async () => {
|
||||||
try {
|
|
||||||
setError('')
|
|
||||||
if (!currentAccount?.did) {
|
if (!currentAccount?.did) {
|
||||||
throw new Error('Invalid did')
|
throw new Error('Invalid did')
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
setError('')
|
||||||
const token = confirmCode.replace(WHITESPACE_RE, '')
|
const token = confirmCode.replace(WHITESPACE_RE, '')
|
||||||
/*
|
/*
|
||||||
* Inform chat service of intent to delete account. A non-2xx response
|
* 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 {useQueryClient} from '@tanstack/react-query'
|
||||||
import {EventEmitter} from 'eventemitter3'
|
import {EventEmitter} from 'eventemitter3'
|
||||||
|
|
||||||
|
import {withCleanup} from '#/lib/async/withCleanup'
|
||||||
import BroadcastChannel from '#/lib/broadcast'
|
import BroadcastChannel from '#/lib/broadcast'
|
||||||
import {resetBadgeCount} from '#/lib/notifications/notifications'
|
import {resetBadgeCount} from '#/lib/notifications/notifications'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
@@ -137,7 +138,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
invalidate,
|
invalidate,
|
||||||
isPoll,
|
isPoll,
|
||||||
}: {invalidate?: boolean; isPoll?: boolean} = {}) {
|
}: {invalidate?: boolean; isPoll?: boolean} = {}) {
|
||||||
try {
|
await withCleanup(
|
||||||
|
async () => {
|
||||||
if (!hasSession) return
|
if (!hasSession) return
|
||||||
if (AppState.currentState !== 'active') {
|
if (AppState.currentState !== 'active') {
|
||||||
return
|
return
|
||||||
@@ -187,7 +189,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
usableInFeed: !!invalidate, // will be used immediately
|
usableInFeed: !!invalidate, // will be used immediately
|
||||||
data: page,
|
data: page,
|
||||||
syncedAt:
|
syncedAt:
|
||||||
!lastIndexedDate || now > lastIndexedDate ? now : lastIndexedDate,
|
!lastIndexedDate || now > lastIndexedDate
|
||||||
|
? now
|
||||||
|
: lastIndexedDate,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,9 +202,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions'))
|
||||||
}
|
}
|
||||||
broadcast.postMessage({event: unreadCountStr})
|
broadcast.postMessage({event: unreadCountStr})
|
||||||
} finally {
|
},
|
||||||
|
() => {
|
||||||
isFetchingRef.current = false
|
isFetchingRef.current = false
|
||||||
}
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
getCachedUnreadPage() {
|
getCachedUnreadPage() {
|
||||||
|
|||||||
Reference in New Issue
Block a user