Skip to content

Commit 01ce7df

Browse files
committed
kvao: add iterateKeys() and keys()
Add a core implementation of KVAO.iterateKeys() which returns an AsyncIterator, inspired by - https://github.com/tc39/proposal-async-iteration - https://www.npmjs.com/package/async-iterators This way we can safely iterate even large sets of data. Also add KVAO.keys(), a sugar API converting the result of iterateKeys() into a single array.
1 parent 56aeeeb commit 01ce7df

File tree

8 files changed

+246
-0
lines changed

8 files changed

+246
-0
lines changed

lib/connectors/kv-memory.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ KeyValueMemoryConnector.prototype._removeIfExpired = function(modelName, key) {
7171
debug('Removing expired key', key);
7272
delete store[key];
7373
item = undefined;
74+
return true;
7475
}
76+
return false;
7577
};
7678

7779
KeyValueMemoryConnector.prototype.get =
@@ -154,6 +156,25 @@ function(modelName, key, options, callback) {
154156
});
155157
};
156158

159+
KeyValueMemoryConnector.prototype.iterateKeys =
160+
function(modelName, filter, options, callback) {
161+
var store = this._getStoreForModel(modelName);
162+
var self = this;
163+
var keys = Object.keys(store).filter(function(key) {
164+
return !self._removeIfExpired(modelName, key);
165+
});
166+
167+
debug('ITERATE KEYS %j -> %s keys', modelName, keys.length);
168+
169+
var ix = 0;
170+
return {
171+
next: function(cb) {
172+
var value = ix < keys.length ? keys[ix++] : undefined;
173+
setImmediate(function() { cb(null, value); });
174+
},
175+
};
176+
};
177+
157178
KeyValueMemoryConnector.prototype.disconnect = function(callback) {
158179
if (this._cleanupTimer)
159180
clearInterval(this._cleanupTimer);

lib/kvao/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ KeyValueAccessObject.get = require('./get');
99
KeyValueAccessObject.set = require('./set');
1010
KeyValueAccessObject.expire = require('./expire');
1111
KeyValueAccessObject.ttl = require('./ttl');
12+
KeyValueAccessObject.iterateKeys = require('./iterate-keys');
13+
KeyValueAccessObject.keys = require('./keys');
1214

1315
KeyValueAccessObject.getConnector = function() {
1416
return this.getDataSource().connector;

lib/kvao/iterate-keys.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
var assert = require('assert');
4+
var utils = require('../utils');
5+
6+
/**
7+
* Asynchronously iterate all keys.
8+
*
9+
* @param {Object} filter An optional filter object with the following
10+
* properties:
11+
* - `match` - glob string to use to filter returned keys, e.g. 'userid.*'
12+
* @param {Object} options
13+
*
14+
* @returns {AsyncIterator} An object implementing "next(cb) -> Promise"
15+
* function that can be used to iterate all keys.
16+
*
17+
* @header KVAO.iterateKeys(filter)
18+
*/
19+
module.exports = function keyValueIterateKeys(filter, options) {
20+
filter = filter || {};
21+
options = options || {};
22+
23+
assert(typeof filter === 'object', 'filter must be an object');
24+
assert(typeof options === 'object', 'options must be an object');
25+
26+
var iter = this.getConnector().iterateKeys(this.modelName, filter, options);
27+
// promisify the returned iterator
28+
return {
29+
next: function(callback) {
30+
callback = callback || utils.createPromiseCallback();
31+
iter.next(callback);
32+
return callback.promise;
33+
},
34+
};
35+
};

lib/kvao/keys.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
var assert = require('assert');
4+
var utils = require('../utils');
5+
6+
/**
7+
* Get all keys.
8+
*
9+
* **NOTE**
10+
* Building an in-memory array of all keys may be expensive.
11+
* Consider using `iterateKeys` instead.
12+
*
13+
* @param {Object} filter An optional filter object with the following
14+
* properties:
15+
* - `match` - glob string to use to filter returned keys, e.g. 'userid.*'
16+
* @param {Object} options
17+
* @callback callback
18+
* @param {Error=} err
19+
* @param {[String]} keys The list of keys.
20+
*
21+
* @promise
22+
*
23+
* @header KVAO.keys(filter, callback)
24+
*/
25+
module.exports = function keyValueKeys(filter, options, callback) {
26+
if (callback === undefined) {
27+
if (typeof options === 'function') {
28+
callback = options;
29+
options = undefined;
30+
} else if (options === undefined && typeof filter === 'function') {
31+
callback = filter;
32+
filter = undefined;
33+
}
34+
}
35+
36+
filter = filter || {};
37+
options = options || {};
38+
39+
assert(typeof filter === 'object', 'filter must be an object');
40+
assert(typeof options === 'object', 'options must be an object');
41+
42+
callback = callback || utils.createPromiseCallback();
43+
44+
var iter = this.iterateKeys(filter, options);
45+
var keys = [];
46+
iter.next(onNextKey);
47+
48+
function onNextKey(err, key) {
49+
if (err) return callback(err);
50+
if (key === undefined) return callback(null, keys);
51+
keys.push(key);
52+
iter.next(onNextKey);
53+
}
54+
55+
return callback.promise;
56+
};
57+

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"node >= 0.6"
3333
],
3434
"devDependencies": {
35+
"async-iterators": "^0.2.2",
3536
"eslint": "^2.5.3",
3637
"eslint-config-loopback": "^2.0.0",
3738
"mocha": "^2.1.0",

test/kvao/_helpers.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
'use strict';
22

3+
var Promise = require('bluebird');
4+
35
exports.givenCacheItem = function(dataSourceFactory) {
46
var dataSource = dataSourceFactory();
57
return dataSource.createModel('CacheItem', {
68
key: String,
79
value: 'any',
810
});
911
};
12+
13+
exports.givenKeys = function(Model, keys, cb) {
14+
var p = Promise.all(
15+
keys.map(function(k) {
16+
return Model.set(k, 'value-' + k);
17+
})
18+
);
19+
if (cb) {
20+
p = p.then(function(r) { cb(null, r); }, cb);
21+
}
22+
return p;
23+
};

test/kvao/iterate-keys.suite.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
var asyncIterators = require('async-iterators');
4+
var helpers = require('./_helpers');
5+
var Promise = require('bluebird');
6+
var should = require('should');
7+
var toArray = Promise.promisify(asyncIterators.toArray);
8+
9+
module.exports = function(dataSourceFactory, connectorCapabilities) {
10+
describe('iterateKeys', function() {
11+
var CacheItem;
12+
beforeEach(function unpackContext() {
13+
CacheItem = helpers.givenCacheItem(dataSourceFactory);
14+
});
15+
16+
it('returns AsyncIterator covering all keys', function() {
17+
return helpers.givenKeys(CacheItem, ['key1', 'key2'])
18+
.then(function() {
19+
var it = CacheItem.iterateKeys();
20+
should(it).have.property('next');
21+
return toArray(it);
22+
})
23+
.then(function(keys) {
24+
keys.sort();
25+
should(keys).eql(['key1', 'key2']);
26+
});
27+
});
28+
29+
it('returns AsyncIterator supporting Promises', function() {
30+
var iterator;
31+
return helpers.givenKeys(CacheItem, ['key'])
32+
.then(function() {
33+
iterator = CacheItem.iterateKeys();
34+
return iterator.next();
35+
})
36+
.then(function(key) {
37+
should(key).equal('key');
38+
return iterator.next();
39+
})
40+
.then(function(key) {
41+
// Note: AsyncIterator contract requires `undefined` to signal
42+
// the end of the sequence. Other false-y values like `null`
43+
// don't work.
44+
should(key).equal(undefined);
45+
});
46+
});
47+
});
48+
};

test/kvao/keys.suite.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
3+
var helpers = require('./_helpers');
4+
var Promise = require('bluebird');
5+
var should = require('should');
6+
7+
module.exports = function(dataSourceFactory, connectorCapabilities) {
8+
describe('keys', function() {
9+
var CacheItem;
10+
beforeEach(function unpackContext() {
11+
CacheItem = helpers.givenCacheItem(dataSourceFactory);
12+
});
13+
14+
it('returns all keys - Callback API', function(done) {
15+
helpers.givenKeys(CacheItem, ['key1', 'key2'], function(err) {
16+
if (err) return done(err);
17+
CacheItem.keys(function(err, keys) {
18+
if (err) return done(err);
19+
keys.sort();
20+
should(keys).eql(['key1', 'key2']);
21+
done();
22+
});
23+
});
24+
});
25+
26+
it('returns all keys - Promise API', function() {
27+
return helpers.givenKeys(CacheItem, ['key1', 'key2'])
28+
.then(function() {
29+
return CacheItem.keys();
30+
})
31+
.then(function(keys) {
32+
keys.sort();
33+
should(keys).eql(['key1', 'key2']);
34+
});
35+
});
36+
37+
it('returns keys of the given model only', function() {
38+
var AnotherModel = CacheItem.dataSource.createModel('AnotherModel');
39+
return helpers.givenKeys(CacheItem, ['key1', 'key2'])
40+
.then(function() {
41+
return helpers.givenKeys(AnotherModel, ['otherKey1', 'otherKey2']);
42+
})
43+
.then(function() {
44+
return CacheItem.keys();
45+
})
46+
.then(function(keys) {
47+
keys.sort();
48+
should(keys).eql(['key1', 'key2']);
49+
});
50+
});
51+
52+
it('handles large key set', function() {
53+
var expectedKeys = [];
54+
for (var ix = 0; ix < 1000; ix++)
55+
expectedKeys.push('key-' + ix);
56+
57+
return helpers.givenKeys(CacheItem, expectedKeys)
58+
.then(function() {
59+
return CacheItem.keys();
60+
})
61+
.then(function(keys) {
62+
keys.sort();
63+
expectedKeys.sort();
64+
should(keys).eql(expectedKeys);
65+
});
66+
});
67+
});
68+
};

0 commit comments

Comments
 (0)