Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promises support #161

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
88 changes: 86 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Note that any implementation of client strategies must return deep copies of the
from a `get()` is owned by the called and can be safely modified without affecting the cache copy.


#### API
#### API (callback)

The `Client` object provides the following methods:

Expand All @@ -70,6 +70,44 @@ The `Client` object provides the following methods:
- `isReady()` - returns `true` if cache engine determines itself as ready, `false` if it is not ready.


Any method with a `key` argument takes an object with the following required properties:
- `segment` - a caching segment name string. Enables using a single cache server for storing different sets of items with overlapping ids.
- `id` - a unique item identifier string (per segment). Can be an empty string.

#### API (Promise)

The `Client` object provides the following methods:

- `start()` - creates a connection to the cache server. Must be called before any other method is available.
Returns a Promise , you can catch any errors with

```js
client.start().then(() => {
console.log('catbox client started ok');
}).catch((err) => {
console.error('catbox client error', err);
});
```

- `stop()` - terminates the connection to the cache server.
- `get(key)` - retrieve an item from the cache engine if found where:
- `key` - a cache key object (see below).
- Returns a Promise, If the item is not found, the `cached` in `client.get(key).then((cached)=>{})` will be `null`.
If found, the `cached` object contains the following:
- `item` - the value stored in the cache using `set()`.
- `stored` - the timestamp when the item was stored in the cache (in milliseconds).
- `ttl` - the remaining time-to-live (not the original value used when storing the object).
- `set(key, value, ttl)` - store an item in the cache for a specified length of time, where:
- `key` - a cache key object (see below).
- `value` - the string or object value to be stored.
- `ttl` - a time-to-live value in milliseconds after which the item is automatically removed from the cache (or is marked invalid).
- Returns a Promise , if any error is thrown you can `.catch((err)=>{})` it.
- `drop(key)` - remove an item from cache where:
- `key` - a cache key object (see below).
- Returns a Promise, with either `.drop(key).catch((err)=>{}).then((message)=>{})`.
- `isReady()` - returns `true` if cache engine determines itself as ready, `false` if it is not ready.


Any method with a `key` argument takes an object with the following required properties:
- `segment` - a caching segment name string. Enables using a single cache server for storing different sets of items with overlapping ids.
- `id` - a unique item identifier string (per segment). Can be an empty string.
Expand Down Expand Up @@ -113,7 +151,7 @@ The object is constructed using `new Policy(options, [cache, segment])` where:
- `segment` - required when `cache` is provided. The segment name used to isolate cached items within the cache partition.


#### API
#### API (callback)

The `Policy` object provides the following methods:

Expand Down Expand Up @@ -154,3 +192,49 @@ The `Policy` object provides the following methods:
- `stales` - number of cache reads with stale requests (only counts the first request in a queued `get()` operation).
- `generates` - number of calls to the generate function.
- `errors` - cache operations errors.

#### API (Promise)

The `Policy` object provides the following methods:

- `get(id, options)` - retrieve an item from the cache. If the item is not found and the `generateFunc` method was provided, a new value
is generated, stored in the cache, and returned. Multiple concurrent requests are queued and processed once. The method arguments are:
- `id` - the unique item identifier (within the policy segment). Can be a string or an object with the required 'id' key.
- `options` - is an optional object that affects the returned result, has the following keys:
- `full` - default is `false`
- Returns a Promise when that resolves :
- when `options.full` is `false` - returns the fetched or generated value, does not trigger error if a generateFunc throws but a valid stale value still exists.
- when `options.full` is `true` - returns/throws an object with the following keys:
- `err` - any errors encountered. (if this is present, it will `reject` (you have to use `.catch`) instead of `resolve` (`.then`))
- `value` - the fetched or generated value.
- `cached` - `null` if a valid item was not found in the cache, or an object with the following keys:
- `item` - the cached `value`.
- `stored` - the timestamp when the item was stored in the cache.
- `ttl` - the cache ttl value for the record.
- `isStale` - `true` if the item is stale.
- `report` - an object with logging information about the generation operation containing the following keys (as relevant):
- `msec` - the cache lookup time in milliseconds.
- `stored` - the timestamp when the item was stored in the cache.
- `isStale` - `true` if the item is stale.
- `ttl` - the cache ttl value for the record.
- `error` - lookup error.
- `set(id, value, ttl)` - store an item in the cache where:
- `id` - the unique item identifier (within the policy segment).
- `value` - the string or object value to be stored.
- `ttl` - a time-to-live **override** value in milliseconds after which the item is automatically removed from the cache (or is marked invalid).
This should be set to `0` in order to use the caching rules configured when creating the `Policy` object.
- Returns a Promise that can resolve or reject with `err` (`.catch((err)=>{})`).
- `drop(id)` - remove the item from cache where:
- `id` - the unique item identifier (within the policy segment).
- Returns a Promise that can resolve or reject with `err` (`.catch((err)=>{})`).
- `ttl(created)` - given a `created` timestamp in milliseconds, returns the time-to-live left based on the configured rules.
- `rules(options)` - changes the policy rules after construction (note that items already stored will not be affected) where:
- `options` - the same `options` as the `Policy` constructor.
- `isReady()` - returns `true` if cache engine determines itself as ready, `false` if it is not ready or if there is no cache engine set.
- `stats` - an object with cache statistics where:
- `sets` - number of cache writes.
- `gets` - number of cache `get()` requests.
- `hits` - number of cache `get()` requests in which the requested id was found in the cache (can be stale).
- `stales` - number of cache reads with stale requests (only counts the first request in a queued `get()` operation).
- `generates` - number of calls to the generate function.
- `errors` - cache operations errors.
145 changes: 92 additions & 53 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

const Hoek = require('hoek');
const Boom = require('boom');
const Deferred = require('./deferred');


// Declare internals
Expand Down Expand Up @@ -38,7 +39,17 @@ internals.Client.prototype.stop = function () {

internals.Client.prototype.start = function (callback) {

let promise;
if (!callback) {
promise = new Deferred();
callback = promise.callback();
}

this.connection.start(callback);

if (promise) {
return promise.promise;
}
};


Expand All @@ -56,88 +67,116 @@ internals.Client.prototype.validateSegmentName = function (name) {

internals.Client.prototype.get = function (key, callback) {

let promise;
if (!callback) {
promise = new Deferred();
callback = promise.callback();
}

if (!this.connection.isReady()) {
// Disconnected
return callback(Boom.internal('Disconnected'));
callback(Boom.internal('Disconnected'));
}

if (!key) {
else if (!key) {
// Not found on null
return callback(null, null);
callback(null, null);
}

if (!internals.validateKey(key)) {
return callback(Boom.internal('Invalid key'));
else if (!internals.validateKey(key)) {
callback(Boom.internal('Invalid key'));
}
else {
this.connection.get(key, (err, result) => {

if (err) {
// Connection error
return callback(err);
}

if (!result ||
result.item === undefined ||
result.item === null) {

// Not found
return callback(null, null);
}

const now = Date.now();
const expires = result.stored + result.ttl;
const ttl = expires - now;
if (ttl <= 0) {
// Expired
return callback(null, null);
}

// Valid

const cached = {
item: result.item,
stored: result.stored,
ttl: ttl
};

return callback(null, cached);
});
}

this.connection.get(key, (err, result) => {

if (err) {
// Connection error
return callback(err);
}

if (!result ||
result.item === undefined ||
result.item === null) {

// Not found
return callback(null, null);
}

const now = Date.now();
const expires = result.stored + result.ttl;
const ttl = expires - now;
if (ttl <= 0) {
// Expired
return callback(null, null);
}

// Valid

const cached = {
item: result.item,
stored: result.stored,
ttl: ttl
};

return callback(null, cached);
});
if (promise) {
return promise.promise;
}
};


internals.Client.prototype.set = function (key, value, ttl, callback) {

let promise;
if (!callback) {
promise = new Deferred();
callback = promise.callback();
}

if (!this.connection.isReady()) {
// Disconnected
return callback(Boom.internal('Disconnected'));
callback(Boom.internal('Disconnected'));
}

if (!internals.validateKey(key)) {
return callback(Boom.internal('Invalid key'));
else if (!internals.validateKey(key)) {
callback(Boom.internal('Invalid key'));
}

if (ttl <= 0) {
else if (ttl <= 0) {
// Not cachable (or bad rules)
return callback();
callback();
}
else {
this.connection.set(key, value, ttl, callback);
}

this.connection.set(key, value, ttl, callback);
if (promise) {
return promise.promise;
}
};


internals.Client.prototype.drop = function (key, callback) {

let promise;
if (!callback) {
promise = new Deferred();
callback = promise.callback();
}

if (!this.connection.isReady()) {
// Disconnected
return callback(Boom.internal('Disconnected'));
callback(Boom.internal('Disconnected'));
}

if (!internals.validateKey(key)) {
return callback(Boom.internal('Invalid key'));
else if (!internals.validateKey(key)) {
callback(Boom.internal('Invalid key'));
}
else {
this.connection.drop(key, callback); // Always drop, regardless of caching rules
}

this.connection.drop(key, callback); // Always drop, regardless of caching rules
if (promise) {
return promise.promise;
}
};


Expand Down
36 changes: 36 additions & 0 deletions lib/deferred.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

// Declare internals

const internals = {};


module.exports = internals.Deferred = class Deferred {

constructor() {

this.promise = new Promise((resolve, reject) => {

this.resolve = resolve;
this.reject = reject;
});
}

callback(options) {

options = options || { full: false };

return (function () {

const args = Array.from(arguments);

if (args[0]) {
this.reject(options.full ? args : args[0]);
}
else {
this.resolve(options.full ? args : args[1]);
}
}).bind(this);
}

};