This commit is contained in:
+44
-15
@@ -8,6 +8,8 @@ import type { QuotaSnapshot, QuotaStatus, QuotaWindow } from "./quota.js";
|
||||
const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
||||
const POLL_MS = 60_000;
|
||||
const TIMEOUT_MS = 5_000;
|
||||
const RETRY_MS = 2_000;
|
||||
const RETRIES = 4;
|
||||
const WEEK = 604_800;
|
||||
|
||||
type QuotaFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
||||
@@ -108,6 +110,7 @@ export class ClaudeQuotaTracker {
|
||||
private timer: ReturnType<typeof setInterval> | undefined;
|
||||
private request: Promise<void> | undefined;
|
||||
private controller: AbortController | undefined;
|
||||
private wake: (() => void) | undefined;
|
||||
private disposed = false;
|
||||
|
||||
constructor(
|
||||
@@ -115,6 +118,7 @@ export class ClaudeQuotaTracker {
|
||||
private readonly fetcher: QuotaFetch = fetch,
|
||||
private readonly pollMs = POLL_MS,
|
||||
private readonly path = claudeCredentialsPath(),
|
||||
private readonly retryMs = RETRY_MS,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
@@ -131,22 +135,44 @@ export class ClaudeQuotaTracker {
|
||||
}
|
||||
|
||||
private async query() {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
if (await this.attempt()) return;
|
||||
if (attempt >= RETRIES || this.disposed) return;
|
||||
await this.sleep(this.retryMs * 2 ** attempt);
|
||||
}
|
||||
}
|
||||
|
||||
private sleep(ms: number) {
|
||||
return new Promise<void>((resolve) => {
|
||||
const timer = setTimeout(wake, ms);
|
||||
this.wake = wake;
|
||||
function wake() {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns true when done, false when the attempt failed and should be retried. */
|
||||
private async attempt() {
|
||||
try {
|
||||
let credential: z.infer<typeof credentials>["claudeAiOauth"] | undefined;
|
||||
let text: string;
|
||||
try {
|
||||
const parsed = credentials.safeParse(JSON.parse(await readFile(this.path, "utf8")));
|
||||
credential = parsed.success ? parsed.data.claudeAiOauth : undefined;
|
||||
} catch {
|
||||
credential = undefined;
|
||||
}
|
||||
if (!credential) {
|
||||
if (this.latest) {
|
||||
text = await readFile(this.path, "utf8");
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
||||
return false;
|
||||
if (this.latest && !this.disposed) {
|
||||
this.latest = undefined;
|
||||
await this.changed();
|
||||
await this.changed().catch(() => {});
|
||||
}
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (credential.expiresAt <= Date.now() || this.disposed) return;
|
||||
const parsed = credentials.safeParse(JSON.parse(text));
|
||||
if (!parsed.success) return false;
|
||||
const credential = parsed.data.claudeAiOauth;
|
||||
if (this.disposed) return true;
|
||||
if (credential.expiresAt <= Date.now()) return false;
|
||||
const controller = new AbortController();
|
||||
this.controller = controller;
|
||||
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||
@@ -164,19 +190,21 @@ export class ClaudeQuotaTracker {
|
||||
clearTimeout(timeout);
|
||||
if (this.controller === controller) this.controller = undefined;
|
||||
}
|
||||
if (!response.ok) return;
|
||||
if (!response.ok) return false;
|
||||
// SAFETY: Response.json yields JSON values; usagePayload validates fields used.
|
||||
const payload = (await response.json()) as Schema.Json;
|
||||
const snapshot = normalizeClaudeQuota(
|
||||
payload,
|
||||
credential.subscriptionType ?? "unknown",
|
||||
);
|
||||
if (!snapshot || this.disposed) return;
|
||||
if (this.disposed) return true;
|
||||
if (!snapshot) return false;
|
||||
this.latest = snapshot;
|
||||
await this.changed();
|
||||
} catch {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
await this.changed().catch(() => {});
|
||||
return true;
|
||||
}
|
||||
|
||||
status(): QuotaStatus | undefined {
|
||||
@@ -193,6 +221,7 @@ export class ClaudeQuotaTracker {
|
||||
async dispose() {
|
||||
this.disposed = true;
|
||||
this.controller?.abort();
|
||||
this.wake?.();
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
await this.request;
|
||||
|
||||
Reference in New Issue
Block a user