tastypie
Version:
Tastypie is a webservice API framework for Node.js based on Django's Tastypie Framework. It provides a convenient, yet powerful and highly customizable, abstraction for creating REST-style interfaces
242 lines (166 loc) • 5.81 kB
JavaScript
'use strict';
// Load modules
const Hoek = require('hoek');
// Declare internals
const internals = {};
internals.defaults = {
maxByteSize: 100 * 1024 * 1024, // 100MB
allowMixedContent: false
};
// Provides a named reference for memory debugging
internals.MemoryCacheSegment = function MemoryCacheSegment() {
};
internals.MemoryCacheEntry = function MemoryCacheEntry(key, value, ttl, allowMixedContent) {
let valueByteSize = 0;
if (allowMixedContent && Buffer.isBuffer(value)) {
this.item = new Buffer(value.length);
// copy buffer to prevent value from changing while in the cache
value.copy(this.item);
valueByteSize = this.item.length;
}
else {
// stringify() to prevent value from changing while in the cache
this.item = JSON.stringify(value);
valueByteSize = Buffer.byteLength(this.item);
}
this.stored = Date.now();
this.ttl = ttl;
// Approximate cache entry size without value: 144 bytes
this.byteSize = 144 + valueByteSize + Buffer.byteLength(key.segment) + Buffer.byteLength(key.id);
this.timeoutId = null;
};
exports = module.exports = internals.Connection = function MemoryCache(options) {
Hoek.assert(this.constructor === internals.Connection, 'Memory cache client must be instantiated using new');
Hoek.assert(!options || options.maxByteSize === undefined || options.maxByteSize >= 0, 'Invalid cache maxByteSize value');
Hoek.assert(!options || options.allowMixedContent === undefined || typeof options.allowMixedContent === 'boolean', 'Invalid allowMixedContent value');
this.settings = Hoek.applyToDefaults(internals.defaults, options || {});
this.cache = null;
};
internals.Connection.prototype.start = function (callback) {
callback = Hoek.nextTick(callback);
if (!this.cache) {
this.cache = {};
this.byteSize = 0;
}
return callback();
};
internals.Connection.prototype.stop = function () {
// Clean up pending eviction timers.
if (this.cache) {
const segments = Object.keys(this.cache);
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
const keys = Object.keys(this.cache[segment]);
for (let j = 0; j < keys.length; ++j) {
const key = keys[j];
clearTimeout(this.cache[segment][key].timeoutId);
}
}
}
this.cache = null;
this.byteSize = 0;
return;
};
internals.Connection.prototype.isReady = function () {
return !!this.cache;
};
internals.Connection.prototype.validateSegmentName = function (name) {
if (!name) {
return new Error('Empty string');
}
if (name.indexOf('\u0000') !== -1) {
return new Error('Includes null character');
}
return null;
};
internals.Connection.prototype.get = function (key, callback) {
callback = Hoek.nextTick(callback);
if (!this.cache) {
return callback(new Error('Connection not started'));
}
const segment = this.cache[key.segment];
if (!segment) {
return callback(null, null);
}
const envelope = segment[key.id];
if (!envelope) {
return callback(null, null);
}
let value = null;
if (Buffer.isBuffer(envelope.item)) {
value = envelope.item;
}
else {
value = internals.parseJSON(envelope.item);
if (value instanceof Error) {
return callback(new Error('Bad value content'));
}
}
const result = {
item: value,
stored: envelope.stored,
ttl: envelope.ttl
};
return callback(null, result);
};
internals.Connection.prototype.set = function (key, value, ttl, callback) {
callback = Hoek.nextTick(callback);
if (!this.cache) {
return callback(new Error('Connection not started'));
}
if (ttl > 2147483647) { // Math.pow(2, 31)
return callback(new Error('Invalid ttl (greater than 2147483647)'));
}
let envelope = null;
try {
envelope = new internals.MemoryCacheEntry(key, value, ttl, this.settings.allowMixedContent);
}
catch (err) {
return callback(err);
}
this.cache[key.segment] = this.cache[key.segment] || new internals.MemoryCacheSegment();
const segment = this.cache[key.segment];
const cachedItem = segment[key.id];
if (cachedItem && cachedItem.timeoutId) {
clearTimeout(cachedItem.timeoutId);
this.byteSize -= cachedItem.byteSize; // If the item existed, decrement the byteSize as the value could be different
}
if (this.settings.maxByteSize) {
if (this.byteSize + envelope.byteSize > this.settings.maxByteSize) {
return callback(new Error('Cache size limit reached'));
}
}
const timeoutId = setTimeout(() => {
this.drop(key, () => {});
}, ttl);
envelope.timeoutId = timeoutId;
segment[key.id] = envelope;
this.byteSize += envelope.byteSize;
return callback(null);
};
internals.Connection.prototype.drop = function (key, callback) {
callback = Hoek.nextTick(callback);
if (!this.cache) {
return callback(new Error('Connection not started'));
}
const segment = this.cache[key.segment];
if (segment) {
const item = segment[key.id];
if (item) {
clearTimeout(item.timeoutId);
this.byteSize -= item.byteSize;
}
delete segment[key.id];
}
return callback();
};
internals.parseJSON = function (json) {
let obj = null;
try {
obj = JSON.parse(json);
}
catch (err) {
obj = err;
}
return obj;
};