1- import { CacheClient } from "../app" ;
21import { ILogger } from "../logging" ;
3- import { AddMemberResponse , CopilotAddResponse , GitHubTeamId , InstalledClient , OrgConfigResponse , OrgInvite , OrgRoles , RemoveMemberResponse , Response } from "./gitHubTypes" ;
4-
5- export class GitHubClientCache implements InstalledClient {
6- client : InstalledClient ;
7- cacheClient : CacheClient ;
8- logger :ILogger ;
9-
10- constructor ( client : InstalledClient , cacheClient : CacheClient , logger :ILogger ) {
11- this . client = client ;
12- this . cacheClient = cacheClient ;
13- this . logger = logger ;
14- }
2+ import { AddMemberResponse , CopilotAddResponse , GitHubTeamId , IInstalledClient as IInstalledClient , IRawInstalledGitHubClient , OrgConfigResponse , OrgInvite , OrgRoles , RemoveMemberResponse , Response } from "./gitHubTypes" ;
3+ import { ICacheClient } from "./CacheClient" ;
4+
5+ /**
6+ * This class decorates the InstalledClient with additional caching logic. In general,
7+ * this logic should be kept simple. However, in an effort to reduce the bleeding of
8+ * complex logic into the concrete InstalledClient implementations, some complexity
9+ * may be acceptable here (such as checking for eTag and then retrying a call).
10+ *
11+ * Chain of responsibility:
12+ *
13+ * eTag checker
14+ * if there are no changes, check cache and return
15+ * if cache has nothing, call client, cache value, then return
16+ * if there are changes, check client, cache changes and etag, return
17+ */
18+ export class GitHubClientCache implements IInstalledClient {
19+ constructor ( private client : IRawInstalledGitHubClient , private cacheClient : ICacheClient , private logger : ILogger ) { }
1520
1621 AddTeamsToCopilotSubscription ( teamNames : string [ ] ) : Response < CopilotAddResponse [ ] > {
1722 return this . client . AddTeamsToCopilotSubscription ( teamNames ) ;
1823 }
19-
24+
2025 ListPendingInvitesForTeam ( teamName : string ) : Response < OrgInvite [ ] > {
2126 return this . client . ListPendingInvitesForTeam ( teamName ) ;
2227 }
23-
28+
2429 CancelOrgInvite ( invite : OrgInvite ) : Response < unknown > {
2530 return this . client . CancelOrgInvite ( invite ) ;
2631 }
@@ -45,36 +50,36 @@ export class GitHubClientCache implements InstalledClient {
4550 return this . client . AddOrgMember ( id ) ;
4651 }
4752
48- async IsUserMember ( id : string ) : Response < boolean > {
53+ async IsUserMember ( id : string ) : Response < boolean > {
4954 const cacheKey = `github-member-1:${ id } -${ this . GetCurrentOrgName ( ) } ` ;
5055
51- const result = await this . cacheClient . get ( cacheKey ) ;
56+ const result = await this . cacheClient . get ( cacheKey ) ;
5257
5358 if ( result ) {
5459 this . ReportCacheHit ( {
5560 cacheKey : cacheKey ,
5661 operation : "IsUserMember" ,
57- value : result ,
62+ value : JSON . stringify ( result ) ,
5863 user : id
5964 } ) ;
6065
6166 return {
6267 successful : true ,
6368 data : Boolean ( result )
6469 }
65- }
70+ }
6671
6772 const actualResult = await this . client . IsUserMember ( id ) ;
68-
69- if ( actualResult . successful ) {
73+
74+ if ( actualResult . successful ) {
7075 // TODO: switch all cache expirations to application configuration values.
71-
76+
7277 const userIsMember = actualResult . data ;
7378
74- if ( userIsMember ) {
79+ if ( userIsMember ) {
7580 await this . cacheClient . set ( cacheKey , userIsMember . toString ( ) , {
7681 EX : 172800 // Expire every 2 days
77- } ) ;
82+ } ) ;
7883 }
7984 else {
8085 // If membership check comes back as "not a member," we still want to cache
@@ -84,11 +89,11 @@ export class GitHubClientCache implements InstalledClient {
8489 await this . cacheClient . set ( cacheKey , userIsMember . toString ( ) , {
8590 EX : 1800 // Expire every 30 minutes
8691 // It is unlikely that 30 minutes will cause much pain
87- } ) ;
88- }
92+ } ) ;
93+ }
8994 }
9095
91- return actualResult ;
96+ return actualResult ;
9297 }
9398
9499 GetAllTeams ( ) : Response < GitHubTeamId [ ] > {
@@ -106,18 +111,18 @@ export class GitHubClientCache implements InstalledClient {
106111 async DoesUserExist ( gitHubId : string ) : Response < string > {
107112 const cacheKey = `github-user-2:${ gitHubId } ` ;
108113
109- const result = await this . cacheClient . get ( cacheKey ) ;
114+ const result = await this . cacheClient . get ( cacheKey ) ;
110115
111116 if ( result ) {
112117 this . ReportCacheHit ( {
113118 operation : "DoesUserExist" ,
114119 user : gitHubId ,
115120 value : result ,
116121 cacheKey : cacheKey
117- } ) ;
122+ } ) ;
118123
119124 return JSON . parse ( result ) ;
120- }
125+ }
121126
122127 const actualResult = await this . client . DoesUserExist ( gitHubId ) ;
123128
@@ -142,8 +147,85 @@ export class GitHubClientCache implements InstalledClient {
142147 return actualResult ;
143148 }
144149
145- ListCurrentMembersOfGitHubTeam ( team : string ) : Response < string [ ] > {
146- return this . client . ListCurrentMembersOfGitHubTeam ( team ) ;
150+ async ListCurrentMembersOfGitHubTeam ( team : string ) : Response < string [ ] > {
151+ const teamSlug = `${ this . GetCurrentOrgName ( ) } _${ team } ` ;
152+ const eTagCacheKey = `t-e:${ teamSlug } ` ;
153+ const teamCacheKey = `t:${ teamSlug } ` ;
154+
155+ const cachedEtag = await this . cacheClient . get ( eTagCacheKey ) ?? "" ;
156+
157+ const eTagResponse = await this . client . ListMembersOfTeamEtagCheck ( team , cachedEtag ) ;
158+
159+ if ( eTagResponse . successful == false ) {
160+ return {
161+ successful : false
162+ }
163+ }
164+
165+ this . ReportCacheHit ( {
166+ operation : "eTag-TeamMembers" ,
167+ team : team ,
168+ value : cachedEtag ,
169+ cacheKey : eTagCacheKey
170+ } )
171+
172+ let newETag = "" ;
173+ let teamMembers : string [ ] | undefined = undefined ;
174+
175+ if ( eTagResponse . successful == "no_changes" ) {
176+ newETag = eTagResponse . eTag ;
177+
178+ const cachedTeamMembers = await this . cacheClient . get ( teamCacheKey ) ;
179+
180+ if ( cachedTeamMembers ) {
181+ teamMembers = JSON . parse ( cachedTeamMembers ) ;
182+ this . ReportCacheHit ( {
183+ operation : "TeamMembers" ,
184+ team : team ,
185+ value : cachedTeamMembers ,
186+ cacheKey : teamCacheKey
187+ } )
188+ }
189+ else {
190+ const newTeamMembersResponse = await this . client . ListCurrentMembersOfGitHubTeam ( team ) ;
191+
192+ if ( newTeamMembersResponse . successful == false ) {
193+ return {
194+ successful : false
195+ }
196+ }
197+
198+ await this . cacheClient . set ( teamCacheKey , JSON . stringify ( newTeamMembersResponse . data ) , { EX : fourteenDaysInSeconds } ) ;
199+ teamMembers = newTeamMembersResponse . data ;
200+ }
201+ }
202+
203+ if ( eTagResponse . successful == true ) {
204+ newETag = eTagResponse . data ;
205+ const newTeamMembersResponse = await this . client . ListCurrentMembersOfGitHubTeam ( team ) ;
206+
207+ if ( newTeamMembersResponse . successful == false ) {
208+ return {
209+ successful : false
210+ }
211+ }
212+
213+ await this . cacheClient . set ( teamCacheKey , JSON . stringify ( newTeamMembersResponse . data ) , { EX : fourteenDaysInSeconds } ) ;
214+ teamMembers = newTeamMembersResponse . data ;
215+ }
216+
217+ await this . cacheClient . set ( eTagCacheKey , newETag , { EX : fourteenDaysInSeconds } ) ;
218+
219+ if ( teamMembers === undefined ) {
220+ return {
221+ successful : false
222+ }
223+ }
224+
225+ return {
226+ successful : true ,
227+ data : teamMembers
228+ } ;
147229 }
148230
149231 RemoveTeamMemberAsync ( team : string , user : string ) : RemoveMemberResponse {
@@ -160,28 +242,31 @@ export class GitHubClientCache implements InstalledClient {
160242
161243 GetConfigurationForInstallation ( ) : OrgConfigResponse {
162244 return this . client . GetConfigurationForInstallation ( ) ;
163- }
245+ }
164246
165- private ReportCacheHit ( props : { operation : string , user ?: string , team ?:string , value : string , cacheKey :string } ) {
166- const properties :any = {
247+ private ReportCacheHit ( props : { operation : string , user ?: string , team ?: string , value : string , cacheKey : string } ) {
248+ const properties : any = {
167249 "Group" : "GitHub" ,
168250 "Operation" : props . operation ,
169- "Org" : this . GetCurrentOrgName ( ) ,
251+ "Org" : this . GetCurrentOrgName ( ) ,
170252 "Value" : props . value ,
171253 "CacheKey" : props . cacheKey
172254 } ;
173255
174- if ( props . user ) {
256+ if ( props . user ) {
175257 properties [ "User" ] = props . user ;
176258 }
177259
178- if ( props . team ) {
260+ if ( props . team ) {
179261 properties [ "Team" ] = props . team ;
180262 }
181263
182264 this . logger . ReportEvent ( {
183- Name :"CacheHit" ,
265+ Name : "CacheHit" ,
184266 properties : properties
185267 } ) ;
186268 }
187- }
269+ }
270+
271+ const twoDaysInSeconds = 172_800 ;
272+ const fourteenDaysInSeconds = 1_209_600 ;
0 commit comments