| /*! |
| * on-finished |
| * Copyright(c) 2013 Jonathan Ong |
| * Copyright(c) 2014 Douglas Christopher Wilson |
| * MIT Licensed |
| */ |
| |
| 'use strict' |
| |
| /** |
| * Module exports. |
| * @public |
| */ |
| |
| module.exports = onFinished |
| module.exports.isFinished = isFinished |
| |
| /** |
| * Module dependencies. |
| * @private |
| */ |
| |
| var asyncHooks = tryRequireAsyncHooks() |
| var first = require('ee-first') |
| |
| /** |
| * Variables. |
| * @private |
| */ |
| |
| /* istanbul ignore next */ |
| var defer = typeof setImmediate === 'function' |
| ? setImmediate |
| : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) } |
| |
| /** |
| * Invoke callback when the response has finished, useful for |
| * cleaning up resources afterwards. |
| * |
| * @param {object} msg |
| * @param {function} listener |
| * @return {object} |
| * @public |
| */ |
| |
| function onFinished (msg, listener) { |
| if (isFinished(msg) !== false) { |
| defer(listener, null, msg) |
| return msg |
| } |
| |
| // attach the listener to the message |
| attachListener(msg, wrap(listener)) |
| |
| return msg |
| } |
| |
| /** |
| * Determine if message is already finished. |
| * |
| * @param {object} msg |
| * @return {boolean} |
| * @public |
| */ |
| |
| function isFinished (msg) { |
| var socket = msg.socket |
| |
| if (typeof msg.finished === 'boolean') { |
| // OutgoingMessage |
| return Boolean(msg.finished || (socket && !socket.writable)) |
| } |
| |
| if (typeof msg.complete === 'boolean') { |
| // IncomingMessage |
| return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable)) |
| } |
| |
| // don't know |
| return undefined |
| } |
| |
| /** |
| * Attach a finished listener to the message. |
| * |
| * @param {object} msg |
| * @param {function} callback |
| * @private |
| */ |
| |
| function attachFinishedListener (msg, callback) { |
| var eeMsg |
| var eeSocket |
| var finished = false |
| |
| function onFinish (error) { |
| eeMsg.cancel() |
| eeSocket.cancel() |
| |
| finished = true |
| callback(error) |
| } |
| |
| // finished on first message event |
| eeMsg = eeSocket = first([[msg, 'end', 'finish']], onFinish) |
| |
| function onSocket (socket) { |
| // remove listener |
| msg.removeListener('socket', onSocket) |
| |
| if (finished) return |
| if (eeMsg !== eeSocket) return |
| |
| // finished on first socket event |
| eeSocket = first([[socket, 'error', 'close']], onFinish) |
| } |
| |
| if (msg.socket) { |
| // socket already assigned |
| onSocket(msg.socket) |
| return |
| } |
| |
| // wait for socket to be assigned |
| msg.on('socket', onSocket) |
| |
| if (msg.socket === undefined) { |
| // istanbul ignore next: node.js 0.8 patch |
| patchAssignSocket(msg, onSocket) |
| } |
| } |
| |
| /** |
| * Attach the listener to the message. |
| * |
| * @param {object} msg |
| * @return {function} |
| * @private |
| */ |
| |
| function attachListener (msg, listener) { |
| var attached = msg.__onFinished |
| |
| // create a private single listener with queue |
| if (!attached || !attached.queue) { |
| attached = msg.__onFinished = createListener(msg) |
| attachFinishedListener(msg, attached) |
| } |
| |
| attached.queue.push(listener) |
| } |
| |
| /** |
| * Create listener on message. |
| * |
| * @param {object} msg |
| * @return {function} |
| * @private |
| */ |
| |
| function createListener (msg) { |
| function listener (err) { |
| if (msg.__onFinished === listener) msg.__onFinished = null |
| if (!listener.queue) return |
| |
| var queue = listener.queue |
| listener.queue = null |
| |
| for (var i = 0; i < queue.length; i++) { |
| queue[i](err, msg) |
| } |
| } |
| |
| listener.queue = [] |
| |
| return listener |
| } |
| |
| /** |
| * Patch ServerResponse.prototype.assignSocket for node.js 0.8. |
| * |
| * @param {ServerResponse} res |
| * @param {function} callback |
| * @private |
| */ |
| |
| // istanbul ignore next: node.js 0.8 patch |
| function patchAssignSocket (res, callback) { |
| var assignSocket = res.assignSocket |
| |
| if (typeof assignSocket !== 'function') return |
| |
| // res.on('socket', callback) is broken in 0.8 |
| res.assignSocket = function _assignSocket (socket) { |
| assignSocket.call(this, socket) |
| callback(socket) |
| } |
| } |
| |
| /** |
| * Try to require async_hooks |
| * @private |
| */ |
| |
| function tryRequireAsyncHooks () { |
| try { |
| return require('async_hooks') |
| } catch (e) { |
| return {} |
| } |
| } |
| |
| /** |
| * Wrap function with async resource, if possible. |
| * AsyncResource.bind static method backported. |
| * @private |
| */ |
| |
| function wrap (fn) { |
| var res |
| |
| // create anonymous resource |
| if (asyncHooks.AsyncResource) { |
| res = new asyncHooks.AsyncResource(fn.name || 'bound-anonymous-fn') |
| } |
| |
| // incompatible node.js |
| if (!res || !res.runInAsyncScope) { |
| return fn |
| } |
| |
| // return bound function |
| return res.runInAsyncScope.bind(res, fn, null) |
| } |