Files
vibn-frontend/lib/feature-flags.ts

63 lines
1.9 KiB
TypeScript

/**
* Runtime feature flags. Backed by a tiny single-row table so an admin
* can flip a flag and have every Vibn pod pick it up within seconds (no
* redeploy required).
*
* Currently used for:
* - path_b_disabled : kill switch for the Path B AI dev-container
* architecture. When true, shell.exec / fs.* /
* devcontainer.* tools return 503 and the chat
* system prompt falls back to Path A guidance.
*
* See AI_PATH_B_EXECUTION_PLAN.md §7 for the rollback story.
*/
import { query, queryOne } from '@/lib/db-postgres';
let tableReady = false;
async function ensureFlagsTable(): Promise<void> {
if (tableReady) return;
await query(
`CREATE TABLE IF NOT EXISTS fs_feature_flags (
key TEXT PRIMARY KEY,
value JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);`,
[],
);
tableReady = true;
}
const TTL_MS = 10_000;
const cache = new Map<string, { value: any; expires: number }>();
export async function getFlag<T = unknown>(key: string, defaultValue: T): Promise<T> {
const cached = cache.get(key);
if (cached && cached.expires > Date.now()) return cached.value as T;
await ensureFlagsTable();
const row = await queryOne<{ value: T }>(
`SELECT value FROM fs_feature_flags WHERE key = $1 LIMIT 1`,
[key],
);
const value = row?.value ?? defaultValue;
cache.set(key, { value, expires: Date.now() + TTL_MS });
return value;
}
export async function setFlag(key: string, value: unknown): Promise<void> {
await ensureFlagsTable();
await query(
`INSERT INTO fs_feature_flags (key, value, updated_at)
VALUES ($1, $2::jsonb, now())
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value,
updated_at = now()`,
[key, JSON.stringify(value)],
);
cache.delete(key);
}
export async function isPathBDisabled(): Promise<boolean> {
return Boolean(await getFlag<boolean>('path_b_disabled', false));
}