From f54b9f8f5bc023a6273cc82e4b13db31275b7a8d Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Fri, 9 Oct 2026 15:48:57 +0200 Subject: [PATCH 01/12] Auto reconnect implementation --- Cargo.lock | 1892 +++++++++++++++++++++++++++++++- Cargo.toml | 2 + crates/codegen/src/rust.rs | 8 + sdks/rust/Cargo.toml | 5 + sdks/rust/src/callbacks.rs | 4 +- sdks/rust/src/client_cache.rs | 34 + sdks/rust/src/db_connection.rs | 723 ++++++++++-- sdks/rust/src/db_context.rs | 3 + sdks/rust/src/error.rs | 18 +- sdks/rust/src/lib.rs | 2 + sdks/rust/src/reconnect.rs | 149 +++ sdks/rust/src/subscription.rs | 78 +- sdks/rust/src/websocket.rs | 114 +- 13 files changed, 2805 insertions(+), 227 deletions(-) create mode 100644 sdks/rust/src/reconnect.rs diff --git a/Cargo.lock b/Cargo.lock index 46f43fca21e..a6272521602 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "addr2line" version = "0.25.1" @@ -82,6 +98,31 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.10.0", + "cc", + "jni 0.22.4", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 2.0.17", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -186,6 +227,26 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win 5.4.1", + "image", + "log", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot 0.12.5", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "arcstr" version = "1.2.0" @@ -210,6 +271,21 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + [[package]] name = "assert_cmd" version = "2.2.2" @@ -538,7 +614,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.1.1", "shlex", "syn 2.0.107", ] @@ -598,6 +674,12 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -607,6 +689,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.3", +] + [[package]] name = "borsh" version = "1.5.7" @@ -746,6 +837,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.10.1" @@ -791,6 +888,56 @@ dependencies = [ "displaydoc", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.10.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags 2.10.0", + "polling", + "rustix 1.1.2", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.5", + "rustix 1.1.2", + "wayland-backend", + "wayland-client", +] + [[package]] name = "camino" version = "1.2.1" @@ -899,6 +1046,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + [[package]] name = "check-license-symlinks" version = "0.1.0" @@ -1326,11 +1482,20 @@ version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" dependencies = [ - "error-code", + "error-code 2.3.1", "str-buf", "winapi", ] +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code 3.4.0", +] + [[package]] name = "cobs" version = "0.3.0" @@ -1340,6 +1505,16 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width 0.1.14", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -1517,6 +1692,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core_affinity" version = "0.8.3" @@ -1618,7 +1817,7 @@ dependencies = [ "log", "pulley-interpreter", "regalloc2", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "smallvec", "target-lexicon", @@ -1850,6 +2049,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -2184,6 +2389,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2195,12 +2416,42 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "dragonbox_ecma" version = "0.1.0" @@ -2239,6 +2490,16 @@ dependencies = [ "spki", ] +[[package]] +name = "ecolor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +dependencies = [ + "bytemuck", + "emath", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -2275,6 +2536,114 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "eframe" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c" +dependencies = [ + "ahash 0.8.12", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot 0.12.5", + "percent-encoding", + "profiling", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winapi", + "windows-sys 0.59.0", + "winit", +] + +[[package]] +name = "egui" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +dependencies = [ + "ahash 0.8.12", + "bitflags 2.10.0", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", +] + +[[package]] +name = "egui-wgpu" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820" +dependencies = [ + "ahash 0.8.12", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 1.0.69", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" +dependencies = [ + "ahash 0.8.12", + "arboard", + "bytemuck", + "egui", + "log", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" +dependencies = [ + "ahash 0.8.12", + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", + "winit", +] + [[package]] name = "either" version = "1.15.0" @@ -2302,6 +2671,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "emath" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" +dependencies = [ + "bytemuck", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -2431,6 +2809,30 @@ dependencies = [ "spacetimedb", ] +[[package]] +name = "epaint" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +dependencies = [ + "ab_glyph", + "ahash 0.8.12", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot 0.12.5", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" + [[package]] name = "equivalent" version = "1.0.2" @@ -2457,6 +2859,12 @@ dependencies = [ "str-buf", ] +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + [[package]] name = "ethnum" version = "1.5.3" @@ -2570,7 +2978,13 @@ dependencies = [ ] [[package]] -name = "fd-lock" +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fd-lock" version = "3.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" @@ -2580,6 +2994,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -2670,7 +3093,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", ] [[package]] @@ -2679,6 +3123,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2894,6 +3344,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.2", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -2945,6 +3405,17 @@ dependencies = [ "url", ] +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + [[package]] name = "glob" version = "0.3.3" @@ -3038,6 +3509,123 @@ dependencies = [ "web-sys", ] +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.10.0", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.10.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.10.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "group" version = "0.13.0" @@ -3208,6 +3796,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hkdf" version = "0.12.4" @@ -3604,6 +4198,20 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -3850,19 +4458,68 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.0", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.17", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.107", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.107", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -3969,6 +4626,23 @@ dependencies = [ "spacetimedb", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "kqueue" version = "1.1.1" @@ -4182,6 +4856,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -4240,6 +4920,15 @@ dependencies = [ "libc", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "mappings" version = "0.7.1" @@ -4308,6 +4997,30 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +dependencies = [ + "bitflags 2.10.0", + "block", + "core-graphics-types", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + [[package]] name = "mimalloc" version = "0.1.48" @@ -4360,6 +5073,16 @@ dependencies = [ "spacetimedb", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multer" version = "3.1.0" @@ -4397,6 +5120,28 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "naga" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.10.0", + "cfg_aliases", + "codespan-reporting", + "hexf-parse", + "indexmap 2.12.0", + "log", + "rustc-hash 1.1.0", + "spirv", + "strum 0.26.3", + "termcolor", + "thiserror 2.0.17", + "unicode-xid", +] + [[package]] name = "names" version = "0.14.0" @@ -4424,12 +5169,45 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys 0.3.0", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + [[package]] name = "ndk-context" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.0", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.0", +] + [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -4743,20 +5521,132 @@ dependencies = [ ] [[package]] -name = "number_prefix" -version = "0.4.0" +name = "num_enum" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] [[package]] -name = "objc2" -version = "0.6.3" +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2 0.5.3", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -4764,6 +5654,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-contacts", + "objc2-foundation 0.2.2", ] [[package]] @@ -4782,6 +5711,19 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2", + "dispatch", + "libc", + "objc2 0.5.3", +] + [[package]] name = "objc2-foundation" version = "0.3.2" @@ -4789,7 +5731,111 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.10.0", - "objc2", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", ] [[package]] @@ -4830,7 +5876,7 @@ checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ "bitflags 2.10.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -4882,6 +5928,25 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -4904,6 +5969,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "owo-colors" version = "3.5.0" @@ -4948,7 +6022,7 @@ checksum = "75b1853bc34cadaa90aa09f95713d8b77ec0c0d3e2d90ccf7a74216f40d20850" dependencies = [ "flate2", "postcard", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 2.0.17", @@ -4990,7 +6064,7 @@ dependencies = [ "hashbrown 0.16.1", "oxc_data_structures", "oxc_estree", - "rustc-hash", + "rustc-hash 2.1.1", "serde", ] @@ -5046,7 +6120,7 @@ dependencies = [ "oxc_index", "oxc_syntax", "petgraph 0.8.3", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5067,7 +6141,7 @@ dependencies = [ "oxc_sourcemap", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5079,7 +6153,7 @@ dependencies = [ "cow-utils", "oxc-browserslist", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", "serde", ] @@ -5155,7 +6229,7 @@ dependencies = [ "oxc_ecmascript", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5172,7 +6246,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5196,7 +6270,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_traverse", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5218,7 +6292,7 @@ dependencies = [ "oxc_regular_expression", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", "seq-macro", ] @@ -5234,7 +6308,7 @@ dependencies = [ "oxc_diagnostics", "oxc_span", "phf", - "rustc-hash", + "rustc-hash 2.1.1", "unicode-id-start", ] @@ -5253,7 +6327,7 @@ dependencies = [ "papaya", "parking_lot 0.12.5", "pnp", - "rustc-hash", + "rustc-hash 2.1.1", "rustix 1.1.2", "self_cell", "serde", @@ -5284,7 +6358,7 @@ dependencies = [ "oxc_index", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", "self_cell", "smallvec", ] @@ -5297,7 +6371,7 @@ checksum = "36801dbbd025f2fa133367494e38eef75a53d334ae6746ba0c889fc4e76fa3a3" dependencies = [ "base64-simd", "json-escape-simd", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", ] @@ -5373,7 +6447,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_traverse", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "sha1", @@ -5398,7 +6472,7 @@ dependencies = [ "oxc_syntax", "oxc_transformer", "oxc_traverse", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5416,7 +6490,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -5669,7 +6743,7 @@ dependencies = [ "ryu", "serde", "serde_json", - "smol_str", + "smol_str 0.3.4", "thiserror 2.0.17", "tokio", "tokio-util", @@ -5818,6 +6892,19 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "pnp" version = "0.12.7" @@ -5831,12 +6918,26 @@ dependencies = [ "nodejs-built-in-modules 1.0.0", "pathdiff", "radix_trie 0.3.0", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 2.0.17", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -6075,6 +7176,12 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + [[package]] name = "prometheus" version = "0.14.0" @@ -6227,12 +7334,24 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-junit" version = "0.3.6" @@ -6274,6 +7393,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quickstart-chat-module" version = "0.1.0" @@ -6437,6 +7565,12 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "rayon" version = "1.11.0" @@ -6466,6 +7600,21 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "reconnect-desktop" +version = "0.1.0" +dependencies = [ + "anyhow", + "eframe", + "jsonwebtoken", + "serde_json", + "spacetimedb-client-api-messages", + "spacetimedb-lib", + "spacetimedb-sdk", + "tokio", + "tokio-tungstenite 0.27.0", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -6475,6 +7624,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -6525,7 +7683,7 @@ dependencies = [ "bumpalo", "hashbrown 0.15.5", "log", - "rustc-hash", + "rustc-hash 2.1.1", "smallvec", ] @@ -6608,6 +7766,12 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "replace-spacetimedb" version = "0.1.0" @@ -6821,7 +7985,7 @@ dependencies = [ "rolldown_std_utils", "rolldown_tracing", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "string_wizard", @@ -6889,7 +8053,7 @@ dependencies = [ "rolldown_sourcemap", "rolldown_std_utils", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "simdutf8", @@ -6916,7 +8080,7 @@ dependencies = [ "blake3", "dashmap 6.1.0", "rolldown_devtools_action", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "tracing", @@ -6970,7 +8134,7 @@ dependencies = [ "oxc_resolver", "rolldown-ariadne", "ropey", - "rustc-hash", + "rustc-hash 2.1.1", "sugar_path", ] @@ -7003,7 +8167,7 @@ dependencies = [ "rolldown_resolver", "rolldown_sourcemap", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "string_wizard", @@ -7022,7 +8186,7 @@ dependencies = [ "rolldown_common", "rolldown_plugin", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde_json", "xxhash-rust", ] @@ -7101,7 +8265,7 @@ dependencies = [ "oxc", "oxc_sourcemap", "rolldown_utils", - "rustc-hash", + "rustc-hash 2.1.1", ] [[package]] @@ -7151,7 +8315,7 @@ dependencies = [ "regress", "rolldown_error", "rolldown_std_utils", - "rustc-hash", + "rustc-hash 2.1.1", "serde_json", "simdutf8", "sugar_path", @@ -7239,6 +8403,12 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -7326,7 +8496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", - "quick-error", + "quick-error 1.2.3", "tempfile", "wait-timeout", ] @@ -7339,7 +8509,7 @@ checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" dependencies = [ "bitflags 2.10.0", "cfg-if", - "clipboard-win", + "clipboard-win 4.5.0", "fd-lock", "home", "libc", @@ -7467,6 +8637,13 @@ dependencies = [ "spacetimedb", ] +[[package]] +name = "sdk-test-reconnect-module" +version = "0.1.0" +dependencies = [ + "spacetimedb", +] + [[package]] name = "sdk-test-view" version = "0.1.0" @@ -7845,11 +9022,21 @@ dependencies = [ ] [[package]] -name = "simdutf8" -version = "0.1.5" +name = "simd_cesu8" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -7896,6 +9083,15 @@ dependencies = [ "parking_lot 0.11.2", ] +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -7911,6 +9107,78 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.10.0", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.10.0", + "calloop 0.14.5", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.2", + "thiserror 2.0.17", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "smol_str" version = "0.3.4" @@ -7952,7 +9220,7 @@ dependencies = [ "data-encoding", "debugid", "if_chain", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "serde_json", "unicode-id-start", @@ -8305,7 +9573,7 @@ dependencies = [ "regex", "reqwest", "rustc-demangle", - "rustc-hash", + "rustc-hash 2.1.1", "scopeguard", "semver", "serde", @@ -8336,7 +9604,7 @@ dependencies = [ "spacetimedb-schema", "spacetimedb-subscription", "spacetimedb-table", - "strum", + "strum 0.25.0", "tempfile", "thiserror 1.0.69", "tikv-jemalloc-ctl", @@ -8405,7 +9673,7 @@ dependencies = [ "spacetimedb-schema", "spacetimedb-snapshot", "spacetimedb-table", - "strum", + "strum 0.25.0", "thin-vec", "thiserror 1.0.69", ] @@ -8810,6 +10078,7 @@ name = "spacetimedb-sdk" version = "2.12.0" dependencies = [ "anymap3", + "base64 0.21.7", "brotli 3.5.0", "bytes", "flate2", @@ -8819,6 +10088,7 @@ dependencies = [ "gloo-console", "gloo-net", "gloo-storage", + "gloo-timers", "gloo-utils", "home", "http", @@ -8827,6 +10097,8 @@ dependencies = [ "native-tls", "once_cell", "prometheus", + "rand 0.9.2", + "serde_json", "shlex", "spacetimedb-client-api-messages", "spacetimedb-data-structures", @@ -9069,6 +10341,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "spki" version = "0.7.3" @@ -9186,7 +10467,7 @@ dependencies = [ "oxc_index", "oxc_sourcemap", "regex", - "rustc-hash", + "rustc-hash 2.1.1", "serde", ] @@ -9228,7 +10509,16 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros", + "strum_macros 0.25.3", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", ] [[package]] @@ -9244,6 +10534,19 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.107", +] + [[package]] name = "subst" version = "0.3.8" @@ -9291,6 +10594,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -9600,6 +10914,20 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg", +] + [[package]] name = "tikv-jemalloc-ctl" version = "0.6.1" @@ -10213,6 +11541,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "tungstenite" version = "0.26.2" @@ -10249,6 +11583,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + [[package]] name = "typedmap" version = "0.6.0" @@ -10937,6 +12280,141 @@ dependencies = [ "wit-parser", ] +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.2", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.10.0", + "rustix 1.1.2", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.10.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.2", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml 0.41.0", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.81" @@ -10964,15 +12442,124 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" dependencies = [ "core-foundation 0.10.1", - "jni", + "jni 0.21.1", "log", "ndk-context", - "objc2", - "objc2-foundation", + "objc2 0.6.3", + "objc2-foundation 0.3.2", "url", "web-sys", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" +dependencies = [ + "arrayvec", + "bitflags 2.10.0", + "cfg_aliases", + "document-features", + "js-sys", + "log", + "parking_lot 0.12.5", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.10.0", + "cfg_aliases", + "document-features", + "indexmap 2.12.0", + "log", + "naga", + "once_cell", + "parking_lot 0.12.5", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.17", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "24.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bitflags 2.10.0", + "bytemuck", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "ordered-float", + "parking_lot 0.12.5", + "profiling", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.17", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", +] + +[[package]] +name = "wgpu-types" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +dependencies = [ + "bitflags 2.10.0", + "js-sys", + "log", + "web-sys", +] + [[package]] name = "which" version = "6.0.3" @@ -11038,6 +12625,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -11081,14 +12678,27 @@ dependencies = [ "windows-core 0.62.2", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -11100,8 +12710,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -11129,6 +12739,17 @@ dependencies = [ "windows-threading 0.2.1", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -11140,6 +12761,17 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -11194,6 +12826,15 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -11212,6 +12853,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -11545,6 +13196,57 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash 0.8.12", + "android-activity", + "atomic-waker", + "bitflags 2.10.0", + "block2", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smithay-client-toolkit 0.19.2", + "smol_str 0.2.2", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "0.7.13" @@ -11599,6 +13301,38 @@ dependencies = [ "tap", ] +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.2", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "xattr" version = "1.6.1" @@ -11609,12 +13343,37 @@ dependencies = [ "rustix 1.1.2", ] +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + [[package]] name = "xdg" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.10.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "xml-rs" version = "0.8.28" @@ -11903,3 +13662,18 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 9e843d8acd8..3dbbe46273f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,8 @@ members = [ "crates/smoketests", "sdks/csharp/test-harness~", "sdks/rust", + "sdks/rust/examples/reconnect-desktop", + "modules/sdk-test-reconnect", "sdks/unreal", "crates/snapshot", "crates/sqltest", diff --git a/crates/codegen/src/rust.rs b/crates/codegen/src/rust.rs index cea7ea87dd9..82c065adb6f 100644 --- a/crates/codegen/src/rust.rs +++ b/crates/codegen/src/rust.rs @@ -1795,6 +1795,10 @@ impl __sdk::DbContext for DbConnection {{ self.imp.is_active() }} + fn is_reconnecting(&self) -> bool {{ + self.imp.is_reconnecting() + }} + fn disconnect(&self) -> __sdk::Result<()> {{ self.imp.disconnect() }} @@ -2122,6 +2126,10 @@ impl __sdk::DbContext for {struct_and_trait_name} {{ self.imp.is_active() }} + fn is_reconnecting(&self) -> bool {{ + self.imp.is_reconnecting() + }} + fn disconnect(&self) -> __sdk::Result<()> {{ self.imp.disconnect() }} diff --git a/sdks/rust/Cargo.toml b/sdks/rust/Cargo.toml index 7f1f24f6ff7..dd3b47f33f9 100644 --- a/sdks/rust/Cargo.toml +++ b/sdks/rust/Cargo.toml @@ -17,6 +17,7 @@ browser = [ "dep:gloo-net", "dep:gloo-storage", "dep:gloo-utils", + "dep:gloo-timers", "dep:js-sys", "dep:tokio-tungstenite-wasm", "dep:wasm-bindgen", @@ -33,6 +34,10 @@ spacetimedb-metrics.workspace = true spacetimedb-query-builder.workspace = true thiserror.workspace = true +base64.workspace = true +serde_json.workspace = true +rand.workspace = true +gloo-timers = { version = "0.3", features = ["futures"], optional = true } anymap3 = "1.0.1" brotli.workspace = true bytes.workspace = true diff --git a/sdks/rust/src/callbacks.rs b/sdks/rust/src/callbacks.rs index a3fb083af1e..6da30881a1b 100644 --- a/sdks/rust/src/callbacks.rs +++ b/sdks/rust/src/callbacks.rs @@ -217,7 +217,7 @@ pub(crate) type ReducerCallback = Box< /// function. Users should not reference this struct directly. pub(crate) struct ReducerCallbacks { /// Maps WebSocket `request_id` to the arguments of that call and user-supplied callback. - callbacks: HashMap)>, + pub(crate) callbacks: HashMap)>, } impl Default for ReducerCallbacks { @@ -247,7 +247,7 @@ pub(crate) type ProcedureCallback = Box::ProcedureEventContext, Result) + Send + 'static>; pub struct ProcedureCallbacks { - request_id_to_callback: HashMap>, + pub(crate) request_id_to_callback: HashMap>, } impl Default for ProcedureCallbacks { diff --git a/sdks/rust/src/client_cache.rs b/sdks/rust/src/client_cache.rs index ab0a90980bd..d7b3487c312 100644 --- a/sdks/rust/src/client_cache.rs +++ b/sdks/rust/src/client_cache.rs @@ -341,6 +341,11 @@ pub struct ClientCache { /// /// The strings are table names, since we may have multiple tables with the same row type. tables: Map, + // Erased per-table readers let reconnect synthesize removals from the live cache + // without extending generated DbUpdate implementations. Every reference is + // included, preserving overlap counts when all replayed sets are applied at once. + snapshotters: + HashMap<&'static str, fn(&Self, &'static str) -> spacetimedb_client_api_messages::websocket::v2::TableUpdate>, /// Clone of the [`crate::db_connection::DbContextImpl::extra_logging`]. extra_logging: Option>, @@ -352,11 +357,19 @@ impl ClientCache { pub(crate) fn new(extra_logging: Option>) -> Self { Self { tables: Map::new(), + snapshotters: HashMap::default(), extra_logging, _module: PhantomData, } } + pub(crate) fn removal_snapshot(&self) -> Vec { + self.snapshotters + .iter() + .map(|(&name, snapshot)| snapshot(self, name)) + .collect() + } + /// Get a handle on the [`TableCache`] which stores rows of type `Row` for the table `table_name`. pub(crate) fn get_table + Send + Sync + 'static>( &self, @@ -373,6 +386,27 @@ impl ClientCache { &mut self, table_name: &'static str, ) -> &mut TableCache { + self.snapshotters.entry(table_name).or_insert(|cache, name| { + use spacetimedb_client_api_messages::websocket::{common::RowSizeHint, v2}; + let mut bytes = Vec::new(); + let mut offsets = Vec::new(); + if let Some(table) = cache.get_table::(name) { + for (bsatn, entry) in &table.entries { + for _ in 0..entry.ref_count { + offsets.push(bytes.len() as u64); + bytes.extend_from_slice(bsatn); + } + } + } + v2::TableUpdate { + table_name: name.into(), + rows: vec![v2::TableUpdateRows::PersistentTable(v2::PersistentTableRows { + inserts: v2::BsatnRowList::default(), + deletes: v2::BsatnRowList::new(RowSizeHint::RowOffsets(offsets.into()), bytes.into()), + })] + .into(), + } + }); self.tables .entry::>>() .or_default() diff --git a/sdks/rust/src/db_connection.rs b/sdks/rust/src/db_connection.rs index c1810f7e5dd..3b8d71fa6b0 100644 --- a/sdks/rust/src/db_connection.rs +++ b/sdks/rust/src/db_connection.rs @@ -28,10 +28,14 @@ use crate::{ spacetime_module::{AbstractEventContext, AppliedDiff, DbConnection, DbUpdate, InModule, SpacetimeModule}, subscription::{PendingUnsubscribeResult, SubscriptionHandleImpl, SubscriptionManager}, websocket::{WsConnection, WsParams}, - Event, ReducerEvent, Status, + AutomaticReconnectOptions, Event, NextReconnect, ReducerEvent, Status, TokenProvider, }; use bytes::Bytes; -use futures::StreamExt; +use futures::{ + future::{AbortHandle, Abortable}, + StreamExt, +}; + #[cfg(feature = "browser")] use futures::{pin_mut, FutureExt}; use futures_channel::mpsc; @@ -103,6 +107,7 @@ pub struct DbContextImpl { /// This may be none if we have not yet received the [`ws::v2::InitialConnection`] message. connection_id: SharedCell>, + reconnect: SharedCell, pub(crate) extra_logging: Option>, } @@ -123,6 +128,7 @@ impl Clone for DbContextImpl { identity: Arc::clone(&self.identity), connection_id: Arc::clone(&self.connection_id), extra_logging: Option::>::clone(&self.extra_logging), + reconnect: self.reconnect.clone(), } } } @@ -135,54 +141,113 @@ impl DbContextImpl { /// Process a parsed WebSocket message, /// applying its mutations to the client cache and invoking callbacks. fn process_message(&self, msg: ParsedMessage) -> crate::Result<()> { + let result = self.process_message_inner(msg); + let ended = self.inner.lock().unwrap().connection_lifecycle == ConnectionLifecycle::Ended; + match result { + Err(crate::Error::Disconnected) if !ended => self.connection_lost(None, false, false), + Err(error) if !ended => self.connection_lost(Some(error), true, false), + result => result, + } + } + + fn process_message_inner(&self, msg: ParsedMessage) -> crate::Result<()> { + // Discard queued messages after explicit disconnect or a terminal failure. + if self.inner.lock().unwrap().connection_lifecycle == ConnectionLifecycle::Ended { + return Err(crate::Error::Disconnected); + } self.debug_log(|out| writeln!(out, "`process_message`: {msg:?}")); match msg { // Error: route as a connection error if we never finished connecting, // otherwise treat it as an erroneous disconnect. - ParsedMessage::Error(e) => Err(self.end_connection(Some(e))), - - // Initial `IdentityToken` message: - // confirm that the received identity and connection ID are what we expect, - // store them, then invoke the on_connect callback. + ParsedMessage::Error(e) => self.connection_lost(Some(e), true, false), + ParsedMessage::TransportError(e) => { + let terminal = e.is_terminal(); + let auth = e.is_auth(); + self.connection_lost( + Some(InternalError::new(e.to_string()).with_cause(e).into()), + terminal, + auth, + ) + } ParsedMessage::IdentityToken(identity, token, conn_id) => { - let on_connect = { - let mut inner = self.inner.lock().unwrap(); - match inner.connection_lifecycle { - ConnectionLifecycle::Connecting => { - inner.connection_lifecycle = ConnectionLifecycle::Connected; - inner.on_connect.take() - } - ConnectionLifecycle::Connected => None, - ConnectionLifecycle::Ended => return Ok(()), - } + if self.inner.lock().unwrap().connection_lifecycle == ConnectionLifecycle::Ended { + return Ok(()); + } + if self + .identity + .lock() + .unwrap() + .is_some_and(|previous| previous != identity) + { + return self.connection_lost( + Some(InternalError::new("Reconnect returned a different identity").into()), + true, + false, + ); + } + let reconnecting = { + let mut state = self.reconnect.lock().unwrap(); + let reconnecting = state.reconnecting; + state.reconnecting = false; + state.attempt = 0; + state.token = Some(token.to_string()); + reconnecting }; + *self.identity.lock().unwrap() = Some(identity); + *self.connection_id.lock().unwrap() = Some(conn_id); { - // Don't hold the `self.identity` lock while running callbacks. - // Callbacks can (will) call [`DbContext::identity`], which acquires that lock, - // so holding it while running a callback causes deadlocks. - let mut ident_store = self.identity.lock().unwrap(); - if let Some(prev_identity) = *ident_store { - assert_eq!(prev_identity, identity); + let mut inner = self.inner.lock().unwrap(); + if inner.connection_lifecycle == ConnectionLifecycle::Connected { + drop(inner); + return self.connection_lost( + Some(InternalError::new("Duplicate InitialConnection").into()), + true, + false, + ); } - *ident_store = Some(identity); + inner.connection_lifecycle = ConnectionLifecycle::Connected; } - { - // Don't hold the `self.connection_id` lock while running callbacks. - // Callbacks can (will) call [`DbContext::connection_id`], which acquires that lock, - // so holding it while running a callback causes deadlocks. - let mut conn_id_store = self.connection_id.lock().unwrap(); - // This would only happen if the client is using the unstable `set_connection_id` method. - if let Some(prev_conn_id) = *conn_id_store { - assert_eq!(prev_conn_id, conn_id); + let ctx = ::new(self.clone()); + if reconnecting { + let callback = self.inner.lock().unwrap().on_automatic_reconnect.take(); + if let Some(mut callback) = callback { + callback(&ctx, identity, &token); + self.inner.lock().unwrap().on_automatic_reconnect = Some(callback); + } + } else { + let callback = self.inner.lock().unwrap().on_connect.take(); + if let Some(callback) = callback { + callback(&ctx, identity, &token); } - *conn_id_store = Some(conn_id); } - if let Some(on_connect) = on_connect { - let ctx = ::new(self.clone()); - on_connect(&ctx, identity, &token); + if reconnecting { + // Include subscriptions queued from the reconnect callback in the batch. + self.reconnect.lock().unwrap().preparing_replay = true; + self.apply_pending_mutations()?; + if self.inner.lock().unwrap().connection_lifecycle != ConnectionLifecycle::Connected { + return Ok(()); + } + let mut inner = self.inner.lock().unwrap(); + let sets = inner.subscriptions.replay(&self.make_event_ctx(())); + let request_id = next_request_id(); + inner.replay = Some((request_id, sets.iter().map(|set| set.query_set_id).collect())); + self.reconnect.lock().unwrap().preparing_replay = false; + drop(inner); + if sets.is_empty() { + self.apply_replay(ws::v2::SubscribeBatchApplied { + request_id, + results: Box::new([]), + })?; + } else { + self.send_message(ws::v2::ClientMessage::SubscribeBatch(ws::v2::SubscribeBatch { + request_id, + sets, + }))?; + } } Ok(()) } + ParsedMessage::SubscribeBatchApplied(batch) => self.apply_replay(batch), // Transaction update: // apply the received diff to the client cache, @@ -312,47 +377,223 @@ impl DbContextImpl { applied_diff.invoke_row_callbacks(&row_event_ctx, &mut inner.db_callbacks); } - /// Mark the connection lifecycle as ended, route the terminal event to the - /// appropriate connection callback, and mark [`Self::is_active`] false. - /// - /// Returns the terminal error that should be returned from `advance_*` methods. - fn end_connection(&self, callback_error: Option) -> crate::Error { - let mut inner = self.inner.lock().unwrap(); - let return_error = callback_error.clone().unwrap_or(crate::Error::Disconnected); + fn send_message(&self, message: ws::v2::ClientMessage) -> crate::Result<()> { + self.send_chan + .lock() + .unwrap() + .as_ref() + .ok_or(crate::Error::Disconnected)? + .unbounded_send(message) + .map_err(|_| crate::Error::Disconnected) + } - let lifecycle = inner.connection_lifecycle; - if lifecycle == ConnectionLifecycle::Ended { - return return_error; + fn apply_replay(&self, batch: ws::v2::SubscribeBatchApplied) -> crate::Result<()> { + let expected = self.inner.lock().unwrap().replay.take(); + let ids: Vec<_> = batch.results.iter().map(|result| result.query_set_id).collect(); + if expected != Some((batch.request_id, ids)) { + return self.connection_lost( + Some(InternalError::new("Unexpected subscription replay response").into()), + true, + false, + ); } - inner.connection_lifecycle = ConnectionLifecycle::Ended; - - // Set `send_chan` to `None`, since `Self::is_active` checks that. - *self.send_chan.lock().unwrap() = None; - - match lifecycle { - ConnectionLifecycle::Connecting => { - let callback_error = callback_error.unwrap_or_else(|| crate::Error::FailedToConnect { - source: InternalError::new("Connection closed before receiving the initial connection message"), - }); - let ctx: M::ErrorContext = self.make_event_ctx(Some(callback_error.clone())); - if let Some(connect_error_callback) = inner.on_connect_error.take() { - connect_error_callback(&ctx, callback_error.clone()); + let mut tables = self.cache.lock().unwrap().removal_snapshot(); + let mut outcomes = Vec::new(); + for result in batch.results { + let error = match result.outcome { + ws::v2::SubscribeSetOutcome::Applied(rows) => { + tables.extend(rows.tables.into_vec().into_iter().map(|table| { + ws::v2::TableUpdate { + table_name: table.table, + rows: vec![ws::v2::TableUpdateRows::PersistentTable(ws::v2::PersistentTableRows { + inserts: table.rows, + deletes: Default::default(), + })] + .into(), + } + })); + None } - callback_error - } - ConnectionLifecycle::Connected => { - let ctx: M::ErrorContext = self.make_event_ctx(callback_error.clone()); - if let Some(disconnect_callback) = inner.on_disconnect.take() { - disconnect_callback(&ctx, callback_error.clone()); + ws::v2::SubscribeSetOutcome::Error(error) => Some(error.to_string()), + }; + outcomes.push((result.query_set_id, error)); + } + let update = M::DbUpdate::parse_update(ws::v2::TransactionUpdate { + query_sets: vec![ws::v2::QuerySetUpdate { + query_set_id: QuerySetId { id: 0 }, + tables: tables.into(), + }] + .into(), + }); + let update = match update { + Ok(update) => update, + Err(error) => return self.connection_lost(Some(error), true, false), + }; + self.apply_update(update, |inner| { + for (id, error) in outcomes { + if let Some(error) = error { + inner.subscriptions.subscription_error( + &self.make_event_ctx(Some(crate::Error::SubscriptionError { error })), + id, + ); + } else { + inner.subscriptions.subscription_applied(&self.make_event_ctx(()), id); } + } + Event::SubscribeApplied + }); + Ok(()) + } - // Call the `on_disconnect` method for all subscriptions. - inner.subscriptions.on_disconnect(&ctx); + fn fail_pending_operations(&self, inner: &mut DbContextImplInner) { + let error = InternalError::new("Connection lost before the result was received") + .with_cause(crate::Error::UnknownResult); + for (_, (reducer, callback)) in std::mem::take(&mut inner.reducer_callbacks.callbacks) { + let ctx = self.make_event_ctx(ReducerEvent { + reducer, + timestamp: Timestamp::UNIX_EPOCH, + status: Status::Panic(error.clone()), + }); + callback(&ctx, Err(error.clone())); + } + for (_, callback) in std::mem::take(&mut inner.procedure_callbacks.request_id_to_callback) { + callback(&self.make_event_ctx(()), Err(error.clone())); + } + } - return_error + fn connection_lost(&self, error: Option, terminal: bool, auth: bool) -> crate::Result<()> { + let lifecycle = self.inner.lock().unwrap().connection_lifecycle; + if lifecycle == ConnectionLifecycle::Ended { + return Err(crate::Error::Disconnected); + } + *self.send_chan.lock().unwrap() = None; + let next = { + let mut state = self.reconnect.lock().unwrap(); + let can_retry = state.options.is_some() + && self.identity.lock().unwrap().is_some() + && !terminal + && !(auth && (state.provider.is_none() || state.used_fresh_token)); + if can_retry { + state.force_refresh |= auth; + state.reconnecting = true; + state.preparing_replay = true; + state.attempt = state.attempt.saturating_add(1); + Some(NextReconnect { + attempt: state.attempt, + delay: state.options.unwrap().delay(state.attempt, rand::random()), + }) + } else { + state.reconnecting = false; + state.preparing_replay = false; + state.generation += 1; + if let Some(abort) = state.abort.take() { + abort.abort(); + } + None } - ConnectionLifecycle::Ended => return_error, + }; + if let Some(next) = next { + self.schedule_reconnect(next); } + let mut inner = self.inner.lock().unwrap(); + inner.connection_lifecycle = if next.is_some() { + ConnectionLifecycle::Connecting + } else { + ConnectionLifecycle::Ended + }; + inner.replay = None; + self.fail_pending_operations(&mut inner); + if next.is_some() { + inner.subscriptions.suspend(&self.make_event_ctx(())); + } else { + inner.subscriptions.on_disconnect(&self.make_event_ctx(())); + } + let callback_error = if lifecycle == ConnectionLifecycle::Connecting { + Some(error.unwrap_or_else(|| InternalError::new("Connection closed before InitialConnection").into())) + } else { + error + }; + let ctx = self.make_event_ctx(callback_error.clone()); + if lifecycle == ConnectionLifecycle::Connected { + if let Some(callback) = inner.on_disconnect.as_mut() { + callback(&ctx, callback_error.clone(), next); + } + } else if let Some(callback) = inner.on_connect_error.as_mut() { + callback(&ctx, callback_error.clone().unwrap(), next); + } + if next.is_some() { + Ok(()) + } else { + Err(callback_error.unwrap_or(crate::Error::Disconnected)) + } + } + + fn schedule_reconnect(&self, next: NextReconnect) { + let (park_send, park_recv) = mpsc::unbounded(); + *get_lock_sync(&self.recv) = park_recv; + let mut state = self.reconnect.lock().unwrap(); + state.generation += 1; + let generation = state.generation; + let config = state.config.clone(); + let mut token = state.token.clone(); + let provider = state.provider.clone(); + let force_refresh = state.force_refresh; + let (abort, registration) = AbortHandle::new_pair(); + state.abort = Some(abort); + drop(state); + let pending = self.pending_mutations_send.clone(); + let extra_logging = self.extra_logging.clone(); + #[cfg(not(feature = "browser"))] + let runtime = self.runtime.clone(); + let task = async move { + #[cfg(not(feature = "browser"))] + tokio::time::sleep(next.delay).await; + #[cfg(feature = "browser")] + gloo_timers::future::TimeoutFuture::new(next.delay.as_millis().min(u32::MAX as u128) as u32).await; + let provider = provider.filter(|_| { + force_refresh || crate::reconnect::token_needs_refresh(token.as_deref(), crate::reconnect::now()) + }); + let fresh = provider.is_some(); + let result = async { + if let Some(provider) = provider { + token = Some(provider.token().await.map_err(ReconnectFailure::Provider)?); + if token.as_ref().is_none_or(|token| token.is_empty()) { + return Err(ReconnectFailure::Provider( + InternalError::new("Token provider returned an empty token").into(), + )); + } + } + let socket = + WsConnection::connect(config.uri, &config.database_name, token.as_deref(), None, config.params) + .await + .map_err(ReconnectFailure::Transport)?; + #[cfg(not(feature = "browser"))] + let (_, raw_recv, send) = socket.spawn_message_loop(&runtime, extra_logging.clone()); + #[cfg(feature = "browser")] + let (raw_recv, send) = socket.spawn_message_loop(); + #[cfg(not(feature = "browser"))] + let (_, recv) = spawn_parse_loop(raw_recv, &runtime, extra_logging); + #[cfg(feature = "browser")] + let recv = spawn_parse_loop(raw_recv, extra_logging); + Ok((send, recv)) + } + .await; + let _ = pending.unbounded_send(PendingMutation::ReconnectReady { + _park_send: park_send, + generation, + token, + fresh, + result, + }); + }; + #[cfg(not(feature = "browser"))] + self.runtime.spawn(async move { + let _ = Abortable::new(task, registration).await; + }); + #[cfg(feature = "browser")] + wasm_bindgen_futures::spawn_local(async move { + let _ = Abortable::new(task, registration).await; + }); } fn make_event_ctx>(&self, event: E) -> Ctx { @@ -371,14 +612,61 @@ impl DbContextImpl { /// Apply an individual [`PendingMutation`]. fn apply_mutation(&self, mutation: PendingMutation) -> crate::Result<()> { + let explicit_disconnect = matches!(&mutation, PendingMutation::Disconnect); + match self.apply_mutation_inner(mutation) { + Err(crate::Error::Disconnected) if !explicit_disconnect => self.connection_lost(None, false, false), + result => result, + } + } + + fn apply_mutation_inner(&self, mutation: PendingMutation) -> crate::Result<()> { self.debug_log(|out| writeln!(out, "`apply_mutation`: {mutation:?}")); match mutation { + PendingMutation::ReconnectReady { + _park_send, + generation, + token, + fresh, + result, + } => { + { + let mut state = self.reconnect.lock().unwrap(); + if generation != state.generation { + return Ok(()); + } + state.used_fresh_token = fresh; + if fresh && !matches!(&result, Err(ReconnectFailure::Provider(_))) { + state.token = token; + state.force_refresh = false; + } + } + match result { + Ok((send, recv)) => { + *self.send_chan.lock().unwrap() = Some(send); + *get_lock_sync(&self.recv) = recv; + } + Err(ReconnectFailure::Provider(error)) => return self.connection_lost(Some(error), false, false), + Err(ReconnectFailure::Transport(error)) => { + let terminal = error.is_terminal(); + let auth = error.is_auth(); + return self.connection_lost( + Some(InternalError::new(error.to_string()).with_cause(error).into()), + terminal, + auth, + ); + } + } + } + // Subscribe: register the subscription in the [`SubscriptionManager`] // and send the `Subscribe` WS message. PendingMutation::Subscribe { query_set_id, handle } => { let mut inner = self.inner.lock().unwrap(); // Register the subscription, so we can handle related messages from the server. inner.subscriptions.register_subscription(query_set_id, handle.clone()); + if self.reconnect.lock().unwrap().preparing_replay { + return Ok(()); + } if let Some(msg) = handle.start() { self.send_chan .lock() @@ -386,13 +674,19 @@ impl DbContextImpl { .as_mut() .ok_or(crate::Error::Disconnected)? .unbounded_send(ws::v2::ClientMessage::Subscribe(msg)) - .expect("Unable to send subscribe message: WS sender loop has dropped its recv channel"); + .map_err(|_| crate::Error::Disconnected)?; } // else, the handle was already cancelled. } PendingMutation::Unsubscribe { query_set_id } => { let mut inner = self.inner.lock().unwrap(); + if self.reconnect.lock().unwrap().preparing_replay { + inner + .subscriptions + .unsubscribe_applied(&self.make_event_ctx(()), query_set_id); + return Ok(()); + } match inner.subscriptions.handle_pending_unsubscribe(query_set_id) { PendingUnsubscribeResult::DoNothing => // The subscription was already unsubscribed, so we don't need to send an unsubscribe message. @@ -410,13 +704,25 @@ impl DbContextImpl { .as_mut() .ok_or(crate::Error::Disconnected)? .unbounded_send(ws::v2::ClientMessage::Unsubscribe(m)) - .expect("Unable to send unsubscribe message: WS sender loop has dropped its recv channel"); + .map_err(|_| crate::Error::Disconnected)?; } } } // CallReducer: send the `CallReducer` WS message. PendingMutation::InvokeReducerWithCallback { reducer, callback } => { + if !self.is_active() { + let error = InternalError::new("Disconnected").with_cause(crate::Error::Disconnected); + callback( + &self.make_event_ctx(ReducerEvent { + reducer, + timestamp: Timestamp::UNIX_EPOCH, + status: Status::Panic(error.clone()), + }), + Err(error), + ); + return Ok(()); + } let request_id = next_request_id(); let reducer_name = reducer.reducer_name(); @@ -443,7 +749,7 @@ impl DbContextImpl { .as_mut() .ok_or(crate::Error::Disconnected)? .unbounded_send(msg) - .expect("Unable to send reducer call message: WS sender loop has dropped its recv channel"); + .map_err(|_| crate::Error::Disconnected)?; } // Invoke a procedure: stash its callback, then send the `CallProcedure` WS message. @@ -452,6 +758,13 @@ impl DbContextImpl { args, callback, } => { + if !self.is_active() { + callback( + &self.make_event_ctx(()), + Err(InternalError::new("Disconnected").with_cause(crate::Error::Disconnected)), + ); + return Ok(()); + } // We need to include a request_id in the message so that we can find the callback once it completes. let request_id = next_request_id(); self.inner @@ -472,25 +785,31 @@ impl DbContextImpl { .as_mut() .ok_or(crate::Error::Disconnected)? .unbounded_send(msg) - .expect("Unable to send procedure call message: WS sender loop has dropped its recv channel"); + .map_err(|_| crate::Error::Disconnected)?; } // Disconnect: close the connection. PendingMutation::Disconnect => { { - let mut inner = self.inner.lock().unwrap(); - if inner.connection_lifecycle == ConnectionLifecycle::Connecting { - // If the user cancels before the initial connection finishes, - // don't report that as a connection error. - inner.connection_lifecycle = ConnectionLifecycle::Ended; + let mut state = self.reconnect.lock().unwrap(); + state.generation += 1; + state.reconnecting = false; + state.preparing_replay = false; + if let Some(abort) = state.abort.take() { + abort.abort(); } } - // Set `send_chan` to `None`, since `Self::is_active` checks that. - // This will close the WebSocket loop in websocket.rs, - // sending a close frame to the server, - // eventually resulting in disconnect callbacks being called - // if the initial connection had completed. *self.send_chan.lock().unwrap() = None; + let mut inner = self.inner.lock().unwrap(); + inner.connection_lifecycle = ConnectionLifecycle::Ended; + inner.replay = None; + self.fail_pending_operations(&mut inner); + let ctx = self.make_event_ctx(None); + inner.subscriptions.on_disconnect(&self.make_event_ctx(())); + if let Some(callback) = inner.on_disconnect.as_mut() { + callback(&ctx, None, None); + } + return Err(crate::Error::Disconnected); } // Callback stuff: these all do what you expect. @@ -578,8 +897,9 @@ impl DbContextImpl { // returns `Err(_)`. Similar behavior as `Iterator::next` and // `Stream::poll_next`. No comment on whether this is a good mental // model or not. - let res = match get_lock_sync(&self.recv).try_next() { - Ok(None) => Err(self.end_connection(None)), + let incoming = get_lock_sync(&self.recv).try_next(); + let res = match incoming { + Ok(None) => self.connection_lost(None, false, false).map(|_| true), Err(_) => Ok(false), Ok(Some(msg)) => self.process_message(msg).map(|_| true), }; @@ -634,7 +954,7 @@ impl DbContextImpl { pub fn advance_one_message_blocking(&self) -> crate::Result<()> { match self.runtime.block_on(self.get_message()) { Message::Local(pending) => self.apply_mutation(pending), - Message::Ws(None) => Err(self.end_connection(None)), + Message::Ws(None) => self.connection_lost(None, false, false), Message::Ws(Some(msg)) => self.process_message(msg), } } @@ -645,7 +965,7 @@ impl DbContextImpl { pub async fn advance_one_message_async(&self) -> crate::Result<()> { match self.get_message().await { Message::Local(pending) => self.apply_mutation(pending), - Message::Ws(None) => Err(self.end_connection(None)), + Message::Ws(None) => self.connection_lost(None, false, false), Message::Ws(Some(msg)) => self.process_message(msg), } } @@ -706,12 +1026,23 @@ impl DbContextImpl { /// Called by the autogenerated `DbConnection` method of the same name. pub fn is_active(&self) -> bool { - self.send_chan.lock().unwrap().is_some() + !self.is_reconnecting() + && self + .send_chan + .lock() + .unwrap() + .as_ref() + .is_some_and(|sender| !sender.is_closed()) + } + + /// True while waiting for or establishing an automatic reconnect. + pub fn is_reconnecting(&self) -> bool { + self.reconnect.lock().unwrap().reconnecting } /// Called by the autogenerated `DbConnection` method of the same name. pub fn disconnect(&self) -> crate::Result<()> { - if !self.is_active() { + if !self.is_active() && !self.is_reconnecting() { return Err(crate::Error::Disconnected); } self.pending_mutations_send @@ -754,6 +1085,9 @@ impl DbContextImpl { where ::Reducer: From, { + if !self.is_active() { + return Err(crate::Error::Disconnected); + } self.queue_mutation(PendingMutation::InvokeReducerWithCallback { reducer: reducer.into(), callback: Box::new(callback), @@ -788,6 +1122,13 @@ impl DbContextImpl { + Send + 'static, ) { + if !self.is_active() { + callback( + &self.make_event_ctx(()), + Err(InternalError::new("Disconnected").with_cause(crate::Error::Disconnected)), + ); + return; + } self.queue_mutation(PendingMutation::InvokeProcedureWithCallback { procedure: procedure_name, args: bsatn::to_vec(&args).expect("Failed to BSATN serialize procedure args"), @@ -806,10 +1147,14 @@ impl DbContextImpl { type OnConnectCallback = Box::DbConnection, Identity, &str) + Send + 'static>; -type OnConnectErrorCallback = Box::ErrorContext, crate::Error) + Send + 'static>; +type OnAutomaticReconnectCallback = + Box::DbConnection, Identity, &str) + Send + 'static>; + +type OnConnectErrorCallback = + Box::ErrorContext, crate::Error, Option) + Send + 'static>; type OnDisconnectCallback = - Box::ErrorContext, Option) + Send + 'static>; + Box::ErrorContext, Option, Option) + Send + 'static>; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ConnectionLifecycle { @@ -835,10 +1180,12 @@ pub(crate) struct DbContextImplInner { connection_lifecycle: ConnectionLifecycle, on_connect: Option>, + on_automatic_reconnect: Option>, on_connect_error: Option>, on_disconnect: Option>, procedure_callbacks: ProcedureCallbacks, + replay: Option<(u32, Vec)>, } /// A builder-pattern constructor for a `DbConnection` connection to the module `M`. @@ -855,12 +1202,15 @@ pub struct DbConnectionBuilder { token: Option, on_connect: Option>, + on_automatic_reconnect: Option>, on_connect_error: Option>, on_disconnect: Option>, additional_logging_path: Option, params: WsParams, + reconnect_options: Option, + token_provider: Option>, } /// This process's global connection ID, which will be attacked to all connections it makes. @@ -914,10 +1264,62 @@ impl DbConnectionBuilder { database_name: None, token: None, on_connect: None, + on_automatic_reconnect: None, on_connect_error: None, on_disconnect: None, additional_logging_path: None, params: <_>::default(), + reconnect_options: None, + token_provider: None, + } + } + + /// Enable unlimited automatic reconnect with the default backoff bounds. + pub fn with_automatic_reconnect(self) -> Self { + self.with_automatic_reconnect_options(AutomaticReconnectOptions::default()) + } + + /// Enable automatic reconnect using custom backoff bounds. + pub fn with_automatic_reconnect_options(mut self, options: AutomaticReconnectOptions) -> Self { + self.reconnect_options = Some(options.resolve()); + self + } + + pub fn with_token_provider(mut self, provider: impl TokenProvider) -> Self { + self.token_provider = Some(Arc::new(provider)); + self + } + + /// Called for each successful automatic reconnect, before subscription replay. + /// `on_connect` is reserved for the initial connection. + pub fn on_automatic_reconnect( + mut self, + callback: impl FnMut(&M::DbConnection, Identity, &str) + Send + 'static, + ) -> Self { + self.on_automatic_reconnect = Some(Box::new(callback)); + self + } + + fn reconnect_state(&mut self) -> ReconnectState { + if self.reconnect_options.is_some() { + self.params.session_id = Some(ConnectionId::from_u128(rand::random())); + } + ReconnectState { + config: ConnectionConfig { + uri: self.uri.clone().expect("with_uri is required"), + database_name: self.database_name.clone().expect("with_database_name is required"), + params: self.params, + }, + options: self.reconnect_options, + provider: self.token_provider.clone(), + token: self.token.clone(), + attempt: 0, + generation: 0, + reconnecting: false, + preparing_replay: false, + used_fresh_token: false, + force_refresh: false, + abort: None, } } @@ -962,7 +1364,8 @@ but you must call one of them, or else the connection will never progress. /// Open a WebSocket connection, build an empty client cache, &c, /// to construct a [`DbContextImpl`]. #[cfg(not(feature = "browser"))] - fn build_impl(self) -> crate::Result> { + fn build_impl(mut self) -> crate::Result> { + let reconnect = self.reconnect_state(); let extra_logging = self .additional_logging_path .map(|path| { @@ -978,7 +1381,7 @@ but you must call one of them, or else the connection will never progress. let connection_id_override = get_connection_id_override(); let ws_connection = tokio::task::block_in_place(|| { handle.block_on(WsConnection::connect( - self.uri.unwrap(), + self.uri.clone().unwrap(), self.database_name.as_ref().unwrap(), self.token.as_deref(), connection_id_override, @@ -998,7 +1401,13 @@ but you must call one of them, or else the connection will never progress. let (pending_mutations_send, pending_mutations_recv) = mpsc::unbounded(); let pending_mutations_recv = Arc::new(TokioMutex::new(pending_mutations_recv)); - let inner_ctx = build_db_ctx_inner(runtime, self.on_connect, self.on_connect_error, self.on_disconnect); + let inner_ctx = build_db_ctx_inner( + runtime, + self.on_connect, + self.on_automatic_reconnect, + self.on_connect_error, + self.on_disconnect, + ); Ok(build_db_ctx( handle, inner_ctx, @@ -1008,13 +1417,15 @@ but you must call one of them, or else the connection will never progress. pending_mutations_recv, connection_id_override, extra_logging, + reconnect, )) } /// Open a WebSocket connection, build an empty client cache, &c, /// to construct a [`DbContextImpl`]. #[cfg(feature = "browser")] - async fn build_impl(self) -> crate::Result> { + async fn build_impl(mut self) -> crate::Result> { + let reconnect = self.reconnect_state(); // The wasm/browser SDK target runs under `wasm32-unknown-unknown`, where we do not // have the native file APIs that back `with_debug_to_file`. Keeping the // shared `extra_logging` field as `None` lets the rest of the connection and @@ -1042,7 +1453,12 @@ but you must call one of them, or else the connection will never progress. let (pending_mutations_send, pending_mutations_recv) = mpsc::unbounded(); let pending_mutations_recv = Arc::new(StdMutex::new(pending_mutations_recv)); - let inner_ctx = build_db_ctx_inner(self.on_connect, self.on_connect_error, self.on_disconnect); + let inner_ctx = build_db_ctx_inner( + self.on_connect, + self.on_automatic_reconnect, + self.on_connect_error, + self.on_disconnect, + ); Ok(build_db_ctx( inner_ctx, raw_msg_send, @@ -1051,6 +1467,7 @@ but you must call one of them, or else the connection will never progress. pending_mutations_recv, connection_id_override, extra_logging, + reconnect, )) } @@ -1159,7 +1576,10 @@ Instead of registering multiple `on_connect` callbacks, register a single callba /// This callback is invoked only before the initial connection message is /// received from the host. Errors which prevent [`Self::build`] from creating /// a connection are returned by [`Self::build`] instead. - pub fn on_connect_error(mut self, callback: impl FnOnce(&M::ErrorContext, crate::Error) + Send + 'static) -> Self { + pub fn on_connect_error( + mut self, + callback: impl FnMut(&M::ErrorContext, crate::Error, Option) + Send + 'static, + ) -> Self { if self.on_connect_error.is_some() { panic!( "DbConnectionBuilder can only register a single `on_connect_error` callback. @@ -1179,7 +1599,7 @@ Instead of registering multiple `on_connect_error` callbacks, register a single /// [`Self::on_connect_error`] instead. pub fn on_disconnect( mut self, - callback: impl FnOnce(&M::ErrorContext, Option) + Send + 'static, + callback: impl FnMut(&M::ErrorContext, Option, Option) + Send + 'static, ) -> Self { if self.on_disconnect.is_some() { panic!( @@ -1198,6 +1618,7 @@ fn build_db_ctx_inner( #[cfg(not(feature = "browser"))] runtime: Option, on_connect_cb: Option>, + on_automatic_reconnect: Option>, on_connect_error_cb: Option>, on_disconnect_cb: Option>, ) -> Arc>> { @@ -1211,10 +1632,12 @@ fn build_db_ctx_inner( connection_lifecycle: ConnectionLifecycle::Connecting, on_connect: on_connect_cb, + on_automatic_reconnect, on_connect_error: on_connect_error_cb, on_disconnect: on_disconnect_cb, procedure_callbacks: ProcedureCallbacks::default(), + replay: None, })) } @@ -1230,6 +1653,7 @@ fn build_db_ctx( pending_mutations_recv: SharedAsyncCell>>, connection_id: Option, extra_logging: Option>, + reconnect: ReconnectState, ) -> DbContextImpl { let mut cache = ClientCache::new(extra_logging.clone()); M::register_tables(&mut cache); @@ -1247,6 +1671,7 @@ fn build_db_ctx( identity: Arc::new(StdMutex::new(None)), connection_id: Arc::new(StdMutex::new(connection_id)), extra_logging, + reconnect: Arc::new(StdMutex::new(reconnect)), } } @@ -1276,13 +1701,15 @@ fn enter_or_create_runtime() -> crate::Result<(Option, runtime::Handle) } } -/// Synchronous lock helper: native = blocking_lock, browser = lock().unwrap() +/// Synchronous receiver access must not overlap another connection advancement. #[cfg(not(feature = "browser"))] fn get_lock_sync(mutex: &TokioMutex) -> tokio::sync::MutexGuard<'_, T> { - mutex.blocking_lock() + mutex + .try_lock() + .expect("Concurrent connection advancement is unsupported") } -/// Synchronous lock helper: native = blocking_lock, browser = lock().unwrap() +/// Browser receiver access uses a synchronous mutex. #[cfg(feature = "browser")] fn get_lock_sync(mutex: &StdMutex) -> std::sync::MutexGuard<'_, T> { mutex.lock().unwrap() @@ -1302,7 +1729,7 @@ pub async fn get_lock_async(mutex: &StdMutex) -> std::sync::MutexGuard<'_, } #[derive(Debug)] -enum ParsedMessage { +pub(crate) enum ParsedMessage { TransactionUpdate(M::DbUpdate), IdentityToken(Identity, Box, ConnectionId), SubscribeApplied { @@ -1318,6 +1745,8 @@ enum ParsedMessage { error: String, }, Error(crate::Error), + TransportError(crate::websocket::WsError), + SubscribeBatchApplied(ws::v2::SubscribeBatchApplied), ReducerResult { request_id: u32, timestamp: Timestamp, @@ -1331,7 +1760,7 @@ enum ParsedMessage { #[cfg(not(feature = "browser"))] fn spawn_parse_loop( - raw_message_recv: mpsc::UnboundedReceiver, + raw_message_recv: mpsc::UnboundedReceiver>, handle: &runtime::Handle, extra_logging: Option>, ) -> (tokio::task::JoinHandle<()>, mpsc::UnboundedReceiver>) { @@ -1342,7 +1771,7 @@ fn spawn_parse_loop( #[cfg(feature = "browser")] fn spawn_parse_loop( - raw_message_recv: mpsc::UnboundedReceiver, + raw_message_recv: mpsc::UnboundedReceiver>, extra_logging: Option>, ) -> mpsc::UnboundedReceiver> { let (parsed_message_send, parsed_message_recv) = mpsc::unbounded(); @@ -1353,7 +1782,7 @@ fn spawn_parse_loop( /// A loop which reads raw WS messages from `recv`, parses them into domain types, /// and pushes the [`ParsedMessage`]s into `send`. async fn parse_loop( - mut recv: mpsc::UnboundedReceiver, + mut recv: mpsc::UnboundedReceiver>, send: mpsc::UnboundedSender>, extra_logging: Option>, ) { @@ -1361,6 +1790,13 @@ async fn parse_loop( debug_log(&extra_logging, |file| { writeln!(file, "`parse_loop`: Got raw message: {msg:?}") }); + let msg = match msg { + Ok(msg) => msg, + Err(error) => { + let _ = send.unbounded_send(ParsedMessage::TransportError(error)); + break; + } + }; let parsed = match msg { ws::v2::ServerMessage::TransactionUpdate(transaction_update) => { match M::DbUpdate::parse_update(transaction_update) { @@ -1387,10 +1823,12 @@ async fn parse_loop( ret_value, transaction_update, }) => { - assert!( - ret_value.is_empty(), - "Reducer return value should be unit, i.e. 0 bytes, but got {ret_value:?}" - ); + if !ret_value.is_empty() { + let _ = send.unbounded_send(ParsedMessage::Error( + InternalError::new("Non-unit reducer return value").into(), + )); + break; + } match M::DbUpdate::parse_update(transaction_update) { Ok(db_update) => ParsedMessage::ReducerResult { request_id, @@ -1437,7 +1875,7 @@ async fn parse_loop( connection_id, }) => ParsedMessage::IdentityToken(identity, token, connection_id), ws::v2::ServerMessage::OneOffQueryResult(_) => { - unreachable!("The Rust SDK does not implement one-off queries") + ParsedMessage::Error(InternalError::new("Unexpected one-off query response").into()) } ws::v2::ServerMessage::SubscribeApplied(subscribe_applied) => { let db_update = subscribe_applied.rows; @@ -1460,7 +1898,10 @@ async fn parse_loop( .. }) => { let Some(db_update) = db_update else { - unreachable!("The Rust SDK always requests rows to delete when unsubscribing") + let _ = send.unbounded_send(ParsedMessage::Error( + InternalError::new("Unsubscribe response omitted dropped rows").into(), + )); + break; }; match M::DbUpdate::parse_unsubscribe_rows(db_update) { Err(e) => ParsedMessage::Error( @@ -1478,11 +1919,7 @@ async fn parse_loop( query_set_id: e.query_set_id, error: e.error.to_string(), }, - // This SDK negotiates v2 and never sends `SubscribeBatch`, - // so the server should never send this response. - ws::v2::ServerMessage::SubscribeBatchApplied(_) => ParsedMessage::Error( - InternalError::new("Received SubscribeBatchApplied, which this client never requests").into(), - ), + ws::v2::ServerMessage::SubscribeBatchApplied(batch) => ParsedMessage::SubscribeBatchApplied(batch), ws::v2::ServerMessage::ProcedureResult(procedure_result) => ParsedMessage::ProcedureResult { request_id: procedure_result.request_id, result: match procedure_result.status { @@ -1494,13 +1931,29 @@ async fn parse_loop( debug_log(&extra_logging, |file| { writeln!(file, "`parse_loop`: Parsed as: {parsed:?}") }); - send.unbounded_send(parsed) - .expect("Failed to send ParsedMessage to main thread"); + if send.unbounded_send(parsed).is_err() { + break; + } } } /// Operations a user can make to a `DbContext` which must be postponed pub(crate) enum PendingMutation { + ReconnectReady { + // Keep the parked receive stream open until this mutation swaps it. + // Otherwise the async runner can observe EOF before ReconnectReady. + _park_send: mpsc::UnboundedSender>, + generation: u64, + token: Option, + fresh: bool, + result: Result< + ( + mpsc::UnboundedSender, + mpsc::UnboundedReceiver>, + ), + ReconnectFailure, + >, + }, Unsubscribe { query_set_id: QuerySetId, }, @@ -1551,6 +2004,9 @@ pub(crate) enum PendingMutation { impl std::fmt::Debug for PendingMutation { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { + PendingMutation::ReconnectReady { generation, .. } => { + f.debug_tuple("ReconnectReady").field(generation).finish() + } PendingMutation::Unsubscribe { query_set_id } => f .debug_struct("PendingMutation::Unsubscribe") .field("query_set_id", query_set_id) @@ -1627,3 +2083,34 @@ pub(crate) fn next_query_set_id() -> QuerySetId { id: NEXT_QUERY_SET_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), } } + +#[derive(Clone)] +struct ConnectionConfig { + uri: Uri, + database_name: String, + params: WsParams, +} +struct ReconnectState { + config: ConnectionConfig, + options: Option, + provider: Option>, + token: Option, + attempt: u32, + generation: u64, + reconnecting: bool, + preparing_replay: bool, + used_fresh_token: bool, + force_refresh: bool, + abort: Option, +} +impl Drop for ReconnectState { + fn drop(&mut self) { + if let Some(abort) = self.abort.take() { + abort.abort(); + } + } +} +pub(crate) enum ReconnectFailure { + Provider(crate::Error), + Transport(crate::websocket::WsError), +} diff --git a/sdks/rust/src/db_context.rs b/sdks/rust/src/db_context.rs index e9a4114a46c..cec09c245ee 100644 --- a/sdks/rust/src/db_context.rs +++ b/sdks/rust/src/db_context.rs @@ -40,6 +40,9 @@ pub trait DbContext { /// Returns `true` if the connection is active, i.e. has not yet disconnected. fn is_active(&self) -> bool; + /// True while the SDK is waiting for or establishing an automatic reconnect. + fn is_reconnecting(&self) -> bool; + /// Close the connection. /// /// Returns an error if we are already disconnected. diff --git a/sdks/rust/src/error.rs b/sdks/rust/src/error.rs index 6b33a41807f..bba9ab8eeb1 100644 --- a/sdks/rust/src/error.rs +++ b/sdks/rust/src/error.rs @@ -8,6 +8,9 @@ pub enum Error { #[error("Connection is already disconnected or has terminated normally")] Disconnected, + #[error("Connection lost before the result was received; the operation may have executed")] + UnknownResult, + #[error("Failed to connect: {source}")] FailedToConnect { #[source] @@ -17,6 +20,9 @@ pub enum Error { #[error("Host returned error when processing subscription query: {error}")] SubscriptionError { error: String }, + #[error("Token provider failed: {message}")] + TokenProvider { message: String }, + #[error("Subscription has already ended")] AlreadyEnded, @@ -34,6 +40,14 @@ pub struct InternalError { } impl InternalError { + /// Whether the operation may have executed before the transport was lost. + pub fn is_unknown_result(&self) -> bool { + self.cause + .as_ref() + .and_then(|e| e.downcast_ref::()) + .is_some_and(|e| matches!(e, Error::UnknownResult)) + } + pub(crate) fn new(message: impl Into) -> Self { Self { message: message.into(), @@ -97,7 +111,9 @@ impl std::error::Error for InternalError { &self.message } fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - None + self.cause + .as_deref() + .map(|cause| cause as &(dyn std::error::Error + 'static)) } } diff --git a/sdks/rust/src/lib.rs b/sdks/rust/src/lib.rs index 09429973105..5f8456c1e55 100644 --- a/sdks/rust/src/lib.rs +++ b/sdks/rust/src/lib.rs @@ -15,6 +15,7 @@ mod client_cache; mod compression; mod db_connection; mod metrics; +mod reconnect; mod spacetime_module; mod subscription; mod websocket; @@ -29,6 +30,7 @@ pub use db_connection::DbConnectionBuilder; pub use db_context::DbContext; pub use error::{Error, Result}; pub use event::{Event, ReducerEvent, Status}; +pub use reconnect::{AutomaticReconnectOptions, NextReconnect, TokenFuture, TokenProvider}; pub use table::{EventTable, Table, TableAccessor, TableWithPrimaryKey}; pub use spacetime_module::SubscriptionHandle; diff --git a/sdks/rust/src/reconnect.rs b/sdks/rust/src/reconnect.rs new file mode 100644 index 00000000000..7966fc1522c --- /dev/null +++ b/sdks/rust/src/reconnect.rs @@ -0,0 +1,149 @@ +//! Automatic reconnect policy and credential refresh. +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use std::{ + future::Future, + pin::Pin, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +/// The upcoming automatic reconnect attempt (numbered starting at one). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct NextReconnect { + pub attempt: u32, + pub delay: Duration, +} + +/// Bounds for exponential backoff with 50% jitter. Attempts are unlimited. +#[derive(Clone, Copy, Debug)] +pub struct AutomaticReconnectOptions { + /// Defaults to one second; clamped to at least 500 milliseconds. + pub min_delay: Duration, + /// Defaults to 30 seconds; clamped to at least one second and `min_delay`. + pub max_delay: Duration, +} +impl Default for AutomaticReconnectOptions { + fn default() -> Self { + Self { + min_delay: Duration::from_secs(1), + max_delay: Duration::from_secs(30), + } + } +} +impl AutomaticReconnectOptions { + pub(crate) fn resolve(mut self) -> Self { + let min = self.min_delay.max(Duration::from_millis(500)); + let max = self.max_delay.max(Duration::from_secs(1)).max(min); + if min != self.min_delay || max != self.max_delay { + log::warn!("Reconnect delays raised to safety bounds: {min:?}..={max:?}"); + } + self.min_delay = min; + self.max_delay = max; + self + } + pub(crate) fn delay(self, attempt: u32, random: f64) -> Duration { + let base = (self.min_delay.as_secs_f64() * 2f64.powi(attempt.saturating_sub(1).min(30) as i32)) + .min(self.max_delay.as_secs_f64()); + Duration::try_from_secs_f64( + (base * (0.5 + random)).clamp(self.min_delay.as_secs_f64(), self.max_delay.as_secs_f64()), + ) + .unwrap_or(self.max_delay) + } +} + +/// Supplies a fresh token for the same identity. Invoked only during reconnect, +/// when the retained JWT approaches expiry, cannot be inspected, or is rejected. +/// Provider errors are retried using the normal backoff policy. +pub trait TokenProvider: Send + Sync + 'static { + fn token(&self) -> TokenFuture; +} + +/// An asynchronous credential result. Browser futures need not be `Send`. +#[cfg(not(feature = "browser"))] +pub type TokenFuture = Pin> + Send + 'static>>; +#[cfg(feature = "browser")] +pub type TokenFuture = Pin> + 'static>>; + +#[cfg(not(feature = "browser"))] +impl TokenProvider for F +where + F: Fn() -> Fut + Send + Sync + 'static, + Fut: Future> + Send + 'static, +{ + fn token(&self) -> TokenFuture { + Box::pin(self()) + } +} +#[cfg(feature = "browser")] +impl TokenProvider for F +where + F: Fn() -> Fut + Send + Sync + 'static, + Fut: Future> + 'static, +{ + fn token(&self) -> TokenFuture { + Box::pin(self()) + } +} + +pub(crate) fn now() -> SystemTime { + #[cfg(not(feature = "browser"))] + { + SystemTime::now() + } + #[cfg(feature = "browser")] + { + UNIX_EPOCH + Duration::from_secs_f64(js_sys::Date::now() / 1000.0) + } +} + +pub(crate) fn token_needs_refresh(token: Option<&str>, now: SystemTime) -> bool { + let claims = (|| { + let payload = token?.split('.').nth(1)?; + let bytes = URL_SAFE_NO_PAD.decode(payload.trim_end_matches('=')).ok()?; + serde_json::from_slice::(&bytes).ok() + })(); + let Some(claims) = claims else { + return true; + }; + let Some(exp) = claims["exp"].as_f64().filter(|n| n.is_finite()) else { + return true; + }; + let margin = claims["iat"] + .as_f64() + .map(|iat| (exp - iat) * 0.05) + .unwrap_or(0.0) + .max(30.0); + exp - now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs_f64() <= margin +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn backoff_bounds_and_saturation() { + let policy = AutomaticReconnectOptions::default(); + for attempt in [1, 2, 3, 6, u32::MAX] { + for random in [0.0, 0.5, 1.0] { + let delay = policy.delay(attempt, random); + assert!(delay >= policy.min_delay && delay <= policy.max_delay); + } + } + assert_eq!(policy.delay(3, 0.5), Duration::from_secs(4)); + let policy = AutomaticReconnectOptions { + min_delay: Duration::ZERO, + max_delay: Duration::ZERO, + } + .resolve(); + assert_eq!(policy.min_delay, Duration::from_millis(500)); + assert_eq!(policy.max_delay, Duration::from_secs(1)); + } + #[test] + fn refresh_margin_and_unreadable_tokens() { + let now = UNIX_EPOCH + Duration::from_secs(1000); + let token = |claims: &str| format!("header.{}.signature", URL_SAFE_NO_PAD.encode(claims)); + assert!(token_needs_refresh(None, now)); + assert!(token_needs_refresh(Some("invalid"), now)); + assert!(token_needs_refresh(Some(&token(r#"{"exp":1030,"iat":900}"#)), now)); + assert!(!token_needs_refresh(Some(&token(r#"{"exp":1031,"iat":900}"#)), now)); + assert!(token_needs_refresh(Some(&token(r#"{"exp":1100,"iat":-1000}"#)), now)); + } +} diff --git a/sdks/rust/src/subscription.rs b/sdks/rust/src/subscription.rs index 2204985ea15..64f428ae259 100644 --- a/sdks/rust/src/subscription.rs +++ b/sdks/rust/src/subscription.rs @@ -30,7 +30,7 @@ impl Default for SubscriptionManager { } pub(crate) type OnAppliedCallback = - Box::SubscriptionEventContext) + Send + 'static>; + Box::SubscriptionEventContext) + Send + 'static>; pub(crate) type OnErrorCallback = Box::ErrorContext, crate::Error) + Send + 'static>; pub type OnEndedCallback = Box::SubscriptionEventContext) + Send + 'static>; @@ -46,24 +46,54 @@ pub(crate) enum PendingUnsubscribeResult { } impl SubscriptionManager { - pub(crate) fn on_disconnect(&mut self, _ctx: &M::ErrorContext) { - // We need to clear all the subscriptions. - // TODO: is this correct? We don't remove them from the client cache, - // we may want to resume them in the future if we impl reconnecting, - // and users can already register on-disconnect callbacks which will run in this case. - - // NOTE(cloutiertyler) - // This function previously invoke `on_error` for all subscriptions. - // However, this is inconsistent behavior given that `on_disconnect` for - // connections no longer always has an error argument and that the user - // can add an `on_ended` callback when unsubscribing. - // - // We propose instead that `on_ended` be added to the subscription - // builder so that it can be invoked when the subscription is ended - // because of a normal disconnect, but without the user calling - // `unsubscribe_then`. This can be done in a non-breaking way. - // - // For now, we will just do nothing when a subscription ends normally. + pub(crate) fn suspend(&mut self, ctx: &M::SubscriptionEventContext) { + let ids: Vec<_> = self.subscriptions.keys().copied().collect(); + for id in ids { + let sub = self.subscriptions[&id].clone(); + let cancelled = sub.inner.lock().unwrap().unsubscribe_called; + if cancelled { + self.unsubscribe_applied(ctx, id); + } else { + sub.inner.lock().unwrap().status = SubscriptionServerState::Pending; + } + } + } + + pub(crate) fn replay(&mut self, ctx: &M::SubscriptionEventContext) -> Box<[ws::v2::SubscribeSet]> { + self.suspend(ctx); + let old = std::mem::take(&mut self.subscriptions); + old.into_values() + .filter_map(|handle| { + let id = next_query_set_id(); + // Rebinding and starting must be atomic with unsubscribe_then: + // a concurrent cancellation must either end locally or target the new ID. + let mut state = handle.inner.lock().unwrap(); + if state.unsubscribe_called { + let on_ended = state.on_ended(); + drop(state); + if let Some(callback) = on_ended { + callback(ctx); + } + return None; + } + state.query_set_id = id; + let msg = state.start().expect("uncancelled replay subscription"); + drop(state); + self.subscriptions.insert(id, handle); + Some(ws::v2::SubscribeSet { + query_set_id: id, + query_strings: msg.query_strings, + }) + }) + .collect() + } + + pub(crate) fn on_disconnect(&mut self, ctx: &M::SubscriptionEventContext) { + for (_, mut sub) in std::mem::take(&mut self.subscriptions) { + if let Some(callback) = sub.on_ended() { + callback(ctx); + } + } } /// Register a new subscription. This does not send the subscription to the server. @@ -80,8 +110,10 @@ impl SubscriptionManager { // TODO: log or double check error handling. return; }; - if let Some(callback) = sub.on_applied() { - callback(ctx) + let callback = sub.on_applied(); + if let Some(mut callback) = callback { + callback(ctx); + sub.inner.lock().unwrap().on_applied = Some(callback); } } @@ -162,7 +194,7 @@ impl SubscriptionBuilder { } /// Register a callback to run when the subscription is applied. - pub fn on_applied(mut self, callback: impl FnOnce(&M::SubscriptionEventContext) + Send + 'static) -> Self { + pub fn on_applied(mut self, callback: impl FnMut(&M::SubscriptionEventContext) + Send + 'static) -> Self { self.on_applied = Some(Box::new(callback)); self } @@ -431,7 +463,7 @@ impl SubscriptionState { self.on_applied.take() } - pub fn on_ended(&mut self) -> Option> { + pub fn on_ended(&mut self) -> Option> { // TODO: Consider logging a warning if the state is wrong (like being in the Error state). if self.is_ended() { return None; diff --git a/sdks/rust/src/websocket.rs b/sdks/rust/src/websocket.rs index 235ef06138f..82421c504c0 100644 --- a/sdks/rust/src/websocket.rs +++ b/sdks/rust/src/websocket.rs @@ -79,6 +79,13 @@ pub enum WsError { source: Arc, }, + #[cfg(not(feature = "browser"))] + #[error("Token cannot be represented as an HTTP authorization header")] + InvalidToken, + + #[error("WebSocket closed with code {code}: {reason}")] + Closed { code: u16, reason: String }, + #[error("Received empty raw message, but valid messages always start with a one-byte compression flag")] EmptyMessage, @@ -104,6 +111,46 @@ pub enum WsError { TokenVerification(String), } +impl WsError { + pub(crate) fn is_auth(&self) -> bool { + #[cfg(not(feature = "browser"))] + if matches!(self, Self::InvalidToken) { + return true; + } + #[cfg(not(feature = "browser"))] + if let Self::Tungstenite { source, .. } = self + && let TokioTungsteniteError::Http(response) = &**source + { + return matches!(response.status().as_u16(), 400 | 401 | 403); + } + #[cfg(feature = "browser")] + if let Self::TokenVerification(message) = self { + return ["HTTP error: 400", "HTTP error: 401", "HTTP error: 403"] + .iter() + .any(|prefix| message.starts_with(prefix)); + } + false + } + pub(crate) fn is_terminal(&self) -> bool { + match self { + #[cfg(not(feature = "browser"))] + Self::InvalidToken => false, + Self::Closed { code, .. } => matches!(code, 1002 | 1003 | 1007 | 1008), + Self::UriError(_) + | Self::EmptyMessage + | Self::DeserializeMessage { .. } + | Self::Decompress { .. } + | Self::UnknownCompressionScheme { .. } => true, + #[cfg(not(feature = "browser"))] + Self::Tungstenite { source, .. } => { + matches!(&**source, TokioTungsteniteError::Protocol(error) if !matches!(error, tokio_tungstenite::tungstenite::error::ProtocolError::ResetWithoutClosingHandshake)) + } + #[cfg(feature = "browser")] + _ => false, + } + } +} + pub(crate) struct WsConnection { db_name: Box, #[cfg(not(feature = "browser"))] @@ -135,6 +182,7 @@ pub(crate) struct WsParams { /// `Some(false)` to disable them. /// `None` to not set the parameter and let the server choose. pub confirmed: Option, + pub session_id: Option, } #[cfg(not(feature = "browser"))] @@ -197,6 +245,11 @@ fn make_uri_impl( path.push_str(&cid.to_hex()); } + if let Some(session_id) = params.session_id { + path.push_str("&session_id="); + path.push_str(&session_id.to_hex()); + } + // Enable confirmed reads if requested. if let Some(confirmed) = params.confirmed { path.push_str("&confirmed="); @@ -239,7 +292,7 @@ fn make_request( source: Arc::new(source), })?; request_insert_protocol_header(&mut req); - request_insert_auth_header(&mut req, token); + request_insert_auth_header(&mut req, token)?; Ok(req) } @@ -252,11 +305,15 @@ fn request_insert_protocol_header(req: &mut http::Request<()>) { } #[cfg(not(feature = "browser"))] -fn request_insert_auth_header(req: &mut http::Request<()>, token: Option<&str>) { +fn request_insert_auth_header(req: &mut http::Request<()>, token: Option<&str>) -> Result<(), WsError> { if let Some(token) = token { - let auth = ["Bearer ", token].concat().try_into().unwrap(); + let auth = ["Bearer ", token] + .concat() + .try_into() + .map_err(|_| WsError::InvalidToken)?; req.headers_mut().insert(http::header::AUTHORIZATION, auth); } + Ok(()) } #[cfg(feature = "browser")] @@ -393,7 +450,7 @@ impl WsConnection { #[cfg(not(feature = "browser"))] async fn message_loop( mut self, - incoming_messages: mpsc::UnboundedSender, + incoming_messages: mpsc::UnboundedSender>, outgoing_messages: mpsc::UnboundedReceiver, extra_logging: Option>>, ) { @@ -448,27 +505,33 @@ impl WsConnection { }, Err(e) => { - maybe_log_error!( - &extra_logging, - "Error reading message from read WebSocket stream", - Result::<(), _>::Err(e) - ); + let error = WsError::Tungstenite { uri: http::Uri::default(), source: Arc::new(e) }; + let _ = incoming_messages.unbounded_send(Err(error)); + break; + }, + + Ok(Some(WebSocketMessage::Close(frame))) => { + if let Some(frame) = frame { + let _ = incoming_messages.unbounded_send(Err(WsError::Closed { + code: frame.code.into(), reason: frame.reason.to_string(), + })); + } break; }, + Ok(Some(WebSocketMessage::Binary(bytes))) => { idle = false; record_metrics(bytes.len()); match Self::parse_response(&bytes) { - Err(e) => maybe_log_error!( - &extra_logging, - "Error decoding WebSocketMessage::Binary payload", - Result::<(), _>::Err(e) - ), + Err(e) => { + let _ = incoming_messages.unbounded_send(Err(e)); + break; + }, Ok(msg) => maybe_log_error!( &extra_logging, "Error sending decoded message to incoming_messages queue", - incoming_messages.unbounded_send(msg) + incoming_messages.unbounded_send(Ok(msg)) ), } } @@ -529,7 +592,7 @@ impl WsConnection { } None => { maybe_log_error!(&extra_logging, "Error sending close frame", SinkExt::close(&mut self.sock).await); - outgoing_messages = None; + break; } }, } @@ -543,7 +606,7 @@ impl WsConnection { extra_logging: Option>>, ) -> ( JoinHandle<()>, - mpsc::UnboundedReceiver, + mpsc::UnboundedReceiver>, mpsc::UnboundedSender, ) { let (outgoing_send, outgoing_recv) = mpsc::unbounded(); @@ -556,7 +619,7 @@ impl WsConnection { pub(crate) fn spawn_message_loop( self, ) -> ( - mpsc::UnboundedReceiver, + mpsc::UnboundedReceiver>, mpsc::UnboundedSender, ) { let websocket_received = CLIENT_METRICS.websocket_received.with_label_values(&self.db_name); @@ -569,7 +632,7 @@ impl WsConnection { }; let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); + let (incoming_tx, incoming_rx) = mpsc::unbounded::>(); let (mut ws_writer, ws_reader) = self.sock.split(); @@ -590,20 +653,23 @@ impl WsConnection { record_metrics(bytes.len()); // parse + forward into `incoming_tx` match Self::parse_response(&bytes) { - Ok(msg) => if let Err(_e) = incoming_tx.unbounded_send(msg) { + Ok(msg) => if let Err(_e) = incoming_tx.unbounded_send(Ok(msg)) { gloo_console::warn!("Incoming receiver dropped."); break; }, Err(e) => { - gloo_console::warn!( - "Error decoding WebSocketMessage::Binay payload: ", - format!("{:?}", e) - ); + let _ = incoming_tx.unbounded_send(Err(e)); + break; }, } }, Some(Ok(WebSocketMessage::Close(r))) => { + if let Some(ref frame) = r { + let _ = incoming_tx.unbounded_send(Err(WsError::Closed { + code: frame.code.into(), reason: frame.reason.to_string(), + })); + } let reason: String = if let Some(r) = r { format!("{}:{:?}", r, r.code) } else {String::default()}; From 517c4d693e02d3b6502616d3c1a7053c37989e88 Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Fri, 9 Oct 2026 15:49:57 +0200 Subject: [PATCH 02/12] Update tests for auto-reconnect --- .../snapshots/codegen__codegen_rust.snap | 24 ++ .../sdk-test-procedure-cpp/client/src/main.rs | 4 +- .../client/src/module_bindings/mod.rs | 24 ++ .../tests/case-conversion-client/src/main.rs | 26 +- .../src/module_bindings/mod.rs | 24 ++ .../src/module_bindings/mod.rs | 24 ++ .../src/test_handlers.rs | 34 +- .../src/module_bindings/mod.rs | 24 ++ .../event-table-client/src/test_handlers.rs | 12 +- .../src/module_bindings/mod.rs | 24 ++ .../src/test_handlers.rs | 18 +- .../src/module_bindings/mod.rs | 24 ++ .../procedure-client/src/test_handlers.rs | 4 +- .../src/module_bindings/mod.rs | 24 ++ .../src/test_handlers.rs | 4 +- sdks/rust/tests/reconnect.rs | 375 ++++++++++++++++++ .../test-client/src/module_bindings/mod.rs | 24 ++ .../tests/test-client/src/test_handlers.rs | 76 ++-- sdks/rust/tests/test-counter/src/lib.rs | 13 + .../view-client/src/module_bindings/mod.rs | 24 ++ .../tests/view-client/src/test_handlers.rs | 40 +- .../view-pk-client/src/module_bindings/mod.rs | 24 ++ .../tests/view-pk-client/src/test_handlers.rs | 12 +- templates/basic-cpp/src/main.rs | 2 +- .../basic-cpp/src/module_bindings/mod.rs | 24 ++ templates/basic-rs/src/main.rs | 2 +- templates/basic-rs/src/module_bindings/mod.rs | 24 ++ templates/chat-console-rs/src/main.rs | 4 +- .../src/module_bindings/mod.rs | 24 ++ 29 files changed, 850 insertions(+), 112 deletions(-) create mode 100644 sdks/rust/tests/reconnect.rs diff --git a/crates/codegen/tests/snapshots/codegen__codegen_rust.snap b/crates/codegen/tests/snapshots/codegen__codegen_rust.snap index af115ee46b5..6628d4f4095 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_rust.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_rust.snap @@ -1602,6 +1602,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -1821,6 +1825,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -1897,6 +1905,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -1969,6 +1981,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -2041,6 +2057,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -2117,6 +2137,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/modules/sdk-test-procedure-cpp/client/src/main.rs b/modules/sdk-test-procedure-cpp/client/src/main.rs index 752f91d622a..65a3091ffd4 100644 --- a/modules/sdk-test-procedure-cpp/client/src/main.rs +++ b/modules/sdk-test-procedure-cpp/client/src/main.rs @@ -66,13 +66,13 @@ fn on_connected(_ctx: &DbConnection, identity: Identity, token: &str) { } /// Our `on_connect_error` callback: print the error, then exit the process. -fn on_connect_error(_ctx: &ErrorContext, err: Error) { +fn on_connect_error(_ctx: &ErrorContext, err: Error, _next: Option) { eprintln!("Connection error: {err}"); std::process::exit(1); } /// Our `on_disconnect` callback: print a note, then exit the process. -fn on_disconnected(_ctx: &ErrorContext, err: Option) { +fn on_disconnected(_ctx: &ErrorContext, err: Option, _next: Option) { if let Some(err) = err { eprintln!("Disconnected: {err}"); std::process::exit(1); diff --git a/modules/sdk-test-procedure-cpp/client/src/module_bindings/mod.rs b/modules/sdk-test-procedure-cpp/client/src/module_bindings/mod.rs index 53e6dd3f0a8..705170164c2 100644 --- a/modules/sdk-test-procedure-cpp/client/src/module_bindings/mod.rs +++ b/modules/sdk-test-procedure-cpp/client/src/module_bindings/mod.rs @@ -299,6 +299,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -528,6 +532,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -614,6 +622,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -696,6 +708,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -778,6 +794,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -864,6 +884,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/case-conversion-client/src/main.rs b/sdks/rust/tests/case-conversion-client/src/main.rs index 3bcc10dadee..52e4c60a058 100644 --- a/sdks/rust/tests/case-conversion-client/src/main.rs +++ b/sdks/rust/tests/case-conversion-client/src/main.rs @@ -86,7 +86,7 @@ fn connect_then( callback(ctx); connected_result(Ok(())); }) - .on_connect_error(|_ctx, error| panic!("Connect errored: {error:?}")) + .on_connect_error(|_ctx, error, _next| panic!("Connect errored: {error:?}")) .build() .unwrap(); conn.run_threaded(); @@ -104,7 +104,7 @@ fn exec_insert_player() { connect_then(&test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.player_1().on_insert(move |_ctx, row| { let check = || { // Verify field names with digit boundaries are correctly case-converted @@ -123,7 +123,7 @@ fn exec_insert_player() { reducer_callback_assert_committed("create_player_1"), ) .unwrap(); - }) + })) // Query builder: subscribe to player_1 table (canonical: Player1Canonical) .add_query(|q| q.from.player_1().build()) .subscribe(); @@ -143,7 +143,7 @@ fn exec_insert_person() { connect_then(&test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.person_2().on_insert(move |_ctx, person| { let check = || { assert_eq_or_bail!("Bob".to_string(), person.first_name); @@ -174,7 +174,7 @@ fn exec_insert_person() { Err(e) => panic!("create_player_1 panicked: {e:?}"), }) .unwrap(); - }) + })) // Query builder: subscribe to both tables .add_query(|q| q.from.player_1().build()) .add_query(|q| q.from.person_2().build()) @@ -194,7 +194,7 @@ fn exec_ban_player() { connect_then(&test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.player_1().on_update(move |_ctx, _old, new| { let check = || { assert_eq_or_bail!(Player2Status::BannedUntil(9999), new.status_3_field); @@ -220,7 +220,7 @@ fn exec_ban_player() { Err(e) => panic!("create_player_1 panicked: {e:?}"), }) .unwrap(); - }) + })) // Query builder: subscribe to player_1 table .add_query(|q| q.from.player_1().build()) .subscribe(); @@ -239,7 +239,7 @@ fn exec_query_builder_filter() { connect_then(&test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.player_1().on_insert(move |_ctx, row| { let check = || { // Only level-5 players should come through the filter @@ -267,7 +267,7 @@ fn exec_query_builder_filter() { reducer_callback_assert_committed("create_player_1"), ) .unwrap(); - }) + })) // Query builder: filter on digit-boundary column current_level_2 .add_query(|q| q.from.player_1().filter(|p| p.current_level_2.eq(5)).build()) .subscribe(); @@ -290,7 +290,7 @@ fn exec_query_builder_join() { connect_then(&test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { // Listen for person_2 inserts that come through the join. // The join is: player_1 RIGHT SEMIJOIN person_2 ON player_1.player_1_id = person_2.player_ref // This means we see person_2 rows that have a matching player_1 row. @@ -335,7 +335,7 @@ fn exec_query_builder_join() { } }) .unwrap(); - }) + })) // Query builder: JOIN player_1 with person_2 on player_1_id = player_ref // player_1 RIGHT SEMIJOIN person_2 means: show person_2 rows that have a matching player_1 .add_query(|q| { @@ -362,7 +362,7 @@ fn exec_view() { connect_then(&test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.person_at_level_2().on_insert(move |_ctx, row| { let check = || { assert_eq_or_bail!("ViewPerson".to_string(), row.first_name); @@ -392,7 +392,7 @@ fn exec_view() { reducer_callback_assert_committed("add_person_2"), ) .unwrap(); - }) + })) // Subscribe to the view which selects people at level 2 .add_query(|q| q.from.person_at_level_2().build()) .subscribe(); diff --git a/sdks/rust/tests/case-conversion-client/src/module_bindings/mod.rs b/sdks/rust/tests/case-conversion-client/src/module_bindings/mod.rs index de71b3ab94e..1255c31139e 100644 --- a/sdks/rust/tests/case-conversion-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/case-conversion-client/src/module_bindings/mod.rs @@ -310,6 +310,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -534,6 +538,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -610,6 +618,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -682,6 +694,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -754,6 +770,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -830,6 +850,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/connect_disconnect_client/src/module_bindings/mod.rs b/sdks/rust/tests/connect_disconnect_client/src/module_bindings/mod.rs index 13c455309e8..fc7c79a77f0 100644 --- a/sdks/rust/tests/connect_disconnect_client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/connect_disconnect_client/src/module_bindings/mod.rs @@ -249,6 +249,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -473,6 +477,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -549,6 +557,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -621,6 +633,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -693,6 +709,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -769,6 +789,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/connect_disconnect_client/src/test_handlers.rs b/sdks/rust/tests/connect_disconnect_client/src/test_handlers.rs index a7795e8530b..a67b41e5066 100644 --- a/sdks/rust/tests/connect_disconnect_client/src/test_handlers.rs +++ b/sdks/rust/tests/connect_disconnect_client/src/test_handlers.rs @@ -15,14 +15,14 @@ pub async fn dispatch(db_name: &str) { let connection = DbConnection::builder() .with_database_name(db_name) .with_uri(server_url()) - .on_connect_error(|_ctx, error| panic!("on_connect_error: {error:?}")) + .on_connect_error(|_ctx, error, _next| panic!("on_connect_error: {error:?}")) .on_connect(move |ctx, _, _| { connected_result(Ok(())); ctx.subscription_builder() .on_error(|_ctx, error| { panic!("Subscription failed: {error:?}"); }) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { let check = || { anyhow::ensure!(ctx.db.connected().count() == 1); match ctx.db.connected().iter().next() { @@ -36,19 +36,21 @@ pub async fn dispatch(db_name: &str) { Ok(()) }; sub_applied_one_row_result(check()); - }) + })) .subscribe("SELECT * FROM connected"); }) - .on_disconnect(move |ctx, error| { - assert!( - !ctx.is_active(), - "on_disconnect callback, but `ctx.is_active()` is true" - ); - match error { - Some(err) => disconnect_result(Err(anyhow::anyhow!("{err:?}"))), - None => disconnect_result(Ok(())), - } - }); + .on_disconnect(test_counter::once3::( + move |ctx, error, _next| { + assert!( + !ctx.is_active(), + "on_disconnect callback, but `ctx.is_active()` is true" + ); + match error { + Some(err) => disconnect_result(Err(anyhow::anyhow!("{err:?}"))), + None => disconnect_result(Ok(())), + } + }, + )); let connection = build_connection(connection).await; #[cfg(not(target_arch = "wasm32"))] @@ -73,7 +75,7 @@ pub async fn dispatch(db_name: &str) { let sub_applied_one_row_result = reconnect_test_counter.add_test("disconnected_row"); let new_connection = DbConnection::builder() - .on_connect_error(|_ctx, error| panic!("on_connect_error: {error:?}")) + .on_connect_error(|_ctx, error, _next| panic!("on_connect_error: {error:?}")) .on_connect(move |_ctx, _, _| { reconnected_result(Ok(())); }) @@ -83,7 +85,7 @@ pub async fn dispatch(db_name: &str) { new_connection .subscription_builder() - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { let check = || { anyhow::ensure!(ctx.db.disconnected().count() == 1); match ctx.db.disconnected().iter().next() { @@ -97,7 +99,7 @@ pub async fn dispatch(db_name: &str) { Ok(()) }; sub_applied_one_row_result(check()); - }) + })) .on_error(|_ctx, error| panic!("subscription on_error: {error:?}")) .subscribe("SELECT * FROM disconnected"); diff --git a/sdks/rust/tests/event-table-client/src/module_bindings/mod.rs b/sdks/rust/tests/event-table-client/src/module_bindings/mod.rs index 1f83a93a8c3..e7f26dcc634 100644 --- a/sdks/rust/tests/event-table-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/event-table-client/src/module_bindings/mod.rs @@ -242,6 +242,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -466,6 +470,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -542,6 +550,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -614,6 +626,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -686,6 +702,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -762,6 +782,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/event-table-client/src/test_handlers.rs b/sdks/rust/tests/event-table-client/src/test_handlers.rs index 3e361125c66..1e4af648add 100644 --- a/sdks/rust/tests/event-table-client/src/test_handlers.rs +++ b/sdks/rust/tests/event-table-client/src/test_handlers.rs @@ -60,7 +60,7 @@ async fn connect_then( callback(ctx); connected_result(Ok(())); }) - .on_connect_error(|_ctx, error| panic!("Connect errored: {error:?}")); + .on_connect_error(|_ctx, error, _next| panic!("Connect errored: {error:?}")); build_and_run(conn).await } @@ -70,7 +70,7 @@ fn subscribe_these_then( callback: impl FnOnce(&SubscriptionEventContext) + Send + 'static, ) { ctx.subscription_builder() - .on_applied(callback) + .on_applied(test_counter::once::(callback)) .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) .subscribe(queries); } @@ -218,9 +218,11 @@ async fn exec_v1_rejects_event_table(db_name: &str) { let error_result = test_counter.add_test("v1-rejects-event-table"); ctx.subscription_builder() - .on_applied(move |_ctx: &SubscriptionEventContext| { - panic!("Subscription to event table should not succeed over v1"); - }) + .on_applied(test_counter::once::( + move |_ctx: &SubscriptionEventContext| { + panic!("Subscription to event table should not succeed over v1"); + }, + )) .on_error(move |_ctx, error| { let msg = format!("{error:?}"); if msg.contains("v2") || msg.contains("upgrade") || msg.contains("Upgrade") { diff --git a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/mod.rs b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/mod.rs index d0cd5e3b7e3..2196851e1e8 100644 --- a/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/procedural-view-pk-client/src/module_bindings/mod.rs @@ -299,6 +299,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -523,6 +527,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -599,6 +607,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -671,6 +683,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -743,6 +759,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -819,6 +839,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs b/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs index 319faf66b8b..64a91be864c 100644 --- a/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs +++ b/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs @@ -55,7 +55,7 @@ async fn connect_then_named( callback(ctx); connected_result(Ok(())); }) - .on_connect_error(|_ctx, error| panic!("Connect errored: {error:?}")); + .on_connect_error(|_ctx, error, _next| panic!("Connect errored: {error:?}")); build_and_run(conn).await } @@ -70,7 +70,7 @@ async fn exec_sender_scoped_pk_view(db_name: &str) { let _sender_a = connect_then(db_name, &test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.sender_left_view().on_update(move |_, old_row, new_row| { assert_eq!(old_row.id, 1); assert_eq!(old_row.filter, 10); @@ -85,7 +85,7 @@ async fn exec_sender_scoped_pk_view(db_name: &str) { ctx.reducers() .update_left_then(1, 11, reducer_callback_assert_committed("update_left")) .unwrap(); - }) + })) .add_query(|q| q.from.sender_left_view().build()) .subscribe(); }) @@ -94,7 +94,7 @@ async fn exec_sender_scoped_pk_view(db_name: &str) { let _sender_b = connect_then_named(db_name, &test_counter, "sender_b_on_connect", move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.sender_left_view().on_update(move |_, old_row, new_row| { assert_eq!(old_row.id, 2); assert_eq!(old_row.filter, 20); @@ -109,7 +109,7 @@ async fn exec_sender_scoped_pk_view(db_name: &str) { ctx.reducers() .update_left_then(2, 21, reducer_callback_assert_committed("update_left")) .unwrap(); - }) + })) .add_query(|q| q.from.sender_left_view().build()) .subscribe(); }) @@ -160,7 +160,7 @@ async fn exec_view_pk_left_semijoin(db_name: &str) { let mut joined_insert = Some(joined_insert); ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.sender_left_view().on_insert(move |ctx, row| { assert_eq!(ctx.db.sender_left_view().count(), 1); assert_eq!(row.id, 10); @@ -169,7 +169,7 @@ async fn exec_view_pk_left_semijoin(db_name: &str) { }); insert_semijoin_source_rows(ctx); - }) + })) .add_query(|q| { q.from .sender_right_view() @@ -188,7 +188,7 @@ async fn exec_view_pk_right_semijoin(db_name: &str) { let mut joined_insert = Some(joined_insert); ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.sender_right_view().on_insert(move |ctx, row| { assert_eq!(ctx.db.sender_right_view().count(), 1); assert_eq!(row.id, 10); @@ -197,7 +197,7 @@ async fn exec_view_pk_right_semijoin(db_name: &str) { }); insert_semijoin_source_rows(ctx); - }) + })) .add_query(|q| { q.from .sender_left_view() diff --git a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs index 97afd723359..43490958f67 100644 --- a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs @@ -309,6 +309,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -533,6 +537,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -609,6 +617,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -681,6 +693,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -753,6 +769,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -829,6 +849,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/procedure-client/src/test_handlers.rs b/sdks/rust/tests/procedure-client/src/test_handlers.rs index fdfc417cd9b..7e4cb8ee89d 100644 --- a/sdks/rust/tests/procedure-client/src/test_handlers.rs +++ b/sdks/rust/tests/procedure-client/src/test_handlers.rs @@ -72,7 +72,7 @@ async fn connect_with_then( callback(ctx); connected_result(Ok(())); }) - .on_connect_error(|_ctx, error| panic!("Connect errored: {error:?}")); + .on_connect_error(|_ctx, error, _next| panic!("Connect errored: {error:?}")); build_and_run(with_builder(builder)).await } @@ -101,7 +101,7 @@ fn subscribe_these_then( callback: impl FnOnce(&SubscriptionEventContext) + Send + 'static, ) { ctx.subscription_builder() - .on_applied(callback) + .on_applied(test_counter::once::(callback)) .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) .subscribe(queries); } diff --git a/sdks/rust/tests/procedure-concurrency-client/src/module_bindings/mod.rs b/sdks/rust/tests/procedure-concurrency-client/src/module_bindings/mod.rs index 286a46580bb..21ce2cee225 100644 --- a/sdks/rust/tests/procedure-concurrency-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/procedure-concurrency-client/src/module_bindings/mod.rs @@ -307,6 +307,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -531,6 +535,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -607,6 +615,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -679,6 +691,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -751,6 +767,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -827,6 +847,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/procedure-concurrency-client/src/test_handlers.rs b/sdks/rust/tests/procedure-concurrency-client/src/test_handlers.rs index e059a8e2ee5..bb9cc0163db 100644 --- a/sdks/rust/tests/procedure-concurrency-client/src/test_handlers.rs +++ b/sdks/rust/tests/procedure-concurrency-client/src/test_handlers.rs @@ -66,7 +66,7 @@ async fn connect_with_then( callback(ctx); connected_result(Ok(())); }) - .on_connect_error(|_ctx, error| panic!("Connect errored: {error:?}")); + .on_connect_error(|_ctx, error, _next| panic!("Connect errored: {error:?}")); build_and_run(with_builder(builder)).await } @@ -82,7 +82,7 @@ const SUBSCRIBE_ALL: &[&str] = &["SELECT * FROM procedure_concurrency_row;"]; fn subscribe_all_then(ctx: &impl RemoteDbContext, callback: impl FnOnce(&SubscriptionEventContext) + Send + 'static) { ctx.subscription_builder() - .on_applied(callback) + .on_applied(test_counter::once::(callback)) .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) .subscribe(SUBSCRIBE_ALL); } diff --git a/sdks/rust/tests/reconnect.rs b/sdks/rust/tests/reconnect.rs new file mode 100644 index 00000000000..fd74f377477 --- /dev/null +++ b/sdks/rust/tests/reconnect.rs @@ -0,0 +1,375 @@ +#![cfg(not(target_arch = "wasm32"))] + +#[path = "../examples/reconnect-desktop/src/module_bindings/mod.rs"] +mod module_bindings; + +use module_bindings::*; +use spacetimedb_client_api_messages::websocket::v2 as ws; +use spacetimedb_lib::bsatn; +use spacetimedb_sdk::{AutomaticReconnectOptions, ConnectionId, DbContext, Identity}; +use std::{ + net::TcpListener, + sync::{mpsc, Arc, Mutex}, + time::{Duration, Instant}, +}; +use tokio_tungstenite::tungstenite::{ + self, + handshake::server::{Request, Response}, + Message, +}; + +fn accept(stream: std::net::TcpStream) -> tungstenite::WebSocket { + tungstenite::accept_hdr(stream, |_: &Request, mut response: Response| { + response + .headers_mut() + .insert("sec-websocket-protocol", "v2.bsatn.spacetimedb".parse().unwrap()); + Ok(response) + }) + .unwrap() +} + +fn send(socket: &mut tungstenite::WebSocket, message: ws::ServerMessage) { + let mut bytes = vec![0]; // uncompressed binary protocol + bytes.extend(bsatn::to_vec(&message).unwrap()); + socket.send(Message::Binary(bytes.into())).unwrap(); +} +fn tick_until(connection: &DbConnection, ready: impl Fn() -> bool) { + let deadline = Instant::now() + Duration::from_secs(8); + while !ready() { + let _ = connection.frame_tick(); + assert!(Instant::now() < deadline, "connection did not reach expected state"); + std::thread::sleep(Duration::from_millis(2)); + } +} +fn policy() -> AutomaticReconnectOptions { + AutomaticReconnectOptions { + min_delay: Duration::from_millis(500), + max_delay: Duration::from_secs(1), + } +} + +#[test] +fn reconnect_preserves_session_and_credentials_and_stops_on_identity_change() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let requests = Arc::new(Mutex::new(Vec::new())); + let captured = requests.clone(); + let (drop_tx, drop_rx) = mpsc::channel(); + let server = std::thread::spawn(move || { + for round in 0..3 { + let stream = listener.accept().unwrap().0; + let mut socket = tungstenite::accept_hdr(stream, |request: &Request, mut response: Response| { + captured.lock().unwrap().push(( + request.uri().to_string(), + request.headers().get("authorization").cloned(), + )); + response + .headers_mut() + .insert("sec-websocket-protocol", "v2.bsatn.spacetimedb".parse().unwrap()); + Ok(response) + }) + .unwrap(); + send( + &mut socket, + ws::ServerMessage::InitialConnection(ws::InitialConnection { + identity: Identity::from_byte_array([if round == 2 { 2 } else { 1 }; 32]), + token: "retained-token".into(), + connection_id: ConnectionId::from_u128(round + 1), + }), + ); + drop_rx.recv_timeout(Duration::from_secs(10)).unwrap(); + } + }); + let events = Arc::new(Mutex::new(Vec::new())); + let connect = events.clone(); + let reconnect = events.clone(); + let disconnect = events.clone(); + let error = events.clone(); + let connection = DbConnection::builder() + .with_uri(format!("http://{address}")) + .with_database_name("test") + .with_automatic_reconnect_options(policy()) + .on_connect(move |_, _, _| connect.lock().unwrap().push("connect")) + .on_automatic_reconnect(move |_, _, _| reconnect.lock().unwrap().push("reconnect")) + .on_disconnect(move |_, _, next| { + assert_eq!(next.unwrap().attempt, 1); + disconnect.lock().unwrap().push("disconnect"); + }) + .on_connect_error(move |_, _, next| { + assert!(next.is_none()); + error.lock().unwrap().push("terminal"); + }) + .build() + .unwrap(); + tick_until(&connection, || events.lock().unwrap().len() == 1); + let identity = connection.identity(); + drop_tx.send(()).unwrap(); + tick_until(&connection, || events.lock().unwrap().len() == 3); + assert_eq!(connection.identity(), identity); + assert_eq!(connection.connection_id(), ConnectionId::from_u128(2)); + drop_tx.send(()).unwrap(); + tick_until(&connection, || events.lock().unwrap().len() == 5); + assert!(!connection.is_active() && !connection.is_reconnecting()); + assert_eq!( + *events.lock().unwrap(), + ["connect", "disconnect", "reconnect", "disconnect", "terminal"] + ); + let requests = requests.lock().unwrap(); + let session = |uri: &str| { + uri.split("session_id=") + .nth(1) + .unwrap() + .split('&') + .next() + .unwrap() + .to_owned() + }; + assert_eq!(session(&requests[0].0), session(&requests[1].0)); + assert!(requests[0].1.is_none()); + assert_eq!(requests[1].1.as_ref().unwrap(), "Bearer retained-token"); + drop_tx.send(()).unwrap(); + server.join().unwrap(); +} + +#[test] +fn initial_handshake_failure_never_retries() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = std::thread::spawn(move || { + let socket = accept(listener.accept().unwrap().0); + drop(socket); // No InitialConnection. + }); + let errors = Arc::new(Mutex::new(0)); + let called = errors.clone(); + let connection = DbConnection::builder() + .with_uri(format!("http://{address}")) + .with_database_name("test") + .with_automatic_reconnect() + .on_connect_error(move |_, _, next| { + assert!(next.is_none()); + *called.lock().unwrap() += 1; + }) + .build() + .unwrap(); + tick_until(&connection, || *errors.lock().unwrap() == 1); + assert!(!connection.is_reconnecting()); + server.join().unwrap(); +} + +#[test] +fn in_flight_call_is_unknown_and_disconnect_cancels_backoff() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = std::thread::spawn(move || { + let mut socket = accept(listener.accept().unwrap().0); + send( + &mut socket, + ws::ServerMessage::InitialConnection(ws::InitialConnection { + identity: Identity::from_byte_array([1; 32]), + token: "token".into(), + connection_id: ConnectionId::from_u128(1), + }), + ); + // Accept a reducer but deliberately lose its result. + let Message::Binary(bytes) = socket.read().unwrap() else { + panic!("expected reducer"); + }; + assert!(matches!( + bsatn::from_slice::(&bytes).unwrap(), + ws::ClientMessage::CallReducer(_) + )); + }); + let disconnects = Arc::new(Mutex::new(Vec::new())); + let events = disconnects.clone(); + let connection = DbConnection::builder() + .with_uri(format!("http://{address}")) + .with_database_name("test") + .with_automatic_reconnect_options(policy()) + .on_disconnect(move |_, error, next| { + events.lock().unwrap().push((error.is_none(), next)); + }) + .build() + .unwrap(); + tick_until(&connection, || connection.try_identity().is_some()); + let unknown = Arc::new(Mutex::new(false)); + let result = unknown.clone(); + connection + .reducers + .change_both_then(move |_, outcome| *result.lock().unwrap() = outcome.unwrap_err().is_unknown_result()) + .unwrap(); + tick_until(&connection, || connection.is_reconnecting()); + assert!(*unknown.lock().unwrap()); + assert!(matches!( + connection.reducers.change_both(), + Err(spacetimedb_sdk::Error::Disconnected) + )); + connection.disconnect().unwrap(); + assert!(connection.frame_tick().is_err()); + assert!(!connection.is_active() && !connection.is_reconnecting()); + assert_eq!(disconnects.lock().unwrap().len(), 2); + assert!(disconnects.lock().unwrap()[1].0); + assert!(disconnects.lock().unwrap()[1].1.is_none()); + server.join().unwrap(); +} + +#[test] +fn malformed_protocol_is_terminal() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let (go, wait) = mpsc::channel(); + let server = std::thread::spawn(move || { + let mut socket = accept(listener.accept().unwrap().0); + send( + &mut socket, + ws::ServerMessage::InitialConnection(ws::InitialConnection { + identity: Identity::from_byte_array([1; 32]), + token: "token".into(), + connection_id: ConnectionId::from_u128(1), + }), + ); + wait.recv().unwrap(); + socket.send(Message::Binary(vec![0, 255].into())).unwrap(); + }); + let terminal = Arc::new(Mutex::new(false)); + let event = terminal.clone(); + let connection = DbConnection::builder() + .with_uri(format!("http://{address}")) + .with_database_name("test") + .with_automatic_reconnect_options(policy()) + .on_disconnect(move |_, error, next| { + assert!(error.is_some()); + assert!(next.is_none()); + *event.lock().unwrap() = true; + }) + .build() + .unwrap(); + tick_until(&connection, || connection.try_identity().is_some()); + go.send(()).unwrap(); + tick_until(&connection, || *terminal.lock().unwrap()); + assert!(!connection.is_reconnecting()); + server.join().unwrap(); +} + +#[test] +fn rejected_retained_token_forces_refresh_even_after_provider_failure() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let (go, wait) = mpsc::channel(); + let server = std::thread::spawn(move || { + for round in 0..3 { + let stream = listener.accept().unwrap().0; + let socket = tungstenite::accept_hdr(stream, |request: &Request, mut response: Response| { + if round == 1 { + assert!(request.headers()["authorization"] + .to_str() + .unwrap() + .starts_with("Bearer header.")); + Err(tungstenite::http::Response::builder() + .status(401) + .body(Some("Rejected retained token".into())) + .unwrap()) + } else { + if round == 2 { + assert_eq!(request.headers()["authorization"], "Bearer refreshed"); + } + response + .headers_mut() + .insert("sec-websocket-protocol", "v2.bsatn.spacetimedb".parse().unwrap()); + Ok(response) + } + }); + if round == 1 { + assert!(socket.is_err()); + continue; + } + let mut socket = socket.unwrap(); + send( + &mut socket, + ws::ServerMessage::InitialConnection(ws::InitialConnection { + identity: Identity::from_byte_array([1; 32]), + // Valid far-future exp: refresh must be forced by the rejection. + token: "header.eyJleHAiOjQwMDAwMDAwMDAsImlhdCI6MTcwMDAwMDAwMH0.signature".into(), + connection_id: ConnectionId::from_u128(round + 1), + }), + ); + wait.recv_timeout(Duration::from_secs(10)).unwrap(); + } + }); + let calls = Arc::new(std::sync::atomic::AtomicU32::new(0)); + let called = calls.clone(); + let reconnects = Arc::new(Mutex::new(0)); + let reconnected = reconnects.clone(); + let connection = DbConnection::builder() + .with_uri(format!("http://{address}")) + .with_database_name("test") + .with_automatic_reconnect_options(policy()) + .with_token_provider(move || { + let attempt = called.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + async move { + if attempt == 0 { + Err(spacetimedb_sdk::Error::TokenProvider { + message: "refresh unavailable".into(), + }) + } else { + Ok("refreshed".into()) + } + } + }) + .on_automatic_reconnect(move |_, _, _| *reconnected.lock().unwrap() += 1) + .build() + .unwrap(); + tick_until(&connection, || connection.try_identity().is_some()); + go.send(()).unwrap(); + tick_until(&connection, || *reconnects.lock().unwrap() == 1); + assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 2); + connection.disconnect().unwrap(); + let _ = connection.frame_tick(); + go.send(()).unwrap(); + server.join().unwrap(); +} + +#[test] +fn async_runner_survives_a_drop_and_stops_from_reconnect_callback() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = std::thread::spawn(move || { + for round in 0..4 { + let mut socket = accept(listener.accept().unwrap().0); + send( + &mut socket, + ws::ServerMessage::InitialConnection(ws::InitialConnection { + identity: Identity::from_byte_array([1; 32]), + token: "retained".into(), + connection_id: ConnectionId::from_u128(round + 1), + }), + ); + if round == 3 { + let _ = socket.read(); + } + } + }); + let reconnected = Arc::new(Mutex::new(0)); + let called = reconnected.clone(); + let connection = DbConnection::builder() + .with_uri(format!("http://{address}")) + .with_database_name("test") + .with_automatic_reconnect_options(policy()) + .on_automatic_reconnect(move |ctx, _, _| { + let mut count = called.lock().unwrap(); + *count += 1; + if *count == 3 { + ctx.disconnect().unwrap(); + } + }) + .build() + .unwrap(); + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + tokio::time::timeout(Duration::from_secs(8), connection.run_async()) + .await + .unwrap() + .unwrap(); + }); + assert_eq!(*reconnected.lock().unwrap(), 3); + server.join().unwrap(); +} diff --git a/sdks/rust/tests/test-client/src/module_bindings/mod.rs b/sdks/rust/tests/test-client/src/module_bindings/mod.rs index 56cb87d3587..a4f40027af9 100644 --- a/sdks/rust/tests/test-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/test-client/src/module_bindings/mod.rs @@ -4352,6 +4352,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -4576,6 +4580,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -4652,6 +4660,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -4724,6 +4736,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -4796,6 +4812,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -4872,6 +4892,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/test-client/src/test_handlers.rs b/sdks/rust/tests/test-client/src/test_handlers.rs index c8b56c978fc..dcdd3c54bcb 100644 --- a/sdks/rust/tests/test-client/src/test_handlers.rs +++ b/sdks/rust/tests/test-client/src/test_handlers.rs @@ -374,7 +374,7 @@ async fn connect_with_then( callback(ctx); connected_result(Ok(())); }) - .on_connect_error(|_ctx, error| panic!("Connect errored: {error:?}")); + .on_connect_error(|_ctx, error, _next| panic!("Connect errored: {error:?}")); build_and_run(with_builder(builder)).await } @@ -420,7 +420,7 @@ fn subscribe_these_then( callback: impl FnOnce(&SubscriptionEventContext) + Send + 'static, ) { ctx.subscription_builder() - .on_applied(callback) + .on_applied(test_counter::once::(callback)) .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) .subscribe(queries); } @@ -446,9 +446,11 @@ async fn exec_subscribe_and_cancel(db_name: &str) { move |ctx| { let handle = ctx .subscription_builder() - .on_applied(move |_ctx: &SubscriptionEventContext| { - panic!("Subscription should never be applied"); - }) + .on_applied(test_counter::once::( + move |_ctx: &SubscriptionEventContext| { + panic!("Subscription should never be applied"); + }, + )) .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) .subscribe("SELECT * FROM one_u_8;"); assert!(!handle.is_active()); @@ -480,22 +482,24 @@ async fn exec_subscribe_and_unsubscribe(db_name: &str) { let hc_clone = handle_cell.clone(); let handle = ctx .subscription_builder() - .on_applied(move |ctx: &SubscriptionEventContext| { - let handle = { hc_clone.lock().unwrap().as_ref().unwrap().clone() }; - assert!(ctx.is_active()); - assert!(handle.is_active()); - assert!(!handle.is_ended()); - assert!(ctx.db.one_u_8().count() == 1); - let handle_clone = handle.clone(); - handle - .unsubscribe_then(Box::new(move |ectx| { - assert!(!handle_clone.is_active()); - assert!(handle_clone.is_ended()); - assert!(ectx.db.one_u_8().count() == 0); - cb(Ok(())); - })) - .unwrap(); - }) + .on_applied(test_counter::once::( + move |ctx: &SubscriptionEventContext| { + let handle = { hc_clone.lock().unwrap().as_ref().unwrap().clone() }; + assert!(ctx.is_active()); + assert!(handle.is_active()); + assert!(!handle.is_ended()); + assert!(ctx.db.one_u_8().count() == 1); + let handle_clone = handle.clone(); + handle + .unsubscribe_then(Box::new(move |ectx| { + assert!(!handle_clone.is_active()); + assert!(handle_clone.is_ended()); + assert!(ectx.db.one_u_8().count() == 0); + cb(Ok(())); + })) + .unwrap(); + }, + )) .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) .subscribe("SELECT * FROM one_u_8;"); handle_cell.lock().unwrap().replace(handle.clone()); @@ -514,9 +518,11 @@ async fn exec_subscription_error_smoke_test(db_name: &str) { move |ctx| { let handle = ctx .subscription_builder() - .on_applied(move |_ctx: &SubscriptionEventContext| { - panic!("Subscription should never be applied"); - }) + .on_applied(test_counter::once::( + move |_ctx: &SubscriptionEventContext| { + panic!("Subscription should never be applied"); + }, + )) .on_error(|_, _| cb(Ok(()))) .subscribe("SELEcCT * FROM one_u8;"); // intentional typo assert!(!handle.is_active()); @@ -1745,7 +1751,7 @@ async fn exec_reauth_part_1(db_name: &str) { .on_connect(move |_, _identity, token| { save_result(creds.save(token).map_err(Into::into)); }) - .on_connect_error(|_ctx, error| panic!("Connect failed: {error:?}")) + .on_connect_error(|_ctx, error, _next| panic!("Connect failed: {error:?}")) .with_database_name(name) .with_uri(server_url()) .build() @@ -1780,7 +1786,7 @@ async fn exec_reauth_part_2(db_name: &str) { creds_match_result(run_checks()); } }) - .on_connect_error(|_ctx, error| panic!("Connect failed: {error:?}")) + .on_connect_error(|_ctx, error, _next| panic!("Connect failed: {error:?}")) .with_database_name(name) .with_token(Some(token)) .with_uri(server_url()) @@ -1815,14 +1821,16 @@ async fn exec_reconnect_different_connection_id(db_name: &str) { DbConnection::builder() .with_database_name(db_name) .with_uri(server_url()) - .on_connect_error(|_ctx, error| panic!("on_connect_error: {error:?}")) + .on_connect_error(|_ctx, error, _next| panic!("on_connect_error: {error:?}")) .on_connect(move |_, _, _| { initial_connect_result(Ok(())); }) - .on_disconnect(|_, error| match error { - None => disconnect_result(Ok(())), - Some(err) => disconnect_result(Err(anyhow::anyhow!("{err:?}"))), - }), + .on_disconnect(test_counter::once3::( + |_, error, _next| match error { + None => disconnect_result(Ok(())), + Some(err) => disconnect_result(Err(anyhow::anyhow!("{err:?}"))), + }, + )), ) .await; @@ -1842,7 +1850,7 @@ async fn exec_reconnect_different_connection_id(db_name: &str) { DbConnection::builder() .with_database_name(db_name) .with_uri(server_url()) - .on_connect_error(|_ctx, error| panic!("on_connect_error: {error:?}")) + .on_connect_error(|_ctx, error, _next| panic!("on_connect_error: {error:?}")) .on_connect(move |ctx, _, _| { reconnect_result(Ok(())); let run_checks = || { @@ -1899,7 +1907,7 @@ async fn exec_subscribe_all_select_star(db_name: &str) { connection .subscription_builder() - .on_applied({ + .on_applied(test_counter::once::({ let test_counter = test_counter.clone(); move |ctx| { insert_one::(ctx, &test_counter, 0); @@ -1925,7 +1933,7 @@ async fn exec_subscribe_all_select_star(db_name: &str) { sub_applied_nothing_result(assert_all_tables_empty(ctx)); } - }) + })) .on_error(|_, e| panic!("Subscription error: {e:?}")) .subscribe_to_all_tables(); diff --git a/sdks/rust/tests/test-counter/src/lib.rs b/sdks/rust/tests/test-counter/src/lib.rs index 5ed52929ab2..0f9bfbcae80 100644 --- a/sdks/rust/tests/test-counter/src/lib.rs +++ b/sdks/rust/tests/test-counter/src/lib.rs @@ -215,3 +215,16 @@ impl TestCounter { } } } + +/// Adapt a one-shot test assertion to a repeatable subscription callback. +/// A second invocation fails the test, preserving the old tests' expectations. +pub fn once(callback: F) -> impl FnMut(&Ctx) { + let mut callback = Some(callback); + move |ctx| callback.take().expect("one-shot test callback fired twice")(ctx) +} + +/// The equivalent adapter for lifecycle callbacks with three arguments. +pub fn once3(callback: F) -> impl FnMut(&Ctx, A, B) { + let mut callback = Some(callback); + move |ctx, a, b| callback.take().expect("one-shot test callback fired twice")(ctx, a, b) +} diff --git a/sdks/rust/tests/view-client/src/module_bindings/mod.rs b/sdks/rust/tests/view-client/src/module_bindings/mod.rs index 2857b5d0af6..5b35de47148 100644 --- a/sdks/rust/tests/view-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/view-client/src/module_bindings/mod.rs @@ -342,6 +342,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -566,6 +570,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -642,6 +650,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -714,6 +726,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -786,6 +802,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -862,6 +882,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/view-client/src/test_handlers.rs b/sdks/rust/tests/view-client/src/test_handlers.rs index c8b0743166b..d35fdcd1f38 100644 --- a/sdks/rust/tests/view-client/src/test_handlers.rs +++ b/sdks/rust/tests/view-client/src/test_handlers.rs @@ -45,7 +45,7 @@ async fn build_connection( .on_connect(|ctx, _, _| { callback(ctx); }) - .on_connect_error(|_ctx, error| panic!("Connect errored: {error:?}")); + .on_connect_error(|_ctx, error, _next| panic!("Connect errored: {error:?}")); build_and_run(with_builder(builder)).await } @@ -78,7 +78,7 @@ fn subscribe_these_then( callback: impl FnOnce(&SubscriptionEventContext) + Send + 'static, ) { ctx.subscription_builder() - .on_applied(callback) + .on_applied(test_counter::once::(callback)) .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) .subscribe(queries); } @@ -95,20 +95,22 @@ async fn connect_my_player_client( build_connection( db_name, |builder| { - builder.on_disconnect(move |ctx, error| { - assert!( - !ctx.is_active(), - "on_disconnect callback, but `ctx.is_active()` is true" - ); - if let Some(disconnected_result) = disconnected_result { - match error { - Some(error) => disconnected_result(Err(anyhow::anyhow!("{error:?}"))), - None => disconnected_result(Ok(())), + builder.on_disconnect(test_counter::once3::( + move |ctx, error, _next| { + assert!( + !ctx.is_active(), + "on_disconnect callback, but `ctx.is_active()` is true" + ); + if let Some(disconnected_result) = disconnected_result { + match error { + Some(error) => disconnected_result(Err(anyhow::anyhow!("{error:?}"))), + None => disconnected_result(Ok(())), + } + } else if let Some(error) = error { + panic!("Disconnect errored: {error:?}"); } - } else if let Some(error) = error { - panic!("Disconnect errored: {error:?}"); - } - }) + }, + )) }, move |ctx| { subscribe_these_then(ctx, &["SELECT * FROM my_player"], { @@ -221,7 +223,7 @@ async fn exec_anonymous_subscribe_with_query_builder(db_name: &str) { connect_then(db_name, &test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.player().on_insert(move |_, player| { if player.identity == Identity::from_byte_array([2; 32]) { return put_result(&mut insert_0, Ok(())); @@ -271,7 +273,7 @@ async fn exec_anonymous_subscribe_with_query_builder(db_name: &str) { reducer_callback_assert_committed("delete_player"), ) .unwrap(); - }) + })) .add_query(|ctx| { ctx.from .player_level() @@ -374,7 +376,7 @@ async fn exec_non_table_query_builder_return(db_name: &str) { connect_then(db_name, &test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { let my_identity = ctx.identity(); ctx.db.my_player_and_level().on_insert(move |_, player| { assert_eq!(player.identity, my_identity); @@ -406,7 +408,7 @@ async fn exec_non_table_query_builder_return(db_name: &str) { ctx.reducers() .delete_player_then(my_identity, reducer_callback_assert_committed("delete_player")) .unwrap(); - }) + })) .add_query(|q_ctx| q_ctx.from.my_player_and_level().filter(|p| p.level.eq(1)).build()) .subscribe(); }) diff --git a/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs b/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs index 429aee0553b..6f9fe559f2f 100644 --- a/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/view-pk-client/src/module_bindings/mod.rs @@ -366,6 +366,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -590,6 +594,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -666,6 +674,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -738,6 +750,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -810,6 +826,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -886,6 +906,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/sdks/rust/tests/view-pk-client/src/test_handlers.rs b/sdks/rust/tests/view-pk-client/src/test_handlers.rs index e19a5677449..2a5843022a9 100644 --- a/sdks/rust/tests/view-pk-client/src/test_handlers.rs +++ b/sdks/rust/tests/view-pk-client/src/test_handlers.rs @@ -58,7 +58,7 @@ async fn connect_then_named( callback(ctx); connected_result(Ok(())); }) - .on_connect_error(|_ctx, error| panic!("Connect errored: {error:?}")); + .on_connect_error(|_ctx, error, _next| panic!("Connect errored: {error:?}")); build_and_run(conn).await } @@ -68,7 +68,7 @@ fn subscribe_these_then( callback: impl FnOnce(&SubscriptionEventContext) + Send + 'static, ) { ctx.subscription_builder() - .on_applied(callback) + .on_applied(test_counter::once::(callback)) .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) .subscribe(queries); } @@ -150,7 +150,7 @@ async fn exec_view_pk_join_query_builder(db_name: &str) { connect_then(db_name, &test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.all_view_pk_players().on_update(move |_, old_row, new_row| { assert_eq!(old_row.id, 1); assert_eq!(old_row.name, "before"); @@ -185,7 +185,7 @@ async fn exec_view_pk_join_query_builder(db_name: &str) { reducer_callback_assert_committed("update_view_pk_player"), ) .unwrap(); - }) + })) .add_query(|q| { q.from .view_pk_membership() @@ -229,7 +229,7 @@ async fn exec_view_pk_semijoin_two_sender_views_query_builder(db_name: &str) { connect_then(db_name, &test_counter, move |ctx| { ctx.subscription_builder() .on_error(|_ctx, error| panic!("Subscription errored: {error:?}")) - .on_applied(move |ctx| { + .on_applied(test_counter::once::(move |ctx| { ctx.db.sender_view_pk_players_b().on_update(move |_, old_row, new_row| { assert_eq!(old_row.id, 1); assert_eq!(old_row.name, "before"); @@ -273,7 +273,7 @@ async fn exec_view_pk_semijoin_two_sender_views_query_builder(db_name: &str) { reducer_callback_assert_committed("update_view_pk_player"), ) .unwrap(); - }) + })) .add_query(|q| { q.from .sender_view_pk_players_a() diff --git a/templates/basic-cpp/src/main.rs b/templates/basic-cpp/src/main.rs index c7d6b1b4f4a..ea39e8673f3 100644 --- a/templates/basic-cpp/src/main.rs +++ b/templates/basic-cpp/src/main.rs @@ -18,7 +18,7 @@ fn main() { .on_connect(|_, _, _| { println!("Connected to SpacetimeDB"); }) - .on_connect_error(|_ctx, e| { + .on_connect_error(|_ctx, e, _next| { eprintln!("Connection error: {:?}", e); std::process::exit(1); }) diff --git a/templates/basic-cpp/src/module_bindings/mod.rs b/templates/basic-cpp/src/module_bindings/mod.rs index 0bf2df1c622..30c372af919 100644 --- a/templates/basic-cpp/src/module_bindings/mod.rs +++ b/templates/basic-cpp/src/module_bindings/mod.rs @@ -224,6 +224,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -440,6 +444,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -516,6 +524,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -588,6 +600,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -660,6 +676,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -736,6 +756,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/templates/basic-rs/src/main.rs b/templates/basic-rs/src/main.rs index c7d6b1b4f4a..ea39e8673f3 100644 --- a/templates/basic-rs/src/main.rs +++ b/templates/basic-rs/src/main.rs @@ -18,7 +18,7 @@ fn main() { .on_connect(|_, _, _| { println!("Connected to SpacetimeDB"); }) - .on_connect_error(|_ctx, e| { + .on_connect_error(|_ctx, e, _next| { eprintln!("Connection error: {:?}", e); std::process::exit(1); }) diff --git a/templates/basic-rs/src/module_bindings/mod.rs b/templates/basic-rs/src/module_bindings/mod.rs index 0bf2df1c622..30c372af919 100644 --- a/templates/basic-rs/src/module_bindings/mod.rs +++ b/templates/basic-rs/src/module_bindings/mod.rs @@ -224,6 +224,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -440,6 +444,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -516,6 +524,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -588,6 +600,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -660,6 +676,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -736,6 +756,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } diff --git a/templates/chat-console-rs/src/main.rs b/templates/chat-console-rs/src/main.rs index 81ee204a456..877ab16fbff 100644 --- a/templates/chat-console-rs/src/main.rs +++ b/templates/chat-console-rs/src/main.rs @@ -71,13 +71,13 @@ fn on_connected(_ctx: &DbConnection, _identity: Identity, token: &str) { // ### Handle errors and disconnections /// Our `on_connect_error` callback: print the error, then exit the process. -fn on_connect_error(_ctx: &ErrorContext, err: Error) { +fn on_connect_error(_ctx: &ErrorContext, err: Error, _next: Option) { eprintln!("Connection error: {err}"); std::process::exit(1); } /// Our `on_disconnect` callback: print a note, then exit the process. -fn on_disconnected(_ctx: &ErrorContext, err: Option) { +fn on_disconnected(_ctx: &ErrorContext, err: Option, _next: Option) { if let Some(err) = err { eprintln!("Disconnected: {err}"); std::process::exit(1); diff --git a/templates/chat-console-rs/src/module_bindings/mod.rs b/templates/chat-console-rs/src/module_bindings/mod.rs index b40a5c09bef..73d7216244e 100644 --- a/templates/chat-console-rs/src/module_bindings/mod.rs +++ b/templates/chat-console-rs/src/module_bindings/mod.rs @@ -245,6 +245,10 @@ impl __sdk::DbContext for DbConnection { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -461,6 +465,10 @@ impl __sdk::DbContext for EventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -537,6 +545,10 @@ impl __sdk::DbContext for ReducerEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -609,6 +621,10 @@ impl __sdk::DbContext for ProcedureEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -681,6 +697,10 @@ impl __sdk::DbContext for SubscriptionEventContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } @@ -757,6 +777,10 @@ impl __sdk::DbContext for ErrorContext { self.imp.is_active() } + fn is_reconnecting(&self) -> bool { + self.imp.is_reconnecting() + } + fn disconnect(&self) -> __sdk::Result<()> { self.imp.disconnect() } From 4ec90af5410c7f2ae3405bdfe66c07e0914ec14d Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Fri, 9 Oct 2026 15:50:08 +0200 Subject: [PATCH 03/12] Update docs for auto-reconnect --- .../00300-tutorials/00100-chat-app.md | 4 ++-- .../00600-clients/00300-connection.md | 20 +++++++++++++++---- .../00600-clients/00500-rust-reference.md | 20 ++++++++++++++++--- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md index 85a00f88829..02d0cb95ad8 100644 --- a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md +++ b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md @@ -2452,13 +2452,13 @@ To `src/main.rs`, add: ```rust /// Our `on_connect_error` callback: print the error, then exit the process. -fn on_connect_error(_ctx: &ErrorContext, err: Error) { +fn on_connect_error(_ctx: &ErrorContext, err: Error, _next: Option) { eprintln!("Connection error: {:?}", err); std::process::exit(1); } /// Our `on_disconnect` callback: print a note, then exit the process. -fn on_disconnected(_ctx: &ErrorContext, err: Option) { +fn on_disconnected(_ctx: &ErrorContext, err: Option, _next: Option) { if let Some(err) = err { eprintln!("Disconnected: {}", err); std::process::exit(1); diff --git a/docs/docs/00200-core-concepts/00600-clients/00300-connection.md b/docs/docs/00200-core-concepts/00600-clients/00300-connection.md index 3101810230e..31f74b62ba1 100644 --- a/docs/docs/00200-core-concepts/00600-clients/00300-connection.md +++ b/docs/docs/00200-core-concepts/00600-clients/00300-connection.md @@ -232,7 +232,7 @@ Failure to advance the connection means your client will not receive any updates ::: -In Rust and TypeScript, the connection processes messages automatically via the browser's event loop or Node.js's event loop. No manual polling is required. +TypeScript processes messages through the browser or Node.js event loop. Rust applications must advance their connection using `frame_tick()`, `run_threaded()`, `run_async()`, or the browser target's `run_background_task()`. ## Connection Lifecycle @@ -327,10 +327,10 @@ let conn = DbConnection::builder() println!("Connected! Saving token..."); // Save token for reconnection }) - .on_connect_error(|_ctx, error| { + .on_connect_error(|_ctx, error, _next| { eprintln!("Connection failed: {}", error); }) - .on_disconnect(|_ctx, error| { + .on_disconnect(|_ctx, error, _next| { if let Some(err) = error { eprintln!("Disconnected with error: {}", err); } else { @@ -450,7 +450,19 @@ Without automatic reconnect, create a new connection after a connection loss. Se -Automatic reconnect support is coming soon. For now, implement reconnection in your application: create a new connection and restore subscriptions after a connection loss. +Enable `.with_automatic_reconnect()` to recover after an established connection drops. The same connection, table handles, callbacks, and subscription handles survive. While `is_reconnecting()` is true, cache reads return the last known rows. **Keep advancing the connection during outages**, using `frame_tick()`, `run_threaded()`, or `run_async()`; do not stop ticking when `is_active()` becomes false. + +`on_connect` fires only for the initial connection. Register `on_automatic_reconnect` for successful automatic reconnects. Both receive the connection, identity, and token. Subscriptions replay together in one atomic batch, and their `on_applied` callbacks fire again; unchanged rows do not fire row callbacks. + +`on_disconnect(|ctx, error, next| ...)` and `on_connect_error(|ctx, error, next| ...)` receive `Option`, with the next attempt number and `Duration`, or `None` when no retry is scheduled. Initial connection failures are not retried. `disconnect()` cancels recovery. + +Use `.with_automatic_reconnect_options(AutomaticReconnectOptions { min_delay, max_delay })` to configure backoff. Defaults are one second and 30 seconds, with exponential growth and 50% jitter clamped to the bounds. The minimum delay is at least 500 ms; the maximum is at least one second and the minimum delay. + +For expiring credentials, combine `.with_token(Some(initial_token))` and `.with_token_provider(|| async { /* return Result */ })`. The provider refreshes near expiry or after rejection and must return a token for the same identity. Provider failures retry; rejection of a fresh token and identity changes are terminal. + +Calls made while reconnecting fail with a disconnected error. Calls interrupted by a drop may have executed: reducer/procedure callback errors expose `is_unknown_result()`. Calls are never replayed. Subscription creation is queued during recovery, and cancellation removes a set from replay. + +Regenerate Rust bindings to expose `is_reconnecting()`. Lifecycle error callbacks gain the third argument; subscription `on_applied` closures must implement `FnMut`. `on_connect` remains `FnOnce`. Automatic reconnect requires a server supporting session replacement and batch subscriptions. diff --git a/docs/docs/00200-core-concepts/00600-clients/00500-rust-reference.md b/docs/docs/00200-core-concepts/00600-clients/00500-rust-reference.md index 83b03542645..c0fbd3959e5 100644 --- a/docs/docs/00200-core-concepts/00600-clients/00500-rust-reference.md +++ b/docs/docs/00200-core-concepts/00600-clients/00500-rust-reference.md @@ -142,7 +142,7 @@ This interface may change in an upcoming release as we rework SpacetimeDB's auth impl DbConnectionBuilder { fn on_connect_error( self, - callback: impl FnOnce(&ErrorContext, spacetimedb_sdk::Error), + callback: impl FnMut(&ErrorContext, spacetimedb_sdk::Error, Option), ) -> DbConnectionBuilder; } ``` @@ -155,13 +155,27 @@ Chain a call to `.on_connect_error(callback)` to your builder to register a call impl DbConnectionBuilder { fn on_disconnect( self, - callback: impl FnOnce(&ErrorContext, Option), + callback: impl FnMut(&ErrorContext, Option, Option), ) -> DbConnectionBuilder; } ``` Chain a call to `.on_disconnect(callback)` to your builder to register a callback to run when your established `DbConnection` disconnects from the remote database, either as a result of a call to [`disconnect`](#method-disconnect) or due to an error. +#### Automatic reconnection + +Enable `with_automatic_reconnect()` on the builder to retain the connection, cache, subscriptions, and callbacks across transient outages. `with_automatic_reconnect_options(AutomaticReconnectOptions { min_delay, max_delay })` changes the default one-second minimum and 30-second maximum delays. Retries use exponential backoff with 50% jitter clamped to these bounds. The SDK enforces a 500 ms minimum and a maximum of at least one second and `min_delay`. + +Register `on_automatic_reconnect(|conn, identity, token| { ... })` for each successful reconnect. It accepts `FnMut`; `on_connect` remains a one-shot initial-connection callback. The third parameter of `on_disconnect` and `on_connect_error` is `Some(NextReconnect { attempt, delay })` when a retry is scheduled, and `None` when the connection has ended. Initial connection failures do not retry. + +A reconnect reuses the retained token. Use `with_token_provider(|| async { /* return spacetimedb_sdk::Result */ })` to refresh expiring tokens. Refresh happens when remaining validity is at most 5% of token lifetime or 30 seconds, whichever is larger, when expiry cannot be read, or after the retained token is rejected. A provider failure retries. A freshly returned token that is rejected, a changed identity, and fatal protocol errors stop recovery. + +`is_reconnecting()` is true while waiting for or establishing a reconnect; `is_active()` is false. Keep advancing the connection during this interval. Cache reads remain available. Existing subscriptions replay in one batch and call `on_applied` again. Row callbacks report only net changes. Subscriptions created during an outage are queued; unsubscribed handles are excluded from replay. + +Reducer calls while reconnecting return `Error::Disconnected`; procedure callbacks receive a disconnected error. In-flight calls fail with an `InternalError` for which `is_unknown_result()` is true, because they may have executed before the connection dropped. The SDK never retries these calls. Synthetic reducer error events use `Timestamp::UNIX_EPOCH` because no server completion timestamp is known. Explicit `disconnect()` cancels all reconnect work. + +Regenerate client bindings when upgrading to expose `is_reconnecting()`. Update lifecycle error callbacks to accept their third parameter and make subscription `on_applied` callbacks repeatable. The server must support session replacement and batch subscriptions. + #### Method `with_token` ```rust @@ -361,7 +375,7 @@ Subscribe to queries by calling `ctx.subscription_builder()` and chaining config ```rust impl SubscriptionBuilder { - fn on_applied(self, callback: impl FnOnce(&SubscriptionEventContext)) -> Self; + fn on_applied(self, callback: impl FnMut(&SubscriptionEventContext)) -> Self; } ``` From 8684a49926f29ad69d92bc423013707b2375e816 Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Mon, 12 Oct 2026 13:51:15 +0200 Subject: [PATCH 04/12] Remove example/tests from cargo.toml --- Cargo.lock | 1884 +--------------------------------- Cargo.toml | 2 - sdks/rust/tests/reconnect.rs | 8 +- 3 files changed, 62 insertions(+), 1832 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a67a7cecfbd..ca736dc72ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ab_glyph" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - [[package]] name = "addr2line" version = "0.25.1" @@ -98,31 +82,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-activity" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" -dependencies = [ - "android-properties", - "bitflags 2.10.0", - "cc", - "jni 0.22.4", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys 0.6.0+11769913", - "num_enum", - "thiserror 2.0.17", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -227,26 +186,6 @@ dependencies = [ "derive_arbitrary", ] -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win 5.4.1", - "image", - "log", - "objc2 0.6.3", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", - "parking_lot 0.12.5", - "percent-encoding", - "windows-sys 0.60.2", - "x11rb", -] - [[package]] name = "arcstr" version = "1.2.0" @@ -271,21 +210,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - [[package]] name = "assert_cmd" version = "2.2.2" @@ -614,7 +538,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn 2.0.107", ] @@ -674,12 +598,6 @@ dependencies = [ "constant_time_eq", ] -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - [[package]] name = "block-buffer" version = "0.10.4" @@ -689,15 +607,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.3", -] - [[package]] name = "borsh" version = "1.5.7" @@ -837,12 +746,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "bytes" version = "1.10.1" @@ -888,56 +791,6 @@ dependencies = [ "displaydoc", ] -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.10.0", - "log", - "polling", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", -] - -[[package]] -name = "calloop" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" -dependencies = [ - "bitflags 2.10.0", - "polling", - "rustix 1.1.2", - "tracing", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" -dependencies = [ - "calloop 0.13.0", - "rustix 0.38.44", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" -dependencies = [ - "calloop 0.14.5", - "rustix 1.1.2", - "wayland-backend", - "wayland-client", -] - [[package]] name = "camino" version = "1.2.1" @@ -1046,15 +899,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "cgl" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" -dependencies = [ - "libc", -] - [[package]] name = "check-license-symlinks" version = "0.1.0" @@ -1491,20 +1335,11 @@ version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" dependencies = [ - "error-code 2.3.1", + "error-code", "str-buf", "winapi", ] -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code 3.4.0", -] - [[package]] name = "cobs" version = "0.3.0" @@ -1514,16 +1349,6 @@ dependencies = [ "thiserror 2.0.17", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width 0.1.14", -] - [[package]] name = "colorchoice" version = "1.0.4" @@ -1701,30 +1526,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types", - "foreign-types 0.5.0", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - [[package]] name = "core_affinity" version = "0.8.3" @@ -1826,7 +1627,7 @@ dependencies = [ "log", "pulley-interpreter", "regalloc2", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "smallvec", "target-lexicon", @@ -2058,12 +1859,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -2398,22 +2193,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -2425,42 +2204,12 @@ dependencies = [ "syn 2.0.107", ] -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" - [[package]] name = "dragonbox_ecma" version = "0.1.0" @@ -2499,16 +2248,6 @@ dependencies = [ "spki", ] -[[package]] -name = "ecolor" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" -dependencies = [ - "bytemuck", - "emath", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -2545,114 +2284,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "eframe" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c" -dependencies = [ - "ahash 0.8.12", - "bytemuck", - "document-features", - "egui", - "egui-wgpu", - "egui-winit", - "egui_glow", - "glow", - "glutin", - "glutin-winit", - "image", - "js-sys", - "log", - "objc2 0.5.3", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "parking_lot 0.12.5", - "percent-encoding", - "profiling", - "raw-window-handle", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "web-time", - "winapi", - "windows-sys 0.59.0", - "winit", -] - -[[package]] -name = "egui" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" -dependencies = [ - "ahash 0.8.12", - "bitflags 2.10.0", - "emath", - "epaint", - "log", - "nohash-hasher", - "profiling", -] - -[[package]] -name = "egui-wgpu" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820" -dependencies = [ - "ahash 0.8.12", - "bytemuck", - "document-features", - "egui", - "epaint", - "log", - "profiling", - "thiserror 1.0.69", - "type-map", - "web-time", - "wgpu", - "winit", -] - -[[package]] -name = "egui-winit" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" -dependencies = [ - "ahash 0.8.12", - "arboard", - "bytemuck", - "egui", - "log", - "profiling", - "raw-window-handle", - "smithay-clipboard", - "web-time", - "webbrowser", - "winit", -] - -[[package]] -name = "egui_glow" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" -dependencies = [ - "ahash 0.8.12", - "bytemuck", - "egui", - "glow", - "log", - "memoffset", - "profiling", - "wasm-bindgen", - "web-sys", - "winit", -] - [[package]] name = "either" version = "1.15.0" @@ -2680,15 +2311,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "emath" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" -dependencies = [ - "bytemuck", -] - [[package]] name = "embedded-io" version = "0.4.0" @@ -2818,30 +2440,6 @@ dependencies = [ "spacetimedb", ] -[[package]] -name = "epaint" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" -dependencies = [ - "ab_glyph", - "ahash 0.8.12", - "bytemuck", - "ecolor", - "emath", - "epaint_default_fonts", - "log", - "nohash-hasher", - "parking_lot 0.12.5", - "profiling", -] - -[[package]] -name = "epaint_default_fonts" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" - [[package]] name = "equivalent" version = "1.0.2" @@ -2868,12 +2466,6 @@ dependencies = [ "str-buf", ] -[[package]] -name = "error-code" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" - [[package]] name = "ethnum" version = "1.5.3" @@ -2986,12 +2578,6 @@ dependencies = [ "simdutf8", ] -[[package]] -name = "fax" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" - [[package]] name = "fd-lock" version = "3.0.13" @@ -3003,15 +2589,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - [[package]] name = "ff" version = "0.13.1" @@ -3102,28 +2679,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared 0.3.1", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", + "foreign-types-shared", ] [[package]] @@ -3132,12 +2688,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -3353,16 +2903,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.2", - "windows-link 0.2.1", -] - [[package]] name = "getrandom" version = "0.2.16" @@ -3414,17 +2954,6 @@ dependencies = [ "url", ] -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - [[package]] name = "glob" version = "0.3.3" @@ -3518,123 +3047,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "glow" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" -dependencies = [ - "bitflags 2.10.0", - "cfg_aliases", - "cgl", - "dispatch2", - "glutin_egl_sys", - "glutin_glx_sys", - "glutin_wgl_sys", - "libloading", - "objc2 0.6.3", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "once_cell", - "raw-window-handle", - "wayland-sys", - "windows-sys 0.52.0", - "x11-dl", -] - -[[package]] -name = "glutin-winit" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" -dependencies = [ - "cfg_aliases", - "glutin", - "raw-window-handle", - "winit", -] - -[[package]] -name = "glutin_egl_sys" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" -dependencies = [ - "gl_generator", - "windows-sys 0.52.0", -] - -[[package]] -name = "glutin_glx_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" -dependencies = [ - "gl_generator", - "x11-dl", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "gpu-alloc" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" -dependencies = [ - "bitflags 2.10.0", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.10.0", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.10.0", -] - [[package]] name = "group" version = "0.13.0" @@ -3805,12 +3217,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - [[package]] name = "hkdf" version = "0.12.4" @@ -4207,20 +3613,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png", - "tiff", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -4467,68 +3859,19 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys 0.3.0", + "jni-sys", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys 0.4.1", - "log", - "simd_cesu8", - "thiserror 2.0.17", - "walkdir", - "windows-link 0.2.1", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.107", -] - [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.107", -] - [[package]] name = "jobserver" version = "0.1.34" @@ -4635,23 +3978,6 @@ dependencies = [ "spacetimedb", ] -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - [[package]] name = "kqueue" version = "1.1.1" @@ -4865,12 +4191,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - [[package]] name = "lock_api" version = "0.4.14" @@ -4929,15 +4249,6 @@ dependencies = [ "libc", ] -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "mappings" version = "0.7.1" @@ -5006,30 +4317,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metal" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" -dependencies = [ - "bitflags 2.10.0", - "block", - "core-graphics-types", - "foreign-types 0.5.0", - "log", - "objc", - "paste", -] - [[package]] name = "mimalloc" version = "0.1.48" @@ -5082,16 +4369,6 @@ dependencies = [ "spacetimedb", ] -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - [[package]] name = "multer" version = "3.1.0" @@ -5129,28 +4406,6 @@ dependencies = [ "syn 2.0.107", ] -[[package]] -name = "naga" -version = "24.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" -dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.10.0", - "cfg_aliases", - "codespan-reporting", - "hexf-parse", - "indexmap 2.12.0", - "log", - "rustc-hash 1.1.0", - "spirv", - "strum 0.26.3", - "termcolor", - "thiserror 2.0.17", - "unicode-xid", -] - [[package]] name = "names" version = "0.14.0" @@ -5178,45 +4433,12 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.10.0", - "jni-sys 0.3.0", - "log", - "ndk-sys 0.6.0+11769913", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - [[package]] name = "ndk-context" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys 0.3.0", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.0", -] - [[package]] name = "netlink-packet-core" version = "0.7.0" @@ -5530,132 +4752,20 @@ dependencies = [ ] [[package]] -name = "num_enum" -version = "0.7.6" +name = "number_prefix" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] -name = "num_enum_derive" -version = "0.7.6" +name = "objc2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.107", -] - -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2 0.5.3", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation 0.2.2", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.3", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2", - "objc2 0.5.3", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.3", - "objc2-foundation 0.2.2", -] - [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -5663,45 +4773,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", - "dispatch2", - "objc2 0.6.3", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2", - "objc2 0.5.3", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2", - "objc2 0.5.3", - "objc2-contacts", - "objc2-foundation 0.2.2", ] [[package]] @@ -5720,19 +4791,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.10.0", - "block2", - "dispatch", - "libc", - "objc2 0.5.3", -] - [[package]] name = "objc2-foundation" version = "0.3.2" @@ -5740,111 +4798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2", - "objc2 0.5.3", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.3", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.3", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2 0.5.3", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.3", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation 0.2.2", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2", - "objc2 0.5.3", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.10.0", - "block2", - "objc2 0.5.3", - "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2", ] [[package]] @@ -5885,7 +4839,7 @@ checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ "bitflags 2.10.0", "cfg-if", - "foreign-types 0.3.2", + "foreign-types", "libc", "once_cell", "openssl-macros", @@ -5937,25 +4891,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "orbclient" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" -dependencies = [ - "libc", - "libredox", -] - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - [[package]] name = "os_pipe" version = "1.2.3" @@ -5978,15 +4913,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "owned_ttf_parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" -dependencies = [ - "ttf-parser", -] - [[package]] name = "owo-colors" version = "3.5.0" @@ -6031,7 +4957,7 @@ checksum = "75b1853bc34cadaa90aa09f95713d8b77ec0c0d3e2d90ccf7a74216f40d20850" dependencies = [ "flate2", "postcard", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "thiserror 2.0.17", @@ -6073,7 +4999,7 @@ dependencies = [ "hashbrown 0.16.1", "oxc_data_structures", "oxc_estree", - "rustc-hash 2.1.1", + "rustc-hash", "serde", ] @@ -6129,7 +5055,7 @@ dependencies = [ "oxc_index", "oxc_syntax", "petgraph 0.8.3", - "rustc-hash 2.1.1", + "rustc-hash", ] [[package]] @@ -6150,7 +5076,7 @@ dependencies = [ "oxc_sourcemap", "oxc_span", "oxc_syntax", - "rustc-hash 2.1.1", + "rustc-hash", ] [[package]] @@ -6162,7 +5088,7 @@ dependencies = [ "cow-utils", "oxc-browserslist", "oxc_syntax", - "rustc-hash 2.1.1", + "rustc-hash", "serde", ] @@ -6238,7 +5164,7 @@ dependencies = [ "oxc_ecmascript", "oxc_span", "oxc_syntax", - "rustc-hash 2.1.1", + "rustc-hash", ] [[package]] @@ -6255,7 +5181,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash 2.1.1", + "rustc-hash", ] [[package]] @@ -6279,7 +5205,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_traverse", - "rustc-hash 2.1.1", + "rustc-hash", ] [[package]] @@ -6301,7 +5227,7 @@ dependencies = [ "oxc_regular_expression", "oxc_span", "oxc_syntax", - "rustc-hash 2.1.1", + "rustc-hash", "seq-macro", ] @@ -6317,7 +5243,7 @@ dependencies = [ "oxc_diagnostics", "oxc_span", "phf", - "rustc-hash 2.1.1", + "rustc-hash", "unicode-id-start", ] @@ -6336,7 +5262,7 @@ dependencies = [ "papaya", "parking_lot 0.12.5", "pnp", - "rustc-hash 2.1.1", + "rustc-hash", "rustix 1.1.2", "self_cell", "serde", @@ -6367,7 +5293,7 @@ dependencies = [ "oxc_index", "oxc_span", "oxc_syntax", - "rustc-hash 2.1.1", + "rustc-hash", "self_cell", "smallvec", ] @@ -6380,7 +5306,7 @@ checksum = "36801dbbd025f2fa133367494e38eef75a53d334ae6746ba0c889fc4e76fa3a3" dependencies = [ "base64-simd", "json-escape-simd", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", ] @@ -6456,7 +5382,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_traverse", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "sha1", @@ -6481,7 +5407,7 @@ dependencies = [ "oxc_syntax", "oxc_transformer", "oxc_traverse", - "rustc-hash 2.1.1", + "rustc-hash", ] [[package]] @@ -6499,7 +5425,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "rustc-hash 2.1.1", + "rustc-hash", ] [[package]] @@ -6752,7 +5678,7 @@ dependencies = [ "ryu", "serde", "serde_json", - "smol_str 0.3.4", + "smol_str", "thiserror 2.0.17", "tokio", "tokio-util", @@ -6901,19 +5827,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.10.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - [[package]] name = "pnp" version = "0.12.7" @@ -6927,26 +5840,12 @@ dependencies = [ "nodejs-built-in-modules 1.0.0", "pathdiff", "radix_trie 0.3.0", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "thiserror 2.0.17", ] -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.5.2", - "pin-project-lite", - "rustix 1.1.2", - "windows-sys 0.61.2", -] - [[package]] name = "portable-atomic" version = "1.11.1" @@ -7185,12 +6084,6 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "profiling" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" - [[package]] name = "prometheus" version = "0.14.0" @@ -7343,24 +6236,12 @@ dependencies = [ "syn 2.0.107", ] -[[package]] -name = "pxfm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" - [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quick-junit" version = "0.3.6" @@ -7402,15 +6283,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quick-xml" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" -dependencies = [ - "memchr", -] - [[package]] name = "quickstart-chat-module" version = "0.1.0" @@ -7574,12 +6446,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - [[package]] name = "rayon" version = "1.11.0" @@ -7609,21 +6475,6 @@ dependencies = [ "rand_core 0.3.1", ] -[[package]] -name = "reconnect-desktop" -version = "0.1.0" -dependencies = [ - "anyhow", - "eframe", - "jsonwebtoken", - "serde_json", - "spacetimedb-client-api-messages", - "spacetimedb-lib", - "spacetimedb-sdk", - "tokio", - "tokio-tungstenite 0.27.0", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -7633,15 +6484,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -7692,7 +6534,7 @@ dependencies = [ "bumpalo", "hashbrown 0.15.5", "log", - "rustc-hash 2.1.1", + "rustc-hash", "smallvec", ] @@ -7775,12 +6617,6 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - [[package]] name = "replace-spacetimedb" version = "0.1.0" @@ -7994,7 +6830,7 @@ dependencies = [ "rolldown_std_utils", "rolldown_tracing", "rolldown_utils", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "string_wizard", @@ -8062,7 +6898,7 @@ dependencies = [ "rolldown_sourcemap", "rolldown_std_utils", "rolldown_utils", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "simdutf8", @@ -8089,7 +6925,7 @@ dependencies = [ "blake3", "dashmap 6.1.0", "rolldown_devtools_action", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "tracing", @@ -8143,7 +6979,7 @@ dependencies = [ "oxc_resolver", "rolldown-ariadne", "ropey", - "rustc-hash 2.1.1", + "rustc-hash", "sugar_path", ] @@ -8176,7 +7012,7 @@ dependencies = [ "rolldown_resolver", "rolldown_sourcemap", "rolldown_utils", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "string_wizard", @@ -8195,7 +7031,7 @@ dependencies = [ "rolldown_common", "rolldown_plugin", "rolldown_utils", - "rustc-hash 2.1.1", + "rustc-hash", "serde_json", "xxhash-rust", ] @@ -8274,7 +7110,7 @@ dependencies = [ "oxc", "oxc_sourcemap", "rolldown_utils", - "rustc-hash 2.1.1", + "rustc-hash", ] [[package]] @@ -8324,7 +7160,7 @@ dependencies = [ "regress", "rolldown_error", "rolldown_std_utils", - "rustc-hash 2.1.1", + "rustc-hash", "serde_json", "simdutf8", "sugar_path", @@ -8412,12 +7248,6 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -8505,7 +7335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", - "quick-error 1.2.3", + "quick-error", "tempfile", "wait-timeout", ] @@ -8518,7 +7348,7 @@ checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" dependencies = [ "bitflags 2.10.0", "cfg-if", - "clipboard-win 4.5.0", + "clipboard-win", "fd-lock", "home", "libc", @@ -8646,13 +7476,6 @@ dependencies = [ "spacetimedb", ] -[[package]] -name = "sdk-test-reconnect-module" -version = "0.1.0" -dependencies = [ - "spacetimedb", -] - [[package]] name = "sdk-test-view" version = "0.1.0" @@ -9031,18 +7854,8 @@ dependencies = [ ] [[package]] -name = "simd_cesu8" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" @@ -9092,15 +7905,6 @@ dependencies = [ "parking_lot 0.11.2", ] -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - [[package]] name = "smallvec" version = "1.15.1" @@ -9116,78 +7920,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" -[[package]] -name = "smithay-client-toolkit" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" -dependencies = [ - "bitflags 2.10.0", - "calloop 0.13.0", - "calloop-wayland-source 0.3.0", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 0.38.44", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" -dependencies = [ - "bitflags 2.10.0", - "calloop 0.14.5", - "calloop-wayland-source 0.4.1", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 1.1.2", - "thiserror 2.0.17", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-experimental", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-clipboard" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" -dependencies = [ - "libc", - "smithay-client-toolkit 0.20.0", - "wayland-backend", -] - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - [[package]] name = "smol_str" version = "0.3.4" @@ -9229,7 +7961,7 @@ dependencies = [ "data-encoding", "debugid", "if_chain", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "serde_json", "unicode-id-start", @@ -9582,7 +8314,7 @@ dependencies = [ "regex", "reqwest", "rustc-demangle", - "rustc-hash 2.1.1", + "rustc-hash", "scopeguard", "semver", "serde", @@ -9613,7 +8345,7 @@ dependencies = [ "spacetimedb-schema", "spacetimedb-subscription", "spacetimedb-table", - "strum 0.25.0", + "strum", "tempfile", "thiserror 1.0.69", "tikv-jemalloc-ctl", @@ -9682,7 +8414,7 @@ dependencies = [ "spacetimedb-schema", "spacetimedb-snapshot", "spacetimedb-table", - "strum 0.25.0", + "strum", "thin-vec", "thiserror 1.0.69", ] @@ -10350,15 +9082,6 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" -dependencies = [ - "bitflags 2.10.0", -] - [[package]] name = "spki" version = "0.7.3" @@ -10476,7 +9199,7 @@ dependencies = [ "oxc_index", "oxc_sourcemap", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "serde", ] @@ -10518,16 +9241,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros 0.25.3", -] - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", + "strum_macros", ] [[package]] @@ -10543,19 +9257,6 @@ dependencies = [ "syn 2.0.107", ] -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.107", -] - [[package]] name = "subst" version = "0.3.8" @@ -10603,17 +9304,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -10923,20 +9613,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error 2.0.1", - "weezl", - "zune-jpeg", -] - [[package]] name = "tikv-jemalloc-ctl" version = "0.6.1" @@ -11550,12 +10226,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - [[package]] name = "tungstenite" version = "0.26.2" @@ -11592,15 +10262,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "type-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" -dependencies = [ - "rustc-hash 2.1.1", -] - [[package]] name = "typedmap" version = "0.6.0" @@ -12289,141 +10950,6 @@ dependencies = [ "wit-parser", ] -[[package]] -name = "wayland-backend" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.2", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" -dependencies = [ - "bitflags 2.10.0", - "rustix 1.1.2", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.10.0", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" -dependencies = [ - "rustix 1.1.2", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-experimental" -version = "20250721.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-misc" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-plasma" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" -dependencies = [ - "bitflags 2.10.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" -dependencies = [ - "proc-macro2", - "quick-xml 0.41.0", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" -dependencies = [ - "dlib", - "log", - "once_cell", - "pkg-config", -] - [[package]] name = "web-sys" version = "0.3.81" @@ -12451,124 +10977,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" dependencies = [ "core-foundation 0.10.1", - "jni 0.21.1", + "jni", "log", "ndk-context", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", "url", "web-sys", ] -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "wgpu" -version = "24.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" -dependencies = [ - "arrayvec", - "bitflags 2.10.0", - "cfg_aliases", - "document-features", - "js-sys", - "log", - "parking_lot 0.12.5", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "24.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" -dependencies = [ - "arrayvec", - "bit-vec", - "bitflags 2.10.0", - "cfg_aliases", - "document-features", - "indexmap 2.12.0", - "log", - "naga", - "once_cell", - "parking_lot 0.12.5", - "profiling", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 2.0.17", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "24.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bitflags 2.10.0", - "bytemuck", - "cfg_aliases", - "core-graphics-types", - "glow", - "glutin_wgl_sys", - "gpu-alloc", - "gpu-descriptor", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "metal", - "naga", - "ndk-sys 0.5.0+25.2.9519653", - "objc", - "once_cell", - "ordered-float", - "parking_lot 0.12.5", - "profiling", - "raw-window-handle", - "renderdoc-sys", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 2.0.17", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "windows 0.58.0", -] - -[[package]] -name = "wgpu-types" -version = "24.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" -dependencies = [ - "bitflags 2.10.0", - "js-sys", - "log", - "web-sys", -] - [[package]] name = "which" version = "6.0.3" @@ -12634,16 +11051,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" @@ -12687,27 +11094,14 @@ dependencies = [ "windows-core 0.62.2", ] -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -12719,8 +11113,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -12748,17 +11142,6 @@ dependencies = [ "windows-threading 0.2.1", ] -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.107", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -12770,17 +11153,6 @@ dependencies = [ "syn 2.0.107", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.107", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -12835,15 +11207,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -12862,16 +11225,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -13205,57 +11558,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winit" -version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" -dependencies = [ - "ahash 0.8.12", - "android-activity", - "atomic-waker", - "bitflags 2.10.0", - "block2", - "bytemuck", - "calloop 0.13.0", - "cfg_aliases", - "concurrent-queue", - "core-foundation 0.9.4", - "core-graphics", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "memmap2", - "ndk", - "objc2 0.5.3", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "orbclient", - "percent-encoding", - "pin-project", - "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "smithay-client-toolkit 0.19.2", - "smol_str 0.2.2", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - [[package]] name = "winnow" version = "0.7.13" @@ -13310,38 +11612,6 @@ dependencies = [ "tap", ] -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.2", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - [[package]] name = "xattr" version = "1.6.1" @@ -13352,37 +11622,12 @@ dependencies = [ "rustix 1.1.2", ] -[[package]] -name = "xcursor" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" - [[package]] name = "xdg" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" -[[package]] -name = "xkbcommon-dl" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" -dependencies = [ - "bitflags 2.10.0", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" - [[package]] name = "xml-rs" version = "0.8.28" @@ -13671,18 +11916,3 @@ dependencies = [ "cc", "pkg-config", ] - -[[package]] -name = "zune-core" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/Cargo.toml b/Cargo.toml index 2db810b198f..5325197d478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,6 @@ members = [ "crates/smoketests", "sdks/csharp/test-harness~", "sdks/rust", - "sdks/rust/examples/reconnect-desktop", - "modules/sdk-test-reconnect", "sdks/unreal", "crates/snapshot", "crates/sqltest", diff --git a/sdks/rust/tests/reconnect.rs b/sdks/rust/tests/reconnect.rs index fd74f377477..2e5022d8200 100644 --- a/sdks/rust/tests/reconnect.rs +++ b/sdks/rust/tests/reconnect.rs @@ -1,6 +1,6 @@ #![cfg(not(target_arch = "wasm32"))] -#[path = "../examples/reconnect-desktop/src/module_bindings/mod.rs"] +#[path = "case-conversion-client/src/module_bindings/mod.rs"] mod module_bindings; use module_bindings::*; @@ -195,12 +195,14 @@ fn in_flight_call_is_unknown_and_disconnect_cancels_backoff() { let result = unknown.clone(); connection .reducers - .change_both_then(move |_, outcome| *result.lock().unwrap() = outcome.unwrap_err().is_unknown_result()) + .create_player_1_then("test".into(), 1, move |_, outcome| { + *result.lock().unwrap() = outcome.unwrap_err().is_unknown_result(); + }) .unwrap(); tick_until(&connection, || connection.is_reconnecting()); assert!(*unknown.lock().unwrap()); assert!(matches!( - connection.reducers.change_both(), + connection.reducers.create_player_1("test".into(), 1), Err(spacetimedb_sdk::Error::Disconnected) )); connection.disconnect().unwrap(); From f7264b0d510c83253e7a298a74450935874cbb30 Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Mon, 12 Oct 2026 14:32:45 +0200 Subject: [PATCH 05/12] Gate native reconnect tests out of browser builds --- sdks/rust/tests/reconnect.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/rust/tests/reconnect.rs b/sdks/rust/tests/reconnect.rs index 2e5022d8200..365cb642202 100644 --- a/sdks/rust/tests/reconnect.rs +++ b/sdks/rust/tests/reconnect.rs @@ -1,4 +1,4 @@ -#![cfg(not(target_arch = "wasm32"))] +#![cfg(all(not(target_arch = "wasm32"), not(feature = "browser")))] #[path = "case-conversion-client/src/module_bindings/mod.rs"] mod module_bindings; From bd2e0991edc2e6307e56fdfa593d1bc0f679fe6b Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Mon, 12 Oct 2026 14:33:34 +0200 Subject: [PATCH 06/12] Treat normal WebSocket closes as successful completion --- sdks/rust/src/db_connection.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sdks/rust/src/db_connection.rs b/sdks/rust/src/db_connection.rs index 3b8d71fa6b0..9a58626108f 100644 --- a/sdks/rust/src/db_connection.rs +++ b/sdks/rust/src/db_connection.rs @@ -163,11 +163,13 @@ impl DbContextImpl { ParsedMessage::TransportError(e) => { let terminal = e.is_terminal(); let auth = e.is_auth(); - self.connection_lost( - Some(InternalError::new(e.to_string()).with_cause(e).into()), - terminal, - auth, - ) + // A normal close is EOF, not a failed connection. Keep other close + // codes as errors so their terminal/retry classification is retained. + let error = match e { + crate::websocket::WsError::Closed { code: 1000, .. } => None, + e => Some(InternalError::new(e.to_string()).with_cause(e).into()), + }; + self.connection_lost(error, terminal, auth) } ParsedMessage::IdentityToken(identity, token, conn_id) => { if self.inner.lock().unwrap().connection_lifecycle == ConnectionLifecycle::Ended { From 031f7093c045eb368bc55479245c9fe5bde9446b Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Mon, 12 Oct 2026 14:34:05 +0200 Subject: [PATCH 07/12] Suppress disconnect callbacks for cancelled initial connections --- sdks/rust/src/db_connection.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdks/rust/src/db_connection.rs b/sdks/rust/src/db_connection.rs index 9a58626108f..0ba27631150 100644 --- a/sdks/rust/src/db_connection.rs +++ b/sdks/rust/src/db_connection.rs @@ -803,12 +803,14 @@ impl DbContextImpl { } *self.send_chan.lock().unwrap() = None; let mut inner = self.inner.lock().unwrap(); + let notify_disconnect = + inner.connection_lifecycle != ConnectionLifecycle::Ended && self.identity.lock().unwrap().is_some(); inner.connection_lifecycle = ConnectionLifecycle::Ended; inner.replay = None; self.fail_pending_operations(&mut inner); let ctx = self.make_event_ctx(None); inner.subscriptions.on_disconnect(&self.make_event_ctx(())); - if let Some(callback) = inner.on_disconnect.as_mut() { + if notify_disconnect && let Some(callback) = inner.on_disconnect.as_mut() { callback(&ctx, None, None); } return Err(crate::Error::Disconnected); From e656e280cfdb257f4fd6eff4e043ca3d390e7c77 Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Mon, 12 Oct 2026 14:34:23 +0200 Subject: [PATCH 08/12] Reuse the shared sender for all client messages --- sdks/rust/src/db_connection.rs | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/sdks/rust/src/db_connection.rs b/sdks/rust/src/db_connection.rs index 0ba27631150..34442c03457 100644 --- a/sdks/rust/src/db_connection.rs +++ b/sdks/rust/src/db_connection.rs @@ -670,13 +670,7 @@ impl DbContextImpl { return Ok(()); } if let Some(msg) = handle.start() { - self.send_chan - .lock() - .unwrap() - .as_mut() - .ok_or(crate::Error::Disconnected)? - .unbounded_send(ws::v2::ClientMessage::Subscribe(msg)) - .map_err(|_| crate::Error::Disconnected)?; + self.send_message(ws::v2::ClientMessage::Subscribe(msg))?; } // else, the handle was already cancelled. } @@ -700,13 +694,7 @@ impl DbContextImpl { callback(&self.make_event_ctx(())); } PendingUnsubscribeResult::SendUnsubscribe(m) => { - self.send_chan - .lock() - .unwrap() - .as_mut() - .ok_or(crate::Error::Disconnected)? - .unbounded_send(ws::v2::ClientMessage::Unsubscribe(m)) - .map_err(|_| crate::Error::Disconnected)?; + self.send_message(ws::v2::ClientMessage::Unsubscribe(m))?; } } } @@ -745,13 +733,7 @@ impl DbContextImpl { request_id, flags, }); - self.send_chan - .lock() - .unwrap() - .as_mut() - .ok_or(crate::Error::Disconnected)? - .unbounded_send(msg) - .map_err(|_| crate::Error::Disconnected)?; + self.send_message(msg)?; } // Invoke a procedure: stash its callback, then send the `CallProcedure` WS message. @@ -781,13 +763,7 @@ impl DbContextImpl { request_id, flags: ws::v2::CallProcedureFlags::Default, }); - self.send_chan - .lock() - .unwrap() - .as_mut() - .ok_or(crate::Error::Disconnected)? - .unbounded_send(msg) - .map_err(|_| crate::Error::Disconnected)?; + self.send_message(msg)?; } // Disconnect: close the connection. From e79abe0e9de24b42d7d2c85c2a7a2a9bb000e53f Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Tue, 13 Oct 2026 11:25:00 +0200 Subject: [PATCH 09/12] update docs --- .../00200-core-concepts/00600-clients/00300-connection.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/00200-core-concepts/00600-clients/00300-connection.md b/docs/docs/00200-core-concepts/00600-clients/00300-connection.md index 31f74b62ba1..2bb448b1c6b 100644 --- a/docs/docs/00200-core-concepts/00600-clients/00300-connection.md +++ b/docs/docs/00200-core-concepts/00600-clients/00300-connection.md @@ -456,11 +456,11 @@ Enable `.with_automatic_reconnect()` to recover after an established connection `on_disconnect(|ctx, error, next| ...)` and `on_connect_error(|ctx, error, next| ...)` receive `Option`, with the next attempt number and `Duration`, or `None` when no retry is scheduled. Initial connection failures are not retried. `disconnect()` cancels recovery. -Use `.with_automatic_reconnect_options(AutomaticReconnectOptions { min_delay, max_delay })` to configure backoff. Defaults are one second and 30 seconds, with exponential growth and 50% jitter clamped to the bounds. The minimum delay is at least 500 ms; the maximum is at least one second and the minimum delay. +Use `.with_automatic_reconnect_options(AutomaticReconnectOptions { min_delay, max_delay })` to configure backoff. Defaults are one second and 30 seconds, with exponential growth and 50% jitter clamped to the bounds. The SDK raises `min_delay` to 500 ms if it is lower, then raises `max_delay` to the greater of one second and `min_delay` if necessary. For expiring credentials, combine `.with_token(Some(initial_token))` and `.with_token_provider(|| async { /* return Result */ })`. The provider refreshes near expiry or after rejection and must return a token for the same identity. Provider failures retry; rejection of a fresh token and identity changes are terminal. -Calls made while reconnecting fail with a disconnected error. Calls interrupted by a drop may have executed: reducer/procedure callback errors expose `is_unknown_result()`. Calls are never replayed. Subscription creation is queued during recovery, and cancellation removes a set from replay. +Function calls (i.e. reducers/procedures) made while reconnecting fail with a disconnected error. Calls interrupted by a drop may have executed: callback errors expose `is_unknown_result()`. Calls are never replayed. Subscription creation is queued during recovery, and cancellation removes a set from replay. Regenerate Rust bindings to expose `is_reconnecting()`. Lifecycle error callbacks gain the third argument; subscription `on_applied` closures must implement `FnMut`. `on_connect` remains `FnOnce`. Automatic reconnect requires a server supporting session replacement and batch subscriptions. From 0605e10984d8edb35b4bf285fee3bdfa0f2ec448 Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Wed, 14 Oct 2026 11:29:50 +0200 Subject: [PATCH 10/12] Make comment and var names sane --- sdks/rust/src/client_cache.rs | 17 ++++++++--------- sdks/rust/src/db_connection.rs | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/sdks/rust/src/client_cache.rs b/sdks/rust/src/client_cache.rs index d7b3487c312..115fa636b83 100644 --- a/sdks/rust/src/client_cache.rs +++ b/sdks/rust/src/client_cache.rs @@ -341,10 +341,9 @@ pub struct ClientCache { /// /// The strings are table names, since we may have multiple tables with the same row type. tables: Map, - // Erased per-table readers let reconnect synthesize removals from the live cache - // without extending generated DbUpdate implementations. Every reference is - // included, preserving overlap counts when all replayed sets are applied at once. - snapshotters: + // One synthetic delete builder per table. Auto-reconnect combines these deletes + // with new subscription inserts and applies them through the existing DbUpdate code. + synthetic_delete_builders: HashMap<&'static str, fn(&Self, &'static str) -> spacetimedb_client_api_messages::websocket::v2::TableUpdate>, /// Clone of the [`crate::db_connection::DbContextImpl::extra_logging`]. @@ -357,16 +356,16 @@ impl ClientCache { pub(crate) fn new(extra_logging: Option>) -> Self { Self { tables: Map::new(), - snapshotters: HashMap::default(), + synthetic_delete_builders: HashMap::default(), extra_logging, _module: PhantomData, } } - pub(crate) fn removal_snapshot(&self) -> Vec { - self.snapshotters + pub(crate) fn build_synthetic_deletes(&self) -> Vec { + self.synthetic_delete_builders .iter() - .map(|(&name, snapshot)| snapshot(self, name)) + .map(|(&name, build_deletes)| build_deletes(self, name)) .collect() } @@ -386,7 +385,7 @@ impl ClientCache { &mut self, table_name: &'static str, ) -> &mut TableCache { - self.snapshotters.entry(table_name).or_insert(|cache, name| { + self.synthetic_delete_builders.entry(table_name).or_insert(|cache, name| { use spacetimedb_client_api_messages::websocket::{common::RowSizeHint, v2}; let mut bytes = Vec::new(); let mut offsets = Vec::new(); diff --git a/sdks/rust/src/db_connection.rs b/sdks/rust/src/db_connection.rs index 34442c03457..df532d185f5 100644 --- a/sdks/rust/src/db_connection.rs +++ b/sdks/rust/src/db_connection.rs @@ -399,7 +399,7 @@ impl DbContextImpl { false, ); } - let mut tables = self.cache.lock().unwrap().removal_snapshot(); + let mut tables = self.cache.lock().unwrap().build_synthetic_deletes(); let mut outcomes = Vec::new(); for result in batch.results { let error = match result.outcome { From 83712224c5115dfa853ad31267188cd97af7b5ef Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Wed, 14 Oct 2026 15:13:53 +0200 Subject: [PATCH 11/12] Clarify 'generation' field with comment --- sdks/rust/src/db_connection.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdks/rust/src/db_connection.rs b/sdks/rust/src/db_connection.rs index df532d185f5..72fbec501e6 100644 --- a/sdks/rust/src/db_connection.rs +++ b/sdks/rust/src/db_connection.rs @@ -2076,6 +2076,8 @@ struct ReconnectState { provider: Option>, token: Option, attempt: u32, + // Incremented to invalidate results from older reconnect tasks. + // Only ReconnectReady messages matching this generation may be applied. generation: u64, reconnecting: bool, preparing_replay: bool, From 773fe37bb5bc7c1cd8ea64440c656cad40f552ff Mon Sep 17 00:00:00 2001 From: Alessandro Asoni Date: Wed, 14 Oct 2026 15:14:12 +0200 Subject: [PATCH 12/12] format --- sdks/rust/src/client_cache.rs | 40 ++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/sdks/rust/src/client_cache.rs b/sdks/rust/src/client_cache.rs index 115fa636b83..8132323671e 100644 --- a/sdks/rust/src/client_cache.rs +++ b/sdks/rust/src/client_cache.rs @@ -385,27 +385,29 @@ impl ClientCache { &mut self, table_name: &'static str, ) -> &mut TableCache { - self.synthetic_delete_builders.entry(table_name).or_insert(|cache, name| { - use spacetimedb_client_api_messages::websocket::{common::RowSizeHint, v2}; - let mut bytes = Vec::new(); - let mut offsets = Vec::new(); - if let Some(table) = cache.get_table::(name) { - for (bsatn, entry) in &table.entries { - for _ in 0..entry.ref_count { - offsets.push(bytes.len() as u64); - bytes.extend_from_slice(bsatn); + self.synthetic_delete_builders + .entry(table_name) + .or_insert(|cache, name| { + use spacetimedb_client_api_messages::websocket::{common::RowSizeHint, v2}; + let mut bytes = Vec::new(); + let mut offsets = Vec::new(); + if let Some(table) = cache.get_table::(name) { + for (bsatn, entry) in &table.entries { + for _ in 0..entry.ref_count { + offsets.push(bytes.len() as u64); + bytes.extend_from_slice(bsatn); + } } } - } - v2::TableUpdate { - table_name: name.into(), - rows: vec![v2::TableUpdateRows::PersistentTable(v2::PersistentTableRows { - inserts: v2::BsatnRowList::default(), - deletes: v2::BsatnRowList::new(RowSizeHint::RowOffsets(offsets.into()), bytes.into()), - })] - .into(), - } - }); + v2::TableUpdate { + table_name: name.into(), + rows: vec![v2::TableUpdateRows::PersistentTable(v2::PersistentTableRows { + inserts: v2::BsatnRowList::default(), + deletes: v2::BsatnRowList::new(RowSizeHint::RowOffsets(offsets.into()), bytes.into()), + })] + .into(), + } + }); self.tables .entry::>>() .or_default()