2016-08-08 08:15:22 +00:00
|
|
|
'use strict';
|
|
|
|
|
2016-08-18 01:17:37 +00:00
|
|
|
var g = require('strong-globalize')();
|
|
|
|
|
2016-08-08 08:15:22 +00:00
|
|
|
var assert = require('assert');
|
|
|
|
var Connector = require('loopback-connector').Connector;
|
|
|
|
var debug = require('debug')('loopback:connector:kv-memory');
|
2016-08-17 12:24:20 +00:00
|
|
|
var minimatch = require('minimatch');
|
2016-08-08 08:15:22 +00:00
|
|
|
var util = require('util');
|
|
|
|
|
|
|
|
exports.initialize = function initializeDataSource(dataSource, cb) {
|
|
|
|
var settings = dataSource.settings;
|
|
|
|
dataSource.connector = new KeyValueMemoryConnector(settings, dataSource);
|
|
|
|
if (cb) process.nextTick(cb);
|
|
|
|
};
|
|
|
|
|
|
|
|
function KeyValueMemoryConnector(settings, dataSource) {
|
|
|
|
Connector.call(this, 'kv-memory', settings);
|
|
|
|
|
|
|
|
debug('Connector settings', settings);
|
|
|
|
|
|
|
|
this.dataSource = dataSource;
|
|
|
|
this.DataAccessObject = dataSource.juggler.KeyValueAccessObject;
|
|
|
|
|
|
|
|
this._store = Object.create(null);
|
|
|
|
|
|
|
|
this._setupRegularCleanup();
|
|
|
|
};
|
|
|
|
util.inherits(KeyValueMemoryConnector, Connector);
|
|
|
|
|
|
|
|
KeyValueMemoryConnector.prototype._setupRegularCleanup = function() {
|
|
|
|
// Scan the database for expired keys at a regular interval
|
|
|
|
// in order to release memory. Note that GET operation checks
|
|
|
|
// key expiration too, the scheduled cleanup is merely a performance
|
|
|
|
// optimization.
|
|
|
|
var self = this;
|
2016-08-09 13:35:23 +00:00
|
|
|
var timer = this._cleanupTimer = setInterval(
|
|
|
|
function() {
|
|
|
|
if (self && self._removeExpiredItems) {
|
|
|
|
self._removeExpiredItems();
|
|
|
|
} else {
|
|
|
|
// The datasource/connector was destroyed - cancel the timer
|
|
|
|
clearInterval(timer);
|
|
|
|
}
|
|
|
|
},
|
2016-08-08 08:15:22 +00:00
|
|
|
1000);
|
|
|
|
this._cleanupTimer.unref();
|
|
|
|
};
|
|
|
|
|
|
|
|
KeyValueMemoryConnector._removeExpiredItems = function() {
|
|
|
|
debug('Running scheduled cleanup of expired items.');
|
|
|
|
for (var modelName in this._store) {
|
|
|
|
var modelStore = this._store[modelName];
|
|
|
|
for (var key in modelStore) {
|
|
|
|
if (modelStore[key].isExpired()) {
|
|
|
|
debug('Removing expired key', key);
|
|
|
|
delete modelStore[key];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
KeyValueMemoryConnector.prototype._getStoreForModel = function(modelName) {
|
|
|
|
if (!(modelName in this._store)) {
|
|
|
|
this._store[modelName] = Object.create(null);
|
|
|
|
}
|
|
|
|
return this._store[modelName];
|
|
|
|
};
|
|
|
|
|
2016-08-10 06:21:51 +00:00
|
|
|
KeyValueMemoryConnector.prototype._removeIfExpired = function(modelName, key) {
|
2016-08-08 08:15:22 +00:00
|
|
|
var store = this._getStoreForModel(modelName);
|
|
|
|
var item = store[key];
|
|
|
|
if (item && item.isExpired()) {
|
|
|
|
debug('Removing expired key', key);
|
|
|
|
delete store[key];
|
|
|
|
item = undefined;
|
2016-08-15 12:55:26 +00:00
|
|
|
return true;
|
2016-08-08 08:15:22 +00:00
|
|
|
}
|
2016-08-15 12:55:26 +00:00
|
|
|
return false;
|
2016-08-10 06:21:51 +00:00
|
|
|
};
|
2016-08-08 08:15:22 +00:00
|
|
|
|
2016-08-10 06:21:51 +00:00
|
|
|
KeyValueMemoryConnector.prototype.get =
|
|
|
|
function(modelName, key, options, callback) {
|
|
|
|
this._removeIfExpired(modelName, key);
|
2016-08-08 08:15:22 +00:00
|
|
|
|
2016-08-10 06:21:51 +00:00
|
|
|
var store = this._getStoreForModel(modelName);
|
|
|
|
var item = store[key];
|
|
|
|
var value = item ? item.value : null;
|
2016-08-08 08:15:22 +00:00
|
|
|
debug('GET %j %j -> %s', modelName, key, value);
|
|
|
|
|
|
|
|
if (/^buffer:/.test(value)) {
|
|
|
|
value = new Buffer(value.slice(7), 'base64');
|
|
|
|
} else if (/^date:/.test(value)) {
|
|
|
|
value = new Date(value.slice(5));
|
|
|
|
} else if (value != null) {
|
|
|
|
value = JSON.parse(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
process.nextTick(function() {
|
|
|
|
callback(null, value);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
KeyValueMemoryConnector.prototype.set =
|
|
|
|
function(modelName, key, value, options, callback) {
|
|
|
|
var store = this._getStoreForModel(modelName);
|
|
|
|
if (Buffer.isBuffer(value)) {
|
|
|
|
value = 'buffer:' + value.toString('base64');
|
|
|
|
} else if (value instanceof Date) {
|
|
|
|
value = 'date:' + value.toISOString();
|
|
|
|
} else {
|
|
|
|
value = JSON.stringify(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
debug('SET %j %j %s %j', modelName, key, value, options);
|
|
|
|
store[key] = new StoreItem(value, options && options.ttl);
|
|
|
|
|
|
|
|
process.nextTick(callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
KeyValueMemoryConnector.prototype.expire =
|
|
|
|
function(modelName, key, ttl, options, callback) {
|
2016-09-02 04:06:08 +00:00
|
|
|
this._removeIfExpired(modelName, key);
|
|
|
|
|
2016-08-08 08:15:22 +00:00
|
|
|
var store = this._getStoreForModel(modelName);
|
|
|
|
|
|
|
|
if (!(key in store)) {
|
|
|
|
return process.nextTick(function() {
|
2016-08-18 01:17:37 +00:00
|
|
|
var err = new Error(g.f('Cannot expire unknown key %j', key));
|
2016-08-08 15:22:33 +00:00
|
|
|
err.statusCode = 404;
|
|
|
|
callback(err);
|
2016-08-08 08:15:22 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
debug('EXPIRE %j %j %s', modelName, key, ttl || '(never)');
|
|
|
|
store[key].setTtl(ttl);
|
|
|
|
process.nextTick(callback);
|
|
|
|
};
|
|
|
|
|
2016-08-10 06:21:51 +00:00
|
|
|
KeyValueMemoryConnector.prototype.ttl =
|
|
|
|
function(modelName, key, options, callback) {
|
|
|
|
this._removeIfExpired(modelName, key);
|
|
|
|
|
|
|
|
var store = this._getStoreForModel(modelName);
|
|
|
|
|
|
|
|
// key is unknown
|
|
|
|
if (!(key in store)) {
|
|
|
|
return process.nextTick(function() {
|
2016-08-18 01:17:37 +00:00
|
|
|
var err = new Error(g.f('Cannot get TTL for unknown key %j', key));
|
2016-08-10 06:21:51 +00:00
|
|
|
err.statusCode = 404;
|
|
|
|
callback(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
var ttl = store[key].getTtl();
|
|
|
|
debug('TTL %j %j -> %s', modelName, key, ttl);
|
|
|
|
|
|
|
|
process.nextTick(function() {
|
|
|
|
callback(null, ttl);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-08-15 12:55:26 +00:00
|
|
|
KeyValueMemoryConnector.prototype.iterateKeys =
|
|
|
|
function(modelName, filter, options, callback) {
|
|
|
|
var store = this._getStoreForModel(modelName);
|
|
|
|
var self = this;
|
2016-08-17 12:24:20 +00:00
|
|
|
var checkFilter = createMatcher(filter.match);
|
|
|
|
|
2016-08-15 12:55:26 +00:00
|
|
|
var keys = Object.keys(store).filter(function(key) {
|
2016-08-17 12:24:20 +00:00
|
|
|
return !self._removeIfExpired(modelName, key) && checkFilter(key);
|
2016-08-15 12:55:26 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
debug('ITERATE KEYS %j -> %s keys', modelName, keys.length);
|
|
|
|
|
|
|
|
var ix = 0;
|
|
|
|
return {
|
|
|
|
next: function(cb) {
|
|
|
|
var value = ix < keys.length ? keys[ix++] : undefined;
|
|
|
|
setImmediate(function() { cb(null, value); });
|
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2016-08-17 12:24:20 +00:00
|
|
|
function createMatcher(pattern) {
|
|
|
|
if (!pattern) return function matchAll() { return true; };
|
|
|
|
|
|
|
|
return minimatch.filter(pattern, {
|
|
|
|
nobrace: true,
|
|
|
|
noglobstar: true,
|
|
|
|
dot: true,
|
|
|
|
noext: true,
|
|
|
|
nocomment: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-08-08 08:15:22 +00:00
|
|
|
KeyValueMemoryConnector.prototype.disconnect = function(callback) {
|
|
|
|
if (this._cleanupTimer)
|
|
|
|
clearInterval(this._cleanupTimer);
|
|
|
|
this._cleanupTimer = null;
|
|
|
|
process.nextTick(callback);
|
|
|
|
};
|
|
|
|
|
2017-01-06 03:32:41 +00:00
|
|
|
KeyValueMemoryConnector.prototype.delete =
|
|
|
|
function(modelName, key, options, callback) {
|
|
|
|
var store = this._getStoreForModel(modelName);
|
|
|
|
delete store[key];
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
KeyValueMemoryConnector.prototype.deleteAll =
|
2016-12-31 01:27:49 +00:00
|
|
|
function(modelName, options, callback) {
|
2017-01-06 03:32:41 +00:00
|
|
|
var modelStore = this._getStoreForModel(modelName);
|
|
|
|
for (var key in modelStore)
|
|
|
|
delete modelStore[key];
|
2016-12-31 01:27:49 +00:00
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
2016-08-08 08:15:22 +00:00
|
|
|
function StoreItem(value, ttl) {
|
|
|
|
this.value = value;
|
|
|
|
this.setTtl(ttl);
|
|
|
|
}
|
|
|
|
|
|
|
|
StoreItem.prototype.isExpired = function() {
|
|
|
|
return this.expires && this.expires <= Date.now();
|
|
|
|
};
|
|
|
|
|
|
|
|
StoreItem.prototype.setTtl = function(ttl) {
|
|
|
|
if (ttl) {
|
|
|
|
this.expires = Date.now() + ttl;
|
|
|
|
} else {
|
|
|
|
this.expires = undefined;
|
|
|
|
}
|
|
|
|
};
|
2016-08-10 06:21:51 +00:00
|
|
|
|
|
|
|
StoreItem.prototype.getTtl = function() {
|
|
|
|
return !this.expires ? undefined : this.expires - Date.now();
|
|
|
|
};
|