pollyjs-meticulous-persister
Version:
Extendable base persister class used by @pollyjs
226 lines (181 loc) • 5.89 kB
JavaScript
import stringify from 'fast-json-stable-stringify';
import { ACTIONS, assert } from '@pollyjs/utils';
import HAR from './har';
import Entry from './har/entry';
const CREATOR_NAME = 'Polly.JS';
export default class Persister {
constructor(polly) {
this.polly = polly;
this.pending = new Map();
this._cache = new Map();
}
static get type() {
return 'persister';
}
/* eslint-disable-next-line getter-return */
static get id() {
assert('Must override the static `id` getter.');
}
get defaultOptions() {
return {};
}
get options() {
return {
...(this.defaultOptions || {}),
...((this.polly.config.persisterOptions || {})[this.constructor.id] || {})
};
}
get hasPending() {
/*
Although the pending map is bucketed by recordingId, the bucket will always
be created with a single item in it so we can assume that if a bucket
exists, then it has items in it.
*/
return this.pending.size > 0;
}
async persist() {
if (!this.hasPending) {
return;
}
const promises = [];
const creator = {
name: CREATOR_NAME,
version: this.polly.constructor.VERSION,
comment: `${this.constructor.type}:${this.constructor.id}`
};
for (const [recordingId, { name, requests }] of this.pending) {
const entries = [];
const recording = await this.find(recordingId);
let har;
if (!recording) {
har = new HAR({ log: { creator, _recordingName: name } });
} else {
har = new HAR(recording);
}
for (const request of requests) {
const entry = new Entry(request);
this.assert(
`Cannot persist response for [${entry.request.method}] ${entry.request.url} because the status code was ${entry.response.status} and \`recordFailedRequests\` is \`false\``,
request.response.ok || request.config.recordFailedRequests
);
/*
Trigger the `beforePersist` event on each new recorded entry.
NOTE: This must be triggered last as this entry can be used to
modify the payload (i.e. encrypting the request & response).
*/
await request._emit('beforePersist', entry);
entries.push(entry);
}
har.log.addEntries(entries);
if (!this.polly.config.persisterOptions.disableSortingHarEntries) {
har.log.sortEntries();
}
if (!this.polly.config.persisterOptions.keepUnusedRequests) {
this._removeUnusedEntries(recordingId, har);
}
promises.push(this.save(recordingId, har));
}
await Promise.all(promises);
this.pending.clear();
}
recordRequest(pollyRequest) {
this.assert(
`You must pass a PollyRequest to 'recordRequest'.`,
pollyRequest
);
this.assert(
`Cannot save a request with no response.`,
pollyRequest.didRespond
);
const { recordingId, recordingName } = pollyRequest;
if (!this.pending.has(recordingId)) {
this.pending.set(recordingId, { name: recordingName, requests: [] });
}
try {
const url =
pollyRequest &&
pollyRequest.identifiers &&
pollyRequest.identifiers.url;
if (
url.includes('user-events-v3.s3-accelerate.amazonaws.com') ||
url.includes('user-events-v3.s3.us-west-2.amazonaws.com')
) {
// Do not record the requests which we generate by uploading to S3.
// TODO: Change these includes calls to check for a magic query param (once we can do this)
return;
}
} catch (e) {
// Defensive catch here, in case pollyRequest doesn't contain identifiers or url.
return;
}
this.pending.get(recordingId).requests.push(pollyRequest);
}
async find(recordingId) {
const { _cache: cache } = this;
if (!cache.has(recordingId)) {
const findRecording = async () => {
const recording = await this.findRecording(recordingId);
if (recording) {
this.assert(
`Recording with id '${recordingId}' is invalid. Please delete the recording so a new one can be created.`,
recording.log && recording.log.creator.name === CREATOR_NAME
);
return recording;
} else {
cache.delete(recordingId);
return null;
}
};
cache.set(recordingId, findRecording());
}
return cache.get(recordingId);
}
async save(recordingId) {
await this.saveRecording(...arguments);
this._cache.delete(recordingId);
}
async delete(recordingId) {
await this.deleteRecording(...arguments);
this._cache.delete(recordingId);
}
async findEntry(pollyRequest) {
const { recordingId } = pollyRequest;
const recording = await this.find(recordingId);
return this.polly.config.findEntryFn(pollyRequest, recording);
}
stringify() {
return stringify(...arguments);
}
assert(message, ...args) {
assert(
`[${this.constructor.type}:${this.constructor.id}] ${message}`,
...args
);
}
/**
* Remove all entries from the given HAR that do not match any requests in
* the current Polly instance.
*
* @param {String} recordingId
* @param {HAR} har
*/
_removeUnusedEntries(recordingId, har) {
const requests = this.polly._requests.filter(
r =>
r.recordingId === recordingId &&
(r.action === ACTIONS.RECORD || r.action === ACTIONS.REPLAY)
);
har.log.entries = har.log.entries.filter(entry =>
requests.find(r => entry._id === r.id && entry._order === r.order)
);
}
findRecording() {
this.assert('Must implement the `findRecording` hook.');
}
saveRecording() {
this.assert('Must implement the `saveRecording` hook.');
}
deleteRecording() {
this.assert('Must implement the `deleteRecording` hook.');
}
}