Skip to content

Commit

Permalink
feat: hash in key builder, extra tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Dunkelheit committed Oct 22, 2020
1 parent 5f922bd commit 659ace6
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 14 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2020-10-22
### Changed
- Used a hash of the function arguments instead of just concatenating everything into a string.

### Added
- Extra tests for scenarios in which promises get rejected.

## [0.1.3] - 2020-10-20
### Changed
- Drop jsdoc documentation.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const recyclableFunc = recycle(func, {});
### Identifying recyclable promises

The internal registry where recyclable promises are stored needs to identify them somehow, by default functions will
be uniquely identified by their function name and their arguments, but it is **strongly recommended to use a custom
be uniquely identified by their function name and hashed arguments, but it is **strongly recommended to use a custom
key builder** to make sure your recycling needs are met. This can be done as follows:

```javascript
Expand Down
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use strict';

const crypto = require('crypto');

const registry = new Map();

function defaultKeyBuilder(func, ...args) {
return `${func.name || 'anonymous'}-${args.join('-')}`;
return `${func.name || 'anonymous'}-${crypto.createHash('sha256').update(JSON.stringify(args)).digest('hex')}`;
}

const defaultOptions = {
Expand Down
83 changes: 72 additions & 11 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,18 @@ describe('pending-promise-recycler', () => {

describe('Basic usage', () => {

it('Executes an asynchronous function once', async () => {
it('Executes a promise function once', async () => {
function beforeResolving() {
const registry = getRegistry();
expect(registry).to.have.keys('a-lorem-ipsum-dolor sit amet');
const p = registry.get('a-lorem-ipsum-dolor sit amet');
expect(registry).to.have.keys('a-91f967512ad54d194006a3cacf3a94d7f9c4ded44bb194c1e9e0fb1c21cb9a37');
const p = registry.get('a-91f967512ad54d194006a3cacf3a94d7f9c4ded44bb194c1e9e0fb1c21cb9a37');
expect(p).to.be.a('promise');
expect(util.format('%s', p)).to.be.equal('Promise { <pending> }');
}
function afterResolving() {
const registry = getRegistry();
expect(registry).to.have.keys('a-lorem-ipsum-dolor sit amet');
const p = registry.get('a-lorem-ipsum-dolor sit amet');
expect(registry).to.have.keys('a-91f967512ad54d194006a3cacf3a94d7f9c4ded44bb194c1e9e0fb1c21cb9a37');
const p = registry.get('a-91f967512ad54d194006a3cacf3a94d7f9c4ded44bb194c1e9e0fb1c21cb9a37');
expect(p).to.be.a('promise');
expect(util.format('%s', p)).to.be.equal('Promise { \'Why hello there\' }');
}
Expand All @@ -72,7 +72,7 @@ describe('pending-promise-recycler', () => {
expect(result).to.equal('Why hello there');
});

it('Executes an asynchronous function twice, relying on the registry run just one promise', async () => {
it('Executes a fulfilled promise function twice, recycling the promise itself', async () => {
const registry = getRegistry();
const registryGetSpy = sinon.spy(registry, 'get');
const func = testFunctionBuilder('a');
Expand All @@ -88,22 +88,41 @@ describe('pending-promise-recycler', () => {
expect(promiseA).to.equal('Why hello there');
expect(promiseB).to.equal('Why hello there');
});

it('Executes a rejected promise function twice, recycling the promise itself', async () => {
const registry = getRegistry();
const registryGetSpy = sinon.spy(registry, 'get');
const func = testFunctionBuilder('a', { isResolved: false });
const funcSpy = sinon.spy(func);
const cachedFunc = recycle(funcSpy);
const [promiseA, promiseB] = await Promise.allSettled([
cachedFunc('lorem', 'ipsum', 'dolor sit amet'),
cachedFunc('lorem', 'ipsum', 'dolor sit amet'),
]);
expect(registryGetSpy).to.be.calledOnce;
expect(funcSpy).to.be.calledOnce;
expect(registry).to.be.empty;
expect(promiseA).to.have.property('status', 'rejected');
expect(promiseA).to.have.property('reason', 'Why hello there');
expect(promiseB).to.have.property('status', 'rejected');
expect(promiseB).to.have.property('reason', 'Why hello there');
});
});

describe('Error handling', () => {

it('Handles rejected promises, making sure the registry stays clean', async () => {
function beforeResolving() {
const registry = getRegistry();
expect(registry).to.have.keys('a-lorem-ipsum-dolor sit amet');
const p = registry.get('a-lorem-ipsum-dolor sit amet');
expect(registry).to.have.keys('a-91f967512ad54d194006a3cacf3a94d7f9c4ded44bb194c1e9e0fb1c21cb9a37');
const p = registry.get('a-91f967512ad54d194006a3cacf3a94d7f9c4ded44bb194c1e9e0fb1c21cb9a37');
expect(p).to.be.a('promise');
expect(util.format('%s', p)).to.be.equal('Promise { <pending> }');
}
function afterResolving() {
const registry = getRegistry();
expect(registry).to.have.keys('a-lorem-ipsum-dolor sit amet');
const p = registry.get('a-lorem-ipsum-dolor sit amet');
expect(registry).to.have.keys('a-91f967512ad54d194006a3cacf3a94d7f9c4ded44bb194c1e9e0fb1c21cb9a37');
const p = registry.get('a-91f967512ad54d194006a3cacf3a94d7f9c4ded44bb194c1e9e0fb1c21cb9a37');
expect(p).to.be.a('promise');
}
const spy = sinon.spy(testFunctionBuilder('a', {
Expand Down Expand Up @@ -155,7 +174,9 @@ describe('pending-promise-recycler', () => {
it('Works with anonymous functions', async () => {
function beforeResolving() {
const registry = getRegistry();
expect(registry).to.have.keys('anonymous-lorem');
expect(registry).to.have.keys(
'anonymous-0d0491105dd08721e0911939ca184e9e5a6f924b00dce27a4163ca333049bf20'
);
}
const spy = sinon.spy(testFunctionBuilder('', { beforeResolving }));
const cachedFunc = recycle(spy);
Expand All @@ -165,4 +186,44 @@ describe('pending-promise-recycler', () => {
expect(result).to.equal('Why hello there');
});
});

describe('Error handling', () => {

it('Handles promises that cause a TypeError', async () => {
const func = () => {
return new Promise(resolve => {
const hmmm = ''[0][0];
resolve(hmmm);
});
};
const spy = sinon.spy(func);
const cachedFunc = recycle(spy);
await expect(cachedFunc()).to.be.rejected;
expect(getRegistry()).to.be.empty;
});

it('Handles promises that throw an unhandled rejection error', async () => {
const err = new Error('Something went wrong!');
const func = () => {
return new Promise(() => {
throw err;
});
};
const spy = sinon.spy(func);
const cachedFunc = recycle(spy);
await expect(cachedFunc()).to.be.rejectedWith(err);
expect(getRegistry()).to.be.empty;
});

it('Handles async functions that throw an unhandled rejection error', async () => {
const err = new Error('Something went wrong!');
const func = async () => {
throw err;
};
const spy = sinon.spy(func);
const cachedFunc = recycle(spy);
await expect(cachedFunc()).to.be.rejectedWith(err);
expect(getRegistry()).to.be.empty;
});
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pending-promise-recycler",
"version": "0.1.3",
"version": "0.2.0",
"description": "Save precious resources and avoid performing the same operation twice by recycling pending promises",
"main": "index.js",
"engines": {
Expand Down

0 comments on commit 659ace6

Please sign in to comment.