tiny
Version:
An in-process key-value store
1,420 lines (1,170 loc) • 29.4 kB
JavaScript
/**
* tiny (https://github.com/chjj/tiny)
* An embedded/in-process document/object store for node.
* Copyright (c) 2011-2014, Christopher Jeffrey (MIT Licensed)
*
* Largely inspired by nStore.
* No schemas, just store your objects.
*/
var fs = require('fs')
, path = require('path')
, EventEmitter = require('events').EventEmitter
, Stream = require('stream').Stream
, util = require('util');
var LRUCache = require('lru-cache');
/**
* Tiny
*/
function Tiny(options, callback) {
if (!(this instanceof Tiny)) {
return Tiny.open(options, callback);
}
var options = options || {};
if (typeof options === 'string') {
options = { file: options };
}
EventEmitter.call(this);
this.options = options;
this.tokens = options.tokens || Tiny.tokens;
this.cacheLimit = options.cacheLimit || Tiny.cacheLimit;
this.cacheSize = options.cacheSize || Tiny.cacheSize;
this.keyUnderscore = options.keyUnderscore !== false;
this.initialCache = options.initialCache;
this.saveIndex = options.saveIndex;
this.name = Tiny._getName(options);
this._queue = [];
this._busy = false;
this._index = {};
this._cache = new LRUCache({
max: this.cacheSize,
stale: true,
maxAge: 2 * 60 * 1000
});
this._total = 0;
this._fd = null;
this._loaded = false;
this._queries = [];
this._load(callback);
}
Tiny.prototype.__proto__ = EventEmitter.prototype;
Tiny.open = function(options, callback) {
var options = options || {}
, name = Tiny._getName(options);
if (Tiny.db[name]) {
if (callback) {
callback(null, Tiny.db[name]);
}
} else {
Tiny.db[name] = new Tiny(options, callback);
}
return Tiny.db[name];
};
Tiny._getName = function(options) {
var options = options || {}
, name = options.file || options.name || options;
if (typeof name !== 'string') {
name = path.resolve(process.env.HOME, 'node-tiny.db');
}
return name;
};
Tiny.db = {};
// The amount of bytes at which
// properties are no longer cached.
Tiny.cacheLimit = 1024;
Tiny.cacheSize = 1024;
// A token to recognize
// deleted properties
// and property separators
Tiny.tokens = {
deleted: '\x00'
};
/**
* Logging
*/
Tiny.print = function(msg) {
var args = slice.call(arguments);
if (typeof args[0] === 'object') {
return process.stdout.write(inspect(args[0]) + '\n')
}
args[0] = 'Tiny: ' + args[0];
return console.log.apply(console, args);
};
Tiny.prototype.print = Tiny.print;
Tiny.error = function() {
var args = slice.call(arguments);
if (typeof args[0] === 'object') {
return process.stdout.write(inspect(args[0]) + '\n')
}
args[0] = 'Tiny: \x1b[41m' + args[0] + '\x1b[m';
return console.error.apply(console, args);
};
Tiny.prototype.error = Tiny.error;
Tiny.debug = process.env.NODE_ENV === 'debug'
? Tiny.print
: noop;
Tiny.prototype.debug = Tiny.debug;
/**
* Garbage Collection
*/
Tiny.prototype._collectGarbage = function() {
var self = this;
this._collector = setInterval(function() {
self._cache.reset();
}, 20 * 1000);
if (this._collector.unref) this._collector.unref();
};
Tiny.prototype._cacheable = function(size) {
if (typeof size === 'number') {
;
} else if (Buffer.isBuffer(size)) {
size = size.length;
} else if (typeof size === 'string') {
size = Buffer.byteLength(size);
} else {
size = JSON.stringify(size).length;
}
return (size <= this.cacheLimit)
&& (this._cache.itemCount < this.cacheSize);
};
/**
* Parsing and Loading
*/
Tiny.prototype._load = function(callback) {
var self = this;
callback = callback || noop;
this.emit('opening');
this._busy = true;
this._loaded = false;
return this._buildIndex(function(err) {
if (err) return callback(err);
self.debug('Done parsing.');
self._busy = false;
self._loaded = true;
self.emit('open');
self.emit('ready');
self.commit();
self._flushQueries();
self._collectGarbage();
return callback(null, self);
});
};
Tiny.prototype._buildIndex = function(start, callback) {
if (!callback) {
callback = start;
start = 0;
}
var self = this
, key = ''
, state = 'key'
, dstart = 0
, pos = start;
// To make sure we start *on* a key.
if (start) pos = start - 1;
var stream = fs.createReadStream(this.name, {
flags: 'a+',
highWaterMark: 1024 * 1024,
autoClose: false,
start: start
});
stream.on('open', function(fd) {
self._fd = fd;
});
stream.on('data', function(data) {
var bytes = data.length
, kstart = 0
, i = 0
, prev;
// If we don't start on a key, ignore
// everything until the next field.
if (start && pos === start - 1) {
if (data[0] !== 0x0A) {
state = 'unknown';
}
pos += 1;
i += 1;
}
for (; i < bytes; i++) {
switch (state) {
case 'unknown':
if (data[0] === 0x0A) {
state = 'key';
key = '';
kstart = i + 1;
}
break;
case 'key':
switch (data[i]) {
case 0x09:
state = 'data';
dstart = pos + 1;
key += data.toString('ascii', kstart, i);
break;
case 0x0A:
self.debug('Unexpected byte at offset: %d', pos);
key = '';
kstart = i + 1;
break;
}
break;
case 'data':
switch (data[i]) {
case 0x0A:
state = 'key';
if (key) {
// prev = self._index[key];
self._index[key] = [dstart, pos - dstart];
// self._index[key].previous = prev;
}
key = '';
kstart = i + 1;
break;
case 0x09:
self.debug('Unexpected byte at offset: %d', pos);
key = '';
dstart = pos + 1;
break;
}
break;
}
pos++;
}
if (state === 'key' && kstart < bytes) {
key += data.toString('ascii', kstart);
}
});
stream.on('error', function(err) {
return callback(err);
});
stream.on('end', function(data) {
self._total = pos;
return callback();
});
};
Tiny.prototype._ensureIntegrity = function(docKey, callback) {
var self = this
, fd = this._fd
, slot = this._index[docKey]
, previous = slot.previous
, pos = slot[0]
, length = slot[1]
, ch = new Buffer(1)
, check
, first
, last;
callback = callback || noop;
function recheck() {
if (!previous) {
self.debug('Non-recoverable: %s:%d', docKey, slot[0]);
self.emit('error', new Error(docKey + ' corrupt. Non-recoverable.'));
return callback(true);
}
self.debug('Corrupt record found: %s:%d.', docKey, slot[0],
'Using previous value.');
self.emit('corrupt', docKey, previous);
self._index[docKey] = previous;
return self._ensureIntegrity(docKey, callback);
}
return fs.read(fd, ch, 0, 1, pos, function(err, bytes) {
if (err || !bytes) {
return recheck();
}
first = ch[0];
return fs.read(fd, ch, 0, 1, pos + length - 1, function(err, bytes) {
if (err || !bytes) {
return recheck();
}
last = ch[0];
switch (first) {
case 0x22:
check = last === 0x22;
break;
case 0x5B:
check = last === 0x5D;
break;
case 0x7B:
check = last === 0x7D;
break;
case 0x74:
case 0x66:
check = last === 0x65;
break;
case 0x6E:
check = last === 0x6C;
break;
case 0x2D:
case 0x30:
case 0x31:
case 0x32:
case 0x33:
case 0x34:
case 0x35:
case 0x36:
case 0x37:
case 0x38:
case 0x39:
check = last >= 0x30
&& last <= 0x39;
break;
default:
check = false;
break;
}
return check
? callback(null, self._index[docKey])
: recheck();
});
});
};
Tiny.prototype._flushQueries = function() {
var queries = this._queries.slice();
this._queries.length = 0;
for (var i = 0; i < queries.length; i++) {
queries[i][0].apply(this, queries[i][1]);
}
};
/**
* Object Lookup
*/
Tiny.prototype._read = function(pos, length, callback) {
var self = this
, fd = this._fd
, data;
callback = callback || noop;
data = new Buffer(length);
return fs.read(fd, data, 0, length, pos, function(err, bytes) {
if (err) return callback(err);
return callback(null, data.toString('utf8'));
});
};
Tiny.prototype._lookup = function(slot, callback) {
var self = this;
return this._read(slot[0], slot[1], function(err, data) {
if (err) return callback(err);
try {
data = JSON.parse(data);
} catch(err) {
return callback(new Error(err.message + '\n\nData: ' + data));
}
if (data === self.tokens.deleted) {
return callback(new Error('Not found.'));
}
return callback(null, data);
});
};
/**
* Data Storage
*/
Tiny.prototype._set = function(docKey, data, callback, action) {
var self = this
, cache = this._cache
, index = this._index
, json;
if (!this._loaded) {
this._queries.push([this._set, slice.call(arguments)]);
return;
}
callback = callback || noop;
if (/[\x09\x0A]/.test(docKey)) {
return callback(new Error('Bad key.'));
}
if (!data || typeof data !== 'object') {
return callback(new Error('Bad object.'));
}
self.debug('setting doc: %s', docKey);
switch (action) {
case 'set':
case 'update':
data._key = docKey;
json = JSON.stringify(data);
break;
case 'delete':
cache.del(docKey);
delete index[docKey];
json = JSON.stringify(this.tokens.deleted);
break;
}
this._queue.push({
action: action,
docKey: docKey,
data: data,
json: json,
callback: callback
});
return this.commit();
};
Tiny.prototype.put =
Tiny.prototype.set = function(docKey, doc, callback) {
return this._set(docKey, doc, callback, 'set');
};
Tiny.prototype.merge =
Tiny.prototype.update = function(docKey, doc, callback) {
callback = callback || noop;
if (!this._loaded) {
this._queries.push([this.update, slice.call(arguments)]);
return;
}
if (!this._index[docKey]) {
return callback(new Error('No such key.'));
}
return this._set(docKey, doc, callback, 'update');
};
Tiny.prototype.del =
Tiny.prototype.remove = function(docKey, callback) {
callback = callback || noop;
if (!this._loaded) {
this._queries.push([this.remove, slice.call(arguments)]);
return;
}
if (!this._index[docKey]) {
return callback(new Error('No such key.'));
}
return this._set(docKey, {}, callback, 'delete');
};
/**
* Commit Changes
*/
Tiny.prototype.commit = function(callback) {
var self = this
, fd = this._fd
, queue = this._queue;
if (!this._loaded) {
this._queries.push([this.commit, slice.call(arguments)]);
return;
}
callback = callback || noop;
if (this._busy
|| !queue.length
|| fd == null) return;
self.debug('committing - %d items in queue.', queue.length);
this.emit('committing', queue);
var data = []
, total = 0;
this._busy = true;
this._queue = [];
queue = queue.map(function(item) {
var docKey = item.docKey
, json = item.json;
var line = docKey + '\t' + json + '\n'
, lineSize = Buffer.byteLength(line)
, keySize = Buffer.byteLength(docKey + '\t');
if (self._cacheable(lineSize)) {
self._cache.set(docKey, item.data);
}
if (!self._index[docKey]) {
self._index[docKey] = [];
}
self._index[docKey][0] = self._total + total + (keySize - 1);
self._index[docKey][1] = lineSize - keySize;
total += lineSize;
data.push(line);
return item;
});
queue.push({
action: 'commit',
docKey: null,
data: null,
json: null,
callback: callback
});
callback = function(err) {
self._busy = false;
queue.forEach(function(item) {
self.emit(item.action, item.docKey, item.data);
// levelup-like events
switch (item.action) {
case 'set':
case 'update':
self.emit('put', item.docKey, item.data);
break;
case 'delete':
self.emit('del', item.docKey, item.data);
break;
case 'commit':
if (queue.length > 1 && item.callback !== noop) {
self.emit('batch', queue);
}
break;
}
if (!item.callback) {
return;
}
return err
? item.callback(err)
: item.callback(null);
});
return self.commit();
};
data = new Buffer(data.join(''));
return fs.write(fd, data, 0, data.length, this._total, function on(err, bytes) {
if (err) {
if (err.code === 'EBADF') {
return callback(err);
}
if (!on.attempt) {
self.debug('write error: %s', err);
on.attempt = 0;
}
if (++on.attempt === 5) {
err.message = 'Write Error:\n' + err.message;
return callback(err);
}
self.emit('retry', on.attempt);
return setTimeout(function() {
return fs.write(fd, data, 0, data.length, self._total, on);
}, 50);
}
self._total += bytes;
return callback();
});
};
/**
* Data Retrieval
*/
Tiny.prototype.get = function(docKey, callback) {
var self = this
, index = this._index
, slot = this._index[docKey]
, cache = this._cache
, cached = cache.get(docKey)
, doc = {};
if (!this._loaded) {
this._queries.push([this.get, slice.call(arguments)]);
return;
}
callback = callback || noop;
if (cached) {
return callback(null, cached);
}
if (!slot) {
return callback(new Error('Not found.'));
}
return this._lookup(slot, function(err, doc) {
if (err) return callback(err);
if (!self.keyUnderscore && doc._key) {
doc.key = doc._key;
}
self.emit('get', docKey, doc);
//if (self._cacheable(slot[1])) {
// cache.set(docKey, data);
//}
return callback(null, doc);
});
};
Tiny.prototype.all = function() {
throw new
Error('`db.all()` has been removed.'
+ ' It is not memory efficient.'
+ ' Please use something else.');
};
Tiny.prototype.each = function(iter, done) {
var self = this
, index = this._index
, keys = Object.keys(index);
if (!this._loaded) {
this._queries.push([this.each, slice.call(arguments)]);
return;
}
iter = iter || noop;
done = done || noop;
return parallel(keys, function(docKey, next) {
return self.get(docKey, function(err, data) {
if (err) return next();
iter(data, docKey);
return next();
});
}, done);
};
Tiny.prototype.createReadStream = function(options, stream) {
var self = this
, index = this._index
, keys = Object.keys(index)
, options = options || {}
, stream = stream || new Stream
, total = 0;
stream.readable = true;
stream.writable = false;
stream.pause = function() {
this._paused = true;
};
stream.resume = function() {
this._paused = false;
};
stream.destroy = function() {
this._destroyed = true;
this.emit('close');
this.emit('end');
};
if (!this._loaded) {
this._queries.push([this.createReadStream, [options, stream]]);
return stream;
}
if (options.reverse) {
keys = keys.reverse();
}
serial(keys, function iter(docKey, next, i) {
if (stream._destroyed) return;
if (options.start && docKey.indexOf(options.start) !== 0) {
return next();
}
if (stream._paused) {
return setTimeout(function() {
return iter(docKey, next, i);
}, 50);
}
if (options.keys || options.values === false) {
if (options.limit && ++total > options.limit) {
return stream.destroy();
}
stream.emit('data', docKey);
if (options.end && docKey.indexOf(options.end) === 0) {
return stream.destroy();
}
return next();
}
return self.get(docKey, function(err, data) {
if (err) {
stream.emit('error', err);
return next();
}
if (options.limit && ++total > options.limit) {
return stream.destroy();
}
if (options.values || options.keys === false) {
stream.emit('data', data);
} else {
stream.emit('data', { key: docKey, value: data });
}
if (options.end && docKey.indexOf(options.end) === 0) {
return stream.destroy();
}
// return setTimeout(function() {
// return next();
// }, 10);
return next();
});
}, function() {
return stream.destroy();
});
return stream;
};
Tiny.prototype.createKeyStream = function(options) {
options.keys = true;
return this.createReadStream(options);
};
Tiny.prototype.createValueStream = function(options) {
options.values = true;
return this.createReadStream(options);
};
Tiny.prototype.createWriteStream = function(options) {
var self = this
, options = options || {}
, stream = new Stream
stream.readable = false;
stream.writable = true;
stream.write = function(data) {
var type = data.type || 'set';
switch (type) {
case 'put':
case 'set':
type = 'set';
break;
case 'update':
case 'merge':
type = 'update';
break;
case 'delete':
case 'del':
type = 'delete';
break;
default:
stream.emit('error', new Error('Unrecognized action: ' + type));
return;
}
return self._set(data.docKey, data.value, function(err) {
if (err) stream.emit('error', err);
}, type);
};
stream.end = function(data) {
var ret;
if (data) {
ret = stream.write(data);
}
this.destroy();
return ret;
};
stream.destroy = function() {
this._destroyed = true;
this.emit('close');
this.emit('end');
};
return stream;
};
/**
* Querying
*/
Tiny.prototype.fetch = function(opt, filter, done) {
var self = this
, results = []
, keys;
if (!done) {
done = filter;
filter = opt;
opt = {};
}
done = done || noop;
if (!this._loaded) {
this._queries.push([this.fetch, slice.call(arguments)]);
return;
}
keys = Object.keys(this._index);
if (opt.count) {
results = 0;
return parallel(keys, function(docKey, next) {
return self.get(docKey, function(err, doc) {
if (err) return next();
if (filter.length >= 3) {
return filter(doc, docKey, function(match) {
if (match) results++;
return next();
});
}
if (filter(doc, docKey) === true) {
results++;
}
return next();
});
});
return done(null, results);
}
return parallel(keys, function iter(docKey, next) {
return self.get(docKey, function(err, doc) {
if (err) return next();
if (filter.length >= 3) {
return filter(doc, docKey, function(match) {
if (match) {
return self.get(docKey, function(err, doc) {
if (err) return next();
results.push(doc);
return next();
});
}
return next();
});
}
if (filter(doc, docKey) === true) {
return self.get(docKey, function(err, doc) {
if (err) return next();
results.push(doc);
return next();
});
}
return next();
});
}, function() {
if (opt.desc || opt.asc) {
results = self._sortResults(results,
opt.asc || opt.desc,
opt.asc ? 'asc' : 'desc');
}
if (opt.skip) {
results = results.slice(opt.skip);
}
if (opt.limit) {
results = results.slice(0, opt.limit);
}
if (opt.one || opt.single) {
results = results[0];
}
return done(null, results);
});
};
Tiny.prototype._sortResults = function(results, prop, order) {
var self = this
, first = results[0]
, numeric;
if (first) {
numeric = isFinite(first[prop]);
}
results = results
.filter(function(result) {
return result[prop] != null;
})
.sort(function(a, b) {
a = a[prop];
b = b[prop];
if (!numeric) {
// if (isFinite(a)) {
a = (a + '').toLowerCase().charCodeAt(0);
b = (b + '').toLowerCase().charCodeAt(0);
}
return a > b ? 1 : (a < b ? -1 : 0);
});
if (order === 'desc') {
results = results.reverse();
}
return results;
};
/**
* Mongo-like Querying
*/
Tiny.prototype.query = (function() {
// operator logic
var ops = {
$lt: function(a, b) {
return a < b;
},
$lte: function(a, b) {
return a <= b;
},
$gt: function(a, b) {
return a > b;
},
$gte: function(a, b) {
return a >= b;
},
$eq: function(a, b) {
return a == b;
},
$ne: function(a, b) {
return a != b;
},
$regex: function(a, b) {
return b.test(a);
},
// contains any of...
$in: function(a, b) {
var keys = Object.keys(b)
, i = 0
, l = keys.length
, val;
for (; i < l; i++) {
val = b[keys[i]];
if (has(a, val)) return true;
}
return false;
},
// does not contain any of...
$nin: function(a, b) {
return !ops.$in(a, b);
},
// contains all...
$all: function(a, b) {
var found = 0
, keys = Object.keys(b)
, i = 0
, l = keys.length
, val;
for (; i < l; i++) {
val = b[keys[i]];
if (has(a, val)) found++;
}
return found === l;
},
$exists: function(a, b) {
return b
? a !== undefined
: a === undefined;
},
$size: function(a, b) {
// why? because i can
return Buffer.byteLength(a) === b;
}
};
// test an object/statement to see
// if it matches a document's properties.
function test(obj, doc) {
if (Array.isArray(obj)) {
var i = obj.length;
while (i--) {
if (test(obj[i], doc)) return true;
}
return false;
}
var keys = Object.keys(obj)
, i = 0
, l = keys.length
, propKey
, targetProp
, prop;
for (; i < l; i++) {
propKey = keys[i];
targetProp = obj[propKey];
prop = doc[propKey];
if (propKey === '$or') {
if (!test(targetProp, doc)) return false;
} else if (targetProp && typeof targetProp === 'object') {
if (!test.object(prop, targetProp, doc)) return false;
} else {
if (prop != targetProp) return false;
}
}
return true;
}
test.object = function(prop, targetProp, doc) {
var propOperations = targetProp;
var keys = Object.keys(propOperations)
, i = 0
, l = keys.length
, operator;
for (; i < l; i++) {
operator = keys[i];
targetProp = propOperations[operator];
if (operator === '$or') {
if (!test(targetProp, doc)) return false;
} else if (ops[operator] && !ops[operator](prop, targetProp)) {
return false;
}
}
return true;
};
return function(where, callback, opt) {
where = where || {};
opt = opt || {};
return this.fetch(opt, function(doc) {
if (test(where, doc)) {
return true;
}
}, callback);
};
})();
Tiny.prototype.find = function() {
var self = this
, args = slice.call(arguments)
, opt = {};
if (!args.length) args.push({});
if (typeof args[1] === 'function') {
return this.query.apply(this, args);
}
var chain = function(callback) {
callback = callback || noop;
return this.query.apply(this, args.concat(callback, opt));
};
chain.select = function() {
throw new
Error('`db.select()` has been removed.');
};
chain.count = function() {
opt.count = true;
return chain;
};
chain.desc = function(prop) {
opt.desc = prop;
return chain;
};
chain.asc = function(prop) {
opt.asc = prop;
return chain;
};
chain.limit = function(limit) {
opt.limit = limit;
return chain;
};
chain.skip = function(skip) {
opt.skip = skip;
return chain;
};
chain.one = function() {
opt.one = true;
opt.limit = 1;
return chain;
};
return chain;
};
/**
* Control
*/
Tiny.prototype.close = function(callback) {
var self = this;
callback = callback || noop;
this.emit('closing');
return fs.close(this._fd, function() {
delete self._fd;
delete Tiny.db[self.name];
self.emit('closed');
return callback();
});
};
Tiny.prototype.compact = function(callback) {
var self = this
, index = this._index
, keys = Object.keys(index)
, name = this.name + '~';
if (!this._loaded) {
this._queries.push([this.compact, slice.call(arguments)]);
return;
}
callback = callback || noop;
return Tiny(name, function(err, tmp) {
if (err) return callback(err);
return serial(keys, function(docKey, next) {
return self.get(docKey, function(err, obj) {
if (err) return callback(err);
return tmp.set(docKey, obj, next);
});
}, function() {
return self.close(function(err) {
if (err) return callback(err);
return fs.unlink(self.name, function(err) {
if (err) return callback(err);
return fs.rename(name, self.name, function(err) {
if (err) return callback(err);
Object.keys(tmp).forEach(function(key) {
self[key] = tmp[key];
});
delete Tiny.db[name];
return callback();
});
});
});
});
});
};
/**
* JSON Dump
*/
Tiny.prototype.dump = function(pretty, callback) {
if (arguments.length === 1) {
callback = pretty;
pretty = undefined;
}
callback = callback || noop;
var self = this
, keys = Object.keys(this._index)
, klength = keys.length
, pretty = pretty ? 2 : 0
, i = 0;
if (!this._loaded) {
this._queries.push([this.dump, slice.call(arguments)]);
return;
}
var stream = fs.createWriteStream(this.name + '.json');
stream.on('error', function(err) {
stream.destroy();
return callback(err);
});
stream.on('open', function(fd) {
stream.write('{\n');
return serial(keys, function(docKey, next) {
i++;
return self.get(docKey, function(err, obj) {
if (err) return callback(err);
var data = '"'
+ docKey
+ '": '
+ JSON.stringify(obj, null, pretty);
if (i !== klength) {
data += ',\n';
}
if (stream.write(data) === false) {
return stream.once('drain', next);
}
return next();
});
}, function() {
stream.end('\n}');
return stream.on('close', function() {
callback(null, stream.path);
});
});
});
};
/**
* Getters
*/
Tiny.prototype.__defineGetter__('length', function() {
return Object.keys(this._index).length;
});
Tiny.prototype.__defineGetter__('size', function() {
return this._total || 0;
});
/**
* Helpers
*/
var hasOwnProperty = Object.prototype.hasOwnProperty
, slice = [].slice;
function has(obj, item) {
var keys = Object.keys(obj)
, i = 0
, l = keys.length;
for (; i < l; i++) {
if (obj[keys[i]] === item) return true;
}
}
function parallel(obj, iter, done) {
done = done || noop;
if (!obj) return done();
var j = 0
, i = 0
, l
, keys
, key;
if (typeof obj.length !== 'number'
|| typeof obj === 'function') {
keys = Object.keys(obj);
l = keys.length;
} else {
l = obj.length;
}
if (!l) return done();
function next() {
if (++j === l) {
return done();
}
}
for (; i < l; i++) {
key = keys ? keys[i] : i;
iter(obj[key], next, key, i);
}
}
function serial(obj, iter, done) {
done = done || noop;
if (!obj) return done();
var i = 0
, keys
, l;
if (typeof obj.length !== 'number'
|| typeof obj === 'function') {
keys = Object.keys(obj);
l = keys.length;
(function next() {
if (i === l) return done();
var j = i++, key = keys[j];
return nextTick(function() {
return iter(obj[key], next, key, j);
});
})();
} else {
l = obj.length;
(function next() {
if (i === l) return done();
var j = i++;
return nextTick(function() {
return iter(obj[j], next, j, j);
});
})();
}
}
function nextTick(callback) {
return global.setImmediate
? global.setImmediate(callback)
: process.nextTick(callback);
}
function stringify(val) {
var type = typeof val;
if (type === 'undefined'
|| type === 'function'
|| val !== val) {
val = null;
}
return JSON.stringify(val);
}
function noop() {}
function inspect(obj) {
return typeof obj !== 'string'
? util.inspect(obj, false, 20, true)
: obj;
}
/**
* Expose
*/
module.exports = Tiny;