@@ -2,6 +2,8 @@ import { type Database } from '@unocha/hpc-api-core/src/db';
2
2
import { type FlowId } from '@unocha/hpc-api-core/src/db/models/flow' ;
3
3
import { Op } from '@unocha/hpc-api-core/src/db/util/conditions' ;
4
4
import { type InstanceOfModel } from '@unocha/hpc-api-core/src/db/util/types' ;
5
+ import { splitIntoChunks } from '@unocha/hpc-api-core/src/util' ;
6
+ import { PG_MAX_QUERY_PARAMS } from '@unocha/hpc-api-core/src/util/consts' ;
5
7
import {
6
8
createBrandedValue ,
7
9
getTableColumns ,
@@ -24,7 +26,6 @@ import type {
24
26
UniqueFlowEntity ,
25
27
} from './model' ;
26
28
import { buildSearchFlowsConditions } from './strategy/impl/utils' ;
27
-
28
29
@Service ( )
29
30
export class FlowService {
30
31
constructor ( private readonly flowObjectService : FlowObjectService ) { }
@@ -94,7 +95,6 @@ export class FlowService {
94
95
95
96
const refDirection = orderBy . direction ?? 'source' ;
96
97
97
- let flowObjects = [ ] ;
98
98
let entityIDsSorted : number [ ] = [ ] ;
99
99
100
100
switch ( entity ) {
@@ -322,29 +322,44 @@ export class FlowService {
322
322
const entityCondKey = orderBy . entity as unknown ;
323
323
const entityCondKeyFlowObjectType = entityCondKey as FlowObjectType ;
324
324
325
- flowObjects = await database . flowObject . find ( {
326
- where : {
327
- objectType : entityCondKeyFlowObjectType ,
328
- refDirection,
329
- objectID : {
330
- [ Op . IN ] : entityIDsSorted ,
331
- } ,
332
- } ,
333
- distinct : [ 'flowID' , 'versionID' ] ,
334
- } ) ;
325
+ // Order map
326
+ const orderMap = new Map < number , number > ( ) ;
327
+ for ( const [ index , entityID ] of entityIDsSorted . entries ( ) ) {
328
+ orderMap . set ( entityID , index ) ;
329
+ }
330
+
331
+ // Instead of doing a single query that may end up on a 'Memory Error'
332
+ // we will do a progressive search
333
+ // by chunks of PG_MAX_QUERY_PARAMS - 2 => ( (2 ** 16 - 1) - 2 = 65533 )
334
+ const flowObjects = (
335
+ await Promise . all (
336
+ splitIntoChunks ( entityIDsSorted , PG_MAX_QUERY_PARAMS - 2 ) . map (
337
+ ( entityIds ) =>
338
+ database . flowObject . find ( {
339
+ where : {
340
+ objectType : entityCondKeyFlowObjectType ,
341
+ refDirection,
342
+ objectID : {
343
+ [ Op . IN ] : entityIds ,
344
+ } ,
345
+ } ,
346
+ distinct : [ 'flowID' , 'versionID' ] ,
347
+ } )
348
+ )
349
+ )
350
+ ) . flat ( ) ;
335
351
336
352
// Then, we need to filter the results from the flowObject table
337
353
// using the planVersions list as sorted reference
338
354
// this is because we cannot apply the order of a given list
339
355
// to the query directly
340
- flowObjects = flowObjects
356
+ const sortedFlowObjects = flowObjects
341
357
. map ( ( flowObject ) => ( {
342
358
...flowObject ,
343
- sortingKey : entityIDsSorted . indexOf ( flowObject . objectID . valueOf ( ) ) ,
359
+ sortingKey : orderMap . get ( flowObject . objectID ) ,
344
360
} ) )
345
- . sort ( ( a , b ) => a . sortingKey - b . sortingKey ) ;
346
-
347
- return this . mapFlowsToUniqueFlowEntities ( flowObjects ) ;
361
+ . toSorted ( ( a , b ) => ( a . sortingKey ?? 0 ) - ( b . sortingKey ?? 0 ) ) ;
362
+ return this . mapFlowsToUniqueFlowEntities ( sortedFlowObjects ) ;
348
363
}
349
364
350
365
private mapFlowsToUniqueFlowEntities (
@@ -359,7 +374,7 @@ export class FlowService {
359
374
) ;
360
375
}
361
376
362
- async getParketParents (
377
+ async getParkedParents (
363
378
flow : FlowInstance ,
364
379
flowLinkArray : Array < InstanceOfModel < Database [ 'flowLink' ] > > ,
365
380
models : Database
@@ -374,7 +389,6 @@ export class FlowService {
374
389
if ( flowLinksParentsIDs . length === 0 ) {
375
390
return null ;
376
391
}
377
-
378
392
const parkedCategory = await models . category . findOne ( {
379
393
where : {
380
394
group : 'flowType' ,
@@ -454,17 +468,18 @@ export class FlowService {
454
468
models : Database ,
455
469
flowObjectFilters : FlowObjectFilterGrouped
456
470
) : Promise < UniqueFlowEntity [ ] > {
471
+ // 1. Retrieve the parked category
457
472
const parkedCategory = await models . category . findOne ( {
458
473
where : {
459
474
name : 'Parked' ,
460
475
group : 'flowType' ,
461
476
} ,
462
477
} ) ;
463
-
464
478
if ( ! parkedCategory ) {
465
479
throw new Error ( 'Parked category not found' ) ;
466
480
}
467
481
482
+ // 2. Get all category references for parked flows
468
483
const categoryRefs = await models . categoryRef . find ( {
469
484
where : {
470
485
categoryID : parkedCategory . id ,
@@ -473,91 +488,87 @@ export class FlowService {
473
488
distinct : [ 'objectID' , 'versionID' ] ,
474
489
} ) ;
475
490
491
+ // Build list of parent IDs from categoryRefs
492
+ const parentIDs : FlowId [ ] = categoryRefs . map ( ( ref ) =>
493
+ createBrandedValue ( ref . objectID )
494
+ ) ;
495
+
496
+ // 3. Retrieve flow links where the parent is among those references and depth > 0
476
497
const flowLinks = await models . flowLink . find ( {
477
498
where : {
478
- depth : {
479
- [ Op . GT ] : 0 ,
480
- } ,
481
- parentID : {
482
- [ Op . IN ] : categoryRefs . map ( ( categoryRef ) =>
483
- createBrandedValue ( categoryRef . objectID )
484
- ) ,
485
- } ,
499
+ depth : { [ Op . GT ] : 0 } ,
500
+ parentID : { [ Op . IN ] : parentIDs } ,
486
501
} ,
487
502
distinct : [ 'parentID' , 'childID' ] ,
488
503
} ) ;
489
504
505
+ // Create a reference list of parent flows from the flow links
490
506
const parentFlowsRef : UniqueFlowEntity [ ] = flowLinks . map ( ( flowLink ) => ( {
491
- id : createBrandedValue ( flowLink . parentID ) ,
507
+ id : flowLink . parentID ,
492
508
versionID : null ,
493
509
} ) ) ;
494
510
495
- // Since this list can be really large in size: ~42k flow links
496
- // This can cause a performance issue when querying the database
497
- // and even end up with a error like:
498
- // could not resize shared memory segment \"/PostgreSQL.2154039724\"
499
- // to 53727360 bytes: No space left on device
500
-
501
- // We need to do this query by chunks
511
+ // 4. Query parent flows progressively in chunks
502
512
const parentFlows = await this . progresiveSearch (
503
513
models ,
504
514
parentFlowsRef ,
505
- 1000 ,
515
+ PG_MAX_QUERY_PARAMS - 2 , // Use a batch size of PG_MAX_QUERY_PARAMS - 2 to avoid hitting the limit
506
516
0 ,
507
- false , // Stop on batch size
517
+ false , // Do not stop on batch size
508
518
[ ] ,
509
519
{ activeStatus : true }
510
520
) ;
511
521
522
+ // 5. Retrieve flow objects using the flow object filters
512
523
const flowObjectsWhere =
513
524
buildWhereConditionsForFlowObjectFilters ( flowObjectFilters ) ;
514
-
515
525
const flowObjects = await this . flowObjectService . getFlowFromFlowObjects (
516
526
models ,
517
527
flowObjectsWhere
518
528
) ;
519
529
520
- // Once we get the flowObjects - we need to keep only those that are present in both lists
521
- const filteredParentFlows = parentFlows . filter ( ( parentFlow ) =>
522
- flowObjects . some (
523
- ( flowObject ) =>
524
- flowObject . id === parentFlow . id &&
525
- flowObject . versionID === parentFlow . versionID
530
+ // 6. Build a Set for flowObjects for fast lookup (using a composite key of id and versionID)
531
+ const flowObjectsSet = new Set (
532
+ flowObjects . map (
533
+ ( flowObject ) => `${ flowObject . id } |${ flowObject . versionID } `
526
534
)
527
535
) ;
528
536
529
- // Once we have the ParentFlows whose status are 'parked'
530
- // We keep look for the flowLinks of those flows to obtain the child flows
531
- // that are linked to them
532
- const childFlowsIDs : FlowId [ ] = [ ] ;
537
+ // 7. Filter parent flows that are present in the flowObjects list
538
+ const filteredParentFlows = parentFlows . filter ( ( parentFlow ) => {
539
+ const key = `${ parentFlow . id } |${ parentFlow . versionID } ` ;
540
+ return flowObjectsSet . has ( key ) ;
541
+ } ) ;
542
+
543
+ // 8. Build a Set of filtered parent flow IDs for quick membership checking
544
+ const filteredParentFlowIds = new Set (
545
+ filteredParentFlows . map ( ( flow ) => flow . id )
546
+ ) ;
547
+
548
+ // 9. Extract child flow IDs from flowLinks where the parent is in the filtered set
549
+ const childFlowsIDsSet = new Set < FlowId > ( ) ;
533
550
for ( const flowLink of flowLinks ) {
534
- if (
535
- filteredParentFlows . some (
536
- ( parentFlow ) => parentFlow . id === flowLink . parentID
537
- )
538
- ) {
539
- childFlowsIDs . push ( flowLink . childID ) ;
551
+ if ( filteredParentFlowIds . has ( flowLink . parentID ) ) {
552
+ childFlowsIDsSet . add ( flowLink . childID ) ;
540
553
}
541
554
}
542
555
556
+ // 10. Retrieve child flows
543
557
const childFlows = await models . flow . find ( {
544
558
where : {
545
- deletedAt : null ,
546
559
activeStatus : true ,
547
- id : {
548
- [ Op . IN ] : childFlowsIDs ,
549
- } ,
560
+ id : { [ Op . IN ] : childFlowsIDsSet } ,
550
561
} ,
551
562
distinct : [ 'id' , 'versionID' ] ,
552
563
} ) ;
553
564
554
- // Once we have the child flows, we need to filter them
555
- // using the flowObjectFilters
556
- // This search needs to be also done by chunks
557
- return childFlows . map ( ( ref ) => ( {
558
- id : createBrandedValue ( ref . id ) ,
565
+ // 11. Map child flows to UniqueFlowEntity and return the result
566
+ const result = childFlows . map ( ( ref ) => ( {
567
+ id : ref . id ,
559
568
versionID : ref . versionID ,
560
569
} ) ) ;
570
+
571
+ return result ;
561
572
}
562
573
563
574
/**
0 commit comments