import { expect } from "chai" ; import { BeDuration , Id64 , Id64Arg , Id64Set , Id64String } from "@itwin/core-bentley" ; import { IModelConnection , SnapshotConnection , SubCategoriesCache } from "@itwin/core-frontend" ; import { TestUtility } from "../TestUtility" ; describe ( "SubCategoriesCache" , ( ) => { let imodel ; before ( async ( ) => { await TestUtility . startFrontend ( undefined , true ) ; imodel = await SnapshotConnection . openFile ( "test.bim" ) ; } ) ; after ( async ( ) => { if ( undefined !== imodel ) await imodel . close ( ) ; await TestUtility . shutdownFrontend ( ) ; } ) ; it ( "should not repeatedly request same categories" , async ( ) => { const catIds = new Set < string > ( ) ; catIds . add ( "0x2f" ) ; catIds . add ( "0x17" ) ; const subcats = new SubCategoriesCache ( imodel ) ; const req1 = subcats . load ( catIds ) ; expect ( req1 ) . not . to . be . undefined ; expect ( req1 ! . missingCategoryIds . size ) . to . equal ( catIds . size ) ; for ( const catId of catIds ) expect ( req1 ! . missingCategoryIds . has ( catId ) ) . to . be . true ; const res1 = await req1 ! . promise ; expect ( res1 ) . to . be . true ; const req2 = subcats . load ( catIds ) ; expect ( req2 ) . to . be . undefined ; catIds . clear ( ) ; catIds . add ( "0x1a" ) ; catIds . add ( "0x12345678" ) ; catIds . add ( "lalala" ) ; const req3 = subcats . load ( catIds ) ; expect ( req3 ) . not . to . be . undefined ; expect ( req3 ! . missingCategoryIds . size ) . to . equal ( catIds . size ) ; for ( const catId of catIds ) expect ( req3 ! . missingCategoryIds . has ( catId ) ) . to . be . true ; const res3 = await req3 ! . promise ; expect ( res3 ) . to . be . true ; const req4 = subcats . load ( catIds ) ; expect ( req4 ) . to . be . undefined ; } ) ; function expectEqualIdSets ( idSet , ids ) { expect ( idSet . size ) . to . equal ( Id64 . sizeOf ( ids ) ) ; for ( const id of Id64 . iterable ( ids ) ) expect ( idSet . has ( id ) ) . to . be . true ; } class Queue extends SubCategoriesCache . Queue { public readonly cache : SubCategoriesCache ; public constructor ( iModel ) { super ( ) ; this . cache = new SubCategoriesCache ( iModel ) ; } public q ( catIds , func = ( ) => undefined ) { this . push ( this . cache , catIds , func ) ; } public expectMembers ( current , next , request , disposed = false ) { expect ( this . current !== undefined ) . to . equal ( current ) ; expect ( this . next !== undefined ) . to . equal ( next ) ; expect ( this . request !== undefined ) . to . equal ( request ) ; expect ( this . disposed ) . to . equal ( disposed ) ; } public expectNotLoaded ( catIds ) { for ( const catId of Id64 . iterable ( catIds ) ) expect ( this . cache . getSubCategories ( catId ) ) . to . be . undefined ; } public expectLoaded ( catId , subcatIds ) { const subcats = this . cache . getSubCategories ( catId ) ; expect ( subcats ) . not . to . be . undefined ; expectEqualIdSets ( subcats ! , subcatIds ) ; } public get current ( ) { return this . _current ; } public get next ( ) { return this . _next ; } public get request ( ) { return this . _request ; } public get disposed ( ) { return this . _disposed ; } public async waitUntilEmpty ( ) { while ( ! this . isEmpty ) await BeDuration . wait ( 1 ) ; } public expectEmpty ( disposed = false ) { this . expectMembers ( false , false , false , disposed ) ; expect ( this . isEmpty ) . to . be . true ; } public expectFull ( ) { this . expectMembers ( true , true , true ) ; expect ( this . isEmpty ) . to . be . false ; } } it ( "should have expected members" , ( ) => { const q = new Queue ( imodel ) ; q . expectEmpty ( ) ; q . dispose ( ) ; q . expectEmpty ( true ) ; } ) ; it ( "should execute empty request immediately" , ( ) => { const q = new Queue ( imodel ) ; let proc = 0 ; q . q ( new Set < string > ( ) , ( ) => ++ proc ) ; expect ( proc ) . to . equal ( 1 ) ; expect ( q . request ) . to . be . undefined ; q . expectEmpty ( ) ; } ) ; it ( "should execute request for unloaded categories asynchronously" , async ( ) => { const q = new Queue ( imodel ) ; q . expectNotLoaded ( "0x2f" ) ; q . q ( "0x2f" ) ; q . expectNotLoaded ( "02f" ) ; await q . waitUntilEmpty ( ) ; q . expectLoaded ( "0x2f" , [ "0x30" , "0x33" ] ) ; } ) ; it ( "should process category asynchronously, then same category synchronously" , async ( ) => { const q = new Queue ( imodel ) ; q . expectNotLoaded ( "0x17" ) ; let processedFirst = 0 ; let processedSecond = 0 ; q . q ( "0x17" , ( ) => ++ processedFirst ) ; q . q ( "0x17" , ( ) => ++ processedSecond ) ; q . expectFull ( ) ; expectEqualIdSets ( q . current ! . categoryIds , q . request ! . missingCategoryIds ) ; expectEqualIdSets ( q . current ! . categoryIds , "0x17" ) ; expectEqualIdSets ( q . current ! . categoryIds , q . next ! . categoryIds ) ; q . expectNotLoaded ( "0x17" ) ; await q . waitUntilEmpty ( ) ; q . expectLoaded ( "0x17" , "0x18" ) ; q . expectEmpty ( ) ; expect ( processedFirst ) . to . equal ( 1 ) ; expect ( processedSecond ) . to . equal ( 1 ) ; } ) ; it ( "should process consecutive requests asynchronously" , async ( ) => { const q = new Queue ( imodel ) ; q . expectNotLoaded ( [ "0x17" , "0x2f" , "0x2d" ] ) ; let lastProcessed = 0 ; q . q ( "0x17" , ( ) => { expect ( lastProcessed ++ ) . to . equal ( 0 ) ; q . expectLoaded ( "0x17" , "0x18" ) ; q . expectNotLoaded ( [ "0x2f" , "0x2d" ] ) ; } ) ; q . q ( "0x2f" , ( ) => { expect ( lastProcessed ++ ) . to . equal ( 1 ) ; q . expectLoaded ( "0x2f" , [ "0x30" , "0x33" ] ) ; q . expectLoaded ( "0x2d" , "0x2e" ) ; } ) ; q . q ( "0x2d" , ( ) => { expect ( lastProcessed ++ ) . to . equal ( 2 ) ; } ) ; q . expectFull ( ) ; expectEqualIdSets ( q . current ! . categoryIds , q . request ! . missingCategoryIds ) ; expectEqualIdSets ( q . current ! . categoryIds , "0x17" ) ; expectEqualIdSets ( q . next ! . categoryIds , [ "0x2f" , "0x2d" ] ) ; q . expectNotLoaded ( [ "0x17" , "0x2f" , "0x2d" ] ) ; await q . waitUntilEmpty ( ) ; q . q ( [ "0x17" , "0x2f" , "0x2d" ] , ( ) => { expect ( lastProcessed ++ ) . to . equal ( 3 ) ; } ) ; q . expectEmpty ( ) ; expect ( lastProcessed ) . to . equal ( 4 ) ; lastProcessed = 0 ; q . q ( "0x17" , ( ) => { expect ( lastProcessed ++ ) . to . equal ( 0 ) ; } ) ; q . q ( "0x2f" , ( ) => { expect ( lastProcessed ++ ) . to . equal ( 1 ) ; } ) ; q . q ( "0x2d" , ( ) => { expect ( lastProcessed ++ ) . to . equal ( 2 ) ; } ) ; q . expectEmpty ( ) ; expect ( lastProcessed ) . to . equal ( 3 ) ; } ) ; it ( "should process loaded categories immediately, then unloaded categories asynchronously" , async ( ) => { const q = new Queue ( imodel ) ; const load = q . cache . load ( "0x17" ) ; expect ( load ) . not . to . be . undefined ; await load ! . promise ; q . expectLoaded ( "0x17" , "0x18" ) ; let lastProcessed = 0 ; q . q ( "0x17" , ( ) => { expect ( lastProcessed ++ ) . to . equal ( 0 ) ; q . expectNotLoaded ( "0x2d" ) ; } ) ; q . q ( "0x2d" , ( ) => { expect ( lastProcessed ++ ) . to . equal ( 1 ) ; q . expectLoaded ( "0x2d" , "0x2e" ) ; } ) ; q . expectMembers ( true , false , true ) ; expect ( lastProcessed ) . to . equal ( 1 ) ; q . expectNotLoaded ( "0x2d" ) ; await q . waitUntilEmpty ( ) ; expect ( lastProcessed ) . to . equal ( 2 ) ; q . expectLoaded ( "0x2d" , "0x2e" ) ; } ) ; it ( "should not process requests fulfilled after disposal" , async ( ) => { const q = new Queue ( imodel ) ; let processed = false ; q . q ( "0x17" , ( ) => processed = true ) ; expect ( q . request ) . not . to . be . undefined ; const promise = q . request ! . promise ; let promiseFulfilled = false ; promise . then ( ( ) => promiseFulfilled = true ) ; q . expectMembers ( true , false , true ) ; q . dispose ( ) ; q . expectEmpty ( true ) ; await promise ; expect ( promiseFulfilled ) . to . be . true ; expect ( processed ) . to . be . false ; q . expectNotLoaded ( "0x17" ) ; } ) ; it ( "should not process synchronous requests after disposal" , async ( ) => { const q = new Queue ( imodel ) ; q . q ( "0x17" ) ; await q . waitUntilEmpty ( ) ; q . expectLoaded ( "0x17" , "0x18" ) ; let processed = false ; q . dispose ( ) ; q . q ( "0x17" , ( ) => processed = true ) ; q . expectEmpty ( true ) ; expect ( processed ) . to . be . false ; } ) ; it ( "should not process pending asynchronous requests after disposal" , async ( ) => { const q = new Queue ( imodel ) ; q . q ( "0x17" , ( ) => q . dispose ( ) ) ; let processedPending = false ; q . q ( "0x2d" , ( ) => processedPending = true ) ; await q . waitUntilEmpty ( ) ; expect ( processedPending ) . to . be . false ; q . expectLoaded ( "0x17" , "0x18" ) ; q . expectNotLoaded ( "0x2d" ) ; } ) ; it ( "should not process pending synchronous requests after disposal" , async ( ) => { const q = new Queue ( imodel ) ; q . q ( "0x17" , ( ) => q . dispose ( ) ) ; let processedPending = false ; q . q ( "0x17" , ( ) => processedPending = true ) ; await q . waitUntilEmpty ( ) ; expect ( processedPending ) . to . be . false ; q . expectLoaded ( "0x17" , "0x18" ) ; } ) ; it ( "should not create requests after disposal" , ( ) => { const q = new Queue ( imodel ) ; q . dispose ( ) ; q . q ( "0x17" ) ; q . expectEmpty ( true ) ; } ) ; } ) ;