happy-dom
Version:
Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.
176 lines (152 loc) • 4.64 kB
text/typescript
import type ICachedResponse from './ICachedResponse.js';
import Headers from '../../Headers.js';
import FS from 'fs';
import Path from 'path';
import Crypto from 'crypto';
import type IResponseCacheFileSystem from './IResponseCacheFileSystem.js';
/**
* Fetch response cache file system implementation.
*/
export default class ResponseCacheFileSystem implements IResponseCacheFileSystem {
#createdDirectories: Set<string> = new Set();
#hashes: Set<string> = new Set();
#entries: Map<string, ICachedResponse[]>;
/**
* Constructor.
*
* @param entries Map of URL to cached responses.
*/
constructor(entries: Map<string, ICachedResponse[]>) {
this.#entries = entries;
}
/**
* Loads cache from file system.
*
* @param directory Directory to load from.
*/
public async load(directory: string): Promise<void> {
if (!directory) {
throw new Error(
"Failed to execute 'load' on 'ResponseCacheFileSystem': Directory is not specified."
);
}
const absoluteDirectory = Path.resolve(directory);
let files: string[] = [];
try {
files = await FS.promises.readdir(absoluteDirectory);
} catch (error) {
// Ignore if the directory does not exist
return;
}
const promises = [];
const entries = this.#entries;
for (const file of files) {
if (file.endsWith('.json')) {
promises.push(
FS.promises
.readFile(Path.join(absoluteDirectory, file), 'utf8')
.then((content) => {
const cachedResponse = <ICachedResponse>JSON.parse(content);
if (cachedResponse.response && cachedResponse.request && !cachedResponse.virtual) {
cachedResponse.response.headers = new Headers(cachedResponse.response.headers);
cachedResponse.request.headers = new Headers(cachedResponse.request.headers);
return FS.promises
.readFile(Path.join(absoluteDirectory, file.split('.')[0] + '.data'))
.then((body) => {
cachedResponse.response.body = body;
let entry = entries.get(cachedResponse.response.url);
if (!entry) {
entry = [];
entries.set(cachedResponse.response.url, entry);
}
entry.push(cachedResponse);
})
.catch(() => {
let entry = entries.get(cachedResponse.response.url);
if (!entry) {
entry = [];
entries.set(cachedResponse.response.url, entry);
}
entry.push(cachedResponse);
});
}
})
.catch(() => {})
);
}
}
await Promise.all(promises);
}
/**
* Saves the cache to file system.
*
* @param directory Directory to store to.
*/
public async save(directory: string): Promise<void> {
if (!directory) {
throw new Error(
"Failed to execute 'store' on 'ResponseCacheFileSystem': Directory is not specified."
);
}
if (!this.#entries.size) {
return;
}
const absoluteDirectory = Path.resolve(directory);
const isDirectoryCreated = this.#createdDirectories.has(absoluteDirectory);
if (!isDirectoryCreated) {
this.#createdDirectories.add(absoluteDirectory);
try {
await FS.promises.mkdir(absoluteDirectory, {
recursive: true
});
} catch (error) {
// Ignore if the directory already exists
}
}
const entries = this.#entries;
const promises = [];
for (const entry of entries.values()) {
for (const cachedResponse of entry) {
if (!cachedResponse.virtual) {
const responseHeaders: { [k: string]: string } = {};
const requestHeaders: { [k: string]: string } = {};
for (const [key, value] of cachedResponse.response.headers.entries()) {
responseHeaders[key] = value;
}
for (const [key, value] of cachedResponse.request.headers.entries()) {
requestHeaders[key] = value;
}
const cacheableEntry = {
...cachedResponse,
response: {
...cachedResponse.response,
headers: responseHeaders,
body: null
},
request: {
...cachedResponse.request,
headers: requestHeaders
}
};
const json = JSON.stringify(cacheableEntry, null, 3);
const hash = Crypto.createHash('md5').update(json).digest('hex');
if (!this.#hashes.has(hash)) {
this.#hashes.add(hash);
promises.push(
FS.promises.writeFile(Path.join(absoluteDirectory, `${hash}.json`), json)
);
if (cachedResponse.response.body) {
promises.push(
FS.promises.writeFile(
Path.join(absoluteDirectory, `${hash}.data`),
cachedResponse.response.body
)
);
}
}
}
}
}
await Promise.all(promises);
}
}