5
5
import { IModelApp } from "@itwin/core-frontend" ;
6
6
import { useEffect , useMemo , useState } from "react" ;
7
7
8
- import type { IModelsClient , NamedVersion } from "../clients/iModelsClient.js" ;
8
+ import type { NamedVersion , Changeset } from "../clients/iModelsClient.js" ;
9
9
import { isAbortError } from "../utils/utils.js" ;
10
10
import { useVersionCompare } from "../VersionCompareContext.js" ;
11
11
@@ -116,129 +116,130 @@ export function useNamedVersionsList(args: UseNamedVersionListArgs): UseNamedVer
116
116
117
117
useEffect (
118
118
( ) => {
119
- const abortController = new AbortController ( ) ;
119
+ let disposed = false ;
120
+
120
121
setIsLoading ( true ) ;
121
122
setIsError ( false ) ;
122
123
setEntries ( [ ] ) ;
124
+ setCurrentNamedVersion ( undefined ) ;
123
125
124
126
void ( async ( ) => {
125
127
try {
126
- abortController . signal . throwIfAborted ( ) ;
127
- // Slow! This loads all Changesets [0 -> Inf) but we'll only use [currentChangeset -> 0].
128
- // We don't need the early Changesets yet because they represent the oldest
129
- // Named Versions which will most likely appear below the fold.
130
- const changesets = await iModelsClient . getChangesets ( {
128
+ // First, get the current changeset to establish our baseline
129
+ const currentChangeset = await iModelsClient . getChangeset ( {
131
130
iModelId,
132
- signal : abortController . signal ,
131
+ changesetId : currentChangesetId ,
133
132
} ) ;
134
- abortController . signal . throwIfAborted ( ) ;
133
+ const allNamedVersions : NamedVersion [ ] = [ ] ;
134
+ if ( disposed ) return ;
135
135
136
- // Discard all future Changesets relative to the current Changeset
137
- const currentChangesetArrayIndex = changesets . findIndex (
138
- ( { id } ) => id === currentChangesetId ,
139
- ) ;
140
- if ( currentChangesetArrayIndex === - 1 ) {
141
- setIsLoading ( false ) ;
136
+ if ( ! currentChangeset ) {
142
137
setIsError ( true ) ;
143
- setCurrentNamedVersion ( undefined ) ;
138
+ setIsLoading ( false ) ;
144
139
return ;
145
140
}
146
141
147
- changesets . splice ( currentChangesetArrayIndex + 1 ) ;
148
-
149
- // We'll be looking at the most recent Named Versions first thus order
150
- // Changesets from current to oldest; highest index to lowest.
151
-
152
- changesets . reverse ( ) ;
153
- const currentChangeset = changesets [ 0 ] ;
154
- let currentNamedVersion : NamedVersion | undefined = undefined ;
155
- let seekHead = 1 ;
156
-
157
- const iterator = loadNamedVersions ( iModelsClient , iModelId , abortController . signal ) ;
158
- for await ( const page of iterator ) {
159
- // Skip pages that are newer than the currentChangeset. We'll always
160
- // find the oldest (smallest) Changeset index at the back of the page.
161
- if ( currentChangeset . index < page [ page . length - 1 ] . changesetIndex ) {
162
- continue ;
142
+ let currentNamedVersionFound : NamedVersion | undefined ;
143
+ let currentPage = 0 ;
144
+ const pageSize = 20 ;
145
+
146
+ // Load Named Versions in pages
147
+ while ( ! disposed ) {
148
+ const namedVersions = await iModelsClient . getNamedVersions ( {
149
+ iModelId,
150
+ top : pageSize ,
151
+ skip : currentPage * pageSize ,
152
+ orderby : "changesetIndex" ,
153
+ ascendingOrDescending : "desc" ,
154
+ } ) ;
155
+ allNamedVersions . push ( ...namedVersions ) ;
156
+ if ( disposed ) return ;
157
+
158
+ // If no more results, we're done
159
+ if ( namedVersions . length === 0 ) {
160
+ break ;
163
161
}
162
+ // Filter to only versions older than current
163
+ const relevantVersions = namedVersions . filter (
164
+ nv => nv . changesetIndex < currentChangeset . index ,
165
+ ) ;
166
+
167
+ // Process this page of named versions with Promise.allSettled for better error handling
168
+ const changesetPromises = relevantVersions . map ( async ( namedVersion ) => {
169
+ // we must offset the named versions , because that changeset is "already applied" to the named version, see this:
170
+ // https://developer.bentley.com/tutorials/changed-elements-api/#221-using-the-api-to-get-changed-elements
171
+ // this assuming latest is current
172
+ const offsetChangesetIndex = ( namedVersion . changesetIndex + 1 ) . toString ( ) ;
173
+
174
+ const changeSet = await iModelsClient . getChangeset ( {
175
+ iModelId : iModelId ,
176
+ changesetId : offsetChangesetIndex ,
177
+ } ) ;
164
178
165
- // According to the Intermediate Value Theorem, we must have crossed
166
- // the current Named Version in between the start and the end of current
167
- // page. If we can't find it here, we'll assume currentChangeset exists
168
- // at its declared index but doesn't have a Named Version pointing at it.
169
-
170
- const entries : NamedVersionEntry [ ] = [ ] ;
171
-
172
- for ( let i = 0 ; i < page . length ; ++ i ) {
173
- const namedVersion = page [ i ] ;
174
-
175
- if ( ! currentNamedVersion ) {
176
- if ( currentChangeset . index < namedVersion . changesetIndex ) {
177
- continue ;
178
- }
179
-
180
- if ( namedVersion . changesetId === currentChangeset . id ) {
181
- currentNamedVersion = namedVersion ;
182
- setCurrentNamedVersion ( namedVersion ) ;
183
- continue ;
184
- }
185
-
186
- currentNamedVersion = {
187
- id : currentChangeset . id ,
188
- displayName : IModelApp . localization . getLocalizedString (
189
- "VersionCompare:versionCompare.currentChangeset" ,
190
- ) ,
191
- changesetId : currentChangeset . id ,
192
- changesetIndex : currentChangeset . index ,
193
- description : currentChangeset . description ,
194
- createdDateTime : currentChangeset . pushDateTime ,
195
- } ;
196
- setCurrentNamedVersion ( currentNamedVersion ) ;
179
+ return {
180
+ namedVersion,
181
+ changeSet,
182
+ offsetChangesetIndex,
183
+ } ;
184
+ } ) ;
185
+
186
+ // Execute all in parallel with individual error handling
187
+ const results = await Promise . allSettled ( changesetPromises ) ;
188
+
189
+ // Process results
190
+ const pageEntries : NamedVersionEntry [ ] = [ ] ;
191
+ results . forEach ( ( result , index ) => {
192
+ if ( result . status === "fulfilled" && result . value . changeSet ) {
193
+ pageEntries . push ( {
194
+ namedVersion : {
195
+ ...result . value . namedVersion ,
196
+ targetChangesetId : result . value . changeSet . id ,
197
+ } ,
198
+ job : undefined ,
199
+ } ) ;
200
+ } else {
201
+ const namedVersion = relevantVersions [ index ] ;
202
+ // eslint-disable-next-line no-console
203
+ console . warn ( `Could not fetch target changeset for named version ${ namedVersion . displayName } ` ) ;
197
204
}
205
+ } ) ;
198
206
199
- // Changed Elements service asks for a changeset range to operate
200
- // on. Because user expects to see changes made since the selected
201
- // NamedVersion, we need to find the first Changeset that follows
202
- // the target NamedVersion.
203
- const recoveryPosition = seekHead ;
204
- while (
205
- seekHead < changesets . length && namedVersion . changesetIndex < changesets [ seekHead ] . index
206
- ) {
207
- seekHead += 1 ;
208
- }
207
+ if ( disposed ) return ;
209
208
210
- if ( changesets [ seekHead ] . id !== namedVersion . changesetId ) {
211
- // We didn't find the Changeset that this Named Version is based
212
- // on. UI should mark this Named Version as invalid but that's not
213
- // yet implemented.
214
- seekHead = recoveryPosition ;
215
- continue ;
216
- }
209
+ // Add to entries if we have any
210
+ if ( pageEntries . length > 0 ) {
211
+ setEntries ( prev => prev . concat ( pageEntries ) ) ;
212
+ }
217
213
218
- entries . push ( {
219
- namedVersion : {
220
- ...namedVersion ,
221
- targetChangesetId : changesets [ seekHead - 1 ] . id ,
222
- } ,
223
- job : undefined ,
224
- } ) ;
214
+ // If we got fewer results than page size, we're done
215
+ if ( namedVersions . length < pageSize ) {
216
+ break ;
225
217
}
226
218
227
- setEntries ( ( prev ) => prev . concat ( entries ) ) ;
219
+ currentPage ++ ;
220
+ }
221
+ // Set current named version if not found yet
222
+ if ( ! currentNamedVersionFound ) {
223
+ currentNamedVersionFound = getOrCreateCurrentNamedVersion (
224
+ allNamedVersions ,
225
+ currentChangeset ,
226
+ ) ;
227
+ if ( disposed ) return ;
228
+ setCurrentNamedVersion ( currentNamedVersionFound ) ;
228
229
}
229
-
230
- setIsLoading ( false ) ;
231
230
} catch ( error ) {
232
- if ( ! isAbortError ( error ) ) {
233
- // eslint-disable-next-line no-console
234
- console . error ( error ) ;
235
- setIsLoading ( false ) ;
231
+ if ( ! disposed && ! isAbortError ( error ) ) {
236
232
setIsError ( true ) ;
237
233
}
234
+ } finally {
235
+ if ( ! disposed ) {
236
+ setIsLoading ( false ) ;
237
+ }
238
238
}
239
239
} ) ( ) ;
240
+
240
241
return ( ) => {
241
- abortController . abort ( ) ;
242
+ disposed = true ;
242
243
} ;
243
244
} ,
244
245
[ iModelsClient , iTwinId , iModelId , currentChangesetId ] ,
@@ -253,33 +254,28 @@ export function useNamedVersionsList(args: UseNamedVersionListArgs): UseNamedVer
253
254
} ;
254
255
}
255
256
256
- /** Returns pages of Named Versions in reverse chronological order. */
257
- async function * loadNamedVersions (
258
- iModelsClient : IModelsClient ,
259
- iModelId : string ,
260
- signal : AbortSignal ,
261
- ) : AsyncGenerator < NamedVersion [ ] > {
262
- signal . throwIfAborted ( ) ;
263
-
264
- const pageSize = 20 ;
265
- let skip = 0 ;
266
-
267
- while ( true ) {
268
- const namedVersions = await iModelsClient . getNamedVersions ( {
269
- iModelId,
270
- top : pageSize ,
271
- skip,
272
- orderby : "changesetIndex" ,
273
- ascendingOrDescending : "desc" ,
274
- signal,
275
- } ) ;
276
- signal . throwIfAborted ( ) ;
277
-
278
- if ( namedVersions . length === 0 ) {
279
- return ;
280
- }
257
+ function getOrCreateCurrentNamedVersion (
258
+ namedVersions : NamedVersion [ ] ,
259
+ currentChangeset : Changeset ,
260
+ ) : NamedVersion {
261
+ // Check if current changeset has a named version
262
+ const existingNamedVersion = namedVersions . find (
263
+ nv => nv . changesetId === currentChangeset . id || nv . changesetIndex === currentChangeset . index ,
264
+ ) ;
281
265
282
- skip += namedVersions . length ;
283
- yield namedVersions ;
266
+ if ( existingNamedVersion ) {
267
+ return existingNamedVersion ;
284
268
}
269
+
270
+ // Create synthetic named version for current changeset
271
+ return {
272
+ id : currentChangeset . id ,
273
+ displayName : IModelApp . localization . getLocalizedString (
274
+ "VersionCompare:versionCompare.currentChangeset" ,
275
+ ) ,
276
+ changesetId : currentChangeset . id ,
277
+ changesetIndex : currentChangeset . index ,
278
+ description : currentChangeset . description || "" ,
279
+ createdDateTime : currentChangeset . pushDateTime || new Date ( ) . toISOString ( ) ,
280
+ } ;
285
281
}
0 commit comments