@@ -86,6 +86,10 @@ type WebuiRpcBridge = {
8686 cm_rpc : ( request : Record < string , unknown > ) => Promise < unknown > | unknown ;
8787} ;
8888
89+ type WebuiFrontendBridge = {
90+ register : ( name : string , handler : ( ...args : unknown [ ] ) => unknown ) => boolean ;
91+ } ;
92+
8993type BackendApis = {
9094 invoke : < T > ( command : string , payload ?: Record < string , unknown > ) => Promise < T > ;
9195 openUrl : ( url : string ) => Promise < void > ;
@@ -94,6 +98,14 @@ type BackendApis = {
9498let backendApisPromise : Promise < BackendApis > | null = null ;
9599let pendingBrowserLogin : PendingBrowserLogin | null = null ;
96100const inflightRefreshByAccountId = new Map < string , Promise < CreditsInfo > > ( ) ;
101+ const pendingUsageRefreshByAccountId = new Map <
102+ string ,
103+ {
104+ promise : Promise < CreditsInfo > ;
105+ resolve : ( credits : CreditsInfo ) => void ;
106+ }
107+ > ( ) ;
108+ let usageRefreshPushRegistered = false ;
97109
98110// Now epoch.
99111const nowEpoch = ( ) : number => Math . floor ( Date . now ( ) / 1000 ) ;
@@ -399,6 +411,115 @@ const waitForWebuiBridge = async (): Promise<WebuiRpcBridge> => {
399411 throw new Error ( "WebUI bridge is unavailable (webuiRpc.cm_rpc missing)." ) ;
400412} ;
401413
414+ // Returns get webui frontend push bridge.
415+ const getWebuiFrontendBridge = ( ) : WebuiFrontendBridge | null => {
416+ const bridge = ( globalThis as { webuiFrontend ?: WebuiFrontendBridge } ) . webuiFrontend ;
417+ if ( ! bridge || typeof bridge . register !== "function" ) {
418+ return null ;
419+ }
420+ return bridge ;
421+ } ;
422+
423+ // Wait for webui frontend push bridge.
424+ const waitForWebuiFrontendBridge = async ( ) : Promise < WebuiFrontendBridge > => {
425+ const immediate = getWebuiFrontendBridge ( ) ;
426+ if ( immediate ) {
427+ return immediate ;
428+ }
429+
430+ await waitForWebuiBridge ( ) ;
431+ for ( let attempt = 0 ; attempt < 40 ; attempt += 1 ) {
432+ await new Promise ( ( resolve ) => window . setTimeout ( resolve , 25 ) ) ;
433+ const bridge = getWebuiFrontendBridge ( ) ;
434+ if ( bridge ) {
435+ return bridge ;
436+ }
437+ }
438+
439+ throw new Error ( "WebUI frontend push bridge is unavailable." ) ;
440+ } ;
441+
442+ // Clears pending usage refresh waiter.
443+ const clearPendingUsageRefresh = ( accountId : string ) => {
444+ pendingUsageRefreshByAccountId . delete ( accountId ) ;
445+ } ;
446+
447+ // Creates create pending usage refresh waiter.
448+ const createPendingUsageRefresh = ( accountId : string ) => {
449+ const existing = pendingUsageRefreshByAccountId . get ( accountId ) ;
450+ if ( existing ) {
451+ return existing ;
452+ }
453+
454+ let resolvePromise : ( credits : CreditsInfo ) => void = ( ) => { } ;
455+ const promise = new Promise < CreditsInfo > ( ( resolve ) => {
456+ resolvePromise = resolve ;
457+ } ) ;
458+
459+ const pending = {
460+ promise,
461+ resolve : ( credits : CreditsInfo ) => {
462+ clearPendingUsageRefresh ( accountId ) ;
463+ resolvePromise ( credits ) ;
464+ } ,
465+ } ;
466+ pendingUsageRefreshByAccountId . set ( accountId , pending ) ;
467+ return pending ;
468+ } ;
469+
470+ // Parses as pushed usage refresh result.
471+ const asPushedUsageRefreshResult = ( value : unknown ) : { accountId : string ; credits : CreditsInfo } | null => {
472+ let parsedValue = value ;
473+ if ( typeof parsedValue === "string" ) {
474+ try {
475+ parsedValue = JSON . parse ( parsedValue ) as unknown ;
476+ } catch {
477+ return null ;
478+ }
479+ }
480+
481+ const parsed = asRecord ( parsedValue ) ;
482+ if ( ! parsed || typeof parsed . accountId !== "string" ) {
483+ return null ;
484+ }
485+
486+ const credits = asCreditsInfo ( parsed . credits ) ;
487+ if ( ! credits ) {
488+ return null ;
489+ }
490+
491+ return {
492+ accountId : parsed . accountId ,
493+ credits,
494+ } ;
495+ } ;
496+
497+ // Registers the pushed usage-refresh completion handler exposed to backend websocket RPC.
498+ const ensureUsageRefreshPushHandler = async ( ) : Promise < void > => {
499+ if ( usageRefreshPushRegistered ) {
500+ return ;
501+ }
502+
503+ const frontendBridge = await waitForWebuiFrontendBridge ( ) ;
504+ if ( usageRefreshPushRegistered ) {
505+ return ;
506+ }
507+
508+ frontendBridge . register ( "cm.handleUsageRefreshCompletion" , ( payload : unknown ) => {
509+ const pushed = asPushedUsageRefreshResult ( payload ) ;
510+ if ( ! pushed ) {
511+ return null ;
512+ }
513+
514+ const pending = pendingUsageRefreshByAccountId . get ( pushed . accountId ) ;
515+ if ( pending ) {
516+ pending . resolve ( pushed . credits ) ;
517+ }
518+ return null ;
519+ } ) ;
520+ usageRefreshPushRegistered = true ;
521+ } ;
522+
402523// Sends a request through the injected webui bridge and decodes the backend response.
403524const callBridge = async < T > ( op : string , payload : Record < string , unknown > = { } ) : Promise < T > => {
404525 const request = { op, ...payload } ;
@@ -410,13 +531,16 @@ const callBridge = async <T>(op: string, payload: Record<string, unknown> = {}):
410531// Loads load backend apis.
411532const loadBackendApis = async ( ) : Promise < BackendApis > => {
412533 if ( ! backendApisPromise ) {
413- backendApisPromise = Promise . resolve ( {
414- invoke : async < T > ( command : string , payload : Record < string , unknown > = { } ) =>
415- callBridge < T > ( `invoke:${ command } ` , payload ) ,
416- openUrl : async ( url : string ) => {
417- await callBridge < null > ( "shell:open_url" , { url } ) ;
418- } ,
419- } ) ;
534+ backendApisPromise = ( async ( ) => {
535+ await ensureUsageRefreshPushHandler ( ) ;
536+ return {
537+ invoke : async < T > ( command : string , payload : Record < string , unknown > = { } ) =>
538+ callBridge < T > ( `invoke:${ command } ` , payload ) ,
539+ openUrl : async ( url : string ) => {
540+ await callBridge < null > ( "shell:open_url" , { url } ) ;
541+ } ,
542+ } ;
543+ } ) ( ) ;
420544 }
421545
422546 return backendApisPromise ;
@@ -748,33 +872,35 @@ export const getRemainingCreditsForAccount = async (id: string): Promise<Credits
748872 const pending = ( async ( ) : Promise < CreditsInfo > => {
749873 try {
750874 const tauri = await loadBackendApis ( ) ;
875+ const pushedRefresh = createPendingUsageRefresh ( id ) ;
751876
752- while ( true ) {
753- let payload : unknown ;
754- try {
755- payload = await tauri . invoke < unknown > ( "refresh_account_usage" , { accountId : id } ) ;
756- } catch ( invokeError ) {
757- const rendered = invokeError instanceof Error ? invokeError . message : String ( invokeError ) ;
758- return errorCreditsInfo ( rendered ) ;
759- }
877+ let payload : unknown ;
878+ try {
879+ payload = await tauri . invoke < unknown > ( "refresh_account_usage" , { accountId : id } ) ;
880+ } catch ( invokeError ) {
881+ clearPendingUsageRefresh ( id ) ;
882+ const rendered = invokeError instanceof Error ? invokeError . message : String ( invokeError ) ;
883+ return errorCreditsInfo ( rendered ) ;
884+ }
760885
761- const record = asRecord ( payload ) ;
762- if ( record ) {
763- const inFlight = valueAsBoolean ( record . inFlight ) ?? false ;
764- if ( inFlight ) {
765- await new Promise ( ( resolve ) => window . setTimeout ( resolve , 500 ) ) ;
766- continue ;
767- }
768-
769- const credits = asCreditsInfo ( record . credits ) ;
770- if ( credits ) {
771- return credits ;
772- }
886+ const record = asRecord ( payload ) ;
887+ if ( record ) {
888+ const inFlight = valueAsBoolean ( record . inFlight ) ?? false ;
889+ if ( inFlight ) {
890+ return await pushedRefresh . promise ;
773891 }
774892
775- return errorCreditsInfo ( "Usage refresh returned an invalid response." ) ;
893+ clearPendingUsageRefresh ( id ) ;
894+ const credits = asCreditsInfo ( record . credits ) ;
895+ if ( credits ) {
896+ return credits ;
897+ }
776898 }
899+
900+ clearPendingUsageRefresh ( id ) ;
901+ return errorCreditsInfo ( "Usage refresh returned an invalid response." ) ;
777902 } catch ( refreshError ) {
903+ clearPendingUsageRefresh ( id ) ;
778904 const rendered = refreshError instanceof Error ? refreshError . message : String ( refreshError ) ;
779905 return errorCreditsInfo ( rendered ) ;
780906 }
0 commit comments