mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
* wip demo example path * load demo example * nit: format * refactor demo cards to their own component * ui nits * more ui nits * feedback
132 lines
5.1 KiB
TypeScript
132 lines
5.1 KiB
TypeScript
import { SourcebotConfig } from "@sourcebot/schemas/v3/index.type";
|
|
import { indexSchema } from "@sourcebot/schemas/v3/index.schema";
|
|
import { readFile } from 'fs/promises';
|
|
import stripJsonComments from 'strip-json-comments';
|
|
import { Ajv } from "ajv";
|
|
import { z } from "zod";
|
|
|
|
const ajv = new Ajv({
|
|
validateFormats: false,
|
|
});
|
|
|
|
// From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
|
export const base64Decode = (base64: string): string => {
|
|
const binString = atob(base64);
|
|
return Buffer.from(Uint8Array.from(binString, (m) => m.codePointAt(0)!).buffer).toString();
|
|
}
|
|
|
|
export const isRemotePath = (path: string) => {
|
|
return path.startsWith('https://') || path.startsWith('http://');
|
|
}
|
|
|
|
// TODO: Merge this with config loading logic which uses AJV
|
|
export const loadJsonFile = async <T>(
|
|
filePath: string,
|
|
schema: any
|
|
): Promise<T> => {
|
|
const fileContent = await (async () => {
|
|
if (isRemotePath(filePath)) {
|
|
const response = await fetch(filePath);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch file ${filePath}: ${response.statusText}`);
|
|
}
|
|
return response.text();
|
|
} else {
|
|
// Retry logic for handling race conditions with mounted volumes
|
|
const maxAttempts = 5;
|
|
const retryDelayMs = 2000;
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
return await readFile(filePath, {
|
|
encoding: 'utf-8',
|
|
});
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
|
|
// Only retry on ENOENT errors (file not found)
|
|
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
|
throw error; // Throw immediately for non-ENOENT errors
|
|
}
|
|
|
|
// Log warning before retry (except on the last attempt)
|
|
if (attempt < maxAttempts) {
|
|
console.warn(`File not found, retrying in 2s... (Attempt ${attempt}/${maxAttempts})`);
|
|
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we've exhausted all retries, throw the last ENOENT error
|
|
if (lastError) {
|
|
throw lastError;
|
|
}
|
|
|
|
throw new Error('Failed to load file after all retry attempts');
|
|
}
|
|
})();
|
|
|
|
const parsedData = JSON.parse(stripJsonComments(fileContent));
|
|
|
|
try {
|
|
return schema.parse(parsedData);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
throw new Error(`File '${filePath}' is invalid: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export const loadConfig = async (configPath: string): Promise<SourcebotConfig> => {
|
|
const configContent = await (async () => {
|
|
if (isRemotePath(configPath)) {
|
|
const response = await fetch(configPath);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch config file ${configPath}: ${response.statusText}`);
|
|
}
|
|
return response.text();
|
|
} else {
|
|
// Retry logic for handling race conditions with mounted volumes
|
|
const maxAttempts = 5;
|
|
const retryDelayMs = 2000;
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
return await readFile(configPath, {
|
|
encoding: 'utf-8',
|
|
});
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
|
|
// Only retry on ENOENT errors (file not found)
|
|
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
|
throw error; // Throw immediately for non-ENOENT errors
|
|
}
|
|
|
|
// Log warning before retry (except on the last attempt)
|
|
if (attempt < maxAttempts) {
|
|
console.warn(`Config file not found, retrying in 2s... (Attempt ${attempt}/${maxAttempts})`);
|
|
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we've exhausted all retries, throw the last ENOENT error
|
|
if (lastError) {
|
|
throw lastError;
|
|
}
|
|
|
|
throw new Error('Failed to load config after all retry attempts');
|
|
}
|
|
})();
|
|
|
|
const config = JSON.parse(stripJsonComments(configContent)) as SourcebotConfig;
|
|
const isValidConfig = ajv.validate(indexSchema, config);
|
|
if (!isValidConfig) {
|
|
throw new Error(`Config file '${configPath}' is invalid: ${ajv.errorsText(ajv.errors)}`);
|
|
}
|
|
return config;
|
|
}
|