diff --git a/Cargo.lock b/Cargo.lock index afac19e..05979ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -63,9 +72,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -73,9 +82,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -91,9 +100,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blazesym" @@ -101,7 +110,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48ceccc54b9c3e60e5f36b0498908c8c0f87387229cb0e0e5d65a074e00a8ba4" dependencies = [ - "cpp_demangle", + "cpp_demangle 0.5.1", "gimli", "libc", "memmap2", @@ -109,6 +118,15 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -118,6 +136,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bumpalo" version = "3.20.2" @@ -130,6 +154,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cadence" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca08f6db9f0c963249cf23a27820f9d973d2acaad4d6bfaeb858b58ebd58a6a" +dependencies = [ + "crossbeam-channel", +] + [[package]] name = "cast" version = "0.3.0" @@ -138,9 +171,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -174,20 +207,31 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -226,6 +270,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpp_demangle" version = "0.5.1" @@ -235,6 +288,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crashtracker" version = "0.2.0" @@ -247,6 +309,31 @@ dependencies = [ "serde_json", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "ctor" version = "0.2.9" @@ -267,20 +354,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "datadog-library-config" -version = "0.0.2" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v18.1.0#5fdc3357f0070d5ad11dcaf89014b7479d32104f" -dependencies = [ - "anyhow", - "memfd", - "rand", - "rmp", - "rmp-serde", - "serde", - "serde_yaml", -] - [[package]] name = "debugid" version = "0.8.0" @@ -290,6 +363,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dispatch2" version = "0.3.1" @@ -346,6 +429,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -446,6 +540,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -508,9 +612,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -565,9 +669,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -578,7 +682,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -586,16 +689,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -653,12 +755,12 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -674,9 +776,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -690,14 +792,31 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -706,9 +825,24 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdatadog-nodejs-capabilities" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "futures-core", + "http", + "js-sys", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", +] [[package]] name = "libdd-capabilities" @@ -721,6 +855,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "libdd-capabilities" +version = "2.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "bytes", + "http", + "thiserror", +] + [[package]] name = "libdd-capabilities-impl" version = "2.0.0" @@ -729,8 +874,21 @@ dependencies = [ "bytes", "http", "http-body-util", - "libdd-capabilities", - "libdd-common", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", + "libdd-common 4.2.0", + "tokio", +] + +[[package]] +name = "libdd-capabilities-impl" +version = "2.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "bytes", + "http", + "http-body-util", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", "tokio", ] @@ -768,6 +926,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "libdd-common" +version = "5.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "bytes", + "cc", + "const_format", + "futures", + "futures-core", + "futures-util", + "hex", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "libc", + "nix 0.29.0", + "pin-project", + "regex", + "serde", + "static_assertions", + "thiserror", + "tokio", + "tower-service", + "windows-sys 0.52.0", +] + [[package]] name = "libdd-crashtracker" version = "1.0.0" @@ -780,7 +968,7 @@ dependencies = [ "errno", "http", "libc", - "libdd-common", + "libdd-common 4.2.0", "libdd-libunwind-sys", "libdd-telemetry", "nix 0.29.0", @@ -801,6 +989,40 @@ dependencies = [ "windows 0.59.0", ] +[[package]] +name = "libdd-data-pipeline" +version = "6.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "arc-swap", + "async-trait", + "bytes", + "either", + "getrandom 0.2.17", + "http", + "http-body-util", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", + "libdd-ddsketch 1.0.1 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-dogstatsd-client", + "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-tinybytes", + "libdd-trace-normalization", + "libdd-trace-protobuf 3.0.2", + "libdd-trace-stats", + "libdd-trace-utils", + "rmp-serde", + "serde", + "serde_json", + "sha2", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + [[package]] name = "libdd-ddsketch" version = "1.0.1" @@ -809,13 +1031,34 @@ dependencies = [ "prost", ] +[[package]] +name = "libdd-ddsketch" +version = "1.0.1" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "prost", +] + +[[package]] +name = "libdd-dogstatsd-client" +version = "3.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "cadence", + "http", + "libdd-common 5.0.0", + "serde", + "tracing", +] + [[package]] name = "libdd-library-config" version = "1.0.0" source = "git+https://github.com/DataDog/libdatadog.git?tag=v29.0.0#001bd56fcbba34fa4ec3f9798a6c4fbcddeffa40" dependencies = [ "anyhow", - "libdd-trace-protobuf", + "libdd-trace-protobuf 1.1.0", "memfd", "prost", "rand", @@ -826,6 +1069,20 @@ dependencies = [ "serde_yaml", ] +[[package]] +name = "libdd-library-config" +version = "1.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?rev=353134770b312b7ccd2df6afabc253090b948e5f#353134770b312b7ccd2df6afabc253090b948e5f" +dependencies = [ + "anyhow", + "memfd", + "rand", + "rmp", + "rmp-serde", + "serde", + "serde_yaml", +] + [[package]] name = "libdd-libunwind-sys" version = "1.0.2" @@ -845,9 +1102,26 @@ dependencies = [ "async-trait", "futures", "futures-util", - "libdd-capabilities", - "libdd-capabilities-impl", - "libdd-common", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", + "libdd-common 4.2.0", + "tokio", + "tokio-util", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "libdd-shared-runtime" +version = "1.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "async-trait", + "futures", + "futures-util", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", "tokio", "tokio-util", "tracing", @@ -868,9 +1142,9 @@ dependencies = [ "http", "http-body-util", "libc", - "libdd-common", - "libdd-ddsketch", - "libdd-shared-runtime", + "libdd-common 4.2.0", + "libdd-ddsketch 1.0.1 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", + "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", "serde", "serde_json", "sys-info", @@ -881,6 +1155,39 @@ dependencies = [ "winver", ] +[[package]] +name = "libdd-tinybytes" +version = "1.1.1" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "serde", +] + +[[package]] +name = "libdd-trace-normalization" +version = "2.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "libdd-trace-protobuf 3.0.2", +] + +[[package]] +name = "libdd-trace-obfuscation" +version = "4.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "fluent-uri", + "libdd-common 5.0.0", + "libdd-trace-protobuf 3.0.2", + "libdd-trace-utils", + "log", + "percent-encoding", + "serde", + "serde_json", +] + [[package]] name = "libdd-trace-protobuf" version = "1.1.0" @@ -891,6 +1198,75 @@ dependencies = [ "serde_bytes", ] +[[package]] +name = "libdd-trace-protobuf" +version = "3.0.2" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "prost", + "serde", + "serde_bytes", +] + +[[package]] +name = "libdd-trace-stats" +version = "5.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "arc-swap", + "async-trait", + "hashbrown 0.15.5", + "http", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", + "libdd-ddsketch 1.0.1 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-trace-obfuscation", + "libdd-trace-protobuf 3.0.2", + "libdd-trace-utils", + "rmp-serde", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "libdd-trace-utils" +version = "8.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "base64", + "bytes", + "futures", + "getrandom 0.2.17", + "hex", + "http", + "http-body", + "http-body-util", + "indexmap", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", + "libdd-tinybytes", + "libdd-trace-normalization", + "libdd-trace-protobuf 3.0.2", + "prost", + "rand", + "rmp", + "rmp-serde", + "rmpv", + "rustc-hash", + "serde", + "serde_json", + "thin-vec", + "tokio", + "tracing", +] + [[package]] name = "libloading" version = "0.8.9" @@ -912,8 +1288,8 @@ name = "library-config" version = "0.2.0" dependencies = [ "anyhow", - "datadog-library-config", "getrandom 0.2.17", + "libdd-library-config 1.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=353134770b312b7ccd2df6afabc253090b948e5f)", "serde", "serde-wasm-bindgen", "wasm-bindgen", @@ -977,9 +1353,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5faa9f23e86bd5768d76def086192ff5f869fb088da12a976ea21e9796b975f6" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -987,9 +1363,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -1273,9 +1649,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -1321,6 +1697,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project" version = "1.1.11" @@ -1348,16 +1730,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "pin-utils" +name = "pipeline" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +dependencies = [ + "bytes", + "console_error_panic_hook", + "getrandom 0.2.17", + "http", + "js-sys", + "libdatadog-nodejs-capabilities", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-data-pipeline", + "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-trace-protobuf 3.0.2", + "libdd-trace-stats", + "libdd-trace-utils", + "rmp-serde", + "serde", + "serde_json", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", +] [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" @@ -1401,7 +1802,7 @@ name = "process-discovery" version = "0.1.0" dependencies = [ "anyhow", - "libdd-library-config", + "libdd-library-config 1.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v29.0.0)", "napi", "napi-derive", ] @@ -1452,9 +1853,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", @@ -1480,6 +1881,26 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.3" @@ -1542,12 +1963,27 @@ dependencies = [ "serde", ] +[[package]] +name = "rmpv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417" +dependencies = [ + "rmp", +] + [[package]] name = "rustc-demangle" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustix" version = "1.1.4" @@ -1563,9 +1999,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", @@ -1590,18 +2026,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -1688,9 +2124,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1760,6 +2196,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -1780,6 +2217,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1788,9 +2236,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "slab" @@ -1834,9 +2282,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symbolic-common" -version = "12.17.2" +version = "12.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751a2823d606b5d0a7616499e4130a516ebd01a44f39811be2b9600936509c23" +checksum = "332615d90111d8eeaf86a84dc9bbe9f65d0d8c5cf11b4caccedc37754eb0dcfd" dependencies = [ "debugid", "memmap2", @@ -1846,11 +2294,11 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.17.2" +version = "12.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b237cfbe320601dd24b4ac817a5b68bb28f5508e33f08d42be0682cadc8ac9" +checksum = "912017718eb4d21930546245af9a3475c9dccf15675a5c215664e76621afc471" dependencies = [ - "cpp_demangle", + "cpp_demangle 0.4.5", "msvc-demangler", "rustc-demangle", "symbolic-common", @@ -1877,6 +2325,12 @@ dependencies = [ "libc", ] +[[package]] +name = "thin-vec" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" + [[package]] name = "thiserror" version = "1.0.69" @@ -1899,9 +2353,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -1914,9 +2368,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -1977,6 +2431,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1985,9 +2445,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -2009,9 +2469,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -2019,6 +2479,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -2046,11 +2512,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -2059,14 +2525,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -2077,23 +2543,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2101,9 +2563,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -2114,18 +2576,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.64" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" +checksum = "29826f9d9ecaa314c480d376b276d1c790e6cb6a4681fab8532da69cbabf977d" dependencies = [ "async-trait", "cast", @@ -2145,9 +2607,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.64" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" +checksum = "c610311887f9e6599a546d278d12d69dfd3a3e92639b2129e4b11ad6cf1961d6" dependencies = [ "proc-macro2", "quote", @@ -2156,9 +2618,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" +checksum = "60238e5b4b1b295701d6f9a66d2a126fe19990348f5fb9dae3b623a370119d94" [[package]] name = "wasm-encoder" @@ -2194,16 +2656,6 @@ dependencies = [ "semver", ] -[[package]] -name = "web-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2583,6 +3035,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -2664,18 +3122,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 4508363..24099b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" default-members = [ "crates/crashtracker", "crates/process_discovery", @@ -12,6 +13,5 @@ codegen-units = 1 lto = true opt-level = "z" panic = "abort" -strip = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# strip = "none" +debug = true diff --git a/crates/capabilities/Cargo.toml b/crates/capabilities/Cargo.toml new file mode 100644 index 0000000..d20be8f --- /dev/null +++ b/crates/capabilities/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "libdatadog-nodejs-capabilities" +version = "0.1.0" +edition = "2021" +description = "Wasm capability implementations for libdatadog-nodejs (backed by JS transports)" + +[lib] +crate-type = ["rlib"] + +[dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +http = "1" +bytes = "1.4" +futures-core = "0.3" +anyhow = "1" +libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", branch = "main" } + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/crates/capabilities/src/http.rs b/crates/capabilities/src/http.rs new file mode 100644 index 0000000..8b3ad03 --- /dev/null +++ b/crates/capabilities/src/http.rs @@ -0,0 +1,271 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Wasm implementation of [`HttpClientTrait`] backed by Node.js `http.request`. +//! +//! The JS transport is imported via `wasm_bindgen(module = ...)` from +//! `http_transport.js`, which ships alongside the wasm output. + +use std::future::Future; +use std::io::Write as _; +use std::sync::LazyLock; +use std::time::Duration; + +use bytes::Bytes; +use http::{HeaderMap, HeaderName, HeaderValue}; +use js_sys::{self, Array, JsString, Number, Uint8Array}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; + +use libdd_capabilities::http::{HttpClientCapability, HttpError}; +use libdd_capabilities::maybe_send::MaybeSend; +use libdd_capabilities::sleep::SleepCapability; + +static WASM_MEMORY: LazyLock = LazyLock::new(|| wasm_bindgen::memory()); + +#[wasm_bindgen(module = "/src/http_transport.js")] +extern "C" { + #[wasm_bindgen(js_name = "httpRequest")] + fn http_request( + host: &str, + port: u16, + is_https: bool, + socket_path: &str, + head_ptr: *const u8, + head_len: u32, + body_ptr: *const u8, + body_len: u32, + wasm_memory: &JsValue, + ) -> js_sys::Promise; + + #[wasm_bindgen(js_name = "sleep")] + fn js_sleep(ms: f64) -> js_sys::Promise; + + #[wasm_bindgen(js_name = "setStorage")] + pub fn set_storage(new_storage: &JsValue); + + #[wasm_bindgen(js_name = "setResponseHeaderObserver")] + pub fn set_response_header_observer(observer: &JsValue); +} + +/// Wasm [`HttpClientTrait`] implementation that delegates to Node.js HTTP. +/// +/// Named `DefaultHttpClient` to match the native version's public API. +#[derive(Debug, Clone)] +pub struct DefaultHttpClient; + +impl HttpClientCapability for DefaultHttpClient { + fn new_client() -> Self { + Self + } + + #[allow(clippy::manual_async_fn)] + fn request( + &self, + req: http::Request, + ) -> impl Future, HttpError>> + MaybeSend { + async move { + let scheme = req.uri().scheme_str().unwrap_or("http"); + + // Unix domain socket / Windows named pipe: ddcommon's `parse_uri` + // hex-encodes the socket path into the URI authority (there is no + // standard URL form for socket paths). On wasm the request bypasses + // ddcommon's native (hyper) connector and reaches us directly, so we + // decode the path here and route over the socket instead of TCP. + let (host, port, is_https, socket_path) = if scheme == "unix" || scheme == "windows" { + (String::new(), 0u16, false, decode_socket_path(req.uri())?) + } else { + let is_https = scheme == "https"; + let host = req.uri().host().ok_or_else(|| { + HttpError::InvalidRequest(anyhow::anyhow!("missing host in URI")) + })?; + let port = req + .uri() + .port_u16() + .unwrap_or(if is_https { 443 } else { 80 }); + (host.to_owned(), port, is_https, String::new()) + }; + + // For a socket request there is no meaningful network host; HTTP/1.1 + // still requires a Host header, so send a stable placeholder (the + // agent does not validate Host over a socket). + let head = if socket_path.is_empty() { + serialize_request_head(&req, &host, port, is_https, false)? + } else { + serialize_request_head(&req, "localhost", port, is_https, true)? + }; + let body = req.into_body(); + + let result = JsFuture::from(http_request( + &host, + port, + is_https, + &socket_path, + head.as_ptr(), + head.len() as u32, + body.as_ptr(), + body.len() as u32, + WASM_MEMORY.as_ref(), + )) + .await + .map_err(|e| HttpError::Network(anyhow::anyhow!("{:?}", e)))?; + + let result: js_sys::ArrayTuple<(Number, Array, Uint8Array)> = + js_sys::ArrayTuple::unchecked_from_js(result); + + let status = result + .get0() + .as_f64() + .ok_or_else(|| HttpError::Other(anyhow::anyhow!("status is not a number")))? + as u16; + + let headers = parse_response_headers(result.get1())?; + + let body = Bytes::from(result.get2().to_vec()); + + let mut builder = http::Response::builder().status(status); + *builder.headers_mut().unwrap() = headers; + builder.body(body).map_err(|e| HttpError::Other(e.into())) + } + } +} + +/// Wasm [`SleepCapability`] backed by JS `setTimeout`. +/// +/// TraceExporter requires its capability bundle to implement `SleepCapability` +/// (used for retry backoff). Native code uses `tokio::time::sleep`; in wasm we +/// delegate to `setTimeout` via a JS-returned Promise. +impl SleepCapability for DefaultHttpClient { + fn new() -> Self { + Self + } + + #[allow(clippy::manual_async_fn)] + fn sleep(&self, duration: Duration) -> impl Future + MaybeSend { + async move { + let ms = duration.as_millis() as f64; + let _ = JsFuture::from(js_sleep(ms)).await; + } + } +} + +/// Parse response headers from a JS object `{ "header-name": "value", ... }`. +/// +/// Node.js `res.headers` returns lowercased header names with string values. +fn parse_response_headers(header_js: Array) -> Result { + let len = header_js.length() as usize; + let mut headers = HeaderMap::with_capacity(len / 2); + for i in 0..(len / 2) { + let key = header_js.get((i * 2) as u32).as_string(); + let val = header_js.get((i * 2 + 1) as u32).as_string(); + if let (Some(key), Some(val)) = (key, val) { + // Response headers come from the agent (untrusted over plaintext + // HTTP); skip any the http crate rejects rather than unwrapping + // and trapping the whole wasm instance on one malformed header. + if let (Ok(name), Ok(value)) = ( + HeaderName::from_bytes(key.as_bytes()), + HeaderValue::from_maybe_shared(Bytes::from(val)), + ) { + headers.insert(name, value); + } + } + } + Ok(headers) +} + +/// Decode the socket path that ddcommon's `parse_uri` hex-encoded into the URI +/// authority for `unix://` / `windows:` agent URLs (see `encode_uri_path_in_authority` +/// in libdd-common). The authority is the lowercase hex of the raw path bytes. +fn decode_socket_path(uri: &http::Uri) -> Result { + let authority = uri + .authority() + .ok_or_else(|| HttpError::InvalidRequest(anyhow::anyhow!("socket URI missing authority")))? + .as_str(); + let bytes = hex_decode(authority).ok_or_else(|| { + HttpError::InvalidRequest(anyhow::anyhow!("socket path authority is not valid hex")) + })?; + String::from_utf8(bytes) + .map_err(|e| HttpError::InvalidRequest(anyhow::anyhow!("socket path is not utf-8: {e}"))) +} + +/// Minimal hex decoder for the socket-path authority. Returns `None` on any +/// malformed input (odd length or non-hex digit) rather than panicking. +fn hex_decode(s: &str) -> Option> { + let bytes = s.as_bytes(); + if !bytes.len().is_multiple_of(2) { + return None; + } + let mut out = Vec::with_capacity(bytes.len() / 2); + for pair in bytes.chunks_exact(2) { + let hi = (pair[0] as char).to_digit(16)?; + let lo = (pair[1] as char).to_digit(16)?; + out.push((hi * 16 + lo) as u8); + } + Some(out) +} + +/// Serialize the full HTTP/1.1 request head (request line + Host + Content-Length +/// + user headers + terminating CRLF) into a contiguous byte buffer. +/// +/// The buffer is handed to JS by pointer; JS assigns it to +/// `req._header`, bypassing Node's `_storeHeader` serialization. +/// +/// `is_socket` requests (unix socket / named pipe) omit the `:port` suffix on +/// the Host header — there is no TCP port for a socket transport. +fn serialize_request_head( + req: &http::Request, + host: &str, + port: u16, + is_https: bool, + is_socket: bool, +) -> Result, HttpError> { + let method = req.method().as_str(); + let path_and_query = req + .uri() + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + let body_len = req.body().len(); + let headers = req.headers(); + + let mut buf = Vec::with_capacity(256 + headers.len() * 64); + + buf.extend_from_slice(method.as_bytes()); + buf.push(b' '); + buf.extend_from_slice(path_and_query.as_bytes()); + buf.extend_from_slice(b" HTTP/1.1\r\n"); + + buf.extend_from_slice(b"Host: "); + buf.extend_from_slice(host.as_bytes()); + if !is_socket { + let default_port = if is_https { 443 } else { 80 }; + if port != default_port { + write!(&mut buf, ":{port}").map_err(|e| HttpError::Other(e.into()))?; + } + } + buf.extend_from_slice(b"\r\n"); + + write!(&mut buf, "Content-Length: {body_len}\r\n").map_err(|e| HttpError::Other(e.into()))?; + + for (name, value) in headers.iter() { + // The request-framing headers above (Host, Content-Length) are + // authoritative. Skip any caller-supplied duplicates of them (and + // Transfer-Encoding) so they can't be emitted twice — duplicate/ + // conflicting framing headers are a request-smuggling vector when the + // agent URL is reached through a proxy. + if matches!( + name.as_str(), + "host" | "content-length" | "transfer-encoding" + ) { + continue; + } + buf.extend_from_slice(name.as_str().as_bytes()); + buf.extend_from_slice(b": "); + buf.extend_from_slice(value.as_bytes()); + buf.extend_from_slice(b"\r\n"); + } + + buf.extend_from_slice(b"\r\n"); + + Ok(buf) +} diff --git a/crates/capabilities/src/http_transport.js b/crates/capabilities/src/http_transport.js new file mode 100644 index 0000000..1c01bb3 --- /dev/null +++ b/crates/capabilities/src/http_transport.js @@ -0,0 +1,108 @@ +const http = require('http'); +const https = require('https'); + +let storage = (f) => f(); + +module.exports.sleep = function (ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +module.exports.setStorage = function (new_storage) { + storage = new_storage; +} + +// Optional observer invoked with each agent response's raw headers +// (Node's flat [name, value, name, value, ...] array). Lets the host tracer +// read response-only headers (e.g. Datadog-Container-Tags-Hash) that are not +// otherwise surfaced through the wasm response body. Never throws into the +// transport: a misbehaving observer must not break trace delivery. +// +// The observer runs synchronously on the response 'end' event, so it must be +// non-blocking and return quickly — long-running synchronous work here would +// stall the event loop. +let responseHeaderObserver = null; + +module.exports.setResponseHeaderObserver = function (new_observer) { + responseHeaderObserver = new_observer; +} + +module.exports.httpRequest = function (host, port, isHttps, socketPath, head_ptr, head_len, body_ptr, body_len, wasm_memory) { + // A non-empty socketPath routes over a Unix domain socket (or Windows named + // pipe) instead of TCP. Sockets are always plaintext HTTP/1.1, so https is + // ignored in that mode. + const useSocket = typeof socketPath === 'string' && socketPath.length > 0; + const transport = useSocket ? http : (isHttps ? https : http); + + function isDetachedBufferError(err) { + return err instanceof TypeError && /detached/i.test(err.message); + } + + function attempt() { + return new Promise((resolve, reject) => { + storage(() => { + // wasm_memory.buffer is replaced each time WebAssembly.Memory grows, so + // the views must be recreated on every attempt against the current buffer. + const headView = new Uint8Array(wasm_memory.buffer, head_ptr, head_len); + const bodyView = new Uint8Array(wasm_memory.buffer, body_ptr, body_len); + + // host/port (or socketPath) drive connection selection; method/path/ + // headers are placeholders because we replace the rendered head below. + const requestOptions = useSocket + ? { socketPath, method: 'POST', path: '/' } + : { host, port, method: 'POST', path: '/' }; + const req = transport.request(requestOptions, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const body = Buffer.concat(chunks) + if (responseHeaderObserver !== null) { + try { + responseHeaderObserver(res.rawHeaders); + } catch (err) { + // Only read `err.message` (a string) rather than stringifying an + // arbitrary thrown value, so a hostile/throwing toString on the + // error can't turn the log line into its own failure path. + process.stderr.write("responseHeaderObserver error: " + (err && err.message) + "\n"); + } + } + resolve([ + res.statusCode, + res.rawHeaders, + // Copy the exact body bytes. `body` is a Buffer from Buffer.concat, + // which for small payloads is a view into Node's shared pool, so + // `body.buffer` is the whole pool — slicing by offset/length (via + // the Uint8Array(typedArray) copy ctor) is required to avoid + // handing the Rust side unrelated pooled memory. + new Uint8Array(body), + ]); + }); + }); + req.on('error', reject); + + // Bypass Node's headers: the Rust side has already produced the full + // request head in HTTP/1.1 wire format. Setting _header before write() + // makes write/end skip _implicitHeader and _send prepends our bytes. + + try { + req._header = Buffer.from(headView); + req.write(bodyView); + req.end(); + } catch (err) { + reject(err); + } + }) + }); + } + + function attemptWithRetry() { + return attempt().catch((err) => { + process.stderr.write("httpRequest error: " + err + "\n") + if (isDetachedBufferError(err)) { + return attemptWithRetry(); + } + throw err; + }); + } + + return attemptWithRetry(); +}; diff --git a/crates/capabilities/src/lib.rs b/crates/capabilities/src/lib.rs new file mode 100644 index 0000000..a6f4deb --- /dev/null +++ b/crates/capabilities/src/lib.rs @@ -0,0 +1,18 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Wasm capability implementations for libdatadog-nodejs. +//! +//! `WasmCapabilities` is the bundle struct that implements all capability +//! traits using wasm_bindgen + JS transports. The wasm binding crate pins +//! this type as the generic parameter for libdatadog structs. + +pub mod http; + +pub use http::DefaultHttpClient; + +/// Bundle struct for wasm platform capabilities. +/// +/// Currently delegates to `DefaultHttpClient` for HTTP. As more capability +/// traits are added (spawn, sleep, etc.), this type will implement all of them. +pub type WasmCapabilities = DefaultHttpClient; diff --git a/crates/datadog-js-zstd/src/lib.rs b/crates/datadog-js-zstd/src/lib.rs index 4896301..fed9b1c 100644 --- a/crates/datadog-js-zstd/src/lib.rs +++ b/crates/datadog-js-zstd/src/lib.rs @@ -1,11 +1,8 @@ -use wasm_bindgen::prelude::*; use js_sys::Uint8Array; +use wasm_bindgen::prelude::*; #[wasm_bindgen] -pub fn zstd_compress( - data: Uint8Array, - level: i32, -) -> Uint8Array { +pub fn zstd_compress(data: Uint8Array, level: i32) -> Uint8Array { let vecdata = data.to_vec(); let compressed_data = zstd::encode_all(&vecdata[..], level).expect("Failed to compress data"); Uint8Array::from(compressed_data.as_slice()) diff --git a/crates/library_config/Cargo.toml b/crates/library_config/Cargo.toml index 80093eb..be76bd4 100644 --- a/crates/library_config/Cargo.toml +++ b/crates/library_config/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] anyhow = "1" -datadog-library-config = { git = "https://github.com/DataDog/libdatadog.git", tag = "v18.1.0" } +libdd-library-config = { git = "https://github.com/DataDog/libdatadog.git", rev = "353134770b312b7ccd2df6afabc253090b948e5f" } wasm-bindgen = "0.2.100" serde = { version = "1.0", features = ["derive"] } diff --git a/crates/library_config/src/lib.rs b/crates/library_config/src/lib.rs index c993fbc..6896a63 100644 --- a/crates/library_config/src/lib.rs +++ b/crates/library_config/src/lib.rs @@ -2,7 +2,7 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct JsConfigurator { - configurator: Box, + configurator: Box, envp: Vec, args: Vec, } @@ -49,7 +49,7 @@ impl JsConfigurator { #[wasm_bindgen(constructor)] pub fn new() -> Self { JsConfigurator { - configurator: Box::new(datadog_library_config::Configurator::new(false)), // No debug log as WASM can't write to stdout + configurator: Box::new(libdd_library_config::Configurator::new(false)), // No debug log as WASM can't write to stdout envp: Vec::new(), args: Vec::new(), } @@ -70,13 +70,13 @@ impl JsConfigurator { #[wasm_bindgen] pub fn get_config_local_path(&self, target: String) -> Result { let target_enum = match target.as_str() { - "linux" => datadog_library_config::Target::Linux, - "win32" => datadog_library_config::Target::Windows, - "darwin" => datadog_library_config::Target::Macos, + "linux" => libdd_library_config::Target::Linux, + "win32" => libdd_library_config::Target::Windows, + "darwin" => libdd_library_config::Target::Macos, _ => return Err(JsValue::from_str("Unsupported target")), }; Ok( - datadog_library_config::Configurator::local_stable_configuration_path(target_enum) + libdd_library_config::Configurator::local_stable_configuration_path(target_enum) .to_string(), ) } @@ -84,13 +84,13 @@ impl JsConfigurator { #[wasm_bindgen] pub fn get_config_managed_path(&self, target: String) -> Result { let target_enum = match target.as_str() { - "linux" => datadog_library_config::Target::Linux, - "win32" => datadog_library_config::Target::Windows, - "darwin" => datadog_library_config::Target::Macos, + "linux" => libdd_library_config::Target::Linux, + "win32" => libdd_library_config::Target::Windows, + "darwin" => libdd_library_config::Target::Macos, _ => return Err(JsValue::from_str("Unsupported target")), }; Ok( - datadog_library_config::Configurator::fleet_stable_configuration_path(target_enum) + libdd_library_config::Configurator::fleet_stable_configuration_path(target_enum) .to_string(), ) } @@ -108,7 +108,7 @@ impl JsConfigurator { let res_config = self.configurator.get_config_from_bytes( config_string_local.as_bytes(), config_string_managed.as_bytes(), - datadog_library_config::ProcessInfo { + libdd_library_config::ProcessInfo { envp: envp, args: args, language: b"nodejs".to_vec(), diff --git a/crates/pipeline/Cargo.toml b/crates/pipeline/Cargo.toml new file mode 100644 index 0000000..514e411 --- /dev/null +++ b/crates/pipeline/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "pipeline" +version = "0.1.0" +edition = "2021" +description = "Wasm binding for pipeline span management and trace export" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1" +libdatadog-nodejs-capabilities = { path = "../capabilities" } +libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", branch = "main" } +libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +libdd-trace-utils = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false, features = ["change-buffer"] } +libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +libdd-trace-protobuf = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +libdd-shared-runtime = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +rmp-serde = "1" +bytes = "1" +http = "1" +console_error_panic_hook = "0.1" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } +uuid = { version = "1", features = ["js"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs new file mode 100644 index 0000000..2d780c9 --- /dev/null +++ b/crates/pipeline/src/lib.rs @@ -0,0 +1,778 @@ +use libdatadog_nodejs_capabilities::WasmCapabilities; +use libdd_data_pipeline::trace_exporter::agent_response::AgentResponse; +use libdd_data_pipeline::trace_exporter::{ + TraceExporter, TraceExporterBuilder, TraceExporterOutputFormat, +}; +use libdd_shared_runtime::LocalRuntime; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::ffi::CStr; +use std::time::Duration; + +use wasm_bindgen::prelude::*; + +mod span_string; + +mod span_bytes; + +mod trace_data; +use trace_data::*; + +mod stats; + +use libdd_trace_utils::change_buffer::{ChangeBuffer, ChangeBufferState}; +use libdd_trace_utils::span::v04::{AttributeAnyValue, AttributeArrayValue, SpanEvent}; +use span_string::SpanString; +use std::collections::HashMap; + +mod utils; +use utils::*; + +#[wasm_bindgen(start)] +fn init() { + console_error_panic_hook::set_once(); +} + +// --- span event attribute decoding --- +// +// `addSpanEvent` receives its attributes as a flat little-endian buffer built +// by dd-trace-js. Layout: repeated entries until the buffer is exhausted, each +// [key_len: u32][key: utf8][tag: u8] + value +// where the value depends on `tag`: +// 0 String [len: u32][utf8] +// 1 Boolean [u8 (0/1)] +// 2 Integer [i64] +// 3 Double [f64] +// 4 Array [count: u32] then `count` items, each [item_tag: u8][scalar] +// (item_tag must be 0..=3; nested arrays are rejected) +// The tags mirror libdatadog's `AttributeArrayValue` discriminants +// (String=0, Boolean=1, Integer=2, Double=3, Array=4). Every read is bounded +// against the buffer so a malformed/truncated buffer errors instead of +// panicking (matching the hardening in `stringTableInsertMany`/`prepareChunk`). + +fn se_need(buf: &[u8], idx: usize, n: usize) -> Result<(), JsValue> { + // Avoid `idx + n` overflowing: on wasm32 `usize` is 32-bit, and `n` can be + // a u32-derived length (e.g. a crafted `key_len`) near `usize::MAX`, which + // would wrap and let a too-large read slip past the bound and trap on the + // slice. `idx` never exceeds `buf.len()` (it only advances after a checked + // read), so `buf.len() - idx` is the safe remaining-byte form. + if idx > buf.len() || n > buf.len() - idx { + return Err(JsValue::from_str( + "addSpanEvent: truncated span-event attribute buffer", + )); + } + Ok(()) +} + +fn se_read_u8(buf: &[u8], idx: &mut usize) -> Result { + se_need(buf, *idx, 1)?; + let b = buf[*idx]; + *idx += 1; + Ok(b) +} + +fn se_read_u32(buf: &[u8], idx: &mut usize) -> Result { + se_need(buf, *idx, 4)?; + Ok(get_num(buf, idx)) +} + +fn se_read_str(buf: &[u8], idx: &mut usize) -> Result { + let len = se_read_u32(buf, idx)? as usize; + se_need(buf, *idx, len)?; + let s = std::str::from_utf8(&buf[*idx..*idx + len]) + .map_err(|e| JsValue::from_str(&format!("addSpanEvent: invalid utf8: {e}")))?; + *idx += len; + Ok(s.into()) +} + +fn se_read_scalar( + buf: &[u8], + idx: &mut usize, + tag: u8, +) -> Result, JsValue> { + match tag { + 0 => Ok(AttributeArrayValue::String(se_read_str(buf, idx)?)), + 1 => Ok(AttributeArrayValue::Boolean(se_read_u8(buf, idx)? != 0)), + 2 => { + se_need(buf, *idx, 8)?; + Ok(AttributeArrayValue::Integer(get_num(buf, idx))) + } + 3 => { + se_need(buf, *idx, 8)?; + Ok(AttributeArrayValue::Double(get_num(buf, idx))) + } + _ => Err(JsValue::from_str( + "addSpanEvent: invalid span-event attribute tag", + )), + } +} + +fn decode_span_event_attributes( + buf: &[u8], +) -> Result>, JsValue> { + let mut attributes = HashMap::new(); + let mut idx = 0usize; + while idx < buf.len() { + let key = se_read_str(buf, &mut idx)?; + let tag = se_read_u8(buf, &mut idx)?; + let value = if tag == 4 { + let count = se_read_u32(buf, &mut idx)? as usize; + // Each item is at least 1 byte (its tag), so cap the pre-allocation + // to the remaining buffer: an inflated count can't force a huge + // allocation, and the per-item bounded reads catch truncation. + let mut items = Vec::with_capacity(count.min(buf.len().saturating_sub(idx))); + for _ in 0..count { + let item_tag = se_read_u8(buf, &mut idx)?; + if item_tag == 4 { + return Err(JsValue::from_str( + "addSpanEvent: nested arrays are not supported", + )); + } + items.push(se_read_scalar(buf, &mut idx, item_tag)?); + } + AttributeAnyValue::Array(items) + } else { + AttributeAnyValue::SingleValue(se_read_scalar(buf, &mut idx, tag)?) + }; + attributes.insert(key, value); + } + Ok(attributes) +} + +#[wasm_bindgen] +/// All mutable state is behind RefCell to allow `&self` methods on the +/// wasm-bindgen wrapper. This prevents re-entrant borrow panics when: +/// - Plugin instrumentation triggers span creation inside another span's +/// creation (e.g., http client span created during express handler) +/// - Async `flushChunk` holds a borrow across await points while other +/// span operations need access +pub struct WasmSpanState { + change_queue: Vec, + string_table_input: Vec, + /// UnsafeCell because send_trace_chunks_async needs &mut self across an + /// await point. WASM is single-threaded so this is safe — we just need + /// to ensure no overlapping mutable borrows (guaranteed by the JS-side + /// _flushInFlight guard which serializes sendPreparedChunk calls). + /// On wasm the exporter must be built asynchronously (`build_async`), but + /// the wasm-bindgen constructor is synchronous. We stash the configured + /// builder here and build the exporter lazily on the first send (the only + /// path that needs it), inside an async context. + // On wasm the exporter runs on a single-threaded `LocalRuntime` (workers + // spawned via wasm_bindgen_futures::spawn_local); the multi-thread + // ForkSafeRuntime/BasicRuntime are native-only. + exporter: UnsafeCell>>, + builder: UnsafeCell>>, + cbs: RefCell>, + stats_collector: RefCell>, + prepared_spans: RefCell>>>, + /// Re-entrancy guard for `sendPreparedChunk`. wasm-bindgen async exports + /// can be invoked again from JS before the prior future resolves; without + /// this, two calls would each take `&mut` out of `exporter`/`builder` and + /// alias across the await (UB). The guard makes a re-entrant call return + /// an error instead. + sending: Cell, + /// When true, the lazily-built exporter is configured for v0.5 output + /// (`/v0.5/traces`) instead of the default v0.4. v0.5 is a smaller, fixed + /// 12-field schema with NO slots for `meta_struct`/`span_events`/`span_links`, + /// so libdatadog's v0.5 serializer silently drops them — this mirrors + /// dd-trace-js master's v0.5 encoder and is intentional. Caller (dd-trace-js) + /// must only enable this after confirming the agent advertises `/v0.5/traces` + /// (libdd does NOT downgrade V05 the way it does V1). The output format is + /// fixed once the exporter is built on the first send, so `setUseV05` only + /// takes effect if called before then. + use_v05: Cell, +} + +/// Clears an in-flight flag on drop, so an early return or a dropped future +/// still resets it. +struct InFlightGuard<'a>(&'a Cell); +impl Drop for InFlightGuard<'_> { + fn drop(&mut self) { + self.0.set(false); + } +} + +#[wasm_bindgen] +impl WasmSpanState { + #[wasm_bindgen(constructor)] + pub fn new( + url: &str, + tracer_version: &str, + lang: &str, + lang_version: &str, + lang_interpreter: &str, + change_queue_size: u32, + string_table_input_size: u32, + pid: u32, + tracer_service: &str, + stats_enabled: bool, + hostname: &str, + env: &str, + app_version: &str, + runtime_id: &str, + ) -> Result { + let mut builder = TraceExporterBuilder::::new(); + builder + .set_url(url) + .set_tracer_version(tracer_version) + .set_language(lang) + .set_language_version(lang_version) + .set_language_interpreter(lang_interpreter) + // Populate the payload-level TracerMetadata (service/env/hostname/ + // app_version) the agent receives. These values are already passed + // in for the stats collector; without these calls the trace + // payload's tracer metadata is sent empty. + .set_service(tracer_service) + .set_env(env) + .set_hostname(hostname) + .set_app_version(app_version) + .enable_agent_rates_payload_version(); + + let mut change_queue = vec![0u8; change_queue_size as usize]; + let change_buffer = unsafe { + ChangeBuffer::from_raw_parts( + std::ptr::NonNull::new(change_queue.as_mut_ptr()).unwrap(), + change_queue.len(), + ) + }; + let change_buffer_state = ChangeBufferState::new( + change_buffer, + tracer_service.into(), + lang.into(), + pid, + ); + + let stats_collector = if stats_enabled { + Some(stats::StatsCollector::new( + Duration::from_secs(10), + url.to_string(), + stats::StatsMeta { + hostname: hostname.to_string(), + env: env.to_string(), + version: app_version.to_string(), + lang: lang.to_string(), + tracer_version: tracer_version.to_string(), + runtime_id: runtime_id.to_string(), + service: tracer_service.to_string(), + }, + )) + } else { + None + }; + + Ok(WasmSpanState { + change_queue, + string_table_input: vec![0u8; string_table_input_size as usize], + exporter: UnsafeCell::new(None), + builder: UnsafeCell::new(Some(builder)), + cbs: RefCell::new(change_buffer_state), + stats_collector: RefCell::new(stats_collector), + prepared_spans: RefCell::new(None), + sending: Cell::new(false), + use_v05: Cell::new(false), + }) + } + + /// Select v0.5 output for the trace exporter. Must be called before the + /// first `sendPreparedChunk` (the exporter is built lazily on first send and + /// the output format is fixed at build time; later calls have no effect). + /// + /// v0.5 silently drops `meta_struct` (and top-level `span_events`/`span_links`) + /// because the v0.5 wire schema has no slots for them — the caller is + /// responsible for only enabling this when the agent supports `/v0.5/traces`. + #[wasm_bindgen(js_name = "setUseV05")] + pub fn set_use_v05(&self, v: bool) { + self.use_v05.set(v); + } + + #[wasm_bindgen] + pub fn change_queue_ptr(&self) -> *const u8 { + self.change_queue.as_ptr() + } + + #[wasm_bindgen] + pub fn change_queue_len(&self) -> u32 { + self.change_queue.len() as u32 + } + + #[wasm_bindgen] + pub fn string_table_input_ptr(&self) -> *const u8 { + self.string_table_input.as_ptr() + } + + #[wasm_bindgen] + pub fn string_table_input_len(&self) -> u32 { + self.string_table_input.len() as u32 + } + + /// Prepare a chunk of spans for sending. Flushes the change buffer, + /// extracts spans, feeds stats. Returns `true` if a chunk was prepared + /// (there are spans to send) and `false` if there was nothing to send. + /// Must be followed by `sendPreparedChunk()` to actually send. + #[wasm_bindgen(js_name = "prepareChunk")] + pub fn prepare_chunk( + &self, + len: u32, + first_is_local_root: bool, + chunk: &[u8], + ) -> Result { + // Validate the JS-supplied count against the actual buffer size before + // doing any work: each span id is a u64 (8 bytes). This prevents an + // out-of-bounds read panic (and a huge `Vec::with_capacity`) when the + // caller passes a `len` larger than the chunk can hold. + if (len as usize).saturating_mul(8) > chunk.len() { + return Err(JsValue::from_str( + "prepareChunk: len exceeds the span-id bytes available in chunk", + )); + } + if len == 0 { + // Nothing to send: drop any previously prepared-but-unsent chunk so + // a caller that ignores this `false` cannot later resend a stale one. + if let Some(old_spans) = self.prepared_spans.borrow_mut().take() { + self.cbs.borrow_mut().recycle_spans(old_spans); + } + return Ok(false); + } + + self.cbs.borrow_mut() + .flush_change_buffer() + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + let mut count = len; + let mut index = 0; + let mut span_ids = Vec::with_capacity(count as usize); + while count > 0 { + let span_id: u64 = get_num(chunk, &mut index); + span_ids.push(span_id); + count -= 1; + } + + let spans_vec = self + .cbs.borrow_mut() + .flush_chunk(&span_ids, first_is_local_root) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + if let Some(collector) = self.stats_collector.borrow_mut().as_mut() { + collector.add_spans(&spans_vec); + } + + // Recycle any previously prepared spans that were never sent (e.g. + // if the prior send was skipped by JS back-pressure). Reusing the + // pre-allocated HashMaps avoids allocator fragmentation in WASM. + if let Some(old_spans) = self.prepared_spans.borrow_mut().take() { + self.cbs.borrow_mut().recycle_spans(old_spans); + } + + // Store prepared spans for the subsequent sendPreparedChunk call + let has_spans = !spans_vec.is_empty(); + *self.prepared_spans.borrow_mut() = Some(spans_vec); + Ok(has_spans) + } + + /// Send the previously prepared chunk. + /// + /// Uses `&self` (not `&mut self`); exclusive access to the exporter is + /// enforced at runtime by the `sending` re-entrancy guard below rather + /// than by the borrow checker. WASM is single-threaded, so the only way + /// two `&mut` to the exporter could co-exist is async re-entrancy (JS + /// calling this again before the prior future resolves) — the guard + /// rejects that with an error instead of allowing aliasing (UB). + #[wasm_bindgen(js_name = "sendPreparedChunk")] + pub async fn send_prepared_chunk(&self) -> Result { + if self.sending.get() { + return Err(JsValue::from_str("sendPreparedChunk is already in flight")); + } + self.sending.set(true); + let _in_flight = InFlightGuard(&self.sending); + + let spans_vec = self + .prepared_spans + .borrow_mut() + .take() + .ok_or_else(|| JsValue::from_str("no prepared chunk to send"))?; + + // SAFETY: WASM is single-threaded and the `sending` guard above + // guarantees no overlapping invocation, so this is the only live + // reference to the exporter for the duration of the awaits. + let exporter_slot = unsafe { &mut *self.exporter.get() }; + if exporter_slot.is_none() { + // First send: build the exporter asynchronously. `build` is not + // available on wasm (it needs a blocking runtime), so we drive + // `build_async` here where we already have an async context. + let mut builder = unsafe { &mut *self.builder.get() } + .take() + .ok_or_else(|| JsValue::from_str("exporter builder already consumed"))?; + // Output format is decided here, at first build, and then fixed. + // v0.5 drops meta_struct/span_events/span_links by design (the v0.5 + // schema has no slots for them); dd-trace-js only enables this after + // confirming agent `/v0.5/traces` support via `/info`. + if self.use_v05.get() { + builder.set_output_format(TraceExporterOutputFormat::V05); + } + let built = builder + .build_async::() + .await + .map_err(|e| JsValue::from_str(&format!("{:?}", e)))?; + *exporter_slot = Some(built); + } + let exporter = exporter_slot.as_mut().unwrap(); + let resp = exporter + .send_trace_chunks_async(vec![spans_vec]) + .await; + let response_str = resp.map(|resp| match resp { + AgentResponse::Unchanged => "unchanged".to_string(), + AgentResponse::Changed { body } => body, + }); + + response_str + .map(|s| JsValue::from_str(&s)) + .map_err(|e| JsValue::from_str(&format!("{:?}", e))) + } + + /// Flush aggregated stats to the agent's /v0.6/stats endpoint. + /// + /// Should be called periodically (e.g. every 10s) from JS, and with + /// `force=true` on shutdown. + #[wasm_bindgen(js_name = "flushStats")] + pub async fn flush_stats(&self, force: bool) -> Result { + // Build the stats request under a brief *synchronous* borrow, then drop + // the borrow BEFORE the async send. The collector therefore stays in + // `stats_collector`, so a concurrent `prepareChunk` during the in-flight + // send still reaches `add_spans` and those spans are counted. (Taking + // the collector out for the whole await would silently drop them from + // client-side stats.) No borrow is held across the await, so there is + // no double-borrow hazard from overlapping calls. + let req = { + let mut guard = self.stats_collector.borrow_mut(); + match guard.as_mut() { + Some(collector) => collector + .prepare_request(force) + .map_err(|e| JsValue::from_str(&e))?, + None => return Ok(false), + } + }; + match req { + Some(req) => { + stats::StatsCollector::send_request(req) + .await + .map_err(|e| JsValue::from_str(&e))?; + Ok(true) + } + None => Ok(false), + } + } + + /// Flush the queued change-buffer operations. On success always returns + /// `true` (the bool exists only for signature symmetry with the other + /// flush methods); failures surface as a thrown error. + #[wasm_bindgen(js_name = "flushChangeQueue")] + pub fn flush_change_queue(&self) -> Result { + self.cbs.borrow_mut() + .flush_change_buffer() + .map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(true) + } + + /// Set default meta tags applied to every new span. + /// Takes a flat array of key-value pairs: [key1, val1, key2, val2, ...] + #[wasm_bindgen(js_name = "setDefaultMeta")] + pub fn set_default_meta(&self, pairs: Vec) -> Result<(), JsValue> { + let mut tags = Vec::with_capacity(pairs.len() / 2); + let mut i = 0; + while i + 1 < pairs.len() { + let key = pairs[i] + .as_string() + .ok_or_else(|| JsValue::from_str("default meta key must be a string"))?; + let val = pairs[i + 1] + .as_string() + .ok_or_else(|| JsValue::from_str("default meta value must be a string"))?; + tags.push((key.into(), val.into())); + i += 2; + } + self.cbs.borrow_mut().set_default_meta(tags); + Ok(()) + } + + #[wasm_bindgen(js_name = "stringTableInsertOne")] + pub fn string_table_insert_one(&self, key: u32, val: &str) { + self.cbs.borrow_mut() + .string_table_insert_one(key, val.into()); + } + + #[wasm_bindgen(js_name = "stringTableInsertMany")] + pub fn string_table_insert_many(&self, count: u32) -> Result<(), JsValue> { + let mut index: usize = 0; + let mut remaining = count as usize; + // Hold one mutable borrow for the whole bulk insert rather than + // re-borrowing the RefCell once per string. + let mut cbs = self.cbs.borrow_mut(); + let buf = &self.string_table_input; + while remaining > 0 { + // Bound the read against the untrusted `count`: a count larger than + // the encoded entries must error, not index out of bounds. + if index + 4 > buf.len() { + return Err(JsValue::from_str( + "stringTableInsertMany: count exceeds the entries in the input buffer", + )); + } + let key: u32 = get_num(buf, &mut index); + let str_slice = &buf[index..]; + // Bound the NUL scan to the input slice so a non-terminated string + // can't read past the buffer, and advance past the NUL terminator + // (+ 1) so the next entry parses from the right offset. + let cstr = CStr::from_bytes_until_nul(str_slice) + .map_err(|e| JsValue::from_str(&format!("{}", e)))?; + let val = cstr + .to_str() + .map_err(|e| JsValue::from_str(&format!("{}", e)))?; + index += val.len() + 1; + // From<&str> for SpanString is a single Arc allocation — no + // intermediate owned String. + cbs.string_table_insert_one(key, val.into()); + remaining -= 1; + } + Ok(()) + } + + #[wasm_bindgen(js_name = "stringTableEvict")] + pub fn string_table_evict(&self, key: u32) { + self.cbs.borrow_mut().string_table_evict_one(key); + } + + // Absent-entity convention: span-level getters return an error (JS throw) + // for an unknown span_id, while trace-level getters and attribute lookups + // return null for an unknown segment / unset attribute. + #[wasm_bindgen(js_name = "getServiceName")] + pub fn get_service_name(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.service.to_string()) + } + + #[wasm_bindgen(js_name = "getResourceName")] + pub fn get_resource_name(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.resource.to_string()) + } + + #[wasm_bindgen(js_name = "getMetaAttr")] + pub fn get_meta_attr(&self, span_id: u64, name: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + // VecMap::get accepts &str directly (SpanString: Borrow), so no + // SpanString allocation is needed for the lookup. + Ok(span.meta.get(name) + .map(|v| JsValue::from_str(v.0.as_ref())) + .unwrap_or(JsValue::NULL)) + } + + #[wasm_bindgen(js_name = "getMetricAttr")] + pub fn get_metric_attr(&self, span_id: u64, name: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.metrics.get(name) + .map(|v| JsValue::from_f64(*v)) + .unwrap_or(JsValue::NULL)) + } + + #[wasm_bindgen(js_name = "getError")] + pub fn get_error(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.error) + } + + // start/duration are i64 nanoseconds. Returning them as JS BigInt (not + // f64) preserves full precision — real epoch-ns values exceed 2^53 and + // would be silently truncated as f64. + #[wasm_bindgen(js_name = "getStart")] + pub fn get_start(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.start) + } + + #[wasm_bindgen(js_name = "getDuration")] + pub fn get_duration(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.duration) + } + + #[wasm_bindgen(js_name = "getType")] + pub fn get_type(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.r#type.to_string()) + } + + #[wasm_bindgen(js_name = "getName")] + pub fn get_name(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.name.to_string()) + } + + // `meta_struct` carries msgpack-encoded structured data (e.g. AppSec, Code + // Origin, Dynamic Instrumentation). There is no change-buffer opcode for it, + // so the value is written directly onto the span after draining the queue — + // meta_struct does not depend on any other queued op, so bypassing the queue + // ordering is safe (subsequent ops are applied on the next flush and never + // touch meta_struct). + #[wasm_bindgen(js_name = "setMetaStruct")] + pub fn set_meta_struct( + &self, + span_id: u64, + key: &str, + value: &[u8], + ) -> Result<(), JsValue> { + self.flush_change_queue()?; + let mut cbs = self.cbs.borrow_mut(); + let span = cbs + .span_mut(span_id) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + span.meta_struct + .insert(key.into(), span_bytes::SpanBytesImpl(value.to_vec())); + Ok(()) + } + + #[wasm_bindgen(js_name = "getMetaStruct")] + pub fn get_meta_struct(&self, span_id: u64, key: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + // VecMap::get accepts &str directly (SpanString: Borrow). + Ok(span.meta_struct.get(key) + .map(|v| JsValue::from(js_sys::Uint8Array::from(v.0.as_slice()))) + .unwrap_or(JsValue::NULL)) + } + + // Span events (OpenTelemetry-style) are serialized by libdatadog as the + // top-level v0.4 `span_events` field when present. Like meta_struct there + // is no change-buffer opcode, so the event is appended directly to the span + // after draining the queue (span_events do not depend on any other queued + // op, so bypassing queue ordering is safe). `attrs_buf` is the flat typed + // attribute encoding decoded by `decode_span_event_attributes`. + #[wasm_bindgen(js_name = "addSpanEvent")] + pub fn add_span_event( + &self, + span_id: u64, + name: &str, + time_unix_nano: u64, + attrs_buf: &[u8], + ) -> Result<(), JsValue> { + self.flush_change_queue()?; + // Decode before borrowing cbs mutably so a malformed buffer errors + // without holding the borrow. + let attributes = decode_span_event_attributes(attrs_buf)?; + let mut cbs = self.cbs.borrow_mut(); + let span = cbs + .span_mut(span_id) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + span.span_events.push(SpanEvent { + time_unix_nano, + name: name.into(), + attributes, + }); + Ok(()) + } + + // Test/inspection helper: serialize the span's events to JSON via the same + // serde `Serialize` impl libdatadog uses for the msgpack wire format, so + // the `type`/`*_value` shape mirrors exactly what is sent to the agent + // (String=0, Boolean=1, Integer=2, Double=3, Array=4). + #[wasm_bindgen(js_name = "getSpanEventsJson")] + pub fn get_span_events_json(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs + .get_span(span_id) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + serde_json::to_string(&span.span_events) + .map_err(|e| JsValue::from_str(&format!("getSpanEventsJson: {e}"))) + } + + // Trace-level attributes live on the Segment (keyed by segment_id, which + // JS allocates and shares across spans in the same local trace). + #[wasm_bindgen(js_name = "getTraceMetaAttr")] + pub fn get_trace_meta_attr(&self, segment_id: u64, name: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + Ok(cbs.get_segment(&segment_id) + .and_then(|s| s.meta.get(name)) + .map(|v| JsValue::from_str(v.0.as_ref())) + .unwrap_or(JsValue::NULL)) + } + + #[wasm_bindgen(js_name = "getTraceMetricAttr")] + pub fn get_trace_metric_attr(&self, segment_id: u64, name: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + Ok(cbs.get_segment(&segment_id) + .and_then(|s| s.metrics.get(name)) + .map(|v| JsValue::from_f64(*v)) + .unwrap_or(JsValue::NULL)) + } + + #[wasm_bindgen(js_name = "getTraceOrigin")] + pub fn get_trace_origin(&self, segment_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + Ok(cbs.get_segment(&segment_id) + .and_then(|s| s.origin.as_ref()) + .map(|v| JsValue::from_str(v.0.as_ref())) + .unwrap_or(JsValue::NULL)) + } +} + +/// Export WASM memory so JS can create views into it +#[wasm_bindgen(js_name = "getWasmMemory")] +pub fn get_wasm_memory() -> JsValue { + wasm_bindgen::memory() +} + +/// Export OpCode values as a JS object. +/// Values match the `#[repr(u64)]` OpCode enum in libdd-trace-utils. +#[wasm_bindgen(js_name = "getOpCodes")] +pub fn get_op_codes() -> JsValue { + let obj = js_sys::Object::new(); + let entries: &[(&str, u32)] = &[ + ("Create", 0), + ("SetMetaAttr", 1), + ("SetMetricAttr", 2), + ("SetServiceName", 3), + ("SetResourceName", 4), + ("SetError", 5), + ("SetStart", 6), + ("SetDuration", 7), + ("SetType", 8), + ("SetName", 9), + ("SetTraceMetaAttr", 10), + ("SetTraceMetricsAttr", 11), + ("SetTraceOrigin", 12), + ]; + for (name, val) in entries { + js_sys::Reflect::set(&obj, &JsValue::from_str(name), &JsValue::from_f64(*val as f64)) + .unwrap(); + } + obj.into() +} + +#[wasm_bindgen(js_name = "setStorage")] +pub fn set_storage(new_storage: &JsValue) { + libdatadog_nodejs_capabilities::http::set_storage(new_storage); +} + +#[wasm_bindgen(js_name = "setResponseHeaderObserver")] +pub fn set_response_header_observer(observer: &JsValue) { + libdatadog_nodejs_capabilities::http::set_response_header_observer(observer); +} diff --git a/crates/pipeline/src/span_bytes.rs b/crates/pipeline/src/span_bytes.rs new file mode 100644 index 0000000..3dfaf1a --- /dev/null +++ b/crates/pipeline/src/span_bytes.rs @@ -0,0 +1,25 @@ +use libdd_trace_utils::span::SpanBytes; +use serde::Serialize; +use std::borrow::Borrow; +use std::hash::{Hash, Hasher}; + +#[derive(Default, Debug, Eq, PartialEq, Clone, Serialize)] +pub struct SpanBytesImpl(pub Vec); + +impl Hash for SpanBytesImpl { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl Borrow<[u8]> for SpanBytesImpl { + fn borrow(&self) -> &[u8] { + &self.0 + } +} + +impl SpanBytes for SpanBytesImpl { + fn from_static_bytes(value: &'static [u8]) -> Self { + SpanBytesImpl(value.to_vec()) + } +} diff --git a/crates/pipeline/src/span_string.rs b/crates/pipeline/src/span_string.rs new file mode 100644 index 0000000..ee75280 --- /dev/null +++ b/crates/pipeline/src/span_string.rs @@ -0,0 +1,46 @@ +use libdd_trace_utils::span::SpanText; +use serde::Serialize; +use std::borrow::Borrow; +use std::fmt::*; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +#[derive(Default, Debug, Eq, PartialEq, Serialize, Clone)] +pub struct SpanString(pub Arc); + +impl Hash for SpanString { + fn hash(&self, state: &mut H) { + // Hash the string content, not the Arc pointer + self.0.as_ref().hash(state); + } +} + +impl Borrow for SpanString { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl SpanText for SpanString { + fn from_static_str(value: &'static str) -> Self { + SpanString(Arc::from(value)) + } +} + +impl From for SpanString { + fn from(s: String) -> SpanString { + SpanString(Arc::from(s)) + } +} + +impl From<&str> for SpanString { + fn from(value: &str) -> SpanString { + SpanString(Arc::from(value)) + } +} + +impl Display for SpanString { + fn fmt(&self, formatter: &mut Formatter) -> Result { + write!(formatter, "{}", self.0) + } +} diff --git a/crates/pipeline/src/stats.rs b/crates/pipeline/src/stats.rs new file mode 100644 index 0000000..7f44bea --- /dev/null +++ b/crates/pipeline/src/stats.rs @@ -0,0 +1,175 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Native stats collection for the pipeline WASM module. +//! +//! Wraps `SpanConcentrator` from `libdd-trace-stats` and provides encoding + +//! HTTP transport for flushing stats to the Datadog agent's `/v0.6/stats` +//! endpoint. + +use std::time::{Duration, SystemTime}; + +/// Wall-clock now() for wasm. `std::time::SystemTime::now()` is unimplemented on +/// `wasm32-unknown-unknown` (it panics/traps), so derive the time from JS +/// `Date.now()` (milliseconds since the Unix epoch). +fn now() -> SystemTime { + SystemTime::UNIX_EPOCH + Duration::from_millis(js_sys::Date::now() as u64) +} + +use bytes::Bytes; +use libdd_capabilities::http::HttpClientCapability; +use libdd_trace_protobuf::pb; +use libdd_trace_stats::span_concentrator::SpanConcentrator; +use libdatadog_nodejs_capabilities::DefaultHttpClient; + +use crate::trace_data::WasmTraceData; + +const STATS_ENDPOINT_PATH: &str = "/v0.6/stats"; + +/// Metadata for the stats payload envelope. +pub struct StatsMeta { + pub hostname: String, + pub env: String, + pub version: String, + pub lang: String, + pub tracer_version: String, + pub runtime_id: String, + pub service: String, +} + +/// Manages stats aggregation and flushing. +pub struct StatsCollector { + concentrator: SpanConcentrator, + meta: StatsMeta, + agent_url: String, + sequence: u64, +} + +impl StatsCollector { + /// Create a new stats collector. + pub fn new(bucket_size: Duration, agent_url: String, meta: StatsMeta) -> Self { + StatsCollector { + concentrator: SpanConcentrator::new( + bucket_size, + now(), + vec![ + "client".to_string(), + "server".to_string(), + "producer".to_string(), + "consumer".to_string(), + ], + Vec::new(), + ), + meta, + agent_url, + sequence: 0, + } + } + + /// Add spans to the concentrator for stats aggregation. + /// + /// The spans should already have `_dd.top_level` and `_dd.measured` metrics + /// set (done by `ChangeBufferState::flush_chunk`). + pub fn add_spans(&mut self, spans: &[libdd_trace_utils::span::v04::Span]) { + for span in spans { + self.concentrator.add_span(span); + } + } + + /// Drain aggregated stats into a ready-to-send request, **synchronously**. + /// + /// Returns `Ok(None)` when there is nothing to flush. The concentrator is + /// drained and the sequence advanced as part of this call, so the returned + /// request must be sent (see `send_request`). Kept synchronous and separate + /// from the send so a caller can build the request under a brief borrow and + /// release the collector *before* the async send — leaving it available for + /// `add_spans` while the stats request is in flight. + pub fn prepare_request(&mut self, force: bool) -> Result>, String> { + let buckets = self.concentrator.flush(now(), force); + if buckets.is_empty() { + return Ok(None); + } + + self.sequence += 1; + let payload = encode_stats_payload(&buckets, &self.meta, self.sequence); + + let body = rmp_serde::encode::to_vec_named(&payload) + .map_err(|e| format!("stats msgpack encode error: {e}"))?; + + let stats_url = format!("{}{}", self.agent_url, STATS_ENDPOINT_PATH); + let uri: http::Uri = stats_url + .parse() + .map_err(|e| format!("invalid stats URL: {e}"))?; + + let req = http::Request::builder() + .method(http::Method::PUT) + .uri(uri) + .header("Content-Type", "application/msgpack") + .header("Datadog-Meta-Lang", &self.meta.lang) + .header("Datadog-Meta-Tracer-Version", &self.meta.tracer_version) + .body(Bytes::from(body)) + .map_err(|e| format!("failed to build stats request: {e}"))?; + + Ok(Some(req)) + } + + /// Send a prepared stats request to the agent. Does **not** borrow the + /// collector, so trace export (`add_spans`) can proceed during the await. + pub async fn send_request(req: http::Request) -> Result<(), String> { + let client = DefaultHttpClient::new_client(); + client + .request(req) + .await + .map_err(|e| format!("stats send error: {e:?}"))?; + Ok(()) + } + + /// Flush aggregated stats and send to the agent. + /// + /// Returns `Ok(true)` if stats were sent, `Ok(false)` if there was nothing + /// to send, or `Err` on transport failure. Convenience wrapper around + /// `prepare_request` + `send_request`; callers that flush concurrently with + /// trace export should use those two directly so the collector isn't held + /// across the await (see `flushStats`). + pub async fn flush(&mut self, force: bool) -> Result { + match self.prepare_request(force)? { + Some(req) => { + Self::send_request(req).await?; + Ok(true) + } + None => Ok(false), + } + } + + /// Update the agent URL (e.g. after reconfiguration). + pub fn set_agent_url(&mut self, url: String) { + self.agent_url = url; + } +} + +/// Encode flushed stats buckets into a `ClientStatsPayload` for msgpack +/// serialization. +fn encode_stats_payload( + buckets: &[pb::ClientStatsBucket], + meta: &StatsMeta, + sequence: u64, +) -> pb::ClientStatsPayload { + pb::ClientStatsPayload { + hostname: meta.hostname.clone(), + env: meta.env.clone(), + version: meta.version.clone(), + lang: meta.lang.clone(), + tracer_version: meta.tracer_version.clone(), + runtime_id: meta.runtime_id.clone(), + sequence, + stats: buckets.to_vec(), + service: meta.service.clone(), + container_id: String::new(), + tags: Vec::new(), + agent_aggregation: String::new(), + git_commit_sha: String::new(), + image_tag: String::new(), + process_tags: String::new(), + process_tags_hash: 0, + } +} diff --git a/crates/pipeline/src/trace_data.rs b/crates/pipeline/src/trace_data.rs new file mode 100644 index 0000000..94cbcfe --- /dev/null +++ b/crates/pipeline/src/trace_data.rs @@ -0,0 +1,16 @@ +use libdd_trace_utils::span::TraceData; +use serde::Serialize; + +use crate::span_bytes::SpanBytesImpl; +use crate::span_string::SpanString; + +// `Serialize` is derived only so the test helper `getSpanEventsJson` can +// serialize `Vec>` (serde's derive on the generic +// `SpanEvent` requires `T: Serialize`). The unit struct carries no data. +#[derive(Clone, Default, Debug, PartialEq, Serialize)] +pub struct WasmTraceData; + +impl TraceData for WasmTraceData { + type Text = SpanString; + type Bytes = SpanBytesImpl; +} diff --git a/crates/pipeline/src/utils.rs b/crates/pipeline/src/utils.rs new file mode 100644 index 0000000..aafbfa5 --- /dev/null +++ b/crates/pipeline/src/utils.rs @@ -0,0 +1,37 @@ +pub trait FromBytes: Sized { + type Bytes: ?Sized; + fn from_bytes(bytes: &[u8]) -> Self; +} + +macro_rules! impl_from_bytes { + ($ty:ty, $len:expr) => { + impl FromBytes for $ty { + type Bytes = $ty; + + // Note that this always does a copy into a new variable. This is + // because the values in the buffer are not aligned. We could save + // ourselves a copy by ensuring alignment from the managed side. + fn from_bytes(bytes: &[u8]) -> Self { + let mut code_buf = [0u8; $len]; + code_buf.copy_from_slice(bytes); + <$ty>::from_le_bytes(code_buf) + } + } + }; +} + +impl_from_bytes!(u128, 16); +impl_from_bytes!(u64, 8); +impl_from_bytes!(f64, 8); +impl_from_bytes!(i64, 8); +impl_from_bytes!(i32, 4); +impl_from_bytes!(u32, 4); + +pub fn get_num(buf: &[u8], index: &mut usize) -> T { + let id: usize = *index; + let size = std::mem::size_of::(); + let result = &buf[id..(id + size)]; + let result: T = T::from_bytes(result); + *index += size; + result +} diff --git a/package.json b/package.json index 6af6637..c48edb1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build-debug": "mkdir -p target && yarn -s cargo-build > ./target/out.ndjson && yarn -s copy-artifacts", "build-release": "mkdir -p target && yarn -s cargo-build-release > ./target/out.ndjson && yarn -s copy-artifacts", "build-all": "mkdir -p target && yarn -s cargo-build -- --workspace > ./target/out.ndjson && yarn -s copy-artifacts && yarn -s build-wasm", - "build-wasm": "yarn -s install-wasm-pack && node scripts/build-wasm.js library_config && node scripts/build-wasm.js datadog-js-zstd", + "build-wasm": "yarn -s install-wasm-pack && node scripts/build-wasm.js library_config && node scripts/build-wasm.js datadog-js-zstd && node scripts/build-wasm.js pipeline", "cargo-build-release": "yarn -s cargo-build -- --release", "cargo-build": "cargo build --message-format=json-render-diagnostics", "copy-artifacts": "node ./scripts/copy-artifacts", @@ -29,6 +29,9 @@ "publishConfig": { "access": "public" }, + "dependencies": { + "@napi-rs/cli": "^3.6.0" + }, "devDependencies": { "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.9.0", diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9b112d4..4001ea7 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.87.0" +channel = "1.90.0" profile = "minimal" components = ["clippy", "rustfmt", "rust-src"] diff --git a/scripts/test.sh b/scripts/test.sh index cfd0ca4..d85f5cb 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -9,7 +9,16 @@ run_test() { yarn --cwd "$dir" install fi echo "Running $1" - node "$1" + # node:test does not force the process to exit when the event loop is kept + # active by async work that has already settled (e.g. the wasm trace + # exporter's runtime machinery after a flush). For the long-lived real + # consumer that is expected; for the test runner we force a clean exit once + # all tests have finished. Only applies to files that use node:test. + if grep -q "node:test" "$1"; then + node --test-force-exit "$1" + else + node "$1" + fi } # Run top-level test files diff --git a/test/http_transport.js b/test/http_transport.js new file mode 100644 index 0000000..82ad7c4 --- /dev/null +++ b/test/http_transport.js @@ -0,0 +1,157 @@ +'use strict' + +// Unit tests for the response-header observer hook in the WASM HTTP transport +// shim. The shim is plain CommonJS (no wasm needed), so we drive `httpRequest` +// directly against a local HTTP server. `httpRequest` reads the request head +// from a Uint8Array view over `wasm_memory.buffer`, so we hand it a fake memory +// object containing a well-formed HTTP/1.1 request head. + +const { describe, it, before, after, beforeEach } = require('node:test') +const assert = require('node:assert') +const http = require('node:http') +const os = require('node:os') +const path = require('node:path') +const fs = require('node:fs') + +const transport = require('../crates/capabilities/src/http_transport.js') + +// Distinctive, multi-byte body so the pooled-buffer slicing in httpRequest +// (the reason for `new Uint8Array(body)` over `body.buffer`) is exercised: +// a small Buffer.concat result lands at a non-zero offset in Node's shared pool. +const RESPONSE_BODY = '{"rate_by_service":{"service:test,env:":0.5}}' + +function fakeWasmMemory (headBytes) { + const buf = new ArrayBuffer(headBytes.length) + new Uint8Array(buf).set(headBytes) + return { buffer: buf } +} + +describe('http_transport response header observer', () => { + let server + let port + + before(async () => { + server = http.createServer((req, res) => { + req.on('data', () => {}) + req.on('end', () => { + res.setHeader('Datadog-Container-Tags-Hash', 'testhash123') + res.end(RESPONSE_BODY) + }) + }) + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + port = server.address().port + }) + + after(() => new Promise((resolve) => server.close(resolve))) + + beforeEach(() => { + transport.setResponseHeaderObserver(null) + }) + + function doRequest () { + const head = Buffer.from( + `POST /v0.4/traces HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\n` + + 'Content-Length: 0\r\nConnection: close\r\n\r\n', + 'utf8' + ) + // head occupies [0, head.length); body is empty (offset 0, length 0). + // Empty socketPath -> TCP transport. + return transport.httpRequest('127.0.0.1', port, false, '', 0, head.length, 0, 0, fakeWasmMemory(head)) + } + + it('invokes the observer with the raw response headers', async () => { + let observed + transport.setResponseHeaderObserver((rawHeaders) => { observed = rawHeaders }) + + await doRequest() + + assert.ok(Array.isArray(observed), 'observer received the raw headers array') + const idx = observed.findIndex((h) => h.toLowerCase() === 'datadog-container-tags-hash') + assert.notStrictEqual(idx, -1, 'container-tags hash header present') + assert.strictEqual(observed[idx + 1], 'testhash123') + }) + + it('still delivers the response when the observer throws, logging the error', async () => { + transport.setResponseHeaderObserver(() => { throw new Error('boom') }) + + const originalWrite = process.stderr.write + let logged = '' + process.stderr.write = (chunk) => { logged += chunk; return true } + try { + const [status] = await doRequest() + assert.strictEqual(status, 200) + } finally { + process.stderr.write = originalWrite + } + assert.match(logged, /responseHeaderObserver error: boom/) + }) + + it('tolerates an observer throwing a non-Error value', async () => { + // Hardened logging reads only err.message, so a thrown string must not + // crash the transport (it logs `undefined` for the missing message). + transport.setResponseHeaderObserver(() => { throw 'boom' }) // eslint-disable-line no-throw-literal + + const originalWrite = process.stderr.write + let logged = '' + process.stderr.write = (chunk) => { logged += chunk; return true } + try { + const [status] = await doRequest() + assert.strictEqual(status, 200) + } finally { + process.stderr.write = originalWrite + } + assert.match(logged, /responseHeaderObserver error: undefined/) + }) + + it('works when no observer is registered', async () => { + const [status] = await doRequest() + assert.strictEqual(status, 200) + }) + + it('returns the exact response body bytes', async () => { + const [status, , body] = await doRequest() + assert.strictEqual(status, 200) + assert.ok(body instanceof Uint8Array, 'body is a Uint8Array') + // Must be exactly the agent's body — not whole-pool bytes or wrong length. + assert.strictEqual(body.length, Buffer.byteLength(RESPONSE_BODY)) + assert.strictEqual(Buffer.from(body).toString('utf8'), RESPONSE_BODY) + }) +}) + +// Unix-domain-socket transport: a non-empty socketPath must route the request +// over the socket instead of TCP. Skipped on Windows (no AF_UNIX path here). +describe('http_transport unix socket', { skip: process.platform === 'win32' }, () => { + let server + let socketPath + + before(async () => { + socketPath = path.join(os.tmpdir(), `libdd-uds-test-${process.pid}-${Date.now()}.sock`) + try { fs.unlinkSync(socketPath) } catch {} + server = http.createServer((req, res) => { + req.on('data', () => {}) + req.on('end', () => { + res.end(RESPONSE_BODY) + }) + }) + await new Promise((resolve) => server.listen(socketPath, resolve)) + }) + + after(() => new Promise((resolve) => server.close(() => { + try { fs.unlinkSync(socketPath) } catch {} + resolve() + }))) + + it('delivers the request over a unix socket and returns the response', async () => { + const head = Buffer.from( + 'POST /v0.4/traces HTTP/1.1\r\nHost: localhost\r\n' + + 'Content-Length: 0\r\nConnection: close\r\n\r\n', + 'utf8' + ) + // host/port empty/0; socketPath drives the connection. + const [status, , body] = await transport.httpRequest( + '', 0, false, socketPath, 0, head.length, 0, 0, fakeWasmMemory(head) + ) + assert.strictEqual(status, 200) + assert.strictEqual(Buffer.from(body).toString('utf8'), RESPONSE_BODY) + }) +}) diff --git a/test/pipeline.js b/test/pipeline.js index 239b3de..13259ea 100644 --- a/test/pipeline.js +++ b/test/pipeline.js @@ -1,10 +1,917 @@ 'use strict' +const { describe, it, before, beforeEach } = require('node:test') +const assert = require('node:assert') +const crypto = require('crypto') + const pipeline = require('..').maybeLoad('pipeline') +const { WasmSpanState } = pipeline +const OpCode = pipeline.getOpCodes() +const wasmMemory = pipeline.getWasmMemory() + +function getRandomBytes (byteCount) { + return new Uint8Array(crypto.randomBytes(byteCount)) +} + +function bytesToBigInt (bytes) { + let val = 0n + for (let i = 0; i < bytes.length; i++) { + val = (val << 8n) | BigInt(bytes[i]) + } + return val +} + +// The Span and NativeSpansInterface classes act as a sketch of what should +// be implemented in dd-trace-js. + +// TODO should NativeSpansInterface actually be implemented in this package? + +class Span { + constructor (nativeSpans, traceId, parentId) { + this.nativeSpans = nativeSpans + this.traceId = traceId || [getRandomBytes(8), getRandomBytes(8)] + this.parentId = parentId || new Uint8Array(8) + this.spanId = getRandomBytes(8) + // Spans are addressed by their span_id (u64). Operations carry the raw + // 8-byte id in their header; getters take the numeric id as a BigInt. + this.spanIdBig = bytesToBigInt(this.spanId) + // Trace-level attributes live on a Segment (a local trace chunk). JS owns + // segment_id allocation and shares it across spans in the same trace. + this.segmentId = nativeSpans.allocSegment(this.traceId) + this._startTime = BigInt(Date.now()) * 1000000n + + this.nativeSpans.queueOp(OpCode.Create, this.spanId, ['u128', this.traceId], ['u64n', this.segmentId], ['u64', this.parentId]) + this.nativeSpans.queueOp(OpCode.SetStart, this.spanId, ['i64', this._startTime]) + } + + setTag (key, value) { + if (typeof value === 'number') { + this.nativeSpans.queueOp(OpCode.SetMetricAttr, this.spanId, key, ['f64', value]) + } else { + this.nativeSpans.queueOp(OpCode.SetMetaAttr, this.spanId, key, value) + } + return this + } + + getTag (key) { + return this.nativeSpans.state.getMetaAttr(this.spanIdBig, key) ?? + this.nativeSpans.state.getMetricAttr(this.spanIdBig, key) + } + + setTraceTag (key, value) { + const opcode = OpCode[typeof value === 'number' ? 'SetTraceMetricsAttr' : 'SetTraceMetaAttr'] + if (typeof value === 'number') { + value = ['f64', value] + } + this.nativeSpans.queueOp(opcode, this.spanId, key, value) + return this + } + + getTraceTag (key) { + return this.nativeSpans.state.getTraceMetaAttr(this.segmentId, key) ?? + this.nativeSpans.state.getTraceMetricAttr(this.segmentId, key) + } + + setTraceOrigin (origin) { + this.nativeSpans.queueOp(OpCode.SetTraceOrigin, this.spanId, origin) + return this + } + + getTraceOrigin () { + return this.nativeSpans.state.getTraceOrigin(this.segmentId) + } + + setMetaStruct (key, bytes) { + this.nativeSpans.state.setMetaStruct(this.spanIdBig, key, bytes) + return this + } + + getMetaStruct (key) { + return this.nativeSpans.state.getMetaStruct(this.spanIdBig, key) + } + + addSpanEvent (name, timeUnixNano, attributes = {}) { + this.nativeSpans.state.addSpanEvent( + this.spanIdBig, + name, + BigInt(timeUnixNano), + encodeSpanEventAttrs(attributes) + ) + return this + } + + getSpanEvents () { + return JSON.parse(this.nativeSpans.state.getSpanEventsJson(this.spanIdBig)) + } + + finish () { + this.duration = BigInt(Date.now()) * 1000000n - this._startTime + return this + } +} + +const spanAccessors = { + // [getterName, opCode, valueType (null for string)] + name: ['getName', 'SetName', null], + service: ['getServiceName', 'SetServiceName', null], + resource: ['getResourceName', 'SetResourceName', null], + type: ['getType', 'SetType', null], + error: ['getError', 'SetError', 'i32'], + start: ['getStart', null, null], + duration: ['getDuration', 'SetDuration', 'i64'] +} + +Object.entries(spanAccessors).forEach(([prop, [getter, setter, valueType]]) => { + Object.defineProperty(Span.prototype, prop, { + get () { + return this.nativeSpans.state[getter](this.spanIdBig) + }, + set (val) { + val = valueType ? [valueType, val] : val + this.nativeSpans.queueOp(OpCode[setter], this.spanId, val); + } + }) +}) + +const CHANGE_QUEUE_SIZE = 64 * 1024 +const STRING_TABLE_INPUT_SIZE = 10 * 1024 + +class NativeSpansInterface { + constructor (options = {}) { + this.flushBuffer = Buffer.alloc(10 * 1024) + + this.cqbIndex = 8 // Start at 8 since first u64 is count + this.cqbCount = 0 + this.stibCount = 0 + this.segmentCount = 0 // Monotonic segment_id allocator + this.segmentByTrace = new Map() // trace key -> segment_id (BigInt) + this.stringMap = new Map() + + this.state = new WasmSpanState( + options.agentUrl || process.env.AGENT_URL || 'http://127.0.0.1:8126', + options.tracerVersion || '1.0.0', + options.lang || 'nodejs', + options.langVersion || process.version, + options.langInterpreter || 'v8', + CHANGE_QUEUE_SIZE, + STRING_TABLE_INPUT_SIZE, + options.pid ?? process.pid, + options.tracerService || 'test-service', + options.statsEnabled ?? false, + options.hostname || 'test-host', + options.env || 'test-env', + options.appVersion || '1.0.0', + options.runtimeId || '00000000-0000-0000-0000-000000000000' + ) + + // Get pointers into WASM memory for direct buffer access + this._wasmMemory = wasmMemory + this._cqbPtr = this.state.change_queue_ptr() + this._refreshViews() + } -if (pipeline) { - pipeline.init_trace_exporter('127.0.0.1', 8126, 10_000, '1.0', 'nodejs', '18.0', 'v8') + _refreshViews () { + this._cqbView = new DataView(this._wasmMemory.buffer, this._cqbPtr) + this._cqbBytes = new Uint8Array(this._wasmMemory.buffer, this._cqbPtr) + } - const ret = pipeline.send_traces(Buffer.alloc(1), 1) - console.log(ret) + resetChangeQueue () { + this.cqbIndex = 8 + this.cqbCount = 0 + // Check if WASM memory was detached/grown + if (this._wasmMemory.buffer !== this._cqbView.buffer) { + this._refreshViews() + } + this._cqbView.setUint32(0, 0, true) + this._cqbView.setUint32(4, 0, true) + } + + flushChangeQueue () { + this.state.flushChangeQueue() + this.resetChangeQueue() + } + + getStringId (str) { + let id = this.stringMap.get(str) + if (typeof id === 'number') return id + + id = this.stibCount++ + this.stringMap.set(str, id) + this.state.stringTableInsertOne(id, str) + return id + } + + // Write 8 big-endian bytes as a little-endian u64 into the change buffer + _writeBytesLE (bytes, offset) { + const buf = this._cqbBytes + for (let i = 0; i < 8; i++) { + buf[offset + i] = bytes[7 - i] + } + } + + // Allocate (or reuse) a segment_id for a given trace. Spans sharing a trace + // share a segment so trace-level attributes are visible across them. + allocSegment (traceId) { + const key = traceId.map(b => Buffer.from(b).toString('hex')).join('') + let id = this.segmentByTrace.get(key) + if (id === undefined) { + id = BigInt(this.segmentCount++) + this.segmentByTrace.set(key, id) + } + return id + } + + queueOp (op, spanId, ...args) { + // Check if WASM memory was detached/grown + if (this._wasmMemory.buffer !== this._cqbView.buffer) { + this._refreshViews() + } + + // Check if Rust flushed the queue (wrote 0 to count position) + if (this._cqbView.getUint32(0, true) === 0 && this.cqbCount > 0) { + this.cqbIndex = 8 + this.cqbCount = 0 + } + + const view = this._cqbView + // Op header: opcode (u16 LE) + span_id (u64 LE) = 10 bytes. Rust reads the + // opcode as a u16, then the span_id as a u64. + view.setUint16(this.cqbIndex, op, true) + this.cqbIndex += 2 + this._writeBytesLE(spanId, this.cqbIndex) + this.cqbIndex += 8 + + for (const arg of args) { + if (typeof arg === 'string') { + const stringId = this.getStringId(arg) + view.setUint32(this.cqbIndex, stringId, true) + this.cqbIndex += 4 + } else { + const [typ, num] = arg + switch (typ) { + case 'u64': + this._writeBytesLE(num, this.cqbIndex) + this.cqbIndex += 8 + break + case 'u64n': + view.setBigUint64(this.cqbIndex, BigInt(num), true) + this.cqbIndex += 8 + break + case 'u32n': // raw, pre-resolved string-table id + view.setUint32(this.cqbIndex, num, true) + this.cqbIndex += 4 + break + case 'u128': + this._writeBytesLE(num[0], this.cqbIndex) + this.cqbIndex += 8 + this._writeBytesLE(num[1], this.cqbIndex) + this.cqbIndex += 8 + break + case 'i64': + view.setBigInt64(this.cqbIndex, num, true) + this.cqbIndex += 8 + break + case 'i32': + view.setInt32(this.cqbIndex, num, true) + this.cqbIndex += 4 + break + case 'f64': + view.setFloat64(this.cqbIndex, num, true) + this.cqbIndex += 8 + break + default: + throw new Error('unsupported number type: ' + typ) + } + } + } + + this.cqbCount++ + view.setBigUint64(0, BigInt(this.cqbCount), true) + } + + createSpan (traceId, parentId) { + return new Span(this, traceId, parentId) + } + + async flushSpans (...spans) { + this.flushBuffer.fill(0) // TODO is this necessary, since we're sending the length? + let index = 0 + for (const span of spans) { + // The chunk buffer carries u64 span IDs (8 bytes LE each). + const spanId = span.spanId ?? span + for (let i = 0; i < 8; i++) { + this.flushBuffer[index + i] = spanId[7 - i] + } + index += 8 + } + const hasSpans = this.state.prepareChunk(spans.length, true, this.flushBuffer) + if (!hasSpans) return false + return this.state.sendPreparedChunk() + } } + +// Build the flat span-event attribute buffer consumed by the Rust decoder +// (`decode_span_event_attributes` in crates/pipeline/src/lib.rs). This mirrors +// what dd-trace-js's `addSpanEvent` wrapper produces. Tags: String=0, +// Boolean=1, Integer=2, Double=3, Array=4 (matching libdatadog's +// AttributeArrayValue discriminants). +function encodeSpanEventAttrs (attributes) { + const enc = new TextEncoder() + const chunks = [] + const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32LE(n >>> 0, 0); return b } + const i64 = (n) => { const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(n), 0); return b } + const f64 = (n) => { const b = Buffer.alloc(8); b.writeDoubleLE(n, 0); return b } + const str = (s) => { const sb = Buffer.from(enc.encode(s)); return Buffer.concat([u32(sb.length), sb]) } + // Returns `[tag][value]` — used both for single values and array items. + const scalar = (v) => { + if (typeof v === 'string') return Buffer.concat([Buffer.from([0]), str(v)]) + if (typeof v === 'boolean') return Buffer.concat([Buffer.from([1]), Buffer.from([v ? 1 : 0])]) + if (typeof v === 'number') { + return Number.isInteger(v) + ? Buffer.concat([Buffer.from([2]), i64(v)]) + : Buffer.concat([Buffer.from([3]), f64(v)]) + } + throw new TypeError(`unsupported span-event attribute value: ${typeof v}`) + } + for (const [key, value] of Object.entries(attributes)) { + chunks.push(str(key)) + if (Array.isArray(value)) { + chunks.push(Buffer.from([4]), u32(value.length)) + for (const item of value) chunks.push(scalar(item)) + } else { + chunks.push(scalar(value)) + } + } + return new Uint8Array(Buffer.concat(chunks)) +} + +describe('pipeline', () => { + let nativeSpans + + before(() => { + nativeSpans = new NativeSpansInterface() + }) + + beforeEach(() => { + nativeSpans.resetChangeQueue() + }) + + describe('module exports', () => { + it('should export WasmSpanState', () => { + assert(WasmSpanState) + }) + + it('should export OpCode', () => { + assert(OpCode) + }) + + it('should export all OpCodes', () => { + const expectedOpCodes = [ + 'Create', 'SetMetaAttr', 'SetMetricAttr', 'SetServiceName', + 'SetResourceName', 'SetError', 'SetStart', 'SetDuration', + 'SetType', 'SetName', 'SetTraceMetaAttr', 'SetTraceMetricsAttr', + 'SetTraceOrigin' + ] + for (const opCode of expectedOpCodes) { + assert.strictEqual(typeof OpCode[opCode], 'number') + } + }) + }) + + describe('WasmSpanState', () => { + it('should create an instance', () => { + assert(nativeSpans.state instanceof WasmSpanState) + }) + }) + + describe('span creation', () => { + it('should create a span with basic attributes', () => { + const span = nativeSpans.createSpan() + span.name = 'test-span' + span.service = 'test-service' + span.resource = '/api/test' + span.type = 'web' + span.error = 0 + + assert.strictEqual(span.name, 'test-span') + assert.strictEqual(span.service, 'test-service') + assert.strictEqual(span.resource, '/api/test') + assert.strictEqual(span.type, 'web') + assert.strictEqual(span.error, 0) + }) + + it('should create a child span with parent', () => { + const parent = nativeSpans.createSpan() + parent.name = 'parent-span' + + const child = nativeSpans.createSpan(parent.traceId, parent.spanId) + child.name = 'child-span' + + assert.strictEqual(child.name, 'child-span') + }) + }) + + describe('span attributes', () => { + it('should set and get string tags', () => { + const span = nativeSpans.createSpan() + span.setTag('http.method', 'GET') + span.setTag('http.url', 'http://example.com/api') + + assert.strictEqual(span.getTag('http.method'), 'GET') + assert.strictEqual(span.getTag('http.url'), 'http://example.com/api') + }) + + it('should set and get numeric tags', () => { + const span = nativeSpans.createSpan() + span.setTag('http.status_code', 200) + span.setTag('custom.metric', 3.14159) + + assert.strictEqual(span.getTag('http.status_code'), 200) + assert.strictEqual(span.getTag('custom.metric'), 3.14159) + }) + + it('should set and get error state', () => { + const span = nativeSpans.createSpan() + span.error = 0 + assert.strictEqual(span.error, 0) + + span.error = 1 + assert.strictEqual(span.error, 1) + }) + }) + + describe('meta_struct', () => { + it('round-trips raw bytes by key', () => { + const span = nativeSpans.createSpan() + const value = new Uint8Array([0x82, 0xa1, 0x61, 0x01, 0xa1, 0x62, 0x02]) + span.setMetaStruct('appsec', value) + + assert.deepStrictEqual(span.getMetaStruct('appsec'), value) + }) + + it('returns null for an unset meta_struct key', () => { + const span = nativeSpans.createSpan() + assert.strictEqual(span.getMetaStruct('missing'), null) + }) + + it('overwrites an existing key on repeated set', () => { + const span = nativeSpans.createSpan() + span.setMetaStruct('k', new Uint8Array([1, 2, 3])) + span.setMetaStruct('k', new Uint8Array([9])) + + assert.deepStrictEqual(span.getMetaStruct('k'), new Uint8Array([9])) + }) + }) + + describe('span_events', () => { + it('appends an event with no attributes', () => { + const span = nativeSpans.createSpan() + span.addSpanEvent('exception', 1727211691770716000n) + + const events = span.getSpanEvents() + assert.strictEqual(events.length, 1) + assert.strictEqual(events[0].name, 'exception') + assert.strictEqual(events[0].time_unix_nano, 1727211691770716000) + // Empty attributes are skipped by libdatadog's serializer. + assert.strictEqual(events[0].attributes, undefined) + }) + + it('round-trips scalar attributes of every type with correct type tags', () => { + const span = nativeSpans.createSpan() + span.addSpanEvent('evt', 1000n, { + s: 'hello', + b: true, + i: 42, + d: 3.5 + }) + + const [event] = span.getSpanEvents() + assert.strictEqual(event.name, 'evt') + assert.strictEqual(event.time_unix_nano, 1000) + // type tags: String=0, Boolean=1, Integer=2, Double=3 + assert.deepStrictEqual(event.attributes.s, { type: 0, string_value: 'hello' }) + assert.deepStrictEqual(event.attributes.b, { type: 1, bool_value: true }) + assert.deepStrictEqual(event.attributes.i, { type: 2, int_value: 42 }) + assert.deepStrictEqual(event.attributes.d, { type: 3, double_value: 3.5 }) + }) + + it('round-trips an array attribute (type 4) with typed items', () => { + const span = nativeSpans.createSpan() + span.addSpanEvent('evt', 1n, { tags: ['a', 'b'], nums: [1, 2, 3] }) + + const [event] = span.getSpanEvents() + assert.deepStrictEqual(event.attributes.tags, { + type: 4, + array_value: { values: [{ type: 0, string_value: 'a' }, { type: 0, string_value: 'b' }] } + }) + assert.deepStrictEqual(event.attributes.nums, { + type: 4, + array_value: { values: [{ type: 2, int_value: 1 }, { type: 2, int_value: 2 }, { type: 2, int_value: 3 }] } + }) + }) + + it('appends multiple events in order', () => { + const span = nativeSpans.createSpan() + span.addSpanEvent('first', 1n) + span.addSpanEvent('second', 2n, { k: 'v' }) + + const events = span.getSpanEvents() + assert.strictEqual(events.length, 2) + assert.strictEqual(events[0].name, 'first') + assert.strictEqual(events[1].name, 'second') + assert.deepStrictEqual(events[1].attributes.k, { type: 0, string_value: 'v' }) + }) + + it('returns an empty array for a span with no events', () => { + const span = nativeSpans.createSpan() + assert.deepStrictEqual(span.getSpanEvents(), []) + }) + + it('rejects a truncated attribute buffer instead of panicking', () => { + const span = nativeSpans.createSpan() + // key_len=5 but no key bytes follow → bounded read must error. + const bad = new Uint8Array([5, 0, 0, 0]) + assert.throws( + () => span.nativeSpans.state.addSpanEvent(span.spanIdBig, 'evt', 1n, bad), + /truncated span-event attribute buffer/ + ) + }) + + it('rejects an overflowing key_len without trapping (wasm32 usize)', () => { + const span = nativeSpans.createSpan() + // key_len = 0xFFFFFFFF: on wasm32 `idx + key_len` would wrap and slip + // past the bound, trapping on the slice. The remaining-byte form must + // reject it as a truncated buffer instead. + const bad = new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00]) + assert.throws( + () => span.nativeSpans.state.addSpanEvent(span.spanIdBig, 'evt', 1n, bad), + /truncated span-event attribute buffer/ + ) + }) + }) + + describe('span timing', () => { + it('should set and get start time', () => { + const span = nativeSpans.createSpan() + assert(span.start > 0n) // populated from the constructor's SetStart (BigInt ns) + + // Verify an exact round-trip. getStart returns an f64, so real ns + // timestamps (> 2^53) can't be checked to the nanosecond; use a small, + // exactly-representable value so an off-by-one would actually be caught. + nativeSpans.queueOp(OpCode.SetStart, span.spanId, ['i64', 12_345n]) + assert.strictEqual(span.start, 12_345n) + }) + + it('should set and get duration', () => { + const duration = 1000000n + const span = nativeSpans.createSpan() + span.duration = duration + // getDuration returns i64 nanoseconds as a BigInt (no f64 truncation). + assert.strictEqual(span.duration, duration) + }) + }) + + describe('trace-level attributes', () => { + it('should set and get trace string tags', () => { + const span = nativeSpans.createSpan() + span.setTraceTag('_dd.p.dm', '-0') + assert.strictEqual(span.getTraceTag('_dd.p.dm'), '-0') + }) + + it('should set and get trace numeric tags', () => { + const span = nativeSpans.createSpan() + span.setTraceTag('_sampling_priority_v1', 1) + assert.strictEqual(span.getTraceTag('_sampling_priority_v1'), 1) + }) + + it('should set and get trace origin', () => { + const span = nativeSpans.createSpan() + span.setTraceOrigin('synthetics') + assert.strictEqual(span.getTraceOrigin(), 'synthetics') + }) + + it('should share trace attributes across spans in same trace', () => { + const parent = nativeSpans.createSpan() + parent.setTraceTag('shared_key', 'shared_value') + parent.setTraceTag('shared_metric', 42) + parent.setTraceOrigin('lambda') + + const child = nativeSpans.createSpan(parent.traceId, parent.spanId) + + assert.strictEqual(child.getTraceTag('shared_key'), 'shared_value') + assert.strictEqual(child.getTraceTag('shared_metric'), 42) + assert.strictEqual(child.getTraceOrigin(), 'lambda') + }) + + it('should isolate trace attributes across different traces', () => { + const a = nativeSpans.createSpan() + a.setTraceTag('iso_key', 'a_value') + a.setTraceOrigin('origin-a') + + // A span in a DIFFERENT trace must not see trace a's segment data. + const b = nativeSpans.createSpan() + assert.notStrictEqual(a.segmentId, b.segmentId) + assert.strictEqual(b.getTraceTag('iso_key'), null) + assert.strictEqual(b.getTraceOrigin(), null) + }) + }) + + describe('absent values and error handling', () => { + it('returns null for tags that were never set', () => { + const span = nativeSpans.createSpan() + assert.strictEqual(nativeSpans.state.getMetaAttr(span.spanIdBig, 'never-set'), null) + assert.strictEqual(nativeSpans.state.getMetricAttr(span.spanIdBig, 'never-set'), null) + assert.strictEqual(span.getTag('never-set'), null) + }) + + it('throws when reading an unknown span id', () => { + // Convention: span-level getters throw on an unknown span_id, while + // trace-level getters return null for an unknown segment. All span + // getters share the get_span error path, so assert each one throws. + const bogus = 0xdeadbeefn + for (const getter of [ + 'getName', 'getServiceName', 'getResourceName', + 'getType', 'getError', 'getStart', 'getDuration' + ]) { + assert.throws(() => nativeSpans.state[getter](bogus), `${getter} should throw`) + } + }) + }) + + describe('default meta', () => { + it('applies default meta to new spans and validates inputs', () => { + // Fresh interface so the default doesn't leak into the shared instance. + const ns = new NativeSpansInterface() + ns.state.setDefaultMeta(['dk', 'dv']) + const span = ns.createSpan() + assert.strictEqual(span.getTag('dk'), 'dv') + + // Non-string key or value must throw. + assert.throws(() => ns.state.setDefaultMeta(['k', 123])) + assert.throws(() => ns.state.setDefaultMeta([123, 'v'])) + // A trailing unpaired key is ignored, not an error. + assert.doesNotThrow(() => ns.state.setDefaultMeta(['lonely'])) + }) + }) + + describe('sampling', () => { + it('should not expose sample() in WASM module', () => { + // Sampling is handled JS-side; the WASM module does not expose a sample() method + assert.strictEqual(typeof nativeSpans.state.sample, 'undefined') + }) + }) + + describe('string table', () => { + it('should evict strings from the table', () => { + const testKey = 'eviction-test-key-' + Math.random() + const testVal = 'eviction-test-value' + const span = nativeSpans.createSpan() + span.setTag(testKey, testVal) + + assert.strictEqual(span.getTag(testKey), testVal) + + const keyId = nativeSpans.stringMap.get(testKey) + assert.strictEqual(typeof keyId, 'number', 'key was interned in the string table') + nativeSpans.state.stringTableEvict(keyId) + + // Evicting the key from the string table must not affect spans that have + // already resolved it: the tag was materialized onto the span at flush + // time, so the span keeps its own copy of the value. + assert.strictEqual(span.getTag(testKey), testVal) + }) + + it('bulk-inserts strings via stringTableInsertMany', () => { + // Wire format per entry: [key:u32 LE][cstr bytes][NUL]. Two entries + // exercise the NUL-terminator advance (a missing +1 would misparse the + // second entry). + const ptr = nativeSpans.state.string_table_input_ptr() + const view = new DataView(wasmMemory.buffer, ptr) + const bytes = new Uint8Array(wasmMemory.buffer, ptr) + const entries = [[60001, 'bulk-key'], [60002, 'bulk-val']] + let off = 0 + for (const [key, str] of entries) { + view.setUint32(off, key, true); off += 4 + for (let i = 0; i < str.length; i++) bytes[off++] = str.charCodeAt(i) + bytes[off++] = 0 + } + nativeSpans.state.stringTableInsertMany(entries.length) + + // Reference the pre-inserted ids directly (raw u32, not via getStringId) + // in a SetMetaAttr op; at flush they must resolve to the bulk strings. + const span = nativeSpans.createSpan() + nativeSpans.queueOp(OpCode.SetMetaAttr, span.spanId, ['u32n', 60001], ['u32n', 60002]) + assert.strictEqual(span.getTag('bulk-key'), 'bulk-val') + }) + + it('rejects a malformed (non-terminated) stringTableInsertMany entry', () => { + const ptr = nativeSpans.state.string_table_input_ptr() + const len = nativeSpans.state.string_table_input_len() + const view = new DataView(wasmMemory.buffer, ptr) + const bytes = new Uint8Array(wasmMemory.buffer, ptr, len) + bytes.fill(0xff) // no NUL terminator anywhere in the buffer + view.setUint32(0, 70001, true) + // from_bytes_until_nul finds no terminator -> error surfaced as a throw, + // not an out-of-bounds read. + assert.throws(() => nativeSpans.state.stringTableInsertMany(1)) + }) + + it('rejects a stringTableInsertMany count larger than the buffer holds', () => { + const ptr = nativeSpans.state.string_table_input_ptr() + const len = nativeSpans.state.string_table_input_len() + const view = new DataView(wasmMemory.buffer, ptr) + const bytes = new Uint8Array(wasmMemory.buffer, ptr, len) + bytes.fill(0) + // One valid entry consuming almost the whole buffer, so claiming a count + // of 2 makes the second u32 key read run past the end -> bounded error, + // not an out-of-bounds panic. + view.setUint32(0, 71000, true) + for (let i = 4; i < len - 1; i++) bytes[i] = 0x61 // 'a' + bytes[len - 1] = 0 // NUL terminator at the very end + assert.throws(() => nativeSpans.state.stringTableInsertMany(2), /exceeds the entries/) + }) + }) + + describe('input validation', () => { + it('throws when prepareChunk len exceeds the chunk size', () => { + // 100 span ids would need 800 bytes; the chunk only has 8. + assert.throws(() => nativeSpans.state.prepareChunk(100, true, Buffer.alloc(8))) + }) + + it('flushSpans with no spans is a no-op returning false', async () => { + assert.strictEqual(await nativeSpans.flushSpans(), false) + }) + }) + + describe('flush to agent', () => { + it('should flush spans to a (mock) agent', async () => { + // Stand up a throwaway HTTP server acting as the agent so the flush path + // (prepareChunk -> build exporter -> serialize -> send) is exercised + // end-to-end in CI, instead of being skipped when no agent is present. + const http = require('node:http') + const payloads = [] + const server = http.createServer((req, res) => { + const chunks = [] + req.on('data', c => chunks.push(c)) + req.on('end', () => { + payloads.push(Buffer.concat(chunks)) + res.writeHead(200, { 'content-type': 'application/json' }) + res.end('{}') + }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() + + const ns = new NativeSpansInterface({ agentUrl: `http://127.0.0.1:${port}` }) + const span = ns.createSpan() + span.name = 'flush-test-span' + span.service = 'test-service' + span.resource = 'test-resource' + span.type = 'web' + span.duration = 1000000n + + try { + const result = await ns.flushSpans(span) + assert(result, 'exporter returned an agent response') + assert(payloads.length > 0, 'agent received a trace payload') + assert(payloads[0].length > 0, 'trace payload is non-empty') + } finally { + server.closeAllConnections?.() + server.close() + } + }) + }) + + describe('v0.5 output format', () => { + // Spin up a mock agent that records the request path, so we can assert the + // exporter targets /v0.4/traces by default and /v0.5/traces after + // setUseV05(true). (v0.5 itself drops meta_struct/span_events by design; + // here we only verify endpoint routing, which is the observable behavior.) + async function flushAndCapturePath (useV05) { + const http = require('node:http') + const seen = [] + const server = http.createServer((req, res) => { + req.on('data', () => {}) + req.on('end', () => { + seen.push({ method: req.method, url: req.url }) + res.writeHead(200, { 'content-type': 'application/json' }) + res.end('{}') + }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() + const ns = new NativeSpansInterface({ agentUrl: `http://127.0.0.1:${port}` }) + if (useV05) ns.state.setUseV05(true) + const span = ns.createSpan() + span.name = 'v05-span' + span.service = 'test-service' + span.resource = 'test-resource' + span.type = 'web' + span.duration = 1000000n + try { + await ns.flushSpans(span) + return seen.find(r => r.method === 'POST') + } finally { + server.closeAllConnections?.() + server.close() + } + } + + it('targets /v0.4/traces by default', async () => { + const req = await flushAndCapturePath(false) + assert.ok(req, 'agent received a POST') + assert.strictEqual(req.url, '/v0.4/traces') + }) + + it('targets /v0.5/traces after setUseV05(true)', async () => { + const req = await flushAndCapturePath(true) + assert.ok(req, 'agent received a POST') + assert.strictEqual(req.url, '/v0.5/traces') + }) + }) + + describe('client-side stats', () => { + it('aggregates and flushes stats to /v0.6/stats', async () => { + const http = require('node:http') + const seen = [] + const server = http.createServer((req, res) => { + const chunks = [] + req.on('data', c => chunks.push(c)) + req.on('end', () => { + seen.push({ method: req.method, url: req.url, len: Buffer.concat(chunks).length }) + res.writeHead(200, { 'content-type': 'application/json' }) + res.end('{}') + }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() + + // statsEnabled:true builds the StatsCollector; prepareChunk feeds spans + // into it, and flushStats(true) force-flushes to /v0.6/stats. + const ns = new NativeSpansInterface({ agentUrl: `http://127.0.0.1:${port}`, statsEnabled: true }) + const span = ns.createSpan() + span.name = 'stats-span' + span.service = 'stats-svc' + span.resource = '/stats' + span.type = 'web' + span.duration = 5_000_000n + + try { + await ns.flushSpans(span) + const sent = await ns.state.flushStats(true) + assert.strictEqual(sent, true, 'flushStats reported a send') + const statsReq = seen.find(r => r.url === '/v0.6/stats') + assert.ok(statsReq, 'agent received a /v0.6/stats request') + assert.strictEqual(statsReq.method, 'PUT') + assert.ok(statsReq.len > 0, 'stats payload is non-empty') + + // Nothing new aggregated -> a second forced flush is a no-op. + assert.strictEqual(await ns.state.flushStats(true), false, 'second flush has nothing to send') + } finally { + server.closeAllConnections?.() + server.close() + } + }) + + it('flushStats returns false when stats are disabled', async () => { + const ns = new NativeSpansInterface({ statsEnabled: false }) + assert.strictEqual(await ns.state.flushStats(true), false) + }) + }) + + describe('send re-entrancy', () => { + it('rejects an overlapping sendPreparedChunk call', async () => { + const http = require('node:http') + const server = http.createServer((req, res) => { + req.resume() + req.on('end', () => { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{}') }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() + const ns = new NativeSpansInterface({ agentUrl: `http://127.0.0.1:${port}` }) + const span = ns.createSpan() + span.name = 'reentrancy' + ns.flushBuffer.fill(0) + for (let i = 0; i < 8; i++) ns.flushBuffer[i] = span.spanId[7 - i] + assert.ok(ns.state.prepareChunk(1, true, ns.flushBuffer)) + + try { + // Two calls without awaiting the first: the in-flight guard must reject + // the second instead of aliasing the exporter (UB). + const settled = await Promise.allSettled([ + ns.state.sendPreparedChunk(), + ns.state.sendPreparedChunk() + ]) + const reasons = settled + .filter(s => s.status === 'rejected') + .map(s => String(s.reason)) + assert.ok( + reasons.some(r => /already in flight/.test(r)), + 'one overlapping call rejected as already-in-flight' + ) + } finally { + server.closeAllConnections?.() + server.close() + } + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index c86291b..5c4d745 100644 --- a/yarn.lock +++ b/yarn.lock @@ -105,6 +105,386 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== +"@inquirer/ansi@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-2.0.7.tgz#86de22810cac3ed406ec10f8d66016815b8226b4" + integrity sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q== + +"@inquirer/checkbox@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-5.2.1.tgz#7f148b3153a776cee202015b10f9a985068d188d" + integrity sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^11.2.1" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" + +"@inquirer/confirm@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-6.1.1.tgz#9c6a7d79c6132b2af57fdb75747f056204e55356" + integrity sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/core@^11.2.1": + version "11.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-11.2.1.tgz#54ccd8f7d47852140b6066cbd77d63b2c2b168fd" + integrity sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" + cli-width "^4.1.0" + fast-wrap-ansi "^0.2.0" + mute-stream "^3.0.0" + signal-exit "^4.1.0" + +"@inquirer/editor@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-5.2.2.tgz#7c73e2fc0e7bd4c40cfd38a180ae5bbd24d32b90" + integrity sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/external-editor" "^3.0.3" + "@inquirer/type" "^4.0.7" + +"@inquirer/expand@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-5.1.1.tgz#e2afeac247d97dd64ee18aa81e902bdd1fe0ea70" + integrity sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/external-editor@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@inquirer/external-editor/-/external-editor-3.0.3.tgz#d79e772542cf8d340642e9dabd3a1ea7f5a30104" + integrity sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA== + dependencies: + chardet "^2.1.1" + iconv-lite "^0.7.2" + +"@inquirer/figures@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-2.0.7.tgz#f5cc5843732a81304d06a0db4b53cc7dbda15541" + integrity sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw== + +"@inquirer/input@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-5.1.2.tgz#9305cb170dfc3a5323e5eac885a945e7cddd5c4b" + integrity sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/number@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-4.1.1.tgz#b133668d8e0e099b4133abb915221501e0ff75d7" + integrity sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/password@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-5.1.1.tgz#f21efb614da9c905095262f51781fd2a721fceac" + integrity sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/prompts@^8.5.2": + version "8.5.2" + resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-8.5.2.tgz#09c0132ada2bbba94c91d341115e1e41cb3f1525" + integrity sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g== + dependencies: + "@inquirer/checkbox" "^5.2.1" + "@inquirer/confirm" "^6.1.1" + "@inquirer/editor" "^5.2.2" + "@inquirer/expand" "^5.1.1" + "@inquirer/input" "^5.1.2" + "@inquirer/number" "^4.1.1" + "@inquirer/password" "^5.1.1" + "@inquirer/rawlist" "^5.3.1" + "@inquirer/search" "^4.2.1" + "@inquirer/select" "^5.2.1" + +"@inquirer/rawlist@^5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-5.3.1.tgz#66f6b8e6aa82d47399c433b8262128e7c1a4f9ce" + integrity sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/search@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-4.2.1.tgz#c8f4b78ab3f866fdf0503fac0cd08c4a6661c11e" + integrity sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" + +"@inquirer/select@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-5.2.1.tgz#3a05e76e58d9e1bb095e912c3e7093aa04cd4604" + integrity sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^11.2.1" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" + +"@inquirer/type@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-4.0.7.tgz#9c6f0d857fe6ad549a3a932343b64e76acb34b10" + integrity sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g== + +"@napi-rs/cli@^3.6.0": + version "3.7.2" + resolved "https://registry.yarnpkg.com/@napi-rs/cli/-/cli-3.7.2.tgz#4a43d7bc7703159da0de1a7e3b1cd94141132b10" + integrity sha512-shDW0Td/XZQpP04Yy+OsMt1ILMKGGkoLcy1zVAsSAK0fLfWm0Upgkmfs/NOV2ZhMQwkgpR3ZEdyHmTwgrUDQuA== + dependencies: + "@inquirer/prompts" "^8.5.2" + "@napi-rs/cross-toolchain" "^1.0.3" + "@napi-rs/wasm-tools" "^1.0.1" + "@octokit/rest" "^22.0.1" + clipanion "^4.0.0-rc.4" + colorette "^2.0.20" + emnapi "^1.11.1" + es-toolkit "^1.47.0" + js-yaml "^4.2.0" + obug "^2.1.2" + semver "^7.8.2" + typanion "^3.14.0" + +"@napi-rs/cross-toolchain@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@napi-rs/cross-toolchain/-/cross-toolchain-1.0.3.tgz#8e345d0c9a8aeeaf9287e7af1d4ce83476681373" + integrity sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg== + dependencies: + "@napi-rs/lzma" "^1.4.5" + "@napi-rs/tar" "^1.1.0" + debug "^4.4.1" + +"@napi-rs/lzma-android-arm-eabi@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-android-arm-eabi/-/lzma-android-arm-eabi-1.4.5.tgz#c6722a1d7201e269fdb6ba997d28cb41223e515c" + integrity sha512-Up4gpyw2SacmyKWWEib06GhiDdF+H+CCU0LAV8pnM4aJIDqKKd5LHSlBht83Jut6frkB0vwEPmAkv4NjQ5u//Q== + +"@napi-rs/lzma-android-arm64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-android-arm64/-/lzma-android-arm64-1.4.5.tgz#05df61667e84419e0550200b48169057b734806f" + integrity sha512-uwa8sLlWEzkAM0MWyoZJg0JTD3BkPknvejAFG2acUA1raXM8jLrqujWCdOStisXhqQjZ2nDMp3FV6cs//zjfuQ== + +"@napi-rs/lzma-darwin-arm64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-darwin-arm64/-/lzma-darwin-arm64-1.4.5.tgz#c37a01c53f25cb7f014870d2ea6c5576138bcaaa" + integrity sha512-0Y0TQLQ2xAjVabrMDem1NhIssOZzF/y/dqetc6OT8mD3xMTDtF8u5BqZoX3MyPc9FzpsZw4ksol+w7DsxHrpMA== + +"@napi-rs/lzma-darwin-x64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-darwin-x64/-/lzma-darwin-x64-1.4.5.tgz#555b1dd65d7b104d28b2a12d925d7059226c7f4b" + integrity sha512-vR2IUyJY3En+V1wJkwmbGWcYiT8pHloTAWdW4pG24+51GIq+intst6Uf6D/r46citObGZrlX0QvMarOkQeHWpw== + +"@napi-rs/lzma-freebsd-x64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-freebsd-x64/-/lzma-freebsd-x64-1.4.5.tgz#683beff15b37774ec91e1de7b4d337894bf43694" + integrity sha512-XpnYQC5SVovO35tF0xGkbHYjsS6kqyNCjuaLQ2dbEblFRr5cAZVvsJ/9h7zj/5FluJPJRDojVNxGyRhTp4z2lw== + +"@napi-rs/lzma-linux-arm-gnueabihf@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-arm-gnueabihf/-/lzma-linux-arm-gnueabihf-1.4.5.tgz#505f659a9131474b7270afa4a4e9caf709c4d213" + integrity sha512-ic1ZZMoRfRMwtSwxkyw4zIlbDZGC6davC9r+2oX6x9QiF247BRqqT94qGeL5ZP4Vtz0Hyy7TEViWhx5j6Bpzvw== + +"@napi-rs/lzma-linux-arm64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-arm64-gnu/-/lzma-linux-arm64-gnu-1.4.5.tgz#ecbb944635fa004a9415d1f50f165bc0d26d3807" + integrity sha512-asEp7FPd7C1Yi6DQb45a3KPHKOFBSfGuJWXcAd4/bL2Fjetb2n/KK2z14yfW8YC/Fv6x3rBM0VAZKmJuz4tysg== + +"@napi-rs/lzma-linux-arm64-musl@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-arm64-musl/-/lzma-linux-arm64-musl-1.4.5.tgz#c0d17f40ce2db0b075469a28f233fd8ce31fbb95" + integrity sha512-yWjcPDgJ2nIL3KNvi4536dlT/CcCWO0DUyEOlBs/SacG7BeD6IjGh6yYzd3/X1Y3JItCbZoDoLUH8iB1lTXo3w== + +"@napi-rs/lzma-linux-ppc64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-ppc64-gnu/-/lzma-linux-ppc64-gnu-1.4.5.tgz#2f17b9d1fc920c6c511d2086c7623752172c2f07" + integrity sha512-0XRhKuIU/9ZjT4WDIG/qnX7Xz7mSQHYZo9Gb3MP2gcvBgr6BA4zywQ9k3gmQaPn9ECE+CZg2V7DV7kT+x2pUMQ== + +"@napi-rs/lzma-linux-riscv64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-riscv64-gnu/-/lzma-linux-riscv64-gnu-1.4.5.tgz#63c2a4e1157586252186e39604370d5b29c6db85" + integrity sha512-QrqDIPEUUB23GCpyQj/QFyMlr8SGxxyExeZz9OWFnHfb70kXdTLWrHS/hEI1Ru+lSbQ/6xRqeoGyQ4Aqdg+/RA== + +"@napi-rs/lzma-linux-s390x-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-s390x-gnu/-/lzma-linux-s390x-gnu-1.4.5.tgz#6f2ca44bf5c5bef1b31d7516bf15d63c35cdf59f" + integrity sha512-k8RVM5aMhW86E9H0QXdquwojew4H3SwPxbRVbl49/COJQWCUjGi79X6mYruMnMPEznZinUiT1jgKbFo2A00NdA== + +"@napi-rs/lzma-linux-x64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.4.5.tgz#54879d88a9c370687b5463c7c1b6208b718c1ab2" + integrity sha512-6rMtBgnIq2Wcl1rQdZsnM+rtCcVCbws1nF8S2NzaUsVaZv8bjrPiAa0lwg4Eqnn1d9lgwqT+cZgm5m+//K08Kw== + +"@napi-rs/lzma-linux-x64-musl@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-musl/-/lzma-linux-x64-musl-1.4.5.tgz#412705f6925f10f45122bd0f3e2fb6e597bed4f8" + integrity sha512-eiadGBKi7Vd0bCArBUOO/qqRYPHt/VQVvGyYvDFt6C2ZSIjlD+HuOl+2oS1sjf4CFjK4eDIog6EdXnL0NE6iyQ== + +"@napi-rs/lzma-wasm32-wasi@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-wasm32-wasi/-/lzma-wasm32-wasi-1.4.5.tgz#4b74abfd144371123cb6f5b7bad5bae868206ecf" + integrity sha512-+VyHHlr68dvey6fXc2hehw9gHVFIW3TtGF1XkcbAu65qVXsA9D/T+uuoRVqhE+JCyFHFrO0ixRbZDRK1XJt1sA== + dependencies: + "@napi-rs/wasm-runtime" "^1.0.3" + +"@napi-rs/lzma-win32-arm64-msvc@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-win32-arm64-msvc/-/lzma-win32-arm64-msvc-1.4.5.tgz#7ed8c80d588fa244a7fd55249cb0d011d04bf984" + integrity sha512-eewnqvIyyhHi3KaZtBOJXohLvwwN27gfS2G/YDWdfHlbz1jrmfeHAmzMsP5qv8vGB+T80TMHNkro4kYjeh6Deg== + +"@napi-rs/lzma-win32-ia32-msvc@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-win32-ia32-msvc/-/lzma-win32-ia32-msvc-1.4.5.tgz#e6f70ca87bd88370102aa610ee9e44ec28911b46" + integrity sha512-OeacFVRCJOKNU/a0ephUfYZ2Yt+NvaHze/4TgOwJ0J0P4P7X1mHzN+ig9Iyd74aQDXYqc7kaCXA2dpAOcH87Cg== + +"@napi-rs/lzma-win32-x64-msvc@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-win32-x64-msvc/-/lzma-win32-x64-msvc-1.4.5.tgz#ecfcfe364e805915608ce0ff41ed4c950fdb51b8" + integrity sha512-T4I1SamdSmtyZgDXGAGP+y5LEK5vxHUFwe8mz6D4R7Sa5/WCxTcCIgPJ9BD7RkpO17lzhlaM2vmVvMy96Lvk9Q== + +"@napi-rs/lzma@^1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma/-/lzma-1.4.5.tgz#43e17cdfe332a3f33fa640422da348db3d8825e1" + integrity sha512-zS5LuN1OBPAyZpda2ZZgYOEDC+xecUdAGnrvbYzjnLXkrq/OBC3B9qcRvlxbDR3k5H/gVfvef1/jyUqPknqjbg== + optionalDependencies: + "@napi-rs/lzma-android-arm-eabi" "1.4.5" + "@napi-rs/lzma-android-arm64" "1.4.5" + "@napi-rs/lzma-darwin-arm64" "1.4.5" + "@napi-rs/lzma-darwin-x64" "1.4.5" + "@napi-rs/lzma-freebsd-x64" "1.4.5" + "@napi-rs/lzma-linux-arm-gnueabihf" "1.4.5" + "@napi-rs/lzma-linux-arm64-gnu" "1.4.5" + "@napi-rs/lzma-linux-arm64-musl" "1.4.5" + "@napi-rs/lzma-linux-ppc64-gnu" "1.4.5" + "@napi-rs/lzma-linux-riscv64-gnu" "1.4.5" + "@napi-rs/lzma-linux-s390x-gnu" "1.4.5" + "@napi-rs/lzma-linux-x64-gnu" "1.4.5" + "@napi-rs/lzma-linux-x64-musl" "1.4.5" + "@napi-rs/lzma-wasm32-wasi" "1.4.5" + "@napi-rs/lzma-win32-arm64-msvc" "1.4.5" + "@napi-rs/lzma-win32-ia32-msvc" "1.4.5" + "@napi-rs/lzma-win32-x64-msvc" "1.4.5" + +"@napi-rs/tar-android-arm-eabi@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-android-arm-eabi/-/tar-android-arm-eabi-1.1.0.tgz#08ae6ebbaf38d416954a28ca09bf77410d5b0c2b" + integrity sha512-h2Ryndraj/YiKgMV/r5by1cDusluYIRT0CaE0/PekQ4u+Wpy2iUVqvzVU98ZPnhXaNeYxEvVJHNGafpOfaD0TA== + +"@napi-rs/tar-android-arm64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-android-arm64/-/tar-android-arm64-1.1.0.tgz#825a76140116f89d7e930245bda9f70b196da565" + integrity sha512-DJFyQHr1ZxNZorm/gzc1qBNLF/FcKzcH0V0Vwan5P+o0aE2keQIGEjJ09FudkF9v6uOuJjHCVDdK6S6uHtShAw== + +"@napi-rs/tar-darwin-arm64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-darwin-arm64/-/tar-darwin-arm64-1.1.0.tgz#8821616c40ea52ec2c00a055be56bf28dee76013" + integrity sha512-Zz2sXRzjIX4e532zD6xm2SjXEym6MkvfCvL2RMpG2+UwNVDVscHNcz3d47Pf3sysP2e2af7fBB3TIoK2f6trPw== + +"@napi-rs/tar-darwin-x64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-darwin-x64/-/tar-darwin-x64-1.1.0.tgz#4a975e41932a145c58181cb43c8f483c3858e359" + integrity sha512-EI+CptIMNweT0ms9S3mkP/q+J6FNZ1Q6pvpJOEcWglRfyfQpLqjlC0O+dptruTPE8VamKYuqdjxfqD8hifZDOA== + +"@napi-rs/tar-freebsd-x64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-freebsd-x64/-/tar-freebsd-x64-1.1.0.tgz#5ebc0633f257b258aacc59ac1420835513ed0967" + integrity sha512-J0PIqX+pl6lBIAckL/c87gpodLbjZB1OtIK+RDscKC9NLdpVv6VGOxzUV/fYev/hctcE8EfkLbgFOfpmVQPg2g== + +"@napi-rs/tar-linux-arm-gnueabihf@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-arm-gnueabihf/-/tar-linux-arm-gnueabihf-1.1.0.tgz#1d309bd4f46f0490353d9608e79d260cf6c7cd43" + integrity sha512-SLgIQo3f3EjkZ82ZwvrEgFvMdDAhsxCYjyoSuWfHCz0U16qx3SuGCp8+FYOPYCECHN3ZlGjXnoAIt9ERd0dEUg== + +"@napi-rs/tar-linux-arm64-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-arm64-gnu/-/tar-linux-arm64-gnu-1.1.0.tgz#88d974821f3f8e9ee6948b4d51c78c019dee88ad" + integrity sha512-d014cdle52EGaH6GpYTQOP9Py7glMO1zz/+ynJPjjzYFSxvdYx0byrjumZk2UQdIyGZiJO2MEFpCkEEKFSgPYA== + +"@napi-rs/tar-linux-arm64-musl@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-arm64-musl/-/tar-linux-arm64-musl-1.1.0.tgz#ab2baee7b288df5e68cef0b2d12fa79d2a551b58" + integrity sha512-L/y1/26q9L/uBqiW/JdOb/Dc94egFvNALUZV2WCGKQXc6UByPBMgdiEyW2dtoYxYYYYc+AKD+jr+wQPcvX2vrQ== + +"@napi-rs/tar-linux-ppc64-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-ppc64-gnu/-/tar-linux-ppc64-gnu-1.1.0.tgz#7500e60d27849ba36fa4802a346249974e7ecf74" + integrity sha512-EPE1K/80RQvPbLRJDJs1QmCIcH+7WRi0F73+oTe1582y9RtfGRuzAkzeBuAGRXAQEjRQw/RjtNqr6UTJ+8UuWQ== + +"@napi-rs/tar-linux-s390x-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-s390x-gnu/-/tar-linux-s390x-gnu-1.1.0.tgz#cfc0923bfad1dea8ef9da22148a8d4932aa52d08" + integrity sha512-B2jhWiB1ffw1nQBqLUP1h4+J1ovAxBOoe5N2IqDMOc63fsPZKNqF1PvO/dIem8z7LL4U4bsfmhy3gBfu547oNQ== + +"@napi-rs/tar-linux-x64-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-x64-gnu/-/tar-linux-x64-gnu-1.1.0.tgz#5fdf9e1bb12b10a951c6ab03268a9f8d9788c929" + integrity sha512-tbZDHnb9617lTnsDMGo/eAMZxnsQFnaRe+MszRqHguKfMwkisc9CCJnks/r1o84u5fECI+J/HOrKXgczq/3Oww== + +"@napi-rs/tar-linux-x64-musl@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-x64-musl/-/tar-linux-x64-musl-1.1.0.tgz#f001fc0a0a2996dcf99e787a15eade8dce215e91" + integrity sha512-dV6cODlzbO8u6Anmv2N/ilQHq/AWz0xyltuXoLU3yUyXbZcnWYZuB2rL8OBGPmqNcD+x9NdScBNXh7vWN0naSQ== + +"@napi-rs/tar-wasm32-wasi@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-wasm32-wasi/-/tar-wasm32-wasi-1.1.0.tgz#c1c7df7738b23f1cdbcff261d5bea6968d0a3c9a" + integrity sha512-jIa9nb2HzOrfH0F8QQ9g3WE4aMH5vSI5/1NYVNm9ysCmNjCCtMXCAhlI3WKCdm/DwHf0zLqdrrtDFXODcNaqMw== + dependencies: + "@napi-rs/wasm-runtime" "^1.0.3" + +"@napi-rs/tar-win32-arm64-msvc@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-win32-arm64-msvc/-/tar-win32-arm64-msvc-1.1.0.tgz#4c8519eab28021e1eda0847433cab949d5389833" + integrity sha512-vfpG71OB0ijtjemp3WTdmBKJm9R70KM8vsSExMsIQtV0lVzP07oM1CW6JbNRPXNLhRoue9ofYLiUDk8bE0Hckg== + +"@napi-rs/tar-win32-ia32-msvc@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-win32-ia32-msvc/-/tar-win32-ia32-msvc-1.1.0.tgz#4f61af0da2c53b23f7d58c77970eaa4449e8eb79" + integrity sha512-hGPyPW60YSpOSgzfy68DLBHgi6HxkAM+L59ZZZPMQ0TOXjQg+p2EW87+TjZfJOkSpbYiEkULwa/f4a2hcVjsqQ== + +"@napi-rs/tar-win32-x64-msvc@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-win32-x64-msvc/-/tar-win32-x64-msvc-1.1.0.tgz#eb63fb44ecde001cce6be238f175e66a06c15035" + integrity sha512-L6Ed1DxXK9YSCMyvpR8MiNAyKNkQLjsHsHK9E0qnHa8NzLFqzDKhvs5LfnWxM2kJ+F7m/e5n9zPm24kHb3LsVw== + +"@napi-rs/tar@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar/-/tar-1.1.0.tgz#acecd9e29f705a3f534d5fb3d8aa36b3266727d0" + integrity sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ== + optionalDependencies: + "@napi-rs/tar-android-arm-eabi" "1.1.0" + "@napi-rs/tar-android-arm64" "1.1.0" + "@napi-rs/tar-darwin-arm64" "1.1.0" + "@napi-rs/tar-darwin-x64" "1.1.0" + "@napi-rs/tar-freebsd-x64" "1.1.0" + "@napi-rs/tar-linux-arm-gnueabihf" "1.1.0" + "@napi-rs/tar-linux-arm64-gnu" "1.1.0" + "@napi-rs/tar-linux-arm64-musl" "1.1.0" + "@napi-rs/tar-linux-ppc64-gnu" "1.1.0" + "@napi-rs/tar-linux-s390x-gnu" "1.1.0" + "@napi-rs/tar-linux-x64-gnu" "1.1.0" + "@napi-rs/tar-linux-x64-musl" "1.1.0" + "@napi-rs/tar-wasm32-wasi" "1.1.0" + "@napi-rs/tar-win32-arm64-msvc" "1.1.0" + "@napi-rs/tar-win32-ia32-msvc" "1.1.0" + "@napi-rs/tar-win32-x64-msvc" "1.1.0" + "@napi-rs/wasm-runtime@^0.2.11": version "0.2.12" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" @@ -114,6 +494,194 @@ "@emnapi/runtime" "^1.4.3" "@tybys/wasm-util" "^0.10.0" +"@napi-rs/wasm-runtime@^1.0.3": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz#cccd6ebc40b991dea6936f9126b1b8155b6c4c95" + integrity sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q== + dependencies: + "@tybys/wasm-util" "^0.10.2" + +"@napi-rs/wasm-tools-android-arm-eabi@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-android-arm-eabi/-/wasm-tools-android-arm-eabi-1.0.1.tgz#a709f93ddd95508a4ef949b5ceff2b2e85b676f7" + integrity sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw== + +"@napi-rs/wasm-tools-android-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-android-arm64/-/wasm-tools-android-arm64-1.0.1.tgz#304b5761b4fcc871b876ebd34975c72c9d11a7fc" + integrity sha512-WDR7S+aRLV6LtBJAg5fmjKkTZIdrEnnQxgdsb7Cf8pYiMWBHLU+LC49OUVppQ2YSPY0+GeYm9yuZWW3kLjJ7Bg== + +"@napi-rs/wasm-tools-darwin-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-darwin-arm64/-/wasm-tools-darwin-arm64-1.0.1.tgz#dafb4330986a8b46e8de1603ea2f6932a19634c6" + integrity sha512-qWTI+EEkiN0oIn/N2gQo7+TVYil+AJ20jjuzD2vATS6uIjVz+Updeqmszi7zq7rdFTLp6Ea3/z4kDKIfZwmR9g== + +"@napi-rs/wasm-tools-darwin-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-darwin-x64/-/wasm-tools-darwin-x64-1.0.1.tgz#0919e63714ee0a52b1120f6452bbc3a4d793ce3c" + integrity sha512-bA6hubqtHROR5UI3tToAF/c6TDmaAgF0SWgo4rADHtQ4wdn0JeogvOk50gs2TYVhKPE2ZD2+qqt7oBKB+sxW3A== + +"@napi-rs/wasm-tools-freebsd-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-freebsd-x64/-/wasm-tools-freebsd-x64-1.0.1.tgz#1f50a2d5d5af041c55634f43f623ae49192bce9c" + integrity sha512-90+KLBkD9hZEjPQW1MDfwSt5J1L46EUKacpCZWyRuL6iIEO5CgWU0V/JnEgFsDOGyyYtiTvHc5bUdUTWd4I9Vg== + +"@napi-rs/wasm-tools-linux-arm64-gnu@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-arm64-gnu/-/wasm-tools-linux-arm64-gnu-1.0.1.tgz#6106d5e65a25ec2ae417c2fcfebd5c8f14d80e84" + integrity sha512-rG0QlS65x9K/u3HrKafDf8cFKj5wV2JHGfl8abWgKew0GVPyp6vfsDweOwHbWAjcHtp2LHi6JHoW80/MTHm52Q== + +"@napi-rs/wasm-tools-linux-arm64-musl@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-arm64-musl/-/wasm-tools-linux-arm64-musl-1.0.1.tgz#0eb3d4d1fbc1938b0edd907423840365ebc53859" + integrity sha512-jAasbIvjZXCgX0TCuEFQr+4D6Lla/3AAVx2LmDuMjgG4xoIXzjKWl7c4chuaD+TI+prWT0X6LJcdzFT+ROKGHQ== + +"@napi-rs/wasm-tools-linux-x64-gnu@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-x64-gnu/-/wasm-tools-linux-x64-gnu-1.0.1.tgz#5de6a567083a83efed16d046f47b680cbe7c9b53" + integrity sha512-Plgk5rPqqK2nocBGajkMVbGm010Z7dnUgq0wtnYRZbzWWxwWcXfZMPa8EYxrK4eE8SzpI7VlZP1tdVsdjgGwMw== + +"@napi-rs/wasm-tools-linux-x64-musl@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-x64-musl/-/wasm-tools-linux-x64-musl-1.0.1.tgz#04cc17ef12b4e5012f2d0e46b09cabe473566e5a" + integrity sha512-GW7AzGuWxtQkyHknHWYFdR0CHmW6is8rG2Rf4V6GNmMpmwtXt/ItWYWtBe4zqJWycMNazpfZKSw/BpT7/MVCXQ== + +"@napi-rs/wasm-tools-wasm32-wasi@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-wasm32-wasi/-/wasm-tools-wasm32-wasi-1.0.1.tgz#6ced3bd03428c854397f00509b1694c3af857a0f" + integrity sha512-/nQVSTrqSsn7YdAc2R7Ips/tnw5SPUcl3D7QrXCNGPqjbatIspnaexvaOYNyKMU6xPu+pc0BTnKVmqhlJJCPLA== + dependencies: + "@napi-rs/wasm-runtime" "^1.0.3" + +"@napi-rs/wasm-tools-win32-arm64-msvc@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-win32-arm64-msvc/-/wasm-tools-win32-arm64-msvc-1.0.1.tgz#e776f66eb637eee312b562e987c0a5871ddc6dac" + integrity sha512-PFi7oJIBu5w7Qzh3dwFea3sHRO3pojMsaEnUIy22QvsW+UJfNQwJCryVrpoUt8m4QyZXI+saEq/0r4GwdoHYFQ== + +"@napi-rs/wasm-tools-win32-ia32-msvc@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-win32-ia32-msvc/-/wasm-tools-win32-ia32-msvc-1.0.1.tgz#9167919a62d24cb3a46f01fada26fee38aeaf884" + integrity sha512-gXkuYzxQsgkj05Zaq+KQTkHIN83dFAwMcTKa2aQcpYPRImFm2AQzEyLtpXmyCWzJ0F9ZYAOmbSyrNew8/us6bw== + +"@napi-rs/wasm-tools-win32-x64-msvc@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-win32-x64-msvc/-/wasm-tools-win32-x64-msvc-1.0.1.tgz#f896ab29a83605795bb12cf2cfc1a215bc830c65" + integrity sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ== + +"@napi-rs/wasm-tools@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools/-/wasm-tools-1.0.1.tgz#f54caa0132322fd5275690b2aeb581d11539262f" + integrity sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ== + optionalDependencies: + "@napi-rs/wasm-tools-android-arm-eabi" "1.0.1" + "@napi-rs/wasm-tools-android-arm64" "1.0.1" + "@napi-rs/wasm-tools-darwin-arm64" "1.0.1" + "@napi-rs/wasm-tools-darwin-x64" "1.0.1" + "@napi-rs/wasm-tools-freebsd-x64" "1.0.1" + "@napi-rs/wasm-tools-linux-arm64-gnu" "1.0.1" + "@napi-rs/wasm-tools-linux-arm64-musl" "1.0.1" + "@napi-rs/wasm-tools-linux-x64-gnu" "1.0.1" + "@napi-rs/wasm-tools-linux-x64-musl" "1.0.1" + "@napi-rs/wasm-tools-wasm32-wasi" "1.0.1" + "@napi-rs/wasm-tools-win32-arm64-msvc" "1.0.1" + "@napi-rs/wasm-tools-win32-ia32-msvc" "1.0.1" + "@napi-rs/wasm-tools-win32-x64-msvc" "1.0.1" + +"@octokit/auth-token@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-6.0.0.tgz#b02e9c08a2d8937df09a2a981f226ad219174c53" + integrity sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w== + +"@octokit/core@^7.0.6": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-7.0.6.tgz#0d58704391c6b681dec1117240ea4d2a98ac3916" + integrity sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q== + dependencies: + "@octokit/auth-token" "^6.0.0" + "@octokit/graphql" "^9.0.3" + "@octokit/request" "^10.0.6" + "@octokit/request-error" "^7.0.2" + "@octokit/types" "^16.0.0" + before-after-hook "^4.0.0" + universal-user-agent "^7.0.0" + +"@octokit/endpoint@^11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-11.0.3.tgz#acf5f7feddde4e12185d5312ee38ff77235d8205" + integrity sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag== + dependencies: + "@octokit/types" "^16.0.0" + universal-user-agent "^7.0.2" + +"@octokit/graphql@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-9.0.3.tgz#5b8341c225909e924b466705c13477face869456" + integrity sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA== + dependencies: + "@octokit/request" "^10.0.6" + "@octokit/types" "^16.0.0" + universal-user-agent "^7.0.0" + +"@octokit/openapi-types@^27.0.0": + version "27.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-27.0.0.tgz#374ea53781965fd02a9d36cacb97e152cefff12d" + integrity sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA== + +"@octokit/plugin-paginate-rest@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz#44dc9fff2dacb148d4c5c788b573ddc044503026" + integrity sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw== + dependencies: + "@octokit/types" "^16.0.0" + +"@octokit/plugin-request-log@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz#de1c1e557df6c08adb631bf78264fa741e01b317" + integrity sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q== + +"@octokit/plugin-rest-endpoint-methods@^17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz#8c54397d3a4060356a1c8a974191ebf945924105" + integrity sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw== + dependencies: + "@octokit/types" "^16.0.0" + +"@octokit/request-error@^7.0.2": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-7.1.0.tgz#440fa3cae310466889778f5a222b47a580743638" + integrity sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw== + dependencies: + "@octokit/types" "^16.0.0" + +"@octokit/request@^10.0.6": + version "10.0.10" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-10.0.10.tgz#45e46934f3d772f006733be6b5ec18f22e54a00c" + integrity sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w== + dependencies: + "@octokit/endpoint" "^11.0.3" + "@octokit/request-error" "^7.0.2" + "@octokit/types" "^16.0.0" + content-type "^2.0.0" + json-with-bigint "^3.5.3" + universal-user-agent "^7.0.2" + +"@octokit/rest@^22.0.1": + version "22.0.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-22.0.1.tgz#4d866c32b76b711d3f736f91992e2b534163b416" + integrity sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw== + dependencies: + "@octokit/core" "^7.0.6" + "@octokit/plugin-paginate-rest" "^14.0.0" + "@octokit/plugin-request-log" "^6.0.0" + "@octokit/plugin-rest-endpoint-methods" "^17.0.0" + +"@octokit/types@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-16.0.0.tgz#fbd7fa590c2ef22af881b1d79758bfaa234dbb7c" + integrity sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg== + dependencies: + "@octokit/openapi-types" "^27.0.0" + "@package-json/types@^0.0.12": version "0.0.12" resolved "https://registry.yarnpkg.com/@package-json/types/-/types-0.0.12.tgz#4629e833ba128ed9880b6b7a947633ee22952462" @@ -138,6 +706,13 @@ dependencies: tslib "^2.4.0" +"@tybys/wasm-util@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz#12b3a1b33db1f9cad4ddff1f604ab7dd00bf464e" + integrity sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg== + dependencies: + tslib "^2.4.0" + "@types/esrecurse@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec" @@ -275,6 +850,11 @@ ajv@^6.14.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + balanced-match@^4.0.2: version "4.0.4" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" @@ -285,6 +865,11 @@ baseline-browser-mapping@^2.9.0: resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9" integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA== +before-after-hook@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-4.0.0.tgz#cf1447ab9160df6a40f3621da64d6ffc36050cb9" + integrity sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ== + brace-expansion@^5.0.2: version "5.0.4" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336" @@ -318,6 +903,11 @@ change-case@^5.4.4: resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== +chardet@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-2.1.1.tgz#5c75593704a642f71ee53717df234031e65373c8" + integrity sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ== + ci-info@^4.3.1: version "4.4.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" @@ -330,11 +920,33 @@ clean-regexp@^1.0.0: dependencies: escape-string-regexp "^1.0.5" +cli-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== + +clipanion@^4.0.0-rc.4: + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/clipanion/-/clipanion-4.0.0-rc.4.tgz#7191a940e47ef197e5f18c9cbbe419278b5f5903" + integrity sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q== + dependencies: + typanion "^3.8.0" + +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + comment-parser@^1.4.1: version "1.4.5" resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.5.tgz#6c595cd090737a1010fe5ff40d86e1d21b7bd6ce" integrity sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw== +content-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-2.0.0.tgz#2fb3ede69dffa0af78ca7c4ce7589680638b56df" + integrity sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ== + core-js-compat@^3.46.0: version "3.48.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.48.0.tgz#7efbe1fc1cbad44008190462217cc5558adaeaa6" @@ -368,6 +980,11 @@ electron-to-chromium@^1.5.263: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz#09f8973100c39fb0d003b890393cd1d58932b1c8" integrity sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg== +emnapi@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/emnapi/-/emnapi-1.11.1.tgz#9f3da4463d61a89822ac902f95a3e324adfd340e" + integrity sha512-kSRjhIcxjMFsBqk7ORvoc9aA5SBKDmecrtF5RMcmOTao0kD/zamaxsuTxMI8C1//wGUuvE7a+19pCE7AEhGVnA== + enhanced-resolve@^5.17.1: version "5.20.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz#323c2a70d2aa7fb4bdfd6d3c24dfc705c581295d" @@ -376,6 +993,11 @@ enhanced-resolve@^5.17.1: graceful-fs "^4.2.4" tapable "^2.3.0" +es-toolkit@^1.47.0: + version "1.47.1" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.47.1.tgz#6f049fef04c2ef27400e8f38255a0dca323e1c3a" + integrity sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q== + escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -586,6 +1208,25 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-string-truncated-width@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz#23afe0da67d752ca0727538f1e6967759728ce49" + integrity sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g== + +fast-string-width@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-string-width/-/fast-string-width-3.0.2.tgz#16dbabb491ce5585b5ecb675b65c165d71688eeb" + integrity sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg== + dependencies: + fast-string-truncated-width "^3.0.2" + +fast-wrap-ansi@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz#95e952a0145bce3f59ad56e179f84c48d4072935" + integrity sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q== + dependencies: + fast-string-width "^3.0.2" + file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" @@ -658,6 +1299,13 @@ graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +iconv-lite@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e" + integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ignore@^5.2.0, ignore@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -697,6 +1345,13 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +js-yaml@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524" + integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw== + dependencies: + argparse "^2.0.1" + jsesc@^3.1.0, jsesc@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" @@ -717,6 +1372,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-with-bigint@^3.5.3: + version "3.5.8" + resolved "https://registry.yarnpkg.com/json-with-bigint/-/json-with-bigint-3.5.8.tgz#1b1edb55a1bc4816ca87ac684297591acd822383" + integrity sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw== + keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -751,6 +1411,11 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +mute-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-3.0.0.tgz#cd8014dd2acb72e1e91bb67c74f0019e620ba2d1" + integrity sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw== + napi-postinstall@^0.3.0: version "0.3.4" resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz#7af256d6588b5f8e952b9190965d6b019653bbb9" @@ -766,6 +1431,11 @@ node-releases@^2.0.27: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.36.tgz#99fd6552aaeda9e17c4713b57a63964a2e325e9d" integrity sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA== +obug@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/obug/-/obug-2.1.3.tgz#c02c60f95abd603409330e767db7f2823193331e" + integrity sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -844,11 +1514,21 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + semver@^7.5.4, semver@^7.6.3, semver@^7.7.2, semver@^7.7.3: version "7.7.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== +semver@^7.8.2: + version "7.8.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.4.tgz#c73eceebae0616934be8dff28a7fd70757c8e696" + integrity sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -861,6 +1541,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + stable-hash-x@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz#dfd76bfa5d839a7470125c6a6b3c8b22061793e9" @@ -888,6 +1573,11 @@ tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +typanion@^3.14.0, typanion@^3.8.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/typanion/-/typanion-3.14.0.tgz#a766a91810ce8258033975733e836c43a2929b94" + integrity sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -895,6 +1585,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.3.tgz#c05870a58125a2dc00431f2df815a77fe69736be" + integrity sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A== + unrs-resolver@^1.9.2: version "1.11.1" resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9"