| 1 | import { |
| 2 | collectLimitAccounts, |
| 3 | collectLimitPools, |
| 4 | type LimitAccount, |
| 5 | type LimitPresentations, |
| 6 | } from "@t3tools/shared/usageLimits"; |
| 7 | |
| 8 | export interface SubscriptionUsageSnapshot { |
| 9 | url?: string; |
| 10 | checkedAt: number; |
| 11 | providers: Array<{ |
| 12 | name: string; |
| 13 | detail: string; |
| 14 | windows: Array<{ kind?: string; label: string; remaining: number; reset: string }>; |
| 15 | expiresAt: number; |
| 16 | totalWindows: number; |
| 17 | }>; |
| 18 | } |
| 19 | |
| 20 | // Snapshots expire after 15 minutes; background refresh needs a |
| 21 | // separate authenticated transport while the mobile app is suspended. |
| 22 | const SNAPSHOT_MAX_AGE = 15 * 60_000; |
| 23 | export const WIDGET_REFRESH_INTERVAL = 5 * 60_000; |
| 24 | |
| 25 | /** Bound probes across config updates, reconnects, and foreground transitions. */ |
| 26 | export function createWidgetRefresher<Id>(refresh: (id: Id) => Promise<unknown>) { |
| 27 | const attempted = new Map<Id, number>(); |
| 28 | const pending = new Set<Id>(); |
| 29 | return async (connected: readonly Id[], now: number) => { |
| 30 | await Promise.allSettled( |
| 31 | connected.map(async (id) => { |
| 32 | if (pending.has(id) || now - (attempted.get(id) ?? -Infinity) < WIDGET_REFRESH_INTERVAL) |
| 33 | return; |
| 34 | attempted.set(id, now); |
| 35 | pending.add(id); |
| 36 | try { |
| 37 | await refresh(id); |
| 38 | } finally { |
| 39 | pending.delete(id); |
| 40 | } |
| 41 | }), |
| 42 | ); |
| 43 | }; |
| 44 | } |
| 45 | |
| 46 | function subscriptionUsageProps( |
| 47 | accounts: readonly LimitAccount[], |
| 48 | now: number, |
| 49 | configuredDrivers: ReadonlySet<string>, |
| 50 | maxWindowsPerProvider: number, |
| 51 | ): SubscriptionUsageSnapshot { |
| 52 | const pools = collectLimitPools(accounts, now); |
| 53 | const checked = accounts |
| 54 | .filter((account) => account.driver === "codex" || account.driver === "claudeAgent") |
| 55 | .map((account) => Date.parse(account.limits.checkedAt)); |
| 56 | return { |
| 57 | checkedAt: checked.length > 0 && checked.every(Number.isFinite) ? Math.min(...checked) : 0, |
| 58 | providers: (["codex", "claudeAgent"] as const) |
| 59 | .filter( |
| 60 | (driver) => |
| 61 | configuredDrivers.has(driver) || accounts.some((account) => account.driver === driver), |
| 62 | ) |
| 63 | .map((driver) => { |
| 64 | const pool = pools.find((candidate) => candidate.driver === driver); |
| 65 | const name = driver === "codex" ? "Codex" : "Claude"; |
| 66 | if (!pool) |
| 67 | return { |
| 68 | name, |
| 69 | detail: "No limits available", |
| 70 | windows: [], |
| 71 | expiresAt: 0, |
| 72 | totalWindows: 0, |
| 73 | }; |
| 74 | const checkedAt = Math.min(...pool.accounts.map((a) => Date.parse(a.limits.checkedAt))); |
| 75 | const expiresAt = Math.min( |
| 76 | checkedAt + SNAPSHOT_MAX_AGE, |
| 77 | ...pool.windows.flatMap((window) => window.resets.map((reset) => reset.at)), |
| 78 | ); |
| 79 | const fresh = Number.isFinite(expiresAt) && expiresAt > now; |
| 80 | const sortedWindows = [...pool.windows].sort( |
| 81 | (a, b) => a.remainingPercent - b.remainingPercent, |
| 82 | ); |
| 83 | // Keep a session and weekly limit when scoped limits fill the storage budget. |
| 84 | const selectedWindows = [ |
| 85 | ...new Set([ |
| 86 | sortedWindows.find((window) => window.kind === "session"), |
| 87 | sortedWindows.find((window) => window.kind === "weekly"), |
| 88 | ...sortedWindows, |
| 89 | ]), |
| 90 | ] |
| 91 | .filter((window) => window !== undefined) |
| 92 | .slice(0, maxWindowsPerProvider) |
| 93 | .sort((a, b) => a.remainingPercent - b.remainingPercent); |
| 94 | return { |
| 95 | name, |
| 96 | detail: !fresh |
| 97 | ? "Open The Knife to refresh" |
| 98 | : pool.accounts.length > 1 |
| 99 | ? `${pool.accounts.length} accounts · pooled` |
| 100 | : "Subscription remaining", |
| 101 | expiresAt: fresh ? expiresAt : 0, |
| 102 | totalWindows: fresh ? pool.windows.length : 0, |
| 103 | windows: fresh |
| 104 | ? selectedWindows.map((window) => ({ |
| 105 | kind: window.kind, |
| 106 | label: window.label, |
| 107 | remaining: Math.round(window.remainingPercent), |
| 108 | reset: window.resets[0] |
| 109 | ? `Next reset ${new Date(window.resets[0].at).toLocaleString(undefined, { |
| 110 | month: "short", |
| 111 | day: "numeric", |
| 112 | hour: "numeric", |
| 113 | minute: "2-digit", |
| 114 | })}` |
| 115 | : "Reset time unavailable", |
| 116 | })) |
| 117 | : [], |
| 118 | }; |
| 119 | }), |
| 120 | }; |
| 121 | } |
| 122 | |
| 123 | /** Deduplicate accounts before pooling, and only publish display data to the OS. */ |
| 124 | export function buildSubscriptionUsageSnapshot( |
| 125 | presentations: LimitPresentations, |
| 126 | url: string, |
| 127 | maxWindowsPerProvider = 6, |
| 128 | ): SubscriptionUsageSnapshot { |
| 129 | // Freshness is evaluated at publication/render time, not on unrelated config emissions. |
| 130 | const configuredDrivers = new Set( |
| 131 | [...presentations.values()].flatMap((presentation) => |
| 132 | (presentation.serverConfig?.providers ?? []) |
| 133 | // Servers report default-enabled drivers even when their CLI is missing. |
| 134 | .filter( |
| 135 | (provider) => |
| 136 | provider.enabled && |
| 137 | provider.installed && |
| 138 | provider.usageLimits?.unavailable?.reason !== "unsupported", |
| 139 | ) |
| 140 | .map((provider) => provider.driver), |
| 141 | ), |
| 142 | ); |
| 143 | return { |
| 144 | ...subscriptionUsageProps( |
| 145 | collectLimitAccounts(presentations), |
| 146 | 0, |
| 147 | configuredDrivers, |
| 148 | maxWindowsPerProvider, |
| 149 | ), |
| 150 | url, |
| 151 | }; |
| 152 | } |
| 153 | |
| 154 | export function subscriptionUsageTimeline(snapshot: SubscriptionUsageSnapshot, now: number) { |
| 155 | const deadlines = [...new Set(snapshot.providers.map((p) => p.expiresAt))] |
| 156 | .filter((deadline) => deadline > now) |
| 157 | .sort((a, b) => a - b); |
| 158 | return [now, ...deadlines].map((date) => ({ |
| 159 | date: new Date(date), |
| 160 | props: { |
| 161 | ...snapshot, |
| 162 | providers: snapshot.providers.map((provider) => |
| 163 | provider.windows.length > 0 && provider.expiresAt <= date |
| 164 | ? { ...provider, detail: "Open The Knife to refresh", windows: [], totalWindows: 0 } |
| 165 | : provider, |
| 166 | ), |
| 167 | }, |
| 168 | })); |
| 169 | } |