/** * Selection and pace maths for the provider limits view, shared by web and * mobile so both agree on which providers show, what "ahead of pace" means, * and how a reset is phrased. * * @module usageLimits */ import { type EnvironmentId, isProviderAvailable, type ServerProvider, type ServerProviderUsageLimits, type ServerProviderUsageWindow, type UsageLimitSourceSnapshot, type UsageLimitSourceSnapshots, } from "@t3tools/contracts"; const MINUTE = 60_000; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; /** * Providers that belong on the Limits view: enabled, installed, and one whose * driver reports subscription usage at all. A driver with no notion of usage * never sets `usageLimits`, so it has no row rather than an empty one. */ export function providersWithLimits( providers: readonly ServerProvider[], ): readonly ServerProvider[] { return providers.filter( (provider) => provider.enabled && provider.installed && isProviderAvailable(provider) && provider.usageLimits !== undefined, ); } export interface LimitsGroup { readonly environmentId: EnvironmentId; /** Null while only one environment is connected; there is nothing to tell apart. */ readonly environmentLabel: string | null; readonly providers: readonly ServerProvider[]; } /** * One group per connected environment with a provider reporting limits. * Provider snapshots come from the config stream every client already holds, * so opening the view costs no extra request. */ export function collectLimitsGroups( presentations: ReadonlyMap< EnvironmentId, { readonly entry: { readonly target: { readonly label: string } }; readonly serverConfig: { readonly providers: readonly ServerProvider[] } | null; } >, ): readonly LimitsGroup[] { const groups: LimitsGroup[] = []; for (const [environmentId, presentation] of presentations) { const providers = providersWithLimits(presentation.serverConfig?.providers ?? []); if (providers.length === 0) continue; groups.push({ environmentId, environmentLabel: presentation.entry.target.label, providers }); } return groups.length > 1 ? groups : groups.map((group) => ({ ...group, environmentLabel: null })); } /** * Every usage-limit source across connected environments, keyed so two * environments pointing at the same hub still get their own rows. The label * carries the environment only when more than one environment has sources. * A native provider with usable limits takes precedence over the same account * in a source, even when it belongs to another connected environment. */ export function collectLimitSources( presentations: ReadonlyMap< EnvironmentId, { readonly entry: { readonly target: { readonly label: string } }; readonly serverConfig: { readonly providers?: readonly ServerProvider[] | undefined; readonly usageLimitSources?: UsageLimitSourceSnapshots | undefined; } | null; } >, ): ReadonlyArray< UsageLimitSourceSnapshot & { readonly key: string; readonly environmentId: EnvironmentId; readonly hiddenAccountCount: number; } > { const nativeAccounts = new Set(); for (const presentation of presentations.values()) { for (const provider of providersWithLimits(presentation.serverConfig?.providers ?? [])) { const key = accountKey(provider.driver, provider.auth.email); if ( key !== null && provider.usageLimits?.windows.length && !provider.usageLimits.unavailable ) { nativeAccounts.add(key); } } } const perEnvironment: Array<{ readonly environmentId: EnvironmentId; readonly environmentLabel: string; readonly sources: UsageLimitSourceSnapshots; }> = []; for (const [environmentId, presentation] of presentations) { const sources = presentation.serverConfig?.usageLimitSources ?? []; if (sources.length === 0) continue; perEnvironment.push({ environmentId, environmentLabel: presentation.entry.target.label, sources, }); } const labelEnvironment = perEnvironment.length > 1; return perEnvironment.flatMap(({ environmentId, environmentLabel, sources }) => sources.map((source) => { const accounts = source.accounts.filter((account) => { const key = accountKey(account.driver, account.email); return key === null || !nativeAccounts.has(key); }); return { ...source, accounts, hiddenAccountCount: source.accounts.length - accounts.length, environmentId, key: `${environmentId}:${source.id}`, label: labelEnvironment ? `${environmentLabel} ยท ${source.label}` : source.label, }; }), ); } function accountKey(driver: ServerProvider["driver"], email: string | undefined): string | null { const normalizedEmail = email?.trim().toLowerCase(); return normalizedEmail ? `${driver}:${normalizedEmail}` : null; } /** The instance's configured name, else the driver's, else its raw kind. */ export function providerLimitsLabel( provider: ServerProvider, driverLabel: (driver: ServerProvider["driver"]) => string | undefined, ): string { return provider.displayName?.trim() || driverLabel(provider.driver) || String(provider.driver); } /** The one-line status under a provider heading when there are no bars to draw. */ export function limitsNotice(limits: ServerProviderUsageLimits): string | null { if (limits.unavailable?.reason === "unsupported") { return limits.unavailable.message ?? "This account has no subscription limits."; } if (limits.unavailable?.reason === "probeFailed") { return limits.unavailable.message ?? "Could not read limits."; } return limits.windows.length === 0 ? "No limits reported." : null; } export function resetMillis(window: ServerProviderUsageWindow): number | null { if (window.resetsAt === undefined) return null; const at = Date.parse(window.resetsAt); return Number.isFinite(at) ? at : null; } /** Elapsed share of the window, 0..1, or null when its length or reset is unknown. */ export function elapsedShare(window: ServerProviderUsageWindow, now: number): number | null { const resetsAt = resetMillis(window); if (resetsAt === null || window.windowDurationMins === undefined) return null; const length = window.windowDurationMins * MINUTE; if (length <= 0) return null; return Math.max(0, Math.min(1, (length - (resetsAt - now)) / length)); } export type LimitPace = "ahead" | "on" | "under"; /** * Usage against the clock. The bar is the whole window, so the elapsed share * is also where even spending would have put the fill; within five points of * it counts as on pace. */ export function paceOf(window: ServerProviderUsageWindow, now: number): LimitPace | null { const elapsed = elapsedShare(window, now); if (elapsed === null) return null; const gap = window.usedPercent - elapsed * 100; if (gap > 5) return "ahead"; if (gap < -5) return "under"; return "on"; } /** `2h 13m`, `3d 4h`, `12m`. */ export function formatDuration(ms: number): string { const remaining = Math.max(0, ms); const days = Math.floor(remaining / DAY); const hours = Math.floor((remaining % DAY) / HOUR); const minutes = Math.floor((remaining % HOUR) / MINUTE); if (days > 0) return `${days}d ${hours}h`; if (hours > 0) return `${hours}h ${minutes}m`; return `${minutes}m`; } /** `resets in 2h 13m`, or null when the window has no reset. */ export function formatResetsIn(window: ServerProviderUsageWindow, now: number): string | null { const resetsAt = resetMillis(window); if (resetsAt === null) return null; return resetsAt <= now ? "resets now" : `resets in ${formatDuration(resetsAt - now)}`; }