@@ -12,8 +12,10 @@ import {
1212 timeIntervalToMilliseconds ,
1313} from '../../../private/node/conf-store.js'
1414import { LocalStorage } from '../local-storage.js'
15- import { abortSignalFromRequestBehaviour , requestMode , RequestModeInput } from '../http.js'
15+ import { abortSignalFromRequestBehaviour , RequestBehaviour , requestMode , RequestModeInput } from '../http.js'
1616import { CLI_KIT_VERSION } from '../../common/version.js'
17+ import { sleep } from '../system.js'
18+ import { outputContent , outputDebug } from '../output.js'
1719import {
1820 GraphQLClient ,
1921 rawRequest ,
@@ -65,6 +67,7 @@ type PerformGraphQLRequestOptions<TResult> = GraphQLRequestBaseOptions<TResult>
6567 queryAsString : string
6668 variables ?: Variables
6769 unauthorizedHandler ?: UnauthorizedHandler
70+ autoRateLimitRestore ?: boolean
6871}
6972
7073export type GraphQLRequestOptions < T > = GraphQLRequestBaseOptions < T > & {
@@ -77,13 +80,28 @@ export type GraphQLRequestDocOptions<TResult, TVariables> = GraphQLRequestBaseOp
7780 query : TypedDocumentNode < TResult , TVariables > | TypedDocumentNode < TResult , Exact < { [ key : string ] : never } > >
7881 variables ?: TVariables
7982 unauthorizedHandler ?: UnauthorizedHandler
83+ autoRateLimitRestore ?: boolean
84+ }
85+
86+ interface RunRawGraphQLRequestOptions < TResult > {
87+ client : {
88+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89+ setAbortSignal : ( signal : any ) => void
90+ rawRequest : ( query : string , variables ?: Variables ) => Promise < GraphQLResponse < TResult > >
91+ }
92+ behaviour : RequestBehaviour
93+ queryAsString : string
94+ variables ?: Variables
95+ autoRateLimitRestore : boolean
8096}
8197
8298export interface GraphQLResponseOptions < T > {
8399 handleErrors ?: boolean
84100 onResponse ?: ( response : GraphQLResponse < T > ) => void
85101}
86102
103+ const MAX_RATE_LIMIT_RESTORE_DELAY_SECONDS = 0.3
104+
87105async function createGraphQLClient ( {
88106 url,
89107 addedHeaders,
@@ -105,38 +123,77 @@ async function createGraphQLClient({
105123 }
106124}
107125
108- /**
109- * Handles execution of a GraphQL query.
110- *
111- * @param options - GraphQL request options.
112- */
126+ async function waitForRateLimitRestore ( fullResponse : GraphQLResponse < unknown > ) {
127+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
128+ const cost = ( fullResponse . extensions as any ) ?. cost
129+ const actualQueryCost = cost ?. actualQueryCost
130+ const restoreRate = cost ?. throttleStatus ?. restoreRate
131+ if ( actualQueryCost && typeof actualQueryCost === 'number' && restoreRate && typeof restoreRate === 'number' ) {
132+ const secondsToRestoreRate = actualQueryCost / restoreRate
133+ outputDebug ( outputContent `Sleeping for ${ secondsToRestoreRate . toString ( ) } seconds to restore the rate limit.` )
134+ await sleep ( Math . min ( secondsToRestoreRate , MAX_RATE_LIMIT_RESTORE_DELAY_SECONDS ) )
135+ }
136+ }
137+
138+ async function runSingleRawGraphQLRequest < TResult > (
139+ options : RunRawGraphQLRequestOptions < TResult > ,
140+ ) : Promise < GraphQLResponse < TResult > > {
141+ const { client, behaviour, queryAsString, variables, autoRateLimitRestore} = options
142+ let fullResponse : GraphQLResponse < TResult >
143+ // there is a errorPolicy option which returns rather than throwing on errors, but we _do_ ultimately want to
144+ // throw.
145+ try {
146+ client . setAbortSignal ( abortSignalFromRequestBehaviour ( behaviour ) )
147+ fullResponse = await client . rawRequest ( queryAsString , variables )
148+ await logLastRequestIdFromResponse ( fullResponse )
149+
150+ if ( autoRateLimitRestore ) {
151+ await waitForRateLimitRestore ( fullResponse )
152+ }
153+
154+ return fullResponse
155+ } catch ( error ) {
156+ if ( error instanceof ClientError ) {
157+ // error.response does have a headers property like a normal response, but it's not typed as such.
158+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
159+ await logLastRequestIdFromResponse ( error . response as any )
160+ }
161+ throw error
162+ }
163+ }
164+
113165async function performGraphQLRequest < TResult > ( options : PerformGraphQLRequestOptions < TResult > ) {
114- const { token, addedHeaders, queryAsString, variables, api, url, responseOptions, unauthorizedHandler, cacheOptions} =
115- options
166+ const {
167+ token,
168+ addedHeaders,
169+ queryAsString,
170+ variables,
171+ api,
172+ url,
173+ responseOptions,
174+ unauthorizedHandler,
175+ cacheOptions,
176+ autoRateLimitRestore,
177+ } = options
116178 const behaviour = requestMode ( options . preferredBehaviour ?? 'default' )
117179
118180 let { headers, client} = await createGraphQLClient ( { url, addedHeaders, token} )
119181 debugLogRequestInfo ( api , queryAsString , url , variables , headers )
120182
121183 const rawGraphQLRequest = async ( ) => {
122- let fullResponse : GraphQLResponse < TResult >
123- // there is a errorPolicy option which returns rather than throwing on errors, but we _do_ ultimately want to
124- // throw.
125- try {
126- // mapping signal to any due to polyfill meaning types don't exactly match (but are functionally equivalent)
127- // eslint-disable-next-line @typescript-eslint/no-explicit-any
128- client . requestConfig . signal = abortSignalFromRequestBehaviour ( behaviour ) as any
129- fullResponse = await client . rawRequest < TResult > ( queryAsString , variables )
130- await logLastRequestIdFromResponse ( fullResponse )
131- return fullResponse
132- } catch ( error ) {
133- if ( error instanceof ClientError ) {
134- // error.response does have a headers property like a normal response, but it's not typed as such.
184+ return runSingleRawGraphQLRequest ( {
185+ client : {
135186 // eslint-disable-next-line @typescript-eslint/no-explicit-any
136- await logLastRequestIdFromResponse ( error . response as any )
137- }
138- throw error
139- }
187+ setAbortSignal : ( signal : any ) => {
188+ client . requestConfig . signal = signal
189+ } ,
190+ rawRequest : ( query : string , variables ?: Variables ) => client . rawRequest < TResult > ( query , variables ) ,
191+ } ,
192+ behaviour,
193+ queryAsString,
194+ variables,
195+ autoRateLimitRestore : autoRateLimitRestore ?? false ,
196+ } )
140197 }
141198
142199 const tokenRefreshHandler = unauthorizedHandler ?. handler
0 commit comments