@@ -46,6 +46,7 @@ import type {
4646import {
4747 extractChatGPTAuth ,
4848 fetchQuotaSnapshot ,
49+ type QuotaClientMode ,
4950} from "../quota-client.js" ;
5051export type {
5152 AccountQuotaSummary ,
@@ -59,6 +60,18 @@ export type {
5960 UpdateAccountResult ,
6061} from "./types.js" ;
6162
63+ const LIST_CACHED_QUOTA_MAX_AGE_MS = 24 * 60 * 60 * 1000 ;
64+
65+ interface RefreshQuotaOptions {
66+ quotaClientMode ?: QuotaClientMode ;
67+ allowCachedQuotaFallback ?: boolean ;
68+ cachedQuotaMaxAgeMs ?: number ;
69+ }
70+
71+ function isFiniteTimestamp ( value : string | undefined ) : value is string {
72+ return typeof value === "string" && Number . isFinite ( Date . parse ( value ) ) ;
73+ }
74+
6275export class AccountStore {
6376 readonly paths : StorePaths ;
6477 readonly fetchImpl ?: typeof fetch ;
@@ -77,8 +90,10 @@ export class AccountStore {
7790
7891 private async quotaSummaryForAccount (
7992 account : ManagedAccount ,
93+ quotaOverride ?: QuotaSnapshot ,
8094 ) : Promise < AccountQuotaSummary > {
81- let planType = account . quota . plan_type ?? null ;
95+ const quota = quotaOverride ?? account . quota ;
96+ let planType = quota . plan_type ?? null ;
8297
8398 try {
8499 const snapshot = await readAuthSnapshotFile ( account . authPath ) ;
@@ -94,13 +109,39 @@ export class AccountStore {
94109 user_id : account . user_id ?? null ,
95110 identity : account . identity ,
96111 plan_type : planType ,
97- credits_balance : account . quota . credits_balance ?? null ,
98- status : account . quota . status ,
99- fetched_at : account . quota . fetched_at ?? null ,
100- error_message : account . quota . error_message ?? null ,
101- unlimited : account . quota . unlimited === true ,
102- five_hour : account . quota . five_hour ?? null ,
103- one_week : account . quota . one_week ?? null ,
112+ credits_balance : quota . credits_balance ?? null ,
113+ status : quota . status ,
114+ fetched_at : quota . fetched_at ?? null ,
115+ error_message : quota . error_message ?? null ,
116+ unlimited : quota . unlimited === true ,
117+ five_hour : quota . five_hour ?? null ,
118+ one_week : quota . one_week ?? null ,
119+ } ;
120+ }
121+
122+ private getCachedQuotaFallback (
123+ account : ManagedAccount ,
124+ refreshError : Error ,
125+ now : Date ,
126+ maxAgeMs : number ,
127+ ) : { quota : QuotaSnapshot ; warning : string } | null {
128+ const cachedQuota = account . quota ;
129+ const cachedFetchedAt = cachedQuota . fetched_at ;
130+ if ( cachedQuota . status === "error" || ! isFiniteTimestamp ( cachedFetchedAt ) ) {
131+ return null ;
132+ }
133+ const cachedFetchedAtMs = Date . parse ( cachedFetchedAt ) ;
134+ if ( now . getTime ( ) - cachedFetchedAtMs > maxAgeMs ) {
135+ return null ;
136+ }
137+
138+ return {
139+ quota : {
140+ ...cachedQuota ,
141+ status : "stale" ,
142+ error_message : refreshError . message ,
143+ } ,
144+ warning : `${ account . name } using cached quota from ${ cachedFetchedAt } after refresh failed: ${ refreshError . message } ` ,
104145 } ;
105146 }
106147
@@ -416,7 +457,10 @@ export class AccountStore {
416457 } ;
417458 }
418459
419- async refreshQuotaForAccount ( name : string ) : Promise < RefreshQuotaResult > {
460+ async refreshQuotaForAccount (
461+ name : string ,
462+ options : RefreshQuotaOptions = { } ,
463+ ) : Promise < RefreshQuotaResult > {
420464 ensureAccountName ( name ) ;
421465 await this . repository . ensureLayout ( ) ;
422466
@@ -430,6 +474,7 @@ export class AccountStore {
430474 homeDir : this . paths . homeDir ,
431475 fetchImpl : this . fetchImpl ,
432476 now,
477+ mode : options . quotaClientMode ,
433478 } ) ;
434479
435480 if ( JSON . stringify ( result . authSnapshot ) !== JSON . stringify ( snapshot ) ) {
@@ -449,6 +494,24 @@ export class AccountStore {
449494 quota : meta . quota ,
450495 } ;
451496 } catch ( error ) {
497+ const refreshError =
498+ error instanceof Error ? error : new Error ( String ( error ) ) ;
499+ if ( options . allowCachedQuotaFallback ) {
500+ const fallback = this . getCachedQuotaFallback (
501+ account ,
502+ refreshError ,
503+ now ,
504+ options . cachedQuotaMaxAgeMs ?? LIST_CACHED_QUOTA_MAX_AGE_MS ,
505+ ) ;
506+ if ( fallback ) {
507+ return {
508+ account,
509+ quota : fallback . quota ,
510+ warning : fallback . warning ,
511+ } ;
512+ }
513+ }
514+
452515 let planType = meta . quota . plan_type ;
453516 try {
454517 const extracted = extractChatGPTAuth ( snapshot ) ;
@@ -463,14 +526,17 @@ export class AccountStore {
463526 status : "error" ,
464527 plan_type : planType ,
465528 fetched_at : now . toISOString ( ) ,
466- error_message : ( error as Error ) . message ,
529+ error_message : refreshError . message ,
467530 } ;
468531 await this . repository . writeAccountMeta ( name , meta ) ;
469- throw new Error ( `Failed to refresh quota for "${ name } ": ${ ( error as Error ) . message } ` ) ;
532+ throw new Error ( `Failed to refresh quota for "${ name } ": ${ refreshError . message } ` ) ;
470533 }
471534 }
472535
473- async refreshAllQuotas ( targetName ?: string ) : Promise < RefreshAllQuotasResult > {
536+ async refreshAllQuotas (
537+ targetName ?: string ,
538+ options : RefreshQuotaOptions = { } ,
539+ ) : Promise < RefreshAllQuotasResult > {
474540 const { accounts } = await this . listAccounts ( ) ;
475541 const targets = targetName
476542 ? accounts . filter ( ( account ) => account . name === targetName )
@@ -481,7 +547,7 @@ export class AccountStore {
481547 }
482548
483549 const results = new Array <
484- | { success : AccountQuotaSummary }
550+ | { success : AccountQuotaSummary ; warning ?: string }
485551 | { failure : { name : string ; error : string } }
486552 > ( targets . length ) ;
487553 let nextIndex = 0 ;
@@ -499,9 +565,10 @@ export class AccountStore {
499565
500566 const account = targets [ index ] ;
501567 try {
502- const refreshed = await this . refreshQuotaForAccount ( account . name ) ;
568+ const refreshed = await this . refreshQuotaForAccount ( account . name , options ) ;
503569 results [ index ] = {
504- success : await this . quotaSummaryForAccount ( refreshed . account ) ,
570+ success : await this . quotaSummaryForAccount ( refreshed . account , refreshed . quota ) ,
571+ ...( refreshed . warning ? { warning : refreshed . warning } : { } ) ,
505572 } ;
506573 } catch ( error ) {
507574 results [ index ] = {
@@ -517,13 +584,17 @@ export class AccountStore {
517584
518585 const successes : AccountQuotaSummary [ ] = [ ] ;
519586 const failures : Array < { name : string ; error : string } > = [ ] ;
587+ const warnings : string [ ] = [ ] ;
520588 for ( const result of results ) {
521589 if ( ! result ) {
522590 continue ;
523591 }
524592
525593 if ( "success" in result ) {
526594 successes . push ( result . success ) ;
595+ if ( typeof result . warning === "string" ) {
596+ warnings . push ( result . warning ) ;
597+ }
527598 } else {
528599 failures . push ( result . failure ) ;
529600 }
@@ -532,6 +603,7 @@ export class AccountStore {
532603 return {
533604 successes,
534605 failures,
606+ warnings,
535607 } ;
536608 }
537609
0 commit comments