345 lines
11 KiB
JavaScript
345 lines
11 KiB
JavaScript
import { expect } from 'chai';
|
|
import { it } from 'mocha';
|
|
import { memoize } from '../../../src/program/utils/memoize.js';
|
|
|
|
describe('Memoize', () => {
|
|
// This is important, since memoization only works when cache is disabled.
|
|
// We want to prevent that another test unintentionally disabled caching.
|
|
memoize.restoreCaching();
|
|
|
|
describe('With primitives', () => {
|
|
describe('Numbers', () => {
|
|
it(`returns cached result when called with same parameters`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {number} a */ a, /** @type {number} a */ b) {
|
|
sumCalled += 1;
|
|
return a + b;
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
|
|
// Put in cache for args combination
|
|
expect(sumMemoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Return from cache
|
|
expect(sumMemoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Put in cache for args combination
|
|
expect(sumMemoized(1, 3)).to.equal(4);
|
|
expect(sumCalled).to.equal(2);
|
|
|
|
// Return from cache
|
|
expect(sumMemoized(1, 3)).to.equal(4);
|
|
expect(sumCalled).to.equal(2);
|
|
});
|
|
|
|
it(`returns cached result per function for same args`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {number} a */ a, /** @type {number} a */ b) {
|
|
sumCalled += 1;
|
|
return a + b;
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
let sum2Called = 0;
|
|
function sum2(/** @type {number} a */ a, /** @type {number} a */ b) {
|
|
sum2Called += 1;
|
|
return a + b;
|
|
}
|
|
const sum2Memoized = memoize(sum2);
|
|
|
|
expect(sumMemoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(0);
|
|
|
|
expect(sum2Memoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
|
|
// Both cached
|
|
expect(sumMemoized(1, 2)).to.equal(3);
|
|
expect(sum2Memoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
});
|
|
});
|
|
describe('Strings', () => {
|
|
it(`returns cached result when called with same parameters`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {string} a */ a, /** @type {string} a */ b) {
|
|
sumCalled += 1;
|
|
return a + b;
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
|
|
// Put in cache for args combination
|
|
expect(sumMemoized('1', '2')).to.equal('12');
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Return from cache
|
|
expect(sumMemoized('1', '2')).to.equal('12');
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Put in cache for args combination
|
|
expect(sumMemoized('1', '3')).to.equal('13');
|
|
expect(sumCalled).to.equal(2);
|
|
|
|
// Return from cache
|
|
expect(sumMemoized('1', '3')).to.equal('13');
|
|
expect(sumCalled).to.equal(2);
|
|
});
|
|
|
|
it(`returns cached result per function for same args`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {string} a */ a, /** @type {string} a */ b) {
|
|
sumCalled += 1;
|
|
return a + b;
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
let sum2Called = 0;
|
|
function sum2(/** @type {string} a */ a, /** @type {string} a */ b) {
|
|
sum2Called += 1;
|
|
return a + b;
|
|
}
|
|
const sum2Memoized = memoize(sum2);
|
|
|
|
expect(sumMemoized('1', '2')).to.equal('12');
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(0);
|
|
|
|
expect(sum2Memoized('1', '2')).to.equal('12');
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
|
|
// Both cached
|
|
expect(sumMemoized('1', '2')).to.equal('12');
|
|
expect(sum2Memoized('1', '2')).to.equal('12');
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('With non primitives', () => {
|
|
describe('Arrays', () => {
|
|
it(`returns cached result when called with same parameters`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {number[]} a */ a, /** @type {number[]} a */ b) {
|
|
sumCalled += 1;
|
|
return [...a, ...b];
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
|
|
// Put in cache for args combination
|
|
expect(sumMemoized([1], [2])).to.deep.equal([1, 2]);
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Return from cache
|
|
expect(sumMemoized([1], [2])).to.deep.equal([1, 2]);
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Put in cache for args combination
|
|
expect(sumMemoized([1], [3])).to.deep.equal([1, 3]);
|
|
expect(sumCalled).to.equal(2);
|
|
});
|
|
|
|
it(`returns cached result per function for same args`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {number[]} a */ a, /** @type {number[]} a */ b) {
|
|
sumCalled += 1;
|
|
return [...a, ...b];
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
let sum2Called = 0;
|
|
function sum2(/** @type {number[]} a */ a, /** @type {number[]} a */ b) {
|
|
sum2Called += 1;
|
|
return [...a, ...b];
|
|
}
|
|
const sum2Memoized = memoize(sum2);
|
|
|
|
expect(sumMemoized([1], [2])).to.deep.equal([1, 2]);
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(0);
|
|
|
|
expect(sum2Memoized([1], [2])).to.deep.equal([1, 2]);
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
|
|
// Both cached
|
|
expect(sumMemoized([1], [2])).to.deep.equal([1, 2]);
|
|
expect(sum2Memoized([1], [2])).to.deep.equal([1, 2]);
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
});
|
|
});
|
|
|
|
describe('Objects', () => {
|
|
it(`returns cached result when called with same parameters`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {object} a */ a, /** @type {object} a */ b) {
|
|
sumCalled += 1;
|
|
return { ...a, ...b };
|
|
}
|
|
const sumMemoized = memoize(sum, { serializeObjects: true });
|
|
|
|
// Put in cache for args combination
|
|
expect(sumMemoized({ x: 1 }, { y: 2 })).to.deep.equal({ x: 1, y: 2 });
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Return from cache
|
|
expect(sumMemoized({ x: 1 }, { y: 2 })).to.deep.equal({ x: 1, y: 2 });
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Put in cache for args combination
|
|
expect(sumMemoized({ x: 1 }, { y: 3 })).to.deep.equal({ x: 1, y: 3 });
|
|
expect(sumCalled).to.equal(2);
|
|
});
|
|
|
|
it(`returns cached result per function for same args`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {object} a */ a, /** @type {object} a */ b) {
|
|
sumCalled += 1;
|
|
return { ...a, ...b };
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
let sum2Called = 0;
|
|
function sum2(/** @type {object} a */ a, /** @type {object} a */ b) {
|
|
sum2Called += 1;
|
|
return { ...a, ...b };
|
|
}
|
|
const sum2Memoized = memoize(sum2);
|
|
|
|
expect(sumMemoized({ x: 1 }, { y: 2 })).to.deep.equal({ x: 1, y: 2 });
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(0);
|
|
|
|
expect(sum2Memoized({ x: 1 }, { y: 2 })).to.deep.equal({ x: 1, y: 2 });
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
|
|
// Both cached
|
|
expect(sumMemoized({ x: 1 }, { y: 2 })).to.deep.equal({ x: 1, y: 2 });
|
|
expect(sum2Memoized({ x: 1 }, { y: 2 })).to.deep.equal({ x: 1, y: 2 });
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
});
|
|
});
|
|
|
|
describe('When non primitives (references) are returned', () => {
|
|
// Solve this by making sure your memoized function uses Object.freeze
|
|
it(`will be affected by edited non primitive returns`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {object} a */ a, /** @type {object} a */ b) {
|
|
sumCalled += 1;
|
|
return { ...a, ...b };
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
|
|
// Put in cache for args combination
|
|
const result = sumMemoized({ x: 1 }, { y: 2 });
|
|
expect(result).to.deep.equal({ x: 1, y: 2 });
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Return from cache
|
|
const resultCached = sumMemoized({ x: 1 }, { y: 2 });
|
|
expect(resultCached).to.equal(result);
|
|
expect(resultCached).to.deep.equal({ x: 1, y: 2 });
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Outside world can edit returned reference
|
|
// @ts-expect-error
|
|
resultCached.x = 3;
|
|
// Return from cache
|
|
const lastResult = sumMemoized({ x: 1 }, { y: 2 });
|
|
expect(lastResult).to.equal(result);
|
|
expect(lastResult).to.not.eql({ x: 1, y: 2 });
|
|
expect(sumCalled).to.equal(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Asynchronous', () => {
|
|
it(`returns cached result when called with same parameters`, async () => {
|
|
let sumCalled = 0;
|
|
async function sum(/** @type {number} a */ a, /** @type {number} a */ b) {
|
|
sumCalled += 1;
|
|
return a + b;
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
|
|
// Put in cache for args combination
|
|
expect(await sumMemoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Return from cache
|
|
expect(await sumMemoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Put in cache for args combination
|
|
expect(await sumMemoized(1, 3)).to.equal(4);
|
|
expect(sumCalled).to.equal(2);
|
|
|
|
// Return from cache
|
|
expect(await sumMemoized(1, 3)).to.equal(4);
|
|
expect(sumCalled).to.equal(2);
|
|
});
|
|
|
|
it(`returns cached result per function for same args`, async () => {
|
|
let sumCalled = 0;
|
|
async function sum(/** @type {number} a */ a, /** @type {number} a */ b) {
|
|
sumCalled += 1;
|
|
return a + b;
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
let sum2Called = 0;
|
|
async function sum2(/** @type {number} a */ a, /** @type {number} a */ b) {
|
|
sum2Called += 1;
|
|
return a + b;
|
|
}
|
|
const sum2Memoized = memoize(sum2);
|
|
|
|
expect(await sumMemoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(0);
|
|
|
|
expect(await sum2Memoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
|
|
// Both cached
|
|
expect(await sumMemoized(1, 2)).to.equal(3);
|
|
expect(await sum2Memoized(1, 2)).to.equal(3);
|
|
expect(sumCalled).to.equal(1);
|
|
expect(sum2Called).to.equal(1);
|
|
});
|
|
});
|
|
|
|
describe('Cache', () => {
|
|
it(`"memoizedFn.clearCache()" clears the cache for a memoized fn"`, async () => {
|
|
let sumCalled = 0;
|
|
function sum(/** @type {string} a */ a, /** @type {string} a */ b) {
|
|
sumCalled += 1;
|
|
return a + b;
|
|
}
|
|
const sumMemoized = memoize(sum);
|
|
|
|
// Put in cache for args combination
|
|
expect(sumMemoized('1', '2')).to.equal('12');
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
// Return from cache
|
|
expect(sumMemoized('1', '2')).to.equal('12');
|
|
expect(sumCalled).to.equal(1);
|
|
|
|
sumMemoized.clearCache();
|
|
|
|
// Now the original function is called again
|
|
expect(sumMemoized('1', '2')).to.equal('12');
|
|
expect(sumCalled).to.equal(3);
|
|
|
|
// Return from new cache again
|
|
expect(sumMemoized('1', '2')).to.equal('12');
|
|
expect(sumCalled).to.equal(3);
|
|
});
|
|
});
|
|
});
|