pax_global_header 0000666 0000000 0000000 00000000064 15173213021 0014505 g ustar 00root root 0000000 0000000 52 comment=928a4ecb11eb50c502b0a6fe8672af9ab78cf490
pixl-cache-1.1.1/ 0000775 0000000 0000000 00000000000 15173213021 0013522 5 ustar 00root root 0000000 0000000 pixl-cache-1.1.1/.npmignore 0000664 0000000 0000000 00000000055 15173213021 0015521 0 ustar 00root root 0000000 0000000 .gitignore
node_modules/
benchmarks/results/
pixl-cache-1.1.1/README.md 0000664 0000000 0000000 00000035534 15173213021 0015013 0 ustar 00root root 0000000 0000000 Table of Contents
- [Overview](#overview)
* [Features](#features)
* [Benchmarks](#benchmarks)
- [Usage](#usage)
* [Overflow](#overflow)
* [Expiration](#expiration)
- [API](#api)
* [constructor](#constructor)
* [set](#set)
* [get](#get)
* [getMeta](#getmeta)
* [has](#has)
* [delete](#delete)
* [clear](#clear)
* [getStats](#getstats)
* [on](#on)
* [Events](#events)
+ [expire](#expire)
* [Properties](#properties)
+ [items](#items)
+ [count](#count)
+ [bytes](#bytes)
- [Development](#development)
* [Unit Tests](#unit-tests)
- [License](#license)
# Overview
**pixl-cache** is a very simple LRU (Least Recently Used) cache module for Node.js. It works like a hash map with `set()` and `get()` methods, but when it detects an overflow (configurable by total keys or total bytes) it automatically expunges the least recently accessed objects from the cache. It is fast, stable, and has no dependencies.
Internally the cache is implemented as a combination of a hash map and a double-linked list. When items are accessed (added, replaced or fetched) they are promoted to the front of the linked list. When the max size (keys or bytes) is exceeded, items are dropped from the back of the list. Items can also have an optional max age (i.e. expiration date).
## Features
- Simple and straightforward API
- Fast and stable
- Low memory overhead
- Predictable results on overflow
- Can expire based on key count or byte count
- Optional expiration date for items
- Event listener for ejecting expired items
- Can store custom metadata along with cache objects
- No dependencies
# Usage
Use [npm](https://www.npmjs.com/) to install the module:
```
npm install pixl-cache
```
Here is a simple usage example, enforcing 5 max items:
```js
const LRU = require('pixl-cache');
let cache = new LRU({ maxItems: 5 });
cache.set( 'key1', "Simple String" );
cache.set( 'key2', Buffer.alloc(10) );
cache.set( 'key3', { complex: { item: 1234 } } );
cache.set( 'key4', true );
cache.set( 'key5', "Cache is full now" );
cache.set( 'key6', "Oops, key1 is gone now");
let value = cache.get( 'key1' );
// value === undefined (key1 got expunged)
cache.delete( 'key2' ); // manual delete
if (cache.has('key3')) {
// check if key exists (true)
}
cache.clear(); // wipe entire cache
```
Instead of setting a maximum item count, you can set a total byte count:
```js
let cache = new LRU({ maxBytes: 1024 * 1024 }); // 1 MB
cache.set( 'key1', Buffer.alloc(1024 * 512) ); // 50% full here
cache.set( 'key2', Buffer.alloc(1024 * 256) ); // 75% full here
```
Or expire objects by their age in the cache:
```js
let cache = new LRU({ maxAge: 86400 }); // 24 hours
cache.set( 'key1', "Expires tomorrow!" );
```
## Overflow
pixl-cache can handle overflow in two different ways: by enforcing a maximum number of items, and/or a maximum number of bytes. When either of these limits are exceeded, the least recently used object(s) will be expunged. These limits can be passed to the class constructor like so:
```js
let cache = new LRU({
maxItems: 1000,
maxBytes: 1048576
});
```
This would allow up to 1,000 items or 1 MB of total value length, whichever is reached first. When using `maxBytes` the cache needs to calculate the size of your objects. The byte size includes both the key and the value. The process is automatic if you use simple primitive value types such as strings, buffers, numbers or booleans. Examples:
```js
cache.set( 'key1', "ABCDEFGHIJ" ); // 20 bytes + 8 for key
cache.set( 'key2', Buffer.alloc(10) ); // 10 bytes + 8 for key
cache.set( 'key3', 12345 ); // 8 bytes + 8 for key
cache.set( 'key4', true ) ; // 4 bytes + 8 for key
```
Note that string length does **not** equal byte length. This is because strings are internally represented as 16-bits (2 bytes) per character in Node.js, so they're basically double size. That is why the 10-character string `ABCDEFGHIJ` is actually 20 bytes in memory.
The byte length of *object* values is not automatically calculated. Meaning, if you pass an object as a value, its byte count is read from a `length` property if it exists (i.e. for buffers), or if not found it defaults to `0`. So, you may want to specify your own custom lengths in certain cases, especially if you are storing objects in the cache. To do this, pass a metadata object to `set()` as the 3rd argument, and include an explicit `length` property therein:
```js
cache.set( 'key1', { "name": "Joe" }, { length: 200 } );
```
This would record the length of the object value as 200 bytes.
## Expiration
In addition to expiring the least recently used objects when the cache is full, objects can also expire based on age. You can specify a `maxAge` configuration property when constructing your cache:
```js
let cache = new LRU({ maxAge: 86400 }); // 24 hours
```
And in this case all items added to the cache will be expired after 24 hours. Replacing existing items resets their age clock. Each item has its own internal expiration date, and if that date comes to pass, fetching the item will immediately cause it to be deleted, and return `undefined`. Note that items are not "actively deleted" based on an interval timer, but rather expired items delete themselves on fetch (or may be expunged for other reasons, i.e. `maxItems` and/or `maxBytes`).
You can specify a custom expiration date for your items individually, by passing a metadata object to `set()` as the 3rd argument, and including an explicit `expires` property therein:
```js
let someFutureDate = ( Date.now() / 1000 ) + 86400; // 24 hours from now
cache.set( 'key1', "ABCDEFGHIJ", { expires: someFutureDate } );
```
The `expires` property needs to be an Epoch timestamp, as shown above. You do not need to enable `maxAge` in order to use the custom `expires` property. It works with any combination of configuration options, and overrides `maxAge` if it is also set. If you set an `expires` date in the past, the key is immediately deleted upon the next fetch.
# API
## constructor
```js
const LRU = require('pixl-cache');
let cache = new LRU();
```
The constructor creates a cache instance. You can optionally pass an object containing any of these properties:
| Property | Default | Description |
|----------|---------|-------------|
| `maxItems` | `0` | Specifies the maximum number of objects allowed in the cache, before it starts expunging the least recently used. |
| `maxBytes` | `0` | Specifies the maximum value bytes allowed in the cache, before it starts expunging the least recently used. |
| `maxAge` | `0` | Specifies the default maximum age in seconds for all keys added to the cache, before they are deleted on fetch. |
The default of `0` means infinite. If multiple configuration properties are specified, whichever one kicks in first will expire keys. Here is an example with all properties set:
```js
let cache = new LRU({
maxItems: 1000,
maxBytes: 1048576, // 1 MB
maxAge: 86400 // 24 hours
});
```
## set
```js
cache.set( 'key1', "Value 1" );
```
The `set()` method adds or replaces a single object in the cache, given a key and value. The key is assumed to be a string, but the value can be anything. Anytime an object is added or replaced, it becomes the most recently used.
Adding new keys may cause the least recently used object(s) to be expired from the cache.
You can optionally pass an object containing arbitrary metadata as the 3rd argument to `set()`. This is stored in the internal cache database, and does not add to the total byte count. Example:
```js
cache.set( 'key1', "Value 1", { mytag: "frog" } );
```
Please make sure your metadata object does *not* include the following four keys: `key`, `value`, `next` or `prev`. Those are for internal cache use. You can, however, include a `length` key in the metadata object, which overrides the default length calculation for the object, and/or an `expires` key, which sets the expiration date for the key (Epoch timestamp).
Subsequent calls to `set()` replacing a key with differing metadata is shallow-merged. If a subsequent call omits metadata entirely, the original data is preserved.
You can use [getMeta()](#getmeta) to retrieve cache objects including your metadata.
Note that if you want to store either `null` or `undefined` as cache values, you *must* specify a non-zero `length` in the metadata object. Otherwise pixl-cache will throw an exception trying to compute the length.
## get
```js
let value = cache.get( 'key1' );
```
The `get()` method fetches a value given a key. If the key is not found in the cache (or it exists but is expired) the return value will be `undefined`. Note that when fetching any key, that object becomes the most recently used.
If the item is fetched on or after its expiration date (i.e. when using `maxAge` or setting an explicit date), it will be deleted, and the call will return `undefined`.
## getMeta
```js
let item = cache.getMeta( 'key1' );
let mytag = item.mytag; // custom metadata
```
The `getMeta()` method fetches the internal cache object wrapper given its key, which includes any metadata you may have specified when you called `set()`. The object will always have the following properties, along with your metadata merged in:
| Key | Description |
|-----|-------------|
| `key` | The raw cache object key, as passed to `set()`. |
| `value` | The raw cache object value, as passed to `set()` and returned by `get()`. |
| `next` | A pointer to the *next* cache object in the linked list. Please do not touch. |
| `prev` | A pointer to the *previous* cache object in the linked list. Please do not touch. |
The metadata object may also have one or both of these additional properties:
| Key | Description |
|-----|-------------|
| `length` | The length of the object's `value`, if it was provided explicitly when `set()` was called, or calculated automatically. |
| `expires` | The expiration date of the object as an Epoch timestamp, if the `maxAge` configuration property is set, or a custom `expires` was provided when `set()` was called. |
## has
```js
if (cache.has('key1')) console.log("key1 is present!");
```
The `has()` method checks if a specified key exists in the cache, and returns `true` or `false`. This does **not** cause the object to get promoted to the most recently used. It is more of a "peek".
Note that if a cache object is technically still present but expired, the return value of this call will be `false`. This is because the item is "effectively" deleted at this point (i.e. it can no longer be fetched), so we properly represent what would happen if you tried to call `get()` after calling `has()`.
## delete
```js
cache.delete( 'key1' );
```
The `delete()` method deletes the specified key from the cache. If successful, this returns `true`. If the key was not found it returns `false`. This will return `true` (success) for deleting expired keys.
## clear
```js
cache.clear();
```
The `clear()` method clears the **entire** cache, deleting all objects. It does not reset any configuration properties, however.
## getStats
```js
let stats = cache.getStats();
```
The `getStats()` method returns an object containing some basic statistics about the cache, including the current total keys, total bytes, fullness percentage estimate, and the 10 hottest (most used) keys. Example response, formatted as JSON:
```json
{
"count": 158,
"bytes": 15098,
"full": "15.8%",
"hotKeys": [
"index/myapp/data/2665",
"index/myapp/data/2664",
"index/myapp/data/2663",
"index/myapp/data/2662",
"index/myapp/data/2661",
"index/myapp/data/2660",
"index/myapp/data/2659",
"index/myapp/data/2658",
"index/myapp/data/2657",
"index/myapp/data/2656"
]
}
```
## on
```js
cache.on( 'expire', function(item, reason) {
console.log(`Cache expired ${item.key} because of ${reason}.`);
});
```
The pixl-cache class inherits from Node's [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter), so it has all those methods available including [on()](https://nodejs.org/api/events.html#events_emitter_on_eventname_listener) and [off()](https://nodejs.org/api/events.html#events_emitter_off_eventname_listener).
## Events
The following events are emitted:
### expire
The `expire` event is emitted when the cache is about to expire the least recently used object, either due to total key count, byte count or age. The event listener will be passed two arguments, the item being expired, and a reason:
```js
cache.on( 'expire', function(item, reason) {
console.log(`Cache expired ${item.key} because of ${reason}.`);
});
```
The `item` is an object containing the original `key` and `value` as originally passed to `set()`, along with any custom metadata if applicable. It is the same object that [getMeta()](#getmeta) returns. You can use the `expire` hook to persist data to disk, for example.
The `reason` will be either `count` (expired due to total key count), `bytes` (expired due to total byte count), or `age` (expired due to age). It is always a string.
## Properties
The following properties are available on pixl-cache class instances:
### items
The `items` property is a hash containing all the objects currently in the cache, keyed by their actual keys. Please do not directly manipulate this object, as it will become out of sync with the internal linked list. However, you are free to read from it.
### count
The `count` property contains the current total key count in the cache.
### bytes
The `bytes` property contains the current total byte count in the cache.
# Development
To install pixl-cache for development, run these commands:
```
git clone https://github.com/jhuckaby/pixl-cache.git
cd pixl-cache
npm install
```
The `npm install` is only needed for running unit tests.
## Unit Tests
When installing locally for development, [pixl-unit](https://github.com/jhuckaby/pixl-unit) will be installed as a dev dependency. Then, to run the unit tests, issue this command:
```
npm test
```
# License
**The MIT License (MIT)**
*Copyright (c) 2019 - 2025 Joseph Huckaby.*
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
pixl-cache-1.1.1/cache.js 0000664 0000000 0000000 00000011232 15173213021 0015122 0 ustar 00root root 0000000 0000000 // Simple LRU Cache for Node.js
// Uses a combination of a hash map and a double-linked list.
// To use maxBytes, your values are expected to have a `length` property.
// Copyright (c) 2019 - 2025 Joseph Huckaby. MIT License.
var EventEmitter = require("events").EventEmitter;
class Cache extends EventEmitter {
constructor(opts) {
// class constructor
// opts: { maxItems, maxBytes, maxAge }
super();
// defaults
this.maxItems = 0;
this.maxBytes = 0;
this.maxAge = 0;
// user overrides
if (opts) {
for (var key in opts) this[key] = opts[key];
}
this.clear();
}
clear() {
// empty the cache
this.items = new Map();
this.first = null;
this.last = null;
// stats
this.count = 0;
this.bytes = 0;
}
set(key, value, meta) {
// set new key or replace existing
// either way, move item to head of list
// run maintenance after
var item = this.items.get(key);
if (!meta) meta = {};
if (!("length" in meta)) {
switch (typeof(value)) {
case 'number': meta.length = 8; break; // numbers are 64-bit
case 'boolean': meta.length = 4; break; // bools are 32-bit
case 'string': meta.length = value.length * 2; break; // strings are 16-bit per char
}
}
if (item) {
// replace existing
this.bytes -= item.length || item.value.length || 0;
item.value = value;
this.bytes += meta.length || value.length || 0;
}
else {
// add new
item = {
key: key,
value: value,
prev: null,
next: null
};
this.items.set(key, item);
this.bytes += (key.length * 2) + (meta.length || value.length || 0);
this.count++;
}
// set expiration if maxAge is set
if (this.maxAge) item.expires = (Date.now() / 1000) + this.maxAge;
// import optional metadata
for (var mkey in meta) item[mkey] = meta[mkey];
// promote to front of list
this.promote(item);
// maintenance
if (this.maxItems && (this.count > this.maxItems)) {
this.emit( 'expire', this.last, 'count' );
this.delete( this.last.key );
}
if (this.maxBytes) {
while (this.bytes > this.maxBytes) {
this.emit( 'expire', this.last, 'bytes' );
this.delete( this.last.key );
}
}
}
get(key) {
// fetch key and return value
// move object to head of list
var item = this.items.get(key);
if (!item) return undefined;
if (item.expires && (Date.now() / 1000 >= item.expires)) {
this.emit( 'expire', item, 'age' );
this.delete( key );
return undefined;
}
this.promote(item);
return item.value;
}
getMeta(key) {
// fetch key and return internal cache wrapper object
// will contain any metadata user added when key was set
// (this still moves object to front of list)
var item = this.items.get(key);
if (!item) return undefined;
if (item.expires && (Date.now() / 1000 >= item.expires)) {
this.emit( 'expire', item, 'age' );
this.delete( key );
return undefined;
}
this.promote(item);
return item;
}
delete(key) {
// remove key from cache
var item = this.items.get(key);
if (!item) return false;
this.bytes -= (key.length * 2) + (item.length || item.value.length || 0);
this.count--;
this.items.delete(key);
// adjust linked list
if (item.prev) item.prev.next = item.next;
if (item.next) item.next.prev = item.prev;
if (item === this.first) this.first = item.next;
if (item === this.last) this.last = item.prev;
return true;
}
has(key) {
// return true if key is present in cache
// (do not change order)
var item = this.items.get(key);
if (!item) return false;
if (item.expires && (Date.now() / 1000 >= item.expires)) {
return false;
}
return true;
}
promote(item) {
// promote item to head of list
// (accepts new item or existing item)
if (item !== this.first) {
if (item.prev) item.prev.next = item.next;
if (item.next) item.next.prev = item.prev;
if (item === this.last) this.last = item.prev;
// install as new head
item.prev = null;
item.next = this.first;
if (this.first) this.first.prev = item;
this.first = item;
if (!this.last) this.last = item;
}
}
getStats() {
// return general stats for cache
var stats = {
count: this.count,
bytes: this.bytes
};
// guess rough percentage of cache fullness
var pct_count = this.maxItems ? ((this.count / this.maxItems) * 100) : 0;
var pct_bytes = this.maxBytes ? ((this.bytes / this.maxBytes) * 100) : 0;
stats.full = '' + Math.floor( Math.max(pct_count, pct_bytes) ) + '%';
// include 10 hottest keys
stats.hotKeys = [];
var item = this.first;
while (item && (stats.hotKeys.length < 10)) {
stats.hotKeys.push( item.key );
item = item.next;
}
return stats;
}
}
module.exports = Cache;
pixl-cache-1.1.1/package.json 0000664 0000000 0000000 00000001111 15173213021 0016002 0 ustar 00root root 0000000 0000000 {
"name": "pixl-cache",
"version": "1.1.1",
"description": "A simple LRU cache module.",
"author": "Joseph Huckaby ",
"homepage": "https://github.com/jhuckaby/pixl-cache",
"license": "MIT",
"main": "cache.js",
"scripts": {
"test": "pixl-unit test.js"
},
"repository": {
"type": "git",
"url": "https://github.com/jhuckaby/pixl-cache"
},
"bugs": {
"url": "https://github.com/jhuckaby/pixl-cache/issues"
},
"keywords": [
"cache",
"lru"
],
"dependencies": {},
"devDependencies": {
"pixl-unit": "^2.0.1"
}
}
pixl-cache-1.1.1/test.js 0000664 0000000 0000000 00000037076 15173213021 0015054 0 ustar 00root root 0000000 0000000 // Unit tests for pixl-cache
var Cache = require('./cache.js');
exports.tests = [
function basic(test) {
var cache = new Cache();
cache.set( 'key1', 'value1' );
var value = cache.get('key1');
test.ok( value === "value1", "Value for key1 is not correct: " + value );
test.ok( cache.count == 1, "Cache has incorrect number of keys: " + cache.count );
test.ok( cache.bytes == 20, "Cache has incorrect number of bytes: " + cache.bytes );
cache.set( 'key1', 'value12345' );
value = cache.get('key1');
test.ok( value === "value12345", "Value for key1 is not correct after replace: " + value );
test.ok( cache.count == 1, "Cache has incorrect number of keys after replace: " + cache.count );
test.ok( cache.bytes == 28, "Cache has incorrect number of bytes after replace: " + cache.bytes );
cache.delete( 'key1' );
value = cache.get('key1');
test.ok( !value, "Value for key1 is not correct after delete: " + value );
test.ok( cache.count == 0, "Cache has incorrect number of keys after delete: " + cache.count );
test.ok( cache.bytes == 0, "Cache has incorrect number of bytes after delete: " + cache.bytes );
cache.set( 'key1', 'value12345' );
cache.clear();
value = cache.get('key1');
test.ok( !value, "Value for key1 is not correct after clear: " + value );
test.ok( cache.count == 0, "Cache has incorrect number of keys after clear: " + cache.count );
test.ok( cache.bytes == 0, "Cache has incorrect number of bytes after clear: " + cache.bytes );
test.done();
},
function advanced(test) {
// try non-string values
var value;
var cache = new Cache();
cache.set( 'key1', 12345 );
value = cache.get('key1');
test.ok( value === 12345, "Number value is incorrect: " + value );
cache.set( 'key1z', 0 );
value = cache.get('key1z');
test.ok( value === 0, "Number value is incorrect: " + value );
cache.set( 'key2', true );
value = cache.get('key2');
test.ok( value === true, "Boolean value is incorrect: " + value );
cache.set( 'key2f', false );
value = cache.get('key2f');
test.ok( value === false, "Boolean value is incorrect: " + value );
cache.set( 'key3', Buffer.alloc(10) );
value = cache.get('key3');
test.ok( !!value.fill, "Value is not a buffer: " + value );
test.ok( value.length == 10, "Buffer length is incorrect: " + value.length );
// must pass length metadata for null value
cache.set( 'key4', null, { length: 1 } );
value = cache.get('key4');
test.ok( value === null, "Null value is incorrect: " + value );
// must pass length metadata for undefined value
cache.set( 'key4u', undefined, { length: 1 } );
value = cache.get('key4u');
test.ok( value === undefined, "Undefined value is incorrect: " + value );
cache.set( 'key5', function() {} );
value = cache.get('key5');
test.ok( typeof(value) == 'function', "Function value is incorrect: " + value );
cache.set( 'key6', { foo: 'bar' } );
value = cache.get('key6');
test.ok( typeof(value) == 'object', "Object value is incorrect: " + value );
test.done();
},
function sizes(test) {
// test javascript native size calculation
var cache = new Cache();
// test key = 2 bytes
// number (8 bytes)
cache.set( 'A', 1 );
test.ok( cache.bytes == 10, "Cache has incorrect bytes for num: " + cache.bytes );
cache.clear();
// boolean (4 bytes)
cache.set( 'A', true );
test.ok( cache.bytes == 6, "Cache has incorrect bytes for bool: " + cache.bytes );
cache.clear();
// string (2 bytes per char)
cache.set( 'A', "1" );
test.ok( cache.bytes == 4, "Cache has incorrect bytes for string: " + cache.bytes );
cache.clear();
// buffer
cache.set( 'A', Buffer.alloc(100) );
test.ok( cache.bytes == 102, "Cache has incorrect bytes for buffer: " + cache.bytes );
cache.clear();
// object (0 bytes unless specified)
cache.set( 'A', { "not": "counted" } );
test.ok( cache.bytes == 2, "Cache has incorrect bytes for object: " + cache.bytes );
cache.clear();
test.done();
},
function metadata(test) {
// store object with metadata
var item;
var cache = new Cache();
cache.set( 'key1', 'value1', { joe: 12345 } );
item = cache.getMeta('key1');
test.ok( !!item, "Failed to fetch meta for key1" );
test.ok( item.key === "key1", "Incorrect key for meta fetch: " + item.key );
test.ok( item.joe === 12345, "Missing metadata in object" );
// update without metadata, should preserve
cache.set( 'key1', 'value1' );
item = cache.getMeta('key1');
test.ok( !!item, "Failed to fetch meta for key1" );
test.ok( item.key === "key1", "Incorrect key for meta fetch: " + item.key );
test.ok( item.joe === 12345, "Missing metadata in object" );
// update metadata
cache.set( 'key1', 'value2', { joe: 12346 } );
item = cache.getMeta('key1');
test.ok( !!item, "Failed to fetch meta for key1 after update" );
test.ok( item.key === "key1", "Incorrect key for meta fetch after update: " + item.key );
test.ok( item.joe === 12346, "Missing metadata in object after update" );
test.ok( cache.bytes == 20, "Incorrect total bytes: " + cache.bytes );
// add key with custom length in metadata
cache.set( 'key2', 'value3', { length: 1000 } );
test.ok( cache.bytes == 1028, "Incorrect total bytes after custom metadata length: " + cache.bytes );
test.done();
},
function fillItems(test) {
var idx, key, value, item;
var cache = new Cache({ maxItems: 10 });
cache.on('expire', function(item, reason) {
test.ok( false, "Expire event fired unexpectedly: " + item.key + " for " + reason );
});
for (idx = 1; idx <= 10; idx++) {
cache.set( 'key' + idx, 'value' + idx );
}
for (idx = 1; idx <= 10; idx++) {
value = cache.get( 'key' + idx );
test.ok( value === 'value' + idx, "Cache key" + idx + " has incorrect value: " + value );
}
test.ok( cache.count == 10, "Cache has incorrect count: " + cache.count );
// walk the linked list forwards (internal API)
item = cache.first;
for (idx = 10; idx >= 1; idx--) {
key = 'key' + idx;
value = 'value' + idx;
test.ok( !!item, "Item is falsey at idx " + idx );
test.ok( item.key === key, "Item key is incorrect: " + item.key + " != " + key );
test.ok( item.value === value, "Item value is incorrect: " + item.value + " != " + value );
item = item.next;
}
test.ok( !item, "Item is not false at end of list" );
// walk the linked list backwards (internal API)
item = cache.last;
for (idx = 1; idx <= 10; idx++) {
key = 'key' + idx;
value = 'value' + idx;
test.ok( !!item, "Item is falsey at idx " + idx );
test.ok( item.key === key, "Item key is incorrect: " + item.key + " != " + key );
test.ok( item.value === value, "Item value is incorrect: " + item.value + " != " + value );
item = item.prev;
}
test.ok( !item, "Item is not false at start of list" );
test.done();
},
function overflowItems(test) {
var idx, key, value, item;
var cache = new Cache({ maxItems: 10 });
test.expect( 13 );
cache.on('expire', function(item, reason) {
test.ok( item.key == "key1", "Expired key is incorrect: " + item.key );
test.ok( reason == "count", "Expired reason is incorrect: " + reason );
});
for (idx = 1; idx <= 11; idx++) {
cache.set( 'key' + idx, 'value' + idx );
}
for (idx = 2; idx <= 11; idx++) {
value = cache.get( 'key' + idx );
test.ok( value === 'value' + idx, "Cache key key" + idx + " has incorrect value: " + value );
}
value = cache.get( 'key1' );
test.ok( !value, "Expected null, got actual value for key1: " + value );
test.done();
},
function fillBytes(test) {
var idx, key, value, item;
var cache = new Cache({ maxBytes: 282 });
cache.on('expire', function(item, reason) {
test.ok( false, "Expire event fired unexpectedly: " + item.key + " for " + reason );
});
for (idx = 1; idx <= 10; idx++) {
cache.set( 'key' + idx, 'ABCDEFGHIJ' );
}
for (idx = 1; idx <= 10; idx++) {
value = cache.get( 'key' + idx );
test.ok( value === 'ABCDEFGHIJ', "Cache key" + idx + " has incorrect value: " + value );
}
test.ok( cache.count == 10, "Cache has incorrect count: " + cache.count );
test.ok( cache.bytes == 282, "Cache has incorrect bytes: " + cache.bytes );
test.done();
},
function overflowBytes(test) {
var idx, key, value, item;
var cache = new Cache({ maxBytes: 300 });
test.expect( 13 );
cache.on('expire', function(item, reason) {
test.ok( item.key == "key11", "Expired key is incorrect: " + item.key );
test.ok( reason == "bytes", "Expired reason is incorrect: " + reason );
});
for (idx = 11; idx <= 21; idx++) {
cache.set( 'key' + idx, 'ABCDEFGHIJ' );
}
for (idx = 12; idx <= 21; idx++) {
value = cache.get( 'key' + idx );
test.ok( value === 'ABCDEFGHIJ', "Cache key key" + idx + " has incorrect value: " + value );
}
value = cache.get( 'key11' );
test.ok( !value, "Expected null, got actual value for key11: " + value );
test.done();
},
function overflowBytesMultiple(test) {
var idx, key, value, item;
var cache = new Cache({ maxBytes: 300 });
for (idx = 11; idx <= 20; idx++) {
cache.set( 'key' + idx, 'ABCDEFGHIJ' );
}
for (idx = 11; idx <= 20; idx++) {
value = cache.get( 'key' + idx );
test.ok( value === 'ABCDEFGHIJ', "Cache key key" + idx + " has incorrect value: " + value );
}
// cause everything to be expunged at once and replaced with boom
// (292 byte buf + `boom` key == 300 bytes exactly)
var buf = Buffer.alloc(292);
cache.set( 'boom', buf );
value = cache.get('boom');
test.ok( !!value, "Unable to fetch boom");
test.ok( value.length == 292, "Boom has incorrect length: " + value.length );
test.ok( cache.count == 1, "Cache has incorrect count after boom: " + cache.count );
test.ok( cache.bytes == 300, "Cache has incorrect bytes after boom: " + cache.bytes );
// internal API checks
test.ok( cache.first.key === "boom", "First list item is not boom: " + cache.first.key );
test.ok( cache.last.key === "boom", "First last item is not boom: " + cache.last.key );
// now cause an implosion (cannot store > 300 bytes, will immediately be expunged)
// (287 byte buf + `implode` key == 301 bytes exactly)
var buf2 = Buffer.alloc(287);
cache.set( 'implode', buf2 );
value = cache.get('implode');
test.ok( !value, "Oops, implode should not be fetchable! But it was!");
test.ok( cache.count == 0, "Cache has incorrect count after implosion: " + cache.count );
test.ok( cache.bytes == 0, "Cache has incorrect bytes after implosion: " + cache.bytes );
test.done();
},
function promote(test) {
var idx, key, value, item;
var cache = new Cache();
for (idx = 1; idx <= 10; idx++) {
cache.set( 'key' + idx, 'value' + idx );
}
// head list item should be last key inserted (internal API)
test.ok( cache.first.key === "key10", "First item in list has incorrect key: " + cache.first.key );
test.ok( cache.first.next.key === "key9", "Second item in list has incorrect key: " + cache.first.next.key );
// promoting head key should have no effect
cache.get( "key10" );
test.ok( cache.first.key === "key10", "First item in list has incorrect key: " + cache.first.key );
test.ok( cache.first.next.key === "key9", "Second item in list has incorrect key: " + cache.first.next.key );
// promote key5 to head
cache.get( "key5" );
test.ok( cache.first.key === "key5", "First item in list has incorrect key after promotion: " + cache.first.key );
test.ok( cache.first.next.key === "key10", "Second item in list has incorrect key after promotion: " + cache.first.next.key );
test.ok( cache.first.next.next.key === "key9", "Third item in list has incorrect key after promotion: " + cache.first.next.next.key );
test.ok( cache.first.next.prev.key === "key5", "Reverse linking is incorrect for promoted head key5" );
// make sure end of list is expected
test.ok( cache.last.key === "key1", "Last item in list is unexpected: " + cache.last.key );
// promote last key to head
cache.get( "key1" );
test.ok( cache.first.key === "key1", "First item in list has incorrect key after promotion: " + cache.first.key );
test.ok( cache.first.next.key === "key5", "Second item in list has incorrect key after promotion: " + cache.first.next.key );
test.ok( cache.first.next.next.key === "key10", "Third item in list has incorrect key after promotion: " + cache.first.next.next.key );
test.ok( cache.first.next.prev.key === "key1", "Reverse linking is incorrect for promoted head key5" );
// make sure end of list is expected
test.ok( cache.last.key === "key2", "Last item in list is unexpected: " + cache.last.key );
test.done();
},
function keepAlive(test) {
var idx, idy, key, value, item;
var cache = new Cache({ maxItems: 10 });
cache.on('expire', function(item, reason) {
if (!item.key.match(/^random_/)) test.ok( false, "Expired key is incorrect: " + item.key );
if (reason != "count") test.ok( false, "Expired reason is incorrect: " + reason );
});
cache.set( 'special', "SPECIAL" );
// flood cache with totally random keys, but make sure to fetch special key between batches
for (idx = 0; idx < 100; idx++) {
for (var idy = 0; idy < 9; idy++) {
cache.set( 'random_' + idx + '_' + idy + '_' + Math.random(), 'RANDOM' + Math.random() );
}
cache.get( 'special' );
}
value = cache.get( 'special' );
test.ok( value === "SPECIAL", "Special key has disappeared unexpectedly: " + value );
// flood cache with semi-random keys (will replace each other)
for (idx = 0; idx < 100; idx++) {
for (var idy = 0; idy < 9; idy++) {
cache.set( 'random_' + Math.floor(Math.random() * 9), 'RANDOM' + Math.random() );
}
cache.get( 'special' );
}
value = cache.get( 'special' );
test.ok( value === "SPECIAL", "Special key has disappeared unexpectedly: " + value );
// remove expire listener
cache.removeAllListeners( 'expire' );
// flood cache with too many keys per match, should expunge special key
for (idx = 0; idx < 100; idx++) {
for (var idy = 0; idy < 10; idy++) {
cache.set( 'random_' + idx + '_' + idy + '_' + Math.random(), 'RANDOM' + Math.random() );
}
cache.get( 'special' );
}
value = cache.get( 'special' );
test.ok( !value, "Special key shuld be expunged, but is still here: " + value );
test.done();
},
function age(test) {
var idx, key, value, item, expires;
var cache = new Cache({ maxAge: 1 });
cache.set( 'aged', "AGED" );
value = cache.get( 'aged' );
test.ok( value === "AGED", "Aged key has disappeared unexpectedly: " + value );
item = cache.getMeta( 'aged' );
test.ok( !!item, "Aged key has disappeared unexpectedly: " + value );
test.ok( item.expires > Date.now()/1000, "Aged key has unexpected expiration: " + expires );
expires = item.expires;
value = null;
// make sure expires event fires for age ejection
var saw_expire_event = false;
cache.on('expire', function(item, reason) {
saw_expire_event = true;
test.ok( item.key === 'aged', "Unexpected key in expire event: " + item.key );
test.ok( reason === 'age', "Unexpected reason for expire event: " + reason );
});
// fetch every 100ms until item expires
var timer = setInterval( function() {
value = cache.get( 'aged' );
if (item.expires <= Date.now()/1000) {
// TTL expired, item should be gone now
clearTimeout( timer );
test.ok( value === undefined, "Aged value expected to be undefined by now: " + value );
test.ok( saw_expire_event === true, "Did not see expire event after full second" );
test.done();
}
else {
// TTL still fresh
test.ok( value === "AGED", "Aged key has disappeared unexpectedly: " + value );
}
}, 100 );
}
];