@@ -11,6 +11,9 @@ export type GitHubSearchItem = {
1111 created_at : string ;
1212 updated_at : string ;
1313 repository_url ?: string ;
14+ pull_request ?: {
15+ merged_at : string | null ;
16+ } ;
1417} ;
1518
1619export type SearchMode = "issues" | "prs" ;
@@ -26,12 +29,14 @@ function midpoint(a: Date, b: Date) {
2629 if ( yyyymmdd ( m ) === yyyymmdd ( a ) ) {
2730 const m2 = new Date ( a ) ;
2831 m2 . setDate ( m2 . getDate ( ) + 1 ) ;
29- return m2 < b ? m2 : new Date ( a . getTime ( ) + 12 * 3600 * 1000 ) ;
32+ if ( m2 <= b ) return m2 ;
33+ const halfDay = new Date ( a . getTime ( ) + 12 * 3600 * 1000 ) ;
34+ return halfDay < b ? halfDay : b ;
3035 }
3136 return m ;
3237}
3338
34- async function gh < T > ( url : string , token ?: string ) : Promise < T > {
39+ async function gh < T > ( url : string , token ?: string , signal ?: AbortSignal ) : Promise < T > {
3540 let attempt = 0 ;
3641 while ( true ) {
3742 const resp = await fetch ( url , {
@@ -40,8 +45,13 @@ async function gh<T>(url: string, token?: string): Promise<T> {
4045 ...( token ? { Authorization : `Bearer ${ token } ` } : { } ) ,
4146 "X-GitHub-Api-Version" : "2022-11-28" ,
4247 } ,
48+ signal,
4349 } ) ;
4450
51+ if ( signal ?. aborted ) {
52+ throw new Error ( "Request aborted" ) ;
53+ }
54+
4555 if ( resp . ok ) {
4656 return ( await resp . json ( ) ) as T ;
4757 }
@@ -81,27 +91,28 @@ function buildQuery(opts: {
8191 q . push ( mode === "prs" ? "type:pr" : "type:issue" ) ;
8292 q . push ( mode === "prs" ? `author:${ username } ` : `involves:${ username } ` ) ;
8393 if ( repo ) q . push ( `repo:${ repo } ` ) ;
84- if ( title ) q . push ( `in:title ${ title } ` ) ;
85- if ( state !== "all" ) q . push ( `state :${ state } ` ) ;
94+ if ( title ) q . push ( `in:title " ${ title . replace ( / " / g , '\\"' ) } " ` ) ;
95+ if ( state !== "all" ) q . push ( `is :${ state } ` ) ;
8696 const s = start ? yyyymmdd ( start ) : "2008-01-01" ;
8797 const e = end ? yyyymmdd ( end ) : yyyymmdd ( new Date ( ) ) ;
8898 q . push ( `created:${ s } ..${ e } ` ) ;
99+ // NOTE: we join with '+' for GitHub search; each token may contain quotes; the URL layer encodes the string.
89100 return q . join ( "+" ) ;
90101}
91102
92- async function searchCount ( q : string , token ?: string ) {
103+ async function searchCount ( q : string , token ?: string , signal ?: AbortSignal ) {
93104 const url = `${ GH_API } /search/issues?q=${ q } &per_page=1&page=1` ;
94- const data = await gh < { total_count : number } > ( url , token ) ;
105+ const data = await gh < { total_count : number } > ( url , token , signal ) ;
95106 return data . total_count ;
96107}
97108
98- async function fetchWindow ( q : string , token ?: string ) : Promise < GitHubSearchItem [ ] > {
109+ async function fetchWindow ( q : string , token ?: string , signal ?: AbortSignal ) : Promise < GitHubSearchItem [ ] > {
99110 const items : GitHubSearchItem [ ] = [ ] ;
100111 let page = 1 ;
101112 while ( true ) {
102113 const url = `${ GH_API } /search/issues?q=${ q } &per_page=${ PER_PAGE } &page=${ page } ` ;
103- const data = await gh < { items : GitHubSearchItem [ ] } > ( url , token ) ;
104- const batch = ( data as any ) . items || [ ] ;
114+ const data = await gh < { items : GitHubSearchItem [ ] } > ( url , token , signal ) ;
115+ const batch = data . items || [ ] ;
105116 if ( ! batch . length ) break ;
106117 items . push ( ...batch ) ;
107118 if ( batch . length < PER_PAGE ) break ;
@@ -123,20 +134,21 @@ export async function searchUserIssuesAndPRs(params: {
123134 title ?: string ;
124135 start ?: Date ;
125136 end ?: Date ;
137+ signal ?: AbortSignal ;
126138} ) : Promise < GitHubSearchItem [ ] > {
127139 const start = params . start ?? new Date ( "2008-01-01" ) ;
128140 const end = params . end ?? new Date ( ) ;
129141
130142 async function recurse ( win : { start : Date ; end : Date } ) : Promise < GitHubSearchItem [ ] > {
131143 const q = buildQuery ( { ...params , start : win . start , end : win . end } ) ;
132- const count = await searchCount ( q , params . token ) ;
144+ const count = await searchCount ( q , params . token , params . signal ) ;
133145
134146 if ( count === 0 ) return [ ] ;
135- if ( count <= 1000 ) return fetchWindow ( q , params . token ) ;
147+ if ( count <= 1000 ) return fetchWindow ( q , params . token , params . signal ) ;
136148
137149 const mid = midpoint ( win . start , win . end ) ;
138150 const left = await recurse ( { start : win . start , end : mid } ) ;
139- const right = await recurse ( { start : new Date ( mid . getTime ( ) + 1000 ) , end : win . end } ) ;
151+ const right = await recurse ( { start : new Date ( mid . getTime ( ) + 1 ) , end : win . end } ) ;
140152 return [ ...left , ...right ] ;
141153 }
142154
@@ -161,6 +173,7 @@ export async function fetchUserItems(opts: {
161173 title ?: string ;
162174 start ?: Date ;
163175 end ?: Date ;
176+ signal ?: AbortSignal ;
164177} ) {
165178 const token =
166179 ( opts . userProvidedToken && opts . userProvidedToken . trim ( ) ) ||
@@ -180,5 +193,6 @@ export async function fetchUserItems(opts: {
180193 title : opts . title ,
181194 start : opts . start ,
182195 end : opts . end ,
196+ signal : opts . signal ,
183197 } ) ;
184198}
0 commit comments