dd-trace 6.21.0 → 6.22.0
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/ci/diagnose.js +9 -14
- package/ci/init.js +1 -2
- package/ci/test-optimization-validation/ci-discovery.js +9 -4
- package/ci/test-optimization-validation/executable.js +1 -5
- package/ci/test-optimization-validation/framework-adapters/cypress.js +1 -4
- package/ci/test-optimization-validation/framework-adapters/vitest.js +1 -1
- package/ci/test-optimization-validation/generated-files.js +6 -4
- package/ci/test-optimization-validation/generated-test-contract.js +7 -3
- package/ci/test-optimization-validation/manifest-scaffold.js +7 -2
- package/ci/test-optimization-validation/manifest-schema.js +6 -7
- package/ci/test-optimization-validation/report-writer.js +6 -2
- package/ci/test-optimization-validation/runner-command.js +2 -4
- package/ci/test-optimization-validation/runner-contract.js +1 -2
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +1 -15
- package/ci/vitest-no-worker-init-setup.mjs +10 -7
- package/index.d.ts +25 -14
- package/initialize.mjs +1 -3
- package/openfeature/rules-based.d.ts +11 -0
- package/openfeature/rules-based.js +7 -0
- package/package.json +12 -10
- package/packages/datadog-core/src/storage.js +16 -0
- package/packages/datadog-esbuild/index.js +18 -12
- package/packages/datadog-instrumentations/src/aerospike.js +3 -3
- package/packages/datadog-instrumentations/src/ai.js +3 -3
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -6
- package/packages/datadog-instrumentations/src/aws-sdk.js +21 -13
- package/packages/datadog-instrumentations/src/cassandra-driver.js +25 -20
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +9 -23
- package/packages/datadog-instrumentations/src/cucumber.js +48 -33
- package/packages/datadog-instrumentations/src/elasticsearch.js +1 -5
- package/packages/datadog-instrumentations/src/electron.js +6 -6
- package/packages/datadog-instrumentations/src/fastify.js +13 -4
- package/packages/datadog-instrumentations/src/fs.js +2 -2
- package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +9 -5
- package/packages/datadog-instrumentations/src/grpc/client.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +2 -2
- package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +1 -2
- package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/register.js +36 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +87 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +5 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +48 -31
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +7 -7
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +42 -4
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +7 -10
- package/packages/datadog-instrumentations/src/ioredis.js +7 -5
- package/packages/datadog-instrumentations/src/iovalkey.js +7 -5
- package/packages/datadog-instrumentations/src/jest.js +3 -10
- package/packages/datadog-instrumentations/src/knex.js +9 -5
- package/packages/datadog-instrumentations/src/mariadb-bundle.js +3 -3
- package/packages/datadog-instrumentations/src/mocha/common.js +20 -8
- package/packages/datadog-instrumentations/src/mocha/main.js +140 -43
- package/packages/datadog-instrumentations/src/mocha/utils.js +3 -2
- package/packages/datadog-instrumentations/src/mocha/worker.js +1 -2
- package/packages/datadog-instrumentations/src/mongodb-core.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/nova-sonic/audio.js +173 -0
- package/packages/datadog-instrumentations/src/nova-sonic/index.js +209 -0
- package/packages/datadog-instrumentations/src/nova-sonic/session.js +483 -0
- package/packages/datadog-instrumentations/src/openai-realtime/index.js +18 -6
- package/packages/datadog-instrumentations/src/openai-realtime/session.js +1 -1
- package/packages/datadog-instrumentations/src/openai.js +7 -0
- package/packages/datadog-instrumentations/src/pino.js +5 -6
- package/packages/datadog-instrumentations/src/redis.js +27 -13
- package/packages/datadog-instrumentations/src/router.js +30 -0
- package/packages/datadog-instrumentations/src/rum-browser-scripts.js +2 -6
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -5
- package/packages/datadog-instrumentations/src/vitest-main.js +9 -15
- package/packages/datadog-instrumentations/src/vitest-util.js +2 -2
- package/packages/datadog-instrumentations/src/vitest-worker.js +1 -2
- package/packages/datadog-instrumentations/src/vitest.js +2 -2
- package/packages/datadog-instrumentations/src/webdriverio.js +5 -8
- package/packages/datadog-instrumentations/src/ws.js +1 -4
- package/packages/datadog-plugin-amqplib/src/client.js +1 -2
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -5
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -2
- package/packages/datadog-plugin-aws-sdk/src/base.js +5 -8
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +18 -0
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/sonic.js +45 -0
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +2 -5
- package/packages/datadog-plugin-azure-durable-functions/src/index.js +5 -4
- package/packages/datadog-plugin-azure-functions/src/index.js +5 -3
- package/packages/datadog-plugin-claude-agent-sdk/src/index.js +0 -2
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +1 -5
- package/packages/datadog-plugin-cypress/src/index.js +6 -6
- package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -2
- package/packages/datadog-plugin-cypress/src/support.js +1 -4
- package/packages/datadog-plugin-express/src/code_origin.js +1 -2
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -2
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -2
- package/packages/datadog-plugin-graphql/src/execute.js +5 -5
- package/packages/datadog-plugin-grpc/src/util.js +1 -2
- package/packages/datadog-plugin-kafkajs/src/producer.js +1 -2
- package/packages/datadog-plugin-mocha/src/index.js +7 -10
- package/packages/datadog-plugin-mocha/src/webdriverio-video.js +8 -4
- package/packages/datadog-plugin-mongodb-core/src/query.js +8 -5
- package/packages/datadog-plugin-next/src/index.js +1 -2
- package/packages/datadog-plugin-openai/src/tracing.js +5 -2
- package/packages/datadog-plugin-openai-agents/src/integration.js +6 -2
- package/packages/datadog-plugin-openai-agents/src/processor.js +2 -4
- package/packages/datadog-plugin-postgres/src/index.js +24 -0
- package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -5
- package/packages/datadog-plugin-rhea/src/consumer.js +1 -4
- package/packages/datadog-plugin-vitest/src/index.js +5 -2
- package/packages/datadog-shimmer/src/shimmer.js +5 -4
- package/packages/datadog-webpack/index.js +55 -43
- package/packages/datadog-webpack/src/loader.js +56 -8
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -4
- package/packages/dd-trace/src/aiguard/messages/utils.js +1 -2
- package/packages/dd-trace/src/aiguard/redaction.js +8 -2
- package/packages/dd-trace/src/appsec/api_security/normalized-route.js +3 -2
- package/packages/dd-trace/src/appsec/api_security/sampler.js +2 -6
- package/packages/dd-trace/src/appsec/downstream_requests.js +1 -9
- package/packages/dd-trace/src/appsec/handlers/http-request.js +3 -9
- package/packages/dd-trace/src/appsec/handlers/http-response.js +7 -2
- package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +1 -4
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +7 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -3
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -3
- package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +1 -6
- package/packages/dd-trace/src/appsec/lambda.js +1 -3
- package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +37 -27
- package/packages/dd-trace/src/appsec/rasp/lfi.js +9 -5
- package/packages/dd-trace/src/appsec/reporter.js +21 -1
- package/packages/dd-trace/src/appsec/stack_trace.js +1 -2
- package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +3 -6
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -7
- package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +1 -2
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +2 -8
- package/packages/dd-trace/src/ci-visibility/resolve-runfile-path.js +54 -0
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +5 -8
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +2 -53
- package/packages/dd-trace/src/config/config-types.d.ts +2 -2
- package/packages/dd-trace/src/config/defaults.js +5 -5
- package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -0
- package/packages/dd-trace/src/config/helper.js +34 -3
- package/packages/dd-trace/src/config/index.js +10 -9
- package/packages/dd-trace/src/config/normalize-service.js +1 -1
- package/packages/dd-trace/src/config/parsers.js +2 -2
- package/packages/dd-trace/src/config/supported-configurations.json +35 -0
- package/packages/dd-trace/src/datastreams/processor.js +3 -2
- package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -4
- package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -0
- package/packages/dd-trace/src/debugger/guardrail-metrics.js +2 -2
- package/packages/dd-trace/src/dogstatsd.js +1 -1
- package/packages/dd-trace/src/encode/tags-processors.js +2 -2
- package/packages/dd-trace/src/exporters/common/request.js +1 -5
- package/packages/dd-trace/src/id.js +14 -1
- package/packages/dd-trace/src/lambda/handler.js +179 -19
- package/packages/dd-trace/src/lambda/index.js +15 -1
- package/packages/dd-trace/src/lambda/runtime/patch.js +34 -7
- package/packages/dd-trace/src/llmobs/audio-utils.js +3 -2
- package/packages/dd-trace/src/llmobs/constants/tags.js +12 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +15 -0
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +1 -2
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +1 -3
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/nova-sonic.js +92 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +56 -2
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +38 -0
- package/packages/dd-trace/src/llmobs/prompts/manager.js +5 -7
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +1 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +1 -2
- package/packages/dd-trace/src/llmobs/tagger.js +2 -2
- package/packages/dd-trace/src/llmobs/util.js +1 -2
- package/packages/dd-trace/src/llmobs/writers/base.js +4 -5
- package/packages/dd-trace/src/log/log.js +1 -5
- package/packages/dd-trace/src/openfeature/evaluation-telemetry-opt-out-hook.js +51 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +48 -21
- package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -2
- package/packages/dd-trace/src/opentelemetry/otlp/common.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -23
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +53 -13
- package/packages/dd-trace/src/otel-sampling.js +4 -5
- package/packages/dd-trace/src/otel-thread-ctx.js +79 -13
- package/packages/dd-trace/src/plugins/database.js +7 -16
- package/packages/dd-trace/src/plugins/util/git.js +9 -25
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -5
- package/packages/dd-trace/src/plugins/util/test.js +48 -4
- package/packages/dd-trace/src/priority_sampler.js +1 -2
- package/packages/dd-trace/src/profiling/profiler.js +4 -6
- package/packages/dd-trace/src/proxy.js +1 -2
- package/packages/dd-trace/src/ritm.js +4 -2
- package/packages/dd-trace/src/serverless.js +5 -0
- package/packages/dd-trace/src/span_processor.js +13 -17
- package/packages/dd-trace/src/telemetry/endpoints.js +1 -3
- package/packages/dd-trace/src/util.js +29 -0
- package/packages/dd-trace/src/web-tags-cache.js +6 -2
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/vendor/dist/@datadog/openfeature-node-server/rules-based/LICENSE +201 -0
- package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.d.ts +925 -0
- package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.js +1 -0
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
|
@@ -1055,9 +1055,9 @@ function createWrapCallbackGetConnection (options) {
|
|
|
1055
1055
|
return function wrapGetConnection (getConnection) {
|
|
1056
1056
|
return function () {
|
|
1057
1057
|
const callback = arguments[arguments.length - 1]
|
|
1058
|
-
if (typeof callback !== 'function'
|
|
1059
|
-
|
|
1060
|
-
|
|
1058
|
+
if (typeof callback !== 'function' || !connectionStartCh.hasSubscribers) {
|
|
1059
|
+
return getConnection.apply(this, arguments)
|
|
1060
|
+
}
|
|
1061
1061
|
|
|
1062
1062
|
if (!acquireStartCh.hasSubscribers) {
|
|
1063
1063
|
const ctx = {}
|
|
@@ -11,33 +11,45 @@ const MINIMUM_MOCHA_VERSION = DD_MAJOR >= 6 ? '>=8.0.0' : '>=5.2.0'
|
|
|
11
11
|
const parameterizedTestCh = channel('ci:mocha:test:parameterize')
|
|
12
12
|
const patched = new WeakSet()
|
|
13
13
|
|
|
14
|
+
/** @typedef {{ moduleBaseDir?: string }} MochaHookMeta */
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
17
|
* Registers every Mocha package view that can expose the constructor.
|
|
16
18
|
*
|
|
17
19
|
* @param {string[]} versions
|
|
18
|
-
* @param {(Mocha: Function, frameworkVersion: string) => Function} wrapMochaRun
|
|
20
|
+
* @param {(Mocha: Function, frameworkVersion: string, hookMeta: MochaHookMeta) => Function} wrapMochaRun
|
|
19
21
|
*/
|
|
20
22
|
function addMochaRunHooks (versions, wrapMochaRun) {
|
|
21
|
-
const patchedMochaConstructors = new
|
|
23
|
+
const patchedMochaConstructors = new WeakMap()
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* @param {Function} Mocha
|
|
25
27
|
* @param {string} frameworkVersion
|
|
28
|
+
* @param {boolean | undefined} isIitm
|
|
29
|
+
* @param {{ moduleBaseDir: string } | undefined} hookMeta
|
|
26
30
|
* @returns {Function}
|
|
27
31
|
*/
|
|
28
|
-
function wrapMochaOnce (Mocha, frameworkVersion) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
function wrapMochaOnce (Mocha, frameworkVersion, isIitm, hookMeta) {
|
|
33
|
+
let metadata = patchedMochaConstructors.get(Mocha)
|
|
34
|
+
if (metadata) {
|
|
35
|
+
// A bundler can expose the constructor before an ordinary loader supplies its package directory.
|
|
36
|
+
metadata.moduleBaseDir ??= hookMeta?.moduleBaseDir
|
|
37
|
+
return Mocha
|
|
38
|
+
}
|
|
39
|
+
metadata = { moduleBaseDir: hookMeta?.moduleBaseDir }
|
|
40
|
+
patchedMochaConstructors.set(Mocha, metadata)
|
|
41
|
+
return wrapMochaRun(Mocha, frameworkVersion, metadata)
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
/**
|
|
35
45
|
* @param {object|Function} MochaPackage
|
|
36
46
|
* @param {string} frameworkVersion
|
|
47
|
+
* @param {boolean | undefined} isIitm
|
|
48
|
+
* @param {{ moduleBaseDir: string } | undefined} hookMeta
|
|
37
49
|
* @returns {object|Function}
|
|
38
50
|
*/
|
|
39
|
-
function wrapMochaPackage (MochaPackage, frameworkVersion) {
|
|
40
|
-
wrapMochaOnce(MochaPackage.default ?? MochaPackage, frameworkVersion)
|
|
51
|
+
function wrapMochaPackage (MochaPackage, frameworkVersion, isIitm, hookMeta) {
|
|
52
|
+
wrapMochaOnce(MochaPackage.default ?? MochaPackage, frameworkVersion, isIitm, hookMeta)
|
|
41
53
|
return MochaPackage
|
|
42
54
|
}
|
|
43
55
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { AsyncResource } = require('node:async_hooks')
|
|
4
|
+
const { realpathSync } = require('node:fs')
|
|
5
|
+
const { resolve, sep } = require('node:path')
|
|
6
|
+
const { fileURLToPath } = require('node:url')
|
|
4
7
|
|
|
5
8
|
const { createCoverageMap } = require('../../../../vendor/dist/istanbul-lib-coverage')
|
|
6
9
|
const satisfies = require('../../../../vendor/dist/semifies')
|
|
@@ -16,6 +19,7 @@ const { EMPTY_EFD_RETRY_POLICY } = require('../../../dd-trace/src/ci-visibility/
|
|
|
16
19
|
const { writeCoverageBackfillToCache } = require('../../../dd-trace/src/ci-visibility/test-optimization-cache')
|
|
17
20
|
const log = require('../../../dd-trace/src/log')
|
|
18
21
|
const { getEnvironmentVariable } = require('../../../dd-trace/src/config/helper')
|
|
22
|
+
const { getCallSites } = require('../../../dd-trace/src/plugins/util/stacktrace')
|
|
19
23
|
const {
|
|
20
24
|
getTestSuitePath,
|
|
21
25
|
MOCHA_WORKER_TELEMETRY_PAYLOAD_CODE,
|
|
@@ -88,6 +92,9 @@ const runnerRecoveryStates = new WeakMap()
|
|
|
88
92
|
const runnersWithPendingCoverageReset = new WeakSet()
|
|
89
93
|
const parallelRunners = new WeakSet()
|
|
90
94
|
const wrappedRunnerEmitPrototypes = new WeakSet()
|
|
95
|
+
const rootSuitesWithHookFiles = new WeakSet()
|
|
96
|
+
const ROOT_HOOK_NAMES = ['_beforeAll', '_afterAll', '_beforeEach', '_afterEach']
|
|
97
|
+
const ROOT_SUITE_ARRAY_NAMES = ['tests', 'suites', '_onlyTests', '_onlySuites', ...ROOT_HOOK_NAMES]
|
|
91
98
|
let hasWarnedDeprecatedMochaVersion = false
|
|
92
99
|
|
|
93
100
|
const unskippableSuites = []
|
|
@@ -169,6 +176,40 @@ function haveRootTestsFinished (rootTests) {
|
|
|
169
176
|
return true
|
|
170
177
|
}
|
|
171
178
|
|
|
179
|
+
/**
|
|
180
|
+
* @param {{ file: string }} test
|
|
181
|
+
*/
|
|
182
|
+
function startRootSuite (test) {
|
|
183
|
+
const ctx = testFileToSuiteCtx.get(test.file)
|
|
184
|
+
if (ctx?._pendingRootStart) {
|
|
185
|
+
ctx._pendingRootStart = false
|
|
186
|
+
testSuiteStartCh.runStores(ctx, () => {})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {string} file
|
|
192
|
+
* @param {'pass' | 'fail' | 'skip'} status
|
|
193
|
+
* @param {object} [runner] Defers coverage reset until this runner's suite-end reporters finish.
|
|
194
|
+
*/
|
|
195
|
+
function finishRootSuite (file, status, runner) {
|
|
196
|
+
const ctx = testFileToSuiteCtx.get(file)
|
|
197
|
+
if (!ctx) {
|
|
198
|
+
log.warn('No ctx found for suite', file)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (global.__coverage__) {
|
|
203
|
+
const coverageFiles = getCoveredFilesFromCoverage(global.__coverage__)
|
|
204
|
+
testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file })
|
|
205
|
+
mergeCoverage(global.__coverage__, originalCoverageMap)
|
|
206
|
+
if (runner) runnersWithPendingCoverageReset.add(runner)
|
|
207
|
+
else resetCoverage(global.__coverage__)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
|
|
211
|
+
}
|
|
212
|
+
|
|
172
213
|
function getSuitePath (suite) {
|
|
173
214
|
return getTestSuitePath(suite.file, process.cwd())
|
|
174
215
|
}
|
|
@@ -931,6 +972,23 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
|
|
|
931
972
|
return
|
|
932
973
|
}
|
|
933
974
|
|
|
975
|
+
if (runner._opts?.cleanReferencesAfterRun === false) {
|
|
976
|
+
const rootSuite = runner.suite
|
|
977
|
+
const originalArrays = ROOT_SUITE_ARRAY_NAMES.map(name => rootSuite[name])
|
|
978
|
+
// Restore definitions before completion callbacks can start another run, including with the plugin disabled.
|
|
979
|
+
runner.prependOnceListener('end', () => {
|
|
980
|
+
for (let index = 0; index < ROOT_SUITE_ARRAY_NAMES.length; index++) {
|
|
981
|
+
rootSuite[ROOT_SUITE_ARRAY_NAMES[index]] = originalArrays[index]
|
|
982
|
+
}
|
|
983
|
+
})
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Root hooks must be selected before .only() removes other files from the test tree.
|
|
987
|
+
for (const name of ROOT_HOOK_NAMES) {
|
|
988
|
+
runner.suite[name] = runner.suite[name].filter(hook => !hook.file ||
|
|
989
|
+
!suitesToSkip.includes(getSuitePath(hook)) || unskippableSuites.includes(hook.file))
|
|
990
|
+
}
|
|
991
|
+
|
|
934
992
|
// We remove the suites that we skip through ITR
|
|
935
993
|
// Mocha normally applies exclusivity after this asynchronous configuration step.
|
|
936
994
|
if (typeof runner.suite.hasOnly === 'function') {
|
|
@@ -940,17 +998,28 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
|
|
|
940
998
|
}
|
|
941
999
|
const numTestsToRun = runner.grepTotal(runner.suite)
|
|
942
1000
|
const filteredSuites = getFilteredSuites(runner.suite.suites)
|
|
1001
|
+
// Root tests share a Mocha suite across files, but TIA still selects them by file.
|
|
1002
|
+
const filteredRootTests = getFilteredSuites(runner.suite.tests)
|
|
943
1003
|
const { suitesToRun, suitesToSkipForRun } = filteredSuites
|
|
944
1004
|
|
|
945
|
-
isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length
|
|
1005
|
+
isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length ||
|
|
1006
|
+
filteredRootTests.suitesToRun.length !== runner.suite.tests.length
|
|
946
1007
|
|
|
947
1008
|
log.debug('%d out of %d suites are going to run.', suitesToRun.length, runner.suite.suites.length)
|
|
948
1009
|
|
|
949
1010
|
runner.suite.suites = suitesToRun
|
|
1011
|
+
runner.suite.tests = filteredRootTests.suitesToRun
|
|
1012
|
+
// Mocha reapplies exclusivity when the delayed runner resumes.
|
|
1013
|
+
if (runner.suite._onlyTests.length) {
|
|
1014
|
+
runner.suite._onlyTests = runner.suite.tests
|
|
1015
|
+
}
|
|
1016
|
+
if (runner.suite._onlySuites.length) {
|
|
1017
|
+
runner.suite._onlySuites = runner.suite._onlySuites.filter(suite => suitesToRun.includes(suite))
|
|
1018
|
+
}
|
|
950
1019
|
areAllSuitesSkipped = isSuitesSkipped && numTestsToRun > 0 && runner.grepTotal(runner.suite) === 0
|
|
951
1020
|
|
|
952
|
-
skippedSuites = [...filteredSuites.skippedSuites]
|
|
953
|
-
suitesToSkip = suitesToSkipForRun
|
|
1021
|
+
skippedSuites = [...new Set([...filteredSuites.skippedSuites, ...filteredRootTests.skippedSuites])]
|
|
1022
|
+
suitesToSkip = [...new Set([...suitesToSkipForRun, ...filteredRootTests.suitesToSkipForRun])]
|
|
954
1023
|
skippedSuitesCoverage = getSkippedSuitesCoverageForRun()
|
|
955
1024
|
writeCoverageBackfillToCache(skippedSuitesCoverage, getCoverageRootDir())
|
|
956
1025
|
|
|
@@ -1095,11 +1164,51 @@ function runMochaRunner (run, runner, args) {
|
|
|
1095
1164
|
/**
|
|
1096
1165
|
* @param {Function} Mocha
|
|
1097
1166
|
* @param {string} frameworkVersion
|
|
1167
|
+
* @param {import('./common').MochaHookMeta} hookMeta
|
|
1098
1168
|
* @returns {Function}
|
|
1099
1169
|
*/
|
|
1100
|
-
function wrapMochaRun (Mocha, frameworkVersion) {
|
|
1170
|
+
function wrapMochaRun (Mocha, frameworkVersion, hookMeta) {
|
|
1101
1171
|
warnDeprecatedMochaVersion(frameworkVersion)
|
|
1102
1172
|
|
|
1173
|
+
// Shimmer is required to attach listeners to the root suite created by each Mocha instance.
|
|
1174
|
+
shimmer.wrap(Mocha.prototype, 'ui', ui => function (...args) {
|
|
1175
|
+
const result = ui.apply(this, args)
|
|
1176
|
+
const suite = this.suite
|
|
1177
|
+
if (!rootSuitesWithHookFiles.has(suite)) {
|
|
1178
|
+
rootSuitesWithHookFiles.add(suite)
|
|
1179
|
+
let file
|
|
1180
|
+
suite.prependListener('pre-require', (context, testFile) => {
|
|
1181
|
+
file = testFile
|
|
1182
|
+
})
|
|
1183
|
+
suite.on('post-require', () => {
|
|
1184
|
+
file = undefined
|
|
1185
|
+
})
|
|
1186
|
+
// Hook objects are created at runtime; inspect the caller while registration is still on the stack.
|
|
1187
|
+
shimmer.wrap(suite, '_createHook', createHook => function createHookWithFile (...args) {
|
|
1188
|
+
const hook = createHook.apply(this, args)
|
|
1189
|
+
if (file && !hook.file) {
|
|
1190
|
+
try {
|
|
1191
|
+
const { moduleBaseDir } = hookMeta
|
|
1192
|
+
const mochaPathPrefix = moduleBaseDir && moduleBaseDir + sep
|
|
1193
|
+
for (const site of getCallSites(createHookWithFile)) {
|
|
1194
|
+
const source = site.getFileName()
|
|
1195
|
+
if (!source || source.startsWith('node:')) continue
|
|
1196
|
+
const sourceFile = source.startsWith('file:') ? fileURLToPath(source) : source
|
|
1197
|
+
if (mochaPathPrefix && sourceFile.startsWith(mochaPathPrefix)) continue
|
|
1198
|
+
// Helpers register shared setup; only direct declarations belong to the loading test file.
|
|
1199
|
+
if (sourceFile === file || realpathSync(sourceFile) === realpathSync(file)) hook.file = file
|
|
1200
|
+
break
|
|
1201
|
+
}
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
log.debug('Error capturing Mocha root hook source: %s', error)
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return hook
|
|
1207
|
+
})
|
|
1208
|
+
}
|
|
1209
|
+
return result
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1103
1212
|
// Shimmer is required because run must return its Runner while execution is paused and resumed after configuration.
|
|
1104
1213
|
shimmer.wrap(Mocha.prototype, 'run', run => function (...args) {
|
|
1105
1214
|
// Workers do not need to request any data, just run the tests
|
|
@@ -1123,7 +1232,7 @@ function wrapMochaRun (Mocha, frameworkVersion) {
|
|
|
1123
1232
|
this.files.forEach((path) => {
|
|
1124
1233
|
const isUnskippable = isMarkedAsUnskippable({ path })
|
|
1125
1234
|
if (isUnskippable) {
|
|
1126
|
-
unskippableSuites.push(path)
|
|
1235
|
+
unskippableSuites.push(resolve(path))
|
|
1127
1236
|
}
|
|
1128
1237
|
})
|
|
1129
1238
|
|
|
@@ -1200,8 +1309,7 @@ addHook({
|
|
|
1200
1309
|
filePattern: String.raw`lib/runner\.(?:c?js)$`,
|
|
1201
1310
|
}, function (runnerPackage, frameworkVersion) {
|
|
1202
1311
|
const Runner = runnerPackage.Runner ?? runnerPackage.default ?? runnerPackage
|
|
1203
|
-
if (typeof Runner !== 'function') return
|
|
1204
|
-
if (patched.has(Runner)) return
|
|
1312
|
+
if (typeof Runner !== 'function' || patched.has(Runner)) return
|
|
1205
1313
|
|
|
1206
1314
|
patched.add(Runner)
|
|
1207
1315
|
wrapRunnerEmit(Runner)
|
|
@@ -1226,12 +1334,12 @@ addHook({
|
|
|
1226
1334
|
runnerFrameworkErrors.delete(this)
|
|
1227
1335
|
}
|
|
1228
1336
|
|
|
1229
|
-
|
|
1337
|
+
let { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite)
|
|
1230
1338
|
// Root-level tests (direct children of root, no describe wrapper) keyed by file.
|
|
1231
1339
|
// Populated during the root 'suite' event so the normal finish path can include them
|
|
1232
1340
|
// in mixed-file status calculation.
|
|
1233
1341
|
const rootTestsByFile = new Map()
|
|
1234
|
-
// Counts how many
|
|
1342
|
+
// Counts how many selected tests per pure-root file still need their final attempt.
|
|
1235
1343
|
// Hits zero when the last test's lifecycle completes, triggering the suite finish.
|
|
1236
1344
|
const rootPendingCountByFile = new Map()
|
|
1237
1345
|
const rootFinalizationPendingCountByFile = new Map()
|
|
@@ -1304,33 +1412,18 @@ addHook({
|
|
|
1304
1412
|
}
|
|
1305
1413
|
rootPendingCountByFile.delete(file)
|
|
1306
1414
|
|
|
1307
|
-
const ctx = testFileToSuiteCtx.get(file)
|
|
1308
|
-
if (!ctx) {
|
|
1309
|
-
log.warn('No ctx found for suite', file)
|
|
1310
|
-
return
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
1415
|
const rootTests = rootTestsByFile.get(file) || []
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (global.__coverage__) {
|
|
1317
|
-
const coverageFiles = getCoveredFilesFromCoverage(global.__coverage__)
|
|
1318
|
-
testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file })
|
|
1319
|
-
mergeCoverage(global.__coverage__, originalCoverageMap)
|
|
1320
|
-
resetCoverage(global.__coverage__)
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
|
|
1416
|
+
finishRootSuite(file, getRootSuiteStatus(rootTests))
|
|
1324
1417
|
}
|
|
1325
1418
|
|
|
1326
|
-
function finishRootSuiteFallbackForFile (file) {
|
|
1419
|
+
function finishRootSuiteFallbackForFile (file, runner) {
|
|
1327
1420
|
const ctx = testFileToSuiteCtx.get(file)
|
|
1328
1421
|
if (!ctx || !rootPendingCountByFile.has(file)) return
|
|
1329
1422
|
|
|
1330
1423
|
const rootTests = rootTestsByFile.get(file) || []
|
|
1331
1424
|
const status = haveRootTestsFinished(rootTests) ? getRootSuiteStatus(rootTests) : 'fail'
|
|
1332
1425
|
rootPendingCountByFile.delete(file)
|
|
1333
|
-
|
|
1426
|
+
finishRootSuite(file, status, runner)
|
|
1334
1427
|
}
|
|
1335
1428
|
|
|
1336
1429
|
function finishRootSuiteAfterFinalAttempt (test) {
|
|
@@ -1361,13 +1454,7 @@ addHook({
|
|
|
1361
1454
|
this.prependListener('test', getOnTestHandler(true))
|
|
1362
1455
|
// Reporters are registered before Runner#run, so prepend this after the test handler
|
|
1363
1456
|
// to start the suite first and preserve the test event if a reporter throws.
|
|
1364
|
-
this.prependListener('test',
|
|
1365
|
-
const ctx = testFileToSuiteCtx.get(test.file)
|
|
1366
|
-
if (ctx?._pendingRootStart) {
|
|
1367
|
-
ctx._pendingRootStart = false
|
|
1368
|
-
testSuiteStartCh.runStores(ctx, () => {})
|
|
1369
|
-
}
|
|
1370
|
-
})
|
|
1457
|
+
this.prependListener('test', startRootSuite)
|
|
1371
1458
|
|
|
1372
1459
|
// Reporters are registered before this run wrapper. Prepend terminal handlers
|
|
1373
1460
|
// so a reporter error cannot strand a test span that Mocha already completed.
|
|
@@ -1407,8 +1494,16 @@ addHook({
|
|
|
1407
1494
|
this.prependListener('fail', getOnFailHandler(true, config))
|
|
1408
1495
|
|
|
1409
1496
|
this.prependListener('pending', getOnPendingHandler())
|
|
1497
|
+
// Static skips emit 'pending' without 'test', so their suite must start here too.
|
|
1498
|
+
this.prependListener('pending', startRootSuite)
|
|
1410
1499
|
|
|
1411
1500
|
this.prependListener('suite', function (suite) {
|
|
1501
|
+
if (suite.root) {
|
|
1502
|
+
// TIA and .only() have now selected the suites that can finish during this run.
|
|
1503
|
+
const selectedSuites = getSuitesByTestFile(suite)
|
|
1504
|
+
suitesByTestFile = selectedSuites.suitesByTestFile
|
|
1505
|
+
numSuitesByTestFile = selectedSuites.numSuitesByTestFile
|
|
1506
|
+
}
|
|
1412
1507
|
if (suite.root || !suite.tests.length) {
|
|
1413
1508
|
// This branch can be triggered when we have top level it(...) inside test files.
|
|
1414
1509
|
// In that case, they all (even if they are from different files) are going to be
|
|
@@ -1418,9 +1513,14 @@ addHook({
|
|
|
1418
1513
|
// below. Since 'suite' fires for root first, in the mixed case the ctx is created
|
|
1419
1514
|
// here and the describe-based handler finds it already set.
|
|
1420
1515
|
if (suite.root && suite.tests.length > 0) {
|
|
1516
|
+
const { _grep: grep, _invert: invert } = this
|
|
1517
|
+
const lastIndex = grep.lastIndex
|
|
1518
|
+
const selectedTests = suite.tests.filter(test => grep.test(test.fullTitle()) !== !!invert)
|
|
1519
|
+
// Inspecting a stateful grep must not change which tests Mocha subsequently selects.
|
|
1520
|
+
if (grep.lastIndex !== lastIndex) grep.lastIndex = lastIndex
|
|
1421
1521
|
const files = new Set(suite.tests.map(test => test.file).filter(Boolean))
|
|
1422
1522
|
for (const file of files) {
|
|
1423
|
-
const testsForFile =
|
|
1523
|
+
const testsForFile = selectedTests.filter(t => t.file === file)
|
|
1424
1524
|
rootTestsByFile.set(file, testsForFile)
|
|
1425
1525
|
// Only track the countdown for pure root-level files.
|
|
1426
1526
|
// Mixed files are finished by the normal 'suite end' path.
|
|
@@ -1461,10 +1561,10 @@ addHook({
|
|
|
1461
1561
|
this.prependListener('suite end', function (suite) {
|
|
1462
1562
|
if (suite.root) {
|
|
1463
1563
|
// Normal case: pure root-level files are finished by the 'test end' / 'hook end'
|
|
1464
|
-
// listeners via finishRootSuiteForFile.
|
|
1564
|
+
// listeners via finishRootSuiteForFile. Fallback cases remain here:
|
|
1465
1565
|
//
|
|
1466
|
-
// 1.
|
|
1467
|
-
// Start and immediately finish with 'skip'.
|
|
1566
|
+
// 1. Unstarted file: no 'test' or 'pending' event fired (e.g. all tests
|
|
1567
|
+
// were filtered out). Start and immediately finish with 'skip'.
|
|
1468
1568
|
//
|
|
1469
1569
|
// 2. Aborted mid-run (e.g. a beforeEach hook failure): Mocha skips remaining
|
|
1470
1570
|
// tests and jumps straight to 'suite end'. rootPendingCountByFile still has
|
|
@@ -1491,7 +1591,7 @@ addHook({
|
|
|
1491
1591
|
continue
|
|
1492
1592
|
}
|
|
1493
1593
|
|
|
1494
|
-
finishRootSuiteFallbackForFile(test.file)
|
|
1594
|
+
finishRootSuiteFallbackForFile(test.file, this)
|
|
1495
1595
|
}
|
|
1496
1596
|
}
|
|
1497
1597
|
return
|
|
@@ -1584,10 +1684,7 @@ addHook({
|
|
|
1584
1684
|
file: 'src/WorkerHandler.js',
|
|
1585
1685
|
}, (workerHandlerPackage) => {
|
|
1586
1686
|
shimmer.wrap(workerHandlerPackage.prototype, 'exec', exec => function (_, path) {
|
|
1587
|
-
if (!testFinishCh.hasSubscribers) {
|
|
1588
|
-
return exec.apply(this, arguments)
|
|
1589
|
-
}
|
|
1590
|
-
if (!path?.length) {
|
|
1687
|
+
if (!testFinishCh.hasSubscribers || !path?.length) {
|
|
1591
1688
|
return exec.apply(this, arguments)
|
|
1592
1689
|
}
|
|
1593
1690
|
const [testSuiteAbsolutePath] = path
|
|
@@ -899,6 +899,7 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
|
|
|
899
899
|
const shouldFinishTest = ctx && (
|
|
900
900
|
test._ddReporterStartFailed ||
|
|
901
901
|
test._ddReporterTerminalFailed ||
|
|
902
|
+
!test.fn ||
|
|
902
903
|
!getAfterEachHooks(test).length ||
|
|
903
904
|
(test._ddIsDisabled && !test._ddIsAttemptToFix)
|
|
904
905
|
)
|
|
@@ -906,8 +907,8 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
|
|
|
906
907
|
let testFinishInfo
|
|
907
908
|
let isFinalAttempt = false
|
|
908
909
|
|
|
909
|
-
// If there are afterEach to
|
|
910
|
-
//
|
|
910
|
+
// If there are afterEach hooks to run, we don't finish the test yet.
|
|
911
|
+
// Static skips have no function and never execute hooks. Disabled tests also finish immediately.
|
|
911
912
|
// In older mocha versions, pending tests don't run afterEach hooks, so we can't rely on
|
|
912
913
|
// getOnHookEndHandler to finish the test. This mirrors Jest's approach where the skip handler
|
|
913
914
|
// directly sets finalStatus without waiting for hooks
|
|
@@ -443,8 +443,7 @@ addHook({
|
|
|
443
443
|
filePattern: String.raw`lib/runner\.(?:c?js)$`,
|
|
444
444
|
}, function (runnerPackage) {
|
|
445
445
|
const Runner = runnerPackage.Runner ?? runnerPackage.default ?? runnerPackage
|
|
446
|
-
if (typeof Runner !== 'function') return
|
|
447
|
-
if (patchedRunners.has(Runner)) return
|
|
446
|
+
if (typeof Runner !== 'function' || patchedRunners.has(Runner)) return
|
|
448
447
|
|
|
449
448
|
patchedRunners.add(Runner)
|
|
450
449
|
shimmer.wrap(Runner.prototype, 'runTests', runTests => getRunTestsWrapper(runTests, config))
|
|
@@ -45,7 +45,7 @@ addHook({ name: 'mongodb', versions: ['>=4.6.0 <6.4.0'], file: 'lib/cmap/connect
|
|
|
45
45
|
|
|
46
46
|
addHook({ name: 'mongodb', versions: ['>=6.4.0'], file: 'lib/cmap/connection.js' }, Connection => {
|
|
47
47
|
const proto = Connection.Connection.prototype
|
|
48
|
-
shimmer.wrap(proto, 'command', command => wrapConnectionCommand(command, 'command',
|
|
48
|
+
shimmer.wrap(proto, 'command', command => wrapConnectionCommand(command, 'command', instrumentPromise))
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
addHook({ name: 'mongodb', versions: ['>=3.3 <4'], file: 'lib/core/wireprotocol/index.js' }, wp => wrapWp(wp))
|
|
@@ -95,8 +95,8 @@ function wrapUnifiedCommand (command, operation, name) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
function wrapConnectionCommand (command, operation,
|
|
99
|
-
const opts = { name }
|
|
98
|
+
function wrapConnectionCommand (command, operation, instrumentFn = instrument) {
|
|
99
|
+
const opts = { name: undefined }
|
|
100
100
|
return function (ns, ops) {
|
|
101
101
|
if (!startCh.hasSubscribers) {
|
|
102
102
|
return command.apply(this, arguments)
|
|
@@ -83,7 +83,7 @@ addHook({
|
|
|
83
83
|
|
|
84
84
|
let callbackWrapped = false
|
|
85
85
|
|
|
86
|
-
const wrapCallbackIfExist = (args
|
|
86
|
+
const wrapCallbackIfExist = (args) => {
|
|
87
87
|
const lastArgumentIndex = args.length - 1
|
|
88
88
|
|
|
89
89
|
if (typeof args[lastArgumentIndex] === 'function') {
|
|
@@ -106,7 +106,7 @@ addHook({
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
return startCh.runStores(ctx, () => {
|
|
109
|
-
wrapCallbackIfExist(args
|
|
109
|
+
wrapCallbackIfExist(args)
|
|
110
110
|
|
|
111
111
|
// A callback query executes synchronously inside `method.apply`, so the
|
|
112
112
|
// exec scope (and the deferred driver call it spawns) must be entered
|
|
@@ -121,7 +121,7 @@ addHook({
|
|
|
121
121
|
shimmer.wrap(res, 'exec', originalExec => {
|
|
122
122
|
return function wrappedExec (...args) {
|
|
123
123
|
if (!callbackWrapped) {
|
|
124
|
-
wrapCallbackIfExist(args
|
|
124
|
+
wrapCallbackIfExist(args)
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
const execResult = execCh.runStores(ctx, () => originalExec.apply(this, args))
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { LLMOBS_AUDIO_ACCUMULATE_MAX_BYTES } = require('../../../dd-trace/src/llmobs/constants/audio')
|
|
4
|
+
|
|
5
|
+
// Leave room for the WAV header when the retained PCM is encoded.
|
|
6
|
+
const MAX_AUDIO = LLMOBS_AUDIO_ACCUMULATE_MAX_BYTES - 128
|
|
7
|
+
const RATES = new Set([8000, 16_000, 24_000, 48_000])
|
|
8
|
+
const INVALID_BASE64 = /[^A-Za-z0-9+/]/
|
|
9
|
+
|
|
10
|
+
/** @param {object} configuration */
|
|
11
|
+
function pcmRate (configuration = {}) {
|
|
12
|
+
return configuration.mediaType === 'audio/lpcm' && configuration.sampleSizeBits === 16 &&
|
|
13
|
+
configuration.channelCount === 1 && configuration.encoding === 'base64' &&
|
|
14
|
+
RATES.has(configuration.sampleRateHertz)
|
|
15
|
+
? configuration.sampleRateHertz
|
|
16
|
+
: 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @param {string} encoded */
|
|
20
|
+
function decodedLength (encoded) {
|
|
21
|
+
if (typeof encoded !== 'string' || encoded.length % 4) return
|
|
22
|
+
const padding = encoded.endsWith('==') ? 2 : encoded.endsWith('=') ? 1 : 0
|
|
23
|
+
if (INVALID_BASE64.test(encoded.slice(0, encoded.length - padding))) return
|
|
24
|
+
return encoded.length / 4 * 3 - padding
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A bounded, connection-wide sample timeline. Content-name changes do not reset its origin. */
|
|
28
|
+
class InputAudio {
|
|
29
|
+
rate = 0
|
|
30
|
+
valid = true
|
|
31
|
+
total = 0
|
|
32
|
+
anchor
|
|
33
|
+
#chunks = []
|
|
34
|
+
#bytes = 0
|
|
35
|
+
|
|
36
|
+
/** @param {object} configuration */
|
|
37
|
+
configure (configuration) {
|
|
38
|
+
const rate = pcmRate(configuration)
|
|
39
|
+
if (!rate || (this.total && rate !== this.rate)) this.valid = false
|
|
40
|
+
this.rate = rate
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} encoded
|
|
45
|
+
* @param {number} now Milliseconds at SDK consumption, not an acknowledgement from the provider.
|
|
46
|
+
*/
|
|
47
|
+
append (encoded, now) {
|
|
48
|
+
const length = decodedLength(encoded)
|
|
49
|
+
if (length === undefined) {
|
|
50
|
+
this.valid = false
|
|
51
|
+
this.clear()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (length % 2) this.valid = false
|
|
55
|
+
this.total += length
|
|
56
|
+
if (this.rate) this.anchor = now - this.total * 1000 / (2 * this.rate)
|
|
57
|
+
if (!this.valid || !length) return
|
|
58
|
+
// Decode from a base64 quartet boundary, retaining at most two alignment bytes
|
|
59
|
+
// beyond the cap instead of allocating the entire incoming frame.
|
|
60
|
+
const start = Math.max(0, length - MAX_AUDIO)
|
|
61
|
+
const retained = Buffer.from(encoded.slice(Math.floor(start / 3) * 4), 'base64').subarray(start % 3)
|
|
62
|
+
this.#chunks.push(retained)
|
|
63
|
+
this.#bytes += retained.length
|
|
64
|
+
while (this.#bytes > MAX_AUDIO) {
|
|
65
|
+
const first = this.#chunks[0]
|
|
66
|
+
const excess = this.#bytes - MAX_AUDIO
|
|
67
|
+
if (first.length <= excess) {
|
|
68
|
+
this.#chunks.shift()
|
|
69
|
+
this.#bytes -= first.length
|
|
70
|
+
} else {
|
|
71
|
+
this.#chunks[0] = Buffer.from(first.subarray(excess))
|
|
72
|
+
this.#bytes -= excess
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {number} startMs
|
|
79
|
+
* @param {number} endMs
|
|
80
|
+
*/
|
|
81
|
+
clip (startMs, endMs) {
|
|
82
|
+
if (!this.valid || !this.rate || !Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) return
|
|
83
|
+
const start = Math.floor(startMs * this.rate / 1000) * 2
|
|
84
|
+
const end = Math.floor(endMs * this.rate / 1000) * 2
|
|
85
|
+
const base = this.total - this.#bytes
|
|
86
|
+
if (start < base || end > this.total) return
|
|
87
|
+
const clip = Buffer.allocUnsafe(end - start)
|
|
88
|
+
let offset = base
|
|
89
|
+
let written = 0
|
|
90
|
+
for (const chunk of this.#chunks) {
|
|
91
|
+
const chunkEnd = offset + chunk.length
|
|
92
|
+
if (chunkEnd > start) {
|
|
93
|
+
written += chunk.copy(clip, written, Math.max(start - offset, 0), Math.min(end - offset, chunk.length))
|
|
94
|
+
}
|
|
95
|
+
offset = chunkEnd
|
|
96
|
+
if (offset >= end) break
|
|
97
|
+
}
|
|
98
|
+
return clip
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clear () {
|
|
102
|
+
this.#chunks = []
|
|
103
|
+
this.#bytes = 0
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Ordered PCM playback projection for one response, including genuine underrun silence. */
|
|
108
|
+
class OutputAudio {
|
|
109
|
+
rate = 0
|
|
110
|
+
start
|
|
111
|
+
end
|
|
112
|
+
interrupted
|
|
113
|
+
generatedBytes = 0
|
|
114
|
+
timingValid = true
|
|
115
|
+
omittedReason
|
|
116
|
+
#chunks = []
|
|
117
|
+
#bytes = 0
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} encoded
|
|
121
|
+
* @param {object} configuration
|
|
122
|
+
* @param {number} now
|
|
123
|
+
*/
|
|
124
|
+
append (encoded, configuration, now) {
|
|
125
|
+
const length = decodedLength(encoded)
|
|
126
|
+
if (length !== undefined) this.generatedBytes += length
|
|
127
|
+
// Late chunks cannot resurrect playback or invalidate the clip already cut at interruption.
|
|
128
|
+
if (this.interrupted !== undefined) return
|
|
129
|
+
const rate = pcmRate(configuration)
|
|
130
|
+
if (length === undefined || !rate || length % 2 || (this.rate && this.rate !== rate)) {
|
|
131
|
+
this.timingValid = false
|
|
132
|
+
this.omittedReason = 'invalid_audio'
|
|
133
|
+
this.#chunks = []
|
|
134
|
+
this.#bytes = 0
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (!length) return
|
|
138
|
+
this.rate ||= rate
|
|
139
|
+
this.start ??= now
|
|
140
|
+
const previousEnd = this.end ?? now
|
|
141
|
+
const gap = Math.floor(Math.max(0, now - previousEnd) * rate / 1000) * 2
|
|
142
|
+
this.end = previousEnd + (gap + length) * 1000 / (2 * rate)
|
|
143
|
+
if (!this.omittedReason && this.#bytes + gap + length <= MAX_AUDIO) {
|
|
144
|
+
if (gap) this.#chunks.push(Buffer.alloc(gap))
|
|
145
|
+
this.#chunks.push(Buffer.from(encoded, 'base64'))
|
|
146
|
+
this.#bytes += gap + length
|
|
147
|
+
} else {
|
|
148
|
+
this.omittedReason ||= 'retention_limit'
|
|
149
|
+
this.#chunks = []
|
|
150
|
+
this.#bytes = 0
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @param {number} now */
|
|
155
|
+
interrupt (now) {
|
|
156
|
+
if (this.interrupted !== undefined) return
|
|
157
|
+
this.interrupted = now
|
|
158
|
+
if (this.start === undefined || this.end === undefined || !this.rate) return
|
|
159
|
+
const frames = Math.floor(Math.max(0, now - this.start) * this.rate / 1000)
|
|
160
|
+
this.end = Math.min(this.end, this.start + frames * 1000 / this.rate)
|
|
161
|
+
if (this.#bytes > frames * 2) {
|
|
162
|
+
const clip = Buffer.concat(this.#chunks, this.#bytes).subarray(0, frames * 2)
|
|
163
|
+
this.#chunks = [Buffer.from(clip)]
|
|
164
|
+
this.#bytes = clip.length
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
pcm () {
|
|
169
|
+
if (!this.omittedReason && this.#bytes) return Buffer.concat(this.#chunks, this.#bytes)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { InputAudio, OutputAudio }
|