pipette
Version:
Stream and pipe utilities for Node
277 lines (221 loc) • 6.68 kB
JavaScript
// Copyright 2012 The Obvious Corporation.
/*
* A concatenation of readable streams.
*/
/*
* Modules used
*/
"use strict";
var stream = require("stream");
var typ = require("typ");
var util = require("util");
var Codec = require("./codec").Codec;
var consts = require("./consts");
var opts = require("./opts");
var sealer = require("./sealer");
var streamsanity = require("./streamsanity");
var Blip = require("./blip").Blip;
var Valve = require("./valve").Valve;
/*
* Module variables
*/
/** Options spec */
var OPTIONS = {
encoding: {},
incomingEncoding: {},
paused: {}
};
/*
* Helper functions
*/
/**
* Construct a Cat state object.
*/
function State(emitter, streams) {
/** Outer event emitter. */
this.emitter = emitter;
/** List of streams to re-emit, in order. */
this.streams = [];
/** Currently paused? */
this.paused = true;
/** Currently readable? */
this.readable = true;
/** Encoding to use when emitting events. */
this.decoder = new Codec();
// We `bind()` the event listener callback methods, so that they
// get an appropriate `this` when they're called during event
// emission.
Object.defineProperty(this, 'onEnd', { value: this.onEnd.bind(this), enumerable: true });
Object.defineProperty(this, 'onData', { value: this.onData.bind(this), enumerable: true });
Object.defineProperty(this, 'onError', { value: this.onError.bind(this), enumerable: true });
if (!Array.isArray(streams)) {
throw new Error("Invalid streams array.");
}
var specialBlip = undefined;
if (streams.length === 0) {
// To keep the logic simpler for what's otherwise a pernicious
// edge case, force there to always be at least one stream. In
// particular, when the stream array is otherwise empty, add
// an empty blip.
specialBlip = new Blip();
streams = [ specialBlip ];
}
for (var i = 0; i < streams.length; i++) {
var one = streams[i];
try {
streamsanity.validateSource(one);
} catch (ex) {
// Clarify with the index.
var message = ex.message.replace(/\.$/, ": index " + i);
throw new Error(message);
}
// Always make a valve around the given streams, so that we
// (a) get consistent event sequencing, and (b) can
// independently pause them without affecting other potential
// users of the stream. With regard to (b), that is to say
// it's a bad idea to try to be clever and do an `instanceof
// Valve` check, since that might mess up the client; they might
// be using a Valve for independent reasons.
one = new Valve(one, { paused: true });
one.on(consts.DATA, this.onData);
one.on(consts.END, this.onEnd);
one.on(consts.ERROR, this.onError);
this.streams.push(one);
}
if (specialBlip) {
specialBlip.resume();
}
}
/**
* Detach any streams that are left, and make this instance well and
* truly closed.
*/
State.prototype.destroy = function destroy() {
var streams = this.streams;
if (streams) {
for (var i = 0; i < streams.length; i++) {
streams[i].destroy();
}
}
this.emitter = undefined;
this.streams = undefined;
this.paused = false;
this.readable = false;
};
State.prototype.isReadable = function isReadable() {
return this.readable;
}
State.prototype.pause = function pause() {
if (this.paused || !this.readable) {
return;
}
this.paused = true;
this.streams[0].pause();
};
State.prototype.resume = function resume() {
if (!(this.paused && this.readable)) {
return;
}
this.paused = false;
this.streams[0].resume();
};
/**
* Sets the incoming encoding. Since we want to do the encoding as events
* are received from upstream, and because we can't control when they
* come, we have to iterate over all active upstream sources informing
* them of the encoding.
*/
State.prototype.setIncomingEncoding =
function setIncomingEncoding(encodingName) {
var streams = this.streams;
if (!streams) {
return;
}
for (var i = 0; i < streams.length; i++) {
streams[i].setIncomingEncoding(encodingName);
}
}
/**
* Any `end` event is taken to mean that we should move on to the next
* stream. Note: We can count on the Valve we wrap around each
* upstream source to consistently deliver eiter a single `end` or a
* single `error` event.
*/
State.prototype.onEnd = function onEnd() {
if (!this.readable) {
// We probably got here because of an event that arrived after an
// `error` (a race-like condition). Nothing to do but ignore it.
return;
}
var streams = this.streams;
streams[0].destroy();
streams.shift();
if (streams.length === 0) {
// We just finished the last stream: Emit `end` and `close`.
this.emitter.emit(consts.END);
this.emitter.emit(consts.CLOSE);
this.readable = false;
} else {
streams[0].resume();
}
}
State.prototype.onData = function onData(data) {
// Do the decoding-to-string at the moment the event is to be
// emitted, to capture the outgoing encoding at the time of actual
// emission.
this.emitter.emit(consts.DATA, this.decoder.decode(data));
}
/**
* An error in a sub-stream causes this instance to emit `error` and
* `close` events (in that order), and then stop.
*/
State.prototype.onError = function onError(error) {
this.emitter.emit(consts.ERROR, error);
this.emitter.emit(consts.CLOSE);
// Clean up and mark ourselves as closed / un-readable.
this.destroy();
}
Object.freeze(State);
Object.freeze(State.prototype);
/*
* Exported bindings
*/
/**
* Construct a Cat instance, which emits the `data` events it receives
* from any number of other `streams` (an array).
*/
function Cat(streams, options) {
options = opts.validate(options, OPTIONS);
stream.Stream.call(this);
this.cat = sealer.seal(new State(this, streams));
opts.handleCommon(options, this, true);
}
util.inherits(Cat, stream.Stream);
Cat.prototype.destroy = function destroy() {
sealer.unseal(this.cat).destroy();
};
Cat.prototype.pause = function pause() {
sealer.unseal(this.cat).pause();
};
Cat.prototype.resume = function resume() {
sealer.unseal(this.cat).resume();
};
Cat.prototype.setEncoding = function setEncoding(encodingName) {
sealer.unseal(this.cat).decoder.setEncoding(encodingName);
};
Cat.prototype.setIncomingEncoding =
function setIncomingEncoding(encodingName) {
sealer.unseal(this.cat).setIncomingEncoding(encodingName);
};
Object.defineProperty(
Cat.prototype,
"readable",
{
get: function() { return sealer.unseal(this.cat).isReadable(); },
enumerable: true
});
Object.freeze(Cat);
Object.freeze(Cat.prototype);
module.exports = {
Cat: Cat
};