/** * Phase 8.3 — Feature flags par provider (`FF_`). * * Motivation : certain upstreams sont fragiles hors de notre contrôle. Rumble * est derrière Cloudflare et peut casser du jour au lendemain ; Odysee dépend * d'un backend NA. Désactiver un provider ne doit pas exiger un redéploiement * ni un revert de code. * * Trois états, pas deux — c'est la distinction qui compte en exploitation : * - unset : le provider est actif (comportement historique) ; * - truthy : actif ; * - falsy : DÉSACTIVÉ, exclu du fan-out et signalé `errors. = disabled_by_ff`. * * Un flag falsy **signale** au lieu de disparaître en silence : une colonne vide * sans explication ressemble à « aucune correspondance », ce qui envoie l'utilisateur * (et le support) chercher au mauvais endroit. * * Convention de nommage : `FF_YT`, `FF_DM`, `FF_TW`, `FF_PT`, `FF_OD`, `FF_RU` * (ids courts, cf. `provider-ids.ts`). */ /** Valeurs explicitement fausses. Tout le reste est vrai (unset inclus). */ const FALSY = new Set(['0', 'false', 'off', 'no', 'disabled']); /** * Lit un flag depuis l'environnement, sans cache : le lire à chaque appel permet * de changer une variable sans redémarrer (utile en local, et ça évite un cache * qui masquerait un changement en CI). * * @param {string} key * @returns {{ set: boolean, enabled: boolean, raw: string|null }} */ function readFlag(key) { const raw = process.env[key]; if (raw === undefined || raw === null || String(raw).trim() === '') { // Unset = actif, et on le dit explicitement pour ne pas confondre // « pas configuré » et « désactivé » dans les logs. return { set: false, enabled: true, raw: null }; } return { set: true, enabled: !FALSY.has(String(raw).trim().toLowerCase()), raw: String(raw) }; } /** * État d'un provider au regard des feature flags. * @param {string} providerId id court ('yt', 'ru', …) */ export function providerFlag(providerId) { const id = String(providerId || '').trim().toLowerCase(); const key = `FF_${id.toUpperCase()}`; const { set, enabled, raw } = readFlag(key); return { provider: id, flag: key, set, enabled, raw }; } /** `true` si le provider est désactivé par un feature flag. */ export function isProviderDisabled(providerId) { return !providerFlag(providerId).enabled; } /** * Filtre une liste de providers, en retirant les désactivés. * * @template {string} T * @param {T[]} providerIds * @returns {{ enabled: T[], disabled: Array<{provider: string, flag: string}> }} */ export function partitionEnabledProviders(providerIds) { const list = Array.isArray(providerIds) ? providerIds : []; const enabled = []; const disabled = []; for (const id of list) { const flag = providerFlag(id); if (flag.enabled) enabled.push(id); else disabled.push({ provider: flag.provider, flag: flag.flag }); } return { enabled, disabled }; } /** * Applique les flags à une liste déjà validée, et renvoie les `errors` à fusionner * dans la réponse de fan-out. Utilisé par `/api/search` ET `/api/suggest` pour * qu'un provider désactivé se comporte pareil partout. * * @template {string} T * @param {T[]} providerIds * @returns {{ providerIds: T[], errors: Record }} */ export function applyProviderFlags(providerIds) { const { enabled, disabled } = partitionEnabledProviders(providerIds); const errors = {}; for (const d of disabled) { errors[d.provider] = { message: `Provider désactivé par le feature flag ${d.flag}`, code: 'disabled_by_ff', }; } return { providerIds: enabled, errors }; }