diff --git a/.github/workflows/adapter-ci/docker-compose.yml b/.github/workflows/adapter-ci/docker-compose.yml index c0ce6637..b9e0ca03 100644 --- a/.github/workflows/adapter-ci/docker-compose.yml +++ b/.github/workflows/adapter-ci/docker-compose.yml @@ -140,3 +140,17 @@ services: '; wait " + + postgres: + image: postgres:18-alpine + ports: + - 5432:5432 + environment: + POSTGRES_DB: socketio + POSTGRES_PASSWORD: socketio + POSTGRES_USER: socketio + healthcheck: + test: "pg_isready -U socketio" + interval: 2s + timeout: 5s + retries: 5 diff --git a/Cargo.lock b/Cargo.lock index 5e7d67f2..845b0369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,7 @@ dependencies = [ "hyper-util", "socketioxide", "socketioxide-mongodb", + "socketioxide-postgres", "socketioxide-redis", "tokio", "tracing", @@ -24,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -32,13 +33,19 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anes" version = "0.1.6" @@ -47,15 +54,15 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "approx" @@ -77,9 +84,12 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] [[package]] name = "arcstr" @@ -100,15 +110,24 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -117,15 +136,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", "bytes", @@ -156,9 +175,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -179,31 +198,11 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -235,8 +234,8 @@ dependencies = [ "ahash", "base64", "bitvec", - "getrandom 0.2.16", - "getrandom 0.3.3", + "getrandom 0.2.17", + "getrandom 0.3.4", "hex", "indexmap", "js-sys", @@ -258,7 +257,7 @@ dependencies = [ "ahash", "base64", "bitvec", - "getrandom 0.3.3", + "getrandom 0.3.4", "hex", "indexmap", "js-sys", @@ -273,9 +272,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -310,27 +309,19 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.26" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ + "find-msvc-tools", "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -376,31 +367,20 @@ dependencies = [ "half", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" -version = "4.5.40" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", @@ -408,34 +388,33 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "codspeed" -version = "4.0.4" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f62ea8934802f8b374bf691eea524c3aa444d7014f604dd4182a3667b69510" +checksum = "b684e94583e85a5ca7e1a6454a89d76a5121240f2fb67eb564129d9bafdb9db0" dependencies = [ "anyhow", - "bindgen", "cc", "colored", + "getrandom 0.2.17", "glob", "libc", "nix", "serde", "serde_json", "statrs", - "uuid", ] [[package]] name = "codspeed-criterion-compat" -version = "4.0.4" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87efbc015fc0ff1b2001cd87df01c442824de677e01a77230bf091534687abb" +checksum = "2e65444156eb73ad7f57618188f8d4a281726d133ef55b96d1dcff89528609ab" dependencies = [ "clap", "codspeed", @@ -446,9 +425,9 @@ dependencies = [ [[package]] name = "codspeed-criterion-compat-walltime" -version = "4.0.4" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5713ace440123bb4f1f78dd068d46872cb8548bfe61f752e7b2ad2c06d7f00" +checksum = "96389aaa4bbb872ea4924dc0335b2bb181bcf28d6eedbe8fea29afcc5bde36a6" dependencies = [ "anes", "cast", @@ -519,11 +498,20 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie-factory" version = "0.3.2" @@ -548,6 +536,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc16" version = "0.4.0" @@ -583,6 +586,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -591,15 +603,15 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -607,9 +619,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -617,11 +629,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -631,9 +642,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", @@ -642,15 +653,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -668,9 +679,9 @@ dependencies = [ [[package]] name = "derive-where" -version = "1.4.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73f2692d4bd3cac41dca28934a39894200c9fabf49586d77d0e5954af1d7902" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", @@ -679,9 +690,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", @@ -690,21 +701,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", + "rustc_version", "syn", "unicode-xid", ] @@ -731,11 +744,20 @@ dependencies = [ "syn", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "engineioxide" @@ -801,6 +823,27 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -822,6 +865,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "float-cmp" version = "0.10.0" @@ -831,12 +886,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" @@ -845,9 +894,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -897,9 +946,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -928,15 +977,26 @@ checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -1008,40 +1068,40 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "r-efi 5.3.0", + "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -1055,23 +1115,41 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heaptrack" version = "0.1.0" @@ -1093,9 +1171,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1103,6 +1181,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1112,6 +1199,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -1195,9 +1291,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1208,9 +1304,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1221,11 +1317,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1236,42 +1331,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1293,9 +1384,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1314,24 +1405,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1345,15 +1437,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" dependencies = [ "once_cell", "wasm-bindgen", @@ -1373,41 +1465,42 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] -name = "libloading" -version = "0.8.9" +name = "libredox" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ - "cfg-if", - "windows-link", + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.3", ] [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loom" @@ -1505,9 +1598,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -1523,13 +1616,13 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", ] [[package]] @@ -1552,9 +1645,9 @@ checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" [[package]] name = "mongodb" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803dd859e8afa084c255a8effd8000ff86f7c8076a50cd6d8c99e8f3496f75c2" +checksum = "2c5941683db2ab2697f71e58dc0319024e808d3b28e7cf20f4bfb445fe54a30b" dependencies = [ "base64", "bitflags", @@ -1581,7 +1674,7 @@ dependencies = [ "serde_with", "sha1", "sha2", - "socket2 0.6.0", + "socket2 0.6.3", "stringprep", "strsim", "take_mut", @@ -1596,9 +1689,9 @@ dependencies = [ [[package]] name = "mongodb-internal-macros" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973ef3dd3dbc6f6e65bbdecfd9ec5e781b9e7493b0f369a7c62e35d8e5ae2c8" +checksum = "47021a12bbf0dffde9c890fa2d36ff6ae342c532016226b04a42301b2b912660" dependencies = [ "macro_magic", "proc-macro2", @@ -1608,9 +1701,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.30.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ "bitflags", "cfg-if", @@ -1630,18 +1723,18 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-traits" @@ -1652,11 +1745,29 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -1672,9 +1783,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1682,23 +1793,17 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pbkdf2" version = "0.12.2" @@ -1710,15 +1815,34 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1726,6 +1850,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plotters" version = "0.3.7" @@ -1754,11 +1884,40 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "postgres-protocol" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.9.2", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1780,9 +1939,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -1790,27 +1949,33 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radium" @@ -1836,7 +2001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1846,7 +2011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -1867,7 +2032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1876,16 +2041,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -1896,9 +2061,9 @@ checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1906,9 +2071,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1933,7 +2098,7 @@ dependencies = [ "pin-project-lite", "rand 0.9.2", "ryu", - "socket2 0.6.0", + "socket2 0.6.3", "tokio", "tokio-util", "url", @@ -1956,18 +2121,27 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1977,9 +2151,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1988,9 +2162,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ring" @@ -2000,7 +2174,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2008,44 +2182,34 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" dependencies = [ - "byteorder", "num-traits", - "paste", ] [[package]] name = "rmp-serde" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" dependencies = [ - "byteorder", "rmp", "serde", ] [[package]] name = "rmpv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" +checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417" dependencies = [ - "num-traits", "rmp", "serde", "serde_bytes", ] -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -2067,9 +2231,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", @@ -2082,9 +2246,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] @@ -2102,15 +2266,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -2135,15 +2299,15 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2151,27 +2315,28 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2194,12 +2359,13 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -2216,20 +2382,19 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ - "serde", - "serde_derive", + "serde_core", "serde_with_macros", ] [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", @@ -2276,10 +2441,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2289,14 +2455,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2319,12 +2488,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2437,6 +2606,27 @@ dependencies = [ "socketioxide-core", ] +[[package]] +name = "socketioxide-postgres" +version = "0.1.0" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "pin-project-lite", + "serde", + "serde_json", + "smallvec", + "socketioxide", + "socketioxide-core", + "sqlx", + "thiserror", + "tokio", + "tokio-postgres", + "tracing", + "tracing-subscriber", +] + [[package]] name = "socketioxide-redis" version = "0.4.0" @@ -2459,11 +2649,129 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-postgres", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-postgres", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami 1.6.1", +] + [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state" @@ -2509,9 +2817,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.101" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2569,40 +2877,39 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -2619,9 +2926,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2639,9 +2946,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2664,27 +2971,53 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.0", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tokio-postgres" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.9.2", + "socket2 0.6.3", + "tokio", + "tokio-util", + "whoami 2.1.1", +] + [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2692,9 +3025,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2729,9 +3062,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -2801,9 +3134,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -2855,9 +3188,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-bidi" @@ -2867,24 +3200,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -2906,13 +3239,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -2929,13 +3263,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.17.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.2", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -2963,17 +3297,17 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", ] [[package]] @@ -2995,36 +3329,38 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasite" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasm-bindgen" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3032,22 +3368,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" dependencies = [ "unicode-ident", ] @@ -3081,16 +3417,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", - "hashbrown", + "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" dependencies = [ "js-sys", "wasm-bindgen", @@ -3098,20 +3434,43 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite 0.1.0", +] + +[[package]] +name = "whoami" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite 1.0.2", + "web-sys", +] + [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3129,6 +3488,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3297,15 +3665,6 @@ dependencies = [ "wit-parser", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] - [[package]] name = "wit-bindgen-rust" version = "0.51.0" @@ -3376,9 +3735,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -3397,11 +3756,10 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3409,9 +3767,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3421,18 +3779,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -3462,15 +3820,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -3479,9 +3837,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3490,9 +3848,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -3501,6 +3859,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.10" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e0d8dffbae3d840f64bda38e28391faef673a7b5a6017840f2a106c8145868" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index af7063a2..5485323b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace.package] edition = "2024" -rust-version = "1.86.0" +rust-version = "1.88.0" authors = ["Théodore Prévot <"] repository = "https://github.com/totodore/socketioxide" homepage = "https://github.com/totodore/socketioxide" diff --git a/crates/socketioxide-postgres/Cargo.toml b/crates/socketioxide-postgres/Cargo.toml new file mode 100644 index 00000000..21724c6f --- /dev/null +++ b/crates/socketioxide-postgres/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "socketioxide-postgres" +description = "PostgreSQL adapter for socketioxide" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +license.workspace = true +readme = "README.md" + +[features] +sqlx = ["dep:sqlx"] +postgres = ["dep:tokio-postgres"] +default = ["postgres"] + +[dependencies] +socketioxide-core = { version = "0.17", path = "../socketioxide-core", features = [ + "remote-adapter", +] } +futures-core.workspace = true +futures-util.workspace = true +pin-project-lite.workspace = true +serde.workspace = true +serde_json = { workspace = true, features = ["raw_value"] } +smallvec = { workspace = true, features = ["serde"] } +tokio = { workspace = true, features = ["time", "rt", "sync"] } +tracing.workspace = true +thiserror.workspace = true + +# PostgreSQL implementations +tokio-postgres = { version = "0.7", default-features = false, optional = true, features = [ + "runtime", +] } +sqlx = { version = "0.8", default-features = false, optional = true, features = [ + "postgres", + "runtime-tokio", +] } + +[dev-dependencies] +tokio = { workspace = true, features = [ + "macros", + "parking_lot", + "rt-multi-thread", +] } +socketioxide = { path = "../socketioxide", features = [ + "tracing", + "__test_harness", +] } +tracing-subscriber.workspace = true +bytes.workspace = true +futures-util.workspace = true + +# docs.rs-specific configuration +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/socketioxide-postgres/README.md b/crates/socketioxide-postgres/README.md new file mode 100644 index 00000000..dd9781db --- /dev/null +++ b/crates/socketioxide-postgres/README.md @@ -0,0 +1,137 @@ +# [`Socketioxide-Postgres`](https://github.com/totodore/socketioxide) 🚀🦀 + +A [***`socket.io`***](https://socket.io) adapter for [***`Socketioxide`***](https://github.com/totodore/socketioxide), using [PostgreSQL LISTEN/NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) for event broadcasting. This adapter enables **horizontal scaling** of your Socketioxide servers across distributed deployments by leveraging PostgreSQL as a message bus. + +[![Crates.io](https://img.shields.io/crates/v/socketioxide-postgres.svg)](https://crates.io/crates/socketioxide-postgres) +[![Documentation](https://docs.rs/socketioxide-postgres/badge.svg)](https://docs.rs/socketioxide-postgres) +[![CI](https://github.com/Totodore/socketioxide/actions/workflows/github-ci.yml/badge.svg)](https://github.com/Totodore/socketioxide/actions/workflows/github-ci.yml) + + + +## Features + +- **PostgreSQL LISTEN/NOTIFY-based adapter** +- **Support for any PostgreSQL client** via the [`Driver`] abstraction +- Built-in driver for the [sqlx](https://docs.rs/sqlx) crate: [`SqlxDriver`](https://docs.rs/socketioxide-postgres/latest/socketioxide_postgres/drivers/sqlx/struct.SqlxDriver.html) +- **Heartbeat-based liveness detection** for tracking active server nodes +- Fully compatible with the asynchronous Rust ecosystem +- Implement your own custom driver by implementing the `Driver` trait + +> [!WARNING] +> This adapter is **not compatible** with [`@socket.io/postgres-adapter`](https://github.com/socketio/socket.io-postgres-adapter). +> These projects use entirely different protocols and cannot interoperate. +> **Do not mix Socket.IO JavaScript servers with Socketioxide Rust servers**. + + + +## Example: Using the PostgreSQL Adapter with Axum + +```rust +use serde::{Deserialize, Serialize}; +use socketioxide::{ + adapter::Adapter, + extract::{Data, Extension, SocketRef}, + SocketIo, +}; +use socketioxide_postgres::{ + drivers::sqlx::sqlx_client::{self as sqlx, PgPool}, + SqlxAdapter, PostgresAdapterCtr, PostgresAdapterConfig, +}; +use tower::ServiceBuilder; +use tower_http::{cors::CorsLayer, services::ServeDir}; +use tracing::info; +use tracing_subscriber::FmtSubscriber; + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(transparent)] +struct Username(String); + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase", untagged)] +enum Res { + Message { + username: Username, + message: String, + }, + Username { + username: Username, + }, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let subscriber = FmtSubscriber::new(); + + tracing::subscriber::set_global_default(subscriber)?; + + info!("Starting server"); + + let pool = PgPool::connect("postgres://user:password@localhost/socketio").await?; + let adapter = PostgresAdapterCtr::new_with_sqlx(pool); + + let (layer, io) = SocketIo::builder() + .with_adapter::>(adapter) + .build_layer(); + io.ns("/", on_connect).await?; + + let app = axum::Router::new() + .fallback_service(ServeDir::new("dist")) + .layer( + ServiceBuilder::new() + .layer(CorsLayer::permissive()) // Enable CORS policy + .layer(layer), + ); + + let port = std::env::var("PORT") + .map(|s| s.parse().unwrap()) + .unwrap_or(3000); + let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)) + .await + .unwrap(); + axum::serve(listener, app).await.unwrap(); + + Ok(()) +} + +async fn on_connect(socket: SocketRef) { + socket.on("new message", on_msg); + socket.on("typing", on_typing); + socket.on("stop typing", on_stop_typing); +} +async fn on_msg( + s: SocketRef, + Data(msg): Data, + Extension(username): Extension, +) { + let msg = &Res::Message { + username, + message: msg, + }; + s.broadcast().emit("new message", msg).await.ok(); +} +async fn on_typing(s: SocketRef, Extension(username): Extension) { + s.broadcast() + .emit("typing", &Res::Username { username }) + .await + .ok(); +} +async fn on_stop_typing(s: SocketRef, Extension(username): Extension) { + s.broadcast() + .emit("stop typing", &Res::Username { username }) + .await + .ok(); +} + +``` + + + +## Contributions and Feedback / Questions + +Contributions are very welcome! Feel free to open an issue or a PR. If you're unsure where to start, check the [issues](https://github.com/totodore/socketioxide/issues). + +For feedback or questions, join the discussion on the [discussions](https://github.com/totodore/socketioxide/discussions) page. + +## License 🔐 + +This project is licensed under the [MIT license](./LICENSE). diff --git a/crates/socketioxide-postgres/src/drivers/mod.rs b/crates/socketioxide-postgres/src/drivers/mod.rs new file mode 100644 index 00000000..10e1f5cd --- /dev/null +++ b/crates/socketioxide-postgres/src/drivers/mod.rs @@ -0,0 +1,47 @@ +//! Drivers are an abstraction over the PostgreSQL LISTEN/NOTIFY backend used by the adapter. +//! You can use the provided implementation or implement your own. + +use futures_core::Stream; + +/// A driver implementation for the [`sqlx`](https://docs.rs/sqlx) PostgreSQL backend. +#[cfg(feature = "sqlx")] +pub mod sqlx; + +// #[cfg(feature = "postgres")] +// pub mod postgres; + +/// The driver trait can be used to support different LISTEN/NOTIFY backends. +/// It must share handlers/connection between its clones. +pub trait Driver: Clone + Send + Sync + 'static { + /// The error type returned by the driver. + type Error: std::error::Error + Send + 'static; + /// The notification type yielded by the notification stream. + type Notification: Notification; + /// The stream of notifications returned by [`Driver::listen`]. + type NotificationStream: Stream + Send; + + /// Initialize the driver. This is called once when the adapter is created. + /// It should create the necessary tables or schema if needed. + fn init(&self, table: &str) -> impl Future> + Send; + + /// Subscribe to the given NOTIFY channels and return a stream of notifications. + fn listen( + &self, + channels: &[&str], + ) -> impl Future> + Send; + + /// Send a NOTIFY message on the given channel with the given payload. + fn notify( + &self, + channel: &str, + message: &str, + ) -> impl Future> + Send; +} + +/// A trait representing a PostgreSQL NOTIFY notification. +pub trait Notification: Send + 'static { + /// The channel name on which the notification was received. + fn channel(&self) -> &str; + /// The payload of the notification. + fn payload(&self) -> &str; +} diff --git a/crates/socketioxide-postgres/src/drivers/postgres.rs b/crates/socketioxide-postgres/src/drivers/postgres.rs new file mode 100644 index 00000000..fea516a5 --- /dev/null +++ b/crates/socketioxide-postgres/src/drivers/postgres.rs @@ -0,0 +1,63 @@ +use std::{pin::Pin, sync::Arc}; + +use futures_core::{Stream, stream::BoxStream}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_postgres::{AsyncMessage, Client, Config, Connection, Socket}; + +use super::Driver; + +#[derive(Debug, Clone)] +pub struct PostgresDriver { + client: Arc, + config: Config, +} + +impl PostgresDriver { + pub fn new(config: Config) -> Self + where + T: AsyncRead + AsyncWrite + Unpin, + { + PostgresDriver { config } + } +} + +impl Driver for PostgresDriver { + type Error = tokio_postgres::Error; + type Notification = tokio_postgres::Notification; + type NotificationStream = BoxStream<'static, Self::Notification>; + + async fn init( + &self, + table: &str, + channels: &[&str], + ) -> Result { + let st = &format!( + r#"CREATE TABLE IF NOT EXISTS "{table}" ( + id BIGSERIAL UNIQUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + payload BYTEA + )"# + ); + + self.client.execute(st, &[]).await?; + + Ok(()) + } + + async fn notify(&self, channel: &str, message: &str) -> Result<(), Self::Error> { + self.client + .execute("SELECT pg_notify($1, $2)", &[&channel, &message]) + .await?; + Ok(()) + } +} + +impl super::Notification for tokio_postgres::Notification { + fn channel(&self) -> &str { + tokio_postgres::Notification::channel(self) + } + + fn payload(&self) -> &str { + tokio_postgres::Notification::payload(self) + } +} diff --git a/crates/socketioxide-postgres/src/drivers/sqlx.rs b/crates/socketioxide-postgres/src/drivers/sqlx.rs new file mode 100644 index 00000000..3241dfac --- /dev/null +++ b/crates/socketioxide-postgres/src/drivers/sqlx.rs @@ -0,0 +1,79 @@ +use futures_core::stream::BoxStream; +use futures_util::StreamExt; +use sqlx::{ + PgPool, + postgres::{PgListener, PgNotification}, +}; + +use super::Driver; + +pub use sqlx as sqlx_client; + +/// A [`Driver`] implementation using the [`sqlx`] PostgreSQL client. +/// +/// It uses [`PgListener`] for LISTEN/NOTIFY and [`PgPool`] for queries. +#[derive(Debug, Clone)] +pub struct SqlxDriver { + client: PgPool, +} + +impl SqlxDriver { + /// Create a new SqlxDriver instance. + pub fn new(client: PgPool) -> Self { + Self { client } + } +} + +impl Driver for SqlxDriver { + type Error = sqlx::Error; + type Notification = PgNotification; + type NotificationStream = BoxStream<'static, Self::Notification>; + + async fn init(&self, table: &str) -> Result<(), Self::Error> { + sqlx::query(&format!( + r#"CREATE TABLE IF NOT EXISTS "{table}" ( + id BIGSERIAL UNIQUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + payload BYTEA + )"#, + )) + .execute(&self.client) + .await?; + + Ok(()) + } + + async fn listen(&self, channels: &[&str]) -> Result { + let mut listener = PgListener::connect_with(&self.client).await?; + listener.listen_all(channels.iter().copied()).await?; + + let stream = listener.into_stream(); + let stream = stream.filter_map(async |res| { + res.inspect_err(|err| { + tracing::warn!("failed to pull sqlx notification from stream: {err}") + }) + .ok() + }); + + Ok(Box::pin(stream)) + } + + async fn notify(&self, channel: &str, message: &str) -> Result<(), Self::Error> { + sqlx::query("SELECT pg_notify($1, $2)") + .bind(channel) + .bind(message) + .execute(&self.client) + .await?; + Ok(()) + } +} + +impl super::Notification for PgNotification { + fn channel(&self) -> &str { + PgNotification::channel(self) + } + + fn payload(&self) -> &str { + PgNotification::payload(self) + } +} diff --git a/crates/socketioxide-postgres/src/lib.rs b/crates/socketioxide-postgres/src/lib.rs new file mode 100644 index 00000000..ec09d9e7 --- /dev/null +++ b/crates/socketioxide-postgres/src/lib.rs @@ -0,0 +1,917 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![warn( + clippy::all, + clippy::todo, + clippy::empty_enums, + clippy::mem_forget, + clippy::unused_self, + clippy::filter_map_next, + clippy::needless_continue, + clippy::needless_borrow, + clippy::match_wildcard_for_single_variants, + clippy::if_let_mutex, + clippy::await_holding_lock, + clippy::indexing_slicing, + clippy::imprecise_flops, + clippy::suboptimal_flops, + clippy::lossy_float_literal, + clippy::rest_pat_in_fully_bound_structs, + clippy::fn_params_excessive_bools, + clippy::exit, + clippy::inefficient_to_string, + clippy::linkedlist, + clippy::macro_use_imports, + clippy::option_option, + clippy::verbose_file_reads, + clippy::unnested_or_patterns, + rust_2018_idioms, + future_incompatible, + nonstandard_style, + missing_docs +)] +//! # A PostgreSQL adapter implementation for the socketioxide crate. +//! The adapter is used to communicate with other nodes of the same application. +//! This allows to broadcast messages to sockets connected on other servers, +//! to get the list of rooms, to add or remove sockets from rooms, etc. +//! +//! To achieve this, the adapter uses [LISTEN/NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) +//! through PostgreSQL to communicate with other servers. +//! +//! The [`Driver`] abstraction allows the use of any PostgreSQL client. +//! One implementation is provided: +//! * [`SqlxDriver`](crate::drivers::sqlx::SqlxDriver) for the [`sqlx`] crate. +//! +//! You can also implement your own driver by implementing the [`Driver`] trait. +//! +//!
+//! Socketioxide-postgres is not compatible with @socketio/postgres-adapter. +//! They use completely different protocols and cannot be used together. +//! Do not mix socket.io JS servers with socketioxide rust servers. +//!
+//! +//! ## How does it work? +//! +//! The [`PostgresAdapterCtr`] is a constructor for the [`SqlxAdapter`] which is an implementation of +//! the [`Adapter`](https://docs.rs/socketioxide/latest/socketioxide/adapter/trait.Adapter.html) trait. +//! +//! Then, for each namespace, an adapter is created and it takes a corresponding [`CoreLocalAdapter`]. +//! The [`CoreLocalAdapter`] allows to manage the local rooms and local sockets. The default `LocalAdapter` +//! is simply a wrapper around this [`CoreLocalAdapter`]. +//! +//! Once it is created the adapter is initialized with the [`CustomPostgresAdapter::init`] method. +//! It will subscribe to three PostgreSQL NOTIFY channels and emit heartbeats. +//! All messages are encoded with JSON. +//! +//! There are 7 types of requests: +//! * Broadcast a packet to all the matching sockets. +//! * Broadcast a packet to all the matching sockets and wait for a stream of acks. +//! * Disconnect matching sockets. +//! * Get all the rooms. +//! * Add matching sockets to rooms. +//! * Remove matching sockets from rooms. +//! * Fetch all the remote sockets matching the options. +//! * Heartbeat +//! * Initial heartbeat. When receiving an initial heartbeat all other servers reply a heartbeat immediately. +//! +//! For ack streams, the adapter will first send a `BroadcastAckCount` response to the server that sent the request, +//! and then send the acks as they are received (more details in [`CustomPostgresAdapter::broadcast_with_ack`] fn). +//! +//! On the other side, each time an action has to be performed on the local server, the adapter will +//! first broadcast a request to all the servers and then perform the action locally. + +use drivers::Driver; +use futures_core::Stream; +use futures_util::{StreamExt, pin_mut}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::value::RawValue; +use socketioxide_core::{ + Sid, Uid, + adapter::{ + BroadcastOptions, CoreAdapter, CoreLocalAdapter, DefinedAdapter, RemoteSocketData, Room, + RoomParam, SocketEmitter, Spawnable, + errors::{AdapterError, BroadcastError}, + remote_packet::{ + RequestIn, RequestOut, RequestTypeIn, RequestTypeOut, Response, ResponseType, + ResponseTypeId, + }, + }, + packet::Packet, +}; +use std::{ + borrow::Cow, + collections::HashMap, + fmt, future, + pin::Pin, + sync::{Arc, Mutex}, + task::{Context, Poll}, + time::{Duration, Instant}, +}; +use tokio::sync::mpsc; + +use crate::{ + drivers::Notification, + stream::{AckStream, ChanStream}, +}; + +pub mod drivers; +mod stream; + +/// The configuration of the [`CustomPostgresAdapter`]. +#[derive(Debug, Clone)] +pub struct PostgresAdapterConfig { + /// The heartbeat timeout duration. If a remote node does not respond within this duration, + /// it will be considered disconnected. Default is 60 seconds. + pub hb_timeout: Duration, + /// The heartbeat interval duration. The current node will broadcast a heartbeat to the + /// remote nodes at this interval. Default is 10 seconds. + pub hb_interval: Duration, + /// The request timeout. When expecting a response from remote nodes, if they do not respond within + /// this duration, the request will be considered failed. Default is 5 seconds. + pub request_timeout: Duration, + /// The channel size used to receive ack responses. Default is 255. + /// + /// If you have a lot of servers/sockets and that you may miss acknowledgement because they arrive faster + /// than you poll them with the returned stream, you might want to increase this value. + pub ack_response_buffer: usize, + /// The table name used to store socket.io attachments. Default is "socket_io_attachments". + /// + /// > The table name must be a sanitized string. Do not use special characters or spaces. + pub table_name: Cow<'static, str>, + /// The prefix used for the channels. Default is "socket.io". + pub prefix: Cow<'static, str>, + /// The threshold to the payload size in bytes. It should match the configured value on your PostgreSQL instance: + /// . By default it is 8KB (8000 bytes). + pub payload_threshold: usize, + /// The duration between cleanup queries on the attachment table. + pub cleanup_interval: Duration, +} + +impl PostgresAdapterConfig { + /// Create a new [`PostgresAdapterConfig`] with default values. + pub fn new() -> Self { + Self::default() + } + + /// The heartbeat timeout duration. If a remote node does not respond within this duration, + /// it will be considered disconnected. Default is 60 seconds. + pub fn with_hb_timeout(mut self, hb_timeout: Duration) -> Self { + self.hb_timeout = hb_timeout; + self + } + + /// The heartbeat interval duration. The current node will broadcast a heartbeat to the + /// remote nodes at this interval. Default is 10 seconds. + pub fn with_hb_interval(mut self, hb_interval: Duration) -> Self { + self.hb_interval = hb_interval; + self + } + + /// The request timeout. When expecting a response from remote nodes, if they do not respond within + /// this duration, the request will be considered failed. Default is 5 seconds. + pub fn with_request_timeout(mut self, request_timeout: Duration) -> Self { + self.request_timeout = request_timeout; + self + } + + /// The channel size used to receive ack responses. Default is 255. + /// + /// If you have a lot of servers/sockets and that you may miss acknowledgement because they arrive faster + /// than you poll them with the returned stream, you might want to increase this value. + pub fn with_ack_response_buffer(mut self, ack_response_buffer: usize) -> Self { + self.ack_response_buffer = ack_response_buffer; + self + } + + /// The table name used to store socket.io attachments. Default is "socket_io_attachments". + /// + /// > The table name must be a sanitized string. Do not use special characters or spaces. + pub fn with_table_name(mut self, table_name: impl Into>) -> Self { + self.table_name = table_name.into(); + self + } + + /// The prefix used for the channels. Default is "socket.io". + pub fn with_prefix(mut self, prefix: impl Into>) -> Self { + self.prefix = prefix.into(); + self + } + + /// The threshold to the payload size in bytes. It should match the configured value on your PostgreSQL instance: + /// . By default it is 8KB (8000 bytes). + pub fn with_payload_threshold(mut self, payload_threshold: usize) -> Self { + self.payload_threshold = payload_threshold; + self + } + + /// The duration between cleanup queries on the attachment table. Default is 60 seconds. + pub fn with_cleanup_interval(mut self, cleanup_interval: Duration) -> Self { + self.cleanup_interval = cleanup_interval; + self + } +} + +impl Default for PostgresAdapterConfig { + fn default() -> Self { + Self { + hb_timeout: Duration::from_secs(60), + hb_interval: Duration::from_secs(10), + request_timeout: Duration::from_secs(5), + ack_response_buffer: 255, + table_name: "socket_io_attachments".into(), + prefix: "socket.io".into(), + payload_threshold: 8_000, + cleanup_interval: Duration::from_secs(60), + } + } +} + +/// Represent any error that might happen when using this adapter. +#[derive(thiserror::Error)] +pub enum Error { + /// Postgres driver error + #[error("driver error: {0}")] + Driver(D::Error), + /// Packet encoding/decoding error + #[error("packet decoding error: {0}")] + Serde(#[from] serde_json::Error), +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Driver(err) => write!(f, "Driver error: {:?}", err), + Self::Serde(err) => write!(f, "Encode/Decode error: {:?}", err), + } + } +} + +impl From> for AdapterError { + fn from(err: Error) -> Self { + AdapterError::from(Box::new(err) as Box) + } +} + +/// The adapter constructor. For each namespace you define, a new adapter instance is created +/// from this constructor. +#[derive(Debug, Clone)] +pub struct PostgresAdapterCtr { + driver: D, + config: PostgresAdapterConfig, +} + +#[cfg(feature = "sqlx")] +impl PostgresAdapterCtr { + /// Create a new adapter constructor with the [`sqlx`](drivers::sqlx) driver + /// and a default config. + pub fn new_with_sqlx(pool: drivers::sqlx::sqlx_client::PgPool) -> Self { + Self::new_with_sqlx_config(pool, PostgresAdapterConfig::default()) + } + + /// Create a new adapter constructor with the [`sqlx`](drivers::sqlx) driver + /// and a custom config. + pub fn new_with_sqlx_config( + pool: drivers::sqlx::sqlx_client::PgPool, + config: PostgresAdapterConfig, + ) -> Self { + let driver = drivers::sqlx::SqlxDriver::new(pool); + Self { driver, config } + } +} + +impl PostgresAdapterCtr { + /// Create a new adapter constructor with a custom postgres driver and a config. + /// + /// You can implement your own driver by implementing the [`Driver`] trait with any postgres client. + /// Check the [`drivers`] module for more information. + pub fn new_with_driver(driver: D, config: PostgresAdapterConfig) -> Self { + Self { driver, config } + } +} + +/// The postgres adapter with the [`sqlx`](drivers::sqlx) driver. +#[cfg(feature = "sqlx")] +pub type SqlxAdapter = CustomPostgresAdapter; + +type ResponseHandlers = HashMap>>; + +/// The postgres adapter implementation. +/// It is generic over the [`Driver`] used to communicate with the postgres server. +/// And over the [`SocketEmitter`] used to communicate with the local server. This allows to +/// avoid cyclic dependencies between the adapter, `socketioxide-core` and `socketioxide` crates. +pub struct CustomPostgresAdapter { + /// The driver used by the adapter. This is used to communicate with the postgres server. + /// All the postgres adapter instances share the same driver. + driver: D, + /// The configuration of the adapter. + config: PostgresAdapterConfig, + /// The local adapter, used to manage local rooms and socket stores. + local: CoreLocalAdapter, + /// A map of nodes liveness, with the last time remote nodes were seen alive. + nodes_liveness: Mutex>, + /// A map of response handlers used to await for responses from the remote servers. + responses: Arc>, +} + +impl DefinedAdapter for CustomPostgresAdapter {} +impl CoreAdapter for CustomPostgresAdapter { + type Error = Error; + type State = PostgresAdapterCtr; + type AckStream = AckStream; + type InitRes = InitRes; + + fn new(state: &Self::State, local: CoreLocalAdapter) -> Self { + Self { + local, + driver: state.driver.clone(), + config: state.config.clone(), + nodes_liveness: Mutex::new(Vec::new()), + responses: Arc::new(Mutex::new(HashMap::new())), + } + } + + fn init(self: Arc, on_success: impl FnOnce() + Send + 'static) -> Self::InitRes { + let fut = async move { + self.driver.init(&self.config.table_name).await?; + + let global_chan = self.get_global_chan(); + let node_chan = self.get_node_chan(self.local.server_id()); + let response_chan = self.get_response_chan(self.local.server_id()); + + let channels = [ + global_chan.as_str(), + node_chan.as_str(), + response_chan.as_str(), + ]; + + let stream = self.driver.listen(&channels).await?; + tokio::spawn(self.clone().handle_ev_stream(stream)); + tokio::spawn(self.clone().heartbeat_job()); + + // Send initial heartbeat when starting. + self.emit_init_heartbeat().await.map_err(|e| match e { + Error::Driver(e) => e, + Error::Serde(_) => unreachable!(), + })?; + + on_success(); + Ok(()) + }; + InitRes(Box::pin(fut)) + } + + async fn close(&self) -> Result<(), Self::Error> { + Ok(()) + } + + /// Get the number of servers by iterating over the node liveness heartbeats. + async fn server_count(&self) -> Result { + let treshold = std::time::Instant::now() - self.config.hb_timeout; + let mut nodes_liveness = self.nodes_liveness.lock().unwrap(); + nodes_liveness.retain(|(_, v)| v > &treshold); + Ok((nodes_liveness.len() + 1) as u16) + } + + /// Broadcast a packet to all the servers to send them through their sockets. + async fn broadcast( + &self, + packet: Packet, + opts: BroadcastOptions, + ) -> Result<(), BroadcastError> { + let node_id = self.local.server_id(); + if !opts.is_local(node_id) { + let req = RequestOut::new(node_id, RequestTypeOut::Broadcast(&packet), &opts); + self.send_req(req, None).await.map_err(AdapterError::from)?; + } + + self.local.broadcast(packet, opts)?; + Ok(()) + } + + /// Broadcast a packet to all the servers to send them through their sockets. + /// + /// Returns a Stream that is a combination of the local ack stream and a remote ack stream. + /// Here is a specific protocol in order to know how many message the server expect to close + /// the stream at the right time: + /// * Get the number `n` of remote servers. + /// * Send the broadcast request. + /// * Expect `n` `BroadcastAckCount` response in the stream to know the number `m` of expected ack responses. + /// * Expect `sum(m)` broadcast counts sent by the servers. + /// + /// Example with 3 remote servers (n = 3): + /// ```text + /// +---+ +---+ +---+ + /// | A | | B | | C | + /// +---+ +---+ +---+ + /// | | | + /// |---BroadcastWithAck--->| | + /// |---BroadcastWithAck--------------------------->| + /// | | | + /// |<-BroadcastAckCount(2)-| (n = 2; m = 2) | + /// |<-BroadcastAckCount(2)-------(n = 2; m = 4)----| + /// | | | + /// |<----------------Ack---------------------------| + /// |<----------------Ack---| | + /// | | | + /// |<----------------Ack---------------------------| + /// |<----------------Ack---| | + async fn broadcast_with_ack( + &self, + packet: Packet, + opts: BroadcastOptions, + timeout: Option, + ) -> Result { + if opts.is_local(self.local.server_id()) { + tracing::debug!(?opts, "broadcast with ack is local"); + let (local, _) = self.local.broadcast_with_ack(packet, opts, timeout); + let stream = AckStream::new_local(local); + return Ok(stream); + } + let req = RequestOut::new( + self.local.server_id(), + RequestTypeOut::BroadcastWithAck(&packet), + &opts, + ); + let req_id = req.id; + + let remote_serv_cnt = self.server_count().await?.saturating_sub(1); + tracing::trace!(?remote_serv_cnt, "expecting acks from remote servers"); + + let (tx, rx) = mpsc::channel(self.config.ack_response_buffer + remote_serv_cnt as usize); + self.responses.lock().unwrap().insert(req_id, tx); + + self.send_req(req, None).await?; + let (local, _) = self.local.broadcast_with_ack(packet, opts, timeout); + + Ok(AckStream::new( + local, + rx, + self.config.request_timeout, + remote_serv_cnt, + req_id, + self.responses.clone(), + )) + } + + async fn disconnect_socket(&self, opts: BroadcastOptions) -> Result<(), BroadcastError> { + if !opts.is_local(self.local.server_id()) { + let req = RequestOut::new( + self.local.server_id(), + RequestTypeOut::DisconnectSockets, + &opts, + ); + self.send_req(req, None).await.map_err(AdapterError::from)?; + } + self.local + .disconnect_socket(opts) + .map_err(BroadcastError::Socket)?; + + Ok(()) + } + + async fn rooms(&self, opts: BroadcastOptions) -> Result, Self::Error> { + if opts.is_local(self.local.server_id()) { + return Ok(self.local.rooms(opts).into_iter().collect()); + } + let req = RequestOut::new(self.local.server_id(), RequestTypeOut::AllRooms, &opts); + let req_id = req.id; + + // First get the remote stream because postgres might send + // the responses before subscription is done. + let stream = self + .get_res::<()>(req_id, ResponseTypeId::AllRooms, opts.server_id) + .await?; + self.send_req(req, opts.server_id).await?; + let local = self.local.rooms(opts); + let rooms = stream + .filter_map(|item| std::future::ready(item.into_rooms())) + .fold(local, |mut acc, item| async move { + acc.extend(item); + acc + }) + .await; + Ok(Vec::from_iter(rooms)) + } + + async fn add_sockets( + &self, + opts: BroadcastOptions, + rooms: impl RoomParam, + ) -> Result<(), Self::Error> { + let rooms: Vec = rooms.into_room_iter().collect(); + if !opts.is_local(self.local.server_id()) { + let req = RequestOut::new( + self.local.server_id(), + RequestTypeOut::AddSockets(&rooms), + &opts, + ); + self.send_req(req, opts.server_id).await?; + } + self.local.add_sockets(opts, rooms); + Ok(()) + } + + async fn del_sockets( + &self, + opts: BroadcastOptions, + rooms: impl RoomParam, + ) -> Result<(), Self::Error> { + let rooms: Vec = rooms.into_room_iter().collect(); + if !opts.is_local(self.local.server_id()) { + let req = RequestOut::new( + self.local.server_id(), + RequestTypeOut::DelSockets(&rooms), + &opts, + ); + self.send_req(req, opts.server_id).await?; + } + self.local.del_sockets(opts, rooms); + Ok(()) + } + + async fn fetch_sockets( + &self, + opts: BroadcastOptions, + ) -> Result, Self::Error> { + if opts.is_local(self.local.server_id()) { + return Ok(self.local.fetch_sockets(opts)); + } + let req = RequestOut::new(self.local.server_id(), RequestTypeOut::FetchSockets, &opts); + // First get the remote stream because postgres might send + // the responses before subscription is done. + let remote = self + .get_res::(req.id, ResponseTypeId::FetchSockets, opts.server_id) + .await?; + + self.send_req(req, opts.server_id).await?; + let local = self.local.fetch_sockets(opts); + let sockets = remote + .filter_map(|item| future::ready(item.into_fetch_sockets())) + .fold(local, |mut acc, item| async move { + acc.extend(item); + acc + }) + .await; + Ok(sockets) + } + + fn get_local(&self) -> &CoreLocalAdapter { + &self.local + } +} + +impl CustomPostgresAdapter { + async fn heartbeat_job(self: Arc) -> Result<(), Error> { + let mut interval = tokio::time::interval(self.config.hb_interval); + interval.tick().await; // first tick yields immediately + loop { + interval.tick().await; + self.emit_heartbeat(None).await?; + } + } + + async fn handle_ev_stream(self: Arc, stream: impl Stream) { + pin_mut!(stream); + while let Some(notif) = stream.next().await { + let chan = notif.channel(); + let resp_chan = self.get_response_chan(self.local.server_id()); + tracing::info!(chan, resp_chan, notif = notif.payload(), ""); + if chan == resp_chan { + match serde_json::from_str(notif.payload()) { + Ok(ResponsePacket { + req_id, + node_id, + payload, + }) if node_id != self.local.server_id() => { + let handlers = self.responses.lock().unwrap(); + if let Some(handler) = handlers.get(&req_id) { + if let Err(e) = handler.try_send(payload) { + tracing::warn!(channel = resp_chan, req_id = %req_id, "error sending response: {e}"); + } + } else { + tracing::warn!(channel = resp_chan, req_id = %req_id, "response handler not found"); + } + } + Ok(_) => { + tracing::trace!("skipping loopback packets"); + } + Err(e) => { + tracing::warn!(channel = %notif.channel(), "error handling response: {e}") + } + }; + } else { + match serde_json::from_str::(notif.payload()) { + Ok(req) if req.node_id != self.local.server_id() => self.recv_req(req), + Ok(_) => { + tracing::trace!("skipping loopback packets") + } + Err(e) => { + tracing::warn!(channel = %notif.channel(), "error decoding request: {e}") + } + }; + } + } + } + + fn recv_req(self: &Arc, req: RequestIn) { + tracing::trace!(?req, "incoming request"); + match (req.r#type, req.opts) { + (RequestTypeIn::Broadcast(p), Some(opts)) => self.recv_broadcast(opts, p), + (RequestTypeIn::BroadcastWithAck(p), Some(opts)) => self + .clone() + .recv_broadcast_with_ack(req.node_id, req.id, p, opts), + (RequestTypeIn::DisconnectSockets, Some(opts)) => self.recv_disconnect_sockets(opts), + (RequestTypeIn::AllRooms, Some(opts)) => self.recv_rooms(req.node_id, req.id, opts), + (RequestTypeIn::AddSockets(rooms), Some(opts)) => self.recv_add_sockets(opts, rooms), + (RequestTypeIn::DelSockets(rooms), Some(opts)) => self.recv_del_sockets(opts, rooms), + (RequestTypeIn::FetchSockets, Some(opts)) => { + self.recv_fetch_sockets(req.node_id, req.id, opts) + } + req_type @ (RequestTypeIn::Heartbeat | RequestTypeIn::InitHeartbeat, _) => { + self.recv_heartbeat(req_type.0, req.node_id) + } + _ => (), + } + } + + fn recv_broadcast(&self, opts: BroadcastOptions, packet: Packet) { + tracing::trace!(?opts, "incoming broadcast"); + if let Err(e) = self.local.broadcast(packet, opts) { + let ns = self.local.path(); + tracing::warn!(node_id = %self.local.server_id(), ?ns, "remote request broadcast handler: {:?}", e); + } + } + + fn recv_disconnect_sockets(&self, opts: BroadcastOptions) { + if let Err(e) = self.local.disconnect_socket(opts) { + let ns = self.local.path(); + tracing::warn!( + node_id = %self.local.server_id(), + %ns, + "remote request disconnect sockets handler: {:?}", + e + ); + } + } + + fn recv_broadcast_with_ack( + self: Arc, + origin: Uid, + req_id: Sid, + packet: Packet, + opts: BroadcastOptions, + ) { + let (stream, count) = self.local.broadcast_with_ack(packet, opts, None); + tokio::spawn(async move { + let on_err = |err| { + let ns = self.local.path(); + tracing::warn!( + node_id = %self.local.server_id(), + %ns, + "remote request broadcast with ack handler errors: {:?}", + err + ); + }; + // First send the count of expected acks to the server that sent the request. + // This is used to keep track of the number of expected acks. + let res = Response { + r#type: ResponseType::<()>::BroadcastAckCount(count), + node_id: self.local.server_id(), + }; + if let Err(err) = self.send_res(req_id, origin, res).await { + on_err(err); + return; + } + + // Then send the acks as they are received. + futures_util::pin_mut!(stream); + while let Some(ack) = stream.next().await { + let res = Response { + r#type: ResponseType::BroadcastAck(ack), + node_id: self.local.server_id(), + }; + if let Err(err) = self.send_res(req_id, origin, res).await { + on_err(err); + return; + } + } + }); + } + + fn recv_rooms(&self, origin: Uid, req_id: Sid, opts: BroadcastOptions) { + let rooms = self.local.rooms(opts); + let res = Response { + r#type: ResponseType::<()>::AllRooms(rooms), + node_id: self.local.server_id(), + }; + let fut = self.send_res(req_id, origin, res); + let ns = self.local.path().clone(); + let uid = self.local.server_id(); + tokio::spawn(async move { + if let Err(err) = fut.await { + tracing::warn!(?uid, ?ns, "remote request rooms handler: {:?}", err); + } + }); + } + + fn recv_add_sockets(&self, opts: BroadcastOptions, rooms: Vec) { + self.local.add_sockets(opts, rooms); + } + + fn recv_del_sockets(&self, opts: BroadcastOptions, rooms: Vec) { + self.local.del_sockets(opts, rooms); + } + fn recv_fetch_sockets(&self, origin: Uid, req_id: Sid, opts: BroadcastOptions) { + let sockets = self.local.fetch_sockets(opts); + let res = Response { + node_id: self.local.server_id(), + r#type: ResponseType::FetchSockets(sockets), + }; + let fut = self.send_res(req_id, origin, res); + let ns = self.local.path().clone(); + let uid = self.local.server_id(); + tokio::spawn(async move { + if let Err(err) = fut.await { + tracing::warn!(?uid, ?ns, "remote request fetch sockets handler: {:?}", err); + } + }); + } + + /// Receive a heartbeat from a remote node. + /// It might be a FirstHeartbeat packet, in which case we are re-emitting a heartbeat to the remote node. + fn recv_heartbeat(self: &Arc, req_type: RequestTypeIn, origin: Uid) { + tracing::debug!(?req_type, "{:?} received", req_type); + let mut node_liveness = self.nodes_liveness.lock().unwrap(); + // Even with a FirstHeartbeat packet we first consume the node liveness to + // ensure that the node is not already in the list. + for (id, liveness) in node_liveness.iter_mut() { + if *id == origin { + *liveness = Instant::now(); + return; + } + } + + node_liveness.push((origin, Instant::now())); + + if matches!(req_type, RequestTypeIn::InitHeartbeat) { + tracing::debug!( + ?origin, + "initial heartbeat detected, saying hello to the new node" + ); + + let this = self.clone(); + tokio::spawn(async move { + if let Err(err) = this.emit_heartbeat(Some(origin)).await { + tracing::warn!( + "could not re-emit heartbeat after new node detection: {:?}", + err + ); + } + }); + } + } + + /// Send a request to a specific target node or broadcast it to all nodes if no target is specified. + async fn send_req(&self, req: RequestOut<'_>, target: Option) -> Result<(), Error> { + tracing::trace!(?req, "sending request"); + let chan = match target { + Some(target) => self.get_node_chan(target), + None => self.get_global_chan(), + }; + let payload = serde_json::to_string(&req)?; + self.driver + .notify(&chan, &payload) + .await + .map_err(Error::Driver)?; + Ok(()) + } + + /// Send a response to the node that sent the request. + fn send_res( + &self, + req_id: Sid, + req_origin: Uid, + payload: Response, + ) -> impl Future>> + 'static { + tracing::trace!( + ?payload, + "sending response for {req_id} req to {req_origin}" + ); + let driver = self.driver.clone(); + let chan = self.get_response_chan(req_origin); + let message = serde_json::to_string(&payload) + .and_then(RawValue::from_string) + .map(|payload| ResponsePacket { + req_id, + node_id: self.local.server_id(), + payload, + }) + .and_then(|res| serde_json::to_string(&res)); + + async move { + driver + .notify(&chan, &message?) + .await + .map_err(Error::Driver)?; + Ok(()) + } + } + + /// Await for all the responses from the remote servers. + /// If the target node is specified, only await for the response from that node. + async fn get_res( + &self, + req_id: Sid, + response_type: ResponseTypeId, + target: Option, + ) -> Result>, Error> { + // Check for specific target node + let remote_serv_cnt = if target.is_none() { + self.server_count().await?.saturating_sub(1) as usize + } else { + 1 + }; + + let (tx, rx) = mpsc::channel(std::cmp::max(remote_serv_cnt, 1)); + self.responses.lock().unwrap().insert(req_id, tx); + let stream = ChanStream::new(rx); + + let stream = stream + .filter_map(|payload| { + let data = match serde_json::from_str::>(payload.get()) { + Ok(data) => Some(data), + Err(e) => { + tracing::warn!("error decoding response: {e}"); + None + } + }; + future::ready(data) + }) + .filter(move |item| future::ready(ResponseTypeId::from(&item.r#type) == response_type)) + .take(remote_serv_cnt) + .take_until(tokio::time::sleep(self.config.request_timeout)); + + Ok(stream) + } + + /// Emit a heartbeat to the specified target node or broadcast to all nodes. + async fn emit_heartbeat(&self, target: Option) -> Result<(), Error> { + self.send_req( + RequestOut::new_empty(self.local.server_id(), RequestTypeOut::Heartbeat), + target, + ) + .await + } + + /// Emit an initial heartbeat to all nodes. + async fn emit_init_heartbeat(&self) -> Result<(), Error> { + // Send initial heartbeat when starting. + self.send_req( + RequestOut::new_empty(self.local.server_id(), RequestTypeOut::InitHeartbeat), + None, + ) + .await + } + + fn get_global_chan(&self) -> String { + format!("{}#{}", self.config.prefix, self.local.path()) + } + fn get_node_chan(&self, uid: Uid) -> String { + format!("{}#{}", self.get_global_chan(), uid) + } + fn get_response_chan(&self, uid: Uid) -> String { + format!( + "{}-response#{}#{}", + &self.config.prefix, + self.local.path(), + uid + ) + } +} + +/// The result of the init future. +#[must_use = "futures do nothing unless you `.await` or poll them"] +pub struct InitRes(futures_core::future::BoxFuture<'static, Result<(), D::Error>>); + +impl Future for InitRes { + type Output = Result<(), D::Error>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.0.as_mut().poll(cx) + } +} +impl Spawnable for InitRes { + fn spawn(self) { + tokio::spawn(async move { + if let Err(e) = self.0.await { + tracing::error!("error initializing adapter: {e}"); + } + }); + } +} + +#[derive(Deserialize, Serialize)] +struct ResponsePacket { + req_id: Sid, + node_id: Uid, + payload: Box, +} diff --git a/crates/socketioxide-postgres/src/stream.rs b/crates/socketioxide-postgres/src/stream.rs new file mode 100644 index 00000000..3fec6df7 --- /dev/null +++ b/crates/socketioxide-postgres/src/stream.rs @@ -0,0 +1,268 @@ +use std::{ + fmt, + pin::Pin, + sync::{Arc, Mutex}, + task::{self, Poll}, + time::Duration, +}; + +use futures_core::{FusedStream, Stream}; +use futures_util::{StreamExt, stream::TakeUntil}; +use pin_project_lite::pin_project; +use serde::de::DeserializeOwned; +use serde_json::value::RawValue; +use socketioxide_core::{ + Sid, + adapter::{ + AckStreamItem, + remote_packet::{Response, ResponseType}, + }, +}; +use tokio::{sync::mpsc, time}; + +use crate::{ResponseHandlers, drivers::Notification}; + +pin_project! { + /// A stream of acknowledgement messages received from the local and remote servers. + /// It merges the local ack stream with the remote ack stream from all the servers. + // The server_cnt is the number of servers that are expected to send a AckCount message. + // It is decremented each time a AckCount message is received. + // + // The ack_cnt is the number of acks that are expected to be received. It is the sum of all the the ack counts. + // And it is decremented each time an ack is received. + // + // Therefore an exhausted stream correspond to `ack_cnt == 0` and `server_cnt == 0`. + pub struct AckStream { + #[pin] + local: S, + #[pin] + remote: DropStream>, time::Sleep>>, + ack_cnt: u32, + total_ack_cnt: usize, + serv_cnt: u16, + } +} + +impl AckStream { + pub fn new( + local: S, + remote: mpsc::Receiver>, + timeout: Duration, + serv_cnt: u16, + req_sid: Sid, + handlers: Arc>, + ) -> Self { + let remote = ChanStream::new(remote).take_until(time::sleep(timeout)); + let remote = DropStream::new(remote, handlers, req_sid); + Self { + local, + ack_cnt: 0, + total_ack_cnt: 0, + serv_cnt, + remote, + } + } + + pub fn new_local(local: S) -> Self { + let handlers = Arc::new(Mutex::new(ResponseHandlers::new())); + let rx = mpsc::channel(1).1; + let remote = ChanStream::new(rx).take_until(time::sleep(Duration::ZERO)); + let remote = DropStream::new(remote, handlers, Sid::ZERO); + Self { + local, + remote, + ack_cnt: 0, + total_ack_cnt: 0, + serv_cnt: 0, + } + } +} +impl AckStream +where + Err: DeserializeOwned + fmt::Debug, + S: Stream> + FusedStream, +{ + /// Poll the remote stream. First the count of acks is receivedhen the acks are received. + /// We expect `serv_cnt` of `BroadcastAckCount` messages to be receivedhen we expect + /// `ack_cnt` of `BroadcastAck` messages. + fn poll_remote( + self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> Poll>> { + // remote stream is not fused, so we need to check if it is terminated + if FusedStream::is_terminated(&self) { + return Poll::Ready(None); + } + let mut projection = self.project(); + loop { + match projection.remote.as_mut().poll_next(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(None) => return Poll::Ready(None), + Poll::Ready(Some(notif)) => { + let res = serde_json::from_str::>(notif.get()); + match res { + Ok(Response { + node_id: uid, + r#type: ResponseType::BroadcastAckCount(count), + }) if *projection.serv_cnt > 0 => { + tracing::trace!(?uid, "receiving broadcast ack count {count}"); + *projection.ack_cnt += count; + *projection.total_ack_cnt += count as usize; + *projection.serv_cnt -= 1; + } + Ok(Response { + node_id: uid, + r#type: ResponseType::BroadcastAck((sid, res)), + }) if *projection.ack_cnt > 0 => { + tracing::trace!(?uid, "receiving broadcast ack {sid} {:?}", res); + *projection.ack_cnt -= 1; + return Poll::Ready(Some((sid, res))); + } + Ok(Response { node_id: uid, .. }) => { + tracing::warn!(?uid, "unexpected response type"); + } + Err(e) => { + tracing::warn!("error decoding ack response: {e}"); + } + } + } + } + } + } +} +impl Stream for AckStream +where + E: DeserializeOwned + fmt::Debug, + S: Stream> + FusedStream, +{ + type Item = AckStreamItem; + fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + match self.as_mut().project().local.poll_next(cx) { + Poll::Pending => match self.poll_remote(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(Some(item)) => Poll::Ready(Some(item)), + Poll::Ready(None) => Poll::Pending, + }, + Poll::Ready(Some(item)) => Poll::Ready(Some(item)), + Poll::Ready(None) => self.poll_remote(cx), + } + } + + fn size_hint(&self) -> (usize, Option) { + let (lower, upper) = self.local.size_hint(); + (lower, upper.map(|upper| upper + self.total_ack_cnt)) + } +} + +impl FusedStream for AckStream +where + Err: DeserializeOwned + fmt::Debug, + S: Stream> + FusedStream, +{ + /// The stream is terminated if: + /// * The local stream is terminated. + /// * All the servers have sent the expected ack count. + /// * We have received all the expected acks. + fn is_terminated(&self) -> bool { + // remote stream is terminated if the timeout is reached + let remote_term = (self.ack_cnt == 0 && self.serv_cnt == 0) || self.remote.is_terminated(); + self.local.is_terminated() && remote_term + } +} +impl fmt::Debug for AckStream { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AckStream") + .field("ack_cnt", &self.ack_cnt) + .field("total_ack_cnt", &self.total_ack_cnt) + .field("serv_cnt", &self.serv_cnt) + .finish() + } +} +pin_project! { + /// A stream of messages received from a channel. + pub struct ChanStream { + #[pin] + rx: mpsc::Receiver + } +} +impl ChanStream { + pub fn new(rx: mpsc::Receiver) -> Self { + Self { rx } + } +} +impl Stream for ChanStream { + type Item = T; + + fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + self.project().rx.poll_recv(cx) + } +} + +pin_project! { + /// A stream that unsubscribes from its source channel when dropped. + pub struct DropStream { + #[pin] + stream: S, + req_id: Sid, + handlers: Arc> + } + impl PinnedDrop for DropStream { + fn drop(this: Pin<&mut Self>) { + let stream = this.project(); + let chan = stream.req_id; + tracing::debug!(?chan, "dropping stream"); + stream.handlers.lock().unwrap().remove(chan); + } + } +} +impl DropStream { + pub fn new(stream: S, handlers: Arc>, req_id: Sid) -> Self { + Self { + stream, + handlers, + req_id, + } + } +} +impl Stream for DropStream { + type Item = S::Item; + + fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + self.project().stream.poll_next(cx) + } +} +impl FusedStream for DropStream { + fn is_terminated(&self) -> bool { + self.stream.is_terminated() + } +} + +#[cfg(test)] +mod tests { + use futures_core::FusedStream; + use futures_util::StreamExt; + use socketioxide_core::{Sid, Value}; + + use super::AckStream; + + #[tokio::test] + async fn local_ack_stream_should_have_a_closed_remote() { + let sid = Sid::new(); + let local = futures_util::stream::once(async move { + (sid, Ok::<_, ()>(Value::Str("local".into(), None))) + }); + let stream = AckStream::<_>::new_local(local); + futures_util::pin_mut!(stream); + assert_eq!(stream.ack_cnt, 0); + assert_eq!(stream.total_ack_cnt, 0); + assert_eq!(stream.serv_cnt, 0); + assert!(!stream.local.is_terminated()); + assert!(!stream.is_terminated()); + let data = stream.next().await; + assert!( + matches!(data, Some((id, Ok(Value::Str(msg, None)))) if id == sid && msg == "local") + ); + assert_eq!(stream.next().await, None); + assert!(stream.is_terminated()); + } +} diff --git a/crates/socketioxide-postgres/tests/broadcast.rs b/crates/socketioxide-postgres/tests/broadcast.rs new file mode 100644 index 00000000..c7ba71a9 --- /dev/null +++ b/crates/socketioxide-postgres/tests/broadcast.rs @@ -0,0 +1,149 @@ +use std::time::Duration; + +use socketioxide::{adapter::Adapter, extract::SocketRef}; +mod fixture; + +#[tokio::test] +pub async fn broadcast() { + async fn handler(socket: SocketRef
) { + // delay to ensure all socket/servers are connected + tokio::time::sleep(Duration::from_millis(1)).await; + socket.broadcast().emit("test", &2).await.unwrap(); + } + + let [io1, io2] = fixture::spawn_servers(); + + io1.ns("/", handler).await.unwrap(); + io2.ns("/", handler).await.unwrap(); + + let ((_tx1, mut rx1), (_tx2, mut rx2)) = + tokio::join!(io1.new_dummy_sock("/", ()), io2.new_dummy_sock("/", ())); + + timeout_rcv!(&mut rx1); // Connect "/" packet + timeout_rcv!(&mut rx2); // Connect "/" packet + assert_eq!(timeout_rcv!(&mut rx1), r#"42["test",2]"#); + assert_eq!(timeout_rcv!(&mut rx2), r#"42["test",2]"#); + + timeout_rcv_err!(&mut rx1); + timeout_rcv_err!(&mut rx2); +} + +#[tokio::test] +pub async fn broadcast_rooms() { + let [io1, io2, io3] = fixture::spawn_servers(); + let handler = |room: &'static str, to: &'static str| { + move |socket: SocketRef<_>| async move { + // delay to ensure all socket/servers are connected + socket.join(room); + tokio::time::sleep(Duration::from_millis(5)).await; + socket.to(to).emit("test", room).await.unwrap(); + } + }; + + io1.ns("/", handler("room1", "room2")).await.unwrap(); + io2.ns("/", handler("room2", "room3")).await.unwrap(); + io3.ns("/", handler("room3", "room1")).await.unwrap(); + + let ((_tx1, mut rx1), (_tx2, mut rx2), (_tx3, mut rx3)) = tokio::join!( + io1.new_dummy_sock("/", ()), + io2.new_dummy_sock("/", ()), + io3.new_dummy_sock("/", ()) + ); + + timeout_rcv!(&mut rx1); // Connect "/" packet + timeout_rcv!(&mut rx2); // Connect "/" packet + timeout_rcv!(&mut rx3); // Connect "/" packet + + // socket 1 is receiving a packet from io3 + assert_eq!(timeout_rcv!(&mut rx1), r#"42["test","room3"]"#); + // socket 2 is receiving a packet from io2 + assert_eq!(timeout_rcv!(&mut rx2), r#"42["test","room1"]"#); + // socket 3 is receiving a packet from io1 + assert_eq!(timeout_rcv!(&mut rx3), r#"42["test","room2"]"#); + + timeout_rcv_err!(&mut rx1); + timeout_rcv_err!(&mut rx2); + timeout_rcv_err!(&mut rx3); +} + +#[tokio::test] +pub async fn broadcast_with_ack() { + use futures_util::stream::StreamExt; + + async fn handler(socket: SocketRef) { + // delay to ensure all socket/servers are connected + tokio::time::sleep(Duration::from_millis(1)).await; + socket + .broadcast() + .emit_with_ack::<_, String>("test", "bar") + .await + .unwrap() + .for_each(|(_, res)| { + socket.emit("ack_res", &res).unwrap(); + async move {} + }) + .await; + } + + let [io1, io2] = fixture::spawn_servers(); + + io1.ns("/", handler).await.unwrap(); + io2.ns("/", async || ()).await.unwrap(); + + let ((_tx1, mut rx1), (tx2, mut rx2)) = + tokio::join!(io1.new_dummy_sock("/", ()), io2.new_dummy_sock("/", ())); + + timeout_rcv!(&mut rx1); // Connect "/" packet + timeout_rcv!(&mut rx2); // Connect "/" packet + + assert_eq!(timeout_rcv!(&mut rx2), r#"421["test","bar"]"#); + let packet_res = r#"431["foo"]"#.to_string().try_into().unwrap(); + tx2.try_send(packet_res).unwrap(); + assert_eq!(timeout_rcv!(&mut rx1), r#"42["ack_res",{"Ok":"foo"}]"#); + + timeout_rcv_err!(&mut rx1); + timeout_rcv_err!(&mut rx2); +} + +#[tokio::test] +pub async fn broadcast_with_ack_timeout() { + use futures_util::StreamExt; + const TIMEOUT: Duration = Duration::from_millis(50); + + async fn handler(socket: SocketRef) { + socket + .broadcast() + .emit_with_ack::<_, String>("test", "bar") + .await + .unwrap() + .for_each(|(_, res)| { + socket.emit("ack_res", &res).unwrap(); + async move {} + }) + .await; + socket.emit("ack_res", "timeout").unwrap(); + } + + let [io1, io2] = fixture::spawn_buggy_servers(TIMEOUT); + + io1.ns("/", handler).await.unwrap(); + io2.ns("/", async || ()).await.unwrap(); + + let now = std::time::Instant::now(); + let ((_tx1, mut rx1), (_tx2, mut rx2)) = + tokio::join!(io1.new_dummy_sock("/", ()), io2.new_dummy_sock("/", ())); + + timeout_rcv!(&mut rx1); // Connect "/" packet + timeout_rcv!(&mut rx2); // Connect "/" packet + + assert_eq!(timeout_rcv!(&mut rx2), r#"421["test","bar"]"#); // emit with ack message + // We do not answer + assert_eq!( + timeout_rcv!(&mut rx1, TIMEOUT.as_millis() as u64 + 100), + r#"42["ack_res","timeout"]"# + ); + assert!(now.elapsed() >= TIMEOUT); + + timeout_rcv_err!(&mut rx1); + timeout_rcv_err!(&mut rx2); +} diff --git a/crates/socketioxide-postgres/tests/fixture.rs b/crates/socketioxide-postgres/tests/fixture.rs new file mode 100644 index 00000000..070d9b97 --- /dev/null +++ b/crates/socketioxide-postgres/tests/fixture.rs @@ -0,0 +1,247 @@ +#![allow(dead_code)] + +use futures_core::Stream; +use socketioxide_core::Uid; +use socketioxide_postgres::{ + CustomPostgresAdapter, PostgresAdapterConfig, PostgresAdapterCtr, + drivers::{Driver, Notification}, +}; +use std::{ + convert::Infallible, + pin::Pin, + str::FromStr, + sync::{Arc, RwLock}, + task, + time::Duration, +}; +use tokio::sync::mpsc; + +use socketioxide::{SocketIo, SocketIoConfig, adapter::Emitter}; + +/// Spawns a number of servers with a stub driver for testing. +/// Every server will be connected to every other server. +pub fn spawn_servers() -> [SocketIo>; N] +{ + let sync_buff = Arc::new(RwLock::new(Vec::with_capacity(N))); + spawn_inner(sync_buff, PostgresAdapterConfig::default()) +} + +pub fn spawn_buggy_servers( + timeout: Duration, +) -> [SocketIo>; N] { + let sync_buff = Arc::new(RwLock::new(Vec::with_capacity(N))); + let config = PostgresAdapterConfig::default().with_request_timeout(timeout); + let res = spawn_inner(sync_buff.clone(), config); + + // Reinject a false heartbeat request to simulate a bad number of servers. + // This will trigger timeouts when expecting responses from all servers. + // The heartbeat type is 20 (RequestTypeOut::Heartbeat) in the wire format. + let uid: Uid = Uid::from_str("PHHq01ObWy7Godqx").unwrap(); + let heartbeat_json = serde_json::json!({ + "node_id": uid.to_string(), + "id": "ZG9K1r7xSLBiJYWD", + "type": 20, + "opts": null, + }); + let payload = serde_json::to_string(&heartbeat_json).unwrap(); + + for (_, tx) in sync_buff.read().unwrap().iter() { + // Send the heartbeat to the global channel of the "/" namespace + tx.try_send(StubNotification { + channel: "socket.io#/".to_string(), + payload: payload.clone(), + }) + .unwrap(); + } + + res +} + +fn spawn_inner( + sync_buff: Arc>, + config: PostgresAdapterConfig, +) -> [SocketIo>; N] { + [0; N].map(|_| { + let server_id = Uid::new(); + let (driver, mut rx, tx) = StubDriver::new(server_id); + + // pipe messages to all other servers + sync_buff.write().unwrap().push((server_id, tx)); + let sync_buff = sync_buff.clone(); + tokio::spawn(async move { + while let Some(notif) = rx.recv().await { + tracing::debug!("received notify on channel {:?}", notif.channel); + for (sid, tx) in sync_buff.read().unwrap().iter() { + if *sid != server_id { + tracing::debug!("forwarding notify to server {:?}", sid); + tx.try_send(notif.clone()).unwrap(); + } + } + } + }); + + let adapter = PostgresAdapterCtr::new_with_driver(driver, config.clone()); + let mut config = SocketIoConfig::default(); + config.server_id = server_id; + let (_svc, io) = SocketIo::builder() + .with_config(config) + .with_adapter::>(adapter) + .build_svc(); + io + }) +} + +type NotifyHandlers = Vec<(Uid, mpsc::Sender)>; + +#[derive(Debug, Clone)] +pub struct StubNotification { + channel: String, + payload: String, +} + +impl Notification for StubNotification { + fn channel(&self) -> &str { + &self.channel + } + + fn payload(&self) -> &str { + &self.payload + } +} + +#[derive(Debug, Clone)] +pub struct StubDriver { + server_id: Uid, + /// Sender to emit outgoing NOTIFY messages (to be broadcast to other servers). + tx: mpsc::Sender, + /// Handlers for incoming notifications per listened channel. + handlers: Arc)>>>, +} + +impl StubDriver { + pub fn new( + server_id: Uid, + ) -> (Self, mpsc::Receiver, mpsc::Sender) { + let (tx, rx) = mpsc::channel(255); // outgoing notifies + let (tx1, rx1) = mpsc::channel(255); // incoming notifies + let handlers: Arc)>>> = + Arc::new(RwLock::new(Vec::new())); + + tokio::spawn(pipe_handlers(rx1, handlers.clone())); + + let driver = Self { + server_id, + tx, + handlers, + }; + (driver, rx, tx1) + } +} + +/// Pipe incoming notifications to the matching channel handlers. +async fn pipe_handlers( + mut rx: mpsc::Receiver, + handlers: Arc)>>>, +) { + while let Some(notif) = rx.recv().await { + let handlers = handlers.read().unwrap(); + for (chan, handler) in &*handlers { + if *chan == notif.channel { + handler.try_send(notif.clone()).unwrap(); + } + } + } +} + +pin_project_lite::pin_project! { + pub struct NotificationStream { + #[pin] + rx: mpsc::Receiver, + } +} + +impl Stream for NotificationStream { + type Item = StubNotification; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll> { + self.project().rx.poll_recv(cx) + } +} + +impl Driver for StubDriver { + type Error = Infallible; + type Notification = StubNotification; + type NotificationStream = NotificationStream; + + async fn init(&self, _table: &str) -> Result<(), Self::Error> { + Ok(()) + } + + async fn listen(&self, channels: &[&str]) -> Result { + let (tx, rx) = mpsc::channel(255); + let mut handlers = self.handlers.write().unwrap(); + for chan in channels { + handlers.push((chan.to_string(), tx.clone())); + } + Ok(NotificationStream { rx }) + } + + async fn notify(&self, channel: &str, message: &str) -> Result<(), Self::Error> { + // Also deliver to local handlers (self-delivery, like real PG NOTIFY). + { + let handlers = self.handlers.read().unwrap(); + for (chan, handler) in &*handlers { + if *chan == channel { + handler + .try_send(StubNotification { + channel: channel.to_string(), + payload: message.to_string(), + }) + .unwrap(); + } + } + } + // Send to the broadcast pipe for delivery to other servers. + self.tx + .try_send(StubNotification { + channel: channel.to_string(), + payload: message.to_string(), + }) + .unwrap(); + Ok(()) + } +} + +#[macro_export] +macro_rules! timeout_rcv_err { + ($srx:expr) => { + tokio::time::timeout(std::time::Duration::from_millis(10), $srx.recv()) + .await + .unwrap_err(); + }; +} + +#[macro_export] +macro_rules! timeout_rcv { + ($srx:expr) => { + TryInto::::try_into( + tokio::time::timeout(std::time::Duration::from_millis(10), $srx.recv()) + .await + .unwrap() + .unwrap(), + ) + .unwrap() + }; + ($srx:expr, $t:expr) => { + TryInto::::try_into( + tokio::time::timeout(std::time::Duration::from_millis($t), $srx.recv()) + .await + .unwrap() + .unwrap(), + ) + .unwrap() + }; +} diff --git a/crates/socketioxide-postgres/tests/local.rs b/crates/socketioxide-postgres/tests/local.rs new file mode 100644 index 00000000..49972933 --- /dev/null +++ b/crates/socketioxide-postgres/tests/local.rs @@ -0,0 +1,32 @@ +//! Check that each adapter function with a broadcast options that is [`Local`] returns an immediate future +mod fixture; + +macro_rules! assert_now { + ($fut:expr) => { + #[allow(unused_must_use)] + futures_util::FutureExt::now_or_never($fut) + .expect("Returned future should be sync") + .unwrap() + }; +} + +#[tokio::test] +async fn test_local_fns() { + let [io1, io2] = fixture::spawn_servers(); + + io1.ns("/", async || ()).await.unwrap(); + io2.ns("/", async || ()).await.unwrap(); + + let (_, mut rx1) = io1.new_dummy_sock("/", ()).await; + let (_, mut rx2) = io2.new_dummy_sock("/", ()).await; + + timeout_rcv!(&mut rx1); // connect packet + timeout_rcv!(&mut rx2); // connect packet + + assert_now!(io1.local().emit("test", "test")); + assert_now!(io1.local().emit_with_ack::<_, ()>("test", "test")); + assert_now!(io1.local().join("test")); + assert_now!(io1.local().leave("test")); + assert_now!(io1.local().disconnect()); + assert_now!(io1.local().fetch_sockets()); +} diff --git a/crates/socketioxide-postgres/tests/rooms.rs b/crates/socketioxide-postgres/tests/rooms.rs new file mode 100644 index 00000000..343d400f --- /dev/null +++ b/crates/socketioxide-postgres/tests/rooms.rs @@ -0,0 +1,119 @@ +use std::time::Duration; + +use socketioxide::extract::SocketRef; + +mod fixture; + +#[tokio::test] +pub async fn all_rooms() { + let [io1, io2, io3] = fixture::spawn_servers(); + let handler = + |rooms: &'static [&'static str]| async move |socket: SocketRef<_>| socket.join(rooms); + + io1.ns("/", handler(&["room1", "room2"])).await.unwrap(); + io2.ns("/", handler(&["room2", "room3"])).await.unwrap(); + io3.ns("/", handler(&["room3", "room1"])).await.unwrap(); + + let ((_tx1, mut rx1), (_tx2, mut rx2), (_tx3, mut rx3)) = tokio::join!( + io1.new_dummy_sock("/", ()), + io2.new_dummy_sock("/", ()), + io3.new_dummy_sock("/", ()) + ); + + timeout_rcv!(&mut rx1); // Connect "/" packet + timeout_rcv!(&mut rx2); // Connect "/" packet + timeout_rcv!(&mut rx3); // Connect "/" packet + + const ROOMS: [&str; 3] = ["room1", "room2", "room3"]; + for io in [io1, io2, io3] { + let mut rooms = io.rooms().await.unwrap(); + rooms.sort(); + assert_eq!(rooms, ROOMS); + } + + timeout_rcv_err!(&mut rx1); + timeout_rcv_err!(&mut rx2); + timeout_rcv_err!(&mut rx3); +} + +#[tokio::test] +pub async fn all_rooms_timeout() { + const TIMEOUT: Duration = Duration::from_millis(50); + let [io1, io2, io3] = fixture::spawn_buggy_servers(TIMEOUT); + let handler = + |rooms: &'static [&'static str]| async move |socket: SocketRef<_>| socket.join(rooms); + + io1.ns("/", handler(&["room1", "room2"])).await.unwrap(); + io2.ns("/", handler(&["room2", "room3"])).await.unwrap(); + io3.ns("/", handler(&["room3", "room1"])).await.unwrap(); + + let ((_tx1, mut rx1), (_tx2, mut rx2), (_tx3, mut rx3)) = tokio::join!( + io1.new_dummy_sock("/", ()), + io2.new_dummy_sock("/", ()), + io3.new_dummy_sock("/", ()) + ); + + timeout_rcv!(&mut rx1); // Connect "/" packet + timeout_rcv!(&mut rx2); // Connect "/" packet + timeout_rcv!(&mut rx3); // Connect "/" packet + + const ROOMS: [&str; 3] = ["room1", "room2", "room3"]; + for io in [io1, io3, io2] { + let now = std::time::Instant::now(); + let mut rooms = io.rooms().await.unwrap(); + dbg!(&rooms); + assert!(dbg!(now.elapsed()) >= TIMEOUT); // timeout time + rooms.sort(); + assert_eq!(rooms, ROOMS); + } + + timeout_rcv_err!(&mut rx1); + timeout_rcv_err!(&mut rx2); + timeout_rcv_err!(&mut rx3); +} +#[tokio::test] +pub async fn add_sockets() { + let handler = |room: &'static str| async move |socket: SocketRef<_>| socket.join(room); + let [io1, io2] = fixture::spawn_servers(); + + io1.ns("/", handler("room1")).await.unwrap(); + io2.ns("/", handler("room3")).await.unwrap(); + + let ((_tx1, mut rx1), (_tx2, mut rx2)) = + tokio::join!(io1.new_dummy_sock("/", ()), io2.new_dummy_sock("/", ())); + + timeout_rcv!(&mut rx1); // Connect "/" packet + timeout_rcv!(&mut rx2); // Connect "/" packet + io1.broadcast().join("room2").await.unwrap(); + let mut rooms = io1.rooms().await.unwrap(); + rooms.sort(); + assert_eq!(rooms, ["room1", "room2", "room3"]); + + timeout_rcv_err!(&mut rx1); + timeout_rcv_err!(&mut rx2); +} + +#[tokio::test] +pub async fn del_sockets() { + let handler = + |rooms: &'static [&'static str]| async move |socket: SocketRef<_>| socket.join(rooms); + let [io1, io2] = fixture::spawn_servers(); + + io1.ns("/", handler(&["room1", "room2"])).await.unwrap(); + io2.ns("/", handler(&["room3", "room2"])).await.unwrap(); + + let ((_tx1, mut rx1), (_tx2, mut rx2)) = + tokio::join!(io1.new_dummy_sock("/", ()), io2.new_dummy_sock("/", ())); + + timeout_rcv!(&mut rx1); // Connect "/" packet + timeout_rcv!(&mut rx2); // Connect "/" packet + + io1.broadcast().leave("room2").await.unwrap(); + + let mut rooms = io1.rooms().await.unwrap(); + rooms.sort(); + assert_eq!(rooms, ["room1", "room3"]); + + timeout_rcv_err!(&mut rx1); + timeout_rcv_err!(&mut rx2); +} diff --git a/crates/socketioxide-postgres/tests/sockets.rs b/crates/socketioxide-postgres/tests/sockets.rs new file mode 100644 index 00000000..947151ff --- /dev/null +++ b/crates/socketioxide-postgres/tests/sockets.rs @@ -0,0 +1,170 @@ +use std::{str::FromStr, time::Duration}; + +use socketioxide::{ + SocketIo, adapter::Adapter, extract::SocketRef, operators::BroadcastOperators, + socket::RemoteSocket, +}; +use socketioxide_core::{Sid, Str, adapter::RemoteSocketData}; +use tokio::time::Instant; + +mod fixture; +fn extract_sid(data: &str) -> Sid { + let data = data + .split("\"sid\":\"") + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap(); + Sid::from_str(data).unwrap() +} +async fn fetch_sockets_data(op: BroadcastOperators) -> Vec { + let mut sockets = op + .fetch_sockets() + .await + .unwrap() + .into_iter() + .map(RemoteSocket::into_data) + .collect::>(); + sockets.sort_by(|a, b| a.id.cmp(&b.id)); + sockets +} +fn create_expected_sockets( + ids: [Sid; N], + ios: [&SocketIo; N], +) -> [RemoteSocketData; N] { + let mut i = 0; + let mut sockets = ios.map(|io| { + let id = ids[i]; + i += 1; + RemoteSocketData { + id, + server_id: io.config().server_id, + ns: Str::from("/"), + } + }); + sockets.sort_by(|a, b| a.id.cmp(&b.id)); + sockets +} + +#[tokio::test] +pub async fn fetch_sockets() { + let [io1, io2, io3] = fixture::spawn_servers::<3>(); + + io1.ns("/", async || ()).await.unwrap(); + io2.ns("/", async || ()).await.unwrap(); + io3.ns("/", async || ()).await.unwrap(); + + let (_, mut rx1) = io1.new_dummy_sock("/", ()).await; + let (_, mut rx2) = io2.new_dummy_sock("/", ()).await; + let (_, mut rx3) = io3.new_dummy_sock("/", ()).await; + + let id1 = extract_sid(&timeout_rcv!(&mut rx1)); + let id2 = extract_sid(&timeout_rcv!(&mut rx2)); + let id3 = extract_sid(&timeout_rcv!(&mut rx3)); + + let mut expected_sockets = create_expected_sockets([id1, id2, id3], [&io1, &io2, &io3]); + expected_sockets.sort_by(|a, b| a.id.cmp(&b.id)); + + let sockets = fetch_sockets_data(io1.broadcast()).await; + assert_eq!(sockets, expected_sockets); + + let sockets = fetch_sockets_data(io2.broadcast()).await; + assert_eq!(sockets, expected_sockets); + + let sockets = fetch_sockets_data(io3.broadcast()).await; + assert_eq!(sockets, expected_sockets); +} + +#[tokio::test] +pub async fn fetch_sockets_with_rooms() { + let [io1, io2, io3] = fixture::spawn_servers::<3>(); + let handler = + |rooms: &'static [&'static str]| async move |socket: SocketRef<_>| socket.join(rooms); + + io1.ns("/", handler(&["room1", "room2"])).await.unwrap(); + io2.ns("/", handler(&["room2", "room3"])).await.unwrap(); + io3.ns("/", handler(&["room3", "room1"])).await.unwrap(); + + let (_, mut rx1) = io1.new_dummy_sock("/", ()).await; + let (_, mut rx2) = io2.new_dummy_sock("/", ()).await; + let (_, mut rx3) = io3.new_dummy_sock("/", ()).await; + + let id1 = extract_sid(&timeout_rcv!(&mut rx1)); + let id2 = extract_sid(&timeout_rcv!(&mut rx2)); + let id3 = extract_sid(&timeout_rcv!(&mut rx3)); + + let sockets = fetch_sockets_data(io1.to("room1")).await; + assert_eq!(sockets, create_expected_sockets([id1, id3], [&io1, &io3])); + + let sockets = fetch_sockets_data(io1.to("room2")).await; + assert_eq!(sockets, create_expected_sockets([id1, id2], [&io1, &io2])); + + let sockets = fetch_sockets_data(io1.to("room3")).await; + assert_eq!(sockets, create_expected_sockets([id2, id3], [&io2, &io3])); +} + +#[tokio::test] +pub async fn fetch_sockets_timeout() { + const TIMEOUT: Duration = Duration::from_millis(50); + let [io1, io2] = fixture::spawn_buggy_servers(TIMEOUT); + + io1.ns("/", async || ()).await.unwrap(); + io2.ns("/", async || ()).await.unwrap(); + + let (_, mut rx1) = io1.new_dummy_sock("/", ()).await; + let (_, mut rx2) = io2.new_dummy_sock("/", ()).await; + + timeout_rcv!(&mut rx1); // connect packet + timeout_rcv!(&mut rx2); // connect packet + + let now = Instant::now(); + io1.fetch_sockets().await.unwrap(); + assert!(now.elapsed() >= TIMEOUT); +} + +#[tokio::test] +pub async fn remote_socket_emit() { + let [io1, io2] = fixture::spawn_servers(); + + io1.ns("/", async || ()).await.unwrap(); + io2.ns("/", async || ()).await.unwrap(); + + let (_, mut rx1) = io1.new_dummy_sock("/", ()).await; + let (_, mut rx2) = io2.new_dummy_sock("/", ()).await; + + timeout_rcv!(&mut rx1); // connect packet + timeout_rcv!(&mut rx2); // connect packet + + let sockets = io1.fetch_sockets().await.unwrap(); + for socket in sockets { + socket.emit("test", "hello").await.unwrap(); + } + + assert_eq!(timeout_rcv!(&mut rx1), r#"42["test","hello"]"#); + assert_eq!(timeout_rcv!(&mut rx2), r#"42["test","hello"]"#); +} + +#[tokio::test] +pub async fn remote_socket_emit_with_ack() { + let [io1, io2] = fixture::spawn_servers(); + + io1.ns("/", async || ()).await.unwrap(); + io2.ns("/", async || ()).await.unwrap(); + + let (_, mut rx1) = io1.new_dummy_sock("/", ()).await; + let (_, mut rx2) = io2.new_dummy_sock("/", ()).await; + + timeout_rcv!(&mut rx1); // connect packet + timeout_rcv!(&mut rx2); // connect packet + + let sockets = io1.fetch_sockets().await.unwrap(); + for socket in sockets { + #[allow(unused_must_use)] + socket + .emit_with_ack::<_, ()>("test", "hello") + .await + .unwrap(); + } + + assert_eq!(timeout_rcv!(&mut rx1), r#"421["test","hello"]"#); + assert_eq!(timeout_rcv!(&mut rx2), r#"421["test","hello"]"#); +} diff --git a/e2e/adapter/Cargo.toml b/e2e/adapter/Cargo.toml index 1e94a487..cc7ea709 100644 --- a/e2e/adapter/Cargo.toml +++ b/e2e/adapter/Cargo.toml @@ -22,6 +22,11 @@ socketioxide-redis = { path = "../../crates/socketioxide-redis", features = [ "fred", ] } socketioxide-mongodb = { path = "../../crates/socketioxide-mongodb" } +socketioxide-postgres = { path = "../../crates/socketioxide-postgres", features = [ + "sqlx", + "postgres", +] } + hyper-util = { workspace = true, features = ["tokio"] } hyper = { workspace = true, features = ["server", "http1"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } @@ -80,3 +85,11 @@ path = "src/bins/mongodb_ttl.rs" [[bin]] name = "mongodb-ttl-e2e-msgpack" path = "src/bins/mongodb_ttl_msgpack.rs" + +[[bin]] +name = "sqlx-e2e" +path = "src/bins/sqlx.rs" + +[[bin]] +name = "sqlx-e2e-msgpack" +path = "src/bins/sqlx_msgpack.rs" diff --git a/e2e/adapter/main.rs b/e2e/adapter/main.rs index f174b849..35125745 100644 --- a/e2e/adapter/main.rs +++ b/e2e/adapter/main.rs @@ -3,7 +3,7 @@ use std::fs; use std::process::{Child, Command}; use std::time::Duration; -const BINS: [&str; 12] = [ +const BINS: &[&str] = &[ "fred-e2e", "fred-e2e-msgpack", "redis-e2e", @@ -16,24 +16,28 @@ const BINS: [&str; 12] = [ "mongodb-ttl-e2e-msgpack", "mongodb-capped-e2e", "mongodb-capped-e2e-msgpack", + "sqlx-e2e", + "sqlx-e2e-msgpack", ]; const EXEC_SUFFIX: &str = if cfg!(windows) { ".exe" } else { "" }; const LOG_DIR: &str = "e2e/adapter/logs"; -fn main() { - let filter = args().skip(1).next().unwrap_or("".to_string()); - println!("filter: {}", filter); +fn main() -> Result<(), Box> { + let bin_filter = args().nth(1).unwrap_or("".to_string()); + println!("binary target filter: {}", bin_filter); - if fs::exists(LOG_DIR).unwrap() { - fs::remove_dir_all(LOG_DIR).unwrap(); + if fs::exists(LOG_DIR)? { + fs::remove_dir_all(LOG_DIR)?; } - fs::create_dir_all(LOG_DIR).unwrap(); + fs::create_dir_all(LOG_DIR)?; // run everything - for target in BINS.into_iter().filter(|name| name.contains(&filter)) { + for target in BINS.iter().filter(|name| name.contains(&bin_filter)) { run(target); } println!("All tests passed!"); + + Ok(()) } fn run(target: &'static str) { @@ -51,7 +55,6 @@ fn run(target: &'static str) { std::thread::sleep(Duration::from_millis(200)); let child = Command::new("node") - .arg("--experimental-strip-types") .arg("--test-reporter=spec") .arg("--test") .arg("e2e/adapter/client.ts") diff --git a/e2e/adapter/src/bins/sqlx.rs b/e2e/adapter/src/bins/sqlx.rs new file mode 100644 index 00000000..37c457ba --- /dev/null +++ b/e2e/adapter/src/bins/sqlx.rs @@ -0,0 +1,60 @@ +use hyper::server::conn::http1; +use hyper_util::rt::TokioIo; +use socketioxide::SocketIo; + +use socketioxide_postgres::{ + PostgresAdapterConfig, PostgresAdapterCtr, SqlxAdapter, drivers::sqlx::sqlx_client::PgPool, +}; +use tokio::net::TcpListener; +use tracing::{Level, info}; +use tracing_subscriber::FmtSubscriber; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let subscriber = FmtSubscriber::builder() + .with_line_number(true) + .with_max_level(Level::TRACE) + .finish(); + tracing::subscriber::set_global_default(subscriber)?; + let variant = std::env::args().next().unwrap(); + let variant = variant.split("/").last().unwrap(); + + let config = PostgresAdapterConfig::new().with_prefix(format!("socket.io-{variant}")); + + let pg_pool = PgPool::connect("postgres://socketio:socketio@localhost:5432/socketio").await?; + let adapter = PostgresAdapterCtr::new_with_sqlx_config(pg_pool, config); + let (svc, io) = SocketIo::builder() + .with_adapter::>(adapter) + .build_svc(); + + io.ns("/", adapter_e2e::handler).await?; + + info!("Starting server with v5 protocol"); + let port: u16 = std::env::var("PORT") + .expect("a PORT env var should be set") + .parse()?; + + let listener = TcpListener::bind(("127.0.0.1", port)).await?; + + // We start a loop to continuously accept incoming connections + loop { + let (stream, _) = listener.accept().await?; + + // Use an adapter to access something implementing `tokio::io` traits as if they implement + // `hyper::rt` IO traits. + let io = TokioIo::new(stream); + let svc = svc.clone(); + + // Spawn a tokio task to serve multiple connections concurrently + tokio::task::spawn(async move { + // Finally, we bind the incoming connection to our `hello` service + if let Err(err) = http1::Builder::new() + .serve_connection(io, svc) + .with_upgrades() + .await + { + println!("Error serving connection: {:?}", err); + } + }); + } +} diff --git a/e2e/adapter/src/bins/sqlx_msgpack.rs b/e2e/adapter/src/bins/sqlx_msgpack.rs new file mode 100644 index 00000000..c5a9482d --- /dev/null +++ b/e2e/adapter/src/bins/sqlx_msgpack.rs @@ -0,0 +1,60 @@ +use hyper::server::conn::http1; +use hyper_util::rt::TokioIo; +use socketioxide::{ParserConfig, SocketIo}; + +use socketioxide_postgres::{ + PostgresAdapterConfig, PostgresAdapterCtr, SqlxAdapter, drivers::sqlx::sqlx_client::PgPool, +}; +use tokio::net::TcpListener; +use tracing::{Level, info}; +use tracing_subscriber::FmtSubscriber; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let subscriber = FmtSubscriber::builder() + .with_line_number(true) + .with_max_level(Level::TRACE) + .finish(); + tracing::subscriber::set_global_default(subscriber)?; + let variant = std::env::args().next().unwrap(); + let variant = variant.split("/").last().unwrap(); + let config = PostgresAdapterConfig::new().with_prefix(format!("socket.io-{variant}")); + + let pg_pool = PgPool::connect("postgres://socketio:socketio@localhost:5432/socketio").await?; + let adapter = PostgresAdapterCtr::new_with_sqlx_config(pg_pool, config); + let (svc, io) = SocketIo::builder() + .with_parser(ParserConfig::msgpack()) + .with_adapter::>(adapter) + .build_svc(); + + io.ns("/", adapter_e2e::handler).await?; + + info!("Starting server with v5 protocol"); + let port: u16 = std::env::var("PORT") + .expect("a PORT env var should be set") + .parse()?; + + let listener = TcpListener::bind(("127.0.0.1", port)).await?; + + // We start a loop to continuously accept incoming connections + loop { + let (stream, _) = listener.accept().await?; + + // Use an adapter to access something implementing `tokio::io` traits as if they implement + // `hyper::rt` IO traits. + let io = TokioIo::new(stream); + let svc = svc.clone(); + + // Spawn a tokio task to serve multiple connections concurrently + tokio::task::spawn(async move { + // Finally, we bind the incoming connection to our `hello` service + if let Err(err) = http1::Builder::new() + .serve_connection(io, svc) + .with_upgrades() + .await + { + println!("Error serving connection: {:?}", err); + } + }); + } +}