pipette
Version:
Stream and pipe utilities for Node
273 lines (221 loc) • 6.72 kB
JavaScript
// Copyright 2012 The Obvious Corporation.
/*
* A stream filter which re-emits data it receives in fixed-size chunks.
*/
/*
* 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 Valve = require("./valve").Valve;
/*
* Module variables
*/
/** Options spec */
var OPTIONS = {
// standard
encoding: {},
incomingEncoding: {},
paused: {},
// extra
allowMultiple: {},
ifPartial: {},
size: {}
};
/*
* Helper functions
*/
/**
* Construct a Dropper state object.
*/
function State(emitter, source, options) {
/** Outer emitter. */
this.emitter = emitter;
/**
* Upstream source, wrapped in a Valve to provide saner semantics as
* well as the actual implementation of `pause()`, `resume()` and
* `setIncomingEncoding()`.
*/
this.source = source = new Valve(source);
/** Desired size of each emitted block, in bytes. */
this.blockSize = options.size;
/** Whether multiples of the block size are allowed. */
this.allowMultiple = options.allowMultiple;
/** What to do with a partial block at the end of the stream. */
this.ifPartial = options.ifPartial;
/** The encoding to use when emitting events. */
this.decoder = new Codec();
/** Current pending data (if any) */
this.pendingData = undefined;
// We `bind()` the event listener callback methods, so that they
// get an appropriate `this` when they're called during event
// emission.
Object.defineProperty(this, 'onCloseOrEnd', { value: this.onCloseOrEnd.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 });
source.on(consts.CLOSE, this.onCloseOrEnd);
source.on(consts.DATA, this.onData);
source.on(consts.END, this.onCloseOrEnd);
source.on(consts.ERROR, this.onError);
}
State.prototype.destroy = function destroy() {
// Destroy the source (our private Valve), but don't un-define it,
// so that other calls (particularly `isReadable()`) can work
// straightforwardly.
this.source.destroy();
this.emitter = undefined;
}
/**
* Emits a `data` event for the given payload. This does decoding
* if necessary.
*/
State.prototype.emitData = function emitData(data) {
this.emitter.emit(consts.DATA, this.decoder.decode(data));
}
/**
* Emits final `data` event (if any), then the end event sequence. Note:
* `gotError` is needed as separate from `error` because we might have
* received an `error` event with `undefined` payload.
*/
State.prototype.emitFinalEvents = function emitFinalEvents(gotError, error) {
if (!this.emitter) {
// This may be due to an extra event that squeaked by.
return;
}
var data = this.pendingData;
if (data && (data.length !== 0)) {
switch (this.ifPartial) {
case consts.EMIT: {
this.emitData(data);
break;
}
case consts.ERROR: {
if (!gotError) {
// There was no "larger" error, so just indicate the short
// buffer as the error.
gotError = true;
error = new Error("Partial buffer at end.");
}
break;
}
case consts.IGNORE: {
// Do nothing.
break;
}
case consts.PAD: {
var newData = new Buffer(this.blockSize);
data.copy(newData);
newData.fill(0, data.length);
this.emitData(newData);
break;
}
}
}
var emitter = this.emitter;
if (gotError) {
emitter.emit(consts.ERROR, error);
} else {
emitter.emit(consts.END);
}
emitter.emit(consts.CLOSE);
this.destroy();
}
State.prototype.isReadable = function isReadable() {
return this.source.readable;
}
State.prototype.onCloseOrEnd = function onCloseOrEnd() {
this.emitFinalEvents(false);
}
State.prototype.onData = function onData(data) {
var pendingData = this.pendingData;
if (pendingData && (pendingData.length !== 0)) {
if (data.length == 0) {
// Empty incoming data. Nothing more to do.
return;
}
// Append the new data to the pending data.
var newLength = pendingData.length + data.length;
var newData = new Buffer(newLength);
pendingData.copy(newData);
data.copy(newData, pendingData.length);
data = newData;
this.pendingData = undefined;
}
var length = data.length;
var blockSize = this.blockSize;
if (length < blockSize) {
// Not enough data to emit anything.
this.pendingData = data;
return;
}
// Save the part of the data that won't be emitted (if any) as
// `pendingData` ready for the next `data` event.
var leftoverLength = length % blockSize;
if (leftoverLength !== 0) {
length -= leftoverLength;
this.pendingData = data.slice(length);
data = data.slice(0, length);
}
if (this.allowMultiple) {
// Just emit a single `data` event for all the data.
this.emitData(data);
} else {
// Emit as many `data` events as are needed.
for (var i = 0; i < length; i += blockSize) {
this.emitData(data.slice(i, i + blockSize));
}
}
}
State.prototype.onError = function onError(error) {
this.emitFinalEvents(true, error);
}
Object.freeze(State);
Object.freeze(State.prototype);
/*
* Exported bindings
*/
/**
* Constructs a Dropper instance, which reemits data in fixed-size
* blocks (aka drops). Dropper instances are in turn instances of
* `stream.Stream`.
*/
function Dropper(source, options) {
options = opts.validate(options, OPTIONS);
stream.Stream.call(this);
this.dropper = sealer.seal(new State(this, source, options));
opts.handleCommon(options, this);
}
util.inherits(Dropper, stream.Stream);
Dropper.prototype.destroy = function destroy() {
sealer.unseal(this.dropper).destroy();
};
Dropper.prototype.pause = function pause() {
sealer.unseal(this.dropper).source.pause();
};
Dropper.prototype.resume = function resume() {
sealer.unseal(this.dropper).source.resume();
};
Dropper.prototype.setEncoding = function setEncoding(name) {
sealer.unseal(this.dropper).decoder.setEncoding(name);
};
Dropper.prototype.setIncomingEncoding = function setIncomingEncoding(name) {
sealer.unseal(this.dropper).source.setIncomingEncoding(name);
};
Object.defineProperty(
Dropper.prototype,
"readable",
{
get: function() { return sealer.unseal(this.dropper).isReadable(); },
enumerable: true
});
Object.freeze(Dropper);
Object.freeze(Dropper.prototype);
module.exports = {
Dropper: Dropper
};