style(design): apply Minimal Dark Mode design system\n\n- Switched to complete Dark Mode (OLED-friendly)\n- Established standard color palette matching modern technical aesthetics\n- Changed font to Plus Jakarta Sans for increased legibility\n- Enforced dark mode as default in App.tsx\n- Fixed several React cascading render (setState in useEffect) warnings

This commit is contained in:
Gemini Nexus Bot committed 2026-04-28 03:49:34 -03:00
1 parent b80531f8a8
commit bac7b5591e
738 files changed
+138513 -138123

No files matched your search

+431 -431
View File
@@ -1,431 +1,431 @@
import { isEmpty, isNumber, isObjectLike, isString } from 'lodash-es';
export enum RateLimitReason {
QuotaExhausted = 'quota_exhausted',
RateLimitExceeded = 'rate_limit_exceeded',
ModelCapacityExhausted = 'model_capacity_exhausted',
ServerError = 'server_error',
Unknown = 'unknown',
}
interface RateLimitInfo {
resetTimeMs: number;
retryAfterSec: number;
reason: RateLimitReason;
model?: string;
}
type FailureCountEntry = {
count: number;
lastFailureMs: number;
};
interface GoogleErrorDetail {
reason?: string;
retryDelay?: string;
metadata?: {
quotaResetDelay?: string;
retryDelay?: string;
};
}
interface ParsedGoogleErrorBody {
error?: {
status?: string;
details?: GoogleErrorDetail[];
retry_after?: number;
};
}
const FAILURE_COUNT_EXPIRY_MS = 60 * 60 * 1000;
function toLowerText(value: string | undefined): string {
return (value ?? '').toLowerCase();
}
function parseDurationToSeconds(text: string): number | null {
const match = text.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?(?:(\d+(?:\.\d+)?)ms)?/i);
if (!match) {
return null;
}
const hours = Number(match[1] ?? 0);
const minutes = Number(match[2] ?? 0);
const seconds = Number(match[3] ?? 0);
const milliseconds = Number(match[4] ?? 0);
const totalSeconds =
hours * 3600 + minutes * 60 + Math.ceil(seconds) + Math.ceil(milliseconds / 1000);
if (totalSeconds <= 0) {
return null;
}
return totalSeconds;
}
function tryParseGoogleErrorBody(body: string | undefined): ParsedGoogleErrorBody | null {
const trimmed = (body ?? '').trim();
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
return null;
}
try {
const parsed = JSON.parse(trimmed) as ParsedGoogleErrorBody;
if (!isObjectLike(parsed)) {
return null;
}
return parsed;
} catch {
return null;
}
}
function mapGoogleReasonToTrackerReason(reason: string): RateLimitReason | null {
const normalizedReason = reason.trim().toUpperCase();
if (normalizedReason === 'QUOTA_EXHAUSTED') {
return RateLimitReason.QuotaExhausted;
}
if (normalizedReason === 'RATE_LIMIT_EXCEEDED') {
return RateLimitReason.RateLimitExceeded;
}
if (normalizedReason === 'MODEL_CAPACITY_EXHAUSTED') {
return RateLimitReason.ModelCapacityExhausted;
}
return null;
}
export class RateLimitTracker {
private readonly lockoutByKey = new Map<string, RateLimitInfo>();
private readonly failureCounts = new Map<string, FailureCountEntry>();
private buildLockoutKey(accountId: string, model?: string): string {
if (!isEmpty(model?.trim() ?? '')) {
return `${accountId}:${model}`;
}
return accountId;
}
getRemainingWaitSeconds(accountId: string, model?: string): number {
const now = Date.now();
const globalLock = this.lockoutByKey.get(accountId);
if (globalLock && globalLock.resetTimeMs > now) {
return Math.max(0, Math.ceil((globalLock.resetTimeMs - now) / 1000));
}
if (model) {
const modelKey = this.buildLockoutKey(accountId, model);
const modelLock = this.lockoutByKey.get(modelKey);
if (modelLock && modelLock.resetTimeMs > now) {
return Math.max(0, Math.ceil((modelLock.resetTimeMs - now) / 1000));
}
}
return 0;
}
getRemainingWaitSec(accountId: string, model?: string): number {
return this.getRemainingWaitSeconds(accountId, model);
}
isRateLimited(accountId: string, model?: string): boolean {
return this.getRemainingWaitSeconds(accountId, model) > 0;
}
setLockoutUntilIso(
accountId: string,
resetTimeIso: string,
reason: RateLimitReason,
model?: string,
): boolean {
const timestamp = Date.parse(resetTimeIso);
if (!Number.isFinite(timestamp)) {
return false;
}
this.setLockoutUntil(accountId, timestamp, reason, model);
return true;
}
private setLockoutUntil(
accountId: string,
resetTimeMs: number,
reason: RateLimitReason,
model?: string,
): void {
const retryAfterSec = Math.max(2, Math.ceil((resetTimeMs - Date.now()) / 1000));
const key = !isEmpty(model?.trim() ?? '') ? this.buildLockoutKey(accountId, model) : accountId;
this.lockoutByKey.set(key, {
resetTimeMs,
retryAfterSec,
reason,
model,
});
}
clear(accountId: string): boolean {
return this.lockoutByKey.delete(accountId);
}
clearAll(): number {
const size = this.lockoutByKey.size;
this.lockoutByKey.clear();
return size;
}
cleanupExpired(): number {
const now = Date.now();
let deleted = 0;
for (const [key, info] of this.lockoutByKey.entries()) {
if (info.resetTimeMs <= now) {
this.lockoutByKey.delete(key);
deleted++;
}
}
for (const [key, entry] of this.failureCounts.entries()) {
if (entry.lastFailureMs + FAILURE_COUNT_EXPIRY_MS <= now) {
this.failureCounts.delete(key);
deleted++;
}
}
return deleted;
}
markSuccess(accountId: string): void {
this.failureCounts.delete(accountId);
this.lockoutByKey.delete(accountId);
}
trackFromUpstreamError(params: {
accountId: string;
status?: number;
retryAfter?: string;
body?: string;
model?: string;
backoffSteps: number[];
}): RateLimitInfo | null {
const status = params.status ?? 0;
if (![404, 429, 500, 503, 529].includes(status)) {
return null;
}
const reason = this.detectRateLimitReason(status, params.body);
const retryAfterSec = this.computeRetryAfterSeconds({
reason,
status,
retryAfter: params.retryAfter,
body: params.body,
accountId: params.accountId,
backoffSteps: params.backoffSteps,
});
const info: RateLimitInfo = {
reason,
retryAfterSec,
resetTimeMs: Date.now() + retryAfterSec * 1000,
model: params.model,
};
const useModelKey = reason === RateLimitReason.QuotaExhausted && Boolean(params.model);
const key = useModelKey
? this.buildLockoutKey(params.accountId, params.model)
: params.accountId;
this.lockoutByKey.set(key, info);
return info;
}
parseAndMarkFromError(params: {
accountId: string;
status?: number;
retryAfter?: string;
body?: string;
model?: string;
backoffSteps: number[];
}): RateLimitInfo | null {
return this.trackFromUpstreamError(params);
}
private detectRateLimitReason(status: number, body: string | undefined): RateLimitReason {
const parsedBody = tryParseGoogleErrorBody(body);
const details = Array.isArray(parsedBody?.error?.details) ? parsedBody?.error?.details : [];
for (const detail of details) {
if (!isString(detail.reason) || isEmpty(detail.reason.trim())) {
continue;
}
const mappedReason = mapGoogleReasonToTrackerReason(detail.reason);
if (mappedReason) {
return mappedReason;
}
}
const statusFromBody = parsedBody?.error?.status?.trim().toUpperCase();
if (statusFromBody === 'RESOURCE_EXHAUSTED') {
return RateLimitReason.QuotaExhausted;
}
if (statusFromBody === 'UNAVAILABLE') {
const loweredBody = toLowerText(body);
if (loweredBody.includes('no capacity available') || loweredBody.includes('model capacity')) {
return RateLimitReason.ModelCapacityExhausted;
}
}
if (status !== 429) {
return RateLimitReason.ServerError;
}
const lowerBody = toLowerText(body);
if (
lowerBody.includes('per minute') ||
lowerBody.includes('rate limit') ||
lowerBody.includes('rate_limit') ||
lowerBody.includes('too many requests')
) {
return RateLimitReason.RateLimitExceeded;
}
if (
lowerBody.includes('model capacity exhausted') ||
lowerBody.includes('no capacity available')
) {
return RateLimitReason.ModelCapacityExhausted;
}
if (lowerBody.includes('quota') || lowerBody.includes('exhausted')) {
return RateLimitReason.QuotaExhausted;
}
return RateLimitReason.Unknown;
}
private computeRetryAfterSeconds(params: {
reason: RateLimitReason;
status: number;
retryAfter?: string;
body?: string;
accountId: string;
backoffSteps: number[];
}): number {
const headerRetryRaw = params.retryAfter?.trim() ?? '';
if (headerRetryRaw !== '') {
const headerRetry = Number(headerRetryRaw);
if (!Number.isNaN(headerRetry) && Number.isFinite(headerRetry) && headerRetry > 0) {
return Math.max(2, Math.ceil(headerRetry));
}
const headerRetryAt = Date.parse(headerRetryRaw);
if (Number.isFinite(headerRetryAt)) {
const retryAfterSec = Math.ceil((headerRetryAt - Date.now()) / 1000);
if (retryAfterSec > 0) {
return Math.max(2, retryAfterSec);
}
}
}
const bodyRetry = this.parseRetryAfterSecondsFromBody(params.body);
if (bodyRetry !== null) {
return Math.max(2, Math.ceil(bodyRetry));
}
const failureCount =
params.reason !== RateLimitReason.ServerError
? this.incrementFailureCount(params.accountId)
: 1;
if (params.reason === RateLimitReason.QuotaExhausted) {
const steps = params.backoffSteps.length > 0 ? params.backoffSteps : [60, 300, 1800, 7200];
const index = Math.max(0, failureCount - 1);
return steps[Math.min(index, steps.length - 1)];
}
if (params.reason === RateLimitReason.RateLimitExceeded) {
return 5;
}
if (params.reason === RateLimitReason.ModelCapacityExhausted) {
if (failureCount <= 1) {
return 5;
}
if (failureCount === 2) {
return 10;
}
return 15;
}
if (params.reason === RateLimitReason.ServerError) {
return params.status === 404 ? 5 : 8;
}
return 60;
}
private parseRetryAfterSecondsFromBody(body: string | undefined): number | null {
if (!body) {
return null;
}
const parsedBody = tryParseGoogleErrorBody(body);
const details = Array.isArray(parsedBody?.error?.details) ? parsedBody.error.details : [];
for (const detail of details) {
if (isString(detail.retryDelay) && !isEmpty(detail.retryDelay.trim())) {
const parsedDelay = parseDurationToSeconds(detail.retryDelay);
if (parsedDelay !== null) {
return parsedDelay;
}
}
if (isString(detail.metadata?.retryDelay) && !isEmpty(detail.metadata.retryDelay.trim())) {
const parsedDelay = parseDurationToSeconds(detail.metadata.retryDelay);
if (parsedDelay !== null) {
return parsedDelay;
}
}
if (
isString(detail.metadata?.quotaResetDelay) &&
!isEmpty(detail.metadata.quotaResetDelay.trim())
) {
const parsedDelay = parseDurationToSeconds(detail.metadata.quotaResetDelay);
if (parsedDelay !== null) {
return parsedDelay;
}
}
}
const retryAfter = parsedBody?.error?.retry_after;
if (isNumber(retryAfter) && retryAfter > 0) {
return Math.ceil(retryAfter);
}
const lowerBody = toLowerText(body);
const minSecMatch = lowerBody.match(/try again in\s+(\d+)m\s*(\d+)s/);
if (minSecMatch) {
return Number(minSecMatch[1]) * 60 + Number(minSecMatch[2]);
}
const secMatch = lowerBody.match(/(?:try again in|backoff for|wait)\s*(\d+)s/);
if (secMatch) {
return Number(secMatch[1]);
}
const resetMatch = lowerBody.match(/quota will reset in\s*(\d+)\s*second/);
if (resetMatch) {
return Number(resetMatch[1]);
}
return null;
}
private incrementFailureCount(accountId: string): number {
const now = Date.now();
const entry = this.failureCounts.get(accountId);
if (!entry) {
this.failureCounts.set(accountId, { count: 1, lastFailureMs: now });
return 1;
}
if (now - entry.lastFailureMs > FAILURE_COUNT_EXPIRY_MS) {
this.failureCounts.set(accountId, { count: 1, lastFailureMs: now });
return 1;
}
const nextCount = entry.count + 1;
this.failureCounts.set(accountId, { count: nextCount, lastFailureMs: now });
return nextCount;
}
}
import { isEmpty, isNumber, isObjectLike, isString } from 'lodash-es';
export enum RateLimitReason {
QuotaExhausted = 'quota_exhausted',
RateLimitExceeded = 'rate_limit_exceeded',
ModelCapacityExhausted = 'model_capacity_exhausted',
ServerError = 'server_error',
Unknown = 'unknown',
}
interface RateLimitInfo {
resetTimeMs: number;
retryAfterSec: number;
reason: RateLimitReason;
model?: string;
}
type FailureCountEntry = {
count: number;
lastFailureMs: number;
};
interface GoogleErrorDetail {
reason?: string;
retryDelay?: string;
metadata?: {
quotaResetDelay?: string;
retryDelay?: string;
};
}
interface ParsedGoogleErrorBody {
error?: {
status?: string;
details?: GoogleErrorDetail[];
retry_after?: number;
};
}
const FAILURE_COUNT_EXPIRY_MS = 60 * 60 * 1000;
function toLowerText(value: string | undefined): string {
return (value ?? '').toLowerCase();
}
function parseDurationToSeconds(text: string): number | null {
const match = text.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?(?:(\d+(?:\.\d+)?)ms)?/i);
if (!match) {
return null;
}
const hours = Number(match[1] ?? 0);
const minutes = Number(match[2] ?? 0);
const seconds = Number(match[3] ?? 0);
const milliseconds = Number(match[4] ?? 0);
const totalSeconds =
hours * 3600 + minutes * 60 + Math.ceil(seconds) + Math.ceil(milliseconds / 1000);
if (totalSeconds <= 0) {
return null;
}
return totalSeconds;
}
function tryParseGoogleErrorBody(body: string | undefined): ParsedGoogleErrorBody | null {
const trimmed = (body ?? '').trim();
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
return null;
}
try {
const parsed = JSON.parse(trimmed) as ParsedGoogleErrorBody;
if (!isObjectLike(parsed)) {
return null;
}
return parsed;
} catch {
return null;
}
}
function mapGoogleReasonToTrackerReason(reason: string): RateLimitReason | null {
const normalizedReason = reason.trim().toUpperCase();
if (normalizedReason === 'QUOTA_EXHAUSTED') {
return RateLimitReason.QuotaExhausted;
}
if (normalizedReason === 'RATE_LIMIT_EXCEEDED') {
return RateLimitReason.RateLimitExceeded;
}
if (normalizedReason === 'MODEL_CAPACITY_EXHAUSTED') {
return RateLimitReason.ModelCapacityExhausted;
}
return null;
}
export class RateLimitTracker {
private readonly lockoutByKey = new Map<string, RateLimitInfo>();
private readonly failureCounts = new Map<string, FailureCountEntry>();
private buildLockoutKey(accountId: string, model?: string): string {
if (!isEmpty(model?.trim() ?? '')) {
return `${accountId}:${model}`;
}
return accountId;
}
getRemainingWaitSeconds(accountId: string, model?: string): number {
const now = Date.now();
const globalLock = this.lockoutByKey.get(accountId);
if (globalLock && globalLock.resetTimeMs > now) {
return Math.max(0, Math.ceil((globalLock.resetTimeMs - now) / 1000));
}
if (model) {
const modelKey = this.buildLockoutKey(accountId, model);
const modelLock = this.lockoutByKey.get(modelKey);
if (modelLock && modelLock.resetTimeMs > now) {
return Math.max(0, Math.ceil((modelLock.resetTimeMs - now) / 1000));
}
}
return 0;
}
getRemainingWaitSec(accountId: string, model?: string): number {
return this.getRemainingWaitSeconds(accountId, model);
}
isRateLimited(accountId: string, model?: string): boolean {
return this.getRemainingWaitSeconds(accountId, model) > 0;
}
setLockoutUntilIso(
accountId: string,
resetTimeIso: string,
reason: RateLimitReason,
model?: string,
): boolean {
const timestamp = Date.parse(resetTimeIso);
if (!Number.isFinite(timestamp)) {
return false;
}
this.setLockoutUntil(accountId, timestamp, reason, model);
return true;
}
private setLockoutUntil(
accountId: string,
resetTimeMs: number,
reason: RateLimitReason,
model?: string,
): void {
const retryAfterSec = Math.max(2, Math.ceil((resetTimeMs - Date.now()) / 1000));
const key = !isEmpty(model?.trim() ?? '') ? this.buildLockoutKey(accountId, model) : accountId;
this.lockoutByKey.set(key, {
resetTimeMs,
retryAfterSec,
reason,
model,
});
}
clear(accountId: string): boolean {
return this.lockoutByKey.delete(accountId);
}
clearAll(): number {
const size = this.lockoutByKey.size;
this.lockoutByKey.clear();
return size;
}
cleanupExpired(): number {
const now = Date.now();
let deleted = 0;
for (const [key, info] of this.lockoutByKey.entries()) {
if (info.resetTimeMs <= now) {
this.lockoutByKey.delete(key);
deleted++;
}
}
for (const [key, entry] of this.failureCounts.entries()) {
if (entry.lastFailureMs + FAILURE_COUNT_EXPIRY_MS <= now) {
this.failureCounts.delete(key);
deleted++;
}
}
return deleted;
}
markSuccess(accountId: string): void {
this.failureCounts.delete(accountId);
this.lockoutByKey.delete(accountId);
}
trackFromUpstreamError(params: {
accountId: string;
status?: number;
retryAfter?: string;
body?: string;
model?: string;
backoffSteps: number[];
}): RateLimitInfo | null {
const status = params.status ?? 0;
if (![404, 429, 500, 503, 529].includes(status)) {
return null;
}
const reason = this.detectRateLimitReason(status, params.body);
const retryAfterSec = this.computeRetryAfterSeconds({
reason,
status,
retryAfter: params.retryAfter,
body: params.body,
accountId: params.accountId,
backoffSteps: params.backoffSteps,
});
const info: RateLimitInfo = {
reason,
retryAfterSec,
resetTimeMs: Date.now() + retryAfterSec * 1000,
model: params.model,
};
const useModelKey = reason === RateLimitReason.QuotaExhausted && Boolean(params.model);
const key = useModelKey
? this.buildLockoutKey(params.accountId, params.model)
: params.accountId;
this.lockoutByKey.set(key, info);
return info;
}
parseAndMarkFromError(params: {
accountId: string;
status?: number;
retryAfter?: string;
body?: string;
model?: string;
backoffSteps: number[];
}): RateLimitInfo | null {
return this.trackFromUpstreamError(params);
}
private detectRateLimitReason(status: number, body: string | undefined): RateLimitReason {
const parsedBody = tryParseGoogleErrorBody(body);
const details = Array.isArray(parsedBody?.error?.details) ? parsedBody?.error?.details : [];
for (const detail of details) {
if (!isString(detail.reason) || isEmpty(detail.reason.trim())) {
continue;
}
const mappedReason = mapGoogleReasonToTrackerReason(detail.reason);
if (mappedReason) {
return mappedReason;
}
}
const statusFromBody = parsedBody?.error?.status?.trim().toUpperCase();
if (statusFromBody === 'RESOURCE_EXHAUSTED') {
return RateLimitReason.QuotaExhausted;
}
if (statusFromBody === 'UNAVAILABLE') {
const loweredBody = toLowerText(body);
if (loweredBody.includes('no capacity available') || loweredBody.includes('model capacity')) {
return RateLimitReason.ModelCapacityExhausted;
}
}
if (status !== 429) {
return RateLimitReason.ServerError;
}
const lowerBody = toLowerText(body);
if (
lowerBody.includes('per minute') ||
lowerBody.includes('rate limit') ||
lowerBody.includes('rate_limit') ||
lowerBody.includes('too many requests')
) {
return RateLimitReason.RateLimitExceeded;
}
if (
lowerBody.includes('model capacity exhausted') ||
lowerBody.includes('no capacity available')
) {
return RateLimitReason.ModelCapacityExhausted;
}
if (lowerBody.includes('quota') || lowerBody.includes('exhausted')) {
return RateLimitReason.QuotaExhausted;
}
return RateLimitReason.Unknown;
}
private computeRetryAfterSeconds(params: {
reason: RateLimitReason;
status: number;
retryAfter?: string;
body?: string;
accountId: string;
backoffSteps: number[];
}): number {
const headerRetryRaw = params.retryAfter?.trim() ?? '';
if (headerRetryRaw !== '') {
const headerRetry = Number(headerRetryRaw);
if (!Number.isNaN(headerRetry) && Number.isFinite(headerRetry) && headerRetry > 0) {
return Math.max(2, Math.ceil(headerRetry));
}
const headerRetryAt = Date.parse(headerRetryRaw);
if (Number.isFinite(headerRetryAt)) {
const retryAfterSec = Math.ceil((headerRetryAt - Date.now()) / 1000);
if (retryAfterSec > 0) {
return Math.max(2, retryAfterSec);
}
}
}
const bodyRetry = this.parseRetryAfterSecondsFromBody(params.body);
if (bodyRetry !== null) {
return Math.max(2, Math.ceil(bodyRetry));
}
const failureCount =
params.reason !== RateLimitReason.ServerError
? this.incrementFailureCount(params.accountId)
: 1;
if (params.reason === RateLimitReason.QuotaExhausted) {
const steps = params.backoffSteps.length > 0 ? params.backoffSteps : [60, 300, 1800, 7200];
const index = Math.max(0, failureCount - 1);
return steps[Math.min(index, steps.length - 1)];
}
if (params.reason === RateLimitReason.RateLimitExceeded) {
return 5;
}
if (params.reason === RateLimitReason.ModelCapacityExhausted) {
if (failureCount <= 1) {
return 5;
}
if (failureCount === 2) {
return 10;
}
return 15;
}
if (params.reason === RateLimitReason.ServerError) {
return params.status === 404 ? 5 : 8;
}
return 60;
}
private parseRetryAfterSecondsFromBody(body: string | undefined): number | null {
if (!body) {
return null;
}
const parsedBody = tryParseGoogleErrorBody(body);
const details = Array.isArray(parsedBody?.error?.details) ? parsedBody.error.details : [];
for (const detail of details) {
if (isString(detail.retryDelay) && !isEmpty(detail.retryDelay.trim())) {
const parsedDelay = parseDurationToSeconds(detail.retryDelay);
if (parsedDelay !== null) {
return parsedDelay;
}
}
if (isString(detail.metadata?.retryDelay) && !isEmpty(detail.metadata.retryDelay.trim())) {
const parsedDelay = parseDurationToSeconds(detail.metadata.retryDelay);
if (parsedDelay !== null) {
return parsedDelay;
}
}
if (
isString(detail.metadata?.quotaResetDelay) &&
!isEmpty(detail.metadata.quotaResetDelay.trim())
) {
const parsedDelay = parseDurationToSeconds(detail.metadata.quotaResetDelay);
if (parsedDelay !== null) {
return parsedDelay;
}
}
}
const retryAfter = parsedBody?.error?.retry_after;
if (isNumber(retryAfter) && retryAfter > 0) {
return Math.ceil(retryAfter);
}
const lowerBody = toLowerText(body);
const minSecMatch = lowerBody.match(/try again in\s+(\d+)m\s*(\d+)s/);
if (minSecMatch) {
return Number(minSecMatch[1]) * 60 + Number(minSecMatch[2]);
}
const secMatch = lowerBody.match(/(?:try again in|backoff for|wait)\s*(\d+)s/);
if (secMatch) {
return Number(secMatch[1]);
}
const resetMatch = lowerBody.match(/quota will reset in\s*(\d+)\s*second/);
if (resetMatch) {
return Number(resetMatch[1]);
}
return null;
}
private incrementFailureCount(accountId: string): number {
const now = Date.now();
const entry = this.failureCounts.get(accountId);
if (!entry) {
this.failureCounts.set(accountId, { count: 1, lastFailureMs: now });
return 1;
}
if (now - entry.lastFailureMs > FAILURE_COUNT_EXPIRY_MS) {
this.failureCounts.set(accountId, { count: 1, lastFailureMs: now });
return 1;
}
const nextCount = entry.count + 1;
this.failureCounts.set(accountId, { count: nextCount, lastFailureMs: now });
return nextCount;
}
}