70 lines
3.8 KiB
JavaScript
70 lines
3.8 KiB
JavaScript
function createFormRequestCache({ getHours, getUserId }) {
|
|
const entries = new Map();
|
|
const pending = new Map();
|
|
let generation = 0;
|
|
let owner = "";
|
|
const queries = new Set([
|
|
"requestWorkloads",
|
|
"requestWeekDetails", "requestTimeEntries", "requestActiveServiceRequests", "requestServiceRequestExpand", "requestLookupTaskType",
|
|
"requestActionTeamMembers", "requestActionFindLookup", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage",
|
|
"requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestLookupTypePlatform", "requestInternalSrServices"
|
|
]);
|
|
const writes = new Set(["requestTimeEntriesDelete", "requestTimeEntriesSave", "requestMgmtAction", "requestPostInternalServiceRequest", "requestPostServiceRequest", "requestWorkloadForecastUpdate"]);
|
|
function invalidate() { generation++; entries.clear(); pending.clear(); }
|
|
function maxAge() {
|
|
const hours = Number(getHours());
|
|
return (Number.isFinite(hours) && hours > 0 ? Math.min(hours, 168) : 24) * 3600000;
|
|
}
|
|
function copy(value) { return JSON.parse(JSON.stringify(value)); }
|
|
function stable(value) {
|
|
if (Array.isArray(value)) return value.map(stable);
|
|
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])]));
|
|
return value;
|
|
}
|
|
async function send(message, transport) {
|
|
const userId = String(getUserId() || "").trim().toLowerCase();
|
|
if (userId !== owner) { invalidate(); owner = userId; }
|
|
const operation = String(message.type || "").split(".").pop();
|
|
const duration = maxAge();
|
|
const request = queries.has(operation) || writes.has(operation) || operation === "requestResourceUser"
|
|
? { ...message, cacheMaxAgeMs: duration } : message;
|
|
if (operation === "invalidateProductsCache") invalidate();
|
|
// Only explicitly known read operations are reusable. Writes and token generation always execute.
|
|
if (!queries.has(operation) || !userId) {
|
|
const result = await transport(request);
|
|
if (result?.ok && writes.has(operation) && !(operation === "requestPostServiceRequest" && message.phase === "prepare")) invalidate();
|
|
return result;
|
|
}
|
|
// Workloads are scoped to the user/account, not the rotating WWB token or the selected Reference.
|
|
const key = JSON.stringify(stable(operation === "requestWorkloads"
|
|
? { type: message.type, customerId: String(message.customerId), principal: String(message.principal || userId).trim().toLowerCase() }
|
|
: message));
|
|
const hit = entries.get(key);
|
|
if (hit && Date.now() >= hit.fetchedAt && Date.now() - hit.fetchedAt < duration) {
|
|
return { ...copy(hit.result), debugRequests: [], fromCache: true, cachedAt: hit.fetchedAt };
|
|
}
|
|
entries.delete(key);
|
|
if (pending.has(key)) {
|
|
const result = await pending.get(key);
|
|
return { ...copy(result), debugRequests: [] };
|
|
}
|
|
const version = generation;
|
|
const promise = transport(request).then(result => {
|
|
const workload = operation === "requestWorkloads";
|
|
const fetchedAt = workload && Number.isFinite(result?.cachedAt) ? result.cachedAt : Date.now();
|
|
const cacheable = !workload || (Array.isArray(result?.workloads) && (!result.fromCache || Number.isFinite(result.cachedAt)));
|
|
if (result?.ok && cacheable && version === generation) {
|
|
// Keep this per-page cache bounded and avoid persisting credentials or customer data to disk.
|
|
if (entries.size >= 500) entries.delete(entries.keys().next().value);
|
|
// Reusing a persistent workload entry must not restart its original TTL.
|
|
entries.set(key, { result: copy({ ...result, debugRequests: [] }), fetchedAt });
|
|
}
|
|
return result;
|
|
});
|
|
pending.set(key, promise);
|
|
try { return await promise; }
|
|
finally { if (pending.get(key) === promise) pending.delete(key); }
|
|
}
|
|
return { send, invalidate };
|
|
}
|