| 'use strict' | 
|   | 
| const check = require('check-types') | 
| const EventEmitter = require('events').EventEmitter | 
| const events = require('./events') | 
| const promise = require('./promise') | 
|   | 
| const invalidTypes = { | 
|   undefined: true, // eslint-disable-line no-undefined | 
|   function: true, | 
|   symbol: true | 
| } | 
|   | 
| module.exports = eventify | 
|   | 
| /** | 
|  * Public function `eventify`. | 
|  * | 
|  * Returns an event emitter and asynchronously traverses a data structure | 
|  * (depth-first), emitting events as it encounters items. Sanely handles | 
|  * promises, buffers, maps and other iterables. The event emitter is | 
|  * decorated with a `pause` method that can be called to pause processing. | 
|  * | 
|  * @param data:       The data structure to traverse. | 
|  * | 
|  * @option promises:  'resolve' or 'ignore', default is 'resolve'. | 
|  * | 
|  * @option buffers:   'toString' or 'ignore', default is 'toString'. | 
|  * | 
|  * @option maps:      'object' or 'ignore', default is 'object'. | 
|  * | 
|  * @option iterables:  'array' or 'ignore', default is 'array'. | 
|  * | 
|  * @option circular:   'error' or 'ignore', default is 'error'. | 
|  * | 
|  * @option yieldRate:  The number of data items to process per timeslice, | 
|  *                     default is 16384. | 
|  * | 
|  * @option Promise:      The promise constructor to use, defaults to bluebird. | 
|  **/ | 
| function eventify (data, options = {}) { | 
|   const coercions = {} | 
|   const emitter = new EventEmitter() | 
|   const Promise = promise(options) | 
|   const references = new Map() | 
|   | 
|   let count = 0 | 
|   let disableCoercions = false | 
|   let ignoreCircularReferences | 
|   let ignoreItems | 
|   let pause | 
|   let yieldRate | 
|   | 
|   emitter.pause = () => { | 
|     let resolve | 
|     pause = new Promise(res => resolve = res) | 
|     return () => { | 
|       pause = null | 
|       count = 0 | 
|       resolve() | 
|     } | 
|   } | 
|   parseOptions() | 
|   setImmediate(begin) | 
|   | 
|   return emitter | 
|   | 
|   function parseOptions () { | 
|     parseCoercionOption('promises') | 
|     parseCoercionOption('buffers') | 
|     parseCoercionOption('maps') | 
|     parseCoercionOption('iterables') | 
|   | 
|     if (Object.keys(coercions).length === 0) { | 
|       disableCoercions = true | 
|     } | 
|   | 
|     if (options.circular === 'ignore') { | 
|       ignoreCircularReferences = true | 
|     } | 
|   | 
|     check.assert.maybe.positive(options.yieldRate) | 
|     yieldRate = options.yieldRate || 16384 | 
|   } | 
|   | 
|   function parseCoercionOption (key) { | 
|     if (options[key] !== 'ignore') { | 
|       coercions[key] = true | 
|     } | 
|   } | 
|   | 
|   function begin () { | 
|     return proceed(data) | 
|       .catch(error => emit(events.error, error)) | 
|       .then(() => emit(events.end)) | 
|   } | 
|   | 
|   function proceed (datum) { | 
|     if (++count % yieldRate !== 0) { | 
|       return coerce(datum).then(after) | 
|     } | 
|   | 
|     return new Promise((resolve, reject) => { | 
|       setImmediate(() => { | 
|         coerce(datum) | 
|           .then(after) | 
|           .then(resolve) | 
|           .catch(reject) | 
|       }) | 
|     }) | 
|   | 
|     function after (coerced) { | 
|       if (isInvalid(coerced)) { | 
|         return | 
|       } | 
|   | 
|       if (coerced === false || coerced === true || coerced === null) { | 
|         return literal(coerced) | 
|       } | 
|   | 
|       if (Array.isArray(coerced)) { | 
|         return array(coerced) | 
|       } | 
|   | 
|       const type = typeof coerced | 
|   | 
|       switch (type) { | 
|         case 'number': | 
|           return value(coerced, type) | 
|         case 'string': | 
|           return value(escapeString(coerced), type) | 
|         default: | 
|           return object(coerced) | 
|       } | 
|     } | 
|   } | 
|   | 
|   function coerce (datum) { | 
|     if (disableCoercions || check.primitive(datum)) { | 
|       return Promise.resolve(datum) | 
|     } | 
|   | 
|     if (check.instanceStrict(datum, Promise)) { | 
|       return coerceThing(datum, 'promises', coercePromise).then(coerce) | 
|     } | 
|   | 
|     if (check.instanceStrict(datum, Buffer)) { | 
|       return coerceThing(datum, 'buffers', coerceBuffer) | 
|     } | 
|   | 
|     if (check.instanceStrict(datum, Map)) { | 
|       return coerceThing(datum, 'maps', coerceMap) | 
|     } | 
|   | 
|     if ( | 
|       check.iterable(datum) && | 
|       check.not.string(datum) && | 
|       check.not.array(datum) | 
|     ) { | 
|       return coerceThing(datum, 'iterables', coerceIterable) | 
|     } | 
|   | 
|     if (check.function(datum.toJSON)) { | 
|       return Promise.resolve(datum.toJSON()) | 
|     } | 
|   | 
|     return Promise.resolve(datum) | 
|   } | 
|   | 
|   function coerceThing (datum, thing, fn) { | 
|     if (coercions[thing]) { | 
|       return fn(datum) | 
|     } | 
|   | 
|     return Promise.resolve() | 
|   } | 
|   | 
|   function coercePromise (p) { | 
|     return p | 
|   } | 
|   | 
|   function coerceBuffer (buffer) { | 
|     return Promise.resolve(buffer.toString()) | 
|   } | 
|   | 
|   function coerceMap (map) { | 
|     const result = {} | 
|   | 
|     return coerceCollection(map, result, (item, key) => { | 
|       result[key] = item | 
|     }) | 
|   } | 
|   | 
|   function coerceCollection (coll, target, push) { | 
|     coll.forEach(push) | 
|   | 
|     return Promise.resolve(target) | 
|   } | 
|   | 
|   function coerceIterable (iterable) { | 
|     const result = [] | 
|   | 
|     return coerceCollection(iterable, result, item => { | 
|       result.push(item) | 
|     }) | 
|   } | 
|   | 
|   function isInvalid (datum) { | 
|     const type = typeof datum | 
|     return !! invalidTypes[type] || ( | 
|       type === 'number' && ! isValidNumber(datum) | 
|     ) | 
|   } | 
|   | 
|   function isValidNumber (datum) { | 
|     return datum > Number.NEGATIVE_INFINITY && datum < Number.POSITIVE_INFINITY | 
|   } | 
|   | 
|   function literal (datum) { | 
|     return value(datum, 'literal') | 
|   } | 
|   | 
|   function value (datum, type) { | 
|     return emit(events[type], datum) | 
|   } | 
|   | 
|   function emit (event, eventData) { | 
|     return (pause || Promise.resolve()) | 
|       .then(() => emitter.emit(event, eventData)) | 
|       .catch(err => { | 
|         try { | 
|           emitter.emit(events.error, err) | 
|         } catch (_) { | 
|           // When calling user code, anything is possible | 
|         } | 
|       }) | 
|   } | 
|   | 
|   function array (datum) { | 
|     // For an array, collection:object and collection:array are the same. | 
|     return collection(datum, datum, 'array', item => { | 
|       if (isInvalid(item)) { | 
|         return proceed(null) | 
|       } | 
|   | 
|       return proceed(item) | 
|     }) | 
|   } | 
|   | 
|   function collection (obj, arr, type, action) { | 
|     let ignoreThisItem | 
|   | 
|     return Promise.resolve() | 
|       .then(() => { | 
|         if (references.has(obj)) { | 
|           ignoreThisItem = ignoreItems = true | 
|   | 
|           if (! ignoreCircularReferences) { | 
|             return emit(events.dataError, new Error('Circular reference.')) | 
|           } | 
|         } else { | 
|           references.set(obj, true) | 
|         } | 
|       }) | 
|       .then(() => emit(events[type])) | 
|       .then(() => item(0)) | 
|   | 
|     function item (index) { | 
|       if (index >= arr.length) { | 
|         if (ignoreThisItem) { | 
|           ignoreItems = false | 
|         } | 
|   | 
|         if (ignoreItems) { | 
|           return Promise.resolve() | 
|         } | 
|   | 
|         return emit(events.endPrefix + events[type]) | 
|           .then(() => references.delete(obj)) | 
|       } | 
|   | 
|       if (ignoreItems) { | 
|         return item(index + 1) | 
|       } | 
|   | 
|       return action(arr[index]) | 
|         .then(() => item(index + 1)) | 
|     } | 
|   } | 
|   | 
|   function object (datum) { | 
|     // For an object, collection:object and collection:array are different. | 
|     return collection(datum, Object.keys(datum), 'object', key => { | 
|       const item = datum[key] | 
|   | 
|       if (isInvalid(item)) { | 
|         return Promise.resolve() | 
|       } | 
|   | 
|       return emit(events.property, escapeString(key)) | 
|         .then(() => proceed(item)) | 
|     }) | 
|   } | 
|   | 
|   function escapeString (string) { | 
|     string = JSON.stringify(string) | 
|     return string.substring(1, string.length - 1) | 
|   } | 
| } |