import { useAtomValue } from "@effect/atom-react"; import { AuthDiagnosticsReadScope, USAGE_CONTRACT_VERSION, type EnvironmentId, type UsageBucket, type UsageSummary, type UsageProviderKind, type UsageSummaryInput, } from "@t3tools/contracts"; import { refreshUsage, usageSummaryInput } from "@t3tools/client-runtime/state/usage"; import { resolveUsageAccess } from "@t3tools/client-runtime/state/usage-access"; import { AsyncResult, Atom } from "effect/reactivity"; import { useState } from "react"; import { mergeUsage, type EnvironmentUsage, type MergedUsage } from "@t3tools/shared/usageMerge"; import { appAtomRegistry } from "../rpc/atomRegistry"; import { environmentPresentations } from "./presentation"; import { serverEnvironment } from "./server"; import { environmentSession, readEnvironmentScope } from "./session"; export interface EnvironmentUsageStatus { readonly environmentId: EnvironmentId; readonly label: string; readonly isPending: boolean; readonly canReadDiagnostics: boolean; readonly isConnected: boolean; readonly error: string | null; readonly summary: UsageSummary | null; } const usageByWindowAtom = Atom.family((windowKey: string) => Atom.make((get): readonly EnvironmentUsageStatus[] => { const input = JSON.parse(windowKey) as UsageSummaryInput; const presentations = get(environmentPresentations.presentationsAtom); const statuses: EnvironmentUsageStatus[] = []; for (const [environmentId, presentation] of presentations) { const isConnected = presentation.connection.phase === "connected"; const sessionResult = get(environmentSession.sessionStateAtom(environmentId)); const access = resolveUsageAccess({ connectionPhase: presentation.connection.phase, session: AsyncResult.getOrElse(sessionResult, () => null), hasSessionError: sessionResult._tag === "Failure", }); if (!access.canReadDiagnostics) { statuses.push({ environmentId, label: presentation.entry.target.label, isConnected, ...access, summary: null, }); continue; } const result = get( serverEnvironment.usageSummary({ environmentId, input: usageSummaryInput( input, get(serverEnvironment.usageModelFilterAtom(environmentId)), ), }), ); const summary = AsyncResult.getOrElse(result, () => null); statuses.push({ environmentId, label: presentation.entry.target.label, isPending: result.waiting, canReadDiagnostics: true, isConnected, error: result._tag === "Failure" ? "This environment could not report usage." : null, summary, }); } return statuses; }).pipe(Atom.withLabel(`web-usage:window:${windowKey}`)), ); export interface UsageView { readonly merged: MergedUsage; readonly environments: readonly EnvironmentUsageStatus[]; readonly selectedEnvironments: readonly EnvironmentUsageStatus[]; readonly isPending: boolean; readonly shown: { readonly window: UsageSummaryInput; readonly merged: MergedUsage } | null; readonly isPartial: boolean; readonly refresh: (input?: UsageSummaryInput) => Promise; } export function mergeAnsweredUsage( environments: readonly EnvironmentUsageStatus[], keepBucket?: (bucket: UsageBucket) => boolean, ): MergedUsage { const answered: EnvironmentUsage[] = environments.flatMap(({ environmentId, label, summary }) => summary === null ? [] : [ { environmentId, label, summary: keepBucket === undefined ? summary : { ...summary, buckets: summary.buckets.filter(keepBucket) }, }, ], ); return mergeUsage(answered, USAGE_CONTRACT_VERSION); } const NO_HIDDEN_PROVIDERS: ReadonlySet = new Set(); function withoutProviders( environments: readonly EnvironmentUsageStatus[], hiddenProviders: ReadonlySet, ): readonly EnvironmentUsageStatus[] { if (hiddenProviders.size === 0) return environments; return environments.map((environment) => environment.summary === null ? environment : { ...environment, summary: { ...environment.summary, buckets: environment.summary.buckets.filter( (bucket) => !hiddenProviders.has(bucket.provider), ), sources: environment.summary.sources.filter( (source) => !hiddenProviders.has(source.fingerprint.provider), ), }, }, ); } function deriveSelection( environments: readonly EnvironmentUsageStatus[], selectedEnvironmentIds: ReadonlySet | null, hiddenProviders: ReadonlySet, ) { const selectedEnvironments = selectedEnvironmentIds === null ? environments : environments.filter((environment) => selectedEnvironmentIds.has(environment.environmentId)); return { environments, selectedEnvironmentIds, hiddenProviders, selectedEnvironments, merged: mergeAnsweredUsage(withoutProviders(selectedEnvironments, hiddenProviders)), }; } export function useUsage( input: UsageSummaryInput, selectedEnvironmentIds: ReadonlySet | null = null, hiddenProviders: ReadonlySet = NO_HIDDEN_PROVIDERS, ): UsageView { const windowKey = JSON.stringify({ sinceDay: input.sinceDay, untilDay: input.untilDay, timeZone: input.timeZone, resolution: input.resolution, sinceTime: input.sinceTime, untilTime: input.untilTime, }); const atom = usageByWindowAtom(windowKey); const environments = useAtomValue(atom); // Identity-based render sync needs a derivation retained by inputs across renders. const [derived, setDerived] = useState(() => deriveSelection(environments, selectedEnvironmentIds, hiddenProviders), ); let current = derived; if ( derived.environments !== environments || derived.selectedEnvironmentIds !== selectedEnvironmentIds || derived.hiddenProviders !== hiddenProviders ) { current = deriveSelection(environments, selectedEnvironmentIds, hiddenProviders); setDerived(current); } const { selectedEnvironments, merged } = current; const refresh = (nextInput?: UsageSummaryInput) => refreshUsage({ registry: appAtomRegistry, server: serverEnvironment, presentations: environmentPresentations, environmentIds: selectedEnvironments .filter( (environment) => environment.canReadDiagnostics && readEnvironmentScope(environment.environmentId, AuthDiagnosticsReadScope), ) .map(({ environmentId }) => environmentId), input: nextInput ?? (JSON.parse(windowKey) as UsageSummaryInput), }); const answeredCount = selectedEnvironments.filter( (environment) => environment.summary !== null, ).length; const stillReporting = selectedEnvironments.filter( (environment) => environment.summary === null && environment.error === null, ).length; const isPending = answeredCount === 0 && stillReporting > 0; const [lastAnswered, setLastAnswered] = useState< | (NonNullable & { readonly selection: typeof selectedEnvironmentIds; readonly hidden: typeof hiddenProviders; }) | null >(null); if ( answeredCount > 0 && (lastAnswered?.merged !== merged || lastAnswered.window !== input || lastAnswered.selection !== selectedEnvironmentIds || lastAnswered.hidden !== hiddenProviders) ) { setLastAnswered({ window: input, merged, selection: selectedEnvironmentIds, hidden: hiddenProviders, }); } const kept = lastAnswered?.selection === selectedEnvironmentIds && lastAnswered.hidden === hiddenProviders ? lastAnswered : null; const shown = answeredCount > 0 ? { window: input, merged } : (kept ?? (isPending ? null : { window: input, merged })); return { merged, environments, selectedEnvironments, isPending, shown, isPartial: answeredCount > 0 && stillReporting > 0, refresh, }; }