UNPKG

pipette

Version:

Stream and pipe utilities for Node

450 lines (371 loc) 11.7 kB
// Copyright 2012 The Obvious Corporation. /* * A readable stream sink which provides incremental read() functionality. */ /* * Modules used */ "use strict"; var assert = require("assert"); var typ = require("typ"); var consts = require("./consts"); var Codec = require("./codec").Codec; var errors = require("./errors"); var opts = require("./opts"); var sealer = require("./sealer"); var streamsanity = require("./streamsanity"); /* * Module variables */ /** Options spec */ var OPTIONS = { incomingEncoding: {} }; /* * Helper functions */ /** * Constructs a pending read object. Arguments are akin to * those from `fs.read()`, except: * * * If `buffer` is unspecified, the callback will provide a * freshly-allocated buffer for the result (with `offset` being * ignored). * * * If `length` is unspecified, it is taken to mean "as much as * possible without blocking". In the case of a defined `buffer`, * this will read up to as much will actually fit in the buffer. */ function PendingRead(buffer, offset, length, callback) { this.buffer = buffer; this.offset = offset; this.length = length; this.callback = callback; } /** * Tries to trigger this pending read. If `force` is `true`, this * indicates the triggering should be forced, even if there isn't * sufficient buffered data; this is used when a stream has ended, * to allow a single partial read at the end. * * This method returns a boolean indicating whether the trigger * actually fired. */ PendingRead.prototype.trigger = function trigger(state, force) { // Grab these as locals, to make them saner to reference and // safe to modify. var buffer = this.buffer; var offset = this.offset; var length = this.length; var buffers = state.buffers; var bufferedLength = state.bufferedLength; var error = false; // High-order bit #1: If the request is for zero bytes, satisfy it // trivially. We do this before the error check, because it makes // sense to let such requests succeed, even in the face of a pending // error. if (length === 0) { this.callCallback(false, 0, buffer || new Buffer(0), offset); return true; } // High-order bit #2: If the slicer is in an error state and there's // nothing to read first, report the error. if ((bufferedLength === 0) && (state.error !== consts.NO_ERROR)) { this.callCallback(true, 0, buffer || new Buffer(0), offset); return true; } // Figure out how much to read, at most, returning if it turns // out the read() can't complete. if (typ.isUndefined(length)) { length = bufferedLength; } if (buffer) { length = Math.min(length, buffer.length - this.offset); } if (length > bufferedLength) { if (!force) { // Not enough data for this read() to trigger. return false; } // We're being forced; just read what's available. length = bufferedLength; error = true; } // Copy the right amount out of the pending buffers. if (!buffer) { buffer = new Buffer(length); offset = 0; } var origOffset = offset; // for callback and to figure out the final length var endOffset = offset + length; while ((offset < endOffset) && (bufferedLength > 0)) { var one = buffers[0]; var oneLength = buffers[0].length; if (oneLength <= length) { // Consume the entire pending buffer, since the read() wants // at least that much. one.copy(buffer, offset); buffers.shift(); } else { // Copy just what's needed to satisfy the read(), and slice() // the remainder to be ready for the next request. one.copy(buffer, offset, 0, length); buffers[0] = one.slice(length); oneLength = length; } offset += oneLength; length -= oneLength; bufferedLength -= oneLength; } state.bufferedLength = bufferedLength; // Write back modified value. this.callCallback(error, endOffset - origOffset, buffer, origOffset); return true; } /** * Calls the `callback` with the given arguments and with *no* defined * `this`. */ PendingRead.prototype.callCallback = function callCallback(error, count, buffer, offset) { this.callback.call(undefined, error, count, buffer, offset); } Object.freeze(PendingRead); Object.freeze(PendingRead.prototype); /** * Constructs a Slicer state object. */ function State(source) { streamsanity.validateSource(source); /** upstream source (must be stream-like, if not actually a Stream) */ this.source = source; /** event receipt encoding handler */ this.encoder = new Codec(); /** queue of pending read operations */ this.pendingReads = []; /** incrementally collected buffers, yet to be read */ this.buffers = []; /** number of bytes of data currently in `buffers` */ this.bufferedLength = 0; /** error payload that terminated the stream */ this.error = consts.NO_ERROR; /** whether the stream has ended */ this.ended = false; // We `bind()` the event listener callback methods, so that they // get an appropriate `this` when they're called during event // emission. Object.defineProperty(this, 'onClose', { value: this.onClose.bind(this), enumerable: true }); Object.defineProperty(this, 'onData', { value: this.onData.bind(this), enumerable: true }); Object.defineProperty(this, 'onEnd', { value: this.onEnd.bind(this), enumerable: true }); Object.defineProperty(this, 'onError', { value: this.onError.bind(this), enumerable: true }); source.on(consts.CLOSE, this.onClose); source.on(consts.DATA, this.onData); source.on(consts.END, this.onEnd); source.on(consts.ERROR, this.onError); } /** * Acts as if the upstream source has been closed. */ State.prototype.destroy = function destroy() { this.detach(); this.triggerIfPossible(); this.buffers = undefined; }; /** * Detaches this instance from its upstream source, marking the * instance as "ended". */ State.prototype.detach = function detach() { var source = this.source; if (source) { source.removeListener(consts.CLOSE, this.onClose); source.removeListener(consts.DATA, this.onData); source.removeListener(consts.END, this.onEnd); source.removeListener(consts.ERROR, this.onError); this.source = undefined; } this.ended = true; } /** * Triggers as many pending reads as possible, given the current * state of affairs. */ State.prototype.triggerIfPossible = function triggerIfPossible() { var pendingReads = this.pendingReads; var force = this.ended; while ((pendingReads.length > 0) && pendingReads[0].trigger(this, force)) { pendingReads.shift(); } } State.prototype.isReadable = function isReadable() { return !(this.ended && (this.bufferedLength === 0)); } State.prototype.onClose = function onClose(info) { if (this.ended) { return; } if (errors.isErrorish(info)) { this.error = info; } this.detach(); this.triggerIfPossible(); } State.prototype.onData = function onData(data) { if (this.ended) { return; } var encoded = this.encoder.encode(data); this.buffers.push(encoded); this.bufferedLength += encoded.length; this.triggerIfPossible(); } State.prototype.onEnd = function onEnd() { if (this.ended) { return; } this.detach(); this.triggerIfPossible(); } State.prototype.onError = function onError(error) { if (this.ended) { return; } this.error = error; this.detach(); this.triggerIfPossible(); } /** * Queues up a pending read. */ State.prototype.queue = function queue(pendingRead) { var pendingReads = this.pendingReads; // for ease of reference if (pendingReads.length === 0) { // This one would be the first in the queue. Try to trigger it, in // case it's already satisfiable. If it triggers, just return // instead of allowing it to be added to the queue. The second // argument is passed as `this.ended` to force triggering when the // underlying stream has ended. if (pendingRead.trigger(this, this.ended)) { return; } } pendingReads.push(pendingRead); } /** * Internal generic-form `read()`. `buffer` and `length` may be * `undefined`. It is up to the callers (below) to do argument * sanitizing / type checking. * * * If `buffer` is `undefined`, then the callback will get a * freshly-allocated buffer. * * * If `length` is `undefined`, it means "read as much as possible * without blocking" up to the allowed length given the `buffer`, or * with no limit at all if `buffer` is `undefined`. * * * If `length` is defined, then the callback will only fire either * when the indicated number of bytes is available *or* the stream * has ended and there is no more data to be read. */ State.prototype.read = function read(buffer, offset, length, callback) { if (buffer) { var bufferLength = buffer.length; assert.ok((offset >= 0) && (offset < bufferLength)); if (length) { var endOffset = offset + length; assert.ok((length >= 0) && (endOffset <= bufferLength)); } } this.queue(new PendingRead(buffer, offset, length, callback)); } Object.freeze(State); Object.freeze(State.prototype); /* * Exported bindings */ /** * Constructs a Slicer instance, which collects data events coming * from the indicated source stream. Slicers in turn provide a * `read(..., callback)` interface for consuming the so-collected * data. */ function Slicer(source, options) { options = opts.validate(options, OPTIONS); this.slicer = sealer.seal(new State(source)); opts.handleCommon(options, this); } /** * Destroys this instance, disconnecting it from its upstream source. */ Slicer.prototype.destroy = function destroy() { sealer.unseal(this.slicer).destroy(); }; /** * Sets the incoming encoding. This is how to interpret strings that are * received as the payloads of `data` events. */ Slicer.prototype.setIncomingEncoding = function setIncomingEncoding(name) { sealer.unseal(this.slicer).encoder.setEncoding(name); } /** * Similar to streams, this is `true` as long as there is potentially * more data to be read. */ Object.defineProperty( Slicer.prototype, "readable", { get: function() { return sealer.unseal(this.slicer).isReadable(); }, enumerable: true }); /** * Gets the error that ended the upstream data, if any. */ Slicer.prototype.getError = function getError() { var error = sealer.unseal(this.slicer).error; return (error === consts.NO_ERROR) ? undefined : error; } /** * Gets whether or not the stream ended with an error. */ Slicer.prototype.gotError = function gotError() { return sealer.unseal(this.slicer).error !== consts.NO_ERROR; } /** * Queues up a read operation to yield a freshly-allocated buffer. */ Slicer.prototype.read = function read(length, callback) { typ.assertUInt(length); typ.assertFunction(callback); sealer.unseal(this.slicer).read(undefined, 0, length, callback); } /** * Queues up a read operation for "as much data as possible", to yield * a freshly-allocated buffer. */ Slicer.prototype.readAll = function readAll(callback) { typ.assertFunction(callback); sealer.unseal(this.slicer).read(undefined, 0, undefined, callback); } /** * Queues up a specific-buffer read operation. */ Slicer.prototype.readInto = function readInto(buffer, offset, length, callback) { typ.assertBuffer(buffer); if (typ.isUndefined(offset)) { offset = 0; } else { typ.assertUInt(offset); } if (typ.isDefined(length)) { typ.assertUInt(length); } typ.assertFunction(callback); sealer.unseal(this.slicer).read(buffer, offset, length, callback); } Object.freeze(Slicer); Object.freeze(Slicer.prototype); module.exports = { Slicer: Slicer };