feat: Implement caching and rate limiting for API requests
This commit is contained in:
@@ -21,8 +21,22 @@ const arr = (value) => (value ? (Array.isArray(value) ? value : [value]) : []);
|
||||
const DISCOVERY_TTL = 15 * 60 * 1000,
|
||||
CATALOG_TTL = 15 * 60 * 1000,
|
||||
CALENDAR_TTL = 6 * 60 * 60 * 1000;
|
||||
// A forced refresh (?refresh=1, the PWA's "Обновить") reaches the university at most this often;
|
||||
// sooner, the copy just read is the answer. Anyone can send the request, and one catalog crawl
|
||||
// is up to 180 listings of the university's Nextcloud.
|
||||
export const MIN_REFRESH = 2 * 60 * 1000;
|
||||
const CALENDAR_MIN_REFRESH = 10 * 60 * 1000;
|
||||
/** Milliseconds since an ISO time; Infinity for a missing or broken one. */
|
||||
const ageOf = (iso) => {
|
||||
const age = Date.now() - new Date(iso).getTime();
|
||||
return Number.isFinite(age) ? age : Infinity;
|
||||
};
|
||||
let catalogue, pending, calendar, calendarPending, sources, sourcesPending;
|
||||
const status = { catalogError: null, calendarError: null };
|
||||
// When the university last failed to answer: background refreshes back off for MIN_REFRESH,
|
||||
// so a down source is not crawled again on every request.
|
||||
const failedAt = { catalog: 0, calendar: 0 };
|
||||
const backingOff = (what) => Date.now() - failedAt[what] < MIN_REFRESH;
|
||||
|
||||
export async function request(path, method = "GET") {
|
||||
// Only the university share is reachable; listings and catalog entries supply the paths.
|
||||
@@ -232,15 +246,17 @@ export async function getCatalog(refresh = false) {
|
||||
const y = academicStartYear(todayIso());
|
||||
return `${y}–${y + 1}`;
|
||||
})();
|
||||
const age = catalogue ? ageOf(catalogue.updatedAt) : Infinity;
|
||||
// A cached catalog of last year is refreshed as soon as the new year begins.
|
||||
const fresh =
|
||||
catalogue &&
|
||||
Date.now() - new Date(catalogue.updatedAt).getTime() < CATALOG_TTL &&
|
||||
(catalogue.year === expected || catalogue.currentYear === false);
|
||||
if (!refresh && fresh) return catalogue;
|
||||
const thisYear =
|
||||
catalogue && (catalogue.year === expected || catalogue.currentYear === false);
|
||||
const forced = refresh && age >= MIN_REFRESH;
|
||||
if (thisYear && age < CATALOG_TTL && !forced) return catalogue;
|
||||
if (thisYear && !forced && backingOff("catalog")) return catalogue;
|
||||
if (!pending)
|
||||
pending = crawl()
|
||||
.catch((error) => {
|
||||
failedAt.catalog = Date.now();
|
||||
status.catalogError = error.message;
|
||||
if (catalogue)
|
||||
return {
|
||||
@@ -253,6 +269,9 @@ export async function getCatalog(refresh = false) {
|
||||
.finally(() => {
|
||||
pending = null;
|
||||
});
|
||||
// Stale-while-revalidate: an outdated copy of this year answers at once and the crawl runs
|
||||
// behind it. Only a first start, a new academic year or a forced refresh wait for the crawl.
|
||||
if (thisYear && !forced) return catalogue;
|
||||
return pending;
|
||||
}
|
||||
export async function documentFor(id) {
|
||||
@@ -324,15 +343,14 @@ async function readCalendarCache() {
|
||||
}
|
||||
export async function getCalendar(refresh = false) {
|
||||
await readCalendarCache();
|
||||
if (
|
||||
!refresh &&
|
||||
calendar &&
|
||||
Date.now() - new Date(calendar.updatedAt).getTime() < CALENDAR_TTL
|
||||
)
|
||||
return calendar;
|
||||
const age = calendar ? ageOf(calendar.updatedAt) : Infinity;
|
||||
const forced = refresh && age >= CALENDAR_MIN_REFRESH;
|
||||
if (calendar && age < CALENDAR_TTL && !forced) return calendar;
|
||||
if (calendar && !forced && backingOff("calendar")) return calendar;
|
||||
if (!calendarPending)
|
||||
calendarPending = loadCalendar()
|
||||
.catch((error) => {
|
||||
failedAt.calendar = Date.now();
|
||||
status.calendarError = error.message;
|
||||
if (calendar) return { ...calendar, stale: true, error: error.message };
|
||||
throw error;
|
||||
@@ -340,6 +358,8 @@ export async function getCalendar(refresh = false) {
|
||||
.finally(() => {
|
||||
calendarPending = null;
|
||||
});
|
||||
// As with the catalog, a saved week table answers while the new one loads.
|
||||
if (calendar && !forced) return calendar;
|
||||
return calendarPending;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user