231 lines
6.0 KiB
JavaScript
231 lines
6.0 KiB
JavaScript
'use strict';
|
|
|
|
const Boom = require('@hapi/boom');
|
|
const Hoek = require('@hapi/hoek');
|
|
|
|
|
|
const internals = {
|
|
maxTimer: 2147483647, // 2 ^ 31 - 1
|
|
entrySize: 144 // Approximate cache entry size without value: 144 bytes
|
|
};
|
|
|
|
|
|
internals.defaults = {
|
|
maxByteSize: 100 * 1024 * 1024, // 100MB
|
|
minCleanupIntervalMsec: 1000,
|
|
cloneBuffersOnGet: false
|
|
};
|
|
|
|
|
|
exports.Engine = class CatboxMemoryEngine {
|
|
|
|
constructor(options = {}) {
|
|
|
|
Hoek.assert(options.maxByteSize === undefined || options.maxByteSize >= 0, 'Invalid cache maxByteSize value');
|
|
Hoek.assert(options.allowMixedContent === undefined, 'allowMixedContent no longer supported');
|
|
Hoek.assert(options.minCleanupIntervalMsec === undefined || options.minCleanupIntervalMsec < internals.maxTimer, 'Invalid cache minCleanupIntervalMsec value');
|
|
Hoek.assert(options.cloneBuffersOnGet === undefined || typeof options.cloneBuffersOnGet === 'boolean', 'Invalid cloneBuffersOnGet value');
|
|
|
|
this.settings = Hoek.applyToDefaults(internals.defaults, options);
|
|
this.cache = null;
|
|
|
|
this._timer = null;
|
|
this._timerDue = null;
|
|
}
|
|
|
|
start() {
|
|
|
|
if (!this.cache) {
|
|
this.cache = new Map();
|
|
this.byteSize = 0;
|
|
}
|
|
}
|
|
|
|
_scheduleCleanup(msec) {
|
|
|
|
const cleanup = () => {
|
|
|
|
this._timer = null;
|
|
this._timerDue = null;
|
|
|
|
const now = Date.now();
|
|
let next = Infinity;
|
|
for (const [, segment] of this.cache) {
|
|
for (const [id, envelope] of segment) {
|
|
const ttl = envelope.stored + envelope.ttl - now;
|
|
if (ttl <= 0) {
|
|
segment.delete(id);
|
|
this.byteSize -= envelope.byteSize;
|
|
}
|
|
else {
|
|
next = Math.min(next, ttl);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (next !== Infinity) {
|
|
this._scheduleCleanup(next);
|
|
}
|
|
};
|
|
|
|
const now = Date.now();
|
|
const timeout = Math.min(Math.max(this.settings.minCleanupIntervalMsec, msec), internals.maxTimer);
|
|
if (this._timer) {
|
|
if (this._timerDue - now < msec) {
|
|
return;
|
|
}
|
|
|
|
clearTimeout(this._timer);
|
|
}
|
|
|
|
this._timerDue = now + timeout;
|
|
this._timer = setTimeout(cleanup, timeout);
|
|
}
|
|
|
|
stop() {
|
|
|
|
clearTimeout(this._timer);
|
|
this._timer = null;
|
|
this._timerDue = null;
|
|
|
|
this.cache = null;
|
|
this.byteSize = 0;
|
|
}
|
|
|
|
isReady() {
|
|
|
|
return !!this.cache;
|
|
}
|
|
|
|
validateSegmentName(name) {
|
|
|
|
if (!name) {
|
|
throw new Boom.Boom('Empty string');
|
|
}
|
|
|
|
if (name.indexOf('\u0000') !== -1) {
|
|
throw new Boom.Boom('Includes null character');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get(key) {
|
|
|
|
if (!this.cache) {
|
|
throw new Boom.Boom('Connection not started');
|
|
}
|
|
|
|
const segment = this.cache.get(key.segment);
|
|
if (!segment) {
|
|
return null;
|
|
}
|
|
|
|
const envelope = segment.get(key.id);
|
|
if (!envelope) {
|
|
return null;
|
|
}
|
|
|
|
if (envelope.stored + envelope.ttl < Date.now()) {
|
|
this.drop(key);
|
|
return null;
|
|
}
|
|
|
|
let item = null;
|
|
if (Buffer.isBuffer(envelope.item)) {
|
|
item = envelope.item;
|
|
if (this.settings.cloneBuffersOnGet) {
|
|
const copy = Buffer.alloc(item.length);
|
|
item.copy(copy);
|
|
item = copy;
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
item = JSON.parse(envelope.item);
|
|
}
|
|
catch (err) {
|
|
throw new Boom.Boom('Bad value content');
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
item,
|
|
stored: envelope.stored,
|
|
ttl: envelope.ttl
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
set(key, value, ttl) {
|
|
|
|
if (!this.cache) {
|
|
throw new Boom.Boom('Connection not started');
|
|
}
|
|
|
|
const envelope = new internals.MemoryCacheEntry(key, value, ttl);
|
|
|
|
let segment = this.cache.get(key.segment);
|
|
if (!segment) {
|
|
segment = new Map();
|
|
this.cache.set(key.segment, segment);
|
|
}
|
|
|
|
const cachedItem = segment.get(key.id);
|
|
if (cachedItem) {
|
|
this.byteSize -= cachedItem.byteSize; // If the item existed, decrement the byteSize as the value could be different
|
|
}
|
|
|
|
if (this.settings.maxByteSize &&
|
|
(this.byteSize + envelope.byteSize > this.settings.maxByteSize)) {
|
|
|
|
throw new Boom.Boom('Cache size limit reached');
|
|
}
|
|
|
|
this._scheduleCleanup(ttl);
|
|
segment.set(key.id, envelope);
|
|
this.byteSize += envelope.byteSize;
|
|
}
|
|
|
|
drop(key) {
|
|
|
|
if (!this.cache) {
|
|
throw new Boom.Boom('Connection not started');
|
|
}
|
|
|
|
const segment = this.cache.get(key.segment);
|
|
if (segment) {
|
|
const item = segment.get(key.id);
|
|
if (item) {
|
|
this.byteSize -= item.byteSize;
|
|
segment.delete(key.id);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
internals.MemoryCacheEntry = class {
|
|
|
|
constructor(key, value, ttl) {
|
|
|
|
let valueByteSize = 0;
|
|
|
|
if (Buffer.isBuffer(value)) {
|
|
this.item = Buffer.alloc(value.length);
|
|
value.copy(this.item); // Copy buffer to prevent value from changing while in the cache
|
|
valueByteSize = this.item.length;
|
|
}
|
|
else {
|
|
this.item = JSON.stringify(value); // stringify() to prevent value from changing while in the cache
|
|
valueByteSize = Buffer.byteLength(this.item);
|
|
}
|
|
|
|
this.stored = Date.now();
|
|
this.ttl = ttl;
|
|
this.byteSize = internals.entrySize + valueByteSize + Buffer.byteLength(key.segment) + Buffer.byteLength(key.id);
|
|
this.timeoutId = null;
|
|
}
|
|
};
|