55import { IModelApp } from "@itwin/core-frontend" ;
66import { useEffect , useMemo , useState } from "react" ;
77
8- import type { IModelsClient , NamedVersion } from "../clients/iModelsClient.js" ;
8+ import type { NamedVersion , Changeset } from "../clients/iModelsClient.js" ;
99import { isAbortError } from "../utils/utils.js" ;
1010import { useVersionCompare } from "../VersionCompareContext.js" ;
1111
@@ -116,129 +116,130 @@ export function useNamedVersionsList(args: UseNamedVersionListArgs): UseNamedVer
116116
117117 useEffect (
118118 ( ) => {
119- const abortController = new AbortController ( ) ;
119+ let disposed = false ;
120+
120121 setIsLoading ( true ) ;
121122 setIsError ( false ) ;
122123 setEntries ( [ ] ) ;
124+ setCurrentNamedVersion ( undefined ) ;
123125
124126 void ( async ( ) => {
125127 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 ( {
131130 iModelId,
132- signal : abortController . signal ,
131+ changesetId : currentChangesetId ,
133132 } ) ;
134- abortController . signal . throwIfAborted ( ) ;
133+ const allNamedVersions : NamedVersion [ ] = [ ] ;
134+ if ( disposed ) return ;
135135
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 ) {
142137 setIsError ( true ) ;
143- setCurrentNamedVersion ( undefined ) ;
138+ setIsLoading ( false ) ;
144139 return ;
145140 }
146141
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 ;
163161 }
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+ } ) ;
164178
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 } ` ) ;
197204 }
205+ } ) ;
198206
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 ;
209208
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+ }
217213
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 ;
225217 }
226218
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 ) ;
228229 }
229-
230- setIsLoading ( false ) ;
231230 } 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 ) ) {
236232 setIsError ( true ) ;
237233 }
234+ } finally {
235+ if ( ! disposed ) {
236+ setIsLoading ( false ) ;
237+ }
238238 }
239239 } ) ( ) ;
240+
240241 return ( ) => {
241- abortController . abort ( ) ;
242+ disposed = true ;
242243 } ;
243244 } ,
244245 [ iModelsClient , iTwinId , iModelId , currentChangesetId ] ,
@@ -253,33 +254,28 @@ export function useNamedVersionsList(args: UseNamedVersionListArgs): UseNamedVer
253254 } ;
254255}
255256
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+ ) ;
281265
282- skip += namedVersions . length ;
283- yield namedVersions ;
266+ if ( existingNamedVersion ) {
267+ return existingNamedVersion ;
284268 }
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+ } ;
285281}
0 commit comments