mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-07 09:38:45 +00:00
131 lines
3.4 KiB
JavaScript
131 lines
3.4 KiB
JavaScript
export class ExpiringPromiseCache {
|
|
#entries = new Map();
|
|
#maxEntries;
|
|
#ttlMs;
|
|
|
|
constructor({ maxEntries, ttlMs }) {
|
|
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
|
|
throw new TypeError("maxEntries must be a positive integer.");
|
|
}
|
|
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
throw new TypeError("ttlMs must be a positive number.");
|
|
}
|
|
this.#maxEntries = maxEntries;
|
|
this.#ttlMs = ttlMs;
|
|
}
|
|
|
|
clear() {
|
|
this.#entries.clear();
|
|
}
|
|
|
|
delete(key) {
|
|
this.#entries.delete(String(key));
|
|
}
|
|
|
|
peek(key) {
|
|
const normalizedKey = String(key);
|
|
const entry = this.#entries.get(normalizedKey);
|
|
if (!entry?.hasValue || entry.expiresAt <= Date.now()) {
|
|
if (!entry?.promise) this.#entries.delete(normalizedKey);
|
|
return undefined;
|
|
}
|
|
this.#touch(normalizedKey, entry);
|
|
return entry.value;
|
|
}
|
|
|
|
set(key, value) {
|
|
const normalizedKey = String(key);
|
|
const entry = {
|
|
expiresAt: Date.now() + this.#ttlMs,
|
|
forceRefresh: false,
|
|
hasValue: true,
|
|
promise: null,
|
|
value,
|
|
};
|
|
this.#entries.set(normalizedKey, entry);
|
|
this.#touch(normalizedKey, entry);
|
|
this.#enforceLimit();
|
|
return value;
|
|
}
|
|
|
|
async get(key, loader, { force = false } = {}) {
|
|
if (typeof loader !== "function") throw new TypeError("loader must be a function.");
|
|
const normalizedKey = String(key);
|
|
const now = Date.now();
|
|
let entry = this.#entries.get(normalizedKey);
|
|
if (!force && entry?.hasValue && entry.expiresAt > now) {
|
|
this.#touch(normalizedKey, entry);
|
|
return entry.value;
|
|
}
|
|
if (entry?.promise && (!force || entry.forceRefresh)) return entry.promise;
|
|
|
|
this.#prune(now);
|
|
if (force) entry = null;
|
|
entry ??= {
|
|
expiresAt: 0,
|
|
forceRefresh: false,
|
|
hasValue: false,
|
|
promise: null,
|
|
value: undefined,
|
|
};
|
|
const promise = Promise.resolve()
|
|
.then(loader)
|
|
.then(
|
|
(value) => {
|
|
if (
|
|
this.#entries.get(normalizedKey) === entry &&
|
|
entry.promise === promise
|
|
) {
|
|
entry.expiresAt = Date.now() + this.#ttlMs;
|
|
entry.forceRefresh = false;
|
|
entry.hasValue = true;
|
|
entry.promise = null;
|
|
entry.value = value;
|
|
this.#touch(normalizedKey, entry);
|
|
this.#enforceLimit();
|
|
}
|
|
return value;
|
|
},
|
|
(error) => {
|
|
if (
|
|
this.#entries.get(normalizedKey) === entry &&
|
|
entry.promise === promise
|
|
) {
|
|
entry.forceRefresh = false;
|
|
entry.promise = null;
|
|
if (!entry.hasValue || entry.expiresAt <= Date.now()) {
|
|
this.#entries.delete(normalizedKey);
|
|
}
|
|
}
|
|
throw error;
|
|
},
|
|
);
|
|
entry.forceRefresh = force;
|
|
entry.promise = promise;
|
|
this.#entries.set(normalizedKey, entry);
|
|
this.#enforceLimit();
|
|
return promise;
|
|
}
|
|
|
|
#touch(key, entry) {
|
|
this.#entries.delete(key);
|
|
this.#entries.set(key, entry);
|
|
}
|
|
|
|
#prune(now) {
|
|
for (const [key, entry] of this.#entries) {
|
|
if (!entry.promise && (!entry.hasValue || entry.expiresAt <= now)) {
|
|
this.#entries.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
#enforceLimit() {
|
|
while (this.#entries.size > this.#maxEntries) {
|
|
const candidate = [...this.#entries].find(([, entry]) => !entry.promise);
|
|
if (!candidate) return;
|
|
this.#entries.delete(candidate[0]);
|
|
}
|
|
}
|
|
}
|