feat(server): select bounded background work fairly across companies

Preserve each owner's ordering while rotating company turns between bounded batches.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-25 01:10:43 -05:00
1 parent dd2208dca7
commit e77979d4fc
2 files changed
+57

No files matched your search

@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { takeCompanyWorkBatch } from "../services/company-work-batch.js";
describe("company work batch", () => {
it("shares capacity while preserving the owner's order within each company", () => {
const entries = [
{ companyId: "a", id: "a-oldest" }, { companyId: "a", id: "a-next" },
{ companyId: "a", id: "a-later" }, { companyId: "b", id: "b-due" },
];
expect(takeCompanyWorkBatch(entries, 3)).toEqual({
entries: [entries[0], entries[3], entries[1]], lastCompany: "a",
});
});
it("rotates between batches when more companies are due than fit in one batch", () => {
const entries = ["a", "b", "c", "d"].flatMap(companyId => [
{ companyId, id: `${companyId}-first` }, { companyId, id: `${companyId}-second` },
]);
const first = takeCompanyWorkBatch(entries, 2);
const remaining = entries.filter(entry => !first.entries.includes(entry));
const second = takeCompanyWorkBatch(remaining, 2, first.lastCompany);
expect([...first.entries, ...second.entries].map(entry => entry.companyId)).toEqual(["a", "b", "c", "d"]);
expect(takeCompanyWorkBatch(remaining, 1, "z").entries[0].companyId).toBe("a");
});
it("finishes an empty or exhausted batch without inventing work", () => {
expect(takeCompanyWorkBatch([], 25, "gone")).toEqual({ entries: [], lastCompany: "gone" });
const entry = { companyId: "only" };
expect(takeCompanyWorkBatch([entry], 25).entries).toEqual([entry]);
});
});
+28
View File
@@ -0,0 +1,28 @@
export function takeCompanyWorkBatch<T extends { companyId: string }>(
ordered: readonly T[],
limit: number,
afterCompany: string | null = null,
): { entries: T[]; lastCompany: string | null } {
const grouped = new Map<string, T[]>();
for (const entry of ordered) {
const group = grouped.get(entry.companyId);
if (group) group.push(entry); else grouped.set(entry.companyId, [entry]);
}
const companies = [...grouped.keys()].sort();
const next = afterCompany === null ? 0 : companies.findIndex(id => id > afterCompany);
const start = next < 0 ? 0 : next;
const rotation = [...companies.slice(start), ...companies.slice(0, start)];
const entries: T[] = [];
let lastCompany = afterCompany;
for (let round = 0; entries.length < limit; round++) {
let added = false;
for (const companyId of rotation) {
const entry = grouped.get(companyId)![round];
if (!entry) continue;
entries.push(entry); lastCompany = companyId; added = true;
if (entries.length === limit) break;
}
if (!added) break;
}
return { entries, lastCompany };
}