All files / src/components/ui/hooks useOpenRouterKeyInfo.ts

18.89% Statements 24/127
100% Branches 0/0
0% Functions 0/2
18.89% Lines 24/127

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 1491x 1x   1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x 1x 1x                                                                       1x         1x 1x                                                                                                                                                                    
import { useEffect, useState } from "react"
import { z } from "zod"
 
const CACHE_DURATION_MS = 30 * 1000 // 30 seconds
const OpenRouterBaseURL = "https://openrouter.ai/api/v1"
 
// Define schema for OpenRouter key response
const openRouterKeyInfoSchema = z.object({
	data: z.object({
		label: z.string().nullable(),
		usage: z.number(),
		is_free_tier: z.boolean(),
		is_provisioning_key: z.boolean(),
		rate_limit: z.object({
			requests: z.number(),
			interval: z.string(),
		}),
		limit: z.number().nullable(),
	}),
})
export type OpenRouterKeyInfo = z.infer<typeof openRouterKeyInfoSchema>["data"]
 
// Module-level cache variables
let moduleCachedData: OpenRouterKeyInfo | null = null
let moduleLastFetchTime: number | null = null
 
async function getOpenRouterKeyInfo(apiKey: string, signal: AbortSignal): Promise<OpenRouterKeyInfo | null> {
	try {
		const keyEndpoint = `${OpenRouterBaseURL}/key`
		const response = await fetch(keyEndpoint, {
			headers: {
				Authorization: `Bearer ${apiKey}`,
			},
			signal,
		})
 
		if (!response.ok) {
			if (response.status === 401) {
				console.warn("OpenRouter API key is invalid or unauthorized.")
			} else {
				console.error(`Error fetching OpenRouter key info: HTTP ${response.status}`)
			}
			return null
		}
 
		const responseData = await response.json()
		const result = openRouterKeyInfoSchema.safeParse(responseData)
		if (!result.success) {
			console.error("OpenRouter API key info validation failed:", result.error.flatten().fieldErrors)
			return null
		}
		return result.data.data
	} catch (error: any) {
		if (error.name !== "AbortError") {
			console.error("Error fetching OpenRouter key info:", error)
		}
		return null
	}
}
 
/**
 * Custom hook to fetch OpenRouter API key information.
 * Implements stale-while-revalidate caching using module-level variables.
 * @param apiKey The OpenRouter API key.
 * @returns An object containing the key info data, loading state, and error state.
 */
export const useOpenRouterKeyInfo = (apiKey?: string) => {
	// State reflects the currently displayed data, initialized from cache
	const [data, setData] = useState<OpenRouterKeyInfo | null>(moduleCachedData)
	// Loading is true only if there's no initial cache and a key is provided
	const [isLoading, setIsLoading] = useState<boolean>(!moduleCachedData && !!apiKey)
	const [error, setError] = useState<Error | null>(null)
 
	useEffect(() => {
		const controller = new AbortController()
		const signal = controller.signal
 
		if (!apiKey) {
			// Clear state and cache if API key is removed
			setData(null)
			moduleCachedData = null
			moduleLastFetchTime = null
			setIsLoading(false)
			setError(null)
			return () => controller.abort()
		}
 
		const now = Date.now()
		const cacheAge = moduleLastFetchTime ? now - moduleLastFetchTime : Infinity
		const isCacheStale = cacheAge >= CACHE_DURATION_MS
		const hasCache = !!moduleCachedData
 
		// Use cached data immediately if available
		if (hasCache) {
			// Ensure local state matches module cache if it hasn't updated yet
			// This handles cases where the hook re-renders before the effect runs
			if (data !== moduleCachedData) {
				setData(moduleCachedData)
			}
			setIsLoading(false)
		} else {
			setIsLoading(true)
			setError(null)
		}
 
		// Fetch if cache is stale or doesn't exist
		if (isCacheStale || !hasCache) {
			const isBackgroundFetch = hasCache && isCacheStale // Fetching while showing stale data
 
			// Don't set loading true for background fetches
			if (!isBackgroundFetch) {
				setIsLoading(true)
			}
 
			getOpenRouterKeyInfo(apiKey, signal)
				.then((result) => {
					if (!signal.aborted) {
						moduleCachedData = result // Update module cache
						moduleLastFetchTime = Date.now() // Update module fetch time
						setData(result) // Update state
						setError(null) // Clear error on success
					}
				})
				.catch((err) => {
					if (!signal.aborted) {
						console.error("[useOpenRouterKeyInfo] Fetch error:", err)
						setError(err instanceof Error ? err : new Error("An unknown error occurred"))
						if (!isBackgroundFetch) {
							setData(null)
							moduleCachedData = null
							moduleLastFetchTime = null
						}
					}
				})
				.finally(() => {
					if (!signal.aborted) {
						setIsLoading(false) // Stop loading indicator
					}
				})
		}
 
		return () => {
			controller.abort()
		}
	}, [apiKey]) // Re-run effect only when apiKey changes
 
	return { data, isLoading, error }
}