mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Every stream write created a WriteWrap JS object up front, even though most writes complete synchronously via uv_try_write() and never use it. Let stream_base_commons pass null instead of a request object. StreamBase::Write() already creates the wrap object only when the write does not complete synchronously; return that object to JS (which attaches oncomplete/callback to it) and a plain error code otherwise. Writes that complete synchronously now cross the JS/C++ boundary once and allocate nothing. Callers that pass in a request object (child_process IPC, webstreams adapters) behave as before. Since Http2Stream::DoWrite() can invoke the completion callback synchronously - before JS has attached oncomplete - such completions are now recorded on the request object's writeStatus field and replayed by stream_base_commons after dispatch. This also replaces a Has() plus name-based MakeCallback() pair with a single Get(). Also pre-create the JS fields of WriteWrap instances in the object template, as was already done for ShutdownWrap, so that they are in-object properties. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: https://github.com/nodejs/node/pull/64455 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com>
293 lines
7.5 KiB
JavaScript
293 lines
7.5 KiB
JavaScript
'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,
|
|
};
|