mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 05:16:50 +01:00
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:
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]);
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user