1import type {
2 EnvironmentId,
3 ServerProvider,
4 UsageSummary,
5 UsageSummaryInput,
6} from "@t3tools/contracts";
7import * as Schema from "effect/Schema";
8import type { AtomRegistry } from "effect/reactivity";
9
10import { EnvironmentRpcUnavailableError } from "../rpc/client.ts";
11import type { createEnvironmentPresentationAtoms } from "./presentation.ts";
12import { executeAtomQuery, runAtomCommand, squashAtomCommandFailure } from "./runtime.ts";
13import type { createServerEnvironmentAtoms } from "./server.ts";
14
15const isEnvironmentRpcUnavailable = Schema.is(EnvironmentRpcUnavailableError);
16
17/** Offer the Cursor Keychain prompt only where a working Cursor provider could use it. */
18export function needsCursorKeychainAccess(
19 summary: UsageSummary | null,
20 providers: readonly ServerProvider[] | null,
21): boolean {
22 return (
23 summary?.sources.some((source) => source.action === "enableCursorKeychain") === true &&
24 providers?.some((provider) => provider.driver === "cursor" && provider.status === "ready") ===
25 true
26 );
27}
28
29/**
30 * Environments to offer the Cursor Keychain prompt in the usage summary.
31 *
32 * Any environment reading a Cursor account already reports that account's
33 * history from every machine, so the prompt only adds duplicates there. A
34 * different account on another machine stays reachable from provider settings.
35 */
36export function cursorKeychainAccessEnvironments<
37 E extends { readonly summary: UsageSummary | null; readonly needsCursorKeychainAccess: boolean },
38>(environments: readonly E[]): readonly E[] {
39 const hasCursorAccount = environments.some((environment) =>
40 environment.summary?.sources.some(
41 (source) =>
42 source.fingerprint.provider === "cursor" && source.fingerprint.hostId === "cursor.com",
43 ),
44 );
45 return hasCursorAccount
46 ? []
47 : environments.filter((environment) => environment.needsCursorKeychainAccess);
48}
49
50const limitsRefreshAfter = new Map<EnvironmentId, number>();
51const limitsRefreshes = new Map<EnvironmentId, Promise<unknown>>();
52
53export async function refreshUsageLimits<A>(
54 environmentId: EnvironmentId,
55 refresh: () => Promise<A>,
56 automatic = false,
57 afterPending = false,
58): Promise<A | undefined> {
59 const pending = limitsRefreshes.get(environmentId);
60 if (pending !== undefined) {
61 if (afterPending) {
62 try {
63 await pending;
64 } catch {
65 // The new check still needs to run if the earlier one failed.
66 }
67 return refreshUsageLimits(environmentId, refresh, false, true);
68 }
69 // Manual refresh waits for the current check; automatic refresh does not repeat it.
70 return automatic ? undefined : ((await pending) as A);
71 }
72 const refreshAfter = limitsRefreshAfter.get(environmentId) ?? 0;
73 // @effect-diagnostics-next-line globalDate:off
74 if (automatic && Date.now() < refreshAfter) return;
75 const current = Promise.resolve()
76 .then(refresh)
77 .finally(() => {
78 limitsRefreshes.delete(environmentId);
79 // @effect-diagnostics-next-line globalDate:off
80 limitsRefreshAfter.set(environmentId, Date.now() + 5 * 60_000);
81 });
82 limitsRefreshes.set(environmentId, current);
83 return await current;
84}
85
86/** Refresh pricing, then await each selected environment's rescan while it remains connected. */
87export async function refreshUsage({
88 registry,
89 server,
90 presentations,
91 environmentIds,
92 input,
93}: {
94 registry: AtomRegistry.AtomRegistry;
95 server: Pick<
96 ReturnType<typeof createServerEnvironmentAtoms>,
97 "usageSummary" | "refreshUsageRates"
98 >;
99 presentations: Pick<ReturnType<typeof createEnvironmentPresentationAtoms>, "presentationAtom">;
100 environmentIds: readonly EnvironmentId[];
101 input: UsageSummaryInput;
102}): Promise<void> {
103 await Promise.all(
104 environmentIds.map(async (environmentId) => {
105 const query = server.usageSummary({ environmentId, input });
106 const presentation = presentations.presentationAtom(environmentId);
107 const controller = new AbortController();
108 const abortWhenDisconnected = () => {
109 if (registry.get(presentation)?.connection.phase !== "connected") controller.abort();
110 };
111 const unsubscribe = registry.subscribe(presentation, abortWhenDisconnected);
112 abortWhenDisconnected();
113 try {
114 const ratesResult = await runAtomCommand(
115 registry,
116 server.refreshUsageRates,
117 { environmentId, input: {} },
118 { reportFailure: false },
119 );
120 const sessionUnavailable =
121 ratesResult._tag === "Failure" &&
122 isEnvironmentRpcUnavailable(squashAtomCommandFailure(ratesResult));
123 // Invalidate even on failure so reconnects cannot reuse the old summary.
124 registry.refresh(query);
125 if (sessionUnavailable || controller.signal.aborted) return;
126 await executeAtomQuery(registry, query, {
127 reportFailure: false,
128 signal: controller.signal,
129 });
130 } finally {
131 unsubscribe();
132 }
133 }),
134 );
135}