velocious 1.0.653 → 1.0.655
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/build/environment-handlers/node/cli/commands/test.js +2 -1
- package/build/http-server/client/index.js +67 -5
- package/build/http-server/client/request-buffer/index.js +15 -1
- package/build/http-server/client/response-compression.js +6 -8
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +4 -2
- package/build/src/http-server/client/index.d.ts +9 -1
- package/build/src/http-server/client/index.d.ts.map +1 -1
- package/build/src/http-server/client/index.js +68 -6
- package/build/src/http-server/client/request-buffer/index.d.ts.map +1 -1
- package/build/src/http-server/client/request-buffer/index.js +16 -2
- package/build/src/http-server/client/response-compression.d.ts +4 -2
- package/build/src/http-server/client/response-compression.d.ts.map +1 -1
- package/build/src/http-server/client/response-compression.js +7 -8
- package/build/src/testing/test-runner.d.ts +23 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +29 -2
- package/build/src/testing/velocious-runner-reporter.d.ts +7 -0
- package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -1
- package/build/src/testing/velocious-runner-reporter.js +54 -12
- package/build/testing/test-runner.js +33 -1
- package/build/testing/velocious-runner-reporter.js +52 -11
- package/package.json +4 -4
- package/scripts/browser-test-session.js +21 -23
- package/src/environment-handlers/node/cli/commands/test.js +2 -1
- package/src/http-server/client/index.js +67 -5
- package/src/http-server/client/request-buffer/index.js +15 -1
- package/src/http-server/client/response-compression.js +6 -8
- package/src/testing/test-runner.js +33 -1
- package/src/testing/velocious-runner-reporter.js +52 -11
|
@@ -8,7 +8,7 @@ import EventEmitter from "../../utils/event-emitter.js"
|
|
|
8
8
|
import Logger from "../../logger.js"
|
|
9
9
|
import Request from "./request.js"
|
|
10
10
|
import RequestRunner from "./request-runner.js"
|
|
11
|
-
import {applyResponseCompression} from "./response-compression.js"
|
|
11
|
+
import {addAcceptEncodingToVary, applyResponseCompression, negotiateContentEncoding} from "./response-compression.js"
|
|
12
12
|
import WebsocketSession from "./websocket-session.js"
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -384,7 +384,15 @@ export default class VeoliciousHttpServerClient {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
/**
|
|
387
|
-
*
|
|
387
|
+
* Sends a finished response to the client. Owns the framework-owned
|
|
388
|
+
* `Vary: Accept-Encoding` dimension (emitted for every selected
|
|
389
|
+
* representation — transformed, identity, 406, and file — and for
|
|
390
|
+
* header-present and header-absent requests alike, so it is stable across
|
|
391
|
+
* requests on the same connection; never added when compression is disabled,
|
|
392
|
+
* the response is truly bodyless, or the application supplied a fixed
|
|
393
|
+
* `Content-Encoding`) and the file 406 rule (a sendFile response whose
|
|
394
|
+
* client forbids identity is answered with the empty 406, the file is never
|
|
395
|
+
* opened or streamed, and `onFinished` settles once as "completed").
|
|
388
396
|
* @param {RequestRunner} requestRunner - Request runner.
|
|
389
397
|
* @returns {Promise<void>} - Resolves when complete.
|
|
390
398
|
*/
|
|
@@ -436,12 +444,41 @@ export default class VeoliciousHttpServerClient {
|
|
|
436
444
|
/** @type {string | Uint8Array | null} */
|
|
437
445
|
let bodyToEmit = body
|
|
438
446
|
|
|
447
|
+
// The response representation can depend on the client's Accept-Encoding:
|
|
448
|
+
// the same request is answered with an identity body (or a file) when
|
|
449
|
+
// identity is acceptable and with an empty 406 when it is forbidden.
|
|
450
|
+
const compression = this.configuration.getHttpServerCompression()
|
|
451
|
+
const negotiated = compression.enabled ? negotiateContentEncoding(request.header("accept-encoding")) : undefined
|
|
452
|
+
// An application-supplied Content-Encoding is an application-owned
|
|
453
|
+
// representation contract, captured before the framework may add its own.
|
|
454
|
+
// A file carrying one is a fixed, application-owned representation: the
|
|
455
|
+
// framework neither negotiates it nor re-advertises it, so it is never a
|
|
456
|
+
// candidate for the identity-only 406.
|
|
457
|
+
const hasApplicationContentEncoding = response.getHeader("Content-Encoding").length > 0
|
|
458
|
+
// A file response only ever serves the identity representation: whenever
|
|
459
|
+
// the client forbids identity (including the not-acceptable case where no
|
|
460
|
+
// coding applies) and the file does not carry an application-supplied
|
|
461
|
+
// Content-Encoding, the file is rejected with the empty 406. A truly
|
|
462
|
+
// bodyless status selects no representation, so it is never rejected here.
|
|
463
|
+
const isFileNotAcceptable = hasFilePath && !!negotiated && ("notAcceptable" in negotiated || negotiated.identityAcceptable === false) && hasApplicationContentEncoding === false && !isBodylessStatus
|
|
464
|
+
|
|
439
465
|
if (!isBodylessStatus) {
|
|
440
466
|
let contentLength
|
|
441
467
|
|
|
442
468
|
if (hasFilePath) {
|
|
443
|
-
|
|
444
|
-
|
|
469
|
+
if (isFileNotAcceptable) {
|
|
470
|
+
// The client forbids identity and files are only ever sent identity:
|
|
471
|
+
// answer with the same empty 406 every other representation path
|
|
472
|
+
// uses. The file is never opened or streamed; onFinished is settled
|
|
473
|
+
// below, after the committed 406 headers are emitted.
|
|
474
|
+
response.setStatus(406)
|
|
475
|
+
response.setBody("")
|
|
476
|
+
bodyToEmit = ""
|
|
477
|
+
contentLength = 0
|
|
478
|
+
} else {
|
|
479
|
+
const stats = await fs.stat(filePath)
|
|
480
|
+
contentLength = stats.size
|
|
481
|
+
}
|
|
445
482
|
} else {
|
|
446
483
|
// String bodies are UTF-8 framed, so the buffered bytes are the UTF-8 encoding;
|
|
447
484
|
// Uint8Array bodies are already the exact wire bytes.
|
|
@@ -474,6 +511,18 @@ export default class VeoliciousHttpServerClient {
|
|
|
474
511
|
response.setHeader("Content-Length", contentLength)
|
|
475
512
|
}
|
|
476
513
|
|
|
514
|
+
// Framework-owned Vary dimension: whenever compression is enabled and a
|
|
515
|
+
// representation was selected, the response depends on Accept-Encoding,
|
|
516
|
+
// so caches must key on it. Applied identically for every outcome
|
|
517
|
+
// (transformed, identity, 406, file) and for header-present and
|
|
518
|
+
// header-absent requests alike, so the header is stable across requests on
|
|
519
|
+
// the same connection. A truly bodyless response selects no representation
|
|
520
|
+
// and carries no dimension; an application-supplied Content-Encoding keeps
|
|
521
|
+
// the representation contract application-owned and is never re-advertised.
|
|
522
|
+
if (negotiated && !isBodylessStatus && hasApplicationContentEncoding === false) {
|
|
523
|
+
addAcceptEncodingToVary(response)
|
|
524
|
+
}
|
|
525
|
+
|
|
477
526
|
response.setHeader("Date", date.toUTCString())
|
|
478
527
|
response.setHeader("Server", "Velocious")
|
|
479
528
|
|
|
@@ -492,8 +541,21 @@ export default class VeoliciousHttpServerClient {
|
|
|
492
541
|
this.events.emit("output", headers)
|
|
493
542
|
this.logger.debug(() => ["sendResponse headers emitted", {clientCount: this.clientCount, headersLength: headers.length}])
|
|
494
543
|
|
|
495
|
-
|
|
544
|
+
// A negotiated file 406 is committed above (status 406, empty body) and its
|
|
545
|
+
// headers were just emitted: settle onFinished now, so the callback runs
|
|
546
|
+
// after the response is committed — a slow or app-stopping callback cannot
|
|
547
|
+
// delay or block delivery of the already-emitted 406. The file is never
|
|
548
|
+
// opened, streamed, or reported (no file event), and the callback settles
|
|
549
|
+
// exactly once as "completed".
|
|
550
|
+
if (isFileNotAcceptable) {
|
|
551
|
+
this.logger.debug(() => ["sendResponse file body suppressed for 406", {clientCount: this.clientCount, filePath}])
|
|
552
|
+
await this.runFileOnFinished({filePath, onFinished: fileOnFinished, result: "completed"})
|
|
553
|
+
} else if (isBodylessStatus) {
|
|
496
554
|
this.logger.debug(() => ["sendResponse body suppressed for no-body status", {clientCount: this.clientCount, statusCode: response.getStatusCode()}])
|
|
555
|
+
// A bodyless status (1xx/204/304) selects no representation, so no file
|
|
556
|
+
// body or framework Vary is emitted. The file-ownership path still settles
|
|
557
|
+
// onFinished exactly once as "completed" (nothing was aborted) — even when
|
|
558
|
+
// the client forbids identity — preserving the pre-change settlement.
|
|
497
559
|
if (hasFilePath) await this.sendFileOutput(filePath, false, fileOnFinished)
|
|
498
560
|
} else if (isHeadRequest) {
|
|
499
561
|
this.logger.debug(() => ["sendResponse body suppressed for HEAD request", {clientCount: this.clientCount}])
|
|
@@ -8,6 +8,12 @@ import Logger from "../../../logger.js"
|
|
|
8
8
|
import ParamsToObject from "../params-to-object.js"
|
|
9
9
|
import querystring from "querystring"
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Request header fields whose repeated wire fields combine into one value
|
|
13
|
+
* (RFC 9110 §5.3) before the server consumes them.
|
|
14
|
+
* @type {Set<string>} */
|
|
15
|
+
const COMBINING_HEADER_FIELDS = new Set(["accept-encoding"])
|
|
16
|
+
|
|
11
17
|
/**
|
|
12
18
|
* Runs truncate preview.
|
|
13
19
|
* @param {string | undefined} input - Input string.
|
|
@@ -367,8 +373,16 @@ export default class RequestBuffer {
|
|
|
367
373
|
*/
|
|
368
374
|
addHeader(header) {
|
|
369
375
|
const formattedName = header.getFormattedName()
|
|
376
|
+
const existingHeader = this.headersByName[formattedName]
|
|
370
377
|
|
|
371
|
-
|
|
378
|
+
// RFC 9110 §5.3: a field may be repeated; its value is the concatenation of
|
|
379
|
+
// all field values separated by commas, in wire order. Only Accept-Encoding
|
|
380
|
+
// is consumed as a combined field by the server.
|
|
381
|
+
if (existingHeader && COMBINING_HEADER_FIELDS.has(formattedName)) {
|
|
382
|
+
existingHeader.value += `, ${header.getValue()}`
|
|
383
|
+
} else {
|
|
384
|
+
this.headersByName[formattedName] = header
|
|
385
|
+
}
|
|
372
386
|
|
|
373
387
|
if (formattedName == "content-length") this.contentLength = parseInt(header.getValue())
|
|
374
388
|
}
|
|
@@ -22,9 +22,9 @@ const COMPRESSIBLE_EXACT_MEDIA_TYPES = new Set([
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* RFC 9110 §12.4.2 qvalue grammar: `0` or `1` with at most three fractional
|
|
25
|
-
* digits, and only zeros after `1`.
|
|
25
|
+
* digits (the boundary forms `0.` and `1.` are valid), and only zeros after `1`.
|
|
26
26
|
* @type {RegExp} */
|
|
27
|
-
const QVALUE_PATTERN = /^(?:0(?:\.\d{
|
|
27
|
+
const QVALUE_PATTERN = /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/u
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Runs parse accept encoding.
|
|
@@ -118,7 +118,9 @@ export function isCompressibleContentType(contentType) {
|
|
|
118
118
|
/**
|
|
119
119
|
* Merges Accept-Encoding into the response Vary header case-insensitively and
|
|
120
120
|
* without duplicates. An existing `Vary: *` already covers every request header
|
|
121
|
-
* and is preserved as-is.
|
|
121
|
+
* and is preserved as-is. Called by the response sender for every
|
|
122
|
+
* framework-selected representation so the header is identical for every
|
|
123
|
+
* request on the same connection.
|
|
122
124
|
* @param {import("./response.js").default} response - Response instance.
|
|
123
125
|
* @returns {void} - No return value.
|
|
124
126
|
*/
|
|
@@ -160,7 +162,7 @@ export function addAcceptEncodingToVary(response) {
|
|
|
160
162
|
* @param {import("../../configuration-types.js").NormalizedHttpCompressionConfiguration} args.compression - Normalized compression configuration.
|
|
161
163
|
* @param {import("./request.js").default | import("./websocket-request.js").default} args.request - Request object.
|
|
162
164
|
* @param {import("./response.js").default} args.response - Response instance.
|
|
163
|
-
* @returns {Promise<{outcome: "identity"} | {outcome: "compressed", body: Buffer} | {outcome: "not-acceptable"}>} - Compression outcome.
|
|
165
|
+
* @returns {Promise<{outcome: "identity"} | {outcome: "compressed", body: Buffer} | {outcome: "not-acceptable"}>} - Compression outcome. The caller owns the Vary header and the file/406 representation decisions.
|
|
164
166
|
*/
|
|
165
167
|
export async function applyResponseCompression({bodyBuffer, compression, request, response}) {
|
|
166
168
|
if (!compression.enabled) return {outcome: "identity"}
|
|
@@ -203,10 +205,6 @@ export async function applyResponseCompression({bodyBuffer, compression, request
|
|
|
203
205
|
return negotiated.identityAcceptable ? {outcome: "identity"} : {outcome: "not-acceptable"}
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
// The representation now depends on the request's Accept-Encoding, even when this
|
|
207
|
-
// particular response ends up identity (missing header, higher-q identity, below threshold).
|
|
208
|
-
addAcceptEncodingToVary(response)
|
|
209
|
-
|
|
210
208
|
if (negotiated.encoding == "identity") return {outcome: "identity"}
|
|
211
209
|
|
|
212
210
|
// Below the threshold the smaller identity representation is sent instead — but only
|
|
@@ -142,6 +142,8 @@ import VelociousTestArguments from "./velocious-test-arguments.js"
|
|
|
142
142
|
* @property {import("../database/pool/base.js").TestSharedConnectionRegistration | undefined} sharedRegistration - Physical-key shared registration once published.
|
|
143
143
|
*/
|
|
144
144
|
|
|
145
|
+
const testingPackageDirectory = path.dirname(fileURLToPath(import.meta.resolve("@velocious/testing/package.json")))
|
|
146
|
+
|
|
145
147
|
/**
|
|
146
148
|
* Runs to file slug.
|
|
147
149
|
* @param {string} value - Value to sanitize.
|
|
@@ -197,6 +199,8 @@ export default class TestRunner {
|
|
|
197
199
|
this._successfulTests = 0
|
|
198
200
|
this._testsCount = 0
|
|
199
201
|
this._failedTestDetails = []
|
|
202
|
+
/** @type {import("@velocious/testing/runner").NonRunTestResult[]} */
|
|
203
|
+
this._notRunTestDetails = []
|
|
200
204
|
/** @type {{fullDescription: string, filePath: string, line: number} | null} */
|
|
201
205
|
this._lastTestContext = null
|
|
202
206
|
/** @type {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} */
|
|
@@ -1007,6 +1011,25 @@ export default class TestRunner {
|
|
|
1007
1011
|
return this._failedTests
|
|
1008
1012
|
}
|
|
1009
1013
|
|
|
1014
|
+
/**
|
|
1015
|
+
* Counts selected tests blocked by a terminal resource.
|
|
1016
|
+
* @returns {number} - Selected tests not executed because a shared resource failed.
|
|
1017
|
+
*/
|
|
1018
|
+
getNotRunTests() { return this._notRunTestDetails.length }
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Returns runtime non-run attribution.
|
|
1022
|
+
* @returns {import("@velocious/testing/runner").NonRunTestResult[]} - Runtime non-run details with the originating failure.
|
|
1023
|
+
*/
|
|
1024
|
+
getNotRunTestDetails() { return this._notRunTestDetails }
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Records a selected test that did not execute.
|
|
1028
|
+
* @param {import("@velocious/testing/runner").NonRunTestResult} result - Terminal-resource non-run record.
|
|
1029
|
+
* @returns {void}
|
|
1030
|
+
*/
|
|
1031
|
+
recordNotRunTest(result) { this._notRunTestDetails.push(result) }
|
|
1032
|
+
|
|
1010
1033
|
/**
|
|
1011
1034
|
* Runs get failed test details.
|
|
1012
1035
|
* @returns {FailedTestDetail[]} - Failed test details.
|
|
@@ -1087,6 +1110,14 @@ export default class TestRunner {
|
|
|
1087
1110
|
return this._packageResult?.tests.length ?? this._testDurations.length
|
|
1088
1111
|
}
|
|
1089
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* Distinguishes an empty selection from a failure before selected cases execute.
|
|
1115
|
+
* @returns {boolean} - Whether selection matched no declarations.
|
|
1116
|
+
*/
|
|
1117
|
+
hasNoMatches() {
|
|
1118
|
+
return this._packageResult?.noMatches === true
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1090
1121
|
/**
|
|
1091
1122
|
* Returns the tests recorded during the run, slowest first.
|
|
1092
1123
|
* @param {number} [limit] - Maximum number of tests to return (0 returns all).
|
|
@@ -1105,6 +1136,7 @@ export default class TestRunner {
|
|
|
1105
1136
|
async prepare() {
|
|
1106
1137
|
this.anyTestsFocussed = false
|
|
1107
1138
|
this._failedTests = 0
|
|
1139
|
+
this._notRunTestDetails = []
|
|
1108
1140
|
this._successfulTests = 0
|
|
1109
1141
|
this._testsCount = 0
|
|
1110
1142
|
this._abortRemainingTests = false
|
|
@@ -1177,7 +1209,7 @@ export default class TestRunner {
|
|
|
1177
1209
|
|
|
1178
1210
|
if (portablePath.endsWith("/src/testing/test-runner.js")) continue
|
|
1179
1211
|
if (portablePath.endsWith("/src/testing/test.js")) continue
|
|
1180
|
-
if (
|
|
1212
|
+
if (resolvedFilePath.startsWith(`${testingPackageDirectory}${path.sep}`)) continue
|
|
1181
1213
|
|
|
1182
1214
|
return {filePath: resolvedFilePath, line: Number(match[2])}
|
|
1183
1215
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { addTrackedStackToError } from "../utils/with-tracked-stack.js"
|
|
4
|
-
import BacktraceCleaner from "../utils/backtrace-cleaner-node.js"
|
|
5
4
|
import picocolors from "picocolors"
|
|
6
5
|
import restArgsError from "../utils/rest-args-error.js"
|
|
7
6
|
import { testEvents } from "./test.js"
|
|
@@ -21,6 +20,8 @@ function errorFromPackageRecord(errorRecord) {
|
|
|
21
20
|
|
|
22
21
|
error.name = errorRecord.name
|
|
23
22
|
if (errorRecord.stack) error.stack = errorRecord.stack
|
|
23
|
+
if (errorRecord.cause) error.cause = errorFromPackageRecord(errorRecord.cause)
|
|
24
|
+
if (errorRecord.terminalResource) Object.assign(error, {terminalResource: errorRecord.terminalResource})
|
|
24
25
|
|
|
25
26
|
return error
|
|
26
27
|
}
|
|
@@ -68,7 +69,27 @@ export default class VelociousRunnerReporter {
|
|
|
68
69
|
return
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
if (event.type === "run
|
|
72
|
+
if (event.type === "test:not-run") {
|
|
73
|
+
const test = this.testRunner.findTestDeclaration(event.test.fullName)
|
|
74
|
+
if (!test) throw new Error(`Package not-run result did not match a declaration: ${event.test.fullName}`)
|
|
75
|
+
this.testRunner.recordNotRunTest(event.test)
|
|
76
|
+
this.testRunner.completeTestDeclaration(test)
|
|
77
|
+
console.error(`Not run: ${event.test.fullName} (terminal resource failure in ${event.test.reason.fullName})`)
|
|
78
|
+
await this.emitEvent("testNotRun", {
|
|
79
|
+
configuration: this.testRunner.getConfiguration(),
|
|
80
|
+
test: event.test,
|
|
81
|
+
testRunner: this.testRunner
|
|
82
|
+
})
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (event.type === "run:finish") {
|
|
87
|
+
this.testRunner.recordPackageResult(event.result)
|
|
88
|
+
for (const failure of event.result.errors) {
|
|
89
|
+
console.error(`Suite ${failure.phase} failed: ${failure.suite}`)
|
|
90
|
+
this.printErrorCauses(errorFromPackageRecord(failure.error))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
72
93
|
}
|
|
73
94
|
|
|
74
95
|
/**
|
|
@@ -95,7 +116,7 @@ export default class VelociousRunnerReporter {
|
|
|
95
116
|
const failed = outcome?.failed ?? Boolean(attempt.error)
|
|
96
117
|
const error = outcome?.error
|
|
97
118
|
const retriesUsed = Math.min(attempt.attemptNumber, retryCount)
|
|
98
|
-
const willRetry = failed && !outcome?.abortRemainingTests && attempt.attemptNumber <= retryCount
|
|
119
|
+
const willRetry = failed && !event.terminalFailure && !outcome?.abortRemainingTests && attempt.attemptNumber <= retryCount
|
|
99
120
|
const {descriptions, testDescription} = this.testRunner.testMetadata(test)
|
|
100
121
|
const compatibility = this.testRunner.testData(test)
|
|
101
122
|
|
|
@@ -245,15 +266,8 @@ export default class VelociousRunnerReporter {
|
|
|
245
266
|
|
|
246
267
|
if (error instanceof Error) {
|
|
247
268
|
console.error(picocolors.red(`${leftPadding} Test failed: ${error.message}`))
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const backtraceCleaner = new BacktraceCleaner(error)
|
|
251
|
-
const cleanedStack = backtraceCleaner.getCleanedStack()
|
|
252
|
-
const stackLines = cleanedStack?.split("\n")
|
|
269
|
+
this.printErrorCauses(error)
|
|
253
270
|
|
|
254
|
-
if (stackLines) {
|
|
255
|
-
for (const stackLine of stackLines) console.error(picocolors.red(`${leftPadding} ${stackLine}`))
|
|
256
|
-
}
|
|
257
271
|
} else {
|
|
258
272
|
console.error(picocolors.red(`${leftPadding} Test failed with a ${typeof error}: ${String(error)}`))
|
|
259
273
|
}
|
|
@@ -274,6 +288,33 @@ export default class VelociousRunnerReporter {
|
|
|
274
288
|
testRunner.printRerunCommand({descriptions, testDescription, testData, leftPadding})
|
|
275
289
|
}
|
|
276
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Prints complete primary and secondary stacks once, including cyclic cause graphs.
|
|
293
|
+
* @param {unknown} error - Thrown value at the reporting boundary.
|
|
294
|
+
* @param {Set<Error>} [reported] - Error identities already printed.
|
|
295
|
+
* @returns {void}
|
|
296
|
+
*/
|
|
297
|
+
printErrorCauses(error, reported = new Set()) {
|
|
298
|
+
if (!(error instanceof Error)) {
|
|
299
|
+
console.error(String(error))
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
if (reported.has(error)) return
|
|
303
|
+
reported.add(error)
|
|
304
|
+
addTrackedStackToError(error)
|
|
305
|
+
console.error(error.stack || `${error.name}: ${error.message}`)
|
|
306
|
+
if (error.cause !== undefined) {
|
|
307
|
+
console.error("Caused by:")
|
|
308
|
+
this.printErrorCauses(error.cause, reported)
|
|
309
|
+
}
|
|
310
|
+
if (error instanceof AggregateError) {
|
|
311
|
+
for (const secondary of error.errors) {
|
|
312
|
+
console.error("Related failure:")
|
|
313
|
+
this.printErrorCauses(secondary, reported)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
277
318
|
/**
|
|
278
319
|
* Emits one legacy event and awaits listeners in registration order.
|
|
279
320
|
* @param {string} eventName - Event name.
|