loopback-datasource-juggler/lib/connectors/kv-memory.js

238 lines
6.3 KiB
JavaScript

'use strict';
const g = require('strong-globalize')();
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');
exports.initialize = function initializeDataSource(dataSource, cb) {
const 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.
this._cleanupTimer = setInterval(
() => {
if (this && this._removeExpiredItems) {
this._removeExpiredItems();
} else {
// The datasource/connector was destroyed - cancel the timer
clearInterval(this._cleanupTimer);
}
},
1000
);
this._cleanupTimer.unref();
};
KeyValueMemoryConnector._removeExpiredItems = function() {
debug('Running scheduled cleanup of expired items.');
for (const modelName in this._store) {
const modelStore = this._store[modelName];
for (const 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];
};
KeyValueMemoryConnector.prototype._removeIfExpired = function(modelName, key) {
const store = this._getStoreForModel(modelName);
let item = store[key];
if (item && item.isExpired()) {
debug('Removing expired key', key);
delete store[key];
item = undefined;
return true;
}
return false;
};
KeyValueMemoryConnector.prototype.get =
function(modelName, key, options, callback) {
this._removeIfExpired(modelName, key);
const store = this._getStoreForModel(modelName);
const item = store[key];
let value = item ? item.value : null;
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) {
const 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) {
this._removeIfExpired(modelName, key);
const store = this._getStoreForModel(modelName);
if (!(key in store)) {
return process.nextTick(function() {
const err = new Error(g.f('Cannot expire unknown key %j', key));
err.statusCode = 404;
callback(err);
});
}
debug('EXPIRE %j %j %s', modelName, key, ttl || '(never)');
store[key].setTtl(ttl);
process.nextTick(callback);
};
KeyValueMemoryConnector.prototype.ttl =
function(modelName, key, options, callback) {
this._removeIfExpired(modelName, key);
const store = this._getStoreForModel(modelName);
// key is unknown
if (!(key in store)) {
return process.nextTick(function() {
const err = new Error(g.f('Cannot get TTL for unknown key %j', key));
err.statusCode = 404;
callback(err);
});
}
const ttl = store[key].getTtl();
debug('TTL %j %j -> %s', modelName, key, ttl);
process.nextTick(function() {
callback(null, ttl);
});
};
KeyValueMemoryConnector.prototype.iterateKeys =
function(modelName, filter, options, callback) {
const store = this._getStoreForModel(modelName);
const self = this;
const checkFilter = createMatcher(filter.match);
const keys = Object.keys(store).filter(function(key) {
return !self._removeIfExpired(modelName, key) && checkFilter(key);
});
debug('ITERATE KEYS %j -> %s keys', modelName, keys.length);
let ix = 0;
return {
next: function(cb) {
const value = ix < keys.length ? keys[ix++] : undefined;
setImmediate(function() { cb(null, value); });
},
};
};
function createMatcher(pattern) {
if (!pattern) return function matchAll() { return true; };
return minimatch.filter(pattern, {
nobrace: true,
noglobstar: true,
dot: true,
noext: true,
nocomment: true,
});
}
KeyValueMemoryConnector.prototype.disconnect = function(callback) {
if (this._cleanupTimer)
clearInterval(this._cleanupTimer);
this._cleanupTimer = null;
process.nextTick(callback);
};
KeyValueMemoryConnector.prototype.delete =
function(modelName, key, options, callback) {
const store = this._getStoreForModel(modelName);
delete store[key];
callback();
};
KeyValueMemoryConnector.prototype.deleteAll =
function(modelName, options, callback) {
const modelStore = this._getStoreForModel(modelName);
for (const key in modelStore)
delete modelStore[key];
callback();
};
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;
}
};
StoreItem.prototype.getTtl = function() {
return !this.expires ? undefined : this.expires - Date.now();
};