From 5208f0ff8a405c10d85ddfe511a0f0cc0755a37d Mon Sep 17 00:00:00 2001 From: scommisso Date: Wed, 16 Sep 2015 17:29:09 -0700 Subject: [PATCH] Break out into separate files, bundle with browserify --- .gitmodules | 3 - Makefile | 2 +- dist/async-json.js | 481 +++++++++++++++++++++++++++++++++++++++ lib/async-json.js | 367 ++++------------------------- lib/browser-polyfills.js | 13 ++ lib/counter.js | 17 ++ lib/polyfill-asap.js | 14 ++ lib/polyfill-get-keys.js | 19 ++ lib/polyfill-is-array.js | 6 + lib/stringify-array.js | 78 +++++++ lib/stringify-object.js | 89 ++++++++ lib/stringify.js | 78 +++++++ package.json | 27 ++- support/expresso | 1 - 14 files changed, 867 insertions(+), 328 deletions(-) delete mode 100644 .gitmodules create mode 100644 dist/async-json.js create mode 100644 lib/browser-polyfills.js create mode 100644 lib/counter.js create mode 100644 lib/polyfill-asap.js create mode 100644 lib/polyfill-get-keys.js create mode 100644 lib/polyfill-is-array.js create mode 100644 lib/stringify-array.js create mode 100644 lib/stringify-object.js create mode 100644 lib/stringify.js delete mode 160000 support/expresso diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ffe0dcb..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "support/expresso"] - path = support/expresso - url = git://github.com/visionmedia/expresso.git diff --git a/Makefile b/Makefile index ec21f31..3ed2df2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -TEST = support/expresso/bin/expresso +TEST = node_modules/expresso/bin/expresso TESTS ?= test/*.test.js SRC = $(shell find lib -type f -name "*.js") diff --git a/dist/async-json.js b/dist/async-json.js new file mode 100644 index 0000000..7b81c90 --- /dev/null +++ b/dist/async-json.js @@ -0,0 +1,481 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= this.limit) { + this.count = 0; + return true; + } else { + return false; + } +}; + +},{}],5:[function(require,module,exports){ +(function (process){ +'use strict'; + +var asap; +if (typeof setImmediate === 'function') { + asap = setImmediate; +} else if (typeof process !== 'undefined' && typeof process.nextTick === 'function') { + asap = process.nextTick +} else { + asap = function asap(callback) { + setTimeout(callback, 0); + }; +} +module.exports = asap; +}).call(this,require('_process')) +},{"_process":11}],6:[function(require,module,exports){ +'use strict'; + +module.exports = Object.keys || (function() { + // older browsers + var has = Object.prototype.hasOwnProperty || function hasOwnProperty() { + // Object.prototype.hasOwnProperty should really always exist. + return true; + }; + + return function getKeys(obj) { + var result = []; + for (var key in obj) { + if (has.call(obj, key)) { + result.push(key); + } + } + return result; + }; +}()); + +},{}],7:[function(require,module,exports){ +'use strict'; + +module.exports = Array.isArray || function isArray(o) { + // older browsers + return Object.prototype.toString.call(o) === '[object Array]'; +}; + +},{}],8:[function(require,module,exports){ +'use strict'; + +var polyfills = require('./browser-polyfills'); +var asap =polyfills.asap; + +module.exports = internalStringifyArray; + +/** + * Stringify an array + * + * @param {Function} stringify the top-level stringify function. + * @param {Array} array The array to stringify. + * @param {Function} callback The callback to invoke when completed the stringification. + * @api private + * + * @example internalStringifyArray([1, 2, 3], function(err, value) { value === "[1,2,3]"; }); + */ + function internalStringifyArray(stringify, array, callback, errback, counter) { + var len = array.length; + if (len === 0) { + callback("[]"); + return; + } + + // buffer is our ultimate return value + var buffer = "["; + + function step(n) { + if (n === len) { + // we're done + buffer += "]"; + callback(buffer); + return false; + } + + var synchronous = true; + var completedSynchronously = false; + // asynchronously stringify the nth element. + stringify(array[n], function(value) { + if (n > 0) { + buffer += ","; + } + + if (value === undefined) { + // JSON.stringify turns bad values in arrays into null, so we need to as well + buffer += "null"; + } else { + buffer += value; + } + + // go to the next element + if (counter.inc()) { + asap(function() { + run(n + 1); + }); + } else if (synchronous) { + completedSynchronously = true; + } else { + run(n + 1); + } + }, errback, String(n), counter, array); + synchronous = false; + return completedSynchronously; + } + + function run(start) { + try { + for (var i = start; step(i); ++i) { + // nothing to do, as step's return value will cause a halt eventually + } + } catch (e) { + errback(e); + } + } + + // let's pump, starting at index 0 + run(0); +} + +},{"./browser-polyfills":3}],9:[function(require,module,exports){ +'use strict'; + +var polyfills = require('./browser-polyfills'); +var getKeys = polyfills.getKeys; +var asap =polyfills.asap; +var jsonStringify = polyfills.jsonStringify; + +module.exports = internalStringifyObject; + +/** + * Stringify an object + * + * @param {Function} stringify the top-level stringify function. + * @param {Object} object The object to stringify. + * @param {Function} callback The callback to invoke when completed the stringification. + * @api private + * + * @example internalStringifyObject({alpha: 1, bravo: 2}, function(err, value) { value === '{"alpha":1,"bravo":2}'; }); + */ +function internalStringifyObject(stringify, object, callback, errback, counter) { + // getKeys _should_ be a reference to Object.keys + // JSON.stringify gets the keys in the same order as this, but that is arbitrary. + var keys = getKeys(object); + var len = keys.length; + if (len === 0) { + callback("{}"); + return; + } + + // whether or not we've placed the first element in yet. + // can't rely on i === 0, since we might skip it if the value === undefined. + var first = true; + + // buffer is our ultimate return value + var buffer = "{"; + + function step(n) { + if (n === len) { + buffer += "}"; + callback(buffer); + return false; + } + + var synchronous = true; + var completedSynchronously = false; + var key = keys[n]; + // asynchronously stringify the nth element in our list of keys + stringify(object[key], function(value) { + // if we get an undefined, rather than placing in null like the array does, we just skip it. + if (value !== undefined) { + if (first) { + first = false; + } else { + buffer += ","; + } + + buffer += jsonStringify(key); + buffer += ":"; + buffer += value; + } + + // go to the next key + if (counter.inc()) { + asap(function() { + run(n + 1); + }); + } else if (synchronous) { + completedSynchronously = true; + } else { + run(n + 1); + } + }, errback, key, counter, object); + synchronous = false; + return completedSynchronously; + }; + + function run(start) { + try { + for (var i = start; step(i); ++i) { + // nothing to do, as step's return value will cause a halt eventually + } + } catch (e) { + errback(e); + } + }; + + // let's pump, starting at index 0 + run(0); +}; +},{"./browser-polyfills":3}],10:[function(require,module,exports){ +'use strinct'; + +var Counter = require('./counter'); +var stringifyArray = require('./stringify-array'); +var stringifyObject = require('./stringify-object'); +var polyfills = require('./browser-polyfills'); +var isArray = polyfills.isArray; +var asap =polyfills.asap; +var jsonStringify = polyfills.jsonStringify; + +module.exports = runStringify; + +var DEFAULT_COUNTER_LIMIT = 200; +function runStringify(data, options, callback, errback) { + asap(function() { + stringify(data, callback, errback, undefined, new Counter(options.limit || DEFAULT_COUNTER_LIMIT), undefined); + }); +} + +function stringify(data, callback, errback, key, counter, context) { + if (!counter) { + throw new Error("Expected counter"); + } + try { + stringifyAux(data, callback, errback, key, counter, context); + } catch (err) { + errback(err); + } +} + +function stringifyAux(data, callback, errback, key, counter, context) { + switch (typeof data) { + case "object": + if (data === null) { + callback("null"); + } else { + var then = data.then; + if (typeof then === 'function') { + then.call(data, function(value) { + stringify(value, callback, errback, key, counter, undefined); + }, errback); + } else if (typeof data.toJSON === "function") { + // used by Date and possibly some others. + callback(jsonStringify(data.toJSON(key))); + } else if (isPrimitiveConstructor(data.constructor)) { + // horrible, someone used the new String(), new Number(), or new Boolean() syntax. + callback(jsonStringify(data)); + } else if (isArray(data)) { + stringifyArray(stringify, data, callback, errback, counter); + } else { + stringifyObject(stringify, data, callback, errback, counter); + } + } + break; + case "function": + if (data.length === 0) { + // assume a sync function that returns a value + stringify(data.call(context), callback, errback, key, counter, undefined); + } else { + // assume an async function that takes a callback + data.call(context, function(err, value) { + if (err) { + errback(err); + } else { + stringify(value, callback, errback, key, counter, undefined); + } + }); + } + break; + default: + callback(jsonStringify(data)); + break; + } +} + +function isPrimitiveConstructor(ctor) { + return ctor === String || ctor === Number || ctor === Boolean; +} + +},{"./browser-polyfills":3,"./counter":4,"./stringify-array":8,"./stringify-object":9}],11:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = setTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + clearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + setTimeout(drainQueue, 0); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}]},{},[1]); diff --git a/lib/async-json.js b/lib/async-json.js index df102c1..a29beee 100644 --- a/lib/async-json.js +++ b/lib/async-json.js @@ -1,323 +1,54 @@ -(function(exports, undefined) { - "use strict"; - - if (typeof JSON === 'undefined' || !JSON || !JSON.stringify) { - // older browsers - throw new Error("The json2.js file must be included before async-json.js"); - } - var jsonStringify = JSON.stringify; - var isArray = Array.isArray || function(o) { - // older browsers - return Object.prototype.toString.call(o) === '[object Array]'; - }; - var getKeys = Object.keys || (function() { - // older browsers - - var has = Object.prototype.hasOwnProperty || function() { - // Object.prototype.hasOwnProperty should really always exist. - return true; - }; - - return function(obj) { - var result = []; - for (var key in obj) { - if (has.call(obj, key)) { - result.push(key); - } - } - return result; - }; - }()); - - var LIMIT = 200; - var asap = (function() { - if (typeof setImmediate === 'function') { - return function(callback) { - setImmediate(callback); - } - } else if (typeof process !== 'undefined' && typeof process.nextTick === 'function') { - return function(callback) { - process.nextTick(callback); - }; - } else { - return function(callback) { - setTimeout(callback, 0); - }; - } - }()); - - function isPrimitiveConstructor(ctor) { - return ctor === String || ctor === Number || ctor === Boolean; - } - - function stringifyAux(data, callback, errback, key, counter, context) { - switch (typeof data) { - case "object": - if (data === null) { - // why is typeof null === "object"? - callback("null"); - } else { - var then = data.then; - if (typeof then === 'function') { - then.call(data, function(value) { - stringify(value, callback, errback, key, counter, undefined); - }, errback); - } else if (typeof data.toJSON === "function") { - // used by Date and possibly some others. - callback(jsonStringify(data.toJSON(key))); - } else if (isPrimitiveConstructor(data.constructor)) { - // horrible, someone used the new String(), new Number(), or new Boolean() syntax. - callback(jsonStringify(data)); - } else if (isArray(data)) { - internalStringifyArray(data, callback, errback, counter); - } else { - internalStringifyObject(data, callback, errback, counter); - } - } - break; - case "function": - if (data.length === 0) { - // assume a sync function that returns a value - stringify(data.call(context), callback, errback, key, counter, undefined); - } else { - // assume an async function that takes a callback - data.call(context, function(err, value) { - if (err) { - errback(err); - } else { - stringify(value, callback, errback, key, counter, undefined); - } - }); - } - break; - default: - callback(jsonStringify(data)); - break; - } +'use strict'; + +var runStringify = require('./stringify'); +var polyfills = require('./browser-polyfills'); +var asap = polyfills.asap; + +/** + * Asynchronously convert a JavaScript object to JSON. + * If any functions are supplied in the data, it will be invoked. + * If the function has 0 parameters, it will be invoked and treated as synchronous, its return value being its replacement. + * Otherwise, the first parameter is assumed to be a callback which should be invoked as callback(error, result) + * + * @param {Any} data Any JavaScript object. + * @param {Function or null} callback A callback that takes an error and the result as parameters. + * @api public + * @return {Promise or undefined} If a callback is not provided, a Promise will be returned. + * + * @example stringify({some: "data"}, function(err, value) { if (err) { throw err; } value === '{"some":"data"}' }) + * @example stringify({some: "data"}.then(function(value) { assert(value === '{"some":"data"}') }) + */ +module.exports.stringify = function(data, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } else { + options = options || {}; } - function stringify(data, callback, errback, key, counter, context) { - if (!counter) { - throw new Error("Expected counter"); - } - try { - stringifyAux(data, callback, errback, key, counter, context); - } catch (err) { - errback(err); - } - } - - /** - * Stringify an array - * - * @param {Array} array The array to stringify. - * @param {Function} callback The callback to invoke when completed the stringification. - * @api private - * - * @example internalStringifyArray([1, 2, 3], function(err, value) { value === "[1,2,3]"; }); - */ - function internalStringifyArray(array, callback, errback, counter) { - var len = array.length; - if (len === 0) { - callback("[]"); - return; - } - - // buffer is our ultimate return value - var buffer = "["; - - function step(n) { - if (n === len) { - // we're done - buffer += "]"; - callback(buffer); - return false; - } - - var synchronous = true; - var completedSynchronously = false; - // asynchronously stringify the nth element. - stringify(array[n], function(value) { - if (n > 0) { - buffer += ","; - } - - if (value === undefined) { - // JSON.stringify turns bad values in arrays into null, so we need to as well - buffer += "null"; - } else { - buffer += value; - } - - // go to the next element - if (counter.inc()) { - asap(function() { - run(n + 1); - }); - } else if (synchronous) { - completedSynchronously = true; - } else { - run(n + 1); - } - }, errback, String(n), counter, array); - synchronous = false; - return completedSynchronously; - } - - function run(start) { - try { - for (var i = start; step(i); ++i) { - // nothing to do, as step's return value will cause a halt eventually - } - } catch (e) { - errback(e); - } - } - - // let's pump, starting at index 0 - run(0); - }; - - /** - * Stringify an object - * - * @param {Object} object The object to stringify. - * @param {Function} callback The callback to invoke when completed the stringification. - * @api private - * - * @example internalStringifyObject({alpha: 1, bravo: 2}, function(err, value) { value === '{"alpha":1,"bravo":2}'; }); - */ - function internalStringifyObject(object, callback, errback, counter) { - // getKeys _should_ be a reference to Object.keys - // JSON.stringify gets the keys in the same order as this, but that is arbitrary. - var keys = getKeys(object); - var len = keys.length; - if (len === 0) { - callback("{}"); - return; - } - - // whether or not we've placed the first element in yet. - // can't rely on i === 0, since we might skip it if the value === undefined. - var first = true; - - // buffer is our ultimate return value - var buffer = "{"; - - function step(n) { - if (n === len) { - buffer += "}"; - callback(buffer); - return false; - } - - var synchronous = true; - var completedSynchronously = false; - var key = keys[n]; - // asynchronously stringify the nth element in our list of keys - stringify(object[key], function(value) { - // if we get an undefined, rather than placing in null like the array does, we just skip it. - if (value !== undefined) { - if (first) { - first = false; - } else { - buffer += ","; - } - - buffer += jsonStringify(key); - buffer += ":"; - buffer += value; - } - - // go to the next key - if (counter.inc()) { - asap(function() { - run(n + 1); - }); - } else if (synchronous) { - completedSynchronously = true; - } else { - run(n + 1); - } - }, errback, key, counter, object); - synchronous = false; - return completedSynchronously; - }; - - function run(start) { - try { - for (var i = start; step(i); ++i) { - // nothing to do, as step's return value will cause a halt eventually - } - } catch (e) { - errback(e); - } - }; - - // let's pump, starting at index 0 - run(0); - }; - - var Counter = (function() { - function Counter() { - this.count = 0; - } - Counter.prototype.inc = function() { - if (++this.count >= LIMIT) { - this.count = 0; - return true; - } else { - return false; - } - }; - return Counter; - }()); - - function runStringify(data, callback, errback) { - asap(function() { - stringify(data, callback, errback, undefined, new Counter(), undefined); - }); + if (callback == null) { + return stringifyPromise(data, options); + } else { + return stringifyNode(data, options, callback); } - - function stringifyPromise(data) { - return new Promise(function(resolve, reject) { - runStringify(data, resolve, reject); +}; + +function stringifyPromise(data, options) { + return new Promise(function(resolve, reject) { + runStringify(data, options, resolve, reject); + }); +}; + +function stringifyNode(data, options, callback) { + // the inner callbacks are wrapped in asap to prevent an error potentially + // being thrown by invoking the callback being handled by an outer caller + runStringify(data, options, function (value) { + asap(function () { + callback(null, value); }); - }; - - function stringifyNode(data, callback) { - // the inner callbacks are wrapped in asap to prevent an error potentially - // being thrown by invoking the callback being handled by an outer caller - runStringify(data, function (value) { - asap(function () { - callback(null, value); - }); - }, function (error) { - asap(function () { - callback(error); - }); + }, function (error) { + asap(function () { + callback(error); }); - }; - - /** - * Asynchronously convert a JavaScript object to JSON. - * If any functions are supplied in the data, it will be invoked. - * If the function has 0 parameters, it will be invoked and treated as synchronous, its return value being its replacement. - * Otherwise, the first parameter is assumed to be a callback which should be invoked as callback(error, result) - * - * @param {Any} data Any JavaScript object. - * @param {Function or null} callback A callback that takes an error and the result as parameters. - * @api public - * @return {Promise or undefined} If a callback is not provided, a Promise will be returned. - * - * @example stringify({some: "data"}, function(err, value) { if (err) { throw err; } value === '{"some":"data"}' }) - * @example stringify({some: "data"}.then(function(value) { assert(value === '{"some":"data"}') }) - */ - exports.stringify = function(data, callback) { - if (callback == null) { - return stringifyPromise(data); - } else { - return stringifyNode(data, callback); - } - }; -}(exports || (this.asyncJSON = {}))); \ No newline at end of file + }); +}; diff --git a/lib/browser-polyfills.js b/lib/browser-polyfills.js new file mode 100644 index 0000000..d419fe7 --- /dev/null +++ b/lib/browser-polyfills.js @@ -0,0 +1,13 @@ +'use strict'; + +if (typeof JSON === 'undefined' || !JSON || !JSON.stringify) { + // older browsers + throw new Error("The json2.js file must be included before async-json.js"); +} + +module.exports = { + asap: require('./polyfill-asap'), + getKeys: require('./polyfill-get-keys'), + isArray: require('./polyfill-is-array'), + jsonStringify: JSON.stringify +}; diff --git a/lib/counter.js b/lib/counter.js new file mode 100644 index 0000000..a10a0ed --- /dev/null +++ b/lib/counter.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = Counter; + +function Counter(limit) { + this.count = 0; + this.limit = limit; +} + +Counter.prototype.inc = function() { + if (++this.count >= this.limit) { + this.count = 0; + return true; + } else { + return false; + } +}; diff --git a/lib/polyfill-asap.js b/lib/polyfill-asap.js new file mode 100644 index 0000000..38afa5a --- /dev/null +++ b/lib/polyfill-asap.js @@ -0,0 +1,14 @@ +'use strict'; + +var asap; +if (typeof setImmediate === 'function') { + asap = setImmediate; +} else if (typeof process !== 'undefined' && typeof process.nextTick === 'function') { + asap = process.nextTick +} else { + asap = function asap(callback) { + setTimeout(callback, 0); + }; +} + +module.exports = asap; diff --git a/lib/polyfill-get-keys.js b/lib/polyfill-get-keys.js new file mode 100644 index 0000000..23a14b1 --- /dev/null +++ b/lib/polyfill-get-keys.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = Object.keys || (function() { + // older browsers + var has = Object.prototype.hasOwnProperty || function hasOwnProperty() { + // Object.prototype.hasOwnProperty should really always exist. + return true; + }; + + return function getKeys(obj) { + var result = []; + for (var key in obj) { + if (has.call(obj, key)) { + result.push(key); + } + } + return result; + }; +}()); diff --git a/lib/polyfill-is-array.js b/lib/polyfill-is-array.js new file mode 100644 index 0000000..8146c12 --- /dev/null +++ b/lib/polyfill-is-array.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = Array.isArray || function isArray(o) { + // older browsers + return Object.prototype.toString.call(o) === '[object Array]'; +}; diff --git a/lib/stringify-array.js b/lib/stringify-array.js new file mode 100644 index 0000000..7e5d6ab --- /dev/null +++ b/lib/stringify-array.js @@ -0,0 +1,78 @@ +'use strict'; + +var polyfills = require('./browser-polyfills'); +var asap =polyfills.asap; + +module.exports = internalStringifyArray; + +/** + * Stringify an array + * + * @param {Function} stringify the top-level stringify function. + * @param {Array} array The array to stringify. + * @param {Function} callback The callback to invoke when completed the stringification. + * @api private + * + * @example internalStringifyArray([1, 2, 3], function(err, value) { value === "[1,2,3]"; }); + */ + function internalStringifyArray(stringify, array, callback, errback, counter) { + var len = array.length; + if (len === 0) { + callback("[]"); + return; + } + + // buffer is our ultimate return value + var buffer = "["; + + function step(n) { + if (n === len) { + // we're done + buffer += "]"; + callback(buffer); + return false; + } + + var synchronous = true; + var completedSynchronously = false; + // asynchronously stringify the nth element. + stringify(array[n], function(value) { + if (n > 0) { + buffer += ","; + } + + if (value === undefined) { + // JSON.stringify turns bad values in arrays into null, so we need to as well + buffer += "null"; + } else { + buffer += value; + } + + // go to the next element + if (counter.inc()) { + asap(function() { + run(n + 1); + }); + } else if (synchronous) { + completedSynchronously = true; + } else { + run(n + 1); + } + }, errback, String(n), counter, array); + synchronous = false; + return completedSynchronously; + } + + function run(start) { + try { + for (var i = start; step(i); ++i) { + // nothing to do, as step's return value will cause a halt eventually + } + } catch (e) { + errback(e); + } + } + + // let's pump, starting at index 0 + run(0); +} diff --git a/lib/stringify-object.js b/lib/stringify-object.js new file mode 100644 index 0000000..edcfce0 --- /dev/null +++ b/lib/stringify-object.js @@ -0,0 +1,89 @@ +'use strict'; + +var polyfills = require('./browser-polyfills'); +var getKeys = polyfills.getKeys; +var asap =polyfills.asap; +var jsonStringify = polyfills.jsonStringify; + +module.exports = internalStringifyObject; + +/** + * Stringify an object + * + * @param {Function} stringify the top-level stringify function. + * @param {Object} object The object to stringify. + * @param {Function} callback The callback to invoke when completed the stringification. + * @api private + * + * @example internalStringifyObject({alpha: 1, bravo: 2}, function(err, value) { value === '{"alpha":1,"bravo":2}'; }); + */ +function internalStringifyObject(stringify, object, callback, errback, counter) { + // getKeys _should_ be a reference to Object.keys + // JSON.stringify gets the keys in the same order as this, but that is arbitrary. + var keys = getKeys(object); + var len = keys.length; + if (len === 0) { + callback("{}"); + return; + } + + // whether or not we've placed the first element in yet. + // can't rely on i === 0, since we might skip it if the value === undefined. + var first = true; + + // buffer is our ultimate return value + var buffer = "{"; + + function step(n) { + if (n === len) { + buffer += "}"; + callback(buffer); + return false; + } + + var synchronous = true; + var completedSynchronously = false; + var key = keys[n]; + // asynchronously stringify the nth element in our list of keys + stringify(object[key], function(value) { + // if we get an undefined, rather than placing in null like the array does, we just skip it. + if (value !== undefined) { + if (first) { + first = false; + } else { + buffer += ","; + } + + buffer += jsonStringify(key); + buffer += ":"; + buffer += value; + } + + // go to the next key + if (counter.inc()) { + asap(function() { + run(n + 1); + }); + } else if (synchronous) { + completedSynchronously = true; + } else { + run(n + 1); + } + }, errback, key, counter, object); + synchronous = false; + return completedSynchronously; + }; + + function run(start) { + try { + for (var i = start; step(i); ++i) { + // nothing to do, as step's return value will cause a halt eventually + } + } catch (e) { + errback(e); + } + }; + + // let's pump, starting at index 0 + run(0); +} diff --git a/lib/stringify.js b/lib/stringify.js new file mode 100644 index 0000000..bf8627c --- /dev/null +++ b/lib/stringify.js @@ -0,0 +1,78 @@ +'use strict'; + +var Counter = require('./counter'); +var stringifyArray = require('./stringify-array'); +var stringifyObject = require('./stringify-object'); +var polyfills = require('./browser-polyfills'); +var isArray = polyfills.isArray; +var asap =polyfills.asap; +var jsonStringify = polyfills.jsonStringify; + +module.exports = runStringify; + +var DEFAULT_COUNTER_LIMIT = 200; +function runStringify(data, options, callback, errback) { + asap(function() { + stringify(data, callback, errback, undefined, new Counter(options.limit || DEFAULT_COUNTER_LIMIT), undefined); + }); +} + +function stringify(data, callback, errback, key, counter, context) { + if (!counter) { + throw new Error("Expected counter"); + } + try { + stringifyAux(data, callback, errback, key, counter, context); + } catch (err) { + errback(err); + } +} + +function stringifyAux(data, callback, errback, key, counter, context) { + switch (typeof data) { + case "object": + if (data === null) { + callback("null"); + } else { + var then = data.then; + if (typeof then === 'function') { + then.call(data, function(value) { + stringify(value, callback, errback, key, counter, undefined); + }, errback); + } else if (typeof data.toJSON === "function") { + // used by Date and possibly some others. + callback(jsonStringify(data.toJSON(key))); + } else if (isPrimitiveConstructor(data.constructor)) { + // horrible, someone used the new String(), new Number(), or new Boolean() syntax. + callback(jsonStringify(data)); + } else if (isArray(data)) { + stringifyArray(stringify, data, callback, errback, counter); + } else { + stringifyObject(stringify, data, callback, errback, counter); + } + } + break; + case "function": + if (data.length === 0) { + // assume a sync function that returns a value + stringify(data.call(context), callback, errback, key, counter, undefined); + } else { + // assume an async function that takes a callback + data.call(context, function(err, value) { + if (err) { + errback(err); + } else { + stringify(value, callback, errback, key, counter, undefined); + } + }); + } + break; + default: + callback(jsonStringify(data)); + break; + } +} + +function isPrimitiveConstructor(ctor) { + return ctor === String || ctor === Number || ctor === Boolean; +} diff --git a/package.json b/package.json index 3d3e9a3..1d8040e 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,27 @@ { "name": "async-json", - "version": "0.0.2", + "version": "1.0.0", "description": "An asynchronous version of JSON.stringify", - "keywords": ["json", "stringify", "async"], + "keywords": [ + "json", + "stringify", + "async" + ], + "main": "index", "repository": "git://github.com/ckknight/async-json.git", "author": "Cameron Kenneth Knight (http://ckknight.com)", - "dependencies": {}, - "main": "index" -} \ No newline at end of file + "dependencies": { + }, + "devDependencies": { + "assert": "^1.3.0", + "bluebird": "^2.10.0", + "browserify": "^11.1.0", + "expresso": "^0.9.2" + }, + "scripts": { + "browserify": "./node_modules/.bin/browserify ./index.js -o ./dist/async-json.js", + "prepublish": "npm run browserify", + "unit": "node_modules/expresso/bin/expresso ./test/*.test.js", + "test": "npm run unit" + } +} diff --git a/support/expresso b/support/expresso deleted file mode 160000 index e8f4476..0000000 --- a/support/expresso +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e8f44768a9553cfcb75b219287bc498d077dd0b9