Assessor_2.0/server/postgres/revision-read-cache.ts

51 lines
2.2 KiB
TypeScript

/** Process-local read reuse. Every access checks the live database revision, including cache hits. */
export class RevisionReadCache<T> {
private readonly entries = new Map<string, { value: Promise<T>; bytes: number }>();
private revision: number | undefined;
constructor(private readonly maxEntries = 8, private readonly maxBytes = 32 * 1024 * 1024) {}
async read(key: string, currentRevision: () => Promise<number>, load: () => Promise<T>): Promise<T> {
for (let attempt = 0; attempt < 3; attempt += 1) {
const before = await currentRevision();
if (!Number.isSafeInteger(before) || before < 0) throw new Error('Cannot verify the database revision.');
if (this.revision !== before) {
this.entries.clear();
this.revision = before;
}
let entry = this.entries.get(key);
if (!entry) {
entry = { value: Promise.resolve().then(load), bytes: 0 };
this.entries.set(key, entry);
while (this.entries.size > this.maxEntries) this.entries.delete(this.entries.keys().next().value!);
}
// Refresh insertion order for bounded least-recently-used retention.
if (this.entries.get(key) === entry) {
this.entries.delete(key);
this.entries.set(key, entry);
}
try {
const value = await entry.value;
if (await currentRevision() !== before) {
if (this.entries.get(key) === entry) this.entries.delete(key);
continue;
}
if (this.entries.get(key) === entry && entry.bytes === 0) {
entry.bytes = Buffer.byteLength(JSON.stringify(value), 'utf8');
let retainedBytes = [...this.entries.values()].reduce((sum, item) => sum + item.bytes, 0);
while (retainedBytes > this.maxBytes && this.entries.size) {
const oldest = this.entries.keys().next().value!;
retainedBytes -= this.entries.get(oldest)!.bytes;
this.entries.delete(oldest);
}
}
return value;
} catch (error) {
if (this.entries.get(key) === entry) this.entries.delete(key);
throw error;
}
}
throw new Error('ASSESSOR data changed repeatedly while results were read. Please retry.');
}
}