mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
feat(quota-share): add hooks (usePools + usePoolUsage + useLocalStoragePoolMigration) (B/F9)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import type { QuotaPool, PoolAllocation, Policy } from "@/lib/quota/dimensions";
|
||||
|
||||
const LS_KEY = "omniroute:quota-share:pools";
|
||||
|
||||
// Shape of a legacy localStorage pool (QuotaSharePageClient.tsx old format)
|
||||
interface LsPool {
|
||||
id?: string;
|
||||
connectionId?: string;
|
||||
provider?: string;
|
||||
accountLabel?: string;
|
||||
window?: string;
|
||||
policy?: string;
|
||||
allocations?: Array<{
|
||||
apiKeyId?: string;
|
||||
percent?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface PoolCreate {
|
||||
connectionId: string;
|
||||
name: string;
|
||||
allocations: Array<{
|
||||
apiKeyId: string;
|
||||
weight: number;
|
||||
capValue?: number;
|
||||
capUnit?: string;
|
||||
policy: Policy;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function adaptLsPoolToApiSchema(lsPool: LsPool): PoolCreate {
|
||||
const connectionId = lsPool.connectionId || "";
|
||||
const name =
|
||||
lsPool.accountLabel ||
|
||||
lsPool.provider ||
|
||||
lsPool.connectionId?.slice(0, 12) ||
|
||||
"Migrated pool";
|
||||
const policy: Policy =
|
||||
lsPool.policy === "soft" || lsPool.policy === "burst"
|
||||
? (lsPool.policy as Policy)
|
||||
: "hard";
|
||||
|
||||
const allocations: PoolAllocation[] = (lsPool.allocations || [])
|
||||
.filter((a) => a.apiKeyId)
|
||||
.map((a) => ({
|
||||
apiKeyId: a.apiKeyId as string,
|
||||
weight: typeof a.percent === "number" ? Math.max(0, Math.min(100, a.percent)) : 0,
|
||||
policy,
|
||||
}));
|
||||
|
||||
return { connectionId, name, allocations };
|
||||
}
|
||||
|
||||
export interface UseLocalStoragePoolMigrationInput {
|
||||
pools: QuotaPool[];
|
||||
mutate: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
export function useLocalStoragePoolMigration({
|
||||
pools,
|
||||
mutate,
|
||||
}: UseLocalStoragePoolMigrationInput): void {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const raw = window.localStorage.getItem(LS_KEY);
|
||||
if (!raw) return;
|
||||
|
||||
// Idempotency: if DB already has pools, do not migrate
|
||||
if (pools.length > 0) {
|
||||
// Leave localStorage key intact (safety — let user verify before cleanup)
|
||||
return;
|
||||
}
|
||||
|
||||
let lsPools: unknown[] = [];
|
||||
try {
|
||||
lsPools = JSON.parse(raw) as unknown[];
|
||||
} catch {
|
||||
window.localStorage.removeItem(LS_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(lsPools) || lsPools.length === 0) {
|
||||
window.localStorage.removeItem(LS_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
// POST batch — migrate all pools
|
||||
Promise.all(
|
||||
lsPools.map((p) =>
|
||||
fetch("/api/quota/pools", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(adaptLsPoolToApiSchema(p as LsPool)),
|
||||
}).then((r) => r.ok)
|
||||
)
|
||||
)
|
||||
.then((results) => {
|
||||
if (results.every(Boolean)) {
|
||||
window.localStorage.removeItem(LS_KEY);
|
||||
void mutate();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// fail silent — try again on next load
|
||||
});
|
||||
}, [pools.length, mutate]);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { PoolUsageSnapshot } from "@/lib/quota/types";
|
||||
|
||||
export interface UsePoolUsageResult {
|
||||
usage: PoolUsageSnapshot | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function usePoolUsage(poolId: string, pollIntervalMs = 15_000): UsePoolUsageResult {
|
||||
const [usage, setUsage] = useState<PoolUsageSnapshot | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
const fetchUsage = useCallback(async () => {
|
||||
if (!poolId) return;
|
||||
try {
|
||||
const res = await fetch(`/api/quota/pools/${poolId}/usage`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = (await res.json()) as PoolUsageSnapshot;
|
||||
if (!mountedRef.current) return;
|
||||
setUsage(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
if (!mountedRef.current) return;
|
||||
setError(err instanceof Error ? err.message : "Failed to load usage");
|
||||
} finally {
|
||||
if (mountedRef.current) setLoading(false);
|
||||
}
|
||||
}, [poolId]);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
void fetchUsage();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
void fetchUsage();
|
||||
}, pollIntervalMs);
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [fetchUsage, pollIntervalMs]);
|
||||
|
||||
return { usage, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { QuotaPool } from "@/lib/quota/dimensions";
|
||||
|
||||
export interface UsePoolsResult {
|
||||
pools: QuotaPool[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
mutate: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function usePools(): UsePoolsResult {
|
||||
const [pools, setPools] = useState<QuotaPool[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
const fetchPools = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch("/api/quota/pools");
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
const data: unknown = await res.json();
|
||||
if (!mountedRef.current) return;
|
||||
const list = Array.isArray(data)
|
||||
? (data as QuotaPool[])
|
||||
: Array.isArray((data as { pools?: QuotaPool[] }).pools)
|
||||
? (data as { pools: QuotaPool[] }).pools
|
||||
: [];
|
||||
setPools(list);
|
||||
} catch (err) {
|
||||
if (!mountedRef.current) return;
|
||||
setError(err instanceof Error ? err.message : "Failed to load pools");
|
||||
} finally {
|
||||
if (mountedRef.current) setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
void fetchPools();
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, [fetchPools]);
|
||||
|
||||
const mutate = useCallback(async () => {
|
||||
await fetchPools();
|
||||
}, [fetchPools]);
|
||||
|
||||
return { pools, loading, error, mutate };
|
||||
}
|
||||
Reference in New Issue
Block a user