153 lines · 5.0 KB · TypeScript
RawDownload
/** * Multi-environment usage state. * * Every connected environment answers the same typed query; the client merges * the results. Raw transcripts never leave the machine that produced them. * * @module state/usage */import { useAtomValue } from "@effect/atom-react";import {  USAGE_CONTRACT_VERSION,  type EnvironmentId,  type UsageSummary,  type UsageSummaryInput,} from "@t3tools/contracts";import { refreshUsage } from "@t3tools/client-runtime/state/usage";import * as Option from "effect/Option";import { AsyncResult, Atom } from "effect/unstable/reactivity";import { useCallback, useMemo } 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"; export interface EnvironmentUsageStatus {  readonly environmentId: EnvironmentId;  readonly label: string;  readonly isPending: boolean;  readonly error: string | null;  readonly summary: UsageSummary | null;} /** * Reads every environment's summary for one window. * * Keyed by the serialised window so switching ranges does not thrash the atom * cache, and so each environment's query is shared with any other reader of the * same window. */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 result = get(serverEnvironment.usageSummary({ environmentId, input }));      statuses.push({        environmentId,        label: presentation.entry.target.label,        isPending: result.waiting,        error: result._tag === "Failure" ? "This environment could not report usage." : null,        summary: Option.getOrNull(AsyncResult.value(result)),      });    }    return statuses;  }).pipe(Atom.withLabel(`web-usage:window:${windowKey}`)),); export interface UsageView {  readonly merged: MergedUsage;  readonly environments: readonly EnvironmentUsageStatus[];  readonly selectedEnvironments: readonly EnvironmentUsageStatus[];  /** True until at least one selected environment has answered. */  readonly isPending: boolean;  /**   * True while environments that have not failed are still answering. Failed   * environments are reported through their own error rows: totals will not   * improve by waiting on them, so they must not read as "still reporting".   */  readonly isPartial: boolean;  readonly refresh: (input?: UsageSummaryInput) => Promise<void>;} export function useUsage(  input: UsageSummaryInput,  selectedEnvironmentIds: ReadonlySet<EnvironmentId> | null = null,): UsageView {  const windowKey = useMemo(    () =>      JSON.stringify({        sinceDay: input.sinceDay,        untilDay: input.untilDay,        timeZone: input.timeZone,        resolution: input.resolution,        sinceTime: input.sinceTime,        untilTime: input.untilTime,      }),    [      input.sinceDay,      input.untilDay,      input.timeZone,      input.resolution,      input.sinceTime,      input.untilTime,    ],  );  const atom = usageByWindowAtom(windowKey);  const environments = useAtomValue(atom);  const selectedEnvironments = useMemo(    () =>      selectedEnvironmentIds === null        ? environments        : environments.filter((environment) =>            selectedEnvironmentIds.has(environment.environmentId),          ),    [environments, selectedEnvironmentIds],  );   const refresh = useCallback(    (nextInput?: UsageSummaryInput) =>      refreshUsage({        registry: appAtomRegistry,        server: serverEnvironment,        presentations: environmentPresentations,        environmentIds: selectedEnvironments.map(({ environmentId }) => environmentId),        input: nextInput ?? (JSON.parse(windowKey) as UsageSummaryInput),      }),    [selectedEnvironments, windowKey],  );   const merged = useMemo(() => {    const answered: EnvironmentUsage[] = selectedEnvironments.flatMap((environment) =>      environment.summary === null        ? []        : [            {              environmentId: environment.environmentId,              label: environment.label,              summary: environment.summary,            },          ],    );    return mergeUsage(answered, USAGE_CONTRACT_VERSION);  }, [selectedEnvironments]);   const answeredCount = selectedEnvironments.filter(    (environment) => environment.summary !== null,  ).length;  const stillReporting = selectedEnvironments.filter(    (environment) => environment.summary === null && environment.error === null,  ).length;   return {    merged,    environments,    selectedEnvironments,    isPending: answeredCount === 0 && stillReporting > 0,    isPartial: answeredCount > 0 && stillReporting > 0,    refresh,  };}