Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ObliviousLRUCache with delete functionality #162

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions oblivious-lru-cache.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Mnemonist ObliviousLRUCache Typings
* ===========================
*/
import {IArrayLikeConstructor} from './utils/types';

export default class ObliviousLRUCache<K, V> implements Iterable<[K, V]> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be wrong but I think you can import the LRUCache class in this definition file and make this class definition extends it, no?


// Members
capacity: number;
size: number;

// Constructor
constructor(capacity: number);
constructor(KeyArrayClass: IArrayLikeConstructor, ValueArrayClass: IArrayLikeConstructor, capacity: number);

// Methods
clear(): void;
set(key: K, value: V): this;
setpop(key: K, value: V): {evicted: boolean, key: K, value: V};
get(key: K): V | undefined;
peek(key: K): V | undefined;
remove(key: K): this;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be delete :)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And probably return boolean, based on next comment.

has(key: K): boolean;
forEach(callback: (value: V, key: K, cache: this) => void, scope?: any): void;
keys(): IterableIterator<K>;
values(): IterableIterator<V>;
entries(): IterableIterator<[K, V]>;
[Symbol.iterator](): IterableIterator<[K, V]>;
inspect(): any;

// Statics
static from<I, J>(
iterable: Iterable<[I, J]> | {[key: string]: J},
KeyArrayClass: IArrayLikeConstructor,
ValueArrayClass: IArrayLikeConstructor,
capacity?: number
): ObliviousLRUCache<I, J>;

static from<I, J>(
iterable: Iterable<[I, J]> | {[key: string]: J},
capacity?: number
): ObliviousLRUCache<I, J>;
}
181 changes: 181 additions & 0 deletions oblivious-lru-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* Mnemonist ObliviousLRUCache
* ===================
*
* An extension of LRUCache with delete functionality.
*/

var LRUCache = require('./lru-cache.js'),
forEach = require('obliterator/foreach'),
typed = require('./utils/typed-arrays.js'),
iterables = require('./utils/iterables.js');

function ObliviousLRUCache(Keys, Values, capacity) {
if (arguments.length < 2) {
LRUCache.call(this, Keys);
}
else {
LRUCache.call(this, Keys, Values, capacity);
}
var PointerArray = typed.getPointerArray(this.capacity);
this.deleted = new PointerArray(this.capacity);
this.deletedSize = 0;
}

ObliviousLRUCache.prototype = Object.create(LRUCache.prototype);
ObliviousLRUCache.prototype.constructor = ObliviousLRUCache;

/**
* Method used to clear the structure.
*
* @return {undefined}
*/
ObliviousLRUCache.prototype.clear = function() {
LRUCache.prototype.clear.call(this);
this.deletedSize = 0;
};

/**
* Method used to set the value for the given key in the cache.
*
* @param {any} key - Key.
* @param {any} value - Value.
* @return {undefined}
*/
ObliviousLRUCache.prototype.set = function(key, value) {
this.setpop(key, value);
};

/**
* Method used to set the value for the given key in the cache
*
* @param {any} key - Key.
* @param {any} value - Value.
* @return {{evicted: boolean, key: any, value: any}} An object containing the
* key and value of an item that was overwritten or evicted in the set
* operation, as well as a boolean indicating whether it was evicted due to
* limited capacity. Return value is null if nothing was evicted or overwritten
* during the set operation.
*/
ObliviousLRUCache.prototype.setpop = function(key, value) {
var oldValue = null;
var oldKey = null;
// The key already exists, we just need to update the value and splay on top
var pointer = this.items[key];

if (typeof pointer !== 'undefined') {
this.splayOnTop(pointer);
oldValue = this.V[pointer];
this.V[pointer] = value;
return {evicted: false, key: key, value: oldValue};
}

// The cache is not yet full
if (this.size < this.capacity) {
if (this.deletedSize > 0) {
pointer = this.deleted[--this.deletedSize];
}
else {
pointer = this.size;
}
this.size++;
}

// Cache is full, we need to drop the last value
else {
pointer = this.tail;
this.tail = this.backward[pointer];
oldValue = this.V[pointer];
oldKey = this.K[pointer];
delete this.items[this.K[pointer]];
}

// Storing key & value
this.items[key] = pointer;
this.K[pointer] = key;
this.V[pointer] = value;

// Moving the item at the front of the list
this.forward[pointer] = this.head;
this.backward[this.head] = pointer;
this.head = pointer;

// Return object if eviction took place, otherwise return null
if (oldKey) {
return {evicted: true, key: oldKey, value: oldValue};
}
else {
return null;
}
};

/**
* Method used to delete the value for the given key in the cache.
*
* @param {any} key - Key.
* @return {undefined}
*/
ObliviousLRUCache.prototype.delete = function(key) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method could a boolean indicating whether an item was delete, like the ES6 Map does, no?


var pointer = this.items[key];

if (typeof pointer === 'undefined') {
return;
}

if (this.head === pointer && this.tail === pointer) {
this.clear();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using #.clear here will reallocate the #.items object. Resetting head, tail and size to 0 should do the trick? Your condition can be simplified to this.size === 1 to be more performant if I understand the problem correctly.

return;
}

var previous = this.backward[pointer],
next = this.forward[pointer];

if (this.head === pointer) {
this.head = next;
}
if (this.tail === pointer) {
this.tail = previous;
}

this.forward[previous] = next;
this.backward[next] = previous;

delete this.items[key];
this.size--;
this.deleted[this.deletedSize++] = pointer;
};

/**
* Static @.from function taking an arbitrary iterable & converting it into
* a structure.
*
* @param {Iterable} iterable - Target iterable.
* @param {function} Keys - Array class for storing keys.
* @param {function} Values - Array class for storing values.
* @param {number} capacity - Cache's capacity.
* @return {ObliviousLRUCache}
*/
ObliviousLRUCache.from = function(iterable, Keys, Values, capacity) {
if (arguments.length < 2) {
capacity = iterables.guessLength(iterable);

if (typeof capacity !== 'number')
throw new Error('mnemonist/lru-cache.from: could not guess iterable length. Please provide desired capacity as last argument.');
}
else if (arguments.length === 2) {
capacity = Keys;
Keys = null;
Values = null;
}

var cache = new ObliviousLRUCache(Keys, Values, capacity);

forEach(iterable, function(value, key) {
cache.set(key, value);
});

return cache;
};

module.exports = ObliviousLRUCache;
49 changes: 47 additions & 2 deletions test/lru-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*/
var assert = require('assert'),
LRUCache = require('../lru-cache.js'),
LRUMap = require('../lru-map.js');
LRUMap = require('../lru-map.js'),
ObliviousLRUCache = require('../oblivious-lru-cache.js');

function makeTests(Cache, name) {
describe(name, function() {
Expand Down Expand Up @@ -56,7 +57,7 @@ function makeTests(Cache, name) {
assert.strictEqual(cache.peek('two'), 5);
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3], ['four', 4], ['two', 5]]);

if (name === 'LRUCache')
if (name === 'LRUCache' || name === 'ObliviousLRUCache')
assert.strictEqual(Object.keys(cache.items).length, 3);
else
assert.strictEqual(cache.items.size, 3);
Expand Down Expand Up @@ -215,8 +216,52 @@ function makeTests(Cache, name) {

assert.deepStrictEqual(entries, Array.from(cache.entries()));
});

if (name === 'ObliviousLRUCache') {
it('should be possible to delete keys from a LRU cache.', function() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering whether a rolling test should be done? Like add 2 remove 1, in a loop of ~10 items for a cache of ~4-5 capacity.

var cache = new Cache(3);

assert.strictEqual(cache.capacity, 3);

cache.set('one', 1);
cache.set('two', 2);
cache.set('three', 3);

assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3], ['two', 2], ['one', 1]]);

// Delete head
cache.delete('three');
assert.deepStrictEqual(Array.from(cache.entries()), [['two', 2], ['one', 1]]);

cache.set('three', 3);
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3], ['two', 2], ['one', 1]]);
// Delete node which is neither head or tail
cache.delete('two');
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3], ['one', 1]]);

// Delete tail
cache.delete('one');
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3]]);

// Delete the only key
cache.delete('three');
assert.strictEqual(cache.capacity, 3);
assert.strictEqual(cache.size, 0);
assert.strictEqual(cache.head, 0);
assert.strictEqual(cache.tail, 0);

cache.set('one', 1);
cache.set('two', 2);
cache.set('three', 3);
cache.set('two', 6);
cache.set('four', 4);

assert.deepStrictEqual(Array.from(cache.entries()), [['four', 4], ['two', 6], ['three', 3]]);
});
}
});
}

makeTests(LRUCache, 'LRUCache');
makeTests(LRUMap, 'LRUMap');
makeTests(ObliviousLRUCache, 'ObliviousLRUCache');