|
|
import { env as publicEnv } from "$env/dynamic/public"; |
|
|
import { env as serverEnv } from "$env/dynamic/private"; |
|
|
import { building } from "$app/environment"; |
|
|
import type { Collection } from "mongodb"; |
|
|
import type { ConfigKey as ConfigKeyType } from "$lib/types/ConfigKey"; |
|
|
import type { Semaphore } from "$lib/types/Semaphore"; |
|
|
import { Semaphores } from "$lib/types/Semaphore"; |
|
|
|
|
|
export type PublicConfigKey = keyof typeof publicEnv; |
|
|
const keysFromEnv = { ...publicEnv, ...serverEnv }; |
|
|
export type ConfigKey = keyof typeof keysFromEnv; |
|
|
|
|
|
class ConfigManager { |
|
|
private keysFromDB: Partial<Record<ConfigKey, string>> = {}; |
|
|
private isInitialized = false; |
|
|
|
|
|
private configCollection: Collection<ConfigKeyType> | undefined; |
|
|
private semaphoreCollection: Collection<Semaphore> | undefined; |
|
|
private lastConfigUpdate: Date | undefined; |
|
|
|
|
|
async init() { |
|
|
if (this.isInitialized) return; |
|
|
|
|
|
if (import.meta.env.MODE === "test") { |
|
|
this.isInitialized = true; |
|
|
return; |
|
|
} |
|
|
|
|
|
const { getCollectionsEarly } = await import("./database"); |
|
|
const collections = await getCollectionsEarly(); |
|
|
|
|
|
this.configCollection = collections.config; |
|
|
this.semaphoreCollection = collections.semaphores; |
|
|
|
|
|
await this.checkForUpdates().then(() => { |
|
|
this.isInitialized = true; |
|
|
}); |
|
|
} |
|
|
|
|
|
get ConfigManagerEnabled() { |
|
|
return serverEnv.ENABLE_CONFIG_MANAGER === "true" && import.meta.env.MODE !== "test"; |
|
|
} |
|
|
|
|
|
get isHuggingChat() { |
|
|
return this.get("PUBLIC_APP_ASSETS") === "huggingchat"; |
|
|
} |
|
|
|
|
|
async checkForUpdates() { |
|
|
if (await this.isConfigStale()) { |
|
|
await this.updateConfig(); |
|
|
} |
|
|
} |
|
|
|
|
|
async isConfigStale(): Promise<boolean> { |
|
|
if (!this.lastConfigUpdate || !this.isInitialized) { |
|
|
return true; |
|
|
} |
|
|
const count = await this.semaphoreCollection?.countDocuments({ |
|
|
key: Semaphores.CONFIG_UPDATE, |
|
|
updatedAt: { $gt: this.lastConfigUpdate }, |
|
|
}); |
|
|
return count !== undefined && count > 0; |
|
|
} |
|
|
|
|
|
async updateConfig() { |
|
|
const configs = (await this.configCollection?.find({}).toArray()) ?? []; |
|
|
this.keysFromDB = configs.reduce( |
|
|
(acc, curr) => { |
|
|
acc[curr.key as ConfigKey] = curr.value; |
|
|
return acc; |
|
|
}, |
|
|
{} as Record<ConfigKey, string> |
|
|
); |
|
|
|
|
|
this.lastConfigUpdate = new Date(); |
|
|
} |
|
|
|
|
|
get(key: ConfigKey): string { |
|
|
if (!this.ConfigManagerEnabled) { |
|
|
return keysFromEnv[key] || ""; |
|
|
} |
|
|
return this.keysFromDB[key] || keysFromEnv[key] || ""; |
|
|
} |
|
|
|
|
|
async updateSemaphore() { |
|
|
await this.semaphoreCollection?.updateOne( |
|
|
{ key: Semaphores.CONFIG_UPDATE }, |
|
|
{ |
|
|
$set: { |
|
|
updatedAt: new Date(), |
|
|
}, |
|
|
$setOnInsert: { |
|
|
createdAt: new Date(), |
|
|
}, |
|
|
}, |
|
|
{ upsert: true } |
|
|
); |
|
|
} |
|
|
|
|
|
async set(key: ConfigKey, value: string) { |
|
|
if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled"); |
|
|
await this.configCollection?.updateOne({ key }, { $set: { value } }, { upsert: true }); |
|
|
this.keysFromDB[key] = value; |
|
|
await this.updateSemaphore(); |
|
|
} |
|
|
|
|
|
async delete(key: ConfigKey) { |
|
|
if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled"); |
|
|
await this.configCollection?.deleteOne({ key }); |
|
|
delete this.keysFromDB[key]; |
|
|
await this.updateSemaphore(); |
|
|
} |
|
|
|
|
|
async clear() { |
|
|
if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled"); |
|
|
await this.configCollection?.deleteMany({}); |
|
|
this.keysFromDB = {}; |
|
|
await this.updateSemaphore(); |
|
|
} |
|
|
|
|
|
getPublicConfig() { |
|
|
let config = { |
|
|
...Object.fromEntries( |
|
|
Object.entries(keysFromEnv).filter(([key]) => key.startsWith("PUBLIC_")) |
|
|
), |
|
|
} as Record<PublicConfigKey, string>; |
|
|
|
|
|
if (this.ConfigManagerEnabled) { |
|
|
config = { |
|
|
...config, |
|
|
...Object.fromEntries( |
|
|
Object.entries(this.keysFromDB).filter(([key]) => key.startsWith("PUBLIC_")) |
|
|
), |
|
|
}; |
|
|
} |
|
|
|
|
|
const publicEnvKeys = Object.keys(publicEnv); |
|
|
|
|
|
return Object.fromEntries( |
|
|
Object.entries(config).filter(([key]) => publicEnvKeys.includes(key)) |
|
|
) as Record<PublicConfigKey, string>; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const configManager = new ConfigManager(); |
|
|
|
|
|
export const ready = (async () => { |
|
|
if (!building) { |
|
|
await configManager.init(); |
|
|
} |
|
|
})(); |
|
|
|
|
|
type ExtraConfigKeys = |
|
|
| "HF_TOKEN" |
|
|
| "OLD_MODELS" |
|
|
| "ENABLE_ASSISTANTS" |
|
|
| "METRICS_ENABLED" |
|
|
| "METRICS_PORT" |
|
|
| "MCP_SERVERS" |
|
|
| "MCP_FORWARD_HF_USER_TOKEN" |
|
|
| "EXA_API_KEY"; |
|
|
|
|
|
type ConfigProxy = ConfigManager & { [K in ConfigKey | ExtraConfigKeys]: string }; |
|
|
|
|
|
export const config: ConfigProxy = new Proxy(configManager, { |
|
|
get(target, prop, receiver) { |
|
|
if (prop in target) { |
|
|
return Reflect.get(target, prop, receiver); |
|
|
} |
|
|
if (typeof prop === "string") { |
|
|
return target.get(prop as ConfigKey); |
|
|
} |
|
|
return undefined; |
|
|
}, |
|
|
set(target, prop, value, receiver) { |
|
|
if (prop in target) { |
|
|
return Reflect.set(target, prop, value, receiver); |
|
|
} |
|
|
if (typeof prop === "string") { |
|
|
target.set(prop as ConfigKey, value); |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
}, |
|
|
}) as ConfigProxy; |
|
|
|