From 1d0aa8d7f1a1171c8e61c7fa00e588e142eea468 Mon Sep 17 00:00:00 2001 From: lonelyhentxi Date: Thu, 3 Jul 2025 03:48:23 +0800 Subject: [PATCH] feat: support system tasks --- .vscode/settings.json | 6 +- Cargo.lock | 130 +++-- apps/proxy/Cargo.toml | 2 +- apps/recorder/Cargo.toml | 3 +- apps/recorder/src/app/builder.rs | 11 +- .../src/extract/mikan/subscription.rs | 53 +- apps/recorder/src/extract/mikan/web.rs | 55 +- apps/recorder/src/graphql/domains/cron.rs | 3 + apps/recorder/src/graphql/domains/mod.rs | 3 +- .../src/graphql/domains/subscriber_tasks.rs | 3 +- .../src/graphql/domains/system_tasks.rs | 248 ++++++++ apps/recorder/src/graphql/schema.rs | 5 + apps/recorder/src/lib.rs | 4 +- apps/recorder/src/media/config.rs | 16 +- apps/recorder/src/migrations/defs.rs | 31 + .../migrations/m20250520_021135_add_tasks.rs | 219 +++++++ .../m20250520_021135_subscriber_tasks.rs | 64 -- ...add_subscription_id_to_subscriber_tasks.rs | 62 -- .../migrations/m20250629_065628_add_cron.rs | 295 +++++++++- apps/recorder/src/migrations/mod.rs | 6 +- apps/recorder/src/models/cron/mod.rs | 52 +- apps/recorder/src/models/mod.rs | 3 +- .../src/models/subscriber_tasks/mod.rs | 21 +- apps/recorder/src/models/subscribers.rs | 10 + apps/recorder/src/models/system_tasks/mod.rs | 99 ++++ apps/recorder/src/storage/client.rs | 7 + apps/recorder/src/task/core.rs | 32 +- apps/recorder/src/task/extern.rs | 16 - apps/recorder/src/task/mod.rs | 13 +- apps/recorder/src/task/registry/mod.rs | 6 +- .../src/task/registry/subscriber/base.rs | 12 +- .../src/task/registry/subscriber/mod.rs | 36 +- .../task/registry/subscriber/subscription.rs | 6 +- .../recorder/src/task/registry/system/base.rs | 67 +++ .../src/task/registry/system/media.rs | 18 +- apps/recorder/src/task/registry/system/mod.rs | 128 +++- apps/recorder/src/task/service.rs | 28 + apps/recorder/src/test_utils/app.rs | 34 +- apps/recorder/src/test_utils/database.rs | 1 + apps/recorder/src/test_utils/mikan.rs | 21 +- apps/recorder/src/test_utils/task.rs | 30 +- justfile | 4 + package.json | 13 +- pnpm-lock.yaml | 552 +++++++++++------- 44 files changed, 1833 insertions(+), 595 deletions(-) create mode 100644 apps/recorder/src/graphql/domains/system_tasks.rs create mode 100644 apps/recorder/src/migrations/m20250520_021135_add_tasks.rs delete mode 100644 apps/recorder/src/migrations/m20250520_021135_subscriber_tasks.rs delete mode 100644 apps/recorder/src/migrations/m20250625_060701_add_subscription_id_to_subscriber_tasks.rs create mode 100644 apps/recorder/src/models/system_tasks/mod.rs delete mode 100644 apps/recorder/src/task/extern.rs create mode 100644 apps/recorder/src/task/registry/system/base.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index a638584..63d8578 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,9 +40,5 @@ } ], "rust-analyzer.cargo.features": "all", - "rust-analyzer.testExplorer": true, - // https://github.com/rust-lang/rust/issues/141540 - "rust-analyzer.runnables.extraEnv": { - "CARGO_INCREMENTAL": "0", - } + "rust-analyzer.testExplorer": true } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f460588..bbdba88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,7 +404,7 @@ dependencies = [ "futures-util", "handlebars", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "lru", "mime", "multer", @@ -474,7 +474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" dependencies = [ "bytes", - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_json", ] @@ -592,9 +592,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +checksum = "19135c0c7a60bfee564dbe44ab5ce0557c6bf3884e5291a50be76a15640c4fbd" dependencies = [ "arrayvec", ] @@ -1009,9 +1009,9 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytecheck" @@ -1672,9 +1672,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -2781,9 +2781,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc37f9a2bfe731e69f1e08d29d91d30604b9ce24bcb2880a961e82d89c6ed89" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" dependencies = [ "color_quant", "weezl", @@ -2873,9 +2873,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -2883,7 +2883,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -3847,9 +3847,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", @@ -3967,6 +3967,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -4174,9 +4185,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" dependencies = [ "bitflags 2.9.1", "libc", @@ -4308,7 +4319,7 @@ dependencies = [ "dashmap 6.1.0", "futures", "hex 0.4.3", - "indexmap 2.9.0", + "indexmap 2.10.0", "leaky-bucket", "librqbit-bencode", "librqbit-clone-to-owned", @@ -4423,9 +4434,9 @@ dependencies = [ [[package]] name = "lightningcss" -version = "1.0.0-alpha.66" +version = "1.0.0-alpha.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a73ffa17de66534e4b527232f44aa0a89fad22c4f4e0735f9be35494f058e54" +checksum = "798fba4e1205eed356b8ed7754cc3f7f04914e27855ca641409f4a532e992149" dependencies = [ "ahash 0.8.12", "bitflags 2.9.1", @@ -4435,7 +4446,7 @@ dependencies = [ "dashmap 5.5.3", "data-encoding", "getrandom 0.2.16", - "indexmap 2.9.0", + "indexmap 2.10.0", "itertools 0.10.5", "lazy_static", "lightningcss-derive", @@ -5362,9 +5373,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" [[package]] name = "p256" @@ -5858,7 +5869,7 @@ dependencies = [ "either", "hashbrown 0.14.5", "hashbrown 0.15.4", - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "num-traits", "polars-arrow", @@ -6019,7 +6030,7 @@ dependencies = [ "either", "hashbrown 0.15.4", "hex 0.4.3", - "indexmap 2.9.0", + "indexmap 2.10.0", "libm", "memchr", "num-traits", @@ -6128,7 +6139,7 @@ version = "0.49.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada7c7e2fbbeffbdd67628cd8a89f02b0a8d21c71d34e297e2463a7c17575203" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "polars-error", "polars-utils", "serde", @@ -6229,7 +6240,7 @@ dependencies = [ "flate2", "foldhash", "hashbrown 0.15.4", - "indexmap 2.9.0", + "indexmap 2.10.0", "libc", "memmap2 0.9.5", "num-traits", @@ -6986,9 +6997,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes", @@ -7440,6 +7451,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -7488,9 +7511,9 @@ dependencies = [ [[package]] name = "sea-orm" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18b7272b88bd608cd846de24f41b74a0315a135fe761b0aed4ec1ce6a6327a93" +checksum = "560ea59f07472886a236e7919b9425cf16914fee1d663d3c32f1af2e922b83f0" dependencies = [ "async-stream", "async-trait", @@ -7517,9 +7540,9 @@ dependencies = [ [[package]] name = "sea-orm-cli" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a4961b0d9098a9dc992d6e75fb761f9e5c442bb46746eeffa08e47b53759fce" +checksum = "00dd755ba3faca11692d8aaca46b68f1b4955c5dfdd6a3f1f9fba3a679a3ec1d" dependencies = [ "chrono", "clap", @@ -7535,9 +7558,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c38255a6b2e6d1ae2d5df35696507a345f03c036ae32caeb0a3b922dbab610d" +checksum = "70d0ea50bb4317c8a58ed34dc410a79d685128e7b77ddcd9e8b59ae6416a56d9" dependencies = [ "heck 0.5.0", "proc-macro-crate", @@ -7550,9 +7573,9 @@ dependencies = [ [[package]] name = "sea-orm-migration" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f58c3b1dcf6c137f08394f0228f9baf1574a2a799e93dc5da3cd9228bef9c5" +checksum = "3e06e0f3ca090091ad58da2bc02cdb63f9afbd276baf029f065f6ff09e79cbe9" dependencies = [ "async-trait", "clap", @@ -7846,16 +7869,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64 0.22.1", "chrono", "hex 0.4.3", "indexmap 1.9.3", - "indexmap 2.9.0", - "schemars", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.3", "serde", "serde_derive", "serde_json", @@ -7865,9 +7889,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling", "proc-macro2", @@ -7881,7 +7905,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -8227,7 +8251,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.4", "hashlink", - "indexmap 2.9.0", + "indexmap 2.10.0", "log", "memchr", "once_cell", @@ -8892,17 +8916,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio 1.0.4", "parking_lot 0.12.4", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -9040,7 +9066,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", @@ -9924,9 +9950,9 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link", "windows-result", @@ -10225,9 +10251,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ "libc", "rustix 1.0.7", diff --git a/apps/proxy/Cargo.toml b/apps/proxy/Cargo.toml index 65d5f04..d14ba91 100644 --- a/apps/proxy/Cargo.toml +++ b/apps/proxy/Cargo.toml @@ -13,7 +13,7 @@ name = "mikan_doppel" path = "src/bin/mikan_doppel.rs" [dependencies] -recorder = { workspace = true } +recorder = { workspace = true, features = ["playground"] } tokio = { workspace = true } tracing-subscriber = { workspace = true } tracing = { workspace = true } diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml index 802a930..0c1430a 100644 --- a/apps/recorder/Cargo.toml +++ b/apps/recorder/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [features] default = ["jxl"] -playground = ["dep:inquire", "dep:color-eyre", "dep:polars"] +playground = ["dep:inquire", "dep:color-eyre", "dep:polars", "test-utils"] testcontainers = [ "dep:testcontainers", "dep:testcontainers-modules", @@ -15,6 +15,7 @@ testcontainers = [ "testcontainers-modules/postgres", ] jxl = ["dep:jpegxl-rs", "dep:jpegxl-sys"] +test-utils = [] [lib] name = "recorder" diff --git a/apps/recorder/src/app/builder.rs b/apps/recorder/src/app/builder.rs index 9644ba9..c461a94 100644 --- a/apps/recorder/src/app/builder.rs +++ b/apps/recorder/src/app/builder.rs @@ -131,11 +131,12 @@ impl AppBuilder { } pub fn working_dir_from_manifest_dir(self) -> Self { - let manifest_dir = if cfg!(debug_assertions) || cfg!(test) || cfg!(feature = "playground") { - env!("CARGO_MANIFEST_DIR") - } else { - "./apps/recorder" - }; + #[cfg(any(test, debug_assertions, feature = "test-utils"))] + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + + #[cfg(not(any(test, debug_assertions, feature = "test-utils")))] + let manifest_dir = "./apps/recorder"; + self.working_dir(manifest_dir.to_string()) } } diff --git a/apps/recorder/src/extract/mikan/subscription.rs b/apps/recorder/src/extract/mikan/subscription.rs index 98817b4..84e4b11 100644 --- a/apps/recorder/src/extract/mikan/subscription.rs +++ b/apps/recorder/src/extract/mikan/subscription.rs @@ -546,14 +546,12 @@ impl MikanBangumiSubscription { #[cfg(test)] #[allow(unused_variables)] mod tests { - use std::sync::Arc; use rstest::{fixture, rstest}; use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait}; use tracing::Level; use crate::{ - app::AppContextTrait, errors::RecorderResult, extract::mikan::{ MikanBangumiHash, MikanSeasonFlowUrlMeta, MikanSeasonStr, @@ -564,34 +562,11 @@ mod tests { subscriptions::{self, SubscriptionTrait}, }, test_utils::{ - app::{TestingAppContext, TestingAppContextPreset}, - mikan::{MikanMockServer, build_testing_mikan_credential_form}, + app::TestingPreset, mikan::build_testing_mikan_credential_form, tracing::try_init_testing_tracing, }, }; - struct TestingResources { - pub app_ctx: Arc, - pub mikan_server: MikanMockServer, - } - - async fn build_testing_app_context() -> RecorderResult { - let mikan_server = MikanMockServer::new().await?; - - let mikan_base_url = mikan_server.base_url().clone(); - - let app_ctx = TestingAppContext::from_preset(TestingAppContextPreset { - mikan_base_url: mikan_base_url.to_string(), - database_config: None, - }) - .await?; - - Ok(TestingResources { - app_ctx, - mikan_server, - }) - } - #[fixture] fn before_each() { try_init_testing_tracing(Level::DEBUG); @@ -600,10 +575,10 @@ mod tests { #[rstest] #[tokio::test] async fn test_mikan_season_subscription_sync_feeds(before_each: ()) -> RecorderResult<()> { - let TestingResources { - app_ctx, - mut mikan_server, - } = build_testing_app_context().await?; + let mut preset = TestingPreset::default().await?; + let app_ctx = preset.app_ctx.clone(); + + let mikan_server = &mut preset.mikan_server; let _resources_mock = mikan_server.mock_resources_with_doppel(); @@ -662,10 +637,11 @@ mod tests { #[rstest] #[tokio::test] async fn test_mikan_subscriber_subscription_sync_feeds(before_each: ()) -> RecorderResult<()> { - let TestingResources { - app_ctx, - mut mikan_server, - } = build_testing_app_context().await?; + let mut preset = TestingPreset::default().await?; + + let app_ctx = preset.app_ctx.clone(); + + let mikan_server = &mut preset.mikan_server; let _resources_mock = mikan_server.mock_resources_with_doppel(); @@ -729,10 +705,11 @@ mod tests { #[rstest] #[tokio::test] async fn test_mikan_bangumi_subscription_sync_feeds(before_each: ()) -> RecorderResult<()> { - let TestingResources { - app_ctx, - mut mikan_server, - } = build_testing_app_context().await?; + let mut preset = TestingPreset::default().await?; + + let app_ctx = preset.app_ctx.clone(); + + let mikan_server = &mut preset.mikan_server; let _resources_mock = mikan_server.mock_resources_with_doppel(); diff --git a/apps/recorder/src/extract/mikan/web.rs b/apps/recorder/src/extract/mikan/web.rs index 38c9cad..f814287 100644 --- a/apps/recorder/src/extract/mikan/web.rs +++ b/apps/recorder/src/extract/mikan/web.rs @@ -35,7 +35,7 @@ use crate::{ EncodeWebpOptions, }, storage::StorageContentCategory, - task::{OptimizeImageTask, SystemTask}, + task::OptimizeImageTask, }; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -818,11 +818,14 @@ pub async fn scrape_mikan_poster_meta_from_image_url( let webp_storage_path = storage_path.with_extension("webp"); if storage_service.exists(&webp_storage_path).await?.is_none() { task_service - .add_system_task(SystemTask::OptimizeImage(OptimizeImageTask { - source_path: storage_path.clone().to_string(), - target_path: webp_storage_path.to_string(), - format_options: EncodeImageOptions::Webp(EncodeWebpOptions::default()), - })) + .add_system_task( + OptimizeImageTask::builder() + .source_path(storage_path.clone().to_string()) + .target_path(webp_storage_path.to_string()) + .format_options(EncodeImageOptions::Webp(EncodeWebpOptions::default())) + .build() + .into(), + ) .await?; } } @@ -830,11 +833,14 @@ pub async fn scrape_mikan_poster_meta_from_image_url( let avif_storage_path = storage_path.with_extension("avif"); if storage_service.exists(&avif_storage_path).await?.is_none() { task_service - .add_system_task(SystemTask::OptimizeImage(OptimizeImageTask { - source_path: storage_path.clone().to_string(), - target_path: avif_storage_path.to_string(), - format_options: EncodeImageOptions::Avif(EncodeAvifOptions::default()), - })) + .add_system_task( + OptimizeImageTask::builder() + .source_path(storage_path.clone().to_string()) + .target_path(avif_storage_path.to_string()) + .format_options(EncodeImageOptions::Avif(EncodeAvifOptions::default())) + .build() + .into(), + ) .await?; } } @@ -842,11 +848,14 @@ pub async fn scrape_mikan_poster_meta_from_image_url( let jxl_storage_path = storage_path.with_extension("jxl"); if storage_service.exists(&jxl_storage_path).await?.is_none() { task_service - .add_system_task(SystemTask::OptimizeImage(OptimizeImageTask { - source_path: storage_path.clone().to_string(), - target_path: jxl_storage_path.to_string(), - format_options: EncodeImageOptions::Jxl(EncodeJxlOptions::default()), - })) + .add_system_task( + OptimizeImageTask::builder() + .source_path(storage_path.clone().to_string()) + .target_path(jxl_storage_path.to_string()) + .format_options(EncodeImageOptions::Jxl(EncodeJxlOptions::default())) + .build() + .into(), + ) .await?; } } @@ -1089,7 +1098,7 @@ mod test { use super::*; use crate::test_utils::{ - app::{TestingAppContext, TestingAppContextPreset}, + app::{TestingAppContext, TestingPreset}, crypto::build_testing_crypto_service, database::build_testing_database_service, mikan::{ @@ -1137,17 +1146,13 @@ mod test { #[rstest] #[tokio::test] async fn test_scrape_mikan_poster_meta_from_image_url(before_each: ()) -> RecorderResult<()> { - let mut mikan_server = MikanMockServer::new().await?; + let mut preset = TestingPreset::default().await?; - let mikan_base_url = mikan_server.base_url().clone(); + let app_ctx = preset.app_ctx.clone(); - let app_ctx = TestingAppContext::from_preset(TestingAppContextPreset { - mikan_base_url: mikan_base_url.to_string(), - database_config: None, - }) - .await?; + let mikan_base_url = preset.mikan_server.base_url().clone(); - let resources_mock = mikan_server.mock_resources_with_doppel(); + let resources_mock = preset.mikan_server.mock_resources_with_doppel(); let bangumi_poster_url = mikan_base_url.join("/images/Bangumi/202309/5ce9fed1.jpg")?; diff --git a/apps/recorder/src/graphql/domains/cron.rs b/apps/recorder/src/graphql/domains/cron.rs index 722fa10..c57be46 100644 --- a/apps/recorder/src/graphql/domains/cron.rs +++ b/apps/recorder/src/graphql/domains/cron.rs @@ -6,6 +6,7 @@ use crate::{ domains::{ subscriber_tasks::restrict_subscriber_tasks_for_entity, subscribers::restrict_subscriber_for_entity, + system_tasks::restrict_system_tasks_for_entity, }, infra::{custom::register_entity_default_writable, name::get_entity_and_column_name}, }, @@ -17,6 +18,7 @@ fn skip_columns_for_entity_input(context: &mut BuilderContext) { if matches!( column, cron::Column::SubscriberTask + | cron::Column::SystemTask | cron::Column::CronExpr | cron::Column::Enabled | cron::Column::TimeoutMs @@ -44,6 +46,7 @@ pub fn register_cron_to_schema_context(context: &mut BuilderContext) { restrict_subscriber_for_entity::(context, &cron::Column::SubscriberId); restrict_subscriber_tasks_for_entity::(context, &cron::Column::SubscriberTask); + restrict_system_tasks_for_entity::(context, &cron::Column::SystemTask); skip_columns_for_entity_input(context); } diff --git a/apps/recorder/src/graphql/domains/mod.rs b/apps/recorder/src/graphql/domains/mod.rs index de8239b..bd98891 100644 --- a/apps/recorder/src/graphql/domains/mod.rs +++ b/apps/recorder/src/graphql/domains/mod.rs @@ -1,6 +1,7 @@ pub mod credential_3rd; pub mod bangumi; +pub mod cron; pub mod downloaders; pub mod downloads; pub mod episodes; @@ -10,4 +11,4 @@ pub mod subscribers; pub mod subscription_bangumi; pub mod subscription_episode; pub mod subscriptions; -pub mod cron; +pub mod system_tasks; diff --git a/apps/recorder/src/graphql/domains/subscriber_tasks.rs b/apps/recorder/src/graphql/domains/subscriber_tasks.rs index 230d7e2..7c7f7be 100644 --- a/apps/recorder/src/graphql/domains/subscriber_tasks.rs +++ b/apps/recorder/src/graphql/domains/subscriber_tasks.rs @@ -30,8 +30,9 @@ use crate::{ }, }, }, + migrations::defs::{ApalisJobs, ApalisSchema}, models::subscriber_tasks, - task::{ApalisJobs, ApalisSchema, SubscriberTaskTrait}, + task::SubscriberTaskTrait, }; fn skip_columns_for_entity_input(context: &mut BuilderContext) { diff --git a/apps/recorder/src/graphql/domains/system_tasks.rs b/apps/recorder/src/graphql/domains/system_tasks.rs new file mode 100644 index 0000000..78c09bd --- /dev/null +++ b/apps/recorder/src/graphql/domains/system_tasks.rs @@ -0,0 +1,248 @@ +use std::{ops::Deref, sync::Arc}; + +use async_graphql::dynamic::{FieldValue, Scalar, TypeRef}; +use convert_case::Case; +use sea_orm::{ + ActiveModelBehavior, ColumnTrait, ConnectionTrait, EntityTrait, Iterable, QueryFilter, + QuerySelect, QueryTrait, prelude::Expr, sea_query::Query, +}; +use seaography::{ + Builder as SeaographyBuilder, BuilderContext, SeaographyError, prepare_active_model, +}; +use ts_rs::TS; + +use crate::{ + auth::AuthUserInfo, + errors::RecorderError, + graphql::{ + domains::subscribers::restrict_subscriber_for_entity, + infra::{ + custom::{ + generate_entity_create_one_mutation_field, + generate_entity_default_basic_entity_object, + generate_entity_default_insert_input_object, generate_entity_delete_mutation_field, + generate_entity_filtered_mutation_field, register_entity_default_readonly, + }, + json::{convert_jsonb_output_for_entity, restrict_jsonb_filter_input_for_entity}, + name::{ + get_entity_and_column_name, get_entity_basic_type_name, + get_entity_custom_mutation_field_name, + }, + }, + }, + migrations::defs::{ApalisJobs, ApalisSchema}, + models::system_tasks, + task::SystemTaskTrait, +}; + +fn skip_columns_for_entity_input(context: &mut BuilderContext) { + for column in system_tasks::Column::iter() { + if matches!( + column, + system_tasks::Column::Job | system_tasks::Column::SubscriberId + ) { + continue; + } + let entity_column_key = + get_entity_and_column_name::(context, &column); + context.entity_input.insert_skips.push(entity_column_key); + } +} + +pub fn restrict_system_tasks_for_entity(context: &mut BuilderContext, column: &T::Column) +where + T: EntityTrait, + ::Model: Sync, +{ + let entity_and_column = get_entity_and_column_name::(context, column); + + restrict_jsonb_filter_input_for_entity::(context, column); + convert_jsonb_output_for_entity::(context, column, Some(Case::Camel)); + let entity_column_name = get_entity_and_column_name::(context, column); + + context.types.input_type_overwrites.insert( + entity_column_name.clone(), + TypeRef::Named(system_tasks::SystemTask::ident().into()), + ); + context.types.output_type_overwrites.insert( + entity_column_name.clone(), + TypeRef::Named(system_tasks::SystemTask::ident().into()), + ); + context.types.input_conversions.insert( + entity_column_name.clone(), + Box::new(move |resolve_context, value_accessor| { + let task: system_tasks::SystemTaskInput = value_accessor.deserialize()?; + + let subscriber_id = resolve_context + .data::()? + .subscriber_auth + .subscriber_id; + + let task = system_tasks::SystemTask::from_input(task, Some(subscriber_id)); + + let json_value = serde_json::to_value(task).map_err(|err| { + SeaographyError::TypeConversionError( + err.to_string(), + format!("Json - {entity_column_name}"), + ) + })?; + + Ok(sea_orm::Value::Json(Some(Box::new(json_value)))) + }), + ); + + context.entity_input.update_skips.push(entity_and_column); +} + +pub fn register_system_tasks_to_schema_context(context: &mut BuilderContext) { + restrict_subscriber_for_entity::( + context, + &system_tasks::Column::SubscriberId, + ); + restrict_system_tasks_for_entity::(context, &system_tasks::Column::Job); + + skip_columns_for_entity_input(context); +} + +pub fn register_system_tasks_to_schema_builder( + mut builder: SeaographyBuilder, +) -> SeaographyBuilder { + builder.schema = builder.schema.register( + Scalar::new(system_tasks::SystemTask::ident()) + .description(system_tasks::SystemTask::decl()), + ); + builder.register_enumeration::(); + + builder = register_entity_default_readonly!(builder, system_tasks); + let builder_context = builder.context; + + { + builder + .outputs + .push(generate_entity_default_basic_entity_object::< + system_tasks::Entity, + >(builder_context)); + } + { + let delete_mutation = generate_entity_delete_mutation_field::( + builder_context, + Arc::new(|_resolver_ctx, app_ctx, filters| { + Box::pin(async move { + let db = app_ctx.db(); + + let select_subquery = system_tasks::Entity::find() + .select_only() + .column(system_tasks::Column::Id) + .filter(filters); + + let delete_query = Query::delete() + .from_table((ApalisSchema::Schema, ApalisJobs::Table)) + .and_where( + Expr::col(ApalisJobs::Id).in_subquery(select_subquery.into_query()), + ) + .to_owned(); + + let db_backend = db.deref().get_database_backend(); + let delete_statement = db_backend.build(&delete_query); + + let result = db.execute(delete_statement).await?; + + Ok::<_, RecorderError>(result.rows_affected()) + }) + }), + ); + builder.mutations.push(delete_mutation); + } + { + let entity_retry_one_mutation_name = get_entity_custom_mutation_field_name::< + system_tasks::Entity, + >(builder_context, "RetryOne"); + let retry_one_mutation = + generate_entity_filtered_mutation_field::( + builder_context, + entity_retry_one_mutation_name, + TypeRef::named_nn(get_entity_basic_type_name::( + builder_context, + )), + Arc::new(|_resolver_ctx, app_ctx, filters| { + Box::pin(async move { + let db = app_ctx.db(); + + let job_id = system_tasks::Entity::find() + .filter(filters) + .select_only() + .column(system_tasks::Column::Id) + .into_tuple::() + .one(db) + .await? + .ok_or_else(|| { + RecorderError::from_entity_not_found::() + })?; + + let task = app_ctx.task(); + task.retry_subscriber_task(job_id.clone()).await?; + + let task_model = system_tasks::Entity::find() + .filter(system_tasks::Column::Id.eq(&job_id)) + .one(db) + .await? + .ok_or_else(|| { + RecorderError::from_entity_not_found::() + })?; + + Ok::<_, RecorderError>(Some(FieldValue::owned_any(task_model))) + }) + }), + ); + builder.mutations.push(retry_one_mutation); + } + { + builder + .inputs + .push(generate_entity_default_insert_input_object::< + system_tasks::Entity, + >(builder_context)); + let create_one_mutation = generate_entity_create_one_mutation_field::( + builder_context, + Arc::new(move |resolver_ctx, app_ctx, input_object| { + Box::pin(async move { + let active_model: Result = + prepare_active_model(builder_context, &input_object, resolver_ctx); + + let task_service = app_ctx.task(); + + let active_model = active_model?; + + let db = app_ctx.db(); + + let active_model = active_model.before_save(db, true).await?; + + let task = active_model.job.unwrap(); + let subscriber_id = active_model.subscriber_id.unwrap(); + + if task.get_subscriber_id() != subscriber_id { + Err(async_graphql::Error::new( + "subscriber_id does not match with job.subscriber_id", + ))?; + } + + let task_id = task_service.add_system_task(task).await?.to_string(); + + let db = app_ctx.db(); + + let task = system_tasks::Entity::find() + .filter(system_tasks::Column::Id.eq(&task_id)) + .one(db) + .await? + .ok_or_else(|| { + RecorderError::from_entity_not_found::() + })?; + + Ok::<_, RecorderError>(task) + }) + }), + ); + builder.mutations.push(create_one_mutation); + } + builder +} diff --git a/apps/recorder/src/graphql/schema.rs b/apps/recorder/src/graphql/schema.rs index c4e18cf..f218838 100644 --- a/apps/recorder/src/graphql/schema.rs +++ b/apps/recorder/src/graphql/schema.rs @@ -39,6 +39,9 @@ use crate::{ subscriptions::{ register_subscriptions_to_schema_builder, register_subscriptions_to_schema_context, }, + system_tasks::{ + register_system_tasks_to_schema_builder, register_system_tasks_to_schema_context, + }, }, infra::{ json::register_jsonb_input_filter_to_schema_builder, @@ -79,6 +82,7 @@ pub fn build_schema( register_subscription_episode_to_schema_context(&mut context); register_bangumi_to_schema_context(&mut context); register_cron_to_schema_context(&mut context); + register_system_tasks_to_schema_context(&mut context); } context }); @@ -103,6 +107,7 @@ pub fn build_schema( builder = register_subscriber_tasks_to_schema_builder(builder); builder = register_bangumi_to_schema_builder(builder); builder = register_cron_to_schema_builder(builder); + builder = register_system_tasks_to_schema_builder(builder); } let schema = builder.schema_builder(); diff --git a/apps/recorder/src/lib.rs b/apps/recorder/src/lib.rs index da3c3aa..ec64527 100644 --- a/apps/recorder/src/lib.rs +++ b/apps/recorder/src/lib.rs @@ -27,6 +27,8 @@ pub mod migrations; pub mod models; pub mod storage; pub mod task; -pub mod test_utils; pub mod utils; pub mod web; + +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; diff --git a/apps/recorder/src/media/config.rs b/apps/recorder/src/media/config.rs index a1a6ce0..f36be91 100644 --- a/apps/recorder/src/media/config.rs +++ b/apps/recorder/src/media/config.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; +use ts_rs::TS; -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TS)] +#[ts(rename_all = "camelCase")] pub enum AutoOptimizeImageFormat { #[serde(rename = "image/webp")] Webp, @@ -10,25 +12,29 @@ pub enum AutoOptimizeImageFormat { Jxl, } -#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[derive(Clone, Debug, Serialize, Deserialize, Default, TS, PartialEq)] +#[ts(rename_all = "camelCase")] pub struct EncodeWebpOptions { pub quality: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[derive(Clone, Debug, Serialize, Deserialize, Default, TS, PartialEq)] +#[ts(rename_all = "camelCase")] pub struct EncodeAvifOptions { pub quality: Option, pub speed: Option, pub threads: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, Default)] +#[derive(Clone, Debug, Serialize, Deserialize, Default, TS, PartialEq)] +#[ts(rename_all = "camelCase")] pub struct EncodeJxlOptions { pub quality: Option, pub speed: Option, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, TS, PartialEq)] +#[ts(tag = "mimeType")] #[serde(tag = "mime_type")] pub enum EncodeImageOptions { #[serde(rename = "image/webp")] diff --git a/apps/recorder/src/migrations/defs.rs b/apps/recorder/src/migrations/defs.rs index 310f068..113df9e 100644 --- a/apps/recorder/src/migrations/defs.rs +++ b/apps/recorder/src/migrations/defs.rs @@ -190,6 +190,37 @@ pub enum Cron { Priority, Status, SubscriberTask, + SystemTask, +} + +#[derive(sea_query::Iden)] + +pub enum ApalisSchema { + #[iden = "apalis"] + Schema, +} + +#[derive(DeriveIden)] + +pub enum ApalisJobs { + #[sea_orm(iden = "jobs")] + Table, + SubscriberId, + SubscriptionId, + Job, + JobType, + Status, + TaskType, + Id, + Attempts, + MaxAttempts, + RunAt, + LastError, + LockAt, + LockBy, + DoneAt, + Priority, + CronId, } macro_rules! create_postgres_enum_for_active_enum { diff --git a/apps/recorder/src/migrations/m20250520_021135_add_tasks.rs b/apps/recorder/src/migrations/m20250520_021135_add_tasks.rs new file mode 100644 index 0000000..bf2164d --- /dev/null +++ b/apps/recorder/src/migrations/m20250520_021135_add_tasks.rs @@ -0,0 +1,219 @@ +use async_trait::async_trait; +use sea_orm_migration::{prelude::*, schema::*}; + +use super::defs::{ApalisJobs, ApalisSchema}; +use crate::{ + migrations::defs::{Subscribers, Subscriptions}, + task::{ + SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, + SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_TRIGGER_NAME, SUBSCRIBER_TASK_APALIS_NAME, + SYSTEM_TASK_APALIS_NAME, + }, +}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + TableAlterStatement::new() + .table((ApalisSchema::Schema, ApalisJobs::Table)) + .add_column_if_not_exists(integer_null(ApalisJobs::SubscriberId)) + .add_column_if_not_exists(integer_null(ApalisJobs::SubscriptionId)) + .add_column_if_not_exists(string_null(ApalisJobs::TaskType)) + .add_foreign_key( + TableForeignKey::new() + .name("fk_apalis_jobs_subscriber_id") + .from_tbl((ApalisSchema::Schema, ApalisJobs::Table)) + .from_col(ApalisJobs::SubscriberId) + .to_tbl(Subscribers::Table) + .to_col(Subscribers::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Restrict), + ) + .add_foreign_key( + TableForeignKey::new() + .name("fk_apalis_jobs_subscription_id") + .from_tbl((ApalisSchema::Schema, ApalisJobs::Table)) + .from_col(ApalisJobs::SubscriptionId) + .to_tbl(Subscriptions::Table) + .to_col(Subscriptions::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Restrict), + ) + .to_owned(), + ) + .await?; + + let db = manager.get_connection(); + + db.execute_unprepared(&format!( + r#"UPDATE {apalis_schema}.{apalis_table} SET {subscriber_id} = ({job} ->> '{subscriber_id}')::integer, {task_type} = ({job} ->> '{task_type}')::text, {subscription_id} = ({job} ->> '{subscription_id}')::integer"#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + job = ApalisJobs::Job.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + subscription_id = ApalisJobs::SubscriptionId.to_string(), + )).await?; + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE FUNCTION {SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME}() RETURNS trigger AS $$ + DECLARE + new_job_subscriber_id integer; + new_job_subscription_id integer; + new_job_task_type text; + BEGIN + new_job_subscriber_id = (NEW.{job} ->> '{subscriber_id}')::integer; + new_job_subscription_id = (NEW.{job} ->> '{subscription_id}')::integer; + new_job_task_type = (NEW.{job} ->> '{task_type}')::text; + IF new_job_subscriber_id != (OLD.{job} ->> '{subscriber_id}')::integer AND new_job_subscriber_id != NEW.{subscriber_id} THEN + NEW.{subscriber_id} = new_job_subscriber_id; + END IF; + IF new_job_subscription_id != (OLD.{job} ->> '{subscription_id}')::integer AND new_job_subscription_id != NEW.{subscription_id} THEN + NEW.{subscription_id} = new_job_subscription_id; + END IF; + IF new_job_task_type != (OLD.{job} ->> '{task_type}')::text AND new_job_task_type != NEW.{task_type} THEN + NEW.{task_type} = new_job_task_type; + END IF; + RETURN NEW; + END; + $$ LANGUAGE plpgsql;"#, + job = ApalisJobs::Job.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + subscription_id = ApalisJobs::SubscriptionId.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + )).await?; + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE TRIGGER {SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_TRIGGER_NAME} + BEFORE INSERT OR UPDATE ON {apalis_schema}.{apalis_table} + FOR EACH ROW + EXECUTE FUNCTION {SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME}();"#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string() + )) + .await?; + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE VIEW subscriber_tasks AS + SELECT + {job}, + {job_type}, + {status}, + {subscriber_id}, + {task_type}, + {id}, + {attempts}, + {max_attempts}, + {run_at}, + {last_error}, + {lock_at}, + {lock_by}, + {done_at}, + {priority}, + {subscription_id} + FROM {apalis_schema}.{apalis_table} + WHERE {job_type} = '{SUBSCRIBER_TASK_APALIS_NAME}' + AND jsonb_path_exists({job}, '$.{subscriber_id} ? (@.type() == "number")') + AND jsonb_path_exists({job}, '$.{task_type} ? (@.type() == "string")')"#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string(), + job = ApalisJobs::Job.to_string(), + job_type = ApalisJobs::JobType.to_string(), + status = ApalisJobs::Status.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + id = ApalisJobs::Id.to_string(), + attempts = ApalisJobs::Attempts.to_string(), + max_attempts = ApalisJobs::MaxAttempts.to_string(), + run_at = ApalisJobs::RunAt.to_string(), + last_error = ApalisJobs::LastError.to_string(), + lock_at = ApalisJobs::LockAt.to_string(), + lock_by = ApalisJobs::LockBy.to_string(), + done_at = ApalisJobs::DoneAt.to_string(), + priority = ApalisJobs::Priority.to_string(), + subscription_id = ApalisJobs::SubscriptionId.to_string(), + )) + .await?; + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE VIEW system_tasks AS + SELECT + {job}, + {job_type}, + {status}, + {subscriber_id}, + {task_type}, + {id}, + {attempts}, + {max_attempts}, + {run_at}, + {last_error}, + {lock_at}, + {lock_by}, + {done_at}, + {priority} + FROM {apalis_schema}.{apalis_table} + WHERE {job_type} = '{SYSTEM_TASK_APALIS_NAME}' + AND jsonb_path_exists({job}, '$.{task_type} ? (@.type() == "string")')"#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string(), + job = ApalisJobs::Job.to_string(), + job_type = ApalisJobs::JobType.to_string(), + status = ApalisJobs::Status.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + id = ApalisJobs::Id.to_string(), + attempts = ApalisJobs::Attempts.to_string(), + max_attempts = ApalisJobs::MaxAttempts.to_string(), + run_at = ApalisJobs::RunAt.to_string(), + last_error = ApalisJobs::LastError.to_string(), + lock_at = ApalisJobs::LockAt.to_string(), + lock_by = ApalisJobs::LockBy.to_string(), + done_at = ApalisJobs::DoneAt.to_string(), + priority = ApalisJobs::Priority.to_string(), + )) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared("DROP VIEW IF EXISTS subscriber_tasks") + .await?; + + db.execute_unprepared("DROP VIEW IF EXISTS system_tasks") + .await?; + + db.execute_unprepared(&format!( + r#"DROP TRIGGER IF EXISTS {SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_TRIGGER_NAME} ON {apalis_schema}.{apalis_table}"#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string() + )).await?; + + db.execute_unprepared(&format!( + r#"DROP FUNCTION IF EXISTS {SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME}()"#, + )) + .await?; + + manager + .alter_table( + TableAlterStatement::new() + .table((ApalisSchema::Schema, ApalisJobs::Table)) + .drop_foreign_key("fk_apalis_jobs_subscriber_id") + .drop_foreign_key("fk_apalis_jobs_subscription_id") + .drop_column(ApalisJobs::SubscriberId) + .drop_column(ApalisJobs::SubscriptionId) + .to_owned(), + ) + .await?; + + Ok(()) + } +} diff --git a/apps/recorder/src/migrations/m20250520_021135_subscriber_tasks.rs b/apps/recorder/src/migrations/m20250520_021135_subscriber_tasks.rs deleted file mode 100644 index 7dd086b..0000000 --- a/apps/recorder/src/migrations/m20250520_021135_subscriber_tasks.rs +++ /dev/null @@ -1,64 +0,0 @@ -use async_trait::async_trait; -use sea_orm_migration::prelude::*; - -use crate::task::SUBSCRIBER_TASK_APALIS_NAME; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - let db = manager.get_connection(); - - db.execute_unprepared(&format!( - r#"CREATE OR REPLACE VIEW subscriber_tasks AS -SELECT - job, - job_type, - status, - (job ->> 'subscriber_id'::text)::integer AS subscriber_id, - job ->> 'task_type'::text AS task_type, - id, - attempts, - max_attempts, - run_at, - last_error, - lock_at, - lock_by, - done_at, - priority -FROM apalis.jobs -WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}' -AND jsonb_path_exists(job, '$.subscriber_id ? (@.type() == "number")') -AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"#, - )) - .await?; - - db.execute_unprepared(&format!( - r#"CREATE INDEX IF NOT EXISTS idx_apalis_jobs_subscriber_id - ON apalis.jobs (((job -> 'subscriber_id')::integer)) - WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}' - AND jsonb_path_exists(job, '$.subscriber_id ? (@.type() == "number")') - AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"# - )) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - let db = manager.get_connection(); - - db.execute_unprepared( - r#"DROP INDEX IF EXISTS idx_apalis_jobs_subscriber_id - ON apalis.jobs"#, - ) - .await?; - - db.execute_unprepared("DROP VIEW IF EXISTS subscriber_tasks") - .await?; - - Ok(()) - } -} diff --git a/apps/recorder/src/migrations/m20250625_060701_add_subscription_id_to_subscriber_tasks.rs b/apps/recorder/src/migrations/m20250625_060701_add_subscription_id_to_subscriber_tasks.rs deleted file mode 100644 index 79dae78..0000000 --- a/apps/recorder/src/migrations/m20250625_060701_add_subscription_id_to_subscriber_tasks.rs +++ /dev/null @@ -1,62 +0,0 @@ -use async_trait::async_trait; -use sea_orm_migration::prelude::*; - -use crate::task::SUBSCRIBER_TASK_APALIS_NAME; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - let db = manager.get_connection(); - - db.execute_unprepared(&format!( - r#"CREATE OR REPLACE VIEW subscriber_tasks AS -SELECT - job, - job_type, - status, - (job ->> 'subscriber_id')::integer AS subscriber_id, - job ->> 'task_type' AS task_type, - id, - attempts, - max_attempts, - run_at, - last_error, - lock_at, - lock_by, - done_at, - priority, - (job ->> 'subscription_id')::integer AS subscription_id -FROM apalis.jobs -WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}' -AND jsonb_path_exists(job, '$.subscriber_id ? (@.type() == "number")') -AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"#, - )) - .await?; - - db.execute_unprepared(&format!( - r#"CREATE INDEX IF NOT EXISTS idx_apalis_jobs_subscription_id - ON apalis.jobs (((job -> 'subscription_id')::integer)) - WHERE job_type = '{SUBSCRIBER_TASK_APALIS_NAME}' - AND jsonb_path_exists(job, '$.subscription_id ? (@.type() == "number")') - AND jsonb_path_exists(job, '$.task_type ? (@.type() == "string")')"# - )) - .await?; - - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - let db = manager.get_connection(); - - db.execute_unprepared( - r#"DROP INDEX IF EXISTS idx_apalis_jobs_subscription_id - ON apalis.jobs"#, - ) - .await?; - - Ok(()) - } -} diff --git a/apps/recorder/src/migrations/m20250629_065628_add_cron.rs b/apps/recorder/src/migrations/m20250629_065628_add_cron.rs index 410d4e6..ed1cee7 100644 --- a/apps/recorder/src/migrations/m20250629_065628_add_cron.rs +++ b/apps/recorder/src/migrations/m20250629_065628_add_cron.rs @@ -4,13 +4,18 @@ use sea_orm_migration::{prelude::*, schema::*}; use crate::{ migrations::defs::{ - Cron, CustomSchemaManagerExt, GeneralIds, Subscribers, Subscriptions, table_auto_z, + ApalisJobs, ApalisSchema, Cron, CustomSchemaManagerExt, GeneralIds, Subscribers, + Subscriptions, table_auto_z, }, models::cron::{ CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_EVENT, CronStatus, CronStatusEnum, NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME, NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME, }, + task::{ + SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SUBSCRIBER_TASK_APALIS_NAME, + SYSTEM_TASK_APALIS_NAME, + }, }; #[derive(DeriveMigrationName)] @@ -52,6 +57,7 @@ impl MigrationTrait for Migration { CronStatus::iden_values(), )) .col(json_binary_null(Cron::SubscriberTask)) + .col(json_binary_null(Cron::SystemTask)) .foreign_key( ForeignKey::create() .name("fk_cron_subscriber_id") @@ -91,12 +97,22 @@ impl MigrationTrait for Migration { db.execute_unprepared(&format!( r#"CREATE OR REPLACE FUNCTION {SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME}() RETURNS trigger AS $$ + DECLARE + new_subscriber_task_subscriber_id integer; + new_subscriber_task_subscription_id integer; + new_system_task_subscriber_id integer; BEGIN - IF jsonb_path_exists(NEW.{subscriber_task}, '$.subscriber_id ? (@.type() == "number")') THEN - NEW.{subscriber_id} = (NEW.{subscriber_task} ->> 'subscriber_id')::integer; + new_subscriber_task_subscriber_id = (NEW.{subscriber_task} ->> 'subscriber_id')::integer; + new_subscriber_task_subscription_id = (NEW.{subscriber_task} ->> 'subscription_id')::integer; + new_system_task_subscriber_id = (NEW.{system_task} ->> 'subscriber_id')::integer; + IF new_subscriber_task_subscriber_id != (OLD.{subscriber_task} ->> 'subscriber_id')::integer AND new_subscriber_task_subscriber_id != NEW.{subscriber_id} THEN + NEW.{subscriber_id} = new_subscriber_task_subscriber_id; END IF; - IF jsonb_path_exists(NEW.{subscriber_task}, '$.subscription_id ? (@.type() == "number")') THEN - NEW.{subscription_id} = (NEW.{subscriber_task} ->> 'subscription_id')::integer; + IF new_subscriber_task_subscription_id != (OLD.{subscriber_task} ->> 'subscription_id')::integer AND new_subscriber_task_subscription_id != NEW.{subscription_id} THEN + NEW.{subscription_id} = new_subscriber_task_subscription_id; + END IF; + IF new_system_task_subscriber_id != (OLD.{system_task} ->> 'subscriber_id')::integer AND new_system_task_subscriber_id != NEW.{subscriber_id} THEN + NEW.{subscriber_id} = new_system_task_subscriber_id; END IF; RETURN NEW; END; @@ -104,6 +120,7 @@ impl MigrationTrait for Migration { subscriber_task = &Cron::SubscriberTask.to_string(), subscriber_id = &Cron::SubscriberId.to_string(), subscription_id = &Cron::SubscriptionId.to_string(), + system_task = &Cron::SystemTask.to_string(), )).await?; db.execute_unprepared(&format!( @@ -208,12 +225,280 @@ impl MigrationTrait for Migration { )) .await?; + manager + .alter_table( + TableAlterStatement::new() + .table((ApalisSchema::Schema, ApalisJobs::Table)) + .add_column_if_not_exists(integer_null(ApalisJobs::CronId)) + .add_foreign_key( + TableForeignKey::new() + .name("fk_apalis_jobs_cron_id") + .from_tbl((ApalisSchema::Schema, ApalisJobs::Table)) + .from_col(ApalisJobs::CronId) + .to_tbl(Cron::Table) + .to_col(Cron::Id) + .on_delete(ForeignKeyAction::NoAction) + .on_update(ForeignKeyAction::NoAction), + ) + .to_owned(), + ) + .await?; + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE VIEW subscriber_tasks AS + SELECT + {job}, + {job_type}, + {status}, + {subscriber_id}, + {task_type}, + {id}, + {attempts}, + {max_attempts}, + {run_at}, + {last_error}, + {lock_at}, + {lock_by}, + {done_at}, + {priority}, + {subscription_id}, + {cron_id} + FROM {apalis_schema}.{apalis_table} + WHERE {job_type} = '{SUBSCRIBER_TASK_APALIS_NAME}' + AND jsonb_path_exists({job}, '$.{subscriber_id} ? (@.type() == "number")') + AND jsonb_path_exists({job}, '$.{task_type} ? (@.type() == "string")')"#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string(), + job = ApalisJobs::Job.to_string(), + job_type = ApalisJobs::JobType.to_string(), + status = ApalisJobs::Status.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + id = ApalisJobs::Id.to_string(), + attempts = ApalisJobs::Attempts.to_string(), + max_attempts = ApalisJobs::MaxAttempts.to_string(), + run_at = ApalisJobs::RunAt.to_string(), + last_error = ApalisJobs::LastError.to_string(), + lock_at = ApalisJobs::LockAt.to_string(), + lock_by = ApalisJobs::LockBy.to_string(), + done_at = ApalisJobs::DoneAt.to_string(), + priority = ApalisJobs::Priority.to_string(), + subscription_id = ApalisJobs::SubscriptionId.to_string(), + cron_id = ApalisJobs::CronId.to_string(), + )) + .await?; + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE VIEW system_tasks AS + SELECT + {job}, + {job_type}, + {status}, + {subscriber_id}, + {task_type}, + {id}, + {attempts}, + {max_attempts}, + {run_at}, + {last_error}, + {lock_at}, + {lock_by}, + {done_at}, + {priority}, + {cron_id} + FROM {apalis_schema}.{apalis_table} + WHERE {job_type} = '{SYSTEM_TASK_APALIS_NAME}' + AND jsonb_path_exists({job}, '$.{task_type} ? (@.type() == "string")')"#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string(), + job = ApalisJobs::Job.to_string(), + job_type = ApalisJobs::JobType.to_string(), + status = ApalisJobs::Status.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + id = ApalisJobs::Id.to_string(), + attempts = ApalisJobs::Attempts.to_string(), + max_attempts = ApalisJobs::MaxAttempts.to_string(), + run_at = ApalisJobs::RunAt.to_string(), + last_error = ApalisJobs::LastError.to_string(), + lock_at = ApalisJobs::LockAt.to_string(), + lock_by = ApalisJobs::LockBy.to_string(), + done_at = ApalisJobs::DoneAt.to_string(), + priority = ApalisJobs::Priority.to_string(), + cron_id = ApalisJobs::CronId.to_string(), + )) + .await?; + + db.execute_unprepared(&format!( + r#" + UPDATE {apalis_schema}.{apalis_table} SET {cron_id} = ({job} ->> '{cron_id}')::integer + "#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string(), + job = ApalisJobs::Job.to_string(), + cron_id = ApalisJobs::CronId.to_string(), + )) + .await?; + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE FUNCTION {SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME}() RETURNS trigger AS $$ + DECLARE + new_job_subscriber_id integer; + new_job_subscription_id integer; + new_job_cron_id integer; + new_job_task_type text; + BEGIN + new_job_subscriber_id = (NEW.{job} ->> '{subscriber_id}')::integer; + new_job_subscription_id = (NEW.{job} ->> '{subscription_id}')::integer; + new_job_cron_id = (NEW.{job} ->> '{cron_id}')::integer; + new_job_task_type = (NEW.{job} ->> '{task_type}')::text; + IF new_job_subscriber_id != (OLD.{job} ->> '{subscriber_id}')::integer AND new_job_subscriber_id != NEW.{subscriber_id} THEN + NEW.{subscriber_id} = new_job_subscriber_id; + END IF; + IF new_job_subscription_id != (OLD.{job} ->> '{subscription_id}')::integer AND new_job_subscription_id != NEW.{subscription_id} THEN + NEW.{subscription_id} = new_job_subscription_id; + END IF; + IF new_job_cron_id != (OLD.{job} ->> '{cron_id}')::integer AND new_job_cron_id != NEW.{cron_id} THEN + NEW.{cron_id} = new_job_cron_id; + END IF; + IF new_job_task_type != (OLD.{job} ->> '{task_type}')::text AND new_job_task_type != NEW.{task_type} THEN + NEW.{task_type} = new_job_task_type; + END IF; + RETURN NEW; + END; + $$ LANGUAGE plpgsql;"#, + job = ApalisJobs::Job.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + subscription_id = ApalisJobs::SubscriptionId.to_string(), + cron_id = ApalisJobs::CronId.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + )).await?; + Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { let db = manager.get_connection(); + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE FUNCTION {SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME}() RETURNS trigger AS $$ + DECLARE + new_job_subscriber_id integer; + new_job_subscription_id integer; + new_job_task_type text; + BEGIN + new_job_subscriber_id = (NEW.{job} ->> '{subscriber_id}')::integer; + new_job_subscription_id = (NEW.{job} ->> '{subscription_id}')::integer; + new_job_task_type = (NEW.{job} ->> '{task_type}')::text; + IF new_job_subscriber_id != (OLD.{job} ->> '{subscriber_id}')::integer AND new_job_subscriber_id != NEW.{subscriber_id} THEN + NEW.{subscriber_id} = new_job_subscriber_id; + END IF; + IF new_job_subscription_id != (OLD.{job} ->> '{subscription_id}')::integer AND new_job_subscription_id != NEW.{subscription_id} THEN + NEW.{subscription_id} = new_job_subscription_id; + END IF; + IF new_job_task_type != (OLD.{job} ->> '{task_type}')::text AND new_job_task_type != NEW.{task_type} THEN + NEW.{task_type} = new_job_task_type; + END IF; + RETURN NEW; + END; + $$ LANGUAGE plpgsql;"#, + job = ApalisJobs::Job.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + subscription_id = ApalisJobs::SubscriptionId.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + )).await?; + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE VIEW subscriber_tasks AS + SELECT + {job}, + {job_type}, + {status}, + {subscriber_id}, + {task_type}, + {id}, + {attempts}, + {max_attempts}, + {run_at}, + {last_error}, + {lock_at}, + {lock_by}, + {done_at}, + {priority}, + {subscription_id} + FROM {apalis_schema}.{apalis_table} + WHERE {job_type} = '{SUBSCRIBER_TASK_APALIS_NAME}' + AND jsonb_path_exists({job}, '$.{subscriber_id} ? (@.type() == "number")') + AND jsonb_path_exists({job}, '$.{task_type} ? (@.type() == "string")')"#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string(), + job = ApalisJobs::Job.to_string(), + job_type = ApalisJobs::JobType.to_string(), + status = ApalisJobs::Status.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + id = ApalisJobs::Id.to_string(), + attempts = ApalisJobs::Attempts.to_string(), + max_attempts = ApalisJobs::MaxAttempts.to_string(), + run_at = ApalisJobs::RunAt.to_string(), + last_error = ApalisJobs::LastError.to_string(), + lock_at = ApalisJobs::LockAt.to_string(), + lock_by = ApalisJobs::LockBy.to_string(), + done_at = ApalisJobs::DoneAt.to_string(), + priority = ApalisJobs::Priority.to_string(), + subscription_id = ApalisJobs::SubscriptionId.to_string(), + )) + .await?; + + db.execute_unprepared(&format!( + r#"CREATE OR REPLACE VIEW system_tasks AS + SELECT + {job}, + {job_type}, + {status}, + {subscriber_id}, + {task_type}, + {id}, + {attempts}, + {max_attempts}, + {run_at}, + {last_error}, + {lock_at}, + {lock_by}, + {done_at}, + {priority} + FROM {apalis_schema}.{apalis_table} + WHERE {job_type} = '{SYSTEM_TASK_APALIS_NAME}' + AND jsonb_path_exists({job}, '$.{task_type} ? (@.type() == "string")')"#, + apalis_schema = ApalisSchema::Schema.to_string(), + apalis_table = ApalisJobs::Table.to_string(), + job = ApalisJobs::Job.to_string(), + job_type = ApalisJobs::JobType.to_string(), + status = ApalisJobs::Status.to_string(), + subscriber_id = ApalisJobs::SubscriberId.to_string(), + task_type = ApalisJobs::TaskType.to_string(), + id = ApalisJobs::Id.to_string(), + attempts = ApalisJobs::Attempts.to_string(), + max_attempts = ApalisJobs::MaxAttempts.to_string(), + run_at = ApalisJobs::RunAt.to_string(), + last_error = ApalisJobs::LastError.to_string(), + lock_at = ApalisJobs::LockAt.to_string(), + lock_by = ApalisJobs::LockBy.to_string(), + done_at = ApalisJobs::DoneAt.to_string(), + priority = ApalisJobs::Priority.to_string(), + )) + .await?; + + manager + .alter_table( + TableAlterStatement::new() + .table((ApalisSchema::Schema, ApalisJobs::Table)) + .drop_column(ApalisJobs::CronId) + .drop_foreign_key("fk_apalis_jobs_cron_id") + .to_owned(), + ) + .await?; + db.execute_unprepared(&format!( r#"DROP TRIGGER IF EXISTS {NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME} ON {table};"#, table = &Cron::Table.to_string(), diff --git a/apps/recorder/src/migrations/mod.rs b/apps/recorder/src/migrations/mod.rs index 8c0a9b4..46a61cc 100644 --- a/apps/recorder/src/migrations/mod.rs +++ b/apps/recorder/src/migrations/mod.rs @@ -7,10 +7,9 @@ pub mod m20220101_000001_init; pub mod m20240224_082543_add_downloads; pub mod m20241231_000001_auth; pub mod m20250501_021523_credential_3rd; -pub mod m20250520_021135_subscriber_tasks; +pub mod m20250520_021135_add_tasks; pub mod m20250622_015618_feeds; pub mod m20250622_020819_bangumi_and_episode_type; -pub mod m20250625_060701_add_subscription_id_to_subscriber_tasks; pub mod m20250629_065628_add_cron; pub struct Migrator; @@ -23,10 +22,9 @@ impl MigratorTrait for Migrator { Box::new(m20240224_082543_add_downloads::Migration), Box::new(m20241231_000001_auth::Migration), Box::new(m20250501_021523_credential_3rd::Migration), - Box::new(m20250520_021135_subscriber_tasks::Migration), + Box::new(m20250520_021135_add_tasks::Migration), Box::new(m20250622_015618_feeds::Migration), Box::new(m20250622_020819_bangumi_and_episode_type::Migration), - Box::new(m20250625_060701_add_subscription_id_to_subscriber_tasks::Migration), Box::new(m20250629_065628_add_cron::Migration), ] } diff --git a/apps/recorder/src/models/cron/mod.rs b/apps/recorder/src/models/cron/mod.rs index 13d5a68..90b6a3e 100644 --- a/apps/recorder/src/models/cron/mod.rs +++ b/apps/recorder/src/models/cron/mod.rs @@ -21,8 +21,10 @@ use sea_orm::{ use serde::{Deserialize, Serialize}; use crate::{ - app::AppContextTrait, errors::RecorderResult, models::subscriber_tasks, - task::SubscriberTaskTrait, + app::AppContextTrait, + errors::RecorderResult, + models::{subscriber_tasks, system_tasks}, + task::{SubscriberTaskTrait, SystemTaskTrait}, }; #[derive( @@ -41,7 +43,7 @@ pub enum CronStatus { Failed, } -#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[derive(Debug, Clone, DeriveEntityModel, PartialEq, Serialize, Deserialize)] #[sea_orm(table_name = "cron")] pub struct Model { #[sea_orm(default_expr = "Expr::current_timestamp()")] @@ -70,6 +72,7 @@ pub struct Model { #[sea_orm(default_expr = "true")] pub enabled: bool, pub subscriber_task: Option, + pub system_task: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -79,7 +82,7 @@ pub enum Relation { from = "Column::SubscriberId", to = "super::subscribers::Column::Id", on_update = "Cascade", - on_delete = "Cascade" + on_delete = "Restrict" )] Subscriber, #[sea_orm( @@ -87,9 +90,13 @@ pub enum Relation { from = "Column::SubscriptionId", to = "super::subscriptions::Column::Id", on_update = "Cascade", - on_delete = "Cascade" + on_delete = "Restrict" )] Subscription, + #[sea_orm(has_many = "super::subscriber_tasks::Entity")] + SubscriberTask, + #[sea_orm(has_many = "super::system_tasks::Entity")] + SystemTask, } impl Related for Entity { @@ -104,12 +111,28 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::SubscriberTask.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::SystemTask.def() + } +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] pub enum RelatedEntity { #[sea_orm(entity = "super::subscribers::Entity")] Subscriber, #[sea_orm(entity = "super::subscriptions::Entity")] Subscription, + #[sea_orm(entity = "super::subscriber_tasks::Entity")] + SubscriberTask, + #[sea_orm(entity = "super::system_tasks::Entity")] + SystemTask, } #[async_trait] @@ -136,6 +159,14 @@ impl ActiveModelBehavior for ActiveModel { "Cron subscriber_id does not match subscriber_task.subscriber_id".to_string(), )); } + if let ActiveValue::Set(Some(subscriber_id)) = self.subscriber_id + && let ActiveValue::Set(Some(ref system_task)) = self.system_task + && system_task.get_subscriber_id() != Some(subscriber_id) + { + return Err(DbErr::Custom( + "Cron subscriber_id does not match system_task.subscriber_id".to_string(), + )); + } Ok(self) } @@ -219,11 +250,18 @@ impl Model { async fn exec_cron(&self, ctx: &dyn AppContextTrait) -> RecorderResult<()> { if let Some(subscriber_task) = self.subscriber_task.as_ref() { let task_service = ctx.task(); + let mut new_subscriber_task = subscriber_task.clone(); + new_subscriber_task.set_cron_id(Some(self.id)); task_service - .add_subscriber_task(subscriber_task.clone()) + .add_subscriber_task(new_subscriber_task) .await?; + } else if let Some(system_task) = self.system_task.as_ref() { + let task_service = ctx.task(); + let mut new_system_task = system_task.clone(); + new_system_task.set_cron_id(Some(self.id)); + task_service.add_system_task(new_system_task).await?; } else { - unimplemented!("Cron without subscriber task is not supported now"); + unimplemented!("Cron without unknown task is not supported now"); } Ok(()) diff --git a/apps/recorder/src/models/mod.rs b/apps/recorder/src/models/mod.rs index c24796e..891a4da 100644 --- a/apps/recorder/src/models/mod.rs +++ b/apps/recorder/src/models/mod.rs @@ -1,6 +1,7 @@ pub mod auth; pub mod bangumi; pub mod credential_3rd; +pub mod cron; pub mod downloaders; pub mod downloads; pub mod episodes; @@ -11,4 +12,4 @@ pub mod subscribers; pub mod subscription_bangumi; pub mod subscription_episode; pub mod subscriptions; -pub mod cron; +pub mod system_tasks; diff --git a/apps/recorder/src/models/subscriber_tasks/mod.rs b/apps/recorder/src/models/subscriber_tasks/mod.rs index b2d53dc..5b0bdf0 100644 --- a/apps/recorder/src/models/subscriber_tasks/mod.rs +++ b/apps/recorder/src/models/subscriber_tasks/mod.rs @@ -24,13 +24,14 @@ pub enum SubscriberTaskStatus { Killed, } -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "subscriber_tasks")] pub struct Model { #[sea_orm(primary_key)] pub id: String, pub subscriber_id: i32, pub subscription_id: Option, + pub cron_id: Option, pub job: SubscriberTask, pub task_type: SubscriberTaskType, pub status: SubscriberTaskStatus, @@ -51,7 +52,7 @@ pub enum Relation { from = "Column::SubscriberId", to = "super::subscribers::Column::Id", on_update = "Cascade", - on_delete = "Cascade" + on_delete = "NoAction" )] Subscriber, #[sea_orm( @@ -62,6 +63,14 @@ pub enum Relation { on_delete = "NoAction" )] Subscription, + #[sea_orm( + belongs_to = "super::cron::Entity", + from = "Column::CronId", + to = "super::cron::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Cron, } impl Related for Entity { @@ -76,12 +85,20 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cron.def() + } +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] pub enum RelatedEntity { #[sea_orm(entity = "super::subscribers::Entity")] Subscriber, #[sea_orm(entity = "super::subscriptions::Entity")] Subscription, + #[sea_orm(entity = "super::cron::Entity")] + Cron, } #[async_trait] diff --git a/apps/recorder/src/models/subscribers.rs b/apps/recorder/src/models/subscribers.rs index f344f61..331c17f 100644 --- a/apps/recorder/src/models/subscribers.rs +++ b/apps/recorder/src/models/subscribers.rs @@ -45,6 +45,8 @@ pub enum Relation { Feed, #[sea_orm(has_many = "super::subscriber_tasks::Entity")] SubscriberTask, + #[sea_orm(has_many = "super::system_tasks::Entity")] + SystemTask, } impl Related for Entity { @@ -95,6 +97,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::SystemTask.def() + } +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] pub enum RelatedEntity { #[sea_orm(entity = "super::subscriptions::Entity")] @@ -111,6 +119,8 @@ pub enum RelatedEntity { Feed, #[sea_orm(entity = "super::subscriber_tasks::Entity")] SubscriberTask, + #[sea_orm(entity = "super::system_tasks::Entity")] + SystemTask, } #[derive(Debug, Deserialize, Serialize)] diff --git a/apps/recorder/src/models/system_tasks/mod.rs b/apps/recorder/src/models/system_tasks/mod.rs new file mode 100644 index 0000000..a9fcd19 --- /dev/null +++ b/apps/recorder/src/models/system_tasks/mod.rs @@ -0,0 +1,99 @@ +use async_trait::async_trait; +use sea_orm::{ActiveValue, entity::prelude::*}; + +pub use crate::task::{ + SystemTask, SystemTaskInput, SystemTaskType, SystemTaskTypeEnum, SystemTaskTypeVariant, + SystemTaskTypeVariantIter, +}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveActiveEnum, EnumIter, DeriveDisplay)] +#[sea_orm(rs_type = "String", db_type = "Text")] +pub enum SystemTaskStatus { + #[sea_orm(string_value = "Pending")] + Pending, + #[sea_orm(string_value = "Scheduled")] + Scheduled, + #[sea_orm(string_value = "Running")] + Running, + #[sea_orm(string_value = "Done")] + Done, + #[sea_orm(string_value = "Failed")] + Failed, + #[sea_orm(string_value = "Killed")] + Killed, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "system_tasks")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: String, + pub subscriber_id: Option, + pub cron_id: Option, + pub job: SystemTask, + pub task_type: SystemTaskType, + pub status: SystemTaskStatus, + pub attempts: i32, + pub max_attempts: i32, + pub run_at: DateTimeUtc, + pub last_error: Option, + pub lock_at: Option, + pub lock_by: Option, + pub done_at: Option, + pub priority: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::subscribers::Entity", + from = "Column::SubscriberId", + to = "super::subscribers::Column::Id", + on_update = "Cascade", + on_delete = "Restrict" + )] + Subscriber, + #[sea_orm( + belongs_to = "super::cron::Entity", + from = "Column::CronId", + to = "super::cron::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Cron, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Subscriber.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cron.def() + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)] +pub enum RelatedEntity { + #[sea_orm(entity = "super::subscribers::Entity")] + Subscriber, + #[sea_orm(entity = "super::cron::Entity")] + Cron, +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + async fn before_save(mut self, _db: &C, _insert: bool) -> Result + where + C: ConnectionTrait, + { + if let ActiveValue::Set(Some(..)) = self.subscriber_id { + return Err(DbErr::Custom( + "SystemTask can not be created by subscribers now".to_string(), + )); + } + Ok(self) + } +} diff --git a/apps/recorder/src/storage/client.rs b/apps/recorder/src/storage/client.rs index 1da2118..2bdb73a 100644 --- a/apps/recorder/src/storage/client.rs +++ b/apps/recorder/src/storage/client.rs @@ -89,6 +89,13 @@ impl StorageService { p } + #[cfg(any(test, feature = "test-utils"))] + pub fn build_test_path(&self, path: impl AsRef) -> PathBuf { + let mut p = PathBuf::from("/test"); + p.push(path); + p + } + pub fn build_public_path(&self, path: impl AsRef) -> PathBuf { let mut p = PathBuf::from("/public"); p.push(path); diff --git a/apps/recorder/src/task/core.rs b/apps/recorder/src/task/core.rs index ffd9fb6..d15ad6a 100644 --- a/apps/recorder/src/task/core.rs +++ b/apps/recorder/src/task/core.rs @@ -2,12 +2,16 @@ use std::sync::Arc; use async_trait::async_trait; use futures::{Stream, StreamExt, pin_mut}; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde::{Serialize, de::DeserializeOwned}; use crate::{app::AppContextTrait, errors::RecorderResult}; pub const SYSTEM_TASK_APALIS_NAME: &str = "system_task"; pub const SUBSCRIBER_TASK_APALIS_NAME: &str = "subscriber_task"; +pub const SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME: &str = + "setup_apalis_jobs_extra_foreign_keys"; +pub const SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_TRIGGER_NAME: &str = + "setup_apalis_jobs_extra_foreign_keys_trigger"; #[async_trait] pub trait AsyncTaskTrait: Serialize + DeserializeOwned + Sized { @@ -41,20 +45,30 @@ where } } +pub trait SystemTaskTrait: AsyncTaskTrait { + type InputType: Serialize + DeserializeOwned + Sized + Send; + + fn get_subscriber_id(&self) -> Option; + + fn set_subscriber_id(&mut self, subscriber_id: Option); + + fn get_cron_id(&self) -> Option; + + fn set_cron_id(&mut self, cron_id: Option); + + fn from_input(input: Self::InputType, subscriber_id: Option) -> Self; +} + pub trait SubscriberTaskTrait: AsyncTaskTrait { type InputType: Serialize + DeserializeOwned + Sized + Send; fn get_subscriber_id(&self) -> i32; + fn set_subscriber_id(&mut self, subscriber_id: i32); + fn get_cron_id(&self) -> Option; + fn set_cron_id(&mut self, cron_id: Option); + fn from_input(input: Self::InputType, subscriber_id: i32) -> Self; } - -pub trait SystemTaskTrait: AsyncTaskTrait {} - -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct SubscriberTaskBase { - pub subscriber_id: i32, - pub cron_id: Option, -} diff --git a/apps/recorder/src/task/extern.rs b/apps/recorder/src/task/extern.rs deleted file mode 100644 index 56edfc6..0000000 --- a/apps/recorder/src/task/extern.rs +++ /dev/null @@ -1,16 +0,0 @@ -use sea_orm::sea_query; - -#[derive(sea_query::Iden)] - -pub enum ApalisSchema { - #[iden = "apalis"] - Schema, -} - -#[derive(sea_query::Iden)] - -pub enum ApalisJobs { - #[iden = "jobs"] - Table, - Id, -} diff --git a/apps/recorder/src/task/mod.rs b/apps/recorder/src/task/mod.rs index e45a4a2..2324b98 100644 --- a/apps/recorder/src/task/mod.rs +++ b/apps/recorder/src/task/mod.rs @@ -1,21 +1,22 @@ mod config; mod core; -mod r#extern; mod registry; mod service; pub use core::{ - AsyncTaskTrait, SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, StreamTaskTrait, - SubscriberTaskBase, SubscriberTaskTrait, SystemTaskTrait, + AsyncTaskTrait, SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, + SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_TRIGGER_NAME, SUBSCRIBER_TASK_APALIS_NAME, + SYSTEM_TASK_APALIS_NAME, StreamTaskTrait, SubscriberTaskTrait, SystemTaskTrait, }; pub use config::TaskConfig; -pub use r#extern::{ApalisJobs, ApalisSchema}; pub use registry::{ OptimizeImageTask, SubscriberTask, SubscriberTaskInput, SubscriberTaskType, SubscriberTaskTypeEnum, SubscriberTaskTypeVariant, SubscriberTaskTypeVariantIter, SyncOneSubscriptionFeedsFullTask, SyncOneSubscriptionFeedsIncrementalTask, - SyncOneSubscriptionSourcesTask, SystemTask, SystemTaskType, SystemTaskTypeEnum, - SystemTaskTypeVariant, SystemTaskTypeVariantIter, + SyncOneSubscriptionSourcesTask, SystemTask, SystemTaskInput, SystemTaskType, + SystemTaskTypeEnum, SystemTaskTypeVariant, SystemTaskTypeVariantIter, }; +#[allow(unused_imports)] +pub(crate) use registry::{register_subscriber_task_type, register_system_task_type}; pub use service::TaskService; diff --git a/apps/recorder/src/task/registry/mod.rs b/apps/recorder/src/task/registry/mod.rs index e740667..4cd4c65 100644 --- a/apps/recorder/src/task/registry/mod.rs +++ b/apps/recorder/src/task/registry/mod.rs @@ -1,12 +1,14 @@ mod subscriber; mod system; +pub(crate) use subscriber::register_subscriber_task_type; pub use subscriber::{ SubscriberTask, SubscriberTaskInput, SubscriberTaskType, SubscriberTaskTypeEnum, SubscriberTaskTypeVariant, SubscriberTaskTypeVariantIter, SyncOneSubscriptionFeedsFullTask, SyncOneSubscriptionFeedsIncrementalTask, SyncOneSubscriptionSourcesTask, }; +pub(crate) use system::register_system_task_type; pub use system::{ - OptimizeImageTask, SystemTask, SystemTaskType, SystemTaskTypeEnum, SystemTaskTypeVariant, - SystemTaskTypeVariantIter, + OptimizeImageTask, SystemTask, SystemTaskInput, SystemTaskType, SystemTaskTypeEnum, + SystemTaskTypeVariant, SystemTaskTypeVariantIter, }; diff --git a/apps/recorder/src/task/registry/subscriber/base.rs b/apps/recorder/src/task/registry/subscriber/base.rs index 7af076b..7933eb1 100644 --- a/apps/recorder/src/task/registry/subscriber/base.rs +++ b/apps/recorder/src/task/registry/subscriber/base.rs @@ -7,7 +7,7 @@ macro_rules! register_subscriber_task_type { ) => { $(#[$type_meta])* #[derive(typed_builder::TypedBuilder, ts_rs::TS, serde::Serialize, serde::Deserialize)] - #[ts(export, rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] $task_vis struct $task_name { $($(#[$field_meta])* pub $field_name: $field_type,)* pub subscriber_id: i32, @@ -20,7 +20,7 @@ macro_rules! register_subscriber_task_type { $(#[$type_meta])* #[derive(ts_rs::TS, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] - #[ts(export, rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] $task_vis struct [<$task_name Input>] { $($(#[$field_meta])* pub $field_name: $field_type,)* #[serde(default, skip_serializing_if = "Option::is_none")] @@ -44,6 +44,14 @@ macro_rules! register_subscriber_task_type { self.cron_id } + fn set_subscriber_id(&mut self, subscriber_id: i32) { + self.subscriber_id = subscriber_id; + } + + fn set_cron_id(&mut self, cron_id: Option) { + self.cron_id = cron_id; + } + fn from_input(input: Self::InputType, subscriber_id: i32) -> Self { Self { $($field_name: input.$field_name,)* diff --git a/apps/recorder/src/task/registry/subscriber/mod.rs b/apps/recorder/src/task/registry/subscriber/mod.rs index a4373c0..00ddada 100644 --- a/apps/recorder/src/task/registry/subscriber/mod.rs +++ b/apps/recorder/src/task/registry/subscriber/mod.rs @@ -1,6 +1,7 @@ mod base; mod subscription; +pub(crate) use base::register_subscriber_task_type; use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter, FromJsonQueryResult}; pub use subscription::{ SyncOneSubscriptionFeedsFullTask, SyncOneSubscriptionFeedsIncrementalTask, @@ -44,7 +45,7 @@ macro_rules! register_subscriber_task_types { $(#[$task_enum_meta])* #[derive(ts_rs::TS, serde::Serialize, serde::Deserialize)] #[serde(tag = "task_type")] - #[ts(export,rename = "SubscriberTaskType", rename_all = "camelCase", tag = "taskType")] + #[ts(export, rename = "SubscriberTaskType", rename_all = "camelCase", tag = "taskType")] $task_vis enum $task_enum_name { $( $(#[$task_variant_meta])* @@ -57,7 +58,7 @@ macro_rules! register_subscriber_task_types { $(#[$task_enum_meta])* #[derive(ts_rs::TS, serde::Serialize, serde::Deserialize)] #[serde(tag = "taskType", rename_all = "camelCase")] - #[ts(export,rename_all = "camelCase", tag = "taskType")] + #[ts(export, rename_all = "camelCase", tag = "taskType")] $task_vis enum [<$task_enum_name Input>] { $( $(#[$task_variant_meta])* @@ -67,23 +68,6 @@ macro_rules! register_subscriber_task_types { } } - impl TryFrom<$task_enum_name> for serde_json::Value { - type Error = $crate::errors::RecorderError; - - fn try_from(value: $task_enum_name) -> Result { - let json_value = serde_json::to_value(value)?; - Ok(match json_value { - serde_json::Value::Object(mut map) => { - map.remove("task_type"); - serde_json::Value::Object(map) - } - _ => { - unreachable!("subscriber task must be an json object"); - } - }) - } - } - impl $task_enum_name { pub fn task_type(&self) -> $type_enum_name { match self { @@ -121,6 +105,18 @@ macro_rules! register_subscriber_task_types { } } + fn set_subscriber_id(&mut self, subscriber_id: i32) { + match self { + $(Self::$task_variant(t) => t.set_subscriber_id(subscriber_id),)* + } + } + + fn set_cron_id(&mut self, cron_id: Option) { + match self { + $(Self::$task_variant(t) => t.set_cron_id(cron_id),)* + } + } + fn from_input(input: Self::InputType, subscriber_id: i32) -> Self { match input { $(Self::InputType::$task_variant(t) => @@ -159,7 +155,7 @@ register_subscriber_task_types!( } }, task_enum: { - #[derive(Clone, Debug, PartialEq, Eq, FromJsonQueryResult)] + #[derive(Clone, Debug, PartialEq, FromJsonQueryResult)] pub enum SubscriberTask { SyncOneSubscriptionFeedsIncremental(SyncOneSubscriptionFeedsIncrementalTask), SyncOneSubscriptionFeedsFull(SyncOneSubscriptionFeedsFullTask), diff --git a/apps/recorder/src/task/registry/subscriber/subscription.rs b/apps/recorder/src/task/registry/subscriber/subscription.rs index 678b636..485dbb4 100644 --- a/apps/recorder/src/task/registry/subscriber/subscription.rs +++ b/apps/recorder/src/task/registry/subscriber/subscription.rs @@ -39,7 +39,7 @@ macro_rules! register_subscription_task_type { } register_subscription_task_type! { - #[derive(Clone, Debug, PartialEq, Eq)] + #[derive(Clone, Debug, PartialEq)] pub struct SyncOneSubscriptionFeedsIncrementalTask { } => async |subscription, ctx| -> RecorderResult<()> { subscription.sync_feeds_incremental(ctx).await?; @@ -48,7 +48,7 @@ register_subscription_task_type! { } register_subscription_task_type! { - #[derive(Clone, Debug, PartialEq, Eq)] + #[derive(Clone, Debug, PartialEq)] pub struct SyncOneSubscriptionFeedsFullTask { } => async |subscription, ctx| -> RecorderResult<()> { subscription.sync_feeds_full(ctx).await?; @@ -57,7 +57,7 @@ register_subscription_task_type! { } register_subscription_task_type! { - #[derive(Clone, Debug, PartialEq, Eq)] + #[derive(Clone, Debug, PartialEq)] pub struct SyncOneSubscriptionSourcesTask { } => async |subscription, ctx| -> RecorderResult<()> { subscription.sync_sources(ctx).await?; diff --git a/apps/recorder/src/task/registry/system/base.rs b/apps/recorder/src/task/registry/system/base.rs new file mode 100644 index 0000000..6f6fa93 --- /dev/null +++ b/apps/recorder/src/task/registry/system/base.rs @@ -0,0 +1,67 @@ +macro_rules! register_system_task_type { + ( + $(#[$type_meta:meta])* + $task_vis:vis struct $task_name:ident { + $($(#[$field_meta:meta])* pub $field_name:ident: $field_type:ty),* $(,)? + } + ) => { + $(#[$type_meta])* + #[derive(typed_builder::TypedBuilder, ts_rs::TS, serde::Serialize, serde::Deserialize)] + #[ts(rename_all = "camelCase")] + $task_vis struct $task_name { + $($(#[$field_meta])* pub $field_name: $field_type,)* + #[serde(default, skip_serializing_if = "Option::is_none")] + #[builder(default = None)] + pub subscriber_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[builder(default = None)] + pub cron_id: Option, + } + + paste::paste! { + $(#[$type_meta])* + #[derive(ts_rs::TS, serde::Serialize, serde::Deserialize)] + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + $task_vis struct [<$task_name Input>] { + $($(#[$field_meta])* pub $field_name: $field_type,)* + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subscriber_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cron_id: Option, + } + } + + impl $crate::task::SystemTaskTrait for $task_name { + paste::paste! { + type InputType = [<$task_name Input>]; + } + + fn get_subscriber_id(&self) -> Option { + self.subscriber_id + } + + fn get_cron_id(&self) -> Option { + self.cron_id + } + + fn set_subscriber_id(&mut self, subscriber_id: Option) { + self.subscriber_id = subscriber_id; + } + + fn set_cron_id(&mut self, cron_id: Option) { + self.cron_id = cron_id; + } + + fn from_input(input: Self::InputType, subscriber_id: Option) -> Self { + Self { + $($field_name: input.$field_name,)* + subscriber_id: input.subscriber_id.or(subscriber_id), + cron_id: input.cron_id, + } + } + } + } +} + +pub(crate) use register_system_task_type; diff --git a/apps/recorder/src/task/registry/system/media.rs b/apps/recorder/src/task/registry/system/media.rs index 86b4de3..add3a9a 100644 --- a/apps/recorder/src/task/registry/system/media.rs +++ b/apps/recorder/src/task/registry/system/media.rs @@ -1,18 +1,22 @@ use std::sync::Arc; use quirks_path::Path; -use serde::{Deserialize, Serialize}; use tracing::instrument; use crate::{ - app::AppContextTrait, errors::RecorderResult, media::EncodeImageOptions, task::AsyncTaskTrait, + app::AppContextTrait, + errors::RecorderResult, + media::EncodeImageOptions, + task::{AsyncTaskTrait, register_system_task_type}, }; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct OptimizeImageTask { - pub source_path: String, - pub target_path: String, - pub format_options: EncodeImageOptions, +register_system_task_type! { + #[derive(Clone, Debug, PartialEq)] + pub struct OptimizeImageTask { + pub source_path: String, + pub target_path: String, + pub format_options: EncodeImageOptions, + } } #[async_trait::async_trait] diff --git a/apps/recorder/src/task/registry/system/mod.rs b/apps/recorder/src/task/registry/system/mod.rs index f248602..ca94ff6 100644 --- a/apps/recorder/src/task/registry/system/mod.rs +++ b/apps/recorder/src/task/registry/system/mod.rs @@ -1,14 +1,15 @@ +mod base; mod media; +pub(crate) use base::register_system_task_type; pub use media::OptimizeImageTask; use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter, FromJsonQueryResult}; -use serde::{Deserialize, Serialize}; macro_rules! register_system_task_types { ( task_type_enum: { $(#[$type_enum_meta:meta])* - pub enum $type_enum_name:ident { + $type_vis:vis enum $type_enum_name:ident { $( $(#[$variant_meta:meta])* $variant:ident => $string_value:literal @@ -17,16 +18,18 @@ macro_rules! register_system_task_types { }, task_enum: { $(#[$task_enum_meta:meta])* - pub enum $task_enum_name:ident { + $task_vis:vis enum $task_enum_name:ident { $( + $(#[$task_variant_meta:meta])* $task_variant:ident($task_type:ty) ),* $(,)? } } ) => { $(#[$type_enum_meta])* + #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq)] #[sea_orm(rs_type = "String", db_type = "Text")] - pub enum $type_enum_name { + $type_vis enum $type_enum_name { $( $(#[$variant_meta])* #[serde(rename = $string_value)] @@ -37,30 +40,17 @@ macro_rules! register_system_task_types { $(#[$task_enum_meta])* + #[derive(ts_rs::TS, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(tag = "task_type")] - pub enum $task_enum_name { + #[ts(export, rename = "SystemTaskType", rename_all = "camelCase", tag = "taskType")] + $task_vis enum $task_enum_name { $( + $(#[$task_variant_meta])* + #[serde(rename = $string_value)] $task_variant($task_type), )* } - impl TryFrom<$task_enum_name> for serde_json::Value { - type Error = $crate::errors::RecorderError; - - fn try_from(value: $task_enum_name) -> Result { - let json_value = serde_json::to_value(value)?; - Ok(match json_value { - serde_json::Value::Object(mut map) => { - map.remove("task_type"); - serde_json::Value::Object(map) - } - _ => { - unreachable!("subscriber task must be an json object"); - } - }) - } - } - impl $task_enum_name { pub fn task_type(&self) -> $type_enum_name { match self { @@ -69,6 +59,21 @@ macro_rules! register_system_task_types { } } + paste::paste! { + $(#[$task_enum_meta])* + #[derive(ts_rs::TS, serde::Serialize, serde::Deserialize, PartialEq)] + #[serde(tag = "taskType", rename_all = "camelCase")] + #[ts(export, rename_all = "camelCase", tag = "taskType")] + $task_vis enum [<$task_enum_name Input>] { + $( + $(#[$task_variant_meta])* + #[serde(rename = $string_value)] + $task_variant(<$task_type as $crate::task::SystemTaskTrait>::InputType), + )* + } + } + + #[async_trait::async_trait] impl $crate::task::AsyncTaskTrait for $task_enum_name { async fn run_async(self, ctx: std::sync::Arc) -> $crate::errors::RecorderResult<()> { @@ -78,18 +83,60 @@ macro_rules! register_system_task_types { } } } + + impl $crate::task::SystemTaskTrait for $task_enum_name { + paste::paste! { + type InputType = [<$task_enum_name Input>]; + } + + fn get_subscriber_id(&self) -> Option { + match self { + $(Self::$task_variant(t) => t.get_subscriber_id(),)* + } + } + + fn get_cron_id(&self) -> Option { + match self { + $(Self::$task_variant(t) => t.get_cron_id(),)* + } + } + + fn set_subscriber_id(&mut self, subscriber_id: Option) { + match self { + $(Self::$task_variant(t) => t.set_subscriber_id(subscriber_id),)* + } + } + + fn set_cron_id(&mut self, cron_id: Option) { + match self { + $(Self::$task_variant(t) => t.set_cron_id(cron_id),)* + } + } + + fn from_input(input: Self::InputType, subscriber_id: Option) -> Self { + match input { + $(Self::InputType::$task_variant(t) => + Self::$task_variant(<$task_type as $crate::task::SystemTaskTrait>::from_input(t, subscriber_id)),)* + } + } + } + + $( + impl From<$task_type> for $task_enum_name { + fn from(task: $task_type) -> Self { + Self::$task_variant(task) + } + } + )* }; } +#[cfg(not(any(test, feature = "test-utils")))] register_system_task_types! { task_type_enum: { #[derive( Clone, Debug, - Serialize, - Deserialize, - PartialEq, - Eq, Copy, DeriveActiveEnum, DeriveDisplay, @@ -100,9 +147,34 @@ register_system_task_types! { } }, task_enum: { - #[derive(Clone, Debug, Serialize, Deserialize, FromJsonQueryResult)] + #[derive(Clone, Debug, FromJsonQueryResult)] pub enum SystemTask { - OptimizeImage(OptimizeImageTask), + OptimizeImage(OptimizeImageTask) + } + } +} + +#[cfg(any(test, feature = "test-utils"))] +register_system_task_types! { + task_type_enum: { + #[derive( + Clone, + Debug, + Copy, + DeriveActiveEnum, + DeriveDisplay, + EnumIter + )] + pub enum SystemTaskType { + OptimizeImage => "optimize_image", + Test => "test", + } + }, + task_enum: { + #[derive(Clone, Debug, FromJsonQueryResult)] + pub enum SystemTask { + OptimizeImage(OptimizeImageTask), + Test(crate::test_utils::task::TestSystemTask), } } } diff --git a/apps/recorder/src/task/service.rs b/apps/recorder/src/task/service.rs index 09e1f91..d22ba64 100644 --- a/apps/recorder/src/task/service.rs +++ b/apps/recorder/src/task/service.rs @@ -294,3 +294,31 @@ impl TaskService { } } } + +#[cfg(test)] +#[allow(unused_variables)] +mod tests { + use rstest::{fixture, rstest}; + use tracing::Level; + + use super::*; + use crate::test_utils::{app::TestingPreset, tracing::try_init_testing_tracing}; + + #[fixture] + fn before_each() { + try_init_testing_tracing(Level::DEBUG); + } + + #[rstest] + #[tokio::test] + async fn test_cron_due_listening(before_each: ()) -> RecorderResult<()> { + let mut preset = TestingPreset::default().await?; + let app_ctx = preset.app_ctx.clone(); + + let db = app_ctx.db(); + + todo!(); + + Ok(()) + } +} diff --git a/apps/recorder/src/test_utils/app.rs b/apps/recorder/src/test_utils/app.rs index 9bec8ec..31baf17 100644 --- a/apps/recorder/src/test_utils/app.rs +++ b/apps/recorder/src/test_utils/app.rs @@ -5,11 +5,12 @@ use typed_builder::TypedBuilder; use crate::{ app::AppContextTrait, + errors::RecorderResult, test_utils::{ crypto::build_testing_crypto_service, database::{TestingDatabaseServiceConfig, build_testing_database_service}, media::build_testing_media_service, - mikan::build_testing_mikan_client, + mikan::{MikanMockServer, build_testing_mikan_client}, storage::build_testing_storage_service, task::build_testing_task_service, }, @@ -42,10 +43,8 @@ impl TestingAppContext { self.task.get_or_init(|| task); } - pub async fn from_preset( - preset: TestingAppContextPreset, - ) -> crate::errors::RecorderResult> { - let mikan_client = build_testing_mikan_client(preset.mikan_base_url.clone()).await?; + pub async fn from_preset(preset: TestingAppContextPreset) -> RecorderResult> { + let mikan_client = build_testing_mikan_client(preset.mikan_base_url).await?; let db_service = build_testing_database_service(preset.database_config.unwrap_or_default()).await?; let crypto_service = build_testing_crypto_service().await?; @@ -137,3 +136,28 @@ pub struct TestingAppContextPreset { pub mikan_base_url: String, pub database_config: Option, } + +#[derive(TypedBuilder)] +pub struct TestingPreset { + pub mikan_server: MikanMockServer, + pub app_ctx: Arc, +} + +impl TestingPreset { + pub async fn default() -> RecorderResult { + let mikan_server = MikanMockServer::new().await?; + let database_config = TestingDatabaseServiceConfig::default(); + + let app_ctx = TestingAppContext::from_preset(TestingAppContextPreset { + mikan_base_url: mikan_server.base_url().to_string(), + database_config: Some(database_config), + }) + .await?; + + let preset = Self::builder() + .mikan_server(mikan_server) + .app_ctx(app_ctx) + .build(); + Ok(preset) + } +} diff --git a/apps/recorder/src/test_utils/database.rs b/apps/recorder/src/test_utils/database.rs index 11df594..ca1bd40 100644 --- a/apps/recorder/src/test_utils/database.rs +++ b/apps/recorder/src/test_utils/database.rs @@ -3,6 +3,7 @@ use crate::{ errors::RecorderResult, }; +#[derive(Clone, Debug)] pub struct TestingDatabaseServiceConfig { pub auto_migrate: bool, } diff --git a/apps/recorder/src/test_utils/mikan.rs b/apps/recorder/src/test_utils/mikan.rs index dd35622..d39a4b6 100644 --- a/apps/recorder/src/test_utils/mikan.rs +++ b/apps/recorder/src/test_utils/mikan.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + fmt::Debug, ops::{Deref, DerefMut}, path::{self, PathBuf}, }; @@ -148,13 +149,15 @@ impl AsRef for MikanDoppelPath { } } +#[cfg(any(test, debug_assertions, feature = "test-utils"))] lazy_static! { static ref TEST_RESOURCES_DIR: String = - if cfg!(any(test, debug_assertions, feature = "playground")) { - format!("{}/tests/resources", env!("CARGO_MANIFEST_DIR")) - } else { - "tests/resources".to_string() - }; + format!("{}/tests/resources", env!("CARGO_MANIFEST_DIR")); +} + +#[cfg(not(any(test, debug_assertions, feature = "test-utils")))] +lazy_static! { + static ref TEST_RESOURCES_DIR: String = "tests/resources".to_string(); } impl From for MikanDoppelPath { @@ -227,6 +230,14 @@ pub struct MikanMockServer { base_url: Url, } +impl Debug for MikanMockServer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MikanMockServer") + .field("base_url", &self.base_url) + .finish() + } +} + impl MikanMockServer { pub async fn new_with_port(port: u16) -> RecorderResult { let server = mockito::Server::new_with_opts_async(mockito::ServerOpts { diff --git a/apps/recorder/src/test_utils/task.rs b/apps/recorder/src/test_utils/task.rs index 2c028e4..07e8a27 100644 --- a/apps/recorder/src/test_utils/task.rs +++ b/apps/recorder/src/test_utils/task.rs @@ -1,15 +1,43 @@ use std::sync::Arc; +use chrono::Utc; + use crate::{ app::AppContextTrait, errors::RecorderResult, - task::{TaskConfig, TaskService}, + task::{AsyncTaskTrait, TaskConfig, TaskService, register_system_task_type}, }; +register_system_task_type! { + #[derive(Debug, Clone, PartialEq)] + pub struct TestSystemTask { + pub task_id: String, + } +} + +#[async_trait::async_trait] +impl AsyncTaskTrait for TestSystemTask { + async fn run_async(self, ctx: Arc) -> RecorderResult<()> { + let storage = ctx.storage(); + + storage + .write( + storage.build_test_path(self.task_id), + serde_json::json!({ "exec_time": Utc::now().timestamp_millis() }) + .to_string() + .into(), + ) + .await?; + + Ok(()) + } +} + pub async fn build_testing_task_service( ctx: Arc, ) -> RecorderResult { let config = TaskConfig::default(); let task_service = TaskService::from_config_and_ctx(config, ctx).await?; + Ok(task_service) } diff --git a/justfile b/justfile index 6ef6bb6..6edd582 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,10 @@ set windows-shell := ["pwsh.exe", "-c"] set dotenv-load := true +clean-cargo-incremental: + # https://github.com/rust-lang/rust/issues/141540 + rm -r target/debug/incremental + prepare-dev: cargo install cargo-binstall cargo binstall sea-orm-cli cargo-llvm-cov cargo-nextest diff --git a/package.json b/package.json index 098bf76..627865f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,10 @@ "version": "0.0.0", "description": "Kono bangumi?", "license": "MIT", - "workspaces": ["packages/*", "apps/*"], + "workspaces": [ + "packages/*", + "apps/*" + ], "type": "module", "repository": { "type": "git", @@ -20,17 +23,17 @@ "node": ">=22" }, "dependencies": { - "es-toolkit": "^1.39.3" + "es-toolkit": "^1.39.6" }, "devDependencies": { "@biomejs/biome": "1.9.4", - "@types/node": "^24.0.1", + "@types/node": "^24.0.10", "cross-env": "^7.0.3", "kill-port": "^2.0.1", "npm-run-all": "^4.1.5", - "tsx": "^4.20.2", + "tsx": "^4.20.3", "typescript": "^5.8.3", - "ultracite": "^4.2.10" + "ultracite": "^4.2.13" }, "pnpm": { "overrides": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99e4e78..75b7f10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,15 +12,15 @@ importers: .: dependencies: es-toolkit: - specifier: ^1.39.3 - version: 1.39.3 + specifier: ^1.39.6 + version: 1.39.6 devDependencies: '@biomejs/biome': specifier: 1.9.4 version: 1.9.4 '@types/node': - specifier: ^24.0.1 - version: 24.0.1 + specifier: ^24.0.10 + version: 24.0.10 cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -31,14 +31,14 @@ importers: specifier: ^4.1.5 version: 4.1.5 tsx: - specifier: ^4.20.2 - version: 4.20.2 + specifier: ^4.20.3 + version: 4.20.3 typescript: specifier: ^5.8.3 version: 5.8.3 ultracite: - specifier: ^4.2.10 - version: 4.2.10 + specifier: ^4.2.13 + version: 4.2.13 apps/docs: {} @@ -91,7 +91,7 @@ importers: version: 0.2.5(solid-js@1.9.7) '@graphiql/toolkit': specifier: ^0.11.3 - version: 0.11.3(@types/node@24.0.1)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0) + version: 0.11.3(@types/node@24.0.10)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0) '@hookform/resolvers': specifier: ^5.1.1 version: 5.1.1(react-hook-form@7.57.0(react@19.1.0)) @@ -217,7 +217,7 @@ importers: version: 8.6.0(react@19.1.0) graphiql: specifier: ^4.1.2 - version: 4.1.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + version: 4.1.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) graphql: specifier: ^16.11.0 version: 16.11.0 @@ -278,7 +278,7 @@ importers: devDependencies: '@graphql-codegen/cli': specifier: ^5.0.7 - version: 5.0.7(@parcel/watcher@2.5.1)(@types/node@24.0.1)(bufferutil@4.0.9)(encoding@0.1.13)(enquirer@2.4.1)(graphql@16.11.0)(typescript@5.8.3)(utf-8-validate@6.0.5) + version: 5.0.7(@parcel/watcher@2.5.1)(@types/node@24.0.10)(bufferutil@4.0.9)(encoding@0.1.13)(enquirer@2.4.1)(graphql@16.11.0)(typescript@5.8.3)(utf-8-validate@6.0.5) '@graphql-codegen/client-preset': specifier: ^4.8.2 version: 4.8.2(encoding@0.1.13)(graphql@16.11.0) @@ -305,7 +305,7 @@ importers: version: 1.121.5(@tanstack/react-router@1.121.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.121.2)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.7)(tiny-invariant@1.3.3) '@tanstack/router-plugin': specifier: ^1.121.4 - version: 1.121.4(@rsbuild/core@1.3.22)(@tanstack/react-router@1.121.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0))(webpack@5.97.1) + version: 1.121.4(@rsbuild/core@1.3.22)(@tanstack/react-router@1.121.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))(webpack@5.97.1) '@types/react': specifier: ^19.1.8 version: 19.1.8 @@ -351,10 +351,10 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.5.2 - version: 4.5.2(vite@5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0)) + version: 4.5.2(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0)) vitest: specifier: ^3.2.3 - version: 3.2.3(@types/node@24.0.1)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) + version: 3.2.3(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) packages: @@ -1460,6 +1460,9 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1472,15 +1475,21 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -2733,38 +2742,38 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@solid-devtools/debugger@0.27.0': - resolution: {integrity: sha512-z0mqbjKmPUZ3x3mlEVTSbMmlMP+DTHjrk4P2L1Bk6352Oy7CkgAL6UNWuxVGlWGueK+9pL60n53045A0ofoEFQ==} + '@solid-devtools/debugger@0.28.1': + resolution: {integrity: sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg==} peerDependencies: solid-js: ^1.9.0 - '@solid-devtools/logger@0.9.8': - resolution: {integrity: sha512-EI4yItbBmHemMwqkmJ8xXEuaPXUwrAWCEghqcflMFP63dszDbsdby+/4ydddSLFpbP/YWlNKj/oybPk2XSfbEQ==} + '@solid-devtools/logger@0.9.11': + resolution: {integrity: sha512-THbiY1iQlieL6vdgJc4FIsLe7V8a57hod/Thm8zdKrTkWL88UPZjkBBfM+mVNGusd4OCnAN20tIFBhNnuT1Dew==} peerDependencies: solid-js: ^1.9.0 - '@solid-devtools/shared@0.19.1': - resolution: {integrity: sha512-bkPrp3dlDveEHqeOyO4v6XqikKCla0A4bGQ3uoYiilPv54LcDUKqrEvQjeSfw26oO8QbGSChetY8OUpT727yug==} + '@solid-devtools/shared@0.20.0': + resolution: {integrity: sha512-o5TACmUOQsxpzpOKCjbQqGk8wL8PMi+frXG9WNu4Lh3PQVUB6hs95Kl/S8xc++zwcMguUKZJn8h5URUiMOca6Q==} peerDependencies: solid-js: ^1.9.0 - '@solid-primitives/bounds@0.1.1': - resolution: {integrity: sha512-b4s8JClkRq2RQlU3K4qeCVASdtct5gfg3HNfWGeD7oPjWlSkp2RyDHJwt9ZtPaXOEOIHhNBbLgjtLax/4B6VUQ==} + '@solid-primitives/bounds@0.1.3': + resolution: {integrity: sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/event-listener@2.4.1': - resolution: {integrity: sha512-Xc/lBCeuh9LwzR4lYbMDtopwWK7N9b4o+FmI4uoI8DOtVGYi0Ip20DG8PtwHk+g31lHgvwtFFVKfnUx2UaqZJg==} + '@solid-primitives/event-listener@2.4.3': + resolution: {integrity: sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/keyboard@1.3.1': - resolution: {integrity: sha512-ib4xPC5ioOGj2A/5PqFTJvWbgGVx/5okFEoU0qXhCrehVB84gPBhKFNRqTlpiYzCbVHPIUZCTO2ZMkqzJdIA2w==} + '@solid-primitives/keyboard@1.3.3': + resolution: {integrity: sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/media@2.3.1': - resolution: {integrity: sha512-UTX8LAaQS7k3rvekme8y5ihOrt5SJpgkw7xyUySlPhIapD7JxlhYncQoSFsys5D1XPCgI/3snobpvbanRcrTAw==} + '@solid-primitives/media@2.3.3': + resolution: {integrity: sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA==} peerDependencies: solid-js: ^1.6.12 @@ -2773,23 +2782,18 @@ packages: peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/platform@0.2.1': - resolution: {integrity: sha512-902jki7Q88/JNl4PIAg9h3lWFC3W/9y4OrpK9cmaYRobD3V5qyXAWTlM4aAKPfwpABhTFu9Ky07FPcfF9hWp+Q==} + '@solid-primitives/refs@1.1.2': + resolution: {integrity: sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/refs@1.1.1': - resolution: {integrity: sha512-MIQ7Bh59IiT9NDQPf6iWRnPe0RgKggEjF0H+iMoIi1KBCcp4Mfss2IkUWYPr9wqQg963ZQFbcg5D6oN9Up6Mww==} + '@solid-primitives/resize-observer@2.1.3': + resolution: {integrity: sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/resize-observer@2.1.1': - resolution: {integrity: sha512-vb/VS9+YdUdVZ2V92JimFmFuaJ2MSyKOGnUay/mQvoQ0R+mtdT7FSylfQlVslCzm0ecx8Jkvsm1Sk2lopvMAdg==} - peerDependencies: - solid-js: ^1.6.12 - - '@solid-primitives/rootless@1.5.1': - resolution: {integrity: sha512-G4eNC6F3ufRT2Mjbodl7rSOH7uq/Emqs3S7/BIBWgh+V/IFUtvu6WELeqSrk4FJX3T/kKKvC+T8gXhepExSWyg==} + '@solid-primitives/rootless@1.5.2': + resolution: {integrity: sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ==} peerDependencies: solid-js: ^1.6.12 @@ -2798,13 +2802,18 @@ packages: peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/static-store@0.1.1': - resolution: {integrity: sha512-daXWvpLjd+4hbYdGaaEJ2kKFuFhshvfIBFLveW7mfk2BWHl9lGQVwUuExp3qllkK9ONA9p+5D2cpwBQosv8odQ==} + '@solid-primitives/scheduled@1.5.2': + resolution: {integrity: sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/styles@0.1.1': - resolution: {integrity: sha512-eOf3GQjxEcYWxUU62CSpTIXOOzF5FBMdiJl/yBb20Dq6h/VVWCABHDPsh1KJ3SKc4AUAimSMbclDG+Co0EBpvQ==} + '@solid-primitives/static-store@0.1.2': + resolution: {integrity: sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/styles@0.1.2': + resolution: {integrity: sha512-7iX5K+J5b1PRrbgw3Ki92uvU2LgQ0Kd/QMsrAZxDg5dpUBwMyTijZkA3bbs1ikZsT1oQhS41bTyKbjrXeU0Awg==} peerDependencies: solid-js: ^1.6.12 @@ -2813,6 +2822,11 @@ packages: peerDependencies: solid-js: ^1.6.12 + '@solid-primitives/utils@6.3.2': + resolution: {integrity: sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==} + peerDependencies: + solid-js: ^1.6.12 + '@solidjs/meta@0.29.4': resolution: {integrity: sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g==} peerDependencies: @@ -3138,6 +3152,9 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -3147,8 +3164,8 @@ packages: '@types/node@22.10.1': resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} - '@types/node@24.0.1': - resolution: {integrity: sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==} + '@types/node@24.0.10': + resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} '@types/react-dom@19.1.6': resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} @@ -3294,6 +3311,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + adm-zip@0.5.10: resolution: {integrity: sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==} engines: {node: '>=6.0'} @@ -3455,11 +3477,11 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} brace-expansion@4.0.1: resolution: {integrity: sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==} @@ -3474,6 +3496,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -3521,6 +3548,9 @@ packages: caniuse-lite@1.0.30001721: resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} + caniuse-lite@1.0.30001726: + resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} + capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -3764,8 +3794,8 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - cssstyle@4.3.1: - resolution: {integrity: sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} csstype@3.1.3: @@ -4000,6 +4030,9 @@ packages: electron-to-chromium@1.5.165: resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==} + electron-to-chromium@1.5.179: + resolution: {integrity: sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==} + embla-carousel-react@8.6.0: resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} peerDependencies: @@ -4045,6 +4078,10 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -4053,8 +4090,8 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@6.0.0: - resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} error-ex@1.3.2: @@ -4094,8 +4131,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - es-toolkit@1.39.3: - resolution: {integrity: sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww==} + es-toolkit@1.39.6: + resolution: {integrity: sha512-uiVjnLem6kkfXumlwUEWEKnwUN5QbSEB0DHy2rNJt0nkYcob5K0TXJ7oJRzhAcvx+SRmz4TahKyN5V9cly/IPA==} esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} @@ -4232,8 +4269,8 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + form-data@4.0.3: + resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} engines: {node: '>= 6'} formdata-polyfill@4.0.10: @@ -5852,6 +5889,10 @@ packages: resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} engines: {node: '>= 0.4'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -6153,6 +6194,11 @@ packages: engines: {node: '>=10'} hasBin: true + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + engines: {node: '>=10'} + hasBin: true + thingies@1.21.0: resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} engines: {node: '>=10.18'} @@ -6259,8 +6305,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.20.2: - resolution: {integrity: sha512-He0ZWr41gLa4vD30Au3yuwpe0HXaCZbclvl8RBieUiJ9aFnPMWUPIyvw3RU8+1Crjfcrauvitae2a4tUzRAGsw==} + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -6310,8 +6356,8 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ultracite@4.2.10: - resolution: {integrity: sha512-4CWIocizBZrUydXT8Ty+jD3Ma/Mz7xHlggVeAVYCb1IzsG/4WjCxpNNfUSryLE4UNaHoAGDFD/NljR2G4u3ZTw==} + ultracite@4.2.13: + resolution: {integrity: sha512-j49R1z3xXIPhdvU19x0z0Z4hNewJYn4F1h42ULeaCOylBuxwGVE401piPxe3aVapwue7+Ec3J6wnL/+mW4zwww==} hasBin: true unbox-primitive@1.1.0: @@ -6505,8 +6551,8 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} - webpack-sources@3.3.2: - resolution: {integrity: sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==} + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: @@ -6615,6 +6661,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -7375,9 +7433,9 @@ snapshots: '@floating-ui/utils@0.2.9': {} - '@graphiql/plugin-doc-explorer@0.2.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))': + '@graphiql/plugin-doc-explorer@0.2.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))': dependencies: - '@graphiql/react': 0.34.1(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + '@graphiql/react': 0.34.1(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) '@headlessui/react': 2.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) graphql: 16.11.0 react: 19.1.0 @@ -7394,10 +7452,10 @@ snapshots: - immer - use-sync-external-store - '@graphiql/plugin-history@0.2.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))': + '@graphiql/plugin-history@0.2.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))': dependencies: - '@graphiql/react': 0.34.1(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) - '@graphiql/toolkit': 0.11.3(@types/node@24.0.1)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0) + '@graphiql/react': 0.34.1(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + '@graphiql/toolkit': 0.11.3(@types/node@24.0.10)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0) react: 19.1.0 react-compiler-runtime: 19.1.0-rc.1(react@19.1.0) react-dom: 19.1.0(react@19.1.0) @@ -7413,9 +7471,9 @@ snapshots: - immer - use-sync-external-store - '@graphiql/react@0.34.1(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))': + '@graphiql/react@0.34.1(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))': dependencies: - '@graphiql/toolkit': 0.11.3(@types/node@24.0.1)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0) + '@graphiql/toolkit': 0.11.3(@types/node@24.0.10)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0) '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-dropdown-menu': 2.1.15(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-tooltip': 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -7445,11 +7503,11 @@ snapshots: - immer - use-sync-external-store - '@graphiql/toolkit@0.11.3(@types/node@24.0.1)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)': + '@graphiql/toolkit@0.11.3(@types/node@24.0.10)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 graphql: 16.11.0 - meros: 1.3.0(@types/node@24.0.1) + meros: 1.3.0(@types/node@24.0.10) optionalDependencies: graphql-ws: 6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)) transitivePeerDependencies: @@ -7461,7 +7519,7 @@ snapshots: graphql: 16.11.0 tslib: 2.6.3 - '@graphql-codegen/cli@5.0.7(@parcel/watcher@2.5.1)(@types/node@24.0.1)(bufferutil@4.0.9)(encoding@0.1.13)(enquirer@2.4.1)(graphql@16.11.0)(typescript@5.8.3)(utf-8-validate@6.0.5)': + '@graphql-codegen/cli@5.0.7(@parcel/watcher@2.5.1)(@types/node@24.0.10)(bufferutil@4.0.9)(encoding@0.1.13)(enquirer@2.4.1)(graphql@16.11.0)(typescript@5.8.3)(utf-8-validate@6.0.5)': dependencies: '@babel/generator': 7.27.1 '@babel/template': 7.27.2 @@ -7472,12 +7530,12 @@ snapshots: '@graphql-tools/apollo-engine-loader': 8.0.20(graphql@16.11.0) '@graphql-tools/code-file-loader': 8.1.20(graphql@16.11.0) '@graphql-tools/git-loader': 8.0.24(graphql@16.11.0) - '@graphql-tools/github-loader': 8.0.20(@types/node@24.0.1)(graphql@16.11.0) + '@graphql-tools/github-loader': 8.0.20(@types/node@24.0.10)(graphql@16.11.0) '@graphql-tools/graphql-file-loader': 8.0.19(graphql@16.11.0) '@graphql-tools/json-file-loader': 8.0.18(graphql@16.11.0) '@graphql-tools/load': 8.1.0(graphql@16.11.0) - '@graphql-tools/prisma-loader': 8.0.17(@types/node@24.0.1)(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.11.0)(utf-8-validate@6.0.5) - '@graphql-tools/url-loader': 8.0.31(@types/node@24.0.1)(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5) + '@graphql-tools/prisma-loader': 8.0.17(@types/node@24.0.10)(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.11.0)(utf-8-validate@6.0.5) + '@graphql-tools/url-loader': 8.0.31(@types/node@24.0.10)(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5) '@graphql-tools/utils': 10.8.6(graphql@16.11.0) '@whatwg-node/fetch': 0.10.6 chalk: 4.1.2 @@ -7485,7 +7543,7 @@ snapshots: debounce: 1.2.1 detect-indent: 6.1.0 graphql: 16.11.0 - graphql-config: 5.1.5(@types/node@24.0.1)(bufferutil@4.0.9)(graphql@16.11.0)(typescript@5.8.3)(utf-8-validate@6.0.5) + graphql-config: 5.1.5(@types/node@24.0.10)(bufferutil@4.0.9)(graphql@16.11.0)(typescript@5.8.3)(utf-8-validate@6.0.5) inquirer: 8.2.6 is-glob: 4.0.3 jiti: 1.21.7 @@ -7698,7 +7756,7 @@ snapshots: - uWebSockets.js - utf-8-validate - '@graphql-tools/executor-http@1.3.3(@types/node@24.0.1)(graphql@16.11.0)': + '@graphql-tools/executor-http@1.3.3(@types/node@24.0.10)(graphql@16.11.0)': dependencies: '@graphql-hive/signal': 1.0.0 '@graphql-tools/executor-common': 0.0.4(graphql@16.11.0) @@ -7708,7 +7766,7 @@ snapshots: '@whatwg-node/fetch': 0.10.6 '@whatwg-node/promise-helpers': 1.3.1 graphql: 16.11.0 - meros: 1.3.0(@types/node@24.0.1) + meros: 1.3.0(@types/node@24.0.10) tslib: 2.8.1 transitivePeerDependencies: - '@types/node' @@ -7747,9 +7805,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@graphql-tools/github-loader@8.0.20(@types/node@24.0.1)(graphql@16.11.0)': + '@graphql-tools/github-loader@8.0.20(@types/node@24.0.10)(graphql@16.11.0)': dependencies: - '@graphql-tools/executor-http': 1.3.3(@types/node@24.0.1)(graphql@16.11.0) + '@graphql-tools/executor-http': 1.3.3(@types/node@24.0.10)(graphql@16.11.0) '@graphql-tools/graphql-tag-pluck': 8.3.19(graphql@16.11.0) '@graphql-tools/utils': 10.8.6(graphql@16.11.0) '@whatwg-node/fetch': 0.10.6 @@ -7817,9 +7875,9 @@ snapshots: graphql: 16.11.0 tslib: 2.8.1 - '@graphql-tools/prisma-loader@8.0.17(@types/node@24.0.1)(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.11.0)(utf-8-validate@6.0.5)': + '@graphql-tools/prisma-loader@8.0.17(@types/node@24.0.10)(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.11.0)(utf-8-validate@6.0.5)': dependencies: - '@graphql-tools/url-loader': 8.0.31(@types/node@24.0.1)(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5) + '@graphql-tools/url-loader': 8.0.31(@types/node@24.0.10)(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5) '@graphql-tools/utils': 10.8.6(graphql@16.11.0) '@types/js-yaml': 4.0.9 '@whatwg-node/fetch': 0.10.6 @@ -7861,10 +7919,10 @@ snapshots: graphql: 16.11.0 tslib: 2.8.1 - '@graphql-tools/url-loader@8.0.31(@types/node@24.0.1)(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5)': + '@graphql-tools/url-loader@8.0.31(@types/node@24.0.10)(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5)': dependencies: '@graphql-tools/executor-graphql-ws': 2.0.5(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5) - '@graphql-tools/executor-http': 1.3.3(@types/node@24.0.1)(graphql@16.11.0) + '@graphql-tools/executor-http': 1.3.3(@types/node@24.0.10)(graphql@16.11.0) '@graphql-tools/executor-legacy-ws': 1.1.17(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5) '@graphql-tools/utils': 10.8.6(graphql@16.11.0) '@graphql-tools/wrap': 10.0.35(graphql@16.11.0) @@ -8014,6 +8072,12 @@ snapshots: dependencies: minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + optional: true + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -8024,19 +8088,28 @@ snapshots: '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.10': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 optional: true '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.4': + optional: true + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + optional: true + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -9250,73 +9323,72 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@solid-devtools/debugger@0.27.0(solid-js@1.9.7)': + '@solid-devtools/debugger@0.28.1(solid-js@1.9.7)': dependencies: '@nothing-but/utils': 0.17.0 - '@solid-devtools/shared': 0.19.1(solid-js@1.9.7) - '@solid-primitives/bounds': 0.1.1(solid-js@1.9.7) - '@solid-primitives/event-listener': 2.4.1(solid-js@1.9.7) - '@solid-primitives/keyboard': 1.3.1(solid-js@1.9.7) - '@solid-primitives/platform': 0.2.1(solid-js@1.9.7) - '@solid-primitives/rootless': 1.5.1(solid-js@1.9.7) - '@solid-primitives/scheduled': 1.5.1(solid-js@1.9.7) - '@solid-primitives/static-store': 0.1.1(solid-js@1.9.7) - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-devtools/shared': 0.20.0(solid-js@1.9.7) + '@solid-primitives/bounds': 0.1.3(solid-js@1.9.7) + '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.7) + '@solid-primitives/keyboard': 1.3.3(solid-js@1.9.7) + '@solid-primitives/rootless': 1.5.2(solid-js@1.9.7) + '@solid-primitives/scheduled': 1.5.2(solid-js@1.9.7) + '@solid-primitives/static-store': 0.1.2(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true - '@solid-devtools/logger@0.9.8(solid-js@1.9.7)': + '@solid-devtools/logger@0.9.11(solid-js@1.9.7)': dependencies: '@nothing-but/utils': 0.17.0 - '@solid-devtools/debugger': 0.27.0(solid-js@1.9.7) - '@solid-devtools/shared': 0.19.1(solid-js@1.9.7) - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-devtools/debugger': 0.28.1(solid-js@1.9.7) + '@solid-devtools/shared': 0.20.0(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true - '@solid-devtools/shared@0.19.1(solid-js@1.9.7)': + '@solid-devtools/shared@0.20.0(solid-js@1.9.7)': dependencies: '@nothing-but/utils': 0.17.0 - '@solid-primitives/event-listener': 2.4.1(solid-js@1.9.7) - '@solid-primitives/media': 2.3.1(solid-js@1.9.7) - '@solid-primitives/refs': 1.1.1(solid-js@1.9.7) - '@solid-primitives/rootless': 1.5.1(solid-js@1.9.7) - '@solid-primitives/scheduled': 1.5.1(solid-js@1.9.7) - '@solid-primitives/static-store': 0.1.1(solid-js@1.9.7) - '@solid-primitives/styles': 0.1.1(solid-js@1.9.7) - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.7) + '@solid-primitives/media': 2.3.3(solid-js@1.9.7) + '@solid-primitives/refs': 1.1.2(solid-js@1.9.7) + '@solid-primitives/rootless': 1.5.2(solid-js@1.9.7) + '@solid-primitives/scheduled': 1.5.2(solid-js@1.9.7) + '@solid-primitives/static-store': 0.1.2(solid-js@1.9.7) + '@solid-primitives/styles': 0.1.2(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true - '@solid-primitives/bounds@0.1.1(solid-js@1.9.7)': + '@solid-primitives/bounds@0.1.3(solid-js@1.9.7)': dependencies: - '@solid-primitives/event-listener': 2.4.1(solid-js@1.9.7) - '@solid-primitives/resize-observer': 2.1.1(solid-js@1.9.7) - '@solid-primitives/static-store': 0.1.1(solid-js@1.9.7) - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.7) + '@solid-primitives/resize-observer': 2.1.3(solid-js@1.9.7) + '@solid-primitives/static-store': 0.1.2(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true - '@solid-primitives/event-listener@2.4.1(solid-js@1.9.7)': + '@solid-primitives/event-listener@2.4.3(solid-js@1.9.7)': dependencies: - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true - '@solid-primitives/keyboard@1.3.1(solid-js@1.9.7)': + '@solid-primitives/keyboard@1.3.3(solid-js@1.9.7)': dependencies: - '@solid-primitives/event-listener': 2.4.1(solid-js@1.9.7) - '@solid-primitives/rootless': 1.5.1(solid-js@1.9.7) - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.7) + '@solid-primitives/rootless': 1.5.2(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true - '@solid-primitives/media@2.3.1(solid-js@1.9.7)': + '@solid-primitives/media@2.3.3(solid-js@1.9.7)': dependencies: - '@solid-primitives/event-listener': 2.4.1(solid-js@1.9.7) - '@solid-primitives/rootless': 1.5.1(solid-js@1.9.7) - '@solid-primitives/static-store': 0.1.1(solid-js@1.9.7) - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.7) + '@solid-primitives/rootless': 1.5.2(solid-js@1.9.7) + '@solid-primitives/static-store': 0.1.2(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true @@ -9326,29 +9398,24 @@ snapshots: '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) solid-js: 1.9.7 - '@solid-primitives/platform@0.2.1(solid-js@1.9.7)': + '@solid-primitives/refs@1.1.2(solid-js@1.9.7)': dependencies: + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true - '@solid-primitives/refs@1.1.1(solid-js@1.9.7)': + '@solid-primitives/resize-observer@2.1.3(solid-js@1.9.7)': dependencies: - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.7) + '@solid-primitives/rootless': 1.5.2(solid-js@1.9.7) + '@solid-primitives/static-store': 0.1.2(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true - '@solid-primitives/resize-observer@2.1.1(solid-js@1.9.7)': + '@solid-primitives/rootless@1.5.2(solid-js@1.9.7)': dependencies: - '@solid-primitives/event-listener': 2.4.1(solid-js@1.9.7) - '@solid-primitives/rootless': 1.5.1(solid-js@1.9.7) - '@solid-primitives/static-store': 0.1.1(solid-js@1.9.7) - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) - solid-js: 1.9.7 - optional: true - - '@solid-primitives/rootless@1.5.1(solid-js@1.9.7)': - dependencies: - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true @@ -9356,16 +9423,21 @@ snapshots: dependencies: solid-js: 1.9.7 - '@solid-primitives/static-store@0.1.1(solid-js@1.9.7)': + '@solid-primitives/scheduled@1.5.2(solid-js@1.9.7)': dependencies: - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) solid-js: 1.9.7 optional: true - '@solid-primitives/styles@0.1.1(solid-js@1.9.7)': + '@solid-primitives/static-store@0.1.2(solid-js@1.9.7)': dependencies: - '@solid-primitives/rootless': 1.5.1(solid-js@1.9.7) - '@solid-primitives/utils': 6.3.1(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) + solid-js: 1.9.7 + optional: true + + '@solid-primitives/styles@0.1.2(solid-js@1.9.7)': + dependencies: + '@solid-primitives/rootless': 1.5.2(solid-js@1.9.7) + '@solid-primitives/utils': 6.3.2(solid-js@1.9.7) solid-js: 1.9.7 optional: true @@ -9373,6 +9445,11 @@ snapshots: dependencies: solid-js: 1.9.7 + '@solid-primitives/utils@6.3.2(solid-js@1.9.7)': + dependencies: + solid-js: 1.9.7 + optional: true + '@solidjs/meta@0.29.4(solid-js@1.9.7)': dependencies: solid-js: 1.9.7 @@ -9575,12 +9652,12 @@ snapshots: prettier: 3.5.3 recast: 0.23.11 source-map: 0.7.4 - tsx: 4.20.2 + tsx: 4.20.3 zod: 3.24.4 transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.121.4(@rsbuild/core@1.3.22)(@tanstack/react-router@1.121.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0))(webpack@5.97.1)': + '@tanstack/router-plugin@1.121.4(@rsbuild/core@1.3.22)(@tanstack/react-router@1.121.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))(webpack@5.97.1)': dependencies: '@babel/core': 7.27.1 '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.27.1) @@ -9599,7 +9676,7 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.3.22 '@tanstack/react-router': 1.121.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - vite: 5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) + vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1) webpack: 5.97.1 transitivePeerDependencies: - supports-color @@ -9617,8 +9694,8 @@ snapshots: '@tanstack/solid-router@1.112.12(solid-js@1.9.7)': dependencies: - '@solid-devtools/logger': 0.9.8(solid-js@1.9.7) - '@solid-primitives/refs': 1.1.1(solid-js@1.9.7) + '@solid-devtools/logger': 0.9.11(solid-js@1.9.7) + '@solid-primitives/refs': 1.1.2(solid-js@1.9.7) '@solidjs/meta': 0.29.4(solid-js@1.9.7) '@tanstack/history': 1.112.8 '@tanstack/router-core': 1.112.12 @@ -9680,7 +9757,7 @@ snapshots: '@types/cors@2.8.17': dependencies: - '@types/node': 24.0.1 + '@types/node': 24.0.10 '@types/d3-array@3.2.1': {} @@ -9711,12 +9788,12 @@ snapshots: '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 optional: true '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 optional: true @@ -9724,6 +9801,9 @@ snapshots: '@types/estree@1.0.7': {} + '@types/estree@1.0.8': + optional: true + '@types/js-yaml@4.0.9': {} '@types/json-schema@7.0.15': @@ -9733,7 +9813,7 @@ snapshots: dependencies: undici-types: 6.20.0 - '@types/node@24.0.1': + '@types/node@24.0.10': dependencies: undici-types: 7.8.0 @@ -9759,9 +9839,9 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.0.1 + '@types/node': 24.0.10 - '@vitejs/plugin-react@4.5.2(vite@5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0))': + '@vitejs/plugin-react@4.5.2(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) @@ -9769,7 +9849,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) + vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) transitivePeerDependencies: - supports-color @@ -9781,13 +9861,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(vite@5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0))': + '@vitest/mocker@3.2.3(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) + vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) '@vitest/pretty-format@3.2.3': dependencies: @@ -9956,6 +10036,9 @@ snapshots: acorn@8.14.1: {} + acorn@8.15.0: + optional: true + adm-zip@0.5.10: {} agent-base@7.1.3: {} @@ -10122,12 +10205,12 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@1.1.11: + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -10146,6 +10229,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.0) + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001726 + electron-to-chromium: 1.5.179 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + optional: true + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -10196,6 +10287,9 @@ snapshots: caniuse-lite@1.0.30001721: {} + caniuse-lite@1.0.30001726: + optional: true + capital-case@1.0.4: dependencies: no-case: 3.0.4 @@ -10468,7 +10562,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - cssstyle@4.3.1: + cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 @@ -10664,6 +10758,9 @@ snapshots: electron-to-chromium@1.5.165: {} + electron-to-chromium@1.5.179: + optional: true + embla-carousel-react@8.6.0(react@19.1.0): dependencies: embla-carousel: 8.6.0 @@ -10697,7 +10794,7 @@ snapshots: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.17 - '@types/node': 24.0.1 + '@types/node': 24.0.10 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -10715,6 +10812,12 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 + enhanced-resolve@5.18.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + optional: true + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -10723,7 +10826,7 @@ snapshots: entities@4.5.0: {} - entities@6.0.0: + entities@6.0.1: optional: true error-ex@1.3.2: @@ -10819,7 +10922,7 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.39.3: {} + es-toolkit@1.39.6: {} esbuild@0.21.5: optionalDependencies: @@ -11038,11 +11141,12 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.2: + form-data@4.0.3: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 optional: true @@ -11170,11 +11274,11 @@ snapshots: graceful-readlink@1.0.1: {} - graphiql@4.1.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): + graphiql@4.1.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): dependencies: - '@graphiql/plugin-doc-explorer': 0.2.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) - '@graphiql/plugin-history': 0.2.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) - '@graphiql/react': 0.34.1(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + '@graphiql/plugin-doc-explorer': 0.2.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + '@graphiql/plugin-history': 0.2.2(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + '@graphiql/react': 0.34.1(@codemirror/language@6.11.1)(@emotion/is-prop-valid@0.8.8)(@types/node@24.0.10)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) graphql: 16.11.0 react: 19.1.0 react-compiler-runtime: 19.1.0-rc.1(react@19.1.0) @@ -11189,13 +11293,13 @@ snapshots: - immer - use-sync-external-store - graphql-config@5.1.5(@types/node@24.0.1)(bufferutil@4.0.9)(graphql@16.11.0)(typescript@5.8.3)(utf-8-validate@6.0.5): + graphql-config@5.1.5(@types/node@24.0.10)(bufferutil@4.0.9)(graphql@16.11.0)(typescript@5.8.3)(utf-8-validate@6.0.5): dependencies: '@graphql-tools/graphql-file-loader': 8.0.19(graphql@16.11.0) '@graphql-tools/json-file-loader': 8.0.18(graphql@16.11.0) '@graphql-tools/load': 8.1.0(graphql@16.11.0) '@graphql-tools/merge': 9.0.24(graphql@16.11.0) - '@graphql-tools/url-loader': 8.0.31(@types/node@24.0.1)(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5) + '@graphql-tools/url-loader': 8.0.31(@types/node@24.0.10)(bufferutil@4.0.9)(graphql@16.11.0)(utf-8-validate@6.0.5) '@graphql-tools/utils': 10.8.6(graphql@16.11.0) cosmiconfig: 8.3.6(typescript@5.8.3) graphql: 16.11.0 @@ -11572,7 +11676,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.0.1 + '@types/node': 24.0.10 merge-stream: 2.0.0 supports-color: 8.1.1 optional: true @@ -11604,10 +11708,10 @@ snapshots: jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5): dependencies: - cssstyle: 4.3.1 + cssstyle: 4.6.0 data-urls: 5.0.0 decimal.js: 10.5.0 - form-data: 4.0.2 + form-data: 4.0.3 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -11623,7 +11727,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -11837,9 +11941,9 @@ snapshots: merge2@1.4.1: {} - meros@1.3.0(@types/node@24.0.1): + meros@1.3.0(@types/node@24.0.10): optionalDependencies: - '@types/node': 24.0.1 + '@types/node': 24.0.10 methods@1.1.2: {} @@ -11872,11 +11976,11 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minipass@7.1.2: {} @@ -12006,7 +12110,7 @@ snapshots: minimatch: 3.1.2 pidtree: 0.3.1 read-pkg: 3.0.0 - shell-quote: 1.8.2 + shell-quote: 1.8.3 string.prototype.padend: 3.1.6 nullthrows@1.1.1: {} @@ -12133,7 +12237,7 @@ snapshots: parse5@7.3.0: dependencies: - entities: 6.0.0 + entities: 6.0.1 optional: true parseley@0.12.1: @@ -12752,6 +12856,8 @@ snapshots: shell-quote@1.8.2: {} + shell-quote@1.8.3: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -13068,18 +13174,26 @@ snapshots: terser-webpack-plugin@5.3.14(webpack@5.97.1): dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.29 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.41.0 + terser: 5.43.1 webpack: 5.97.1 optional: true terser@5.41.0: dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + '@jridgewell/source-map': 0.3.10 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + optional: true + + terser@5.43.1: + dependencies: + '@jridgewell/source-map': 0.3.10 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 optional: true @@ -13174,7 +13288,7 @@ snapshots: tslib@2.8.1: {} - tsx@4.20.2: + tsx@4.20.3: dependencies: esbuild: 0.25.5 get-tsconfig: 4.10.1 @@ -13233,7 +13347,7 @@ snapshots: uc.micro@2.1.0: {} - ultracite@4.2.10: + ultracite@4.2.13: dependencies: commander: 14.0.0 @@ -13269,6 +13383,13 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + optional: true + upper-case-first@2.0.2: dependencies: tslib: 2.8.1 @@ -13345,13 +13466,13 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-node@3.2.3(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0): + vite-node@3.2.3(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) + vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) transitivePeerDependencies: - '@types/node' - less @@ -13363,23 +13484,36 @@ snapshots: - supports-color - terser - vite@5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0): + vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0): dependencies: esbuild: 0.21.5 postcss: 8.5.5 rollup: 4.29.1 optionalDependencies: - '@types/node': 24.0.1 + '@types/node': 24.0.10 fsevents: 2.3.3 lightningcss: 1.30.1 sass: 1.77.4 terser: 5.41.0 - vitest@3.2.3(@types/node@24.0.1)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0): + vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.5 + rollup: 4.29.1 + optionalDependencies: + '@types/node': 24.0.10 + fsevents: 2.3.3 + lightningcss: 1.30.1 + sass: 1.77.4 + terser: 5.43.1 + optional: true + + vitest@3.2.3(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(vite@5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0)) + '@vitest/mocker': 3.2.3(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0)) '@vitest/pretty-format': 3.2.3 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 @@ -13397,11 +13531,11 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.0 tinyrainbow: 2.0.0 - vite: 5.4.11(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) - vite-node: 3.2.3(@types/node@24.0.1)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) + vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) + vite-node: 3.2.3(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.41.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.0.1 + '@types/node': 24.0.10 jsdom: 25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) transitivePeerDependencies: - less @@ -13440,7 +13574,7 @@ snapshots: webidl-conversions@7.0.0: optional: true - webpack-sources@3.3.2: + webpack-sources@3.3.3: optional: true webpack-virtual-modules@0.6.2: {} @@ -13448,14 +13582,14 @@ snapshots: webpack@5.97.1: dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.1 - browserslist: 4.25.0 + acorn: 8.15.0 + browserslist: 4.25.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.2 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -13469,7 +13603,7 @@ snapshots: tapable: 2.2.2 terser-webpack-plugin: 5.3.14(webpack@5.97.1) watchpack: 2.4.4 - webpack-sources: 3.3.2 + webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild @@ -13621,6 +13755,12 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 6.0.5 + ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 6.0.5 + optional: true + xml-name-validator@5.0.0: optional: true