Skip to content

Commit 3b47de4

Browse files
trivikrghostdevv
andauthored
fix: retry replacement suggestions after transient fetch failures (#2469)
Co-authored-by: Willow (GHOST) <ghostdevbusiness@gmail.com>
1 parent a1f6487 commit 3b47de4

File tree

2 files changed

+71
-13
lines changed

2 files changed

+71
-13
lines changed

app/composables/useCompareReplacements.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,17 @@ export function useCompareReplacements(packageNames: MaybeRefOrGetter<string[]>)
4242
namesToCheck.map(async name => {
4343
try {
4444
const replacement = await $fetch<ModuleReplacement | null>(`/api/replacements/${name}`)
45-
return { name, replacement }
45+
return { name, replacement, failed: false as const }
4646
} catch {
47-
return { name, replacement: null }
47+
return { name, failed: true as const }
4848
}
4949
}),
5050
)
5151

5252
const newReplacements = new Map(replacements.value)
53-
for (const { name, replacement } of results) {
54-
newReplacements.set(name, replacement)
53+
for (const result of results) {
54+
if (result.failed) continue
55+
newReplacements.set(result.name, result.replacement)
5556
}
5657
replacements.value = newReplacements
5758
} finally {

test/nuxt/composables/use-compare-replacements.spec.ts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import type { ReplacementSuggestion } from '~/composables/useCompareReplacements
66
/**
77
* Helper to test useCompareReplacements by wrapping it in a component.
88
*/
9-
async function useCompareReplacementsInComponent(packageNames: string[]) {
9+
async function useCompareReplacementsInComponent(
10+
packageNames: Parameters<typeof useCompareReplacements>[0],
11+
) {
1012
const capturedNoDepSuggestions = ref<ReplacementSuggestion[]>([])
1113
const capturedInfoSuggestions = ref<ReplacementSuggestion[]>([])
1214
const capturedLoading = ref(false)
@@ -212,27 +214,82 @@ describe('useCompareReplacements', () => {
212214
})
213215

214216
it('handles fetch errors gracefully', async () => {
215-
vi.stubGlobal(
216-
'$fetch',
217-
vi.fn().mockImplementation(() => {
218-
return Promise.reject(new Error('Network error'))
219-
}),
220-
)
217+
const fetchMock = vi.fn().mockImplementation(() => {
218+
return Promise.reject(new Error('Network error'))
219+
})
220+
221+
vi.stubGlobal('$fetch', fetchMock)
221222

222223
const { noDepSuggestions, infoSuggestions, replacements } =
223224
await useCompareReplacementsInComponent(['some-package'])
224225

225226
await vi.waitFor(() => {
226-
expect(replacements.value.has('some-package')).toBe(true)
227+
expect(fetchMock).toHaveBeenCalledTimes(1)
227228
})
228229

229-
expect(replacements.value.get('some-package')).toBeNull()
230+
expect(replacements.value.has('some-package')).toBe(false)
230231
expect(noDepSuggestions.value).toHaveLength(0)
231232
expect(infoSuggestions.value).toHaveLength(0)
232233
})
233234
})
234235

235236
describe('caching', () => {
237+
it('retries a package after a transient fetch failure', async () => {
238+
const packageNames = ref(['is-even'])
239+
const fetchMock = vi
240+
.fn()
241+
.mockRejectedValueOnce(new Error('Temporary network error'))
242+
.mockResolvedValueOnce({
243+
type: 'simple',
244+
moduleName: 'is-even',
245+
replacement: 'Use (n % 2) === 0',
246+
category: 'micro-utilities',
247+
} satisfies ModuleReplacement)
248+
249+
vi.stubGlobal('$fetch', fetchMock)
250+
251+
const { noDepSuggestions, replacements } =
252+
await useCompareReplacementsInComponent(packageNames)
253+
254+
await vi.waitFor(() => {
255+
expect(fetchMock).toHaveBeenCalledTimes(1)
256+
})
257+
258+
expect(replacements.value.has('is-even')).toBe(false)
259+
260+
packageNames.value = []
261+
await nextTick()
262+
packageNames.value = ['is-even']
263+
264+
await vi.waitFor(() => {
265+
expect(fetchMock).toHaveBeenCalledTimes(2)
266+
expect(noDepSuggestions.value).toHaveLength(1)
267+
})
268+
269+
expect(replacements.value.get('is-even')?.type).toBe('simple')
270+
})
271+
272+
it('caches successful null replacement data and does not refetch', async () => {
273+
const fetchMock = vi.fn().mockResolvedValue(null)
274+
275+
vi.stubGlobal('$fetch', fetchMock)
276+
277+
const packageNames = ref(['react'])
278+
const { replacements } = await useCompareReplacementsInComponent(packageNames)
279+
280+
await vi.waitFor(() => {
281+
expect(replacements.value.has('react')).toBe(true)
282+
})
283+
284+
packageNames.value = []
285+
await nextTick()
286+
packageNames.value = ['react']
287+
288+
await vi.waitFor(() => {
289+
expect(fetchMock).toHaveBeenCalledTimes(1)
290+
})
291+
})
292+
236293
it('caches replacement data and does not refetch', async () => {
237294
const fetchMock = vi.fn().mockImplementation((url: string) => {
238295
if (url.includes('/api/replacements/is-even')) {

0 commit comments

Comments
 (0)