Skip to content

Commit 259e544

Browse files
committed
fix(jobs): fix unsatisfactory job sprinkler
1 parent cf0e2aa commit 259e544

File tree

1 file changed

+95
-108
lines changed

1 file changed

+95
-108
lines changed

src/shared/helpers/index.ts

Lines changed: 95 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -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

792779
export const isValidFilterConfig = (value: string): boolean =>

0 commit comments

Comments
 (0)