promistreamus
Version:
Convert Stream into an Iterator yielding value promises
185 lines (172 loc) • 6.55 kB
JavaScript
/*!
* promistreamus - Convert Stream into an Iterator yielding value promises
* Copyright (c) 2015 Yuri Astrakhan <YuriAstrakhan@gmail.com>
* MIT Licensed
* Author: YuriAstrakhan@gmail.com
*/
var Promise = require('bluebird'),
undefinedPromise = Promise.resolve(undefined);
/**
* BlueBird has made Promise.pending() obsolete, but that makes working with streams
* very difficult, so introducing a simple workaround.
* @constructor
*/
function Deferred() {
var self = this;
self.promise = new Promise(function (resolve, reject) {
self.resolve = resolve;
self.reject = reject;
});
}
/**
* Transform stream into an iterator that yields Promises.
* @param {Stream|Function} streamOrFunc function that will return a stream object, or promise of a stream. If missing, the resulting
* object needs to be initialized with init() call
* @param {Function} [selectorFunc] optional function that can convert data that came from the stream into the promise value.
* If undefined is returned, the value will not be yielded.
* @returns {Function} Iterator function that will produce a Promise each time it is called.
Streaming may be stoped by calling stop() on the returned value.
*/
module.exports = function(streamOrFunc, selectorFunc) {
var readablePromise = new Deferred(),
initPromise,
isDone = false,
error, stream;
var prepareStream = function (strm) {
stream = strm
.on('readable', function () {
// Notify waiting promises that data is available,
// and create a new one to wait for the next chunk of data
readablePromise.resolve(true);
readablePromise = new Deferred();
})
.on('end', function () {
isDone = true;
readablePromise.resolve(true);
})
.on('error', function (err) {
error = err;
readablePromise.reject(err);
});
initPromise = undefined;
};
var p;
if (!streamOrFunc) {
initPromise = new Deferred();
p = initPromise.promise.then(function(streamOrFunc) {
return typeof streamOrFunc === "function" ? streamOrFunc() : streamOrFunc;
}).then(prepareStream);
} else if (typeof streamOrFunc === "function") {
p = Promise.try(streamOrFunc).then(prepareStream);
} else {
p = Promise.try(function () {
return prepareStream(streamOrFunc)
});
}
p.catch(function (err) {
error = err;
readablePromise.reject(err);
});
var readStream = function () {
if (error) {
// Error out right away, without exhausting the stream
throw error;
} else if (!stream) {
return undefined;
}
var value;
while ((value = stream.read()) !== null) {
res = selectorFunc ? selectorFunc(value) : value;
if (res === undefined)
continue;
return res;
}
return undefined;
};
var iterator = function () {
if (isDone) {
return undefinedPromise;
}
var res = !initPromise
? Promise.try(readStream)
: initPromise.promise.then(readStream);
return res
.then(function (value) {
return value === undefined ? readablePromise.promise.then(readStream) : value;
})
.then(function (value) {
if (value !== undefined || isDone) {
return value;
}
// If we are here, the current promise has been triggered,
// but by now other "threads" have consumed all buffered rows,
// so start waiting for the next one
// Note: there is a minor inefficiency here - readStream is called twice in a value, but its a rare case
return iterator();
});
};
if (initPromise) {
// If streamOrFunc has not been given, allow future initialization
iterator.init = function(streamOrFunc) {
initPromise.resolve(streamOrFunc);
delete iterator.init; // Single invocation only
};
}
return iterator;
};
/**
* Converts and filters all values of an iterator using the converter function.
* If converter returns undefined, the value is skipped.
* NOTE: This function might not work as expected if the next value is requested before the previous is resolved.
* @param {Function} iterator a promistreamus-style iterator function
* @param {Function} converter a function that takes a value and returns a value or a promise of a value.
* If the result resolves as undefined, it will be skipped.
* @returns {Function} a promistreamus-style iterator function
*/
module.exports.select = function(iterator, converter) {
var doneValue = false;
var getNextValAsync = function () {
return doneValue || iterator().then(function (val) {
if (val === undefined) {
doneValue = undefinedPromise;
return undefined;
}
var newVal = converter(val);
if (newVal === undefined) {
return getNextValAsync();
}
return newVal;
});
};
return getNextValAsync;
};
/**
* Flatten multiple promistreamus iterators of items into one iterator of items
* @param {Function} iterator is a "stream of streams" function - each call to it must return a Promise of an iterator function.
* @returns {Function} a promistreamus-style iterator function
*/
module.exports.flatten = function(iterator) {
var subIterator, doneValue;
var getNextValAsync = function() {
if (!subIterator) {
subIterator = Promise.try(iterator);
}
var currentSubIterator = subIterator;
return doneValue || currentSubIterator.then(function(iter) {
if (!iter) {
doneValue = undefinedPromise;
return undefined;
}
return iter().then(function(val) {
if (val !== undefined) {
return val;
}
if (currentSubIterator === subIterator) {
subIterator = Promise.try(iterator);
}
return getNextValAsync();
});
});
};
return getNextValAsync;
};