sourcebot/packages/shared/src/utils.ts

133 lines
5.1 KiB
TypeScript
Raw Normal View History

fix(search-contexts): Fix issue where a repository would not appear in a search context if it was created after the search context was created (#354) ## Problem If a repository is added **after** a search context (e.g., a new repository is synced from the code host), then it will never be added to the context even if it should be included. The workaround is to restart the instance. ## Solution This PR adds a call to re-sync all search contexts whenever a connection is successfully synced. This PR adds the `@sourcebot/shared` package that contains `syncSearchContexts.ts` (previously in web) and it's dependencies (namely the entitlements system). ## Why another package? Because the `syncSearchContexts` call is now called from: 1. `initialize.ts` in **web** - handles syncing search contexts on startup and whenever the config is modified in watch mode. This is the same as before. 2. `connectionManager.ts` in **backend** - syncs the search contexts whenever a connection is successfully synced. ## Follow-up devex work Two things: 1. We have several very thin shared packages (i.e., `crypto`, `error`, and `logger`) that we can probably fold into this "general" shared package. `schemas` and `db` _feels_ like they should remain separate (mostly because they are "code-gen" packages). 2. When running `yarn dev`, any changes made to the shared package will only get picked if you `ctrl+c` and restart the instance. Would be nice if we have watch mode work across package dependencies in the monorepo.
2025-06-17 21:04:25 +00:00
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";
fix(search-contexts): Fix issue where a repository would not appear in a search context if it was created after the search context was created (#354) ## Problem If a repository is added **after** a search context (e.g., a new repository is synced from the code host), then it will never be added to the context even if it should be included. The workaround is to restart the instance. ## Solution This PR adds a call to re-sync all search contexts whenever a connection is successfully synced. This PR adds the `@sourcebot/shared` package that contains `syncSearchContexts.ts` (previously in web) and it's dependencies (namely the entitlements system). ## Why another package? Because the `syncSearchContexts` call is now called from: 1. `initialize.ts` in **web** - handles syncing search contexts on startup and whenever the config is modified in watch mode. This is the same as before. 2. `connectionManager.ts` in **backend** - syncs the search contexts whenever a connection is successfully synced. ## Follow-up devex work Two things: 1. We have several very thin shared packages (i.e., `crypto`, `error`, and `logger`) that we can probably fold into this "general" shared package. `schemas` and `db` _feels_ like they should remain separate (mostly because they are "code-gen" packages). 2. When running `yarn dev`, any changes made to the shared package will only get picked if you `ctrl+c` and restart the instance. Would be nice if we have watch mode work across package dependencies in the monorepo.
2025-06-17 21:04:25 +00:00
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;
}
}
fix(search-contexts): Fix issue where a repository would not appear in a search context if it was created after the search context was created (#354) ## Problem If a repository is added **after** a search context (e.g., a new repository is synced from the code host), then it will never be added to the context even if it should be included. The workaround is to restart the instance. ## Solution This PR adds a call to re-sync all search contexts whenever a connection is successfully synced. This PR adds the `@sourcebot/shared` package that contains `syncSearchContexts.ts` (previously in web) and it's dependencies (namely the entitlements system). ## Why another package? Because the `syncSearchContexts` call is now called from: 1. `initialize.ts` in **web** - handles syncing search contexts on startup and whenever the config is modified in watch mode. This is the same as before. 2. `connectionManager.ts` in **backend** - syncs the search contexts whenever a connection is successfully synced. ## Follow-up devex work Two things: 1. We have several very thin shared packages (i.e., `crypto`, `error`, and `logger`) that we can probably fold into this "general" shared package. `schemas` and `db` _feels_ like they should remain separate (mostly because they are "code-gen" packages). 2. When running `yarn dev`, any changes made to the shared package will only get picked if you `ctrl+c` and restart the instance. Would be nice if we have watch mode work across package dependencies in the monorepo.
2025-06-17 21:04:25 +00:00
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 {
2025-07-24 18:35:00 +00:00
// 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');
fix(search-contexts): Fix issue where a repository would not appear in a search context if it was created after the search context was created (#354) ## Problem If a repository is added **after** a search context (e.g., a new repository is synced from the code host), then it will never be added to the context even if it should be included. The workaround is to restart the instance. ## Solution This PR adds a call to re-sync all search contexts whenever a connection is successfully synced. This PR adds the `@sourcebot/shared` package that contains `syncSearchContexts.ts` (previously in web) and it's dependencies (namely the entitlements system). ## Why another package? Because the `syncSearchContexts` call is now called from: 1. `initialize.ts` in **web** - handles syncing search contexts on startup and whenever the config is modified in watch mode. This is the same as before. 2. `connectionManager.ts` in **backend** - syncs the search contexts whenever a connection is successfully synced. ## Follow-up devex work Two things: 1. We have several very thin shared packages (i.e., `crypto`, `error`, and `logger`) that we can probably fold into this "general" shared package. `schemas` and `db` _feels_ like they should remain separate (mostly because they are "code-gen" packages). 2. When running `yarn dev`, any changes made to the shared package will only get picked if you `ctrl+c` and restart the instance. Would be nice if we have watch mode work across package dependencies in the monorepo.
2025-06-17 21:04:25 +00:00
}
})();
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;
}