@@ -665,128 +665,115 @@ export const slugify = (str: string | null | undefined): string => {
665665 return slug ;
666666} ;
667667
668- export function sprinkleProtectedJobs ( jobs : JobListResult [ ] ) : JobListResult [ ] {
669- if ( jobs . length <= 1 ) return jobs ;
670-
671- // Fast path: use typed arrays for better performance
672- const protectedIndices = new Uint16Array ( jobs . length ) ;
673- const publicIndices = new Uint16Array ( jobs . length ) ;
674- let protectedCount = 0 ;
675- let publicCount = 0 ;
676-
677- // Single pass separation (faster than filter)
678- for ( let i = 0 ; i < jobs . length ; i ++ ) {
679- if ( jobs [ i ] . access === "protected" ) {
680- protectedIndices [ protectedCount ++ ] = i ;
681- } else {
682- publicIndices [ publicCount ++ ] = i ;
683- }
684- }
685-
686- // Early return if no mixing needed
687- if ( protectedCount === 0 || publicCount === 0 ) return jobs ;
688-
689- // Pre-allocate result array
690- const result = new Array ( jobs . length ) ;
691- let resultIndex = 0 ;
692-
693- // Place first protected job at the very top
694- result [ resultIndex ++ ] = jobs [ protectedIndices [ 0 ] ] ;
695-
696- let protectedIndex = 1 ; // Start from second protected job
697- let publicIndex = 0 ;
668+ function makeRng ( seed : number ) {
669+ let t = seed >>> 0 ;
670+ return function rand ( ) : number {
671+ t += 0x6d2b79f5 ;
672+ let r = Math . imul ( t ^ ( t >>> 15 ) , 1 | t ) ;
673+ r ^= r + Math . imul ( r ^ ( r >>> 7 ) , 61 | r ) ;
674+ return ( ( r ^ ( r >>> 14 ) ) >>> 0 ) / 4294967296 ;
675+ } ;
676+ }
698677
699- // For very small protected sets (< 3%), concentrate them in first 100 positions
700- const isVerySmallSubset = protectedCount / jobs . length < 0.03 ;
678+ interface MixOptions {
679+ /** Size of the "priority window" we care about, default 100 */
680+ N ?: number ;
681+ /** How many from B must appear within the first N; default min(B.length, ceil(0.25*N)) */
682+ targetBInFirstN ?: number ;
683+ /** Max random jitter around evenly spaced B positions (in indices), default auto */
684+ maxJitter ?: number ;
685+ /** Seed for reproducible randomness; if omitted, Math.random() is used */
686+ seed ?: number ;
687+ }
701688
702- if ( isVerySmallSubset ) {
703- // Calculate base spacing within first 100 positions
704- const baseSpacing = Math . floor ( 100 / ( protectedCount * 1.5 ) ) ;
689+ /**
690+ * Mix B into A so that exactly kB of B appear within the first N results,
691+ * roughly evenly spaced but with jitter so it doesn't look mechanical.
692+ *
693+ * - Preserves internal order of A and of B.
694+ * - After the first N, remaining items are appended A-then-B (preserving each list's order).
695+ */
696+ function interleaveSemiRandom < A extends JobListResult , B extends JobListResult > (
697+ A : A [ ] ,
698+ B : B [ ] ,
699+ opts : MixOptions = { } ,
700+ ) : ( A | B ) [ ] {
701+ const N = opts . N ?? 100 ;
702+ const kB = Math . max (
703+ 0 ,
704+ Math . min ( B . length , N , opts . targetBInFirstN ?? Math . ceil ( 0.25 * N ) ) ,
705+ ) ;
705706
706- // Fibonacci-based spacing multipliers for less obvious distribution
707- const spacingMultipliers = [ 1 , 2 , 3 , 5 , 8 , 13 ] ;
708- let multiplierIndex = 0 ;
707+ if ( N <= 0 || kB === 0 || B . length === 0 ) {
708+ return [ ... A , ... B ] ;
709+ }
709710
710- // Place protected jobs with variable spacing in first 100 positions
711- while ( protectedIndex < protectedCount && resultIndex < 100 ) {
712- // Calculate variable spacing using multiplier
713- const currentSpacing = Math . max (
714- baseSpacing * ( spacingMultipliers [ multiplierIndex ] / 5 ) ,
715- 3 ,
716- ) ;
711+ const rng = opts . seed != null ? makeRng ( opts . seed ) : Math . random ;
712+ const maxJitter = opts . maxJitter ?? Math . max ( 1 , Math . floor ( N / ( kB * 3 ) ) ) ;
717713
718- // Add public jobs batch
719- const chunk = Math . min (
720- Math . floor ( currentSpacing ) ,
721- publicCount - publicIndex ,
722- 100 - resultIndex ,
723- ) ;
714+ const idealPositions : number [ ] = [ ] ;
715+ for ( let i = 0 ; i < kB ; i ++ ) {
716+ const pos = Math . round ( ( ( i + 0.5 ) * N ) / kB - 0.5 ) ;
717+ idealPositions . push ( Math . max ( 0 , Math . min ( N - 1 , pos ) ) ) ;
718+ }
724719
725- for ( let i = 0 ; i < chunk ; i ++ ) {
726- result [ resultIndex ++ ] = jobs [ publicIndices [ publicIndex ++ ] ] ;
727- }
720+ const positions : number [ ] = [ ] ;
721+ let lastPlaced = - 1 ;
722+ for ( let i = 0 ; i < idealPositions . length ; i ++ ) {
723+ const base = idealPositions [ i ] ;
724+ const jitter = Math . floor ( ( rng ( ) * 2 - 1 ) * maxJitter ) ;
725+ let candidate = base + jitter ;
728726
729- // Add protected job if we haven't hit position 100
730- if ( resultIndex < 100 ) {
731- result [ resultIndex ++ ] = jobs [ protectedIndices [ protectedIndex ++ ] ] ;
732- }
727+ const minAllowed = lastPlaced + 1 ;
728+ const maxAllowed = N - ( kB - i ) ;
729+ candidate = Math . max ( candidate , minAllowed ) ;
730+ candidate = Math . min ( candidate , maxAllowed ) ;
733731
734- // Cycle through multipliers
735- multiplierIndex = ( multiplierIndex + 2 ) % spacingMultipliers . length ;
736- }
732+ positions . push ( candidate ) ;
733+ lastPlaced = candidate ;
734+ }
737735
738- // Fast append remaining jobs
739- while ( publicIndex < publicCount ) {
740- result [ resultIndex ++ ] = jobs [ publicIndices [ publicIndex ++ ] ] ;
741- }
742- while ( protectedIndex < protectedCount ) {
743- result [ resultIndex ++ ] = jobs [ protectedIndices [ protectedIndex ++ ] ] ;
736+ const aIdxEnd = Math . min ( A . length , N ) ;
737+ let ai = 0 ;
738+ let bi = 0 ;
739+ let pi = 0 ;
740+ const firstWindow : ( A | B ) [ ] = [ ] ;
741+
742+ for ( let i = 0 ; i < N ; i ++ ) {
743+ const shouldPlaceB = pi < positions . length && i === positions [ pi ] ;
744+
745+ if ( shouldPlaceB && bi < B . length ) {
746+ firstWindow . push ( B [ bi ++ ] ) ;
747+ pi ++ ;
748+ } else if ( ai < aIdxEnd ) {
749+ firstWindow . push ( A [ ai ++ ] ) ;
750+ } else if ( bi < B . length && pi < positions . length ) {
751+ firstWindow . push ( B [ bi ++ ] ) ;
752+ pi ++ ;
753+ } else if ( ai < A . length ) {
754+ firstWindow . push ( A [ ai ++ ] ) ;
755+ } else if ( bi < B . length ) {
756+ firstWindow . push ( B [ bi ++ ] ) ;
757+ } else {
758+ break ;
744759 }
745- } else {
746- // Standard distribution for larger protected sets
747- const baseSpacing = Math . max (
748- Math . floor ( publicCount / ( protectedCount - 1 ) ) ,
749- 1 ,
750- ) ;
751-
752- let protectedIndex = 1 ;
753- let publicIndex = 0 ;
754-
755- // Prime numbers for spacing variation
756- const primeFactors = [ 2 , 3 , 5 , 7 , 11 ] ;
757- let primeIndex = 0 ;
758-
759- // Main distribution loop
760- while ( publicIndex < publicCount ) {
761- const variation = primeFactors [ primeIndex ] / 3 ;
762- const spacing = Math . max ( Math . floor ( baseSpacing * variation ) , 2 ) ;
763-
764- // Bulk copy public jobs
765- const chunk = Math . min ( spacing , publicCount - publicIndex ) ;
766- for ( let i = 0 ; i < chunk ; i ++ ) {
767- result [ resultIndex ++ ] = jobs [ publicIndices [ publicIndex ++ ] ] ;
768- }
760+ }
769761
770- // Insert protected job if available
771- if ( protectedIndex < protectedCount ) {
772- result [ resultIndex ++ ] = jobs [ protectedIndices [ protectedIndex ++ ] ] ;
773- }
762+ const rest : ( A | B ) [ ] = [ ] ;
763+ while ( bi < B . length ) rest . push ( B [ bi ++ ] ) ;
764+ while ( ai < A . length ) rest . push ( A [ ai ++ ] ) ;
774765
775- // Cycle through prime factors
776- primeIndex = ( primeIndex + 2 ) % primeFactors . length ;
777- }
778- }
766+ return firstWindow . concat ( rest ) ;
767+ }
779768
780- // Fill any remaining slots (shouldn't usually be needed, but ensures no undefineds)
781- while ( resultIndex < jobs . length ) {
782- if ( publicIndex < publicCount ) {
783- result [ resultIndex ++ ] = jobs [ publicIndices [ publicIndex ++ ] ] ;
784- } else if ( protectedIndex < protectedCount ) {
785- result [ resultIndex ++ ] = jobs [ protectedIndices [ protectedIndex ++ ] ] ;
786- }
787- }
769+ export function sprinkleProtectedJobs ( jobs : JobListResult [ ] ) : JobListResult [ ] {
770+ const protectedJobs = jobs . filter ( job => job . access === "protected" ) ;
771+ const publicJobs = jobs . filter ( job => job . access === "public" ) ;
788772
789- return result ;
773+ return interleaveSemiRandom ( publicJobs , protectedJobs , {
774+ N : Math . floor ( jobs . length * 0.5 ) ,
775+ targetBInFirstN : Math . floor ( protectedJobs . length * 0.5 ) ,
776+ } ) ;
790777}
791778
792779export const isValidFilterConfig = ( value : string ) : boolean =>
0 commit comments