Files
IPFS-portal/src/lib/swr.ts
T

165 lines
6.1 KiB
TypeScript
Raw Normal View History

/* ── SWR Data Fetching Hooks ──
*
* Gelijktijdige data-fetching met auto-refresh, caching en revalidation.
* Vervangt handmatige useEffect + useState + setInterval patronen.
*
* Alles fetched via de Next.js API proxy (krijgt cookies/sessie mee).
*/
import useSWR from 'swr';
import useSWRInfinite from 'swr/infinite';
import type { IPFSPin, RepoStats, IPFSNodeInfo, IPFSUser } from './api';
/* ════════════════════════════ Fetcher ════════════════════════════ */
const API_BASE = '';
async function fetcher<T>(path: string): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, { credentials: 'include' });
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`${res.status}: ${text.slice(0, 200)}`);
}
return res.json();
}
/* ════════════════════════════ Config ════════════════════════════ */
const DEFAULT_REFRESH = 15_000; // 15 seconden
/* ════════════════════════════ Pins ════════════════════════════ */
export function usePins(refreshInterval = DEFAULT_REFRESH) {
return useSWR<IPFSPin[]>('/api/pins', fetcher, {
refreshInterval,
revalidateOnFocus: true,
});
}
/* ════════════════════════════ Peers ════════════════════════════ */
export interface Peer {
id: string;
addr: string;
latency: string;
}
export function usePeers(refreshInterval = DEFAULT_REFRESH) {
return useSWR<Peer[]>('/api/peers', async (path) => {
const raw = await fetcher<{ Peers?: { Peer: string; Addr: string; Latency: string }[] }>(path);
return (raw.Peers || []).map((p) => ({
id: p.Peer,
addr: p.Addr,
latency: p.Latency || '—',
}));
}, {
refreshInterval,
revalidateOnFocus: true,
});
}
/* ════════════════════════════ Node Info ════════════════════════════ */
export function useNodeInfo(refreshInterval?: number) {
return useSWR<IPFSNodeInfo>('/api/node/info', fetcher, {
refreshInterval,
revalidateOnFocus: true,
});
}
/* ════════════════════════════ Repo Stats ════════════════════════════ */
export function useRepoStats(refreshInterval = DEFAULT_REFRESH) {
return useSWR<RepoStats>('/api/repo/stats', async (path) => {
const raw = await fetcher<{
RepoSize?: number; StorageMax?: number; NumObjects?: number;
RepoPath?: string; Version?: string;
}>(path);
return {
repoSize: raw.RepoSize ?? 0,
storageMax: raw.StorageMax ?? 0,
numObjects: raw.NumObjects ?? 0,
repoPath: raw.RepoPath ?? '',
version: raw.Version ?? '',
};
}, {
refreshInterval,
revalidateOnFocus: true,
});
}
/* ════════════════════════════ Dashboard (alles-in-1) ════════════════════════════ */
export interface DashboardData {
peers: Peer[];
pins: IPFSPin[];
repo: RepoStats | null;
bw: null;
health: { status: string; node: string } | null;
}
export function useDashboard(refreshInterval = DEFAULT_REFRESH) {
return useSWR<DashboardData>('/api/health', async (healthPath) => {
const [health, peers, pins, repo] = await Promise.all([
fetcher<{ status: string; node: string }>(healthPath).catch(() => null as any),
fetcher<{ Peers?: { Peer: string; Addr: string; Latency: string }[] }>('/api/peers')
.then((raw) => (raw.Peers || []).map((p) => ({ id: p.Peer, addr: p.Addr, latency: p.Latency || '—' })))
.catch(() => [] as Peer[]),
fetcher<{ Keys?: Record<string, { Type: string }> }>('/api/pins')
.then((raw) => raw.Keys ? Object.entries(raw.Keys).map(([cid]) => ({ cid, name: '', size: 0, created: '' })) : [])
.catch(() => [] as IPFSPin[]),
fetcher<{ RepoSize?: number; StorageMax?: number; NumObjects?: number }>('/api/repo/stats')
.then((raw) => ({ repoSize: raw.RepoSize ?? 0, storageMax: raw.StorageMax ?? 0, numObjects: raw.NumObjects ?? 0, repoPath: '', version: '' }))
.catch(() => null),
]);
return { peers, pins, repo, bw: null, health };
}, {
refreshInterval,
revalidateOnFocus: true,
});
}
/* ════════════════════════════ Daily Stats (paginated) ════════════════════════════ */
export interface DailyStat {
date: string;
uploads: number;
bytesAdded: number;
bytesRemoved: number;
}
const PAGE_SIZE = 30;
export function useDailyStats() {
return useSWRInfinite<DailyStat[]>(
(pageIndex, previousData) => {
if (previousData && previousData.length < PAGE_SIZE) return null;
return `/api/stats/daily?page=${pageIndex}&limit=${PAGE_SIZE}`;
},
fetcher,
{ revalidateFirstPage: false },
);
}
/* ════════════════════════════ Health ════════════════════════════ */
export function useHealth() {
return useSWR<{ status: string; node: string }>('/api/health', fetcher, {
refreshInterval: 30_000,
});
}
/* ════════════════════════════ Users (admin) ════════════════════════════ */
export function useUsers() {
return useSWR<IPFSUser[]>('/api/users', async (path) => {
const raw = await fetcher<IPFSUser[] | { users: IPFSUser[] }>(path);
if (Array.isArray(raw)) return raw;
if (raw && Array.isArray(raw.users)) return raw.users;
return [];
}, {
refreshInterval: 30_000,
});
}