@@ -25,9 +25,11 @@ interface ConversionState {
2525
2626interface ConversionError extends Error {
2727 manualRetryStrategy ?: string ;
28- autoRetryAttempted ?: boolean ;
2928}
3029
30+ const PREVIEW_UNAVAILABLE_MESSAGE = 'Preview unavailable right now.' ;
31+ const NON_RETRYABLE_ERROR_CODES = new Set ( [ 'BAD_REQUEST' , 'UNAUTHORIZED' , 'FORBIDDEN' ] ) ;
32+
3133export function useFeedConversion ( ) {
3234 const requestIdRef = useRef ( 0 ) ;
3335 const [ state , setState ] = useState < ConversionState > ( {
@@ -50,66 +52,35 @@ export function useFeedConversion() {
5052
5153 const requestId = requestIdRef . current + 1 ;
5254 requestIdRef . current = requestId ;
53- setState ( ( prev ) => ( { ... prev , isConverting : true , error : null } ) ) ;
55+ markConversionStarted ( setState ) ;
5456
5557 try {
5658 const feed = await requestFeedCreation ( normalizedUrl , requestedStrategy , token ) ;
57- const result = {
58- feed,
59- preview : buildLoadingPreviewState ( ) ,
60- retry : null ,
61- } ;
62-
63- setState ( ( prev ) => ( { ...prev , isConverting : false , result, error : null } ) ) ;
64- void hydratePreview ( feed , requestId , null , setState , requestIdRef ) ;
65- return result ;
59+ return publishCreatedFeed ( feed , null , requestId , setState , requestIdRef ) ;
6660 } catch ( firstError ) {
6761 if ( shouldAutoRetry ( requestedStrategy , fallbackStrategy , firstError ) ) {
6862 try {
6963 const feed = await requestFeedCreation ( normalizedUrl , fallbackStrategy , token ) ;
70- const result = {
64+ return publishCreatedFeed (
7165 feed ,
72- preview : buildLoadingPreviewState ( ) ,
73- retry : { automatic : true , from : requestedStrategy , to : fallbackStrategy } ,
74- } ;
75-
76- setState ( ( prev ) => ( { ...prev , isConverting : false , result, error : null } ) ) ;
77- void hydratePreview ( feed , requestId , result . retry , setState , requestIdRef ) ;
78- return result ;
66+ { automatic : true , from : requestedStrategy , to : fallbackStrategy } ,
67+ requestId ,
68+ setState ,
69+ requestIdRef
70+ ) ;
7971 } catch ( secondError ) {
8072 const message = buildRetryFailureMessage (
8173 firstError ,
8274 secondError ,
8375 requestedStrategy ,
8476 fallbackStrategy
8577 ) ;
86- const retryError = buildConversionError ( message , {
87- manualRetryStrategy : undefined ,
88- autoRetryAttempted : true ,
89- } ) ;
90-
91- setState ( ( prev ) => ( {
92- ...prev ,
93- isConverting : false ,
94- error : message ,
95- result : null ,
96- } ) ) ;
97- throw retryError ;
78+ failConversion ( setState , message , { manualRetryStrategy : undefined } ) ;
9879 }
9980 }
10081
10182 const message = toErrorMessage ( firstError ) ;
102- const retryError = buildConversionError ( message , {
103- manualRetryStrategy : alternateStrategy ( requestedStrategy ) ,
104- } ) ;
105-
106- setState ( ( prev ) => ( {
107- ...prev ,
108- isConverting : false ,
109- error : message ,
110- result : null ,
111- } ) ) ;
112- throw retryError ;
83+ failConversion ( setState , message , { manualRetryStrategy : alternateStrategy ( requestedStrategy ) } ) ;
11384 }
11485 } ;
11586
@@ -143,7 +114,7 @@ async function loadPreview(feed: FeedRecord): Promise<CreatedFeedResult['preview
143114 headers : { Accept : 'application/feed+json' } ,
144115 } ) ;
145116
146- if ( ! response . ok ) throw new Error ( 'Preview unavailable right now.' ) ;
117+ if ( ! response . ok ) throw new Error ( PREVIEW_UNAVAILABLE_MESSAGE ) ;
147118
148119 const payload = ( await response . json ( ) ) as JsonFeedResponse ;
149120 const items =
@@ -154,7 +125,7 @@ async function loadPreview(feed: FeedRecord): Promise<CreatedFeedResult['preview
154125
155126 return {
156127 items,
157- error : items . length > 0 ? null : 'Preview unavailable right now.' ,
128+ error : items . length > 0 ? null : PREVIEW_UNAVAILABLE_MESSAGE ,
158129 isLoading : false ,
159130 } ;
160131}
@@ -243,19 +214,7 @@ function shouldAutoRetry(
243214 error : unknown
244215) : fallbackStrategy is string {
245216 if ( strategy !== 'faraday' || ! fallbackStrategy ) return false ;
246-
247- const normalized = toErrorMessage ( error ) . toLowerCase ( ) ;
248- return ! (
249- normalized . includes ( 'unauthorized' ) ||
250- normalized . includes ( 'bad request' ) ||
251- normalized . includes ( 'forbidden' ) ||
252- normalized . includes ( 'access token' ) ||
253- normalized . includes ( 'authentication' ) ||
254- normalized . includes ( 'invalid response format' ) ||
255- normalized . includes ( 'network error' ) ||
256- normalized . includes ( 'url' ) ||
257- normalized . includes ( 'unsupported strategy' )
258- ) ;
217+ return retryableForFallback ( error ) ;
259218}
260219
261220function buildRetryFailureMessage (
@@ -279,30 +238,97 @@ function buildConversionError(message: string, metadata: Partial<ConversionError
279238}
280239
281240const toErrorMessage = ( error : unknown ) : string => {
241+ const details = extractErrorDetails ( error ) ;
242+ if ( details ?. message ) return details . message ;
282243 if ( error instanceof SyntaxError ) return 'Invalid response format from feed creation API' ;
283244 if ( error instanceof Error ) return error . message ;
284245 if ( typeof error === 'string' && error . trim ( ) ) return error ;
285-
286- const message = extractMessage ( error ) ;
287- return message ?? 'An unexpected error occurred' ;
246+ return 'An unexpected error occurred' ;
288247} ;
289248
290249const toPreviewErrorMessage = ( error : unknown ) : string => {
291- if ( error instanceof SyntaxError ) return 'Preview unavailable right now.' ;
250+ if ( error instanceof SyntaxError ) return PREVIEW_UNAVAILABLE_MESSAGE ;
292251 if ( error instanceof Error && error . message . trim ( ) ) return error . message ;
293- return 'Preview unavailable right now.' ;
252+ return PREVIEW_UNAVAILABLE_MESSAGE ;
294253} ;
295254
296- const extractMessage = ( error : unknown ) : string | null => {
255+ function markConversionStarted (
256+ setState : ( value : ConversionState | ( ( prev : ConversionState ) => ConversionState ) ) => void
257+ ) {
258+ setState ( ( prev ) => ( { ...prev , isConverting : true , error : null } ) ) ;
259+ }
260+
261+ function publishCreatedFeed (
262+ feed : FeedRecord ,
263+ retry : CreatedFeedResult [ 'retry' ] ,
264+ requestId : number ,
265+ setState : ( value : ConversionState | ( ( prev : ConversionState ) => ConversionState ) ) => void ,
266+ requestIdRef : { current : number }
267+ ) : CreatedFeedResult {
268+ const result : CreatedFeedResult = {
269+ feed,
270+ preview : buildLoadingPreviewState ( ) ,
271+ retry,
272+ } ;
273+
274+ setState ( ( prev ) => ( { ...prev , isConverting : false , result, error : null } ) ) ;
275+ void hydratePreview ( feed , requestId , retry , setState , requestIdRef ) ;
276+ return result ;
277+ }
278+
279+ function failConversion (
280+ setState : ( value : ConversionState | ( ( prev : ConversionState ) => ConversionState ) ) => void ,
281+ message : string ,
282+ metadata : Partial < ConversionError >
283+ ) : never {
284+ setState ( ( prev ) => ( {
285+ ...prev ,
286+ isConverting : false ,
287+ error : message ,
288+ result : null ,
289+ } ) ) ;
290+
291+ throw buildConversionError ( message , metadata ) ;
292+ }
293+
294+ const extractErrorDetails = ( error : unknown ) : { message ?: string ; code ?: string } | null => {
297295 if ( ! error || typeof error !== 'object' ) return null ;
298296
299- const candidate =
300- ( error as { error ?: { message ?: unknown } ; message ?: unknown } ) . error ?. message ??
301- ( error as { message ?: unknown } ) . message ;
297+ const candidate = error as {
298+ error ?: { message ?: unknown ; code ?: unknown } ;
299+ message ?: unknown ;
300+ code ?: unknown ;
301+ } ;
302302
303- return typeof candidate === 'string' && candidate . trim ( ) ? candidate : null ;
303+ const message = normalizeString ( candidate . error ?. message ?? candidate . message ) ;
304+ const code = normalizeString ( candidate . error ?. code ?? candidate . code ) ;
305+ return { message, code } ;
304306} ;
305307
308+ function retryableForFallback ( error : unknown ) : boolean {
309+ const details = extractErrorDetails ( error ) ;
310+ const errorCode = details ?. code ?. toUpperCase ( ) ;
311+ if ( errorCode && NON_RETRYABLE_ERROR_CODES . has ( errorCode ) ) return false ;
312+
313+ const message = ( details ?. message ?? toErrorMessage ( error ) ) . toLowerCase ( ) ;
314+ if ( ! details ?. code && ( message . includes ( 'unauthorized' ) || message . includes ( 'forbidden' ) ) ) return false ;
315+ if ( ! details ?. code && message . includes ( 'bad request' ) ) return false ;
316+ if ( message . includes ( 'access token' ) || message . includes ( 'authentication' ) ) return false ;
317+ if ( message . includes ( 'unsupported strategy' ) ) return false ;
318+ if ( message . includes ( 'invalid response format' ) ) return false ;
319+
320+ return ! networkFailure ( error , message ) ;
321+ }
322+
323+ function networkFailure ( error : unknown , normalizedMessage : string ) : boolean {
324+ if ( error instanceof TypeError ) return true ;
325+ return normalizedMessage . includes ( 'network error' ) ;
326+ }
327+
328+ function normalizeString ( value : unknown ) : string | undefined {
329+ return typeof value === 'string' && value . trim ( ) ? value : undefined ;
330+ }
331+
306332function normalizePreviewText ( value ?: string ) : string | null {
307333 if ( ! value ) return null ;
308334
0 commit comments