- ✅ Ticket 1.1: Estructura Clean Architecture en backend - ✅ Ticket 1.2: Schemas Zod compartidos - ✅ Ticket 1.3: Refactorización drugs.ts (1362 → 8 archivos modulares) - ✅ Ticket 1.4: Refactorización procedures.ts (3583 → 6 archivos modulares) - ✅ Ticket 1.5: Eliminación de duplicidades (~50 líneas) Cambios principales: - Creada estructura Clean Architecture en backend/src/ - Schemas Zod compartidos en backend/src/shared/schemas/ - Refactorización modular de drugs y procedures - Utilidades genéricas en src/utils/ (filter, validation) - Eliminados scripts obsoletos y documentación antigua - Corregidos errores: QueryClient, import test-error-handling - Build verificado y funcionando correctamente
223 lines
8.2 KiB
JavaScript
223 lines
8.2 KiB
JavaScript
import CachedResponseStateEnum from './CachedResponseStateEnum.js';
|
|
import Headers from '../../Headers.js';
|
|
import ResponseCacheFileSystem from './ResponseCacheFileSystem.js';
|
|
const UPDATE_RESPONSE_HEADERS = ['Cache-Control', 'Last-Modified', 'Vary', 'ETag'];
|
|
/**
|
|
* Fetch response cache.
|
|
*
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
|
|
* @see https://www.mnot.net/cache_docs/
|
|
*/
|
|
export default class ResponseCache {
|
|
fileSystem;
|
|
#entries = new Map();
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
constructor() {
|
|
this.fileSystem = new ResponseCacheFileSystem(this.#entries);
|
|
}
|
|
/**
|
|
* Returns cached response.
|
|
*
|
|
* @param request Request.
|
|
* @returns Cached response.
|
|
*/
|
|
get(request) {
|
|
if (request.headers.get('Cache-Control')?.includes('no-cache')) {
|
|
return null;
|
|
}
|
|
const url = request.url;
|
|
const entries = this.#entries.get(url);
|
|
if (entries) {
|
|
for (let i = 0, max = entries.length; i < max; i++) {
|
|
const entry = entries[i];
|
|
let isMatch = entry.request.method === request.method;
|
|
if (isMatch) {
|
|
for (const header of Object.keys(entry.vary)) {
|
|
const requestHeader = request.headers.get(header);
|
|
if (requestHeader !== null && entry.vary[header] !== requestHeader) {
|
|
isMatch = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (isMatch) {
|
|
if (entry.expires && entry.expires < Date.now()) {
|
|
if (entry.lastModified) {
|
|
entry.state = CachedResponseStateEnum.stale;
|
|
}
|
|
else if (!entry.etag) {
|
|
entries.splice(i, 1);
|
|
return null;
|
|
}
|
|
}
|
|
return entry;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Adds a cache entity.
|
|
*
|
|
* @param request Request.
|
|
* @param response Response.
|
|
* @returns Cached response.
|
|
*/
|
|
add(request, response) {
|
|
// We should only cache GET and HEAD requests.
|
|
if ((request.method !== 'GET' && request.method !== 'HEAD') ||
|
|
request.headers.get('Cache-Control')?.includes('no-cache')) {
|
|
return null;
|
|
}
|
|
const url = request.url;
|
|
let cachedResponse = this.get(request);
|
|
if (response.status === 304) {
|
|
if (!cachedResponse) {
|
|
throw new Error('ResponseCache: Cached response not found.');
|
|
}
|
|
for (const name of UPDATE_RESPONSE_HEADERS) {
|
|
if (response.headers.has(name)) {
|
|
cachedResponse.response.headers.set(name, response.headers.get(name));
|
|
}
|
|
}
|
|
cachedResponse.cacheUpdateTime = Date.now();
|
|
cachedResponse.state = CachedResponseStateEnum.fresh;
|
|
}
|
|
else {
|
|
if (cachedResponse) {
|
|
const entries = this.#entries.get(url);
|
|
if (entries) {
|
|
const index = entries.indexOf(cachedResponse);
|
|
if (index !== -1) {
|
|
entries.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
cachedResponse = {
|
|
response: {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
url: response.url,
|
|
headers: new Headers(response.headers),
|
|
// We need to wait for the body to be consumed and then populated if set to true (e.g. by using Response.text()).
|
|
waitingForBody: response.waitingForBody,
|
|
body: response.body ?? null
|
|
},
|
|
request: {
|
|
headers: request.headers,
|
|
method: request.method
|
|
},
|
|
vary: {},
|
|
expires: null,
|
|
etag: null,
|
|
cacheUpdateTime: Date.now(),
|
|
lastModified: null,
|
|
mustRevalidate: false,
|
|
staleWhileRevalidate: false,
|
|
state: CachedResponseStateEnum.fresh,
|
|
virtual: response.virtual ?? false
|
|
};
|
|
let entries = this.#entries.get(url);
|
|
if (!entries) {
|
|
entries = [];
|
|
this.#entries.set(url, entries);
|
|
}
|
|
entries.push(cachedResponse);
|
|
}
|
|
if (response.headers.has('Cache-Control')) {
|
|
const age = response.headers.get('Age');
|
|
for (const part of response.headers.get('Cache-Control').split(',')) {
|
|
const [key, value] = part.trim().split('=');
|
|
switch (key) {
|
|
case 'max-age':
|
|
cachedResponse.expires =
|
|
Date.now() + parseInt(value) * 1000 - (age ? parseInt(age) * 1000 : 0);
|
|
break;
|
|
case 'no-cache':
|
|
case 'no-store':
|
|
const entries = this.#entries.get(url);
|
|
if (entries) {
|
|
const index = entries.indexOf(cachedResponse);
|
|
if (index !== -1) {
|
|
entries.splice(index, 1);
|
|
}
|
|
}
|
|
return null;
|
|
case 'must-revalidate':
|
|
cachedResponse.mustRevalidate = true;
|
|
break;
|
|
case 'stale-while-revalidate':
|
|
cachedResponse.staleWhileRevalidate = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (response.headers.has('Last-Modified')) {
|
|
cachedResponse.lastModified = Date.parse(response.headers.get('Last-Modified'));
|
|
}
|
|
if (response.headers.has('Vary')) {
|
|
for (const header of response.headers.get('Vary').split(',')) {
|
|
const name = header.trim();
|
|
const value = request.headers.get(name);
|
|
if (value) {
|
|
cachedResponse.vary[name] = value;
|
|
}
|
|
}
|
|
}
|
|
if (response.headers.has('ETag')) {
|
|
cachedResponse.etag = response.headers.get('ETag');
|
|
}
|
|
if (!cachedResponse.expires) {
|
|
const expires = response.headers.get('Expires');
|
|
if (expires) {
|
|
cachedResponse.expires = Date.parse(expires);
|
|
}
|
|
}
|
|
// Cache is invalid if it has expired and doesn't have an ETag.
|
|
if (!cachedResponse.etag && (!cachedResponse.expires || cachedResponse.expires < Date.now())) {
|
|
const entries = this.#entries.get(url);
|
|
if (entries) {
|
|
const index = entries.indexOf(cachedResponse);
|
|
if (index !== -1) {
|
|
entries.splice(index, 1);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
return cachedResponse;
|
|
}
|
|
/**
|
|
* Clears the cache.
|
|
*
|
|
* @param [options] Options.
|
|
* @param [options.url] URL.
|
|
* @param [options.toTime] Removes all entries that are older than this time. Time in MS.
|
|
*/
|
|
clear(options) {
|
|
if (options) {
|
|
if (options.toTime) {
|
|
for (const key of options.url ? [options.url] : this.#entries.keys()) {
|
|
const entry = this.#entries.get(key);
|
|
if (entry) {
|
|
for (let i = 0, max = entry.length; i < max; i++) {
|
|
if (entry[i].cacheUpdateTime < options.toTime) {
|
|
entry.splice(i, 1);
|
|
i--;
|
|
max--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (options.url) {
|
|
this.#entries.delete(options.url);
|
|
}
|
|
}
|
|
else {
|
|
this.#entries.clear();
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=ResponseCache.js.map
|