Skip to content

Commit

Permalink
[breaking refactor] Move most of the logic from the Memory store in…
Browse files Browse the repository at this point in the history
…to the provider.
  • Loading branch information
indexzero committed Apr 22, 2015
1 parent d335b5a commit f8fe7e9
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 409 deletions.
303 changes: 150 additions & 153 deletions lib/nconf/provider.js
Expand Up @@ -22,6 +22,20 @@ var Provider = exports.Provider = function (options) {
options = options || {};
this.stores = {};
this.sources = [];

//
// Actual values stores on this instance.
//
this.store = {};
this.mtimes = {};
this.readOnly = false;
this.loadFrom = options.loadFrom || null;
this.logicalSeparator = options.logicalSeparator || ':';

if (this.loadFrom) {
this.store = common.loadFilesSync(this.loadFrom);
}

this.init(options);
};

Expand Down Expand Up @@ -204,132 +218,176 @@ Provider.prototype.init = function (options) {
};

//
// ### function get (key, callback)
// ### function get (key)
// #### @key {string} Key to retrieve for this instance.
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Retrieves the value for the specified key (if any).
//
Provider.prototype.get = function (key, callback) {
//
// If there is no callback we can short-circuit into the default
// logic for traversing stores.
//
if (!callback) {
return this._execute('get', 1, key, callback);
}
Provider.prototype.get = function (key) {
var target = this.store,
path = common.path(key, this.logicalSeparator);

//
// Otherwise the asynchronous, hierarchical `get` is
// slightly more complicated because we do not need to traverse
// the entire set of stores, but up until there is a defined value.
// Scope into the object to get the appropriate nested context
//
var current = 0,
names = Object.keys(this.stores),
self = this,
response,
mergeObjs = [];

async.whilst(function () {
return typeof response === 'undefined' && current < names.length;
}, function (next) {
var store = self.stores[names[current]];
current++;

if (store.get.length >= 2) {
return store.get(key, function (err, value) {
if (err) {
return next(err);
}

response = value;

// Merge objects if necessary
if (typeof response === 'object' && !Array.isArray(response)) {
mergeObjs.push(response);
response = undefined;
}

next();
});
}

response = store.get(key);

// Merge objects if necessary
if (typeof response === 'object' && !Array.isArray(response)) {
mergeObjs.push(response);
response = undefined;
while (path.length > 0) {
key = path.shift();
if (target && target.hasOwnProperty(key)) {
target = target[key];
continue;
}
return undefined;
}

next();
}, function (err) {
if (!err && mergeObjs.length) {
response = common.merge(mergeObjs.reverse());
}
return err ? callback(err) : callback(null, response);
});
return target;
};

//
// ### function set (key, value, callback)
// ### function set (key, value)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Sets the `value` for the specified `key` in this instance.
//
Provider.prototype.set = function (key, value, callback) {
return this._execute('set', 2, key, value, callback);
};
Provider.prototype.set = function (key, value) {
if (this.readOnly) {
return false;
}

//
// ### function reset (callback)
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Clears all keys associated with this instance.
//
Provider.prototype.reset = function (callback) {
return this._execute('reset', 0, callback);
var target = this.store,
path = common.path(key, this.logicalSeparator);

if (path.length === 0) {
//
// Root must be an object
//
if (!value || typeof value !== 'object') {
return false;
}
else {
this.reset();
this.store = value;
return true;
}
}

//
// Update the `mtime` (modified time) of the key
//
this.mtimes[key] = Date.now();

//
// Scope into the object to get the appropriate nested context
//
while (path.length > 1) {
key = path.shift();
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}

target = target[key];
}

// Set the specified value in the nested JSON structure
key = path.shift();
target[key] = value;
return true;
};

//
// ### function clear (key, callback)
// ### function clear (key)
// #### @key {string} Key to remove from this instance
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Removes the value for the specified `key` from this instance.
//
Provider.prototype.clear = function (key, callback) {
return this._execute('clear', 1, key, callback);
Provider.prototype.clear = function (key) {
if (this.readOnly) {
return false;
}

var target = this.store,
value = target,
path = common.path(key, this.logicalSeparator);

//
// Remove the key from the set of `mtimes` (modified times)
//
delete this.mtimes[key];

//
// Scope into the object to get the appropriate nested context
//
for (var i = 0; i < path.length - 1; i++) {
key = path[i];
value = target[key];
if (typeof value !== 'function' && typeof value !== 'object') {
return false;
}
target = value;
}

// Delete the key from the nested JSON structure
key = path[i];
delete target[key];
return true;
};

//
// ### function merge ([key,] value [, callback])
// ### function merge (key, value)
// #### @key {string} Key to merge the value into
// #### @value {literal|Object} Value to merge into the key
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Merges the properties in `value` into the existing object value at `key`.
//
// 1. If the existing value `key` is not an Object, it will be completely overwritten.
// 2. If `key` is not supplied, then the `value` will be merged into the root.
// Merges the properties in `value` into the existing object value
// at `key`. If the existing value `key` is not an Object, it will be
// completely overwritten.
//
Provider.prototype.merge = function () {
var self = this,
args = Array.prototype.slice.call(arguments),
callback = typeof args[args.length - 1] === 'function' && args.pop(),
value = args.pop(),
key = args.pop();
Provider.prototype.merge = function (key, value) {
if (this.readOnly) {
return false;
}

function mergeProperty (prop, next) {
return self._execute('merge', 2, prop, value[prop], next);
//
// If the key is not an `Object` or is an `Array`,
// then simply set it. Merging is for Objects.
//
if (typeof value !== 'object' || Array.isArray(value) || value === null) {
return this.set(key, value);
}

if (!key) {
if (Array.isArray(value) || typeof value !== 'object') {
return onError(new Error('Cannot merge non-Object into top-level.'), callback);
var self = this,
target = this.store,
path = common.path(key, this.logicalSeparator),
fullKey = key;

//
// Update the `mtime` (modified time) of the key
//
this.mtimes[key] = Date.now();

//
// Scope into the object to get the appropriate nested context
//
while (path.length > 1) {
key = path.shift();
if (!target[key]) {
target[key] = {};
}

return async.forEach(Object.keys(value), mergeProperty, callback || function () { })
target = target[key];
}

// Set the specified value in the nested JSON structure
key = path.shift();

//
// If the current value at the key target is not an `Object`,
// or is an `Array` then simply override it because the new value
// is an Object.
//
if (typeof target[key] !== 'object' || Array.isArray(target[key])) {
target[key] = value;
return true;
}

return this._execute('merge', 2, key, value, callback);
return Object.keys(value).every(function (nested) {
return self.merge(common.keyed(self.logicalSeparator, fullKey, nested), value[nested]);
});
};

//
Expand Down Expand Up @@ -428,7 +486,7 @@ Provider.prototype.load = function (callback) {
// ignored. Returns an object consisting of all of the data which was
// actually saved.
//
Provider.prototype.save = function (value, callback) {
Provider.prototype.sync = function (value, callback) {
if (!callback && typeof value === 'function') {
callback = value;
value = null;
Expand Down Expand Up @@ -495,67 +553,6 @@ Provider.prototype.save = function (value, callback) {
});
};

//
// ### @private function _execute (action, syncLength, [arguments])
// #### @action {string} Action to execute on `this.store`.
// #### @syncLength {number} Function length of the sync version.
// #### @arguments {Array} Arguments array to apply to the action
// Executes the specified `action` on all stores for this instance, ensuring a callback supplied
// to a synchronous store function is still invoked.
//
Provider.prototype._execute = function (action, syncLength /* [arguments] */) {
var args = Array.prototype.slice.call(arguments, 2),
callback = typeof args[args.length - 1] === 'function' && args.pop(),
destructive = ['set', 'clear', 'merge', 'reset'].indexOf(action) !== -1,
self = this,
response,
mergeObjs = [],
keys = Object.keys(this.stores);


function runAction (name, next) {
var store = self.stores[name];

if (destructive && store.readOnly) {
return next();
}

return store[action].length > syncLength
? store[action].apply(store, args.concat(next))
: next(null, store[action].apply(store, args));
}

if (callback) {
return async.forEach(keys, runAction, function (err) {
return err ? callback(err) : callback();
});
}

keys.forEach(function (name) {
if (typeof response === 'undefined') {
var store = self.stores[name];

if (destructive && store.readOnly) {
return;
}

response = store[action].apply(store, args);

// Merge objects if necessary
if (response && action === 'get' && typeof response === 'object' && !Array.isArray(response)) {
mergeObjs.push(response);
response = undefined;
}
}
});

if (mergeObjs.length) {
response = common.merge(mergeObjs.reverse());
}

return response;
}

//
// Throw the `err` if a callback is not supplied
//
Expand Down

0 comments on commit f8fe7e9

Please sign in to comment.