- Add reset counts and actions to auth file and quota pages - Reuse the Codex confirmation flow with Claude eligibility and cooldown checks - Preserve request IDs on retries to prevent duplicate credit consumption - Add translations, regression tests, and layout checks
87 lines
4.0 KiB
TypeScript
87 lines
4.0 KiB
TypeScript
import { resetGrantOperations, RETRY_WINDOW_MS } from './claudeResetOperations';
|
|
import { selectResetGrant } from './selectClaudeResetGrant';
|
|
import {
|
|
captureQuotaCacheGeneration,
|
|
commitQuotaCacheIfCurrent,
|
|
getQuotaCacheSnapshot,
|
|
updateQuotaCache,
|
|
} from './quotaCache';
|
|
import { normalizeAuthIndex, readBoolean } from './managementApi';
|
|
import {
|
|
consumeClaudeResetCredit, consumeCodexResetCredit, idleQuota, providerForFile, quotaKey,
|
|
type AuthFile, type QuotaState,
|
|
} from './quotaService';
|
|
|
|
const pendingActions = new Set<string>();
|
|
type QuotaActionOutcome = 'cancelled' | 'success' | 'refresh-error' | 'error';
|
|
|
|
export const canResetCodexQuota = (file: AuthFile, quota: QuotaState): boolean =>
|
|
providerForFile(file) === 'codex'
|
|
&& !readBoolean(file, 'disabled')
|
|
&& quota.status !== 'loading'
|
|
&& (quota.resetCredits ?? 0) > 0;
|
|
|
|
async function runConfirmedQuotaAction(
|
|
file: AuthFile,
|
|
action: 'reset',
|
|
confirmAction: () => Promise<boolean>,
|
|
execute: (file: AuthFile) => Promise<QuotaState>,
|
|
): Promise<QuotaActionOutcome> {
|
|
const key = quotaKey(file);
|
|
const original = getQuotaCacheSnapshot()[key];
|
|
const previous = original ?? idleQuota();
|
|
if (pendingActions.has(key) || previous.status === 'loading' || readBoolean(file, 'disabled')) return 'cancelled';
|
|
const generation = captureQuotaCacheGeneration();
|
|
pendingActions.add(key);
|
|
let pending: QuotaState | undefined;
|
|
const commit = (quota: QuotaState) => {
|
|
commitQuotaCacheIfCurrent(generation, () => {
|
|
updateQuotaCache((current) => current[key] === pending ? { ...current, [key]: quota } : current);
|
|
});
|
|
};
|
|
try {
|
|
if (await confirmAction() !== true || captureQuotaCacheGeneration() !== generation
|
|
|| getQuotaCacheSnapshot()[key] !== original) return 'cancelled';
|
|
pending = { ...previous, status: 'loading', pendingAction: action, actionResult: undefined };
|
|
updateQuotaCache((current) => ({ ...current, [key]: pending! }));
|
|
const result = await execute(file);
|
|
if (result.status === 'error') {
|
|
const status = 'refresh-error';
|
|
commit({ ...previous, actionResult: { action, status, error: result.error } });
|
|
return status;
|
|
}
|
|
commit({ ...result, actionResult: { action, status: 'success' } });
|
|
return 'success';
|
|
} catch (error) {
|
|
if (!pending) throw error;
|
|
commit({ ...previous, actionResult: { action, status: 'error', error: error instanceof Error ? error.message : String(error) } });
|
|
return 'error';
|
|
} finally {
|
|
pendingActions.delete(key);
|
|
}
|
|
}
|
|
|
|
export function resetCodexQuotaWithConfirmation(file: AuthFile, confirmReset: () => Promise<boolean>): Promise<QuotaActionOutcome> {
|
|
if (!canResetCodexQuota(file, getQuotaCacheSnapshot()[quotaKey(file)] ?? idleQuota())) return Promise.resolve('cancelled');
|
|
return runConfirmedQuotaAction(file, 'reset', confirmReset, consumeCodexResetCredit);
|
|
}
|
|
|
|
export function hasPendingClaudeReset(file: AuthFile): boolean {
|
|
const operation = resetGrantOperations.inspect(quotaKey(file));
|
|
return Boolean(operation && !operation.code);
|
|
}
|
|
|
|
export function canResetQuota(file: AuthFile, quota: QuotaState): boolean {
|
|
if (providerForFile(file) !== 'claude') return canResetCodexQuota(file, quota);
|
|
if (readBoolean(file, 'disabled') || quota.status === 'loading' || !normalizeAuthIndex(file.auth_index ?? file.authIndex)) return false;
|
|
const operation = resetGrantOperations.inspect(quotaKey(file));
|
|
if (operation && !operation.code) return Date.now() - operation.createdAt < RETRY_WINDOW_MS;
|
|
return Boolean(quota.claudeResetGrants && selectResetGrant(quota.claudeResetGrants, Date.now()));
|
|
}
|
|
|
|
export function resetQuotaWithConfirmation(file: AuthFile, confirmReset: () => Promise<boolean>): Promise<QuotaActionOutcome> {
|
|
if (providerForFile(file) !== 'claude') return resetCodexQuotaWithConfirmation(file, confirmReset);
|
|
if (!canResetQuota(file, getQuotaCacheSnapshot()[quotaKey(file)] ?? idleQuota())) return Promise.resolve('cancelled');
|
|
return runConfirmedQuotaAction(file, 'reset', confirmReset, consumeClaudeResetCredit);
|
|
}
|