make until error types accurate
This commit is contained in:
@@ -102,7 +102,7 @@ export function CreateListFromStarterPackDialog({
|
|||||||
await until(
|
await until(
|
||||||
5,
|
5,
|
||||||
1e3,
|
1e3,
|
||||||
(res: {items: unknown[]}) => res.items.length > 0,
|
res => !!res?.items.length,
|
||||||
() =>
|
() =>
|
||||||
appviewClient.call(app.bsky.graph.getList, {
|
appviewClient.call(app.bsky.graph.getList, {
|
||||||
list: listUri as AtUriString,
|
list: listUri as AtUriString,
|
||||||
|
|||||||
@@ -3,27 +3,40 @@ import {describe, expect, it, jest} from '@jest/globals'
|
|||||||
import {until} from './until'
|
import {until} from './until'
|
||||||
|
|
||||||
describe('until', () => {
|
describe('until', () => {
|
||||||
it('does not invoke the condition when an attempt rejects', async () => {
|
it('passes attempt errors to the condition', async () => {
|
||||||
|
const error = new Error('failed')
|
||||||
const fn = jest
|
const fn = jest
|
||||||
.fn<() => Promise<string>>()
|
.fn<() => Promise<string>>()
|
||||||
.mockRejectedValueOnce(new Error('failed'))
|
.mockRejectedValueOnce(error)
|
||||||
.mockResolvedValue('ready')
|
.mockResolvedValue('ready')
|
||||||
const cond = jest.fn((value: string) => value === 'ready')
|
const cond = jest.fn((value: string | undefined) => value === 'ready')
|
||||||
|
|
||||||
await expect(until(2, 0, cond, fn)).resolves.toBe(true)
|
await expect(until(2, 0, cond, fn)).resolves.toBe(true)
|
||||||
expect(fn).toHaveBeenCalledTimes(2)
|
expect(fn).toHaveBeenCalledTimes(2)
|
||||||
expect(cond).toHaveBeenCalledTimes(1)
|
expect(cond).toHaveBeenNthCalledWith(1, undefined, error)
|
||||||
expect(cond).toHaveBeenCalledWith('ready')
|
expect(cond).toHaveBeenNthCalledWith(2, 'ready', undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns false when every attempt rejects', async () => {
|
it('returns false when every attempt rejects', async () => {
|
||||||
const fn = jest
|
const fn = jest
|
||||||
.fn<() => Promise<string>>()
|
.fn<() => Promise<string>>()
|
||||||
.mockRejectedValue(new Error('failed'))
|
.mockRejectedValue(new Error('failed'))
|
||||||
const cond = jest.fn((_value: string) => true)
|
const cond = jest.fn((_value: string | undefined) => false)
|
||||||
|
|
||||||
await expect(until(2, 0, cond, fn)).resolves.toBe(false)
|
await expect(until(2, 0, cond, fn)).resolves.toBe(false)
|
||||||
expect(fn).toHaveBeenCalledTimes(2)
|
expect(fn).toHaveBeenCalledTimes(2)
|
||||||
expect(cond).not.toHaveBeenCalled()
|
expect(cond).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can stop when an attempt rejects', async () => {
|
||||||
|
const error = new Error('failed')
|
||||||
|
const fn = jest.fn<() => Promise<string>>().mockRejectedValue(error)
|
||||||
|
const cond = jest.fn(
|
||||||
|
(_value: string | undefined, err: unknown) => err === error,
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(until(2, 0, cond, fn)).resolves.toBe(true)
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cond).toHaveBeenCalledWith(undefined, error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+12
-10
@@ -1,22 +1,24 @@
|
|||||||
import {timeout} from './timeout'
|
import {timeout} from './timeout'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retries an async operation until its result or error matches `cond`.
|
||||||
|
*/
|
||||||
export async function until<T>(
|
export async function until<T>(
|
||||||
retries: number,
|
retries: number,
|
||||||
delay: number,
|
delay: number,
|
||||||
cond: (v: T) => boolean,
|
cond: (v: T | undefined, err: unknown) => boolean,
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
while (retries > 0) {
|
while (retries > 0) {
|
||||||
let v: T
|
|
||||||
try {
|
try {
|
||||||
v = await fn()
|
const v = await fn()
|
||||||
} catch {
|
if (cond(v, undefined)) {
|
||||||
await timeout(delay)
|
return true
|
||||||
retries--
|
}
|
||||||
continue
|
} catch (err) {
|
||||||
}
|
if (cond(undefined, err)) {
|
||||||
if (cond(v)) {
|
return true
|
||||||
return true
|
}
|
||||||
}
|
}
|
||||||
await timeout(delay)
|
await timeout(delay)
|
||||||
retries--
|
retries--
|
||||||
|
|||||||
@@ -141,7 +141,10 @@ function createListItem({
|
|||||||
async function whenAppViewReady(
|
async function whenAppViewReady(
|
||||||
client: Client,
|
client: Client,
|
||||||
uri: string,
|
uri: string,
|
||||||
fn: (res?: app.bsky.graph.getStarterPack.$OutputBody) => boolean,
|
fn: (
|
||||||
|
res: app.bsky.graph.getStarterPack.$OutputBody | undefined,
|
||||||
|
err: unknown,
|
||||||
|
) => boolean,
|
||||||
) {
|
) {
|
||||||
await until(
|
await until(
|
||||||
5, // 5 tries
|
5, // 5 tries
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export async function bulkWriteFollows(
|
|||||||
writes: chunk,
|
writes: chunk,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await whenFollowsIndexed(appviewClient, did, res => !!res.follows.length)
|
await whenFollowsIndexed(appviewClient, did, res => !!res?.follows.length)
|
||||||
|
|
||||||
const followUris = new Map<string, string>()
|
const followUris = new Map<string, string>()
|
||||||
for (const r of followWrites) {
|
for (const r of followWrites) {
|
||||||
@@ -58,7 +58,10 @@ export async function bulkWriteFollows(
|
|||||||
async function whenFollowsIndexed(
|
async function whenFollowsIndexed(
|
||||||
appviewClient: Client,
|
appviewClient: Client,
|
||||||
actor: string,
|
actor: string,
|
||||||
fn: (res: app.bsky.graph.getFollows.$OutputBody) => boolean,
|
fn: (
|
||||||
|
res: app.bsky.graph.getFollows.$OutputBody | undefined,
|
||||||
|
err: unknown,
|
||||||
|
) => boolean,
|
||||||
) {
|
) {
|
||||||
await until(
|
await until(
|
||||||
5, // 5 tries
|
5, // 5 tries
|
||||||
|
|||||||
@@ -137,7 +137,11 @@ function GermSelfButton({did}: {did: string}) {
|
|||||||
rkey: 'self',
|
rkey: 'self',
|
||||||
})
|
})
|
||||||
|
|
||||||
await whenAppViewReady(appviewClient, did, res => !res.associated?.germ)
|
await whenAppViewReady(
|
||||||
|
appviewClient,
|
||||||
|
did,
|
||||||
|
res => !!res && !res.associated?.germ,
|
||||||
|
)
|
||||||
|
|
||||||
return previousRecord
|
return previousRecord
|
||||||
},
|
},
|
||||||
@@ -154,7 +158,7 @@ function GermSelfButton({did}: {did: string}) {
|
|||||||
await whenAppViewReady(
|
await whenAppViewReady(
|
||||||
appviewClient,
|
appviewClient,
|
||||||
did,
|
did,
|
||||||
res => !!res.associated?.germ,
|
res => !!res?.associated?.germ,
|
||||||
)
|
)
|
||||||
await queryClient.refetchQueries({queryKey: RQKEY(did)})
|
await queryClient.refetchQueries({queryKey: RQKEY(did)})
|
||||||
|
|
||||||
@@ -326,7 +330,10 @@ function platform() {
|
|||||||
async function whenAppViewReady(
|
async function whenAppViewReady(
|
||||||
appviewClient: Client,
|
appviewClient: Client,
|
||||||
actor: string,
|
actor: string,
|
||||||
fn: (res: app.bsky.actor.getProfile.$OutputBody) => boolean,
|
fn: (
|
||||||
|
res: app.bsky.actor.getProfile.$OutputBody | undefined,
|
||||||
|
err: unknown,
|
||||||
|
) => boolean,
|
||||||
) {
|
) {
|
||||||
await until(
|
await until(
|
||||||
5, // 5 tries
|
5, // 5 tries
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export function AutomationLabelSettingsScreen({}: Props) {
|
|||||||
return existing
|
return existing
|
||||||
},
|
},
|
||||||
checkCommitted: profile => {
|
checkCommitted: profile => {
|
||||||
|
if (!profile) return false
|
||||||
const exists = !!profile.labels?.some(l => l.val === 'bot')
|
const exists = !!profile.labels?.some(l => l.val === 'bot')
|
||||||
return exists === wasAdded
|
return exists === wasAdded
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export function PwiOptOut() {
|
|||||||
return existing
|
return existing
|
||||||
},
|
},
|
||||||
checkCommitted: profile => {
|
checkCommitted: profile => {
|
||||||
|
if (!profile) return false
|
||||||
const exists = !!profile.labels?.some(
|
const exists = !!profile.labels?.some(
|
||||||
l => l.val === '!no-unauthenticated',
|
l => l.val === '!no-unauthenticated',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -152,9 +152,9 @@ export function useListMetadataMutation() {
|
|||||||
|
|
||||||
// wait for the appview to update
|
// wait for the appview to update
|
||||||
await whenAppViewReady(appviewClient, res.uri, v => {
|
await whenAppViewReady(appviewClient, res.uri, v => {
|
||||||
const list = v.list
|
const list = v?.list
|
||||||
return (
|
return (
|
||||||
list.name === record.name && list.description === record.description
|
list?.name === record.name && list.description === record.description
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
@@ -228,14 +228,10 @@ export function useListDeleteMutation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Wait for the appview to update. Once the list is deleted `getList`
|
* Once the deletion is indexed, `getList` throws and `until` passes the
|
||||||
* throws, `until` catches it and passes `undefined` here, so an absent
|
* error to this predicate with an undefined response.
|
||||||
* body signals a completed delete - the old check read `!v.success` on
|
|
||||||
* the legacy response envelope, which lex does not expose.
|
|
||||||
*/
|
*/
|
||||||
await whenAppViewReady(appviewClient, uri, v => {
|
await whenAppViewReady(appviewClient, uri, v => !v)
|
||||||
return !v
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
invalidateMyLists(queryClient)
|
invalidateMyLists(queryClient)
|
||||||
@@ -299,7 +295,10 @@ export function useListBlockMutation() {
|
|||||||
async function whenAppViewReady(
|
async function whenAppViewReady(
|
||||||
client: Client,
|
client: Client,
|
||||||
uri: string,
|
uri: string,
|
||||||
fn: (res: app.bsky.graph.getList.$OutputBody) => boolean,
|
fn: (
|
||||||
|
res: app.bsky.graph.getList.$OutputBody | undefined,
|
||||||
|
err: unknown,
|
||||||
|
) => boolean,
|
||||||
) {
|
) {
|
||||||
await until(
|
await until(
|
||||||
5, // 5 tries
|
5, // 5 tries
|
||||||
|
|||||||
@@ -79,10 +79,12 @@ export function usePinnedPostMutation() {
|
|||||||
: undefined
|
: undefined
|
||||||
return existing
|
return existing
|
||||||
},
|
},
|
||||||
checkCommitted: profile =>
|
checkCommitted: profile => {
|
||||||
pinCurrentPost
|
if (!profile) return false
|
||||||
|
return pinCurrentPost
|
||||||
? profile.pinnedPost?.uri === postUri
|
? profile.pinnedPost?.uri === postUri
|
||||||
: !profile.pinnedPost,
|
: !profile.pinnedPost
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (pinCurrentPost) {
|
if (pinCurrentPost) {
|
||||||
|
|||||||
@@ -146,7 +146,10 @@ interface ProfileUpdateParams {
|
|||||||
) => Un$Typed<app.bsky.actor.profile.Main>)
|
) => Un$Typed<app.bsky.actor.profile.Main>)
|
||||||
newUserAvatar?: ImageMeta | undefined | null
|
newUserAvatar?: ImageMeta | undefined | null
|
||||||
newUserBanner?: ImageMeta | undefined | null
|
newUserBanner?: ImageMeta | undefined | null
|
||||||
checkCommitted?: (profile: app.bsky.actor.getProfile.$OutputBody) => boolean
|
checkCommitted?: (
|
||||||
|
profile: app.bsky.actor.getProfile.$OutputBody | undefined,
|
||||||
|
err: unknown,
|
||||||
|
) => boolean
|
||||||
}
|
}
|
||||||
export function useProfileUpdateMutation() {
|
export function useProfileUpdateMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -207,6 +210,7 @@ export function useProfileUpdateMutation() {
|
|||||||
profile.did,
|
profile.did,
|
||||||
checkCommitted ||
|
checkCommitted ||
|
||||||
(fresh => {
|
(fresh => {
|
||||||
|
if (!fresh) return false
|
||||||
if (typeof newUserAvatar !== 'undefined') {
|
if (typeof newUserAvatar !== 'undefined') {
|
||||||
if (newUserAvatar === null && fresh.avatar) {
|
if (newUserAvatar === null && fresh.avatar) {
|
||||||
// url hasn't cleared yet
|
// url hasn't cleared yet
|
||||||
@@ -678,7 +682,10 @@ function useProfileUnblockMutation() {
|
|||||||
async function whenAppViewReady(
|
async function whenAppViewReady(
|
||||||
client: Client,
|
client: Client,
|
||||||
actor: string,
|
actor: string,
|
||||||
fn: (profile: app.bsky.actor.getProfile.$OutputBody) => boolean,
|
fn: (
|
||||||
|
profile: app.bsky.actor.getProfile.$OutputBody | undefined,
|
||||||
|
err: unknown,
|
||||||
|
) => boolean,
|
||||||
) {
|
) {
|
||||||
await until(
|
await until(
|
||||||
5, // 5 tries
|
5, // 5 tries
|
||||||
|
|||||||
@@ -325,9 +325,12 @@ export function useDeleteStarterPackMutation({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (uri) {
|
if (uri) {
|
||||||
await whenAppViewReady(appviewClient, uri, v => {
|
/* Once the deletion is indexed, `getStarterPack` throws. */
|
||||||
return Boolean(v?.starterPack) === false
|
await whenAppViewReady(
|
||||||
})
|
appviewClient,
|
||||||
|
uri,
|
||||||
|
v => Boolean(v?.starterPack) === false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (listUri) {
|
if (listUri) {
|
||||||
@@ -353,7 +356,10 @@ export function useDeleteStarterPackMutation({
|
|||||||
async function whenAppViewReady(
|
async function whenAppViewReady(
|
||||||
client: Client,
|
client: Client,
|
||||||
uri: string,
|
uri: string,
|
||||||
fn: (res?: app.bsky.graph.getStarterPack.$OutputBody) => boolean,
|
fn: (
|
||||||
|
res: app.bsky.graph.getStarterPack.$OutputBody | undefined,
|
||||||
|
err: unknown,
|
||||||
|
) => boolean,
|
||||||
) {
|
) {
|
||||||
await until(
|
await until(
|
||||||
5, // 5 tries
|
5, // 5 tries
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ export function useVerificationCreateMutation() {
|
|||||||
await until(
|
await until(
|
||||||
5,
|
5,
|
||||||
1e3,
|
1e3,
|
||||||
(profile: app.bsky.actor.getProfile.$OutputBody) => {
|
profile => {
|
||||||
|
if (!profile) return false
|
||||||
if (
|
if (
|
||||||
profile.verification &&
|
profile.verification &&
|
||||||
profile.verification.verifications.find(v => v.uri === uri)
|
profile.verification.verifications.find(v => v.uri === uri)
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ export function useVerificationsRemoveMutation() {
|
|||||||
await until(
|
await until(
|
||||||
5,
|
5,
|
||||||
1e3,
|
1e3,
|
||||||
(profile: app.bsky.actor.getProfile.$OutputBody) => {
|
profile => {
|
||||||
|
if (!profile) return false
|
||||||
if (
|
if (
|
||||||
!profile.verification?.verifications.some(v => uris.includes(v.uri))
|
!profile.verification?.verifications.some(v => uris.includes(v.uri))
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -2482,6 +2482,7 @@ async function whenAppViewReady(
|
|||||||
uri: string,
|
uri: string,
|
||||||
fn: (
|
fn: (
|
||||||
res: app.bsky.unspecced.getPostThreadV2.$OutputBody | undefined,
|
res: app.bsky.unspecced.getPostThreadV2.$OutputBody | undefined,
|
||||||
|
err: unknown,
|
||||||
) => boolean,
|
) => boolean,
|
||||||
) {
|
) {
|
||||||
await until(
|
await until(
|
||||||
|
|||||||
Reference in New Issue
Block a user