feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,7 @@
import Headers from '../../Headers.js';
export default interface ICachablePreflightRequest {
url: string;
method: string;
headers: Headers;
}
//# sourceMappingURL=ICachablePreflightRequest.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachablePreflightRequest.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/ICachablePreflightRequest.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,kBAAkB,CAAC;AAEvC,MAAM,CAAC,OAAO,WAAW,yBAAyB;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CACjB"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ICachablePreflightRequest.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachablePreflightRequest.js","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/ICachablePreflightRequest.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,7 @@
import Headers from '../../Headers.js';
export default interface ICachablePreflightResponse {
status: number;
url: string;
headers: Headers;
}
//# sourceMappingURL=ICachablePreflightResponse.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachablePreflightResponse.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/ICachablePreflightResponse.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,kBAAkB,CAAC;AAEvC,MAAM,CAAC,OAAO,WAAW,0BAA0B;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;CACjB"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ICachablePreflightResponse.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachablePreflightResponse.js","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/ICachablePreflightResponse.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,6 @@
export default interface ICachedPreflightResponse {
allowOrigin: string;
allowMethods: string[];
expires: number;
}
//# sourceMappingURL=ICachedPreflightResponse.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachedPreflightResponse.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/ICachedPreflightResponse.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,WAAW,wBAAwB;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CAChB"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ICachedPreflightResponse.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachedPreflightResponse.js","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/ICachedPreflightResponse.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,28 @@
import ICachedPreflightResponse from './ICachedPreflightResponse.js';
import ICachablePreflightRequest from './ICachablePreflightRequest.js';
import ICachablePreflightResponse from './ICachablePreflightResponse.js';
/**
* Fetch response cache.
*/
export default interface IPreflightResponseCache {
/**
* Returns cached response.
*
* @param request Request.
* @returns Cached response.
*/
get(request: ICachablePreflightRequest): ICachedPreflightResponse | null;
/**
* Adds a cached response.
*
* @param request Request.
* @param response Response.
* @returns Cached response.
*/
add(request: ICachablePreflightRequest, response: ICachablePreflightResponse): ICachedPreflightResponse | null;
/**
* Clears the cache.
*/
clear(): void;
}
//# sourceMappingURL=IPreflightResponseCache.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"IPreflightResponseCache.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/IPreflightResponseCache.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,MAAM,+BAA+B,CAAC;AACrE,OAAO,yBAAyB,MAAM,gCAAgC,CAAC;AACvE,OAAO,0BAA0B,MAAM,iCAAiC,CAAC;AAEzE;;GAEG;AACH,MAAM,CAAC,OAAO,WAAW,uBAAuB;IAC/C;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,GAAG,IAAI,CAAC;IAEzE;;;;;;OAMG;IACH,GAAG,CACF,OAAO,EAAE,yBAAyB,EAClC,QAAQ,EAAE,0BAA0B,GAClC,wBAAwB,GAAG,IAAI,CAAC;IAEnC;;OAEG;IACH,KAAK,IAAI,IAAI,CAAC;CACd"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=IPreflightResponseCache.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"IPreflightResponseCache.js","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/IPreflightResponseCache.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,32 @@
import IPreflightResponseCache from './IPreflightResponseCache.js';
import ICachablePreflightRequest from './ICachablePreflightRequest.js';
import ICachedPreflightResponse from './ICachedPreflightResponse.js';
import ICachablePreflightResponse from './ICachablePreflightResponse.js';
/**
* Fetch preflight response cache.
*
* @see https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
*/
export default class PreflightResponseCache implements IPreflightResponseCache {
#private;
/**
* Returns cached response.
*
* @param request Request.
* @returns Cached response.
*/
get(request: ICachablePreflightRequest): ICachedPreflightResponse | null;
/**
* Adds a cache entity.
*
* @param request Request.
* @param response Response.
* @returns Cached response.
*/
add(request: ICachablePreflightRequest, response: ICachablePreflightResponse): ICachedPreflightResponse;
/**
* Clears the cache.
*/
clear(): void;
}
//# sourceMappingURL=PreflightResponseCache.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"PreflightResponseCache.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/PreflightResponseCache.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,MAAM,8BAA8B,CAAC;AACnE,OAAO,yBAAyB,MAAM,gCAAgC,CAAC;AACvE,OAAO,wBAAwB,MAAM,+BAA+B,CAAC;AACrE,OAAO,0BAA0B,MAAM,iCAAiC,CAAC;AAEzE;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,uBAAuB;;IAG7E;;;;;OAKG;IACI,GAAG,CAAC,OAAO,EAAE,yBAAyB,GAAG,wBAAwB,GAAG,IAAI;IAc/E;;;;;;OAMG;IACI,GAAG,CACT,OAAO,EAAE,yBAAyB,EAClC,QAAQ,EAAE,0BAA0B,GAClC,wBAAwB;IA4C3B;;OAEG;IACI,KAAK,IAAI,IAAI;CAGpB"}

View File

@@ -0,0 +1,72 @@
/**
* Fetch preflight response cache.
*
* @see https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
*/
export default class PreflightResponseCache {
#entries = {};
/**
* Returns cached response.
*
* @param request Request.
* @returns Cached response.
*/
get(request) {
const cachedResponse = this.#entries[request.url];
if (cachedResponse) {
if (cachedResponse.expires < Date.now()) {
delete this.#entries[request.url];
return null;
}
return cachedResponse;
}
return null;
}
/**
* Adds a cache entity.
*
* @param request Request.
* @param response Response.
* @returns Cached response.
*/
add(request, response) {
delete this.#entries[request.url];
if (request.headers.get('Cache-Control')?.includes('no-cache')) {
return null;
}
if (response.status < 200 || response.status >= 300) {
return null;
}
const maxAge = response.headers.get('Access-Control-Max-Age');
const allowOrigin = response.headers.get('Access-Control-Allow-Origin');
if (!maxAge || !allowOrigin) {
return null;
}
const allowMethods = [];
if (response.headers.has('Access-Control-Allow-Methods')) {
const allowMethodsHeader = response.headers.get('Access-Control-Allow-Methods');
if (allowMethodsHeader !== '*') {
for (const method of response.headers.get('Access-Control-Allow-Methods').split(',')) {
allowMethods.push(method.trim().toUpperCase());
}
}
}
const cachedResponse = {
allowOrigin,
allowMethods,
expires: Date.now() + parseInt(maxAge) * 1000
};
if (isNaN(cachedResponse.expires) || cachedResponse.expires < Date.now()) {
return null;
}
this.#entries[request.url] = cachedResponse;
return cachedResponse;
}
/**
* Clears the cache.
*/
clear() {
this.#entries = {};
}
}
//# sourceMappingURL=PreflightResponseCache.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"PreflightResponseCache.js","sourceRoot":"","sources":["../../../../src/fetch/cache/preflight/PreflightResponseCache.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,sBAAsB;IAC1C,QAAQ,GAAgD,EAAE,CAAC;IAE3D;;;;;OAKG;IACI,GAAG,CAAC,OAAkC;QAC5C,MAAM,cAAc,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAElD,IAAI,cAAc,EAAE,CAAC;YACpB,IAAI,cAAc,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;gBACzC,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBAClC,OAAO,IAAI,CAAC;YACb,CAAC;YACD,OAAO,cAAc,CAAC;QACvB,CAAC;QAED,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;;;OAMG;IACI,GAAG,CACT,OAAkC,EAClC,QAAoC;QAEpC,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAElC,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAChE,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YACrD,OAAO,IAAI,CAAC;QACb,CAAC;QAED,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QAC9D,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;QAExE,IAAI,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC;QACb,CAAC;QAED,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,IAAI,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,EAAE,CAAC;YAC1D,MAAM,kBAAkB,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;YAChF,IAAI,kBAAkB,KAAK,GAAG,EAAE,CAAC;gBAChC,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtF,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;gBAChD,CAAC;YACF,CAAC;QACF,CAAC;QAED,MAAM,cAAc,GAA6B;YAChD,WAAW;YACX,YAAY;YACZ,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI;SAC7C,CAAC;QAEF,IAAI,KAAK,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,cAAc,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAC1E,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC;QAE5C,OAAO,cAAc,CAAC;IACvB,CAAC;IAED;;OAEG;IACI,KAAK;QACX,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;IACpB,CAAC;CACD"}

View File

@@ -0,0 +1,6 @@
declare enum CachedResponseStateEnum {
fresh = "fresh",
stale = "stale"
}
export default CachedResponseStateEnum;
//# sourceMappingURL=CachedResponseStateEnum.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CachedResponseStateEnum.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/response/CachedResponseStateEnum.ts"],"names":[],"mappings":"AAAA,aAAK,uBAAuB;IAC3B,KAAK,UAAU;IACf,KAAK,UAAU;CACf;AACD,eAAe,uBAAuB,CAAC"}

View File

@@ -0,0 +1,7 @@
var CachedResponseStateEnum;
(function (CachedResponseStateEnum) {
CachedResponseStateEnum["fresh"] = "fresh";
CachedResponseStateEnum["stale"] = "stale";
})(CachedResponseStateEnum || (CachedResponseStateEnum = {}));
export default CachedResponseStateEnum;
//# sourceMappingURL=CachedResponseStateEnum.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CachedResponseStateEnum.js","sourceRoot":"","sources":["../../../../src/fetch/cache/response/CachedResponseStateEnum.ts"],"names":[],"mappings":"AAAA,IAAK,uBAGJ;AAHD,WAAK,uBAAuB;IAC3B,0CAAe,CAAA;IACf,0CAAe,CAAA;AAChB,CAAC,EAHI,uBAAuB,KAAvB,uBAAuB,QAG3B;AACD,eAAe,uBAAuB,CAAC"}

View File

@@ -0,0 +1,7 @@
import Headers from '../../Headers.js';
export default interface ICachableRequest {
url: string;
method: string;
headers: Headers;
}
//# sourceMappingURL=ICachableRequest.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachableRequest.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/response/ICachableRequest.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,kBAAkB,CAAC;AAEvC,MAAM,CAAC,OAAO,WAAW,gBAAgB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CACjB"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ICachableRequest.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachableRequest.js","sourceRoot":"","sources":["../../../../src/fetch/cache/response/ICachableRequest.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,10 @@
import Headers from '../../Headers.js';
export default interface ICachableResponse {
status: number;
statusText: string;
url: string;
headers: Headers;
body: Buffer | null;
waitingForBody: boolean;
}
//# sourceMappingURL=ICachableResponse.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachableResponse.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/response/ICachableResponse.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,kBAAkB,CAAC;AAEvC,MAAM,CAAC,OAAO,WAAW,iBAAiB;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;CACxB"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ICachableResponse.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachableResponse.js","sourceRoot":"","sources":["../../../../src/fetch/cache/response/ICachableResponse.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,37 @@
import CachedResponseStateEnum from './CachedResponseStateEnum.js';
import Headers from '../../Headers.js';
export default interface ICachedResponse {
/** Response. */
response: {
status: number;
statusText: string;
url: string;
headers: Headers;
waitingForBody: boolean;
body: Buffer | null;
};
/** Request. */
request: {
headers: Headers;
method: string;
};
/** Cache update time in milliseconds. */
cacheUpdateTime: number;
/** Last modified time in milliseconds. */
lastModified: number | null;
/** Vary headers. */
vary: {
[header: string]: string;
};
/** Expire time in milliseconds. */
expires: number | null;
/** ETag */
etag: string | null;
/** Must revalidate using "If-Modified-Since" request when expired. Not supported yet. */
mustRevalidate: boolean;
/** Stale while revalidate using "If-Modified-Since" request when expired */
staleWhileRevalidate: boolean;
/** Used when "mustRevalidate" or "staleWhileRevalidate" is true. */
state: CachedResponseStateEnum;
}
//# sourceMappingURL=ICachedResponse.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachedResponse.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/response/ICachedResponse.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,MAAM,8BAA8B,CAAC;AACnE,OAAO,OAAO,MAAM,kBAAkB,CAAC;AAEvC,MAAM,CAAC,OAAO,WAAW,eAAe;IACvC,gBAAgB;IAChB,QAAQ,EAAE;QACT,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,OAAO,CAAC;QAEjB,cAAc,EAAE,OAAO,CAAC;QACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACpB,CAAC;IACF,eAAe;IACf,OAAO,EAAE;QACR,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KACf,CAAC;IACF,yCAAyC;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,oBAAoB;IACpB,IAAI,EAAE;QAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACnC,mCAAmC;IACnC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,yFAAyF;IACzF,cAAc,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,oBAAoB,EAAE,OAAO,CAAC;IAC9B,oEAAoE;IACpE,KAAK,EAAE,uBAAuB,CAAC;CAC/B"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ICachedResponse.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ICachedResponse.js","sourceRoot":"","sources":["../../../../src/fetch/cache/response/ICachedResponse.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,35 @@
import ICachedResponse from './ICachedResponse.js';
import ICachableRequest from './ICachableRequest.js';
import ICachableResponse from './ICachableResponse.js';
/**
* Fetch response cache.
*/
export default interface IResponseCache {
/**
* Returns cached response.
*
* @param request Request.
* @returns Cached response.
*/
get(request: ICachableRequest): ICachedResponse | null;
/**
* Adds a cached response.
*
* @param request Request.
* @param response Response.
* @returns Cached response.
*/
add(request: ICachableRequest, response: ICachableResponse): ICachedResponse | null;
/**
* Clears the cache.
*
* @param [options] Options.
* @param [options.url] URL.
* @param [options.toTime] Time in MS.
*/
clear(options?: {
url?: string;
toTime?: number;
}): void;
}
//# sourceMappingURL=IResponseCache.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"IResponseCache.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/response/IResponseCache.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,sBAAsB,CAAC;AACnD,OAAO,gBAAgB,MAAM,uBAAuB,CAAC;AACrD,OAAO,iBAAiB,MAAM,wBAAwB,CAAC;AAEvD;;GAEG;AACH,MAAM,CAAC,OAAO,WAAW,cAAc;IACtC;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,GAAG,IAAI,CAAC;IAEvD;;;;;;OAMG;IACH,GAAG,CAAC,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,GAAG,eAAe,GAAG,IAAI,CAAC;IAEpF;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACzD"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=IResponseCache.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"IResponseCache.js","sourceRoot":"","sources":["../../../../src/fetch/cache/response/IResponseCache.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,40 @@
import IResponseCache from './IResponseCache.js';
import ICachedResponse from './ICachedResponse.js';
import ICachableRequest from './ICachableRequest.js';
import ICachableResponse from './ICachableResponse.js';
/**
* 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 implements IResponseCache {
#private;
/**
* Returns cached response.
*
* @param request Request.
* @returns Cached response.
*/
get(request: ICachableRequest): ICachedResponse | null;
/**
* Adds a cache entity.
*
* @param request Request.
* @param response Response.
* @returns Cached response.
*/
add(request: ICachableRequest, response: ICachableResponse): ICachedResponse;
/**
* 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?: {
url?: string;
toTime?: number;
}): void;
}
//# sourceMappingURL=ResponseCache.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ResponseCache.d.ts","sourceRoot":"","sources":["../../../../src/fetch/cache/response/ResponseCache.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,qBAAqB,CAAC;AACjD,OAAO,eAAe,MAAM,sBAAsB,CAAC;AAEnD,OAAO,gBAAgB,MAAM,uBAAuB,CAAC;AACrD,OAAO,iBAAiB,MAAM,wBAAwB,CAAC;AAKvD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,OAAO,aAAc,YAAW,cAAc;;IAG3D;;;;;OAKG;IACI,GAAG,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,GAAG,IAAI;IAoC7D;;;;;;OAMG;IACI,GAAG,CAAC,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,GAAG,eAAe;IA6HnF;;;;;;OAMG;IACI,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAqB/D"}

View File

@@ -0,0 +1,199 @@
import CachedResponseStateEnum from './CachedResponseStateEnum.js';
import Headers from '../../Headers.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 {
#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;
if (this.#entries[url]) {
for (let i = 0, max = this.#entries[url].length; i < max; i++) {
const entry = this.#entries[url][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) {
this.#entries[url].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 index = this.#entries[url].indexOf(cachedResponse);
if (index !== -1) {
this.#entries[url].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
};
this.#entries[url] = this.#entries[url] || [];
this.#entries[url].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 index = this.#entries[url].indexOf(cachedResponse);
if (index !== -1) {
this.#entries[url].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 index = this.#entries[url].indexOf(cachedResponse);
if (index !== -1) {
this.#entries[url].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] : Object.keys(this.#entries)) {
if (this.#entries[key]) {
for (let i = 0, max = this.#entries[key].length; i < max; i++) {
if (this.#entries[key][i].cacheUpdateTime < options.toTime) {
this.#entries[key].splice(i, 1);
i--;
max--;
}
}
}
}
}
else if (options.url) {
delete this.#entries[options.url];
}
}
else {
this.#entries = {};
}
}
}
//# sourceMappingURL=ResponseCache.js.map

File diff suppressed because one or more lines are too long