Skip to content

Commit

Permalink
feat(persister): Cache recordings (#31)
Browse files Browse the repository at this point in the history
Cache recordings and bring `findRecordingEntry` into the base persister.

* feat: Cleanup public facing persister API + bust cache

* test: Add test cases
  • Loading branch information
offirgolan authored Jun 21, 2018
1 parent 43db361 commit a04d7a7
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 78 deletions.
2 changes: 0 additions & 2 deletions docs/persisters/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ yarn add @pollyjs/persister -D
import Persister from '@pollyjs/persister';

class CustomPersister extends Persister {
findRecordingEntry() {}

findRecording() {}

saveRecording() {}
Expand Down
4 changes: 1 addition & 3 deletions packages/@pollyjs/adapter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,7 @@ export default class Adapter {

async replay(pollyRequest) {
const { config } = this.polly;
const recordingEntry = await this.persister.findRecordingEntry(
pollyRequest
);
const recordingEntry = await this.persister.findEntry(pollyRequest);

if (recordingEntry) {
await pollyRequest._trigger('beforeReplay', recordingEntry);
Expand Down
8 changes: 0 additions & 8 deletions packages/@pollyjs/core/src/persisters/local-storage/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Persister from '@pollyjs/persister';
import get from 'lodash-es/get';
import stringify from 'json-stable-stringify';

export default class LocalStoragePersister extends Persister {
Expand All @@ -19,13 +18,6 @@ export default class LocalStoragePersister extends Persister {
this._store.setItem(this._namespace, stringify(db));
}

findRecordingEntry(pollyRequest) {
const { id, order, recordingId } = pollyRequest;
const entries = get(this.db, `${recordingId}.entries.${id}`) || [];

return entries[order] || null;
}

findRecording(recordingId) {
return this.db[recordingId] || null;
}
Expand Down
13 changes: 0 additions & 13 deletions packages/@pollyjs/core/src/persisters/rest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,6 @@ export default class RestPersister extends Persister {
return ajax(buildUrl(host, apiNamespace, url), ...args);
}

async findRecordingEntry(pollyRequest) {
const { id, order, recordingId } = pollyRequest;

const response = await this.ajax(
`/${encodeURIComponent(recordingId)}/${id}?order=${order}`,
{
Accept: 'application/json; charset=utf-8'
}
);

return this._normalize(response);
}

async findRecording(recordingId) {
const response = await this.ajax(`/${encodeURIComponent(recordingId)}`, {
Accept: 'application/json; charset=utf-8'
Expand Down
4 changes: 2 additions & 2 deletions packages/@pollyjs/core/tests/integration/adapters-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ describe('Integration | Adapters', function() {

server.get(this.recordUrl()).passthrough();

expect(await persister.findRecording(recordingId)).to.be.null;
expect(await persister.find(recordingId)).to.be.null;
expect((await this.fetchRecord()).status).to.equal(404);
expect(await persister.findRecording(recordingId)).to.be.null;
expect(await persister.find(recordingId)).to.be.null;
});

it('should handle recording requests posting FormData + Blob/File', async function() {
Expand Down
16 changes: 8 additions & 8 deletions packages/@pollyjs/core/tests/integration/persisters-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('Integration | Persisters', function() {
setupFetch.beforeEach(defaults.adapters[0]);

afterEach(async function() {
await this.polly.persister.deleteRecording(this.polly.recordingId);
await this.polly.persister.delete(this.polly.recordingId);
});

setupFetch.afterEach();
Expand All @@ -26,7 +26,7 @@ describe('Integration | Persisters', function() {
await this.fetch('/api/db/foo?order=1');
await persister.persist();

const recording = await persister.findRecording(recordingId);
const recording = await persister.find(recordingId);
const entriesKeys = keys(recording.entries);

// Top Level Schema
Expand Down Expand Up @@ -81,7 +81,7 @@ describe('Integration | Persisters', function() {
await this.fetch('/api/db/foo?order=1');
await persister.persist();

let savedRecording = await persister.findRecording(recordingId);
let savedRecording = await persister.find(recordingId);
let entryKeys = keys(savedRecording.entries);

expect(entryKeys.length).to.equal(1);
Expand All @@ -98,7 +98,7 @@ describe('Integration | Persisters', function() {
await this.fetch('/api/db/foo?order=2');
await persister.persist();

savedRecording = await persister.findRecording(recordingId);
savedRecording = await persister.find(recordingId);
entryKeys = keys(savedRecording.entries).sort();

expect(entryKeys.length).to.equal(2);
Expand Down Expand Up @@ -138,7 +138,7 @@ describe('Integration | Persisters', function() {
setupFetch.beforeEach(defaults.adapters[0]);

afterEach(function() {
return this.polly.persister.deleteRecording(this.polly.recordingId);
return this.polly.persister.delete(this.polly.recordingId);
});

setupFetch.afterEach();
Expand All @@ -152,7 +152,7 @@ describe('Integration | Persisters', function() {
} catch (e) {
error = e;
} finally {
const savedRecording = await this.polly.persister.findRecording(
const savedRecording = await this.polly.persister.find(
this.polly.recordingId
);

Expand All @@ -170,7 +170,7 @@ describe('Integration | Persisters', function() {
setupFetch.beforeEach(defaults.adapters[0]);

afterEach(function() {
return this.polly.persister.deleteRecording(this.polly.recordingId);
return this.polly.persister.delete(this.polly.recordingId);
});

setupFetch.afterEach();
Expand All @@ -180,7 +180,7 @@ describe('Integration | Persisters', function() {
await this.fetch('/should-not-exist-also');
await this.polly.stop();

const savedRecording = await this.polly.persister.findRecording(
const savedRecording = await this.polly.persister.find(
this.polly.recordingId
);

Expand Down
15 changes: 0 additions & 15 deletions packages/@pollyjs/node-server/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,6 @@ export default class API {
this.recordingsDir = recordingsDir;
}

getRecordingEntry(recording, entryId, order) {
const recordingFilename = this.filenameFor(recording);

if (fs.existsSync(recordingFilename)) {
const data = fs.readJsonSync(recordingFilename);
const entries = data.entries[entryId];

if (entries && entries[order]) {
return this.respond(200, entries[order]);
}
}

return this.respond(204);
}

getRecording(recording) {
const recordingFilename = this.filenameFor(recording);

Expand Down
19 changes: 1 addition & 18 deletions packages/@pollyjs/node-server/src/express/register-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,6 @@ export default function registerAPI(app, config) {

router.use(nocache());

router.get('/:recording/:entryId', function(req, res) {
const { recording, entryId } = req.params;
const { order } = req.query;
const { status, body } = api.getRecordingEntry(recording, entryId, order);

res.status(status);

if (status === 200) {
res.json(body);
} else {
res.end();
}
});

router.get('/:recording', function(req, res) {
const { recording } = req.params;
const { status, body } = api.getRecording(recording);
Expand All @@ -48,10 +34,7 @@ export default function registerAPI(app, config) {
}
});

router.post('/:recording', bodyParser.json({ limit: '50mb' }), function(
req,
res
) {
router.post('/:recording', bodyParser.json(), function(req, res) {
const { recording } = req.params;
const { status, body } = api.saveRecording(recording, req.body);

Expand Down
2 changes: 0 additions & 2 deletions packages/@pollyjs/persister/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ documentation for more details.
import Persister from '@pollyjs/persister';

class CustomPersister extends Persister {
findRecordingEntry() {}

findRecording() {}

saveRecording() {}
Expand Down
40 changes: 36 additions & 4 deletions packages/@pollyjs/persister/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const SCHEMA_VERSION = 0.1;
export default class Persister {
constructor(polly) {
this.polly = polly;
this.cache = new Map();
this.pending = new Map();
}

Expand All @@ -30,7 +31,7 @@ export default class Persister {
const promises = [];

for (const [recordingId, { name, entries }] of this.pending) {
let recording = await this.findRecording(recordingId);
let recording = await this.find(recordingId);

for (const { request, entry } of entries) {
assert(
Expand Down Expand Up @@ -65,7 +66,7 @@ export default class Persister {
recording.created_at = createdAt;
}

promises.push(this.saveRecording(recordingId, recording));
promises.push(this.save(recordingId, recording));
}

await Promise.all(promises);
Expand Down Expand Up @@ -130,8 +131,39 @@ export default class Persister {
});
}

findRecordingEntry() {
assert('[Persister] Must implement the `findRecordingEntry` hook.', false);
async find(recordingId) {
if (this.cache.has(recordingId)) {
return this.cache.get(recordingId);
}

const recording = await this.findRecording(recordingId);

if (recording) {
this.cache.set(recordingId, recording);
}

return recording;
}

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 { id, order, recordingId } = pollyRequest;
const recording = await this.find(recordingId);

if (!recording) {
return null;
}

return (recording.entries[id] || [])[order] || null;
}

findRecording() {
Expand Down
71 changes: 68 additions & 3 deletions packages/@pollyjs/persister/tests/unit/persister-test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,72 @@
import Adapter from '../../src';
import Persister from '../../src';

describe('Unit | Adapter', function() {
describe('Unit | Persister', function() {
it('should exist', function() {
expect(Adapter).to.be.a('function');
expect(Persister).to.be.a('function');
});

describe('Caching', function() {
let callCounts, recording;

beforeEach(function() {
callCounts = { find: 0, save: 0, delete: 0 };
recording = {};

class CustomPersister extends Persister {
findRecording() {
callCounts.find++;

return recording;
}

saveRecording() {
callCounts.save++;
}

deleteRecording() {
callCounts.delete++;
}
}

this.persister = new CustomPersister({});
});

it('caches', async function() {
await this.persister.find('test');
await this.persister.find('test');
await this.persister.find('test');

expect(callCounts.find).to.equal(1);
});

it('does not cache falsy values', async function() {
recording = null;

await this.persister.find('test');
await this.persister.find('test');
await this.persister.find('test');

expect(callCounts.find).to.equal(3);
});

it('busts the cache after a save', async function() {
await this.persister.find('test');
await this.persister.save('test');
await this.persister.find('test');
await this.persister.find('test');

expect(callCounts.save).to.equal(1);
expect(callCounts.find).to.equal(2);
});

it('busts the cache after a delete', async function() {
await this.persister.find('test');
await this.persister.delete('test');
await this.persister.find('test');
await this.persister.find('test');

expect(callCounts.delete).to.equal(1);
expect(callCounts.find).to.equal(2);
});
});
});

0 comments on commit a04d7a7

Please sign in to comment.