current commit
260907.18 · stream: create write request objects lazily
'use strict';

const {
  Array,
  Symbol,
} = primordials;

const { Buffer } = require('buffer');
const { FastBuffer } = require('internal/buffer');
const {
  kReadBytesOrError,
  kArrayBufferOffset,
  kBytesWritten,
  kLastWriteErr,
  streamBaseState,
} = internalBinding('stream_wrap');
const { UV_EOF } = internalBinding('uv');
const {
  ErrnoException,
} = require('internal/errors');
const { owner_symbol } = require('internal/async_hooks').symbols;
const {
  kTimeout,
  reuseOrCreateUnrefTimeout,
  getTimerDuration,
} = require('internal/timers');
const { isUint8Array } = require('internal/util/types');
const { clearTimeout } = require('timers');
const { validateFunction } = require('internal/validators');

const kMaybeDestroy = Symbol('kMaybeDestroy');
const kUpdateTimer = Symbol('kUpdateTimer');
const kAfterAsyncWrite = Symbol('kAfterAsyncWrite');
const kHandle = Symbol('kHandle');
const kBoundSession = Symbol('kBoundSession');
const kSession = Symbol('kSession');

let debug = require('internal/util/debuglog').debuglog('stream', (fn) => {
  debug = fn;
});
const kBuffer = Symbol('kBuffer');
const kBufferGen = Symbol('kBufferGen');
const kBufferCb = Symbol('kBufferCb');

function onWriteComplete(status) {
  debug('onWriteComplete', status, this.error);

  const stream = this.handle[owner_symbol];

  if (status < 0) {
    const error = new ErrnoException(status, 'write', this.error);
    if (typeof this.callback === 'function') {
      return this.callback(error);
    }

    return stream.destroy(error);
  }

  if (stream.destroyed) {
    if (typeof this.callback === 'function')
      this.callback(null);
    return;
  }

  stream[kUpdateTimer]();
  stream[kAfterAsyncWrite](this);

  if (typeof this.callback === 'function')
    this.callback(null);
}

function writevGeneric(self, data, cb) {
  const handle = self[kHandle];
  const allBuffers = data.allBuffers;
  let chunks;
  if (allBuffers) {
    chunks = data;
    for (let i = 0; i < data.length; i++)
      data[i] = data[i].chunk;
  } else {
    chunks = new Array(data.length << 1);
    for (let i = 0; i < data.length; i++) {
      const entry = data[i];
      chunks[i * 2] = entry.chunk;
      chunks[i * 2 + 1] = entry.encoding;
    }
  }
  const ret = handle.writev(null, chunks, allBuffers);

  return afterWriteDispatched(handle, ret, chunks, cb);
}

function writeGeneric(self, data, encoding, cb) {
  const handle = self[kHandle];
  let ret;
  let buffer = null;

  switch (encoding) {
    case 'buffer':
      buffer = data;
      ret = handle.writeBuffer(null, data);
      break;
    case 'latin1':
    case 'binary':
      ret = handle.writeLatin1String(null, data);
      break;
    case 'utf8':
    case 'utf-8':
      ret = handle.writeUtf8String(null, data);
      break;
    case 'ascii':
      ret = handle.writeAsciiString(null, data);
      break;
    case 'ucs2':
    case 'ucs-2':
    case 'utf16le':
    case 'utf-16le':
      ret = handle.writeUcs2String(null, data);
      break;
    default:
      buffer = Buffer.from(data, encoding);
      ret = handle.writeBuffer(null, buffer);
      break;
  }

  return afterWriteDispatched(handle, ret, buffer, cb);
}

// `ret` is either a numeric error code (when the write - or its failure -
// completed synchronously and no write request was created) or the WriteWrap
// object of a dispatched write.
function afterWriteDispatched(handle, ret, buffer, cb) {
  const bytes = streamBaseState[kBytesWritten];

  if (typeof ret === 'number') {
    // The write (or its failure) completed synchronously.
    if (ret !== 0)
      cb(new ErrnoException(ret, 'write'));
    else if (typeof cb === 'function')
      cb();
    return { async: false, bytes };
  }

  const req = ret;
  const err = streamBaseState[kLastWriteErr];
  if (err !== 0) {
    cb(new ErrnoException(err, 'write', req.error));
    return { async: false, bytes };
  }

  req.handle = handle;
  req.oncomplete = onWriteComplete;
  req.callback = cb;
  req.async = true;
  req.bytes = bytes;
  // Retain the data (or chunks) being written until the write completes.
  req.buffer = buffer;

  // If the write completed synchronously inside the write call, before
  // `oncomplete` could be attached, the completion status has been recorded;
  // deliver it now.
  const status = req.writeStatus;
  if (status !== null) {
    req.writeStatus = null;
    req.oncomplete(status);
  }

  return req;
}

function onStreamRead(arrayBuffer) {
  const nread = streamBaseState[kReadBytesOrError];

  const handle = this;
  const stream = this[owner_symbol];

  stream[kUpdateTimer]();

  if (nread > 0 && !stream.destroyed) {
    let ret;
    let result;
    const userBuf = stream[kBuffer];
    if (userBuf) {
      result = (stream[kBufferCb](nread, userBuf) !== false);
      const bufGen = stream[kBufferGen];
      if (bufGen !== null) {
        const nextBuf = bufGen();
        if (isUint8Array(nextBuf))
          stream[kBuffer] = ret = nextBuf;
      }
    } else {
      const offset = streamBaseState[kArrayBufferOffset];
      const buf = new FastBuffer(arrayBuffer, offset, nread);
      result = stream.push(buf);
    }
    if (!result) {
      handle.reading = false;
      if (!stream.destroyed) {
        const err = handle.readStop();
        if (err)
          stream.destroy(new ErrnoException(err, 'read'));
      }
    }

    return ret;
  }

  if (nread === 0) {
    return;
  }

  // After seeing EOF, most streams will be closed permanently,
  // and will not deliver any more read events after this point.
  // (equivalently, it should have called readStop on itself already).
  // Some streams may be reset and explicitly started again with a call
  // to readStart, such as TTY.

  if (nread !== UV_EOF) {
    // CallJSOnreadMethod expects the return value to be a buffer.
    // Ref: https://github.com/nodejs/node/pull/34375
    stream.destroy(new ErrnoException(nread, 'read'));
    return;
  }

  // Defer this until we actually emit end
  if (stream._readableState.endEmitted) {
    if (stream[kMaybeDestroy])
      stream[kMaybeDestroy]();
  } else {
    if (stream[kMaybeDestroy])
      stream.on('end', stream[kMaybeDestroy]);

    // Push a null to signal the end of data.
    // Do it before `maybeDestroy` for correct order of events:
    // `end` -> `close`
    stream.push(null);
    stream.read(0);
  }
}

function onStreamTimeout(stream) {
  stream._onTimeout();
}

function setStreamTimeout(msecs, callback) {
  if (this.destroyed)
    return this;

  this.timeout = msecs;

  // Type checking identical to timers.enroll()
  msecs = getTimerDuration(msecs, 'msecs');

  // Attempt to clear an existing timer in both cases -
  //  even if it will be rescheduled we don't want to leak an existing timer.
  // The cleared Timeout stays referenced in this[kTimeout] so that it can be
  // reused the next time a timeout is set, e.g. on keep-alive connections.
  clearTimeout(this[kTimeout]);

  if (msecs === 0) {
    if (callback !== undefined) {
      validateFunction(callback, 'callback');
      this.removeListener('timeout', callback);
    }
  } else {
    this[kTimeout] =
      reuseOrCreateUnrefTimeout(this[kTimeout], onStreamTimeout, msecs, this);
    if (this[kSession]) this[kSession][kUpdateTimer]();
    if (this[kBoundSession]) this[kBoundSession][kUpdateTimer]();

    if (callback !== undefined) {
      validateFunction(callback, 'callback');
      this.once('timeout', callback);
    }
  }
  return this;
}

module.exports = {
  writevGeneric,
  writeGeneric,
  onStreamRead,
  kAfterAsyncWrite,
  kMaybeDestroy,
  kUpdateTimer,
  kHandle,
  kSession,
  setStreamTimeout,
  kBuffer,
  kBufferCb,
  kBufferGen,
};