diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..864b864 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +target/ +painter-js/node_modules/ +painter-js/dist/ +painter-js/.env +snapshots/ +.git/ +.claude/ +.env +*.DS_Store diff --git a/.env.example b/.env.example index ef918a9..7510bde 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,13 @@ ADDRESS=0.0.0.0 PORT=3000 -ENABLE_CORS=true \ No newline at end of file +ENABLE_CORS=true +CANVAS_WIDTH=500 +CANVAS_HEIGHT=500 +COOLDOWN_SECS=10 +RATE_LIMIT_BURST=10 +RATE_LIMIT_PER_SECOND=10 +BROADCAST_CAPACITY=1024 +SNAPSHOT_ENABLED=true +SNAPSHOT_INTERVAL_SECS=300 +SNAPSHOT_MAX=5 +SNAPSHOT_DIR=snapshots/ diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..c651be6 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: ["*"] + pull_request: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - uses: oven-sh/setup-bun@v2 + + - name: Build frontend + run: cd painter-js && bun install --frozen-lockfile && bun run build + + - name: Check formatting + run: | + cargo fmt --all -- --check + cd painter-js && bun run fmt:check + + - name: Clippy + run: cargo clippy --workspace -- -D warnings + + - name: Tests + run: cargo test --workspace diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..127aca6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + push: + branches: ["*"] + tags: ["v*"] + pull_request: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - uses: oven-sh/setup-bun@v2 + + - name: Build frontend + run: cd painter-js && bun install --frozen-lockfile && bun run build + + - name: Check formatting + run: | + cargo fmt --all -- --check + cd painter-js && bun run fmt:check + + - name: Clippy + run: cargo clippy --workspace -- -D warnings + + - name: Tests + run: cargo test --workspace + + docker: + needs: ci + runs-on: ubuntu-latest + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v5 + id: meta + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 0b745e2..bf2d1b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target -.env \ No newline at end of file +.env +/snapshots +*.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9f43e9f..2970fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,35 +1,38 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] -name = "addr2line" -version = "0.21.0" +name = "allocator-api2" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "api-types" +version = "1.0.0" dependencies = [ - "gimli", + "domain", + "serde", ] [[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +name = "application" +version = "1.0.0" dependencies = [ - "libc", + "domain", + "thiserror 2.0.20", + "tokio", + "tracing", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", ] [[package]] @@ -40,9 +43,15 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.63", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -51,13 +60,14 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.7.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ - "async-trait", "axum-core", + "base64", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", @@ -65,18 +75,19 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sha1", + "sync_wrapper", "tokio", + "tokio-tungstenite", "tower", "tower-layer", "tower-service", @@ -85,55 +96,29 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ - "async-trait", "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", - "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "backtrace" -version = "0.3.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bitflags" version = "2.5.0" @@ -149,32 +134,38 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" -version = "1.6.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] [[package]] -name = "cc" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +name = "canvas-file" +version = "1.0.0" +dependencies = [ + "config", + "domain", + "tracing", +] [[package]] name = "cfg-if" @@ -183,24 +174,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "chrono" -version = "0.4.38" +name = "chacha20" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets 0.52.5", + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] -name = "core-foundation-sys" -version = "0.8.6" +name = "config" +version = "1.0.0" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "config-env" +version = "1.0.0" +dependencies = [ + "config", +] + +[[package]] +name = "const-oid" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "cpufeatures" @@ -211,6 +213,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -228,13 +239,23 @@ dependencies = [ ] [[package]] -name = "dashmap" -version = "5.5.3" +name = "crypto-common" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", - "hashbrown", + "crossbeam-utils", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -246,14 +267,64 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "domain" +version = "1.0.0" +dependencies = [ + "thiserror 2.0.20", ] [[package]] @@ -264,12 +335,12 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "engineioxide" -version = "0.13.1" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76e79b0c318bbff227b003843c56d5fc50b43276441d162124166f4cbacc80" +checksum = "1c48884059941b2b6255eabe1e9dca48e1aa01242299a7ef60844ded113b1d50" dependencies = [ - "base64", "bytes", + "engineioxide-core", "futures-core", "futures-util", "http", @@ -278,22 +349,54 @@ dependencies = [ "hyper", "hyper-util", "pin-project-lite", - "rand", "serde", "serde_json", "smallvec", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-tungstenite", - "tower", + "tokio-util", + "tower-layer", + "tower-service", ] +[[package]] +name = "engineioxide-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c5f98b2d2338ec0713bc222c753bfd18746bd76cbb115afa18aa07c6096553" +dependencies = [ + "base64", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "rand 0.10.2", + "serde", + "serde_json", + "smallvec", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -310,14 +413,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" dependencies = [ "nonempty", - "thiserror", + "thiserror 1.0.60", ] [[package]] name = "futures" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -330,9 +433,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -340,15 +443,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -357,32 +460,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" @@ -392,9 +495,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -404,7 +507,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -420,9 +522,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -430,29 +532,71 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.28.1" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] [[package]] name = "governor" -version = "0.6.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" dependencies = [ "cfg-if", "dashmap", - "futures", + "futures-sink", "futures-timer", - "no-std-compat", + "futures-util", + "getrandom 0.3.4", + "hashbrown 0.16.1", "nonzero_ext", "parking_lot", "portable-atomic", "quanta", - "rand", + "rand 0.9.5", "smallvec", "spinning_top", + "web-time", +] + +[[package]] +name = "h2" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -462,22 +606,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "http" -version = "1.1.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", - "fnv", "itoa", ] +[[package]] +name = "http-axum" +version = "1.0.0" +dependencies = [ + "axum", + "config", + "rust-embed", + "tokio", + "tower-http", + "tower_governor", +] + [[package]] name = "http-body" version = "1.0.0" @@ -501,17 +667,11 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" - [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -520,14 +680,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] -name = "hyper" -version = "1.3.1" +name = "hybrid-array" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", + "h2", "http", "http-body", "httparse", @@ -536,55 +707,50 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", + "libc", "pin-project-lite", "socket2", "tokio", + "tower-service", + "tracing", ] [[package]] -name = "iana-time-zone" -version = "0.1.60" +name = "indexmap" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", + "equivalent", + "hashbrown 0.17.1", ] [[package]] @@ -595,10 +761,12 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -610,9 +778,18 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.154" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] [[package]] name = "lock_api" @@ -632,9 +809,15 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "matchit" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8863b587001c1b9a8a4e36008cebc6b3612cb1226fe2de94858e06092687b608" [[package]] name = "memchr" @@ -642,16 +825,6 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" -[[package]] -name = "memory-stats" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34f79cf9964c5c9545493acda1263f1912f8d2c56c8a2ffee2606cb960acaacc" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "mime" version = "0.3.17" @@ -660,40 +833,25 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", ] -[[package]] -name = "miniz_oxide" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" -dependencies = [ - "adler", -] - [[package]] name = "mio" -version = "0.8.11" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nonempty" version = "0.7.0" @@ -716,66 +874,24 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "painter" -version = "0.1.0" -dependencies = [ - "axum", - "bincode", - "bytes", - "chrono", - "dotenv", - "memory-stats", - "serde", - "serde_json", - "socketioxide", - "tokio", - "tower-http", - "tower_governor", - "tracing", - "tracing-subscriber", -] - [[package]] name = "parking_lot" version = "0.12.2" @@ -796,7 +912,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets", ] [[package]] @@ -822,7 +938,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.63", ] [[package]] @@ -831,12 +947,6 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "portable-atomic" version = "1.6.0" @@ -851,9 +961,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -883,35 +993,63 @@ dependencies = [ ] [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "libc", "rand_chacha", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "raw-cpuid" version = "11.0.2" @@ -931,10 +1069,52 @@ dependencies = [ ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "redox_users" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "rust-embed" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.63", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +dependencies = [ + "mime_guess", + "sha2", + "walkdir", +] [[package]] name = "rustversion" @@ -948,6 +1128,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -956,22 +1145,32 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.201" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -1007,6 +1206,26 @@ dependencies = [ "serde", ] +[[package]] +name = "server" +version = "1.0.0" +dependencies = [ + "application", + "axum", + "canvas-file", + "config", + "config-env", + "domain", + "dotenv", + "http-axum", + "socketio", + "socketioxide", + "tokio", + "tracing", + "tracing-subscriber", + "websocket", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1014,8 +1233,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.12", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -1027,6 +1257,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -1047,25 +1286,41 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] [[package]] name = "socket2" -version = "0.5.7" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", +] + +[[package]] +name = "socketio" +version = "1.0.0" +dependencies = [ + "api-types", + "application", + "domain", + "serde_json", + "socketioxide", + "tokio", + "tracing", ] [[package]] name = "socketioxide" -version = "0.13.1" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "178953184593e92c3f58d0d3eda74865c520b6fc05f80dc6c99bb235e39f400a" +checksum = "8b4ccb119c03dd29d44ad7de4dd7049ec0812d1aa05bdca5cc6f1cb5210d4f96" dependencies = [ "bytes", "engineioxide", @@ -1074,13 +1329,43 @@ dependencies = [ "http", "http-body", "hyper", - "itoa", + "matchit 0.9.2", "pin-project-lite", "serde", - "serde_json", - "thiserror", + "socketioxide-core", + "socketioxide-parser-common", + "thiserror 2.0.20", "tokio", - "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "socketioxide-core" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e520fedd1addc257506026a31268d9a6eeee79b158c7fc8b270c4c7a1cfc4f" +dependencies = [ + "arbitrary", + "bytes", + "engineioxide-core", + "futures-core", + "serde", + "smallvec", + "thiserror 2.0.20", +] + +[[package]] +name = "socketioxide-parser-common" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba9a856b0de7f665fe0d66fb7d60c74ad2e1810552c60a31f72e9aa6372cc02" +dependencies = [ + "bytes", + "itoa", + "serde", + "serde_json", + "socketioxide-core", ] [[package]] @@ -1104,16 +1389,21 @@ dependencies = [ ] [[package]] -name = "sync_wrapper" -version = "0.1.2" +name = "syn" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "thiserror" @@ -1121,7 +1411,16 @@ version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.60", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", ] [[package]] @@ -1132,7 +1431,18 @@ checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.63", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -1145,56 +1455,50 @@ dependencies = [ "once_cell", ] -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.37.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ - "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", ] [[package]] name = "tokio-tungstenite" -version = "0.21.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", @@ -1204,51 +1508,59 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] [[package]] -name = "tower" -version = "0.4.13" +name = "tonic" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ - "futures-core", - "futures-util", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", "pin-project", - "pin-project-lite", + "socket2", + "sync_wrapper", "tokio", + "tokio-stream", + "tower", "tower-layer", "tower-service", "tracing", ] [[package]] -name = "tower-http" -version = "0.5.2" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ - "bitflags", - "bytes", + "futures-core", "futures-util", - "http", - "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", + "indexmap", "pin-project-lite", + "slab", + "sync_wrapper", "tokio", "tokio-util", "tower-layer", @@ -1257,29 +1569,45 @@ dependencies = [ ] [[package]] -name = "tower-layer" -version = "0.3.2" +name = "tower-http" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" +dependencies = [ + "bitflags", + "bytes", + "http", + "percent-encoding", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tower_governor" -version = "0.4.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313fa625fea5790ed56360a30ea980e41229cf482b4835801a67ef1922bf63b9" +checksum = "44de9b94d849d3c46e06a883d72d408c2de6403367b39df2b1c9d9e7b6736fe6" dependencies = [ "axum", "forwarded-header-value", "governor", "http", "pin-project", - "thiserror", + "thiserror 2.0.20", + "tonic", "tower", "tracing", ] @@ -1304,7 +1632,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.63", ] [[package]] @@ -1343,22 +1671,25 @@ dependencies = [ ] [[package]] -name = "tungstenite" -version = "0.21.0" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" dependencies = [ - "byteorder", "bytes", "data-encoding", "http", "httparse", "log", - "rand", + "rand 0.9.5", "sha1", - "thiserror", - "url", - "utf-8", + "thiserror 2.0.20", ] [[package]] @@ -1369,18 +1700,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.7.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" @@ -1388,32 +1710,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "url" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "valuable" version = "0.1.0" @@ -1426,6 +1722,25 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1433,35 +1748,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.92" +name = "wasip2" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" +name = "wasm-bindgen" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ - "bumpalo", - "log", + "cfg-if", "once_cell", - "proc-macro2", - "quote", - "syn", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1469,22 +1781,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 2.0.63", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" @@ -1496,6 +1811,30 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "websocket" +version = "1.0.0" +dependencies = [ + "application", + "axum", + "domain", + "futures", + "serde", + "serde_json", + "tokio", + "tracing", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1512,6 +1851,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1519,45 +1867,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.5", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.5", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link", ] [[package]] @@ -1566,46 +1887,28 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.5" @@ -1618,50 +1921,32 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" diff --git a/Cargo.toml b/Cargo.toml index d0f737a..ed17265 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,60 @@ -[package] -name = "painter" -version = "0.1.0" -edition = "2021" +[workspace] +members = [ + "crates/config", + "crates/domain", + "crates/application", + "crates/api-types", + "crates/adapters/canvas-file", + "crates/adapters/config-env", + "crates/adapters/http-axum", + "crates/adapters/socketio", + "crates/adapters/websocket", + "crates/server", +] +default-members = [ + "crates/config", + "crates/domain", + "crates/application", + "crates/api-types", + "crates/adapters/canvas-file", + "crates/adapters/config-env", + "crates/adapters/http-axum", + "crates/server", +] +resolver = "2" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace.package] +version = "1.0.0" +edition = "2024" -[dependencies] -axum = "0.7.5" -bincode = "1.3.3" -bytes = "1.6.0" -chrono = "0.4.38" +[workspace.dependencies] +config = { path = "crates/config" } +domain = { path = "crates/domain" } +application = { path = "crates/application" } +api-types = { path = "crates/api-types" } +canvas-file = { path = "crates/adapters/canvas-file" } +config-env = { path = "crates/adapters/config-env" } +http-axum = { path = "crates/adapters/http-axum" } +socketio = { path = "crates/adapters/socketio" } +websocket = { path = "crates/adapters/websocket" } +futures = "0.3" + +axum = "0.8.9" dotenv = "0.15.0" -memory-stats = "1.1.0" -serde = { version = "1.0.201", features = ["derive"] } -serde_json = "1.0.117" -socketioxide = "0.13.1" -tokio = { version = "1.37.0", features = ["full"] } -tower-http = { version = "0.5.2", features = ["cors", "fs"] } -tower_governor = "0.4.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +socketioxide = "0.18.6" +thiserror = "2" +tokio = { version = "1.37", features = ["full"] } +rust-embed = { version = "8", features = ["interpolate-folder-path", "mime-guess"] } +tower-http = { version = "0.7.0", features = ["cors"] } +tower_governor = "0.8.0" tracing = "0.1.40" tracing-subscriber = "0.3.18" + +[profile.release] +lto = true +opt-level = "z" +codegen-units = 1 +panic = "abort" +strip = true diff --git a/Dockerfile b/Dockerfile index 779af35..ee97062 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,24 @@ -FROM rust:1.76 as builder +FROM oven/bun:1 AS frontend +WORKDIR /app/painter-js +COPY painter-js/package.json painter-js/bun.lock ./ +RUN bun install --frozen-lockfile +COPY painter-js/ . +RUN bun run build + +FROM rust:1-alpine AS builder WORKDIR /app +RUN apk add --no-cache musl-dev COPY Cargo.toml Cargo.lock ./ +COPY crates/ ./crates/ -COPY src ./src +RUN mkdir -p painter-js/dist && echo '' > painter-js/dist/index.html +RUN cargo build --release --bin server 2>&1 || true -RUN cargo build --release - -FROM rust:1.76 -WORKDIR /app - -COPY --from=builder /app/target/release/painter . -COPY painter-js/dist ./dist -COPY .env .env +COPY --from=frontend /app/painter-js/dist ./painter-js/dist +RUN touch crates/adapters/http-axum/src/routes.rs && cargo build --release --bin server +FROM scratch +COPY --from=builder /app/target/release/server /painter EXPOSE 3000 - -CMD ["./painter"] \ No newline at end of file +ENTRYPOINT ["/painter"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..31bafd2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Gabriel Kaszewski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c84216c --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: build dev check check-all test fmt run clean fix + +build: + cargo build --release + +dev: + RUST_LOG=debug cargo run + +check: + cargo fmt --all -- --check + cargo clippy -- -D warnings + cargo test + +check-all: + cargo fmt --all -- --check + cargo clippy --workspace -- -D warnings + cargo test --workspace + +test: + cargo test + +fmt: + cargo fmt --all + +run: + cargo run --release + +fix: + cargo fmt --all + cargo clippy --fix --allow-dirty --allow-staged + +clean: + cargo clean diff --git a/README.md b/README.md index 797d3a2..9152de9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,121 @@ -# painter +# Painter -Web app inspired by r/place +A collaborative pixel canvas inspired by r/place. Users connect in real-time and place colored pixels on a shared 500x500 canvas. + +## Quick Start + +```bash +# Install frontend dependencies and build +cd painter-js && bun install && bun run build && cd .. + +# Run the server (serves embedded frontend) +cargo run --release +``` + +Open `http://localhost:3000` in your browser. + +## Configuration + +All configuration is via environment variables (or `.env` file): + +| Variable | Default | Description | +|---|---|---| +| `ADDRESS` | `0.0.0.0` | Bind address | +| `PORT` | `3000` | Bind port | +| `ENABLE_CORS` | `true` | Enable CORS headers | +| `CANVAS_WIDTH` | `500` | Canvas width in pixels | +| `CANVAS_HEIGHT` | `500` | Canvas height in pixels | +| `COOLDOWN_SECS` | `10` | Seconds between pixel placements per user | +| `RATE_LIMIT_BURST` | `10` | HTTP rate limit burst size | +| `RATE_LIMIT_PER_SECOND` | `10` | HTTP rate limit per second | +| `BROADCAST_CAPACITY` | `1024` | Broadcast channel buffer size | +| `SNAPSHOT_ENABLED` | `true` | Enable periodic canvas snapshots | +| `SNAPSHOT_INTERVAL_SECS` | `300` | Seconds between snapshots | +| `SNAPSHOT_MAX` | `5` | Maximum snapshot files to keep | +| `SNAPSHOT_DIR` | `snapshots/` | Snapshot storage directory | + +## Transport Adapters + +The server supports two real-time transport protocols, selected at compile time: + +```bash +# Socket.IO (default) +cargo run --release + +# Native WebSocket +cargo run --release --no-default-features --features websocket +``` + +The frontend auto-detects via `VITE_TRANSPORT` environment variable (`socketio` or `websocket`). + +## Docker + +```bash +# Build +docker build -t painter . + +# Run +docker run -p 3000:3000 painter + +# With custom config +docker run -p 3000:3000 \ + -e CANVAS_WIDTH=1000 \ + -e CANVAS_HEIGHT=1000 \ + -e COOLDOWN_SECS=30 \ + -v ./snapshots:/app/snapshots \ + painter +``` + +The Docker image is a single statically-linked binary on `scratch` — **~3MB** total, no OS, no runtime dependencies. The frontend is embedded at compile time. + +The server shuts down gracefully on SIGTERM/SIGINT (Ctrl+C) — in-flight connections are drained and a final canvas snapshot is saved before exit. + +## Development + +```bash +# Backend (watches for changes) +RUST_LOG=debug cargo run + +# Frontend (Vite dev server with HMR) +cd painter-js +VITE_IS_DEBUG=true bun run dev + +# Run all checks (fmt + clippy + tests) +make check + +# Format +make fmt +cd painter-js && bun run fmt +``` + +## Architecture + +Hexagonal architecture with clean dependency boundaries. See [architecture.mmd](architecture.mmd) for the full diagram. + +``` +crates/ + config/ Config structs, ConfigSource trait + domain/ Canvas, value objects, port traits, events + application/ Use cases, AppState, InProcessBroadcaster + api-types/ Shared DTOs, event constants + adapters/ + config-env/ Environment variable config loader + canvas-file/ File-based snapshot persistence + http-axum/ HTTP routes, rate limiting, embedded static files + socketio/ Socket.IO transport adapter + websocket/ Native WebSocket transport adapter + server/ Composition root +``` + +All infrastructure is behind port traits: + +| Port | Adapter | Swappable to | +|---|---|---| +| `CanvasStore` | In-memory | Redis, mmap'd file | +| `CanvasPersistence` | File system | S3, SQLite | +| `EventBroadcaster` | tokio broadcast | NATS, Redis pub/sub | +| `ConfigSource` | Env vars | TOML, JSON, remote API | + +## License + +[MIT](LICENSE) diff --git a/architecture.mmd b/architecture.mmd new file mode 100644 index 0000000..9bd5ca5 --- /dev/null +++ b/architecture.mmd @@ -0,0 +1,83 @@ +graph TB + subgraph Binary ["Server (Composition Root)"] + server["server
wires adapters, starts Axum"] + end + + subgraph Application ["Application Layer"] + AppState["AppState"] + PlacePixel["canvas::place_pixel"] + GetState["canvas::get_state"] + SaveSnapshot["canvas::save_snapshot"] + RestoreSnapshot["canvas::restore_snapshot"] + Connect["soldiers::connect"] + Disconnect["soldiers::disconnect"] + InMemoryCanvas["InMemoryCanvasStore"] + InProcessBroadcast["InProcessBroadcaster"] + end + + subgraph Domain ["Domain Layer (0 infra deps)"] + Canvas["Canvas"] + ValueObjects["Color | Position | PixelUpdate"] + BroadcastEvent["BroadcastEvent"] + DomainError["DomainError"] + + subgraph Ports ["Port Traits"] + CanvasStore["CanvasStore"] + CanvasPersistence["CanvasPersistence"] + EventBroadcaster["EventBroadcaster"] + end + end + + subgraph Shared ["Shared Types"] + Config["config
AppConfig, ConfigSource trait"] + ApiTypes["api-types
PixelUpdatePayload, event constants"] + end + + subgraph Adapters ["Adapters"] + subgraph Transport ["Transport (driving)"] + SocketIO["socketio
Socket.IO protocol"] + WebSocket["websocket
native WebSocket + JSON"] + end + + subgraph Infrastructure ["Infrastructure (driven)"] + ConfigEnv["config-env
env var loader"] + CanvasFile["canvas-file
file snapshots"] + HttpAxum["http-axum
HTTP routes, rate limit,
embedded static files
"] + end + end + + server --> Application + server --> Transport + server --> Infrastructure + server --> Config + + PlacePixel --> CanvasStore + PlacePixel --> EventBroadcaster + SaveSnapshot --> CanvasStore + SaveSnapshot --> CanvasPersistence + RestoreSnapshot --> CanvasStore + RestoreSnapshot --> CanvasPersistence + Connect --> EventBroadcaster + Disconnect --> EventBroadcaster + + InMemoryCanvas -.->|implements| CanvasStore + InProcessBroadcast -.->|implements| EventBroadcaster + CanvasFile -.->|implements| CanvasPersistence + ConfigEnv -.->|implements| Config + + SocketIO --> Application + WebSocket --> Application + SocketIO --> ApiTypes + HttpAxum --> Config + + classDef domain fill:#e8f5e9,stroke:#2e7d32 + classDef app fill:#e3f2fd,stroke:#1565c0 + classDef adapter fill:#fff3e0,stroke:#e65100 + classDef shared fill:#f3e5f5,stroke:#6a1b9a + classDef binary fill:#fce4ec,stroke:#c62828 + + class Canvas,ValueObjects,BroadcastEvent,DomainError,CanvasStore,CanvasPersistence,EventBroadcaster domain + class AppState,PlacePixel,GetState,SaveSnapshot,RestoreSnapshot,Connect,Disconnect,InMemoryCanvas,InProcessBroadcast app + class SocketIO,WebSocket,ConfigEnv,CanvasFile,HttpAxum adapter + class Config,ApiTypes shared + class server binary diff --git a/crates/adapters/canvas-file/Cargo.toml b/crates/adapters/canvas-file/Cargo.toml new file mode 100644 index 0000000..bb97dee --- /dev/null +++ b/crates/adapters/canvas-file/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "canvas-file" +version.workspace = true +edition.workspace = true + +[dependencies] +config = { workspace = true } +domain = { workspace = true } +tracing = { workspace = true } diff --git a/crates/adapters/canvas-file/src/lib.rs b/crates/adapters/canvas-file/src/lib.rs new file mode 100644 index 0000000..6091f33 --- /dev/null +++ b/crates/adapters/canvas-file/src/lib.rs @@ -0,0 +1,158 @@ +use std::fs; +use std::io::{BufWriter, Write}; +use std::path::{Path, PathBuf}; + +use config::SnapshotConfig; +use domain::ports::CanvasPersistence; +use domain::{Color, DomainError}; +use tracing::{error, info}; + +pub struct FileCanvasPersistence { + directory: PathBuf, + max_snapshots: usize, +} + +impl FileCanvasPersistence { + pub fn new(config: &SnapshotConfig) -> Result { + let directory = PathBuf::from(&config.directory); + fs::create_dir_all(&directory).map_err(|err| { + DomainError::Persistence(format!( + "Failed to create snapshot directory '{}': {err}", + directory.display() + )) + })?; + + Ok(Self { + directory, + max_snapshots: config.max_snapshots, + }) + } + + fn snapshot_path(&self, index: usize) -> PathBuf { + self.directory.join(format!("canvas_{index}.bin")) + } + + fn latest_index_path(&self) -> PathBuf { + self.directory.join("latest") + } + + fn read_latest_index(&self) -> Option { + let path = self.latest_index_path(); + fs::read_to_string(&path) + .ok() + .and_then(|content| content.trim().parse().ok()) + } + + fn write_latest_index(&self, index: usize) -> Result<(), DomainError> { + let path = self.latest_index_path(); + fs::write(&path, index.to_string()) + .map_err(|err| DomainError::Persistence(format!("Failed to write latest index: {err}"))) + } + + fn rotate_and_cleanup(&self, new_index: usize) { + if self.max_snapshots == 0 { + return; + } + let oldest_to_keep = new_index.saturating_sub(self.max_snapshots - 1); + for stale_index in 0..oldest_to_keep { + let stale_path = self.snapshot_path(stale_index); + if stale_path.exists() + && let Err(err) = fs::remove_file(&stale_path) + { + error!( + "Failed to remove old snapshot '{}': {err}", + stale_path.display() + ); + } + } + } +} + +impl CanvasPersistence for FileCanvasPersistence { + fn save(&self, pixels: &[Color]) -> Result<(), DomainError> { + let next_index = self.read_latest_index().map(|i| i + 1).unwrap_or(0); + let path = self.snapshot_path(next_index); + + write_pixels_to_file(&path, pixels)?; + self.write_latest_index(next_index)?; + self.rotate_and_cleanup(next_index); + + info!( + "Saved canvas snapshot #{next_index} to '{}'", + path.display() + ); + Ok(()) + } + + fn load_latest(&self) -> Result>, DomainError> { + let Some(index) = self.read_latest_index() else { + info!("No canvas snapshots found"); + return Ok(None); + }; + + let path = self.snapshot_path(index); + if !path.exists() { + info!("Snapshot file '{}' not found", path.display()); + return Ok(None); + } + + let pixels = read_pixels_from_file(&path)?; + info!( + "Loaded canvas snapshot #{index} from '{}' ({} pixels)", + path.display(), + pixels.len() + ); + Ok(Some(pixels)) + } +} + +fn write_pixels_to_file(path: &Path, pixels: &[Color]) -> Result<(), DomainError> { + let file = fs::File::create(path).map_err(|err| { + DomainError::Persistence(format!( + "Failed to create snapshot '{}': {err}", + path.display() + )) + })?; + let mut writer = BufWriter::new(file); + for color in pixels { + writer + .write_all(&color.as_u32().to_ne_bytes()) + .map_err(|err| { + DomainError::Persistence(format!( + "Failed to write snapshot '{}': {err}", + path.display() + )) + })?; + } + writer.flush().map_err(|err| { + DomainError::Persistence(format!( + "Failed to flush snapshot '{}': {err}", + path.display() + )) + })?; + Ok(()) +} + +fn read_pixels_from_file(path: &Path) -> Result, DomainError> { + let bytes = fs::read(path).map_err(|err| { + DomainError::Persistence(format!( + "Failed to read snapshot '{}': {err}", + path.display() + )) + })?; + + if bytes.len() % 4 != 0 { + return Err(DomainError::Persistence(format!( + "Snapshot '{}' has invalid size: {} bytes (not a multiple of 4)", + path.display(), + bytes.len() + ))); + } + + let pixels = bytes + .chunks_exact(4) + .map(|chunk| Color::new(u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))) + .collect(); + + Ok(pixels) +} diff --git a/crates/adapters/config-env/Cargo.toml b/crates/adapters/config-env/Cargo.toml new file mode 100644 index 0000000..20e4ec2 --- /dev/null +++ b/crates/adapters/config-env/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "config-env" +version.workspace = true +edition.workspace = true + +[dependencies] +config = { workspace = true } diff --git a/crates/adapters/config-env/src/lib.rs b/crates/adapters/config-env/src/lib.rs new file mode 100644 index 0000000..239d67c --- /dev/null +++ b/crates/adapters/config-env/src/lib.rs @@ -0,0 +1,70 @@ +use config::{ + AppConfig, BroadcastConfig, CanvasConfig, ConfigError, ConfigSource, CooldownConfig, + RateLimitConfig, ServerConfig, SnapshotConfig, +}; + +const DEFAULT_ADDRESS: &str = "0.0.0.0"; +const DEFAULT_PORT: u16 = 3000; +const DEFAULT_CANVAS_WIDTH: u32 = 500; +const DEFAULT_CANVAS_HEIGHT: u32 = 500; +const DEFAULT_COOLDOWN_SECS: u64 = 10; +const DEFAULT_RATE_LIMIT_BURST: u32 = 10; +const DEFAULT_RATE_LIMIT_PER_SECOND: u64 = 10; +const DEFAULT_BROADCAST_CAPACITY: usize = 1024; +const DEFAULT_SNAPSHOT_INTERVAL_SECS: u64 = 300; +const DEFAULT_SNAPSHOT_MAX: usize = 5; +const DEFAULT_SNAPSHOT_DIR: &str = "snapshots/"; + +pub struct EnvConfigSource; + +impl ConfigSource for EnvConfigSource { + fn load(&self) -> Result { + Ok(AppConfig { + server: ServerConfig { + address: env_or("ADDRESS", DEFAULT_ADDRESS), + port: parse_env("PORT", DEFAULT_PORT)?, + enable_cors: parse_bool_env("ENABLE_CORS", true), + }, + canvas: CanvasConfig { + width: parse_env("CANVAS_WIDTH", DEFAULT_CANVAS_WIDTH)?, + height: parse_env("CANVAS_HEIGHT", DEFAULT_CANVAS_HEIGHT)?, + }, + cooldown: CooldownConfig { + placement_secs: parse_env("COOLDOWN_SECS", DEFAULT_COOLDOWN_SECS)?, + }, + rate_limit: RateLimitConfig { + burst_size: parse_env("RATE_LIMIT_BURST", DEFAULT_RATE_LIMIT_BURST)?, + per_second: parse_env("RATE_LIMIT_PER_SECOND", DEFAULT_RATE_LIMIT_PER_SECOND)?, + }, + broadcast: BroadcastConfig { + channel_capacity: parse_env("BROADCAST_CAPACITY", DEFAULT_BROADCAST_CAPACITY)?, + }, + snapshot: SnapshotConfig { + enabled: parse_bool_env("SNAPSHOT_ENABLED", true), + interval_secs: parse_env("SNAPSHOT_INTERVAL_SECS", DEFAULT_SNAPSHOT_INTERVAL_SECS)?, + max_snapshots: parse_env("SNAPSHOT_MAX", DEFAULT_SNAPSHOT_MAX)?, + directory: env_or("SNAPSHOT_DIR", DEFAULT_SNAPSHOT_DIR), + }, + }) + } +} + +fn env_or(key: &str, default: &str) -> String { + std::env::var(key).unwrap_or_else(|_| default.to_string()) +} + +fn parse_bool_env(key: &str, default: bool) -> bool { + std::env::var(key) + .map(|value| value == "true") + .unwrap_or(default) +} + +fn parse_env(key: &str, default: T) -> Result { + match std::env::var(key) { + Ok(value) => value.parse().map_err(|_| ConfigError::InvalidValue { + field: key.to_string(), + reason: format!("'{value}' is not a valid {}", std::any::type_name::()), + }), + Err(_) => Ok(default), + } +} diff --git a/crates/adapters/http-axum/Cargo.toml b/crates/adapters/http-axum/Cargo.toml new file mode 100644 index 0000000..314d9d9 --- /dev/null +++ b/crates/adapters/http-axum/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "http-axum" +version.workspace = true +edition.workspace = true + +[dependencies] +config = { workspace = true } +axum = { workspace = true } +rust-embed = { workspace = true } +tokio = { workspace = true } +tower-http = { workspace = true } +tower_governor = { workspace = true } diff --git a/crates/adapters/http-axum/src/lib.rs b/crates/adapters/http-axum/src/lib.rs new file mode 100644 index 0000000..ff03cc3 --- /dev/null +++ b/crates/adapters/http-axum/src/lib.rs @@ -0,0 +1,3 @@ +mod routes; + +pub use routes::build_router; diff --git a/crates/adapters/http-axum/src/routes.rs b/crates/adapters/http-axum/src/routes.rs new file mode 100644 index 0000000..915581d --- /dev/null +++ b/crates/adapters/http-axum/src/routes.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use axum::http::{StatusCode, header}; +use axum::response::{IntoResponse, Response}; +use axum::{Router, routing::get}; +use config::RateLimitConfig; +use rust_embed::Embed; +use tower_governor::{GovernorLayer, governor::GovernorConfigBuilder}; +use tower_http::cors::{Any, CorsLayer}; + +const RATE_LIMIT_CLEANUP_INTERVAL_SECS: u64 = 1; + +#[derive(Embed)] +#[folder = "$CARGO_MANIFEST_DIR/../../../painter-js/dist/"] +struct StaticAssets; + +async fn serve_static(path: axum::extract::Path) -> Response { + let path = path.0; + serve_embedded_file(&path) +} + +async fn serve_index() -> Response { + serve_embedded_file("index.html") +} + +fn serve_embedded_file(path: &str) -> Response { + match StaticAssets::get(path) { + Some(file) => { + let content_type = file.metadata.mimetype(); + ( + StatusCode::OK, + [(header::CONTENT_TYPE, content_type.to_string())], + file.data, + ) + .into_response() + } + None => serve_embedded_file("index.html"), + } +} + +pub fn build_router( + enable_cors: bool, + rate_limit_config: &RateLimitConfig, +) -> Result { + let rate_governor = Arc::new( + GovernorConfigBuilder::default() + .burst_size(rate_limit_config.burst_size) + .per_second(rate_limit_config.per_second) + .finish() + .ok_or("Invalid rate limit configuration")?, + ); + + let governor = rate_governor.limiter().clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs( + RATE_LIMIT_CLEANUP_INTERVAL_SECS, + )) + .await; + governor.retain_recent(); + } + }); + + let router = Router::new() + .route("/check/", get(|| async { "OK" })) + .route("/{*path}", get(serve_static)) + .fallback(get(serve_index)) + .layer(GovernorLayer::new(rate_governor)); + + Ok(if enable_cors { + router.layer( + CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any), + ) + } else { + router + }) +} diff --git a/crates/adapters/socketio/Cargo.toml b/crates/adapters/socketio/Cargo.toml new file mode 100644 index 0000000..411167b --- /dev/null +++ b/crates/adapters/socketio/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "socketio" +version.workspace = true +edition.workspace = true + +[dependencies] +application = { workspace = true } +api-types = { workspace = true } +domain = { workspace = true } +serde_json = { workspace = true } +socketioxide = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/adapters/socketio/src/handlers.rs b/crates/adapters/socketio/src/handlers.rs new file mode 100644 index 0000000..8d0515e --- /dev/null +++ b/crates/adapters/socketio/src/handlers.rs @@ -0,0 +1,109 @@ +use std::sync::Arc; + +use api_types::{self, PixelUpdatePayload}; +use application::AppState; +use application::canvas::place_pixel; +use domain::{BroadcastEvent, BroadcastSubscription, Color, Position}; +use socketioxide::extract::{Data, SocketRef}; +use tracing::info; + +pub async fn on_connect(socket: SocketRef, state: Arc) { + info!("Socket connected: {:?} {:?}", socket.ns(), socket.id); + + let subscription = state.broadcaster().subscribe(); + + send_canvas_state(&socket, &state); + register_soldier(&state, &socket); + spawn_broadcast_forwarder(socket.clone(), subscription); + register_place_pixel_handler(&socket, state.clone()); + register_disconnect_handler(&socket, state); +} + +fn send_canvas_state(socket: &SocketRef, state: &AppState) { + let canvas_pixels = application::canvas::get_state::execute(state); + let pixel_values = Color::collect_as_u32(&canvas_pixels); + socket + .emit(api_types::events::CANVAS_STATE, &pixel_values) + .ok(); +} + +fn register_soldier(state: &AppState, socket: &SocketRef) { + let socket_id = socket.id.to_string(); + application::soldiers::connect::execute(state, socket_id); +} + +fn spawn_broadcast_forwarder(socket: SocketRef, mut subscription: BroadcastSubscription) { + tokio::spawn(async move { + while let Some(event) = subscription.recv().await { + if forward_broadcast_event(&socket, &event).is_err() { + break; + } + } + }); +} + +fn forward_broadcast_event(socket: &SocketRef, event: &BroadcastEvent) -> Result<(), ()> { + match event { + BroadcastEvent::PixelUpdated(update) => { + let payload = PixelUpdatePayload::from(*update); + socket + .emit(api_types::events::PIXEL_UPDATED, &payload) + .map_err(|_| ()) + } + BroadcastEvent::SoldierCountChanged(count) => socket + .emit(api_types::events::CURRENT_SOLDIERS, count) + .map_err(|_| ()), + } +} + +fn register_place_pixel_handler(socket: &SocketRef, state: Arc) { + socket.on( + api_types::events::PLACE_PIXEL, + move |socket: SocketRef, Data::(payload)| async move { + handle_place_pixel(&socket, &state, payload); + }, + ); +} + +fn handle_place_pixel(socket: &SocketRef, state: &AppState, payload: PixelUpdatePayload) { + let position = Position::new(payload.x, payload.y); + let color = Color::new(payload.color); + + info!("Received pixel update: {position} color={}", color.as_u32()); + + let socket_id = socket.id.to_string(); + let command = place_pixel::Command { + user_id: &socket_id, + position, + color, + }; + + match place_pixel::execute(state, command) { + Ok(place_pixel::Outcome::Placed(_)) => {} + Ok(place_pixel::Outcome::CooldownActive) => { + emit_error(socket, domain::COOLDOWN_MESSAGE); + } + Err(err) => { + emit_error(socket, &err.to_string()); + } + } +} + +fn register_disconnect_handler(socket: &SocketRef, state: Arc) { + socket.on_disconnect(move |socket: SocketRef| async move { + handle_disconnect(&socket, &state); + }); +} + +fn handle_disconnect(socket: &SocketRef, state: &AppState) { + info!("Socket disconnected: {:?}", socket.id); + let socket_id = socket.id.to_string(); + application::soldiers::disconnect::execute(state, &socket_id); +} + +fn emit_error(socket: &SocketRef, message: &str) { + let _ = socket.emit( + api_types::events::ERROR, + &serde_json::Value::String(message.to_string()), + ); +} diff --git a/crates/adapters/socketio/src/lib.rs b/crates/adapters/socketio/src/lib.rs new file mode 100644 index 0000000..bb9d321 --- /dev/null +++ b/crates/adapters/socketio/src/lib.rs @@ -0,0 +1,12 @@ +mod handlers; + +use std::sync::Arc; + +use application::AppState; +use socketioxide::{SocketIo, extract::SocketRef}; + +pub fn setup_namespaces(io: &SocketIo, state: Arc) { + io.ns("/", move |socket: SocketRef| async move { + handlers::on_connect(socket, state).await; + }); +} diff --git a/crates/adapters/websocket/Cargo.toml b/crates/adapters/websocket/Cargo.toml new file mode 100644 index 0000000..445a798 --- /dev/null +++ b/crates/adapters/websocket/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "websocket" +version.workspace = true +edition.workspace = true + +[dependencies] +application = { workspace = true } +domain = { workspace = true } +axum = { workspace = true, features = ["ws"] } +futures = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/adapters/websocket/src/handler.rs b/crates/adapters/websocket/src/handler.rs new file mode 100644 index 0000000..2e247cd --- /dev/null +++ b/crates/adapters/websocket/src/handler.rs @@ -0,0 +1,163 @@ +use std::sync::Arc; +use std::sync::atomic::Ordering; + +use axum::extract::State; +use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; +use axum::response::IntoResponse; +use domain::{BroadcastEvent, BroadcastSubscription, Color, Position}; +use futures::{SinkExt, StreamExt, stream::SplitSink}; +use tokio::sync::mpsc; +use tracing::{error, info}; + +use crate::WsState; +use crate::messages::{ClientMessage, ServerMessage}; +use application::AppState; +use application::canvas::place_pixel; + +type WsSender = SplitSink; + +pub async fn ws_upgrade( + ws: WebSocketUpgrade, + State(state): State>, +) -> impl IntoResponse { + ws.on_upgrade(|socket| handle_connection(socket, state)) +} + +async fn handle_connection(socket: WebSocket, state: Arc) { + let (mut sender, mut receiver) = socket.split(); + + let connection_id = state + .connection_counter + .fetch_add(1, Ordering::Relaxed) + .to_string(); + + info!("WebSocket connected: {connection_id}"); + + // Subscribe before snapshotting to avoid missing updates + let subscription = state.app_state.broadcaster().subscribe(); + + if !send_canvas_snapshot(&mut sender, &state.app_state).await { + return; + } + + application::soldiers::connect::execute(&state.app_state, connection_id.clone()); + + let (error_sender, error_receiver) = mpsc::unbounded_channel::(); + + let mut send_task = tokio::spawn(run_send_loop(sender, subscription, error_receiver)); + + let app_state = state.app_state.clone(); + let recv_connection_id = connection_id.clone(); + let mut recv_task = tokio::spawn(async move { + while let Some(Ok(message)) = receiver.next().await { + if let Message::Text(text) = message { + handle_client_message(&app_state, &error_sender, &recv_connection_id, &text); + } else if let Message::Close(_) = message { + break; + } + } + }); + + tokio::select! { + _ = &mut send_task => recv_task.abort(), + _ = &mut recv_task => send_task.abort(), + } + + info!("WebSocket disconnected: {connection_id}"); + application::soldiers::disconnect::execute(&state.app_state, &connection_id); +} + +async fn send_canvas_snapshot(sender: &mut WsSender, state: &AppState) -> bool { + let pixels = application::canvas::get_state::execute(state); + let bytes = Color::collect_as_bytes(&pixels); + sender.send(Message::Binary(bytes.into())).await.is_ok() +} + +async fn run_send_loop( + mut sender: WsSender, + mut subscription: BroadcastSubscription, + mut error_receiver: mpsc::UnboundedReceiver, +) { + loop { + tokio::select! { + Some(event) = subscription.recv() => { + let Some(json) = serialize_broadcast_event(&event) else { continue }; + if sender.send(Message::Text(json.into())).await.is_err() { + break; + } + } + Some(error_json) = error_receiver.recv() => { + if sender.send(Message::Text(error_json.into())).await.is_err() { + break; + } + } + else => break, + } + } +} + +fn serialize_broadcast_event(event: &BroadcastEvent) -> Option { + let message = match event { + BroadcastEvent::PixelUpdated(update) => ServerMessage::from(*update), + BroadcastEvent::SoldierCountChanged(count) => { + ServerMessage::CurrentSoldiers { count: *count } + } + }; + serde_json::to_string(&message) + .inspect_err(|err| error!("Failed to serialize broadcast event: {err}")) + .ok() +} + +fn handle_client_message( + state: &AppState, + error_sender: &mpsc::UnboundedSender, + connection_id: &str, + text: &str, +) { + let Ok(message) = serde_json::from_str::(text) else { + return; + }; + + match message { + ClientMessage::PlacePixel { x, y, color } => { + handle_place_pixel(state, error_sender, connection_id, x, y, color); + } + } +} + +fn handle_place_pixel( + state: &AppState, + error_sender: &mpsc::UnboundedSender, + connection_id: &str, + x: u32, + y: u32, + color: u32, +) { + let command = place_pixel::Command { + user_id: connection_id, + position: Position::new(x, y), + color: Color::new(color), + }; + + match place_pixel::execute(state, command) { + Ok(place_pixel::Outcome::Placed(_)) => {} + Ok(place_pixel::Outcome::CooldownActive) => { + send_error(error_sender, domain::COOLDOWN_MESSAGE); + } + Err(err) => { + send_error(error_sender, &err.to_string()); + } + } +} + +fn send_error(error_sender: &mpsc::UnboundedSender, message: &str) { + let error_message = ServerMessage::Error { + message: message.to_string(), + }; + match serde_json::to_string(&error_message) { + Ok(json) => { + let _ = error_sender.send(json); + } + Err(err) => error!("Failed to serialize error message: {err}"), + } +} diff --git a/crates/adapters/websocket/src/lib.rs b/crates/adapters/websocket/src/lib.rs new file mode 100644 index 0000000..f193514 --- /dev/null +++ b/crates/adapters/websocket/src/lib.rs @@ -0,0 +1,24 @@ +mod handler; +mod messages; + +use std::sync::Arc; +use std::sync::atomic::AtomicU64; + +use application::AppState; +use axum::{Router, routing::get}; + +pub(crate) struct WsState { + app_state: Arc, + connection_counter: AtomicU64, +} + +pub fn build_router(state: Arc) -> Router { + let ws_state = Arc::new(WsState { + app_state: state, + connection_counter: AtomicU64::new(0), + }); + + Router::new() + .route("/ws", get(handler::ws_upgrade)) + .with_state(ws_state) +} diff --git a/crates/adapters/websocket/src/messages.rs b/crates/adapters/websocket/src/messages.rs new file mode 100644 index 0000000..c9aa32b --- /dev/null +++ b/crates/adapters/websocket/src/messages.rs @@ -0,0 +1,30 @@ +use domain::PixelUpdate; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +#[serde(tag = "type")] +pub enum ClientMessage { + #[serde(rename = "place-pixel")] + PlacePixel { x: u32, y: u32, color: u32 }, +} + +#[derive(Serialize)] +#[serde(tag = "type")] +pub enum ServerMessage { + #[serde(rename = "pixel-updated")] + PixelUpdated { x: u32, y: u32, color: u32 }, + #[serde(rename = "current_soldiers")] + CurrentSoldiers { count: usize }, + #[serde(rename = "error")] + Error { message: String }, +} + +impl From for ServerMessage { + fn from(update: PixelUpdate) -> Self { + Self::PixelUpdated { + x: update.position().x(), + y: update.position().y(), + color: update.color().as_u32(), + } + } +} diff --git a/crates/api-types/Cargo.toml b/crates/api-types/Cargo.toml new file mode 100644 index 0000000..2d80631 --- /dev/null +++ b/crates/api-types/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "api-types" +version.workspace = true +edition.workspace = true + +[dependencies] +domain = { workspace = true } +serde = { workspace = true } diff --git a/crates/api-types/src/lib.rs b/crates/api-types/src/lib.rs new file mode 100644 index 0000000..9474198 --- /dev/null +++ b/crates/api-types/src/lib.rs @@ -0,0 +1,27 @@ +use domain::PixelUpdate; +use serde::{Deserialize, Serialize}; + +pub mod events { + pub const CANVAS_STATE: &str = "canvas_state"; + pub const PIXEL_UPDATED: &str = "pixel-updated"; + pub const PLACE_PIXEL: &str = "place-pixel"; + pub const CURRENT_SOLDIERS: &str = "current_soldiers"; + pub const ERROR: &str = "error"; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PixelUpdatePayload { + pub x: u32, + pub y: u32, + pub color: u32, +} + +impl From for PixelUpdatePayload { + fn from(update: PixelUpdate) -> Self { + Self { + x: update.position().x(), + y: update.position().y(), + color: update.color().as_u32(), + } + } +} diff --git a/crates/application/Cargo.toml b/crates/application/Cargo.toml new file mode 100644 index 0000000..7156e61 --- /dev/null +++ b/crates/application/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "application" +version.workspace = true +edition.workspace = true + +[dependencies] +domain = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/application/src/canvas/get_state.rs b/crates/application/src/canvas/get_state.rs new file mode 100644 index 0000000..6d14c08 --- /dev/null +++ b/crates/application/src/canvas/get_state.rs @@ -0,0 +1,9 @@ +use std::sync::Arc; + +use domain::Color; + +use crate::AppState; + +pub fn execute(state: &AppState) -> Arc<[Color]> { + state.canvas().pixels() +} diff --git a/crates/application/src/canvas/mod.rs b/crates/application/src/canvas/mod.rs new file mode 100644 index 0000000..2fe741c --- /dev/null +++ b/crates/application/src/canvas/mod.rs @@ -0,0 +1,4 @@ +pub mod get_state; +pub mod place_pixel; +pub mod restore_snapshot; +pub mod save_snapshot; diff --git a/crates/application/src/canvas/place_pixel.rs b/crates/application/src/canvas/place_pixel.rs new file mode 100644 index 0000000..871ff35 --- /dev/null +++ b/crates/application/src/canvas/place_pixel.rs @@ -0,0 +1,29 @@ +use domain::{BroadcastEvent, Color, PixelUpdate, Position}; + +use crate::{AppState, ApplicationError}; + +pub struct Command<'a> { + pub user_id: &'a str, + pub position: Position, + pub color: Color, +} + +pub enum Outcome { + Placed(PixelUpdate), + CooldownActive, +} + +pub fn execute(state: &AppState, command: Command<'_>) -> Result { + if state.cooldowns().is_on_cooldown(command.user_id) { + return Ok(Outcome::CooldownActive); + } + state + .canvas() + .place_pixel(command.position, command.color)?; + state.cooldowns().record(command.user_id); + let update = PixelUpdate::new(command.position, command.color); + state + .broadcaster() + .publish(BroadcastEvent::PixelUpdated(update)); + Ok(Outcome::Placed(update)) +} diff --git a/crates/application/src/canvas/restore_snapshot.rs b/crates/application/src/canvas/restore_snapshot.rs new file mode 100644 index 0000000..5967867 --- /dev/null +++ b/crates/application/src/canvas/restore_snapshot.rs @@ -0,0 +1,14 @@ +use crate::{AppState, ApplicationError}; + +pub fn execute(state: &AppState) -> Result { + let Some(persistence) = state.persistence() else { + return Ok(false); + }; + match persistence.load_latest()? { + Some(pixels) => { + state.canvas().restore(pixels)?; + Ok(true) + } + None => Ok(false), + } +} diff --git a/crates/application/src/canvas/save_snapshot.rs b/crates/application/src/canvas/save_snapshot.rs new file mode 100644 index 0000000..85b6846 --- /dev/null +++ b/crates/application/src/canvas/save_snapshot.rs @@ -0,0 +1,10 @@ +use crate::{AppState, ApplicationError}; + +pub fn execute(state: &AppState) -> Result<(), ApplicationError> { + let Some(persistence) = state.persistence() else { + return Ok(()); + }; + let pixels = state.canvas().pixels(); + persistence.save(&pixels)?; + Ok(()) +} diff --git a/crates/application/src/errors.rs b/crates/application/src/errors.rs new file mode 100644 index 0000000..235e7ed --- /dev/null +++ b/crates/application/src/errors.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ApplicationError { + #[error(transparent)] + Domain(#[from] domain::DomainError), +} diff --git a/crates/application/src/lib.rs b/crates/application/src/lib.rs new file mode 100644 index 0000000..f2b3ff9 --- /dev/null +++ b/crates/application/src/lib.rs @@ -0,0 +1,8 @@ +mod errors; +mod state; + +pub mod canvas; +pub mod soldiers; + +pub use errors::ApplicationError; +pub use state::{AppState, InMemoryCanvasStore, InProcessBroadcaster}; diff --git a/crates/application/src/soldiers/connect.rs b/crates/application/src/soldiers/connect.rs new file mode 100644 index 0000000..6725374 --- /dev/null +++ b/crates/application/src/soldiers/connect.rs @@ -0,0 +1,11 @@ +use domain::BroadcastEvent; + +use crate::AppState; + +pub fn execute(state: &AppState, user_id: String) -> usize { + let count = state.soldiers().add(user_id); + state + .broadcaster() + .publish(BroadcastEvent::SoldierCountChanged(count)); + count +} diff --git a/crates/application/src/soldiers/disconnect.rs b/crates/application/src/soldiers/disconnect.rs new file mode 100644 index 0000000..89b3b8f --- /dev/null +++ b/crates/application/src/soldiers/disconnect.rs @@ -0,0 +1,12 @@ +use domain::BroadcastEvent; + +use crate::AppState; + +pub fn execute(state: &AppState, user_id: &str) -> usize { + state.cooldowns().remove(user_id); + let count = state.soldiers().remove(user_id); + state + .broadcaster() + .publish(BroadcastEvent::SoldierCountChanged(count)); + count +} diff --git a/crates/application/src/soldiers/mod.rs b/crates/application/src/soldiers/mod.rs new file mode 100644 index 0000000..587edf0 --- /dev/null +++ b/crates/application/src/soldiers/mod.rs @@ -0,0 +1,2 @@ +pub mod connect; +pub mod disconnect; diff --git a/crates/application/src/state.rs b/crates/application/src/state.rs new file mode 100644 index 0000000..77fae3c --- /dev/null +++ b/crates/application/src/state.rs @@ -0,0 +1,199 @@ +use std::collections::{HashMap, HashSet}; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::{Duration, Instant}; + +use domain::ports::{ + BroadcastReceiverInner, BroadcastSubscription, CanvasPersistence, CanvasStore, EventBroadcaster, +}; +use domain::{BroadcastEvent, Canvas, Color, DomainError, Position}; +use tokio::sync::broadcast; +use tracing::warn; + +const INITIAL_CONNECTION_CAPACITY: usize = 128; + +pub struct AppState { + canvas: Box, + broadcaster: Box, + persistence: Option>, + cooldowns: CooldownTracker, + soldiers: SoldierTracker, +} + +impl AppState { + pub fn new( + canvas: Box, + broadcaster: Box, + cooldown: Duration, + ) -> Self { + Self { + canvas, + broadcaster, + persistence: None, + cooldowns: CooldownTracker::new(cooldown), + soldiers: SoldierTracker::new(), + } + } + + pub fn with_persistence(mut self, persistence: Box) -> Self { + self.persistence = Some(persistence); + self + } + + pub fn canvas(&self) -> &dyn CanvasStore { + &*self.canvas + } + + pub fn broadcaster(&self) -> &dyn EventBroadcaster { + &*self.broadcaster + } + + pub fn persistence(&self) -> Option<&dyn CanvasPersistence> { + self.persistence.as_deref() + } + + pub fn cooldowns(&self) -> &CooldownTracker { + &self.cooldowns + } + + pub fn soldiers(&self) -> &SoldierTracker { + &self.soldiers + } +} + +pub struct InProcessBroadcaster { + sender: broadcast::Sender, +} + +impl InProcessBroadcaster { + pub fn new(sender: broadcast::Sender) -> Self { + Self { sender } + } +} + +impl EventBroadcaster for InProcessBroadcaster { + fn publish(&self, event: BroadcastEvent) { + let _ = self.sender.send(event); + } + + fn subscribe(&self) -> BroadcastSubscription { + BroadcastSubscription::new(Box::new(TokioBroadcastReceiver(self.sender.subscribe()))) + } +} + +struct TokioBroadcastReceiver(broadcast::Receiver); + +impl BroadcastReceiverInner for TokioBroadcastReceiver { + fn recv_boxed(&mut self) -> Pin> + Send + '_>> { + Box::pin(async { self.0.recv().await.ok() }) + } +} + +fn acquire_lock(mutex: &Mutex) -> MutexGuard<'_, T> { + mutex.lock().unwrap_or_else(|poisoned| { + warn!("Recovered from poisoned mutex"); + poisoned.into_inner() + }) +} + +struct CanvasState { + canvas: Canvas, + snapshot: Option>, +} + +pub struct InMemoryCanvasStore { + state: Mutex, +} + +impl InMemoryCanvasStore { + pub fn new(width: u32, height: u32) -> Self { + Self { + state: Mutex::new(CanvasState { + canvas: Canvas::new(width, height), + snapshot: None, + }), + } + } +} + +impl CanvasStore for InMemoryCanvasStore { + fn pixels(&self) -> Arc<[Color]> { + let mut state = acquire_lock(&self.state); + if let Some(ref cached) = state.snapshot { + return cached.clone(); + } + let new_snapshot: Arc<[Color]> = Arc::from(state.canvas.pixels()); + state.snapshot = Some(new_snapshot.clone()); + new_snapshot + } + + fn place_pixel(&self, position: Position, color: Color) -> Result<(), DomainError> { + let mut state = acquire_lock(&self.state); + state.canvas.place_pixel(position, color)?; + state.snapshot = None; + Ok(()) + } + + fn restore(&self, pixels: Vec) -> Result<(), DomainError> { + let mut state = acquire_lock(&self.state); + let new_canvas = Canvas::from_pixels(state.canvas.width(), state.canvas.height(), pixels)?; + state.canvas = new_canvas; + state.snapshot = None; + Ok(()) + } +} + +pub struct CooldownTracker { + entries: Mutex>, + cooldown: Duration, +} + +impl CooldownTracker { + pub fn new(cooldown: Duration) -> Self { + Self { + entries: Mutex::new(HashMap::with_capacity(INITIAL_CONNECTION_CAPACITY)), + cooldown, + } + } + + pub fn is_on_cooldown(&self, user_id: &str) -> bool { + let entries = acquire_lock(&self.entries); + entries + .get(user_id) + .map(|last| last.elapsed() < self.cooldown) + .unwrap_or(false) + } + + pub fn record(&self, user_id: &str) { + acquire_lock(&self.entries).insert(user_id.to_string(), Instant::now()); + } + + pub fn remove(&self, user_id: &str) { + acquire_lock(&self.entries).remove(user_id); + } +} + +pub struct SoldierTracker { + connected: Mutex>, +} + +impl SoldierTracker { + pub fn new() -> Self { + Self { + connected: Mutex::new(HashSet::with_capacity(INITIAL_CONNECTION_CAPACITY)), + } + } + + pub fn add(&self, user_id: String) -> usize { + let mut connected = acquire_lock(&self.connected); + connected.insert(user_id); + connected.len() + } + + pub fn remove(&self, user_id: &str) -> usize { + let mut connected = acquire_lock(&self.connected); + connected.remove(user_id); + connected.len() + } +} diff --git a/crates/application/tests/common/mod.rs b/crates/application/tests/common/mod.rs new file mode 100644 index 0000000..8d5c430 --- /dev/null +++ b/crates/application/tests/common/mod.rs @@ -0,0 +1,109 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use application::{AppState, InMemoryCanvasStore}; +use domain::ports::{ + BroadcastReceiverInner, BroadcastSubscription, CanvasPersistence, EventBroadcaster, +}; +use domain::{BroadcastEvent, Color, DomainError}; + +#[derive(Clone)] +pub struct SpyBroadcaster { + events: Arc>>, +} + +impl SpyBroadcaster { + pub fn new() -> Self { + Self { + events: Arc::new(Mutex::new(Vec::new())), + } + } + + pub fn events(&self) -> Vec { + self.events.lock().unwrap().clone() + } + + pub fn event_count(&self) -> usize { + self.events.lock().unwrap().len() + } +} + +impl EventBroadcaster for SpyBroadcaster { + fn publish(&self, event: BroadcastEvent) { + self.events.lock().unwrap().push(event); + } + + fn subscribe(&self) -> BroadcastSubscription { + BroadcastSubscription::new(Box::new(NoopReceiver)) + } +} + +struct NoopReceiver; + +impl BroadcastReceiverInner for NoopReceiver { + fn recv_boxed(&mut self) -> Pin> + Send + '_>> { + Box::pin(async { None }) + } +} + +pub struct FakePersistence { + saved: Arc>>>, + to_load: Mutex>>, +} + +impl FakePersistence { + pub fn empty() -> Self { + Self { + saved: Arc::new(Mutex::new(Vec::new())), + to_load: Mutex::new(None), + } + } + + pub fn with_snapshot(pixels: Vec) -> Self { + Self { + saved: Arc::new(Mutex::new(Vec::new())), + to_load: Mutex::new(Some(pixels)), + } + } + + pub fn saved_ref(&self) -> Arc>>> { + self.saved.clone() + } +} + +impl CanvasPersistence for FakePersistence { + fn save(&self, pixels: &[Color]) -> Result<(), DomainError> { + self.saved.lock().unwrap().push(pixels.to_vec()); + Ok(()) + } + + fn load_latest(&self) -> Result>, DomainError> { + Ok(self.to_load.lock().unwrap().clone()) + } +} + +pub fn test_state() -> (Arc, SpyBroadcaster) { + test_state_sized(10, 10) +} + +pub fn test_state_sized(width: u32, height: u32) -> (Arc, SpyBroadcaster) { + let spy = SpyBroadcaster::new(); + let state = AppState::new( + Box::new(InMemoryCanvasStore::new(width, height)), + Box::new(spy.clone()), + Duration::from_secs(10), + ); + (Arc::new(state), spy) +} + +pub fn test_state_no_cooldown() -> (Arc, SpyBroadcaster) { + let spy = SpyBroadcaster::new(); + let state = AppState::new( + Box::new(InMemoryCanvasStore::new(10, 10)), + Box::new(spy.clone()), + Duration::ZERO, + ); + (Arc::new(state), spy) +} diff --git a/crates/application/tests/place_pixel.rs b/crates/application/tests/place_pixel.rs new file mode 100644 index 0000000..72b18f0 --- /dev/null +++ b/crates/application/tests/place_pixel.rs @@ -0,0 +1,91 @@ +mod common; + +use application::canvas::place_pixel::{Command, Outcome}; +use domain::{BroadcastEvent, Color, Position}; + +macro_rules! place { + ($state:expr, $user:expr, $x:expr, $y:expr, $color:expr) => { + application::canvas::place_pixel::execute( + &$state, + Command { + user_id: $user, + position: Position::new($x, $y), + color: Color::new($color), + }, + ) + }; +} + +#[test] +fn successful_placement_returns_update() { + let (state, _) = common::test_state(); + let result = place!(state, "user-1", 3, 4, 0xFF0000).unwrap(); + + let Outcome::Placed(update) = result else { + panic!("expected Placed outcome"); + }; + assert_eq!(update.position(), Position::new(3, 4)); + assert_eq!(update.color(), Color::new(0xFF0000)); +} + +#[test] +fn placement_updates_canvas() { + let (state, _) = common::test_state(); + place!(state, "user-1", 5, 5, 0xAA).unwrap(); + + let pixels = application::canvas::get_state::execute(&state); + let idx = 5 * 10 + 5; + assert_eq!(pixels[idx], Color::new(0xAA)); +} + +#[test] +fn placement_publishes_broadcast_event() { + let (state, spy) = common::test_state(); + place!(state, "user-1", 0, 0, 0xFF).unwrap(); + + let events = spy.events(); + assert_eq!(events.len(), 1); + assert!(matches!(events[0], BroadcastEvent::PixelUpdated(_))); +} + +#[test] +fn cooldown_blocks_rapid_placement() { + let (state, _) = common::test_state(); + place!(state, "user-1", 0, 0, 0xFF).unwrap(); + + let result = place!(state, "user-1", 1, 1, 0xAA).unwrap(); + assert!(matches!(result, Outcome::CooldownActive)); +} + +#[test] +fn cooldown_is_per_user() { + let (state, _) = common::test_state(); + place!(state, "user-1", 0, 0, 0xFF).unwrap(); + + let result = place!(state, "user-2", 1, 1, 0xAA).unwrap(); + assert!(matches!(result, Outcome::Placed(_))); +} + +#[test] +fn zero_cooldown_allows_rapid_placement() { + let (state, _) = common::test_state_no_cooldown(); + place!(state, "user-1", 0, 0, 0xFF).unwrap(); + + let result = place!(state, "user-1", 1, 1, 0xAA).unwrap(); + assert!(matches!(result, Outcome::Placed(_))); +} + +#[test] +fn out_of_bounds_returns_error() { + let (state, _) = common::test_state(); + assert!(place!(state, "user-1", 99, 99, 0xFF).is_err()); +} + +#[test] +fn failed_placement_does_not_trigger_cooldown() { + let (state, _) = common::test_state(); + let _ = place!(state, "user-1", 99, 99, 0xFF); + + let result = place!(state, "user-1", 0, 0, 0xFF).unwrap(); + assert!(matches!(result, Outcome::Placed(_))); +} diff --git a/crates/application/tests/snapshots.rs b/crates/application/tests/snapshots.rs new file mode 100644 index 0000000..cacd493 --- /dev/null +++ b/crates/application/tests/snapshots.rs @@ -0,0 +1,75 @@ +mod common; + +use std::sync::Arc; + +use application::AppState; +use domain::Color; + +fn state_with_persistence(persistence: common::FakePersistence) -> Arc { + let spy = common::SpyBroadcaster::new(); + let state = AppState::new( + Box::new(application::InMemoryCanvasStore::new(10, 10)), + Box::new(spy), + std::time::Duration::from_secs(10), + ) + .with_persistence(Box::new(persistence)); + Arc::new(state) +} + +#[test] +fn save_snapshot_persists_current_canvas() { + let persistence = common::FakePersistence::empty(); + let saved_ref = persistence.saved_ref(); + let state = state_with_persistence(persistence); + + application::canvas::place_pixel::execute( + &state, + application::canvas::place_pixel::Command { + user_id: "user", + position: domain::Position::new(0, 0), + color: Color::new(0xFF), + }, + ) + .unwrap(); + + application::canvas::save_snapshot::execute(&state).unwrap(); + + let snapshots = saved_ref.lock().unwrap(); + assert_eq!(snapshots.len(), 1); + assert_eq!(snapshots[0][0], Color::new(0xFF)); + assert_eq!(snapshots[0].len(), 100); +} + +#[test] +fn restore_snapshot_loads_canvas() { + let mut snapshot = vec![Color::white(); 100]; + snapshot[0] = Color::new(0xDEAD); + snapshot[99] = Color::new(0xBEEF); + + let state = state_with_persistence(common::FakePersistence::with_snapshot(snapshot)); + + let restored = application::canvas::restore_snapshot::execute(&state).unwrap(); + assert!(restored); + + let pixels = application::canvas::get_state::execute(&state); + assert_eq!(pixels[0], Color::new(0xDEAD)); + assert_eq!(pixels[99], Color::new(0xBEEF)); +} + +#[test] +fn restore_returns_false_when_no_snapshot() { + let state = state_with_persistence(common::FakePersistence::empty()); + assert!(!application::canvas::restore_snapshot::execute(&state).unwrap()); +} + +#[test] +fn save_without_persistence_is_noop() { + let (state, _) = common::test_state(); + assert!(application::canvas::save_snapshot::execute(&state).is_ok()); +} + +#[test] +fn restore_without_persistence_returns_false() { + let (state, _) = common::test_state(); + assert!(!application::canvas::restore_snapshot::execute(&state).unwrap()); +} diff --git a/crates/application/tests/soldiers.rs b/crates/application/tests/soldiers.rs new file mode 100644 index 0000000..f28e259 --- /dev/null +++ b/crates/application/tests/soldiers.rs @@ -0,0 +1,96 @@ +mod common; + +use domain::BroadcastEvent; + +#[test] +fn connect_increments_count() { + let (state, _) = common::test_state(); + assert_eq!( + application::soldiers::connect::execute(&state, "a".into()), + 1 + ); + assert_eq!( + application::soldiers::connect::execute(&state, "b".into()), + 2 + ); + assert_eq!( + application::soldiers::connect::execute(&state, "c".into()), + 3 + ); +} + +#[test] +fn disconnect_decrements_count() { + let (state, _) = common::test_state(); + application::soldiers::connect::execute(&state, "a".into()); + application::soldiers::connect::execute(&state, "b".into()); + + assert_eq!(application::soldiers::disconnect::execute(&state, "a"), 1); + assert_eq!(application::soldiers::disconnect::execute(&state, "b"), 0); +} + +#[test] +fn disconnect_unknown_user_is_harmless() { + let (state, _) = common::test_state(); + application::soldiers::connect::execute(&state, "a".into()); + assert_eq!( + application::soldiers::disconnect::execute(&state, "unknown"), + 1 + ); +} + +#[test] +fn connect_publishes_soldier_count() { + let (state, spy) = common::test_state(); + application::soldiers::connect::execute(&state, "a".into()); + + let events = spy.events(); + assert_eq!(events.len(), 1); + assert!(matches!(events[0], BroadcastEvent::SoldierCountChanged(1))); +} + +#[test] +fn disconnect_publishes_soldier_count() { + let (state, spy) = common::test_state(); + application::soldiers::connect::execute(&state, "a".into()); + application::soldiers::disconnect::execute(&state, "a"); + + let events = spy.events(); + assert_eq!(events.len(), 2); + assert!(matches!(events[1], BroadcastEvent::SoldierCountChanged(0))); +} + +#[test] +fn disconnect_clears_cooldown() { + let (state, _) = common::test_state(); + application::soldiers::connect::execute(&state, "user-1".into()); + + application::canvas::place_pixel::execute( + &state, + application::canvas::place_pixel::Command { + user_id: "user-1", + position: domain::Position::new(0, 0), + color: domain::Color::new(0xFF), + }, + ) + .unwrap(); + + application::soldiers::disconnect::execute(&state, "user-1"); + + // Reconnect with same ID — cooldown should be gone + application::soldiers::connect::execute(&state, "user-1".into()); + let result = application::canvas::place_pixel::execute( + &state, + application::canvas::place_pixel::Command { + user_id: "user-1", + position: domain::Position::new(1, 1), + color: domain::Color::new(0xAA), + }, + ) + .unwrap(); + + assert!(matches!( + result, + application::canvas::place_pixel::Outcome::Placed(_) + )); +} diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml new file mode 100644 index 0000000..66d1f82 --- /dev/null +++ b/crates/config/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "config" +version.workspace = true +edition.workspace = true + +[dependencies] +thiserror = { workspace = true } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs new file mode 100644 index 0000000..79bd31e --- /dev/null +++ b/crates/config/src/lib.rs @@ -0,0 +1,61 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("invalid value for '{field}': {reason}")] + InvalidValue { field: String, reason: String }, + + #[error("failed to load config: {0}")] + LoadFailed(String), +} + +#[derive(Debug, Clone)] +pub struct AppConfig { + pub server: ServerConfig, + pub canvas: CanvasConfig, + pub cooldown: CooldownConfig, + pub rate_limit: RateLimitConfig, + pub broadcast: BroadcastConfig, + pub snapshot: SnapshotConfig, +} + +#[derive(Debug, Clone)] +pub struct ServerConfig { + pub address: String, + pub port: u16, + pub enable_cors: bool, +} + +#[derive(Debug, Clone)] +pub struct CanvasConfig { + pub width: u32, + pub height: u32, +} + +#[derive(Debug, Clone)] +pub struct CooldownConfig { + pub placement_secs: u64, +} + +#[derive(Debug, Clone)] +pub struct RateLimitConfig { + pub burst_size: u32, + pub per_second: u64, +} + +#[derive(Debug, Clone)] +pub struct BroadcastConfig { + pub channel_capacity: usize, +} + +#[derive(Debug, Clone)] +pub struct SnapshotConfig { + pub enabled: bool, + pub interval_secs: u64, + pub max_snapshots: usize, + pub directory: String, +} + +pub trait ConfigSource { + fn load(&self) -> Result; +} diff --git a/crates/domain/Cargo.toml b/crates/domain/Cargo.toml new file mode 100644 index 0000000..94e5d6d --- /dev/null +++ b/crates/domain/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "domain" +version.workspace = true +edition.workspace = true + +[dependencies] +thiserror = { workspace = true } diff --git a/crates/domain/src/canvas.rs b/crates/domain/src/canvas.rs new file mode 100644 index 0000000..5ce9018 --- /dev/null +++ b/crates/domain/src/canvas.rs @@ -0,0 +1,57 @@ +use crate::{Color, DomainError, PixelUpdate, Position}; + +pub struct Canvas { + pixels: Vec, + width: u32, + height: u32, +} + +impl Canvas { + pub fn new(width: u32, height: u32) -> Self { + Self { + pixels: vec![Color::white(); (width * height) as usize], + width, + height, + } + } + + pub fn from_pixels(width: u32, height: u32, pixels: Vec) -> Result { + let expected = (width * height) as usize; + if pixels.len() != expected { + return Err(DomainError::InvalidCanvasData { + expected_width: width, + expected_height: height, + actual: pixels.len(), + }); + } + Ok(Self { + pixels, + width, + height, + }) + } + + pub fn place_pixel( + &mut self, + position: Position, + color: Color, + ) -> Result { + if position.x() >= self.width || position.y() >= self.height { + return Err(DomainError::PixelOutOfBounds(position)); + } + self.pixels[(position.y() * self.width + position.x()) as usize] = color; + Ok(PixelUpdate::new(position, color)) + } + + pub fn pixels(&self) -> &[Color] { + &self.pixels + } + + pub fn width(&self) -> u32 { + self.width + } + + pub fn height(&self) -> u32 { + self.height + } +} diff --git a/crates/domain/src/errors.rs b/crates/domain/src/errors.rs new file mode 100644 index 0000000..ed5dab9 --- /dev/null +++ b/crates/domain/src/errors.rs @@ -0,0 +1,21 @@ +use thiserror::Error; + +use crate::Position; + +#[derive(Debug, Error)] +pub enum DomainError { + #[error("pixel position {0} is out of bounds")] + PixelOutOfBounds(Position), + + #[error( + "invalid canvas dimensions: expected {expected_width}x{expected_height}, got {actual} pixels" + )] + InvalidCanvasData { + expected_width: u32, + expected_height: u32, + actual: usize, + }, + + #[error("persistence error: {0}")] + Persistence(String), +} diff --git a/crates/domain/src/events.rs b/crates/domain/src/events.rs new file mode 100644 index 0000000..1633da9 --- /dev/null +++ b/crates/domain/src/events.rs @@ -0,0 +1,7 @@ +use crate::PixelUpdate; + +#[derive(Debug, Clone, Copy)] +pub enum BroadcastEvent { + PixelUpdated(PixelUpdate), + SoldierCountChanged(usize), +} diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs new file mode 100644 index 0000000..f4fac02 --- /dev/null +++ b/crates/domain/src/lib.rs @@ -0,0 +1,13 @@ +pub mod canvas; +pub mod errors; +pub mod events; +pub mod ports; +pub mod value_objects; + +pub use canvas::Canvas; +pub use errors::DomainError; +pub use events::BroadcastEvent; +pub use ports::BroadcastSubscription; +pub use value_objects::{Color, PixelUpdate, Position}; + +pub const COOLDOWN_MESSAGE: &str = "You can only place one pixel per minute"; diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs new file mode 100644 index 0000000..aafa93c --- /dev/null +++ b/crates/domain/src/ports.rs @@ -0,0 +1,39 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use crate::{BroadcastEvent, Color, DomainError, Position}; + +pub trait CanvasStore: Send + Sync { + fn pixels(&self) -> Arc<[Color]>; + fn place_pixel(&self, position: Position, color: Color) -> Result<(), DomainError>; + fn restore(&self, pixels: Vec) -> Result<(), DomainError>; +} + +pub trait CanvasPersistence: Send + Sync { + fn save(&self, pixels: &[Color]) -> Result<(), DomainError>; + fn load_latest(&self) -> Result>, DomainError>; +} + +pub trait EventBroadcaster: Send + Sync { + fn publish(&self, event: BroadcastEvent); + fn subscribe(&self) -> BroadcastSubscription; +} + +pub trait BroadcastReceiverInner: Send { + fn recv_boxed(&mut self) -> Pin> + Send + '_>>; +} + +pub struct BroadcastSubscription { + inner: Box, +} + +impl BroadcastSubscription { + pub fn new(inner: Box) -> Self { + Self { inner } + } + + pub async fn recv(&mut self) -> Option { + self.inner.recv_boxed().await + } +} diff --git a/crates/domain/src/value_objects.rs b/crates/domain/src/value_objects.rs new file mode 100644 index 0000000..603c72c --- /dev/null +++ b/crates/domain/src/value_objects.rs @@ -0,0 +1,76 @@ +use std::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct Color(u32); + +impl Color { + pub fn new(value: u32) -> Self { + Self(value) + } + + pub fn as_u32(self) -> u32 { + self.0 + } + + pub fn white() -> Self { + Self(0xFFFFFFFF) + } + + pub fn collect_as_u32(colors: &[Color]) -> Vec { + colors.iter().map(|color| color.as_u32()).collect() + } + + pub fn collect_as_bytes(colors: &[Color]) -> Vec { + colors + .iter() + .flat_map(|color| color.as_u32().to_ne_bytes()) + .collect() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Position { + x: u32, + y: u32, +} + +impl Position { + pub fn new(x: u32, y: u32) -> Self { + Self { x, y } + } + + pub fn x(self) -> u32 { + self.x + } + + pub fn y(self) -> u32 { + self.y + } +} + +impl fmt::Display for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct PixelUpdate { + position: Position, + color: Color, +} + +impl PixelUpdate { + pub fn new(position: Position, color: Color) -> Self { + Self { position, color } + } + + pub fn position(self) -> Position { + self.position + } + + pub fn color(self) -> Color { + self.color + } +} diff --git a/crates/domain/tests/canvas.rs b/crates/domain/tests/canvas.rs new file mode 100644 index 0000000..aa0b324 --- /dev/null +++ b/crates/domain/tests/canvas.rs @@ -0,0 +1,125 @@ +use domain::{Canvas, Color, DomainError, Position}; + +macro_rules! pos { + ($x:expr, $y:expr) => { + Position::new($x, $y) + }; +} + +macro_rules! color { + ($v:expr) => { + Color::new($v) + }; +} + +macro_rules! assert_pixel { + ($canvas:expr, $x:expr, $y:expr, $expected:expr) => {{ + let (x, y): (u32, u32) = ($x, $y); + let idx = y as usize * $canvas.width() as usize + x as usize; + let expected = color!($expected); + assert_eq!($canvas.pixels()[idx], expected, "pixel at ({x}, {y})"); + }}; +} + +fn small_canvas() -> Canvas { + Canvas::new(10, 10) +} + +#[test] +fn new_canvas_is_all_white() { + let canvas = small_canvas(); + assert_eq!(canvas.pixels().len(), 100); + assert!(canvas.pixels().iter().all(|&c| c == Color::white())); +} + +#[test] +fn dimensions_match_construction() { + let canvas = Canvas::new(42, 17); + assert_eq!(canvas.width(), 42); + assert_eq!(canvas.height(), 17); + assert_eq!(canvas.pixels().len(), 42 * 17); +} + +#[test] +fn place_pixel_updates_correct_position() { + let mut canvas = small_canvas(); + let update = canvas.place_pixel(pos!(3, 4), color!(0xFF0000)).unwrap(); + + assert_pixel!(canvas, 3, 4, 0xFF0000); + assert_eq!(update.position(), pos!(3, 4)); + assert_eq!(update.color(), color!(0xFF0000)); +} + +#[test] +fn place_pixel_does_not_affect_neighbors() { + let mut canvas = small_canvas(); + canvas.place_pixel(pos!(5, 5), color!(0xFF)).unwrap(); + + for (x, y) in [(4, 5), (6, 5), (5, 4), (5, 6)] { + assert_pixel!(canvas, x, y, 0xFFFFFFFF); + } +} + +#[test] +fn place_pixel_overwrites_previous() { + let mut canvas = small_canvas(); + canvas.place_pixel(pos!(0, 0), color!(0xAA)).unwrap(); + canvas.place_pixel(pos!(0, 0), color!(0xBB)).unwrap(); + assert_pixel!(canvas, 0, 0, 0xBB); +} + +#[test] +fn place_pixel_at_boundary() { + let mut canvas = small_canvas(); + assert!(canvas.place_pixel(pos!(9, 9), color!(0xFF)).is_ok()); + assert!(canvas.place_pixel(pos!(0, 0), color!(0xFF)).is_ok()); + assert!(canvas.place_pixel(pos!(9, 0), color!(0xFF)).is_ok()); + assert!(canvas.place_pixel(pos!(0, 9), color!(0xFF)).is_ok()); +} + +#[test] +fn place_pixel_out_of_bounds() { + let mut canvas = small_canvas(); + + for (x, y) in [(10, 0), (0, 10), (10, 10), (100, 100)] { + let result = canvas.place_pixel(pos!(x, y), color!(0xFF)); + assert!( + matches!(result, Err(DomainError::PixelOutOfBounds(_))), + "({x}, {y}) should be out of bounds" + ); + } +} + +#[test] +fn from_pixels_with_correct_size() { + let pixels = vec![color!(0xAA); 25]; + let canvas = Canvas::from_pixels(5, 5, pixels).unwrap(); + assert_eq!(canvas.width(), 5); + assert_eq!(canvas.height(), 5); + assert!(canvas.pixels().iter().all(|&c| c == color!(0xAA))); +} + +#[test] +fn from_pixels_with_wrong_size() { + let too_few = vec![color!(0); 10]; + let too_many = vec![color!(0); 30]; + + for pixels in [too_few, too_many] { + assert!( + matches!( + Canvas::from_pixels(5, 5, pixels), + Err(DomainError::InvalidCanvasData { .. }) + ), + "should reject pixel vec that doesn't match dimensions" + ); + } +} + +#[test] +fn from_pixels_preserves_content() { + let mut pixels = vec![Color::white(); 9]; + pixels[4] = color!(0xFF0000); // center pixel of 3x3 + let canvas = Canvas::from_pixels(3, 3, pixels).unwrap(); + assert_pixel!(canvas, 1, 1, 0xFF0000); + assert_pixel!(canvas, 0, 0, 0xFFFFFFFF); +} diff --git a/crates/domain/tests/value_objects.rs b/crates/domain/tests/value_objects.rs new file mode 100644 index 0000000..2739050 --- /dev/null +++ b/crates/domain/tests/value_objects.rs @@ -0,0 +1,69 @@ +use domain::{Color, PixelUpdate, Position}; + +#[test] +fn color_roundtrips_through_u32() { + for value in [0, 0xFF, 0xFF0000, 0xFFFFFFFF, 0xDEADBEEF] { + assert_eq!(Color::new(value).as_u32(), value); + } +} + +#[test] +fn color_white_is_full_alpha() { + assert_eq!(Color::white().as_u32(), 0xFFFFFFFF); +} + +#[test] +fn color_equality() { + assert_eq!(Color::new(42), Color::new(42)); + assert_ne!(Color::new(1), Color::new(2)); +} + +#[test] +fn collect_as_u32_preserves_values() { + let colors = [Color::new(1), Color::new(2), Color::new(3)]; + assert_eq!(Color::collect_as_u32(&colors), vec![1, 2, 3]); +} + +#[test] +fn collect_as_bytes_length() { + let colors = vec![Color::white(); 10]; + assert_eq!(Color::collect_as_bytes(&colors).len(), 40); +} + +#[test] +fn collect_as_bytes_roundtrips() { + let original = vec![Color::new(0x01020304), Color::new(0xAABBCCDD)]; + let bytes = Color::collect_as_bytes(&original); + let restored: Vec = bytes + .chunks_exact(4) + .map(|c| Color::new(u32::from_ne_bytes([c[0], c[1], c[2], c[3]]))) + .collect(); + assert_eq!(original, restored); +} + +#[test] +fn position_accessors() { + let pos = Position::new(42, 17); + assert_eq!(pos.x(), 42); + assert_eq!(pos.y(), 17); +} + +#[test] +fn position_display() { + assert_eq!(Position::new(3, 7).to_string(), "(3, 7)"); +} + +#[test] +fn position_equality() { + assert_eq!(Position::new(1, 2), Position::new(1, 2)); + assert_ne!(Position::new(1, 2), Position::new(2, 1)); +} + +#[test] +fn pixel_update_carries_position_and_color() { + let pos = Position::new(5, 10); + let color = Color::new(0xFF00FF); + let update = PixelUpdate::new(pos, color); + assert_eq!(update.position(), pos); + assert_eq!(update.color(), color); +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml new file mode 100644 index 0000000..d08710e --- /dev/null +++ b/crates/server/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "server" +version.workspace = true +edition.workspace = true + +[features] +default = ["socketio"] +socketio = ["dep:socketio", "dep:socketioxide"] +websocket = ["dep:websocket"] + +[dependencies] +application = { workspace = true } +canvas-file = { workspace = true } +config = { workspace = true } +config-env = { workspace = true } +axum = { workspace = true } +domain = { workspace = true } +http-axum = { workspace = true } +dotenv = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +socketio = { workspace = true, optional = true } +socketioxide = { workspace = true, optional = true } +websocket = { workspace = true, optional = true } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs new file mode 100644 index 0000000..a45b014 --- /dev/null +++ b/crates/server/src/main.rs @@ -0,0 +1,133 @@ +#[cfg(not(any(feature = "socketio", feature = "websocket")))] +compile_error!("Enable either the `socketio` or `websocket` feature"); + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use application::{AppState, InMemoryCanvasStore, InProcessBroadcaster}; +use canvas_file::FileCanvasPersistence; +use config::{AppConfig, ConfigSource}; +use config_env::EnvConfigSource; +use domain::BroadcastEvent; +use tokio::signal; +use tokio::sync::broadcast; +use tracing::{error, info, warn}; +use tracing_subscriber::FmtSubscriber; + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenv::dotenv().ok(); + tracing::subscriber::set_global_default(FmtSubscriber::new())?; + + let config = EnvConfigSource.load()?; + let state = build_state(&config)?; + + if config.snapshot.enabled { + if let Err(err) = application::canvas::restore_snapshot::execute(&state) { + warn!("Failed to restore canvas snapshot: {err}"); + } + spawn_snapshot_scheduler(state.clone(), config.snapshot.interval_secs); + } + + let app = build_app(state.clone(), &config)?; + + let server_address = format!("{}:{}", config.server.address, config.server.port); + info!("Starting server on {server_address}"); + + let listener = tokio::net::TcpListener::bind(server_address).await?; + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + info!("Shutting down gracefully..."); + + if config.snapshot.enabled { + info!("Saving final canvas snapshot..."); + if let Err(err) = application::canvas::save_snapshot::execute(&state) { + error!("Failed to save final snapshot: {err}"); + } + } + + info!("Server stopped"); + Ok(()) +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("Failed to install SIGTERM handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + () = ctrl_c => info!("Received Ctrl+C"), + () = terminate => info!("Received SIGTERM"), + } +} + +fn build_state(config: &AppConfig) -> Result, Box> { + let (broadcast_tx, _) = broadcast::channel::(config.broadcast.channel_capacity); + + let canvas_store = InMemoryCanvasStore::new(config.canvas.width, config.canvas.height); + let broadcaster = InProcessBroadcaster::new(broadcast_tx); + let cooldown = Duration::from_secs(config.cooldown.placement_secs); + + let mut state = AppState::new(Box::new(canvas_store), Box::new(broadcaster), cooldown); + + if config.snapshot.enabled { + let persistence = FileCanvasPersistence::new(&config.snapshot)?; + state = state.with_persistence(Box::new(persistence)); + } + + Ok(Arc::new(state)) +} + +fn spawn_snapshot_scheduler(state: Arc, interval_secs: u64) { + let interval = Duration::from_secs(interval_secs); + info!("Snapshot scheduler started (every {interval_secs}s)"); + + tokio::spawn(async move { + loop { + tokio::time::sleep(interval).await; + if let Err(err) = application::canvas::save_snapshot::execute(&state) { + error!("Failed to save canvas snapshot: {err}"); + } + } + }); +} + +#[cfg(feature = "socketio")] +fn build_app( + state: Arc, + config: &AppConfig, +) -> Result> { + let (layer, io) = socketioxide::SocketIo::new_layer(); + socketio::setup_namespaces(&io, state); + let router = http_axum::build_router(config.server.enable_cors, &config.rate_limit)?; + Ok(router.layer(layer)) +} + +#[cfg(feature = "websocket")] +fn build_app( + state: Arc, + config: &AppConfig, +) -> Result> { + let ws_router = websocket::build_router(state); + let http_router = http_axum::build_router(config.server.enable_cors, &config.rate_limit)?; + Ok(ws_router.merge(http_router)) +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..0ba3426 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +REGISTRY="registry.gabrielkaszewski.dev" +REPO="painter" + +tag="latest" + +while [[ $# -gt 0 ]]; do + case $1 in + --tag) tag="$2"; shift 2 ;; + *) echo "usage: $0 [--tag T]" >&2; exit 1 ;; + esac +done + +image="${REGISTRY}/${REPO}:${tag}" + +echo "building ${image}" +docker buildx build --platform linux/amd64 \ + -t "$image" --push . +echo "pushed $image" diff --git a/painter-js/bun.lock b/painter-js/bun.lock new file mode 100644 index 0000000..cd2c5ff --- /dev/null +++ b/painter-js/bun.lock @@ -0,0 +1,315 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "painter-js", + "dependencies": { + "socket.io-client": "^4.7.5", + }, + "devDependencies": { + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "prettier": "^3.9.6", + "tailwindcss": "^3.4.3", + "typescript": "^5.2.2", + "vite": "^5.2.0", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="], + + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "autoprefixer": ["autoprefixer@10.5.4", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.408", "", {}, "sha512-SLoprcYpJ/OH2v2ps0+N5biv9H4/KBT3+YmmDew64TwK5y9j2wv7pMOFY7IorVkyMtEyLSCRlXKLsNlakeAlPw=="], + + "engine.io-client": ["engine.io-client@6.6.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], + + "socket.io-parser": ["socket.io-parser@4.2.7", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "tinyglobby/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + } +} diff --git a/painter-js/index.html b/painter-js/index.html index ab2608c..c9652f1 100644 --- a/painter-js/index.html +++ b/painter-js/index.html @@ -1,44 +1,126 @@ - + - - Pixel War + + Painter + + + + + + + + + + + + + - -
-
- - - - - - - - - - - - + +
+
+ + + + + + + + + + + +
-

Your selected color COLOR

-
You can place a pixel now
-
- - - +

+ Your selected color + COLOR +

+
You can place a pixel now
+
+ + 1.0x + + +

-
-

+
+
-
- -
-
-

Current soldiers: 0

- +
+

+ Soldiers: 0 +

+
diff --git a/painter-js/package-lock.json b/painter-js/package-lock.json deleted file mode 100644 index 702b221..0000000 --- a/painter-js/package-lock.json +++ /dev/null @@ -1,917 +0,0 @@ -{ - "name": "painter-js", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "painter-js", - "version": "0.0.0", - "dependencies": { - "socket.io-client": "^4.7.5" - }, - "devDependencies": { - "typescript": "^5.2.2", - "vite": "^5.2.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/socket.io-client": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", - "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", - "dev": true, - "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "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 - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", - "engines": { - "node": ">=0.4.0" - } - } - } -} diff --git a/painter-js/package.json b/painter-js/package.json index 3688d4a..921d031 100644 --- a/painter-js/package.json +++ b/painter-js/package.json @@ -6,17 +6,19 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "fmt": "prettier --write 'src/**/*.{js,ts,css,html}'", + "fmt:check": "prettier --check 'src/**/*.{js,ts,css,html}'" }, "devDependencies": { "autoprefixer": "^10.4.19", "postcss": "^8.4.38", + "prettier": "^3.9.6", "tailwindcss": "^3.4.3", "typescript": "^5.2.2", "vite": "^5.2.0" }, "dependencies": { - "socket.io-client": "^4.7.5", - "three": "^0.164.1" + "socket.io-client": "^4.7.5" } } diff --git a/painter-js/pnpm-lock.yaml b/painter-js/pnpm-lock.yaml deleted file mode 100644 index 6819446..0000000 --- a/painter-js/pnpm-lock.yaml +++ /dev/null @@ -1,1466 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - socket.io-client: - specifier: ^4.7.5 - version: 4.7.5 - three: - specifier: ^0.164.1 - version: 0.164.1 - devDependencies: - autoprefixer: - specifier: ^10.4.19 - version: 10.4.19(postcss@8.4.38) - postcss: - specifier: ^8.4.38 - version: 8.4.38 - tailwindcss: - specifier: ^3.4.3 - version: 3.4.3 - typescript: - specifier: ^5.2.2 - version: 5.4.5 - vite: - specifier: ^5.2.0 - version: 5.2.11 - -packages: - - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} - - '@esbuild/aix-ppc64@0.20.2': - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.20.2': - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.20.2': - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.20.2': - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.20.2': - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.20.2': - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.20.2': - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.20.2': - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.20.2': - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.20.2': - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.20.2': - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.20.2': - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.20.2': - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.20.2': - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.20.2': - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.20.2': - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.20.2': - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.20.2': - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.20.2': - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.20.2': - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.20.2': - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.20.2': - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.20.2': - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.4.15': - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@rollup/rollup-android-arm-eabi@4.17.2': - resolution: {integrity: sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.17.2': - resolution: {integrity: sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.17.2': - resolution: {integrity: sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.17.2': - resolution: {integrity: sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-linux-arm-gnueabihf@4.17.2': - resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.17.2': - resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.17.2': - resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.17.2': - resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.17.2': - resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.17.2': - resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.17.2': - resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.17.2': - resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.17.2': - resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.17.2': - resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.17.2': - resolution: {integrity: sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.17.2': - resolution: {integrity: sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==} - cpu: [x64] - os: [win32] - - '@socket.io/component-emitter@3.1.2': - resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - - autoprefixer@10.4.19: - resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - - braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - - browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - - caniuse-lite@1.0.30001618: - resolution: {integrity: sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg==} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - electron-to-chromium@1.4.767: - resolution: {integrity: sha512-nzzHfmQqBss7CE3apQHkHjXW77+8w3ubGCIoEijKCJebPufREaFETgGXWTkh32t259F3Kcq+R8MZdFdOJROgYw==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - engine.io-client@6.5.3: - resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} - - engine.io-parser@5.2.2: - resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} - engines: {node: '>=10.0.0'} - - esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - - fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - - foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} - - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@10.3.15: - resolution: {integrity: sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==} - engines: {node: '>=16 || 14 >=14.18'} - hasBin: true - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - - jiti@1.21.0: - resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} - hasBin: true - - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - lilconfig@3.1.1: - resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - lru-cache@10.2.2: - resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} - engines: {node: 14 || >=16.14} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - - minimatch@9.0.4: - resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} - engines: {node: '>=16 || 14 >=14.17'} - - minipass@7.1.1: - resolution: {integrity: sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==} - engines: {node: '>=16 || 14 >=14.17'} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-js@4.0.1: - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - - postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-nested@6.0.1: - resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - - postcss-selector-parser@6.0.16: - resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} - engines: {node: '>=4'} - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss@8.4.38: - resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} - engines: {node: ^10 || ^12 || >=14} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@4.17.2: - resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - socket.io-client@4.7.5: - resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} - engines: {node: '>=10.0.0'} - - socket.io-parser@4.2.4: - resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} - engines: {node: '>=10.0.0'} - - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - - sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - tailwindcss@3.4.3: - resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==} - engines: {node: '>=14.0.0'} - hasBin: true - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - three@0.164.1: - resolution: {integrity: sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} - engines: {node: '>=14.17'} - hasBin: true - - update-browserslist-db@1.0.16: - resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vite@5.2.11: - resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - ws@8.11.0: - resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} - 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 - - xmlhttprequest-ssl@2.0.0: - resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} - engines: {node: '>=0.4.0'} - - yaml@2.4.2: - resolution: {integrity: sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==} - engines: {node: '>= 14'} - hasBin: true - -snapshots: - - '@alloc/quick-lru@5.2.0': {} - - '@esbuild/aix-ppc64@0.20.2': - optional: true - - '@esbuild/android-arm64@0.20.2': - optional: true - - '@esbuild/android-arm@0.20.2': - optional: true - - '@esbuild/android-x64@0.20.2': - optional: true - - '@esbuild/darwin-arm64@0.20.2': - optional: true - - '@esbuild/darwin-x64@0.20.2': - optional: true - - '@esbuild/freebsd-arm64@0.20.2': - optional: true - - '@esbuild/freebsd-x64@0.20.2': - optional: true - - '@esbuild/linux-arm64@0.20.2': - optional: true - - '@esbuild/linux-arm@0.20.2': - optional: true - - '@esbuild/linux-ia32@0.20.2': - optional: true - - '@esbuild/linux-loong64@0.20.2': - optional: true - - '@esbuild/linux-mips64el@0.20.2': - optional: true - - '@esbuild/linux-ppc64@0.20.2': - optional: true - - '@esbuild/linux-riscv64@0.20.2': - optional: true - - '@esbuild/linux-s390x@0.20.2': - optional: true - - '@esbuild/linux-x64@0.20.2': - optional: true - - '@esbuild/netbsd-x64@0.20.2': - optional: true - - '@esbuild/openbsd-x64@0.20.2': - optional: true - - '@esbuild/sunos-x64@0.20.2': - optional: true - - '@esbuild/win32-arm64@0.20.2': - optional: true - - '@esbuild/win32-ia32@0.20.2': - optional: true - - '@esbuild/win32-x64@0.20.2': - optional: true - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@jridgewell/gen-mapping@0.3.5': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/sourcemap-codec@1.4.15': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@rollup/rollup-android-arm-eabi@4.17.2': - optional: true - - '@rollup/rollup-android-arm64@4.17.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.17.2': - optional: true - - '@rollup/rollup-darwin-x64@4.17.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.17.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.17.2': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.17.2': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.17.2': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.17.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.17.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.17.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.17.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.17.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.17.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.17.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.17.2': - optional: true - - '@socket.io/component-emitter@3.1.2': {} - - '@types/estree@1.0.5': {} - - ansi-regex@5.0.1: {} - - ansi-regex@6.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.1: {} - - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - arg@5.0.2: {} - - autoprefixer@10.4.19(postcss@8.4.38): - dependencies: - browserslist: 4.23.0 - caniuse-lite: 1.0.30001618 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.4.38 - postcss-value-parser: 4.2.0 - - balanced-match@1.0.2: {} - - binary-extensions@2.3.0: {} - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.2: - dependencies: - fill-range: 7.0.1 - - browserslist@4.23.0: - dependencies: - caniuse-lite: 1.0.30001618 - electron-to-chromium: 1.4.767 - node-releases: 2.0.14 - update-browserslist-db: 1.0.16(browserslist@4.23.0) - - camelcase-css@2.0.1: {} - - caniuse-lite@1.0.30001618: {} - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - commander@4.1.1: {} - - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - cssesc@3.0.0: {} - - debug@4.3.4: - dependencies: - ms: 2.1.2 - - didyoumean@1.2.2: {} - - dlv@1.1.3: {} - - eastasianwidth@0.2.0: {} - - electron-to-chromium@1.4.767: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - engine.io-client@6.5.3: - dependencies: - '@socket.io/component-emitter': 3.1.2 - debug: 4.3.4 - engine.io-parser: 5.2.2 - ws: 8.11.0 - xmlhttprequest-ssl: 2.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - engine.io-parser@5.2.2: {} - - esbuild@0.20.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - - escalade@3.1.2: {} - - fast-glob@3.3.2: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - - fastq@1.17.1: - dependencies: - reusify: 1.0.4 - - fill-range@7.0.1: - dependencies: - to-regex-range: 5.0.1 - - foreground-child@3.1.1: - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.1.0 - - fraction.js@4.3.7: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@10.3.15: - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.4 - minipass: 7.1.1 - path-scurry: 1.11.1 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-core-module@2.13.1: - dependencies: - hasown: 2.0.2 - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - isexe@2.0.0: {} - - jackspeak@2.3.6: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jiti@1.21.0: {} - - lilconfig@2.1.0: {} - - lilconfig@3.1.1: {} - - lines-and-columns@1.2.4: {} - - lru-cache@10.2.2: {} - - merge2@1.4.1: {} - - micromatch@4.0.5: - dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - - minimatch@9.0.4: - dependencies: - brace-expansion: 2.0.1 - - minipass@7.1.1: {} - - ms@2.1.2: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.7: {} - - node-releases@2.0.14: {} - - normalize-path@3.0.0: {} - - normalize-range@0.1.2: {} - - object-assign@4.1.1: {} - - object-hash@3.0.0: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.2.2 - minipass: 7.1.1 - - picocolors@1.0.1: {} - - picomatch@2.3.1: {} - - pify@2.3.0: {} - - pirates@4.0.6: {} - - postcss-import@15.1.0(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.8 - - postcss-js@4.0.1(postcss@8.4.38): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.4.38 - - postcss-load-config@4.0.2(postcss@8.4.38): - dependencies: - lilconfig: 3.1.1 - yaml: 2.4.2 - optionalDependencies: - postcss: 8.4.38 - - postcss-nested@6.0.1(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - postcss-selector-parser: 6.0.16 - - postcss-selector-parser@6.0.16: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-value-parser@4.2.0: {} - - postcss@8.4.38: - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 - - queue-microtask@1.2.3: {} - - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - resolve@1.22.8: - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.0.4: {} - - rollup@4.17.2: - dependencies: - '@types/estree': 1.0.5 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.17.2 - '@rollup/rollup-android-arm64': 4.17.2 - '@rollup/rollup-darwin-arm64': 4.17.2 - '@rollup/rollup-darwin-x64': 4.17.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.17.2 - '@rollup/rollup-linux-arm-musleabihf': 4.17.2 - '@rollup/rollup-linux-arm64-gnu': 4.17.2 - '@rollup/rollup-linux-arm64-musl': 4.17.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.17.2 - '@rollup/rollup-linux-riscv64-gnu': 4.17.2 - '@rollup/rollup-linux-s390x-gnu': 4.17.2 - '@rollup/rollup-linux-x64-gnu': 4.17.2 - '@rollup/rollup-linux-x64-musl': 4.17.2 - '@rollup/rollup-win32-arm64-msvc': 4.17.2 - '@rollup/rollup-win32-ia32-msvc': 4.17.2 - '@rollup/rollup-win32-x64-msvc': 4.17.2 - fsevents: 2.3.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - signal-exit@4.1.0: {} - - socket.io-client@4.7.5: - dependencies: - '@socket.io/component-emitter': 3.1.2 - debug: 4.3.4 - engine.io-client: 6.5.3 - socket.io-parser: 4.2.4 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - socket.io-parser@4.2.4: - dependencies: - '@socket.io/component-emitter': 3.1.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - - source-map-js@1.2.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.0.1 - - sucrase@3.35.0: - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - commander: 4.1.1 - glob: 10.3.15 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.6 - ts-interface-checker: 0.1.13 - - supports-preserve-symlinks-flag@1.0.0: {} - - tailwindcss@3.4.3: - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.2 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.0 - lilconfig: 2.1.0 - micromatch: 4.0.5 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.0.1 - postcss: 8.4.38 - postcss-import: 15.1.0(postcss@8.4.38) - postcss-js: 4.0.1(postcss@8.4.38) - postcss-load-config: 4.0.2(postcss@8.4.38) - postcss-nested: 6.0.1(postcss@8.4.38) - postcss-selector-parser: 6.0.16 - resolve: 1.22.8 - sucrase: 3.35.0 - transitivePeerDependencies: - - ts-node - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - three@0.164.1: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - ts-interface-checker@0.1.13: {} - - typescript@5.4.5: {} - - update-browserslist-db@1.0.16(browserslist@4.23.0): - dependencies: - browserslist: 4.23.0 - escalade: 3.1.2 - picocolors: 1.0.1 - - util-deprecate@1.0.2: {} - - vite@5.2.11: - dependencies: - esbuild: 0.20.2 - postcss: 8.4.38 - rollup: 4.17.2 - optionalDependencies: - fsevents: 2.3.3 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - - ws@8.11.0: {} - - xmlhttprequest-ssl@2.0.0: {} - - yaml@2.4.2: {} diff --git a/painter-js/public/og-image.png b/painter-js/public/og-image.png new file mode 100644 index 0000000..e2576d1 Binary files /dev/null and b/painter-js/public/og-image.png differ diff --git a/painter-js/src/Astronaut.glb b/painter-js/src/Astronaut.glb deleted file mode 100644 index b0b85d7..0000000 Binary files a/painter-js/src/Astronaut.glb and /dev/null differ diff --git a/painter-js/src/canvas.js b/painter-js/src/canvas.js deleted file mode 100644 index 31366d3..0000000 --- a/painter-js/src/canvas.js +++ /dev/null @@ -1,267 +0,0 @@ -import { - hexToU32, - u32ToHex, - getColorFromElementCSS, - rgbToHex, -} from "./utils.js"; -import { - pixelSize, - pixelCooldown, - canvasEndpoint, - WIDTH, - HEIGHT, -} from "./constants.js"; - -let socket = null; - -const canvas = document.getElementById("canvas"); -const ctx = canvas.getContext("2d"); -const countdownDiv = document.getElementById("countdown"); -let lastPixelTime = parseInt(localStorage.getItem("lastPixelTime") || "0"); - -const colorPicker = document.getElementById("color-picker"); -const redButton = document.getElementById("red"); -const greenButton = document.getElementById("green"); -const blueButton = document.getElementById("blue"); -const yellowButton = document.getElementById("yellow"); -const purpleButton = document.getElementById("purple"); -const pinkButton = document.getElementById("pink"); -const cyanButton = document.getElementById("cyan"); -const whiteButton = document.getElementById("white"); -const blackButton = document.getElementById("black"); -const orangeButton = document.getElementById("orange"); -const brownButton = document.getElementById("brown"); - -const currentColorSpan = document.getElementById("current-color-span"); -const toggleGridToggle = document.getElementById("toggle-grid"); -const placePixelButton = document.getElementById("place-pixel"); -const saveCanvasButton = document.getElementById("save-canvas"); - -colorPicker.value = localStorage.getItem("currentColor") || "#000000"; - -let currentColor = colorPicker.value; -let showGrid = toggleGridToggle.checked; -currentColorSpan.style.backgroundColor = currentColor; - -let canvasState = []; -let confirmPlacePixel = false; -let previewPixel = null; - -const setCurrentColor = (color, isColorPicker = false) => { - const hexColor = isColorPicker ? color : rgbToHex(color); - currentColor = hexColor; - currentColorSpan.style.backgroundColor = hexColor; - localStorage.setItem("currentColor", hexColor); -}; - -const handleColorPicker = () => { - colorPicker.addEventListener("input", (event) => { - setCurrentColor(event.target.value, true); - }); - - redButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(redButton)); - }); - - greenButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(greenButton)); - }); - - blueButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(blueButton)); - }); - - yellowButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(yellowButton)); - }); - - purpleButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(purpleButton)); - }); - - pinkButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(pinkButton)); - }); - - cyanButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(cyanButton)); - }); - - whiteButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(whiteButton)); - }); - - blackButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(blackButton)); - }); - - orangeButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(orangeButton)); - }); - - brownButton.addEventListener("click", () => { - setCurrentColor(getColorFromElementCSS(brownButton)); - }); -}; - -const fetchCanvasState = async () => { - fetch(canvasEndpoint) - .then((response) => response.json()) - .then((data) => { - canvasState = data; - drawCanvasState(data); - }) - .catch((error) => { - alert("Error fetching canvas state from server. Please try again later."); - }); -}; - -const drawCanvasState = (canvasState) => { - for (let y = 0; y < HEIGHT; y++) { - for (let x = 0; x < WIDTH; x++) { - const index = y * WIDTH + x; - const color = u32ToHex(canvasState[index]); - ctx.fillStyle = color; - ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); - } - } -}; - -const checkIfCanPlacePixel = () => { - const now = Date.now(); - return now - lastPixelTime >= pixelCooldown; -}; - -const setLastPixelTime = () => { - lastPixelTime = Date.now(); - localStorage.setItem("lastPixelTime", lastPixelTime.toString()); -}; - -const handlePlacePixel = (pixelData) => { - if (!checkIfCanPlacePixel()) { - alert("You can't place a pixel yet"); - return; - } - - socket.emit("place-pixel", pixelData); - const index = pixelData.y * WIDTH + pixelData.x; - canvasState[index] = pixelData.color; - setLastPixelTime(); - pixelData = null; -}; - -canvas.addEventListener("click", (event) => { - const rect = canvas.getBoundingClientRect(); - const x = Math.floor((event.clientX - rect.left) / pixelSize); - const y = Math.floor((event.clientY - rect.top) / pixelSize); - - const color = hexToU32(currentColor); - const oldPreviewPixel = previewPixel; - - const update = { x, y, color }; - - if (confirmPlacePixel) { - handlePlacePixel(update); - } else { - previewPixel = update; - } - - if (previewPixel) { - if (oldPreviewPixel) { - ctx.clearRect( - oldPreviewPixel.x * pixelSize, - oldPreviewPixel.y * pixelSize, - pixelSize, - pixelSize - ); - } - - ctx.fillStyle = `rgba(${(color >> 16) & 0xff}, ${(color >> 8) & 0xff}, ${ - color & 0xff - }, 0.5)`; - ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); - } -}); - -const removePreviewPixel = () => { - previewPixel = null; - ctx.clearRect(0, 0, canvas.width, canvas.height); - drawCanvasState(canvasState); -}; - -const drawGrid = () => { - ctx.strokeStyle = "#000"; - for (let x = 0; x < canvas.width; x += pixelSize) { - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, canvas.height); - ctx.stroke(); - } - for (let y = 0; y < canvas.height; y += pixelSize) { - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(canvas.width, y); - ctx.stroke(); - } -}; - -const handleToggleGrid = () => { - toggleGridToggle.addEventListener("change", (event) => { - showGrid = event.target.checked; - localStorage.setItem("showGrid", showGrid); - if (showGrid) { - drawGrid(); - } else { - ctx.clearRect(0, 0, canvas.width, canvas.height); - drawCanvasState(canvasState); - } - }); -}; - -window.onkeydown = (event) => { - // on enter (keycode 13 is enter) - if (event.keyCode === 13) { - if (previewPixel) { - handlePlacePixel(previewPixel); - removePreviewPixel(); - } - } -}; - -placePixelButton.addEventListener("click", () => { - if (previewPixel) { - handlePlacePixel(previewPixel); - removePreviewPixel(); - } -}); - -saveCanvasButton.addEventListener("click", () => { - const a = document.createElement("a"); - a.href = canvas.toDataURL(); - a.download = "canvas.png"; - a.click(); -}); - -handleColorPicker(); -handleToggleGrid(); - -export const handleSocketEvents = (_socket) => { - socket = _socket; - socket.on("connect", () => { - fetchCanvasState(); - }); - - socket.on("pixel-updated", (update) => { - const color = u32ToHex(update.color); - ctx.fillStyle = color; - ctx.fillRect( - update.x * pixelSize, - update.y * pixelSize, - pixelSize, - pixelSize - ); - - const index = update.y * WIDTH + update.x; - canvasState[index] = update.color; - }); -}; diff --git a/painter-js/src/challenge.js b/painter-js/src/challenge.js deleted file mode 100644 index e6494df..0000000 --- a/painter-js/src/challenge.js +++ /dev/null @@ -1,139 +0,0 @@ -import * as three from 'three'; -import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; - -let scene, camera, renderer, character, mixer, clock; -let walkAction, idleAction; -let moving = false; -const keys = {} - -let boxes = []; - -const container = document.getElementById('challenge'); - -const onWindowResize = () => { - camera.aspect = container.clientWidth / container.clientHeight; - camera.updateProjectionMatrix(); - renderer.setSize(container.clientWidth, container.clientHeight); -} - -const onKeyDown = (e) => { - keys[e.code] = true; -} - -const onKeyUp = (e) => { - keys[e.code] = false; -} - -const init = () => { - scene = new three.Scene(); - camera = new three.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); - renderer = new three.WebGLRenderer({ antialias: true}); - renderer.setSize(window.innerWidth, window.innerHeight); - container.appendChild(renderer.domElement); - - clock = new three.Clock(); - - // Adjust light positions and intensity - const light = new three.DirectionalLight(0xffffff, 1); - light.position.set(5, 10, 7.5); - light.castShadow = true; - scene.add(light); - - - - for (let i = 0; i < 10; i++) { - const box = new three.Mesh( - new three.BoxGeometry(1, 1, 1), - new three.MeshStandardMaterial({ color: 0x00ff00 }) - ); - box.position.set(Math.random() * 10 - 5, 0.5, Math.random() * 10 - 5); - box.castShadow = true; - scene.add(box); - boxes.push(box); - } - - const loader = new GLTFLoader(); - loader.load('src/Astronaut.glb',(gltf) => { - character = gltf.scene; - scene.add(character); - - mixer = new three.AnimationMixer(character); - gltf.animations.forEach((clip) => { - if (clip.name === 'CharacterArmature|Walk') { - walkAction = mixer.clipAction(clip); - } - if (clip.name === 'CharacterArmature|Idle') { - idleAction = mixer.clipAction(clip); - } - }); - - if (walkAction) walkAction.play(); - if (idleAction) idleAction.play(); - - character.position.set(-10, 0, -60); - character.rotation.y = Math.PI; - }) - - //blue sky - scene.background = new three.Color(0x87ceeb); - - camera.position.set(20, 10, 10) - camera.lookAt(0, 10, 0) - - window.addEventListener('resize', onWindowResize, false); - document.addEventListener('keydown', onKeyDown, false); - document.addEventListener('keyup', onKeyUp, false); -} - -const animate = () => { - requestAnimationFrame(animate); - - const delta = clock.getDelta(); - if (mixer) mixer.update(delta); - - if (character) { - if (keys['KeyW']) { - character.position.z -= 0.1; - character.rotation.y = Math.PI; // North - } - - if (keys['KeyS']) { - character.position.z += 0.1; - character.rotation.y = 0; // South - } - - if (keys['KeyA']) { - character.position.x -= 0.1; - character.rotation.y = Math.PI / 2; // West - } - - if (keys['KeyD']) { - character.position.x += 0.1; - character.rotation.y = -Math.PI / 2; // East - } - - // check if any key is pressed - if (keys['KeyW'] || keys['KeyS'] || keys['KeyA'] || keys['KeyD']) { - moving = true; - } else { - moving = false; - } - - // Play walking animation if moving - if (mixer) { - if (walkAction && idleAction) { - walkAction.enabled = moving; - idleAction.enabled = !moving; - } - } - } - - renderer.render(scene, camera); -} - -const threejs = import.meta.env.VITE_THREE_JS === "true"; -if (threejs) { - init(); - animate(); -} diff --git a/painter-js/src/constants.js b/painter-js/src/constants.js deleted file mode 100644 index 5f5b670..0000000 --- a/painter-js/src/constants.js +++ /dev/null @@ -1,12 +0,0 @@ -const isDebug = import.meta.env.VITE_IS_DEBUG === "true"; - -export const pixelSize = 10; -export const pixelCooldown = 10 * 1000; // 10 seconds -export const canvasEndpoint = isDebug - ? "http://localhost:3000/canvas/" - : "/canvas/"; -export const checkEndpoint = isDebug - ? "http://localhost:3000/check/" - : "/check/"; -export const WIDTH = 500; -export const HEIGHT = 500; diff --git a/painter-js/src/counter.js b/painter-js/src/counter.js deleted file mode 100644 index 16c3674..0000000 --- a/painter-js/src/counter.js +++ /dev/null @@ -1,20 +0,0 @@ -import { pixelCooldown } from "./constants.js"; - -const countdownDiv = document.getElementById("countdown"); - -export const updateCountdown = () => { - setInterval(() => { - const lastPixelTime = parseInt( - localStorage.getItem("lastPixelTime") || "0" - ); - const now = Date.now(); - const timeLeft = Math.max(0, pixelCooldown - (now - lastPixelTime)); - if (timeLeft > 0) { - countdownDiv.textContent = `You can place a pixel in ${Math.ceil( - timeLeft / 1000 - )} seconds`; - } else { - countdownDiv.textContent = "You can place a pixel now"; - } - }, 1000); -}; diff --git a/painter-js/src/domain/canvas-state.js b/painter-js/src/domain/canvas-state.js new file mode 100644 index 0000000..1d40671 --- /dev/null +++ b/painter-js/src/domain/canvas-state.js @@ -0,0 +1,11 @@ +import { WIDTH } from "./constants.js"; + +export const getPixelIndex = (x, y) => y * WIDTH + x; + +export const setPixel = (state, x, y, color) => { + state[getPixelIndex(x, y)] = color; +}; + +export const getPixel = (state, x, y) => { + return state[getPixelIndex(x, y)]; +}; diff --git a/painter-js/src/utils.js b/painter-js/src/domain/color.js similarity index 66% rename from painter-js/src/utils.js rename to painter-js/src/domain/color.js index e7dac6c..6cf79cc 100644 --- a/painter-js/src/utils.js +++ b/painter-js/src/domain/color.js @@ -6,13 +6,16 @@ export const hexToU32 = (color) => { return parseInt(color.slice(1), 16); }; -export const getColorFromElementCSS = (element) => { - return window.getComputedStyle(element).backgroundColor; -}; - export const rgbToHex = (rgbProperty) => { const rgb = rgbProperty.match(/\d+/g); return `#${rgb .map((x) => parseInt(x).toString(16).padStart(2, "0")) .join("")}`; }; + +export const u32ToRGBA = (color, alpha) => { + const r = (color >> 16) & 0xff; + const g = (color >> 8) & 0xff; + const b = color & 0xff; + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +}; diff --git a/painter-js/src/domain/constants.js b/painter-js/src/domain/constants.js new file mode 100644 index 0000000..73c8a20 --- /dev/null +++ b/painter-js/src/domain/constants.js @@ -0,0 +1,4 @@ +export const PIXEL_SIZE = 1; +export const PIXEL_COOLDOWN = 10 * 1000; +export const WIDTH = 500; +export const HEIGHT = 500; diff --git a/painter-js/src/domain/cooldown.js b/painter-js/src/domain/cooldown.js new file mode 100644 index 0000000..a94269b --- /dev/null +++ b/painter-js/src/domain/cooldown.js @@ -0,0 +1,9 @@ +import { PIXEL_COOLDOWN } from "./constants.js"; + +export const canPlacePixel = (lastPlacementTime) => { + return Date.now() - lastPlacementTime >= PIXEL_COOLDOWN; +}; + +export const timeRemaining = (lastPlacementTime) => { + return Math.max(0, PIXEL_COOLDOWN - (Date.now() - lastPlacementTime)); +}; diff --git a/painter-js/src/domain/coords.js b/painter-js/src/domain/coords.js new file mode 100644 index 0000000..cbbee59 --- /dev/null +++ b/painter-js/src/domain/coords.js @@ -0,0 +1,19 @@ +import { WIDTH, HEIGHT } from "./constants.js"; + +export const getCanvasCoords = (event, canvas) => { + const rect = canvas.getBoundingClientRect(); + const clientX = event.touches ? event.touches[0].clientX : event.clientX; + const clientY = event.touches ? event.touches[0].clientY : event.clientY; + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + return { + x: Math.min( + Math.max(Math.floor((clientX - rect.left) * scaleX), 0), + WIDTH - 1, + ), + y: Math.min( + Math.max(Math.floor((clientY - rect.top) * scaleY), 0), + HEIGHT - 1, + ), + }; +}; diff --git a/painter-js/src/infrastructure/api.js b/painter-js/src/infrastructure/api.js new file mode 100644 index 0000000..0784b7c --- /dev/null +++ b/painter-js/src/infrastructure/api.js @@ -0,0 +1,5 @@ +const isDebug = import.meta.env.VITE_IS_DEBUG === "true"; + +const CHECK_ENDPOINT = isDebug ? "http://localhost:3000/check/" : "/check/"; + +export const checkServer = () => fetch(CHECK_ENDPOINT); diff --git a/painter-js/src/infrastructure/socket-client.js b/painter-js/src/infrastructure/socket-client.js new file mode 100644 index 0000000..08b671a --- /dev/null +++ b/painter-js/src/infrastructure/socket-client.js @@ -0,0 +1,72 @@ +import io from "socket.io-client"; + +const isDebug = import.meta.env.VITE_IS_DEBUG === "true"; +const transport = import.meta.env.VITE_TRANSPORT || "socketio"; + +const createSocketIoTransport = () => { + const url = isDebug ? "ws://localhost:3000" : undefined; + const socket = url ? io(url) : io({ transports: ["websocket"] }); + + return { + on: (event, handler) => socket.on(event, handler), + emit: (event, data) => socket.emit(event, data), + }; +}; + +const createWebSocketTransport = () => { + const handlers = {}; + const wsUrl = isDebug + ? "ws://localhost:3000/ws" + : `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws`; + + const ws = new WebSocket(wsUrl); + ws.binaryType = "arraybuffer"; + + const on = (event, handler) => { + if (!handlers[event]) handlers[event] = []; + handlers[event].push(handler); + }; + + const emit = (event, data) => { + if (ws.readyState !== WebSocket.OPEN) return; + ws.send(JSON.stringify({ type: event, ...data })); + }; + + const dispatch = (event, ...args) => { + (handlers[event] || []).forEach((handler) => handler(...args)); + }; + + ws.addEventListener("open", () => dispatch("connect")); + + ws.addEventListener("message", (event) => { + if (event.data instanceof ArrayBuffer) { + const pixels = new Uint32Array(event.data); + dispatch("canvas_state", Array.from(pixels)); + return; + } + + const message = JSON.parse(event.data); + switch (message.type) { + case "pixel-updated": + dispatch("pixel-updated", message); + break; + case "current_soldiers": + dispatch("current_soldiers", message.count); + break; + case "error": + dispatch("error", message.message); + break; + } + }); + + ws.addEventListener("close", () => dispatch("disconnect")); + + return { on, emit }; +}; + +export const createSocketConnection = () => { + if (transport === "websocket") { + return createWebSocketTransport(); + } + return createSocketIoTransport(); +}; diff --git a/painter-js/src/main.js b/painter-js/src/main.js index 99c1b20..ed544af 100644 --- a/painter-js/src/main.js +++ b/painter-js/src/main.js @@ -1,58 +1,88 @@ -import { connectToWS } from "./socket.js"; -import "./canvas.js"; -import "./counter.js"; -import { updateCountdown } from "./counter.js"; -import { checkEndpoint, pixelSize } from "./constants.js"; -import { handleSocketEvents } from "./canvas.js"; -import "./challenge.js" +import { createSocketConnection } from "./infrastructure/socket-client.js"; +import { checkServer } from "./infrastructure/api.js"; +import { createCanvasRenderer } from "./ui/canvas-renderer.js"; +import { createColorPalette } from "./ui/color-palette.js"; +import { createPixelPlacer } from "./ui/pixel-placer.js"; +import { startCooldownDisplay } from "./ui/cooldown-display.js"; +import { createCanvasViewport } from "./ui/canvas-viewport.js"; +import { setPixel } from "./domain/canvas-state.js"; +import { getCanvasCoords } from "./domain/coords.js"; -const isDebug = import.meta.env.VITE_IS_DEBUG === "true"; - -const currentSoldiersSpan = document.getElementById("current-soldiers"); - -let coords = []; -const canvas = document.getElementById("canvas"); +const canvasEl = document.getElementById("canvas"); const coordsText = document.getElementById("coords"); -const ogCanvasStyle = canvas.style.display; -canvas.style.display = "none"; +const currentSoldiersSpan = document.getElementById("current-soldiers"); +const statusEl = document.getElementById("connection-status"); -fetch(checkEndpoint) +const savedDisplay = canvasEl.style.display; +canvasEl.style.display = "none"; + +let canvasState = []; + +const renderer = createCanvasRenderer(canvasEl); +const palette = createColorPalette(); + +startCooldownDisplay(); +createCanvasViewport(canvasEl); + +canvasEl.addEventListener("mousemove", (event) => { + const { x, y } = getCanvasCoords(event, canvasEl); + coordsText.textContent = `${x}, ${y}`; +}); + +document.getElementById("save-canvas").addEventListener("click", () => { + const a = document.createElement("a"); + a.href = renderer.toDataURL(); + a.download = "canvas.png"; + a.click(); +}); + +const showStatus = (message, isError) => { + if (!statusEl) return; + statusEl.textContent = message; + statusEl.className = isError + ? "text-red-500 text-sm" + : "text-green-500 text-sm"; +}; + +checkServer() .then((response) => { - if (response.ok) { - const socket = connectToWS(); + if (!response.ok) throw new Error("Server unavailable"); - socket.on("connect", () => { - canvas.style.display = ogCanvasStyle; - console.log("connect"); - }); + const socket = createSocketConnection(); - socket.on("error", (message) => { - alert(message); - }); + socket.on("connect", () => { + canvasEl.style.display = savedDisplay; + showStatus("Connected", false); + }); - socket.on("current_soldiers", (currentSoldiers) => { - currentSoldiersSpan.textContent = currentSoldiers; - }); + socket.on("canvas_state", (data) => { + canvasState = data; + renderer.drawState(data); + }); - handleSocketEvents(socket); + socket.on("error", (message) => showStatus(message, true)); - requestAnimationFrame(updateCountdown); + socket.on("current_soldiers", (count) => { + currentSoldiersSpan.textContent = count; + }); - window.addEventListener("mousemove", (event) => { - // get coordinates of the mouse inside the canvas - const rect = canvas.getBoundingClientRect(); - const x = Math.floor((event.clientX - rect.left) / pixelSize); - const y = Math.floor((event.clientY - rect.top) / pixelSize); - coords = [x, y]; + socket.on("pixel-updated", (update) => { + renderer.drawPixel(update.x, update.y, update.color); + setPixel(canvasState, update.x, update.y, update.color); + }); - coordsText.textContent = `${x}, ${y}`; - }); - } else { - throw new Error("Can't connect to the server"); - } + socket.on("disconnect", () => { + showStatus("Disconnected — reconnecting...", true); + }); + + createPixelPlacer({ + canvas: canvasEl, + renderer, + getColor: palette.getColor, + getState: () => canvasState, + socket, + }); }) - .catch((error) => { - alert( - "You have already connected to the server from another tab or window. Please close the other tab or window and refresh this page." - ); + .catch(() => { + showStatus("Cannot connect to server", true); }); diff --git a/painter-js/src/socket.js b/painter-js/src/socket.js deleted file mode 100644 index 5774edb..0000000 --- a/painter-js/src/socket.js +++ /dev/null @@ -1,21 +0,0 @@ -import io from "socket.io-client"; - -const isDebug = import.meta.env.VITE_IS_DEBUG === "true"; -const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; -const wsHost = window.location.host; - -let socket; - -export const connectToWS = () => { - if (isDebug) { - socket = io("ws://localhost:3000"); - } else { - socket = io(`${wsProtocol}//${wsHost}`, { - transports: ["websocket"], - }); - } - - return socket; -}; - -export default socket; diff --git a/painter-js/src/style.css b/painter-js/src/style.css index 15550e5..abd3730 100644 --- a/painter-js/src/style.css +++ b/painter-js/src/style.css @@ -2,20 +2,17 @@ @tailwind components; @tailwind utilities; -@layer base { - .rainbow-border { - position: relative; - display: inline-block; - background: linear-gradient(to right, red, orange, yellow, green, blue, indigo, violet); +.canvas-viewport { + overflow: auto; + width: min(92vw, 70vh); + height: min(92vw, 70vh); + border: 2px solid #94a3b8; + border-radius: 4px; + cursor: crosshair; } -.rainbow-border::before { - content: ''; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: white; - z-index: -1; /* Place behind the content */ + +#canvas { + image-rendering: pixelated; + image-rendering: crisp-edges; + display: block; } -} \ No newline at end of file diff --git a/painter-js/src/ui/canvas-renderer.js b/painter-js/src/ui/canvas-renderer.js new file mode 100644 index 0000000..e8af1a8 --- /dev/null +++ b/painter-js/src/ui/canvas-renderer.js @@ -0,0 +1,39 @@ +import { u32ToHex, u32ToRGBA } from "../domain/color.js"; +import { WIDTH, HEIGHT } from "../domain/constants.js"; + +export const createCanvasRenderer = (canvas) => { + const ctx = canvas.getContext("2d"); + + const drawState = (state) => { + const imageData = ctx.createImageData(WIDTH, HEIGHT); + const data = imageData.data; + for (let i = 0; i < state.length; i++) { + const color = state[i]; + const offset = i * 4; + data[offset] = (color >> 16) & 0xff; + data[offset + 1] = (color >> 8) & 0xff; + data[offset + 2] = color & 0xff; + data[offset + 3] = 255; + } + ctx.putImageData(imageData, 0, 0); + }; + + const drawPixel = (x, y, colorU32) => { + ctx.fillStyle = u32ToHex(colorU32); + ctx.fillRect(x, y, 1, 1); + }; + + const drawPreview = (x, y, colorU32) => { + ctx.fillStyle = u32ToRGBA(colorU32, 0.5); + ctx.fillRect(x, y, 1, 1); + }; + + const restorePixel = (state, x, y) => { + ctx.fillStyle = u32ToHex(state[y * WIDTH + x]); + ctx.fillRect(x, y, 1, 1); + }; + + const toDataURL = () => canvas.toDataURL(); + + return { drawState, drawPixel, drawPreview, restorePixel, toDataURL }; +}; diff --git a/painter-js/src/ui/canvas-viewport.js b/painter-js/src/ui/canvas-viewport.js new file mode 100644 index 0000000..fa8fc18 --- /dev/null +++ b/painter-js/src/ui/canvas-viewport.js @@ -0,0 +1,78 @@ +const MIN_ZOOM = 1; +const MAX_ZOOM = 40; + +export const createCanvasViewport = (canvas) => { + const viewport = canvas.parentElement; + const zoomLabel = document.getElementById("zoom-level"); + let zoom = 1; + let baseSize = viewport.clientWidth; + + const applyZoom = () => { + const size = baseSize * zoom; + canvas.style.width = `${size}px`; + canvas.style.height = `${size}px`; + if (zoomLabel) zoomLabel.textContent = `${zoom.toFixed(1)}x`; + }; + + const setZoom = (newZoom, centerX, centerY) => { + const oldZoom = zoom; + zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, newZoom)); + if (zoom === oldZoom) return; + + if (centerX !== undefined && centerY !== undefined) { + const ratio = zoom / oldZoom; + viewport.scrollLeft = (viewport.scrollLeft + centerX) * ratio - centerX; + viewport.scrollTop = (viewport.scrollTop + centerY) * ratio - centerY; + } + + applyZoom(); + }; + + viewport.addEventListener("wheel", (event) => { + event.preventDefault(); + const factor = event.deltaY > 0 ? 0.8 : 1.25; + const rect = viewport.getBoundingClientRect(); + setZoom(zoom * factor, event.clientX - rect.left, event.clientY - rect.top); + }); + + let lastPinchDist = 0; + viewport.addEventListener("touchmove", (event) => { + if (event.touches.length !== 2) return; + event.preventDefault(); + const dist = Math.hypot( + event.touches[0].clientX - event.touches[1].clientX, + event.touches[0].clientY - event.touches[1].clientY, + ); + if (lastPinchDist > 0) { + const midX = + (event.touches[0].clientX + event.touches[1].clientX) / 2 - + viewport.getBoundingClientRect().left; + const midY = + (event.touches[0].clientY + event.touches[1].clientY) / 2 - + viewport.getBoundingClientRect().top; + setZoom(zoom * (dist / lastPinchDist), midX, midY); + } + lastPinchDist = dist; + }); + + viewport.addEventListener("touchend", () => { + lastPinchDist = 0; + }); + + document.getElementById("zoom-in")?.addEventListener("click", () => { + setZoom(zoom * 1.5); + applyZoom(); + }); + + document.getElementById("zoom-out")?.addEventListener("click", () => { + setZoom(zoom / 1.5); + applyZoom(); + }); + + window.addEventListener("resize", () => { + baseSize = viewport.clientWidth; + applyZoom(); + }); + + applyZoom(); +}; diff --git a/painter-js/src/ui/color-palette.js b/painter-js/src/ui/color-palette.js new file mode 100644 index 0000000..5234a11 --- /dev/null +++ b/painter-js/src/ui/color-palette.js @@ -0,0 +1,53 @@ +import { rgbToHex } from "../domain/color.js"; + +const PALETTE_IDS = [ + "red", + "green", + "blue", + "yellow", + "purple", + "pink", + "cyan", + "white", + "black", + "orange", + "brown", +]; + +export const createColorPalette = () => { + const colorPicker = document.getElementById("color-picker"); + const currentColorSpan = document.getElementById("current-color-span"); + let activeButton = null; + + let currentColor = localStorage.getItem("currentColor") || "#000000"; + colorPicker.value = currentColor; + currentColorSpan.style.backgroundColor = currentColor; + + const setColor = (hex, button) => { + currentColor = hex; + currentColorSpan.textContent = hex; + currentColorSpan.style.backgroundColor = hex; + localStorage.setItem("currentColor", hex); + + if (activeButton) + activeButton.classList.remove("ring-2", "ring-offset-2", "ring-cyan-400"); + if (button) { + button.classList.add("ring-2", "ring-offset-2", "ring-cyan-400"); + activeButton = button; + } + }; + + colorPicker.addEventListener("input", (e) => setColor(e.target.value, null)); + + for (const id of PALETTE_IDS) { + const button = document.getElementById(id); + button.addEventListener("click", () => { + setColor( + rgbToHex(window.getComputedStyle(button).backgroundColor), + button, + ); + }); + } + + return { getColor: () => currentColor }; +}; diff --git a/painter-js/src/ui/cooldown-display.js b/painter-js/src/ui/cooldown-display.js new file mode 100644 index 0000000..eb99b52 --- /dev/null +++ b/painter-js/src/ui/cooldown-display.js @@ -0,0 +1,16 @@ +import { timeRemaining } from "../domain/cooldown.js"; + +export const startCooldownDisplay = () => { + const countdownDiv = document.getElementById("countdown"); + + setInterval(() => { + const lastPlacementTime = parseInt( + localStorage.getItem("lastPixelTime") || "0", + ); + const remaining = timeRemaining(lastPlacementTime); + countdownDiv.textContent = + remaining > 0 + ? `You can place a pixel in ${Math.ceil(remaining / 1000)} seconds` + : "You can place a pixel now"; + }, 1000); +}; diff --git a/painter-js/src/ui/pixel-placer.js b/painter-js/src/ui/pixel-placer.js new file mode 100644 index 0000000..2127eed --- /dev/null +++ b/painter-js/src/ui/pixel-placer.js @@ -0,0 +1,57 @@ +import { hexToU32 } from "../domain/color.js"; +import { canPlacePixel } from "../domain/cooldown.js"; +import { setPixel } from "../domain/canvas-state.js"; +import { getCanvasCoords } from "../domain/coords.js"; + +export const createPixelPlacer = ({ + canvas, + renderer, + getColor, + getState, + socket, +}) => { + let previewPixel = null; + + const place = (update) => { + const lastPixelTime = parseInt( + localStorage.getItem("lastPixelTime") || "0", + ); + if (!canPlacePixel(lastPixelTime)) return; + + socket.emit("place-pixel", update); + setPixel(getState(), update.x, update.y, update.color); + localStorage.setItem("lastPixelTime", Date.now().toString()); + }; + + const clearPreview = () => { + if (!previewPixel) return; + renderer.restorePixel(getState(), previewPixel.x, previewPixel.y); + previewPixel = null; + }; + + const confirmPlacement = () => { + if (!previewPixel) return; + place(previewPixel); + clearPreview(); + }; + + const selectPixel = (event) => { + const { x, y } = getCanvasCoords(event, canvas); + const color = hexToU32(getColor()); + + clearPreview(); + previewPixel = { x, y, color }; + renderer.drawPreview(x, y, color); + }; + + canvas.addEventListener("click", selectPixel); + + window.addEventListener("keydown", (event) => { + if (event.key === "Enter") confirmPlacement(); + if (event.key === "Escape") clearPreview(); + }); + + document + .getElementById("place-pixel") + .addEventListener("click", confirmPlacement); +}; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 2da7653..0000000 --- a/src/main.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - net::SocketAddr, - sync::{Arc, Mutex}, -}; - -use axum::{routing::get, Extension, Json, Router}; -use chrono::{DateTime, Duration, Utc}; -use memory_stats::memory_stats; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use socketioxide::{ - extract::{Bin, Data, SocketRef}, - SocketIo, -}; -use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer}; -use tower_http::{ - cors::{Any, CorsLayer}, - services::ServeDir, -}; -use tracing::info; -use tracing_subscriber::FmtSubscriber; - -type Canvas = Arc>>; -type LastUpdate = Arc>>>; -type Soldiers = Arc>>; - -#[derive(Serialize, Deserialize, Debug)] -struct PixelUpdate { - x: u32, - y: u32, - color: u32, -} - -async fn on_connect( - socket: SocketRef, - canvas: Canvas, - last_update: LastUpdate, - soldiers: Soldiers, -) { - info!("Socket connected: {:?} {:?}", socket.ns(), socket.id,); - - let socket_id = socket.id.to_string(); - - { - let mut soldiers = soldiers.lock().unwrap(); - soldiers.insert(socket_id.clone()); - let soldiers_num = soldiers.len(); - - socket.emit("current_soldiers", &soldiers_num).ok(); - socket - .broadcast() - .emit("current_soldiers", &soldiers_num) - .ok(); - } - - info!("Memory usage after connection and before cloning state: "); - print_memory_usage(); - - let canvas_clone = canvas.clone(); - let last_update_clone = last_update.clone(); - - info!("Memory usage after connection and after cloning state: "); - print_memory_usage(); - - socket.on( - "place-pixel", - move |socket: SocketRef, Data::(update), Bin(_bin)| { - let socket_id = socket.id.to_string(); - let last_update = last_update_clone.clone(); - let canvas = canvas_clone.clone(); - - { - let now = Utc::now(); - let mut last_update = last_update.lock().unwrap(); - - if let Some(&last_time) = last_update.get(&socket_id) { - if now < last_time + Duration::seconds(10) { - let _ = socket.emit( - "error", - Value::String("You can only place one pixel per minute".to_string()), - ); - return; - } - } - - last_update.insert(socket_id.clone(), now); - } - - info!("Received pixel update: {:?}", update); - let mut canvas = canvas.lock().unwrap(); - canvas[update.y as usize * 500 + update.x as usize] = update.color; - - info!("Emitting pixel update"); - socket.emit("pixel-updated", &update).ok(); // Send to the user who placed the pixel - socket.broadcast().emit("pixel-updated", &update).ok(); // Send to all other users - - info!("Memory usage after pixel update: "); - print_memory_usage(); - }, - ); - - socket.on_disconnect(move |socket: SocketRef| { - info!("Socket disconnected: {:?}", socket.id); - info!("Memory usage after disconnection: "); - print_memory_usage(); - let socket_id = socket.id.to_string(); - - { - let mut soldiers = soldiers.lock().unwrap(); - soldiers.remove(&socket_id.to_string()); - info!("Soldiers: {:?}", soldiers.len()); - - let soldiers_num = soldiers.len(); - socket - .broadcast() - .emit("current_soldiers", &soldiers_num) - .ok(); - } - - { - let mut last_update = last_update.lock().unwrap(); - last_update.remove(&socket_id); - - info!("Last update: {:?}", last_update.len()); - } - - info!("Memory usage after disconnection and cleanup: "); - print_memory_usage(); - }); -} - -fn print_memory_usage() { - if let Some(usage) = memory_stats() { - info!( - "Current physical memory usage: {} MB", - usage.physical_mem as f64 / 1024.0 / 1024.0 - ); - info!( - "Current virtual memory usage: {} MB", - usage.virtual_mem as f64 / 1024.0 / 1024.0 - ); - } -} - -async fn get_canvas_state(Extension(canvas): Extension) -> Json> { - let canvas = canvas.lock().unwrap(); - Json(canvas.clone()) -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - dotenv::dotenv().ok(); - tracing::subscriber::set_global_default(FmtSubscriber::new())?; - - let rate_governor = Arc::new( - GovernorConfigBuilder::default() - .burst_size(10) - .per_second(10) - .finish() - .unwrap(), - ); - - let governor = rate_governor.limiter().clone(); - let interval = std::time::Duration::from_secs(1); - - tokio::spawn(async move { - loop { - tokio::time::sleep(interval).await; - governor.retain_recent(); - } - }); - - let (layer, io) = SocketIo::new_layer(); - - let canvas = Arc::new(Mutex::new(vec![0xFFFFFFFF; 500 * 500])); - let last_update = Arc::new(Mutex::new(HashMap::new())); - let soldiers = Arc::new(Mutex::new(HashSet::new())); - - let used_memory = std::mem::size_of_val(&*canvas) - + std::mem::size_of_val(&*last_update) - + std::mem::size_of_val(&*soldiers); - info!( - "Used memory of state: {} bytes, {} KB, {} MB", - used_memory, - used_memory / 1024, - used_memory / 1024 / 1024 - ); - - info!("Memory usage after state setup and before socket.io setup: "); - print_memory_usage(); - - let canvas_for_socket = canvas.clone(); - - io.ns("/", move |socket: SocketRef| { - tokio::spawn(on_connect( - socket, - canvas_for_socket.clone(), - last_update.clone(), - soldiers.clone(), - )); - }); - - info!("Memory usage after socket.io setup: "); - print_memory_usage(); - - let address = std::env::var("ADDRESS").unwrap_or_else(|_| "0.0.0.0".to_string()); - let port: u16 = std::env::var("PORT") - .unwrap_or_else(|_| "3000".to_string()) - .parse() - .expect("Invalid port"); - let enable_cors = std::env::var("ENABLE_CORS").unwrap_or_else(|_| "true".to_string()) == "true"; - - let cors = if enable_cors { - Some( - CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any), - ) - } else { - None - }; - - let app = Router::new() - .route("/canvas/", get(get_canvas_state)) - .route("/check/", get(|| async { "OK" })) - .fallback_service(ServeDir::new("dist/")) - .layer(GovernorLayer { - config: rate_governor, - }) - .layer(layer) - .layer(Extension(canvas.clone())); - - let app = if let Some(cors) = cors { - app.layer(cors) - } else { - app - }; - - let server_address = format!("{}:{}", address, port); - info!("Starting server on {}", server_address); - let listener = tokio::net::TcpListener::bind(server_address).await?; - axum::serve( - listener, - app.into_make_service_with_connect_info::(), - ) - .await?; - - Ok(()) -}