Cache map improvements (#856)

* Update CacheMap API; get=>getOrCreate; add get; add set; add has

* Update tests

* Add more tests
This commit is contained in:
toasted-nutbread 2020-09-22 20:09:12 -04:00 committed by GitHub
parent d395a2a6bf
commit 7d78e8737f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 147 additions and 55 deletions

View File

@ -125,7 +125,7 @@ class JsonSchemaProxyHandler {
class JsonSchemaValidator { class JsonSchemaValidator {
constructor() { constructor() {
this._regexCache = new CacheMap(100, (pattern, flags) => new RegExp(pattern, flags)); this._regexCache = new CacheMap(100, ([pattern, flags]) => new RegExp(pattern, flags));
} }
createProxy(target, schema) { createProxy(target, schema) {
@ -676,7 +676,7 @@ class JsonSchemaValidator {
} }
_getRegex(pattern, flags) { _getRegex(pattern, flags) {
const regex = this._regexCache.get(pattern, flags); const regex = this._regexCache.getOrCreate([pattern, flags]);
regex.lastIndex = 0; regex.lastIndex = 0;
return regex; return regex;
} }

View File

@ -24,7 +24,7 @@ class CacheMap {
* Creates a new CacheMap. * Creates a new CacheMap.
* @param maxCount The maximum number of entries able to be stored in the cache. * @param maxCount The maximum number of entries able to be stored in the cache.
* @param create A function to create a value for the corresponding path. * @param create A function to create a value for the corresponding path.
* The signature is: create(...path) * The signature is: create(path)
*/ */
constructor(maxCount, create) { constructor(maxCount, create) {
if (!( if (!(
@ -59,12 +59,57 @@ class CacheMap {
return this._maxCount; return this._maxCount;
} }
/**
* Returns whether or not an item exists at the given path.
* @param path Array corresponding to the key of the cache.
* @returns A boolean indicating whether ot not the item exists.
*/
has(path) {
const node = this._accessNode(false, false, null, false, path);
return (node !== null);
}
/**
* Gets an item at the given path, if it exists. Otherwise, returns undefined.
* @param path Array corresponding to the key of the cache.
* @returns The existing value at the path, if any; otherwise, undefined.
*/
get(path) {
const node = this._accessNode(false, false, null, true, path);
return (node !== null ? node.value : void 0);
}
/** /**
* Gets an item at the given path, if it exists. Otherwise, creates a new item * Gets an item at the given path, if it exists. Otherwise, creates a new item
* and adds it to the cache. If the count exceeds the maximum count, items will be removed. * and adds it to the cache. If the count exceeds the maximum count, items will be removed.
* @param path Arguments corresponding to the key of the cache. * @param path Array corresponding to the key of the cache.
* @returns The existing value at the path, if any; otherwise, a newly created value.
*/ */
get(...path) { getOrCreate(path) {
return this._accessNode(true, false, null, true, path).value;
}
/**
* Sets a value at a given path.
* @param path Array corresponding to the key of the cache.
* @param value The value to store in the cache.
*/
set(path, value) {
this._accessNode(false, true, value, true, path);
}
/**
* Clears the cache.
*/
clear() {
this._map.clear();
this._count = 0;
this._resetEndNodes();
}
// Private
_accessNode(create, set, value, updateRecency, path) {
let ii = path.length; let ii = path.length;
if (ii === 0) { throw new Error('Invalid path'); } if (ii === 0) { throw new Error('Invalid path'); }
@ -77,13 +122,18 @@ class CacheMap {
} }
if (i === ii) { if (i === ii) {
// Found (map is now a node) // Found (map should now be a node)
this._updateRecency(map); if (updateRecency) { this._updateRecency(map); }
return map.value; if (set) { map.value = value; }
return map;
} }
// Create new // Create new
const value = this._create(...path); if (create) {
value = this._create(path);
} else if (!set) {
return null;
}
// Create mapping // Create mapping
--ii; --ii;
@ -101,20 +151,9 @@ class CacheMap {
this._updateCount(); this._updateCount();
return value; return node;
} }
/**
* Clears the cache.
*/
clear() {
this._map.clear();
this._count = 0;
this._resetEndNodes();
}
// Private
_updateRecency(node) { _updateRecency(node) {
this._removeNode(node); this._removeNode(node);
this._addNode(node, this._listFirst); this._addNode(node, this._listFirst);

View File

@ -57,101 +57,154 @@ function testApi() {
maxCount: 1, maxCount: 1,
expectedCount: 1, expectedCount: 1,
calls: [ calls: [
['get', 'a', 'b', 'c'] {func: 'getOrCreate', args: [['a', 'b', 'c']]}
] ]
}, },
{ {
maxCount: 10, maxCount: 10,
expectedCount: 1, expectedCount: 1,
calls: [ calls: [
['get', 'a', 'b', 'c'], {func: 'getOrCreate', args: [['a', 'b', 'c']]},
['get', 'a', 'b', 'c'], {func: 'getOrCreate', args: [['a', 'b', 'c']]},
['get', 'a', 'b', 'c'] {func: 'getOrCreate', args: [['a', 'b', 'c']]}
] ]
}, },
{ {
maxCount: 10, maxCount: 10,
expectedCount: 3, expectedCount: 3,
calls: [ calls: [
['get', 'a1', 'b', 'c'], {func: 'getOrCreate', args: [['a1', 'b', 'c']]},
['get', 'a2', 'b', 'c'], {func: 'getOrCreate', args: [['a2', 'b', 'c']]},
['get', 'a3', 'b', 'c'] {func: 'getOrCreate', args: [['a3', 'b', 'c']]}
] ]
}, },
{ {
maxCount: 10, maxCount: 10,
expectedCount: 3, expectedCount: 3,
calls: [ calls: [
['get', 'a', 'b1', 'c'], {func: 'getOrCreate', args: [['a', 'b1', 'c']]},
['get', 'a', 'b2', 'c'], {func: 'getOrCreate', args: [['a', 'b2', 'c']]},
['get', 'a', 'b3', 'c'] {func: 'getOrCreate', args: [['a', 'b3', 'c']]}
] ]
}, },
{ {
maxCount: 10, maxCount: 10,
expectedCount: 3, expectedCount: 3,
calls: [ calls: [
['get', 'a', 'b', 'c1'], {func: 'getOrCreate', args: [['a', 'b', 'c1']]},
['get', 'a', 'b', 'c2'], {func: 'getOrCreate', args: [['a', 'b', 'c2']]},
['get', 'a', 'b', 'c3'] {func: 'getOrCreate', args: [['a', 'b', 'c3']]}
] ]
}, },
{ {
maxCount: 1, maxCount: 1,
expectedCount: 1, expectedCount: 1,
calls: [ calls: [
['get', 'a1', 'b', 'c'], {func: 'getOrCreate', args: [['a1', 'b', 'c']]},
['get', 'a2', 'b', 'c'], {func: 'getOrCreate', args: [['a2', 'b', 'c']]},
['get', 'a3', 'b', 'c'] {func: 'getOrCreate', args: [['a3', 'b', 'c']]}
] ]
}, },
{ {
maxCount: 1, maxCount: 1,
expectedCount: 1, expectedCount: 1,
calls: [ calls: [
['get', 'a', 'b1', 'c'], {func: 'getOrCreate', args: [['a', 'b1', 'c']]},
['get', 'a', 'b2', 'c'], {func: 'getOrCreate', args: [['a', 'b2', 'c']]},
['get', 'a', 'b3', 'c'] {func: 'getOrCreate', args: [['a', 'b3', 'c']]}
] ]
}, },
{ {
maxCount: 1, maxCount: 1,
expectedCount: 1, expectedCount: 1,
calls: [ calls: [
['get', 'a', 'b', 'c1'], {func: 'getOrCreate', args: [['a', 'b', 'c1']]},
['get', 'a', 'b', 'c2'], {func: 'getOrCreate', args: [['a', 'b', 'c2']]},
['get', 'a', 'b', 'c3'] {func: 'getOrCreate', args: [['a', 'b', 'c3']]}
] ]
}, },
{ {
maxCount: 10, maxCount: 10,
expectedCount: 0, expectedCount: 0,
calls: [ calls: [
['get', 'a', 'b', 'c1'], {func: 'getOrCreate', args: [['a', 'b', 'c1']]},
['get', 'a', 'b', 'c2'], {func: 'getOrCreate', args: [['a', 'b', 'c2']]},
['get', 'a', 'b', 'c3'], {func: 'getOrCreate', args: [['a', 'b', 'c3']]},
['clear'] {func: 'clear', args: []}
] ]
}, },
{ {
maxCount: 0, maxCount: 0,
expectedCount: 0, expectedCount: 0,
calls: [ calls: [
['get', 'a1', 'b', 'c'], {func: 'getOrCreate', args: [['a1', 'b', 'c']]},
['get', 'a', 'b2', 'c'], {func: 'getOrCreate', args: [['a', 'b2', 'c']]},
['get', 'a', 'b', 'c3'] {func: 'getOrCreate', args: [['a', 'b', 'c3']]}
]
},
{
maxCount: 10,
expectedCount: 1,
calls: [
{func: 'get', args: [['a1', 'b', 'c']], returnValue: void 0},
{func: 'has', args: [['a1', 'b', 'c']], returnValue: false},
{func: 'set', args: [['a1', 'b', 'c'], 32], returnValue: void 0},
{func: 'get', args: [['a1', 'b', 'c']], returnValue: 32},
{func: 'has', args: [['a1', 'b', 'c']], returnValue: true}
]
},
{
maxCount: 10,
expectedCount: 2,
calls: [
{func: 'set', args: [['a1', 'b', 'c'], 32], returnValue: void 0},
{func: 'get', args: [['a1', 'b', 'c']], returnValue: 32},
{func: 'set', args: [['a1', 'b', 'c'], 64], returnValue: void 0},
{func: 'get', args: [['a1', 'b', 'c']], returnValue: 64},
{func: 'set', args: [['a2', 'b', 'c'], 96], returnValue: void 0},
{func: 'get', args: [['a2', 'b', 'c']], returnValue: 96}
]
},
{
maxCount: 2,
expectedCount: 2,
calls: [
{func: 'has', args: [['a1', 'b', 'c']], returnValue: false},
{func: 'has', args: [['a2', 'b', 'c']], returnValue: false},
{func: 'has', args: [['a3', 'b', 'c']], returnValue: false},
{func: 'set', args: [['a1', 'b', 'c'], 1], returnValue: void 0},
{func: 'has', args: [['a1', 'b', 'c']], returnValue: true},
{func: 'has', args: [['a2', 'b', 'c']], returnValue: false},
{func: 'has', args: [['a3', 'b', 'c']], returnValue: false},
{func: 'set', args: [['a2', 'b', 'c'], 2], returnValue: void 0},
{func: 'has', args: [['a1', 'b', 'c']], returnValue: true},
{func: 'has', args: [['a2', 'b', 'c']], returnValue: true},
{func: 'has', args: [['a3', 'b', 'c']], returnValue: false},
{func: 'set', args: [['a3', 'b', 'c'], 3], returnValue: void 0},
{func: 'has', args: [['a1', 'b', 'c']], returnValue: false},
{func: 'has', args: [['a2', 'b', 'c']], returnValue: true},
{func: 'has', args: [['a3', 'b', 'c']], returnValue: true}
] ]
} }
]; ];
const create = (...args) => args.join(','); const create = (args) => args.join(',');
for (const {maxCount, expectedCount, calls} of data) { for (const {maxCount, expectedCount, calls} of data) {
const cache = new CacheMap(maxCount, create); const cache = new CacheMap(maxCount, create);
assert.strictEqual(cache.maxCount, maxCount); assert.strictEqual(cache.maxCount, maxCount);
for (const [name, ...args] of calls) { for (const call of calls) {
switch (name) { const {func, args} = call;
case 'get': cache.get(...args); break; let returnValue;
case 'clear': cache.clear(); break; switch (func) {
case 'get': returnValue = cache.get(...args); break;
case 'getOrCreate': returnValue = cache.getOrCreate(...args); break;
case 'set': returnValue = cache.set(...args); break;
case 'has': returnValue = cache.has(...args); break;
case 'clear': returnValue = cache.clear(...args); break;
}
if (Object.prototype.hasOwnProperty.call(call, 'returnValue')) {
const {returnValue: expectedReturnValue} = call;
assert.deepStrictEqual(returnValue, expectedReturnValue);
} }
} }
assert.strictEqual(cache.count, expectedCount); assert.strictEqual(cache.count, expectedCount);