1import { useAtomValue } from "@effect/atom-react";
2import {
3 AuthDiagnosticsReadScope,
4 USAGE_CONTRACT_VERSION,
5 type EnvironmentId,
6 type UsageSummary,
7 type UsageSummaryInput,
8} from "@t3tools/contracts";
9import { refreshUsage, usageSummaryInput } from "@t3tools/client-runtime/state/usage";
10import { resolveUsageAccess } from "@t3tools/client-runtime/state/usage-access";
11import { mergeUsage, type EnvironmentUsage, type MergedUsage } from "@t3tools/shared/usageMerge";
12import { AsyncResult, Atom } from "effect/reactivity";
13import { useCallback, useMemo } from "react";
14
15import { appAtomRegistry } from "./atom-registry";
16import { environmentPresentations } from "./presentation";
17import { serverEnvironment } from "./server";
18import { environmentSession, readEnvironmentScope } from "./session";
19
20export interface EnvironmentUsageStatus {
21 readonly environmentId: EnvironmentId;
22 readonly label: string;
23 readonly isPending: boolean;
24 readonly isConnected: boolean;
25 readonly canReadDiagnostics: boolean;
26 readonly error: string | null;
27 readonly summary: UsageSummary | null;
28}
29
30const usageByWindowAtom = Atom.family((windowKey: string) =>
31 Atom.make((get): readonly EnvironmentUsageStatus[] => {
32 const input = JSON.parse(windowKey) as UsageSummaryInput;
33 const presentations = get(environmentPresentations.presentationsAtom);
34
35 const statuses: EnvironmentUsageStatus[] = [];
36 for (const [environmentId, presentation] of presentations) {
37 const sessionResult = get(environmentSession.sessionStateAtom(environmentId));
38 const access = resolveUsageAccess({
39 connectionPhase: presentation.connection.phase,
40 session: AsyncResult.getOrElse(sessionResult, () => null),
41 hasSessionError: sessionResult._tag === "Failure",
42 });
43 if (!access.canReadDiagnostics) {
44 statuses.push({
45 environmentId,
46 label: presentation.entry.target.label,
47 isConnected: presentation.connection.phase === "connected",
48 ...access,
49 summary: null,
50 });
51 continue;
52 }
53 const result = get(
54 serverEnvironment.usageSummary({
55 environmentId,
56 input: usageSummaryInput(
57 input,
58 get(serverEnvironment.usageModelFilterAtom(environmentId)),
59 ),
60 }),
61 );
62 const summary = AsyncResult.getOrElse(result, () => null);
63 statuses.push({
64 environmentId,
65 label: presentation.entry.target.label,
66 isPending: result.waiting,
67 isConnected: presentation.connection.phase === "connected",
68 canReadDiagnostics: true,
69 error: result._tag === "Failure" ? "This environment could not report usage." : null,
70 summary,
71 });
72 }
73 return statuses;
74 }).pipe(Atom.withLabel(`mobile-usage:window:${windowKey}`)),
75);
76
77export interface UsageView {
78 readonly merged: MergedUsage;
79 readonly environments: readonly EnvironmentUsageStatus[];
80 readonly selectedEnvironments: readonly EnvironmentUsageStatus[];
81
82 readonly isPending: boolean;
83
84 readonly isPartial: boolean;
85 readonly refresh: (input?: UsageSummaryInput) => Promise<void>;
86}
87
88export function useUsage(
89 input: UsageSummaryInput,
90 selectedEnvironmentIds: ReadonlySet<EnvironmentId> | null = null,
91): UsageView {
92 const windowKey = useMemo(
93 () =>
94 JSON.stringify({
95 sinceDay: input.sinceDay,
96 untilDay: input.untilDay,
97 timeZone: input.timeZone,
98 resolution: input.resolution,
99 sinceTime: input.sinceTime,
100 untilTime: input.untilTime,
101 }),
102 [
103 input.sinceDay,
104 input.untilDay,
105 input.timeZone,
106 input.resolution,
107 input.sinceTime,
108 input.untilTime,
109 ],
110 );
111 const atom = usageByWindowAtom(windowKey);
112 const environments = useAtomValue(atom);
113 const selectedEnvironments = useMemo(
114 () =>
115 selectedEnvironmentIds === null
116 ? environments
117 : environments.filter(({ environmentId }) => selectedEnvironmentIds.has(environmentId)),
118 [environments, selectedEnvironmentIds],
119 );
120
121 const refresh = useCallback(
122 (nextInput?: UsageSummaryInput) =>
123 refreshUsage({
124 registry: appAtomRegistry,
125 server: serverEnvironment,
126 presentations: environmentPresentations,
127
128 environmentIds: selectedEnvironments
129 .filter(
130 (environment) =>
131 environment.canReadDiagnostics &&
132 readEnvironmentScope(environment.environmentId, AuthDiagnosticsReadScope),
133 )
134 .map(({ environmentId }) => environmentId),
135 input: nextInput ?? (JSON.parse(windowKey) as UsageSummaryInput),
136 }),
137 [selectedEnvironments, windowKey],
138 );
139
140 const merged = useMemo(() => {
141 const answered: EnvironmentUsage[] = selectedEnvironments.flatMap((environment) =>
142 environment.summary === null
143 ? []
144 : [
145 {
146 environmentId: environment.environmentId,
147 label: environment.label,
148 summary: environment.summary,
149 },
150 ],
151 );
152 return mergeUsage(answered, USAGE_CONTRACT_VERSION);
153 }, [selectedEnvironments]);
154
155 const answeredCount = selectedEnvironments.filter(
156 (environment) => environment.summary !== null,
157 ).length;
158 const stillReporting = selectedEnvironments.filter(
159 (environment) => environment.summary === null && environment.error === null,
160 ).length;
161
162 return {
163 merged,
164 environments,
165 selectedEnvironments,
166 isPending: answeredCount === 0 && stillReporting > 0,
167 isPartial: answeredCount > 0 && stillReporting > 0,
168 refresh,
169 };
170}