88import { BatchRequestContent , MiddlewareOptions } from '@microsoft/microsoft-graph-client' ;
99import { IGraph } from '../IGraph' ;
1010import { prepScopes } from './GraphHelpers' ;
11+ import { delay } from './Utils' ;
1112
1213/**
13- * Method to reduce repetitive requests to the Graph utilized in Batch
14+ * Represents a request to be executed in a batch
1415 *
1516 * @class BatchRequest
1617 */
@@ -21,6 +22,7 @@ export class BatchRequest {
2122 * @type {string }
2223 * @memberof BatchRequest
2324 */
25+
2426 public resource : string ;
2527 /**
2628 * method passed to be requested
@@ -29,15 +31,71 @@ export class BatchRequest {
2931 * @memberof BatchRequest
3032 */
3133 public method : string ;
32- constructor ( resource : string , method : string ) {
34+
35+ /**
36+ * The index of the requests as it was added to the queue
37+ * Use this value if you need to sort the responses
38+ * in the order they were added
39+ *
40+ * @type {number }
41+ * @memberof BatchRequest
42+ */
43+ public index : number ;
44+
45+ /**
46+ * The id of the requests
47+ *
48+ * @type {string }
49+ * @memberof BatchRequest
50+ */
51+ public id : string ;
52+
53+ constructor ( index , id , resource : string , method : string ) {
3354 if ( resource . charAt ( 0 ) !== '/' ) {
3455 resource = '/' + resource ;
3556 }
3657 this . resource = resource ;
3758 this . method = method ;
59+ this . index = index ;
60+ this . id = id ;
3861 }
3962}
4063
64+ /**
65+ * Represents a response from a request executed in a batch
66+ *
67+ * @export
68+ * @class BatchResponse
69+ */
70+ // tslint:disable-next-line:max-classes-per-file
71+ export class BatchResponse {
72+ /**
73+ * The index of the requests as it was added to the queue
74+ * Use this value if you need to sort the responses
75+ * in the order they were added
76+ *
77+ * @type {number }
78+ * @memberof BatchResponse
79+ */
80+ public index : number ;
81+
82+ /**
83+ * The id of the requests
84+ *
85+ * @type {string }
86+ * @memberof BatchResponse
87+ */
88+ public id : string ;
89+
90+ /**
91+ * The content of the response
92+ *
93+ * @type {* }
94+ * @memberof BatchResponse
95+ */
96+ public content : any ;
97+ }
98+
4199/**
42100 * Method to reduce repetitive requests to the Graph
43101 *
@@ -49,12 +107,33 @@ export class Batch {
49107 // this doesn't really mater what it is as long as it's a root base url
50108 // otherwise a Request assumes the current path and that could change the relative path
51109 private static baseUrl = 'https://graph.microsoft.com' ;
52- private requests : Map < string , BatchRequest > = new Map < string , BatchRequest > ( ) ;
53- private scopes : string [ ] = [ ] ;
110+
111+ private allRequests : BatchRequest [ ] ;
112+ private requestsQueue : number [ ] ;
113+ private scopes : string [ ] ;
114+ private retryAfter : number ;
115+
54116 private graph : IGraph ;
55117
118+ private nextIndex : number ;
119+
56120 constructor ( graph : IGraph ) {
57121 this . graph = graph ;
122+ this . allRequests = [ ] ;
123+ this . requestsQueue = [ ] ;
124+ this . scopes = [ ] ;
125+ this . nextIndex = 0 ;
126+ this . retryAfter = 0 ;
127+ }
128+
129+ /**
130+ * Get whether there are requests that have not been executed
131+ *
132+ * @readonly
133+ * @memberof Batch
134+ */
135+ public get hasRequests ( ) {
136+ return this . requestsQueue . length > 0 ;
58137 }
59138
60139 /**
@@ -66,31 +145,44 @@ export class Batch {
66145 * @memberof Batch
67146 */
68147 public get ( id : string , resource : string , scopes ?: string [ ] ) {
69- const request = new BatchRequest ( resource , 'GET' ) ;
70- this . requests . set ( id , request ) ;
148+ const index = this . nextIndex ++ ;
149+ const request = new BatchRequest ( index , id , resource , 'GET' ) ;
150+ this . allRequests . push ( request ) ;
151+ this . requestsQueue . push ( index ) ;
71152 if ( scopes ) {
72153 this . scopes = this . scopes . concat ( scopes ) ;
73154 }
74155 }
75156
76157 /**
77- * Promise to handle Graph request response
158+ * Execute the next set of requests.
159+ * This will execute up to 20 requests at a time
78160 *
79- * @returns {Promise<any > }
161+ * @returns {Promise<Map<string, BatchResponse> > }
80162 * @memberof Batch
81163 */
82- public async execute ( ) : Promise < any > {
83- const responses = { } ;
84- if ( ! this . requests . size ) {
164+ public async executeNext ( ) : Promise < Map < string , BatchResponse > > {
165+ const responses : Map < string , BatchResponse > = new Map ( ) ;
166+
167+ if ( this . retryAfter ) {
168+ await delay ( this . retryAfter * 1000 ) ;
169+ this . retryAfter = 0 ;
170+ }
171+
172+ if ( ! this . hasRequests ) {
85173 return responses ;
86174 }
87175
176+ // batch can support up to 20 requests
177+ const nextBatch = this . requestsQueue . splice ( 0 , 20 ) ;
178+
88179 const batchRequestContent = new BatchRequestContent ( ) ;
89- for ( const request of this . requests ) {
180+
181+ for ( const request of nextBatch . map ( i => this . allRequests [ i ] ) ) {
90182 batchRequestContent . addRequest ( {
91- id : request [ 0 ] ,
92- request : new Request ( Batch . baseUrl + request [ 1 ] . resource , {
93- method : request [ 1 ] . method
183+ id : request . index . toString ( ) ,
184+ request : new Request ( Batch . baseUrl + request . resource , {
185+ method : request . method
94186 } )
95187 } ) ;
96188 }
@@ -101,13 +193,47 @@ export class Batch {
101193 const batchRequestBody = await batchRequestContent . getContent ( ) ;
102194 const batchResponse = await batchRequest . post ( batchRequestBody ) ;
103195
104- for ( const response of batchResponse . responses ) {
105- if ( response . status !== 200 ) {
106- response [ response . id ] = null ;
107- } else if ( response . headers [ 'Content-Type' ] . includes ( 'image/jpeg' ) ) {
108- responses [ response . id ] = 'data:image/jpeg;base64,' + response . body ;
196+ for ( const r of batchResponse . responses ) {
197+ const response = new BatchResponse ( ) ;
198+ const request = this . allRequests [ r . id ] ;
199+
200+ response . id = request . id ;
201+ response . index = request . index ;
202+
203+ if ( r . status !== 200 ) {
204+ if ( r . status === 429 ) {
205+ // this request was throttled
206+ // add request back to queue and set retry wait time
207+ this . requestsQueue . unshift ( r . id ) ;
208+ this . retryAfter = Math . max ( this . retryAfter , parseInt ( r . headers [ 'Retry-After' ] , 10 ) || 1 ) ;
209+ }
210+ continue ;
211+ } else if ( r . headers [ 'Content-Type' ] . includes ( 'image/jpeg' ) ) {
212+ response . content = 'data:image/jpeg;base64,' + r . body ;
109213 } else {
110- responses [ response . id ] = response . body ;
214+ response . content = r . body ;
215+ }
216+
217+ responses . set ( request . id , response ) ;
218+ }
219+
220+ return responses ;
221+ }
222+
223+ /**
224+ * Execute all requests, up to 20 at a time until
225+ * all requests have been executed
226+ *
227+ * @returns {Promise<Map<string, BatchResponse>> }
228+ * @memberof Batch
229+ */
230+ public async executeAll ( ) : Promise < Map < string , BatchResponse > > {
231+ const responses : Map < string , BatchResponse > = new Map ( ) ;
232+
233+ while ( this . hasRequests ) {
234+ const r = await this . executeNext ( ) ;
235+ for ( const [ key , value ] of r ) {
236+ responses . set ( key , value ) ;
111237 }
112238 }
113239
0 commit comments