2020-01-21 18:12:14 +00:00
|
|
|
// Copyright IBM Corp. 2016,2019. All Rights Reserved.
|
2019-05-08 15:45:37 +00:00
|
|
|
// Node module: loopback-datasource-juggler
|
|
|
|
// This file is licensed under the MIT License.
|
|
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
|
2016-08-08 08:15:22 +00:00
|
|
|
'use strict';
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
const g = require('strong-globalize')();
|
2016-08-18 01:17:37 +00:00
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
const assert = require('assert');
|
|
|
|
const Connector = require('loopback-connector').Connector;
|
|
|
|
const debug = require('debug')('loopback:connector:kv-memory');
|
|
|
|
const minimatch = require('minimatch');
|
|
|
|
const util = require('util');
|
2016-08-08 08:15:22 +00:00
|
|
|
|
|
|
|
exports.initialize = function initializeDataSource(dataSource, cb) {
|
2018-12-07 14:54:29 +00:00
|
|
|
const settings = dataSource.settings;
|
2016-08-08 08:15:22 +00:00
|
|
|
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();
|
2018-12-07 14:43:40 +00:00
|
|
|
}
|
2016-08-08 08:15:22 +00:00
|
|
|
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.
|
2018-12-07 15:22:36 +00:00
|
|
|
this._cleanupTimer = setInterval(
|
|
|
|
() => {
|
|
|
|
if (this && this._removeExpiredItems) {
|
|
|
|
this._removeExpiredItems();
|
2016-08-09 13:35:23 +00:00
|
|
|
} else {
|
|
|
|
// The datasource/connector was destroyed - cancel the timer
|
2018-12-07 15:22:36 +00:00
|
|
|
clearInterval(this._cleanupTimer);
|
2016-08-09 13:35:23 +00:00
|
|
|
}
|
|
|
|
},
|
2019-12-03 09:09:16 +00:00
|
|
|
1000,
|
2018-07-16 06:46:25 +00:00
|
|
|
);
|
2016-08-08 08:15:22 +00:00
|
|
|
this._cleanupTimer.unref();
|
|
|
|
};
|
|
|
|
|
|
|
|
KeyValueMemoryConnector._removeExpiredItems = function() {
|
|
|
|
debug('Running scheduled cleanup of expired items.');
|
2018-12-07 14:54:29 +00:00
|
|
|
for (const modelName in this._store) {
|
|
|
|
const modelStore = this._store[modelName];
|
|
|
|
for (const key in modelStore) {
|
2016-08-08 08:15:22 +00:00
|
|
|
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) {
|
2018-12-07 14:54:29 +00:00
|
|
|
const store = this._getStoreForModel(modelName);
|
|
|
|
let item = store[key];
|
2016-08-08 08:15:22 +00:00
|
|
|
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
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
const store = this._getStoreForModel(modelName);
|
|
|
|
const item = store[key];
|
|
|
|
let 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) {
|
2018-12-07 14:54:29 +00:00
|
|
|
const store = this._getStoreForModel(modelName);
|
2016-08-08 08:15:22 +00:00
|
|
|
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);
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
const store = this._getStoreForModel(modelName);
|
2016-08-08 08:15:22 +00:00
|
|
|
|
|
|
|
if (!(key in store)) {
|
|
|
|
return process.nextTick(function() {
|
2018-12-07 14:54:29 +00:00
|
|
|
const 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);
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
const store = this._getStoreForModel(modelName);
|
2016-08-10 06:21:51 +00:00
|
|
|
|
|
|
|
// key is unknown
|
|
|
|
if (!(key in store)) {
|
|
|
|
return process.nextTick(function() {
|
2018-12-07 14:54:29 +00:00
|
|
|
const 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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
const ttl = store[key].getTtl();
|
2016-08-10 06:21:51 +00:00
|
|
|
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) {
|
2018-12-07 14:54:29 +00:00
|
|
|
const store = this._getStoreForModel(modelName);
|
|
|
|
const self = this;
|
|
|
|
const checkFilter = createMatcher(filter.match);
|
2016-08-17 12:24:20 +00:00
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
const 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);
|
|
|
|
|
2018-12-07 14:54:29 +00:00
|
|
|
let ix = 0;
|
2016-08-15 12:55:26 +00:00
|
|
|
return {
|
|
|
|
next: function(cb) {
|
2018-12-07 14:54:29 +00:00
|
|
|
const value = ix < keys.length ? keys[ix++] : undefined;
|
2016-08-15 12:55:26 +00:00
|
|
|
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) {
|
2018-12-07 14:54:29 +00:00
|
|
|
const store = this._getStoreForModel(modelName);
|
2017-01-06 03:32:41 +00:00
|
|
|
delete store[key];
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
KeyValueMemoryConnector.prototype.deleteAll =
|
2016-12-31 01:27:49 +00:00
|
|
|
function(modelName, options, callback) {
|
2018-12-07 14:54:29 +00:00
|
|
|
const modelStore = this._getStoreForModel(modelName);
|
|
|
|
for (const key in modelStore)
|
2017-01-06 03:32:41 +00:00
|
|
|
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();
|
|
|
|
};
|