conduit
Version: 
Evented pipelines.
251 lines (219 loc) • 9.88 kB
JavaScript
// Control-flow utilities.
var cadence = require('cadence')
// An evented message queue.
var Procession = require('procession')
// Ever increasing serial value with no maximum value.
var Monotonic = require('monotonic').asString
// Return the first not null-like value.
var coalesce = require('extant')
var Signal = require('signal')
var abend = require('abend')
var Interrupt = require('interrupt').createInterrupter('conduit/window')
var logger = require('prolific.logger').createLogger('conduit.window')
var Destructible = require('destructible')
function Window (destructible, options) {
    this.outbox = new Procession
    this.inbox = new Procession
    this._received = '0'
    this._sequence = '0'
    this._window = coalesce(options.window, 64)
    this._flush = Monotonic.add('0', this._window)
    this.destroyed = false
    destructible.destruct.wait(this, function () { this.destroyed = true })
    this._destructible = destructible
    this.outbox.pump(this, '_send').run(destructible.ephemeral('outbox'))
    this._outbox = new Procession
    this._socket = { outbox: new Procession, destructible: new Destructible() }
    this._reservoir = this._outbox.shifter()
    this._tracer = this._reservoir.shifter()
    this._socket.destructible.completed.unlatch()
    this._connection = 0
    this._id = coalesce(options.id)
    logger.trace('created', { id: this._id })
}
Window.prototype._connect = cadence(function (async, destructible, inbox, outbox, connection) {
    async(function () {
        // Shutdown our previous connections to a bi-directional pair.
        // TODO Possible race when two or more calls to `_connect` wait for the
        // previous socket to destruct.
        // TODO We could at least assert that `completed` has unlatched.
        this._socket.destructible.destroy()
        this._socket.destructible.completed.wait(async())
    }, function () {
        Interrupt.assert(connection == this._connection, 'suspected race condition', { id: this._id })
        // Read the new input into our `_receive` function.
        this._outbox = new Procession()
        destructible.durable('inbox', inbox.pump(this, '_receive'), 'destructible', null)
        destructible.durable('outbox', this._outbox.pump(outbox, 'enqueue'), 'destructible', null)
        var reservoir = this._reservoir
        this._reservoir = this._outbox.shifter()
        this._tracer = this._reservoir.shifter()
        var entry = reservoir.shift(), first = entry, last = null
        while (entry != null) {
            this._outbox.push(entry)
            last = entry
            entry = reservoir.shift()
        }
        logger.trace('connecting', { id: this._id, sequence: this._sequence, connection: connection, $first: first, $last: last })
        this._sendsToLog = 3
        this._socket.outbox.end()
        this._socket = { outbox: outbox, destructible: destructible, connection: connection }
    })
})
Window.prototype.connect = function (inbox, outbox) {
    var connection = ++this._connection
    this._destructible.ephemeral([ 'connect', connection ], this, '_connect', inbox, outbox, connection, null)
}
// We can shutdown our side of the window by running null through the window's
// outbox. The other side can shutdown down the inbox during normal operation,
// but if the connection to the other side is cut and not coming back, we need
// to be able to send a wrapped end of stream through the inbox. We don't have
// the counter on the the ohter side and our Procession queues are generally
// opaque, so we don't want to poke around in them for a counter.
//
// Thus, this is a way to hangup the inbox, but let's call it truncate.
// We need a hangup because we are resisting shutting down due to end of stream
// and rejecting messages that are out of order. We'd need to put an envelope
// with a `null` body right at the of the stream with the correct sequence. The
// queue doesn't really have a good way of looking at the input end, maybe it
// does, but I haven't used it. Could use it, it's just the head of the inbox,
// but we're filtering the inbox for our specific envelopes still, so we're not
// expecting the inbox to contain only content related to us, so we have to
// scan the inbox. Forget that. Let's just have a special control message.
Window.prototype.truncate = function () {
    this._socket.destructible.destroy()
    this._socket.destructible.completed.wait(this, function () {
        this.inbox.end()
    })
}
// Why does reconnect work? Well, one side is going to realize that the
// connection is broken and close it. If it is the client side then it will open
// a new connection and the server will know to replace it. It will destroy its
// Conduit and give the window to a new conduit. It will then send the reconnect
// message (or rebuffer or something) and the client will reply.
//
// If the server detects disconnection, then the client might keep on chatting
// with a half-open socket indefinately. We might want to add a keep-alive
// reciever that will destroy the socket, or we might decide to add keep-alive
// to this here, splitting it out only if we decide that we want to have
// alternative flow-control methods.
//
// Actually, for now we could have keep-alive as a seprate receiver. It has a
// Signal you can wire to destroy your Conduit. Simpler and we can optimize it
// away if it is too expensive. (Rather optimize Procession so we're not shy
// about creating pipelines.)
//
// Anyway, with a keep-alive, the server can disconnect and just chill. The
// client can timeout and then go through the reconnect. It is not going to
// empty it's queue until it gets a flush and it won't get one off the closed
// socket.
//
Window.prototype._receive = cadence(function (async, envelope) {
    if (envelope == null) {
        // Nothing to do really, it will have canceled our pump to this
        // function, so now we're going to wait for a reconnect.
    } else if (envelope.module == 'conduit/window') {
        switch (envelope.method) {
        case 'envelope':
            // If we've seen this one already, don't bother.
            if (Monotonic.compare(this._received, envelope.sequence) >= 0) {
                break
            }
            // We might lose an envelope. We're going to count on this being a
            // break where a conduit reconnect causes the messages to be resent
            // but we could probably request a replay ourselves.
            if (this._received != envelope.previous) {
                logger.trace('ahead', {
                    id: this._id,
                    previous: this._received,
                    connection: this._socket.connection,
                    $envelope: envelope
                })
            }
            Interrupt.assert(this._received == envelope.previous, 'ahead', {
                received: this._received,
                envelope: envelope
            })
            // Note the last received sequence.
            this._received = envelope.sequence
            // Send a flush if we've reached the end of a window.
            if (this._received == this._flush) {
                this._socket.outbox.push({
                    module: 'conduit/window',
                    method: 'flush',
                    sequence: this._received
                })
                this._flush = Monotonic.add(this._flush, this._window)
                logger.trace('flush', {
                    id: this._id,
                    connection: this._socket.connection,
                    sequence: this._received,
                    flush: this._flush
                })
            }
            // Forward the body which might actually be `null` end-of-stream.
            this.inbox.enqueue(envelope.body, async())
            break
        case 'flush':
            // Shift the messages that we've received off of the reservoir.
            logger.trace('flushing', {
                id: this._id,
                connection: this._socket.connection,
                $envelope: envelope
            })
            for (;;) {
                var peek = this._reservoir.peek()
                // TODO `peek` should never be `null`.
                Interrupt.assert(peek != null, 'null peek on flush')
                if (peek.sequence == envelope.sequence) {
                    break
                }
                this._reservoir.shift()
            }
            break
        }
    }
})
// Input into window from nested listener. It is wrapped in an envelope and
// added to a queue.
//
// TODO Place a nested `null` here. You may want to assert that you've shutdown
// the Window with a guard, or you can start to destroy the Window. Are
// end-of-stream and destruction separate concerns? Probably not, no.
//
Window.prototype._send = function (envelope) {
    this._outbox.push(envelope = {
        module: 'conduit/window',
        method: 'envelope',
        previous: this._sequence,
        sequence: this._sequence = Monotonic.increment(this._sequence, 0),
        body: envelope
    })
    var trace = this._tracer.shift()
    for (;;) {
        if (trace == null) {
            logger.trace('missing', {
                id: this._id,
                $envelope: envelope
            })
            break
        } else if (trace.sequence == envelope.sequence) {
            break
        }
        trace = this._tracer.shift()
    }
    if (this._sendsToLog != 0) {
        this._sendsToLog--
        logger.trace('send', {
            id: this._id,
            $envelope: envelope
        })
    }
    // When the nested stream ends, the underlying stream ends.
    if (envelope == null) {
        this._outbox.push(null)
    }
}
module.exports = cadence(function (async, destructible, options) {
    return new Window(destructible, coalesce(options, {}))
})