refactor: remove dynamic config legacy fallback
This commit is contained in:
parent
5bba149a7b
commit
b4d312ce74
@ -424,13 +424,12 @@ export function applyDynamicConfigEntries(data: Array<{ key: string; value: unkn
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads global configuration from Cosmos-first dynamic config storage to override .env defaults.
|
* Loads global configuration from Cosmos-backed dynamic config storage to override .env defaults.
|
||||||
* Falls back to the legacy Supabase table during migration.
|
|
||||||
*/
|
*/
|
||||||
export const loadDynamicConfig = async (supabase?: any) => {
|
export const loadDynamicConfig = async () => {
|
||||||
try {
|
try {
|
||||||
logger.info('--- Loading Dynamic Global Config from control-plane storage ---');
|
logger.info('--- Loading Dynamic Global Config from control-plane storage ---');
|
||||||
const data = await listDynamicConfigEntries(supabase);
|
const data = await listDynamicConfigEntries();
|
||||||
|
|
||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
const loadedKeys = applyDynamicConfigEntries(data);
|
const loadedKeys = applyDynamicConfigEntries(data);
|
||||||
|
|||||||
@ -27,13 +27,13 @@ async function main() {
|
|||||||
validateConfig();
|
validateConfig();
|
||||||
|
|
||||||
// --- 0. Primary Account Setup (for Market Data) ---
|
// --- 0. Primary Account Setup (for Market Data) ---
|
||||||
await loadDynamicConfig(supabaseService);
|
await loadDynamicConfig();
|
||||||
let dynamicConfigRefreshInFlight = false;
|
let dynamicConfigRefreshInFlight = false;
|
||||||
const refreshDynamicConfig = async () => {
|
const refreshDynamicConfig = async () => {
|
||||||
if (dynamicConfigRefreshInFlight) return;
|
if (dynamicConfigRefreshInFlight) return;
|
||||||
dynamicConfigRefreshInFlight = true;
|
dynamicConfigRefreshInFlight = true;
|
||||||
try {
|
try {
|
||||||
await loadDynamicConfig(supabaseService);
|
await loadDynamicConfig();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`[Config] Dynamic refresh failed: ${error.message || error}`);
|
logger.error(`[Config] Dynamic refresh failed: ${error.message || error}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -1969,7 +1969,7 @@ export class ApiServer {
|
|||||||
|
|
||||||
this.app.get('/api/admin/config/dynamic', this.requireAuth, this.requireAdmin, async (_req, res) => {
|
this.app.get('/api/admin/config/dynamic', this.requireAuth, this.requireAdmin, async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const items = await listDynamicConfigEntries(supabaseService);
|
const items = await listDynamicConfigEntries();
|
||||||
res.json({ items });
|
res.json({ items });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
res.status(500).json({ error: `Failed to load dynamic config: ${error.message}` });
|
res.status(500).json({ error: `Failed to load dynamic config: ${error.message}` });
|
||||||
@ -1979,9 +1979,9 @@ export class ApiServer {
|
|||||||
this.app.put('/api/admin/config/dynamic', this.requireAuth, this.requireAdmin, async (req, res) => {
|
this.app.put('/api/admin/config/dynamic', this.requireAuth, this.requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const items = Array.isArray(req.body?.items) ? req.body.items : [];
|
const items = Array.isArray(req.body?.items) ? req.body.items : [];
|
||||||
await upsertDynamicConfigEntries(items, supabaseService);
|
await upsertDynamicConfigEntries(items);
|
||||||
applyDynamicConfigEntries(items);
|
applyDynamicConfigEntries(items);
|
||||||
await loadDynamicConfig(supabaseService);
|
await loadDynamicConfig();
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
res.status(500).json({ error: `Failed to update dynamic config: ${error.message}` });
|
res.status(500).json({ error: `Failed to update dynamic config: ${error.message}` });
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { getContainer } from '@bytelyst/cosmos';
|
import { getContainer } from '@bytelyst/cosmos';
|
||||||
import { config } from '../config/index.js';
|
import { config } from '../config/index.js';
|
||||||
import logger from '../utils/logger.js';
|
import logger from '../utils/logger.js';
|
||||||
import type { supabaseService } from './SupabaseService.js';
|
|
||||||
|
|
||||||
export interface DynamicConfigEntry {
|
export interface DynamicConfigEntry {
|
||||||
key: string;
|
key: string;
|
||||||
@ -16,8 +15,6 @@ interface DynamicConfigDocument extends DynamicConfigEntry {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type LegacySupabaseService = typeof supabaseService;
|
|
||||||
|
|
||||||
const CONTAINER_NAME = 'dynamic_config';
|
const CONTAINER_NAME = 'dynamic_config';
|
||||||
|
|
||||||
function isCosmosConfigured(): boolean {
|
function isCosmosConfigured(): boolean {
|
||||||
@ -55,92 +52,21 @@ async function listFromCosmos(): Promise<DynamicConfigEntry[]> {
|
|||||||
.filter((entry): entry is DynamicConfigEntry => Boolean(entry));
|
.filter((entry): entry is DynamicConfigEntry => Boolean(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listFromSupabase(legacyService?: LegacySupabaseService): Promise<DynamicConfigEntry[]> {
|
export async function listDynamicConfigEntries(): Promise<DynamicConfigEntry[]> {
|
||||||
const client = legacyService?.getClient?.();
|
|
||||||
if (!client) return [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data, error } = await client
|
|
||||||
.from('bot_config')
|
|
||||||
.select('*')
|
|
||||||
.order('key');
|
|
||||||
if (error || !Array.isArray(data)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
.map((row) => normalizeEntry(row as DynamicConfigEntry))
|
|
||||||
.filter((entry): entry is DynamicConfigEntry => Boolean(entry));
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`[DynamicConfig] Legacy Supabase read failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function seedCosmosFromLegacy(legacyService?: LegacySupabaseService): Promise<DynamicConfigEntry[]> {
|
|
||||||
const legacyEntries = await listFromSupabase(legacyService);
|
|
||||||
if (!legacyEntries.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
const container = getContainer(CONTAINER_NAME);
|
|
||||||
await Promise.all(legacyEntries.map((entry) => container.items.upsert<DynamicConfigDocument>({
|
|
||||||
id: entry.key,
|
|
||||||
productId: config.PRODUCT_ID,
|
|
||||||
key: entry.key,
|
|
||||||
value: entry.value,
|
|
||||||
description: entry.description || '',
|
|
||||||
updated_at: entry.updated_at || now,
|
|
||||||
updatedAt: entry.updated_at || now,
|
|
||||||
})));
|
|
||||||
|
|
||||||
logger.info(`[DynamicConfig] Seeded ${legacyEntries.length} config entries from legacy store into Cosmos.`);
|
|
||||||
return legacyEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function mirrorToSupabase(entries: DynamicConfigEntry[], legacyService?: LegacySupabaseService): Promise<void> {
|
|
||||||
const client = legacyService?.getClient?.();
|
|
||||||
if (!client || entries.length === 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = entries.map((entry) => ({
|
|
||||||
key: entry.key,
|
|
||||||
value: entry.value,
|
|
||||||
description: entry.description || '',
|
|
||||||
updated_at: entry.updated_at || new Date().toISOString(),
|
|
||||||
}));
|
|
||||||
const { error } = await client
|
|
||||||
.from('bot_config')
|
|
||||||
.upsert(payload, { onConflict: 'key' });
|
|
||||||
if (error) {
|
|
||||||
logger.warn(`[DynamicConfig] Legacy Supabase mirror failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`[DynamicConfig] Legacy Supabase mirror failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listDynamicConfigEntries(legacyService?: LegacySupabaseService): Promise<DynamicConfigEntry[]> {
|
|
||||||
if (!isCosmosConfigured()) {
|
if (!isCosmosConfigured()) {
|
||||||
return listFromSupabase(legacyService);
|
logger.warn('[DynamicConfig] Cosmos is not configured; dynamic config overrides are unavailable in this environment.');
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cosmosEntries = await listFromCosmos();
|
return await listFromCosmos();
|
||||||
if (cosmosEntries.length > 0) {
|
|
||||||
return cosmosEntries;
|
|
||||||
}
|
|
||||||
return await seedCosmosFromLegacy(legacyService);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`[DynamicConfig] Cosmos read/seed failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
logger.warn(`[DynamicConfig] Cosmos read failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upsertDynamicConfigEntries(
|
export async function upsertDynamicConfigEntries(entries: DynamicConfigEntry[]): Promise<void> {
|
||||||
entries: DynamicConfigEntry[],
|
|
||||||
legacyService?: LegacySupabaseService
|
|
||||||
): Promise<void> {
|
|
||||||
const normalized = entries
|
const normalized = entries
|
||||||
.map((entry) => normalizeEntry(entry))
|
.map((entry) => normalizeEntry(entry))
|
||||||
.filter((entry): entry is DynamicConfigEntry => Boolean(entry));
|
.filter((entry): entry is DynamicConfigEntry => Boolean(entry));
|
||||||
@ -150,22 +76,23 @@ export async function upsertDynamicConfigEntries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
if (isCosmosConfigured()) {
|
if (!isCosmosConfigured()) {
|
||||||
try {
|
logger.warn('[DynamicConfig] Cosmos is not configured; skipping dynamic config upsert.');
|
||||||
const container = getContainer(CONTAINER_NAME);
|
return;
|
||||||
await Promise.all(normalized.map((entry) => container.items.upsert<DynamicConfigDocument>({
|
|
||||||
id: entry.key,
|
|
||||||
productId: config.PRODUCT_ID,
|
|
||||||
key: entry.key,
|
|
||||||
value: entry.value,
|
|
||||||
description: entry.description || '',
|
|
||||||
updated_at: entry.updated_at || now,
|
|
||||||
updatedAt: entry.updated_at || now,
|
|
||||||
})));
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`[DynamicConfig] Cosmos upsert failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await mirrorToSupabase(normalized, legacyService);
|
try {
|
||||||
|
const container = getContainer(CONTAINER_NAME);
|
||||||
|
await Promise.all(normalized.map((entry) => container.items.upsert<DynamicConfigDocument>({
|
||||||
|
id: entry.key,
|
||||||
|
productId: config.PRODUCT_ID,
|
||||||
|
key: entry.key,
|
||||||
|
value: entry.value,
|
||||||
|
description: entry.description || '',
|
||||||
|
updated_at: entry.updated_at || now,
|
||||||
|
updatedAt: entry.updated_at || now,
|
||||||
|
})));
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[DynamicConfig] Cosmos upsert failed: ${error instanceof Error ? error.message : 'unknown error'}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,7 @@ Rule:
|
|||||||
- platform-service and Cosmos are the only supported production systems for this repo
|
- platform-service and Cosmos are the only supported production systems for this repo
|
||||||
- legacy repos may still be consulted as code references, but they are not runtime dependencies
|
- legacy repos may still be consulted as code references, but they are not runtime dependencies
|
||||||
- trading user profiles, dynamic config, trading controls, snapshots, capital ledgers, and strategy presets already use Cosmos-backed authority paths
|
- trading user profiles, dynamic config, trading controls, snapshots, capital ledgers, and strategy presets already use Cosmos-backed authority paths
|
||||||
|
- dynamic config runtime refresh and admin updates no longer seed from or mirror to legacy storage in the active backend runtime path
|
||||||
|
|
||||||
## Verification Standard
|
## Verification Standard
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ It assumes:
|
|||||||
- [x] Mobile migrated into `mobile/` with product identity, shared runtime bootstrap, launch-time kill-switch gate, platform-service auth, live backend polling plus websocket-backed updates, startup/error telemetry capture, secure session storage with invalidation handling, and explicit degraded/offline status surfacing
|
- [x] Mobile migrated into `mobile/` with product identity, shared runtime bootstrap, launch-time kill-switch gate, platform-service auth, live backend polling plus websocket-backed updates, startup/error telemetry capture, secure session storage with invalidation handling, and explicit degraded/offline status surfacing
|
||||||
- [x] Backend now accepts common-platform JWTs and persists global trading-control state through Cosmos-backed control storage
|
- [x] Backend now accepts common-platform JWTs and persists global trading-control state through Cosmos-backed control storage
|
||||||
- [x] Dynamic config now flows through backend control-plane APIs with Cosmos-backed storage
|
- [x] Dynamic config now flows through backend control-plane APIs with Cosmos-backed storage
|
||||||
|
- [x] Dynamic config runtime loading and admin writes are now Cosmos-only in the new backend runtime path
|
||||||
- [x] Backend snapshots now use a Cosmos-backed repository
|
- [x] Backend snapshots now use a Cosmos-backed repository
|
||||||
- [x] Distributed entry and reconciliation locks now use a Cosmos-backed repository
|
- [x] Distributed entry and reconciliation locks now use a Cosmos-backed repository
|
||||||
- [x] Capital ledger persistence now uses a Cosmos-backed repository
|
- [x] Capital ledger persistence now uses a Cosmos-backed repository
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user