Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deps/sqlite/sqlite3.c
Original file line number Diff line number Diff line change
Expand Up @@ -238388,7 +238388,7 @@ static int sessionApplyOneOp(
for(i=0; rc==SQLITE_OK && i<nCol; i++){
sqlite3_value *pOld = sessionChangesetOld(pIter, i);
sqlite3_value *pNew = sessionChangesetNew(pIter, i);
if( p->abPK[i] || (bPatchset==0 && pOld) ){
if( pOld && (p->abPK[i] || bPatchset==0) ){
rc = sessionBindValue(pUp, i*2+2, pOld);
}
if( rc==SQLITE_OK && pNew ){
Expand Down
8 changes: 5 additions & 3 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -1113,10 +1113,12 @@ creates and returns an `Http2Stream` instance that can be used to send an
HTTP/2 request to the connected server.

When a `ClientHttp2Session` is first created, the socket may not yet be
connected. if `clienthttp2session.request()` is called during this time, the
connected. If `clienthttp2session.request()` is called during this time, the
actual request will be deferred until the socket is ready to go.
If the `session` is closed before the actual request be executed, an
`ERR_HTTP2_GOAWAY_SESSION` is thrown.

If the session becomes unavailable before the request can be created, the
returned stream will emit `ERR_HTTP2_GOAWAY_SESSION` or
`ERR_HTTP2_INVALID_SESSION` asynchronously.

This method is only available if `http2session.type` is equal to
`http2.constants.NGHTTP2_SESSION_CLIENT`.
Expand Down
54 changes: 33 additions & 21 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,10 @@ function requestOnConnect(headersList, options) {
}
}

function requestOnError(error) {
this.destroy(error);
}

// Validates that priority options are correct, specifically:
// 1. options.weight must be a number
// 2. options.parent must be a positive number
Expand Down Expand Up @@ -1153,7 +1157,7 @@ function setupHandle(socket, type, options) {
process.nextTick(emit, this, 'connect', this, socket);
}

// Emits a close event followed by an error event if err is truthy. Used
// Emits an error event followed by a close event if err is truthy. Used
// by Http2Session.prototype.destroy()
function emitClose(self, error) {
if (error)
Expand Down Expand Up @@ -1224,17 +1228,16 @@ function closeSession(session, code, error) {
session.setTimeout(0);
session.removeAllListeners('timeout');

const socket = session[kSocket];
const handle = session[kHandle];

// Destroy any pending and open streams
if (state.pendingStreams.size > 0 || state.streams.size > 0) {
const cancel = new ERR_HTTP2_STREAM_CANCEL(error);
state.pendingStreams.forEach((stream) => stream.destroy(cancel));
state.streams.forEach((stream) => stream.destroy(error));
}

// Disassociate from the socket and server.
const socket = session[kSocket];
const handle = session[kHandle];

// Destroy the handle if it exists at this point.
if (handle !== undefined) {
handle.ondone = finishSessionClose.bind(null, session, error);
Expand Down Expand Up @@ -1809,11 +1812,15 @@ class ClientHttp2Session extends Http2Session {
request(headersParam, options) {
debugSessionObj(this, 'initiating request');

if (this.destroyed)
throw new ERR_HTTP2_INVALID_SESSION();

if (this.closed)
throw new ERR_HTTP2_GOAWAY_SESSION();
// Keep argument validation synchronous, but defer session-state failures
// to the returned stream so request retries from stream callbacks do not
// throw before session lifecycle handlers run.
let requestError;
if (this.destroyed) {
requestError = new ERR_HTTP2_INVALID_SESSION();
} else if (this.closed) {
requestError = new ERR_HTTP2_GOAWAY_SESSION();
}

this[kUpdateTimer]();

Expand Down Expand Up @@ -1899,19 +1906,24 @@ class ClientHttp2Session extends Http2Session {
}
}

const onConnect = reqAsync.bind(requestOnConnect.bind(stream, headersList, options));
if (this.connecting) {
if (this[kPendingRequestCalls] !== null) {
this[kPendingRequestCalls].push(onConnect);
if (requestError) {
process.nextTick(reqAsync.bind(requestOnError.bind(stream, requestError)));
} else {
const onConnect = reqAsync.bind(
requestOnConnect.bind(stream, headersList, options));
if (this.connecting) {
if (this[kPendingRequestCalls] !== null) {
this[kPendingRequestCalls].push(onConnect);
} else {
this[kPendingRequestCalls] = [onConnect];
this.once('connect', () => {
this[kPendingRequestCalls].forEach((f) => f());
this[kPendingRequestCalls] = null;
});
}
} else {
this[kPendingRequestCalls] = [onConnect];
this.once('connect', () => {
this[kPendingRequestCalls].forEach((f) => f());
this[kPendingRequestCalls] = null;
});
onConnect();
}
} else {
onConnect();
}

if (onClientStreamCreatedChannel.hasSubscribers) {
Expand Down
6 changes: 6 additions & 0 deletions lib/internal/modules/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ function processTypeScriptCode(code, options) {
return transformedCode;
}

function stripTypeScriptTypesForCoverage(code) {
validateString(code, 'code');
return processTypeScriptCode(code, { mode: 'strip-only' });
}


/**
* Performs type-stripping to TypeScript source code internally.
Expand Down Expand Up @@ -205,4 +210,5 @@ function addSourceMap(code, sourceMap) {
module.exports = {
stripTypeScriptModuleTypes,
stripTypeScriptTypes,
stripTypeScriptTypesForCoverage,
};
75 changes: 72 additions & 3 deletions lib/internal/test_runner/coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
StringPrototypeIncludes,
StringPrototypeLocaleCompare,
StringPrototypeStartsWith,
StringPrototypeTrim,
} = primordials;
const {
copyFileSync,
Expand Down Expand Up @@ -44,6 +45,20 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?<count>\d+ )?\*\//;
const kLineEndingRegex = /\r?\n$/u;
const kLineSplitRegex = /(?<=\r?\n)/u;
const kStatusRegex = /\/\* node:coverage (?<status>enable|disable) \*\//;
const kTypeOnlyImportRegex = /^\s*import\s+type\b/u;
const kTypeScriptSourceRegex = /\.(?:cts|mts|ts)$/u;

let stripTypeScriptTypesForCoverage;

function getStripTypeScriptTypesForCoverage() {
if (!process.config.variables.node_use_amaro) {
return;
}

stripTypeScriptTypesForCoverage ??=
require('internal/modules/typescript').stripTypeScriptTypesForCoverage;
return stripTypeScriptTypesForCoverage;
}

class CoverageLine {
constructor(line, startOffset, src, length = src?.length) {
Expand All @@ -69,6 +84,7 @@ class TestCoverage {
}

#sourceLines = new SafeMap();
#typeScriptLines = new SafeSet();

getLines(fileUrl, source) {
// Split the file source into lines. Make sure the lines maintain their
Expand Down Expand Up @@ -133,6 +149,57 @@ class TestCoverage {
return lines;
}

markTypeScriptOnlyLines(fileUrl, source) {
if (this.#typeScriptLines.has(fileUrl)) {
return;
}
this.#typeScriptLines.add(fileUrl);

if (RegExpPrototypeExec(kTypeScriptSourceRegex, fileUrl) === null) {
return;
}

const lines = this.getLines(fileUrl, source);
if (!lines) {
return;
}

let strippedLines;
const stripSource = getStripTypeScriptTypesForCoverage();

if (stripSource) {
source ??= readFileSync(fileURLToPath(fileUrl), 'utf8');

try {
strippedLines = RegExpPrototypeSymbolSplit(
kLineSplitRegex,
stripSource(source),
);
} catch {
strippedLines = undefined;
}
}

for (let i = 0; i < lines.length; ++i) {
const originalLine = lines[i].src;

if (StringPrototypeTrim(originalLine).length === 0) {
continue;
}

if (strippedLines?.[i] !== undefined) {
if (StringPrototypeTrim(strippedLines[i]).length === 0) {
lines[i].ignore = true;
}
continue;
}

if (RegExpPrototypeExec(kTypeOnlyImportRegex, originalLine) !== null) {
lines[i].ignore = true;
}
}
}

summary() {
internalBinding('profiler').takeCoverage();
const coverage = this.getCoverageFromDirectory();
Expand Down Expand Up @@ -368,10 +435,12 @@ class TestCoverage {
offset += length + 1;
return coverageLine;
});
if (data.sourcesContent != null) {
for (let j = 0; j < data.sources.length; ++j) {
this.getLines(data.sources[j], data.sourcesContent[j]);
for (let j = 0; j < data.sources.length; ++j) {
const source = data.sourcesContent?.[j];
if (source != null) {
this.getLines(data.sources[j], source);
}
this.markTypeScriptOnlyLines(data.sources[j], source);
}
const sourceMap = new SourceMap(data, { __proto__: null, lineLengths });

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
console.log('Hi');
export {};
//# sourceMappingURL=a.mjs.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type {} from 'node:assert';

console.log('Hi');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './dist/a.mjs';
15 changes: 13 additions & 2 deletions test/parallel/test-http2-client-destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,19 @@ const { listenerCount } = require('events');
assert.throws(() => client.ping(), sessionError);
assert.throws(() => client.settings({}), sessionError);
assert.throws(() => client.goaway(), sessionError);
assert.throws(() => client.request(), sessionError);

const pendingReq = client.request();
pendingReq.on('response', common.mustNotCall());
pendingReq.on('error', common.expectsError(sessionError));
pendingReq.on('close', common.mustCall());

client.on('close', common.mustCall(() => {
const postCloseReq = client.request();
postCloseReq.on('response', common.mustNotCall());
postCloseReq.on('error', common.expectsError(sessionError));
postCloseReq.on('close', common.mustCall());
}));

client.close(); // Should be a non-op at this point

// Wait for setImmediate call from destroy() to complete
Expand All @@ -92,7 +104,6 @@ const { listenerCount } = require('events');
assert.throws(() => client.ping(), sessionError);
assert.throws(() => client.settings({}), sessionError);
assert.throws(() => client.goaway(), sessionError);
assert.throws(() => client.request(), sessionError);
client.close(); // Should be a non-op at this point
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const http2 = require('http2');

const server = http2.createServer();
let serverSocket;

server.on('connection', common.mustCall((socket) => {
serverSocket = socket;
socket.on('error', () => {});
}));

server.on('sessionError', () => {});
server.on('stream', common.mustCall((stream, headers) => {
if (headers[':path'] === '/close') {
stream.respond({ ':status': 200 });
stream.write('partial', common.mustCall(() => {
setImmediate(() => serverSocket.destroy());
}));
return;
}

stream.respond({ ':status': 200 });
stream.end('ok');
}));

server.listen(0, common.mustCall(() => {
const session = http2.connect(`http://localhost:${server.address().port}`);
let cachedSession = session;

session.on('error', () => {});
session.on('close', common.mustCall(() => {
cachedSession = undefined;
server.close();
}));

const req = session.request({ ':path': '/close' });
req.on('response', common.mustCall());
req.on('error', () => {});
req.on('close', common.mustCall(() => {
// This must not throw synchronously even though the session is no longer
// usable. Depending on teardown timing, the returned stream may report a
// closed session before the destroy state is fully observable here.
const req2 = session.request({ ':path': '/again' });

req2.on('error', common.mustCall((err) => {
assert.ok(
err.code === 'ERR_HTTP2_INVALID_SESSION' ||
err.code === 'ERR_HTTP2_GOAWAY_SESSION');
assert.strictEqual(cachedSession, undefined);
}));
}));
req.resume();
}));
11 changes: 6 additions & 5 deletions test/parallel/test-net-pipe-connect-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const common = require('../common');
const fixtures = require('../common/fixtures');
const fs = require('fs');
const net = require('net');
const path = require('path');
const assert = require('assert');

// Test if ENOTSOCK is fired when trying to connect to a file which is not
Expand All @@ -38,11 +39,11 @@ if (common.isWindows) {
} else {
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
// Keep the file name very short so that we don't exceed the 108 char limit
// on CI for a POSIX socket. Even though this isn't actually a socket file,
// the error will be different from the one we are expecting if we exceed the
// limit.
emptyTxt = `${tmpdir.path}0.txt`;
// Use a short relative path so that we don't exceed the 108 byte limit for
// Unix socket paths in long or multibyte CI workspaces. Even though this
// isn't actually a socket file, the error will be different from the one we
// are expecting if the path is too long.
emptyTxt = path.join(path.relative(process.cwd(), tmpdir.path), '0.txt');

function cleanup() {
try {
Expand Down
Loading
Loading