diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 4d5688725..12608ad76 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -312,6 +312,136 @@ ] } }, + "@@aspect_rules_js+//npm:extensions.bzl%pnpm": { + "general": { + "bzlTransitiveDigest": "c1Q7VkLeafam8apaxHVrYHpOEETl0bYTUqiCLs3p7DE=", + "usagesDigest": "kbjSw2REjlSC0HtTZDf2p+l/dmiMt3NHLoiWEXYAoQI=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "pnpm": { + "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_rule", + "attributes": { + "package": "pnpm", + "version": "8.6.7", + "root_package": "", + "link_workspace": "", + "link_packages": {}, + "integrity": "sha512-vRIWpD/L4phf9Bk2o/O2TDR8fFoJnpYrp2TKqTIZF/qZ2/rgL3qKXzHofHgbXsinwMoSEigz28sqk3pQ+yMEQQ==", + "url": "", + "commit": "", + "patch_args": [ + "-p0" + ], + "patches": [], + "custom_postinstall": "", + "npm_auth": "", + "npm_auth_basic": "", + "npm_auth_username": "", + "npm_auth_password": "", + "lifecycle_hooks": [], + "extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])", + "generate_bzl_library_targets": false, + "extract_full_archive": true, + "exclude_package_contents": [], + "system_tar": "auto" + } + }, + "pnpm__links": { + "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_links", + "attributes": { + "package": "pnpm", + "version": "8.6.7", + "dev": false, + "root_package": "", + "link_packages": {}, + "deps": {}, + "transitive_closure": {}, + "lifecycle_build_target": false, + "lifecycle_hooks_env": [], + "lifecycle_hooks_execution_requirements": [ + "no-sandbox" + ], + "lifecycle_hooks_use_default_shell_env": false, + "bins": {}, + "package_visibility": [ + "//visibility:public" + ], + "replace_package": "", + "exclude_package_contents": [] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "aspect_bazel_lib+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "aspect_bazel_lib+", + "bazel_tools", + "bazel_tools" + ], + [ + "aspect_bazel_lib+", + "tar.bzl", + "tar.bzl+" + ], + [ + "aspect_rules_js+", + "aspect_bazel_lib", + "aspect_bazel_lib+" + ], + [ + "aspect_rules_js+", + "aspect_rules_js", + "aspect_rules_js+" + ], + [ + "aspect_rules_js+", + "bazel_features", + "bazel_features+" + ], + [ + "aspect_rules_js+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "aspect_rules_js+", + "bazel_tools", + "bazel_tools" + ], + [ + "bazel_features+", + "bazel_features_globals", + "bazel_features++version_extension+bazel_features_globals" + ], + [ + "bazel_features+", + "bazel_features_version", + "bazel_features++version_extension+bazel_features_version" + ], + [ + "tar.bzl+", + "aspect_bazel_lib", + "aspect_bazel_lib+" + ], + [ + "tar.bzl+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "tar.bzl+", + "tar.bzl", + "tar.bzl+" + ] + ] + } + }, "@@aspect_rules_ts+//ts:extensions.bzl%ext": { "general": { "bzlTransitiveDigest": "aVqwKoRPrSXO367SJABlye04kmpR/9VM2xiXB3nh3Ls=", @@ -346,6 +476,57 @@ ] } }, + "@@bazel_bats+//:extensions.bzl%bazel_bats_deps": { + "general": { + "bzlTransitiveDigest": "dMxpmRKtIYzMONCZV8oDEdFhT7orcOnBrTyMoCa+ppA=", + "usagesDigest": "t+ex/oJhALiD6nk6VMpJT83/RVXO6TaIVwWMSo4V6sA=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "bats_core": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file_content": "\nsh_library(\n name = \"bats_lib\",\n srcs = glob([\"libexec/**\"]),\n)\n\nsh_binary(\n name = \"bats\",\n srcs = [\"bin/bats\"],\n visibility = [\"//visibility:public\"],\n deps = [\":bats_lib\"],\n)\n\nsh_library(\n name = \"file_setup_teardown_lib\",\n srcs = [\"test/file_setup_teardown.bats\"],\n visibility = [\"//visibility:public\"],\n data = glob([\"test/fixtures/file_setup_teardown/**\"]),\n)\n\nsh_library(\n name = \"junit_formatter_lib\",\n srcs = [\"test/junit-formatter.bats\"],\n visibility = [\"//visibility:public\"],\n data = glob([\"test/fixtures/junit-formatter/**\"]),\n)\n\nsh_library(\n name = \"parallel_lib\",\n srcs = [\"test/parallel.bats\"],\n visibility = [\"//visibility:public\"],\n data = glob([\n \"test/concurrent-coordination.bash\",\n \"test/fixtures/parallel/**\",\n ]),\n)\n\nsh_library(\n name = \"run_lib\",\n srcs = [\"test/run.bats\"],\n visibility = [\"//visibility:public\"],\n data = glob([\"test/fixtures/run/**\"]),\n)\n\nsh_library(\n name = \"suite_lib\",\n srcs = [\"test/suite.bats\"],\n visibility = [\"//visibility:public\"],\n data = glob([\"test/fixtures/suite/**\"]),\n)\n\nsh_library(\n name = \"test_helper\",\n srcs = [\"test/test_helper.bash\"],\n visibility = [\"//visibility:public\"],\n)\n\nsh_library(\n name = \"trace_lib\",\n srcs = [\"test/trace.bats\"],\n visibility = [\"//visibility:public\"],\n data = glob([\"test/fixtures/trace/**\"]),\n)\n\nexports_files(glob([\"test/*.bats\"]))\n", + "urls": [ + "https://github.com/bats-core/bats-core/archive/refs/tags/v1.7.0.tar.gz" + ], + "strip_prefix": "bats-core-1.7.0", + "sha256": "ac70c2a153f108b1ac549c2eaa4154dea4a7c1cc421e3352f0ce6ea49435454e" + } + }, + "bats_assert": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file_content": "\nfilegroup(\n name = \"load_files\",\n srcs = [\n \"load.bash\",\n ] + glob([\n \"src/**/*.bash\",\n ]),\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "15dbf1abb98db785323b9327c86ee2b3114541fe5aa150c410a1632ec06d9903", + "strip_prefix": "bats-assert-2.0.0", + "urls": [ + "https://github.com/bats-core/bats-assert/archive/refs/tags/v2.0.0.tar.gz" + ] + } + }, + "bats_support": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file_content": "\nfilegroup(\n name = \"load_files\",\n srcs = [\n \"load.bash\",\n ] + glob([\n \"src/**/*.bash\",\n ]),\n visibility = [\"//visibility:public\"],\n)\n", + "sha256": "7815237aafeb42ddcc1b8c698fc5808026d33317d8701d5ec2396e9634e2918f", + "strip_prefix": "bats-support-0.3.0", + "urls": [ + "https://github.com/bats-core/bats-support/archive/refs/tags/v0.3.0.tar.gz" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "bazel_bats+", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "@@buildifier_prebuilt+//:defs.bzl%buildifier_prebuilt_deps_extension": { "general": { "bzlTransitiveDigest": "u4exIm/Ie7MnBJpWnXNoPRCqYpyYnW2ns2kmg+V/3To=", @@ -501,6 +682,40 @@ "recordedRepoMappingEntries": [] } }, + "@@pybind11_bazel+//:python_configure.bzl%extension": { + "general": { + "bzlTransitiveDigest": "OMjJ8aOAn337bDg7jdyvF/juIrC2PpUcX6Dnf+nhcF0=", + "usagesDigest": "fycyB39YnXIJkfWCIXLUKJMZzANcuLy9ZE73hRucjFk=", + "recordedFileInputs": { + "@@pybind11_bazel+//MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e" + }, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_python": { + "repoRuleId": "@@pybind11_bazel+//:python_configure.bzl%python_configure", + "attributes": {} + }, + "pybind11": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file": "@@pybind11_bazel+//:pybind11.BUILD", + "strip_prefix": "pybind11-2.11.1", + "urls": [ + "https://github.com/pybind/pybind11/archive/v2.11.1.zip" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "pybind11_bazel+", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "@@rules_android+//bzlmod_extensions:apksig.bzl%apksig_extension": { "general": { "bzlTransitiveDigest": "6NJSHjev77G4XUXqswn+uU8LwqBD5JUgxtcFMWhBJUQ=", @@ -923,6 +1138,89 @@ ] } }, + "@@rules_fuzzing+//fuzzing/private:extensions.bzl%non_module_dependencies": { + "general": { + "bzlTransitiveDigest": "lxvzPQyluk241QRYY81nZHOcv5Id/5U2y6dp42qibis=", + "usagesDigest": "wy6ISK6UOcBEjj/mvJ/S3WeXoO67X+1llb9yPyFtPgc=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "platforms": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "urls": [ + "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz", + "https://github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz" + ], + "sha256": "8150406605389ececb6da07cbcb509d5637a3ab9a24bc69b1101531367d89d74" + } + }, + "rules_python": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "d70cd72a7a4880f0000a6346253414825c19cdd40a28289bdf67b8e6480edff8", + "strip_prefix": "rules_python-0.28.0", + "url": "https://github.com/bazelbuild/rules_python/releases/download/0.28.0/rules_python-0.28.0.tar.gz" + } + }, + "bazel_skylib": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "cd55a062e763b9349921f0f5db8c3933288dc8ba4f76dd9416aac68acee3cb94", + "urls": [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz" + ] + } + }, + "com_google_absl": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "urls": [ + "https://github.com/abseil/abseil-cpp/archive/refs/tags/20240116.1.zip" + ], + "strip_prefix": "abseil-cpp-20240116.1", + "integrity": "sha256-7capMWOvWyoYbUaHF/b+I2U6XLMaHmky8KugWvfXYuk=" + } + }, + "rules_fuzzing_oss_fuzz": { + "repoRuleId": "@@rules_fuzzing+//fuzzing/private/oss_fuzz:repository.bzl%oss_fuzz_repository", + "attributes": {} + }, + "honggfuzz": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file": "@@rules_fuzzing+//:honggfuzz.BUILD", + "sha256": "6b18ba13bc1f36b7b950c72d80f19ea67fbadc0ac0bb297ec89ad91f2eaa423e", + "url": "https://github.com/google/honggfuzz/archive/2.5.zip", + "strip_prefix": "honggfuzz-2.5" + } + }, + "rules_fuzzing_jazzer": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar", + "attributes": { + "sha256": "ee6feb569d88962d59cb59e8a31eb9d007c82683f3ebc64955fd5b96f277eec2", + "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer/0.20.1/jazzer-0.20.1.jar" + } + }, + "rules_fuzzing_jazzer_api": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar", + "attributes": { + "sha256": "f5a60242bc408f7fa20fccf10d6c5c5ea1fcb3c6f44642fec5af88373ae7aa1b", + "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer-api/0.20.1/jazzer-api-0.20.1.jar" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_fuzzing+", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { "general": { "bzlTransitiveDigest": "CgSFQ7VRhs6G8nojJKNB6szAhYnHEblrCU/AruTOxtw=", @@ -1150,6 +1448,2655 @@ "recordedRepoMappingEntries": [] } }, + "@@rules_python+//python/extensions:pip.bzl%pip": { + "general": { + "bzlTransitiveDigest": "+Axnk/K7KLa1phIMd8uP2eZjKDtvrIX/ipSZZnlu/Mg=", + "usagesDigest": "ngGqXeODRuOjHb6v0DsJ4oyFvV0nhcFNXcy7gVm7bEc=", + "recordedFileInputs": { + "@@protobuf+//python/requirements.txt": "983be60d3cec4b319dcab6d48aeb3f5b2f7c3350f26b3a9e97486c37967c73c5", + "@@rules_fuzzing+//fuzzing/requirements.txt": "ab04664be026b632a0d2a2446c4f65982b7654f5b6851d2f9d399a19b7242a5b", + "@@rules_python+//tools/publish/requirements_darwin.txt": "095d4a4f3d639dce831cd493367631cd51b53665292ab20194bac2c0c6458fa8", + "@@rules_python+//tools/publish/requirements_linux.txt": "d576e0d8542df61396a9b38deeaa183c24135ed5e8e73bb9622f298f2671811e", + "@@rules_python+//tools/publish/requirements_windows.txt": "d18538a3982beab378fd5687f4db33162ee1ece69801f9a451661b1b64286b76" + }, + "recordedDirentsInputs": {}, + "envVariables": { + "RULES_PYTHON_REPO_DEBUG": null, + "RULES_PYTHON_REPO_DEBUG_VERBOSITY": null + }, + "generatedRepoSpecs": { + "pip_deps_310_numpy": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_10_host//:python", + "repo": "pip_deps_310", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_310_setuptools": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_10_host//:python", + "repo": "pip_deps_310", + "requirement": "setuptools<=70.3.0" + } + }, + "pip_deps_311_numpy": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "pip_deps_311", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_311_setuptools": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "pip_deps_311", + "requirement": "setuptools<=70.3.0" + } + }, + "pip_deps_312_numpy": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_12_host//:python", + "repo": "pip_deps_312", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_312_setuptools": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_12_host//:python", + "repo": "pip_deps_312", + "requirement": "setuptools<=70.3.0" + } + }, + "pip_deps_38_numpy": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_8_host//:python", + "repo": "pip_deps_38", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_38_setuptools": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_8_host//:python", + "repo": "pip_deps_38", + "requirement": "setuptools<=70.3.0" + } + }, + "pip_deps_39_numpy": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_9_host//:python", + "repo": "pip_deps_39", + "requirement": "numpy<=1.26.1" + } + }, + "pip_deps_39_setuptools": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@pip_deps//{name}:{target}", + "python_interpreter_target": "@@rules_python++python+python_3_9_host//:python", + "repo": "pip_deps_39", + "requirement": "setuptools<=70.3.0" + } + }, + "rules_fuzzing_py_deps_310_absl_py": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_10_host//:python", + "repo": "rules_fuzzing_py_deps_310", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_310_six": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_10_host//:python", + "repo": "rules_fuzzing_py_deps_310", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_fuzzing_py_deps_311_absl_py": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_fuzzing_py_deps_311", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_311_six": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_fuzzing_py_deps_311", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_fuzzing_py_deps_312_absl_py": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_12_host//:python", + "repo": "rules_fuzzing_py_deps_312", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_312_six": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_12_host//:python", + "repo": "rules_fuzzing_py_deps_312", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_fuzzing_py_deps_38_absl_py": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_8_host//:python", + "repo": "rules_fuzzing_py_deps_38", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_38_six": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_8_host//:python", + "repo": "rules_fuzzing_py_deps_38", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_fuzzing_py_deps_39_absl_py": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_9_host//:python", + "repo": "rules_fuzzing_py_deps_39", + "requirement": "absl-py==2.0.0 --hash=sha256:9a28abb62774ae4e8edbe2dd4c49ffcd45a6a848952a5eccc6a49f3f0fc1e2f3" + } + }, + "rules_fuzzing_py_deps_39_six": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_fuzzing_py_deps//{name}:{target}", + "extra_pip_args": [ + "--require-hashes" + ], + "python_interpreter_target": "@@rules_python++python+python_3_9_host//:python", + "repo": "rules_fuzzing_py_deps_39", + "requirement": "six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + } + }, + "rules_python_publish_deps_311_backports_tarfile_py3_none_any_77e284d7": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "backports.tarfile-1.2.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "backports-tarfile==1.2.0", + "sha256": "77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", + "urls": [ + "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_backports_tarfile_sdist_d75e02c2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "backports_tarfile-1.2.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "backports-tarfile==1.2.0", + "sha256": "d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", + "urls": [ + "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_certifi_py3_none_any_922820b5": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "certifi-2024.8.30-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "certifi==2024.8.30", + "sha256": "922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "urls": [ + "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_certifi_sdist_bec941d2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "certifi-2024.8.30.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "certifi==2024.8.30", + "sha256": "bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", + "urls": [ + "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_aarch64_a1ed2dd2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "urls": [ + "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_s390x_a24ed04c": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "urls": [ + "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_x86_64_610faea7": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "urls": [ + "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_aarch64_a9b15d49": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "urls": [ + "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_x86_64_fc48c783": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", + "urls": [ + "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_sdist_1c39c601": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "cffi-1.17.1.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "urls": [ + "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_universal2_0d99dd8f": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "urls": [ + "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_x86_64_c57516e5": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "urls": [ + "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_11_0_arm64_6dba5d19": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "urls": [ + "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_aarch64_bf4475b8": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "urls": [ + "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_s390x_8ff4e7cd": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "urls": [ + "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_x86_64_3710a975": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "urls": [ + "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_aarch64_47334db7": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "urls": [ + "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_s390x_63bc5c4a": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "urls": [ + "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_x86_64_bcb4f8ea": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "urls": [ + "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_win_amd64_cee4373f": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "urls": [ + "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_py3_none_any_fe9f97fe": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "urls": [ + "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_sdist_223217c3": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "charset_normalizer-3.4.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "urls": [ + "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_aarch64_846da004": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "urls": [ + "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_x86_64_0f996e72": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "urls": [ + "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_aarch64_f7b178f1": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", + "urls": [ + "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_x86_64_c2e6fc39": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "urls": [ + "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_aarch64_e1be4655": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "urls": [ + "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_x86_64_df6b6c6d": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "urls": [ + "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_sdist_315b9001": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "cryptography-43.0.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "urls": [ + "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_docutils_py3_none_any_dafca5b9": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "docutils-0.21.2-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "docutils==0.21.2", + "sha256": "dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", + "urls": [ + "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_docutils_sdist_3a6b1873": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "docutils-0.21.2.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "docutils==0.21.2", + "sha256": "3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", + "urls": [ + "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_idna_py3_none_any_946d195a": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "idna-3.10-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "idna==3.10", + "sha256": "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", + "urls": [ + "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_idna_sdist_12f65c9b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "idna-3.10.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "idna==3.10", + "sha256": "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "urls": [ + "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_importlib_metadata_py3_none_any_45e54197": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "importlib_metadata-8.5.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "importlib-metadata==8.5.0", + "sha256": "45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", + "urls": [ + "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_importlib_metadata_sdist_71522656": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "importlib_metadata-8.5.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "importlib-metadata==8.5.0", + "sha256": "71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", + "urls": [ + "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jaraco_classes_py3_none_any_f662826b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "jaraco.classes-3.4.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-classes==3.4.0", + "sha256": "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", + "urls": [ + "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jaraco_classes_sdist_47a024b5": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jaraco.classes-3.4.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-classes==3.4.0", + "sha256": "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "urls": [ + "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jaraco_context_py3_none_any_f797fc48": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "jaraco.context-6.0.1-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-context==6.0.1", + "sha256": "f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", + "urls": [ + "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jaraco_context_sdist_9bae4ea5": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jaraco_context-6.0.1.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-context==6.0.1", + "sha256": "9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "urls": [ + "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jaraco_functools_py3_none_any_ad159f13": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "jaraco.functools-4.1.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-functools==4.1.0", + "sha256": "ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", + "urls": [ + "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jaraco_functools_sdist_70f7e0e2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jaraco_functools-4.1.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-functools==4.1.0", + "sha256": "70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", + "urls": [ + "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jeepney_py3_none_any_c0a454ad": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "jeepney-0.8.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jeepney==0.8.0", + "sha256": "c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", + "urls": [ + "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jeepney_sdist_5efe48d2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jeepney-0.8.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jeepney==0.8.0", + "sha256": "5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "urls": [ + "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_keyring_py3_none_any_5426f817": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "keyring-25.4.1-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "keyring==25.4.1", + "sha256": "5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf", + "urls": [ + "https://files.pythonhosted.org/packages/83/25/e6d59e5f0a0508d0dca8bb98c7f7fd3772fc943ac3f53d5ab18a218d32c0/keyring-25.4.1-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_keyring_sdist_b07ebc55": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "keyring-25.4.1.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "keyring==25.4.1", + "sha256": "b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b", + "urls": [ + "https://files.pythonhosted.org/packages/a5/1c/2bdbcfd5d59dc6274ffb175bc29aa07ecbfab196830e0cfbde7bd861a2ea/keyring-25.4.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_markdown_it_py_py3_none_any_35521684": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "markdown_it_py-3.0.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "markdown-it-py==3.0.0", + "sha256": "355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "urls": [ + "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_markdown_it_py_sdist_e3f60a94": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "markdown-it-py-3.0.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "markdown-it-py==3.0.0", + "sha256": "e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", + "urls": [ + "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_mdurl_py3_none_any_84008a41": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "mdurl-0.1.2-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "mdurl==0.1.2", + "sha256": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "urls": [ + "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_mdurl_sdist_bb413d29": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "mdurl-0.1.2.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "mdurl==0.1.2", + "sha256": "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", + "urls": [ + "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_more_itertools_py3_none_any_037b0d32": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "more_itertools-10.5.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "more-itertools==10.5.0", + "sha256": "037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", + "urls": [ + "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_more_itertools_sdist_5482bfef": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "more-itertools-10.5.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "more-itertools==10.5.0", + "sha256": "5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", + "urls": [ + "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_14c5a72e": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", + "urls": [ + "https://files.pythonhosted.org/packages/b3/89/1daff5d9ba5a95a157c092c7c5f39b8dd2b1ddb4559966f808d31cfb67e0/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_7b7c2a3c": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", + "urls": [ + "https://files.pythonhosted.org/packages/2c/b6/42fc3c69cabf86b6b81e4c051a9b6e249c5ba9f8155590222c2622961f58/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_aarch64_42c64511": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", + "urls": [ + "https://files.pythonhosted.org/packages/45/b9/833f385403abaf0023c6547389ec7a7acf141ddd9d1f21573723a6eab39a/nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_armv7l_0411beb0": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", + "urls": [ + "https://files.pythonhosted.org/packages/05/2b/85977d9e11713b5747595ee61f381bc820749daf83f07b90b6c9964cf932/nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64_5f36b271": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", + "urls": [ + "https://files.pythonhosted.org/packages/72/f2/5c894d5265ab80a97c68ca36f25c8f6f0308abac649aaf152b74e7e854a8/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_s390x_19aaba96": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", + "urls": [ + "https://files.pythonhosted.org/packages/c2/a8/3bb02d0c60a03ad3a112b76c46971e9480efa98a8946677b5a59f60130ca/nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_x86_64_de3ceed6": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", + "urls": [ + "https://files.pythonhosted.org/packages/1b/63/6ab90d0e5225ab9780f6c9fb52254fa36b52bb7c188df9201d05b647e5e1/nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_aarch64_f0eca9ca": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe", + "urls": [ + "https://files.pythonhosted.org/packages/a3/da/0c4e282bc3cff4a0adf37005fa1fb42257673fbc1bbf7d1ff639ec3d255a/nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_armv7l_3a157ab1": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", + "urls": [ + "https://files.pythonhosted.org/packages/de/81/c291231463d21da5f8bba82c8167a6d6893cc5419b0639801ee5d3aeb8a9/nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_x86_64_36c95d4b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", + "urls": [ + "https://files.pythonhosted.org/packages/eb/61/73a007c74c37895fdf66e0edcd881f5eaa17a348ff02f4bb4bc906d61085/nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_win_amd64_8ce0f819": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-win_amd64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", + "urls": [ + "https://files.pythonhosted.org/packages/26/8d/53c5b19c4999bdc6ba95f246f4ef35ca83d7d7423e5e38be43ad66544e5d/nh3-0.2.18-cp37-abi3-win_amd64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_sdist_94a16692": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "nh3-0.2.18.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", + "urls": [ + "https://files.pythonhosted.org/packages/62/73/10df50b42ddb547a907deeb2f3c9823022580a7a47281e8eae8e003a9639/nh3-0.2.18.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pkginfo_py3_none_any_889a6da2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "pkginfo-1.10.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pkginfo==1.10.0", + "sha256": "889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", + "urls": [ + "https://files.pythonhosted.org/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pkginfo_sdist_5df73835": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pkginfo-1.10.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pkginfo==1.10.0", + "sha256": "5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", + "urls": [ + "https://files.pythonhosted.org/packages/2f/72/347ec5be4adc85c182ed2823d8d1c7b51e13b9a6b0c1aae59582eca652df/pkginfo-1.10.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pycparser_py3_none_any_c3702b6d": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "pycparser-2.22-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pycparser==2.22", + "sha256": "c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", + "urls": [ + "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pycparser_sdist_491c8be9": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pycparser-2.22.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pycparser==2.22", + "sha256": "491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "urls": [ + "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pygments_py3_none_any_b8e6aca0": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "pygments-2.18.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pygments==2.18.0", + "sha256": "b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", + "urls": [ + "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pygments_sdist_786ff802": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pygments-2.18.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pygments==2.18.0", + "sha256": "786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "urls": [ + "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pywin32_ctypes_py3_none_any_8a151337": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_windows_x86_64" + ], + "filename": "pywin32_ctypes-0.2.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pywin32-ctypes==0.2.3", + "sha256": "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", + "urls": [ + "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pywin32_ctypes_sdist_d162dc04": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pywin32-ctypes-0.2.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pywin32-ctypes==0.2.3", + "sha256": "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", + "urls": [ + "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_readme_renderer_py3_none_any_2fbca89b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "readme_renderer-44.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "readme-renderer==44.0", + "sha256": "2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", + "urls": [ + "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_readme_renderer_sdist_8712034e": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "readme_renderer-44.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "readme-renderer==44.0", + "sha256": "8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", + "urls": [ + "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_requests_py3_none_any_70761cfe": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "requests-2.32.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests==2.32.3", + "sha256": "70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", + "urls": [ + "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_requests_sdist_55365417": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "requests-2.32.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests==2.32.3", + "sha256": "55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "urls": [ + "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_requests_toolbelt_py2_none_any_cccfdd66": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "requests_toolbelt-1.0.0-py2.py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests-toolbelt==1.0.0", + "sha256": "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", + "urls": [ + "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_requests_toolbelt_sdist_7681a0a3": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "requests-toolbelt-1.0.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests-toolbelt==1.0.0", + "sha256": "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "urls": [ + "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_rfc3986_py2_none_any_50b1502b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "rfc3986-2.0.0-py2.py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rfc3986==2.0.0", + "sha256": "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "urls": [ + "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_rfc3986_sdist_97aacf9d": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "rfc3986-2.0.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rfc3986==2.0.0", + "sha256": "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", + "urls": [ + "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_rich_py3_none_any_6049d5e6": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "rich-13.9.4-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rich==13.9.4", + "sha256": "6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", + "urls": [ + "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_rich_sdist_43959497": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "rich-13.9.4.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rich==13.9.4", + "sha256": "439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", + "urls": [ + "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_secretstorage_py3_none_any_f356e662": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "SecretStorage-3.3.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "secretstorage==3.3.3", + "sha256": "f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", + "urls": [ + "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_secretstorage_sdist_2403533e": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "SecretStorage-3.3.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "secretstorage==3.3.3", + "sha256": "2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "urls": [ + "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_twine_py3_none_any_215dbe7b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "twine-5.1.1-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "twine==5.1.1", + "sha256": "215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", + "urls": [ + "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_twine_sdist_9aa08251": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "twine-5.1.1.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "twine==5.1.1", + "sha256": "9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db", + "urls": [ + "https://files.pythonhosted.org/packages/77/68/bd982e5e949ef8334e6f7dcf76ae40922a8750aa2e347291ae1477a4782b/twine-5.1.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_urllib3_py3_none_any_ca899ca0": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "urllib3-2.2.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "urllib3==2.2.3", + "sha256": "ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "urls": [ + "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_urllib3_sdist_e7d814a8": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "urllib3-2.2.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "urllib3==2.2.3", + "sha256": "e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", + "urls": [ + "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_zipp_py3_none_any_a817ac80": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "zipp-3.20.2-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "zipp==3.20.2", + "sha256": "a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", + "urls": [ + "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_zipp_sdist_bc9eb26f": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "zipp-3.20.2.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "zipp==3.20.2", + "sha256": "bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", + "urls": [ + "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz" + ] + } + }, + "pip_deps": { + "repoRuleId": "@@rules_python+//python/private/pypi:hub_repository.bzl%hub_repository", + "attributes": { + "repo_name": "pip_deps", + "extra_hub_aliases": {}, + "whl_map": { + "numpy": "{\"pip_deps_310_numpy\":[{\"version\":\"3.10\"}],\"pip_deps_311_numpy\":[{\"version\":\"3.11\"}],\"pip_deps_312_numpy\":[{\"version\":\"3.12\"}],\"pip_deps_38_numpy\":[{\"version\":\"3.8\"}],\"pip_deps_39_numpy\":[{\"version\":\"3.9\"}]}", + "setuptools": "{\"pip_deps_310_setuptools\":[{\"version\":\"3.10\"}],\"pip_deps_311_setuptools\":[{\"version\":\"3.11\"}],\"pip_deps_312_setuptools\":[{\"version\":\"3.12\"}],\"pip_deps_38_setuptools\":[{\"version\":\"3.8\"}],\"pip_deps_39_setuptools\":[{\"version\":\"3.9\"}]}" + }, + "packages": [ + "numpy", + "setuptools" + ], + "groups": {} + } + }, + "rules_fuzzing_py_deps": { + "repoRuleId": "@@rules_python+//python/private/pypi:hub_repository.bzl%hub_repository", + "attributes": { + "repo_name": "rules_fuzzing_py_deps", + "extra_hub_aliases": {}, + "whl_map": { + "absl_py": "{\"rules_fuzzing_py_deps_310_absl_py\":[{\"version\":\"3.10\"}],\"rules_fuzzing_py_deps_311_absl_py\":[{\"version\":\"3.11\"}],\"rules_fuzzing_py_deps_312_absl_py\":[{\"version\":\"3.12\"}],\"rules_fuzzing_py_deps_38_absl_py\":[{\"version\":\"3.8\"}],\"rules_fuzzing_py_deps_39_absl_py\":[{\"version\":\"3.9\"}]}", + "six": "{\"rules_fuzzing_py_deps_310_six\":[{\"version\":\"3.10\"}],\"rules_fuzzing_py_deps_311_six\":[{\"version\":\"3.11\"}],\"rules_fuzzing_py_deps_312_six\":[{\"version\":\"3.12\"}],\"rules_fuzzing_py_deps_38_six\":[{\"version\":\"3.8\"}],\"rules_fuzzing_py_deps_39_six\":[{\"version\":\"3.9\"}]}" + }, + "packages": [ + "absl_py", + "six" + ], + "groups": {} + } + }, + "rules_python_publish_deps": { + "repoRuleId": "@@rules_python+//python/private/pypi:hub_repository.bzl%hub_repository", + "attributes": { + "repo_name": "rules_python_publish_deps", + "extra_hub_aliases": {}, + "whl_map": { + "backports_tarfile": "{\"rules_python_publish_deps_311_backports_tarfile_py3_none_any_77e284d7\":[{\"filename\":\"backports.tarfile-1.2.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_backports_tarfile_sdist_d75e02c2\":[{\"filename\":\"backports_tarfile-1.2.0.tar.gz\",\"version\":\"3.11\"}]}", + "certifi": "{\"rules_python_publish_deps_311_certifi_py3_none_any_922820b5\":[{\"filename\":\"certifi-2024.8.30-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_certifi_sdist_bec941d2\":[{\"filename\":\"certifi-2024.8.30.tar.gz\",\"version\":\"3.11\"}]}", + "cffi": "{\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_aarch64_a1ed2dd2\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_s390x_a24ed04c\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_x86_64_610faea7\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_aarch64_a9b15d49\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_x86_64_fc48c783\":[{\"filename\":\"cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cffi_sdist_1c39c601\":[{\"filename\":\"cffi-1.17.1.tar.gz\",\"version\":\"3.11\"}]}", + "charset_normalizer": "{\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_universal2_0d99dd8f\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_x86_64_c57516e5\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_11_0_arm64_6dba5d19\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_aarch64_bf4475b8\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_s390x_8ff4e7cd\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_x86_64_3710a975\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_aarch64_47334db7\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_s390x_63bc5c4a\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_x86_64_bcb4f8ea\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_win_amd64_cee4373f\":[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_py3_none_any_fe9f97fe\":[{\"filename\":\"charset_normalizer-3.4.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_charset_normalizer_sdist_223217c3\":[{\"filename\":\"charset_normalizer-3.4.0.tar.gz\",\"version\":\"3.11\"}]}", + "cryptography": "{\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_aarch64_846da004\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_x86_64_0f996e72\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_aarch64_f7b178f1\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_x86_64_c2e6fc39\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_aarch64_e1be4655\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_x86_64_df6b6c6d\":[{\"filename\":\"cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_cryptography_sdist_315b9001\":[{\"filename\":\"cryptography-43.0.3.tar.gz\",\"version\":\"3.11\"}]}", + "docutils": "{\"rules_python_publish_deps_311_docutils_py3_none_any_dafca5b9\":[{\"filename\":\"docutils-0.21.2-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_docutils_sdist_3a6b1873\":[{\"filename\":\"docutils-0.21.2.tar.gz\",\"version\":\"3.11\"}]}", + "idna": "{\"rules_python_publish_deps_311_idna_py3_none_any_946d195a\":[{\"filename\":\"idna-3.10-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_idna_sdist_12f65c9b\":[{\"filename\":\"idna-3.10.tar.gz\",\"version\":\"3.11\"}]}", + "importlib_metadata": "{\"rules_python_publish_deps_311_importlib_metadata_py3_none_any_45e54197\":[{\"filename\":\"importlib_metadata-8.5.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_importlib_metadata_sdist_71522656\":[{\"filename\":\"importlib_metadata-8.5.0.tar.gz\",\"version\":\"3.11\"}]}", + "jaraco_classes": "{\"rules_python_publish_deps_311_jaraco_classes_py3_none_any_f662826b\":[{\"filename\":\"jaraco.classes-3.4.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_jaraco_classes_sdist_47a024b5\":[{\"filename\":\"jaraco.classes-3.4.0.tar.gz\",\"version\":\"3.11\"}]}", + "jaraco_context": "{\"rules_python_publish_deps_311_jaraco_context_py3_none_any_f797fc48\":[{\"filename\":\"jaraco.context-6.0.1-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_jaraco_context_sdist_9bae4ea5\":[{\"filename\":\"jaraco_context-6.0.1.tar.gz\",\"version\":\"3.11\"}]}", + "jaraco_functools": "{\"rules_python_publish_deps_311_jaraco_functools_py3_none_any_ad159f13\":[{\"filename\":\"jaraco.functools-4.1.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_jaraco_functools_sdist_70f7e0e2\":[{\"filename\":\"jaraco_functools-4.1.0.tar.gz\",\"version\":\"3.11\"}]}", + "jeepney": "{\"rules_python_publish_deps_311_jeepney_py3_none_any_c0a454ad\":[{\"filename\":\"jeepney-0.8.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_jeepney_sdist_5efe48d2\":[{\"filename\":\"jeepney-0.8.0.tar.gz\",\"version\":\"3.11\"}]}", + "keyring": "{\"rules_python_publish_deps_311_keyring_py3_none_any_5426f817\":[{\"filename\":\"keyring-25.4.1-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_keyring_sdist_b07ebc55\":[{\"filename\":\"keyring-25.4.1.tar.gz\",\"version\":\"3.11\"}]}", + "markdown_it_py": "{\"rules_python_publish_deps_311_markdown_it_py_py3_none_any_35521684\":[{\"filename\":\"markdown_it_py-3.0.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_markdown_it_py_sdist_e3f60a94\":[{\"filename\":\"markdown-it-py-3.0.0.tar.gz\",\"version\":\"3.11\"}]}", + "mdurl": "{\"rules_python_publish_deps_311_mdurl_py3_none_any_84008a41\":[{\"filename\":\"mdurl-0.1.2-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_mdurl_sdist_bb413d29\":[{\"filename\":\"mdurl-0.1.2.tar.gz\",\"version\":\"3.11\"}]}", + "more_itertools": "{\"rules_python_publish_deps_311_more_itertools_py3_none_any_037b0d32\":[{\"filename\":\"more_itertools-10.5.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_more_itertools_sdist_5482bfef\":[{\"filename\":\"more-itertools-10.5.0.tar.gz\",\"version\":\"3.11\"}]}", + "nh3": "{\"rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_14c5a72e\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_7b7c2a3c\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_aarch64_42c64511\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_armv7l_0411beb0\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64_5f36b271\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_s390x_19aaba96\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_x86_64_de3ceed6\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_aarch64_f0eca9ca\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_armv7l_3a157ab1\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_x86_64_36c95d4b\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_cp37_abi3_win_amd64_8ce0f819\":[{\"filename\":\"nh3-0.2.18-cp37-abi3-win_amd64.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_nh3_sdist_94a16692\":[{\"filename\":\"nh3-0.2.18.tar.gz\",\"version\":\"3.11\"}]}", + "pkginfo": "{\"rules_python_publish_deps_311_pkginfo_py3_none_any_889a6da2\":[{\"filename\":\"pkginfo-1.10.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_pkginfo_sdist_5df73835\":[{\"filename\":\"pkginfo-1.10.0.tar.gz\",\"version\":\"3.11\"}]}", + "pycparser": "{\"rules_python_publish_deps_311_pycparser_py3_none_any_c3702b6d\":[{\"filename\":\"pycparser-2.22-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_pycparser_sdist_491c8be9\":[{\"filename\":\"pycparser-2.22.tar.gz\",\"version\":\"3.11\"}]}", + "pygments": "{\"rules_python_publish_deps_311_pygments_py3_none_any_b8e6aca0\":[{\"filename\":\"pygments-2.18.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_pygments_sdist_786ff802\":[{\"filename\":\"pygments-2.18.0.tar.gz\",\"version\":\"3.11\"}]}", + "pywin32_ctypes": "{\"rules_python_publish_deps_311_pywin32_ctypes_py3_none_any_8a151337\":[{\"filename\":\"pywin32_ctypes-0.2.3-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_pywin32_ctypes_sdist_d162dc04\":[{\"filename\":\"pywin32-ctypes-0.2.3.tar.gz\",\"version\":\"3.11\"}]}", + "readme_renderer": "{\"rules_python_publish_deps_311_readme_renderer_py3_none_any_2fbca89b\":[{\"filename\":\"readme_renderer-44.0-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_readme_renderer_sdist_8712034e\":[{\"filename\":\"readme_renderer-44.0.tar.gz\",\"version\":\"3.11\"}]}", + "requests": "{\"rules_python_publish_deps_311_requests_py3_none_any_70761cfe\":[{\"filename\":\"requests-2.32.3-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_requests_sdist_55365417\":[{\"filename\":\"requests-2.32.3.tar.gz\",\"version\":\"3.11\"}]}", + "requests_toolbelt": "{\"rules_python_publish_deps_311_requests_toolbelt_py2_none_any_cccfdd66\":[{\"filename\":\"requests_toolbelt-1.0.0-py2.py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_requests_toolbelt_sdist_7681a0a3\":[{\"filename\":\"requests-toolbelt-1.0.0.tar.gz\",\"version\":\"3.11\"}]}", + "rfc3986": "{\"rules_python_publish_deps_311_rfc3986_py2_none_any_50b1502b\":[{\"filename\":\"rfc3986-2.0.0-py2.py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_rfc3986_sdist_97aacf9d\":[{\"filename\":\"rfc3986-2.0.0.tar.gz\",\"version\":\"3.11\"}]}", + "rich": "{\"rules_python_publish_deps_311_rich_py3_none_any_6049d5e6\":[{\"filename\":\"rich-13.9.4-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_rich_sdist_43959497\":[{\"filename\":\"rich-13.9.4.tar.gz\",\"version\":\"3.11\"}]}", + "secretstorage": "{\"rules_python_publish_deps_311_secretstorage_py3_none_any_f356e662\":[{\"filename\":\"SecretStorage-3.3.3-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_secretstorage_sdist_2403533e\":[{\"filename\":\"SecretStorage-3.3.3.tar.gz\",\"version\":\"3.11\"}]}", + "twine": "{\"rules_python_publish_deps_311_twine_py3_none_any_215dbe7b\":[{\"filename\":\"twine-5.1.1-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_twine_sdist_9aa08251\":[{\"filename\":\"twine-5.1.1.tar.gz\",\"version\":\"3.11\"}]}", + "urllib3": "{\"rules_python_publish_deps_311_urllib3_py3_none_any_ca899ca0\":[{\"filename\":\"urllib3-2.2.3-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_urllib3_sdist_e7d814a8\":[{\"filename\":\"urllib3-2.2.3.tar.gz\",\"version\":\"3.11\"}]}", + "zipp": "{\"rules_python_publish_deps_311_zipp_py3_none_any_a817ac80\":[{\"filename\":\"zipp-3.20.2-py3-none-any.whl\",\"version\":\"3.11\"}],\"rules_python_publish_deps_311_zipp_sdist_bc9eb26f\":[{\"filename\":\"zipp-3.20.2.tar.gz\",\"version\":\"3.11\"}]}" + }, + "packages": [ + "backports_tarfile", + "certifi", + "charset_normalizer", + "docutils", + "idna", + "importlib_metadata", + "jaraco_classes", + "jaraco_context", + "jaraco_functools", + "keyring", + "markdown_it_py", + "mdurl", + "more_itertools", + "nh3", + "pkginfo", + "pygments", + "readme_renderer", + "requests", + "requests_toolbelt", + "rfc3986", + "rich", + "twine", + "urllib3", + "zipp" + ], + "groups": {} + } + } + }, + "moduleExtensionMetadata": { + "useAllRepos": "NO", + "reproducible": false + }, + "recordedRepoMappingEntries": [ + [ + "bazel_features+", + "bazel_features_globals", + "bazel_features++version_extension+bazel_features_globals" + ], + [ + "bazel_features+", + "bazel_features_version", + "bazel_features++version_extension+bazel_features_version" + ], + [ + "rules_python+", + "bazel_features", + "bazel_features+" + ], + [ + "rules_python+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "rules_python+", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_python+", + "pypi__build", + "rules_python++internal_deps+pypi__build" + ], + [ + "rules_python+", + "pypi__click", + "rules_python++internal_deps+pypi__click" + ], + [ + "rules_python+", + "pypi__colorama", + "rules_python++internal_deps+pypi__colorama" + ], + [ + "rules_python+", + "pypi__importlib_metadata", + "rules_python++internal_deps+pypi__importlib_metadata" + ], + [ + "rules_python+", + "pypi__installer", + "rules_python++internal_deps+pypi__installer" + ], + [ + "rules_python+", + "pypi__more_itertools", + "rules_python++internal_deps+pypi__more_itertools" + ], + [ + "rules_python+", + "pypi__packaging", + "rules_python++internal_deps+pypi__packaging" + ], + [ + "rules_python+", + "pypi__pep517", + "rules_python++internal_deps+pypi__pep517" + ], + [ + "rules_python+", + "pypi__pip", + "rules_python++internal_deps+pypi__pip" + ], + [ + "rules_python+", + "pypi__pip_tools", + "rules_python++internal_deps+pypi__pip_tools" + ], + [ + "rules_python+", + "pypi__pyproject_hooks", + "rules_python++internal_deps+pypi__pyproject_hooks" + ], + [ + "rules_python+", + "pypi__setuptools", + "rules_python++internal_deps+pypi__setuptools" + ], + [ + "rules_python+", + "pypi__tomli", + "rules_python++internal_deps+pypi__tomli" + ], + [ + "rules_python+", + "pypi__wheel", + "rules_python++internal_deps+pypi__wheel" + ], + [ + "rules_python+", + "pypi__zipp", + "rules_python++internal_deps+pypi__zipp" + ], + [ + "rules_python+", + "pythons_hub", + "rules_python++python+pythons_hub" + ], + [ + "rules_python++python+pythons_hub", + "python_3_10_host", + "rules_python++python+python_3_10_host" + ], + [ + "rules_python++python+pythons_hub", + "python_3_11_host", + "rules_python++python+python_3_11_host" + ], + [ + "rules_python++python+pythons_hub", + "python_3_12_host", + "rules_python++python+python_3_12_host" + ], + [ + "rules_python++python+pythons_hub", + "python_3_8_host", + "rules_python++python+python_3_8_host" + ], + [ + "rules_python++python+pythons_hub", + "python_3_9_host", + "rules_python++python+python_3_9_host" + ] + ] + } + }, "@@rules_python+//python/uv:uv.bzl%uv": { "general": { "bzlTransitiveDigest": "Xpqjnjzy6zZ90Es9Wa888ZLHhn7IsNGbph/e6qoxzw8=", diff --git a/android/player/src/main/kotlin/com/intuit/playerui/android/asset/AssetRenderException.kt b/android/player/src/main/kotlin/com/intuit/playerui/android/asset/AssetRenderException.kt index e399680c8..0ff714ca2 100644 --- a/android/player/src/main/kotlin/com/intuit/playerui/android/asset/AssetRenderException.kt +++ b/android/player/src/main/kotlin/com/intuit/playerui/android/asset/AssetRenderException.kt @@ -1,9 +1,17 @@ package com.intuit.playerui.android.asset import com.intuit.playerui.android.AssetContext +import com.intuit.playerui.core.bridge.serialization.serializers.ThrowableSerializer +import com.intuit.playerui.core.error.ErrorSeverity +import com.intuit.playerui.core.error.ErrorTypes import com.intuit.playerui.core.player.PlayerException +import com.intuit.playerui.core.player.PlayerExceptionMetadata +import kotlinx.serialization.Serializable -class AssetRenderException : PlayerException { +@Serializable(ThrowableSerializer::class) +class AssetRenderException : + PlayerException, + PlayerExceptionMetadata { private var _assetParentPath: List = emptyList() var assetParentPath: List get() = _assetParentPath @@ -28,6 +36,16 @@ Caused by: ${exception.message} """.trimMargin() } initialMessage = "$errorMessage\nException occurred in asset with id '${rootAsset.id}' of type '${rootAsset.type}" + this.message = initialMessage this.rootAsset = rootAsset } + + override val type: String + get() = ErrorTypes.RENDER + override val severity: ErrorSeverity? + get() = ErrorSeverity.ERROR + override val metadata: Map? + get() = mapOf( + "assetId" to rootAsset.asset.id, + ) } diff --git a/android/player/src/main/kotlin/com/intuit/playerui/android/asset/SuspendableAsset.kt b/android/player/src/main/kotlin/com/intuit/playerui/android/asset/SuspendableAsset.kt index 5ce8a72de..0b7d5cf2c 100644 --- a/android/player/src/main/kotlin/com/intuit/playerui/android/asset/SuspendableAsset.kt +++ b/android/player/src/main/kotlin/com/intuit/playerui/android/asset/SuspendableAsset.kt @@ -52,7 +52,22 @@ public abstract class SuspendableAsset( } private suspend fun doInitView() = withContext(Dispatchers.Default) { - initView(getData()).apply { setTag(R.bool.view_hydrated, false) } + // TODO: Centralize some of this error handling so that it can be repeated easily. + try { + initView(getData()).apply { setTag(R.bool.view_hydrated, false) } + } catch (exception: Throwable) { + // ignore cancellation exceptions because those are used to rehydrate the view + if (exception is CancellationException) { + throw exception + } + + if (exception is AssetRenderException) { + exception.assetParentPath += assetContext + throw exception + } else { + throw AssetRenderException(assetContext, "Failed to render asset", exception) + } + } } // To be launched in Dispatchers.Main diff --git a/android/player/src/main/kotlin/com/intuit/playerui/android/compose/ComposableAsset.kt b/android/player/src/main/kotlin/com/intuit/playerui/android/compose/ComposableAsset.kt index 22358e6a9..ea7c44b45 100644 --- a/android/player/src/main/kotlin/com/intuit/playerui/android/compose/ComposableAsset.kt +++ b/android/player/src/main/kotlin/com/intuit/playerui/android/compose/ComposableAsset.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.viewinterop.AndroidView import com.intuit.playerui.android.AssetContext +import com.intuit.playerui.android.asset.AssetRenderException import com.intuit.playerui.android.asset.RenderableAsset import com.intuit.playerui.android.asset.SuspendableAsset import com.intuit.playerui.android.build @@ -24,9 +25,12 @@ import com.intuit.playerui.android.extensions.Styles import com.intuit.playerui.android.extensions.into import com.intuit.playerui.android.withContext import com.intuit.playerui.android.withTag +import com.intuit.playerui.core.error.ErrorTypes import com.intuit.playerui.core.experimental.ExperimentalPlayerApi +import com.intuit.playerui.core.player.state.inProgressState import kotlinx.coroutines.launch import kotlinx.serialization.KSerializer +import kotlin.coroutines.cancellation.CancellationException /** * Base class for assets that render using Jetpack Compose. @@ -53,7 +57,19 @@ public abstract class ComposableAsset( @Composable public fun compose(data: Data? = null) { val data: Data? by produceState(initialValue = data, key1 = this) { - value = getData() + try { + value = getData() + } catch (error: Throwable) { + if (error is CancellationException) { + throw error + } + + player.inProgressState?.controllers?.error?.captureError( + AssetRenderException(assetContext, "Error fetching data while rendering asset. See cause for details", error), + ErrorTypes.RENDER, + ) + null + } } data?.let { @@ -82,9 +98,12 @@ public abstract class ComposableAsset( styles: AssetStyle? = null, tag: String? = null, ) { - val assetTag = tag ?: asset.id - val containerModifier = Modifier.testTag(assetTag) then modifier - assetContext.withContext(LocalContext.current).withTag(assetTag).build().run { + val containerModifier = Modifier.testTag(tag ?: asset.id) then modifier + var context = assetContext.withContext(LocalContext.current) + if (tag != null) { + context = context.withTag(tag) + } + context.build().run { renewHydrationScope("Creating view within a ComposableAsset") when (this) { is ComposableAsset<*> -> CompositionLocalProvider( diff --git a/android/player/src/main/kotlin/com/intuit/playerui/android/lifecycle/PlayerViewModel.kt b/android/player/src/main/kotlin/com/intuit/playerui/android/lifecycle/PlayerViewModel.kt index f61efa898..08871b03a 100644 --- a/android/player/src/main/kotlin/com/intuit/playerui/android/lifecycle/PlayerViewModel.kt +++ b/android/player/src/main/kotlin/com/intuit/playerui/android/lifecycle/PlayerViewModel.kt @@ -8,6 +8,7 @@ import com.intuit.playerui.android.AndroidPlayer import com.intuit.playerui.android.AndroidPlayerPlugin import com.intuit.playerui.android.asset.RenderableAsset import com.intuit.playerui.core.bridge.runtime.Runtime +import com.intuit.playerui.core.error.ErrorTypes import com.intuit.playerui.core.experimental.ExperimentalPlayerApi import com.intuit.playerui.core.managed.AsyncFlowIterator import com.intuit.playerui.core.managed.AsyncIterationManager @@ -207,7 +208,10 @@ public open class PlayerViewModel( } public fun fail(cause: Throwable) { - player.inProgressState?.fail(cause) + player.inProgressState?.controllers?.error?.captureError( + cause, + ErrorTypes.RENDER, + ) } /** Helper to progress the [FlowManager] in within the [viewModelScope] */ @@ -225,5 +229,6 @@ public open class PlayerViewModel( } public inline fun PlayerViewModel.fail(message: String, cause: Throwable? = null) { - fail(PlayerException(message, cause)) + val playerException = cause as? PlayerException ?: PlayerException(message, cause) + fail(playerException) } diff --git a/codecov.yaml b/codecov.yaml index 41c36f76e..04c575a94 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -2,5 +2,9 @@ codecov: require_ci_to_pass: no # Post comment if there are changes in bundle size increases more than 1Kb comment: - require_bundle_changes: "bundle_increase" - bundle_change_threshold: "1Kb" \ No newline at end of file + require_bundle_changes: "bundle_increase" + bundle_change_threshold: "1Kb" + +# Ignore test utils in coverage +ignore: + - "jvm/testutils" diff --git a/core/player/src/__tests__/view.test.ts b/core/player/src/__tests__/view.test.ts index a75bcc873..c09b76383 100644 --- a/core/player/src/__tests__/view.test.ts +++ b/core/player/src/__tests__/view.test.ts @@ -3,7 +3,7 @@ import type { Flow, NavigationFlowViewState } from "@player-ui/types"; import type { FlowController } from "../controllers"; import TrackBindingPlugin from "./helpers/binding.plugin"; import type { InProgressState } from "../types"; -import { Player } from ".."; +import { ErrorSeverity, ErrorTypes, Player, PlayerPlugin } from ".."; import { ActionExpPlugin } from "./helpers/action-exp.plugin"; const minimal: Flow = { @@ -713,3 +713,42 @@ describe("view update scheduling", () => { }); }); }); + +describe("view error capturing", () => { + test("should capture errors caused during view resolution and send them to the errorController", async () => { + const errorControllerSpy = vitest.fn(() => undefined); + const viewFailurePlugin: PlayerPlugin = { + name: "ViewFailurePlugin", + apply: (player) => { + // Force resolution failures to capture in the view controller + player.hooks.view.tap("fail", (view) => { + view.hooks.resolver.tap("fail", (resolver) => { + resolver.hooks.beforeResolve.tap("fail", () => { + throw new Error("ERROR!"); + }); + }); + }); + + player.hooks.errorController.tap("fail", (controller) => { + controller.hooks.onError.tap("fail", errorControllerSpy); + }); + }, + }; + + const player = new Player({ plugins: [viewFailurePlugin] }); + player.start(minimal).catch(() => {}); + await vitest.waitFor(() => { + expect(errorControllerSpy).toHaveBeenCalledWith({ + error: expect.objectContaining({ + cause: new Error("ERROR!"), + }), + errorType: ErrorTypes.VIEW, + skipped: false, + metadata: { + node: expect.anything(), + }, + severity: ErrorSeverity.ERROR, + }); + }); + }); +}); diff --git a/core/player/src/controllers/error/__tests__/controller.test.ts b/core/player/src/controllers/error/__tests__/controller.test.ts index 97f4351a7..d5a2fe7da 100644 --- a/core/player/src/controllers/error/__tests__/controller.test.ts +++ b/core/player/src/controllers/error/__tests__/controller.test.ts @@ -1,10 +1,27 @@ import { describe, it, beforeEach, expect, vitest } from "vitest"; import { ErrorController } from "../controller"; -import { ErrorSeverity, ErrorTypes } from "../types"; +import { + ErrorMetadata, + ErrorSeverity, + ErrorTypes, + PlayerErrorMetadata, +} from "../types"; import type { DataController } from "../../data/controller"; import type { FlowController } from "../../flow/controller"; import type { Logger } from "../../../logger"; +/** Test class to create an error with any additional properties */ +class ErrorWithProps extends Error implements PlayerErrorMetadata { + constructor( + message: string, + public type: string, + public severity?: ErrorSeverity, + public metadata?: ErrorMetadata, + ) { + super(message); + } +} + describe("ErrorController", () => { let errorController: ErrorController; let mockDataController: DataController; @@ -99,6 +116,36 @@ describe("ErrorController", () => { }), ); }); + + it("should merge options from args and error object when avaiable", () => { + const error = new ErrorWithProps( + "Message", + ErrorTypes.EXPRESSION, + ErrorSeverity.FATAL, + { fromError: "value", overlap: "error" }, + ); + const err = errorController.captureError( + error, + ErrorTypes.VIEW, + ErrorSeverity.WARNING, + { + fromFunctionCall: "value", + overlap: "function", + }, + ); + + expect(err).toStrictEqual({ + skipped: false, + metadata: { + fromError: "value", + overlap: "error", + fromFunctionCall: "value", + }, + severity: ErrorSeverity.FATAL, + errorType: ErrorTypes.EXPRESSION, + error, + }); + }); }); describe("getCurrentError", () => { @@ -247,6 +294,11 @@ describe("ErrorController", () => { expect(observer2).not.toHaveBeenCalled(); // Execution stops after bail // Data model should not be updated when skipped expect(mockDataController.set).not.toHaveBeenCalled(); + expect(playerError).toStrictEqual( + expect.objectContaining({ + skipped: true, + }), + ); }); it("should continue to next plugin when undefined is returned", () => { diff --git a/core/player/src/controllers/error/__tests__/middleware.test.ts b/core/player/src/controllers/error/__tests__/middleware.test.ts index 22a94deb9..87d59781f 100644 --- a/core/player/src/controllers/error/__tests__/middleware.test.ts +++ b/core/player/src/controllers/error/__tests__/middleware.test.ts @@ -1,13 +1,19 @@ import { describe, it, beforeEach, expect, vitest } from "vitest"; -import { ErrorStateMiddleware } from "../middleware"; -import { BindingParser } from "../../../binding"; -import type { DataModelImpl } from "../../../data"; +import { ERROR_BINDING_PREFIX, ErrorStateMiddleware } from "../middleware"; +import { BindingInstance, BindingParser } from "../../../binding"; +import type { + BatchSetTransaction, + DataModelImpl, + DataModelOptions, +} from "../../../data"; import { LocalModel } from "../../../data"; import type { Logger } from "../../../logger"; describe("ErrorStateMiddleware", () => { let middleware: ErrorStateMiddleware; let baseDataModel: DataModelImpl; + // Shortcut to using middleware with baseDataModel as "next" + let pipelineModel: DataModelImpl; let mockLogger: Logger; let parser: BindingParser; let writeSymbol: symbol; @@ -33,19 +39,33 @@ describe("ErrorStateMiddleware", () => { set: () => undefined, evaluate: () => undefined, }); + + pipelineModel = { + get: (binding: BindingInstance, options?: DataModelOptions) => + middleware.get(binding, options, baseDataModel), + set: (transaction: BatchSetTransaction, options?: DataModelOptions) => + middleware.set(transaction, options, baseDataModel), + delete: (binding: BindingInstance, options?: DataModelOptions) => + middleware.delete(binding, options, baseDataModel), + }; }); describe("set", () => { + it("should not write to the base data model", () => { + const binding = parser.parse(ERROR_BINDING_PREFIX); + pipelineModel.set([[binding, { message: "test" }]], { + writeSymbol, + }); + + expect(pipelineModel.get(binding)).toStrictEqual({ message: "test" }); + expect(baseDataModel.get(binding)).toBeUndefined(); + }); it("should block writes to errorState without writeSymbol", () => { - const binding = parser.parse("errorState"); - const updates = middleware.set( - [[binding, { message: "test" }]], - undefined, - baseDataModel, - ); + const binding = parser.parse(ERROR_BINDING_PREFIX); + const updates = pipelineModel.set([[binding, { message: "test" }]]); // Should not write to base model - expect(baseDataModel.get(binding)).toBeUndefined(); + expect(pipelineModel.get(binding)).toBeUndefined(); // Should log warning expect(mockLogger.warn).toHaveBeenCalledWith( @@ -60,10 +80,10 @@ describe("ErrorStateMiddleware", () => { }); it("should block writes to nested errorState paths", () => { - const binding = parser.parse("errorState.message"); - middleware.set([[binding, "test message"]], undefined, baseDataModel); + const binding = parser.parse(`${ERROR_BINDING_PREFIX}.message`); + pipelineModel.set([[binding, "test message"]]); - expect(baseDataModel.get(binding)).toBeUndefined(); + expect(pipelineModel.get(binding)).toBeUndefined(); expect(mockLogger.warn).toHaveBeenCalledWith( expect.stringContaining( "Blocked write to protected path: errorState.message", @@ -73,65 +93,53 @@ describe("ErrorStateMiddleware", () => { it("should allow writes to other paths", () => { const binding = parser.parse("foo"); - const updates = middleware.set( - [[binding, "newValue"]], - undefined, - baseDataModel, - ); + const updates = pipelineModel.set([[binding, "newValue"]]); - expect(baseDataModel.get(binding)).toBe("newValue"); + expect(pipelineModel.get(binding)).toBe("newValue"); expect(mockLogger.warn).not.toHaveBeenCalled(); expect(updates.length).toBe(1); expect(updates[0]!.newValue).toBe("newValue"); }); it("should allow writes when authorized with writeSymbol", () => { - const binding = parser.parse("errorState"); + const binding = parser.parse(ERROR_BINDING_PREFIX); - const updates = middleware.set( - [[binding, { message: "test" }]], - { writeSymbol: writeSymbol }, - baseDataModel, - ); + const updates = pipelineModel.set([[binding, { message: "test" }]], { + writeSymbol: writeSymbol, + }); - expect(baseDataModel.get(binding)).toEqual({ message: "test" }); + expect(pipelineModel.get(binding)).toEqual({ message: "test" }); expect(mockLogger.warn).not.toHaveBeenCalled(); expect(updates.length).toBe(1); expect(updates[0]!.newValue).toEqual({ message: "test" }); }); it("should block writes with wrong writeSymbol", () => { - const binding = parser.parse("errorState"); + const binding = parser.parse(ERROR_BINDING_PREFIX); const wrongSymbol = Symbol("wrong-auth"); - middleware.set( - [[binding, { message: "test" }]], - { writeSymbol: wrongSymbol }, - baseDataModel, - ); + pipelineModel.set([[binding, { message: "test" }]], { + writeSymbol: wrongSymbol, + }); - expect(baseDataModel.get(binding)).toBeUndefined(); + expect(pipelineModel.get(binding)).toBeUndefined(); expect(mockLogger.warn).toHaveBeenCalled(); }); it("should handle mixed transactions with blocked and allowed paths", () => { - const errorBinding = parser.parse("errorState"); + const errorBinding = parser.parse(ERROR_BINDING_PREFIX); const fooBinding = parser.parse("foo"); - const updates = middleware.set( - [ - [errorBinding, { message: "blocked" }], - [fooBinding, "allowed"], - ], - undefined, - baseDataModel, - ); + const updates = pipelineModel.set([ + [errorBinding, { message: "blocked" }], + [fooBinding, "allowed"], + ]); // foo should be updated - expect(baseDataModel.get(fooBinding)).toBe("allowed"); + expect(pipelineModel.get(fooBinding)).toBe("allowed"); // errorState should not be updated - expect(baseDataModel.get(errorBinding)).toBeUndefined(); + expect(pipelineModel.get(errorBinding)).toBeUndefined(); // Should have logged warning for errorState expect(mockLogger.warn).toHaveBeenCalledWith( @@ -144,55 +152,62 @@ describe("ErrorStateMiddleware", () => { }); describe("get", () => { - it("should always allow reads", () => { - const binding = parser.parse("errorState"); + it("should not read error state from the base model", () => { + const binding = parser.parse(ERROR_BINDING_PREFIX); // Set value directly on base model baseDataModel.set([[binding, { message: "test" }]]); - const value = middleware.get(binding, undefined, baseDataModel); - expect(value).toEqual({ message: "test" }); + expect(pipelineModel.get(binding)).toBeUndefined(); + }); + + it("should read without needing any permissions", () => { + const binding = parser.parse(ERROR_BINDING_PREFIX); + pipelineModel.set([[binding, { message: "test" }]], { writeSymbol }); + + const value = pipelineModel.get(binding); + expect(value).toStrictEqual({ message: "test" }); }); }); describe("delete", () => { it("should block deletes to errorState without writeSymbol", () => { - const binding = parser.parse("errorState"); + const binding = parser.parse(ERROR_BINDING_PREFIX); // Set value first - baseDataModel.set([[binding, { message: "test" }]]); + pipelineModel.set([[binding, { message: "test" }]], { writeSymbol }); - middleware.delete(binding, undefined, baseDataModel); + pipelineModel.delete(binding); // Should still exist - expect(baseDataModel.get(binding)).toEqual({ message: "test" }); + expect(pipelineModel.get(binding)).toEqual({ message: "test" }); expect(mockLogger.warn).toHaveBeenCalledWith( expect.stringContaining("Blocked delete of protected path: errorState"), ); }); it("should allow deletes when authorized with writeSymbol", () => { - const binding = parser.parse("errorState"); + const binding = parser.parse(ERROR_BINDING_PREFIX); // Set value first - baseDataModel.set([[binding, { message: "test" }]]); + pipelineModel.set([[binding, { message: "test" }]], { writeSymbol }); - middleware.delete(binding, { writeSymbol: writeSymbol }, baseDataModel); + pipelineModel.delete(binding, { writeSymbol: writeSymbol }); - expect(baseDataModel.get(binding)).toBeUndefined(); + expect(pipelineModel.get(binding)).toBeUndefined(); expect(mockLogger.warn).not.toHaveBeenCalled(); }); it("should block deletes with wrong writeSymbol", () => { - const binding = parser.parse("errorState"); + const binding = parser.parse(ERROR_BINDING_PREFIX); const wrongSymbol = Symbol("wrong-auth"); // Set value first - baseDataModel.set([[binding, { message: "test" }]]); + pipelineModel.set([[binding, { message: "test" }]], { writeSymbol }); - middleware.delete(binding, { writeSymbol: wrongSymbol }, baseDataModel); + pipelineModel.delete(binding, { writeSymbol: wrongSymbol }); - expect(baseDataModel.get(binding)).toEqual({ message: "test" }); + expect(pipelineModel.get(binding)).toEqual({ message: "test" }); expect(mockLogger.warn).toHaveBeenCalledWith( expect.stringContaining("Blocked delete of protected path: errorState"), ); @@ -201,21 +216,21 @@ describe("ErrorStateMiddleware", () => { it("should allow deletes to other paths", () => { const binding = parser.parse("foo"); - middleware.delete(binding, undefined, baseDataModel); + pipelineModel.delete(binding); expect(baseDataModel.get(binding)).toBeUndefined(); expect(mockLogger.warn).not.toHaveBeenCalled(); }); it("should allow deletes to nested errorState paths when authorized", () => { - const binding = parser.parse("errorState.nested.path"); + const binding = parser.parse(`${ERROR_BINDING_PREFIX}.nested.path`); // Set value first - baseDataModel.set([[binding, "test"]]); + pipelineModel.set([[binding, "test"]], { writeSymbol }); - middleware.delete(binding, { writeSymbol: writeSymbol }, baseDataModel); + pipelineModel.delete(binding, { writeSymbol: writeSymbol }); - expect(baseDataModel.get(binding)).toBeUndefined(); + expect(pipelineModel.get(binding)).toBeUndefined(); expect(mockLogger.warn).not.toHaveBeenCalled(); }); }); diff --git a/core/player/src/controllers/error/__tests__/navigation.test.ts b/core/player/src/controllers/error/__tests__/navigation.test.ts index 977bfee90..9697764ee 100644 --- a/core/player/src/controllers/error/__tests__/navigation.test.ts +++ b/core/player/src/controllers/error/__tests__/navigation.test.ts @@ -44,6 +44,7 @@ describe("ErrorController Navigation", () => { }, }, errorTransition: vitest.fn(), + getErrorTransitionState: vitest.fn(() => true), } as any; // Mock FlowController @@ -101,6 +102,15 @@ describe("ErrorController Navigation", () => { "custom_type", ); }); + + it("should fail the player state when there is no available transition", () => { + vitest + .mocked(mockFlowController.current?.getErrorTransitionState) + ?.mockReturnValue(undefined); + const error = new Error("Test error"); + errorController.captureError(error, ErrorTypes.VIEW); + expect(mockFail).toHaveBeenCalled(); + }); }); describe("Hook integration", () => { diff --git a/core/player/src/controllers/error/controller.ts b/core/player/src/controllers/error/controller.ts index 06e2ffb3d..4da624dda 100644 --- a/core/player/src/controllers/error/controller.ts +++ b/core/player/src/controllers/error/controller.ts @@ -4,6 +4,7 @@ import type { DataController } from "../data/controller"; import type { FlowController } from "../flow/controller"; import type { PlayerError, ErrorMetadata, ErrorSeverity } from "./types"; import { ErrorStateMiddleware } from "./middleware"; +import { isErrorWithMetadata, makeJsonStringifyReplacer } from "./utils"; /** * Private symbol used to authorize ErrorController's writes to errorState @@ -88,8 +89,18 @@ export class ErrorController { errorType, severity, metadata, + skipped: false, }; + if (isErrorWithMetadata(error)) { + playerError.errorType = error.type; + playerError.severity = error.severity ?? playerError.severity; + playerError.metadata = { + ...playerError.metadata, + ...error.metadata, + }; + } + // Add to history this.errorHistory.push(playerError); @@ -98,7 +109,14 @@ export class ErrorController { this.options.logger.debug( `[ErrorController] Captured error: ${error.message}`, - { errorType, severity, metadata }, + JSON.stringify( + { + errorType: playerError.errorType, + severity: playerError.severity, + metadata: playerError.metadata, + }, + makeJsonStringifyReplacer(), + ), ); // Notify listeners and check if navigation should be skipped @@ -106,6 +124,7 @@ export class ErrorController { const shouldSkip = this.hooks.onError.call(playerError) ?? false; if (shouldSkip) { + playerError.skipped = true; this.options.logger.debug( "[ErrorController] Error state navigation skipped by plugin", ); @@ -135,6 +154,13 @@ export class ErrorController { return; } + if ( + flowInstance.getErrorTransitionState(playerError.errorType) === undefined + ) { + this.options.fail(playerError.error); + return; + } + try { flowInstance.errorTransition(playerError.errorType); } catch (e) { diff --git a/core/player/src/controllers/error/middleware.ts b/core/player/src/controllers/error/middleware.ts index 2eb51d6e0..33a5d25da 100644 --- a/core/player/src/controllers/error/middleware.ts +++ b/core/player/src/controllers/error/middleware.ts @@ -1,13 +1,20 @@ import type { BindingInstance } from "../../binding"; -import type { +import { BatchSetTransaction, DataModelImpl, DataModelMiddleware, DataModelOptions, + LocalModel, Updates, } from "../../data"; import type { Logger } from "../../logger"; +/** Top-level key for all error information. */ +export const ERROR_BINDING_PREFIX = "errorState"; + +const isErrorBinding = (binding: BindingInstance): boolean => + binding.asArray()[0] === ERROR_BINDING_PREFIX; + /** * Middleware that prevents external writes to errorState * Only authorized callers (with the write symbol) can write to this path @@ -17,6 +24,8 @@ export class ErrorStateMiddleware implements DataModelMiddleware { private logger?: Logger; private writeSymbol: symbol; + // Internal model for error state to avoid data serialization + private dataModel: LocalModel = new LocalModel(); constructor(options: { logger?: Logger; writeSymbol: symbol }) { this.logger = options.logger; @@ -28,41 +37,41 @@ export class ErrorStateMiddleware implements DataModelMiddleware { options?: DataModelOptions, next?: DataModelImpl, ): Updates { - // Check if this write is authorized by comparing the write symbols - if (options?.writeSymbol === this.writeSymbol) { - return next?.set(transaction, options) ?? []; - } - // Filter out any writes to errorState const filteredTransaction: BatchSetTransaction = []; - const blockedBindings: BindingInstance[] = []; + const errorTransaction: BatchSetTransaction = []; - transaction.forEach(([binding, value]) => { - const path = binding.asString(); + transaction.forEach((transaction) => { + const [binding] = transaction; + const targetArray = isErrorBinding(binding) + ? errorTransaction + : filteredTransaction; - // Block writes to errorState namespace - if (path === "errorState" || path.startsWith("errorState.")) { - blockedBindings.push(binding); - this.logger?.warn( - `[ErrorStateMiddleware] Blocked write to protected path: ${path}`, - ); - } else { - filteredTransaction.push([binding, value]); - } + targetArray.push(transaction); }); // Process allowed writes - const validResults = next?.set(filteredTransaction, options) ?? []; + const nonErrorResults = next?.set(filteredTransaction, options) ?? []; - // Return no-op updates for blocked paths - const blockedResults: Updates = blockedBindings.map((binding) => ({ - binding, - oldValue: next?.get(binding, options), - newValue: next?.get(binding, options), // Keep old value - force: false, - })); + const errorResults = + options?.writeSymbol === this.writeSymbol + ? this.dataModel.set(errorTransaction) + : errorTransaction.map((transaction) => { + const [binding] = transaction; + this.logger?.warn( + `[ErrorStateMiddleware] Blocked write to protected path: ${binding.asString()}`, + ); - return [...validResults, ...blockedResults]; + const oldValue = next?.get(binding, options); + return { + binding, + oldValue, + newValue: oldValue, // Keep old value + force: false, + }; + }); + + return [...nonErrorResults, ...errorResults]; } public get( @@ -70,7 +79,9 @@ export class ErrorStateMiddleware implements DataModelMiddleware { options?: DataModelOptions, next?: DataModelImpl, ): unknown { - return next?.get(binding, options); + return isErrorBinding(binding) + ? this.dataModel.get(binding) + : next?.get(binding, options); } public delete( @@ -78,22 +89,18 @@ export class ErrorStateMiddleware implements DataModelMiddleware { options?: DataModelOptions, next?: DataModelImpl, ): void { - // Check if this delete is authorized by comparing the write symbols - if (options?.writeSymbol === this.writeSymbol) { + if (!isErrorBinding(binding)) { next?.delete(binding, options); return; } - - const path = binding.asString(); - // Block deletes to errorState namespace - if (path === "errorState" || path.startsWith("errorState.")) { + if (options?.writeSymbol !== this.writeSymbol) { this.logger?.warn( - `[ErrorStateMiddleware] Blocked delete of protected path: ${path}`, + `[ErrorStateMiddleware] Blocked delete of protected path: ${binding.asString()}`, ); return; } - next?.delete(binding, options); + this.dataModel.delete(binding); } } diff --git a/core/player/src/controllers/error/types.ts b/core/player/src/controllers/error/types.ts index f44e236d2..0d46f6bc5 100644 --- a/core/player/src/controllers/error/types.ts +++ b/core/player/src/controllers/error/types.ts @@ -17,6 +17,7 @@ export const ErrorTypes = { SCHEMA: "schema", NETWORK: "network", PLUGIN: "plugin", + RENDER: "render", } as const; /** @@ -27,7 +28,9 @@ export interface ErrorMetadata { [key: string]: unknown; } -export interface PlayerError { +export interface PlayerError< + MetadataType extends ErrorMetadata = ErrorMetadata, +> { /** Native Error object */ error: Error; /** Error category (use ErrorTypes constants or custom plugin types) */ @@ -35,5 +38,15 @@ export interface PlayerError { /** Impact level */ severity?: ErrorSeverity; /** Additional metadata */ - metadata?: ErrorMetadata; + metadata?: MetadataType; + /** Whether or not the error was skipped. */ + skipped: boolean; +} + +export interface PlayerErrorMetadata< + ErrorMetadataType extends ErrorMetadata = ErrorMetadata, +> { + type: string; + severity?: ErrorSeverity; + metadata?: ErrorMetadataType; } diff --git a/core/player/src/controllers/error/utils/__tests__/isErrorWithMetadata.test.ts b/core/player/src/controllers/error/utils/__tests__/isErrorWithMetadata.test.ts new file mode 100644 index 000000000..e4633c32b --- /dev/null +++ b/core/player/src/controllers/error/utils/__tests__/isErrorWithMetadata.test.ts @@ -0,0 +1,114 @@ +import { describe, it, expect } from "vitest"; +import { isErrorWithMetadata } from "../isErrorWithMetadata"; +import { ErrorSeverity } from "../../types"; + +/** Test class to create an error with any additional properties */ +class ErrorWithProps extends Error implements Record { + [key: PropertyKey]: unknown; +} + +const createTestError = ( + additionalProps?: Record, +): ErrorWithProps => { + const err: ErrorWithProps = new ErrorWithProps("Message"); + if (additionalProps) { + for (const [key, val] of Object.entries(additionalProps)) { + err[key] = val; + } + } + + return err; +}; + +describe("isErrorWithMetadata", () => { + const correctCases = [ + createTestError({ type: "type" }), + createTestError({ type: "type", metadata: {} }), + createTestError({ type: "type", severity: ErrorSeverity.ERROR }), + createTestError({ + type: "type", + metadata: {}, + severity: ErrorSeverity.ERROR, + }), + createTestError({ + type: "type", + metadata: {}, + severity: ErrorSeverity.ERROR, + someUnknownProperty: "more data should not impact test case.", + }), + ]; + it.each(correctCases)( + "should return true if type is present and all properties match their expected types.", + (err) => { + expect(isErrorWithMetadata(err)).toBe(true); + }, + ); + + const badTypeCases = [ + // `type` must be defined + createTestError({ + metadata: {}, + severity: ErrorSeverity.ERROR, + }), + // `type` must be a string + createTestError({ + type: 100, + metadata: {}, + severity: ErrorSeverity.ERROR, + }), + ]; + it.each(badTypeCases)( + "should return false if type is not present or not a string", + (err) => { + expect(isErrorWithMetadata(err)).toBe(false); + }, + ); + + const badSeverityCases = [ + // `severity` must be a string + createTestError({ + type: "type", + metadata: {}, + severity: 100, + }), + // `severity` must be an option in the `ErrorSeverity` enum + createTestError({ + type: "type", + metadata: {}, + severity: "NotARealErrorSeverity", + }), + ]; + it.each(badSeverityCases)( + "should return false if severity is not a value from the ErrorSeverity enum", + (err) => { + expect(isErrorWithMetadata(err)).toBe(false); + }, + ); + + const badMetadataCases = [ + // `metadata` must be an object + createTestError({ + type: "type", + metadata: 100, + severity: ErrorSeverity.ERROR, + }), + // `metadata` cannot be an array + createTestError({ + type: "type", + metadata: [], + severity: ErrorSeverity.ERROR, + }), + // `metadata` cannot be null + createTestError({ + type: "type", + metadata: null, + severity: ErrorSeverity.ERROR, + }), + ]; + it.each(badMetadataCases)( + "should return false if metadata is not an object", + (err) => { + expect(isErrorWithMetadata(err)).toBe(false); + }, + ); +}); diff --git a/core/player/src/controllers/error/utils/__tests__/makeJsonStringifyReplacer.test.ts b/core/player/src/controllers/error/utils/__tests__/makeJsonStringifyReplacer.test.ts new file mode 100644 index 000000000..a9e3217fb --- /dev/null +++ b/core/player/src/controllers/error/utils/__tests__/makeJsonStringifyReplacer.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { makeJsonStringifyReplacer } from "../makeJsonStringifyReplacer"; + +describe("makeJsonStringifyReplacer", () => { + it("should return [CIRCULAR] when the same object is used as the value multiple times", () => { + const val = { + prop: "value", + }; + const fn = makeJsonStringifyReplacer(); + + expect(fn("", val)).toStrictEqual({ + prop: "value", + }); + + expect(fn("", val)).toBe("[CIRCULAR]"); + }); + + it("should return the value when it is not an object or is null", () => { + const fn = makeJsonStringifyReplacer(); + + expect(fn("", null)).toBeNull(); + expect(fn("", "test")).toBe("test"); + }); +}); diff --git a/core/player/src/controllers/error/utils/index.ts b/core/player/src/controllers/error/utils/index.ts new file mode 100644 index 000000000..0676e829a --- /dev/null +++ b/core/player/src/controllers/error/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./isErrorWithMetadata"; +export * from "./makeJsonStringifyReplacer"; diff --git a/core/player/src/controllers/error/utils/isErrorWithMetadata.ts b/core/player/src/controllers/error/utils/isErrorWithMetadata.ts new file mode 100644 index 000000000..51abb2fe3 --- /dev/null +++ b/core/player/src/controllers/error/utils/isErrorWithMetadata.ts @@ -0,0 +1,28 @@ +import { ErrorSeverity, PlayerErrorMetadata } from "../types"; + +const SEVERITY_SET = new Set(Object.values(ErrorSeverity)); + +export const isErrorWithMetadata = ( + error: Error, +): error is Error & PlayerErrorMetadata => { + // 1. "type" property must be present and a string + if (!("type" in error) || typeof error.type !== "string") { + return false; + } + + // 2. "severity" property is optional. If present, must be a string within the set of severity options + if ( + "severity" in error && + (typeof error.severity !== "string" || !SEVERITY_SET.has(error.severity)) + ) { + return false; + } + + // 3. "metadata" property is optional. If present, must be a non-array object. + return ( + !("metadata" in error) || + (typeof error.metadata === "object" && + error.metadata !== null && + !Array.isArray(error.metadata)) + ); +}; diff --git a/core/player/src/controllers/error/utils/makeJsonStringifyReplacer.ts b/core/player/src/controllers/error/utils/makeJsonStringifyReplacer.ts new file mode 100644 index 000000000..b9f41a3ac --- /dev/null +++ b/core/player/src/controllers/error/utils/makeJsonStringifyReplacer.ts @@ -0,0 +1,17 @@ +type ReplacerFunction = (key: string, value: any) => any; + +/** Returns a function to be used as the `replacer` for JSON.stringify that tracks and ignores circular references. */ +export const makeJsonStringifyReplacer = (): ReplacerFunction => { + const cache = new Set(); + return (_: string, value: any) => { + if (typeof value === "object" && value !== null) { + if (cache.has(value)) { + // Circular reference found, discard key + return "[CIRCULAR]"; + } + // Store value in our collection + cache.add(value); + } + return value; + }; +}; diff --git a/core/player/src/controllers/flow/__tests__/flow.test.ts b/core/player/src/controllers/flow/__tests__/flow.test.ts index 79268fb75..88398e14a 100644 --- a/core/player/src/controllers/flow/__tests__/flow.test.ts +++ b/core/player/src/controllers/flow/__tests__/flow.test.ts @@ -603,3 +603,47 @@ describe("errorTransition", () => { expect(flow.currentState?.name).toBe("ErrorView"); }); }); + +describe("getErrorTransitionState", () => { + test("should return true when the error exists", () => { + const flow = new FlowInstance("flow", { + startState: "View1", + errorTransitions: { + init: "ErrorView", + }, + View1: { + state_type: "VIEW", + ref: "view-1", + transitions: {}, + }, + ErrorView: { + state_type: "VIEW", + ref: "error-view", + transitions: {}, + }, + }); + + expect(flow.getErrorTransitionState("init")).toBe("ErrorView"); + }); + + test("should return false when the error does not exist", () => { + const flow = new FlowInstance("flow", { + startState: "View1", + errorTransitions: { + init: "ErrorView", + }, + View1: { + state_type: "VIEW", + ref: "view-1", + transitions: {}, + }, + ErrorView: { + state_type: "VIEW", + ref: "error-view", + transitions: {}, + }, + }); + + expect(flow.getErrorTransitionState("not-init")).toBe(undefined); + }); +}); diff --git a/core/player/src/controllers/flow/flow.ts b/core/player/src/controllers/flow/flow.ts index 26ab827ba..8ba36a9da 100644 --- a/core/player/src/controllers/flow/flow.ts +++ b/core/player/src/controllers/flow/flow.ts @@ -168,17 +168,12 @@ export class FlowInstance { return map[key] || map["*"]; } - /** - * Navigate using errorTransitions map. - * Tries node-level first, then falls back to flow-level. - * Bypasses validation hooks and expression resolution. - * @throws Error if errorTransitions references a non-existent state - */ - public errorTransition(errorType: string): void { + /** Check if the flow has a transition for the given error type in its current state. */ + public getErrorTransitionState(errorType: string): string | undefined { // Can't navigate from END state if (this.currentState?.value.state_type === "END") { this.log?.warn("Cannot error transition from END state"); - return; + return undefined; } // Try node-level errorTransitions (only if we have a current state) @@ -198,7 +193,7 @@ export class FlowInstance { this.log?.debug( `Error transition (node-level) from ${this.currentState.name} to ${nodeState} using ${errorType}`, ); - return this.pushHistory(nodeState); + return nodeState; } } } @@ -217,14 +212,29 @@ export class FlowInstance { this.log?.debug( `Error transition (flow-level) to ${flowState} using ${errorType}${this.currentState ? ` from ${this.currentState.name}` : ""}`, ); - return this.pushHistory(flowState); + return flowState; } } - // No match found - this.log?.warn( - `No errorTransition found for ${errorType} (checked node and flow level)`, - ); + return undefined; + } + + /** + * Navigate using errorTransitions map. + * Tries node-level first, then falls back to flow-level. + * Bypasses validation hooks and expression resolution. + * @throws Error if errorTransitions references a non-existent state + */ + public errorTransition(errorType: string): void { + const transitionState = this.getErrorTransitionState(errorType); + if (transitionState === undefined) { + this.log?.warn( + `No errorTransition found for ${errorType} (checked node and flow level)`, + ); + return; + } + + this.pushHistory(transitionState); } public transition( diff --git a/core/player/src/controllers/view/controller.ts b/core/player/src/controllers/view/controller.ts index 7ad9e1d41..d4b8cabd8 100644 --- a/core/player/src/controllers/view/controller.ts +++ b/core/player/src/controllers/view/controller.ts @@ -22,6 +22,7 @@ import type { DataController } from "../data/controller"; import type { TransformRegistry } from "./types"; import type { BindingInstance } from "../../binding"; import type { Node } from "../../view"; +import { ErrorController, ErrorTypes } from "../error"; export interface ViewControllerOptions { /** Where to get data from */ @@ -32,6 +33,9 @@ export interface ViewControllerOptions { /** A flow-controller instance to listen for view changes */ flowController: FlowController; + + /** Error controller to use when managing view-level errors */ + errorController: ErrorController; } export type ViewControllerHooks = { @@ -104,7 +108,7 @@ export class ViewController { if (this.optimizeUpdates) { this.queueUpdate(updates, undefined, silent); } else { - this.currentView.update(); + this.updateView(); } } }; @@ -158,11 +162,26 @@ export class ViewController { queueMicrotask(() => { const { changedBindings, changedNodes } = this.pendingUpdate ?? {}; this.pendingUpdate = undefined; - this.currentView?.update(changedBindings, changedNodes); + this.updateView(changedBindings, changedNodes); }); } } + private updateView( + changedBindings?: Set, + changedNodes?: Set, + ) { + try { + this.currentView?.update(changedBindings, changedNodes); + } catch (exception: unknown) { + const err = + exception instanceof Error ? exception : new Error(String(exception)); + // Can't assume any node or binding changes were consumed correctly during the update, so trigger a silent update to ensure that any additional update triggered to recover still updates everything. + this.queueUpdate(changedBindings, changedNodes, true); + this.viewOptions.errorController.captureError(err, ErrorTypes.VIEW); + } + } + private getViewForRef(viewRef: string): View | undefined { // First look for a 1:1 viewRef -> id mapping (this is most common) if (this.viewMap[viewRef]) { @@ -204,7 +223,7 @@ export class ViewController { // own listeners to the view before we resolve it this.applyViewPlugins(view); this.hooks.view.call(view); - view.update(); + this.updateView(); } private applyViewPlugins(view: ViewInstance): void { diff --git a/core/player/src/player.ts b/core/player/src/player.ts index 73a2776f7..e42e2cdf2 100644 --- a/core/player/src/player.ts +++ b/core/player/src/player.ts @@ -460,6 +460,7 @@ export class Player { type: (b) => schema.getType(parseBinding(b)), }, constants: this.constantsController, + errorController, }); viewController.hooks.view.tap("player", (view) => { diff --git a/core/player/src/view/plugins/__tests__/template.test.ts b/core/player/src/view/plugins/__tests__/template.test.ts index 097ab7d4b..92b3c9191 100644 --- a/core/player/src/view/plugins/__tests__/template.test.ts +++ b/core/player/src/view/plugins/__tests__/template.test.ts @@ -7,8 +7,13 @@ import { SchemaController } from "../../../schema"; import { Parser } from "../../parser"; import { ViewInstance } from "../../view"; import type { Options } from "../options"; -import { TemplatePlugin, MultiNodePlugin, AssetPlugin } from "../"; -import { StringResolverPlugin, toNodeResolveOptions } from "../.."; +import { + TemplatePlugin, + MultiNodePlugin, + AssetPlugin, + StringResolverPlugin, +} from "../"; +import { toNodeResolveOptions } from "../../resolver"; import type { View } from "@player-ui/types"; const templateJoinValues = { diff --git a/core/player/src/view/resolver/ResolverError.ts b/core/player/src/view/resolver/ResolverError.ts new file mode 100644 index 000000000..529195690 --- /dev/null +++ b/core/player/src/view/resolver/ResolverError.ts @@ -0,0 +1,25 @@ +import { Node } from "../parser"; +import { + ErrorSeverity, + ErrorTypes, + type PlayerErrorMetadata, +} from "../../controllers"; +import type { ResolverErrorMetadata, ResolverStage } from "./types"; + +/** Error class to represent errors in the player resolver. */ +export class ResolverError extends Error implements PlayerErrorMetadata { + readonly type: string = ErrorTypes.VIEW; + readonly severity: ErrorSeverity = ErrorSeverity.ERROR; + readonly metadata: ResolverErrorMetadata; + + constructor( + public readonly cause: unknown, + public readonly stage: ResolverStage, + node: Node.Node, + ) { + super(`An error in the resolver occurred at stage '${stage}'`); + this.metadata = { + node, + }; + } +} diff --git a/core/player/src/view/resolver/__tests__/index.test.ts b/core/player/src/view/resolver/__tests__/index.test.ts index ed5f61c98..88cc2ac75 100644 --- a/core/player/src/view/resolver/__tests__/index.test.ts +++ b/core/player/src/view/resolver/__tests__/index.test.ts @@ -3,7 +3,7 @@ import { BindingParser } from "../../../binding"; import { ExpressionEvaluator } from "../../../expressions"; import { LocalModel, withParser } from "../../../data"; import { SchemaController } from "../../../schema"; -import { Resolve, Resolver } from ".."; +import { Resolve, Resolver, ResolverError } from ".."; import type { Node } from "../../parser"; import { NodeType, Parser } from "../../parser"; @@ -137,3 +137,55 @@ describe("Node cache updates", () => { ); }); }); + +describe("error handling", () => { + let resolverOptions: Resolve.ResolverOptions; + + beforeEach(() => { + const model = new LocalModel({}); + const parser = new Parser(); + const bindingParser = new BindingParser(); + + resolverOptions = { + model, + parseBinding: bindingParser.parse.bind(bindingParser), + parseNode: parser.parseObject.bind(parser), + evaluator: new ExpressionEvaluator({ + model: withParser(model, bindingParser.parse), + }), + schema: new SchemaController(), + }; + }); + + const computeTreeHooks: Array = [ + "afterNodeUpdate", + "afterResolve", + "beforeResolve", + "resolve", + "resolveOptions", + "skipResolve", + ]; + it.each(computeTreeHooks)( + "should wrap errors in hooks in a ResolverError", + (hook) => { + const resolver = new Resolver(simpleViewWithAsync, resolverOptions); + + resolver.hooks[hook].tap("test", () => { + throw new Error("ERROR!"); + }); + + let error: unknown; + try { + resolver.update(); + } catch (err: unknown) { + error = err; + } + + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(ResolverError); + const resolverError = error as ResolverError; + expect(resolverError.cause).toStrictEqual(new Error("ERROR!")); + expect(resolverError.stage).toStrictEqual(hook); + }, + ); +}); diff --git a/core/player/src/view/resolver/index.ts b/core/player/src/view/resolver/index.ts index 24f434f88..6043c297c 100644 --- a/core/player/src/view/resolver/index.ts +++ b/core/player/src/view/resolver/index.ts @@ -12,11 +12,13 @@ import { DependencyModel, withParser } from "../../data"; import type { Logger } from "../../logger"; import { Node, NodeType } from "../parser"; import { caresAboutDataChanges, toNodeResolveOptions } from "./utils"; -import type { Resolve } from "./types"; +import { ResolverStage, type Resolve } from "./types"; import { getNodeID } from "../parser/utils"; +import { ResolverError } from "./ResolverError"; export * from "./types"; export * from "./utils"; +export * from "./ResolverError"; interface NodeUpdate extends Resolve.ResolvedNode { /** A flag to track if a node has changed since the last resolution */ @@ -252,36 +254,44 @@ export class Resolver { nodeChanges: Set, ): NodeUpdate { const dependencyModel = new DependencyModel(options.data.model); - dependencyModel.trackSubset("core"); const depModelWithParser = withContext( withParser(dependencyModel, this.options.parseBinding), ); - const resolveOptions = this.hooks.resolveOptions.call( - { - ...options, - data: { - ...options.data, - model: depModelWithParser, - }, - evaluate: (exp) => - this.options.evaluator.evaluate(exp, { model: depModelWithParser }), - node, + let resolveOptions: Resolve.NodeResolveOptions = { + ...options, + data: { + ...options.data, + model: depModelWithParser, }, + evaluate: (exp) => + this.options.evaluator.evaluate(exp, { model: depModelWithParser }), node, - ); + }; + + try { + resolveOptions = this.hooks.resolveOptions.call(resolveOptions, node); + } catch (err: unknown) { + throw new ResolverError(err, ResolverStage.ResolveOptions, node); + } const previousResult = this.getPreviousResult(node); const previousDeps = previousResult?.dependencies; const isChanged = nodeChanges.has(node); const dataChanged = caresAboutDataChanges(dataChanges, previousDeps); - const shouldUseLastValue = this.hooks.skipResolve.call( - !dataChanged && !isChanged, - node, - resolveOptions, - ); + let shouldUseLastValue = !dataChanged && !isChanged; + + try { + shouldUseLastValue = this.hooks.skipResolve.call( + shouldUseLastValue, + node, + resolveOptions, + ); + } catch (err: unknown) { + throw new ResolverError(err, ResolverStage.SkipResolve, node); + } if (previousResult && shouldUseLastValue) { const update = { @@ -325,7 +335,11 @@ export class Resolver { resolvedASTLocal.values.forEach(handleChildNode); } - this.hooks.afterNodeUpdate.call(AST, ASTParent, resolvedUpdate); + try { + this.hooks.afterNodeUpdate.call(AST, ASTParent, resolvedUpdate); + } catch (err: unknown) { + throw new ResolverError(err, ResolverStage.AfterNodeUpdate, node); + } }; // Point the root of the cached node to the new resolved node. @@ -338,16 +352,20 @@ export class Resolver { // Shallow clone the node so that changes to it during the resolve steps don't impact the original. // We are trusting that this becomes a deep clone once the whole node tree has been traversed. - const clonedNode: Node.Node = { + let resolvedAST: Node.Node = { ...this.cloneNode(node), parent: partiallyResolvedParent, }; - const resolvedAST = this.hooks.beforeResolve.call( - clonedNode, - resolveOptions, - ) ?? { - type: NodeType.Empty, - }; + try { + resolvedAST = this.hooks.beforeResolve.call( + resolvedAST, + resolveOptions, + ) ?? { + type: NodeType.Empty, + }; + } catch (err: unknown) { + throw new ResolverError(err, ResolverStage.BeforeResolve, node); + } resolvedAST.parent = partiallyResolvedParent; @@ -355,11 +373,16 @@ export class Resolver { this.ASTMap.set(resolvedAST, node); - let resolved = this.hooks.resolve.call( - undefined, - resolvedAST, - resolveOptions, - ); + let resolved: any = undefined; + try { + resolved = this.hooks.resolve.call( + undefined, + resolvedAST, + resolveOptions, + ); + } catch (err: unknown) { + throw new ResolverError(err, ResolverStage.Resolve, node); + } let updated = !dequal(previousResult?.value, resolved); @@ -449,11 +472,15 @@ export class Resolver { resolved = previousResult?.value; } - resolved = this.hooks.afterResolve.call(resolved, resolvedAST, { - ...resolveOptions, - getDependencies: (scope?: "core" | "children") => - dependencyModel.getDependencies(scope), - }); + try { + resolved = this.hooks.afterResolve.call(resolved, resolvedAST, { + ...resolveOptions, + getDependencies: (scope?: "core" | "children") => + dependencyModel.getDependencies(scope), + }); + } catch (err: unknown) { + throw new ResolverError(err, ResolverStage.AfterResolve, node); + } const update: NodeUpdate = { node: resolvedAST, @@ -465,7 +492,11 @@ export class Resolver { ]), }; - this.hooks.afterNodeUpdate.call(node, rawParent, update); + try { + this.hooks.afterNodeUpdate.call(node, rawParent, update); + } catch (err: unknown) { + throw new ResolverError(err, ResolverStage.AfterNodeUpdate, node); + } cacheUpdate.set(node, update); return update; diff --git a/core/player/src/view/resolver/types.ts b/core/player/src/view/resolver/types.ts index 6631177d7..ea6a70069 100644 --- a/core/player/src/view/resolver/types.ts +++ b/core/player/src/view/resolver/types.ts @@ -200,3 +200,16 @@ export declare namespace Resolve { afterResolve?: NodeResolveFunction; } } + +export enum ResolverStage { + ResolveOptions = "resolveOptions", + SkipResolve = "skipResolve", + BeforeResolve = "beforeResolve", + Resolve = "resolve", + AfterResolve = "afterResolve", + AfterNodeUpdate = "afterNodeUpdate", +} + +export type ResolverErrorMetadata = { + node: Node.Node; +}; diff --git a/core/player/src/view/resolver/utils.ts b/core/player/src/view/resolver/utils.ts index d10f257e1..49768c844 100644 --- a/core/player/src/view/resolver/utils.ts +++ b/core/player/src/view/resolver/utils.ts @@ -7,7 +7,7 @@ import type { Resolve } from "./types"; export function caresAboutDataChanges( dataChanges?: Set, dependencies?: Set, -) { +): boolean { if (!dataChanges || !dependencies) { return true; } diff --git a/docs/site/src/content/docs/plugins/multiplatform/async-node.mdx b/docs/site/src/content/docs/plugins/multiplatform/async-node.mdx index 15fe0d539..bcaebe9ac 100644 --- a/docs/site/src/content/docs/plugins/multiplatform/async-node.mdx +++ b/docs/site/src/content/docs/plugins/multiplatform/async-node.mdx @@ -8,12 +8,14 @@ import PodAndPackage from "../../../../components/PodAndPackage.astro"; The AsyncNode Plugin is used to enable streaming additional content into a flow that has already been loaded and rendered. A common use case for this plugin is conversational UI, as the users input more dialogue, new content must be streamed into Player in order to keep the UI up to date. -The pillar that makes this possible is the concept of an `AsyncNode`. An `AsyncNode` is any tree node with the property `async: true`, it represents a placeholder node that will be replaced by a concrete node in the future. `AsyncNode` is added to the AST tree during asset transform process(You can read more about what transform is [here](/assets/transforms)). In the example below, it shows the content and resolved result. +The pillar that makes this possible is the concept of an `AsyncNode`. An `AsyncNode` is any tree node with the property `async: true`, it represents a placeholder node that will be replaced by a concrete node in the future. `AsyncNode` is added to the AST tree during asset transform process(You can read more about what transform is [here](/assets/transforms)). In the example below, it shows the content and resolved result. #### Content authoring: Value of `chat-message` is the asset to render + - JSON content + ```json { "id": "chat-1", @@ -25,7 +27,9 @@ Value of `chat-message` is the asset to render }, }, ``` + - DSL + ```typescript @@ -36,23 +40,26 @@ Value of `chat-message` is the asset to render #### Resolved -`chat-message` asset is wrapped into a wrapper container with children including the `Text` asset in value and an `AsyncNode` in the associated asset transform before resolving. +`chat-message` asset is wrapped into a wrapper container with children including the `Text` asset in value and an `AsyncNode` in the associated asset transform before resolving. + ```json +{ + "id": "collection-async-chat-1", + "type": "collection", + "values": [ { - "id": "collection-async-chat-1", - "type": "collection", - "values": [ - { - "asset": { - "id": "1", - "type": "text", - "value": "chat message", - }, - }, - ], + "asset": { + "id": "1", + "type": "text", + "value": "chat message" + } } + ] +} ``` + If there is no asset to display, there will be only one `AsyncNode` in resolved result and no content will be rendered. + ```json { "id": "chat-1", @@ -60,7 +67,6 @@ If there is no asset to display, there will be only one `AsyncNode` in resolved }, ``` - The `AsyncNodePlugin` exposes an `onAsyncNode` hook on all platforms. The `onAsyncNode` hook will be invoked with the current node when the plugin is available and an `AsyncNode` is detected during the resolve process. The node used to call the hook with could contain metadata according to content spec. User should tap into the `onAsyncNode` hook to examine the node's metadata before making a decision on what to replace the async node with. The return could be a single asset node or an array of asset nodes, or the return could be even a null|undefined if the async node is no longer relevant. @@ -118,13 +124,13 @@ const player = new Player({ or initialize plugin with async handler ```typescript -import { Player } from '@player-ui/player'; -import { AsyncNodePlugin, AsyncNodePluginPlugin } from '@player-ui/async-node-plugin'; +import { Player } from "@player-ui/player"; +import { + AsyncNodePlugin, + AsyncNodePluginPlugin, +} from "@player-ui/async-node-plugin"; -const asyncHandler = ( - node: Node.Async, - callback: (content: any) => void, -) => { +const asyncHandler = (node: Node.Async, callback: (content: any) => void) => { // do some async task to get content return result; }; @@ -137,10 +143,8 @@ const plugin = new AsyncNodePlugin( ); const player = new Player({ - plugins: [ - asyncNodePlugin - ] -}) + plugins: [asyncNodePlugin], +}); ``` @@ -169,18 +173,26 @@ const player = new ReactPlayer({ ### Error Handling -By default, errors happening during the `onAsyncNode` hook bubble up as player flow failures. These errors can come from the taps themselves, or from parsing the result of those taps within the core plugin. Use the `onAsyncNodeError` to catch these errors and handle them gracefully. Even if the error is handled, it will be logged by the player logger. The return on the hook will determine what gets rendered in the view. To render nothing, return `null` +The `AsyncNodePlugin` makes use of the `ErrorController` to catch and resolve errors related to anything that happens within the `onAsyncNode` hook, or at resolve or render time of content generated through that hook. The `onAsyncNodeError` hook is a `SyncBailHook` that offers the ability to change the content when an error comes up. Returning new content or `null` will recover and update the view. Returning `undefined` declares your intent to not handle the error and let it bubble up. In cases where async nodes are generated by content from other async nodes, unhandled errors will bubble up and call the `onAsyncNodeError` hook for each node, starting with the node that immediately generated the content, until something handles the error or until all relevant nodes have been checked. ```typescript const asyncNodePlugin = new AsyncNodePlugin({ plugins: [new AsyncNodePluginPlugin()], }); -asyncNodePlugin.hooks.onAsyncNodeError.tap("handleAsyncError", (error, node) => { - // Do something to handle the error in the background +asyncNodePlugin.hooks.onAsyncNodeError.tap( + "handleAsyncError", + (error, node) => { + // Check that the error can be recovered from for this node. + if (!isRecoverable(error, node)) { + return undefined; + } - return null; -}); + // Create new content to be shown to the user. + const newContent = getErrorPlaceholder(error, node); + return newContent; + }, +); ``` @@ -217,6 +229,7 @@ var body: some View { ) } ``` + or tapping asycHandler after initializing the plugin ```swift @@ -233,7 +246,7 @@ var body: some View { let replacementNode = try await (asyncHandler)(node, callback) return replacementNode.handlerTypeToJSValue(context: context ?? JSContext()) ?? JSValue() }) - + SwiftUIPlayer( flow: flow, plugins: [plugin], @@ -334,9 +347,10 @@ AndroidPlayer(asyncNodePlugin) - ### AsyncTransform + `createAsyncTransform` is a helper function to create the transform for async asset. The function takes a set of options that help adjust the transform generated to your needs: + - flatten (Optional. Default: true) - Whether or not to flatten chained results of the same asset type into a single container. - path (Optional. Default: ["values"]) - The path to the array within the `wrapperAssetType` that will contain the async content. - transformAssetType (Required) - The asset type that the transform is matching against. @@ -353,8 +367,8 @@ Setting up a transform like below: const beforeResolve = createAsyncTransform({ transformAssetType: "chat-message", wrapperAssetType: "collection", - getNestedAsset: (node) => node.children?.[0]?.value -}) + getNestedAsset: (node) => node.children?.[0]?.value, +}); ``` transforms the following content: @@ -371,7 +385,7 @@ transforms the following content: }, ``` -into: +into: ```json { @@ -391,4 +405,5 @@ into: And using additional `chat-message` assets in the async content will flatten itself into this top level collection. ### Flatten + Flatten is introduced to avoid nested structure caused by continuous streaming. eg. When a new `chat-message` asset is streamed in, in AST tree, instead of `[chat1, [chat2, asyncNode]]`, it's flattened into `[chat1, chat2, asyncNode]` diff --git a/ios/core/Sources/Types/Core/ErrorController.swift b/ios/core/Sources/Types/Core/ErrorController.swift index 78a7ab5b9..3390fa16f 100644 --- a/ios/core/Sources/Types/Core/ErrorController.swift +++ b/ios/core/Sources/Types/Core/ErrorController.swift @@ -34,6 +34,7 @@ public struct ErrorTypes { public static let schema = "schema" public static let network = "network" public static let plugin = "plugin" + public static let render = "render" } /** @@ -146,13 +147,15 @@ public class ErrorController: CreatedFromJSValue { severity: ErrorSeverity? = nil, metadata: [String: Any]? = nil ) -> JSValue? { - var args: [Any] = [ - [ - "message": error.localizedDescription, - "name": String(describing: type(of: error)) - ] as [String: Any], - errorType - ] + var args: [Any] = [] + + if let err = error as? JSConvertibleError & Error { + args.append(value.context.error(for: err) as Any) + } else { + args.append(value.context.error(for: PlayerError.unknownResponse(error)) as Any) + } + + args.append(errorType) if let severity = severity { args.append(severity.rawValue) diff --git a/ios/core/Sources/utilities/JSUtilities.swift b/ios/core/Sources/utilities/JSUtilities.swift index 0e13d3856..4b518c13c 100644 --- a/ios/core/Sources/utilities/JSUtilities.swift +++ b/ios/core/Sources/utilities/JSUtilities.swift @@ -63,7 +63,16 @@ public class JSUtilities { internal extension JSContext { func error(for error: E) -> JSValue? where E: Error, E: JSConvertibleError { - objectForKeyedSubscript("Error").construct(withArguments: [error.jsDescription]) + let errObj = objectForKeyedSubscript("Error").construct(withArguments: [error.jsDescription]) + if let errorWithMetadata = error as? ErrorWithMetadata, let err = errObj { + err.setValue(errorWithMetadata.type, forProperty: "type") + err.setValue(errorWithMetadata.severity?.rawValue, forProperty: "severity") + if let metadata = errorWithMetadata.metadata { + err.setValue(metadata, forProperty: "metadata") + } + } + + return errObj } } @@ -72,3 +81,9 @@ public protocol JSConvertibleError { /// The description to use when send to JavaScriptCore var jsDescription: String { get } } + +public protocol ErrorWithMetadata : Error { + var type: String { get } + var severity: ErrorSeverity? { get } + var metadata: [String: Any]? { get } +} diff --git a/ios/core/Tests/ErrorControllerTests.swift b/ios/core/Tests/ErrorControllerTests.swift index 2d9079694..9f95013fa 100644 --- a/ios/core/Tests/ErrorControllerTests.swift +++ b/ios/core/Tests/ErrorControllerTests.swift @@ -98,7 +98,7 @@ class ErrorControllerTests: XCTestCase { // Convert JSValue to PlayerErrorInfo if let jsValue = capturedErrorValue { let capturedError = PlayerErrorInfo(jsValue) - XCTAssertEqual(capturedError.message, "Not found") + XCTAssertEqual(capturedError.message, "Error Domain=com.test Code=404 \"Not found\" UserInfo={NSLocalizedDescription=Not found}") XCTAssertEqual(capturedError.errorType, ErrorTypes.network) XCTAssertEqual(capturedError.severity, .error) XCTAssertNotNil(capturedError.metadata) @@ -131,7 +131,7 @@ class ErrorControllerTests: XCTestCase { // Convert JSValue to PlayerErrorInfo if let jsValue = capturedErrorValue { let capturedError = PlayerErrorInfo(jsValue) - XCTAssertEqual(capturedError.message, "Internal error") + XCTAssertEqual(capturedError.message, "Error Domain=com.test Code=500 \"Internal error\" UserInfo={NSLocalizedDescription=Internal error}") XCTAssertEqual(capturedError.errorType, ErrorTypes.plugin) XCTAssertNil(capturedError.severity) XCTAssertNil(capturedError.metadata) @@ -157,7 +157,7 @@ class ErrorControllerTests: XCTestCase { // Verify current error is the first one if let firstErrorValue = errorController?.getCurrentError(), !firstErrorValue.isUndefined { - XCTAssertEqual(PlayerErrorInfo(firstErrorValue).message, "First error") + XCTAssertEqual(PlayerErrorInfo(firstErrorValue).message, "Error Domain=test Code=1 \"First error\" UserInfo={NSLocalizedDescription=First error}") } // Capture second error @@ -169,7 +169,7 @@ class ErrorControllerTests: XCTestCase { // Current error should be updated to the second one if let secondErrorValue = errorController?.getCurrentError(), !secondErrorValue.isUndefined { - XCTAssertEqual(PlayerErrorInfo(secondErrorValue).message, "Second error") + XCTAssertEqual(PlayerErrorInfo(secondErrorValue).message, "Error Domain=test Code=2 \"Second error\" UserInfo={NSLocalizedDescription=Second error}") } // Capture third error @@ -181,7 +181,7 @@ class ErrorControllerTests: XCTestCase { // Current error should be updated to the third one if let thirdErrorValue = errorController?.getCurrentError(), !thirdErrorValue.isUndefined { - XCTAssertEqual(PlayerErrorInfo(thirdErrorValue).message, "Third error") + XCTAssertEqual(PlayerErrorInfo(thirdErrorValue).message, "Error Domain=test Code=3 \"Third error\" UserInfo={NSLocalizedDescription=Third error}") } // Get all errors and verify history @@ -198,9 +198,9 @@ class ErrorControllerTests: XCTestCase { let secondError = PlayerErrorInfo(errorsValue.atIndex(1)) let thirdError = PlayerErrorInfo(errorsValue.atIndex(2)) - XCTAssertEqual(firstError.message, "First error") - XCTAssertEqual(secondError.message, "Second error") - XCTAssertEqual(thirdError.message, "Third error") + XCTAssertEqual(firstError.message, "Error Domain=test Code=1 \"First error\" UserInfo={NSLocalizedDescription=First error}") + XCTAssertEqual(secondError.message, "Error Domain=test Code=2 \"Second error\" UserInfo={NSLocalizedDescription=Second error}") + XCTAssertEqual(thirdError.message, "Error Domain=test Code=3 \"Third error\" UserInfo={NSLocalizedDescription=Third error}") } // MARK: - Get Current Error Tests @@ -239,7 +239,7 @@ class ErrorControllerTests: XCTestCase { } let currentError = PlayerErrorInfo(currentErrorValue) - XCTAssertEqual(currentError.message, "Current error") + XCTAssertEqual(currentError.message, "Error Domain=test Code=100 \"Current error\" UserInfo={NSLocalizedDescription=Current error}") XCTAssertEqual(currentError.errorType, ErrorTypes.data) } diff --git a/ios/demo/Sources/MockFlows.swift b/ios/demo/Sources/MockFlows.swift index 1ee599025..f00ee7a8d 100644 --- a/ios/demo/Sources/MockFlows.swift +++ b/ios/demo/Sources/MockFlows.swift @@ -1444,7 +1444,7 @@ static let chatMessageBasic: String = """ }, "views": [ { - "id": "root", + "id": "chat-view", "type": "collection", "values": [ { @@ -1453,7 +1453,7 @@ static let chatMessageBasic: String = """ "type": "chat-message", "value": { "asset": { - "id": "values-0-value", + "id": "chat-view-values-0-value", "type": "text", "value": "Start chatting now!" } @@ -1469,17 +1469,52 @@ static let chatMessageBasic: String = """ }, { "asset": { - "id": "values-2", + "id": "chat-view-values-2", "type": "action", "exp": "send({{content}})", "label": { "asset": { - "id": "values-2-label", + "id": "chat-view-values-2-label", "type": "text", "value": " Send " } } } + }, + { + "asset": { + "id": "chat-view-values-3", + "type": "text", + "value": "To demonstrate error recovery mechanisms, the message can be sent in poorly formatted content that will throw during the transform or at render-time of the asset:" + } + }, + { + "asset": { + "id": "chat-view-values-4", + "type": "action", + "exp": "sendBroken({{content}})", + "label": { + "asset": { + "id": "chat-view-values-4-label", + "type": "text", + "value": " Send Broken Render Asset " + } + } + } + }, + { + "asset": { + "id": "chat-view-values-5", + "type": "action", + "exp": "sendBrokenTransform({{content}})", + "label": { + "asset": { + "id": "chat-view-values-5-label", + "type": "text", + "value": " Send Broken Transform Asset " + } + } + } } ] } @@ -1490,7 +1525,7 @@ static let chatMessageBasic: String = """ "startState": "VIEW_1", "VIEW_1": { "state_type": "VIEW", - "ref": "root", + "ref": "chat-view", "transitions": { "*": "END_Done" } diff --git a/ios/swiftui/Sources/SwiftUIPlayer.swift b/ios/swiftui/Sources/SwiftUIPlayer.swift index 9a55a76eb..0e03298e0 100644 --- a/ios/swiftui/Sources/SwiftUIPlayer.swift +++ b/ios/swiftui/Sources/SwiftUIPlayer.swift @@ -160,7 +160,7 @@ public struct SwiftUIPlayer: View, HeadlessPlayer { do { try registry.decode(value: value) } catch { - (state as? InProgressState)?.fail(PlayerError.unknownResponse(error)) + (state as? InProgressState)?.controllers?.error.captureError(error: error, errorType: ErrorTypes.render, severity: .error) } } } diff --git a/ios/swiftui/Sources/types/assets/ControlledAsset.swift b/ios/swiftui/Sources/types/assets/ControlledAsset.swift index 579b6e126..5c63fa6be 100644 --- a/ios/swiftui/Sources/types/assets/ControlledAsset.swift +++ b/ios/swiftui/Sources/types/assets/ControlledAsset.swift @@ -17,6 +17,23 @@ public enum AssetRenderError: Error { case decodingFailure(innerError: Error, asset: AssetData? = nil, pathToAsset: [AssetData]) } +extension AssetRenderError: ErrorWithMetadata { + public var type: String { + ErrorTypes.render + } + + public var severity: ErrorSeverity? { + ErrorSeverity.error + } + + public var metadata: [String: Any]? { + switch self { + case .decodingFailure(_, let asset, _): + return ["assetId": asset?.id ?? ""] + } + } +} + extension AssetRenderError: CustomDebugStringConvertible { public var debugDescription: String { switch self { @@ -30,6 +47,12 @@ Exception occurred in asset with id '\(asset?.id ?? "UNKNOWN")' of type '\(asset } } +extension AssetRenderError: JSConvertibleError { + public var jsDescription: String { + debugDescription + } +} + struct MinimumAssetData: AssetData { public var id: String public var type: String diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/JSErrorException.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/JSErrorException.kt index ecd6b054f..e746a888f 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/JSErrorException.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/JSErrorException.kt @@ -2,15 +2,21 @@ package com.intuit.playerui.core.bridge import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField import com.intuit.playerui.core.bridge.serialization.serializers.ThrowableSerializer +import com.intuit.playerui.core.error.ErrorSeverity import com.intuit.playerui.core.player.PlayerException +import com.intuit.playerui.core.player.PlayerExceptionMetadata import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.serializer public class JSErrorException( override val node: Node, cause: Throwable? = node.getSerializable("innerErrors", ListSerializer(ThrowableSerializer()))?.first(), + override val type: String = "", + override val severity: ErrorSeverity?, + override val metadata: Map?, ) : PlayerException(node.getString("message") ?: "", cause), - NodeWrapper { + NodeWrapper, + PlayerExceptionMetadata { public val name: String by NodeSerializableField(String.serializer()) { "Error" } diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/runtime/PlayerRuntimeConfig.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/runtime/PlayerRuntimeConfig.kt index a62154d83..194e947bc 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/runtime/PlayerRuntimeConfig.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/runtime/PlayerRuntimeConfig.kt @@ -1,5 +1,4 @@ package com.intuit.playerui.core.bridge.runtime - import kotlinx.coroutines.CoroutineExceptionHandler /** Base configuration for [Runtime] */ diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/ThrowableSerializer.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/ThrowableSerializer.kt index 82a125d50..4484d4553 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/ThrowableSerializer.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/bridge/serialization/serializers/ThrowableSerializer.kt @@ -4,7 +4,9 @@ import com.intuit.playerui.core.bridge.JSErrorException import com.intuit.playerui.core.bridge.serialization.encoding.NodeDecoder import com.intuit.playerui.core.bridge.serialization.encoding.requireNodeDecoder import com.intuit.playerui.core.bridge.serialization.encoding.requireNodeEncoder +import com.intuit.playerui.core.error.ErrorSeverity import com.intuit.playerui.core.player.PlayerException +import com.intuit.playerui.core.player.PlayerExceptionMetadata import com.intuit.playerui.core.utils.InternalPlayerApi import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -31,6 +33,9 @@ public class ThrowableSerializer : KSerializer { element("stack", String.serializer().descriptor.nullable, isOptional = true) element("stackTrace", serializedStackTraceSerializer.descriptor.nullable, isOptional = true) element("cause", defer { ThrowableSerializer().descriptor.nullable }, isOptional = true) + element("type", String.serializer().descriptor.nullable, isOptional = true) + element("severity", ErrorSeverity.serializer().descriptor.nullable, isOptional = true) + element("metadata", GenericSerializer().descriptor.nullable, isOptional = true) } override fun deserialize(decoder: Decoder): PlayerException { @@ -48,6 +53,9 @@ public class ThrowableSerializer : KSerializer { var message = "" var stackTrace: Array = emptyArray() var cause: Throwable? = null + var type: String? = null + var severity: ErrorSeverity? = null + var metadata: Map? = null fun decodeStackTraceFromStack( stack: String? = decodeNullableSerializableElement(descriptor, 2, String.serializer().nullable), @@ -72,17 +80,36 @@ public class ThrowableSerializer : KSerializer { message = decodeNullableSerializableElement(descriptor, 1, String.serializer().nullable) ?: "" stackTrace = decodeSerializedStackTrace() cause = decodeNullableSerializableElement(descriptor, 4, nullable) + type = decodeNullableSerializableElement(descriptor, 5, String.serializer().nullable) + severity = decodeNullableSerializableElement(descriptor, 6, ErrorSeverity.serializer().nullable) + metadata = decodeNullableSerializableElement(descriptor, 7, GenericSerializer()) as? Map } else { stackTrace = decodeStackTraceFromStack() } } else { while (true) { when (val index = decodeElementIndex(descriptor)) { - 0 -> serialized = decodeNullableSerializableElement(descriptor, 0, Boolean.serializer().nullable) ?: false - 1 -> message = decodeNullableSerializableElement(descriptor, 1, String.serializer().nullable) ?: "" + 0 -> + serialized = + decodeNullableSerializableElement(descriptor, 0, Boolean.serializer().nullable) ?: false + + 1 -> + message = + decodeNullableSerializableElement(descriptor, 1, String.serializer().nullable) ?: "" + 2 -> stackTrace = decodeStackTraceFromStack() 3 -> stackTrace = decodeSerializedStackTrace() 4 -> cause = decodeNullableSerializableElement(descriptor, 4, nullable) + 5 -> type = decodeNullableSerializableElement(descriptor, 5, String.serializer().nullable) + 6 -> + severity = + decodeNullableSerializableElement(descriptor, 6, ErrorSeverity.serializer().nullable) + 7 -> metadata = decodeNullableSerializableElement( + descriptor, + 7, + GenericSerializer(), + ) as? Map + CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index") } @@ -90,11 +117,12 @@ public class ThrowableSerializer : KSerializer { } if (serialized) { + // TODO: Solve for serialized exceptions with specific types defining their error type, severity and metadata PlayerException(message, cause) } else { val error = decoder.requireNodeDecoder().decodeNode() stackTrace = decodeStackTraceFromStack(error.getString("stack")) - JSErrorException(error) + JSErrorException(error, type = type ?: "", severity = severity, metadata = metadata) }.apply { setStackTrace(stackTrace) } } } @@ -120,6 +148,11 @@ public class ThrowableSerializer : KSerializer { }, ) encodeNullableSerializableElement(descriptor, 4, nullable, value.cause) + if (value is PlayerExceptionMetadata) { + encodeStringElement(descriptor, 5, value.type) + encodeNullableSerializableElement(descriptor, 6, String.serializer(), value.severity?.value) + encodeNullableSerializableElement(descriptor, 7, GenericSerializer(), value.metadata) + } } } } diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/error/ErrorController.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/error/ErrorController.kt index 989e654bb..f13792222 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/error/ErrorController.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/error/ErrorController.kt @@ -4,24 +4,32 @@ import com.intuit.playerui.core.bridge.Invokable import com.intuit.playerui.core.bridge.Node import com.intuit.playerui.core.bridge.NodeWrapper import com.intuit.playerui.core.bridge.hooks.NodeSyncBailHook1 +import com.intuit.playerui.core.bridge.serialization.serializers.GenericSerializer import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableFunction import com.intuit.playerui.core.bridge.serialization.serializers.NodeWrapperSerializer +import com.intuit.playerui.core.bridge.serialization.serializers.ThrowableSerializer +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.nullable import kotlinx.serialization.builtins.serializer /** Severity levels for errors */ +@Serializable public enum class ErrorSeverity( public val value: String, ) { /** Cannot continue, flow must end */ + @SerialName("fatal") FATAL("fatal"), /** Standard error, may allow recovery */ + @SerialName("error") ERROR("error"), /** Non-blocking, logged for telemetry */ + @SerialName("warning") WARNING("warning"), } @@ -37,6 +45,7 @@ public object ErrorTypes { public const val SCHEMA: String = "schema" public const val NETWORK: String = "network" public const val PLUGIN: String = "plugin" + public const val RENDER: String = "render" } /** @@ -47,26 +56,29 @@ public class PlayerErrorInfo internal constructor( override val node: Node, ) : NodeWrapper { /** Nested error object containing message and name */ - private val error: Node? by NodeSerializableField(Node.serializer().nullable) + private val error: Throwable? by NodeSerializableField(ThrowableSerializer().nullable) /** The error message */ public val message: String - get() = error?.getString("message") ?: "" + get() = error?.message ?: "" /** The error name */ public val name: String - get() = error?.getString("name") ?: "" + get() = if (error == null) "" else (error!!::class.simpleName ?: "") /** Error category */ public val errorType: String by NodeSerializableField(String.serializer()) { "" } /** Impact level */ - public val severity: ErrorSeverity? - get() = node.getString("severity")?.let { ErrorSeverity.valueOf(it.uppercase()) } + public val severity: ErrorSeverity? by NodeSerializableField(ErrorSeverity.serializer().nullable) /** Additional metadata */ - public val metadata: Map? - get() = node.getObject("metadata") as? Map + public val metadata: Map? by NodeSerializableField( + MapSerializer( + String.serializer(), + GenericSerializer(), + ).nullable, + ) internal object Serializer : NodeWrapperSerializer(::PlayerErrorInfo) } @@ -99,22 +111,15 @@ public class ErrorController internal constructor( errorType: String, severity: ErrorSeverity? = null, metadata: Map? = null, - ): Node? { - val errorObj = mapOf( - "message" to error.message, - "name" to error::class.simpleName, - ) - - return when { - severity != null && metadata != null -> - captureError?.invoke(errorObj, errorType, severity.value, metadata) - severity != null -> - captureError?.invoke(errorObj, errorType, severity.value) - metadata != null -> - captureError?.invoke(errorObj, errorType, null, metadata) - else -> - captureError?.invoke(errorObj, errorType) - } + ): Node? = when { + severity != null && metadata != null -> + captureError?.invoke(error, errorType, severity.value, metadata) + severity != null -> + captureError?.invoke(error, errorType, severity.value) + metadata != null -> + captureError?.invoke(error, errorType, null, metadata) + else -> + captureError?.invoke(error, errorType) } /** diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt index bfa6cb92f..2f4e145bc 100644 --- a/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/HeadlessPlayer.kt @@ -14,6 +14,7 @@ import com.intuit.playerui.core.bridge.runtime.runtimeContainers import com.intuit.playerui.core.bridge.runtime.runtimeFactory import com.intuit.playerui.core.bridge.serialization.serializers.NodeSerializableField import com.intuit.playerui.core.constants.ConstantsController +import com.intuit.playerui.core.error.ErrorTypes import com.intuit.playerui.core.experimental.ExperimentalPlayerApi import com.intuit.playerui.core.logger.TapableLogger import com.intuit.playerui.core.player.HeadlessPlayer.Companion.bundledSource @@ -92,17 +93,19 @@ public class HeadlessPlayer @ExperimentalPlayerApi @JvmOverloads public construc public val runtime: Runtime<*> = explicitRuntime ?: runtimeFactory.create { debuggable = config.debuggable timeout = config.timeout - coroutineExceptionHandler = config.coroutineExceptionHandler ?: CoroutineExceptionHandler { _, throwable -> - if (state !is ReleasedState) { - inProgressState?.fail(throwable) ?: logger.error( - "Exception caught in Player scope: ${throwable.message}", - throwable.stackTrace - .joinToString("\n") { - "\tat $it" - }.replaceFirst("\tat ", "\n"), - ) + coroutineExceptionHandler = + config.coroutineExceptionHandler ?: CoroutineExceptionHandler { _, throwable -> + if (state !is ReleasedState) { + logger.error("[HeadlessPlayer]: Error has been found") + inProgressState?.controllers?.error?.captureError(throwable, ErrorTypes.RENDER) ?: logger.error( + "Exception caught in Player scope: ${throwable.message}", + throwable.stackTrace + .joinToString("\n") { + "\tat $it" + }.replaceFirst("\tat ", "\n"), + ) + } } - } } override val scope: CoroutineScope by runtime::scope diff --git a/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/PlayerExceptionMetadata.kt b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/PlayerExceptionMetadata.kt new file mode 100644 index 000000000..617a24825 --- /dev/null +++ b/jvm/core/src/main/kotlin/com/intuit/playerui/core/player/PlayerExceptionMetadata.kt @@ -0,0 +1,9 @@ +package com.intuit.playerui.core.player + +import com.intuit.playerui.core.error.ErrorSeverity + +public interface PlayerExceptionMetadata { + public val type: String + public val severity: ErrorSeverity? + public val metadata: Map? +} diff --git a/jvm/hermes/src/test/kotlin/com/intuit/playerui/jsi/serialization/serializers/ThrowableSerializerTest.kt b/jvm/hermes/src/test/kotlin/com/intuit/playerui/jsi/serialization/serializers/ThrowableSerializerTest.kt index 86a3ef31a..5ddbc2b64 100644 --- a/jvm/hermes/src/test/kotlin/com/intuit/playerui/jsi/serialization/serializers/ThrowableSerializerTest.kt +++ b/jvm/hermes/src/test/kotlin/com/intuit/playerui/jsi/serialization/serializers/ThrowableSerializerTest.kt @@ -1,8 +1,11 @@ package com.intuit.playerui.jsi.serialization.serializers +import com.intuit.playerui.core.bridge.JSErrorException import com.intuit.playerui.core.bridge.serialization.serializers.ThrowableSerializer import com.intuit.playerui.core.bridge.serialization.serializers.ThrowableSerializer.SerializableStackTraceElement +import com.intuit.playerui.core.error.ErrorSeverity import com.intuit.playerui.core.player.PlayerException +import com.intuit.playerui.core.player.PlayerExceptionMetadata import com.intuit.playerui.hermes.base.HermesTest import com.intuit.playerui.hermes.extensions.evaluateInJSThreadBlocking import com.intuit.playerui.jsi.Value @@ -12,6 +15,21 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import kotlin.test.currentStackTrace +private class ExceptionWithMetadata( + message: String, + cause: Throwable? = null, +) : PlayerException(message, cause), + PlayerExceptionMetadata { + override val type: String + get() = "TestError" + override val severity: ErrorSeverity? + get() = ErrorSeverity.ERROR + override val metadata: Map? + get() = mapOf( + "testProperty" to "testValue", + ) +} + // TODO: This should be a core [RuntimeTest] internal class ThrowableSerializerTest : HermesTest() { @Test @@ -32,6 +50,31 @@ internal class ThrowableSerializerTest : HermesTest() { ) } + @Test + fun `JS Error is deserialized as PlayerException (with additional metadata)`() = runtime.evaluateInJSThreadBlocking { + val error = runtime + .global() + .getPropertyAsFunction(runtime, "Error") + .callAsConstructor(runtime, Value.from(runtime, "hello")) + + error.asObject(runtime).setProperty(runtime, "type", Value.from(runtime, "ErrorType")) + error.asObject(runtime).setProperty(runtime, "severity", Value.from(runtime, "error")) + error.asObject(runtime).setProperty(runtime, "metadata", Value.createFromJsonUtf8(runtime, "{\"testProperty\":\"testValue\"}")) + val exception = format.decodeFromRuntimeValue(ThrowableSerializer(), error) + + Assertions.assertTrue(exception is JSErrorException) + exception as JSErrorException + Assertions.assertEquals("Error: hello", exception.message) + Assertions.assertEquals( + """com.intuit.playerui.core.bridge.JSErrorException: Error: hello +""", + exception.stackTraceToString(), + ) + Assertions.assertEquals("ErrorType", exception.type) + Assertions.assertEquals(ErrorSeverity.ERROR, exception.severity) + Assertions.assertEquals(mapOf("testProperty" to "testValue"), exception.metadata) + } + @Test fun `PlayerException is serialized as JS Error`() = runtime.evaluateInJSThreadBlocking { val stackTraceElement = currentStackTrace().first() @@ -61,6 +104,42 @@ internal class ThrowableSerializerTest : HermesTest() { ) } + @Test + fun `Additional PlayerExceptionMetadata is serialized into JS Error`() = runtime.evaluateInJSThreadBlocking { + val stackTraceElement = currentStackTrace().first() + val className = stackTraceElement.className + val methodName = stackTraceElement.methodName + val fileName = stackTraceElement.fileName + val lineNumber = stackTraceElement.lineNumber + val serializableStackTraceElement = SerializableStackTraceElement( + className, + methodName, + fileName, + lineNumber, + ) + + val exception = ExceptionWithMetadata("world") + exception.stackTrace = arrayOf(stackTraceElement) + val error = format.encodeToRuntimeValue(ThrowableSerializer(), exception).asObject(runtime) + Assertions.assertEquals("world", error.getProperty(runtime, "message").asString(runtime)) + Assertions.assertEquals(exception.stackTraceToString(), error.getProperty(runtime, "stack").asString(runtime)) + + Assertions.assertEquals(true, error.getProperty(runtime, "serialized").asBoolean()) + Assertions.assertEquals( + serializableStackTraceElement, + format.decodeFromValue( + error.getPropertyAsObject(runtime, "stackTrace").asArray(runtime).getValueAtIndex(runtime, 0), + ), + ) + + Assertions.assertEquals(ErrorSeverity.ERROR.value, error.getProperty(runtime, "severity").asString(runtime)) + Assertions.assertEquals("TestError", error.getProperty(runtime, "type").asString(runtime)) + Assertions.assertEquals( + "testValue", + error.getPropertyAsObject(runtime, "metadata").getProperty(runtime, "testProperty").asString(runtime), + ) + } + @Test fun `JS Error is deserialized as PlayerException using serialized stack`() { val stackTraceElement = currentStackTrace().first() diff --git a/jvm/j2v8/src/test/kotlin/com/intuit/playerui/j2v8/bridge/serialization/serializers/ThrowableSerializerTest.kt b/jvm/j2v8/src/test/kotlin/com/intuit/playerui/j2v8/bridge/serialization/serializers/ThrowableSerializerTest.kt index 9280a653b..d06671971 100644 --- a/jvm/j2v8/src/test/kotlin/com/intuit/playerui/j2v8/bridge/serialization/serializers/ThrowableSerializerTest.kt +++ b/jvm/j2v8/src/test/kotlin/com/intuit/playerui/j2v8/bridge/serialization/serializers/ThrowableSerializerTest.kt @@ -3,7 +3,9 @@ package com.intuit.playerui.j2v8.bridge.serialization.serializers import com.eclipsesource.v8.V8Object import com.intuit.playerui.core.bridge.serialization.serializers.ThrowableSerializer import com.intuit.playerui.core.bridge.serialization.serializers.ThrowableSerializer.SerializableStackTraceElement +import com.intuit.playerui.core.error.ErrorSeverity import com.intuit.playerui.core.player.PlayerException +import com.intuit.playerui.core.player.PlayerExceptionMetadata import com.intuit.playerui.j2v8.base.J2V8Test import com.intuit.playerui.j2v8.bridge.serialization.format.decodeFromV8Value import com.intuit.playerui.j2v8.extensions.evaluateInJSThreadBlocking @@ -14,6 +16,21 @@ import org.junit.jupiter.api.Test private inline fun currentStackTrace() = Exception().stackTrace +private class ExceptionWithMetadata( + message: String, + cause: Throwable? = null, +) : PlayerException(message, cause), + PlayerExceptionMetadata { + override val type: String + get() = "TestError" + override val severity: ErrorSeverity? + get() = ErrorSeverity.ERROR + override val metadata: Map? + get() = mapOf( + "testProperty" to "testValue", + ) +} + // TODO: This should be a core [RuntimeTest] internal class ThrowableSerializerTest : J2V8Test() { @Test @@ -70,6 +87,46 @@ internal class ThrowableSerializerTest : J2V8Test() { } } + @Test + fun `Additional PlayerExceptionMetadata properties are added to JS Error`() { + val stackTraceElement = currentStackTrace().first() + val className = stackTraceElement.className + val methodName = stackTraceElement.methodName + val fileName = stackTraceElement.fileName + val lineNumber = stackTraceElement.lineNumber + val serializableStackTraceElement = SerializableStackTraceElement( + className, + methodName, + fileName, + lineNumber, + ) + + val exception = ExceptionWithMetadata("world") + exception.stackTrace = arrayOf(stackTraceElement) + val error = format.encodeToRuntimeValue(ThrowableSerializer(), exception) + + assertTrue(error is V8Object) + error as V8Object + + error.evaluateInJSThreadBlocking(runtime) { + assertEquals("world", error.get("message")) + assertEquals(exception.stackTraceToString(), error.getString("stack")) + + assertEquals(true, error.get("serialized")) + assertTrue( + format + .encodeToRuntimeValue( + SerializableStackTraceElement.serializer(), + serializableStackTraceElement, + ).jsEquals(error.getArray("stackTrace").getObject(0)), + ) + + assertEquals(ErrorSeverity.ERROR.value, error.get("severity")) + assertEquals("TestError", error.get("type")) + assertEquals("testValue", error.get("metadata.testProperty")) + } + } + @Test fun `JS Error is deserialized as PlayerException using serialized stack`() { val stackTraceElement = currentStackTrace().first() diff --git a/jvm/testutils/src/main/kotlin/com/intuit/playerui/utils/test/PlayerTest.kt b/jvm/testutils/src/main/kotlin/com/intuit/playerui/utils/test/PlayerTest.kt index f95acdf32..b834f2487 100644 --- a/jvm/testutils/src/main/kotlin/com/intuit/playerui/utils/test/PlayerTest.kt +++ b/jvm/testutils/src/main/kotlin/com/intuit/playerui/utils/test/PlayerTest.kt @@ -29,8 +29,13 @@ public abstract class PlayerTest : } /** Helper method for setting a [player] with configurable [plugins] and [runtime] */ - public fun setupPlayer(plugins: List = this.plugins + this, runtime: Runtime<*> = this.runtime) { - player = HeadlessPlayer(plugins, runtime, config = runtime.config) + public fun setupPlayer( + plugins: List = this.plugins, + runtime: Runtime<*> = this.runtime, + includeThisInPlugins: Boolean = true, + ) { + val allPlugins = if (includeThisInPlugins) plugins + this else plugins + player = HeadlessPlayer(allPlugins, runtime, config = runtime.config) } } diff --git a/plugins/async-node/core/BUILD b/plugins/async-node/core/BUILD index fe3cb54ae..60381b64a 100644 --- a/plugins/async-node/core/BUILD +++ b/plugins/async-node/core/BUILD @@ -23,6 +23,7 @@ js_pipeline( test_deps = [ ":node_modules/@player-ui/check-path-plugin", ":node_modules/@player-ui/partial-match-registry", + ":node_modules/@player-ui/expression-plugin", "//:vitest_config", ] ) diff --git a/plugins/async-node/core/package.json b/plugins/async-node/core/package.json index fd511995b..12c36fd56 100644 --- a/plugins/async-node/core/package.json +++ b/plugins/async-node/core/package.json @@ -7,6 +7,7 @@ }, "devDependencies": { "@player-ui/check-path-plugin": "workspace:*", - "@player-ui/partial-match-registry": "workspace:*" + "@player-ui/partial-match-registry": "workspace:*", + "@player-ui/expression-plugin": "workspace:*" } } diff --git a/plugins/async-node/core/src/AsyncNodeError.ts b/plugins/async-node/core/src/AsyncNodeError.ts new file mode 100644 index 000000000..bc3e2228a --- /dev/null +++ b/plugins/async-node/core/src/AsyncNodeError.ts @@ -0,0 +1,30 @@ +import { + ErrorSeverity, + type Node, + type PlayerErrorMetadata, +} from "@player-ui/player"; + +export const ASYNC_ERROR_TYPE = "ASYNC-PLUGIN"; +export type AsyncErrorMetadata = { + node: Node.Async; +}; +export class AsyncNodeError + extends Error + implements PlayerErrorMetadata +{ + readonly type: string = ASYNC_ERROR_TYPE; + readonly severity: ErrorSeverity = ErrorSeverity.ERROR; + readonly metadata: AsyncErrorMetadata; + + constructor( + node: Node.Async, + message?: string, + public readonly cause?: Error | undefined, + ) { + super(message); + + this.metadata = { + node, + }; + } +} diff --git a/plugins/async-node/core/src/__tests__/index.test.ts b/plugins/async-node/core/src/__tests__/index.test.ts index d9b152b40..eea6625f5 100644 --- a/plugins/async-node/core/src/__tests__/index.test.ts +++ b/plugins/async-node/core/src/__tests__/index.test.ts @@ -8,6 +8,9 @@ import { BeforeTransformFunction, Flow, NodeType, + Logger, + ErrorTypes, + ErrorSeverity, } from "@player-ui/player"; import { Player, Parser } from "@player-ui/player"; import { waitFor } from "@testing-library/react"; @@ -18,6 +21,8 @@ import { } from "../index"; import { CheckPathPlugin } from "@player-ui/check-path-plugin"; import { Registry } from "@player-ui/partial-match-registry"; +import { ExpressionPlugin } from "@player-ui/expression-plugin"; +import { AsyncNodeError } from "../AsyncNodeError"; const transform: BeforeTransformFunction = createAsyncTransform({ transformAssetType: "chat-message", @@ -79,7 +84,35 @@ const asyncAssetFrf: Flow = { VIEW_1: { state_type: "VIEW", ref: "my-view", - transitions: {}, + transitions: { + "*": "END", + }, + }, + END: { + state_type: "END", + outcome: "done", + }, + }, + }, +}; + +const nonViewErrorFlow: Flow = { + id: "test-flow", + views: [], + navigation: { + BEGIN: "FLOW_1", + FLOW_1: { + startState: "ACTION_1", + ACTION_1: { + state_type: "ACTION", + exp: ["captureError()"], + transitions: { + "*": "END", + }, + }, + END: { + state_type: "END", + outcome: "done", }, }, }, @@ -1075,7 +1108,7 @@ describe("view", () => { await waitFor(() => { expect(onAsyncNodeErrorCallback).toHaveBeenCalledWith( - new Error("Promise Rejected"), + expect.objectContaining({ cause: new Error("Promise Rejected") }), expect.anything(), ); @@ -1105,14 +1138,242 @@ describe("view", () => { await waitFor(() => { expect(onAsyncNodeErrorCallback).toHaveBeenCalledWith( - new Error("Promise Rejected"), + expect.objectContaining({ cause: new Error("Promise Rejected") }), expect.anything(), ); const playerState = player.getState(); expect(playerState.status).toBe("error"); const errorState = playerState as ErrorState; - expect(errorState.error.message).toBe("Promise Rejected"); + expect(errorState.error.message).toBe( + "An error occured during async node resolution. See cause for details.", + ); + expect(errorState.error).toBeInstanceOf(AsyncNodeError); + expect((errorState.error as AsyncNodeError).cause?.message).toBe( + "Promise Rejected", + ); + }); + }); + + test("should log and absorb errors occuring outside of an in-progress state", async () => { + const vitestLogger: Logger = { + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + trace: vi.fn(), + warn: vi.fn(), + }; + const plugin = new AsyncNodePlugin({ + plugins: [new AsyncNodePluginPlugin()], + }); + + let throwAsyncError: ((err: Error) => void) | undefined; + + plugin.hooks.onAsyncNode.tap("test", async () => { + return new Promise((_, rej) => { + throwAsyncError = rej; + }); + }); + + const plugins = [plugin, new TestAsyncPlugin()]; + + const player = new Player({ + plugins: plugins, + logger: vitestLogger, + }); + + player.start(asyncAssetFrf); + + await vi.waitFor(() => { + expect(throwAsyncError).toBeDefined(); + }); + + (player.getState() as InProgressState).controllers.flow.transition( + "done", + ); + await vi.waitFor(() => { + const playerState = player.getState(); + expect(playerState.status).toBe("completed"); + }); + + throwAsyncError!(new Error("Test Error")); + + await vi.waitFor(() => { + const state = player.getState(); + // should not leave completed state + expect(state.status).toBe("completed"); + + expect(vitestLogger.warn).toHaveBeenCalledWith( + expect.any(String), // Message doesn't matter, just check that the logged error is correct + new Error("Test Error"), + ); + }); + }); + + test("should do nothing with errors when not in a view state", async () => { + const plugin = new AsyncNodePlugin({ + plugins: [new AsyncNodePluginPlugin()], + }); + // Call capture error in an action state to make sure asyncnodeplugin doesn't try to handle this + const expPlugin = new ExpressionPlugin( + new Map([ + [ + "captureError", + () => { + ( + player.getState() as InProgressState + ).controllers.error.captureError( + new Error("Test Error"), + ErrorTypes.RENDER, + ErrorSeverity.ERROR, + { assetId: "asset" }, + ); + }, + ], + ]), + ); + const plugins = [plugin, expPlugin, new TestAsyncPlugin()]; + + const player = new Player({ + plugins: plugins, + }); + + player.start(nonViewErrorFlow).catch(() => {}); + + await vi.waitFor(() => { + const state = player.getState(); + expect(state.status).toBe("error"); + expect(onAsyncNodeErrorCallback).not.toHaveBeenCalled(); + }); + }); + + test("should fail to handle errors if the plugin is setup incorrectly", async () => { + const vitestLogger: Logger = { + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + trace: vi.fn(), + warn: vi.fn(), + }; + const nodePluginPlugin = new AsyncNodePluginPlugin(); + + // Apply AsyncNodePluginPlugin in isolation to observe failures + const plugin: PlayerPlugin = { + name: "TestPlayerPlugin", + apply: (player: Player) => { + nodePluginPlugin.applyPlayer(player); + player.hooks.view.tap("test", (view) => { + nodePluginPlugin.apply(view); + }); + }, + }; + const plugins = [plugin, new TestAsyncPlugin()]; + + const player = new Player({ + plugins: plugins, + logger: vitestLogger, + }); + player.start(asyncAssetFrf).catch(() => {}); + + await vi.waitFor(() => { + const state = player.getState(); + expect(state.status).toBe("in-progress"); + }); + + (player.getState() as InProgressState).controllers.error.captureError( + new Error("Test Error"), + ErrorTypes.VIEW, + ErrorSeverity.ERROR, + { + node: { + type: NodeType.Async, + value: {}, + }, + }, + ); + + await vi.waitFor(() => { + const state = player.getState(); + expect(state.status).toBe("error"); + expect(onAsyncNodeErrorCallback).not.toHaveBeenCalled(); + expect(vitestLogger.warn).toHaveBeenCalledWith( + "[AsyncNodePlugin]: No plugin detected. Error handling will fail", + ); + }); + }); + + test("should call onAsyncNodeError hook for any async node involved in generating the current one", async () => { + const plugin = new AsyncNodePlugin({ + plugins: [new AsyncNodePluginPlugin()], + }); + + const errorHandler = vi.fn((err: Error, node: Node.Async) => { + if (node.id === "async-chat-id-2") { + return { + asset: { + type: "text", + value: "text", + id: "FIXED", + }, + }; + } + + return undefined; + }); + + plugin.hooks.onAsyncNodeError.tap("test", errorHandler); + + let id = 0; + plugin.hooks.onAsyncNode.tap("test", () => { + const messageId = id++; + if (messageId > 5) { + throw new Error("Test Error"); + } + + return Promise.resolve({ + asset: { + type: "chat-message", + id: `chat-id-${messageId}`, + value: { + id: `chat-id-text-${messageId}`, + type: "text", + value: `Test Message ${messageId}`, + }, + }, + }); + }); + + const player = new Player({ + plugins: [plugin, new TestAsyncPlugin()], + }); + player.start(asyncAssetFrf).catch(() => {}); + + await vi.waitFor(() => { + expect(id).toBeGreaterThan(5); + expect(errorHandler).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + id: "async-chat-id-5", + }), + ); + expect(errorHandler).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + id: "async-chat-id-4", + }), + ); + expect(errorHandler).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + id: "async-chat-id-3", + }), + ); + expect(errorHandler).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + id: "async-chat-id-2", + }), + ); }); }); }); diff --git a/plugins/async-node/core/src/index.ts b/plugins/async-node/core/src/index.ts index 1c0fb3524..44d6ec56b 100644 --- a/plugins/async-node/core/src/index.ts +++ b/plugins/async-node/core/src/index.ts @@ -10,33 +10,17 @@ import type { ViewPlugin, Resolver, Resolve, - ViewController, } from "@player-ui/player"; import { AsyncSeriesBailHook, SyncBailHook } from "tapable-ts"; import queueMicrotask from "queue-microtask"; +import { AsyncNodeError } from "./AsyncNodeError"; +import { AsyncNodeInfo, AsyncPluginContext } from "./internal-types"; +import { getNodeFromError } from "./utils"; export * from "./types"; export * from "./transform"; export * from "./createAsyncTransform"; -/** Object type for storing data related to a single `apply` of the `AsyncNodePluginPlugin` - * This object should be setup once per ViewInstance to keep any cached info just for that view to avoid conflicts of shared async node ids across different view states. - */ -type AsyncPluginContext = { - /** Map of async node id to resolved content */ - nodeResolveCache: Map; - /** The view instance this context is attached to. */ - view: ViewInstance; - /** The view controller this context is attached to. */ - viewController: ViewController; - /** Map of async node id to promises being used to resolve them */ - inProgressNodes: Set; - /** Map of async node ids to the original node they represent. - * In some cases, async nodes are transformed into from other node types so the original reference is needed in order to trigger an update on the view when the async node changes. - */ - originalNodeCache: Map>; -}; - export interface AsyncNodePluginOptions { /** A set of plugins to load */ plugins?: AsyncNodeViewPlugin[]; @@ -142,10 +126,10 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin { node: Node.Async, context: AsyncPluginContext, result: any, - options: Resolve.NodeResolveOptions, + parseFunction?: (content: any) => Node.Node | null, ) { let parsedNode = - options.parseNode && result ? options.parseNode(result) : undefined; + parseFunction && result ? parseFunction(result) : undefined; if (parsedNode && node.onValueReceived) { parsedNode = node.onValueReceived(parsedNode); @@ -168,24 +152,43 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin { context: AsyncPluginContext, newNode?: Node.Node | null, ) { - const { nodeResolveCache, viewController, originalNodeCache } = context; - if (nodeResolveCache.get(node.id) !== newNode) { - nodeResolveCache.set(node.id, newNode ? newNode : node); - const originalNode = originalNodeCache.get(node.id) ?? new Set([node]); - viewController.updateViewAST(originalNode); + const { asyncNodeCache: asyncNodeInfo, viewController } = context; + const entry = asyncNodeInfo.get(node.id); + if (!entry) { + throw new Error("Failed to update async content. Cache entry not found"); + } + if (entry.resolvedContent !== newNode) { + entry.resolvedContent = newNode ? newNode : entry.asyncNode; + viewController.updateViewAST(entry.updateNodes); } } private hasValidMapping( - node: Node.Async, - context: AsyncPluginContext, - ): boolean { - const { nodeResolveCache } = context; + cacheEntry: AsyncNodeInfo, + ): cacheEntry is Required { return ( - nodeResolveCache.has(node.id) && nodeResolveCache.get(node.id) !== node + cacheEntry.resolvedContent !== undefined && + cacheEntry.resolvedContent !== cacheEntry.asyncNode ); } + private getOrCreateAsyncNodeCacheEntry( + node: Node.Async, + context: AsyncPluginContext, + ): AsyncNodeInfo { + const { asyncNodeCache: asyncNodeInfo } = context; + let entry = asyncNodeInfo.get(node.id); + if (!entry) { + entry = { + asyncNode: node, + updateNodes: new Set(), + }; + asyncNodeInfo.set(node.id, entry); + } + + return entry; + } + /** * Handles the asynchronous API integration for resolving nodes. * This method sets up a hook on the resolver's `beforeResolve` event to process async nodes. @@ -193,17 +196,32 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin { * @param view */ applyResolver(resolver: Resolver, context: AsyncPluginContext): void { + const { assetIdCache } = context; + resolver.hooks.afterNodeUpdate.tap(this.name, (original, _, update) => { + if ( + update.node.type !== NodeType.Asset && + update.node.type !== NodeType.View + ) { + return; + } + + assetIdCache.set(update.value.id, original); + }); + resolver.hooks.beforeResolve.tap(this.name, (node, options) => { if (!this.isAsync(node)) { return node === null ? node : this.resolveAsyncChildren(node, context); } + + const entry = this.getOrCreateAsyncNodeCacheEntry(node, context); + if (options.node) { - context.originalNodeCache.set(node.id, new Set([options.node])); + entry.updateNodes = new Set([options.node]); + context.generatedByMap.set(options.node, node.id); } - const resolvedNode = context.nodeResolveCache.get(node.id); - if (resolvedNode !== undefined) { - return this.resolveAsyncChildren(resolvedNode, context); + if (entry.resolvedContent !== undefined) { + return this.resolveAsyncChildren(entry.resolvedContent, context); } if (context.inProgressNodes.has(node.id)) { @@ -236,20 +254,24 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin { let index = 0; while (index < node.values.length) { const childNode = node.values[index]; - if ( - childNode?.type !== NodeType.Async || - !this.hasValidMapping(childNode, context) - ) { + if (childNode?.type !== NodeType.Async) { index++; continue; } + const entry = this.getOrCreateAsyncNodeCacheEntry(childNode, context); - const mappedNode = context.nodeResolveCache.get(childNode.id)!; + if (!this.hasValidMapping(entry)) { + index++; + continue; + } + + const mappedNode = entry.resolvedContent; const nodeSet = new Set(); if (mappedNode.type === NodeType.MultiNode && childNode.flatten) { mappedNode.values.forEach((v: Node.Node) => { v.parent = node; nodeSet.add(v); + context.originalParentMap.set(v, childNode); }); node.values = [ ...node.values.slice(0, index), @@ -261,17 +283,23 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin { mappedNode.parent = node; nodeSet.add(mappedNode); } - context.originalNodeCache.set(childNode.id, nodeSet); + entry.updateNodes = nodeSet; + for (const n of nodeSet) { + context.generatedByMap.set(n, childNode.id); + } } } else if ("children" in node) { node.children?.forEach((c) => { // Similar to above, using a while loop lets us handle when async nodes produce more async nodes. - while ( - c.value.type === NodeType.Async && - this.hasValidMapping(c.value, context) - ) { - const mappedNode = context.nodeResolveCache.get(c.value.id)!; - context.originalNodeCache.set(c.value.id, new Set([mappedNode])); + while (c.value.type === NodeType.Async) { + const entry = this.getOrCreateAsyncNodeCacheEntry(c.value, context); + if (!this.hasValidMapping(entry)) { + break; + } + + const mappedNode = entry.resolvedContent; + entry.updateNodes = new Set([mappedNode]); + context.generatedByMap.set(mappedNode, c.value.id); c.value = mappedNode; c.value.parent = node; } @@ -290,35 +318,31 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin { const result = await this.basePlugin?.hooks.onAsyncNode.call( node, (result) => { - this.parseNodeAndUpdate(node, context, result, options); + this.parseNodeAndUpdate(node, context, result, options.parseNode); }, ); // Stop tracking before the next update is triggered context.inProgressNodes.delete(node.id); - this.parseNodeAndUpdate(node, context, result, options); + this.parseNodeAndUpdate(node, context, result, options.parseNode); } catch (e: unknown) { - const error = e instanceof Error ? e : new Error(String(e)); - const result = this.basePlugin?.hooks.onAsyncNodeError.call(error, node); - - if (result === undefined) { - const playerState = this.basePlugin?.getPlayerInstance()?.getState(); - - if (playerState?.status === "in-progress") { - playerState.fail(error); - } - + const cause = e instanceof Error ? e : new Error(String(e)); + const playerState = this.basePlugin?.getPlayerInstance()?.getState(); + + if (playerState?.status !== "in-progress") { + options.logger?.warn( + "[AsyncNodePlugin]: An error occured during async node resolution, but the player instance is no londer running. Exception: ", + cause, + ); return; } - options.logger?.error( - "Async node handling failed and resolved with a fallback. Error:", - error, + const error = new AsyncNodeError( + node, + "An error occured during async node resolution. See cause for details.", + cause, ); - - // Stop tracking before the next update is triggered - context.inProgressNodes.delete(node.id); - this.parseNodeAndUpdate(node, context, result, options); + playerState.controllers.error.captureError(error, error.type); } } @@ -385,15 +409,112 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin { } applyPlayer(player: Player): void { + // TODO: Need a better mechanism for storing the current context. + let currentContext: AsyncPluginContext | undefined = undefined; + let parser: Parser | undefined = undefined; + + player.hooks.errorController.tap("async", (errorController) => { + errorController.hooks.onError.tap("async", (playerError) => { + if (currentContext === undefined) { + return undefined; + } + + /** Try to handle the error using the onAsyncNodeError hook. Returns true if new content is provided. */ + const tryHandleError = (asyncNode: Node.Async): boolean => { + if (this.basePlugin === undefined) { + player.logger.warn( + `[AsyncNodePlugin]: No plugin detected. Error handling will fail`, + ); + } + + let result: any = undefined; + result = this.basePlugin?.hooks.onAsyncNodeError.call( + playerError.error, + asyncNode, + ); + + if (result === undefined) { + return false; + } + + player.logger?.warn( + "[AsyncNodePlugin]: Async node handling failed and resolved with a fallback. Cause:", + playerError.error.message, + ); + + // Stop tracking before the next update is triggered + currentContext!.inProgressNodes.delete(asyncNode.id); + this.parseNodeAndUpdate( + asyncNode, + currentContext!, + result, + parser?.parseObject.bind(parser), + ); + + return true; + }; + + const getNextNode = (node: Node.Node): Node.Node | undefined => { + const parent = + currentContext?.originalParentMap.get(node) ?? node.parent; + + if (!parent) { + return undefined; + } + + // asyncNodeCache has current asyncNode reference more up to date with what's happening in the resolver. Sometimes AsyncNodeError has old references so this helps us move up the tree more accurately + return this.isAsync(parent) + ? currentContext?.asyncNodeCache.get(parent.id)?.asyncNode + : parent; + }; + + let node = getNodeFromError(playerError, currentContext); + // If the node is an async node try, to handle errors with it first. + if (node?.type === NodeType.Async && tryHandleError(node)) { + return true; + } + + // Loop through the nodes to see if something is generated by something else. Continue until the error is handled or there are no more nodes to check + while (node !== undefined) { + const generatedBy = currentContext.generatedByMap.get(node); + if (generatedBy) { + const entry = currentContext.asyncNodeCache.get(generatedBy); + + if (!entry) { + node = getNextNode(node); + continue; + } + + const { asyncNode } = entry; + + // Don't return false when the error isn't handled to allow for cases where one async is generated by another. Give different nodes a chance to try to recover from the error. + if (tryHandleError(asyncNode)) { + return true; + } + } + + node = getNextNode(node); + } + + return undefined; + }); + }); + player.hooks.viewController.tap("async", (viewController) => { viewController.hooks.view.tap("async", (view) => { + view.hooks.parser.tap(this.name, (p) => { + parser = p; + }); const context: AsyncPluginContext = { - nodeResolveCache: new Map(), inProgressNodes: new Set(), view, viewController, - originalNodeCache: new Map(), + generatedByMap: new Map(), + assetIdCache: new Map(), + asyncNodeCache: new Map(), + originalParentMap: new Map(), }; + currentContext = context; view.hooks.resolver.tap("async", (resolver) => { this.applyResolver(resolver, context); diff --git a/plugins/async-node/core/src/internal-types.ts b/plugins/async-node/core/src/internal-types.ts new file mode 100644 index 000000000..55c8344f8 --- /dev/null +++ b/plugins/async-node/core/src/internal-types.ts @@ -0,0 +1,30 @@ +import type { Node, ViewController, ViewInstance } from "@player-ui/player"; + +export type AsyncNodeInfo = { + /** The async node */ + asyncNode: Node.Async; + /** All nodes that need to be updated when the async content changes. This could be the async node itself, or a node that created it (i.e. an asset node that transformed into an async node). */ + updateNodes: Set; + /** The resolved content for this async node */ + resolvedContent?: Node.Node; +}; + +/** Object type for storing data related to a single `apply` of the `AsyncNodePluginPlugin` + * This object should be setup once per ViewInstance to keep any cached info just for that view to avoid conflicts of shared async node ids across different view states. + */ +export type AsyncPluginContext = { + /** The view instance this context is attached to. */ + view: ViewInstance; + /** The view controller this context is attached to. */ + viewController: ViewController; + /** Set of async node ids that are currently being managed by an incomplete promise. */ + inProgressNodes: Set; + /** Map of async node ids to related info for that node. */ + asyncNodeCache: Map; + /** Maps asset ids to its original node. Used to then search for what if could be generated by. */ + assetIdCache: Map; + /** Maps nodes to the async id that generated them. */ + generatedByMap: Map; + /** Map to track the original parent of nodes that were flattened into another. This is needed to track which async nodes actually generated these nodes. */ + originalParentMap: Map; +}; diff --git a/plugins/async-node/core/src/utils/__tests__/getNodeFromError.test.ts b/plugins/async-node/core/src/utils/__tests__/getNodeFromError.test.ts new file mode 100644 index 000000000..4e67042dc --- /dev/null +++ b/plugins/async-node/core/src/utils/__tests__/getNodeFromError.test.ts @@ -0,0 +1,236 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { getNodeFromError } from "../getNodeFromError"; +import { isAsyncPlayerError } from "../isAsyncPlayerError"; +import { + ErrorMetadata, + ErrorSeverity, + ErrorTypes, + NodeType, + PlayerError, + PlayerErrorMetadata, +} from "@player-ui/player"; +import { AsyncNodeInfo, AsyncPluginContext } from "../../internal-types"; +import { ASYNC_ERROR_TYPE, AsyncNodeError } from "../../AsyncNodeError"; + +vi.mock("../isAsyncPlayerError"); + +/** Test class to create an error with any additional properties */ +class ErrorWithProps extends Error implements PlayerErrorMetadata { + constructor( + message: string, + public type: string, + public severity?: ErrorSeverity, + public metadata?: ErrorMetadata, + ) { + super(message); + } +} + +const createPlayerError = ( + errorType: string, + metadata?: ErrorMetadata, +): PlayerError => ({ + error: new ErrorWithProps("Error", errorType, ErrorSeverity.ERROR, metadata), + errorType, + skipped: false, + metadata, + severity: ErrorSeverity.ERROR, +}); + +const createPlayerErrorForError = ( + error: Error & PlayerErrorMetadata, +): PlayerError => ({ + error, + errorType: error.type, + metadata: error.metadata, + severity: error.severity, + skipped: false, +}); + +const createContext = ( + baseContext?: Partial, +): AsyncPluginContext => ({ + assetIdCache: new Map(), + asyncNodeCache: new Map(), + generatedByMap: new Map(), + inProgressNodes: new Set(), + originalParentMap: new Map(), + view: {} as any, + viewController: {} as any, + ...baseContext, +}); + +describe("getNodeFromError", () => { + beforeEach(() => { + vi.mocked(isAsyncPlayerError).mockReturnValue(false); + }); + describe("render errors", () => { + it("should be undefined when no string assetId is in the error metadata", () => { + const result = getNodeFromError( + createPlayerError(ErrorTypes.RENDER, {}), + createContext(), + ); + + expect(result).toBeUndefined(); + }); + + it("should be undefined the assetId is not in the context cache", () => { + const result = getNodeFromError( + createPlayerError(ErrorTypes.RENDER, { assetId: "test-id" }), + createContext(), + ); + + expect(result).toBeUndefined(); + }); + + it("should be the node from the assetIdCache if the assetId is available", () => { + const context = createContext(); + context.assetIdCache.set("test-id", { + type: NodeType.Value, + value: { + prop: "value", + }, + }); + const result = getNodeFromError( + createPlayerError(ErrorTypes.RENDER, { assetId: "test-id" }), + context, + ); + + expect(result).toStrictEqual({ + type: NodeType.Value, + value: { + prop: "value", + }, + }); + }); + }); + + describe("view errors", () => { + const notRealNodes = [undefined, null, [], "node"]; + it.each(notRealNodes)( + "should return undefined for any view error without a node", + (node) => { + const result = getNodeFromError( + createPlayerError(ErrorTypes.VIEW, { node }), + createContext(), + ); + + expect(result).toBeUndefined(); + }, + ); + + it("should return the node property if it is a node", () => { + const result = getNodeFromError( + createPlayerError(ErrorTypes.VIEW, { + node: { + type: NodeType.Value, + value: { + prop: "value", + }, + }, + }), + createContext(), + ); + + expect(result).toStrictEqual({ + type: NodeType.Value, + value: { + prop: "value", + }, + }); + }); + }); + + describe("async errors", () => { + it("should return undefined if the error is not recognized as an async error", () => { + const result = getNodeFromError( + { + error: new ErrorWithProps( + "Error", + ASYNC_ERROR_TYPE, + ErrorSeverity.ERROR, + { + node: { + type: NodeType.Value, + value: { + prop: "value", + }, + }, + }, + ), + errorType: ASYNC_ERROR_TYPE, + skipped: false, + }, + createContext(), + ); + + expect(result).toBeUndefined(); + }); + + it("should return undefined if the node from the error is undefined", () => { + vi.mocked(isAsyncPlayerError).mockReturnValue(true); + const result = getNodeFromError( + createPlayerError(ASYNC_ERROR_TYPE, undefined), + createContext(), + ); + + expect(result).toBeUndefined(); + }); + + it("should return the node the asyncNodeCache if an id matches", () => { + vi.mocked(isAsyncPlayerError).mockReturnValue(true); + const cacheEntry: AsyncNodeInfo = { + asyncNode: { + type: NodeType.Async, + id: "test-id", + value: { + type: NodeType.Value, + value: { + prop: "value cached", + }, + }, + }, + updateNodes: new Set(), + }; + const result = getNodeFromError( + createPlayerErrorForError( + new AsyncNodeError({ + type: NodeType.Async, + id: "test-id", + value: { + type: NodeType.Value, + value: { + prop: "value", + }, + }, + }), + ), + createContext({ + asyncNodeCache: new Map([["test-id", cacheEntry]]), + }), + ); + + expect(result).toStrictEqual({ + type: NodeType.Async, + id: "test-id", + value: { + type: NodeType.Value, + value: { + prop: "value cached", + }, + }, + }); + }); + }); + + describe("other errors", () => { + it("should return undefined for all other errors", () => { + const result = getNodeFromError( + createPlayerError("UNKNOWN", {}), + createContext(), + ); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/plugins/async-node/core/src/utils/__tests__/isAsyncPlayerError.test.ts b/plugins/async-node/core/src/utils/__tests__/isAsyncPlayerError.test.ts new file mode 100644 index 000000000..1c3f7ff98 --- /dev/null +++ b/plugins/async-node/core/src/utils/__tests__/isAsyncPlayerError.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { isAsyncPlayerError } from "../isAsyncPlayerError"; + +describe("isAsyncPlayerError", () => { + it("should return true for errors with the type 'ASYNC-PLUGIN'", () => { + expect( + isAsyncPlayerError({ + error: new Error("test"), + errorType: "ASYNC-PLUGIN", + skipped: false, + }), + ).toBe(true); + }); + + it("should return false for errors without the type 'ASYNC-PLUGIN'", () => { + expect( + isAsyncPlayerError({ + error: new Error("test"), + errorType: "UNKNOWN", + skipped: false, + }), + ).toBe(false); + }); +}); diff --git a/plugins/async-node/core/src/utils/getNodeFromError.ts b/plugins/async-node/core/src/utils/getNodeFromError.ts new file mode 100644 index 000000000..f481d9ab1 --- /dev/null +++ b/plugins/async-node/core/src/utils/getNodeFromError.ts @@ -0,0 +1,34 @@ +import { PlayerError, Node, ErrorTypes } from "@player-ui/player"; +import { AsyncPluginContext } from "../internal-types"; +import { isAsyncPlayerError } from "./isAsyncPlayerError"; + +/** Get the AST Node related to a specific error if available. */ +export const getNodeFromError = ( + playerError: PlayerError, + context: AsyncPluginContext, +): Node.Node | undefined => { + if (playerError.errorType === ErrorTypes.RENDER) { + const { assetId } = playerError.metadata ?? {}; + + if (typeof assetId !== "string") { + return undefined; + } + + return context.assetIdCache.get(assetId); + } + + if (playerError.errorType === ErrorTypes.VIEW) { + const { node } = playerError.metadata ?? {}; + // TODO: Remove some of this from here. Maybe export type assertion functions from where the errors are generated? + if (typeof node === "object" && node !== null && !Array.isArray(node)) { + return node as Node.Node; + } + } + + if (isAsyncPlayerError(playerError) && playerError.metadata !== undefined) { + // Use the node from the cache to ensure it is the latest version of the async node from the resolver + return context.asyncNodeCache.get(playerError.metadata.node.id)?.asyncNode; + } + + return undefined; +}; diff --git a/plugins/async-node/core/src/utils/index.ts b/plugins/async-node/core/src/utils/index.ts index 98139ad44..478cf5c74 100644 --- a/plugins/async-node/core/src/utils/index.ts +++ b/plugins/async-node/core/src/utils/index.ts @@ -2,3 +2,5 @@ export * from "./extractNodeFromPath"; export * from "./traverseAndReplace"; export * from "./unwrapAsset"; export * from "./requiresAssetWrapper"; +export * from "./isAsyncPlayerError"; +export * from "./getNodeFromError"; diff --git a/plugins/async-node/core/src/utils/isAsyncPlayerError.ts b/plugins/async-node/core/src/utils/isAsyncPlayerError.ts new file mode 100644 index 000000000..d93174b0e --- /dev/null +++ b/plugins/async-node/core/src/utils/isAsyncPlayerError.ts @@ -0,0 +1,8 @@ +import { PlayerError } from "@player-ui/player"; +import { ASYNC_ERROR_TYPE, AsyncErrorMetadata } from "../AsyncNodeError"; + +export const isAsyncPlayerError = ( + error: PlayerError, +): error is PlayerError => { + return error.errorType === ASYNC_ERROR_TYPE; +}; diff --git a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt index 6d713a17b..8ff66c58d 100644 --- a/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt +++ b/plugins/async-node/jvm/src/test/kotlin/com/intuit/playerui/plugins/asyncnode/AsyncNodePluginTest.kt @@ -2,7 +2,10 @@ package com.intuit.playerui.plugins.asyncnode import com.intuit.hooks.BailResult import com.intuit.playerui.core.asset.Asset +import com.intuit.playerui.core.bridge.JSErrorException import com.intuit.playerui.core.bridge.Node +import com.intuit.playerui.core.bridge.getInvokable +import com.intuit.playerui.core.player.HeadlessPlayer import com.intuit.playerui.core.player.state.inProgressState import com.intuit.playerui.core.player.state.lastViewUpdate import com.intuit.playerui.plugins.assets.ReferenceAssetsPlugin @@ -200,16 +203,24 @@ internal class AsyncNodePluginTest : PlayerTest() { throw Exception("This is an error message from onAsyncNode") } - val errorMessage = assertThrows { + val refPlugin = ReferenceAssetsPlugin() + refPlugin.apply(runtime) + if (player is HeadlessPlayer) { + val invokable = (player as HeadlessPlayer).node.getInvokable("registerPlugin") + invokable?.invoke(refPlugin.node) + } + val err = assertThrows { runBlockingTest { player.start(chatMessageContent).await() } - }.message - assertEquals("This is an error message from onAsyncNode", errorMessage) + } + assertEquals("Error: An error occured during async node resolution. See cause for details.", err.message) + assertEquals("This is an error message from onAsyncNode", err.node.getObject("cause")?.getString("message")) } @TestTemplate fun `async node error hook catches and gracefully handles the error`() = runBlockingTest { + setupPlayer(listOf(AsyncNodePlugin())) plugin.hooks.onAsyncNode.tap("test") { _, node, callback -> throw Exception("This is an error message from onAsyncNode") } @@ -225,6 +236,13 @@ internal class AsyncNodePluginTest : PlayerTest() { ), ) } + // TODO: Remove this. Need to make sure the tests don't rely on the reference assets plugin since it can change but shouldn't impact AsyncNodePlugin tests + val refPlugin = ReferenceAssetsPlugin() + refPlugin.apply(runtime) + if (player is HeadlessPlayer) { + val invokable = (player as HeadlessPlayer).node.getInvokable("registerPlugin") + invokable?.invoke(refPlugin.node) + } var count = 0 var update: Asset? = null diff --git a/plugins/reference-assets/core/src/__tests__/plugin.test.ts b/plugins/reference-assets/core/src/__tests__/plugin.test.ts index 370186a22..532abd96a 100644 --- a/plugins/reference-assets/core/src/__tests__/plugin.test.ts +++ b/plugins/reference-assets/core/src/__tests__/plugin.test.ts @@ -16,7 +16,7 @@ const makeFlow = (asyncNodeCount: number, useDemoId = true): Flow => ({ id: "flow-with-async", views: [ { - id: useDemoId ? "collection-async-chat-demo" : "view", + id: useDemoId ? "chat-view" : "view", type: "view", values: Array.from({ length: asyncNodeCount }, (_, index) => ({ async: true, @@ -30,7 +30,44 @@ const makeFlow = (asyncNodeCount: number, useDemoId = true): Flow => ({ startState: "VIEW_1", VIEW_1: { state_type: "VIEW", - ref: useDemoId ? "collection-async-chat-demo" : "view", + ref: useDemoId ? "chat-view" : "view", + transitions: { + "*": "END_DONE", + }, + }, + END_DONE: { + state_type: "END", + outcome: "DONE", + }, + }, + }, +}); + +const makeBrokenFlow = (asyncNodeCount: number, useDemoId = true): Flow => ({ + id: "flow-with-async", + views: [ + { + id: useDemoId ? "chat-view" : "view", + type: "view", + collection: { + asset: { + type: "collection", + id: "collection-async-chat-demo", + values: Array.from({ length: asyncNodeCount }, (_, index) => ({ + async: true, + id: `id-${index}`, + })), + }, + }, + }, + ], + navigation: { + BEGIN: "FlowStart", + FlowStart: { + startState: "VIEW_1", + VIEW_1: { + state_type: "VIEW", + ref: useDemoId ? "chat-view" : "view", transitions: { "*": "END_DONE", }, @@ -284,6 +321,129 @@ describe("ReferenceAssetsPlugin", () => { expect(asyncHookTap).toHaveBeenCalled(); }); }); + + // This test succeeds because the `sendBroken` content is meant to fail at render-time. Need to render in any framework to see issues. + it("should resolve allow for single resolution by id with sendBroken", async () => { + const asyncHookTap = vi.fn(); + asyncPlugin.hooks.onAsyncNode.intercept({ + context: false, + call: asyncHookTap, + }); + player.start(makeFlow(2)); + + await vi.waitFor(() => { + expect(asyncHookTap).toHaveBeenCalledTimes(2); + }); + + const state = player.getState(); + + expect(state.status).toBe("in-progress"); + // resolve the second async node by targeting it by id + (state as InProgressState).controllers.expression.evaluate( + "sendBroken('first resolve', 'id-1')", + ); + + await vi.waitFor(() => { + const nextState = player.getState(); + expect(nextState.status).toBe("in-progress"); + const inProgress = nextState as InProgressState; + const view = inProgress.controllers.view.currentView?.lastUpdate; + expect(view).toBeDefined(); + // Don't need to test the whole view, just that the values array has been updated with the results of the 'send' command + expect(view).toStrictEqual( + expect.objectContaining({ + values: [ + { + asset: expect.objectContaining({ + id: "chat-demo-0", + type: "input", + binding: "binding", + label: 100, + }), + }, + ], + }), + ); + }); + }); + }); + + describe("ErrorRecoveryPlugin", () => { + it("should not recover from errors in views other than the 'chat-view' demo", async () => { + const asyncHookTap = vi.fn(); + asyncPlugin.hooks.onAsyncNode.intercept({ + context: false, + call: asyncHookTap, + }); + // Catch the exception to ensure vitest does not report unresolved exceptions. can also be used to compare with the final error state. + player.start(makeBrokenFlow(2, false)).catch(() => {}); + + await vi.waitFor(() => { + expect(asyncHookTap).toHaveBeenCalledTimes(2); + }); + + const state = player.getState(); + + expect(state.status).toBe("in-progress"); + (state as InProgressState).controllers.expression.evaluate( + "sendBrokenTransform('message')", + ); + + await vi.waitFor(() => { + const nextState = player.getState(); + expect(nextState.status).toBe("in-progress"); + }); + }); + + it("should recover from errors in 'chat-view'", async () => { + const asyncHookTap = vi.fn(); + asyncPlugin.hooks.onAsyncNode.intercept({ + context: false, + call: asyncHookTap, + }); + player.start(makeFlow(1)); + + await vi.waitFor(() => { + expect(asyncHookTap).toHaveBeenCalledTimes(1); + }); + + const state = player.getState(); + + expect(state.status).toBe("in-progress"); + (state as InProgressState).controllers.expression.evaluate( + "sendBrokenTransform('message')", + ); + + await vi.waitFor(() => { + const nextState = player.getState(); + expect(nextState.status).toBe("in-progress"); + const inProgress = nextState as InProgressState; + const view = inProgress.controllers.view.currentView?.lastUpdate; + expect(view).toBeDefined(); + // Don't need to test the whole view, just that the values array has been updated with the results of the 'send' command + expect(view).toStrictEqual( + expect.objectContaining({ + values: [ + { + asset: { + type: "collection", + id: "collection-async-id-0-recovery", + values: [ + { + asset: { + id: "id-0-recovery-text", + type: "text", + value: "Something went wrong, please try again.", + }, + }, + ], + }, + }, + ], + }), + ); + }); + }); }); }); diff --git a/plugins/reference-assets/core/src/plugin.ts b/plugins/reference-assets/core/src/plugin.ts index 4ed7e7577..d9f976508 100644 --- a/plugins/reference-assets/core/src/plugin.ts +++ b/plugins/reference-assets/core/src/plugin.ts @@ -5,6 +5,7 @@ import { AsyncNodePlugin, AsyncNodePluginPlugin, } from "@player-ui/async-node-plugin"; +import { ErrorRecoveryPlugin } from "./plugins/error-recovery-plugin"; /** * A plugin to add transforms for the reference assets @@ -18,6 +19,7 @@ export class ReferenceAssetsPlugin implements PlayerPlugin { }), new ReferenceAssetsTransformPlugin(), new ChatUiDemoPlugin(), + new ErrorRecoveryPlugin(), ]); apply(player: Player): void { diff --git a/plugins/reference-assets/core/src/plugins/chat-ui-demo-plugin.ts b/plugins/reference-assets/core/src/plugins/chat-ui-demo-plugin.ts index fe1f7fe20..a811ff796 100644 --- a/plugins/reference-assets/core/src/plugins/chat-ui-demo-plugin.ts +++ b/plugins/reference-assets/core/src/plugins/chat-ui-demo-plugin.ts @@ -4,10 +4,25 @@ import { ExtendedPlayerPlugin, NodeType, Player, + Node, } from "@player-ui/player"; import { ExpressionPlugin } from "@player-ui/expression-plugin"; import { send } from "./send"; +const isInChatDemo = (node: Node.Node) => { + if ( + node.parent?.parent?.type === NodeType.View && + node.parent.parent.value.id === "chat-view" + ) { + return true; + } + + return ( + node.parent?.parent?.type === NodeType.Asset && + node.parent.parent.value.id.startsWith("collection-async-chat-demo") + ); +}; + const createContentFromMessage = (message: string, id: string): any => ({ asset: { type: "chat-message", @@ -22,6 +37,27 @@ const createContentFromMessage = (message: string, id: string): any => ({ }, }); +/** This content will fail to display its label since it isn't a valid asset */ +const createBrokenRenderContent = (id: string): any => ({ + asset: { + id, + type: "input", + binding: "binding", + label: 100, + }, +}); + +/** this content will fail to fetch from the data model since the binding is an object */ +const createBrokenTransformContent = (id: string): any => ({ + asset: { + id, + type: "input", + binding: { + prop: "value", + }, + }, +}); + export class ChatUiDemoPlugin implements ExtendedPlayerPlugin<[], [], [send]> { public readonly name = "chat-ui-demo-plugin"; @@ -41,10 +77,10 @@ export class ChatUiDemoPlugin implements ExtendedPlayerPlugin<[], [], [send]> { let allPromiseKeys: string[] = []; let counter = 0; - const sendMessage: send = ( + const sendMessage = ( context: ExpressionContext, - message: string, nodeId?: string, + getContent?: () => any, ): void => { if (nodeId && !(nodeId in deferredPromises)) { context.logger?.warn( @@ -62,12 +98,8 @@ export class ChatUiDemoPlugin implements ExtendedPlayerPlugin<[], [], [send]> { const keys = nodeId ? [nodeId] : allPromiseKeys; for (const id of keys) { - const content = createContentFromMessage( - message, - `chat-demo-${counter++}`, - ); const resolveFunction = deferredPromises[id]; - resolveFunction?.(content); + resolveFunction?.(getContent?.()); delete deferredPromises[id]; } @@ -81,11 +113,7 @@ export class ChatUiDemoPlugin implements ExtendedPlayerPlugin<[], [], [send]> { asyncNodePlugin.hooks.onAsyncNode.tap(this.name, (node) => { // Ensure this is only used on the chat-ui.tsx mock to prevent the promise from setting up during tests. - if ( - (node.parent?.parent?.type !== NodeType.Asset && - node.parent?.parent?.type !== NodeType.View) || - !node.parent.parent.value.id.startsWith("collection-async-chat-demo") - ) { + if (!isInChatDemo(node)) { return Promise.resolve(undefined); } @@ -95,6 +123,37 @@ export class ChatUiDemoPlugin implements ExtendedPlayerPlugin<[], [], [send]> { }); }); + const sendRealMessage: send = ( + context: ExpressionContext, + message: string, + nodeId?: string, + ) => { + return sendMessage(context, nodeId, () => + createContentFromMessage(message, `chat-demo-${counter++}`), + ); + }; + + /** These expressions are used as examples in the storybook to allow broken content through and show the error recovery fallback pattern. */ + const sendBrokenMessage: send = ( + context: ExpressionContext, + _: string, + nodeId?: string, + ) => { + return sendMessage(context, nodeId, () => + createBrokenRenderContent(`chat-demo-${counter++}`), + ); + }; + + const sendBrokenTransformMessage: send = ( + context: ExpressionContext, + _: string, + nodeId?: string, + ) => { + return sendMessage(context, nodeId, () => + createBrokenTransformContent(`chat-demo-${counter++}`), + ); + }; + // Reset at the start of a new view. player.hooks.view.tap(this.name, (_) => { deferredPromises = {}; @@ -104,7 +163,11 @@ export class ChatUiDemoPlugin implements ExtendedPlayerPlugin<[], [], [send]> { // Register 'send' expression const expressionPlugin = new ExpressionPlugin( - new Map([["send", sendMessage]]), + new Map([ + ["send", sendRealMessage], + ["sendBroken", sendBrokenMessage], + ["sendBrokenTransform", sendBrokenTransformMessage], + ]), ); player.registerPlugin(expressionPlugin); } diff --git a/plugins/reference-assets/core/src/plugins/error-recovery-plugin.ts b/plugins/reference-assets/core/src/plugins/error-recovery-plugin.ts new file mode 100644 index 000000000..3d05bdb3a --- /dev/null +++ b/plugins/reference-assets/core/src/plugins/error-recovery-plugin.ts @@ -0,0 +1,37 @@ +import { AsyncNodePlugin } from "@player-ui/async-node-plugin"; +import { Player, PlayerPlugin } from "@player-ui/player"; + +export class ErrorRecoveryPlugin implements PlayerPlugin { + readonly name = "ErrorRecoveryPlugin"; + /** */ + apply(player: Player): void { + player.applyTo(AsyncNodePlugin.Symbol, (plugin) => { + plugin.hooks.onAsyncNodeError.tap(this.name, (err, node) => { + const playerState = player.getState(); + if (playerState.status !== "in-progress") { + return; + } + + // Limit error recovery to chat-ui view example to avoid breaking tests. + const viewId = playerState.controllers.view.currentView?.initialView.id; + if (viewId !== "chat-view") { + return; + } + + return { + asset: { + type: "chat-message", + id: `${node.id}-recovery`, + value: { + asset: { + id: `${node.id}-recovery-text`, + type: "text", + value: "Something went wrong, please try again.", + }, + }, + }, + }; + }); + }); + } +} diff --git a/plugins/reference-assets/mocks/BUILD b/plugins/reference-assets/mocks/BUILD index fe9bb643d..d848126b6 100644 --- a/plugins/reference-assets/mocks/BUILD +++ b/plugins/reference-assets/mocks/BUILD @@ -20,7 +20,7 @@ compile_mocks( "info", "input", "text", - "chat-message", + "chat-message" ], dsl_config = ":dsl_config", data = [ diff --git a/plugins/reference-assets/mocks/chat-message/chat-ui.tsx b/plugins/reference-assets/mocks/chat-message/chat-ui.tsx index 6061e050f..05346dafd 100644 --- a/plugins/reference-assets/mocks/chat-message/chat-ui.tsx +++ b/plugins/reference-assets/mocks/chat-message/chat-ui.tsx @@ -11,7 +11,7 @@ import { binding as b } from "@player-tools/dsl"; import { expression as e } from "@player-tools/dsl"; const view1 = ( - + @@ -23,6 +23,17 @@ const view1 = ( Send + + To demonstrate error recovery mechanisms, the message can be sent in + poorly formatted content that will throw during the transform or at + render-time of the asset: + + + Send Broken Render Asset + + + Send Broken Transform Asset + ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c9345125..8ce7aa569 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -605,6 +605,9 @@ importers: '@player-ui/check-path-plugin': specifier: workspace:* version: link:../../check-path/core + '@player-ui/expression-plugin': + specifier: workspace:* + version: link:../../expression/core '@player-ui/partial-match-registry': specifier: workspace:* version: link:../../../core/partial-match-registry diff --git a/react/player/src/__tests__/player.test.tsx b/react/player/src/__tests__/player.test.tsx index d9a0d3639..77ddb822f 100644 --- a/react/player/src/__tests__/player.test.tsx +++ b/react/player/src/__tests__/player.test.tsx @@ -1,8 +1,25 @@ import { useSubscribedState } from "@player-ui/react-subscribe"; -import { act, render, screen } from "@testing-library/react"; +import { act, render, screen, waitFor } from "@testing-library/react"; import React, { Suspense, type ComponentType } from "react"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { ReactPlayer, type ReactPlayerPlugin } from "../player"; +import { Asset } from "@player-ui/player"; +import { makeFlow } from "@player-ui/make-flow"; + +type ErrorViewProps = Asset<"throwing"> & { + shouldThrow: boolean; + displayValue: string; +}; + +const makeViewForErrorFlow = ( + shouldThrow: boolean, + displayValue: string, +): ErrorViewProps => ({ + type: "throwing", + id: "view-3", + shouldThrow, + displayValue, +}); describe("ReactPlayer", () => { const genericViewEvent = { type: "generic", id: "view-1" }; @@ -65,6 +82,21 @@ describe("ReactPlayer", () => { reactPlayer.assetRegistry.set({ type: altViewEvent.type }, () => (
Alternative Asset
)); + reactPlayer.assetRegistry.set( + { type: "throwing" }, + (props: ErrorViewProps) => { + if (props.shouldThrow) { + throw new Error( + `Error for '${props.id}' with display value '${props.displayValue}'`, + ); + } + return ( +
+ {props.displayValue} +
+ ); + }, + ); } function registerWebComponent(reactPlayer: ReactPlayer): void { @@ -94,4 +126,168 @@ describe("ReactPlayer", () => { , ); } + + describe("Error Handling", () => { + it("should render nothing when an unrecoverable error occurs", async () => { + let rp: ReactPlayer; + + const errorHandler = vi.fn(() => false); + const TestPlugin: ReactPlayerPlugin = { + name: "test-plugin", + applyReact: (reactPlayer) => { + rp = reactPlayer; + registerAssets(reactPlayer); + registerWebComponent(reactPlayer); + }, + apply: (player) => { + player.hooks.errorController.tap("test", (errController) => { + errController.hooks.onError.tap("test", errorHandler); + }); + }, + }; + + const { findByTestId } = renderWithPlugin([TestPlugin]); + await waitFor(() => { + expect(rp).toBeDefined(); + }); + + // 1. Publish a non-throwing view to ensure everything renders. + await act(() => { + rp.player + .start(makeFlow(makeViewForErrorFlow(false, "First Value"))) + .catch(() => {}); + }); + + const viewElement = await findByTestId("view-3"); + expect(viewElement).toHaveTextContent("First Value"); + + // 2. Publish a throwing and check that view renders nothing + await act(() => { + rp.viewUpdateSubscription.publish( + makeViewForErrorFlow(true, "Second Value"), + ); + }); + + await expect(findByTestId("view-3")).rejects.toThrow(); + expect(errorHandler).toHaveBeenCalled(); + + // 3. Publish a new view that doesn't throw and see no recovery + await act(() => { + rp.viewUpdateSubscription.publish( + makeViewForErrorFlow(false, "Third Value"), + ); + }); + + await expect(findByTestId("view-3")).rejects.toThrow(); + }); + + it("should render nothing and not deal with errors outside of an in-progress state", async () => { + let rp: ReactPlayer; + + const errorHandler = vi.fn(() => true); + const TestPlugin: ReactPlayerPlugin = { + name: "test-plugin", + applyReact: (reactPlayer) => { + rp = reactPlayer; + registerAssets(reactPlayer); + registerWebComponent(reactPlayer); + }, + apply: (player) => { + player.hooks.errorController.tap("test", (errController) => { + errController.hooks.onError.tap("test", errorHandler); + }); + }, + }; + + const { findByTestId } = renderWithPlugin([TestPlugin]); + await waitFor(() => { + expect(rp).toBeDefined(); + }); + + // 1. Publish a non-throwing view to ensure everything renders. + await act(() => { + rp.viewUpdateSubscription.publish( + makeViewForErrorFlow(false, "First Value"), + ); + }); + + const viewElement = await findByTestId("view-3"); + expect(viewElement).toHaveTextContent("First Value"); + + // 2. Publish a throwing and check that view renders nothing + await act(() => { + rp.viewUpdateSubscription.publish( + makeViewForErrorFlow(true, "Second Value"), + ); + }); + + await expect(findByTestId("view-3")).rejects.toThrow(); + expect(errorHandler).not.toHaveBeenCalled(); + + // 3. Publish a new view that doesn't throw and see no recovery + await act(() => { + rp.viewUpdateSubscription.publish( + makeViewForErrorFlow(false, "Third Value"), + ); + }); + + await expect(findByTestId("view-3")).rejects.toThrow(); + }); + + it("should render the previous view on error and new view on update when error recovery is enabled", async () => { + let rp: ReactPlayer; + + const errorHandler = vi.fn(() => true); + const TestPlugin: ReactPlayerPlugin = { + name: "test-plugin", + applyReact: (reactPlayer) => { + rp = reactPlayer; + registerAssets(reactPlayer); + registerWebComponent(reactPlayer); + }, + apply: (player) => { + player.hooks.errorController.tap("test", (errController) => { + errController.hooks.onError.tap("test", errorHandler); + }); + }, + }; + + const { findByTestId } = renderWithPlugin([TestPlugin]); + await waitFor(() => { + expect(rp).toBeDefined(); + }); + + // 1. Publish a non-throwing view to ensure everything renders. + await act(() => { + rp.player.start(makeFlow(makeViewForErrorFlow(false, "First Value"))); + }); + + let viewElement = await findByTestId("view-3"); + expect(viewElement).toHaveTextContent("First Value"); + expect(errorHandler).not.toHaveBeenCalled(); + + // 2. Publish a throwing and check that view renders nothing + await act(() => { + rp.viewUpdateSubscription.publish( + makeViewForErrorFlow(true, "Second Value"), + ); + }); + + await vi.waitFor(() => { + expect(errorHandler).toHaveBeenCalled(); + }); + viewElement = await findByTestId("view-3"); + expect(viewElement).toHaveTextContent("First Value"); + + // 3. Publish a new view that doesn't throw and see recovery + await act(() => { + rp.viewUpdateSubscription.publish( + makeViewForErrorFlow(false, "Third Value"), + ); + }); + + viewElement = await findByTestId("view-3"); + expect(viewElement).toHaveTextContent("Third Value"); + }); + }); }); diff --git a/react/player/src/asset/AssetRenderError.ts b/react/player/src/asset/AssetRenderError.ts index 8d095496f..78b6ca1e7 100644 --- a/react/player/src/asset/AssetRenderError.ts +++ b/react/player/src/asset/AssetRenderError.ts @@ -1,16 +1,34 @@ -import type { Asset } from "@player-ui/player"; +import { + ErrorSeverity, + ErrorTypes, + type Asset, + type PlayerErrorMetadata, +} from "@player-ui/player"; -export class AssetRenderError extends Error { +export type AssetRenderErrorMetadata = { + assetId: string; +}; +export class AssetRenderError + extends Error + implements PlayerErrorMetadata +{ private assetParentPath: Array = []; initialMessage: string; innerExceptionMessage: string; + readonly type: string = ErrorTypes.RENDER; + readonly severity: ErrorSeverity = ErrorSeverity.ERROR; + readonly metadata: AssetRenderErrorMetadata; + constructor( readonly rootAsset: Asset, message?: string, readonly innerException?: unknown, ) { super(message); + this.metadata = { + assetId: rootAsset.id, + }; this.initialMessage = message ?? ""; this.innerExceptionMessage = innerException instanceof Error diff --git a/react/player/src/asset/index.tsx b/react/player/src/asset/index.tsx index 61872f6dd..1e43527b3 100644 --- a/react/player/src/asset/index.tsx +++ b/react/player/src/asset/index.tsx @@ -104,7 +104,9 @@ export const ReactAsset = ( return ( { - const { error } = props; + const { error, resetErrorBoundary } = props; + + resetErrorBoundary(); if (error instanceof AssetRenderError) { error.addAssetParent(unwrapped); throw error; @@ -115,6 +117,7 @@ export const ReactAsset = ( error, ); } + return null; }} > diff --git a/react/player/src/player.tsx b/react/player/src/player.tsx index 09f30c6a7..e1e00d82c 100644 --- a/react/player/src/player.tsx +++ b/react/player/src/player.tsx @@ -1,6 +1,10 @@ import React from "react"; import { SyncWaterfallHook, AsyncParallelHook } from "tapable-ts"; -import { Subscribe, useSubscribedState } from "@player-ui/react-subscribe"; +import { + Subscribe, + useSubscribedState, + useSubscriber, +} from "@player-ui/react-subscribe"; import { Registry } from "@player-ui/partial-match-registry"; import type { CompletedState, @@ -9,8 +13,8 @@ import type { View, PlayerInfo, } from "@player-ui/player"; -import { Player } from "@player-ui/player"; -import { ErrorBoundary } from "react-error-boundary"; +import { ErrorTypes, Player } from "@player-ui/player"; +import { ErrorBoundary, FallbackProps } from "react-error-boundary"; import type { AssetRegistryType } from "./asset"; import { AssetContext } from "./asset"; import { PlayerContext } from "./utils"; @@ -19,6 +23,10 @@ import type { ReactPlayerProps } from "./app"; import { ReactPlayer as PlayerComp } from "./app"; import { OnUpdatePlugin } from "./plugins/onupdate-plugin"; +/** Backup context for receiving ReactPlayerComponentProps when components setup in the webComponent call don't pass the props down to their inner components. */ +export const ReactPlayerPropsContext: React.Context = + React.createContext({ isInErrorState: false }); + export interface DevtoolsGlobals { /** A global for a plugin to load to Player for devtools */ __PLAYER_DEVTOOLS_PLUGIN?: { @@ -52,7 +60,11 @@ export interface ReactPlayerOptions { plugins?: Array; } -export type ReactPlayerComponentProps = Record; +export type ReactPlayerComponentProps = { + /** Whether or not player is currently recovering from an error. */ + isInErrorState?: boolean; + [key: string]: unknown; +}; /** A Player that renders UI through React */ export class ReactPlayer { @@ -64,7 +76,10 @@ export class ReactPlayer { /** * A hook to create a React Component to be used for Player, regardless of the current flow state */ - webComponent: SyncWaterfallHook<[React.ComponentType], Record>; + webComponent: SyncWaterfallHook< + [React.ComponentType], + Record + >; /** * A hook to create a React Component that's used to render a specific view. * It will be called for each view update from the core player. @@ -97,7 +112,8 @@ export class ReactPlayer { onBeforeViewReset: new AsyncParallelHook(), }; - public readonly viewUpdateSubscription = new Subscribe(); + public readonly viewUpdateSubscription: Subscribe = + new Subscribe(); private reactPlayerInfo: ReactPlayerInfo; constructor(options?: ReactPlayerOptions) { @@ -181,20 +197,122 @@ export class ReactPlayer { /** Wrap the Error boundary and context provider after the hook call to catch anything wrapped by the hook */ const ReactPlayerComponent = (props: ReactPlayerComponentProps) => { + const trackedErrors = React.useRef(new Map()); + const [errorSubId, setErrorSubId] = React.useState( + undefined, + ); + const { subscribe, unsubscribe } = useSubscriber( + this.viewUpdateSubscription, + ); + + const componentProps: ReactPlayerComponentProps = React.useMemo( + () => ({ + ...props, + isInErrorState: errorSubId !== undefined, + }), + [props, errorSubId], + ); + + /** Callback to remove all tracked errors and unsub from */ + const clearErrorTracking = React.useCallback(() => { + trackedErrors.current.clear(); + setErrorSubId((prev) => { + if (prev !== undefined) { + unsubscribe(prev); + } + + return undefined; + }); + }, []); + + React.useEffect(() => { + // Clear errors and error subscription on unmount + return clearErrorTracking; + }, [clearErrorTracking]); + + /** capture error and return true or false to represent if we are recovering from the error or not. */ + const captureError = React.useCallback( + (err: Error) => { + // If player isn't in progress we can't actually render anything so render errors are irrelevant. + const playerState = this.player.getState(); + if (playerState.status !== "in-progress") { + this.player.logger.warn( + `[ReactPlayer]: An error occurred during rendering but was ignored due to a change in the player state (current state: '${playerState.status}'). Error Details:`, + err, + ); + return false; + } + + // Only capture each error once. + const currentError = trackedErrors.current.get(err); + if (currentError !== undefined) { + return currentError; + } + + let isRecovering = false; + setErrorSubId((prev) => { + // subscribe only if no subscription available. + // Needs to happen before capture error to ensure error recovery isn't missed + const subId = + prev === undefined + ? subscribe(clearErrorTracking, { + initializeWithPreviousValue: false, + }) + : prev; + + // Get skipped state after trying to capture. + const playerError = playerState.controllers.error.captureError( + err, + ErrorTypes.RENDER, + ); + isRecovering = playerError.skipped; + trackedErrors.current.set(err, isRecovering); + + // If we can't recover from the error, avoid updating state to stay in error boundary + if (!isRecovering) { + // Unsub if not previously subbed since we don't need to reset the view + if (subId !== prev) { + unsubscribe(subId); + } + return prev; + } + + return subId; + }); + + return isRecovering; + }, + [errorSubId], + ); + return ( null} - onError={(err) => { - const playerState = this.player.getState(); + fallbackRender={(fallbackProps: FallbackProps) => { + const isRecovering = captureError(fallbackProps.error); - if (playerState.status === "in-progress") { - playerState.fail(err); + if (!isRecovering) { + // Display nothing if not recovering. Let the player state fail and handle what the view will be. + return null; } + fallbackProps.resetErrorBoundary(); + + // Render the same as on success when recovering to preserve the react tree. + return ( + + + + + + ); }} > - - - + + + + + ); }; @@ -206,17 +324,27 @@ export class ReactPlayer { const ActualPlayerComp = this.hooks.playerComponent.call(PlayerComp); /** the component to use to render the player */ - const WebPlayerComponent = () => { + const WebPlayerComponent: React.ComponentType = (): React.ReactElement => { + const { isInErrorState } = React.useContext(ReactPlayerPropsContext); const view = useSubscribedState(this.viewUpdateSubscription); + const lastSuccessfulView = React.useRef(undefined); this.viewUpdateSubscription.suspend(); + React.useEffect(() => { + if (!isInErrorState) { + lastSuccessfulView.current = view; + } + }, [isInErrorState, view]); + + const displayedView = isInErrorState ? lastSuccessfulView.current : view; + return ( - {view && } + {displayedView && } ); }; diff --git a/react/subscribe/src/index.test.tsx b/react/subscribe/src/index.test.tsx index f0776c2ba..a663f7ef5 100644 --- a/react/subscribe/src/index.test.tsx +++ b/react/subscribe/src/index.test.tsx @@ -1,6 +1,6 @@ import { test, vitest, expect, describe, vi } from "vitest"; import { renderHook } from "@testing-library/react"; -import { Subscribe, useSubscribedState } from "."; +import { Subscribe, useSubscribedState, useSubscriber } from "."; test("Passes events to subscriptions", async () => { const stateSub = new Subscribe<{ @@ -81,3 +81,59 @@ describe("useSubscribedState", () => { expect(removeSpy).toHaveBeenCalledTimes(1); }); }); + +describe("useSubscriber", () => { + test("should expose subscribe and unsubscribe functions and unsubscribe all on unmount", async () => { + const stateSub = new Subscribe<{ + value: boolean; + }>(); + await stateSub.publish({ value: true }); + + const { result, unmount } = renderHook(() => useSubscriber(stateSub)); + + const firstSubFunction = vi.fn(); + const secondSubFunction = vi.fn(); + + // 1. Subscribe and check options are passed as expected. + const firstSubId = result.current.subscribe(firstSubFunction, { + initializeWithPreviousValue: true, + }); + result.current.subscribe(secondSubFunction, { + initializeWithPreviousValue: false, + }); + + expect(firstSubFunction).toHaveBeenCalledOnce(); + expect(firstSubFunction).toHaveBeenCalledWith({ value: true }); + expect(secondSubFunction).not.toHaveBeenCalled(); + + // Clear for future tests + firstSubFunction.mockClear(); + + // 2. Check both subscriptions work + await stateSub.publish({ value: false }); + expect(firstSubFunction).toHaveBeenCalledOnce(); + expect(firstSubFunction).toHaveBeenCalledWith({ value: false }); + expect(secondSubFunction).toHaveBeenCalledOnce(); + expect(secondSubFunction).toHaveBeenCalledWith({ value: false }); + + // Clear for future tests + firstSubFunction.mockClear(); + secondSubFunction.mockClear(); + + // 3. Check unsubscribe works + result.current.unsubscribe(firstSubId); + await stateSub.publish({ value: true }); + expect(firstSubFunction).not.toHaveBeenCalled(); + expect(secondSubFunction).toHaveBeenCalledOnce(); + expect(secondSubFunction).toHaveBeenCalledWith({ value: true }); + + // Clear for future tests + secondSubFunction.mockClear(); + + // 4. Check unsub on unmount + unmount(); + stateSub.publish({ value: false }); + expect(firstSubFunction).not.toHaveBeenCalled(); + expect(secondSubFunction).not.toHaveBeenCalled(); + }); +}); diff --git a/react/subscribe/src/index.tsx b/react/subscribe/src/index.tsx index 23e7dbad8..5165a7aef 100644 --- a/react/subscribe/src/index.tsx +++ b/react/subscribe/src/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback } from "react"; import { useSyncExternalStore } from "use-sync-external-store/shim"; export type SubscribeID = number; @@ -77,6 +77,7 @@ export class Subscribe { this.callbacks.forEach((c) => c(val)); } + private lastId: number = 0; /** * Subscribe to updates */ @@ -87,7 +88,7 @@ export class Subscribe { initializeWithPreviousValue?: boolean; }, ): SubscribeID { - const id = this.callbacks.size; + const id = this.lastId++; this.callbacks.set(id, callback); if ( @@ -194,3 +195,47 @@ export function useSubscribedState(subscriber: Subscribe): T | undefined { return state; } + +type SubOptions = { + initializeWithPreviousValue?: boolean; +}; + +type UnsubFunction = (id: SubscribeID) => void; + +type SubFunction = ( + callback: (arg: T | undefined) => void, + options?: SubOptions, +) => SubscribeID; + +export type ReactSubscriber = { + subscribe: SubFunction; + unsubscribe: UnsubFunction; +}; + +/** Hook to manage subscriptions within a react component. Any subscriptions setup using the subscribe callback will be unsubscribed on unmount. */ +export function useSubscriber(subscriber: Subscribe): ReactSubscriber { + const subscriptions = React.useMemo>( + () => new Set(), + [], + ); + React.useEffect(() => { + return () => { + for (const subId of subscriptions.values()) { + subscriber.remove(subId); + } + }; + }, [subscriptions]); + + const unsubscribe = useCallback((id) => { + subscriptions.delete(id); + subscriber.remove(id); + }, []); + + const subscribe = useCallback>((callback, options) => { + const id = subscriber.add(callback, options); + subscriptions.add(id); + return id; + }, []); + + return { subscribe, unsubscribe }; +}