(
filePath: string,
schema: any
)
| 16 | |
| 17 | // TODO: Merge this with config loading logic which uses AJV |
| 18 | export const loadJsonFile = async <T>( |
| 19 | filePath: string, |
| 20 | schema: any |
| 21 | ): Promise<T> => { |
| 22 | const fileContent = await (async () => { |
| 23 | if (isRemotePath(filePath)) { |
| 24 | const response = await fetch(filePath); |
| 25 | if (!response.ok) { |
| 26 | throw new Error(`Failed to fetch file ${filePath}: ${response.statusText}`); |
| 27 | } |
| 28 | return response.text(); |
| 29 | } else { |
| 30 | // Retry logic for handling race conditions with mounted volumes |
| 31 | const maxAttempts = 5; |
| 32 | const retryDelayMs = 2000; |
| 33 | let lastError: Error | null = null; |
| 34 | |
| 35 | for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
| 36 | try { |
| 37 | return await readFile(filePath, { |
| 38 | encoding: 'utf-8', |
| 39 | }); |
| 40 | } catch (error) { |
| 41 | lastError = error as Error; |
| 42 | |
| 43 | // Only retry on ENOENT errors (file not found) |
| 44 | if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') { |
| 45 | throw error; // Throw immediately for non-ENOENT errors |
| 46 | } |
| 47 | |
| 48 | // Log warning before retry (except on the last attempt) |
| 49 | if (attempt < maxAttempts) { |
| 50 | console.warn(`File not found, retrying in 2s... (Attempt ${attempt}/${maxAttempts})`); |
| 51 | await new Promise(resolve => setTimeout(resolve, retryDelayMs)); |
| 52 | } |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // If we've exhausted all retries, throw the last ENOENT error |
| 57 | if (lastError) { |
| 58 | throw lastError; |
| 59 | } |
| 60 | |
| 61 | throw new Error('Failed to load file after all retry attempts'); |
| 62 | } |
| 63 | })(); |
| 64 | |
| 65 | const parsedData = JSON.parse(stripJsonComments(fileContent)); |
| 66 | |
| 67 | try { |
| 68 | return schema.parse(parsedData); |
| 69 | } catch (error) { |
| 70 | if (error instanceof z.ZodError) { |
| 71 | throw new Error(`File '${filePath}' is invalid: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`); |
| 72 | } |
| 73 | throw error; |
| 74 | } |
| 75 | } |
no test coverage detected