Derive the retained octets from ExpiringGroups' weight
Test / lint (pull_request) Successful in 22s
Test / test (18) (pull_request) Successful in 31s
Test / test (20) (pull_request) Successful in 31s
Test / test (22) (pull_request) Successful in 31s
Test / test (24) (pull_request) Successful in 33s
Test / test (26) (pull_request) Successful in 32s
Mirror / push (push) Successful in 5s

This commit is contained in:
2026-09-28 01:09:12 +02:00
parent 0b9b210fb2
commit 91c155175f
6 changed files with 79 additions and 76 deletions
+47 -4
View File
@@ -1,5 +1,7 @@
export type ExpiringGroupsOptions = {
max: number;
/** What the groups may weigh together before weigh() evicts the oldest. */
maxWeight?: number | undefined;
/** Injected so expiry can be exercised without a wall clock. */
now?: (() => number) | undefined;
/** Runs on the sweeper's own timer; the owner reports whatever it takes out. */
@@ -10,6 +12,7 @@ export type ExpiringGroupsOptions = {
type Entry<T> = {
deadline: number;
group: T;
weight: number;
};
/**
@@ -19,13 +22,16 @@ type Entry<T> = {
export class ExpiringGroups<T> {
private readonly entries = new Map<string, Entry<T>>();
private readonly max: number;
private readonly maxWeight: number;
private readonly now: () => number;
private readonly onSweep: () => void;
private readonly timeout: number;
private sweeper: NodeJS.Timeout | undefined;
private total = 0;
constructor(options: ExpiringGroupsOptions) {
this.max = options.max;
this.maxWeight = options.maxWeight ?? Infinity;
this.now = options.now ?? Date.now;
this.onSweep = options.onSweep;
this.timeout = options.timeout;
@@ -39,13 +45,19 @@ export class ExpiringGroups<T> {
return this.entries.size;
}
get weight(): number {
return this.total;
}
get(key: string): T | undefined {
return this.entries.get(key)?.group;
}
/** Starts the group's deadline, and the sweeper if this is the only group held. */
set(key: string, group: T): void {
this.entries.set(key, { deadline: this.now() + this.timeout, group });
set(key: string, group: T, weight = 0): void {
this.remove(key);
this.entries.set(key, { deadline: this.now() + this.timeout, group, weight });
this.total += weight;
if (this.sweeper) return;
@@ -54,10 +66,31 @@ export class ExpiringGroups<T> {
}
delete(key: string): void {
this.entries.delete(key);
this.remove(key);
this.idle();
}
/** Records what a group weighs now, and hands over the oldest groups evicted to bring the total under maxWeight. */
weigh(key: string, weight: number): [string, T][] {
const entry = this.entries.get(key);
const taken: [string, T][] = [];
if (entry) {
this.total += weight - entry.weight;
entry.weight = weight;
}
while (this.total > this.maxWeight) {
const oldest = this.takeOldest();
if (!oldest) break;
taken.push(oldest);
}
return taken;
}
/** Removes every group and hands them over, so an owner that must account for them can. */
takeAll(): [string, T][] {
const taken: [string, T][] = [];
@@ -67,6 +100,7 @@ export class ExpiringGroups<T> {
}
this.entries.clear();
this.total = 0;
this.idle();
return taken;
@@ -81,7 +115,7 @@ export class ExpiringGroups<T> {
if (entry.deadline > now) continue;
taken.push([key, entry.group]);
this.entries.delete(key);
this.remove(key);
}
this.idle();
@@ -102,6 +136,15 @@ export class ExpiringGroups<T> {
return [key, entry.group];
}
private remove(key: string): void {
const entry = this.entries.get(key);
if (!entry) return;
this.entries.delete(key);
this.total -= entry.weight;
}
private idle(): void {
if (!this.sweeper || this.entries.size > 0) return;