51 lines
2.2 KiB
TypeScript
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.');
|
|
}
|
|
}
|