diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f745dac..3faa2723 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,7 @@ jobs: - 'crates/pjs-core/**' - 'crates/pjs-domain/**' - 'crates/pjs-wasm/**' + - 'crates/pjs-demo/**' - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' @@ -394,12 +395,17 @@ jobs: ~/.cargo/registry ~/.cargo/git + # pjs-demo excluded: 3000+ LOC of demo server binaries with zero tests + # (already listed under codecov.yml's ignore:, so this doesn't change + # what's reported); skipping its LLVM-instrumented compile here saves + # build time. See #318. - name: Generate workspace coverage env: RUSTC_WRAPPER: sccache SCCACHE_GHA_ENABLED: "true" run: | cargo llvm-cov nextest --all-features --all-targets --workspace \ + --exclude pjs-demo \ --lcov --output-path lcov.info \ --status-level fail --final-status-level skip diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6e0c67..cb3a6a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Both `WebSocketRateLimiter::spawn_cleanup_task` and `InMemoryDictionaryStore::new` require a Tokio runtime to actually spawn their periodic cleanup task; outside one, they log a warning and skip spawning rather than panicking, so bare construction remains usable from synchronous/non-async call sites — and, for `WebSocketRateLimiter`, a later call from within a runtime can still succeed (#346, #329) - `test_wire_stalled_write_times_out_and_closes_connection` (added alongside #364's write-timeout fix) asserted only that a stalled connection closes within its deadline, with no negative control distinguishing that closure from an unrelated closure path (e.g. rejecting the large inbound frame). Added `test_wire_stalled_write_stays_open_before_write_timeout`, a sibling test reusing the same stall setup but with a `write_timeout` far longer than its observation window. It asserts both halves of the causal claim: the connection stays open through the window, and once it does close, the elapsed time is `>= write_timeout` — proving the stalled-write phase was actually reached and that closure is genuinely gated on the timeout, not just a closure-within-window race (#357) - `AdaptiveStreamController::start_streaming` discarded the `JoinHandle` of the per-session frame-streaming task, so a panic in that task (e.g. on a malformed frame) was silently swallowed by the runtime and the session simply stopped streaming with no diagnostic signal. The handle's `AbortHandle` is now stored on `WebSocketStreamSession` and aborted on session teardown (`remove_session`, `cleanup_expired_sessions`, and a repeated `start_streaming` call replacing an in-flight task), and a supervisor task awaits the `JoinHandle` and logs via `tracing::error!` when it resolves to a panic. `AxumWebSocketTransport::handle_socket` now tracks which streaming sessions each connection created and calls `remove_session` for all of them on connection teardown, so the abort actually happens on client disconnect in production, not just in tests; a new background sweep (`with_rate_limit_config`, weakly holding the controller so it can't keep it alive past the transport's own lifetime) also periodically removes sessions that outlive any connection (#315) +- `crates/pjs-demo` was unbuildable by any documented method (`-p pjs-demo`, `--manifest-path`, or `cd`-ing into the directory): it was excluded from the Cargo workspace while its manifest used full `{ workspace = true }` field inheritance, which Cargo cannot resolve for a crate outside the workspace — a regression of #112. Chose option (a) from #112: restore `crates/pjs-demo` to `[workspace] members`, mirroring how `crates/pjs-bench` was restored in PR #190, while keeping it out of `default-members` so a bare `cargo build`/`cargo test` still only builds `pjs-core`, `pjs-domain`, and `pjs-wasm`. Fixed one additional compile error and one clippy lint this surfaced: three `crates/pjs-demo/src/data/*.rs` files imported `rand::Rng` instead of `rand::RngExt` (`random_range`/`random_bool` moved trait in `rand` 0.10), and a clippy `unnecessary_sort_by` lint in `websocket_streaming.rs`. Documented commands (`README.md`, `.claude/rules/continuous-improvement.md`) now pass pjson-rs feature flags with the required `pjson-rs/` prefix, since `pjs-demo` declares no `[features]` of its own. CI's `rust` paths-filter now includes `crates/pjs-demo/**` so PRs touching only the demo crate trigger quality/build/test/doctest/docs checks; the coverage job excludes `pjs-demo` (untested demo binaries would dilute the reported percentage) (#318) - `InMemoryEventPublisher.event_log` evicted at capacity by removing an arbitrary `DashMap`-iteration-order slice, not the oldest entries, and `recent_events()` compounded this by reversing that same arbitrary order and presenting it as "most recent." `StoredEvent` now carries a `sequence: u64` stamped by a monotonic counter at store time (`EventId` is a random UUIDv4 and cannot serve as an ordering key); eviction removes the lowest-`sequence` entries down to 9,000 in a single pass (correctly bounding even oversized `publish_batch` calls, not just 1,000 per call), and `recent_events()` sorts by `sequence` to reliably return the newest entries first. `publish_batch` reserves its sequence block with one `fetch_add` before parallelizing, since stamping per-event inside `rayon`'s `into_par_iter` would assign sequences in thread-scheduling order rather than batch order (#350) - Removed stale TODO comments in `axum_adapter.rs` describing authentication and HTTP rate limiting as unimplemented; both already exist (`ApiKeyAuthLayer`/`JwtAuthLayer` in `infrastructure::http::auth`, `RateLimitMiddleware` in `infrastructure::http::middleware`) and are now documented and cross-referenced in place (#319) - `DomainEvent::event_id()` derived a content-hash-based UUID, so two structurally-identical events (same variant, session/stream ID, and timestamp) produced the same `EventId`. `InMemoryEventPublisher.event_log` keys solely on `EventId`, so colliding events silently overwrote each other with no error. `InMemoryEventPublisher` and `HttpEventPublisher` now mint a fresh `EventId::new()` per stored/published event at publish time, guaranteeing distinct identity even for structurally-identical events. `EventPublisherGat`'s doc now states this as the identity contract implementors must follow (#328) diff --git a/Cargo.lock b/Cargo.lock index 90afe642..d6a421ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,12 +78,56 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -401,6 +445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -409,8 +454,22 @@ version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -434,6 +493,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -1331,6 +1396,12 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -1765,6 +1836,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" @@ -1854,6 +1931,25 @@ dependencies = [ "uuid", ] +[[package]] +name = "pjs-demo" +version = "0.6.2" +dependencies = [ + "axum", + "chrono", + "clap", + "futures", + "pjson-rs", + "rand 0.10.2", + "serde", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "pjs-wasm" version = "0.6.2" @@ -2805,6 +2901,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -2924,6 +3030,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -3129,6 +3241,7 @@ dependencies = [ "mio", "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -3408,6 +3521,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.24.0" diff --git a/Cargo.toml b/Cargo.toml index 42ead371..54790760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = ["crates/*"] default-members = ["crates/pjs-core", "crates/pjs-domain", "crates/pjs-wasm"] -exclude = ["crates/pjs-demo", "crates/pjs-js-client"] +exclude = ["crates/pjs-js-client"] resolver = "3" [workspace.package] diff --git a/README.md b/README.md index 9975ebd5..a620b5f7 100644 --- a/README.md +++ b/README.md @@ -205,9 +205,9 @@ cargo nextest run --workspace # Run benchmarks cargo bench -p pjs-bench -# Run demo servers -cargo run --manifest-path crates/pjs-demo/Cargo.toml --bin interactive-demo-server --features "simd-auto,schema-validation,compression,http-server,websocket-server" -cargo run --manifest-path crates/pjs-demo/Cargo.toml --bin simple-demo-server --features "simd-auto,http-server" +# Run demo servers (feature names are forwarded to pjson-rs, hence the prefix) +cargo run --manifest-path crates/pjs-demo/Cargo.toml --bin interactive-demo-server --features "pjson-rs/simd-auto,pjson-rs/schema-validation,pjson-rs/compression,pjson-rs/http-server,pjson-rs/websocket-server" +cargo run --manifest-path crates/pjs-demo/Cargo.toml --bin simple-demo-server --features "pjson-rs/simd-auto,pjson-rs/http-server" ``` ### Feature Flags diff --git a/crates/pjs-demo/src/data/analytics.rs b/crates/pjs-demo/src/data/analytics.rs index 8442bc26..dd9a0934 100644 --- a/crates/pjs-demo/src/data/analytics.rs +++ b/crates/pjs-demo/src/data/analytics.rs @@ -1,7 +1,7 @@ //! Analytics dashboard dataset generation use super::DatasetSize; -use rand::Rng; +use rand::RngExt; use serde_json::{Value, json}; /// Generate analytics dashboard data diff --git a/crates/pjs-demo/src/data/ecommerce.rs b/crates/pjs-demo/src/data/ecommerce.rs index ffe516e9..b3272d2e 100644 --- a/crates/pjs-demo/src/data/ecommerce.rs +++ b/crates/pjs-demo/src/data/ecommerce.rs @@ -1,7 +1,7 @@ //! E-commerce dataset generation for PJS demonstrations use super::DatasetSize; -use rand::Rng; +use rand::RngExt; use serde_json::{Value, json}; const PRODUCT_NAMES: &[&str] = &[ diff --git a/crates/pjs-demo/src/data/social.rs b/crates/pjs-demo/src/data/social.rs index 2892fc5c..89660e50 100644 --- a/crates/pjs-demo/src/data/social.rs +++ b/crates/pjs-demo/src/data/social.rs @@ -1,7 +1,7 @@ //! Social media dataset generation for PJS demonstrations use super::DatasetSize; -use rand::Rng; +use rand::RngExt; use serde_json::{Value, json}; const USERNAMES: &[&str] = &[ diff --git a/crates/pjs-demo/src/servers/websocket_streaming.rs b/crates/pjs-demo/src/servers/websocket_streaming.rs index 642486bf..308cf42e 100644 --- a/crates/pjs-demo/src/servers/websocket_streaming.rs +++ b/crates/pjs-demo/src/servers/websocket_streaming.rs @@ -384,7 +384,7 @@ fn create_demo_frames(data: &JsonValue) -> DomainResult