diff --git a/.cargo/config.template.toml b/.cargo/config.template.toml new file mode 100644 index 00000000..922e9636 --- /dev/null +++ b/.cargo/config.template.toml @@ -0,0 +1,15 @@ +[build] +rustflags = ["--cfg=web_sys_unstable_apis"] + +[alias] +test-debug = "test --features debug" +clippy-debug = "clippy --fix --features debug --target wasm32-unknown-unknown" + +[patch."https://github.com/unyt-org/datex-core"] +datex-core = { path = "../datex-core", default-features = false, features = [ + "std", + "serde", + "wasm_logger", + "wasm_runtime", + "wasm_webrtc", +] } diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 9cd25198..00000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,6 +0,0 @@ -[build] -rustflags = ["--cfg=web_sys_unstable_apis"] - -[alias] -test-debug = "test --features debug" -clippy-debug = "clippy --fix --features debug --target wasm32-unknown-unknown" \ No newline at end of file diff --git a/.github/workflows/deploy-beta.yml b/.github/workflows/deploy-beta.yml index ee6fc067..d4f04aa0 100644 --- a/.github/workflows/deploy-beta.yml +++ b/.github/workflows/deploy-beta.yml @@ -34,13 +34,13 @@ jobs: - name: Generate datex.js bundle for pages (without inline WASM) run: | - deno task build-bundle + deno task build-bundle - name: Set up GitHub Pages directory with datex.js and WASM run: | - mkdir -p ./pages - cp ./datex.js ./pages/datex.js - cp ./src/datex-core/datex_core_js.wasm ./pages/datex_core_js.wasm + mkdir -p ./pages + cp ./datex.js ./pages/datex.js + cp ./src/datex-core/datex_core_js.wasm ./pages/datex_core_js.wasm - name: Generate datex.js bundle for release artifact (with inline WASM) run: | @@ -57,13 +57,18 @@ jobs: - name: Create Draft Release uses: softprops/action-gh-release@v2 with: - draft: true + prerelease: true + make_latest: false tag_name: "beta" files: ./datex.js generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Store commit-hash in pages directory + run: | + echo "$(git rev-parse HEAD)" > ./pages/commit-hash.txt + - name: Upload build artifacts uses: actions/upload-pages-artifact@v3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13dacf5e..fca1ce6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,13 +34,13 @@ jobs: - name: Generate datex.js bundle for pages (without inline WASM) run: | - deno task build-bundle + deno task build-bundle - name: Set up GitHub Pages directory with datex.js and WASM run: | - mkdir -p ./pages - cp ./datex.js ./pages/datex.js - cp ./src/datex-core/datex_core_js.wasm ./pages/datex_core_js.wasm + mkdir -p ./pages + cp ./datex.js ./pages/datex.js + cp ./src/datex-core/datex_core_js.wasm ./pages/datex_core_js.wasm - name: Generate datex.js bundle for release artifact (with inline WASM) run: | diff --git a/.gitignore b/.gitignore index 5f03a4ec..6bd67c77 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ npm src/datex-core/datex_core_js.wasm src/datex-core/datex_core_js.internal.js rustc-ice*.txt -/datex.js \ No newline at end of file +/datex.js +.cargo/config.toml diff --git a/Cargo.lock b/Cargo.lock index 649fa6bc..78e82d29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "adler2" version = "2.0.1" @@ -23,12 +29,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -73,18 +73,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -158,29 +158,23 @@ dependencies = [ "owo-colors", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bumpalo" @@ -190,9 +184,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -208,26 +202,26 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.31" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "num-traits", "serde", @@ -240,7 +234,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14377e276b2c8300513dff55ba4cc4142b44e5d6de6d00eb5b2307d650bb4ec1" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", "regex-automata 0.3.9", "serde", "unicode-ident", @@ -290,9 +284,9 @@ checksum = "a2f4a431c5c9f662e1200b7c7f02c34e91361150e382089a8f2dec3ba680cbda" [[package]] name = "darling" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", @@ -300,33 +294,34 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "datex-core" -version = "0.0.5" -source = "git+https://github.com/unyt-org/datex-core?branch=feat%2Fref#57e570b3352a6a432958fbd7f438150434c16cc0" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b45f844946d7722ef155e91be17f357c1384e2b001cb5820c8c63354da43dd" dependencies = [ "ariadne", "async-stream", @@ -348,8 +343,9 @@ dependencies = [ "futures-util", "gloo-timers 0.3.0", "hex", - "indexmap 2.10.0", - "itertools", + "indexmap 2.11.4", + "internment", + "itertools 0.14.0", "lazy_static", "log", "logos", @@ -365,19 +361,22 @@ dependencies = [ "pad", "ringmap", "serde", + "serde-big-array", "serde_json", "serde_with", "strum", "strum_macros", "syntect", - "thiserror 2.0.12", + "thiserror 2.0.17", + "tsify", "url", + "wasm-bindgen", "wasm-bindgen-futures", ] [[package]] name = "datex-core-js" -version = "0.0.6" +version = "0.0.7" dependencies = [ "async-trait", "datex-core", @@ -389,6 +388,9 @@ dependencies = [ "log", "serde", "serde-wasm-bindgen", + "specta 1.0.5", + "specta-typescript", + "tsify", "url", "uuid", "wasm-bindgen", @@ -399,12 +401,13 @@ dependencies = [ [[package]] name = "datex_macros" -version = "0.1.0" -source = "git+https://github.com/unyt-org/datex-core?branch=feat%2Fref#57e570b3352a6a432958fbd7f438150434c16cc0" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1922189d0e176d0b815a911357b3c20fc878512496928dc13b72c281aa6ecee" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -420,12 +423,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -436,7 +439,16 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", ] [[package]] @@ -459,19 +471,26 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "fancy-regex" -version = "0.11.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" dependencies = [ "bit-set", - "regex", + "regex-automata 0.4.13", + "regex-syntax 0.8.8", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide", @@ -491,9 +510,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -554,7 +573,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -621,6 +640,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -629,15 +661,21 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "heck" version = "0.5.0" @@ -652,9 +690,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -768,9 +806,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -800,13 +838,38 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.0", "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "internment" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "636d4b0f6a39fd684effe2a73f5310df16a3fa7954c26d36833e98f44d1977a2" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", ] [[package]] @@ -826,9 +889,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "6247da8b8658ad4e73a186e747fcc5fc2a29f979d6fe6269127fdb5fd08298d0" dependencies = [ "once_cell", "wasm-bindgen", @@ -842,9 +905,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libm" @@ -864,54 +927,60 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "serde", ] [[package]] name = "logos" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6f536c1af4c7cc81edf73da1f8029896e7e1e16a219ef09b184e76a296f3db" +checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" dependencies = [ "logos-derive", ] [[package]] name = "logos-codegen" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189bbfd0b61330abea797e5e9276408f2edbe4f822d7ad08685d67419aafb34e" +checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" dependencies = [ "beef", "fnv", "lazy_static", "proc-macro2", "quote", - "regex-syntax 0.8.5", + "regex-syntax 0.8.8", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "logos-derive" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebfe8e1a19049ddbfccbd14ac834b215e11b85b90bab0c2dba7c7b92fb5d5cba" +checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" dependencies = [ "logos-codegen", ] [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "minicov" @@ -930,6 +999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -950,7 +1020,7 @@ checksum = "f8eec4327f127d4d18c54c8bfbf7b05d74cc9a1befdcc6283a241238ffbc84c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1069,7 +1139,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1080,18 +1150,18 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "ordered-float" -version = "5.0.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" dependencies = [ "num-traits", ] [[package]] name = "owo-colors" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "pad" @@ -1102,11 +1172,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1122,12 +1198,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "plist" -version = "1.7.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64", - "indexmap 2.10.0", + "indexmap 2.11.4", "quick-xml", "serde", "time", @@ -1135,9 +1211,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -1150,70 +1226,58 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.38.1" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "syn 2.0.106", ] [[package]] @@ -1229,13 +1293,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.8", ] [[package]] @@ -1246,18 +1310,18 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "ringmap" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e281d65739bbe3b6effa840c6317a7e032127303f62d13b3e18e51482da936e7" +checksum = "b5009b8c75e4c1c7ff5842d2305eba7f3b76958169493f7a97ba3bd9bcac532d" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.0", ] [[package]] @@ -1271,9 +1335,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1316,9 +1380,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "send_wrapper" @@ -1328,13 +1392,23 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -1346,44 +1420,64 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_with" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.11.4", "schemars 0.9.0", "schemars 1.0.4", - "serde", - "serde_derive", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -1391,14 +1485,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1407,11 +1501,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -1419,11 +1519,71 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "specta" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2240c3aa020aa61d2c569087d213baafbb212f4ceb9de9dd162376ea6aa0fe3" +dependencies = [ + "document-features", + "indoc", + "once_cell", + "paste", + "serde", + "serde_json", + "specta-macros", + "thiserror 1.0.69", +] + +[[package]] +name = "specta" +version = "2.0.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "specta-macros" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4605306321c356e03873b8ee71d7592a5e7c508add325c3ed0677c16fdf1bcfb" +dependencies = [ + "Inflector", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", + "termcolor", +] + +[[package]] +name = "specta-serde" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63" +dependencies = [ + "specta 2.0.0-rc.22", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-typescript" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d" +dependencies = [ + "specta 2.0.0-rc.22", + "specta-serde", + "thiserror 1.0.69", +] + [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -1455,7 +1615,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1471,9 +1631,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1488,31 +1648,39 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "syntect" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" dependencies = [ "bincode", - "bitflags", "fancy-regex", "flate2", "fnv", "once_cell", "plist", - "regex-syntax 0.8.5", + "regex-syntax 0.8.8", "serde", "serde_derive", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.17", "walkdir", "yaml-rust", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1524,11 +1692,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -1539,25 +1707,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -1570,15 +1738,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -1596,26 +1764,64 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.4", "toml_datetime", + "toml_parser", "winnow", ] +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "tsify" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ec91b85e6c6592ed28636cb1dd1fac377ecbbeb170ff1d79f97aac5e38926d" +dependencies = [ + "gloo-utils", + "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a324606929ad11628a19206d7853807481dcaecd6c08be70a235930b8241955" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.106", +] + [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -1631,13 +1837,14 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -1648,9 +1855,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "js-sys", "wasm-bindgen", @@ -1668,35 +1875,36 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "4ad224d2776649cfb4f4471124f8176e54c1cca67a88108e30a0cd98b90e7ad3" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "3a1364104bdcd3c03f22b16a3b1c9620891469f5e9f09bc38b2db121e593e732" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "9c0a08ecf5d99d5604a6666a70b3cde6ab7cc6142f5e641a8ef48fc744ce8854" dependencies = [ "cfg-if", "js-sys", @@ -1707,9 +1915,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "0d7ab4ca3e367bb1ed84ddbd83cc6e41e115f8337ed047239578210214e36c76" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1717,31 +1925,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "4a518014843a19e2dbbd0ed5dfb6b99b23fb886b14e6192a00803a3e14c552b0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "255eb0aa4cc2eea3662a00c2bbd66e93911b7361d5e0fcd62385acfd7e15dcee" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.50" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +checksum = "6cd223a62fa31b44130386e885e096aac33c13abbe67ea8c993f5279b233bca8" dependencies = [ "js-sys", "minicov", @@ -1752,20 +1960,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.50" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +checksum = "e78b9abad0f2a54d17769c8c0d56f2495f54e465903d6f680c97d612440eabe9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "50462a022f46851b81d5441d1a6f5bac0b21a1d72d64bd4906fbdd4bf7230ec7" dependencies = [ "js-sys", "wasm-bindgen", @@ -1773,18 +1981,18 @@ dependencies = [ [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -1795,128 +2003,64 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -1962,7 +2106,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -1983,7 +2127,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -2000,9 +2144,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdbb9122ea75b11bf96e7492afb723e8a7fbe12c67417aa95e7e3d18144d37cd" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -2017,5 +2161,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] diff --git a/DEVELOP.md b/DEVELOP.md index bfe5da9a..a70cb99a 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -1,36 +1,64 @@ # Development Guide -## Building the rust library +## Building the library -The rust adapter code can be found in the `rs-lib` directory, the generated WASM -and JS glue code in the `src/datex-core` directory. +The rust adapter code can be found in the [`rs-lib`](./rs-lib/) directory, the +generated WASM and JS glue code in the [`src/datex-core`](./src/datex-core/) +directory. -The `rs-lib` directory contains the `datex-core` submodule, which contains the -complete [DATEX Core](https://github.com/unyt-org/datex-core.git) library. +This project has a strong dependency on +[DATEX Core](https://github.com/unyt-org/datex-core.git) (see +[Cargo.toml](./rs-lib/Cargo.toml)). -To generate new WASM and JS glue code for the rust library located in `rs-lib`, -run `deno task build`. +To generate a WASM binary and JS glue code, run the following command: -Rust nightly is required for coroutines: +```sh +deno task release +``` + +To generate a debug build, run: ```sh -rustup install nightly -rustup default nightly +deno task build +``` + +Note that the project is built with **Rust Nightly** +([`rustc 1.91.0-nightly`](https://releases.rs/docs/1.91.0/)) + +--- + +If you want to build the library with a local version of the +[`datex-core`](https://github.com/unyt-org/datex-core) crate, you can override +the dependency in a `.cargo/config.toml` file in the project root like this: + +```toml +[patch."https://github.com/unyt-org/datex-core"] +datex-core = { + path = "../datex-core", # the path to your local datex-core clone + default-features = false, + features = [ + "std", + "serde", + "wasm_logger", + "wasm_runtime", + "wasm_webrtc", + ] +} ``` -## Testing +## Running tests -The JS runtime can be tested by running `deno task test`. This compiles the rust -library, generates the WASM and JS glue code, and runs all tests in the `test` -directory. If you only want to run the tests without rebuilding the rust -library, you can run `deno task test-no-build`. +The JS build can be tested by running `deno task test`. This compiles the +library, generates the WASM binary and JS glue code, and runs all tests in the +[`test`](./test/) directory. If you only want to run the tests without +rebuilding the rust library, you can run `deno task test-no-build`. -## Running in the browser +## Browser testing -You can test the library in the browser by running `deno task serve`. Now, you -can open `http://localhost:8042/test/browser.html` in your browser. A new Datex -runtime instance is automatically created and can be accessed in the developer -console via the global `Datex` variable. +You can test the library in the browser by running `deno task serve`. This will +spin up a web server at `http://localhost:8042/test/browser.html` in your +browser. A new runtime instance is automatically created and can be accessed in +the developer console via the global `Datex` variable. ## Creating a new release diff --git a/README.md b/README.md index a97e1e03..31289284 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # DATEX Core JS +> Check out our specification at [datex.unyt.org](https://datex.unyt.org). + [![Twitter badge][]][Twitter link] [![Discord badge][]][Discord link] The DATEX logo @@ -10,11 +12,11 @@ high-level API for working with DATEX in JavaScript and TypeScript. DATEX is developed and maintained by the [unyt.org](https://unyt.org) organization and community. -You can find the legacy version of the DATEX Core library (implemented in -TypeScript) at +You can find the legacy version of the DATEX Core library _(implemented in +TypeScript)_ at [unyt-org/datex-core-js-legacy](https://github.com/unyt-org/datex-core-js-legacy). -The legacy version is still actively maintained and supported until this library -reaches feature parity. +The legacy version is still maintained and supported until this library reaches +feature parity. ## Contributing diff --git a/deno.json b/deno.json index 1a53b961..c48767fe 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@unyt/datex", - "version": "0.0.6", + "version": "0.0.7", "license": "MIT", "exports": "./src/mod.ts", "compilerOptions": { @@ -24,7 +24,14 @@ "exclude": ["./npm"] }, "imports": { - "@deno/dnt": "jsr:@deno/dnt@^0.42.1" + "@david/path": "jsr:@david/path@^0.2.0", + "@deno/dnt": "jsr:@deno/dnt@^0.42.1", + "@deno/wasmbuild": "https://jsr.io/@deno/wasmbuild/0.19.3/lib/commands/build_command.ts", + "@qnighy/dedent": "jsr:@qnighy/dedent@^0.1.2", + "@std/assert": "jsr:@std/assert@^1.0.15", + "@std/cli": "jsr:@std/cli@^1.0.23", + "@std/fmt": "jsr:@std/fmt@^1.0.8", + "@std/uuid": "jsr:@std/uuid@^1.0.9" }, "test": { "exclude": ["./npm"] diff --git a/deno.lock b/deno.lock index 04c97f7e..be50dfd8 100644 --- a/deno.lock +++ b/deno.lock @@ -1,38 +1,38 @@ { "version": "5", "specifiers": { - "jsr:@david/code-block-writer@^13.0.2": "13.0.3", + "jsr:@david/code-block-writer@^13.0.3": "13.0.3", "jsr:@david/path@0.2": "0.2.0", "jsr:@david/temp@~0.1.1": "0.1.1", - "jsr:@deno/cache-dir@0.20": "0.20.1", - "jsr:@deno/dnt@~0.42.1": "0.42.1", - "jsr:@deno/graph@0.86": "0.86.9", + "jsr:@deno/dnt@~0.42.1": "0.42.3", "jsr:@qnighy/dedent@*": "0.1.2", + "jsr:@qnighy/dedent@~0.1.2": "0.1.2", "jsr:@std/assert@*": "1.0.13", - "jsr:@std/bytes@^1.0.5": "1.0.6", + "jsr:@std/assert@^1.0.15": "1.0.15", "jsr:@std/bytes@^1.0.6": "1.0.6", "jsr:@std/cli@*": "1.0.20", + "jsr:@std/cli@^1.0.11": "1.0.23", + "jsr:@std/cli@^1.0.23": "1.0.23", "jsr:@std/crypto@^1.0.5": "1.0.5", - "jsr:@std/encoding@*": "1.0.10", "jsr:@std/encoding@^1.0.6": "1.0.10", "jsr:@std/fmt@1": "1.0.8", - "jsr:@std/fmt@^1.0.3": "1.0.8", "jsr:@std/fmt@^1.0.4": "1.0.8", + "jsr:@std/fmt@^1.0.8": "1.0.8", "jsr:@std/fs@1": "1.0.19", "jsr:@std/fs@^1.0.10": "1.0.19", - "jsr:@std/fs@^1.0.6": "1.0.19", + "jsr:@std/internal@^1.0.10": "1.0.12", + "jsr:@std/internal@^1.0.12": "1.0.12", "jsr:@std/internal@^1.0.6": "1.0.9", - "jsr:@std/internal@^1.0.9": "1.0.9", - "jsr:@std/io@0.225": "0.225.2", - "jsr:@std/path@1": "1.1.1", - "jsr:@std/path@^1.0.8": "1.1.1", - "jsr:@std/path@^1.1.1": "1.1.1", - "jsr:@std/streams@^1.0.9": "1.0.10", + "jsr:@std/internal@^1.0.9": "1.0.12", + "jsr:@std/path@1": "1.1.2", + "jsr:@std/path@^1.1.1": "1.1.2", + "jsr:@std/streams@^1.0.9": "1.0.12", "jsr:@std/tar@~0.1.4": "0.1.6", - "jsr:@std/uuid@*": "1.0.9", - "jsr:@ts-morph/bootstrap@0.25": "0.25.0", - "jsr:@ts-morph/common@0.25": "0.25.0", - "npm:@unyt/datex@0.0.6": "0.0.6" + "jsr:@std/uuid@^1.0.9": "1.0.9", + "jsr:@ts-morph/bootstrap@0.27": "0.27.0", + "jsr:@ts-morph/common@0.27": "0.27.0", + "npm:@unyt/datex@0.0.6": "0.0.6", + "npm:typescript@5.8.3": "5.8.3" }, "jsr": { "@david/code-block-writer@13.0.3": { @@ -51,36 +51,23 @@ "jsr:@david/path" ] }, - "@deno/cache-dir@0.20.1": { - "integrity": "dc4f3add14307f3ff3b712441ea4acabcbfc9a13f67c5adc78c3aac16ac5e2a0", - "dependencies": [ - "jsr:@deno/graph", - "jsr:@std/fmt@^1.0.3", - "jsr:@std/fs@^1.0.6", - "jsr:@std/io", - "jsr:@std/path@^1.0.8" - ] - }, - "@deno/dnt@0.42.1": { - "integrity": "85322b38eb40d4e8c5216d62536152c35b1bda9dc47c8c60860610397b960223", + "@deno/dnt@0.42.3": { + "integrity": "62a917a0492f3c8af002dce90605bb0d41f7d29debc06aca40dba72ab65d8ae3", "dependencies": [ "jsr:@david/code-block-writer", - "jsr:@deno/cache-dir", "jsr:@std/fmt@1", "jsr:@std/fs@1", "jsr:@std/path@1", "jsr:@ts-morph/bootstrap" ] }, - "@deno/graph@0.86.9": { - "integrity": "c4f353a695bcc5246c099602977dabc6534eacea9999a35a8cb24e807192e6a1" - }, - "@deno/wasmbuild@0.19.2": { - "integrity": "f4a92beae788e2fd68189aefb6a6d8c3be79519c3ef8429a10519296379c33d1", + "@deno/wasmbuild@0.19.3": { + "integrity": "71c641f2c8edeb9344fa2dd783bfdf9944e2ee7227ed6195237044c2e0db3f2a", "dependencies": [ "jsr:@david/path", "jsr:@david/temp", - "jsr:@std/encoding@^1.0.6", + "jsr:@std/cli@^1.0.11", + "jsr:@std/encoding", "jsr:@std/fmt@^1.0.4", "jsr:@std/fs@^1.0.10", "jsr:@std/tar" @@ -95,12 +82,24 @@ "jsr:@std/internal@^1.0.6" ] }, + "@std/assert@1.0.15": { + "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", + "dependencies": [ + "jsr:@std/internal@^1.0.12" + ] + }, "@std/bytes@1.0.6": { "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" }, "@std/cli@1.0.20": { "integrity": "a8c384a2c98cec6ec6a2055c273a916e2772485eb784af0db004c5ab8ba52333" }, + "@std/cli@1.0.23": { + "integrity": "bf95b7a9425ba2af1ae5a6359daf58c508f2decf711a76ed2993cd352498ccca", + "dependencies": [ + "jsr:@std/internal@^1.0.12" + ] + }, "@std/crypto@1.0.5": { "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" }, @@ -120,20 +119,17 @@ "@std/internal@1.0.9": { "integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8" }, - "@std/io@0.225.2": { - "integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7", - "dependencies": [ - "jsr:@std/bytes@^1.0.5" - ] + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" }, - "@std/path@1.1.1": { - "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", + "@std/path@1.1.2": { + "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038", "dependencies": [ - "jsr:@std/internal@^1.0.9" + "jsr:@std/internal@^1.0.10" ] }, - "@std/streams@1.0.10": { - "integrity": "75c0b1431873cd0d8b3d679015220204d36d3c7420d93b60acfc379eb0dc30af" + "@std/streams@1.0.12": { + "integrity": "ae925fa1dc459b1abf5cbaa28cc5c7b0485853af3b2a384b0dc22d86e59dfbf4" }, "@std/tar@0.1.6": { "integrity": "7f06a5359518262207e1c28ea965fd552dff3ba2047d2e9225339af287b1f3a4", @@ -144,18 +140,18 @@ "@std/uuid@1.0.9": { "integrity": "44b627bf2d372fe1bd099e2ad41b2be41a777fc94e62a3151006895a037f1642", "dependencies": [ - "jsr:@std/bytes@^1.0.6", + "jsr:@std/bytes", "jsr:@std/crypto" ] }, - "@ts-morph/bootstrap@0.25.0": { - "integrity": "3cd33ee80ac0aab8e5d2660c639a02187f0c8abfe454636ce86c00eb7e8407db", + "@ts-morph/bootstrap@0.27.0": { + "integrity": "b8d7bc8f7942ce853dde4161b28f9aa96769cef3d8eebafb379a81800b9e2448", "dependencies": [ "jsr:@ts-morph/common" ] }, - "@ts-morph/common@0.25.0": { - "integrity": "e3ed1771e2fb61fbc3d2cb39ebbc4f89cd686d6d9bc6d91a71372be055ac1967", + "@ts-morph/common@0.27.0": { + "integrity": "c7b73592d78ce8479b356fd4f3d6ec3c460d77753a8680ff196effea7a939052", "dependencies": [ "jsr:@std/fs@1", "jsr:@std/path@1" @@ -200,6 +196,10 @@ "isexe@3.1.1": { "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==" }, + "typescript@5.8.3": { + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "bin": true + }, "undici@6.21.3": { "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==" }, @@ -211,11 +211,7 @@ "bin": true }, "ws@8.18.3": { - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "optionalPeers": [ - "bufferutil@^4.0.1", - "utf-8-validate@>=5.0.2" - ] + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==" } }, "redirects": { @@ -275,7 +271,13 @@ }, "workspace": { "dependencies": [ - "jsr:@deno/dnt@~0.42.1" + "jsr:@david/path@0.2", + "jsr:@deno/dnt@~0.42.1", + "jsr:@qnighy/dedent@~0.1.2", + "jsr:@std/assert@^1.0.15", + "jsr:@std/cli@^1.0.23", + "jsr:@std/fmt@^1.0.8", + "jsr:@std/uuid@^1.0.9" ] } } diff --git a/rs-lib/Cargo.toml b/rs-lib/Cargo.toml index 1266447d..b89a6573 100644 --- a/rs-lib/Cargo.toml +++ b/rs-lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "datex-core-js" -version = "0.0.6" +version = "0.0.7" authors = [ "Benedikt Strehle ", "Jonas Strehle ", @@ -20,37 +20,44 @@ panic = "abort" [dependencies] log = { version = "0.4", features = ["std", "serde"] } -datex-core = { version = "0.0.5", git = "https://github.com/unyt-org/datex-core", branch = "feat/ref", default-features = false, features = [ +datex-core = { version = "0.0.6", default-features = false, features = [ "std", + "serde", "wasm_logger", "wasm_runtime", "wasm_webrtc", ] } -datex_macros = { version = "0.1.0", git = "https://github.com/unyt-org/datex-core", branch = "feat/ref", package = "datex_macros" } +datex_macros = { version = "0.1.1", package = "datex_macros" } async-trait = "0.1.87" url = "2.5.4" -wasm-bindgen = { version = "=0.2.100", features = [] } +wasm-bindgen = { version = "=0.2.102", features = [] } # FIXME make serde-wasm-bindgen optional serde-wasm-bindgen = "0.6" serde = { version = "1.0", features = ["derive"] } # serde-wasm-bindgen = "0.6" wasm-bindgen-futures = "0.4.50" -js-sys = "=0.3.77" +js-sys = "=0.3.79" + # webrtc futures = { version = "0.3", default-features = false } # = "0.11", futures-timer = { version = "3", features = ["wasm-bindgen"] } uuid = { version = "1.15.1", features = ["js"], optional = true } futures-channel = "0.3.31" +specta-typescript = "0.0.9" +specta = "1.0.5" +tsify = "0.5.5" # webrtc # tokio = { version = "1.43.0", default-features = false, features = [] } [dependencies.web-sys] -version = "0.3.77" +version = "0.3.79" features = [ "Window", + "AesCtrParams", + "AesGcmParams", "BinaryType", "Blob", "ErrorEvent", @@ -86,6 +93,7 @@ features = [ "RtcDataChannelEvent", "RtcConfiguration", "RtcIceServer", + "MediaStream", ] [dev-dependencies] @@ -98,7 +106,7 @@ default = [ "wasm_serial", "wasm_webrtc", ] -debug = ["datex-core/debug"] +debug = ["datex-core/debug", "datex-core/wasm_runtime"] wasm_websocket_server = [] # only required for a backend js runtime wasm_websocket_client = [] # full support wasm_serial = [] # only required for frontend js runtime diff --git a/rs-lib/src/crypto/crypto_js.rs b/rs-lib/src/crypto/crypto_js.rs index 7cdb8a7c..92350cae 100644 --- a/rs-lib/src/crypto/crypto_js.rs +++ b/rs-lib/src/crypto/crypto_js.rs @@ -4,11 +4,11 @@ use datex_core::crypto::crypto::{CryptoError, CryptoTrait}; use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen_futures::JsFuture; use web_sys::{ - js_sys::{ArrayBuffer, Object, Uint8Array}, - CryptoKey, CryptoKeyPair, + AesCtrParams, AesGcmParams, CryptoKey, CryptoKeyPair, + js_sys::{Array, ArrayBuffer, Object, Reflect, Uint8Array}, }; -use crate::js_utils::{js_array, js_object, AsByteSlice, TryAsByteSlice}; +use crate::js_utils::{AsByteSlice, TryAsByteSlice, js_array, js_object}; mod sealed { use super::*; @@ -95,139 +95,75 @@ impl CryptoJS { })?; Ok(key_or_pair) } - - async fn new_encryption_key_pair() -> Result { - let algorithm = js_object(vec![ - ("name", JsValue::from_str("RSA-OAEP")), - ("modulusLength", JsValue::from_f64(4096.0)), - ( - "publicExponent", - JsValue::from(Uint8Array::from(&[1, 0, 1][..])), - ), - ("hash", JsValue::from_str("SHA-256")), - ]); - Self::generate_crypto_key(&algorithm, true, &["encrypt", "decrypt"]) - .await - } - async fn new_sign_key_pair() -> Result { - let algorithm = js_object(vec![ - ("name", JsValue::from_str("ECDSA")), - ("namedCurve", JsValue::from_str("P-384")), - ]); - Self::generate_crypto_key(&algorithm, true, &["sign", "verify"]).await - } } impl CryptoTrait for CryptoJS { - fn encrypt_rsa( - &self, - data: Vec, // FIXME how to handle lifetime and let data pass as slice - public_key: Vec, - ) -> Pin, CryptoError>> + 'static>> - { - Box::pin(async move { - let key = Self::import_crypto_key( - &public_key, - "spki", - &js_object(vec![ - ("name", JsValue::from_str("RSA-OAEP")), - ("hash", JsValue::from_str("SHA-256")), - ]), - &["encrypt"], - ) - .await?; - - let encryption_promise = Self::crypto_subtle() - .encrypt_with_str_and_u8_array("RSA-OAEP", &key, &data) - .map_err(|_| CryptoError::EncryptionError)?; - - let result: ArrayBuffer = JsFuture::from(encryption_promise) - .await - .map_err(|_| CryptoError::EncryptionError)? - .try_into() - .map_err(|_: std::convert::Infallible| { - CryptoError::EncryptionError - })?; - - let message: Vec = result.as_u8_slice(); + fn create_uuid(&self) -> String { + Self::crypto().random_uuid() + } - Ok(message) - }) + fn random_bytes(&self, length: usize) -> Vec { + let buffer = &mut vec![0u8; length]; + Self::crypto() + .get_random_values_with_u8_array(buffer) + .unwrap(); + buffer.to_vec() } - fn decrypt_rsa( + // Signature and Verification + fn gen_ed25519( &self, - data: Vec, - private_key: Vec, - ) -> Pin, CryptoError>> + 'static>> - { + ) -> Pin< + Box< + dyn Future, Vec), CryptoError>> + + 'static, + >, + > { Box::pin(async move { - let key = Self::import_crypto_key( - &private_key, - "pkcs8", - &js_object(vec![ - ("name", JsValue::from_str("RSA-OAEP")), - ("hash", JsValue::from_str("SHA-256")), - ]), - &["decrypt"], + let algorithm = + js_object(vec![("name", JsValue::from_str("Ed25519"))]); + let key_pair: CryptoKeyPair = Self::generate_crypto_key( + &algorithm, + true, + &["sign", "verify"], ) - .await?; - - let decryption_promise = Self::crypto_subtle() - .decrypt_with_str_and_u8_array("RSA-OAEP", &key, &data) - .map_err(|_| CryptoError::DecryptionError)?; - - let result: JsValue = JsFuture::from(decryption_promise) - .await - .map_err(|_| CryptoError::DecryptionError)? - .try_into() - .map_err(|_: std::convert::Infallible| { - CryptoError::DecryptionError - })?; + .await + .map_err(|_| CryptoError::KeyGeneratorFailed)?; - let message: Vec = result - .try_as_u8_slice() - .map_err(|_| CryptoError::DecryptionError)?; + let pub_key = + Self::export_crypto_key(&key_pair.get_public_key(), "spki") + .await?; + let pri_key = + Self::export_crypto_key(&key_pair.get_private_key(), "pkcs8") + .await?; - Ok(message) + Ok((pub_key, pri_key)) }) } - fn sign_rsa( + fn sig_ed25519<'a>( &self, - data: Vec, - private_key: Vec, - ) -> Pin, CryptoError>>>> { + pri_key: &'a [u8], + data: &'a [u8], + ) -> Pin> + 'a>> { Box::pin(async move { let key = Self::import_crypto_key( - &private_key, + &pri_key, "pkcs8", - &js_object(vec![ - ("name", JsValue::from_str("ECDSA")), - ("namedCurve", JsValue::from_str("P-384")), - ]), + &js_object(vec![("name", JsValue::from_str("Ed25519"))]), &["sign"], ) .await?; - let signature_promise = Self::crypto_subtle() + let sig_prom = Self::crypto_subtle() .sign_with_object_and_u8_array( - &js_object(vec![ - ("name", JsValue::from_str("ECDSA")), - ( - "hash", - JsValue::from(js_object(vec![( - "name", - JsValue::from_str("SHA-384"), - )])), - ), - ]), + &js_object(vec![("name", JsValue::from_str("Ed25519"))]), &key, &data, ) .map_err(|_| CryptoError::SigningError)?; - let result: ArrayBuffer = JsFuture::from(signature_promise) + let result: ArrayBuffer = JsFuture::from(sig_prom) .await .map_err(|_| CryptoError::SigningError)? .try_into() @@ -235,44 +171,35 @@ impl CryptoTrait for CryptoJS { CryptoError::SigningError })?; - let signature: Vec = result.as_u8_slice(); + let sig: [u8; 64] = result + .as_u8_slice() + .try_into() + .expect("Signature length incorrect"); - Ok(signature) + Ok(sig) }) } - fn verify_rsa( + fn ver_ed25519<'a>( &self, - data: Vec, - signature: Vec, - public_key: Vec, - ) -> Pin>>> { + pub_key: &'a [u8], + sig: &'a [u8], + data: &'a [u8], + ) -> Pin> + 'a>> { Box::pin(async move { let key = Self::import_crypto_key( - &public_key, + &pub_key, "spki", - &js_object(vec![ - ("name", JsValue::from_str("ECDSA")), - ("namedCurve", JsValue::from_str("P-384")), - ]), + &js_object(vec![("name", JsValue::from_str("Ed25519"))]), &["verify"], ) .await?; let verified_promise = Self::crypto_subtle() .verify_with_object_and_u8_array_and_u8_array( - &js_object(vec![ - ("name", JsValue::from_str("ECDSA")), - ( - "hash", - JsValue::from(js_object(vec![( - "name", - JsValue::from_str("SHA-384"), - )])), - ), - ]), + &js_object(vec![("name", JsValue::from_str("Ed25519"))]), &key, - &signature, + &sig, &data, ) .map_err(|_| CryptoError::VerificationError)?; @@ -287,45 +214,389 @@ impl CryptoTrait for CryptoJS { }) } - fn create_uuid(&self) -> String { - Self::crypto().random_uuid() + // aes ctr + fn aes_ctr_encrypt<'a>( + &'a self, + hash: &'a [u8; 32], + iv: &'a [u8; 16], + plaintext: &'a [u8], + ) -> Pin, CryptoError>> + 'a>> { + Box::pin(async move { + let subtle = Self::crypto_subtle(); + + let usages = Array::of1( + &JsValue::from_str("encrypt"), + // &JsValue::from_str("decrypt"), + ); + + let ikm_buf = Uint8Array::from(hash.as_slice()).buffer(); + + let key_js = JsFuture::from( + subtle + .import_key_with_object( + "raw", + &ikm_buf.into(), + &js_object(vec![("name", "AES-CTR")]), + false, + &usages, + ) + .map_err(|_| CryptoError::KeyImportFailed)?, + ) + .await + .map_err(|_| CryptoError::KeyImportFailed)?; + let base_key: CryptoKey = key_js + .dyn_into() + .map_err(|_| CryptoError::KeyImportFailed)?; + + let params = AesCtrParams::new( + &"AES-CTR", + &Uint8Array::from(iv.as_slice()), + 64u8, + ); + + let pt = Uint8Array::from(plaintext); + + let ct = JsFuture::from( + subtle + .encrypt_with_object_and_buffer_source( + ¶ms.into(), + &base_key, + &pt, + ) + .map_err(|_| CryptoError::EncryptionError)?, + ) + .await + .map_err(|_| CryptoError::EncryptionError)?; + + let ct_buf: ArrayBuffer = + ct.dyn_into().map_err(|_| CryptoError::EncryptionError)?; + let ct_bytes = Uint8Array::new(&ct_buf).to_vec(); + + Ok(ct_bytes) + }) } - fn random_bytes(&self, length: usize) -> Vec { - let buffer = &mut vec![0u8; length]; - Self::crypto() - .get_random_values_with_u8_array(buffer) - .unwrap(); - buffer.to_vec() + fn aes_ctr_decrypt<'a>( + &'a self, + hash: &'a [u8; 32], + iv: &'a [u8; 16], + ciphertext: &'a [u8], + ) -> Pin, CryptoError>> + 'a>> { + Box::pin(async move { + let subtle = CryptoJS::crypto_subtle(); + + let usages = Array::of1( + // &JsValue::from_str("encrypt"), + &JsValue::from_str("decrypt"), + ); + + let ikm_buf = Uint8Array::from(hash.as_slice()).buffer(); + + let key_js = JsFuture::from( + subtle + .import_key_with_object( + "raw", + &ikm_buf.into(), + &js_object(vec![("name", "AES-CTR")]), + false, + &usages, + ) + .map_err(|_| CryptoError::KeyImportFailed)?, + ) + .await + .map_err(|_| CryptoError::KeyImportFailed)?; + let base_key: CryptoKey = key_js + .dyn_into() + .map_err(|_| CryptoError::KeyImportFailed)?; + + let params = AesCtrParams::new( + &"AES-CTR", + &Uint8Array::from(iv.as_slice()), + 64u8, + ); + + let ct = Uint8Array::from(ciphertext); + + let pt = JsFuture::from( + subtle + .decrypt_with_object_and_buffer_source( + ¶ms.into(), + &base_key, + &ct, + ) + .map_err(|_| CryptoError::DecryptionError)?, + ) + .await + .map_err(|_| CryptoError::DecryptionError)?; + + let pt_buf: ArrayBuffer = + pt.dyn_into().map_err(|_| CryptoError::DecryptionError)?; + let pt_bytes = Uint8Array::new(&pt_buf).to_vec(); + + Ok(pt_bytes) + }) } - fn new_encryption_key_pair<'a>( - &self, - ) -> Pin, Vec), CryptoError>>>> - { + fn key_upwrap<'a>( + &'a self, + // Key Encryption Key (AES-256) + kek_bytes: &'a [u8; 32], + // The AES-CTR key to wrap + key_to_wrap_bytes: &'a [u8; 32], + ) -> Pin> + 'a>> { Box::pin(async move { - let key = Self::new_encryption_key_pair().await?; - let public_key = - Self::export_crypto_key(&key.get_public_key(), "spki").await?; - let private_key = - Self::export_crypto_key(&key.get_private_key(), "pkcs8") - .await?; - Ok((public_key, private_key)) + let subtle = Self::crypto_subtle(); + + // Import the Key Encryption Key (KEK) + let kek_algorithm = + js_object(vec![("name", JsValue::from_str("AES-KW"))]); + + let kek_promise = subtle.import_key_with_object( + "raw", + &Uint8Array::from(kek_bytes.as_slice()).buffer(), + &kek_algorithm, + false, // not extractable + &Array::of2( + &JsValue::from_str("wrapKey"), + &JsValue::from_str("unwrapKey"), + ), + ); + + let kek: CryptoKey = JsFuture::from( + kek_promise.map_err(|_| CryptoError::KeyImportFailed)?, + ) + .await + .map_err(|_| CryptoError::KeyImportFailed)? + .dyn_into() + .map_err(|_| CryptoError::KeyImportFailed)?; + + // Import the key to be wrapped (AES-CTR key) + let key_algorithm = + js_object(vec![("name", JsValue::from_str("AES-CTR"))]); + + let key_promise = subtle.import_key_with_object( + "raw", + &Uint8Array::from(key_to_wrap_bytes.as_slice()).buffer(), + &key_algorithm, + true, // must be extractable to wrap it + &Array::of2( + &JsValue::from_str("encrypt"), + &JsValue::from_str("decrypt"), + ), + ); + + let key_to_wrap: CryptoKey = JsFuture::from( + key_promise.map_err(|_| CryptoError::KeyImportFailed)?, + ) + .await + .map_err(|_| CryptoError::KeyImportFailed)? + .dyn_into() + .map_err(|_| CryptoError::KeyImportFailed)?; + + // Wrap the key + let wrap_promise = subtle.wrap_key_with_str( + "raw", // format to wrap in + &key_to_wrap, // key to wrap + &kek, // wrapping key + "AES-KW", // wrapping algorithm + ); + + let wrapped_buffer = JsFuture::from( + wrap_promise.map_err(|_| CryptoError::KeyImportFailed)?, + ) + .await + .map_err(|_| CryptoError::KeyImportFailed)?; + + let uint8_array = Uint8Array::new(&wrapped_buffer); + let mut result: [u8; 40] = vec![0u8; uint8_array.length() as usize] + .try_into() + .map_err(|_| CryptoError::KeyImportFailed)?; + uint8_array.copy_to(&mut result); + + Ok(result) }) } - fn new_sign_key_pair( + fn key_unwrap<'a>( + &'a self, + kek_bytes: &'a [u8; 32], // Key Encryption Key (same as used for wrapping) + wrapped_key: &'a [u8; 40], // The wrapped key data + ) -> Pin> + 'a>> { + Box::pin(async move { + let subtle = CryptoJS::crypto_subtle(); + + // Import the Key Encryption Key (KEK) + let kek_algorithm = + js_object(vec![("name", JsValue::from_str("AES-KW"))]); + + let kek_promise = subtle.import_key_with_object( + "raw", + &Uint8Array::from(kek_bytes.as_slice()).buffer(), + &kek_algorithm, + false, // not extractable + &Array::of2( + &JsValue::from_str("wrapKey"), + &JsValue::from_str("unwrapKey"), + ), + ); + + let kek: CryptoKey = JsFuture::from( + kek_promise.map_err(|_| CryptoError::KeyImportFailed)?, + ) + .await + .map_err(|_| CryptoError::KeyImportFailed)? + .dyn_into() + .map_err(|_| CryptoError::KeyImportFailed)?; + + // Unwrap the key + let unwrapped_algorithm = + js_object(vec![("name", JsValue::from_str("AES-CTR"))]); + + // Convert wrapped_key to Uint8Array + let wrapped_key_array = Uint8Array::from(wrapped_key.as_slice()); + + let unwrap_promise = subtle + .unwrap_key_with_js_u8_array_and_str_and_object( + "raw", // format the wrapped key is in + &wrapped_key_array, // wrapped key as Uint8Array + &kek, // unwrapping key + "AES-KW", // unwrapping algorithm + &unwrapped_algorithm, // algorithm of the unwrapped key + true, // extractable + &Array::of2( + &JsValue::from_str("encrypt"), + &JsValue::from_str("decrypt"), + ), + ); + + let unwrapped_key: CryptoKey = JsFuture::from( + unwrap_promise.map_err(|_| CryptoError::KeyExportFailed)?, + ) + .await + .map_err(|_| CryptoError::KeyExportFailed)? + .dyn_into() + .map_err(|_| CryptoError::KeyExportFailed)?; + + // Export the unwrapped key as raw bytes + let export_promise = subtle + .export_key("raw", &unwrapped_key) + .map_err(|_| CryptoError::KeyExportFailed)?; + + let exported_buffer = JsFuture::from(export_promise) + .await + .map_err(|_| CryptoError::KeyExportFailed)?; + + let uint8_array = Uint8Array::new(&exported_buffer); + let mut result: [u8; 32] = vec![0u8; uint8_array.length() as usize] + .try_into() + .map_err(|_| CryptoError::KeyExportFailed)?; + uint8_array.copy_to(&mut result); + + Ok(result) + }) + } + + // x25519 key gen + fn gen_x25519( &self, - ) -> Pin, Vec), CryptoError>>>> + ) -> Pin>>> { Box::pin(async move { - let key = Self::new_sign_key_pair().await?; - let public_key = - Self::export_crypto_key(&key.get_public_key(), "spki").await?; - let private_key = - Self::export_crypto_key(&key.get_private_key(), "pkcs8") - .await?; - Ok((public_key, private_key)) + let algorithm = + js_object(vec![("name", JsValue::from_str("X25519"))]); + + let key_pair: CryptoKeyPair = Self::generate_crypto_key( + &algorithm, + true, + &["deriveKey", "deriveBits"], + ) + .await + .map_err(|_| CryptoError::KeyGeneratorFailed)?; + + let pub_key: [u8; 44] = + Self::export_crypto_key(&key_pair.get_public_key(), "spki") + .await + .map_err(|_| CryptoError::KeyGeneratorFailed)? + .try_into() + .map_err(|_| CryptoError::KeyGeneratorFailed)?; + let pri_key: [u8; 48] = + Self::export_crypto_key(&key_pair.get_private_key(), "pkcs8") + .await + .map_err(|_| CryptoError::KeyGeneratorFailed)? + .try_into() + .map_err(|_| CryptoError::KeyGeneratorFailed)?; + + Ok((pub_key, pri_key)) + }) + } + + fn derive_x25519<'a>( + &'a self, + my_raw: &'a [u8; 48], + peer_pub: &'a [u8; 44], + ) -> Pin, CryptoError>> + 'a>> { + Box::pin(async move { + let subtle = Self::crypto_subtle(); + + // Private Key + let pri_key_algorithm = + js_object(vec![("name", JsValue::from_str("X25519"))]); + + let pri_key_promise = subtle + .import_key_with_object( + "pkcs8", + &Uint8Array::from(my_raw.as_slice()).buffer(), + &pri_key_algorithm, + false, // not extractable + &Array::of2( + &JsValue::from_str("deriveKey"), + &JsValue::from_str("deriveBits"), + ), + ) + .map_err(|_| CryptoError::KeyImportFailed)?; + + let pri_key: CryptoKey = JsFuture::from(pri_key_promise) + .await + .map_err(|_| CryptoError::KeyImportFailed)? + .dyn_into() + .map_err(|_| CryptoError::KeyImportFailed)?; + + // Public Key + let pub_key_promise = subtle + .import_key_with_object( + "spki", + &Uint8Array::from(peer_pub.as_slice()).buffer(), + &pri_key_algorithm, // same algorithm object + false, // not extractable + &Array::new(), // no usage for public key + ) + .map_err(|_| CryptoError::KeyImportFailed)?; + + let pub_key: CryptoKey = JsFuture::from(pub_key_promise) + .await + .map_err(|_| CryptoError::KeyImportFailed)? + .dyn_into() + .map_err(|_| CryptoError::KeyImportFailed)?; + + let derive_algorithm = js_object(vec![ + ("name", JsValue::from_str("X25519")), + ("public", pub_key.into()), + ]); + + // Derive bits + let derive_promise = subtle + .derive_bits_with_object(&derive_algorithm, &pri_key, 256u32) + .map_err(|_| CryptoError::KeyGeneratorFailed)?; + + let derived_buffer = JsFuture::from(derive_promise) + .await + .map_err(|_| CryptoError::KeyExportFailed)?; + + let uint8_array = Uint8Array::new(&derived_buffer); + let mut result = vec![0u8; uint8_array.length() as usize]; + uint8_array.copy_to(&mut result); + + Ok(result) }) } } diff --git a/rs-lib/src/js_utils.rs b/rs-lib/src/js_utils.rs index 590770f7..0067c4fb 100644 --- a/rs-lib/src/js_utils.rs +++ b/rs-lib/src/js_utils.rs @@ -1,10 +1,6 @@ -use wasm_bindgen::JsValue; +use wasm_bindgen::{JsError, JsValue}; use web_sys::js_sys::{self, Array, ArrayBuffer, Object, Reflect}; -pub enum JsError { - ConversionError, -} - pub trait TryAsByteSlice { fn try_as_u8_slice(&self) -> Result, JsError>; } @@ -15,10 +11,9 @@ pub trait AsByteSlice { impl TryAsByteSlice for JsValue { fn try_as_u8_slice(&self) -> Result, JsError> { - let buffer: ArrayBuffer = self - .clone() - .try_into() - .map_err(|_| JsError::ConversionError)?; + let buffer: ArrayBuffer = self.clone().try_into().map_err(|_| { + JsError::new("Failed to convert JsValue to ArrayBuffer") + })?; Ok(buffer.as_u8_slice()) } @@ -54,3 +49,17 @@ where JsValue::from(js_array) } + +pub fn js_error(err: T) -> JsError { + JsError::new(&err.to_string()) +} + +trait ToJsError { + fn js(self) -> Result; +} + +impl ToJsError for Result { + fn js(self) -> Result { + self.map_err(js_error) + } +} diff --git a/rs-lib/src/lib.rs b/rs-lib/src/lib.rs index b5d54612..3d7d2157 100644 --- a/rs-lib/src/lib.rs +++ b/rs-lib/src/lib.rs @@ -6,12 +6,11 @@ extern crate core; use serde_wasm_bindgen::from_value; // use datex_cli_core::CLI; -use datex_core::compiler; -use datex_core::compiler::{compile_script, compile_template, CompileOptions}; -use datex_core::decompiler::{decompile_body, DecompileOptions}; +use datex_core::compiler::{CompileOptions, compile_script, compile_template}; +use datex_core::decompiler::{DecompileOptions, decompile_body}; use datex_core::runtime::execution::{ - execute_dxb_sync, ExecutionInput, ExecutionOptions, + ExecutionInput, ExecutionOptions, execute_dxb_sync, }; use wasm_bindgen::prelude::*; @@ -23,24 +22,15 @@ pub mod network; pub mod crypto; pub mod js_utils; -pub mod memory; pub mod pointer; pub mod utils; -// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global -// allocator. -#[cfg(feature = "wee_alloc")] -#[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; - -// console.log #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console, final)] pub fn log(s: &str); } -// export compiler/runtime functions to JavaScript #[wasm_bindgen] pub fn create_runtime(config: &str, debug_flags: JsValue) -> JSRuntime { let debug_flags: Option = @@ -48,11 +38,6 @@ pub fn create_runtime(config: &str, debug_flags: JsValue) -> JSRuntime { JSRuntime::create(config, debug_flags) } -#[wasm_bindgen] -pub fn compile(datex_script: &str) { - compiler::compile_block(datex_script); -} - /// Executes a Datex script and returns the result as a string. #[wasm_bindgen] pub fn execute(datex_script: &str, formatted: bool) -> String { @@ -72,12 +57,12 @@ pub fn execute(datex_script: &str, formatted: bool) -> String { let (result_dxb, _) = compile_template("?", &[result], CompileOptions::default()) .unwrap(); - + decompile_body( &result_dxb, DecompileOptions { colorized: formatted, - formatted, + formatting: Default::default(), json_compat: true, ..DecompileOptions::default() }, diff --git a/rs-lib/src/memory.rs b/rs-lib/src/memory.rs deleted file mode 100644 index 8a91d015..00000000 --- a/rs-lib/src/memory.rs +++ /dev/null @@ -1,57 +0,0 @@ -use datex_core::stdlib::cell::RefCell; - -use datex_core::runtime::memory::Memory; -use datex_core::runtime::Runtime; -use wasm_bindgen::prelude::*; -use web_sys::js_sys::Uint8Array; - -use crate::pointer::JSPointer; - -#[wasm_bindgen] -#[derive(Clone)] -pub struct JSMemory { - runtime: Runtime -} -/** - * Internal impl of the JSRuntime, not exposed to JavaScript - */ -impl JSMemory { - pub fn new(runtime: Runtime) -> JSMemory { - JSMemory { runtime } - } -} - -/** - * Exposed properties and methods for JavaScript - */ -#[wasm_bindgen] -impl JSMemory { - pub fn get_pointer_by_id( - &mut self, - address: Uint8Array, - ) -> Option { - let mut memory = self.memory().borrow_mut(); - let pointer = memory.get_pointer_by_id_vec(address.to_vec()); - match pointer { - Some(p) => None, //Some(JSPointer::new(p)), - None => None, - } - } - - fn memory(&self) -> &RefCell { - self.runtime.memory() - } - - pub fn get_pointer_ids(&self) -> Vec { - let memory = self.memory().borrow_mut(); - let mut ids: Vec = Vec::new(); - for id in memory.get_pointer_ids() { - ids.push(Uint8Array::from(&id[..])); - } - ids - } - - // pub fn store_pointer(&mut self, address: [i8; 26], pointer: Pointer) { - // self.pointers.insert(address, pointer); - // } -} diff --git a/rs-lib/src/network/com_hub.rs b/rs-lib/src/network/com_hub.rs index be22dea1..9a0b5eb4 100644 --- a/rs-lib/src/network/com_hub.rs +++ b/rs-lib/src/network/com_hub.rs @@ -3,17 +3,19 @@ use super::com_interfaces::matchbox_js_interface::MatchboxClientRegistry; use datex_core::global::dxb_block::IncomingSection; use datex_core::network::com_hub::{ComHubError, InterfacePriority}; -use datex_core::network::com_interfaces::com_interface::{ComInterface, ComInterfaceFactory, ComInterfaceUUID}; +use datex_core::network::com_interfaces::com_interface::{ + ComInterface, ComInterfaceFactory, ComInterfaceUUID, +}; use datex_core::network::com_interfaces::com_interface_socket::ComInterfaceSocketUUID; +use datex_core::runtime::Runtime; use datex_core::stdlib::{cell::RefCell, rc::Rc}; +use datex_core::values::core_values::endpoint::Endpoint; use datex_core::{network::com_hub::ComHub, utils::uuid::UUID}; -use datex_core::runtime::Runtime; use log::error; +use std::str::FromStr; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; use web_sys::js_sys::{self, Promise}; -use std::str::FromStr; -use datex_core::values::core_values::endpoint::Endpoint; #[wasm_bindgen] #[derive(Clone)] @@ -27,16 +29,17 @@ pub struct JSComHub { */ impl JSComHub { pub fn new(runtime: Runtime) -> JSComHub { - JSComHub { - runtime - } + JSComHub { runtime } } pub fn com_hub(&self) -> &ComHub { self.runtime.com_hub() } - pub fn get_interface_for_uuid(&self, uuid: String) -> Result>, ComHubError> { + pub fn get_interface_for_uuid( + &self, + uuid: String, + ) -> Result>, ComHubError> { let base_interface = self .com_hub() .get_interface_by_uuid::(&ComInterfaceUUID::from_string(uuid)); @@ -78,7 +81,12 @@ impl JSComHub { crate::network::com_interfaces::serial_js_interface::SerialJSInterface::factory ); - // TODO: wasm_webrtc + //wasm_webrtc + #[cfg(feature = "wasm_webrtc")] + self.com_hub().register_interface_factory( + "webrtc".to_string(), + crate::network::com_interfaces::webrtc_js_interface::WebRTCJSInterface::factory + ); } pub fn create_interface( @@ -89,17 +97,26 @@ impl JSComHub { let runtime = self.runtime.clone(); future_to_promise(async move { let com_hub = runtime.com_hub(); - let properties = runtime.execute_sync(&properties, &[], None) + let properties = runtime + .execute_sync(&properties, &[], None) .map_err(|e| JsError::new(&format!("{e:?}")))?; if let Some(properties) = properties { let interface = com_hub - .create_interface(&interface_type, properties, InterfacePriority::default()) + .create_interface( + &interface_type, + properties, + InterfacePriority::default(), + ) .await .map_err(|e| JsError::new(&format!("{e:?}")))?; - Ok(JsValue::from_str(&interface.borrow().get_uuid().0.to_string())) - } - else { - Err(JsError::new("Failed to create interface: properties are empty").into()) + Ok(JsValue::from_str( + &interface.borrow().get_uuid().0.to_string(), + )) + } else { + Err(JsError::new( + "Failed to create interface: properties are empty", + ) + .into()) } }) } @@ -142,7 +159,8 @@ impl JSComHub { ComInterfaceUUID(UUID::from_string(interface_uuid)); let socket_uuid = ComInterfaceSocketUUID(UUID::from_string(socket_uuid)); - self.com_hub().get_dyn_interface_by_uuid(&interface_uuid) + self.com_hub() + .get_dyn_interface_by_uuid(&interface_uuid) .expect("Failed to find interface") .borrow_mut() .send_block(block, socket_uuid) @@ -194,10 +212,9 @@ impl JSComHub { if let Ok(endpoint) = endpoint { let trace = self.com_hub().record_trace(endpoint).await; trace.map(|t| t.to_string()) - } - else { + } else { println!("Invalid endpoint: {}", endpoint.unwrap_err()); None - } + } } } diff --git a/rs-lib/src/network/com_interfaces/base_interface.rs b/rs-lib/src/network/com_interfaces/base_interface.rs index f5a4ba52..e30d9a52 100644 --- a/rs-lib/src/network/com_interfaces/base_interface.rs +++ b/rs-lib/src/network/com_interfaces/base_interface.rs @@ -40,52 +40,12 @@ pub struct BaseJSInterface { properties: InterfaceProperties, } -#[wasm_bindgen(typescript_custom_section)] -const BASE_INTERFACE_SETUP_DATA: &'static str = r#" -type BaseInterfaceSetupData = { - name?: string; - interface_type: string; - channel: string; - direction: "In" | "Out" | "InOut"; - round_trip_time: number; - max_bandwidth: number; - continuous_connection: boolean; - allow_redirects: boolean; - is_secure_channel: boolean; - reconnect_attempts?: number; - reconnection_config: - "NoReconnect" | - "InstantReconnect" | - { - ReconnectWithTimeout: { - timeout: number; - } - } | - { - ReconnectWithTimeoutAndAttempts: { - timeout: number; - attempts: number; - } - }; -}; -"#; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "InterfaceProperties | string")] - pub type JSInterfacePropertiesOrName; - - #[wasm_bindgen(typescript_type = "InterfaceProperties")] - pub type JSInterfaceProperties; -} - impl Default for BaseJSInterface { fn default() -> Self { Self::new_with_name("unknown") } } - #[com_interface] impl BaseJSInterface { pub fn new_with_single_socket( @@ -166,15 +126,18 @@ impl BaseJSInterface { receiver_socket_uuid: ComInterfaceSocketUUID, data: Vec, ) -> Result<(), BaseInterfaceError> { - match self.get_socket_with_uuid(receiver_socket_uuid) { Some(socket) => { - let socket = socket.lock().unwrap(); - let receive_queue = socket.get_receive_queue(); - receive_queue.lock().unwrap().extend(data); - Ok(()) - } _ => { - error!("Socket not found"); - Err(BaseInterfaceError::SocketNotFound) - }} + match self.get_socket_with_uuid(receiver_socket_uuid) { + Some(socket) => { + let socket = socket.lock().unwrap(); + let receive_queue = socket.get_receive_queue(); + receive_queue.lock().unwrap().extend(data); + Ok(()) + } + _ => { + error!("Socket not found"); + Err(BaseInterfaceError::SocketNotFound) + } + } } } @@ -226,20 +189,36 @@ impl ComInterfaceFactory for BaseJSInterface { #[wasm_bindgen] impl JSComHub { - pub fn base_interface_register_socket(&self, uuid: String, direction: String) -> Result { - let interface_direction = InterfaceDirection::from_str(&direction).map_err(|_| { - BaseInterfaceError::InvalidInput("Invalid direction".to_string()) - })?; - let base_interface = self.get_interface_for_uuid::(uuid)?; - Ok(base_interface.borrow_mut().register_new_socket(interface_direction).0.to_string()) + pub fn base_interface_register_socket( + &self, + uuid: String, + direction: String, + ) -> Result { + let interface_direction = InterfaceDirection::from_str(&direction) + .map_err(|_| { + BaseInterfaceError::InvalidInput( + "Invalid direction".to_string(), + ) + })?; + let base_interface = + self.get_interface_for_uuid::(uuid)?; + Ok(base_interface + .borrow_mut() + .register_new_socket(interface_direction) + .0 + .to_string()) } - pub fn base_interface_receive(&self, uuid: String, socket_uuid: String, data: Vec) -> Result<(), JsBaseInterfaceError> { - let base_interface = self.get_interface_for_uuid::(uuid)?; + pub fn base_interface_receive( + &self, + uuid: String, + socket_uuid: String, + data: Vec, + ) -> Result<(), JsBaseInterfaceError> { + let base_interface = + self.get_interface_for_uuid::(uuid)?; let socket_uuid = ComInterfaceSocketUUID::from_string(socket_uuid); - Ok(base_interface - .borrow_mut() - .receive(socket_uuid, data)?) + Ok(base_interface.borrow_mut().receive(socket_uuid, data)?) } pub fn base_interface_destroy_socket( @@ -247,9 +226,9 @@ impl JSComHub { uuid: String, socket_uuid: String, ) -> Result<(), JsBaseInterfaceError> { - let base_interface = self.get_interface_for_uuid::(uuid)?; - let socket_uuid = - ComInterfaceSocketUUID::from_string(socket_uuid); + let base_interface = + self.get_interface_for_uuid::(uuid)?; + let socket_uuid = ComInterfaceSocketUUID::from_string(socket_uuid); if base_interface .borrow() .has_socket_with_uuid(socket_uuid.clone()) @@ -264,10 +243,13 @@ impl JSComHub { pub fn base_interface_on_send( &mut self, uuid: String, - func: Function + func: Function, ) -> Result<(), JsBaseInterfaceError> { - let base_interface = self.get_interface_for_uuid::(uuid)?; - let callback = move | block: &[u8], uuid: ComInterfaceSocketUUID| -> Pin>> { + let base_interface = + self.get_interface_for_uuid::(uuid)?; + let callback = move |block: &[u8], + uuid: ComInterfaceSocketUUID| + -> Pin>> { let block = Uint8Array::from(block); let socket_uuid = JsValue::from(uuid.0.to_string()); let result = func @@ -293,15 +275,14 @@ impl JSComHub { socket_uuid: String, data: &[u8], ) -> Result { - let base_interface = self.get_interface_for_uuid::(uuid)?; - Ok( - base_interface + let base_interface = + self.get_interface_for_uuid::(uuid)?; + Ok(base_interface .borrow_mut() .send_block( data, ComInterfaceSocketUUID(UUID::from_string(socket_uuid)), ) - .await - ) + .await) } -} \ No newline at end of file +} diff --git a/rs-lib/src/network/com_interfaces/webrtc_js_interface.rs b/rs-lib/src/network/com_interfaces/webrtc_js_interface.rs index 2d892e22..68f42372 100644 --- a/rs-lib/src/network/com_interfaces/webrtc_js_interface.rs +++ b/rs-lib/src/network/com_interfaces/webrtc_js_interface.rs @@ -6,17 +6,18 @@ use std::sync::Mutex; use std::time::Duration; // FIXME no-std use async_trait::async_trait; +use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common::media_tracks::{MediaKind, MediaTrack, MediaTracks}; use datex_core::values::core_values::endpoint::Endpoint; use datex_core::network::com_interfaces::com_interface::{ - ComInterface, ComInterfaceInfo, ComInterfaceSockets, ComInterfaceUUID, + ComInterface, ComInterfaceError, ComInterfaceFactory, ComInterfaceInfo, ComInterfaceSockets }; use datex_core::network::com_interfaces::com_interface_properties::InterfaceProperties; use datex_core::network::com_interfaces::com_interface_socket::ComInterfaceSocketUUID; -use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common_new::data_channels::{DataChannel, DataChannels}; -use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common_new::structures::{RTCIceCandidateInitDX, RTCIceServer, RTCSdpTypeDX, RTCSessionDescriptionDX}; -use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common_new::utils::WebRTCError; -use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common_new::webrtc_commons::WebRTCCommon; -use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common_new::webrtc_trait::{WebRTCTrait, WebRTCTraitInternal}; +use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common::data_channels::{DataChannel, DataChannels}; +use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common::structures::{RTCIceCandidateInitDX, RTCIceServer, RTCSdpTypeDX, RTCSessionDescriptionDX}; +use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common::utils::WebRTCError; +use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common::webrtc_commons::{WebRTCCommon, WebRTCInterfaceSetupData}; +use datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common::webrtc_trait::{WebRTCTrait, WebRTCTraitInternal}; use datex_core::network::com_interfaces::socket_provider::SingleSocketProvider; use datex_core::stdlib::sync::Arc; use datex_core::task::spawn_local; @@ -26,37 +27,53 @@ use datex_core::network::com_interfaces::com_interface::ComInterfaceState; use js_sys::{Array, Function, Reflect}; use wasm_bindgen_futures::JsFuture; -use crate::define_registry; use crate::js_utils::TryAsByteSlice; -use datex_core::network::com_hub::InterfacePriority; +use crate::network::com_hub::JSComHub; +use crate::wrap_error_for_js; +use datex_core::network::com_hub::ComHubError; use datex_macros::{com_interface, create_opener}; use log::{error, info}; -use wasm_bindgen::prelude::{wasm_bindgen, Closure}; -use wasm_bindgen::{JsCast, JsError, JsValue}; +use wasm_bindgen::prelude::{Closure, wasm_bindgen}; +use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ - MessageEvent, RtcConfiguration, RtcDataChannel, RtcDataChannelEvent, - RtcIceCandidateInit, RtcIceServer, RtcPeerConnection, + MediaStream, MessageEvent, RtcConfiguration, RtcDataChannel, + RtcDataChannelEvent, RtcIceCandidateInit, RtcIceServer, RtcPeerConnection, RtcPeerConnectionIceEvent, RtcSdpType, RtcSessionDescriptionInit, RtcSignalingState, }; + +wrap_error_for_js!(JSWebRTCError, datex_core::network::com_interfaces::default_com_interfaces::webrtc::webrtc_common::utils::WebRTCError); + +impl From for JSWebRTCError { + fn from(err: ComHubError) -> Self { + WebRTCError::ComHubError(err).into() + } +} + pub struct WebRTCJSInterface { info: ComInterfaceInfo, commons: Arc>, peer_connection: Rc>, data_channels: Rc>>, + local_media_tracks: Rc>>, + remote_media_tracks: Rc>>, } impl SingleSocketProvider for WebRTCJSInterface { fn provide_sockets(&self) -> Arc> { self.get_sockets() } } -impl WebRTCTrait for WebRTCJSInterface { +impl WebRTCTrait + for WebRTCJSInterface +{ fn new(peer_endpoint: impl Into) -> Self { WebRTCJSInterface { info: ComInterfaceInfo::default(), commons: Arc::new(Mutex::new(WebRTCCommon::new(peer_endpoint))), peer_connection: Rc::new(None), data_channels: Rc::new(RefCell::new(DataChannels::default())), + local_media_tracks: Rc::new(RefCell::new(MediaTracks::default())), + remote_media_tracks: Rc::new(RefCell::new(MediaTracks::default())), } } fn new_with_ice_servers( @@ -70,7 +87,34 @@ impl WebRTCTrait for WebRTCJSInterface { } #[async_trait(?Send)] -impl WebRTCTraitInternal for WebRTCJSInterface { +impl WebRTCTraitInternal + for WebRTCJSInterface +{ + fn provide_remote_media_tracks( + &self, + ) -> Rc>> { + self.remote_media_tracks.clone() + } + fn provide_local_media_tracks( + &self, + ) -> Rc>> { + self.local_media_tracks.clone() + } + + async fn handle_create_media_channel( + &self, + id: String, + kind: MediaKind, + ) -> Result, WebRTCError> { + todo!("Implement media channel creation") + } + + async fn handle_setup_media_channel( + channel: Rc>>, + ) -> Result<(), WebRTCError> { + todo!("Implement media channel setup") + } + fn provide_data_channels( &self, ) -> Rc>> { @@ -79,7 +123,6 @@ impl WebRTCTraitInternal for WebRTCJSInterface { fn provide_info(&self) -> &ComInterfaceInfo { &self.info } - async fn handle_create_data_channel( &self, ) -> Result, WebRTCError> { @@ -446,65 +489,75 @@ impl ComInterface for WebRTCJSInterface { set_opener!(open); } -define_registry!(WebRTCRegistry, WebRTCJSInterface); -#[wasm_bindgen] -impl WebRTCRegistry { - pub async fn register(&self, endpoint: &str) -> Result { - let com_hub = self.runtime.com_hub(); - let endpoint = Endpoint::new(endpoint); - let mut webrtc_interface = WebRTCJSInterface::new(endpoint); - let uuid = webrtc_interface.get_uuid().clone(); - webrtc_interface - .open() - .await - .map_err(|e| JsError::new(&format!("{e:?}")))?; +impl ComInterfaceFactory for WebRTCJSInterface { + fn create( + setup_data: WebRTCInterfaceSetupData, + ) -> Result { + if let Some(ice_servers) = setup_data.ice_servers.as_ref() { + if ice_servers.is_empty() { + error!( + "Ice servers list is empty, at least one ice server is required" + ); + Err(ComInterfaceError::InvalidSetupData) + } else { + Ok(WebRTCJSInterface::new_with_ice_servers( + setup_data.peer_endpoint, + ice_servers.to_owned(), + )) + } + } else { + Ok(WebRTCJSInterface::new(setup_data.peer_endpoint)) + } + } - com_hub - .add_interface( - Rc::new(RefCell::new(webrtc_interface)), - InterfacePriority::default(), - ) - .map_err(|e| JsError::new(&format!("{e:?}")))?; - Ok(uuid.0.to_string()) + fn get_default_properties() -> InterfaceProperties { + InterfaceProperties::default() } +} - pub async fn create_offer( +#[wasm_bindgen] +impl JSComHub { + pub async fn webrtc_interface_create_offer( &self, interface_uuid: String, - ) -> Result, JsError> { - let interface = self.get_interface(interface_uuid); + ) -> Result, JSWebRTCError> { + let interface = + self.get_interface_for_uuid::(interface_uuid)?; let webrtc_interface = interface.borrow(); let offer = webrtc_interface.create_offer().await?; Ok(offer) } - pub async fn create_answer( + pub async fn webrtc_interface_create_answer( &self, interface_uuid: String, offer: Vec, - ) -> Result, JsError> { - let interface = self.get_interface(interface_uuid); + ) -> Result, JSWebRTCError> { + let interface = + self.get_interface_for_uuid::(interface_uuid)?; let webrtc_interface = interface.borrow(); let answer = webrtc_interface.create_answer(offer).await?; Ok(answer) } - pub async fn set_answer( + pub async fn webrtc_interface_set_answer( &self, interface_uuid: String, answer: Vec, - ) -> Result<(), JsError> { - let interface = self.get_interface(interface_uuid); + ) -> Result<(), JSWebRTCError> { + let interface = + self.get_interface_for_uuid::(interface_uuid)?; let webrtc_interface = interface.borrow(); webrtc_interface.set_answer(answer).await?; Ok(()) } - pub fn set_on_ice_candidate( + pub fn webrtc_interface_set_on_ice_candidate( &self, interface_uuid: String, on_ice_candidate: Function, - ) -> Result<(), JsError> { - let interface = self.get_interface(interface_uuid); + ) -> Result<(), JSWebRTCError> { + let interface = + self.get_interface_for_uuid::(interface_uuid)?; let webrtc_interface = interface.borrow(); webrtc_interface.set_on_ice_candidate(Box::new(move |candidate| { on_ice_candidate @@ -514,22 +567,24 @@ impl WebRTCRegistry { Ok(()) } - pub async fn add_ice_candidate( + pub async fn webrtc_interface_add_ice_candidate( &self, interface_uuid: String, candidate: Vec, - ) -> Result<(), JsError> { - let interface = self.get_interface(interface_uuid); + ) -> Result<(), JSWebRTCError> { + let interface = + self.get_interface_for_uuid::(interface_uuid)?; let webrtc_interface = interface.borrow(); webrtc_interface.add_ice_candidate(candidate).await?; Ok(()) } - pub async fn wait_for_connection( + pub async fn webrtc_interface_wait_for_connection( &self, interface_uuid: String, - ) -> Result<(), JsError> { - let interface = self.get_interface(interface_uuid); + ) -> Result<(), JSWebRTCError> { + let interface = + self.get_interface_for_uuid::(interface_uuid)?; let webrtc_interface = interface.borrow(); webrtc_interface.wait_for_connection().await?; Ok(()) diff --git a/rs-lib/src/network/com_interfaces/websocket_client_js_interface.rs b/rs-lib/src/network/com_interfaces/websocket_client_js_interface.rs index 26cf0ecd..84bb029d 100644 --- a/rs-lib/src/network/com_interfaces/websocket_client_js_interface.rs +++ b/rs-lib/src/network/com_interfaces/websocket_client_js_interface.rs @@ -24,9 +24,8 @@ use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use log::{error, info, warn}; use url::Url; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::{prelude::Closure, JsCast}; -use web_sys::{js_sys, ErrorEvent, MessageEvent}; +use wasm_bindgen::{JsCast, prelude::Closure}; +use web_sys::{ErrorEvent, MessageEvent, js_sys}; pub struct WebSocketClientJSInterface { pub address: Url, @@ -42,15 +41,6 @@ impl SingleSocketProvider for WebSocketClientJSInterface { wrap_error_for_js!(JSWebSocketError, datex_core::network::com_interfaces::default_com_interfaces::websocket::websocket_common::WebSocketError); use datex_macros::{com_interface, create_opener}; - - -#[wasm_bindgen(typescript_custom_section)] -const WEBSOCKET_CLIENT_INTERFACE_SETUP_DATA: &'static str = r#" -type WebSocketClientInterfaceSetupData = { - address: string; -}; -"#; - #[com_interface] impl WebSocketClientJSInterface { pub fn new( @@ -223,4 +213,4 @@ impl ComInterface for WebSocketClientJSInterface { } delegate_com_interface_info!(); set_opener!(open); -} \ No newline at end of file +} diff --git a/rs-lib/src/network/com_interfaces/websocket_server_js_interface.rs b/rs-lib/src/network/com_interfaces/websocket_server_js_interface.rs index 35516be8..3b0129d6 100644 --- a/rs-lib/src/network/com_interfaces/websocket_server_js_interface.rs +++ b/rs-lib/src/network/com_interfaces/websocket_server_js_interface.rs @@ -23,9 +23,9 @@ use datex_core::network::com_hub::ComHubError; use datex_core::network::com_interfaces::com_interface::ComInterfaceState; use log::{debug, error, info}; use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::{prelude::Closure, JsCast}; +use wasm_bindgen::{JsCast, prelude::Closure}; use wasm_bindgen::{JsError, JsValue}; -use web_sys::{js_sys, ErrorEvent, MessageEvent}; +use web_sys::{ErrorEvent, MessageEvent, js_sys}; pub struct WebSocketServerJSInterface { sockets: HashMap, @@ -46,23 +46,18 @@ impl From for JSWebSocketServerError { } } -use datex_macros::{com_interface, create_opener}; use crate::network::com_hub::JSComHub; - -#[wasm_bindgen(typescript_custom_section)] -const WEBSOCKET_SERVER_INTERFACE_SETUP_DATA: &'static str = r#" -type WebSocketServerInterfaceSetupData = { - port: number; -}; -"#; +use datex_macros::{com_interface, create_opener}; #[com_interface] impl WebSocketServerJSInterface { - pub fn new(setup_data: WebSocketServerInterfaceSetupData) -> WebSocketServerJSInterface { + pub fn new( + setup_data: WebSocketServerInterfaceSetupData, + ) -> WebSocketServerJSInterface { WebSocketServerJSInterface { info: ComInterfaceInfo::default(), sockets: HashMap::new(), - port: setup_data.port + port: setup_data.port, } } @@ -218,9 +213,14 @@ impl JSComHub { interface_uuid: String, websocket: web_sys::WebSocket, ) -> Result { - - let interface = self.get_interface_for_uuid::(interface_uuid)?; - - Ok(interface.borrow_mut().register_socket(websocket).to_string()) + let interface = self + .get_interface_for_uuid::( + interface_uuid, + )?; + + Ok(interface + .borrow_mut() + .register_socket(websocket) + .to_string()) } } diff --git a/rs-lib/src/pointer.rs b/rs-lib/src/pointer.rs index c12e6a07..263a228d 100644 --- a/rs-lib/src/pointer.rs +++ b/rs-lib/src/pointer.rs @@ -1,5 +1,5 @@ use wasm_bindgen::prelude::*; -use datex_core::values::reference::Reference; +use datex_core::references::reference::Reference; #[wasm_bindgen] pub struct JSPointer { diff --git a/rs-lib/src/runtime.rs b/rs-lib/src/runtime.rs index dfe94085..948ecacf 100644 --- a/rs-lib/src/runtime.rs +++ b/rs-lib/src/runtime.rs @@ -1,33 +1,48 @@ -use datex_core::values::core_values::endpoint::Endpoint; -#[cfg(feature = "debug")] -use datex_core::runtime::global_context::DebugFlags; -use datex_core::runtime::global_context::GlobalContext; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; -use log::info; use crate::crypto::crypto_js::CryptoJS; -use crate::js_utils::js_array; +use crate::js_utils::{js_array, js_error}; +use crate::network::com_hub::JSComHub; use crate::utils::time::TimeJS; use datex_core::crypto::crypto::CryptoTrait; -use datex_core::decompiler::{decompile_value, DecompileOptions}; +use datex_core::decompiler::{DecompileOptions, decompile_value}; +use datex_core::dif::interface::{ + DIFApplyError, DIFCreatePointerError, DIFInterface, DIFObserveError, + DIFResolveReferenceError, DIFUpdateError, +}; +use datex_core::dif::reference::DIFReference; +use datex_core::dif::r#type::DIFTypeContainer; +use datex_core::dif::update::{DIFUpdate, DIFUpdateData}; +use datex_core::dif::value::DIFValueContainer; use datex_core::global::dxb_block::DXBBlock; use datex_core::global::protocol_structures::block_header::{ BlockHeader, FlagsAndTimestamp, }; +use datex_core::references::observers::{ObserveOptions, TransceiverId}; +use datex_core::references::reference::ReferenceMutability; +#[cfg(feature = "debug")] +use datex_core::runtime::global_context::DebugFlags; +use datex_core::runtime::global_context::GlobalContext; use datex_core::runtime::{Runtime, RuntimeConfig, RuntimeInternal}; -use datex_core::values::serde::deserializer::DatexDeserializer; +use datex_core::serde::deserializer::DatexDeserializer; +use datex_core::values::core_values::endpoint::Endpoint; +use datex_core::values::pointer::PointerAddress; +use datex_core::values::value_container::ValueContainer; +use futures::FutureExt; +use js_sys::Function; +use log::info; +use serde::{Deserialize, Serialize}; +use serde_wasm_bindgen::{Error, from_value}; +use std::fmt::Display; +use std::rc::Rc; +use std::str::FromStr; +use std::sync::Arc; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; use web_sys::js_sys::Promise; -use crate::memory::JSMemory; -use crate::network::com_hub::JSComHub; #[wasm_bindgen(getter_with_clone)] pub struct JSRuntime { runtime: Runtime, pub com_hub: JSComHub, - pub memory: JSMemory } #[derive(Serialize, Deserialize, Default)] @@ -48,11 +63,30 @@ impl From for DebugFlags { } } +#[derive(Serialize, Deserialize, Default)] +struct JSDecompileOptions { + pub formatted: Option, + pub colorized: Option, + pub resolve_slots: Option, + pub json_compat: Option, +} + +#[derive(Debug, PartialEq)] +enum ConversionError { + InvalidValue, +} +impl Display for ConversionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConversionError::InvalidValue => write!(f, "Invalid value"), + } + } +} + /** * Internal impl of the JSRuntime, not exposed to JavaScript */ impl JSRuntime { - pub fn runtime(&self) -> &Runtime { &self.runtime } @@ -62,12 +96,13 @@ impl JSRuntime { debug_flags: Option, ) -> JSRuntime { let deserializer = DatexDeserializer::from_script(config).unwrap(); - let config: RuntimeConfig = Deserialize::deserialize(deserializer).unwrap(); + let config: RuntimeConfig = + Deserialize::deserialize(deserializer).unwrap(); let runtime = Runtime::init( config, GlobalContext { - crypto: Arc::new(Mutex::new(CryptoJS)), - time: Arc::new(Mutex::new(TimeJS)), + crypto: Arc::new(CryptoJS), + time: Arc::new(TimeJS), #[cfg(feature = "debug")] debug_flags: debug_flags.unwrap_or_default().into(), @@ -85,15 +120,9 @@ impl JSRuntime { runtime } - pub fn new(runtime: Runtime) -> JSRuntime { let com_hub = JSComHub::new(runtime.clone()); - let memory = JSMemory::new(runtime.clone()); - JSRuntime { - runtime, - com_hub, - memory - } + JSRuntime { runtime, com_hub } } } @@ -102,68 +131,77 @@ impl JSRuntime { */ #[wasm_bindgen] impl JSRuntime { - pub async fn crypto_test_tmp(&self) -> Promise { future_to_promise(async move { let crypto = CryptoJS {}; - let sign_key_pair = crypto - .new_sign_key_pair() - .await - .map_err(|e| JsValue::from_str(&format!("{e:?}")))?; + // ed25519 and x25519 generation + let (sig_pub, sig_pri) = crypto.gen_ed25519().await.unwrap(); + assert_eq!(sig_pub.len(), 44_usize); + assert_eq!(sig_pri.len(), 48_usize); - info!("#1"); + let (ser_pub, ser_pri) = crypto.gen_x25519().await.unwrap(); + let (cli_pub, cli_pri) = crypto.gen_x25519().await.unwrap(); + assert_eq!(ser_pub.len(), 44_usize); + assert_eq!(ser_pri.len(), 48_usize); - let encryption_key_pair = crypto - .new_encryption_key_pair() + // Signature + let sig = crypto + .sig_ed25519(&sig_pri, &ser_pub.to_vec()) .await - .map_err(|e| JsValue::from_str(&format!("{e:?}")))?; - info!("#2"); + .unwrap(); - let encrypted_message = crypto - .encrypt_rsa(vec![1, 2, 3], encryption_key_pair.0.clone()) + let ver = crypto + .ver_ed25519(&sig_pub, &sig, &ser_pub.to_vec()) .await - .map_err(|e| JsValue::from_str(&format!("{e:?}")))?; - info!("#3"); - - let decrypted_message = crypto - .decrypt_rsa( - encrypted_message.clone(), - encryption_key_pair.1.clone(), - ) - .await - .map_err(|e| JsValue::from_str(&format!("{e:?}")))?; - info!("#4"); + .unwrap(); - let signed_message = crypto - .sign_rsa(vec![1, 2, 3], sign_key_pair.1.clone()) - .await - .map_err(|e| JsValue::from_str(&format!("{e:?}")))?; - info!("#5"); - - let verified = crypto - .verify_rsa( - vec![1, 2, 3], - signed_message.clone(), - sign_key_pair.0.clone(), - ) + assert_eq!(sig.len(), 64); + assert!(ver); + + // Derivation + let cli_sec = + crypto.derive_x25519(&cli_pri, &ser_pub).await.unwrap(); + let ser_sec = + crypto.derive_x25519(&ser_pri, &cli_pub).await.unwrap(); + + assert_eq!(cli_sec, ser_sec); + assert_eq!(cli_sec.len(), 32_usize); + + let hash: [u8; 32] = crypto.random_bytes(32).try_into().unwrap(); + + // aes entailing hkdf + let msg: Vec = b"Some message".to_vec(); + let ctr_iv: [u8; 16] = [0u8; 16]; + + // ctr + let ctr_ciphered = + crypto.aes_ctr_encrypt(&hash, &ctr_iv, &msg).await.unwrap(); + + let ctr_deciphered = crypto + .aes_ctr_decrypt(&hash, &ctr_iv, &ctr_ciphered) .await - .map_err(|e| JsValue::from_str(&format!("{e:?}")))?; - info!("#6"); + .unwrap(); - if !verified { - return Err(JsValue::from_str("Verification failed")); - } - info!("#7"); + assert_eq!(msg, ctr_deciphered); + assert_ne!(msg, ctr_ciphered); + + let wrapped = crypto.key_upwrap(&hash, &hash).await.unwrap(); + let unwrapped = crypto.key_unwrap(&hash, &wrapped).await.unwrap(); + + assert_eq!(hash.to_vec(), unwrapped); + // assert_ne!(wrapped, unwrapped); let js_array = js_array(&[ - encryption_key_pair.0, - encryption_key_pair.1, - sign_key_pair.0, - sign_key_pair.1, - encrypted_message, - decrypted_message, - signed_message, + ser_pub.to_vec(), + ser_pri.to_vec(), + sig_pub.to_vec(), + sig_pri.to_vec(), + cli_sec.to_vec(), + ser_sec.to_vec(), + hash.to_vec(), + wrapped.to_vec(), + unwrapped.to_vec(), ]); Ok(js_array) }) @@ -197,7 +235,7 @@ impl JSRuntime { block.recalculate_struct(); block.set_receivers( - &receivers + receivers .iter() .map(|r| Endpoint::from_str(r)) .collect::, _>>() @@ -214,47 +252,413 @@ impl JSRuntime { RuntimeInternal::stop_update_loop(self.runtime.internal.clone()).await } + pub async fn execute_with_string_result( + &self, + script: &str, + dif_values: Option>, + decompile_options: JsValue, + ) -> Result { + let val = &self + .js_values_to_value_containers(dif_values) + .map_err(js_error)?; + let result = self + .runtime + .execute(script, val, None) + .await + .map_err(js_error)?; + match result { + None => Ok("".to_string()), + Some(result) => Ok(decompile_value( + &result, + Self::decompile_options_from_js_value(decompile_options), + )), + } + } + pub async fn execute( &self, script: &str, - formatted: bool, - ) -> String { - let input = self.runtime.execute(script, &[], None).await.unwrap(); + dif_values: Option>, + ) -> Result { + let result = self + .runtime + .execute( + script, + &self + .js_values_to_value_containers(dif_values) + .map_err(js_error)?, + None, + ) + .await + .map_err(js_error)?; + Ok(self.maybe_value_container_to_dif(result)) + } + + pub fn execute_sync_with_string_result( + &self, + script: &str, + dif_values: Option>, + decompile_options: JsValue, + ) -> Result { + let input = self + .runtime + .execute_sync( + script, + &self + .js_values_to_value_containers(dif_values) + .map_err(js_error)?, + None, + ) + .map_err(js_error)?; match input { - None => { - "".to_string() - } - Some(result) => { - decompile_value( - &result, - DecompileOptions { - formatted, - ..DecompileOptions::default() - }, - ) - } + None => Ok("".to_string()), + Some(result) => Ok(decompile_value( + &result, + Self::decompile_options_from_js_value(decompile_options), + )), } } pub fn execute_sync( &self, script: &str, - formatted: bool, - ) -> String { - let input = self.runtime.execute_sync(script, &[], None).unwrap(); - match input { - None => { - "".to_string() + dif_values: Option>, + ) -> Result { + let result = self + .runtime + .execute_sync( + script, + &self + .js_values_to_value_containers(dif_values) + .map_err(js_error)?, + None, + ) + .map_err(js_error)?; + Ok(self.maybe_value_container_to_dif(result)) + } + + pub fn value_to_string( + &self, + dif_value: JsValue, + decompile_options: JsValue, + ) -> Result { + let value_container = self + .js_value_to_value_container(dif_value) + .map_err(js_error)?; + Ok(decompile_value( + &value_container, + Self::decompile_options_from_js_value(decompile_options), + )) + } + + fn maybe_value_container_to_dif( + &self, + maybe_value_container: Option, + ) -> JsValue { + match maybe_value_container { + None => JsValue::NULL, + Some(value_container) => { + let dif_value_container = + DIFValueContainer::from_value_container( + &value_container, + self.runtime.memory(), + ); + to_js_value(&dif_value_container).unwrap() } - Some(result) => { - decompile_value( - &result, - DecompileOptions { - formatted, - ..DecompileOptions::default() - }, - ) + } + } + + fn js_values_to_value_containers( + &self, + js_values: Option>, + ) -> Result, ConversionError> { + js_values + .unwrap_or_default() + .into_iter() + .map(|js_value| self.js_value_to_value_container(js_value)) + .collect() + } + + /// Convert a JsValue (DIFValue) to a ValueContainer + /// Returns Err(()) if the conversion fails (invalid json or ref not found) + fn js_value_to_value_container( + &self, + js_value: JsValue, + ) -> Result { + // convert JsValue to DIFValue + let dif_value: DIFValueContainer = from_value(js_value).unwrap(); + // convert DIFValue to ValueContainer + if let Ok(value_container) = + dif_value.to_value_container(self.runtime.memory()) + { + Ok(value_container) + } else { + // ref not found + Err(ConversionError::InvalidValue) + } + } + + fn decompile_options_from_js_value( + decompile_options: JsValue, + ) -> DecompileOptions { + // if null, return default options + if decompile_options.is_null() { + DecompileOptions::default() + } + // if not null, try to deserialize + else { + let js_decompile_options: JSDecompileOptions = + from_value(decompile_options).unwrap_or_default(); + DecompileOptions { + formatting: Default::default(), + colorized: js_decompile_options.colorized.unwrap_or(false), + resolve_slots: js_decompile_options + .resolve_slots + .unwrap_or(false), + json_compat: js_decompile_options.json_compat.unwrap_or(false), } } } + /// Get a handle to the DIF interface of the runtime + pub fn dif(&self) -> RuntimeDIFHandle { + RuntimeDIFHandle { + internal: self.runtime.internal.clone(), + } + } +} + +/// Convert a serializable value to a JsValue (JSON compatible) +fn to_js_value(value: &T) -> Result { + value.serialize(&serde_wasm_bindgen::Serializer::json_compatible()) +} + +#[wasm_bindgen] +pub struct RuntimeDIFHandle { + internal: Rc, +} + +#[wasm_bindgen] +impl RuntimeDIFHandle { + fn js_value_to_pointer_address( + address: &str, + ) -> Result { + PointerAddress::try_from(address) + .map_err(|_| js_error(ConversionError::InvalidValue)) + } + + pub fn observe_pointer( + &self, + transceiver_id: TransceiverId, + address: &str, + observe_options: JsValue, + callback: &Function, + ) -> Result { + let address = RuntimeDIFHandle::js_value_to_pointer_address(address)?; + let cb = callback.clone(); + let observe_options: ObserveOptions = + from_value(observe_options).map_err(js_error)?; + let observer = move |update: &DIFUpdate| { + let js_value = to_js_value(update).unwrap(); + let _ = cb.call1(&JsValue::NULL, &js_value); + }; + self.internal + .observe_pointer(transceiver_id, address, observe_options, observer) + .map_err(js_error) + } + + pub fn unobserve_pointer( + &self, + address: &str, + observer_id: u32, + ) -> Result<(), JsError> { + let address = RuntimeDIFHandle::js_value_to_pointer_address(address)?; + DIFInterface::unobserve_pointer(self, address, observer_id) + .map_err(js_error) + } + + pub fn update_observer_options( + &self, + address: &str, + observer_id: u32, + observe_options: JsValue, + ) -> Result<(), JsError> { + let address = RuntimeDIFHandle::js_value_to_pointer_address(address)?; + let observe_options: ObserveOptions = + from_value(observe_options).map_err(js_error)?; + DIFInterface::update_observer_options( + self, + address, + observer_id, + observe_options, + ) + .map_err(js_error) + } + + pub fn update( + &mut self, + transceiver_id: TransceiverId, + address: &str, + update: JsValue, + ) -> Result<(), JsError> { + let address = Self::js_value_to_pointer_address(address)?; + let dif_update_data: DIFUpdateData = + from_value(update).map_err(js_error)?; + DIFInterface::update(self, transceiver_id, address, dif_update_data) + .map_err(js_error) + } + + pub fn apply( + &mut self, + callee: JsValue, + value: JsValue, + ) -> Result { + let dif_callee: DIFValueContainer = + from_value(callee).map_err(js_error)?; + let dif_value: DIFValueContainer = + from_value(value).map_err(js_error)?; + let result = DIFInterface::apply(self, dif_callee, dif_value) + .map_err(js_error)?; + to_js_value(&result).map_err(js_error) + } + + pub fn create_pointer( + &self, + value: JsValue, + allowed_type: JsValue, + mutability: u8, + ) -> Result { + let dif_value: DIFValueContainer = + from_value(value).map_err(js_error)?; + let dif_allowed_type: Option = + if allowed_type.is_null() || allowed_type.is_undefined() { + None + } else { + Some(from_value(allowed_type).map_err(js_error)?) + }; + let dif_mutability = ReferenceMutability::try_from(mutability) + .map_err(|_| js_error(ConversionError::InvalidValue))?; + let address = DIFInterface::create_pointer( + self, + dif_value, + dif_allowed_type, + dif_mutability, + ) + .map_err(js_error)?; + Ok(address.to_address_string()) + } + + /// Resolve a pointer address synchronously if it's in memory, otherwise return an error + pub fn resolve_pointer_address_sync( + &self, + address: &str, + ) -> Result { + let address = Self::js_value_to_pointer_address(address)?; + let result = + DIFInterface::resolve_pointer_address_in_memory(self, address) + .map_err(js_error)?; + to_js_value(&result).map_err(js_error) + } + + /// Resolve a pointer address, returning a Promise + /// If the pointer is in memory, the promise resolves immediately + /// If the pointer is not in memory, it will be loaded first + pub fn resolve_pointer_address( + &self, + address: &str, + ) -> Result { + if let Ok(sync) = self.resolve_pointer_address_sync(address) { + return Ok(sync); + } + let address = Self::js_value_to_pointer_address(address)?; + let runtime = self.internal.clone(); + Ok(future_to_promise(async move { + let result = runtime + .resolve_pointer_address_external(address) + .await + .map_err(js_error)?; + Ok(to_js_value(&result).map_err(js_error)?) + }) + .unchecked_into()) + } } + +impl DIFInterface for RuntimeDIFHandle { + fn update( + &self, + source_id: TransceiverId, + address: PointerAddress, + update: DIFUpdateData, + ) -> Result<(), DIFUpdateError> { + self.internal.update(source_id, address, update) + } + + async fn resolve_pointer_address_external( + &self, + address: PointerAddress, + ) -> Result { + self.internal + .resolve_pointer_address_external(address) + .await + } + + fn resolve_pointer_address_in_memory( + &self, + address: PointerAddress, + ) -> Result { + self.internal.resolve_pointer_address_in_memory(address) + } + + fn apply( + &self, + callee: DIFValueContainer, + value: DIFValueContainer, + ) -> Result { + self.internal.apply(callee, value) + } + + fn create_pointer( + &self, + value: DIFValueContainer, + allowed_type: Option, + mutability: ReferenceMutability, + ) -> Result { + self.internal + .create_pointer(value, allowed_type, mutability) + } + + fn observe_pointer( + &self, + transceiver_id: TransceiverId, + address: PointerAddress, + options: ObserveOptions, + observer: F, + ) -> Result { + self.internal.observe_pointer( + transceiver_id, + address, + options, + observer, + ) + } + + fn unobserve_pointer( + &self, + address: PointerAddress, + observer_id: u32, + ) -> Result<(), DIFObserveError> { + self.internal.unobserve_pointer(address, observer_id) + } + + fn update_observer_options( + &self, + address: PointerAddress, + observer_id: u32, + options: ObserveOptions, + ) -> Result<(), DIFObserveError> { + self.internal + .update_observer_options(address, observer_id, options) + } +} + +#[cfg(test)] +mod tests {} diff --git a/rs-lib/tests/core_types.rs b/rs-lib/tests/core_types.rs new file mode 100644 index 00000000..d42d0fa1 --- /dev/null +++ b/rs-lib/tests/core_types.rs @@ -0,0 +1,29 @@ +use datex_core::libs::core::{CoreLibPointerId, create_core_lib}; +use datex_core::values::pointer::PointerAddress; + +#[test] +#[ignore] +/// Generates a TypeScript mapping of core type addresses to their names. +/// Run this test and copy the output into `src/dif/definitions.ts`. +/// +/// `cargo test create_core_type_ts_mapping -- --show-output --ignored` +fn create_core_type_ts_mapping() { + let core_lib = create_core_lib(); + let mut core_lib: Vec<(CoreLibPointerId, PointerAddress)> = core_lib + .keys() + .map(|key| (key.clone(), PointerAddress::from(key.clone()))) + .collect(); + core_lib.sort_by_key(|(key, _)| { + PointerAddress::from(key.clone()).bytes().to_vec() + }); + + println!("export const CoreTypeAddress = {{"); + for (core_lib_id, address) in core_lib { + println!( + " {}: \"{}\",", + core_lib_id.to_string().replace("/", "_"), + address.to_string().strip_prefix("$").unwrap() + ); + } + println!("}} as const;"); +} diff --git a/rs-lib/tests/mod.rs b/rs-lib/tests/mod.rs new file mode 100644 index 00000000..af2e4f45 --- /dev/null +++ b/rs-lib/tests/mod.rs @@ -0,0 +1,2 @@ +#[cfg(test)] +pub mod core_types; diff --git a/scripts/build-bundle.ts b/scripts/build-bundle.ts index dca90496..f7b9c363 100644 --- a/scripts/build-bundle.ts +++ b/scripts/build-bundle.ts @@ -1,4 +1,4 @@ -import { encodeBase64 } from "jsr:@std/encoding/base64"; +import { encodeBase64 } from "@std/encoding/base64"; // check if --inline-wasm argument is passed // if --inline-wasm is passed, the wasm file will be embedded into the bundle diff --git a/scripts/build-wasm.ts b/scripts/build-wasm.ts index 31c5da24..6b590174 100644 --- a/scripts/build-wasm.ts +++ b/scripts/build-wasm.ts @@ -1,13 +1,10 @@ -import { runBuildCommand } from "https://jsr.io/@deno/wasmbuild/0.19.2/lib/commands/build_command.ts"; -import { Path } from "jsr:@david/path@^0.2.0"; -import { format } from "https://deno.land/std@0.224.0/fmt/bytes.ts"; -import { parseArgs } from "jsr:@std/cli/parse-args"; -import { parse } from "https://deno.land/std@0.224.0/toml/mod.ts"; -import { dedent } from "jsr:@qnighy/dedent"; +import { runBuildCommand } from "@deno/wasmbuild"; +import { Path } from "@david/path"; +import { format } from "@std/fmt/bytes"; +import { parseArgs } from "@std/cli/parse-args"; +import { dedent } from "@qnighy/dedent"; -const configText = await Deno.readTextFile(".cargo/config.toml"); -const RUST_FLAGS = - (parse(configText).build as { rustflags?: string[] })?.rustflags ?? []; +const RUST_FLAGS = ["--cfg=web_sys_unstable_apis"]; const PREVIOUS_RUSTFLAGS = Deno.env.has("RUSTFLAGS") ? Deno.env.get("RUSTFLAGS") : null; diff --git a/src/datex-core/datex_core_js.d.ts b/src/datex-core/datex_core_js.d.ts index c7ec2e35..b6f9f426 100644 --- a/src/datex-core/datex_core_js.d.ts +++ b/src/datex-core/datex_core_js.d.ts @@ -2,8 +2,6 @@ // deno-lint-ignore-file // deno-fmt-ignore-file -export function create_runtime(config: string, debug_flags: any): JSRuntime; -export function compile(datex_script: string): void; /** * Executes a Datex script and returns the result as a string. */ @@ -13,41 +11,117 @@ export function execute(datex_script: string, formatted: boolean): string; * Does not return the result of the script, but only indicates success or failure. */ export function execute_internal(datex_script: string): boolean; +export function create_runtime(config: string, debug_flags: any): JSRuntime; +export type BaseInterfaceSetupData = InterfaceProperties; -type WebSocketServerInterfaceSetupData = { - port: number; +export interface WebRTCInterfaceSetupData { + peer_endpoint: string; + ice_servers: RTCIceServer[] | null; +} + +export type ReconnectionConfig = "NoReconnect" | "InstantReconnect" | { + ReconnectWithTimeout: { timeout: { secs: number; nanos: number } }; +} | { + ReconnectWithTimeoutAndAttempts: { + timeout: { secs: number; nanos: number }; + attempts: number; + }; }; -type BaseInterfaceSetupData = { - name?: string; +export interface InterfaceProperties { + /** + * the type of the interface, by which it is identified + * e.g. \"tcp-client\", \"websocket-server\", + * multiple interfaces implementations (e.g. for native and web) + * can have the same interface type if they are compatible and + * have an identical initialization function + */ interface_type: string; + /** + * the channel that the interface is using, + * e.g. \"tcp\", \"websocket\ + */ channel: string; - direction: "In" | "Out" | "InOut"; + /** + * a unique name that further identifies an interface instance + * e.g. \"wss://example.com:443\ + */ + name: string | null; + /** + * The support message direction of the interface + */ + direction: InterfaceDirection; + /** + * Estimated mean latency for this interface type in milliseconds (round trip time). + * Lower latency interfaces are preferred over higher latency channels + */ round_trip_time: number; + /** + * Bandwidth in bytes per second + */ max_bandwidth: number; + /** + * If true, the interface does support continuous connections + */ continuous_connection: boolean; + /** + * If true, the interface can be used to redirect DATEX messages to other endpoints + * which are not directly connected to the interface (default: true) + * Currently only enforced for broadcast messages + */ allow_redirects: boolean; + /** + * If true, the interface is a secure channel (can not be eavesdropped). + * This might be an already encrypted channel such as WebRTC or a channel + * that is end-to-end and not interceptable by third parties + */ is_secure_channel: boolean; - reconnect_attempts?: number; - reconnection_config: - | "NoReconnect" - | "InstantReconnect" - | { - ReconnectWithTimeout: { - timeout: number; - }; - } - | { - ReconnectWithTimeoutAndAttempts: { - timeout: number; - attempts: number; - }; - }; -}; + reconnection_config: ReconnectionConfig; + /** + * Timestamp of the interface close event + * This is used to determine if the interface shall be reopened + */ + close_timestamp: number | null; + /** + * Number of reconnection attempts already made + * This is used to determine if the interface shall be reopened + * and if the interface shall be destroyed + */ + reconnect_attempts: number | null; +} + +export type InterfaceDirection = "In" | "Out" | "InOut"; + +export interface WebSocketServerInterfaceSetupData { + port: number; + /** + * if true, the server will use wss (secure WebSocket). Defaults to true. + */ + secure: boolean | null; +} -type WebSocketClientInterfaceSetupData = { +export interface WebSocketClientInterfaceSetupData { address: string; -}; +} + +export interface RTCIceServer { + urls: string[]; + username: string | null; + credential: string | null; +} + +export interface TCPClientInterfaceSetupData { + address: string; +} + +export interface TCPServerInterfaceSetupData { + port: number; +} + +export interface SerialInterfaceSetupData { + port_name: string | null; + baud_rate: number; +} export class BaseJSInterface { private constructor(); @@ -56,14 +130,12 @@ export class BaseJSInterface { export class JSComHub { private constructor(); free(): void; - websocket_server_interface_add_socket( - interface_uuid: string, - websocket: WebSocket, - ): string; + get_metadata_string(): string; register_default_interface_factories(): void; - create_interface(interface_type: string, properties: string): Promise; close_interface(interface_uuid: string): Promise; + create_interface(interface_type: string, properties: string): Promise; update(): Promise; + get_trace_string(endpoint: string): Promise; /** * Send a block to the given interface and socket * This does not involve the routing on the ComHub level. @@ -76,27 +148,41 @@ export class JSComHub { socket_uuid: string, ): Promise; _drain_incoming_blocks(): Uint8Array[]; - get_metadata_string(): string; - get_trace_string(endpoint: string): Promise; - base_interface_register_socket(uuid: string, direction: string): string; + websocket_server_interface_add_socket( + interface_uuid: string, + websocket: WebSocket, + ): string; + base_interface_test_send_block( + uuid: string, + socket_uuid: string, + data: Uint8Array, + ): Promise; base_interface_receive( uuid: string, socket_uuid: string, data: Uint8Array, ): void; + base_interface_register_socket(uuid: string, direction: string): string; base_interface_destroy_socket(uuid: string, socket_uuid: string): void; base_interface_on_send(uuid: string, func: Function): void; - base_interface_test_send_block( - uuid: string, - socket_uuid: string, - data: Uint8Array, - ): Promise; -} -export class JSMemory { - private constructor(); - free(): void; - get_pointer_by_id(address: Uint8Array): JSPointer | undefined; - get_pointer_ids(): Uint8Array[]; + webrtc_interface_create_answer( + interface_uuid: string, + offer: Uint8Array, + ): Promise; + webrtc_interface_add_ice_candidate( + interface_uuid: string, + candidate: Uint8Array, + ): Promise; + webrtc_interface_wait_for_connection(interface_uuid: string): Promise; + webrtc_interface_set_on_ice_candidate( + interface_uuid: string, + on_ice_candidate: Function, + ): void; + webrtc_interface_set_answer( + interface_uuid: string, + answer: Uint8Array, + ): Promise; + webrtc_interface_create_offer(interface_uuid: string): Promise; } export class JSPointer { private constructor(); @@ -105,40 +191,62 @@ export class JSPointer { export class JSRuntime { private constructor(); free(): void; + /** + * Get a handle to the DIF interface of the runtime + */ + dif(): RuntimeDIFHandle; + _stop(): Promise; crypto_test_tmp(): Promise>; _create_block( body: Uint8Array | null | undefined, receivers: string[], ): Uint8Array; + value_to_string(dif_value: any, decompile_options: any): string; + execute(script: string, dif_values?: any[] | null): Promise; start(): Promise; - _stop(): Promise; - execute(script: string, formatted: boolean): Promise; - execute_sync(script: string, formatted: boolean): string; - com_hub: JSComHub; - memory: JSMemory; + execute_sync_with_string_result( + script: string, + dif_values: any[] | null | undefined, + decompile_options: any, + ): string; + execute_sync(script: string, dif_values?: any[] | null): any; + execute_with_string_result( + script: string, + dif_values: any[] | null | undefined, + decompile_options: any, + ): Promise; readonly version: string; + com_hub: JSComHub; readonly endpoint: string; } -export class WebRTCRegistry { +export class RuntimeDIFHandle { private constructor(); free(): void; - close(interface_uuid: string): Promise; - register(endpoint: string): Promise; - create_offer(interface_uuid: string): Promise; - create_answer( - interface_uuid: string, - offer: Uint8Array, - ): Promise; - set_answer(interface_uuid: string, answer: Uint8Array): Promise; - set_on_ice_candidate( - interface_uuid: string, - on_ice_candidate: Function, + /** + * Resolve a pointer address synchronously if it's in memory, otherwise return an error + */ + resolve_pointer_address_sync(address: string): any; + unobserve_pointer(address: string, observer_id: number): void; + apply(callee: any, value: any): any; + update_observer_options( + address: string, + observer_id: number, + observe_options: any, ): void; - add_ice_candidate( - interface_uuid: string, - candidate: Uint8Array, - ): Promise; - wait_for_connection(interface_uuid: string): Promise; + create_pointer(value: any, allowed_type: any, mutability: number): string; + /** + * Resolve a pointer address, returning a Promise + * If the pointer is in memory, the promise resolves immediately + * If the pointer is not in memory, it will be loaded first + */ + resolve_pointer_address(address: string): any; + observe_pointer( + transceiver_id: number, + address: string, + observe_options: any, + callback: Function, + ): number; + update(transceiver_id: number, address: string, update: any): void; } export class WebSocketServerRegistry { private constructor(); diff --git a/src/dif/builders.ts b/src/dif/builders.ts new file mode 100644 index 00000000..76afff9f --- /dev/null +++ b/src/dif/builders.ts @@ -0,0 +1,33 @@ +import { type DIFUpdateData, DIFUpdateKind } from "./definitions.ts"; +import type { DIFHandler } from "./dif-handler.ts"; + +/** + * Creates a DIFUpdate object that describes replacing a pointer's value. + */ +export function DIF_Replace( + difHandler: DIFHandler, + value: T, +): DIFUpdateData { + const difValue = difHandler.convertJSValueToDIFValue(value); + return { + kind: DIFUpdateKind.Replace, + value: difValue, + }; +} + +/** + * Creates a DIFUpdate object that describes updating a property of a pointer's value. + */ +export function DIF_UpdateProperty( + difHandler: DIFHandler, + property: K, + value: V, +): DIFUpdateData { + const difKey = difHandler.convertJSValueToDIFValue(property); + const difValue = difHandler.convertJSValueToDIFValue(value); + return { + kind: DIFUpdateKind.Set, + key: { kind: "value", value: difKey }, + value: difValue, + }; +} diff --git a/src/dif/definitions.ts b/src/dif/definitions.ts new file mode 100644 index 00000000..b54bc7cc --- /dev/null +++ b/src/dif/definitions.ts @@ -0,0 +1,153 @@ +export const CoreTypeAddress = { + null: "010000", + type: "020000", + boolean: "030000", + endpoint: "070000", + text: "080000", + list: "090000", + unit: "0b0000", + map: "0c0000", + decimal: "2c0100", + decimal_f32: "2d0100", + decimal_f64: "2e0100", + decimal_big: "2f0100", + integer: "640000", + integer_u8: "650000", + integer_u16: "660000", + integer_u32: "670000", + integer_u64: "680000", + integer_u128: "690000", + integer_i8: "6a0000", + integer_i16: "6b0000", + integer_i32: "6c0000", + integer_i64: "6d0000", + integer_i128: "6e0000", + integer_big: "6f0000", +} as const; +export type CoreTypeAddress = + typeof CoreTypeAddress[keyof typeof CoreTypeAddress]; + +export const CoreTypeAddressRanges = { + small_unsigned_integers: new Set([ + CoreTypeAddress.integer_u8, + CoreTypeAddress.integer_u16, + CoreTypeAddress.integer_u32, + CoreTypeAddress.integer_u64, + CoreTypeAddress.integer_u128, + ]), + big_unsigned_integers: new Set([CoreTypeAddress.integer_big]), + small_signed_integers: new Set([ + CoreTypeAddress.integer_i8, + CoreTypeAddress.integer_i16, + CoreTypeAddress.integer_i32, + CoreTypeAddress.integer_i64, + CoreTypeAddress.integer_i128, + ]), + big_signed_integers: new Set([CoreTypeAddress.integer_big]), + decimals: new Set([ + CoreTypeAddress.decimal, + CoreTypeAddress.decimal_f32, + CoreTypeAddress.decimal_f64, + CoreTypeAddress.decimal_big, + ]), +} as const; + +/** 3, 5, or 26 byte hex string */ +export type DIFPointerAddress = string; +export type DIFValue = { + type?: DIFTypeContainer; + value: DIFRepresentationValue; +}; +export type DIFContainer = DIFValue | DIFPointerAddress; + +export const DIFTypeKinds = { + Structural: 0, + Reference: 1, + Intersection: 2, + Union: 3, + Unit: 4, + Function: 5, +} as const; +export type DIFTypeKind = typeof DIFTypeKinds[keyof typeof DIFTypeKinds]; + +export const DIFReferenceMutability = { + Mutable: 0, + Immutable: 1, + Final: 2, +} as const; +export type DIFReferenceMutability = + typeof DIFReferenceMutability[keyof typeof DIFReferenceMutability]; + +export type DIFTypeDefinition = + Kind extends typeof DIFTypeKinds.Structural ? DIFValue + : Kind extends typeof DIFTypeKinds.Reference ? DIFPointerAddress + : Kind extends typeof DIFTypeKinds.Intersection + ? Array + : Kind extends typeof DIFTypeKinds.Union ? Array + : Kind extends typeof DIFTypeKinds.Unit ? null + : Kind extends typeof DIFTypeKinds.Function ? unknown // TODO + : never; + +export type DIFType = { + name?: string; + kind: Kind; + def: DIFTypeDefinition; + mut?: DIFReferenceMutability; +}; + +export type DIFReference = { + value: DIFValueContainer; + allowed_type: DIFTypeContainer; + mut: DIFReferenceMutability; +}; + +export type DIFValueContainer = DIFValue | DIFPointerAddress; +export type DIFTypeContainer = DIFType | DIFPointerAddress; + +export type DIFObject = Record; +export type DIFArray = DIFValueContainer[]; +export type DIFMap = [DIFValueContainer, DIFValueContainer][]; + +export type DIFRepresentationValue = + | string + | number + | boolean + | null + | DIFObject + | DIFMap + | DIFArray; + +// DIFProperty +export type DIFProperty = + | { kind: "text"; value: string } + | { kind: "index"; value: number } // FIXME shall we optimize this? as number of wrap pointer address in obj and use plain dif value container without nesting + | { kind: "value"; value: DIFValueContainer }; + +export const DIFUpdateKind = { + Replace: "replace", + Push: "push", + Set: "set", + Remove: "remove", + Clear: "clear", +} as const; +export type DIFUpdateKind = typeof DIFUpdateKind[keyof typeof DIFUpdateKind]; + +export type DIFUpdateData = + | { kind: typeof DIFUpdateKind.Replace; value: DIFValueContainer } + | { kind: typeof DIFUpdateKind.Push; value: DIFValueContainer } + | { kind: typeof DIFUpdateKind.Remove; key: DIFProperty } + | { + kind: typeof DIFUpdateKind.Set; + key: DIFProperty; + value: DIFValueContainer; + } + | { kind: typeof DIFUpdateKind.Clear }; + +export type DIFUpdate = { + source_id: number; + data: DIFUpdateData; +}; + +export type ObserveOptions = { + relay_own_updates: boolean; +}; diff --git a/src/dif/dif-handler.ts b/src/dif/dif-handler.ts new file mode 100644 index 00000000..35492921 --- /dev/null +++ b/src/dif/dif-handler.ts @@ -0,0 +1,912 @@ +import type { JSRuntime, RuntimeDIFHandle } from "../datex-core.ts"; +import { Ref } from "../refs/ref.ts"; +import { Endpoint } from "../runtime/special-core-types.ts"; +import { + CoreTypeAddress, + CoreTypeAddressRanges, + type DIFArray, + type DIFContainer, + type DIFMap, + type DIFObject, + type DIFPointerAddress, + type DIFProperty, + type DIFReference, + DIFReferenceMutability, + type DIFTypeContainer, + type DIFUpdate, + type DIFUpdateData, + DIFUpdateKind, + type DIFValue, + type DIFValueContainer, + type ObserveOptions, +} from "./definitions.ts"; +import { difValueContainerToDisplayString } from "./display.ts"; + +export class DIFHandler { + /** The JSRuntime interface for the underlying Datex Core runtime */ + #runtime: JSRuntime; + #handle: RuntimeDIFHandle; + + // always 0 for now - potentially used for multi DIF transceivers using the same underlying runtime + readonly #transceiver_id = 0; + + /** The pointer cache for storing and reusing object instances on the JS side + * The observerId is only set if the pointer is being observed (if not final). + */ + readonly #cache = new Map< + string, + { val: WeakRef; observerId: number | null } + >(); + + readonly #observers = new Map< + string, + Map void> + >(); + + get _observers(): Map void>> { + return this.#observers; + } + + get _handle(): RuntimeDIFHandle { + return this.#handle; + } + + get _transceiver_id(): number { + return this.#transceiver_id; + } + + /** + * Creates a new DIFHandler instance. + * @param runtime - The JSRuntime instance for executing Datex scripts. + * @param pointerCache - The PointerCache instance for managing object pointers. If not provided, a new PointerCache will be created. + */ + constructor( + runtime: JSRuntime, + ) { + this.#runtime = runtime; + this.#handle = runtime.dif(); + } + + /** + * Executes a Datex script asynchronously and returns a Promise that resolves to a DIFContainer. + * @param datexScript - The Datex script source code to execute. + * @param values - An optional array of values to inject into the script. + * @returns A Promise that resolves to the execution result as a DIFContainer. + * @throws If an error occurs during execution. + */ + public executeDIF( + datexScript: string, + values: unknown[] | null = [], + ): Promise { + return this.#runtime.execute( + datexScript, + this.convertToDIFValues(values), + ); + } + + /** + * Executes a Datex script synchronously and returns the result as a DIFContainer. + * @param datexScript - The Datex script source code to execute. + * @param values - An optional array of values to inject into the script. + * @returns The execution result as a DIFContainer. + * @throws If an error occurs during execution. + */ + public executeSyncDIF( + datexScript: string, + values: unknown[] | null = [], + ): DIFContainer { + return this.#runtime.execute_sync( + datexScript, + this.convertToDIFValues(values), + ); + } + + /** + * Creates a new pointer for the specified value. + * @param difValue - The DIFValue value to create a pointer for. + * @param allowedType - The allowed type for the pointer. + * @param mutability - The mutability of the pointer. + * @returns The created pointer address. + */ + public createPointer( + difValue: DIFValue, + allowedType: DIFTypeContainer | null = null, + mutability: DIFReferenceMutability, + ): string { + return this.#handle.create_pointer( + difValue, + allowedType, + mutability, + ); + } + + /** + * Creates a new pointer that points to an existing address. + * @param address - The address to create a reference pointer for. + * @param allowedType - The allowed type for the pointer. + * @param mutability - The mutability of the pointer. + * @returns A Promise that resolves to the created pointer address. + */ + public createRefPointer( + address: string, + allowedType: DIFTypeContainer | null = null, + mutability: DIFReferenceMutability, + ): string { + return this.#handle.create_pointer( + address, + allowedType, + mutability, + ); + } + + /** + * Updates the DIF value at the specified address. + * @param address - The address of the DIF value to update. + * @param dif - The DIFUpdate object containing the update information. + */ + public updatePointer(address: string, dif: DIFUpdateData) { + this.#handle.update(this.#transceiver_id, address, dif); + } + + /** + * Registers an observer callback for changes to the DIF value at the specified address + * directly on the DATEX core runtime. + * This method should only be used internally, since it comes with additional overhead. + * For normal use cases, use the observePointer method instead. + * The callback will be invoked whenever the value at the address is updated. + * @param address - The address of the DIF value to observe. + * @param callback - The callback function to invoke on updates. + * @returns An observer ID that can be used to unregister the observer. + * @throws If the pointer is final. + */ + public observePointerBindDirect( + address: string, + callback: (value: DIFUpdate) => void, + options: ObserveOptions = { relay_own_updates: false }, + ): number { + return this.#runtime.dif().observe_pointer( + this.#transceiver_id, + address, + options, + callback, + ); + } + + /** + * Updates the observe options for a registered observer. + * @param address - The address of the DIF value being observed. + * @param observerId - The observer ID returned by the observePointer method. + * @param options - The new observe options to apply. + */ + private updateObserverOptions( + address: string, + observerId: number, + options: ObserveOptions, + ) { + this.#runtime.dif().update_observer_options( + address, + observerId, + options, + ); + } + + /** + * Enables propagation of own updates for a registered observer. + * @param address - The address of the DIF value being observed. + * @param observerId - The observer ID returned by the observePointer method. + */ + public enableOwnUpdatesPropagation( + address: string, + observerId: number, + ) { + this.updateObserverOptions(address, observerId, { + relay_own_updates: true, + }); + } + + /** + * Disables propagation of own updates for a registered observer. + * @param address - The address of the DIF value being observed. + * @param observerId - The observer ID returned by the observePointer method. + */ + public disableOwnUpdatesPropagation( + address: string, + observerId: number, + ) { + this.updateObserverOptions(address, observerId, { + relay_own_updates: false, + }); + } + + /** + * Unregisters an observer that was registered directly on the DATEX core runtime + * with the observePointerBindDirect method. + * For internal use only. + * @param address - The address of the DIF value being observed. + * @param observerId - The observer ID returned by the observePointer method. + */ + public unobservePointerBindDirect(address: string, observerId: number) { + this.#runtime.dif().unobserve_pointer(address, observerId); + } + + /** + * Registers a local observer callback for changes to the DIF value at the specified address. + * The callback will be invoked whenever the value at the address is updated. + * In contrast to observePointerBindDirect, this method does not register the observer + * directly on the DATEX core runtime, but keeps it local in the JS side, which prevents + * unnecessary overhead from additional cross-language calls. + * @param address - The address of the DIF value to observe. + * @param callback - The callback function to invoke on updates. + * @returns An observer ID that can be used to unregister the observer. + * @throws If the pointer is final. + */ + public observePointer( + address: string, + callback: (value: DIFUpdateData) => void, + ): number { + let cached = this.#cache.get(address); + if (!cached) { + // first resolve the pointer to make sure it's loaded in the cache + this.resolvePointerAddressSync(address); + cached = this.#cache.get(address)!; + } + + // make sure the pointer is not final (no observer) + if (cached.observerId === null) { + throw new Error(`Cannot observe final reference $${address}`); + } + + // directly add to observers map + let observers = this.#observers.get(address); + if (!observers) { + observers = new Map(); + this.#observers.set(address, observers); + // first local observer for this address - enable own updates propagation + this.enableOwnUpdatesPropagation(address, cached.observerId); + } + // FIXME make this more robust for delete/re-add cases + const observerId = observers.size + 1; + observers.set(observerId, callback); + return observerId; + } + + /** + * Unregisters an observer that was registered with the observePointer method. + * @param address - The address of the DIF value being observed. + * @param observerId - The observer ID returned by the observePointer method. + * @returns True if the observer was successfully unregistered, false otherwise. + */ + public unobservePointer(address: string, observerId: number): boolean { + const observers = this.#observers.get(address); + if (observers) { + observers.delete(observerId); + if (observers.size === 0) { + // no local observers left - disable own updates propagation and remove from map + const cached = this.#cache.get(address); + if (cached?.observerId) { + this.disableOwnUpdatesPropagation( + address, + cached.observerId, + ); + } else { + console.error(`No observer found for address ${address}`); + } + return this.#observers.delete(address); + } + } + return false; + } + + /** + * Resolves a DIFValue to its corresponding JS value. + * This function handles core types and custom types (not yet implemented). + * It returns the resolved value as the specified type T. + * @param value + */ + public resolveDIFValue( + value: DIFValue, + ): T | Promise { + console.log("RESOLVE", value); + let type = value.type; + if (type === undefined) { + if (Array.isArray(value.value)) { + if (Array.isArray(value.value[0])) { + type = CoreTypeAddress.map; + } else { + type = CoreTypeAddress.list; + } + } else { + return value.value as T; + } + } + console.log(type, value); + + // null, boolean and text types values are just returned as is + if ( + type === CoreTypeAddress.boolean || + type == CoreTypeAddress.text || + type === CoreTypeAddress.null + ) { + return value.value as T; + } // small integers are interpreted as JS numbers + else if ( + typeof type === "string" && ( + type == CoreTypeAddress.integer || + this.isPointerAddressInAdresses( + type, + CoreTypeAddressRanges.small_signed_integers, + ) || + this.isPointerAddressInAdresses( + type, + CoreTypeAddressRanges.small_unsigned_integers, + ) + ) + ) { + return Number(value.value as number) as T; + } // big integers are interpreted as JS BigInt + else if ( + typeof type === "string" && ( + this.isPointerAddressInAdresses( + type, + CoreTypeAddressRanges.big_signed_integers, + ) || + this.isPointerAddressInAdresses( + type, + CoreTypeAddressRanges.big_unsigned_integers, + ) + ) + ) { + return BigInt(value.value as number) as T; + } // decimal types are interpreted as JS numbers + else if ( + typeof type === "string" && + this.isPointerAddressInAdresses( + type, + CoreTypeAddressRanges.decimals, + ) + ) { + return (Number(value.value) as number) as T; + } // endpoint types are resolved to Endpoint instances + else if (type === CoreTypeAddress.endpoint) { + return Endpoint.get(value.value as string) as T; + } else if (type === CoreTypeAddress.list) { + return this.promiseAllOrSync( + (value.value as DIFArray).map((v) => + this.resolveDIFValueContainer(v) + ), + ) as T | Promise; + } // map types are resolved from a DIFObject (aka JS Map) or Array of key-value pairs to a JS object + else if (type === CoreTypeAddress.map) { + if (Array.isArray(value.value)) { + const resolvedMap = new Map(); + for (const [key, val] of (value.value as DIFMap)) { + // TODO: currently always converting to an object here, but this should be a Map per default + resolvedMap.set( + this.resolveDIFValueContainer(key), + this.resolveDIFValueContainer(val), + ); + } + // TODO: map promises + return resolvedMap as unknown as T | Promise; + } else { + const resolvedObj: { [key: string]: unknown } = {}; + for ( + const [key, val] of Object.entries(value.value as DIFObject) + ) { + // TODO: currently always converting to an object here, but this should be a Map per default + resolvedObj[key as string] = this.resolveDIFValueContainer( + val, + ); + } + return this.promiseFromObjectOrSync(resolvedObj) as + | T + | Promise; + } + } else { + // custom types not implemented yet + throw new Error("Custom type resolution not implemented yet"); + } + } + + /** + * Converts an array of Promises or resolved values to either a Promise of an array of resolved values, + * or an array of resolved values if all values are already resolved. + */ + promiseAllOrSync(values: (T | Promise)[]): Promise | T[] { + if (values.some((v) => v instanceof Promise)) { + return Promise.all(values); + } else { + return values as T[]; + } + } + + /** + * Converts an object with values that may be Promises to either a Promise of an object with resolved values, + * or an object with resolved values if all values are already resolved. + */ + public promiseFromObjectOrSync( + values: { [key: string]: T | Promise }, + ): Promise<{ [key: string]: T }> | { [key: string]: T } { + const valueArray = Object.values(values); + if (valueArray.some((v) => v instanceof Promise)) { + return Promise.all(valueArray).then((resolvedValues) => { + const resolvedObj: { [key: string]: T } = {}; + let i = 0; + for (const key of Object.keys(values)) { + resolvedObj[key] = resolvedValues[i++]; + } + return resolvedObj; + }); + } else { + return values as { [key: string]: T }; + } + } + + /** + * Maps a value or Promise of a value to another value or Promise of a value using the provided onfulfilled function. + */ + public mapPromise( + value: T | Promise, + onfulfilled: (value: T) => N, + ): N | Promise { + if (value instanceof Promise) { + return value.then(onfulfilled); + } else { + return onfulfilled(value); + } + } + + /** + * Resolves a DIFValueContainer (either a DIFValue or a pointer address) to its corresponding JS value. + * If the container contains pointers that are not yet loaded in memory, it returns a Promise that resolves to the value. + * Otherwise, it returns the resolved value directly. + * @param value - The DIFValueContainer to resolve. + * @returns The resolved value as type T, or a Promise that resolves to type T. + */ + public resolveDIFValueContainer( + value: DIFValueContainer, + ): T | Promise { + if (typeof value !== "string") { + return this.resolveDIFValue(value); + } else { + return this.resolvePointerAddress(value); + } + } + + /** + * Synchronous version of resolveDIFValueContainer. + * This method can only be used if the value only contains pointer addresses that are already loaded in memory - + * otherwise, use the asynchronous `resolveDIFValueContainer` method instead. + * @param value - The DIFValueContainer to resolve. + * @returns The resolved value as type T. + * @throws If the resolution would require asynchronous operations. + */ + public resolveDIFValueContainerSync( + value: DIFValueContainer, + ): T { + const result = this.resolveDIFValueContainer(value); + if (result instanceof Promise) { + throw new Error( + "resolveDIFValueContainerSync cannot return a Promise. Use resolveDIFValueContainer() instead.", + ); + } + return result as T; + } + + /** + * Resolves a pointer address to its corresponding JS value. + * If the pointer address is not yet loaded in memory, it returns a Promise that resolves to the value. + * Otherwise, it returns the resolved value directly. + * @param address - The pointer address to resolve. + * @returns The resolved value as type T, or a Promise that resolves to type T. + */ + public resolvePointerAddress( + address: string, + ): Promise | T { + // check cache first + const cached = this.getCachedPointer(address); + if (cached) { + return cached as T; + } + // if not in cache, resolve from runtime + const reference: DIFReference | Promise = this + .#handle.resolve_pointer_address(address); + return this.mapPromise(reference, (reference) => { + const value: T | Promise = this.resolveDIFValueContainer( + reference.value, + ); + return this.mapPromise(value, (v) => { + // init pointer + this.initPointer( + address, + v, + reference.mut, + reference.allowed_type, + ); + return v; + }); + }) as Promise | T; + } + + /** + * Resolves a pointer address to its corresponding JS value synchronously. + * If the pointer address is not yet loaded in memory, it returns a Promise that resolves to the value. + * Otherwise, it returns the resolved value directly. + * @param address - The pointer address to resolve. + * @returns The resolved value as type T, or a Promise that resolves to type T. + * @throws If the resolution would require asynchronous operations. + */ + public resolvePointerAddressSync( + address: string, + ): T { + // check cache first + const cached = this.getCachedPointer(address); + if (cached) { + return cached as T; + } + // if not in cache, resolve from runtime + const entry: DIFReference = this.#handle + .resolve_pointer_address_sync(address); + const value: T = this.resolveDIFValueContainerSync( + entry.value, + ); + this.initPointer(address, value, entry.mut, entry.allowed_type); + return value; + } + + /** + * Converts an array of JS values to an array of DIFValues. + * If the input is null, it returns null. + * @param values + */ + public convertToDIFValues( + values: T | null, + ): DIFValue[] | null { + return values?.map((value) => this.convertJSValueToDIFValue(value)) || + null; + } + + /** + * Returns true if the given address is within the specified address range. + */ + protected isPointerAddressInAdresses( + address: DIFPointerAddress, + range: Set, + ): boolean { + return range.has(address); + } + + /** + * Initializes a pointer with the given value and mutability, by + * adding a proxy wrapper if necessary, and setting up observation and caching on the JS side. + */ + protected initPointer( + ptrAddress: string, + value: T, + mutability: DIFReferenceMutability, + allowedType: DIFTypeContainer | null = null, + ): T | Ref { + const refValue = this.wrapJSValueInProxy( + value, + ptrAddress, + allowedType, + ); + + // if not final, observe to keep the pointer 'live' and receive updates + let observerId: number | null = null; + if (mutability !== DIFReferenceMutability.Final) { + observerId = this.observePointerBindDirect( + ptrAddress, + (update) => { + // if source_id is not own transceiver id, handle pointer update + if (update.source_id !== this.#transceiver_id) { + this.handlePointerUpdate(ptrAddress, update.data); + } + // call all local observers + const observers = this.#observers.get(ptrAddress); + if (observers) { + for (const cb of observers.values()) { + try { + cb(update.data); + } catch (e) { + console.error( + "Error in pointer observer callback", + e, + ); + } + } + } + console.debug("Pointer update received", update); + }, + ); + } + + this.cacheWrappedPointerValue(ptrAddress, refValue, observerId); + return refValue as T | Ref; + } + + /** + * Handles a pointer update received from the DATEX core runtime. + * If the pointer is cached and has a dereferenceable value, it updates the value. + * @param address - The address of the pointer being updated. + * @param update - The DIFUpdateData containing the update information. + * @returns True if the pointer was found and updated, false otherwise. + */ + protected handlePointerUpdate( + address: string, + update: DIFUpdateData, + ): boolean { + const cached = this.#cache.get(address); + if (!cached) return false; + const deref = cached.val.deref(); + if (!deref) return false; + + if (deref instanceof Ref && update.kind === DIFUpdateKind.Replace) { + deref.updateValueSilently(this.resolveDIFValueContainerSync( + update.value, + )); + } + // TODO: handle generic updates for values (depending on type interface definition) + + return true; + } + + /** + * Caches the given pointer value with the given address in the JS side cache. + * The pointer must already be wrapped if necessary. + */ + protected cacheWrappedPointerValue( + address: string, + value: WeakKey, + observerId: number | null, + ): void { + this.#cache.set(address, { + val: new WeakRef(value), + observerId, + }); + // register finalizer to clean up the cache and free the pointer in the runtime + // when the object is garbage collected + const finalizationRegistry = new FinalizationRegistry( + (address: string) => { + this.#cache.delete(address); + // remove local observers + this.#observers.delete(address); + // if observer is active, unregister it + if (observerId !== null) { + this.unobservePointerBindDirect(address, observerId); + } + }, + ); + finalizationRegistry.register(value, address); + } + + protected getCachedPointer(address: string): WeakKey | undefined { + const cached = this.#cache.get(address); + if (cached) { + const deref = cached.val.deref(); + if (deref) { + return deref; + } + } + return undefined; + } + + /** + * Creates a new pointer containg the given JS value. + * The returned value is a proxy object that behaves like the original object, + * but also propagates changes between JS and the DATEX runtime. + */ + public createPointerFromJSValue( + value: unknown, + allowedType: DIFTypeContainer | null = null, + mutability: DIFReferenceMutability = DIFReferenceMutability.Mutable, + ): unknown | Ref { + const difValue = this.convertJSValueToDIFValue(value); + console.log("DIF", difValueContainerToDisplayString(difValue)); + const ptrAddress = this.createPointer( + difValue, + allowedType, + mutability, + ); + // get inferred allowed type from pointer if not explicitly set + if (!allowedType) { + allowedType = (this.#handle.resolve_pointer_address_sync( + ptrAddress, + ) as DIFReference).allowed_type; + } + return this.initPointer(ptrAddress, value, mutability, allowedType); // TODO: map to correct pointer wrapper type + } + + protected wrapJSValueInProxy( + value: T, + pointerAddress: string, + _type: DIFTypeContainer | null = null, + ): (T | Ref) & WeakKey { + // primitive values are always wrapped in a Ref proxy + if ( + value === null || value === undefined || + typeof value === "boolean" || + typeof value === "number" || typeof value === "bigint" || + typeof value === "string" + ) { + return new Ref(value, pointerAddress, this); + } else if (value instanceof Map) { + return this.proxifyJSMap(value, pointerAddress); + } else if (typeof value === "object") { + return this.wrapJSObjectInProxy(value); + } else { + return value; + } + } + + private isRef(value: unknown): value is Ref { + return value instanceof Ref; + } + + private proxifyJSMap, K, V>( + map: T, + pointerAddress: string, + ): T { + const originalSet = map.set; + const originalDelete = map.delete; + const originalClear = map.clear; + // deno-lint-ignore no-this-alias + const self = this; + Object.defineProperties(map, { + set: { + value: function (key: K, value: V) { + self.updatePointer(pointerAddress, { + kind: DIFUpdateKind.Set, + key: { + kind: "value", + value: self.convertJSValueToDIFValue(key), + } as DIFProperty, + value: self.convertJSValueToDIFValue(value), + }); + return originalSet.call(this, key, value); + }, + configurable: true, + writable: true, + }, + delete: { + value: function (key: K) { + self.updatePointer(pointerAddress, { + kind: DIFUpdateKind.Remove, + key: { + kind: "value", + value: self.convertJSValueToDIFValue(key), + } as DIFProperty, + }); + const result = originalDelete.call(this, key); + return result; + }, + configurable: true, + writable: true, + }, + clear: { + value: function () { + self.updatePointer(pointerAddress, { + kind: DIFUpdateKind.Clear, + }); + const result = originalClear.call(this); + return result; + }, + configurable: true, + writable: true, + }, + }); + + return map; + } + + private wrapJSObjectInProxy( + value: T, + ): (T | Ref) & WeakKey { + // deno-lint-ignore no-this-alias + const self = this; + return new Proxy(value, { + get(target, prop, receiver) { + const val = Reflect.get(target, prop, receiver); + if (val && typeof val === "object" && !self.isRef(val)) { + return self.wrapJSObjectInProxy(val); + } + return val; + }, + set(target, prop, newValue, receiver) { + const oldValue = Reflect.get(target, prop, receiver); + if (!self.isRef(oldValue)) { + throw new Error( + `Cannot modify non-Ref property "${String(prop)}"`, + ); + } + oldValue.value = newValue; + return true; + }, + deleteProperty() { + throw new Error( + "Cannot delete properties from a Refs-only object", + ); + }, + defineProperty() { + throw new Error( + "Cannot define new properties on a Refs-only object", + ); + }, + }); + } + + /// Returns the pointer address for the given value if it is already cached, or null otherwise. + /// TODO: optimize by using a reverse map or direct Symbols on the objects + /// But this is probably not important for normal use cases + public getPointerAddressForValue(value: T): string | null { + for (const [address, entry] of this.#cache) { + const deref = entry.val.deref(); + if (deref === value) { + return address; + } + } + return null; + } + + /** + * Converts a given JS value to its DIFValue representation. + */ + public convertJSValueToDIFValue( + value: T, + ): DIFValue { + // assuming core values + // TODO: handle custom types + if (value === null) { + return { + value: null, + }; + } else if (typeof value === "boolean") { + return { + value, + }; + } else if (typeof value === "number") { + return { + value, + }; + } else if (typeof value === "bigint") { + return { + type: CoreTypeAddress.integer_big, + value: value.toString(), // convert bigint to string for DIFValue + }; + } else if (typeof value === "string") { + return { + value, + }; + } else if (value instanceof Endpoint) { + return { + type: CoreTypeAddress.endpoint, + value: value.toString(), + }; + } else if (Array.isArray(value)) { + return { + value: value.map((v) => this.convertJSValueToDIFValue(v)), + }; + } else if (value instanceof Map) { + const map: [DIFValue, DIFValue][] = value.entries().map(( + [k, v], + ) => [ + this.convertJSValueToDIFValue(k), + this.convertJSValueToDIFValue(v), + ] as [DIFValue, DIFValue]).toArray(); + return { + type: CoreTypeAddress.map, + value: map, + }; + } else if (typeof value === "object") { + const map: Record = {}; + for (const [key, val] of Object.entries(value)) { + map[key] = this.convertJSValueToDIFValue(val); + } + return { + type: CoreTypeAddress.map, + value: map, + }; + } + throw new Error("Unsupported type for conversion to DIFValue"); + } +} diff --git a/src/dif/display.ts b/src/dif/display.ts new file mode 100644 index 00000000..77cd14e6 --- /dev/null +++ b/src/dif/display.ts @@ -0,0 +1,100 @@ +import { + CoreTypeAddress, + type DIFReference, + DIFReferenceMutability, + type DIFRepresentationValue, + type DIFTypeContainer, + type DIFValueContainer, +} from "./definitions.ts"; + +export function mutabilityToDisplayString(mut: DIFReferenceMutability): string { + if (mut === DIFReferenceMutability.Mutable) { + return "&mut "; + } else if (mut === DIFReferenceMutability.Final) { + return "&final "; + } else if (mut === DIFReferenceMutability.Immutable) { + return "&"; + } + throw new Error("Unknown mutability: " + mut); +} + +export function difReferenceToDisplayString( + reference: DIFReference, +): string { + const typeString = difTypeContainerToDisplayString(reference.allowed_type); + const valueString = difValueContainerToDisplayString(reference.value); + const mutString = mutabilityToDisplayString(reference.mut); + return `${mutString}${valueString} (allowed: ${typeString})`; +} + +export function difValueContainerToDisplayString( + container: DIFValueContainer, +): string { + if (typeof container === "string") { + return addressToDisplayString(container); + } else { + const typeString = container.type + ? difTypeContainerToDisplayString(container.type) + : null; + const valueString = difRepresentationValueToDisplayString( + container.value, + ); + if (typeString) { + return `{ type: ${typeString}, value: ${valueString} }`; + } else { + return valueString; + } + } +} + +export function difRepresentationValueToDisplayString( + difRepValue: DIFRepresentationValue, +): string { + if (Array.isArray(difRepValue)) { + return `[${ + difRepValue.map((v) => { + if (Array.isArray(v)) { + return `[${ + v.map((vv) => difValueContainerToDisplayString(vv)) + .join(", ") + }]`; + } else { + return difValueContainerToDisplayString(v); + } + }).join( + ", ", + ) + }]`; + } else if (difRepValue && typeof difRepValue === "object") { + return `{ ${ + Object.entries(difRepValue).map(([k, v]) => + `${k}: ${difValueContainerToDisplayString(v)}` + ).join(", ") + } }`; + } else { + return JSON.stringify(difRepValue); + } +} + +export function difTypeContainerToDisplayString( + difType: DIFTypeContainer, +): string { + if (typeof difType === "string") { + return addressToDisplayString(difType); + } else { + return `{ name: ${difType.name}, kind: ${difType.kind}, def: ${ + JSON.stringify(difType.def) + }, mut: ${difType.mut} }`; + } +} + +export function addressToDisplayString(address: string): string { + const found = Object.entries(CoreTypeAddress).find(([_, addr]) => { + return addr === address; + }); + if (found) { + return found[0]; + } else { + return "$" + address; + } +} diff --git a/src/mod.ts b/src/mod.ts index 1fbb1bf0..5f14db21 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -24,11 +24,11 @@ export const Datex: Runtime = await Runtime.create(defaultConfig, { allow_unsigned_blocks: true, }); -try { - console.log( - "@example responded with:", - await Datex.execute("@example :: 'Hello, World!'"), - ); -} catch (error) { - console.error("Could not reach @example", error); -} +// try { +// console.log( +// "@example responded with:", +// await Datex.execute("@example :: 'Hello, World!'"), +// ); +// } catch (error) { +// console.error("Could not reach @example", error); +// } diff --git a/src/network/com-hub.ts b/src/network/com-hub.ts index 3b2138c9..f5c27cae 100644 --- a/src/network/com-hub.ts +++ b/src/network/com-hub.ts @@ -105,4 +105,12 @@ export class ComHub { } console.log(trace); } + + public sendBlock( + block: Uint8Array, + interface_uuid: string, + socket_uuid: string, + ): Promise { + return this.#jsComHub.send_block(block, interface_uuid, socket_uuid); + } } diff --git a/src/network/interface-impls/base.ts b/src/network/interface-impls/base.ts index f84ee3fc..f97d31e0 100644 --- a/src/network/interface-impls/base.ts +++ b/src/network/interface-impls/base.ts @@ -1,8 +1,9 @@ import { ComInterfaceImpl } from "../com-interface.ts"; import { ComHub } from "../com-hub.ts"; -import type { BaseInterfaceSetupData } from "../../datex-core/datex_core_js.d.ts"; - -type InterfaceDirection = "In" | "Out" | "InOut"; +import type { + BaseInterfaceSetupData, + InterfaceDirection, +} from "../../datex-core/datex_core_js.d.ts"; export class BaseInterfaceImpl extends ComInterfaceImpl { diff --git a/src/network/interface-impls/serial.ts b/src/network/interface-impls/serial.ts new file mode 100644 index 00000000..7984c460 --- /dev/null +++ b/src/network/interface-impls/serial.ts @@ -0,0 +1,9 @@ +import { ComInterfaceImpl } from "../com-interface.ts"; +import { ComHub } from "../com-hub.ts"; +import type { SerialInterfaceSetupData } from "../../datex-core.ts"; + +export class SerialInterfaceImpl + extends ComInterfaceImpl { +} + +ComHub.registerInterfaceImpl("serial", SerialInterfaceImpl); diff --git a/src/network/interface-impls/webrtc.ts b/src/network/interface-impls/webrtc.ts new file mode 100644 index 00000000..e7c6b8ec --- /dev/null +++ b/src/network/interface-impls/webrtc.ts @@ -0,0 +1,38 @@ +import { ComInterfaceImpl } from "../com-interface.ts"; +import { ComHub } from "../com-hub.ts"; +import type { WebRTCInterfaceSetupData } from "../../datex-core.ts"; + +export class WebRTCInterfaceImpl + extends ComInterfaceImpl { + public setOnIceCandidate( + onIceCandidate: (candidate: Uint8Array) => void, + ): void { + this.jsComHub.webrtc_interface_set_on_ice_candidate( + this.uuid, + onIceCandidate, + ); + } + public addIceCandidate(candidate: Uint8Array): Promise { + return this.jsComHub.webrtc_interface_add_ice_candidate( + this.uuid, + candidate, + ); + } + public createOffer(): Promise { + return this.jsComHub.webrtc_interface_create_offer(this.uuid); + } + public createAnswer(offer: Uint8Array): Promise { + return this.jsComHub.webrtc_interface_create_answer( + this.uuid, + offer, + ); + } + public setAnswer(answer: Uint8Array): Promise { + return this.jsComHub.webrtc_interface_set_answer(this.uuid, answer); + } + public waitForConnection(): Promise { + return this.jsComHub.webrtc_interface_wait_for_connection(this.uuid); + } +} + +ComHub.registerInterfaceImpl("webrtc", WebRTCInterfaceImpl); diff --git a/src/refs/ref.ts b/src/refs/ref.ts new file mode 100644 index 00000000..6e07ba3d --- /dev/null +++ b/src/refs/ref.ts @@ -0,0 +1,55 @@ +import { DIF_Replace } from "../dif/builders.ts"; +import type { DIFHandler } from "../dif/dif-handler.ts"; + +/** + * The Ref class is a wrapper around a value that is stored in a pointer. + * Primitive values (string, number, boolean, null) are always wrapped in a Ref when stored in a pointer. + */ +export class Ref { + #value: T; + #pointerAddress: string; + #difHandler: DIFHandler; + + constructor(value: T, pointerAddress: string, difHandler: DIFHandler) { + this.#value = value; + this.#pointerAddress = pointerAddress; + this.#difHandler = difHandler; + } + + get pointerAddress(): string { + return this.#pointerAddress; + } + + /** + * Silently updates the value of the reference without notifying observers. + * This should only be used internally. + * @param newValue - The new value to set. + */ + updateValueSilently(newValue: T) { + this.#value = newValue; + } + + /** + * Gets the current value of the reference. + */ + get value(): T { + return this.#value; + } + + /** + * Replaces the current value of the reference with a new value. + * Also notifies all observers of the pointer about the change. + * @throws If the reference is immutable or the new value is of an incompatible type. + */ + set value(newValue: T) { + const oldValue = this.#value; + if (oldValue === newValue) return; + + // Try to update the pointer + this.#difHandler.updatePointer( + this.#pointerAddress, + DIF_Replace(this.#difHandler, newValue), + ); + this.#value = newValue; + } +} diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 52f32d64..8d225184 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -1,13 +1,18 @@ import { create_runtime, execute_internal, - type JSMemory, type JSRuntime, } from "../datex-core.ts"; import { ComHub } from "../network/com-hub.ts"; +import { DIFHandler } from "../dif/dif-handler.ts"; +import type { + DIFReferenceMutability, + DIFTypeContainer, +} from "../dif/definitions.ts"; +import type { Ref } from "../refs/ref.ts"; // auto-generated version - do not edit: -const VERSION: string = "0.0.6"; +const VERSION: string = "0.0.7"; interface DebugFlags { allow_unsigned_blocks?: boolean; @@ -20,17 +25,24 @@ export type RuntimeConfig = { debug?: boolean; }; +export type DecompileOptions = { + formatted?: boolean; + colorized?: boolean; + resolve_slots?: boolean; + json_compat?: boolean; +}; + export class Runtime { public readonly js_version = VERSION; readonly #runtime: JSRuntime; - readonly #memory: JSMemory; readonly #comHub: ComHub; + readonly #difHandler: DIFHandler; constructor(config: RuntimeConfig, debug_flags?: DebugFlags) { this.#runtime = create_runtime(JSON.stringify(config), debug_flags); - this.#memory = this.#runtime.memory; this.#comHub = new ComHub(this.#runtime.com_hub); + this.#difHandler = new DIFHandler(this.#runtime); } public static async create( @@ -61,8 +73,8 @@ export class Runtime { return this.#runtime.version; } - get memory(): JSMemory { - return this.#memory; + get dif(): DIFHandler { + return this.#difHandler; } get comHub(): ComHub { @@ -76,21 +88,267 @@ export class Runtime { return this.#runtime; } - public execute( - datex_script: string, - formatted: boolean = false, + public executeWithStringResult( + datexScript: string, + values: unknown[] | null = [], + decompileOptions: DecompileOptions | null = null, ): Promise { - return this.#runtime.execute(datex_script, formatted); + return this.#runtime.execute_with_string_result( + datexScript, + this.#difHandler.convertToDIFValues(values), + decompileOptions, + ); + } + + public executeSyncWithStringResult( + datexScript: string, + values: unknown[] | null = [], + decompileOptions: DecompileOptions | null = null, + ): string { + return this.#runtime.execute_sync_with_string_result( + datexScript, + this.#difHandler.convertToDIFValues(values), + decompileOptions, + ); + } + + /** + * Asynchronously executes a Datex script and returns the result as a Promise. + * Injected values can be passed as an array in `values`. + * If the script returns no value, it will return `undefined`. + * Example usage: + * ```ts + * const result = await runtime.execute("1 + ?", [41]); + * console.log(result); // 42 + * ``` + */ + public execute( + datexScript: string, + values?: unknown[], + ): Promise; + + /** + * Asynchronously executes a Datex script and returns the result as a Promise. + * Injected values can be passed to the template string. + * Example usage: + * ```ts + * const result = await runtime.execute`1 + ${41}`; + * console.log(result); // 42 + * ``` + */ + public execute( + templateStrings: TemplateStringsArray, + ...values: unknown[] + ): Promise; + + public execute( + datexScriptOrTemplateStrings: string | TemplateStringsArray, + ...values: unknown[] + ): Promise { + const { datexScript, valuesArray } = this.#getScriptAndValues( + datexScriptOrTemplateStrings, + ...values, + ); + return this.#executeInternal(datexScript, valuesArray); + } + + async #executeInternal( + datexScript: string, + values: unknown[] | null = [], + ): Promise { + const difValueContainer = await this.#difHandler.executeDIF( + datexScript, + values, + ); + if (difValueContainer === null) { + return undefined as T; + } + return this.#difHandler.resolveDIFValueContainer(difValueContainer); + } + + /** + * Executes a Datex script synchronously and returns the result as a generic type T. + * Injected values can be passed as an array in `values`. + * If the script returns no value, it will return `undefined`. + * Example usage: + * ```ts + * const result = runtime.executeSync("1 + ?", [41]); + * console.log(result); // 42 + * ``` + */ + public executeSync( + datexScript: string, + values?: unknown[], + ): T; + + /** + * Executes a Datex script synchronously and returns the result as a generic type T. + * Injected values can be passed to the template string. + * Example usage: + * ```ts + * const result = runtime.executeSync`1 + ${41}`; + * console.log(result); // 42 + * ``` + */ + public executeSync( + templateStrings: TemplateStringsArray, + ...values: unknown[] + ): T; + + public executeSync( + datexScriptOrTemplateStrings: string | TemplateStringsArray, + ...values: unknown[] + ): T { + // determine datexScript and valuesArray based on the type of datexScriptOrTemplateStrings + const { datexScript, valuesArray } = this.#getScriptAndValues( + datexScriptOrTemplateStrings, + ...values, + ); + return this.#executeSyncInternal(datexScript, valuesArray); + } + + #executeSyncInternal( + datexScript: string, + values: unknown[] | null = [], + ): T { + const difValue = this.#difHandler.executeSyncDIF(datexScript, values); + if (difValue === null) { + return undefined as T; + } + const result = this.#difHandler.resolveDIFValueContainer(difValue); + if (result instanceof Promise) { + throw new Error( + "executeSync cannot return a Promise. Use execute() instead.", + ); + } + return result; } - public execute_sync( - datex_script: string, - formatted: boolean = false, + public valueToString( + value: unknown, + decompileOptions: DecompileOptions | null = null, ): string { - return this.#runtime.execute_sync(datex_script, formatted); + return this.#runtime.value_to_string( + this.#difHandler.convertJSValueToDIFValue(value), + decompileOptions, + ); } - public _execute_internal(datex_script: string): boolean { - return execute_internal(datex_script); + /** + * Handles the function arguments to a normal function call or a template function call, + * always returning a normalized datexScript and valuesArray. + */ + #getScriptAndValues( + datexScriptOrTemplateStrings: string | TemplateStringsArray, + ...values: unknown[] + ): { datexScript: string; valuesArray: unknown[] } { + let datexScript: string; + let valuesArray: unknown[]; + if (typeof datexScriptOrTemplateStrings === "string") { + datexScript = datexScriptOrTemplateStrings; + valuesArray = values[0] as unknown[] ?? []; + } else if (Array.isArray(datexScriptOrTemplateStrings)) { + // if it's a TemplateStringsArray, join the strings and interpolate the values + datexScript = datexScriptOrTemplateStrings.join("?"); + valuesArray = values; + } else { + throw new Error("Invalid argument type for executeSync"); + } + return { datexScript, valuesArray }; + } + + public _execute_internal(datexScript: string): boolean { + return execute_internal(datexScript); + } + + /** + * Creates a new pointer containg the given JS value. + * The returned value is a proxy object that behaves like the original object, + * but also propagates changes between JS and the DATEX runtime. + */ + public createPointer< + V, + M extends DIFReferenceMutability = + typeof DIFReferenceMutability.Mutable, + >( + // deno-lint-ignore ban-types + value: V & {}, + allowedType?: DIFTypeContainer | null, + mutability?: M, + ): PointerOut { + return this.#difHandler.createPointerFromJSValue( + value, + allowedType, + mutability, + ) as PointerOut; } } + +type WidenLiteral = T extends string ? string + : T extends number ? number + : T extends boolean ? boolean + : T extends bigint ? bigint + : T extends symbol ? symbol + : T; + +type IsRef = T extends Ref ? true : false; +type ContainsRef = IsRef extends true ? true + : T extends object + ? { [K in keyof T]: ContainsRef }[keyof T] extends true ? true + : false + : false; + +export type AssignableRef = Ref | T & { value?: T }; + +type Builtins = + // deno-lint-ignore ban-types + | Function + | Date + | RegExp + | Map + | Set + | WeakMap + | WeakSet + | Array; + +type IsPlainObject = T extends Builtins ? false + : T extends object ? true + : false; + +type ObjectFieldOut = T extends + Ref + ? M extends typeof DIFReferenceMutability.Final ? Ref : AssignableRef + : IsPlainObject extends true ? ( + ContainsRef extends true + ? M extends typeof DIFReferenceMutability.Final + ? { readonly [K in keyof T]: ObjectFieldOut } + : { [K in keyof T]: ObjectFieldOut } + : { readonly [K in keyof T]: ObjectFieldOut } + ) + : T; + +type PointerOut = V extends Ref + ? M extends typeof DIFReferenceMutability.Final ? Ref : AssignableRef + : IsPlainObject extends true ? ( + ContainsRef extends true + ? M extends typeof DIFReferenceMutability.Final + ? { readonly [K in keyof V]: ObjectFieldOut } + : { [K in keyof V]: ObjectFieldOut } + : { readonly [K in keyof V]: ObjectFieldOut } + ) + : V extends Builtins ? Pointer + : M extends typeof DIFReferenceMutability.Final ? Ref + : Ref>; + +type CollectionProps = { + [K in keyof T as K extends "value" ? never : K]: T[K]; +}; + +interface MapRef extends Ref>, CollectionProps> {} +interface SetRef extends Ref>, CollectionProps> {} +interface ArrayRef extends Ref, CollectionProps {} + +type Pointer = T extends Map ? MapRef + : T extends Set ? SetRef + : T extends Array ? ArrayRef + : Ref; diff --git a/src/runtime/special-core-types.ts b/src/runtime/special-core-types.ts new file mode 100644 index 00000000..19506668 --- /dev/null +++ b/src/runtime/special-core-types.ts @@ -0,0 +1,37 @@ +export class Endpoint { + readonly #endpoint: string; + + // map with weak values that keeps track of all currently instantiated endpoints + static endpoints: Map> = new Map(); + + private constructor(endpoint: string) { + this.#endpoint = endpoint; + Endpoint.registerEndpoint(this); + } + + public static registerEndpoint(endpoint: Endpoint) { + // set as a weak reference in the static map + const weakRef = new WeakRef(endpoint); + Endpoint.endpoints.set(endpoint.toString(), weakRef); + new FinalizationRegistry((key: string) => { + Endpoint.endpoints.delete(key); + }).register(endpoint, endpoint.toString()); + } + + public static get(endpoint: string): Endpoint { + if (Endpoint.endpoints.has(endpoint)) { + const weakRef = Endpoint.endpoints.get(endpoint); + if (weakRef) { + const existingEndpoint = weakRef.deref(); + if (existingEndpoint) { + return existingEndpoint; + } + } + } + return new Endpoint(endpoint); + } + + public toString(): string { + return this.#endpoint; + } +} diff --git a/src/types/ts-types.ts b/src/types/ts-types.ts new file mode 100644 index 00000000..149f9d91 --- /dev/null +++ b/src/types/ts-types.ts @@ -0,0 +1,34 @@ +import { CoreTypeAddress, type DIFTypeContainer } from "../dif/definitions.ts"; + +// NOTE: this only a proof of concept prototype impl. We should probably move this to rust in the future. +/// Template function for creating TypeScript types +export function TS_TYPE( + def: TemplateStringsArray, + ...args: unknown[] +): DIFTypeContainer { + let full_definition = ""; + for (let i = 0; i < def.length; i++) { + full_definition += def[i]; + if (i < args.length) { + full_definition += args[i]; + } + } + return convertTSTypeToDIFType(full_definition.trim()); +} + +function convertTSTypeToDIFType(ts_type_str: string): DIFTypeContainer { + ts_type_str = ts_type_str.trim(); + if (ts_type_str === "number") { + return CoreTypeAddress.decimal_f64; + } else if (ts_type_str === "string") { + return CoreTypeAddress.text; + } else if (ts_type_str === "null") { + return CoreTypeAddress.null; + } else if (ts_type_str === "boolean") { + return CoreTypeAddress.boolean; + } else { + throw new Error( + `TS type conversion not implemented for: ${ts_type_str}`, + ); + } +} diff --git a/test/browser.ts b/test/browser.ts index b62ad6ee..a9f8551b 100644 --- a/test/browser.ts +++ b/test/browser.ts @@ -1,20 +1,38 @@ import { Datex } from "../src/mod.ts"; +import { SerialInterfaceImpl } from "../src/network/interface-impls/serial.ts"; +import { WebRTCInterfaceImpl } from "../src/network/interface-impls/webrtc.ts"; // @ts-ignore global variable for debugging globalThis.Datex = Datex; document.getElementById("serial")!.addEventListener("click", async () => { - const serial = await Datex.comHub.serial.register(19200); + const serial = await Datex.comHub.createInterface( + SerialInterfaceImpl, + { baud_rate: 19200, port_name: null }, + ); console.log(serial); }); document.getElementById("webrtc")!.addEventListener("click", async () => { - const webrtc = Datex.comHub.webrtc; - const interface_a = await webrtc.register("@jonas"); - const interface_b = await webrtc.register("@ben"); + const interface_a = await Datex.comHub.createInterface( + WebRTCInterfaceImpl, + { + peer_endpoint: "@jonas", + ice_servers: null, + }, + ); - webrtc.set_on_ice_candidate(interface_a, (candidate: Uint8Array) => { - webrtc.add_ice_candidate(interface_b, candidate) + const interface_b = await Datex.comHub.createInterface( + WebRTCInterfaceImpl, + { + peer_endpoint: "@ben", + ice_servers: null, + }, + ); + console.log("Interface A:", interface_a); + interface_a.impl.setOnIceCandidate((candidate: Uint8Array) => { + console.log("Interface A ICE candidate:", candidate); + interface_b.impl.addIceCandidate(candidate) .then(() => console.log("ICE candidate added to interface B")) .catch((e) => console.error( @@ -24,8 +42,8 @@ document.getElementById("webrtc")!.addEventListener("click", async () => { ); }); - webrtc.set_on_ice_candidate(interface_b, (candidate: Uint8Array) => { - webrtc.add_ice_candidate(interface_a, candidate) + interface_b.impl.setOnIceCandidate((candidate: Uint8Array) => { + interface_a.impl.addIceCandidate(candidate) .then(() => console.log("ICE candidate added to interface A")) .catch((e) => console.error( @@ -34,24 +52,25 @@ document.getElementById("webrtc")!.addEventListener("click", async () => { ) ); }); + const offer = await interface_a.impl.createOffer(); + console.log("Offer from A:", offer); - const offer = await webrtc.create_offer(interface_a); - console.log("Offer:", offer); - - const answer = await webrtc.create_answer(interface_b, offer); - console.log("Answer:", answer); - await webrtc.set_answer(interface_a, answer); + const answer = await interface_b.impl.createAnswer(offer); + console.log("Answer from B:", answer); + await interface_a.impl.setAnswer(answer); - await webrtc.wait_for_connection(interface_a); - await webrtc.wait_for_connection(interface_b); + await interface_a.impl.waitForConnection(); + console.log("Interface A connected"); + await interface_b.impl.waitForConnection(); + console.log("Interface B connected"); - const success = await Datex.comHub.send_block( + const success = await Datex.comHub.sendBlock( new Uint8Array([1, 2, 3, 4]), - interface_a, + interface_a.uuid, "", - ) && await Datex.comHub.send_block( + ) && await Datex.comHub.sendBlock( new Uint8Array([1, 2, 3, 4]), - interface_b, + interface_b.uuid, "", ); diff --git a/test/init.test.ts b/test/init.test.ts index d77ace98..37c46de5 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "jsr:@std/assert"; +import { assertEquals } from "@std/assert"; import { Runtime } from "../src/runtime/runtime.ts"; /** diff --git a/test/network/BaseInterface.test.ts b/test/network/BaseInterface.test.ts index 7dbdc1f5..805260aa 100644 --- a/test/network/BaseInterface.test.ts +++ b/test/network/BaseInterface.test.ts @@ -1,10 +1,10 @@ -import { assert } from "jsr:@std/assert/assert"; +import { assert } from "@std/assert/assert"; import { Runtime } from "../../src/runtime/runtime.ts"; -import * as uuid from "jsr:@std/uuid"; +import * as uuid from "@std/uuid"; import { sleep } from "../utils.ts"; -import { assertFalse } from "jsr:@std/assert/false"; -import { assertEquals } from "jsr:@std/assert/equals"; -import { assertThrows } from "jsr:@std/assert/throws"; +import { assertFalse } from "@std/assert/false"; +import { assertEquals } from "@std/assert/equals"; +import { assertThrows } from "@std/assert/throws"; import { isNodeOrBun } from "../is-node.ts"; import type { BaseInterfaceSetupData } from "../../src/datex-core/datex_core_js.d.ts"; import "../../src/network/interface-impls/base.ts"; @@ -15,34 +15,17 @@ const config: BaseInterfaceSetupData = { interface_type: "base", channel: "test", direction: "InOut", - round_trip_time: 1000, + round_trip_time: 5, max_bandwidth: 1, continuous_connection: true, allow_redirects: true, is_secure_channel: true, reconnection_config: "NoReconnect", - reconnect_attempts: undefined, + reconnect_attempts: null, + close_timestamp: null, }; -// Deno.test("custom properties no reconnect", () => { -// const runtime = new Runtime("@unyt"); -// const config: InterfaceProperties = { -// name: "base", -// interface_type: "base", -// channel: "test", -// direction: "InOut", -// round_trip_time: 1000, -// max_bandwidth: 1, -// continuous_connection: true, -// allow_redirects: true, -// is_secure_channel: true, -// reconnection_config: "NoReconnect", -// reconnect_attempts: undefined, -// }; -// const baseInterface = new BaseJSInterface(runtime._runtime, config); -// assertEquals(baseInterface.properties, config); -// }); - -Deno.test("custom properties with reconnect", async () => { + +Deno.test("custom properties no reconnect", async () => { const runtime = new Runtime({ endpoint: "@unyt" }); const baseInterface = await runtime.comHub.createInterface< @@ -140,7 +123,7 @@ Deno.test("test receive and send", async () => { ); }); -// TODO: +// TODO: add tests for worker Deno.test("worker", async () => { // FIXME: temporarily disabled because workers are not yet supported for node.js/dnt if (isNodeOrBun) { diff --git a/test/network/WebsocketClientInterface.test.ts b/test/network/WebsocketClientInterface.test.ts index 687d6db2..34d745fe 100644 --- a/test/network/WebsocketClientInterface.test.ts +++ b/test/network/WebsocketClientInterface.test.ts @@ -1,8 +1,8 @@ -import { assert, assertEquals, assertRejects } from "jsr:@std/assert"; +import { assert, assertEquals, assertRejects } from "@std/assert"; import { createMockupServer } from "./WebsocketMockupServer.ts"; import { Runtime } from "../../src/runtime/runtime.ts"; import { sleep } from "../utils.ts"; -import * as uuid from "jsr:@std/uuid"; +import * as uuid from "@std/uuid"; import { isNodeOrBun } from "../is-node.ts"; import "../../src/network/interface-impls/websocket-client.ts"; diff --git a/test/network/WebsocketServerInterface.test.ts b/test/network/WebsocketServerInterface.test.ts index 3d5c27df..95a5d020 100644 --- a/test/network/WebsocketServerInterface.test.ts +++ b/test/network/WebsocketServerInterface.test.ts @@ -1,6 +1,6 @@ -import { assert } from "jsr:@std/assert/assert"; +import { assert } from "@std/assert/assert"; import { Runtime } from "../../src/runtime/runtime.ts"; -import * as uuid from "jsr:@std/uuid"; +import * as uuid from "@std/uuid"; import { isNodeOrBun } from "../is-node.ts"; import "../../src/network/interface-impls/websocket-client.ts"; import "../../src/network/interface-impls/websocket-server-deno.ts"; @@ -33,7 +33,7 @@ Deno.test("connect two runtimes", async () => { WebSockerServerDenoInterfaceImpl >( "websocket-server", - { port: PORT }, + { port: PORT, secure: false }, ); const runtimeB = new Runtime({ endpoint: "@test_b" }); @@ -76,7 +76,7 @@ Deno.test("send data between two runtimes", async () => { await sleep(1000); - const res = await runtimeA.execute("@test_b :: 1 + 2"); + const res = await runtimeA.executeWithStringResult("@test_b :: 1 + 2"); assert(res === "3", "Expected result from remote execution to be 3"); await serverInterface.close(); diff --git a/test/runtime/decompile.test.ts b/test/runtime/decompile.test.ts new file mode 100644 index 00000000..b6abfdd3 --- /dev/null +++ b/test/runtime/decompile.test.ts @@ -0,0 +1,17 @@ +import { Runtime } from "../../src/runtime/runtime.ts"; +import { assertEquals } from "@std/assert"; +Deno.test("decompile integer without formatting", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.valueToString(42); + assertEquals(result, "42f64"); +}); + +// FIXME: colorization in new decompiler +// Deno.test("decompile integer colorized", () => { +// const runtime = new Runtime({ endpoint: "@jonas" }); +// const result = runtime.valueToString(42, { colorized: true }); +// assertEquals( +// result, +// "\x1B[38;2;231;139;71m42\x1B[38;2;212;212;212m.\x1B[38;2;231;139;71m0\x1B[0m", +// ); +// }); diff --git a/test/runtime/dif.test.ts b/test/runtime/dif.test.ts new file mode 100644 index 00000000..81e537a8 --- /dev/null +++ b/test/runtime/dif.test.ts @@ -0,0 +1,640 @@ +import { Runtime } from "../../src/runtime/runtime.ts"; +import { assert, assertEquals } from "@std/assert"; +import { assertThrows } from "@std/assert/throws"; +import { + CoreTypeAddress, + type DIFReference, + DIFReferenceMutability, + type DIFRepresentationValue, + type DIFUpdate, + type DIFUpdateData, + DIFUpdateKind, +} from "../../src/dif/definitions.ts"; +import { assertStrictEquals } from "@std/assert/strict-equals"; +import { Ref } from "../../src/refs/ref.ts"; +import { + difReferenceToDisplayString, + difValueContainerToDisplayString, +} from "../../src/dif/display.ts"; + +const runtime = new Runtime({ endpoint: "@jonas", debug: true }); +Deno.test("pointer create with observe", () => { + const ref = runtime.dif.createPointer( + { + value: "Hello, Datex!", + }, + undefined, + DIFReferenceMutability.Mutable, + ); + assertEquals(typeof ref, "string"); + + let observed: DIFUpdate | null = null; + const observerId = runtime.dif.observePointerBindDirect(ref, (value) => { + runtime.executeSync("'xy'"); + runtime.dif.unobservePointerBindDirect(ref, observerId); + observed = value; + // TODO: print error message somewhere (don't throw) + throw new Error("Should not be called again"); + }, { relay_own_updates: true }); + + runtime.dif.updatePointer(ref, { + value: { value: "Hello, Datex 2" }, + kind: DIFUpdateKind.Replace, + }); + + // if not equal, unobservePointer potentially failed + assertEquals(observed, { + source_id: runtime.dif._transceiver_id, + data: { + value: { value: "Hello, Datex 2" }, + kind: DIFUpdateKind.Replace, + }, + }); +}); + +Deno.test("pointer create without observe", () => { + const ref = runtime.dif.createPointer( + { + value: "Hello, Datex!", + }, + undefined, + DIFReferenceMutability.Mutable, + ); + assertEquals(typeof ref, "string"); + + let observed: DIFUpdate | null = null; + const observerId = runtime.dif.observePointerBindDirect(ref, (value) => { + runtime.executeSync("'xy'"); + runtime.dif.unobservePointerBindDirect(ref, observerId); + observed = value; + }); + + runtime.dif.updatePointer(ref, { + value: { value: "Hello, Datex 2" }, + kind: DIFUpdateKind.Replace, + }); + + // observer should not be called, because relay_own_updates is false and the source is the same as the observer + assertEquals(observed, null); +}); + +Deno.test("pointer create primitive", () => { + runtime.createPointer( + 42, + undefined, + DIFReferenceMutability.Final, + ) satisfies Ref<42>; + + runtime.createPointer( + 42, + undefined, + DIFReferenceMutability.Mutable, + ) satisfies Ref; + + runtime.createPointer( + "hello world", + undefined, + DIFReferenceMutability.Final, + ) satisfies Ref<"hello world">; + + runtime.createPointer( + "hello world", + undefined, + DIFReferenceMutability.Mutable, + ) satisfies Ref; + + runtime.createPointer( + true, + undefined, + DIFReferenceMutability.Final, + ) satisfies Ref; + + runtime.createPointer( + { x: true } as const, + undefined, + DIFReferenceMutability.Final, + ) satisfies { + readonly x: true; + }; + + const a = runtime.createPointer(5, undefined, DIFReferenceMutability.Final); + const b = runtime.createPointer( + { x: a }, + undefined, + DIFReferenceMutability.Mutable, + ) satisfies { + x: Ref<5> | 5; + }; + b.x satisfies Ref<5> | 5; + b.x = 5; +}); + +Deno.test("pointer create struct", () => { + const innerPtr = runtime.createPointer( + 3, + undefined, + DIFReferenceMutability.Mutable, + ); + const struct = { a: 1.0, b: "text", c: { d: true }, e: { f: innerPtr } }; + + { // can not assign to ptrObjFinal.e.f + const ptrObjFinal = runtime.createPointer( + struct, + undefined, + DIFReferenceMutability.Final, + ); + ptrObjFinal.e satisfies { readonly f: Ref | number }; + } + + const ptrObj = runtime.createPointer( + struct, + undefined, + DIFReferenceMutability.Mutable, + ); + + assertThrows( + () => { + // @ts-ignore: Property 'a' is read-only + ptrObj.a = 2; + }, + Error, + `modify`, + ); + assertThrows( + () => { + // @ts-ignore: Property 'x' does not exist + ptrObj.x = 2; + }, + Error, + `modify`, + ); + assertThrows( + () => { + // @ts-ignore: Property 'd' is read-only + ptrObj.c.d = false; + }, + Error, + `modify`, + ); + innerPtr.value = 42; + assertEquals(innerPtr.value, 42); + assertEquals(ptrObj.e.f.value, 42); + innerPtr.value = 7; + assertEquals(ptrObj.e.f.value, 7); + assertEquals(innerPtr.value, 7); + + ptrObj.e.f = 10; +}); + +Deno.test("pointer create and resolve", () => { + // TODO: reenable, currently panics because async resolution is not yet implemented + // assertThrows( + // () => { + // runtime.dif.resolveDIFValueContainerSync("abcdef"); + // }, + // Error, + // `Invalid`, + // ); + + const ptr = runtime.dif.createPointer( + { value: "unyt.org" }, + undefined, + DIFReferenceMutability.Mutable, + ); + const resolved = runtime.dif.resolveDIFValueContainerSync( + ptr, + ); + assertEquals(resolved, "unyt.org"); +}); + +Deno.test("pointer object create and resolve", () => { + const initialDIFValue: DIFRepresentationValue = [ + [{ value: "a" }, { value: 123 }], + [{ value: "b" }, { value: 456 }], + ]; + const ptr = runtime.dif.createPointer( + { + value: initialDIFValue, + }, + undefined, + DIFReferenceMutability.Mutable, + ); + console.log("ptr address", ptr); + const loadedDIFValue = runtime.dif._handle.resolve_pointer_address_sync( + ptr, + ); + console.log("loadedObj", loadedDIFValue); + + assertEquals( + loadedDIFValue, + { + allowed_type: "0c0000", + mut: DIFReferenceMutability.Mutable, + value: { + value: initialDIFValue, + }, + } satisfies DIFReference, + ); +}); + +Deno.test("pointer object create and cache", () => { + const val = { a: 123, b: 456 }; + const ptrObj = runtime.createPointer(val); + console.log("ptrObj", ptrObj); + assertEquals( + ptrObj, + val, + ); + + const ptrId = runtime.dif.getPointerAddressForValue(ptrObj); + console.log("ptrId", ptrId); + if (!ptrId) { + throw new Error("Pointer ID not found for value"); + } + + // check if cache is used when resolving the pointer again + const loadedObj = runtime.dif.resolvePointerAddress(ptrId); + console.log("loadedObj", loadedObj); + + console.log( + difValueContainerToDisplayString( + runtime.dif._handle.resolve_pointer_address(ptrId), + ), + ); + + // identical object reference + assertStrictEquals(loadedObj, ptrObj); +}); + +Deno.test("pointer map create and cache", () => { + const val = new Map([[1, 2], [3, 4]]); + const ptrMap = runtime.createPointer(val); + assertEquals(ptrMap, val); + ptrMap.set(5, 6); + ptrMap satisfies Map; + ptrMap.value satisfies Map; + assertEquals(ptrMap.get(5), 6); + + ptrMap.delete(1); + assertEquals(ptrMap.has(1), false); + assertEquals(ptrMap.size, 2); + + const ptrId = runtime.dif.getPointerAddressForValue(ptrMap); + if (!ptrId) { + throw new Error("Pointer ID not found for value"); + } + + // check if cache is used when resolving the pointer again + // FIXME avoid cache for this check + const loadedMap = runtime.dif.resolvePointerAddress(ptrId); + console.log("loadedMap", loadedMap); + console.log( + difReferenceToDisplayString( + runtime.dif._handle.resolve_pointer_address( + ptrId, + ) as DIFReference, + ), + ); + + ptrMap.clear(); + // identical object reference + assertStrictEquals(loadedMap, ptrMap); +}); + +Deno.test("pointer primitive ref create and cache", () => { + const val = 123; + const ptrObj = runtime.createPointer(val); + if (!(ptrObj instanceof Ref)) { + throw new Error("Pointer object is not a Ref"); + } + console.log("ptrObj", ptrObj); + assertEquals(ptrObj.value, val); + + const ptrId = ptrObj.pointerAddress; + + // check if cache is used when resolving the pointer again + const loadedObj = runtime.dif.resolvePointerAddress(ptrId) as number; + console.log("loadedObj", loadedObj); + // identical primitive value + assertStrictEquals(loadedObj, ptrObj); +}); + +Deno.test("pointer primitive ref update", () => { + const val = 123; + const ptrObj = runtime.createPointer(val as number); + if (!(ptrObj instanceof Ref)) { + throw new Error("Pointer object is not a Ref"); + } + assertEquals(ptrObj.value, val); + + // get value of ptrObj from DATEX execution + let result = runtime.executeSyncWithStringResult( + "$" + ptrObj.pointerAddress, + ); + assertEquals(result, "&mut 123f64"); + + // update the ref value + ptrObj.value = 456; + + // get value of ptrObj from DATEX execution + result = runtime.executeSyncWithStringResult( + "$" + ptrObj.pointerAddress, + ); + assertEquals(result, "&mut 456f64"); +}); + +Deno.test("immutable pointer primitive ref update", () => { + const val = 123; + const ptrObj = runtime.createPointer( + val as number, + undefined, + DIFReferenceMutability.Immutable, + ); + if (!(ptrObj instanceof Ref)) { + throw new Error("Pointer object is not a Ref"); + } + assertEquals(ptrObj.value, val); + + // get value of ptrObj from DATEX execution + const result = runtime.executeSyncWithStringResult( + "$" + ptrObj.pointerAddress, + ); + assertEquals(result, "&123f64"); + + // update the ref value + assertThrows( + () => { + ptrObj.value = 456; + }, + Error, + `immutable reference`, + ); +}); + +Deno.test("final pointer primitive ref update", () => { + const val = 123; + const ptrObj = runtime.createPointer( + val as number, + undefined, + DIFReferenceMutability.Final, + ); + if (!(ptrObj instanceof Ref)) { + throw new Error("Pointer object is not a Ref"); + } + assertEquals(ptrObj.value, val); + + // get value of ptrObj from DATEX execution + const result = runtime.executeSyncWithStringResult( + "$" + ptrObj.pointerAddress, + ); + assertEquals(result, "&final 123f64"); + + // update the ref value + assertThrows( + () => { + ptrObj.value = 456; + }, + Error, + `immutable reference`, + ); +}); + +Deno.test("pointer primitive ref update and observe", () => { + const val = 123; + const ptrObj = runtime.createPointer(val as number) as Ref; + assertEquals(ptrObj.value, val); + + let observedUpdate: DIFUpdate | null = null; + runtime.dif.observePointerBindDirect(ptrObj.pointerAddress, (update) => { + console.log("Observed pointer update:", update); + observedUpdate = update; + }, { relay_own_updates: true }); + + // update the ref value + ptrObj.value = 456; + + // check if the update was observed + assertEquals(observedUpdate, { + source_id: runtime.dif._transceiver_id, + data: { + kind: DIFUpdateKind.Replace, + value: { + value: 456, + }, + }, + }); +}); + +Deno.test("pointer primitive ref update and observe local", () => { + const val = 123; + const ptrObj = runtime.createPointer(val as number) as Ref; + assertEquals(ptrObj.value, val); + + let observedUpdate: DIFUpdateData | null = null; + const observerId = runtime.dif.observePointer( + ptrObj.pointerAddress, + (update) => { + console.log("Observed pointer update:", update); + observedUpdate = update; + }, + ); + // check if observer is registered + assertEquals(runtime.dif._observers.get(ptrObj.pointerAddress)?.size, 1); + assert(runtime.dif._observers.get(ptrObj.pointerAddress)?.has(observerId)); + + // update the ref value + ptrObj.value = 456; + + // check if the update was observed + assertEquals(observedUpdate, { + kind: DIFUpdateKind.Replace, + value: { + value: 456, + }, + }); + + // unobserve + runtime.dif.unobservePointer(ptrObj.pointerAddress, observerId); + // check if observer is unregistered + assertEquals( + runtime.dif._observers.get(ptrObj.pointerAddress)?.size, + undefined, + ); + assert( + !runtime.dif._observers.has(ptrObj.pointerAddress), + ); + + // update the ref value again + observedUpdate = null; + ptrObj.value = 789; + // check that no update was observed + assertEquals(observedUpdate, null); +}); + +Deno.test("pointer primitive ref remote update and observe bind direct", () => { + const val = 123; + const ptrObj = runtime.createPointer(val as number) as Ref; + assertEquals(ptrObj.value, val); + + let observedUpdate: DIFUpdate | null = null; + runtime.dif.observePointerBindDirect( + ptrObj.pointerAddress, + (update) => { + console.log("Observed pointer update:", update); + observedUpdate = update; + }, + ); + + // fake a remote update from transceiver 42 + runtime.dif._handle.update(42, ptrObj.pointerAddress, { + value: { value: 456 }, + kind: DIFUpdateKind.Replace, + }); + + // check if the update was observed + assertEquals(observedUpdate, { + source_id: 42, + data: { + kind: DIFUpdateKind.Replace, + value: { + value: 456, + }, + }, + }); + + assertEquals(ptrObj.value, 456); +}); + +Deno.test("pointer primitive ref remote update and observe local", () => { + const val = 123; + const ptrObj = runtime.createPointer(val as number) as Ref; + assertEquals(ptrObj.value, val); + + let observedUpdate: DIFUpdateData | null = null; + runtime.dif.observePointer( + ptrObj.pointerAddress, + (update) => { + console.log("Observed pointer update:", update); + observedUpdate = update; + }, + ); + + // fake a remote update from transceiver 42 + runtime.dif._handle.update(42, ptrObj.pointerAddress, { + value: { value: 456 }, + kind: DIFUpdateKind.Replace, + }); + + // check if the update was observed + assertEquals(observedUpdate, { + kind: DIFUpdateKind.Replace, + value: { + value: 456, + }, + }); + + assertEquals(ptrObj.value, 456); + + observedUpdate = null; + + // fake a local update + runtime.dif._handle.update( + runtime.dif._transceiver_id, + ptrObj.pointerAddress, + { + value: { value: 789 }, + kind: DIFUpdateKind.Replace, + }, + ); + + // local observer should still be triggered + assertEquals(observedUpdate, { + kind: DIFUpdateKind.Replace, + value: { + value: 789, + }, + }); + + // local value should not be updated since the update came from own transceiver + assertEquals(ptrObj.value, 456); +}); + +Deno.test("observer final", () => { + const ref = runtime.dif.createPointer( + { value: "Final" }, + undefined, + DIFReferenceMutability.Final, + ); + assertThrows( + () => { + runtime.dif.observePointerBindDirect(ref, (_) => {}); + }, + Error, + `final reference`, + ); +}); + +Deno.test("pointer observe unobserve", () => { + const ref = runtime.dif.createPointer( + { value: "42" }, + undefined, + DIFReferenceMutability.Mutable, + ); + assertThrows( + () => { + runtime.dif.unobservePointerBindDirect(ref, 42); + }, + Error, + `not found`, + ); + + const observerId = runtime.dif.observePointerBindDirect(ref, (value) => { + console.log("Observed pointer value:", value); + runtime.dif.unobservePointerBindDirect(ref, observerId); + }); + assertEquals(observerId, 0); + runtime.dif.unobservePointerBindDirect(ref, observerId); + assertThrows( + () => { + runtime.dif.unobservePointerBindDirect(ref, observerId); + }, + Error, + `not found`, + ); +}); + +Deno.test("core text", () => { + const script = `"Hello, world!"`; + const result = runtime.dif.executeSyncDIF(script); + assertEquals(result, { value: "Hello, world!" }); +}); + +Deno.test("core integer", () => { + const script = "42"; + const result = runtime.dif.executeSyncDIF(script); + assertEquals(result, { + type: CoreTypeAddress.integer, + value: "42", + }); +}); + +Deno.test("core boolean", () => { + const script = "true"; + const result = runtime.dif.executeSyncDIF(script); + assertEquals(result, { value: true }); +}); + +Deno.test("core null", () => { + const script = "null"; + const result = runtime.dif.executeSyncDIF(script); + assertEquals(result, { value: null }); +}); + +Deno.test("core integer variants", () => { + const script = "42u8"; + const result = runtime.dif.executeSyncDIF(script); + assertEquals(result, { + value: 42, + type: CoreTypeAddress.integer_u8, + }); +}); diff --git a/test/runtime/execute.test.ts b/test/runtime/execute.test.ts index 7f272f6b..7cc402c9 100644 --- a/test/runtime/execute.test.ts +++ b/test/runtime/execute.test.ts @@ -1,25 +1,147 @@ import { Runtime } from "../../src/runtime/runtime.ts"; -import { assertEquals } from "jsr:@std/assert"; -Deno.test("execute sync", () => { +import { assertEquals } from "@std/assert"; +import { Endpoint } from "../../src/runtime/special-core-types.ts"; +import { CoreTypeAddress } from "../../src/dif/definitions.ts"; +Deno.test("execute sync with string result", () => { const runtime = new Runtime({ endpoint: "@jonas" }); const script = "1 + 2"; - const result = runtime.execute_sync(script, false); + const result = runtime.executeSyncWithStringResult(script); assertEquals(result, "3"); console.log(result); }); -Deno.test("execute", async () => { +Deno.test("execute sync dif value", () => { const runtime = new Runtime({ endpoint: "@jonas" }); const script = "1 + 2"; - const result = await runtime.execute(script, false); + // NOTE: in an optimized version of DIF, we could also just return a plain number in this case. + // For now, all DIF values are returned in the same format to reduce complexity. + const result = runtime.dif.executeSyncDIF(script); + assertEquals(result, { + type: CoreTypeAddress.integer, + value: "3", + }); + console.log(result); +}); + +Deno.test("execute sync number", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync("1 + 2"); + assertEquals(result, 3); +}); + +// FIXME +Deno.test("execute sync typed integer", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.dif.executeSyncDIF( + "42u8", + ); + assertEquals(result, { + type: CoreTypeAddress.integer_u8, + value: 42, + }); +}); + +Deno.test("execute sync normal integer", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync( + "123456781234567891234567812345678", + ); + assertEquals(typeof result, "number"); + assertEquals( + result, + 1.234567812345679e+32, + ); +}); + +Deno.test("execute sync bigint", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync( + "123456781234567891234567812345678big", + ); + assertEquals(typeof result, "bigint"); + assertEquals( + result, + 123456781234567891234567812345678n, + ); +}); + +Deno.test("execute sync string", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync("'lol'"); + assertEquals(result, "lol"); +}); + +Deno.test("execute sync boolean", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync("true"); + assertEquals(result, true); +}); + +Deno.test("execute sync null", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync("null"); + assertEquals(result, null); +}); + +Deno.test("execute sync array", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync("[1, 2, 3]"); + assertEquals(result, [1, 2, 3]); +}); + +Deno.test("execute sync none", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync("42;"); + assertEquals(result, undefined); +}); + +Deno.test("execute sync object", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync>( + "{ a: 1, b: 'test' }", + ); + assertEquals( + result, + new Map([["a", 1], ["b", "test"]]), + ); +}); + +Deno.test("execute sync endpoint", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync("#endpoint"); + assertEquals(result, Endpoint.get("@jonas")); +}); + +Deno.test("execute sync pass number from JS", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync("1 + ?", [41]); + assertEquals(result, 42); +}); + +Deno.test("execute sync pass multiple values from JS", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync("[?, 2, ?]", [1, 3]); + assertEquals(result, [1, 2, 3]); +}); + +Deno.test("execute sync pass multiple values from JS with template syntax", () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync`[${1}, 2, ${3}]`; + assertEquals(result, [1, 2, 3]); +}); + +Deno.test("execute with string result", async () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const script = "1 + 2"; + const result = await runtime.executeWithStringResult(script); assertEquals(result, "3"); console.log(result); }); -Deno.test("execute remote ", async () => { +Deno.test("execute remote with string result", async () => { const runtime = new Runtime({ endpoint: "@jonas" }); const script = "1 + 2"; - const result = await runtime.execute(script, false); + const result = await runtime.executeWithStringResult(script); assertEquals(result, "3"); console.log(result); }); diff --git a/test/runtime/value-parity.ts b/test/runtime/value-parity.ts new file mode 100644 index 00000000..ab51996d --- /dev/null +++ b/test/runtime/value-parity.ts @@ -0,0 +1,49 @@ +/** + * This test suite is used as a verification that all JS values that are passed to + * the DATEX runtime come out as the exact same values after execution. + * This is a test for the full integration of the JS runtime with the DATEX runtime. + * NOTE: as more JS values are supported, this test should be extended to cover all of them. + */ +import { Runtime } from "../../src/runtime/runtime.ts"; +import { assertEquals } from "@std/assert"; + +/** + * Test values that are used to verify the value parity. + * These values should cover all basic JS types and some complex types. + * If you add new types, make sure to also add them to the `TEST_VALUES` + * array below. + */ +const TEST_VALUES = [ + // simple JSON values + 42, + -10, + 3.14, + "Hello, World!", + true, + false, + null, + [1, 2, 3], + { a: 1, b: "test" }, +]; + +// initialization of the test cases +const valueTypeCounter = new Map(); +for (const value of TEST_VALUES) { + // class name or primitive type + const valueType = value == null + ? "null" + : value == "undefined" + ? "undefined" + : value?.constructor.name; + // increment counter for this type + const count = valueTypeCounter.get(valueType) || 0; + valueTypeCounter.set(valueType, count + 1); + Deno.test(`test value parity for value of type ${valueType} #${count + 1}`, () => { + const runtime = new Runtime({ endpoint: "@jonas" }); + const result = runtime.executeSync( + "?", + [value], + ); + assertEquals(result, value); + }); +} diff --git a/test/types/ts-types.ts b/test/types/ts-types.ts new file mode 100644 index 00000000..2e58ede5 --- /dev/null +++ b/test/types/ts-types.ts @@ -0,0 +1,17 @@ +import { assertEquals } from "@std/assert/equals"; +import { CoreTypeAddress } from "../../src/dif/definitions.ts"; +import { TS_TYPE } from "../../src/types/ts-types.ts"; + +Deno.test("convert simple TS type to DIF type", () => { + const ts_type = TS_TYPE`number`; + assertEquals(ts_type, CoreTypeAddress.decimal_f64); + + const ts_type2 = TS_TYPE`string`; + assertEquals(ts_type2, CoreTypeAddress.text); + + const ts_type3 = TS_TYPE`null`; + assertEquals(ts_type3, CoreTypeAddress.null); + + const ts_type4 = TS_TYPE`boolean`; + assertEquals(ts_type4, CoreTypeAddress.boolean); +});